@zhihand/mcp 0.32.4 → 0.33.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
@@ -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.4";
33
+ const VERSION = "0.33.0";
34
34
 
35
35
  const CLI_TOOL_MAP = {
36
36
  claude: "claudecode",
@@ -555,8 +555,7 @@ switch (command) {
555
555
  }
556
556
 
557
557
  case "test": {
558
- const { createControlCommand, createSystemCommand, enqueueCommand } = await import("../dist/core/command.js");
559
- const { waitForCommandAck } = await import("../dist/core/ws.js");
558
+ const { createControlCommand, createSystemCommand } = await import("../dist/core/command.js");
560
559
  const { fetchScreenshot, getSnapshotStaleThresholdMs } = await import("../dist/core/screenshot.js");
561
560
  const { fetchDeviceProfileOnce, extractStatic, computeCapabilities, formatDeviceStatus } = await import("../dist/core/device.js");
562
561
 
@@ -630,6 +629,34 @@ switch (command) {
630
629
  process.exit(1);
631
630
  }
632
631
 
632
+ // Require daemon to be running (provides WS connections for command ACK)
633
+ const DAEMON_PORT = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
634
+ const DAEMON_BASE = `http://127.0.0.1:${DAEMON_PORT}`;
635
+ let daemonOk = false;
636
+ try {
637
+ const resp = await fetch(`${DAEMON_BASE}/internal/status`, { signal: AbortSignal.timeout(2000) });
638
+ daemonOk = resp.ok;
639
+ } catch { /* daemon not reachable */ }
640
+ if (!daemonOk) {
641
+ console.error("❌ Daemon is not running. Start it first: zhihand start");
642
+ process.exit(1);
643
+ }
644
+
645
+ // Execute command via daemon's /internal/exec endpoint
646
+ async function execViaDaemon(command, timeoutMs = 10_000) {
647
+ const resp = await fetch(`${DAEMON_BASE}/internal/exec`, {
648
+ method: "POST",
649
+ headers: { "Content-Type": "application/json" },
650
+ body: JSON.stringify({ command, credentialId: testConfig.credentialId, timeoutMs }),
651
+ signal: AbortSignal.timeout(timeoutMs + 5000),
652
+ });
653
+ if (!resp.ok) {
654
+ const body = await resp.text();
655
+ throw new Error(`Daemon exec failed: ${resp.status} ${body}`);
656
+ }
657
+ return resp.json();
658
+ }
659
+
633
660
  const forceRun = values.force === true;
634
661
  let selectedIds = null;
635
662
  let includeUnsafe = false;
@@ -652,7 +679,7 @@ switch (command) {
652
679
  }
653
680
  }
654
681
 
655
- console.log("ZhiHand Device Test");
682
+ console.log("🔧 ZhiHand Device Test");
656
683
  console.log(` Device: ${testConfig.credentialId}`);
657
684
  console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}\n`);
658
685
 
@@ -672,18 +699,18 @@ switch (command) {
672
699
  } catch { /* non-fatal */ }
673
700
  const getDevicePlatform = () => currentProfile?.platform ?? "unknown";
674
701
 
675
- console.log(" -- Capability readiness --");
702
+ console.log(" ── Capability readiness ──");
676
703
  if (!currentCaps) {
677
- console.log(" [!] Device profile not loaded — all capability gates will allow tests through.");
704
+ console.log(" ⚠️ Device profile not loaded — all capability gates will allow tests through.");
678
705
  } else {
679
- const fmt = (name, cap) => ` ${cap.ready ? "[ok]" : "[!]"} ${name.padEnd(14)} ${cap.ready ? "ready" : "NOT ready"} — ${cap.reason}`;
706
+ const fmt = (name, cap) => ` ${cap.ready ? "" : "⚠️"} ${name.padEnd(16)} ${cap.ready ? "ready" : "NOT ready"} — ${cap.reason}`;
680
707
  console.log(fmt("screen_sharing", currentCaps.screen_sharing));
681
708
  console.log(fmt("hid", currentCaps.hid));
682
709
  console.log(fmt("live_session", currentCaps.live_session));
683
710
  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)" : ""}`);
711
+ console.log(` ${currentCaps.profile.stale ? "⚠️" : ""} profile age=${ageStr}${currentCaps.profile.stale ? " (STALE)" : ""}`);
685
712
  if (forceRun) {
686
- console.log(" --force passed: capability gates disabled.");
713
+ console.log(" --force passed: capability gates disabled.");
687
714
  }
688
715
  }
689
716
  console.log("");
@@ -712,25 +739,24 @@ switch (command) {
712
739
  process.stdout.write(` ${String(t.id).padStart(2)}. ${t.label}... `);
713
740
  const t0 = Date.now();
714
741
  try {
715
- const queued = await enqueueCommand(testConfig, command);
716
- const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
742
+ const result = await execViaDaemon(command);
717
743
  const ms = Date.now() - t0;
718
- if (ack.acked) {
719
- const ackStatus = ack.command?.ack_status ?? "ok";
720
- const resultInfo = ack.command?.ack_result ? ` ${JSON.stringify(ack.command.ack_result)}` : "";
744
+ if (result.acked) {
745
+ const ackStatus = result.command?.ack_status ?? "ok";
746
+ const resultInfo = result.command?.ack_result ? ` ${JSON.stringify(result.command.ack_result)}` : "";
721
747
  if (ackStatus === "ok") {
722
- console.log(`[PASS] (${ms}ms)${resultInfo}`);
748
+ console.log(`✅ (${ms}ms)${resultInfo}`);
723
749
  passed++;
724
750
  } else {
725
- console.log(`[FAIL] [${ackStatus}] (${ms}ms)${resultInfo}`);
751
+ console.log(`❌ [${ackStatus}] (${ms}ms)${resultInfo}`);
726
752
  failed++;
727
753
  }
728
754
  } else {
729
- console.log(`[TIMEOUT] (${ms}ms)`);
755
+ console.log(`⏱️ timeout (${ms}ms)`);
730
756
  failed++;
731
757
  }
732
758
  } catch (err) {
733
- console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
759
+ console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
734
760
  failed++;
735
761
  }
736
762
  }
@@ -739,7 +765,7 @@ switch (command) {
739
765
  const currentPlatform = getDevicePlatform();
740
766
  if (t.platform && t.platform !== currentPlatform) {
741
767
  totalSteps++; skipped++;
742
- console.log(` ${String(t.id).padStart(2)}. ${t.label}... [SKIP] (${t.platform}-only, device is ${currentPlatform})`);
768
+ console.log(` ${String(t.id).padStart(2)}. ${t.label}... ⏭️ (${t.platform}-only, device is ${currentPlatform})`);
743
769
  return;
744
770
  }
745
771
  if (!forceRun && currentCaps) {
@@ -748,7 +774,7 @@ switch (command) {
748
774
  const gate = requiredCap === "screen" ? currentCaps.screen_sharing : currentCaps.hid;
749
775
  if (!gate.ready) {
750
776
  totalSteps++; skipped++;
751
- console.log(` ${String(t.id).padStart(2)}. ${t.label}... [SKIP] (${requiredCap} not ready: ${gate.reason})`);
777
+ console.log(` ${String(t.id).padStart(2)}. ${t.label}... ⏭️ (${requiredCap} not ready: ${gate.reason})`);
752
778
  return;
753
779
  }
754
780
  }
@@ -768,14 +794,14 @@ switch (command) {
768
794
  currentProfile = extractStatic(currentRawAttrs);
769
795
  currentCaps = computeCapabilities(currentRawAttrs, profileReceivedAtMs);
770
796
  const s = currentProfile;
771
- console.log(`[PASS] ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
797
+ console.log(`✅ ${s.platform} ${s.model}, ${s.osVersion}, ${s.screenWidthPx}x${s.screenHeightPx} (${ms}ms)`);
772
798
  passed++;
773
799
  } else {
774
- console.log(`[!] Loaded but empty (${ms}ms)`);
800
+ console.log(`⚠️ loaded but empty (${ms}ms)`);
775
801
  failed++;
776
802
  }
777
803
  } catch (err) {
778
- console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
804
+ console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
779
805
  failed++;
780
806
  }
781
807
  break;
@@ -805,12 +831,12 @@ switch (command) {
805
831
  const capReadySummary = ["screen_sharing", "hid", "live_session"]
806
832
  .map((k) => `${k}=${caps[k]?.ready ? "ready" : "not-ready"}`)
807
833
  .join(", ");
808
- console.log(`[PASS] ${topLevel.length} curated + ${rawKeys.length} raw attributes; ${capReadySummary}`);
834
+ console.log(`✅ ${topLevel.length} curated + ${rawKeys.length} raw attributes; ${capReadySummary}`);
809
835
  console.log(` curated: ${topLevel.join(", ")}`);
810
836
  console.log(` raw: ${rawKeys.join(", ")}`);
811
837
  passed++;
812
838
  } catch (err) {
813
- console.log(`[FAIL] ${err.message}`);
839
+ console.log(`❌ ${err.message}`);
814
840
  failed++;
815
841
  }
816
842
  break;
@@ -821,10 +847,9 @@ switch (command) {
821
847
  const t0 = Date.now();
822
848
  try {
823
849
  const cmd = createControlCommand({ action: "screenshot" });
824
- const queued = await enqueueCommand(testConfig, cmd);
825
- const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
826
- if (!ack.acked) {
827
- console.log(`[TIMEOUT] (${Date.now() - t0}ms)`);
850
+ const result = await execViaDaemon(cmd);
851
+ if (!result.acked) {
852
+ console.log(`⏱️ timeout (${Date.now() - t0}ms)`);
828
853
  failed++;
829
854
  break;
830
855
  }
@@ -833,14 +858,14 @@ switch (command) {
833
858
  const ms = Date.now() - t0;
834
859
  if (shot.stale) {
835
860
  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)`);
861
+ 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
862
  failed++;
838
863
  } else {
839
- console.log(`[PASS] ${kb}KB, ${shot.width}x${shot.height}, age=${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "?"}, seq=${shot.sequence} (${ms}ms)`);
864
+ console.log(`✅ ${kb}KB, ${shot.width}x${shot.height}, age=${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "?"}, seq=${shot.sequence} (${ms}ms)`);
840
865
  passed++;
841
866
  }
842
867
  } catch (err) {
843
- console.log(`[FAIL] ${err.message} (${Date.now() - t0}ms)`);
868
+ console.log(`❌ ${err.message} (${Date.now() - t0}ms)`);
844
869
  failed++;
845
870
  }
846
871
  break;
@@ -873,23 +898,24 @@ switch (command) {
873
898
  if (selectedIds) {
874
899
  const foundIds = new Set(toRun.map((t) => t.id));
875
900
  const missing = [...selectedIds].filter((id) => !foundIds.has(id));
876
- if (missing.length) console.warn(`[!] Unknown test IDs: ${missing.join(", ")}`);
901
+ if (missing.length) console.warn(` ⚠️ Unknown test IDs: ${missing.join(", ")}`);
877
902
  }
878
903
 
879
904
  let currentPhase = "";
880
905
  for (let i = 0; i < toRun.length; i++) {
881
906
  const t = toRun[i];
882
907
  if (t.phase !== currentPhase) {
883
- console.log(` -- ${t.phase} --`);
908
+ console.log(`\n ── ${t.phase} ──`);
884
909
  currentPhase = t.phase;
885
910
  }
886
911
  await runSingleTest(t);
887
912
  if (i < toRun.length - 1) await pause();
888
913
  }
889
914
 
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.`);
915
+ console.log(`\n ── Result ──`);
916
+ console.log(` ${passed}/${totalSteps} passed, ${failed} failed, ${skipped} skipped`);
917
+ if (failed === 0) console.log(" 🎉 All tests passed! Device is fully responsive.");
918
+ else console.log(` ⚠️ ${failed} test(s) failed. Check phone connectivity.`);
893
919
  process.exit(failed > 0 ? 1 : 0);
894
920
  }
895
921
 
@@ -43,5 +43,4 @@ export interface SystemParams {
43
43
  }
44
44
  export declare function createSystemCommand(params: SystemParams, platform?: string): QueuedControlCommand;
45
45
  export declare function enqueueCommand(config: ZhiHandRuntimeConfig, command: QueuedControlCommand): Promise<QueuedCommandRecord>;
46
- export declare function getCommand(config: ZhiHandRuntimeConfig, commandId: string): Promise<QueuedCommandRecord>;
47
46
  export declare function formatAckSummary(action: string, result: WaitForCommandAckResult): string;
@@ -170,21 +170,6 @@ export async function enqueueCommand(config, command) {
170
170
  dbg(`[cmd] Enqueued: id=${payload.command.id}, status=${payload.command.status}`);
171
171
  return payload.command;
172
172
  }
173
- export async function getCommand(config, commandId) {
174
- const url = `${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/commands/${encodeURIComponent(commandId)}`;
175
- dbg(`[cmd] GET ${url}`);
176
- const response = await fetch(url, {
177
- headers: { "Authorization": `Bearer ${config.controllerToken}` },
178
- });
179
- if (!response.ok) {
180
- dbg(`[cmd] Get failed: ${response.status}`);
181
- throw new Error(`Get command failed: ${response.status}`);
182
- }
183
- const payload = (await response.json());
184
- const cmd = payload.command;
185
- dbg(`[cmd] Got: id=${cmd.id}, status=${cmd.status}, acked=${!!cmd.acked_at}, ack_status=${cmd.ack_status ?? "-"}, ack_result=${JSON.stringify(cmd.ack_result ?? null)}`);
186
- return payload.command;
187
- }
188
173
  export function formatAckSummary(action, result) {
189
174
  if (!result.acked) {
190
175
  return `Sent ${action}, waiting for ACK (timed out).`;
package/dist/core/ws.d.ts CHANGED
@@ -85,9 +85,9 @@ export interface CredentialResponse {
85
85
  export declare function fetchUserCredentials(endpoint: string, userId: string, controllerToken: string, onlineFilter?: boolean): Promise<CredentialResponse[]>;
86
86
  /**
87
87
  * Wait for command ACK via WS push (which should already be connected by the
88
- * registry). Falls back to polling.
88
+ * registry). WS-only no polling fallback.
89
89
  */
90
- export declare function waitForCommandAck(config: ZhiHandRuntimeConfig, options: {
90
+ export declare function waitForCommandAck(_config: ZhiHandRuntimeConfig, options: {
91
91
  commandId: string;
92
92
  timeoutMs?: number;
93
93
  signal?: AbortSignal;
package/dist/core/ws.js CHANGED
@@ -9,7 +9,6 @@
9
9
  * - fetchUserCredentials: HTTP REST helper (unchanged from sse.ts).
10
10
  */
11
11
  import WebSocket from "ws";
12
- import { getCommand } from "./command.js";
13
12
  import { log } from "./logger.js";
14
13
  // ── Shared reconnecting base ─────────────────────────────
15
14
  const BACKOFF_INITIAL_MS = 1000;
@@ -279,59 +278,27 @@ export async function fetchUserCredentials(endpoint, userId, controllerToken, on
279
278
  }
280
279
  /**
281
280
  * Wait for command ACK via WS push (which should already be connected by the
282
- * registry). Falls back to polling.
281
+ * registry). WS-only no polling fallback.
283
282
  */
284
- export async function waitForCommandAck(config, options) {
283
+ export async function waitForCommandAck(_config, options) {
285
284
  const timeoutMs = options.timeoutMs ?? 15_000;
286
285
  log.debug(`[ws-cmd] Waiting for ACK: commandId=${options.commandId}, timeout=${timeoutMs}ms`);
287
286
  return new Promise((resolve, reject) => {
288
- let resolved = false;
289
- let pollInterval;
290
287
  const timeout = setTimeout(() => {
291
288
  cleanup();
292
289
  resolve({ acked: false });
293
290
  }, timeoutMs);
294
291
  const unsubscribe = subscribeToCommandAck(options.commandId, (ackedCommand) => {
295
- if (resolved)
296
- return;
297
- resolved = true;
298
292
  cleanup();
299
293
  resolve({ acked: true, command: ackedCommand });
300
294
  });
301
- // Delay polling startup by 2s so WS push ACK normally wins in the
302
- // registry-connected path. CLI (zhihand test) still resolves via polling
303
- // after the initial delay.
304
- const POLL_START_DELAY_MS = 2000;
305
- const POLL_INTERVAL_MS = 500;
306
- const startPolling = setTimeout(() => {
307
- if (resolved)
308
- return;
309
- pollInterval = setInterval(async () => {
310
- if (resolved)
311
- return;
312
- try {
313
- const cmd = await getCommand(config, options.commandId);
314
- if (cmd.acked_at) {
315
- resolved = true;
316
- cleanup();
317
- resolve({ acked: true, command: cmd });
318
- }
319
- }
320
- catch {
321
- // non-fatal
322
- }
323
- }, POLL_INTERVAL_MS);
324
- }, POLL_START_DELAY_MS);
325
295
  options.signal?.addEventListener("abort", () => {
326
296
  cleanup();
327
297
  reject(new Error("The operation was aborted"));
328
298
  }, { once: true });
329
299
  function cleanup() {
330
300
  clearTimeout(timeout);
331
- clearTimeout(startPolling);
332
301
  unsubscribe();
333
- if (pollInterval)
334
- clearInterval(pollInterval);
335
302
  }
336
303
  });
337
304
  }
@@ -12,6 +12,8 @@ import { PromptListener } from "./prompt-listener.js";
12
12
  import { dispatchToCLI, postReply, killActiveChild } from "./dispatcher.js";
13
13
  import { setDebugEnabled, dbg } from "./logger.js";
14
14
  import { registry } from "../core/registry.js";
15
+ import { enqueueCommand } from "../core/command.js";
16
+ import { waitForCommandAck } from "../core/ws.js";
15
17
  const DEFAULT_PORT = 18686;
16
18
  const PID_FILE = "daemon.pid";
17
19
  // ── State ────────���─────────────────────────────────────────
@@ -102,6 +104,39 @@ function handleInternalAPI(req, res) {
102
104
  });
103
105
  return true;
104
106
  }
107
+ // Execute command via daemon's WS (used by zhihand test)
108
+ if (url === "/internal/exec" && req.method === "POST") {
109
+ dbg(`[api] POST /internal/exec`);
110
+ let body = "";
111
+ const MAX_BODY = 10 * 1024;
112
+ req.on("data", (chunk) => {
113
+ body += chunk.toString();
114
+ if (body.length > MAX_BODY) {
115
+ res.writeHead(413, { "Content-Type": "application/json" });
116
+ res.end(JSON.stringify({ error: "Payload too large" }));
117
+ req.destroy();
118
+ }
119
+ });
120
+ req.on("end", async () => {
121
+ try {
122
+ const { command, credentialId, timeoutMs } = JSON.parse(body);
123
+ const cfg = resolveConfig(credentialId);
124
+ const effectiveTimeout = timeoutMs ?? 10_000;
125
+ const queued = await enqueueCommand(cfg, command);
126
+ const ack = await waitForCommandAck(cfg, {
127
+ commandId: queued.id,
128
+ timeoutMs: effectiveTimeout,
129
+ });
130
+ res.writeHead(200, { "Content-Type": "application/json" });
131
+ res.end(JSON.stringify({ id: queued.id, ...ack }));
132
+ }
133
+ catch (err) {
134
+ res.writeHead(500, { "Content-Type": "application/json" });
135
+ res.end(JSON.stringify({ error: err.message }));
136
+ }
137
+ });
138
+ return true;
139
+ }
105
140
  if (url === "/internal/status" && req.method === "GET") {
106
141
  dbg(`[api] GET /internal/status`);
107
142
  const effectiveModel = activeBackend ? (activeModel ?? DEFAULT_MODELS[activeBackend]) : null;
@@ -17,8 +17,6 @@ export declare class PromptListener {
17
17
  private log;
18
18
  private processedIds;
19
19
  private rws;
20
- private pollTimer;
21
- private wsConnected;
22
20
  private stopped;
23
21
  constructor(config: ZhiHandConfig, handler: PromptHandler, log: (msg: string) => void);
24
22
  start(): void;
@@ -27,9 +25,5 @@ export declare class PromptListener {
27
25
  private connectWS;
28
26
  private handleWSMessage;
29
27
  private handleEvent;
30
- private startPolling;
31
- private schedulePoll;
32
- private stopPolling;
33
- private poll;
34
28
  }
35
29
  export {};
@@ -1,14 +1,11 @@
1
1
  import { ReconnectingWebSocket } from "../core/ws.js";
2
2
  import { dbg } from "./logger.js";
3
- const POLL_INTERVAL = 2_000;
4
3
  export class PromptListener {
5
4
  config;
6
5
  handler;
7
6
  log;
8
7
  processedIds = new Set();
9
8
  rws = null;
10
- pollTimer = null;
11
- wsConnected = false;
12
9
  stopped = false;
13
10
  constructor(config, handler, log) {
14
11
  this.config = config;
@@ -23,8 +20,6 @@ export class PromptListener {
23
20
  this.stopped = true;
24
21
  this.rws?.stop();
25
22
  this.rws = null;
26
- this.wsConnected = false;
27
- this.stopPolling();
28
23
  }
29
24
  dispatchPrompt(prompt) {
30
25
  if (this.processedIds.has(prompt.id)) {
@@ -60,11 +55,7 @@ export class PromptListener {
60
55
  // onConnected deferred until auth_ok is received (see handleWSMessage)
61
56
  },
62
57
  onClose: (_code, _reason) => {
63
- if (this.wsConnected) {
64
- this.wsConnected = false;
65
- this.log("[ws] Disconnected. Falling back to polling.");
66
- this.startPolling();
67
- }
58
+ dbg("[ws] Disconnected. ReconnectingWebSocket will retry.");
68
59
  },
69
60
  onMessage: (data) => {
70
61
  this.handleWSMessage(data);
@@ -79,8 +70,6 @@ export class PromptListener {
79
70
  const msg = data;
80
71
  // Auth responses
81
72
  if (msg.type === "auth_ok") {
82
- this.wsConnected = true;
83
- this.stopPolling();
84
73
  this.log("[ws] Connected to prompt stream.");
85
74
  return;
86
75
  }
@@ -88,8 +77,6 @@ export class PromptListener {
88
77
  this.log(`[ws] Auth failed: ${msg.error}`);
89
78
  this.rws?.stop();
90
79
  this.rws = null;
91
- this.wsConnected = false;
92
- this.startPolling();
93
80
  return;
94
81
  }
95
82
  // Application-level ping (if server sends these alongside protocol pings)
@@ -119,50 +106,4 @@ export class PromptListener {
119
106
  this.log("[device] device_profile.updated event received on prompts stream (ignored; registry handles it)");
120
107
  }
121
108
  }
122
- startPolling() {
123
- if (this.pollTimer || this.stopped)
124
- return;
125
- this.schedulePoll();
126
- }
127
- schedulePoll() {
128
- if (this.pollTimer)
129
- return;
130
- this.pollTimer = setTimeout(async () => {
131
- this.pollTimer = null;
132
- await this.poll();
133
- if (!this.wsConnected && !this.stopped) {
134
- this.schedulePoll();
135
- }
136
- }, POLL_INTERVAL);
137
- }
138
- stopPolling() {
139
- if (this.pollTimer) {
140
- clearTimeout(this.pollTimer);
141
- this.pollTimer = null;
142
- }
143
- }
144
- async poll() {
145
- try {
146
- const url = `${this.config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(this.config.credentialId)}/prompts?limit=5`;
147
- dbg(`[poll] GET ${url}`);
148
- const response = await fetch(url, {
149
- headers: { "Authorization": `Bearer ${this.config.controllerToken}` },
150
- signal: AbortSignal.timeout(10_000),
151
- });
152
- if (!response.ok) {
153
- dbg(`[poll] Response: ${response.status}`);
154
- return;
155
- }
156
- const data = (await response.json());
157
- dbg(`[poll] Got ${data.items?.length ?? 0} prompt(s)`);
158
- if (this.stopped)
159
- return; // Guard against late responses after stop()
160
- for (const prompt of data.items ?? []) {
161
- this.dispatchPrompt(prompt);
162
- }
163
- }
164
- catch (err) {
165
- dbg(`[poll] Error: ${err.message}`);
166
- }
167
- }
168
109
  }
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.4";
2
+ export declare const PACKAGE_VERSION = "0.33.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.32.4";
11
+ export const PACKAGE_VERSION = "0.33.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.32.4",
3
+ "version": "0.33.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",