@zhihand/mcp 0.34.0 → 0.35.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
@@ -605,7 +605,7 @@ switch (command) {
605
605
  { id: 17, phase: "Text+Keys", label: "Key combo (select all)", kind: "hid", platformAware: "select_all" },
606
606
  { id: 18, phase: "Navigation", label: "Press Home", kind: "hid", params: { action: "home" } },
607
607
  { id: 19, phase: "Navigation", label: "Press Back", kind: "hid", params: { action: "back" } },
608
- { id: 20, phase: "Navigation", label: "Open WeChat", kind: "hid", platformAware: "open_wechat" },
608
+ { id: 20, phase: "Navigation", label: "Open Settings", kind: "hid", platformAware: "open_settings" },
609
609
  { id: 21, phase: "Clipboard", label: "Clipboard set", kind: "hid", platformAware: "clipboard_set" },
610
610
  { id: 22, phase: "System Nav", label: "Notification shade", kind: "system", params: { action: "notification" } },
611
611
  { id: 23, phase: "System Nav", label: "Recent apps", kind: "system", params: { action: "recent" } },
@@ -758,10 +758,10 @@ switch (command) {
758
758
 
759
759
  function resolvePlatformAwareParams(variant) {
760
760
  const platform = getDevicePlatform();
761
- if (variant === "open_wechat") {
761
+ if (variant === "open_settings") {
762
762
  return platform === "ios"
763
- ? { action: "open_app", bundleId: "com.tencent.xin" }
764
- : { action: "open_app", appPackage: "com.tencent.mm" };
763
+ ? { action: "open_app", bundleId: "com.apple.Preferences" }
764
+ : { action: "open_app", appPackage: "com.android.settings" };
765
765
  }
766
766
  if (variant === "clipboard_set") {
767
767
  return { action: "clipboard", text: `zhihand_test_${Date.now()}` };
package/dist/core/pair.js CHANGED
@@ -76,8 +76,20 @@ export async function ensurePluginIdentity(endpoint) {
76
76
  plugin_secret: plugin.plugin_secret,
77
77
  };
78
78
  savePluginIdentity(identity);
79
+ // Notify running daemon to hot-reload identity (best-effort, daemon may not be running)
80
+ notifyDaemonReload();
79
81
  return identity;
80
82
  }
83
+ /** Best-effort POST to daemon's reload-identity endpoint. */
84
+ function notifyDaemonReload() {
85
+ const port = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
86
+ fetch(`http://127.0.0.1:${port}/internal/reload-identity`, {
87
+ method: "POST",
88
+ signal: AbortSignal.timeout(3_000),
89
+ }).catch(() => {
90
+ // Daemon not running — that's fine, next start will pick up identity
91
+ });
92
+ }
81
93
  /**
82
94
  * Poll pairing session until claimed or expired.
83
95
  */
@@ -64,6 +64,8 @@ function onPromptReceived(config, prompt) {
64
64
  }
65
65
  }
66
66
  // ── Internal API ───────────────────────────────────────────
67
+ /** Set by startDaemon to allow identity hot-reload via /internal/reload-identity */
68
+ let reloadIdentityHandler = null;
67
69
  function handleInternalAPI(req, res) {
68
70
  const url = req.url ?? "";
69
71
  if (url === "/internal/backend" && req.method === "POST") {
@@ -145,6 +147,18 @@ function handleInternalAPI(req, res) {
145
147
  });
146
148
  return true;
147
149
  }
150
+ if (url === "/internal/reload-identity" && req.method === "POST") {
151
+ dbg(`[api] POST /internal/reload-identity`);
152
+ if (!reloadIdentityHandler) {
153
+ res.writeHead(503, { "Content-Type": "application/json" });
154
+ res.end(JSON.stringify({ error: "Daemon not ready" }));
155
+ return true;
156
+ }
157
+ const result = reloadIdentityHandler();
158
+ res.writeHead(result.ok ? 200 : 500, { "Content-Type": "application/json" });
159
+ res.end(JSON.stringify(result));
160
+ return true;
161
+ }
148
162
  if (url === "/internal/status" && req.method === "GET") {
149
163
  dbg(`[api] GET /internal/status`);
150
164
  const effectiveModel = activeBackend ? (activeModel ?? DEFAULT_MODELS[activeBackend]) : null;
@@ -359,45 +373,67 @@ export async function startDaemon(options) {
359
373
  httpServer.listen(port, "127.0.0.1", () => resolve());
360
374
  });
361
375
  writePid();
362
- // Load Plugin identity for edge-level heartbeat + prompt WS
363
- const identity = loadPluginIdentity();
364
- if (!identity) {
365
- log("[identity] No plugin identity found. Run 'zhihand pair' first.");
366
- process.exit(1);
376
+ // ── Identity hot-reload support ──────────────────────────
377
+ let currentHeartbeatTarget = null;
378
+ let promptListener = null;
379
+ /** (Re)start heartbeat + prompt listener with current identity. Stops previous if running. */
380
+ function activateIdentity(identity) {
381
+ // Stop previous if running
382
+ if (promptListener)
383
+ promptListener.stop();
384
+ stopHeartbeatLoop();
385
+ currentHeartbeatTarget = {
386
+ controlPlaneEndpoint: resolveDefaultEndpoint(),
387
+ edgeId: identity.edge_id,
388
+ pluginSecret: identity.plugin_secret,
389
+ };
390
+ startHeartbeatLoop(currentHeartbeatTarget, log);
391
+ promptListener = new PromptListener({
392
+ controlPlaneEndpoint: resolveDefaultEndpoint(),
393
+ edgeId: identity.edge_id,
394
+ pluginSecret: identity.plugin_secret,
395
+ }, (prompt) => onPromptReceived(config, prompt), log, (reason) => {
396
+ log(`[fatal] ${reason}`);
397
+ process.exit(1);
398
+ });
399
+ promptListener.start();
400
+ log(`[identity] Active: edge_id=${identity.edge_id}, stable_identity=${identity.stable_identity}`);
367
401
  }
368
- log(`[identity] Loaded: edge_id=${identity.edge_id}, stable_identity=${identity.stable_identity}`);
369
- const heartbeatTarget = {
370
- controlPlaneEndpoint: resolveDefaultEndpoint(),
371
- edgeId: identity.edge_id,
372
- pluginSecret: identity.plugin_secret,
402
+ // Wire up the reload-identity API handler
403
+ reloadIdentityHandler = () => {
404
+ const identity = loadPluginIdentity();
405
+ if (!identity) {
406
+ return { ok: false, error: "No identity.json found" };
407
+ }
408
+ activateIdentity(identity);
409
+ return { ok: true, edge_id: identity.edge_id };
373
410
  };
374
- // Start heartbeat (edge-level, pluginSecret auth)
375
- startHeartbeatLoop(heartbeatTarget, log);
376
- // Start prompt listener (edge-level single WS)
377
- const promptListener = new PromptListener({
378
- controlPlaneEndpoint: resolveDefaultEndpoint(),
379
- edgeId: identity.edge_id,
380
- pluginSecret: identity.plugin_secret,
381
- }, (prompt) => onPromptReceived(config, prompt), log, (reason) => {
382
- log(`[fatal] ${reason}`);
383
- process.exit(1);
384
- });
385
- promptListener.start();
411
+ // Try loading identity at startup (non-fatal if missing)
412
+ const initialIdentity = loadPluginIdentity();
413
+ if (initialIdentity) {
414
+ activateIdentity(initialIdentity);
415
+ }
416
+ else {
417
+ log("[identity] No plugin identity found. Waiting for 'zhihand pair'...");
418
+ }
386
419
  log(`ZhiHand daemon started.`);
387
420
  log(` PID: ${process.pid}`);
388
421
  log(` MCP: http://127.0.0.1:${port}/mcp`);
389
422
  log(` Backend: ${activeBackend ?? "(none)"}`);
390
- log(` Edge: ${identity.edge_id}`);
423
+ if (initialIdentity)
424
+ log(` Edge: ${initialIdentity.edge_id}`);
391
425
  log(` Device: ${config.credentialId}`);
392
426
  log(`Listening for prompts...`);
393
427
  // Graceful shutdown
394
428
  const shutdown = async () => {
395
429
  log("\nShutting down...");
396
- promptListener.stop();
430
+ if (promptListener)
431
+ promptListener.stop();
397
432
  stopHeartbeatLoop();
398
433
  clearInterval(sessionCleanupTimer);
399
434
  await killActiveChild();
400
- await sendBrainOffline(heartbeatTarget);
435
+ if (currentHeartbeatTarget)
436
+ await sendBrainOffline(currentHeartbeatTarget);
401
437
  // Close all MCP sessions
402
438
  for (const session of mcpSessions.values()) {
403
439
  try {
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- export declare const PACKAGE_VERSION = "0.34.0";
2
+ export declare const PACKAGE_VERSION = "0.35.0";
3
3
  export declare function createServer(): McpServer;
4
4
  export declare function startStdioServer(): Promise<void>;
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@ import { handlePair } from "./tools/pair.js";
8
8
  import { resolveTargetDevice } from "./tools/resolve.js";
9
9
  import { buildControlToolDescription, buildSystemToolDescription, buildScreenshotToolDescription, formatDeviceStatus, extractDynamic, } from "./core/device.js";
10
10
  import { registry } from "./core/registry.js";
11
- export const PACKAGE_VERSION = "0.34.0";
11
+ export const PACKAGE_VERSION = "0.35.0";
12
12
  function errorResult(message) {
13
13
  return { content: [{ type: "text", text: message }], isError: true };
14
14
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhihand/mcp",
3
- "version": "0.34.0",
3
+ "version": "0.35.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",