@zhihand/mcp 0.28.0 → 0.30.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 +254 -158
- package/dist/core/command.d.ts +5 -5
- package/dist/core/command.js +2 -5
- package/dist/core/config.d.ts +32 -20
- package/dist/core/config.js +102 -40
- package/dist/core/device.d.ts +41 -16
- package/dist/core/device.js +199 -79
- package/dist/core/pair.d.ts +9 -9
- package/dist/core/pair.js +54 -30
- package/dist/core/registry.d.ts +67 -0
- package/dist/core/registry.js +288 -0
- package/dist/core/screenshot.d.ts +13 -2
- package/dist/core/screenshot.js +43 -3
- package/dist/core/sse.d.ts +13 -16
- package/dist/core/sse.js +46 -54
- package/dist/daemon/dispatcher.d.ts +3 -1
- package/dist/daemon/dispatcher.js +3 -2
- package/dist/daemon/heartbeat.d.ts +4 -4
- package/dist/daemon/index.js +8 -6
- package/dist/daemon/prompt-listener.d.ts +3 -1
- package/dist/daemon/prompt-listener.js +2 -6
- package/dist/index.d.ts +3 -3
- package/dist/index.js +102 -40
- package/dist/openclaw.adapter.js +10 -2
- package/dist/tools/control.d.ts +10 -3
- package/dist/tools/control.js +58 -29
- package/dist/tools/pair.js +15 -18
- package/dist/tools/resolve.d.ts +7 -0
- package/dist/tools/resolve.js +22 -0
- package/dist/tools/schemas.d.ts +9 -1
- package/dist/tools/schemas.js +10 -8
- package/dist/tools/screenshot.d.ts +3 -2
- package/dist/tools/screenshot.js +2 -2
- package/dist/tools/system.d.ts +3 -5
- package/dist/tools/system.js +18 -5
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ZhiHandRuntimeConfig } from "../core/config.ts";
|
|
2
|
+
type ZhiHandConfig = ZhiHandRuntimeConfig;
|
|
2
3
|
export interface MobilePrompt {
|
|
3
4
|
id: string;
|
|
4
5
|
credential_id: string;
|
|
@@ -32,3 +33,4 @@ export declare class PromptListener {
|
|
|
32
33
|
private stopPolling;
|
|
33
34
|
private poll;
|
|
34
35
|
}
|
|
36
|
+
export {};
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { updateDeviceProfile } from "../core/device.js";
|
|
2
1
|
import { dbg } from "./logger.js";
|
|
3
2
|
const SSE_WATCHDOG_TIMEOUT = 120_000; // 120s no data → reconnect (servers may not send keepalive frequently)
|
|
4
3
|
const SSE_RECONNECT_DELAY = 3_000;
|
|
@@ -133,11 +132,8 @@ export class PromptListener {
|
|
|
133
132
|
}
|
|
134
133
|
}
|
|
135
134
|
else if (event.kind === "device_profile.updated" && event.device_profile) {
|
|
136
|
-
//
|
|
137
|
-
|
|
138
|
-
// If not received, device profile is still fetched at daemon startup.
|
|
139
|
-
this.log("[device] Device profile updated via SSE");
|
|
140
|
-
updateDeviceProfile(event.device_profile);
|
|
135
|
+
// Registry owns device-profile updates; this listener is only for prompts.
|
|
136
|
+
this.log("[device] device_profile.updated event received on prompts stream (ignored; registry handles it)");
|
|
141
137
|
}
|
|
142
138
|
}
|
|
143
139
|
startPolling() {
|
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.
|
|
3
|
-
export declare function createServer(
|
|
4
|
-
export declare function startStdioServer(
|
|
2
|
+
export declare const PACKAGE_VERSION = "0.30.0";
|
|
3
|
+
export declare function createServer(): McpServer;
|
|
4
|
+
export declare function startStdioServer(): Promise<void>;
|
package/dist/index.js
CHANGED
|
@@ -1,79 +1,141 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import {
|
|
4
|
-
import { controlSchema, systemSchema, screenshotSchema, pairSchema } from "./tools/schemas.js";
|
|
3
|
+
import { controlSchema, systemSchema, screenshotSchema, pairSchema, listDevicesSchema, statusSchema, } from "./tools/schemas.js";
|
|
5
4
|
import { executeControl } from "./tools/control.js";
|
|
6
5
|
import { executeSystem } from "./tools/system.js";
|
|
7
6
|
import { handleScreenshot } from "./tools/screenshot.js";
|
|
8
7
|
import { handlePair } from "./tools/pair.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
import { resolveTargetDevice } from "./tools/resolve.js";
|
|
9
|
+
import { buildControlToolDescription, buildSystemToolDescription, buildScreenshotToolDescription, formatDeviceStatus, } from "./core/device.js";
|
|
10
|
+
import { registry } from "./core/registry.js";
|
|
11
|
+
export const PACKAGE_VERSION = "0.30.0";
|
|
12
|
+
function errorResult(message) {
|
|
13
|
+
return { content: [{ type: "text", text: message }], isError: true };
|
|
14
|
+
}
|
|
15
|
+
export function createServer() {
|
|
12
16
|
const server = new McpServer({
|
|
13
17
|
name: "zhihand",
|
|
14
18
|
version: PACKAGE_VERSION,
|
|
15
19
|
});
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
20
|
+
const controlTool = server.tool("zhihand_control", buildControlToolDescription(null, registry.listOnline()), controlSchema, async (params) => {
|
|
21
|
+
const resolved = resolveTargetDevice(params.device_id);
|
|
22
|
+
if ("error" in resolved)
|
|
23
|
+
return errorResult(resolved.error);
|
|
24
|
+
const { state } = resolved;
|
|
25
|
+
const cfg = registry.toRuntimeConfig(state);
|
|
26
|
+
const platform = state.profile?.platform ?? "unknown";
|
|
27
|
+
return await executeControl(cfg, params, platform, state.capabilities);
|
|
21
28
|
});
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
const systemTool = server.tool("zhihand_system", buildSystemToolDescription(null, registry.listOnline()), systemSchema, async (params) => {
|
|
30
|
+
const resolved = resolveTargetDevice(params.device_id);
|
|
31
|
+
if ("error" in resolved)
|
|
32
|
+
return errorResult(resolved.error);
|
|
33
|
+
const { state } = resolved;
|
|
34
|
+
const cfg = registry.toRuntimeConfig(state);
|
|
35
|
+
const platform = state.profile?.platform ?? "unknown";
|
|
36
|
+
return await executeSystem(cfg, params, platform);
|
|
26
37
|
});
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
const screenshotTool = server.tool("zhihand_screenshot", buildScreenshotToolDescription(null, registry.listOnline()), screenshotSchema, async (params) => {
|
|
39
|
+
const resolved = resolveTargetDevice(params.device_id);
|
|
40
|
+
if ("error" in resolved)
|
|
41
|
+
return errorResult(resolved.error);
|
|
42
|
+
const { state } = resolved;
|
|
43
|
+
const cfg = registry.toRuntimeConfig(state);
|
|
44
|
+
return await handleScreenshot(cfg, state.capabilities);
|
|
31
45
|
});
|
|
32
|
-
|
|
33
|
-
|
|
46
|
+
server.tool("zhihand_status", "Get device status and capability readiness for a device. Returns curated fields (platform, model, OS, screen, battery, network, BLE, ...), a `capabilities` object with `ready`/`reason` for screen_sharing, hid, live_session, profile.age, AND a `raw` map of allowlisted device attributes. Pass device_id when multiple devices are online.", statusSchema, async (params) => {
|
|
47
|
+
const resolved = resolveTargetDevice(params.device_id);
|
|
48
|
+
if ("error" in resolved)
|
|
49
|
+
return errorResult(resolved.error);
|
|
34
50
|
return {
|
|
35
51
|
content: [{
|
|
36
52
|
type: "text",
|
|
37
|
-
text: JSON.stringify(formatDeviceStatus(), null, 2),
|
|
53
|
+
text: JSON.stringify(formatDeviceStatus(resolved.state), null, 2),
|
|
54
|
+
}],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
server.tool("zhihand_list_devices", "List ALL configured ZhiHand devices with their online status. Returns credential_id, label, platform, online, last_seen_ms_ago for each. Call this before zhihand_control/system/screenshot/status when multiple devices may be online.", listDevicesSchema, async () => {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
const devices = registry.list().map((d) => ({
|
|
60
|
+
credential_id: d.credentialId,
|
|
61
|
+
label: d.label,
|
|
62
|
+
platform: d.platform,
|
|
63
|
+
online: d.online,
|
|
64
|
+
last_seen_ms_ago: d.lastSeenAtMs > 0 ? now - d.lastSeenAtMs : -1,
|
|
65
|
+
}));
|
|
66
|
+
return {
|
|
67
|
+
content: [{
|
|
68
|
+
type: "text",
|
|
69
|
+
text: JSON.stringify({ devices }, null, 2),
|
|
38
70
|
}],
|
|
39
71
|
};
|
|
40
72
|
});
|
|
41
|
-
// zhihand_pair — device pairing
|
|
42
73
|
server.tool("zhihand_pair", "Pair a new mobile device via QR code.", pairSchema, async (params) => {
|
|
43
74
|
return await handlePair(params);
|
|
44
75
|
});
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
76
|
+
// Dynamic tool-description updates on online-set change.
|
|
77
|
+
// Each .update() call also triggers sendToolListChanged() internally, but we
|
|
78
|
+
// wrap each individually so one throwing does NOT block the others, and we
|
|
79
|
+
// still call sendToolListChanged() explicitly as a belt-and-braces signal.
|
|
80
|
+
registry.subscribe(() => {
|
|
81
|
+
const online = registry.listOnline();
|
|
82
|
+
try {
|
|
83
|
+
controlTool.update({ description: buildControlToolDescription(null, online) });
|
|
84
|
+
}
|
|
85
|
+
catch { /* best-effort */ }
|
|
86
|
+
try {
|
|
87
|
+
systemTool.update({ description: buildSystemToolDescription(null, online) });
|
|
88
|
+
}
|
|
89
|
+
catch { /* best-effort */ }
|
|
90
|
+
try {
|
|
91
|
+
screenshotTool.update({ description: buildScreenshotToolDescription(null, online) });
|
|
92
|
+
}
|
|
93
|
+
catch { /* best-effort */ }
|
|
94
|
+
try {
|
|
95
|
+
server.server.sendToolListChanged();
|
|
96
|
+
}
|
|
97
|
+
catch { /* best-effort */ }
|
|
98
|
+
});
|
|
99
|
+
// device://profile — returns default online device
|
|
100
|
+
server.resource("device-profile", "device://profile", { description: "Device static and dynamic context for the default online device." }, async () => {
|
|
101
|
+
const state = registry.resolveDefault();
|
|
102
|
+
if (!state) {
|
|
103
|
+
return {
|
|
104
|
+
contents: [{
|
|
105
|
+
uri: "device://profile",
|
|
106
|
+
mimeType: "application/json",
|
|
107
|
+
text: JSON.stringify({ error: "No device online" }, null, 2),
|
|
108
|
+
}],
|
|
109
|
+
};
|
|
110
|
+
}
|
|
49
111
|
return {
|
|
50
112
|
contents: [{
|
|
51
113
|
uri: "device://profile",
|
|
52
114
|
mimeType: "application/json",
|
|
53
|
-
text: JSON.stringify(
|
|
115
|
+
text: JSON.stringify(formatDeviceStatus(state), null, 2),
|
|
54
116
|
}],
|
|
55
117
|
};
|
|
56
118
|
});
|
|
57
119
|
return server;
|
|
58
120
|
}
|
|
59
|
-
export async function startStdioServer(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const config = resolveConfig(deviceName);
|
|
63
|
-
await fetchDeviceProfile(config);
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
// Non-fatal — server will use generic descriptions
|
|
67
|
-
}
|
|
68
|
-
const server = createServer(deviceName);
|
|
121
|
+
export async function startStdioServer() {
|
|
122
|
+
await registry.init();
|
|
123
|
+
const server = createServer();
|
|
69
124
|
const transport = new StdioServerTransport();
|
|
70
125
|
await server.connect(transport);
|
|
71
126
|
}
|
|
72
|
-
|
|
127
|
+
function setupShutdown() {
|
|
128
|
+
const shutdown = () => {
|
|
129
|
+
registry.shutdown();
|
|
130
|
+
process.exit(0);
|
|
131
|
+
};
|
|
132
|
+
process.on("SIGINT", shutdown);
|
|
133
|
+
process.on("SIGTERM", shutdown);
|
|
134
|
+
}
|
|
135
|
+
setupShutdown();
|
|
73
136
|
const isDirectRun = process.argv[1]?.endsWith("index.ts") || process.argv[1]?.endsWith("index.js");
|
|
74
137
|
if (isDirectRun) {
|
|
75
|
-
|
|
76
|
-
startStdioServer(deviceArg ?? process.env.ZHIHAND_DEVICE).catch((err) => {
|
|
138
|
+
startStdioServer().catch((err) => {
|
|
77
139
|
process.stderr.write(`ZhiHand MCP Server failed: ${err.message}\n`);
|
|
78
140
|
process.exit(1);
|
|
79
141
|
});
|
package/dist/openclaw.adapter.js
CHANGED
|
@@ -8,6 +8,7 @@ import { handleScreenshot } from "./tools/screenshot.js";
|
|
|
8
8
|
import { handlePair } from "./tools/pair.js";
|
|
9
9
|
import { detectCLITools, formatDetectedTools } from "./cli/detect.js";
|
|
10
10
|
import { controlSchema, screenshotSchema, pairSchema } from "./tools/schemas.js";
|
|
11
|
+
import { registry } from "./core/registry.js";
|
|
11
12
|
function zodSchemaToJsonSchema(zodShape) {
|
|
12
13
|
// Simplified conversion — OpenClaw uses JSON Schema-like parameter objects.
|
|
13
14
|
// The actual Zod schemas are used for validation inside tool handlers.
|
|
@@ -23,6 +24,8 @@ function zodSchemaToJsonSchema(zodShape) {
|
|
|
23
24
|
}
|
|
24
25
|
export function registerOpenClawTools(api, deviceName) {
|
|
25
26
|
const log = (msg) => api.logger.info?.(msg);
|
|
27
|
+
// Kick off registry in the background so runtime config resolution benefits.
|
|
28
|
+
void registry.init().catch(() => { });
|
|
26
29
|
// zhihand_control
|
|
27
30
|
api.registerTool({
|
|
28
31
|
name: "zhihand_control",
|
|
@@ -31,7 +34,10 @@ export function registerOpenClawTools(api, deviceName) {
|
|
|
31
34
|
parameters: zodSchemaToJsonSchema(controlSchema),
|
|
32
35
|
execute: async (_id, params) => {
|
|
33
36
|
const config = resolveConfig(deviceName);
|
|
34
|
-
const
|
|
37
|
+
const state = registry.get(config.credentialId);
|
|
38
|
+
const platform = state?.profile?.platform ?? "unknown";
|
|
39
|
+
const caps = state?.capabilities ?? null;
|
|
40
|
+
const result = await executeControl(config, params, platform, caps);
|
|
35
41
|
return result;
|
|
36
42
|
},
|
|
37
43
|
});
|
|
@@ -43,7 +49,9 @@ export function registerOpenClawTools(api, deviceName) {
|
|
|
43
49
|
parameters: zodSchemaToJsonSchema(screenshotSchema),
|
|
44
50
|
execute: async (_id, _params) => {
|
|
45
51
|
const config = resolveConfig(deviceName);
|
|
46
|
-
const
|
|
52
|
+
const state = registry.get(config.credentialId);
|
|
53
|
+
const caps = state?.capabilities ?? null;
|
|
54
|
+
const result = await handleScreenshot(config, caps);
|
|
47
55
|
return result;
|
|
48
56
|
},
|
|
49
57
|
});
|
package/dist/tools/control.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ZhiHandRuntimeConfig } from "../core/config.ts";
|
|
2
2
|
import type { ControlParams } from "../core/command.ts";
|
|
3
|
+
import type { ScreenshotResult } from "../core/screenshot.ts";
|
|
4
|
+
import type { Capabilities } from "../core/device.ts";
|
|
3
5
|
type TextContent = {
|
|
4
6
|
type: "text";
|
|
5
7
|
text: string;
|
|
@@ -13,6 +15,11 @@ type ToolContent = TextContent | ImageContent;
|
|
|
13
15
|
type ToolResult = {
|
|
14
16
|
content: ToolContent[];
|
|
15
17
|
};
|
|
16
|
-
|
|
17
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Build a short human-readable warning for the LLM if the underlying
|
|
20
|
+
* capability isn't ready, or if the last screenshot is stale.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildReadinessWarning(requiredCapability: "hid" | "screen" | "none", capabilities: Capabilities | null, screenshot: ScreenshotResult | null): string;
|
|
23
|
+
export declare function executeControl(config: ZhiHandRuntimeConfig, params: ControlParams, platform: string, capabilities: Capabilities | null): Promise<ToolResult>;
|
|
24
|
+
export declare function executeScreenshot(config: ZhiHandRuntimeConfig, capabilities: Capabilities | null): Promise<ToolResult>;
|
|
18
25
|
export {};
|
package/dist/tools/control.js
CHANGED
|
@@ -1,52 +1,81 @@
|
|
|
1
1
|
import { createControlCommand, enqueueCommand, formatAckSummary } from "../core/command.js";
|
|
2
|
-
import {
|
|
2
|
+
import { fetchScreenshot } from "../core/screenshot.js";
|
|
3
3
|
import { waitForCommandAck } from "../core/sse.js";
|
|
4
4
|
function sleep(ms) {
|
|
5
5
|
return new Promise((r) => setTimeout(r, ms));
|
|
6
6
|
}
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Build a short human-readable warning for the LLM if the underlying
|
|
9
|
+
* capability isn't ready, or if the last screenshot is stale.
|
|
10
|
+
*/
|
|
11
|
+
export function buildReadinessWarning(requiredCapability, capabilities, screenshot) {
|
|
12
|
+
if (!capabilities)
|
|
13
|
+
return "";
|
|
14
|
+
const warnings = [];
|
|
15
|
+
if (requiredCapability === "hid" && !capabilities.hid.ready) {
|
|
16
|
+
warnings.push(`⚠️ HID not ready: ${capabilities.hid.reason}`);
|
|
17
|
+
}
|
|
18
|
+
if (requiredCapability === "screen" && !capabilities.screen_sharing.ready) {
|
|
19
|
+
warnings.push(`⚠️ Screen sharing not active: ${capabilities.screen_sharing.reason}`);
|
|
20
|
+
}
|
|
21
|
+
if (screenshot && screenshot.stale) {
|
|
22
|
+
warnings.push(`⚠️ Stale screenshot: age=${(screenshot.ageMs / 1000).toFixed(1)}s (phone may not be actively sharing the screen).`);
|
|
23
|
+
}
|
|
24
|
+
if (capabilities.profile.stale) {
|
|
25
|
+
warnings.push(`⚠️ Stale device profile: ${(capabilities.profile.age_ms / 1000).toFixed(1)}s old — readiness flags may be out of date.`);
|
|
26
|
+
}
|
|
27
|
+
return warnings.join("\n");
|
|
28
|
+
}
|
|
29
|
+
export async function executeControl(config, params, platform, capabilities) {
|
|
9
30
|
if (params.action === "wait") {
|
|
10
31
|
await sleep(params.durationMs ?? 1000);
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
|
|
32
|
+
const shot = await fetchScreenshot(config);
|
|
33
|
+
const warning = buildReadinessWarning("screen", capabilities, shot);
|
|
34
|
+
const content = [];
|
|
35
|
+
if (warning)
|
|
36
|
+
content.push({ type: "text", text: warning });
|
|
37
|
+
content.push({ type: "text", text: `Waited ${params.durationMs ?? 1000}ms` });
|
|
38
|
+
content.push({ type: "image", data: shot.buffer.toString("base64"), mimeType: "image/jpeg" });
|
|
39
|
+
return { content };
|
|
40
|
+
}
|
|
20
41
|
if (params.action === "screenshot") {
|
|
21
|
-
return await executeScreenshot(config);
|
|
42
|
+
return await executeScreenshot(config, capabilities);
|
|
22
43
|
}
|
|
23
|
-
|
|
24
|
-
const command = createControlCommand(params);
|
|
44
|
+
const command = createControlCommand(params, platform);
|
|
25
45
|
const queued = await enqueueCommand(config, command);
|
|
26
46
|
const ack = await waitForCommandAck(config, { commandId: queued.id, timeoutMs: 15_000 });
|
|
27
|
-
const content = [
|
|
28
|
-
|
|
29
|
-
];
|
|
47
|
+
const content = [];
|
|
48
|
+
let shot = null;
|
|
30
49
|
if (ack.acked) {
|
|
31
50
|
try {
|
|
32
|
-
|
|
33
|
-
content.push({ type: "image", data: screenshot.toString("base64"), mimeType: "image/jpeg" });
|
|
51
|
+
shot = await fetchScreenshot(config);
|
|
34
52
|
}
|
|
35
53
|
catch {
|
|
36
|
-
//
|
|
54
|
+
// best-effort
|
|
37
55
|
}
|
|
38
56
|
}
|
|
57
|
+
const warning = buildReadinessWarning("hid", capabilities, shot);
|
|
58
|
+
if (warning)
|
|
59
|
+
content.push({ type: "text", text: warning });
|
|
60
|
+
content.push({ type: "text", text: formatAckSummary(params.action, ack) });
|
|
61
|
+
if (shot) {
|
|
62
|
+
content.push({ type: "image", data: shot.buffer.toString("base64"), mimeType: "image/jpeg" });
|
|
63
|
+
}
|
|
39
64
|
return { content };
|
|
40
65
|
}
|
|
41
|
-
export async function executeScreenshot(config) {
|
|
66
|
+
export async function executeScreenshot(config, capabilities) {
|
|
42
67
|
const command = createControlCommand({ action: "screenshot" });
|
|
43
68
|
const queued = await enqueueCommand(config, command);
|
|
44
69
|
const ack = await waitForCommandAck(config, { commandId: queued.id, timeoutMs: 5_000 });
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
const shot = await fetchScreenshot(config);
|
|
71
|
+
const warning = buildReadinessWarning("screen", capabilities, shot);
|
|
72
|
+
const content = [];
|
|
73
|
+
if (warning)
|
|
74
|
+
content.push({ type: "text", text: warning });
|
|
75
|
+
content.push({
|
|
76
|
+
type: "text",
|
|
77
|
+
text: `Screenshot captured (acked: ${ack.acked}, age: ${shot.ageMs >= 0 ? `${shot.ageMs}ms` : "unknown"}, size: ${shot.width}x${shot.height}, seq: ${shot.sequence})`,
|
|
78
|
+
});
|
|
79
|
+
content.push({ type: "image", data: shot.buffer.toString("base64"), mimeType: "image/jpeg" });
|
|
80
|
+
return { content };
|
|
52
81
|
}
|
package/dist/tools/pair.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { createPairingSession, registerPlugin, renderPairingQRCode,
|
|
1
|
+
import { loadConfig } from "../core/config.js";
|
|
2
|
+
import { createPairingSession, registerPlugin, renderPairingQRCode, } from "../core/pair.js";
|
|
3
3
|
const DEFAULT_ENDPOINT = "https://api.zhihand.com";
|
|
4
4
|
const DEFAULT_EDGE_ID_PREFIX = "mcp-";
|
|
5
5
|
function generateEdgeId() {
|
|
@@ -7,26 +7,23 @@ function generateEdgeId() {
|
|
|
7
7
|
}
|
|
8
8
|
export async function handlePair(params, endpoint) {
|
|
9
9
|
const resolvedEndpoint = endpoint ?? DEFAULT_ENDPOINT;
|
|
10
|
-
// Check existing credential
|
|
11
10
|
if (!params.forceNew) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
11
|
+
const cfg = loadConfig();
|
|
12
|
+
const records = Object.values(cfg.devices);
|
|
13
|
+
if (records.length > 0) {
|
|
14
|
+
const lines = [
|
|
15
|
+
"Already paired with:",
|
|
16
|
+
"",
|
|
17
|
+
...records.map((r) => ` - ${r.credential_id} (${r.label}, ${r.platform}) via ${r.endpoint}`),
|
|
18
|
+
"",
|
|
19
|
+
"Pass forceNew=true to pair another device.",
|
|
20
|
+
];
|
|
21
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
19
22
|
}
|
|
20
23
|
}
|
|
21
|
-
// Register plugin first — server requires a known edge_id before pairing
|
|
22
24
|
const stableIdentity = generateEdgeId();
|
|
23
|
-
const plugin = await registerPlugin(resolvedEndpoint, {
|
|
24
|
-
|
|
25
|
-
});
|
|
26
|
-
// Create new pairing session with the registered edge_id
|
|
27
|
-
const session = await createPairingSession(resolvedEndpoint, {
|
|
28
|
-
edgeId: plugin.edge_id,
|
|
29
|
-
});
|
|
25
|
+
const plugin = await registerPlugin(resolvedEndpoint, { stableIdentity });
|
|
26
|
+
const session = await createPairingSession(resolvedEndpoint, { edgeId: plugin.edge_id });
|
|
30
27
|
const qr = await renderPairingQRCode(session.pair_url);
|
|
31
28
|
return {
|
|
32
29
|
content: [
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { registry } from "../core/registry.js";
|
|
2
|
+
export function resolveTargetDevice(deviceId) {
|
|
3
|
+
const online = registry.listOnline();
|
|
4
|
+
if (deviceId) {
|
|
5
|
+
const s = registry.get(deviceId);
|
|
6
|
+
if (!s) {
|
|
7
|
+
return { error: `Device '${deviceId}' not found. Call zhihand_list_devices.` };
|
|
8
|
+
}
|
|
9
|
+
if (!s.online) {
|
|
10
|
+
return { error: `Device '${deviceId}' is offline. Call zhihand_list_devices for online devices.` };
|
|
11
|
+
}
|
|
12
|
+
return { state: s };
|
|
13
|
+
}
|
|
14
|
+
if (online.length === 0) {
|
|
15
|
+
return { error: "No devices online. Ask user to open the ZhiHand app." };
|
|
16
|
+
}
|
|
17
|
+
if (online.length === 1) {
|
|
18
|
+
return { state: online[0] };
|
|
19
|
+
}
|
|
20
|
+
const ids = online.map((d) => `${d.credentialId} (${d.label})`).join(", ");
|
|
21
|
+
return { error: `Multiple devices online — pass device_id. Online: ${ids}` };
|
|
22
|
+
}
|
package/dist/tools/schemas.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const controlSchema: {
|
|
3
|
+
device_id: z.ZodOptional<z.ZodString>;
|
|
3
4
|
action: z.ZodEnum<["click", "doubleclick", "longclick", "rightclick", "middleclick", "type", "swipe", "scroll", "keycombo", "back", "home", "enter", "clipboard", "open_app", "wait", "screenshot"]>;
|
|
4
5
|
xRatio: z.ZodOptional<z.ZodNumber>;
|
|
5
6
|
yRatio: z.ZodOptional<z.ZodNumber>;
|
|
@@ -17,10 +18,17 @@ export declare const controlSchema: {
|
|
|
17
18
|
urlScheme: z.ZodOptional<z.ZodString>;
|
|
18
19
|
};
|
|
19
20
|
export declare const systemSchema: {
|
|
21
|
+
device_id: z.ZodOptional<z.ZodString>;
|
|
20
22
|
action: z.ZodEnum<["notification", "recent", "search", "switch_input", "siri", "control_center", "open_browser", "shortcut_help", "volume_up", "volume_down", "mute", "play_pause", "stop", "next_track", "prev_track", "fast_forward", "rewind", "brightness_up", "brightness_down", "power"]>;
|
|
21
23
|
text: z.ZodOptional<z.ZodString>;
|
|
22
24
|
};
|
|
23
|
-
export declare const screenshotSchema: {
|
|
25
|
+
export declare const screenshotSchema: {
|
|
26
|
+
device_id: z.ZodOptional<z.ZodString>;
|
|
27
|
+
};
|
|
24
28
|
export declare const pairSchema: {
|
|
25
29
|
forceNew: z.ZodOptional<z.ZodDefault<z.ZodBoolean>>;
|
|
26
30
|
};
|
|
31
|
+
export declare const listDevicesSchema: {};
|
|
32
|
+
export declare const statusSchema: {
|
|
33
|
+
device_id: z.ZodOptional<z.ZodString>;
|
|
34
|
+
};
|
package/dist/tools/schemas.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
const deviceIdSchema = z.string().optional().describe("Credential ID of the target device. Optional if only one device is online. Call zhihand_list_devices to see online devices.");
|
|
2
3
|
export const controlSchema = {
|
|
4
|
+
device_id: deviceIdSchema,
|
|
3
5
|
action: z.enum([
|
|
4
6
|
"click", "doubleclick", "longclick", "rightclick", "middleclick",
|
|
5
7
|
"type", "swipe", "scroll", "keycombo",
|
|
@@ -13,7 +15,6 @@ export const controlSchema = {
|
|
|
13
15
|
direction: z.enum(["up", "down", "left", "right"]).optional().describe("Scroll direction"),
|
|
14
16
|
amount: z.number().int().positive().default(3).optional().describe("Scroll steps (default 3)"),
|
|
15
17
|
keys: z.string().optional().describe("Key combo string, e.g. 'ctrl+c', 'alt+tab'"),
|
|
16
|
-
// clipboardAction removed — app only supports set (text via "text" param)
|
|
17
18
|
durationMs: z.number().int().positive().max(10000).optional().describe("Duration in ms: wait (default 1000), longclick (default 800), swipe (default 300). Max 10000"),
|
|
18
19
|
startXRatio: z.number().min(0).max(1).optional().describe("Swipe start X [0,1]"),
|
|
19
20
|
startYRatio: z.number().min(0).max(1).optional().describe("Swipe start Y [0,1]"),
|
|
@@ -23,25 +24,26 @@ export const controlSchema = {
|
|
|
23
24
|
bundleId: z.string().optional().describe("iOS bundle ID, e.g. 'com.tencent.xin'"),
|
|
24
25
|
urlScheme: z.string().optional().describe("URL scheme, e.g. 'weixin://'"),
|
|
25
26
|
};
|
|
26
|
-
// zhihand_system — system navigation + media controls (separate from UI control)
|
|
27
27
|
export const systemSchema = {
|
|
28
|
+
device_id: deviceIdSchema,
|
|
28
29
|
action: z.enum([
|
|
29
|
-
// System navigation — cross-platform
|
|
30
30
|
"notification", "recent", "search", "switch_input",
|
|
31
|
-
// System navigation — iOS only
|
|
32
31
|
"siri", "control_center",
|
|
33
|
-
// System navigation — Android only
|
|
34
32
|
"open_browser", "shortcut_help",
|
|
35
|
-
// Media controls — cross-platform
|
|
36
33
|
"volume_up", "volume_down", "mute",
|
|
37
34
|
"play_pause", "stop", "next_track", "prev_track",
|
|
38
35
|
"fast_forward", "rewind",
|
|
39
|
-
// Hardware — cross-platform
|
|
40
36
|
"brightness_up", "brightness_down", "power",
|
|
41
37
|
]).describe("System or media action to perform"),
|
|
42
38
|
text: z.string().optional().describe("Optional text, e.g. search query for 'search' action"),
|
|
43
39
|
};
|
|
44
|
-
export const screenshotSchema = {
|
|
40
|
+
export const screenshotSchema = {
|
|
41
|
+
device_id: deviceIdSchema,
|
|
42
|
+
};
|
|
45
43
|
export const pairSchema = {
|
|
46
44
|
forceNew: z.boolean().default(false).optional().describe("Force new pairing even if already paired"),
|
|
47
45
|
};
|
|
46
|
+
export const listDevicesSchema = {};
|
|
47
|
+
export const statusSchema = {
|
|
48
|
+
device_id: deviceIdSchema,
|
|
49
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { ZhiHandRuntimeConfig } from "../core/config.ts";
|
|
2
|
+
import type { Capabilities } from "../core/device.ts";
|
|
3
|
+
export declare function handleScreenshot(config: ZhiHandRuntimeConfig, capabilities: Capabilities | null): Promise<{
|
|
3
4
|
content: ({
|
|
4
5
|
type: "text";
|
|
5
6
|
text: string;
|
package/dist/tools/screenshot.js
CHANGED
package/dist/tools/system.d.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* zhihand_system tool handler — system navigation + media controls.
|
|
3
|
-
*
|
|
4
|
-
* Separated from zhihand_control to keep UI-control schema focused and
|
|
5
|
-
* reduce LLM parameter hallucination (Gemini design review recommendation).
|
|
6
3
|
*/
|
|
7
|
-
import type {
|
|
4
|
+
import type { ZhiHandRuntimeConfig } from "../core/config.ts";
|
|
8
5
|
import type { SystemParams } from "../core/command.ts";
|
|
9
6
|
type TextContent = {
|
|
10
7
|
type: "text";
|
|
@@ -12,6 +9,7 @@ type TextContent = {
|
|
|
12
9
|
};
|
|
13
10
|
type ToolResult = {
|
|
14
11
|
content: TextContent[];
|
|
12
|
+
isError?: boolean;
|
|
15
13
|
};
|
|
16
|
-
export declare function executeSystem(config:
|
|
14
|
+
export declare function executeSystem(config: ZhiHandRuntimeConfig, params: SystemParams, platform: string): Promise<ToolResult>;
|
|
17
15
|
export {};
|
package/dist/tools/system.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { createSystemCommand, enqueueCommand, formatAckSummary } from "../core/command.js";
|
|
2
2
|
import { waitForCommandAck } from "../core/sse.js";
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
const IOS_ONLY = new Set(["siri", "control_center"]);
|
|
4
|
+
const ANDROID_ONLY = new Set(["open_browser", "shortcut_help"]);
|
|
5
|
+
export async function executeSystem(config, params, platform) {
|
|
6
|
+
// Platform guard
|
|
7
|
+
if (platform !== "ios" && IOS_ONLY.has(params.action)) {
|
|
8
|
+
return {
|
|
9
|
+
content: [{ type: "text", text: `Action ${params.action} is not supported on platform ${platform}` }],
|
|
10
|
+
isError: true,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
if (platform !== "android" && ANDROID_ONLY.has(params.action)) {
|
|
14
|
+
return {
|
|
15
|
+
content: [{ type: "text", text: `Action ${params.action} is not supported on platform ${platform}` }],
|
|
16
|
+
isError: true,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
const command = createSystemCommand(params, platform);
|
|
5
20
|
const queued = await enqueueCommand(config, command);
|
|
6
21
|
const ack = await waitForCommandAck(config, { commandId: queued.id, timeoutMs: 15_000 });
|
|
7
22
|
const summary = formatAckSummary(params.action, ack);
|
|
8
|
-
return {
|
|
9
|
-
content: [{ type: "text", text: summary }],
|
|
10
|
-
};
|
|
23
|
+
return { content: [{ type: "text", text: summary }] };
|
|
11
24
|
}
|