@zhihand/mcp 0.12.0 → 0.12.2
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 +288 -0
- package/bin/zhihand +6 -6
- package/bin/zhihand.openclaw +2 -2
- package/dist/cli/detect.d.ts +10 -0
- package/dist/cli/detect.js +75 -0
- package/dist/cli/openclaw.d.ts +6 -0
- package/dist/cli/openclaw.js +47 -0
- package/dist/cli/spawn.d.ts +2 -0
- package/dist/cli/spawn.js +31 -0
- package/dist/core/command.d.ts +41 -0
- package/dist/core/command.js +84 -0
- package/dist/core/config.d.ts +26 -0
- package/dist/core/config.js +67 -0
- package/dist/core/pair.d.ts +45 -0
- package/dist/core/pair.js +130 -0
- package/dist/core/screenshot.d.ts +2 -0
- package/dist/core/screenshot.js +21 -0
- package/dist/core/sse.d.ts +35 -0
- package/dist/core/sse.js +149 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +43 -0
- package/dist/openclaw.adapter.d.ts +49 -0
- package/dist/openclaw.adapter.js +72 -0
- package/dist/tools/control.d.ts +18 -0
- package/dist/tools/control.js +52 -0
- package/dist/tools/pair.d.ts +8 -0
- package/dist/tools/pair.js +49 -0
- package/dist/tools/schemas.d.ts +20 -0
- package/dist/tools/schemas.js +25 -0
- package/dist/tools/screenshot.d.ts +11 -0
- package/dist/tools/screenshot.js +4 -0
- package/package.json +13 -5
- package/src/cli/detect.ts +0 -90
- package/src/cli/openclaw.ts +0 -50
- package/src/cli/spawn.ts +0 -34
- package/src/core/command.ts +0 -144
- package/src/core/config.ts +0 -91
- package/src/core/pair.ts +0 -143
- package/src/core/screenshot.ts +0 -28
- package/src/core/sse.ts +0 -88
- package/src/index.ts +0 -53
- package/src/openclaw.adapter.ts +0 -116
- package/src/tools/control.ts +0 -66
- package/src/tools/pair.ts +0 -58
- package/src/tools/schemas.ts +0 -28
- package/src/tools/screenshot.ts +0 -8
package/src/core/pair.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import QRCode from "qrcode";
|
|
2
|
-
import type { ZhiHandConfig, DeviceCredential } from "./config.ts";
|
|
3
|
-
import { saveCredential, loadDefaultCredential, ensureZhiHandDir, saveState } from "./config.ts";
|
|
4
|
-
|
|
5
|
-
export interface PairingSession {
|
|
6
|
-
id: string;
|
|
7
|
-
pair_url: string;
|
|
8
|
-
qr_payload: string;
|
|
9
|
-
controller_token?: string;
|
|
10
|
-
edge_id: string;
|
|
11
|
-
status: "pending" | "claimed" | "expired" | string;
|
|
12
|
-
credential_id?: string;
|
|
13
|
-
expires_at: string;
|
|
14
|
-
requested_scopes?: string[];
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export interface CreatePairingOptions {
|
|
18
|
-
edgeId: string;
|
|
19
|
-
ttlSeconds?: number;
|
|
20
|
-
requestedScopes?: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const DEFAULT_SCOPES = [
|
|
24
|
-
"observe",
|
|
25
|
-
"session.control",
|
|
26
|
-
"screen.read",
|
|
27
|
-
"screen.capture",
|
|
28
|
-
"ble.control",
|
|
29
|
-
];
|
|
30
|
-
|
|
31
|
-
export async function createPairingSession(
|
|
32
|
-
endpoint: string,
|
|
33
|
-
options: CreatePairingOptions
|
|
34
|
-
): Promise<PairingSession> {
|
|
35
|
-
const response = await fetch(`${endpoint}/v1/pairing/sessions`, {
|
|
36
|
-
method: "POST",
|
|
37
|
-
headers: { "Content-Type": "application/json" },
|
|
38
|
-
body: JSON.stringify({
|
|
39
|
-
edge_id: options.edgeId,
|
|
40
|
-
ttl_seconds: options.ttlSeconds ?? 600,
|
|
41
|
-
requested_scopes: options.requestedScopes ?? DEFAULT_SCOPES,
|
|
42
|
-
}),
|
|
43
|
-
});
|
|
44
|
-
if (!response.ok) {
|
|
45
|
-
throw new Error(`Create pairing session failed: ${response.status}`);
|
|
46
|
-
}
|
|
47
|
-
const payload = (await response.json()) as { session: PairingSession; controller_token?: string };
|
|
48
|
-
return {
|
|
49
|
-
...payload.session,
|
|
50
|
-
controller_token: payload.controller_token ?? payload.session.controller_token,
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export async function getPairingSession(
|
|
55
|
-
endpoint: string,
|
|
56
|
-
sessionId: string
|
|
57
|
-
): Promise<PairingSession> {
|
|
58
|
-
const response = await fetch(
|
|
59
|
-
`${endpoint}/v1/pairing/sessions/${encodeURIComponent(sessionId)}`
|
|
60
|
-
);
|
|
61
|
-
if (!response.ok) {
|
|
62
|
-
throw new Error(`Get pairing session failed: ${response.status}`);
|
|
63
|
-
}
|
|
64
|
-
const payload = (await response.json()) as { session: PairingSession };
|
|
65
|
-
return payload.session;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export async function waitForPairingClaim(
|
|
69
|
-
endpoint: string,
|
|
70
|
-
sessionId: string,
|
|
71
|
-
timeoutMs: number = 600_000
|
|
72
|
-
): Promise<PairingSession> {
|
|
73
|
-
const deadline = Date.now() + timeoutMs;
|
|
74
|
-
while (Date.now() < deadline) {
|
|
75
|
-
const session = await getPairingSession(endpoint, sessionId);
|
|
76
|
-
if (session.status === "claimed" && session.credential_id) {
|
|
77
|
-
return session;
|
|
78
|
-
}
|
|
79
|
-
if (session.status === "expired") {
|
|
80
|
-
throw new Error("Pairing session expired.");
|
|
81
|
-
}
|
|
82
|
-
await new Promise((r) => setTimeout(r, 2000));
|
|
83
|
-
}
|
|
84
|
-
throw new Error("Pairing timeout.");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function renderPairingQRCode(url: string): Promise<string> {
|
|
88
|
-
return QRCode.toString(url, { type: "utf8", margin: 1 });
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export async function executePairing(
|
|
92
|
-
endpoint: string,
|
|
93
|
-
edgeId: string,
|
|
94
|
-
deviceName?: string
|
|
95
|
-
): Promise<{ session: PairingSession; credential: DeviceCredential }> {
|
|
96
|
-
const session = await createPairingSession(endpoint, { edgeId });
|
|
97
|
-
|
|
98
|
-
// Save pending state
|
|
99
|
-
saveState({
|
|
100
|
-
sessionId: session.id,
|
|
101
|
-
controllerToken: session.controller_token,
|
|
102
|
-
edgeId: session.edge_id,
|
|
103
|
-
pairUrl: session.pair_url,
|
|
104
|
-
status: "pending",
|
|
105
|
-
expiresAt: session.expires_at,
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// Wait for phone to scan
|
|
109
|
-
const claimed = await waitForPairingClaim(endpoint, session.id);
|
|
110
|
-
|
|
111
|
-
const credential: DeviceCredential = {
|
|
112
|
-
credentialId: claimed.credential_id!,
|
|
113
|
-
controllerToken: claimed.controller_token ?? session.controller_token!,
|
|
114
|
-
endpoint,
|
|
115
|
-
deviceName: deviceName ?? `device_${Date.now()}`,
|
|
116
|
-
pairedAt: new Date().toISOString(),
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const name = deviceName ?? credential.deviceName!;
|
|
120
|
-
saveCredential(name, credential, true);
|
|
121
|
-
|
|
122
|
-
// Update state
|
|
123
|
-
saveState({
|
|
124
|
-
sessionId: session.id,
|
|
125
|
-
controllerToken: credential.controllerToken,
|
|
126
|
-
edgeId: session.edge_id,
|
|
127
|
-
credentialId: credential.credentialId,
|
|
128
|
-
pairUrl: session.pair_url,
|
|
129
|
-
status: "claimed",
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
return { session: claimed, credential };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function formatPairingStatus(cred: DeviceCredential | null): string {
|
|
136
|
-
if (!cred) return "Not paired. Run 'zhihand pair' to connect a device.";
|
|
137
|
-
return [
|
|
138
|
-
`Paired to: ${cred.deviceName ?? "unknown device"}`,
|
|
139
|
-
`Endpoint: ${cred.endpoint}`,
|
|
140
|
-
`Credential: ${cred.credentialId}`,
|
|
141
|
-
`Paired at: ${cred.pairedAt ?? "unknown"}`,
|
|
142
|
-
].join("\n");
|
|
143
|
-
}
|
package/src/core/screenshot.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type { ZhiHandConfig } from "./config.ts";
|
|
2
|
-
|
|
3
|
-
export async function fetchScreenshotBinary(config: ZhiHandConfig): Promise<Buffer> {
|
|
4
|
-
const controller = new AbortController();
|
|
5
|
-
const timeout = setTimeout(() => controller.abort(), config.timeoutMs ?? 10_000);
|
|
6
|
-
|
|
7
|
-
try {
|
|
8
|
-
const response = await fetch(
|
|
9
|
-
`${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/screen`,
|
|
10
|
-
{
|
|
11
|
-
method: "GET",
|
|
12
|
-
headers: {
|
|
13
|
-
"x-zhihand-controller-token": config.controllerToken,
|
|
14
|
-
"Accept": "image/jpeg",
|
|
15
|
-
},
|
|
16
|
-
signal: controller.signal,
|
|
17
|
-
}
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
if (!response.ok) {
|
|
21
|
-
throw new Error(`Screenshot fetch failed: ${response.status}`);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
return Buffer.from(await response.arrayBuffer());
|
|
25
|
-
} finally {
|
|
26
|
-
clearTimeout(timeout);
|
|
27
|
-
}
|
|
28
|
-
}
|
package/src/core/sse.ts
DELETED
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { ZhiHandConfig } from "./config.ts";
|
|
2
|
-
import type { QueuedCommandRecord, WaitForCommandAckResult } from "./command.ts";
|
|
3
|
-
import { getCommand } from "./command.ts";
|
|
4
|
-
|
|
5
|
-
export interface SSEEvent {
|
|
6
|
-
id: string;
|
|
7
|
-
topic: string;
|
|
8
|
-
kind: string;
|
|
9
|
-
credential_id: string;
|
|
10
|
-
command?: QueuedCommandRecord;
|
|
11
|
-
sequence: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// Per-commandId callback registry for SSE-based ACK
|
|
15
|
-
const ackCallbacks = new Map<string, (command: QueuedCommandRecord) => void>();
|
|
16
|
-
|
|
17
|
-
export function handleSSEEvent(event: SSEEvent): void {
|
|
18
|
-
if (event.kind === "command.acked" && event.command) {
|
|
19
|
-
const callback = ackCallbacks.get(event.command.id);
|
|
20
|
-
if (callback) {
|
|
21
|
-
callback(event.command);
|
|
22
|
-
ackCallbacks.delete(event.command.id);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function subscribeToCommandAck(
|
|
28
|
-
commandId: string,
|
|
29
|
-
callback: (cmd: QueuedCommandRecord) => void
|
|
30
|
-
): () => void {
|
|
31
|
-
ackCallbacks.set(commandId, callback);
|
|
32
|
-
return () => { ackCallbacks.delete(commandId); };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Wait for command ACK via SSE push.
|
|
37
|
-
* Falls back to polling if SSE is not active.
|
|
38
|
-
*/
|
|
39
|
-
export async function waitForCommandAck(
|
|
40
|
-
config: ZhiHandConfig,
|
|
41
|
-
options: { commandId: string; timeoutMs?: number; signal?: AbortSignal }
|
|
42
|
-
): Promise<WaitForCommandAckResult> {
|
|
43
|
-
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
44
|
-
|
|
45
|
-
// Try SSE-based ACK first (if callbacks are being dispatched by an active SSE stream)
|
|
46
|
-
return new Promise<WaitForCommandAckResult>((resolve, reject) => {
|
|
47
|
-
let resolved = false;
|
|
48
|
-
let pollInterval: ReturnType<typeof setInterval> | undefined;
|
|
49
|
-
|
|
50
|
-
const timeout = setTimeout(() => {
|
|
51
|
-
cleanup();
|
|
52
|
-
resolve({ acked: false });
|
|
53
|
-
}, timeoutMs);
|
|
54
|
-
|
|
55
|
-
const unsubscribe = subscribeToCommandAck(options.commandId, (ackedCommand) => {
|
|
56
|
-
if (resolved) return;
|
|
57
|
-
resolved = true;
|
|
58
|
-
cleanup();
|
|
59
|
-
resolve({ acked: true, command: ackedCommand });
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
// Also poll as fallback (SSE may not be active)
|
|
63
|
-
pollInterval = setInterval(async () => {
|
|
64
|
-
if (resolved) return;
|
|
65
|
-
try {
|
|
66
|
-
const cmd = await getCommand(config, options.commandId);
|
|
67
|
-
if (cmd.acked_at) {
|
|
68
|
-
resolved = true;
|
|
69
|
-
cleanup();
|
|
70
|
-
resolve({ acked: true, command: cmd });
|
|
71
|
-
}
|
|
72
|
-
} catch {
|
|
73
|
-
// Polling failure is non-fatal; SSE or next poll may succeed
|
|
74
|
-
}
|
|
75
|
-
}, 500);
|
|
76
|
-
|
|
77
|
-
options.signal?.addEventListener("abort", () => {
|
|
78
|
-
cleanup();
|
|
79
|
-
reject(new Error("The operation was aborted"));
|
|
80
|
-
}, { once: true });
|
|
81
|
-
|
|
82
|
-
function cleanup() {
|
|
83
|
-
clearTimeout(timeout);
|
|
84
|
-
unsubscribe();
|
|
85
|
-
if (pollInterval) clearInterval(pollInterval);
|
|
86
|
-
}
|
|
87
|
-
});
|
|
88
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
-
import { z } from "zod";
|
|
4
|
-
|
|
5
|
-
import { resolveConfig } from "./core/config.ts";
|
|
6
|
-
import { controlSchema, screenshotSchema, pairSchema } from "./tools/schemas.ts";
|
|
7
|
-
import { executeControl } from "./tools/control.ts";
|
|
8
|
-
import { handleScreenshot } from "./tools/screenshot.ts";
|
|
9
|
-
import { handlePair } from "./tools/pair.ts";
|
|
10
|
-
|
|
11
|
-
const PACKAGE_VERSION = "0.11.0";
|
|
12
|
-
|
|
13
|
-
export function createServer(deviceName?: string): McpServer {
|
|
14
|
-
const server = new McpServer({
|
|
15
|
-
name: "zhihand",
|
|
16
|
-
version: PACKAGE_VERSION,
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
// zhihand_control — main phone control tool
|
|
20
|
-
server.tool("zhihand_control", controlSchema, async (params) => {
|
|
21
|
-
const config = resolveConfig(deviceName);
|
|
22
|
-
return await executeControl(config, params);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
// zhihand_screenshot — capture current screen without any action
|
|
26
|
-
server.tool("zhihand_screenshot", screenshotSchema, async () => {
|
|
27
|
-
const config = resolveConfig(deviceName);
|
|
28
|
-
return await handleScreenshot(config);
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
// zhihand_pair — device pairing
|
|
32
|
-
server.tool("zhihand_pair", pairSchema, async (params) => {
|
|
33
|
-
return await handlePair(params);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
return server;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export async function startStdioServer(deviceName?: string): Promise<void> {
|
|
40
|
-
const server = createServer(deviceName);
|
|
41
|
-
const transport = new StdioServerTransport();
|
|
42
|
-
await server.connect(transport);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Direct execution: start stdio server
|
|
46
|
-
const isDirectRun = process.argv[1]?.endsWith("index.ts") || process.argv[1]?.endsWith("index.js");
|
|
47
|
-
if (isDirectRun) {
|
|
48
|
-
const deviceArg = process.argv.find((a) => a.startsWith("--device="))?.split("=")[1];
|
|
49
|
-
startStdioServer(deviceArg ?? process.env.ZHIHAND_DEVICE).catch((err) => {
|
|
50
|
-
process.stderr.write(`ZhiHand MCP Server failed: ${err.message}\n`);
|
|
51
|
-
process.exit(1);
|
|
52
|
-
});
|
|
53
|
-
}
|
package/src/openclaw.adapter.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenClaw Plugin adapter — thin wrapper that bridges OpenClaw Plugin API
|
|
3
|
-
* to MCP core logic. All business logic lives in core/ and tools/.
|
|
4
|
-
*/
|
|
5
|
-
import { resolveConfig } from "./core/config.ts";
|
|
6
|
-
import { executeControl } from "./tools/control.ts";
|
|
7
|
-
import { handleScreenshot } from "./tools/screenshot.ts";
|
|
8
|
-
import { handlePair } from "./tools/pair.ts";
|
|
9
|
-
import { detectCLITools, formatDetectedTools } from "./cli/detect.ts";
|
|
10
|
-
import { controlSchema, screenshotSchema, pairSchema } from "./tools/schemas.ts";
|
|
11
|
-
|
|
12
|
-
type OpenClawLogger = {
|
|
13
|
-
info?: (message: string) => void;
|
|
14
|
-
warn?: (message: string) => void;
|
|
15
|
-
error?: (message: string) => void;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type OpenClawRuntime = {
|
|
19
|
-
state: { resolveStateDir: () => string };
|
|
20
|
-
stt?: { transcribeAudioFile: (input: { path: string }) => Promise<{ text?: string } | string> };
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type OpenClawToolRegistration = {
|
|
24
|
-
name: string;
|
|
25
|
-
label: string;
|
|
26
|
-
description: string;
|
|
27
|
-
parameters: Record<string, unknown>;
|
|
28
|
-
execute: (id: string, params: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
type OpenClawPluginApi = {
|
|
32
|
-
logger: OpenClawLogger;
|
|
33
|
-
runtime: OpenClawRuntime;
|
|
34
|
-
pluginConfig?: Record<string, unknown>;
|
|
35
|
-
registerService: (service: { id: string; start: () => Promise<void>; stop: () => Promise<void> }) => void;
|
|
36
|
-
registerCommand: (command: {
|
|
37
|
-
name: string;
|
|
38
|
-
description: string;
|
|
39
|
-
acceptsArgs?: boolean;
|
|
40
|
-
handler: (ctx: { args?: string }) => Promise<{ text: string }>;
|
|
41
|
-
}) => void;
|
|
42
|
-
registerTool: (tool: OpenClawToolRegistration, options?: { optional?: boolean }) => void;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function zodSchemaToJsonSchema(zodShape: Record<string, unknown>): Record<string, unknown> {
|
|
46
|
-
// Simplified conversion — OpenClaw uses JSON Schema-like parameter objects.
|
|
47
|
-
// The actual Zod schemas are used for validation inside tool handlers.
|
|
48
|
-
const properties: Record<string, unknown> = {};
|
|
49
|
-
for (const [key, value] of Object.entries(zodShape)) {
|
|
50
|
-
const v = value as { description?: string; _def?: { typeName?: string } };
|
|
51
|
-
properties[key] = {
|
|
52
|
-
type: "string",
|
|
53
|
-
description: v.description ?? key,
|
|
54
|
-
};
|
|
55
|
-
}
|
|
56
|
-
return { type: "object", properties };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function registerOpenClawTools(api: OpenClawPluginApi, deviceName?: string): void {
|
|
60
|
-
const log = (msg: string) => api.logger.info?.(msg);
|
|
61
|
-
|
|
62
|
-
// zhihand_control
|
|
63
|
-
api.registerTool({
|
|
64
|
-
name: "zhihand_control",
|
|
65
|
-
label: "ZhiHand Control",
|
|
66
|
-
description: "Control a paired phone: tap, swipe, type, scroll, screenshot, and more.",
|
|
67
|
-
parameters: zodSchemaToJsonSchema(controlSchema),
|
|
68
|
-
execute: async (_id, params) => {
|
|
69
|
-
const config = resolveConfig(deviceName);
|
|
70
|
-
const result = await executeControl(config, params as Parameters<typeof executeControl>[1]);
|
|
71
|
-
return result as unknown as Record<string, unknown>;
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// zhihand_screenshot
|
|
76
|
-
api.registerTool({
|
|
77
|
-
name: "zhihand_screenshot",
|
|
78
|
-
label: "ZhiHand Screenshot",
|
|
79
|
-
description: "Capture current phone screen without performing any action.",
|
|
80
|
-
parameters: zodSchemaToJsonSchema(screenshotSchema),
|
|
81
|
-
execute: async (_id, _params) => {
|
|
82
|
-
const config = resolveConfig(deviceName);
|
|
83
|
-
const result = await handleScreenshot(config);
|
|
84
|
-
return result as unknown as Record<string, unknown>;
|
|
85
|
-
},
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// zhihand_pair
|
|
89
|
-
api.registerTool(
|
|
90
|
-
{
|
|
91
|
-
name: "zhihand_pair",
|
|
92
|
-
label: "ZhiHand Pair",
|
|
93
|
-
description: "Pair with a phone. Returns QR code and pairing URL.",
|
|
94
|
-
parameters: zodSchemaToJsonSchema(pairSchema),
|
|
95
|
-
execute: async (_id, params) => {
|
|
96
|
-
const result = await handlePair(params as { forceNew?: boolean });
|
|
97
|
-
return result as unknown as Record<string, unknown>;
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
{ optional: true },
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
// detect command
|
|
104
|
-
api.registerCommand({
|
|
105
|
-
name: "zhihand-detect",
|
|
106
|
-
description: "Detect available CLI tools (Claude Code, Codex, Gemini, OpenClaw)",
|
|
107
|
-
handler: async () => {
|
|
108
|
-
const tools = await detectCLITools();
|
|
109
|
-
return { text: formatDetectedTools(tools) };
|
|
110
|
-
},
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
log("[zhihand] OpenClaw tools registered via MCP core adapter");
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export default registerOpenClawTools;
|
package/src/tools/control.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import type { ZhiHandConfig } from "../core/config.ts";
|
|
2
|
-
import { createControlCommand, enqueueCommand, formatAckSummary } from "../core/command.ts";
|
|
3
|
-
import type { ControlParams } from "../core/command.ts";
|
|
4
|
-
import { fetchScreenshotBinary } from "../core/screenshot.ts";
|
|
5
|
-
import { waitForCommandAck } from "../core/sse.ts";
|
|
6
|
-
|
|
7
|
-
function sleep(ms: number): Promise<void> {
|
|
8
|
-
return new Promise((r) => setTimeout(r, ms));
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export async function executeControl(
|
|
12
|
-
config: ZhiHandConfig,
|
|
13
|
-
params: ControlParams
|
|
14
|
-
): Promise<{ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> }> {
|
|
15
|
-
// wait: Plugin-local implementation, no server round-trip
|
|
16
|
-
if (params.action === "wait") {
|
|
17
|
-
await sleep(params.durationMs ?? 1000);
|
|
18
|
-
const screenshot = await fetchScreenshotBinary(config);
|
|
19
|
-
return {
|
|
20
|
-
content: [
|
|
21
|
-
{ type: "text", text: `Waited ${params.durationMs ?? 1000}ms` },
|
|
22
|
-
{ type: "image", data: screenshot.toString("base64"), mimeType: "image/jpeg" },
|
|
23
|
-
],
|
|
24
|
-
};
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
// screenshot: send receive_screenshot, App captures immediately (no 2s delay)
|
|
28
|
-
if (params.action === "screenshot") {
|
|
29
|
-
return await executeScreenshot(config);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// HID operations: enqueue → ACK → GET screenshot
|
|
33
|
-
const command = createControlCommand(params);
|
|
34
|
-
const queued = await enqueueCommand(config, command);
|
|
35
|
-
const ack = await waitForCommandAck(config, { commandId: queued.id, timeoutMs: 15_000 });
|
|
36
|
-
|
|
37
|
-
const content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> = [
|
|
38
|
-
{ type: "text", text: formatAckSummary(params.action, ack) },
|
|
39
|
-
];
|
|
40
|
-
|
|
41
|
-
if (ack.acked) {
|
|
42
|
-
try {
|
|
43
|
-
const screenshot = await fetchScreenshotBinary(config);
|
|
44
|
-
content.push({ type: "image", data: screenshot.toString("base64"), mimeType: "image/jpeg" });
|
|
45
|
-
} catch {
|
|
46
|
-
// Screenshot is best-effort after ACK
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return { content };
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function executeScreenshot(
|
|
54
|
-
config: ZhiHandConfig
|
|
55
|
-
): Promise<{ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> }> {
|
|
56
|
-
const command = createControlCommand({ action: "screenshot" });
|
|
57
|
-
const queued = await enqueueCommand(config, command);
|
|
58
|
-
const ack = await waitForCommandAck(config, { commandId: queued.id, timeoutMs: 5_000 });
|
|
59
|
-
const screenshot = await fetchScreenshotBinary(config);
|
|
60
|
-
return {
|
|
61
|
-
content: [
|
|
62
|
-
{ type: "text", text: `Screenshot captured (acked: ${ack.acked})` },
|
|
63
|
-
{ type: "image", data: screenshot.toString("base64"), mimeType: "image/jpeg" },
|
|
64
|
-
],
|
|
65
|
-
};
|
|
66
|
-
}
|
package/src/tools/pair.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { loadDefaultCredential } from "../core/config.ts";
|
|
2
|
-
import {
|
|
3
|
-
createPairingSession,
|
|
4
|
-
renderPairingQRCode,
|
|
5
|
-
formatPairingStatus,
|
|
6
|
-
} from "../core/pair.ts";
|
|
7
|
-
|
|
8
|
-
const DEFAULT_ENDPOINT = "https://api.zhihand.com";
|
|
9
|
-
const DEFAULT_EDGE_ID_PREFIX = "mcp-";
|
|
10
|
-
|
|
11
|
-
function generateEdgeId(): string {
|
|
12
|
-
return `${DEFAULT_EDGE_ID_PREFIX}${Date.now().toString(36)}`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export async function handlePair(
|
|
16
|
-
params: { forceNew?: boolean },
|
|
17
|
-
endpoint?: string
|
|
18
|
-
): Promise<{ content: Array<{ type: string; text?: string }> }> {
|
|
19
|
-
const resolvedEndpoint = endpoint ?? DEFAULT_ENDPOINT;
|
|
20
|
-
|
|
21
|
-
// Check existing credential
|
|
22
|
-
if (!params.forceNew) {
|
|
23
|
-
const existing = loadDefaultCredential();
|
|
24
|
-
if (existing) {
|
|
25
|
-
return {
|
|
26
|
-
content: [
|
|
27
|
-
{ type: "text", text: formatPairingStatus(existing) },
|
|
28
|
-
],
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Create new pairing session
|
|
34
|
-
const session = await createPairingSession(resolvedEndpoint, {
|
|
35
|
-
edgeId: generateEdgeId(),
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
const qr = await renderPairingQRCode(session.pair_url);
|
|
39
|
-
|
|
40
|
-
return {
|
|
41
|
-
content: [
|
|
42
|
-
{
|
|
43
|
-
type: "text",
|
|
44
|
-
text: [
|
|
45
|
-
"Scan QR code or open URL on your phone to pair:",
|
|
46
|
-
"",
|
|
47
|
-
qr,
|
|
48
|
-
"",
|
|
49
|
-
`URL: ${session.pair_url}`,
|
|
50
|
-
`Expires at: ${session.expires_at}`,
|
|
51
|
-
"",
|
|
52
|
-
"Waiting for phone to scan...",
|
|
53
|
-
"(Call zhihand_pair again after scanning to check status)",
|
|
54
|
-
].join("\n"),
|
|
55
|
-
},
|
|
56
|
-
],
|
|
57
|
-
};
|
|
58
|
-
}
|
package/src/tools/schemas.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
|
|
3
|
-
export const controlSchema = {
|
|
4
|
-
action: z.enum([
|
|
5
|
-
"click", "doubleclick", "rightclick", "middleclick",
|
|
6
|
-
"type", "swipe", "scroll", "keycombo",
|
|
7
|
-
"clipboard",
|
|
8
|
-
"wait", "screenshot",
|
|
9
|
-
]),
|
|
10
|
-
xRatio: z.number().min(0).max(1).optional().describe("Normalized horizontal position [0,1]"),
|
|
11
|
-
yRatio: z.number().min(0).max(1).optional().describe("Normalized vertical position [0,1]"),
|
|
12
|
-
text: z.string().optional().describe("Text for type or clipboard set"),
|
|
13
|
-
direction: z.enum(["up", "down", "left", "right"]).optional().describe("Scroll direction"),
|
|
14
|
-
amount: z.number().int().positive().default(3).optional().describe("Scroll steps (default 3)"),
|
|
15
|
-
keys: z.string().optional().describe("Key combo string, e.g. 'ctrl+c', 'alt+tab'"),
|
|
16
|
-
clipboardAction: z.enum(["get", "set"]).optional().describe("Clipboard action"),
|
|
17
|
-
durationMs: z.number().int().positive().max(10000).default(1000).optional().describe("Duration in ms for wait (default 1000, max 10000)"),
|
|
18
|
-
startXRatio: z.number().min(0).max(1).optional().describe("Swipe start X [0,1]"),
|
|
19
|
-
startYRatio: z.number().min(0).max(1).optional().describe("Swipe start Y [0,1]"),
|
|
20
|
-
endXRatio: z.number().min(0).max(1).optional().describe("Swipe end X [0,1]"),
|
|
21
|
-
endYRatio: z.number().min(0).max(1).optional().describe("Swipe end Y [0,1]"),
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
export const screenshotSchema = {};
|
|
25
|
-
|
|
26
|
-
export const pairSchema = {
|
|
27
|
-
forceNew: z.boolean().default(false).optional().describe("Force new pairing even if already paired"),
|
|
28
|
-
};
|
package/src/tools/screenshot.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import type { ZhiHandConfig } from "../core/config.ts";
|
|
2
|
-
import { executeScreenshot } from "./control.ts";
|
|
3
|
-
|
|
4
|
-
export async function handleScreenshot(
|
|
5
|
-
config: ZhiHandConfig
|
|
6
|
-
): Promise<{ content: Array<{ type: string; text?: string; data?: string; mimeType?: string }> }> {
|
|
7
|
-
return await executeScreenshot(config);
|
|
8
|
-
}
|