@zhihand/mcp 0.32.5 → 0.33.1

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.5";
33
+ const VERSION = "0.33.1";
34
34
 
35
35
  const CLI_TOOL_MAP = {
36
36
  claude: "claudecode",
@@ -555,10 +555,12 @@ 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");
561
+ const { setDebugEnabled: setCoreDebug, setTimestampEnabled, log: coreLog } = await import("../dist/core/logger.js");
562
+ if (values.debug) setCoreDebug(true);
563
+ setTimestampEnabled(true);
562
564
 
563
565
  const KIND_CAPABILITY = {
564
566
  profile: "none", status: "none", screenshot: "screen", hid: "hid", system: "none",
@@ -630,6 +632,46 @@ switch (command) {
630
632
  process.exit(1);
631
633
  }
632
634
 
635
+ // Require daemon to be running (provides WS connections for command ACK)
636
+ const DAEMON_PORT = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
637
+ const DAEMON_BASE = `http://127.0.0.1:${DAEMON_PORT}`;
638
+ let daemonOk = false;
639
+ let daemonStatus = null;
640
+ try {
641
+ const resp = await fetch(`${DAEMON_BASE}/internal/status`, { signal: AbortSignal.timeout(2000) });
642
+ daemonOk = resp.ok;
643
+ if (resp.ok) daemonStatus = await resp.json();
644
+ } catch { /* daemon not reachable */ }
645
+ if (!daemonOk) {
646
+ console.error("❌ Daemon is not running. Start it first: zhihand start");
647
+ process.exit(1);
648
+ }
649
+
650
+ const testDbg = values.debug
651
+ ? (msg) => coreLog.debug(`[test] ${msg}`)
652
+ : () => {};
653
+
654
+ // Execute command via daemon's /internal/exec endpoint
655
+ async function execViaDaemon(command, timeoutMs = 10_000) {
656
+ const action = command?.payload?.action ?? command?.type ?? "?";
657
+ testDbg(`exec action=${action} timeout=${timeoutMs}ms`);
658
+ const t0 = Date.now();
659
+ const resp = await fetch(`${DAEMON_BASE}/internal/exec`, {
660
+ method: "POST",
661
+ headers: { "Content-Type": "application/json" },
662
+ body: JSON.stringify({ command, credentialId: testConfig.credentialId, timeoutMs }),
663
+ signal: AbortSignal.timeout(timeoutMs + 5000),
664
+ });
665
+ if (!resp.ok) {
666
+ const body = await resp.text();
667
+ testDbg(`exec FAILED ${resp.status} ${Date.now() - t0}ms`);
668
+ throw new Error(`Daemon exec failed: ${resp.status} ${body}`);
669
+ }
670
+ const result = await resp.json();
671
+ testDbg(`exec done action=${action} acked=${result.acked} id=${result.id ?? "-"} ${Date.now() - t0}ms`);
672
+ return result;
673
+ }
674
+
633
675
  const forceRun = values.force === true;
634
676
  let selectedIds = null;
635
677
  let includeUnsafe = false;
@@ -654,7 +696,11 @@ switch (command) {
654
696
 
655
697
  console.log("🔧 ZhiHand Device Test");
656
698
  console.log(` Device: ${testConfig.credentialId}`);
657
- console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}\n`);
699
+ console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}`);
700
+ if (daemonStatus) {
701
+ console.log(` Daemon: v${daemonStatus.version} pid=${daemonStatus.pid} backend=${daemonStatus.backend ?? "none"}`);
702
+ }
703
+ console.log("");
658
704
 
659
705
  // Pre-fetch device profile
660
706
  let currentRawAttrs = {};
@@ -712,12 +758,11 @@ switch (command) {
712
758
  process.stdout.write(` ${String(t.id).padStart(2)}. ${t.label}... `);
713
759
  const t0 = Date.now();
714
760
  try {
715
- const queued = await enqueueCommand(testConfig, command);
716
- const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
761
+ const result = await execViaDaemon(command);
717
762
  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)}` : "";
763
+ if (result.acked) {
764
+ const ackStatus = result.command?.ack_status ?? "ok";
765
+ const resultInfo = result.command?.ack_result ? ` ${JSON.stringify(result.command.ack_result)}` : "";
721
766
  if (ackStatus === "ok") {
722
767
  console.log(`✅ (${ms}ms)${resultInfo}`);
723
768
  passed++;
@@ -821,9 +866,8 @@ switch (command) {
821
866
  const t0 = Date.now();
822
867
  try {
823
868
  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) {
869
+ const result = await execViaDaemon(cmd);
870
+ if (!result.acked) {
827
871
  console.log(`⏱️ timeout (${Date.now() - t0}ms)`);
828
872
  failed++;
829
873
  break;
@@ -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).`;
@@ -1,12 +1,15 @@
1
1
  /**
2
2
  * Unified logger — all log output goes to stderr so stdout stays clean
3
- * for MCP JSON-RPC. Replaces ad-hoc process.stderr.write and dbg() calls
4
- * in core/ and tools/ code.
3
+ * for MCP JSON-RPC.
5
4
  *
6
- * The daemon has its own stdout-based log() in daemon/index.ts — that is
7
- * intentional (it writes to daemon.log). The daemon's debug logger
8
- * (daemon/logger.ts) remains for daemon-specific verbose output.
5
+ * All modules (core/, daemon/, tools/) should use this logger.
6
+ * The daemon's dbg() in daemon/logger.ts delegates here for the debug flag.
9
7
  */
8
+ /**
9
+ * Redact sensitive tokens from log messages.
10
+ * Replaces Bearer tokens and controller_token values with <REDACTED>.
11
+ */
12
+ export declare function redact(msg: string): string;
10
13
  export declare const log: {
11
14
  info: (...args: unknown[]) => void;
12
15
  warn: (...args: unknown[]) => void;
@@ -15,3 +18,5 @@ export declare const log: {
15
18
  };
16
19
  export declare function setDebugEnabled(v: boolean): void;
17
20
  export declare function isDebugEnabled(): boolean;
21
+ /** Enable timestamps in log output (for daemon / CLI long-running processes). */
22
+ export declare function setTimestampEnabled(v: boolean): void;
@@ -1,26 +1,50 @@
1
1
  /**
2
2
  * Unified logger — all log output goes to stderr so stdout stays clean
3
- * for MCP JSON-RPC. Replaces ad-hoc process.stderr.write and dbg() calls
4
- * in core/ and tools/ code.
3
+ * for MCP JSON-RPC.
5
4
  *
6
- * The daemon has its own stdout-based log() in daemon/index.ts — that is
7
- * intentional (it writes to daemon.log). The daemon's debug logger
8
- * (daemon/logger.ts) remains for daemon-specific verbose output.
5
+ * All modules (core/, daemon/, tools/) should use this logger.
6
+ * The daemon's dbg() in daemon/logger.ts delegates here for the debug flag.
9
7
  */
10
8
  let debugEnabled = false;
9
+ let timestampEnabled = false;
10
+ // ── Token redaction ──────────────────────────────────────
11
+ const REDACT_PATTERNS = [
12
+ // Bearer tokens in headers / JSON
13
+ /(Bearer\s+)[^\s"',}]+/gi,
14
+ // controller_token in JSON / key=value
15
+ /(controller_token["']?\s*[:=]\s*["']?)[^\s"',}]+/gi,
16
+ ];
17
+ /**
18
+ * Redact sensitive tokens from log messages.
19
+ * Replaces Bearer tokens and controller_token values with <REDACTED>.
20
+ */
21
+ export function redact(msg) {
22
+ let result = msg;
23
+ for (const pattern of REDACT_PATTERNS) {
24
+ result = result.replace(pattern, "$1<REDACTED>");
25
+ }
26
+ return result;
27
+ }
28
+ // ── Logger ───────────────────────────────────────────────
29
+ function prefix(level) {
30
+ if (timestampEnabled) {
31
+ return `[${new Date().toLocaleTimeString()}] [${level}] `;
32
+ }
33
+ return `[${level.padEnd(5)}] `;
34
+ }
11
35
  export const log = {
12
36
  info: (...args) => {
13
- process.stderr.write(`[info] ${args.map(String).join(" ")}\n`);
37
+ process.stderr.write(`${prefix("info")}${redact(args.map(String).join(" "))}\n`);
14
38
  },
15
39
  warn: (...args) => {
16
- process.stderr.write(`[warn] ${args.map(String).join(" ")}\n`);
40
+ process.stderr.write(`${prefix("warn")}${redact(args.map(String).join(" "))}\n`);
17
41
  },
18
42
  error: (...args) => {
19
- process.stderr.write(`[error] ${args.map(String).join(" ")}\n`);
43
+ process.stderr.write(`${prefix("error")}${redact(args.map(String).join(" "))}\n`);
20
44
  },
21
45
  debug: (...args) => {
22
46
  if (debugEnabled) {
23
- process.stderr.write(`[debug] ${args.map(String).join(" ")}\n`);
47
+ process.stderr.write(`${prefix("debug")}${redact(args.map(String).join(" "))}\n`);
24
48
  }
25
49
  },
26
50
  };
@@ -30,3 +54,7 @@ export function setDebugEnabled(v) {
30
54
  export function isDebugEnabled() {
31
55
  return debugEnabled;
32
56
  }
57
+ /** Enable timestamps in log output (for daemon / CLI long-running processes). */
58
+ export function setTimestampEnabled(v) {
59
+ timestampEnabled = v;
60
+ }
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,30 @@ 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`);
286
+ const t0 = Date.now();
287
287
  return new Promise((resolve, reject) => {
288
- let resolved = false;
289
- let pollInterval;
290
288
  const timeout = setTimeout(() => {
289
+ log.debug(`[ws-cmd] ACK timeout: commandId=${options.commandId} after ${Date.now() - t0}ms`);
291
290
  cleanup();
292
291
  resolve({ acked: false });
293
292
  }, timeoutMs);
294
293
  const unsubscribe = subscribeToCommandAck(options.commandId, (ackedCommand) => {
295
- if (resolved)
296
- return;
297
- resolved = true;
294
+ log.debug(`[ws-cmd] ACK received: commandId=${options.commandId} status=${ackedCommand.ack_status ?? "ok"} ${Date.now() - t0}ms`);
298
295
  cleanup();
299
296
  resolve({ acked: true, command: ackedCommand });
300
297
  });
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
298
  options.signal?.addEventListener("abort", () => {
326
299
  cleanup();
327
300
  reject(new Error("The operation was aborted"));
328
301
  }, { once: true });
329
302
  function cleanup() {
330
303
  clearTimeout(timeout);
331
- clearTimeout(startPolling);
332
304
  unsubscribe();
333
- if (pollInterval)
334
- clearInterval(pollInterval);
335
305
  }
336
306
  });
337
307
  }
@@ -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 ────────���─────────────────────────────────────────
@@ -19,9 +21,9 @@ let activeBackend = null;
19
21
  let activeModel = null; // user-selected model alias, null = use default
20
22
  let isProcessing = false;
21
23
  const promptQueue = [];
24
+ import { log as coreLog, setTimestampEnabled } from "../core/logger.js";
22
25
  function log(msg) {
23
- const ts = new Date().toLocaleTimeString();
24
- process.stdout.write(`[${ts}] ${msg}\n`);
26
+ coreLog.info(msg);
25
27
  }
26
28
  // ── Prompt Processing ──────────────────────────────────────
27
29
  async function processPrompt(config, prompt) {
@@ -102,6 +104,47 @@ 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
+ let body = "";
110
+ const MAX_BODY = 10 * 1024;
111
+ req.on("data", (chunk) => {
112
+ body += chunk.toString();
113
+ if (body.length > MAX_BODY) {
114
+ res.writeHead(413, { "Content-Type": "application/json" });
115
+ res.end(JSON.stringify({ error: "Payload too large" }));
116
+ req.destroy();
117
+ }
118
+ });
119
+ req.on("end", async () => {
120
+ const t0 = Date.now();
121
+ try {
122
+ const { command, credentialId, timeoutMs } = JSON.parse(body);
123
+ const cmdType = command.type ?? "unknown";
124
+ const cmdAction = command.payload?.action ?? "-";
125
+ dbg(`[api] POST /internal/exec cred=${credentialId} type=${cmdType} action=${cmdAction} timeout=${timeoutMs ?? 10_000}ms`);
126
+ const cfg = resolveConfig(credentialId);
127
+ const effectiveTimeout = timeoutMs ?? 10_000;
128
+ const queued = await enqueueCommand(cfg, command);
129
+ dbg(`[api] /internal/exec enqueued id=${queued.id}`);
130
+ const ack = await waitForCommandAck(cfg, {
131
+ commandId: queued.id,
132
+ timeoutMs: effectiveTimeout,
133
+ });
134
+ const ms = Date.now() - t0;
135
+ const ackStatus = ack.acked ? (ack.command?.ack_status ?? "ok") : "timeout";
136
+ dbg(`[api] /internal/exec done id=${queued.id} acked=${ack.acked} status=${ackStatus} ${ms}ms`);
137
+ res.writeHead(200, { "Content-Type": "application/json" });
138
+ res.end(JSON.stringify({ id: queued.id, ...ack }));
139
+ }
140
+ catch (err) {
141
+ dbg(`[api] /internal/exec error: ${err.message} ${Date.now() - t0}ms`);
142
+ res.writeHead(500, { "Content-Type": "application/json" });
143
+ res.end(JSON.stringify({ error: err.message }));
144
+ }
145
+ });
146
+ return true;
147
+ }
105
148
  if (url === "/internal/status" && req.method === "GET") {
106
149
  dbg(`[api] GET /internal/status`);
107
150
  const effectiveModel = activeBackend ? (activeModel ?? DEFAULT_MODELS[activeBackend]) : null;
@@ -155,6 +198,7 @@ export function isAlreadyRunning() {
155
198
  }
156
199
  // ── Main Daemon Entry ──────���───────────────────────────────
157
200
  export async function startDaemon(options) {
201
+ setTimestampEnabled(true);
158
202
  if (options?.debug)
159
203
  setDebugEnabled(true);
160
204
  const port = options?.port ?? (parseInt(process.env.ZHIHAND_PORT ?? "", 10) || DEFAULT_PORT);
@@ -1,10 +1,12 @@
1
1
  /**
2
2
  * Debug logger for ZhiHand daemon.
3
3
  *
4
- * Enable with `zhihand start --debug` to see detailed request/response,
5
- * CLI spawn args, stdin/stdout data, SSE events, and timing information.
4
+ * Delegates to core/logger.ts for the debug flag, redaction, and output.
5
+ * All output goes to stderr to keep stdout clean for MCP JSON-RPC.
6
+ *
7
+ * Enable with `zhihand start --debug`.
6
8
  */
7
9
  export declare function setDebugEnabled(enabled: boolean): void;
8
10
  export declare function isDebugEnabled(): boolean;
9
- /** Debug log — only outputs when --debug is active. */
11
+ /** Debug log — only outputs when --debug is active. Writes to stderr with redaction. */
10
12
  export declare function dbg(msg: string): void;
@@ -1,22 +1,21 @@
1
1
  /**
2
2
  * Debug logger for ZhiHand daemon.
3
3
  *
4
- * Enable with `zhihand start --debug` to see detailed request/response,
5
- * CLI spawn args, stdin/stdout data, SSE events, and timing information.
4
+ * Delegates to core/logger.ts for the debug flag, redaction, and output.
5
+ * All output goes to stderr to keep stdout clean for MCP JSON-RPC.
6
+ *
7
+ * Enable with `zhihand start --debug`.
6
8
  */
7
- let debugEnabled = false;
9
+ import { log, setDebugEnabled as coreSetDebug, isDebugEnabled as coreIsDebug, } from "../core/logger.js";
8
10
  export function setDebugEnabled(enabled) {
9
- debugEnabled = enabled;
11
+ coreSetDebug(enabled);
10
12
  }
11
13
  export function isDebugEnabled() {
12
- return debugEnabled;
13
- }
14
- function ts() {
15
- return new Date().toLocaleTimeString();
14
+ return coreIsDebug();
16
15
  }
17
- /** Debug log — only outputs when --debug is active. */
16
+ /** Debug log — only outputs when --debug is active. Writes to stderr with redaction. */
18
17
  export function dbg(msg) {
19
- if (!debugEnabled)
18
+ if (!coreIsDebug())
20
19
  return;
21
- process.stdout.write(`[${ts()}] [DEBUG] ${msg}\n`);
20
+ log.debug(msg);
22
21
  }
@@ -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.5";
2
+ export declare const PACKAGE_VERSION = "0.33.1";
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.5";
11
+ export const PACKAGE_VERSION = "0.33.1";
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.5",
3
+ "version": "0.33.1",
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",