@zhihand/mcp 0.23.1 โ†’ 0.24.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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ZhiHand MCP Server โ€” let AI agents see and control your phone.
4
4
 
5
- Version: `0.23.0`
5
+ Version: `0.24.1`
6
6
 
7
7
  ## What is this?
8
8
 
@@ -96,6 +96,7 @@ zhihand start -d Start daemon in background (logs to ~/.zhihand/daemon
96
96
  zhihand stop Stop the running daemon
97
97
  zhihand status Show daemon status, pairing info, device, backend, and model
98
98
 
99
+ zhihand test Test device connectivity (screenshot, click, swipe, home, back)
99
100
  zhihand pair Pair with a phone (QR code in terminal)
100
101
  zhihand detect List detected CLI tools and their login status
101
102
  zhihand serve Start MCP Server (stdio mode, backward compatible)
package/bin/zhihand CHANGED
@@ -51,6 +51,7 @@ Usage:
51
51
  zhihand pair Pair with a phone device
52
52
  zhihand detect Detect available CLI tools
53
53
 
54
+ zhihand test Test device connectivity (sends click + type commands)
54
55
  zhihand serve Start MCP Server (stdio mode, backward compat)
55
56
 
56
57
  Options:
@@ -280,6 +281,87 @@ switch (command) {
280
281
  break;
281
282
  }
282
283
 
284
+ case "test": {
285
+ const { resolveConfig: resolveTestConfig } = await import("../dist/core/config.js");
286
+ const { createControlCommand, enqueueCommand } = await import("../dist/core/command.js");
287
+ const { waitForCommandAck } = await import("../dist/core/sse.js");
288
+ const { fetchScreenshotBinary } = await import("../dist/core/screenshot.js");
289
+
290
+ let testConfig;
291
+ try {
292
+ testConfig = resolveTestConfig(values.device ?? process.env.ZHIHAND_DEVICE);
293
+ } catch (err) {
294
+ console.error(`Error: ${err.message}`);
295
+ console.error("Run 'zhihand setup' to pair a device first.");
296
+ process.exit(1);
297
+ }
298
+
299
+ console.log("๐Ÿงช ZhiHand Device Test");
300
+ console.log(` Device: ${testConfig.credentialId}`);
301
+ console.log(` Endpoint: ${testConfig.controlPlaneEndpoint}\n`);
302
+
303
+ const steps = [
304
+ { label: "1. Screenshot", type: "screenshot" },
305
+ { label: "2. Click center", type: "hid", params: { action: "click", xRatio: 0.5, yRatio: 0.5 } },
306
+ { label: "3. Swipe up", type: "hid", params: { action: "swipe", startXRatio: 0.5, startYRatio: 0.7, endXRatio: 0.5, endYRatio: 0.3, durationMs: 300 } },
307
+ { label: "4. Swipe down", type: "hid", params: { action: "swipe", startXRatio: 0.5, startYRatio: 0.3, endXRatio: 0.5, endYRatio: 0.7, durationMs: 300 } },
308
+ { label: "5. Press Home", type: "hid", params: { action: "home" } },
309
+ { label: "6. Press Back", type: "hid", params: { action: "back" } },
310
+ { label: "7. Screenshot", type: "screenshot" },
311
+ ];
312
+
313
+ let passed = 0;
314
+ let failed = 0;
315
+
316
+ for (const step of steps) {
317
+ process.stdout.write(` ${step.label}... `);
318
+ const t0 = Date.now();
319
+ try {
320
+ if (step.type === "screenshot") {
321
+ // Screenshot: send receive_screenshot command, then fetch binary
322
+ const cmd = createControlCommand({ action: "screenshot" });
323
+ const queued = await enqueueCommand(testConfig, cmd);
324
+ const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
325
+ if (ack.acked) {
326
+ const buf = await fetchScreenshotBinary(testConfig);
327
+ const ms = Date.now() - t0;
328
+ console.log(`โœ… ${(buf.length / 1024).toFixed(0)}KB (${ms}ms)`);
329
+ passed++;
330
+ } else {
331
+ console.log(`โฑ๏ธ Timeout (${Date.now() - t0}ms)`);
332
+ failed++;
333
+ }
334
+ } else {
335
+ const cmd = createControlCommand(step.params);
336
+ const queued = await enqueueCommand(testConfig, cmd);
337
+ const ack = await waitForCommandAck(testConfig, { commandId: queued.id, timeoutMs: 10_000 });
338
+ const ms = Date.now() - t0;
339
+ if (ack.acked) {
340
+ console.log(`โœ… (${ms}ms)`);
341
+ passed++;
342
+ } else {
343
+ console.log(`โฑ๏ธ Timeout (${ms}ms)`);
344
+ failed++;
345
+ }
346
+ }
347
+ } catch (err) {
348
+ const ms = Date.now() - t0;
349
+ console.log(`โŒ ${err.message} (${ms}ms)`);
350
+ failed++;
351
+ }
352
+ // Pause between commands: let phone process + user can observe
353
+ await new Promise((r) => setTimeout(r, 2000));
354
+ }
355
+
356
+ console.log(`\n Result: ${passed}/${steps.length} passed`);
357
+ if (failed === 0) {
358
+ console.log(" โœ… Device is responding to all commands!");
359
+ } else {
360
+ console.log(" โš ๏ธ Some commands failed. Check phone connectivity.");
361
+ }
362
+ process.exit(failed > 0 ? 1 : 0);
363
+ }
364
+
283
365
  default:
284
366
  console.error(`Unknown command: ${command}. Run 'zhihand --help' for usage.`);
285
367
  process.exit(1);
@@ -558,11 +558,29 @@ async function dispatchClaudeWithHistory(prompt, startTime, log, model) {
558
558
  // Write prompt to stdin, then close to signal EOF
559
559
  child.stdin?.write(fullPrompt);
560
560
  child.stdin?.end();
561
- const result = await collectChildOutput(child, startTime);
561
+ const raw = await collectChildOutput(child, startTime);
562
+ // Claude --output-format json wraps the result in a JSON envelope โ€” extract the actual text
563
+ const result = extractClaudeResult(raw);
562
564
  recordTurn("user", prompt);
563
565
  recordTurn("assistant", result.text);
564
566
  return result;
565
567
  }
568
+ /** Parse Claude JSON output and extract the result text. */
569
+ function extractClaudeResult(raw) {
570
+ try {
571
+ const parsed = JSON.parse(raw.text);
572
+ if (!parsed || typeof parsed !== "object")
573
+ return raw;
574
+ const resultText = typeof parsed.result === "string" ? parsed.result : raw.text;
575
+ const isError = parsed.is_error === true || parsed.subtype === "error";
576
+ // Preserve process exit failure: only succeed if both JSON and process agree
577
+ return { text: resultText, success: raw.success && !isError, durationMs: raw.durationMs };
578
+ }
579
+ catch {
580
+ // Not JSON โ€” return as-is
581
+ return raw;
582
+ }
583
+ }
566
584
  // โ”€โ”€ Codex JSONL Output Collector โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
567
585
  function collectCodexOutput(child, startTime) {
568
586
  return new Promise((resolve) => {
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.23.1";
2
+ export declare const PACKAGE_VERSION = "0.24.1";
3
3
  export declare function createServer(deviceName?: string): McpServer;
4
4
  export declare function startStdioServer(deviceName?: string): Promise<void>;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { controlSchema, screenshotSchema, pairSchema } from "./tools/schemas.js"
5
5
  import { executeControl } from "./tools/control.js";
6
6
  import { handleScreenshot } from "./tools/screenshot.js";
7
7
  import { handlePair } from "./tools/pair.js";
8
- export const PACKAGE_VERSION = "0.23.1";
8
+ export const PACKAGE_VERSION = "0.24.1";
9
9
  export function createServer(deviceName) {
10
10
  const server = new McpServer({
11
11
  name: "zhihand",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhihand/mcp",
3
- "version": "0.23.1",
3
+ "version": "0.24.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",