@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 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
- if (previous === backendName) {
86
- console.log(`Already using ${displayName(backendName)} as backend.`);
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
- console.log(`Active backend: ${backend.activeBackend ? displayName(backend.activeBackend) : "(none)"}`);
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
@@ -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;
@@ -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, model);
375
+ return dispatchGemini(wrappedPrompt, startTime, log, resolvedModel);
372
376
  }
373
377
  if (backend === "codex") {
374
- return dispatchCodex(wrappedPrompt, startTime, model);
378
+ return dispatchCodex(wrappedPrompt, startTime, resolvedModel);
375
379
  }
376
380
  if (backend === "claudecode") {
377
- return dispatchClaude(wrappedPrompt, startTime, model);
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", geminiModel,
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
- const codexModel = model ?? process.env.CLAUDE_CODEX_MODEL;
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,
@@ -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
- saveBackendConfig({ activeBackend });
81
- log(`[config] Backend switched to ${activeBackend}.`);
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
@@ -1,3 +1,4 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ export declare const PACKAGE_VERSION = "0.20.0";
2
3
  export declare function createServer(deviceName?: string): McpServer;
3
4
  export declare function startStdioServer(deviceName?: string): Promise<void>;
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.19.1";
8
+ export const PACKAGE_VERSION = "0.20.0";
9
9
  export function createServer(deviceName) {
10
10
  const server = new McpServer({
11
11
  name: "zhihand",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhihand/mcp",
3
- "version": "0.19.1",
3
+ "version": "0.20.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "ZhiHand MCP Server — phone control tools for Claude Code, Codex, Gemini CLI, and OpenClaw",