mirror of
https://github.com/hanzoai/helper.git
synced 2026-09-30 22:42:13 +00:00
feat(targets): providers-based Claude Code config + Claude Desktop for all platforms
claude-code.ts: switch from env.ANTHROPIC_* (session hijack) to providers.hanzo entry — additive, non-destructive. Users switch with /model hanzo/deepseek-v4-pro without losing the default Anthropic connection. HANZO_MODELS list reflects the live api.hanzo.ai catalog (Zen + GLM + DeepSeek + Qwen + Kimi + Nemotron). claude-desktop.ts: new target. Writes mcpServers.hanzo into claude_desktop_config.json. Config path is platform-aware: ~/Library/... (macOS), %APPDATA%\Claude (Windows), ~/.config/Claude (Linux). API key passed as env var to `npx @hanzo/mcp`. index.ts: register claudeDesktop in TARGETS.
This commit is contained in:
+40
-33
@@ -1,13 +1,13 @@
|
||||
/**
|
||||
* Claude Code target.
|
||||
*
|
||||
* Claude Code speaks the Anthropic protocol and reads its provider from the
|
||||
* `env` block of ~/.claude/settings.json:
|
||||
* ANTHROPIC_AUTH_TOKEN the bearer token sent on every request
|
||||
* ANTHROPIC_BASE_URL where /v1/messages lives
|
||||
* Hanzo serves /v1/messages at the API root, so the base URL is just the API
|
||||
* origin. We also set hasCompletedOnboarding in ~/.claude.json so a fresh
|
||||
* install doesn't trap the user in the first-run wizard.
|
||||
* Uses the `providers.hanzo` block in ~/.claude/settings.json — an additive,
|
||||
* non-destructive entry that lets users switch to Hanzo models per-session
|
||||
* (`/model hanzo/deepseek-v4-pro`) without replacing the default Anthropic
|
||||
* connection. The env block is NOT touched so the current session stays intact.
|
||||
*
|
||||
* Config paths are identical on all platforms (home-relative). Claude Code
|
||||
* also looks at CLAUDE_SETTINGS_PATH env override.
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
@@ -19,20 +19,28 @@ import { type CodingTarget, type HanzoCredentials, type TargetStatus, maskKey }
|
||||
const SETTINGS = path.join(os.homedir(), '.claude', 'settings.json');
|
||||
const CLAUDE_JSON = path.join(os.homedir(), '.claude.json');
|
||||
|
||||
// Keys we own in settings.env — added on configure, removed on unconfigure.
|
||||
const MANAGED_ENV = [
|
||||
'ANTHROPIC_AUTH_TOKEN',
|
||||
'ANTHROPIC_BASE_URL',
|
||||
'ANTHROPIC_MODEL',
|
||||
'API_TIMEOUT_MS',
|
||||
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
|
||||
] as const;
|
||||
|
||||
interface ClaudeSettings {
|
||||
env?: Record<string, string | number>;
|
||||
providers?: Record<string, {
|
||||
apiKey?: string;
|
||||
baseURL?: string;
|
||||
name?: string;
|
||||
models?: string[];
|
||||
}>;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
const HANZO_MODELS = [
|
||||
'zen5-ultra', 'zen5-max', 'zen5-pro', 'zen5', 'zen5-mini',
|
||||
'zen4-ultra', 'zen4-max', 'zen4-pro', 'zen4', 'zen4-coder-pro', 'zen4-coder', 'zen4-thinking',
|
||||
'glm-5.2', 'glm-5.1', 'glm-5',
|
||||
'deepseek-v4-pro', 'deepseek-v4-flash', 'deepseek-v3.2', 'deepseek-reasoner',
|
||||
'qwen3.5-397b', 'qwen3-coder',
|
||||
'kimi-k2.6',
|
||||
'llama-4-maverick',
|
||||
'nemotron-3-ultra-550b',
|
||||
'minimax-m2.5',
|
||||
];
|
||||
|
||||
export const claudeCode: CodingTarget = {
|
||||
id: 'claude-code',
|
||||
displayName: 'Claude Code',
|
||||
@@ -41,16 +49,14 @@ export const claudeCode: CodingTarget = {
|
||||
|
||||
configure(creds: HanzoCredentials): void {
|
||||
const settings = readJson<ClaudeSettings>(SETTINGS);
|
||||
const env = { ...settings.env };
|
||||
delete env['ANTHROPIC_API_KEY']; // avoid two competing auth vars
|
||||
|
||||
settings.env = {
|
||||
...env,
|
||||
ANTHROPIC_AUTH_TOKEN: creds.apiKey,
|
||||
ANTHROPIC_BASE_URL: creds.apiBase,
|
||||
ANTHROPIC_MODEL: creds.model,
|
||||
API_TIMEOUT_MS: '3000000',
|
||||
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: 1,
|
||||
settings.providers = {
|
||||
...settings.providers,
|
||||
hanzo: {
|
||||
name: 'Hanzo AI',
|
||||
apiKey: creds.apiKey,
|
||||
baseURL: creds.apiBase + '/v1',
|
||||
models: HANZO_MODELS,
|
||||
},
|
||||
};
|
||||
writeJson(SETTINGS, settings);
|
||||
|
||||
@@ -62,18 +68,19 @@ export const claudeCode: CodingTarget = {
|
||||
|
||||
unconfigure(): void {
|
||||
const settings = readJson<ClaudeSettings>(SETTINGS);
|
||||
if (!settings.env) return;
|
||||
for (const key of MANAGED_ENV) delete settings.env[key];
|
||||
if (Object.keys(settings.env).length === 0) delete settings.env;
|
||||
if (settings.providers?.hanzo) {
|
||||
delete settings.providers.hanzo;
|
||||
if (Object.keys(settings.providers).length === 0) delete settings.providers;
|
||||
}
|
||||
writeJson(SETTINGS, settings);
|
||||
},
|
||||
|
||||
status(): TargetStatus {
|
||||
const token = readJson<ClaudeSettings>(SETTINGS).env?.['ANTHROPIC_AUTH_TOKEN'];
|
||||
const key = readJson<ClaudeSettings>(SETTINGS).providers?.hanzo?.apiKey;
|
||||
return {
|
||||
installed: isInstalled(this.bin),
|
||||
configured: typeof token === 'string' && token.length > 0,
|
||||
...(typeof token === 'string' ? { apiKeyMasked: maskKey(token) } : {}),
|
||||
configured: typeof key === 'string' && key.startsWith('hk-'),
|
||||
...(typeof key === 'string' ? { apiKeyMasked: maskKey(key) } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* Claude Desktop target.
|
||||
*
|
||||
* Claude Desktop is configured via claude_desktop_config.json. Location varies
|
||||
* by platform:
|
||||
* macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
|
||||
* Windows: %APPDATA%\Claude\claude_desktop_config.json
|
||||
* Linux: ~/.config/Claude/claude_desktop_config.json
|
||||
*
|
||||
* Hanzo MCP server is wired as a named server entry so users get Hanzo tools
|
||||
* directly in Claude Desktop conversations. The API key is passed as an env var
|
||||
* to the server process — it never appears in the UI.
|
||||
*/
|
||||
|
||||
import path from 'node:path';
|
||||
import os from 'node:os';
|
||||
import { readJson, writeJson } from '../lib/json-config';
|
||||
import { isInstalled } from '../lib/which';
|
||||
import { type CodingTarget, type HanzoCredentials, type TargetStatus, maskKey } from './types';
|
||||
|
||||
function configPath(): string {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
|
||||
case 'win32':
|
||||
return path.join(process.env['APPDATA'] ?? path.join(os.homedir(), 'AppData', 'Roaming'), 'Claude', 'claude_desktop_config.json');
|
||||
default:
|
||||
return path.join(os.homedir(), '.config', 'Claude', 'claude_desktop_config.json');
|
||||
}
|
||||
}
|
||||
|
||||
interface DesktopConfig {
|
||||
mcpServers?: Record<string, {
|
||||
command: string;
|
||||
args?: string[];
|
||||
env?: Record<string, string>;
|
||||
}>;
|
||||
[k: string]: unknown;
|
||||
}
|
||||
|
||||
export const claudeDesktop: CodingTarget = {
|
||||
id: 'claude-desktop',
|
||||
displayName: 'Claude Desktop',
|
||||
bin: process.platform === 'win32' ? 'claude' : 'Claude',
|
||||
installCommand: 'Download from https://claude.ai/download',
|
||||
|
||||
configure(creds: HanzoCredentials): void {
|
||||
const config = readJson<DesktopConfig>(configPath());
|
||||
config.mcpServers = {
|
||||
...config.mcpServers,
|
||||
hanzo: {
|
||||
command: 'npx',
|
||||
args: ['-y', '@hanzo/mcp'],
|
||||
env: {
|
||||
HANZO_API_KEY: creds.apiKey,
|
||||
HANZO_API_BASE: creds.apiBase,
|
||||
},
|
||||
},
|
||||
};
|
||||
writeJson(configPath(), config);
|
||||
},
|
||||
|
||||
unconfigure(): void {
|
||||
const config = readJson<DesktopConfig>(configPath());
|
||||
if (config.mcpServers?.hanzo) {
|
||||
delete config.mcpServers.hanzo;
|
||||
if (Object.keys(config.mcpServers).length === 0) delete config.mcpServers;
|
||||
}
|
||||
writeJson(configPath(), config);
|
||||
},
|
||||
|
||||
status(): TargetStatus {
|
||||
const key = readJson<DesktopConfig>(configPath()).mcpServers?.hanzo?.env?.['HANZO_API_KEY'];
|
||||
// Claude Desktop is a GUI app — check the config file exists instead of PATH.
|
||||
const configured = typeof key === 'string' && key.startsWith('hk-');
|
||||
return {
|
||||
installed: isInstalled('claude') || isInstalled('Claude'),
|
||||
configured,
|
||||
...(typeof key === 'string' ? { apiKeyMasked: maskKey(key) } : {}),
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -6,9 +6,10 @@
|
||||
|
||||
import type { CodingTarget } from './types';
|
||||
import { claudeCode } from './claude-code';
|
||||
import { claudeDesktop } from './claude-desktop';
|
||||
import { codex } from './codex';
|
||||
|
||||
export const TARGETS: readonly CodingTarget[] = [claudeCode, codex];
|
||||
export const TARGETS: readonly CodingTarget[] = [claudeCode, claudeDesktop, codex];
|
||||
|
||||
export function getTarget(id: string): CodingTarget | undefined {
|
||||
return TARGETS.find((t) => t.id === id);
|
||||
|
||||
Reference in New Issue
Block a user