@zhihand/mcp 0.19.1 → 0.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/zhihand +21 -11
- package/dist/core/config.d.ts +9 -0
- package/dist/core/config.js +12 -0
- package/dist/daemon/dispatcher.js +37 -10
- package/dist/daemon/index.js +24 -7
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/bin/zhihand
CHANGED
|
@@ -6,7 +6,7 @@ import { startStdioServer } from "../dist/index.js";
|
|
|
6
6
|
import { startDaemon, stopDaemon, isAlreadyRunning } from "../dist/daemon/index.js";
|
|
7
7
|
import { detectCLITools, formatDetectedTools } from "../dist/cli/detect.js";
|
|
8
8
|
import { detectAndSetupOpenClaw } from "../dist/cli/openclaw.js";
|
|
9
|
-
import { loadDefaultCredential, loadBackendConfig, saveBackendConfig } from "../dist/core/config.js";
|
|
9
|
+
import { loadDefaultCredential, loadBackendConfig, saveBackendConfig, DEFAULT_MODELS } from "../dist/core/config.js";
|
|
10
10
|
import { executePairing } from "../dist/core/pair.js";
|
|
11
11
|
import { configureMCP, displayName } from "../dist/cli/mcp-config.js";
|
|
12
12
|
|
|
@@ -23,6 +23,7 @@ const { positionals, values } = parseArgs({
|
|
|
23
23
|
strict: false,
|
|
24
24
|
options: {
|
|
25
25
|
device: { type: "string" },
|
|
26
|
+
model: { type: "string", short: "m" },
|
|
26
27
|
help: { type: "boolean", short: "h", default: false },
|
|
27
28
|
detach: { type: "boolean", short: "d", default: false },
|
|
28
29
|
port: { type: "string" },
|
|
@@ -41,9 +42,10 @@ Usage:
|
|
|
41
42
|
zhihand stop Stop daemon
|
|
42
43
|
zhihand status Show status (pairing, backend, brain)
|
|
43
44
|
|
|
44
|
-
zhihand gemini Switch backend to Gemini CLI
|
|
45
|
-
zhihand claude Switch backend to Claude Code
|
|
46
|
-
zhihand codex Switch backend to Codex CLI
|
|
45
|
+
zhihand gemini Switch backend to Gemini CLI (default model: flash)
|
|
46
|
+
zhihand claude Switch backend to Claude Code (default model: sonnet)
|
|
47
|
+
zhihand codex Switch backend to Codex CLI (default model: gpt-5.4-mini)
|
|
48
|
+
zhihand gemini --model pro Switch backend with custom model
|
|
47
49
|
|
|
48
50
|
zhihand setup Interactive setup: pair + configure + start
|
|
49
51
|
zhihand pair Pair with a phone device
|
|
@@ -53,6 +55,7 @@ Usage:
|
|
|
53
55
|
|
|
54
56
|
Options:
|
|
55
57
|
--device <name> Use a specific paired device
|
|
58
|
+
--model, -m <name> Set model alias (e.g. flash, pro, sonnet, opus, gpt-5.4-mini)
|
|
56
59
|
--port <port> Override daemon port (default: 18686)
|
|
57
60
|
-d, --detach Run daemon in background
|
|
58
61
|
-h, --help Show this help
|
|
@@ -82,12 +85,15 @@ if (Object.prototype.hasOwnProperty.call(CLI_TOOL_MAP, command)) {
|
|
|
82
85
|
const config = loadBackendConfig();
|
|
83
86
|
const previous = config.activeBackend;
|
|
84
87
|
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
const userModel = values.model ?? null;
|
|
89
|
+
const effectiveModel = userModel ?? DEFAULT_MODELS[backendName];
|
|
90
|
+
|
|
91
|
+
if (previous === backendName && !userModel) {
|
|
92
|
+
console.log(`Already using ${displayName(backendName)} as backend (model: ${effectiveModel}).`);
|
|
87
93
|
process.exit(0);
|
|
88
94
|
}
|
|
89
95
|
|
|
90
|
-
console.log(`Switching backend to ${displayName(backendName)}...`);
|
|
96
|
+
console.log(`Switching backend to ${displayName(backendName)} (model: ${effectiveModel})...`);
|
|
91
97
|
|
|
92
98
|
// Configure MCP (HTTP transport)
|
|
93
99
|
const { configured, removed } = configureMCP(backendName, previous);
|
|
@@ -100,7 +106,7 @@ if (Object.prototype.hasOwnProperty.call(CLI_TOOL_MAP, command)) {
|
|
|
100
106
|
const res = await fetch(`http://127.0.0.1:${port}/internal/backend`, {
|
|
101
107
|
method: "POST",
|
|
102
108
|
headers: { "Content-Type": "application/json" },
|
|
103
|
-
body: JSON.stringify({ backend: backendName }),
|
|
109
|
+
body: JSON.stringify({ backend: backendName, model: userModel }),
|
|
104
110
|
signal: AbortSignal.timeout(5000),
|
|
105
111
|
});
|
|
106
112
|
if (res.ok) {
|
|
@@ -108,11 +114,11 @@ if (Object.prototype.hasOwnProperty.call(CLI_TOOL_MAP, command)) {
|
|
|
108
114
|
}
|
|
109
115
|
} catch {
|
|
110
116
|
// Daemon not responding, just save config
|
|
111
|
-
saveBackendConfig({ activeBackend: backendName });
|
|
117
|
+
saveBackendConfig({ activeBackend: backendName, model: userModel });
|
|
112
118
|
console.log(`\nBackend config saved. Daemon not responding — restart with 'zhihand start'.`);
|
|
113
119
|
}
|
|
114
120
|
} else {
|
|
115
|
-
saveBackendConfig({ activeBackend: backendName });
|
|
121
|
+
saveBackendConfig({ activeBackend: backendName, model: userModel });
|
|
116
122
|
console.log(`\nBackend switched to ${displayName(backendName)}.`);
|
|
117
123
|
console.log(`Start the daemon to receive prompts: zhihand start`);
|
|
118
124
|
}
|
|
@@ -199,7 +205,11 @@ switch (command) {
|
|
|
199
205
|
} else {
|
|
200
206
|
console.log("No paired device. Run: zhihand setup");
|
|
201
207
|
}
|
|
202
|
-
|
|
208
|
+
const backendLabel = backend.activeBackend ? displayName(backend.activeBackend) : "(none)";
|
|
209
|
+
const modelLabel = backend.activeBackend
|
|
210
|
+
? (backend.model ?? DEFAULT_MODELS[backend.activeBackend])
|
|
211
|
+
: "-";
|
|
212
|
+
console.log(`Active backend: ${backendLabel} (model: ${modelLabel})`);
|
|
203
213
|
console.log(`Daemon: ${daemonPid ? `running (PID ${daemonPid})` : "not running"}`);
|
|
204
214
|
|
|
205
215
|
// If daemon running, get live status
|
package/dist/core/config.d.ts
CHANGED
|
@@ -19,7 +19,16 @@ export interface ZhiHandConfig {
|
|
|
19
19
|
export type BackendName = "claudecode" | "codex" | "gemini" | "openclaw";
|
|
20
20
|
export interface BackendConfig {
|
|
21
21
|
activeBackend: BackendName | null;
|
|
22
|
+
model?: string | null;
|
|
22
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Default model aliases per backend.
|
|
26
|
+
* These are generic aliases that the respective CLIs resolve to the latest version:
|
|
27
|
+
* - Gemini CLI: "flash" → latest flash model (e.g. gemini-2.5-flash)
|
|
28
|
+
* - Claude Code: "sonnet" → latest sonnet (e.g. claude-sonnet-4-20250514)
|
|
29
|
+
* - Codex CLI: requires full model name, no alias support
|
|
30
|
+
*/
|
|
31
|
+
export declare const DEFAULT_MODELS: Record<Exclude<BackendName, "openclaw">, string>;
|
|
23
32
|
export declare function resolveZhiHandDir(): string;
|
|
24
33
|
export declare function ensureZhiHandDir(): void;
|
|
25
34
|
export declare function loadCredentialStore(): CredentialStore | null;
|
package/dist/core/config.js
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
/**
|
|
5
|
+
* Default model aliases per backend.
|
|
6
|
+
* These are generic aliases that the respective CLIs resolve to the latest version:
|
|
7
|
+
* - Gemini CLI: "flash" → latest flash model (e.g. gemini-2.5-flash)
|
|
8
|
+
* - Claude Code: "sonnet" → latest sonnet (e.g. claude-sonnet-4-20250514)
|
|
9
|
+
* - Codex CLI: requires full model name, no alias support
|
|
10
|
+
*/
|
|
11
|
+
export const DEFAULT_MODELS = {
|
|
12
|
+
gemini: "flash", // Gemini CLI resolves to latest flash
|
|
13
|
+
claudecode: "sonnet", // Claude Code resolves to latest sonnet
|
|
14
|
+
codex: "gpt-5.4-mini", // Codex default: latest GPT mini model
|
|
15
|
+
};
|
|
4
16
|
const ZHIHAND_DIR = path.join(os.homedir(), ".zhihand");
|
|
5
17
|
const CREDENTIALS_PATH = path.join(ZHIHAND_DIR, "credentials.json");
|
|
6
18
|
const STATE_PATH = path.join(ZHIHAND_DIR, "state.json");
|
|
@@ -3,6 +3,7 @@ import fsp from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { DEFAULT_MODELS } from "../core/config.js";
|
|
6
7
|
import { resolveGemini, resolveClaude, resolveCodex } from "../core/resolve-path.js";
|
|
7
8
|
const CLI_TIMEOUT = 120_000; // 120s
|
|
8
9
|
const SIGKILL_DELAY = 2_000; // 2s after SIGTERM
|
|
@@ -367,14 +368,17 @@ ${userPrompt}`;
|
|
|
367
368
|
export function dispatchToCLI(backend, prompt, log, model) {
|
|
368
369
|
const startTime = Date.now();
|
|
369
370
|
const wrappedPrompt = wrapPrompt(prompt);
|
|
371
|
+
// Resolve model: explicit > env > default
|
|
372
|
+
const resolvedModel = resolveModel(backend, model);
|
|
373
|
+
log(`[dispatch] Backend: ${backend}, Model: ${resolvedModel}`);
|
|
370
374
|
if (backend === "gemini") {
|
|
371
|
-
return dispatchGemini(wrappedPrompt, startTime, log,
|
|
375
|
+
return dispatchGemini(wrappedPrompt, startTime, log, resolvedModel);
|
|
372
376
|
}
|
|
373
377
|
if (backend === "codex") {
|
|
374
|
-
return dispatchCodex(wrappedPrompt, startTime,
|
|
378
|
+
return dispatchCodex(wrappedPrompt, startTime, resolvedModel);
|
|
375
379
|
}
|
|
376
380
|
if (backend === "claudecode") {
|
|
377
|
-
return dispatchClaude(wrappedPrompt, startTime,
|
|
381
|
+
return dispatchClaude(wrappedPrompt, startTime, resolvedModel);
|
|
378
382
|
}
|
|
379
383
|
return Promise.resolve({
|
|
380
384
|
text: `Unsupported backend: ${backend}`,
|
|
@@ -382,12 +386,38 @@ export function dispatchToCLI(backend, prompt, log, model) {
|
|
|
382
386
|
durationMs: 0,
|
|
383
387
|
});
|
|
384
388
|
}
|
|
389
|
+
/**
|
|
390
|
+
* Resolve the model to use for a backend.
|
|
391
|
+
* Priority: explicit parameter > ZHIHAND_MODEL env > backend-specific env > default alias.
|
|
392
|
+
*
|
|
393
|
+
* Each backend CLI handles alias→full-name resolution natively:
|
|
394
|
+
* - Gemini CLI: "flash" → gemini-2.5-flash, "pro" → gemini-2.5-pro
|
|
395
|
+
* - Claude Code: "sonnet" → claude-sonnet-4-*, "opus" → claude-opus-4-*, "haiku" → claude-haiku-4-*
|
|
396
|
+
* - Codex CLI: no alias support — pass full model name directly (e.g. "o4-mini", "codex-mini")
|
|
397
|
+
*/
|
|
398
|
+
function resolveModel(backend, explicit) {
|
|
399
|
+
if (explicit)
|
|
400
|
+
return explicit;
|
|
401
|
+
// Global env override
|
|
402
|
+
const globalEnv = process.env.ZHIHAND_MODEL;
|
|
403
|
+
if (globalEnv)
|
|
404
|
+
return globalEnv;
|
|
405
|
+
// Per-backend env override
|
|
406
|
+
const envMap = {
|
|
407
|
+
gemini: process.env.ZHIHAND_GEMINI_MODEL,
|
|
408
|
+
claudecode: process.env.ZHIHAND_CLAUDE_MODEL,
|
|
409
|
+
codex: process.env.ZHIHAND_CODEX_MODEL,
|
|
410
|
+
};
|
|
411
|
+
const perBackend = envMap[backend];
|
|
412
|
+
if (perBackend)
|
|
413
|
+
return perBackend;
|
|
414
|
+
return DEFAULT_MODELS[backend];
|
|
415
|
+
}
|
|
385
416
|
// ── Gemini Dispatch (PTY + Session File Monitoring) ────────
|
|
386
417
|
function dispatchGemini(prompt, startTime, log, model) {
|
|
387
|
-
const geminiModel = model ?? process.env.CLAUDE_GEMINI_MODEL ?? "gemini-3.1-pro-preview";
|
|
388
418
|
const cliArgs = [
|
|
389
419
|
"--approval-mode", "yolo",
|
|
390
|
-
"--model",
|
|
420
|
+
"--model", model,
|
|
391
421
|
"-i", prompt,
|
|
392
422
|
];
|
|
393
423
|
const env = {
|
|
@@ -414,10 +444,7 @@ function dispatchCodex(prompt, startTime, model) {
|
|
|
414
444
|
// --dangerously-bypass-approvals-and-sandbox is required so MCP tool calls
|
|
415
445
|
// are not auto-cancelled in non-interactive mode (--full-auto cancels them)
|
|
416
446
|
const args = ["exec", "--dangerously-bypass-approvals-and-sandbox", "--skip-git-repo-check", "--json"];
|
|
417
|
-
|
|
418
|
-
if (codexModel) {
|
|
419
|
-
args.push("-m", codexModel);
|
|
420
|
-
}
|
|
447
|
+
args.push("-m", model);
|
|
421
448
|
args.push(prompt);
|
|
422
449
|
const codexPath = resolveCodex();
|
|
423
450
|
const child = spawn(codexPath, args, {
|
|
@@ -431,7 +458,7 @@ function dispatchCodex(prompt, startTime, model) {
|
|
|
431
458
|
// ── Claude Dispatch ────────────────────────────────────────
|
|
432
459
|
function dispatchClaude(prompt, startTime, model) {
|
|
433
460
|
const claudePath = resolveClaude();
|
|
434
|
-
const child = spawn(claudePath, ["-p", prompt, "--output-format", "json"], {
|
|
461
|
+
const child = spawn(claudePath, ["-p", prompt, "--model", model, "--output-format", "json"], {
|
|
435
462
|
env: process.env,
|
|
436
463
|
stdio: ["ignore", "pipe", "pipe"],
|
|
437
464
|
detached: false,
|
package/dist/daemon/index.js
CHANGED
|
@@ -5,7 +5,8 @@ import path from "node:path";
|
|
|
5
5
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
6
6
|
// Transport type used only for cleanup interface
|
|
7
7
|
import { createServer as createMcpServer } from "../index.js";
|
|
8
|
-
import { resolveConfig, loadBackendConfig, saveBackendConfig, resolveZhiHandDir, ensureZhiHandDir, } from "../core/config.js";
|
|
8
|
+
import { resolveConfig, loadBackendConfig, saveBackendConfig, resolveZhiHandDir, ensureZhiHandDir, DEFAULT_MODELS, } from "../core/config.js";
|
|
9
|
+
import { PACKAGE_VERSION } from "../index.js";
|
|
9
10
|
import { startHeartbeatLoop, stopHeartbeatLoop, sendBrainOffline } from "./heartbeat.js";
|
|
10
11
|
import { PromptListener } from "./prompt-listener.js";
|
|
11
12
|
import { dispatchToCLI, postReply, killActiveChild } from "./dispatcher.js";
|
|
@@ -13,6 +14,7 @@ const DEFAULT_PORT = 18686;
|
|
|
13
14
|
const PID_FILE = "daemon.pid";
|
|
14
15
|
// ── State ──────────────────────────────────────────────────
|
|
15
16
|
let activeBackend = null;
|
|
17
|
+
let activeModel = null; // user-selected model alias, null = use default
|
|
16
18
|
let isProcessing = false;
|
|
17
19
|
const promptQueue = [];
|
|
18
20
|
function log(msg) {
|
|
@@ -28,7 +30,7 @@ async function processPrompt(config, prompt) {
|
|
|
28
30
|
}
|
|
29
31
|
const preview = prompt.text.length > 40 ? prompt.text.slice(0, 40) + "..." : prompt.text;
|
|
30
32
|
log(`[relay] Prompt: "${preview}" → dispatching to ${activeBackend}...`);
|
|
31
|
-
const result = await dispatchToCLI(activeBackend, prompt.text, log);
|
|
33
|
+
const result = await dispatchToCLI(activeBackend, prompt.text, log, activeModel ?? undefined);
|
|
32
34
|
const ok = await postReply(config, prompt.id, result.text);
|
|
33
35
|
const dur = (result.durationMs / 1000).toFixed(1);
|
|
34
36
|
if (ok) {
|
|
@@ -69,7 +71,7 @@ function handleInternalAPI(req, res) {
|
|
|
69
71
|
});
|
|
70
72
|
req.on("end", () => {
|
|
71
73
|
try {
|
|
72
|
-
const { backend } = JSON.parse(body);
|
|
74
|
+
const { backend, model } = JSON.parse(body);
|
|
73
75
|
const allowed = ["claudecode", "codex", "gemini"];
|
|
74
76
|
if (!allowed.includes(backend)) {
|
|
75
77
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -77,10 +79,12 @@ function handleInternalAPI(req, res) {
|
|
|
77
79
|
return;
|
|
78
80
|
}
|
|
79
81
|
activeBackend = backend;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
+
activeModel = model ?? null;
|
|
83
|
+
saveBackendConfig({ activeBackend, model: activeModel });
|
|
84
|
+
const effectiveModel = activeModel ?? DEFAULT_MODELS[activeBackend];
|
|
85
|
+
log(`[config] Backend switched to ${activeBackend}, model: ${effectiveModel}`);
|
|
82
86
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
83
|
-
res.end(JSON.stringify({ ok: true, backend: activeBackend }));
|
|
87
|
+
res.end(JSON.stringify({ ok: true, backend: activeBackend, model: effectiveModel }));
|
|
84
88
|
}
|
|
85
89
|
catch {
|
|
86
90
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -90,9 +94,12 @@ function handleInternalAPI(req, res) {
|
|
|
90
94
|
return true;
|
|
91
95
|
}
|
|
92
96
|
if (url === "/internal/status" && req.method === "GET") {
|
|
97
|
+
const effectiveModel = activeBackend ? (activeModel ?? DEFAULT_MODELS[activeBackend]) : null;
|
|
93
98
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
94
99
|
res.end(JSON.stringify({
|
|
100
|
+
version: PACKAGE_VERSION,
|
|
95
101
|
backend: activeBackend,
|
|
102
|
+
model: effectiveModel,
|
|
96
103
|
processing: isProcessing,
|
|
97
104
|
queueLength: promptQueue.length,
|
|
98
105
|
pid: process.pid,
|
|
@@ -155,9 +162,19 @@ export async function startDaemon(options) {
|
|
|
155
162
|
log("Run 'zhihand setup' to pair a device first.");
|
|
156
163
|
process.exit(1);
|
|
157
164
|
}
|
|
158
|
-
// Load backend
|
|
165
|
+
// Load backend + model
|
|
159
166
|
const backendConfig = loadBackendConfig();
|
|
160
167
|
activeBackend = backendConfig.activeBackend ?? null;
|
|
168
|
+
activeModel = backendConfig.model ?? null;
|
|
169
|
+
// Log startup info
|
|
170
|
+
log(`ZhiHand v${PACKAGE_VERSION} starting...`);
|
|
171
|
+
if (activeBackend) {
|
|
172
|
+
const effectiveModel = activeModel ?? DEFAULT_MODELS[activeBackend];
|
|
173
|
+
log(`[config] Backend: ${activeBackend}, Model: ${effectiveModel}`);
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
log(`[config] No backend configured. Use: zhihand gemini / zhihand claude / zhihand codex`);
|
|
177
|
+
}
|
|
161
178
|
// MCP sessions: each client gets its own McpServer + Transport pair
|
|
162
179
|
// because McpServer.connect() can only be called once per instance
|
|
163
180
|
const MAX_MCP_SESSIONS = 20;
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { controlSchema, screenshotSchema, pairSchema } from "./tools/schemas.js"
|
|
|
5
5
|
import { executeControl } from "./tools/control.js";
|
|
6
6
|
import { handleScreenshot } from "./tools/screenshot.js";
|
|
7
7
|
import { handlePair } from "./tools/pair.js";
|
|
8
|
-
const PACKAGE_VERSION = "0.
|
|
8
|
+
export const PACKAGE_VERSION = "0.20.0";
|
|
9
9
|
export function createServer(deviceName) {
|
|
10
10
|
const server = new McpServer({
|
|
11
11
|
name: "zhihand",
|