@zhihand/mcp 0.23.0 โ 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 +2 -1
- package/bin/zhihand +82 -0
- package/dist/daemon/dispatcher.js +24 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
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.
|
|
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);
|
|
@@ -7,6 +7,8 @@ import { DEFAULT_MODELS } from "../core/config.js";
|
|
|
7
7
|
import { resolveGemini, resolveClaude, resolveCodex } from "../core/resolve-path.js";
|
|
8
8
|
const CLI_TIMEOUT = 300_000; // 300s (5min) per prompt โ MCP tool chains need multiple turns
|
|
9
9
|
const SIGKILL_DELAY = 2_000; // 2s after SIGTERM
|
|
10
|
+
const MCP_PORT = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
|
|
11
|
+
const MCP_URL = `http://127.0.0.1:${MCP_PORT}/mcp`;
|
|
10
12
|
const MAX_OUTPUT_BYTES = 100 * 1024; // 100KB (for one-shot backends)
|
|
11
13
|
const MAX_HISTORY_TURNS = 20; // keep last N exchanges in conversation history
|
|
12
14
|
// Gemini session file polling
|
|
@@ -540,11 +542,14 @@ async function dispatchClaudeWithHistory(prompt, startTime, log, model) {
|
|
|
540
542
|
log(`[claude] One-shot dispatch (history: ${conversationHistory.length} turns)`);
|
|
541
543
|
// Pass prompt via stdin (-p -) to avoid ARG_MAX limit with long conversation history
|
|
542
544
|
// --permission-mode bypassPermissions: auto-approve all tool calls (like gemini's --approval-mode yolo)
|
|
545
|
+
// --mcp-config: explicitly pass MCP server URL so Claude finds it regardless of cwd
|
|
546
|
+
const mcpConfig = JSON.stringify({ mcpServers: { zhihand: { type: "http", url: MCP_URL } } });
|
|
543
547
|
const child = spawn(claudePath, [
|
|
544
548
|
"-p", "-",
|
|
545
549
|
"--model", model,
|
|
546
550
|
"--output-format", "json",
|
|
547
551
|
"--permission-mode", "bypassPermissions",
|
|
552
|
+
"--mcp-config", mcpConfig,
|
|
548
553
|
], {
|
|
549
554
|
env: process.env,
|
|
550
555
|
stdio: ["pipe", "pipe", "pipe"],
|
|
@@ -553,11 +558,29 @@ async function dispatchClaudeWithHistory(prompt, startTime, log, model) {
|
|
|
553
558
|
// Write prompt to stdin, then close to signal EOF
|
|
554
559
|
child.stdin?.write(fullPrompt);
|
|
555
560
|
child.stdin?.end();
|
|
556
|
-
const
|
|
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);
|
|
557
564
|
recordTurn("user", prompt);
|
|
558
565
|
recordTurn("assistant", result.text);
|
|
559
566
|
return result;
|
|
560
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
|
+
}
|
|
561
584
|
// โโ Codex JSONL Output Collector โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
562
585
|
function collectCodexOutput(child, startTime) {
|
|
563
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.
|
|
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.
|
|
8
|
+
export const PACKAGE_VERSION = "0.24.1";
|
|
9
9
|
export function createServer(deviceName) {
|
|
10
10
|
const server = new McpServer({
|
|
11
11
|
name: "zhihand",
|