@zhihand/mcp 0.32.3 → 0.32.5

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
@@ -30,7 +30,7 @@ import { fetchUserCredentials } from "../dist/core/ws.js";
30
30
  import { configureMCP, displayName } from "../dist/cli/mcp-config.js";
31
31
 
32
32
  const DEFAULT_ENDPOINT = "https://api.zhihand.com";
33
- const VERSION = "0.32.3";
33
+ const VERSION = "0.32.5";
34
34
 
35
35
  const CLI_TOOL_MAP = {
36
36
  claude: "claudecode",
@@ -652,7 +652,7 @@ switch (command) {
652
652
  }
653
653
  }
654
654
 
655
- console.log("ZhiHand Device Test");
655
+ console.log("🔧 ZhiHand Device Test");
656
656
  console.log(` Device: ${testConfig.credentialId}`);
657
657
  console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}\n`);
658
658
 
@@ -672,18 +672,18 @@ switch (command) {
672
672
  } catch { /* non-fatal */ }
673
673
  const getDevicePlatform = () => currentProfile?.platform ?? "unknown";
674
674
 
675
- console.log(" -- Capability readiness --");
675
+ console.log(" ── Capability readiness ──");
676
676
  if (!currentCaps) {
677
- console.log(" [!] Device profile not loaded — all capability gates will allow tests through.");
677
+ console.log(" ⚠️ Device profile not loaded — all capability gates will allow tests through.");
678
678
  } else {
679
- const fmt = (name, cap) => ` ${cap.ready ? "[ok]" : "[!]"} ${name.padEnd(14)} ${cap.ready ? "ready" : "NOT ready"} — ${cap.reason}`;
679
+ const fmt = (name, cap) => ` ${cap.ready ? "" : "⚠️"} ${name.padEnd(16)} ${cap.ready ? "ready" : "NOT ready"} — ${cap.reason}`;
680
680
  console.log(fmt("screen_sharing", currentCaps.screen_sharing));
681
681
  console.log(fmt("hid", currentCaps.hid));
682
682
  console.log(fmt("live_session", currentCaps.live_session));
683
683
  const ageStr = currentCaps.profile.age_ms >= 0 ? `${(currentCaps.profile.age_ms / 1000).toFixed(1)}s` : "unknown";
684
- console.log(` ${currentCaps.profile.stale ? "[!]" : "[ok]"} profile age=${ageStr}${currentCaps.profile.stale ? " (STALE)" : ""}`);
684
+ console.log(` ${currentCaps.profile.stale ? "⚠️" : ""} profile age=${ageStr}${currentCaps.profile.stale ? " (STALE)" : ""}`);
685
685
  if (forceRun) {
686
- console.log(" --force passed: capability gates disabled.");
686
+ console.log(" --force passed: capability gates disabled.");
687
687
  }
688
688
  }
689
689
  console.log("");
@@ -719,18 +719,18 @@ switch (command) {
719
719
  const ackStatus = ack.command?.ack_status ?? "ok";
720
720
  const resultInfo = ack.command?.ack_result ? ` ${JSON.stringify(ack.command.ack_result)}` : "";
721
721
  if (ackStatus === "ok") {
722
- console.log(`[PASS] (${ms}ms)${resultInfo}`);
722
+ console.log(`✅ (${ms}ms)${resultInfo}`);
723
723
  passed++;
724
724
  } else {
725
- console.log(`[FAIL] [${ackStatus}] (${ms}ms)${resultInfo}`);
725
+ console.log(`❌ [${ackStatus}] (${ms}ms)${resultInfo}`);
726
726
  failed++;
727
727
  }
728
728
  } else {
729
- console.log(`[TIMEOUT] (${ms}ms)`);
729
+ console.log(`⏱️ timeout (${ms}ms)`);
730
730
  failed++;
731
731
  }
732
732
  } catch (err) {
733
- console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
733
+ console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
734
734
  failed++;
735
735
  }
736
736
  }
@@ -739,7 +739,7 @@ switch (command) {
739
739
  const currentPlatform = getDevicePlatform();
740
740
  if (t.platform && t.platform !== currentPlatform) {
741
741
  totalSteps++; skipped++;
742
- console.log(` ${String(t.id).padStart(2)}. ${t.label}... [SKIP] (${t.platform}-only, device is ${currentPlatform})`);
742
+ console.log(` ${String(t.id).padStart(2)}. ${t.label}... ⏭️ (${t.platform}-only, device is ${currentPlatform})`);
743
743
  return;
744
744
  }
745
745
  if (!forceRun && currentCaps) {
@@ -748,7 +748,7 @@ switch (command) {
748
748
  const gate = requiredCap === "screen" ? currentCaps.screen_sharing : currentCaps.hid;
749
749
  if (!gate.ready) {
750
750
  totalSteps++; skipped++;
751
- console.log(` ${String(t.id).padStart(2)}. ${t.label}... [SKIP] (${requiredCap} not ready: ${gate.reason})`);
751
+ console.log(` ${String(t.id).padStart(2)}. ${t.label}... ⏭️ (${requiredCap} not ready: ${gate.reason})`);
752
752
  return;
753
753
  }
754
754
  }
@@ -768,14 +768,14 @@ switch (command) {
768
768
  currentProfile = extractStatic(currentRawAttrs);
769
769
  currentCaps = computeCapabilities(currentRawAttrs, profileReceivedAtMs);
770
770
  const s = currentProfile;
771
- console.log(`[PASS] ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
771
+ console.log(`✅ ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
772
772
  passed++;
773
773
  } else {
774
- console.log(`[!] Loaded but empty (${ms}ms)`);
774
+ console.log(`⚠️ loaded but empty (${ms}ms)`);
775
775
  failed++;
776
776
  }
777
777
  } catch (err) {
778
- console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
778
+ console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
779
779
  failed++;
780
780
  }
781
781
  break;
@@ -805,12 +805,12 @@ switch (command) {
805
805
  const capReadySummary = ["screen_sharing", "hid", "live_session"]
806
806
  .map((k) => `${k}=${caps[k]?.ready ? "ready" : "not-ready"}`)
807
807
  .join(", ");
808
- console.log(`[PASS] ${topLevel.length} curated + ${rawKeys.length} raw attributes; ${capReadySummary}`);
808
+ console.log(`✅ ${topLevel.length} curated + ${rawKeys.length} raw attributes; ${capReadySummary}`);
809
809
  console.log(` curated: ${topLevel.join(", ")}`);
810
810
  console.log(` raw: ${rawKeys.join(", ")}`);
811
811
  passed++;
812
812
  } catch (err) {
813
- console.log(`[FAIL] ${err.message}`);
813
+ console.log(`❌ ${err.message}`);
814
814
  failed++;
815
815
  }
816
816
  break;
@@ -824,7 +824,7 @@ switch (command) {
824
824
  const queued = await enqueueCommand(testConfig, cmd);
825
825
  const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
826
826
  if (!ack.acked) {
827
- console.log(`[TIMEOUT] (${Date.now() - t0}ms)`);
827
+ console.log(`⏱️ timeout (${Date.now() - t0}ms)`);
828
828
  failed++;
829
829
  break;
830
830
  }
@@ -833,14 +833,14 @@ switch (command) {
833
833
  const ms = Date.now() - t0;
834
834
  if (shot.stale) {
835
835
  const threshold = getSnapshotStaleThresholdMs();
836
- console.log(`[FAIL] Stale (${kb}KB, age=${(shot.ageMs / 1000).toFixed(1)}s > ${(threshold / 1000).toFixed(1)}s) ${shot.width}x${shot.height} seq=${shot.sequence} — phone may not be screen-sharing (${ms}ms)`);
836
+ console.log(`❌ stale (${kb}KB, age=${(shot.ageMs / 1000).toFixed(1)}s > ${(threshold / 1000).toFixed(1)}s) ${shot.width}x${shot.height} seq=${shot.sequence} — phone may not be screen-sharing (${ms}ms)`);
837
837
  failed++;
838
838
  } else {
839
- console.log(`[PASS] ${kb}KB, ${shot.width}x${shot.height}, age=${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "?"}, seq=${shot.sequence} (${ms}ms)`);
839
+ console.log(`✅ ${kb}KB, ${shot.width}x${shot.height}, age=${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "?"}, seq=${shot.sequence} (${ms}ms)`);
840
840
  passed++;
841
841
  }
842
842
  } catch (err) {
843
- console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
843
+ console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
844
844
  failed++;
845
845
  }
846
846
  break;
@@ -873,23 +873,24 @@ switch (command) {
873
873
  if (selectedIds) {
874
874
  const foundIds = new Set(toRun.map((t) => t.id));
875
875
  const missing = [...selectedIds].filter((id) => !foundIds.has(id));
876
- if (missing.length) console.warn(`[!] Unknown test IDs: ${missing.join(", ")}`);
876
+ if (missing.length) console.warn(` ⚠️ Unknown test IDs: ${missing.join(", ")}`);
877
877
  }
878
878
 
879
879
  let currentPhase = "";
880
880
  for (let i = 0; i < toRun.length; i++) {
881
881
  const t = toRun[i];
882
882
  if (t.phase !== currentPhase) {
883
- console.log(` -- ${t.phase} --`);
883
+ console.log(`\n ── ${t.phase} ──`);
884
884
  currentPhase = t.phase;
885
885
  }
886
886
  await runSingleTest(t);
887
887
  if (i < toRun.length - 1) await pause();
888
888
  }
889
889
 
890
- console.log(`\n Result: ${passed}/${totalSteps} passed, ${failed} failed, ${skipped} skipped`);
891
- if (failed === 0) console.log(" All tests passed! Device is fully responsive.");
892
- else console.log(` ${failed} test(s) failed. Check phone connectivity.`);
890
+ console.log(`\n ── Result ──`);
891
+ console.log(` ${passed}/${totalSteps} passed, ${failed} failed, ${skipped} skipped`);
892
+ if (failed === 0) console.log(" 🎉 All tests passed! Device is fully responsive.");
893
+ else console.log(` ⚠️ ${failed} test(s) failed. Check phone connectivity.`);
893
894
  process.exit(failed > 0 ? 1 : 0);
894
895
  }
895
896
 
@@ -59,10 +59,11 @@ export function configureMCP(backend, previousBackend) {
59
59
  }
60
60
  else {
61
61
  const cmds = MCP_COMMANDS[backend];
62
- const addCmd = cmds.add();
63
62
  console.log(` Configuring MCP for ${DISPLAY_NAMES[backend]} (HTTP transport)...`);
63
+ // Remove existing entry first to avoid "already exists" error on re-pair
64
+ tryRun(cmds.remove());
64
65
  try {
65
- execSync(addCmd, { stdio: "inherit", timeout: 10_000 });
66
+ execSync(cmds.add(), { stdio: "inherit", timeout: 10_000 });
66
67
  configured = true;
67
68
  }
68
69
  catch (err) {
@@ -43,6 +43,11 @@ export declare function loadConfig(): ZhihandConfigV3;
43
43
  * when the daemon and CLI write concurrently (Gemini code review v0.31).
44
44
  */
45
45
  export declare function saveConfig(cfg: ZhihandConfigV3): void;
46
+ /**
47
+ * Clean up legacy config files (v2 schema, credentials.json) before re-pairing.
48
+ * Replaces old config with empty v3 so loadConfig() won't warn.
49
+ */
50
+ export declare function cleanupLegacyConfig(): void;
46
51
  export declare function addUser(user: UserRecord): void;
47
52
  export declare function removeUser(userId: string): void;
48
53
  export declare function addDeviceToUser(userId: string, device: DeviceRecord): void;
@@ -27,11 +27,11 @@ function emptyConfig() {
27
27
  }
28
28
  export function loadConfig() {
29
29
  if (!fs.existsSync(CONFIG_PATH)) {
30
- // Check for v2 or legacy credentials
30
+ // Check for legacy credentials.json (pre-v3)
31
31
  const legacyCredentials = path.join(ZHIHAND_DIR, "credentials.json");
32
- if (!legacyWarningPrinted && (fs.existsSync(legacyCredentials) || checkForV2Config())) {
32
+ if (!legacyWarningPrinted && fs.existsSync(legacyCredentials)) {
33
33
  legacyWarningPrinted = true;
34
- process.stderr.write("[zhihand] old config detected (v2 or legacy) — run 'zhihand pair' to re-pair on v0.31 schema\n");
34
+ process.stderr.write("[zhihand] old config detected (legacy credentials) — run 'zhihand pair' to re-pair on v0.31 schema\n");
35
35
  }
36
36
  return emptyConfig();
37
37
  }
@@ -43,7 +43,7 @@ export function loadConfig() {
43
43
  users: raw.users ?? {},
44
44
  };
45
45
  }
46
- // Old schema version detected
46
+ // Old schema version (v2 or unknown) in config.json
47
47
  if (!legacyWarningPrinted) {
48
48
  legacyWarningPrinted = true;
49
49
  process.stderr.write("[zhihand] old config detected (schema v" + (raw.schema_version ?? "?") + ") — run 'zhihand pair' to re-pair on v0.31 schema\n");
@@ -54,17 +54,6 @@ export function loadConfig() {
54
54
  }
55
55
  return emptyConfig();
56
56
  }
57
- function checkForV2Config() {
58
- if (!fs.existsSync(CONFIG_PATH))
59
- return false;
60
- try {
61
- const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
62
- return raw && raw.schema_version === 2;
63
- }
64
- catch {
65
- return false;
66
- }
67
- }
68
57
  /**
69
58
  * Atomically write config: write to .tmp, then rename. Prevents corruption
70
59
  * when the daemon and CLI write concurrently (Gemini code review v0.31).
@@ -75,6 +64,27 @@ export function saveConfig(cfg) {
75
64
  fs.writeFileSync(tmpPath, JSON.stringify(cfg, null, 2), { mode: 0o600 });
76
65
  fs.renameSync(tmpPath, CONFIG_PATH);
77
66
  }
67
+ /**
68
+ * Clean up legacy config files (v2 schema, credentials.json) before re-pairing.
69
+ * Replaces old config with empty v3 so loadConfig() won't warn.
70
+ */
71
+ export function cleanupLegacyConfig() {
72
+ const legacyCredentials = path.join(ZHIHAND_DIR, "credentials.json");
73
+ if (fs.existsSync(legacyCredentials)) {
74
+ fs.unlinkSync(legacyCredentials);
75
+ }
76
+ if (fs.existsSync(CONFIG_PATH)) {
77
+ try {
78
+ const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
79
+ if (!raw || raw.schema_version !== 3) {
80
+ saveConfig(emptyConfig());
81
+ }
82
+ }
83
+ catch {
84
+ saveConfig(emptyConfig());
85
+ }
86
+ }
87
+ }
78
88
  // ── User helpers ──────────────────────────────────────────
79
89
  export function addUser(user) {
80
90
  const cfg = loadConfig();
package/dist/core/pair.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import QRCode from "qrcode";
2
- import { addUser, addDeviceToUser, ensureZhiHandDir, saveState, resolveDefaultEndpoint, getUserRecord, } from "./config.js";
2
+ import { addUser, addDeviceToUser, ensureZhiHandDir, saveState, resolveDefaultEndpoint, getUserRecord, cleanupLegacyConfig, } from "./config.js";
3
3
  import { fetchDeviceProfileOnce, extractStatic } from "./device.js";
4
4
  import { fetchUserCredentials } from "./ws.js";
5
5
  // ── Server API helpers ─────────────────────────────────────
@@ -93,6 +93,8 @@ export async function executePairingNewUser(preferredLabel) {
93
93
  const label = preferredLabel ?? `User-${Date.now().toString(36)}`;
94
94
  // 1. Create user
95
95
  const userResp = await createUser(endpoint, label);
96
+ // Clean up v2/legacy config after network call succeeds (avoids data loss on failure)
97
+ cleanupLegacyConfig();
96
98
  const userId = userResp.user_id;
97
99
  const controllerToken = userResp.controller_token;
98
100
  // 2. Register plugin (get edge_id)
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.32.3";
2
+ export declare const PACKAGE_VERSION = "0.32.5";
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.32.3";
11
+ export const PACKAGE_VERSION = "0.32.5";
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.32.3",
3
+ "version": "0.32.5",
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",