@zhihand/mcp 0.33.0 → 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.33.0";
33
+ const VERSION = "0.33.1";
34
34
 
35
35
  const CLI_TOOL_MAP = {
36
36
  claude: "claudecode",
@@ -558,6 +558,9 @@ switch (command) {
558
558
  const { createControlCommand, createSystemCommand } = await import("../dist/core/command.js");
559
559
  const { fetchScreenshot, getSnapshotStaleThresholdMs } = await import("../dist/core/screenshot.js");
560
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);
561
564
 
562
565
  const KIND_CAPABILITY = {
563
566
  profile: "none", status: "none", screenshot: "screen", hid: "hid", system: "none",
@@ -633,17 +636,26 @@ switch (command) {
633
636
  const DAEMON_PORT = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
634
637
  const DAEMON_BASE = `http://127.0.0.1:${DAEMON_PORT}`;
635
638
  let daemonOk = false;
639
+ let daemonStatus = null;
636
640
  try {
637
641
  const resp = await fetch(`${DAEMON_BASE}/internal/status`, { signal: AbortSignal.timeout(2000) });
638
642
  daemonOk = resp.ok;
643
+ if (resp.ok) daemonStatus = await resp.json();
639
644
  } catch { /* daemon not reachable */ }
640
645
  if (!daemonOk) {
641
646
  console.error("❌ Daemon is not running. Start it first: zhihand start");
642
647
  process.exit(1);
643
648
  }
644
649
 
650
+ const testDbg = values.debug
651
+ ? (msg) => coreLog.debug(`[test] ${msg}`)
652
+ : () => {};
653
+
645
654
  // Execute command via daemon's /internal/exec endpoint
646
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();
647
659
  const resp = await fetch(`${DAEMON_BASE}/internal/exec`, {
648
660
  method: "POST",
649
661
  headers: { "Content-Type": "application/json" },
@@ -652,9 +664,12 @@ switch (command) {
652
664
  });
653
665
  if (!resp.ok) {
654
666
  const body = await resp.text();
667
+ testDbg(`exec FAILED ${resp.status} ${Date.now() - t0}ms`);
655
668
  throw new Error(`Daemon exec failed: ${resp.status} ${body}`);
656
669
  }
657
- return resp.json();
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;
658
673
  }
659
674
 
660
675
  const forceRun = values.force === true;
@@ -681,7 +696,11 @@ switch (command) {
681
696
 
682
697
  console.log("🔧 ZhiHand Device Test");
683
698
  console.log(` Device: ${testConfig.credentialId}`);
684
- 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("");
685
704
 
686
705
  // Pre-fetch device profile
687
706
  let currentRawAttrs = {};
@@ -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.js CHANGED
@@ -283,12 +283,15 @@ export async function fetchUserCredentials(endpoint, userId, controllerToken, on
283
283
  export async function waitForCommandAck(_config, options) {
284
284
  const timeoutMs = options.timeoutMs ?? 15_000;
285
285
  log.debug(`[ws-cmd] Waiting for ACK: commandId=${options.commandId}, timeout=${timeoutMs}ms`);
286
+ const t0 = Date.now();
286
287
  return new Promise((resolve, reject) => {
287
288
  const timeout = setTimeout(() => {
289
+ log.debug(`[ws-cmd] ACK timeout: commandId=${options.commandId} after ${Date.now() - t0}ms`);
288
290
  cleanup();
289
291
  resolve({ acked: false });
290
292
  }, timeoutMs);
291
293
  const unsubscribe = subscribeToCommandAck(options.commandId, (ackedCommand) => {
294
+ log.debug(`[ws-cmd] ACK received: commandId=${options.commandId} status=${ackedCommand.ack_status ?? "ok"} ${Date.now() - t0}ms`);
292
295
  cleanup();
293
296
  resolve({ acked: true, command: ackedCommand });
294
297
  });
@@ -21,9 +21,9 @@ let activeBackend = null;
21
21
  let activeModel = null; // user-selected model alias, null = use default
22
22
  let isProcessing = false;
23
23
  const promptQueue = [];
24
+ import { log as coreLog, setTimestampEnabled } from "../core/logger.js";
24
25
  function log(msg) {
25
- const ts = new Date().toLocaleTimeString();
26
- process.stdout.write(`[${ts}] ${msg}\n`);
26
+ coreLog.info(msg);
27
27
  }
28
28
  // ── Prompt Processing ──────────────────────────────────────
29
29
  async function processPrompt(config, prompt) {
@@ -106,7 +106,6 @@ function handleInternalAPI(req, res) {
106
106
  }
107
107
  // Execute command via daemon's WS (used by zhihand test)
108
108
  if (url === "/internal/exec" && req.method === "POST") {
109
- dbg(`[api] POST /internal/exec`);
110
109
  let body = "";
111
110
  const MAX_BODY = 10 * 1024;
112
111
  req.on("data", (chunk) => {
@@ -118,19 +117,28 @@ function handleInternalAPI(req, res) {
118
117
  }
119
118
  });
120
119
  req.on("end", async () => {
120
+ const t0 = Date.now();
121
121
  try {
122
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`);
123
126
  const cfg = resolveConfig(credentialId);
124
127
  const effectiveTimeout = timeoutMs ?? 10_000;
125
128
  const queued = await enqueueCommand(cfg, command);
129
+ dbg(`[api] /internal/exec enqueued id=${queued.id}`);
126
130
  const ack = await waitForCommandAck(cfg, {
127
131
  commandId: queued.id,
128
132
  timeoutMs: effectiveTimeout,
129
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`);
130
137
  res.writeHead(200, { "Content-Type": "application/json" });
131
138
  res.end(JSON.stringify({ id: queued.id, ...ack }));
132
139
  }
133
140
  catch (err) {
141
+ dbg(`[api] /internal/exec error: ${err.message} ${Date.now() - t0}ms`);
134
142
  res.writeHead(500, { "Content-Type": "application/json" });
135
143
  res.end(JSON.stringify({ error: err.message }));
136
144
  }
@@ -190,6 +198,7 @@ export function isAlreadyRunning() {
190
198
  }
191
199
  // ── Main Daemon Entry ──────���───────────────────────────────
192
200
  export async function startDaemon(options) {
201
+ setTimestampEnabled(true);
193
202
  if (options?.debug)
194
203
  setDebugEnabled(true);
195
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
  }
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.33.0";
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.33.0";
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.33.0",
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",