@zhihand/mcp 0.12.0 → 0.12.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.
Files changed (46) hide show
  1. package/README.md +288 -0
  2. package/bin/zhihand +6 -6
  3. package/bin/zhihand.openclaw +2 -2
  4. package/dist/cli/detect.d.ts +10 -0
  5. package/dist/cli/detect.js +75 -0
  6. package/dist/cli/openclaw.d.ts +6 -0
  7. package/dist/cli/openclaw.js +47 -0
  8. package/dist/cli/spawn.d.ts +2 -0
  9. package/dist/cli/spawn.js +31 -0
  10. package/dist/core/command.d.ts +41 -0
  11. package/dist/core/command.js +84 -0
  12. package/dist/core/config.d.ts +26 -0
  13. package/dist/core/config.js +67 -0
  14. package/dist/core/pair.d.ts +45 -0
  15. package/dist/core/pair.js +124 -0
  16. package/dist/core/screenshot.d.ts +2 -0
  17. package/dist/core/screenshot.js +21 -0
  18. package/dist/core/sse.d.ts +35 -0
  19. package/dist/core/sse.js +149 -0
  20. package/dist/index.d.ts +3 -0
  21. package/dist/index.js +43 -0
  22. package/dist/openclaw.adapter.d.ts +49 -0
  23. package/dist/openclaw.adapter.js +72 -0
  24. package/dist/tools/control.d.ts +18 -0
  25. package/dist/tools/control.js +52 -0
  26. package/dist/tools/pair.d.ts +8 -0
  27. package/dist/tools/pair.js +49 -0
  28. package/dist/tools/schemas.d.ts +20 -0
  29. package/dist/tools/schemas.js +25 -0
  30. package/dist/tools/screenshot.d.ts +11 -0
  31. package/dist/tools/screenshot.js +4 -0
  32. package/package.json +13 -5
  33. package/src/cli/detect.ts +0 -90
  34. package/src/cli/openclaw.ts +0 -50
  35. package/src/cli/spawn.ts +0 -34
  36. package/src/core/command.ts +0 -144
  37. package/src/core/config.ts +0 -91
  38. package/src/core/pair.ts +0 -143
  39. package/src/core/screenshot.ts +0 -28
  40. package/src/core/sse.ts +0 -88
  41. package/src/index.ts +0 -53
  42. package/src/openclaw.adapter.ts +0 -116
  43. package/src/tools/control.ts +0 -66
  44. package/src/tools/pair.ts +0 -58
  45. package/src/tools/schemas.ts +0 -28
  46. package/src/tools/screenshot.ts +0 -8
package/README.md ADDED
@@ -0,0 +1,288 @@
1
+ # @zhihand/mcp
2
+
3
+ ZhiHand MCP Server — let AI agents see and control your phone.
4
+
5
+ Version: `0.12.1`
6
+
7
+ ## What is this?
8
+
9
+ `@zhihand/mcp` is the core integration layer for ZhiHand. It provides an [MCP (Model Context Protocol)](https://modelcontextprotocol.io/) server that exposes phone control tools to any compatible AI agent, including:
10
+
11
+ - **Claude Code**
12
+ - **Codex CLI**
13
+ - **Gemini CLI**
14
+ - **OpenClaw**
15
+
16
+ One npm package, two entry points:
17
+
18
+ | Entry | Purpose |
19
+ |---|---|
20
+ | `zhihand serve` | MCP Server (stdio) — used by Claude Code, Codex, Gemini CLI |
21
+ | `zhihand.openclaw` | OpenClaw Plugin entry — thin wrapper calling the same core |
22
+
23
+ ## Requirements
24
+
25
+ - **Node.js >= 22**
26
+ - A **ZhiHand mobile app** (Android or iOS) installed on your phone
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ npm install -g @zhihand/mcp
32
+ ```
33
+
34
+ Or use directly with `npx`:
35
+
36
+ ```bash
37
+ npx @zhihand/mcp serve
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### 1. Pair your phone
43
+
44
+ ```bash
45
+ zhihand setup
46
+ ```
47
+
48
+ This runs the full interactive setup:
49
+
50
+ 1. Registers as a plugin with the ZhiHand server
51
+ 2. Creates a pairing session and displays a QR code in the terminal
52
+ 3. Waits for you to scan the QR code with the ZhiHand mobile app
53
+ 4. Saves credentials to `~/.zhihand/credentials.json`
54
+ 5. Detects installed CLI tools (Claude Code, Codex, Gemini CLI, OpenClaw)
55
+ 6. Prints the MCP configuration snippet for your tools
56
+
57
+ ### 2. Configure your AI tool
58
+
59
+ Add the ZhiHand MCP server to your tool's configuration:
60
+
61
+ **Claude Code** — Add to `.mcp.json` in your project root, or run:
62
+
63
+ ```bash
64
+ claude mcp add zhihand -- zhihand serve
65
+ ```
66
+
67
+ Or manually create/edit `.mcp.json`:
68
+
69
+ ```json
70
+ {
71
+ "mcpServers": {
72
+ "zhihand": {
73
+ "command": "zhihand",
74
+ "args": ["serve"]
75
+ }
76
+ }
77
+ }
78
+ ```
79
+
80
+ **Codex CLI** — Add to your MCP config:
81
+
82
+ ```json
83
+ {
84
+ "mcpServers": {
85
+ "zhihand": {
86
+ "command": "zhihand",
87
+ "args": ["serve"]
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ **Gemini CLI** — Add to `~/.gemini/settings.json`:
94
+
95
+ ```json
96
+ {
97
+ "mcpServers": {
98
+ "zhihand": {
99
+ "command": "zhihand",
100
+ "args": ["serve"]
101
+ }
102
+ }
103
+ }
104
+ ```
105
+
106
+ **OpenClaw** — Install the plugin directly:
107
+
108
+ ```bash
109
+ openclaw plugins install @zhihand/mcp
110
+ ```
111
+
112
+ ### 3. Start using it
113
+
114
+ Once configured, your AI agent can use ZhiHand tools directly. For example, in Claude Code:
115
+
116
+ ```
117
+ > Take a screenshot of my phone
118
+ > Tap on the Settings icon
119
+ > Type "hello world" into the search box
120
+ > Scroll down to find the About section
121
+ ```
122
+
123
+ ## CLI Commands
124
+
125
+ ```
126
+ zhihand serve Start MCP Server (stdio mode, called by AI tools)
127
+ zhihand setup Interactive setup: pair + detect tools + print config
128
+ zhihand pair Pair with a phone (QR code in terminal)
129
+ zhihand status Show current pairing status and device info
130
+ zhihand detect List detected CLI tools and their login status
131
+ zhihand --help Show help
132
+ ```
133
+
134
+ ### Options
135
+
136
+ | Option | Description |
137
+ |---|---|
138
+ | `--device <name>` | Use a specific paired device (if you have multiple) |
139
+ | `-h, --help` | Show help |
140
+
141
+ ### Environment Variables
142
+
143
+ | Variable | Description |
144
+ |---|---|
145
+ | `ZHIHAND_DEVICE` | Default device name (same as `--device`) |
146
+ | `ZHIHAND_CLI` | Override CLI tool selection for mobile-initiated tasks |
147
+
148
+ ## MCP Tools
149
+
150
+ The server exposes three tools to AI agents:
151
+
152
+ ### `zhihand_control`
153
+
154
+ The main phone control tool. Supports these actions:
155
+
156
+ | Action | Parameters | Description |
157
+ |---|---|---|
158
+ | `click` | `xRatio`, `yRatio` | Tap at normalized coordinates [0,1] |
159
+ | `doubleclick` | `xRatio`, `yRatio` | Double-tap |
160
+ | `rightclick` | `xRatio`, `yRatio` | Right-click (long press) |
161
+ | `middleclick` | `xRatio`, `yRatio` | Middle-click |
162
+ | `type` | `text` | Type text into the focused field |
163
+ | `swipe` | `startXRatio`, `startYRatio`, `endXRatio`, `endYRatio` | Swipe gesture |
164
+ | `scroll` | `xRatio`, `yRatio`, `direction`, `amount` | Scroll up/down/left/right |
165
+ | `keycombo` | `keys` | Key combination (e.g. `"ctrl+c"`, `"alt+tab"`) |
166
+ | `clipboard` | `clipboardAction` (`get`/`set`), `text` | Read or write clipboard |
167
+ | `wait` | `durationMs` | Wait (local sleep, no server round-trip) |
168
+ | `screenshot` | — | Capture screen immediately |
169
+
170
+ Coordinates use **normalized ratios** (0.0 to 1.0), where `(0, 0)` is the top-left corner and `(1, 1)` is the bottom-right. This works across any screen resolution.
171
+
172
+ Every action returns a text summary and a screenshot of the result.
173
+
174
+ ### `zhihand_screenshot`
175
+
176
+ Capture the current phone screen without performing any action. Returns an image.
177
+
178
+ No parameters required.
179
+
180
+ ### `zhihand_pair`
181
+
182
+ Pair with a phone device. Returns a QR code and pairing URL.
183
+
184
+ | Parameter | Type | Description |
185
+ |---|---|---|
186
+ | `forceNew` | `boolean` | Force new pairing even if already paired (default: `false`) |
187
+
188
+ ## How It Works
189
+
190
+ ```
191
+ AI Agent ←stdio→ zhihand serve (MCP Server)
192
+
193
+ ├── POST /v1/plugins Register plugin
194
+ ├── POST /v1/pairing/sessions Create pairing
195
+ ├── POST /v1/credentials/{id}/commands Send command
196
+ ├── GET /v1/credentials/{id}/commands/{cid} Poll ACK
197
+ ├── SSE /v1/credentials/{id}/events?topic=commands Real-time ACK
198
+ └── GET /v1/credentials/{id}/screen Fetch screenshot (JPEG)
199
+
200
+ ZhiHand Server
201
+
202
+ ZhiHand Mobile App
203
+ ```
204
+
205
+ 1. AI agent calls a tool (e.g. `zhihand_control` with `action: "click"`)
206
+ 2. MCP Server translates to a device command and enqueues it via the ZhiHand API
207
+ 3. Mobile app picks up the command, executes it, and sends an ACK
208
+ 4. MCP Server receives the ACK (via SSE or polling fallback)
209
+ 5. MCP Server fetches a fresh screenshot and returns it to the AI agent
210
+
211
+ Screenshots are transferred as raw JPEG binary and only base64-encoded at the LLM API boundary, minimizing bandwidth.
212
+
213
+ ## Credential Storage
214
+
215
+ Pairing credentials are stored at:
216
+
217
+ ```
218
+ ~/.zhihand/
219
+ ├── credentials.json # Device credentials (credentialId, controllerToken, endpoint)
220
+ └── state.json # Current pairing session state
221
+ ```
222
+
223
+ You can manage multiple devices. The `credentials.json` file stores a `default` device name and a `devices` map:
224
+
225
+ ```json
226
+ {
227
+ "default": "mcp-myhost",
228
+ "devices": {
229
+ "mcp-myhost": {
230
+ "credentialId": "cred_abc123",
231
+ "controllerToken": "tok_...",
232
+ "endpoint": "https://api.zhihand.com",
233
+ "deviceName": "mcp-myhost",
234
+ "pairedAt": "2026-04-01T00:00:00.000Z"
235
+ }
236
+ }
237
+ }
238
+ ```
239
+
240
+ ## Architecture
241
+
242
+ ```
243
+ packages/mcp/
244
+ ├── bin/
245
+ │ ├── zhihand # Main CLI entry (serve/setup/pair/status/detect)
246
+ │ └── zhihand.openclaw # OpenClaw plugin entry
247
+ ├── src/
248
+ │ ├── index.ts # MCP Server (stdio transport)
249
+ │ ├── openclaw.adapter.ts # OpenClaw Plugin adapter (thin wrapper)
250
+ │ ├── core/
251
+ │ │ ├── config.ts # Credential & config management (~/.zhihand/)
252
+ │ │ ├── command.ts # Command creation, enqueue, ACK formatting
253
+ │ │ ├── screenshot.ts # Binary screenshot fetch (JPEG)
254
+ │ │ ├── sse.ts # SSE client + hybrid ACK (SSE push + polling fallback)
255
+ │ │ └── pair.ts # Plugin registration + device pairing flow
256
+ │ ├── tools/
257
+ │ │ ├── schemas.ts # Zod parameter schemas
258
+ │ │ ├── control.ts # zhihand_control handler
259
+ │ │ ├── screenshot.ts # zhihand_screenshot handler
260
+ │ │ └── pair.ts # zhihand_pair handler
261
+ │ └── cli/
262
+ │ ├── detect.ts # CLI tool detection (Claude Code, Codex, Gemini, OpenClaw)
263
+ │ ├── spawn.ts # CLI process spawning (for mobile-initiated tasks)
264
+ │ └── openclaw.ts # OpenClaw auto-detect & plugin install
265
+ ├── dist/ # Compiled JavaScript (shipped in npm package)
266
+ ├── package.json
267
+ └── tsconfig.json
268
+ ```
269
+
270
+ ## Development
271
+
272
+ ```bash
273
+ # Install dependencies
274
+ npm install
275
+
276
+ # Build (compiles TypeScript to dist/)
277
+ npm run build
278
+
279
+ # Run in development mode (uses --experimental-strip-types)
280
+ npm run dev
281
+
282
+ # Run tests
283
+ npm test
284
+ ```
285
+
286
+ ## License
287
+
288
+ MIT
package/bin/zhihand CHANGED
@@ -1,12 +1,12 @@
1
- #!/usr/bin/env node --experimental-strip-types
1
+ #!/usr/bin/env node
2
2
 
3
3
  import os from "node:os";
4
4
  import { parseArgs } from "node:util";
5
- import { startStdioServer } from "../src/index.ts";
6
- import { detectCLITools, formatDetectedTools } from "../src/cli/detect.ts";
7
- import { detectAndSetupOpenClaw } from "../src/cli/openclaw.ts";
8
- import { loadDefaultCredential } from "../src/core/config.ts";
9
- import { executePairing } from "../src/core/pair.ts";
5
+ import { startStdioServer } from "../dist/index.js";
6
+ import { detectCLITools, formatDetectedTools } from "../dist/cli/detect.js";
7
+ import { detectAndSetupOpenClaw } from "../dist/cli/openclaw.js";
8
+ import { loadDefaultCredential } from "../dist/core/config.js";
9
+ import { executePairing } from "../dist/core/pair.js";
10
10
 
11
11
  const DEFAULT_ENDPOINT = "https://api.zhihand.com";
12
12
 
@@ -1,11 +1,11 @@
1
- #!/usr/bin/env node --experimental-strip-types
1
+ #!/usr/bin/env node
2
2
 
3
3
  /**
4
4
  * OpenClaw Plugin entry point.
5
5
  * This script is invoked by OpenClaw when the zhihand plugin is loaded.
6
6
  * It bridges the OpenClaw Plugin API to MCP core logic via the adapter.
7
7
  */
8
- import { registerOpenClawTools } from "../src/openclaw.adapter.ts";
8
+ import { registerOpenClawTools } from "../dist/openclaw.adapter.js";
9
9
 
10
10
  // OpenClaw injects the plugin API as the default export's argument
11
11
  export default function activate(api) {
@@ -0,0 +1,10 @@
1
+ export interface CLITool {
2
+ name: "claudecode" | "codex" | "gemini" | "openclaw";
3
+ command: string;
4
+ version: string;
5
+ loggedIn: boolean;
6
+ priority: number;
7
+ }
8
+ export declare function detectCLITools(): Promise<CLITool[]>;
9
+ export declare function detectBestCLI(): Promise<CLITool | null>;
10
+ export declare function formatDetectedTools(tools: CLITool[]): string;
@@ -0,0 +1,75 @@
1
+ import { execSync } from "node:child_process";
2
+ function tryExec(cmd) {
3
+ try {
4
+ return execSync(cmd, { encoding: "utf8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
5
+ }
6
+ catch {
7
+ return null;
8
+ }
9
+ }
10
+ function isCommandAvailable(cmd) {
11
+ return tryExec(`which ${cmd}`) !== null;
12
+ }
13
+ async function detectClaudeCode() {
14
+ if (!isCommandAvailable("claude"))
15
+ return null;
16
+ const version = tryExec("claude --version") ?? "unknown";
17
+ // Check login: claude has config in ~/.claude/
18
+ const loggedIn = tryExec("ls ~/.claude/settings.json") !== null;
19
+ return { name: "claudecode", command: "claude", version, loggedIn, priority: 1 };
20
+ }
21
+ async function detectCodex() {
22
+ if (!isCommandAvailable("codex"))
23
+ return null;
24
+ const version = tryExec("codex --version") ?? "unknown";
25
+ // Check login: OPENAI_API_KEY env var or config
26
+ const loggedIn = !!process.env.OPENAI_API_KEY || tryExec("ls ~/.codex/") !== null;
27
+ return { name: "codex", command: "codex", version, loggedIn, priority: 2 };
28
+ }
29
+ async function detectGemini() {
30
+ if (!isCommandAvailable("gemini"))
31
+ return null;
32
+ const version = tryExec("gemini --version") ?? "unknown";
33
+ // Check login: Google Cloud auth
34
+ const loggedIn = tryExec("gemini auth status") !== null;
35
+ return { name: "gemini", command: "gemini", version, loggedIn, priority: 3 };
36
+ }
37
+ async function detectOpenClaw() {
38
+ if (!isCommandAvailable("openclaw"))
39
+ return null;
40
+ const version = tryExec("openclaw --version") ?? "unknown";
41
+ const loggedIn = tryExec("ls ~/.openclaw/openclaw.json") !== null;
42
+ return { name: "openclaw", command: "openclaw", version, loggedIn, priority: 4 };
43
+ }
44
+ export async function detectCLITools() {
45
+ const results = await Promise.allSettled([
46
+ detectClaudeCode(),
47
+ detectCodex(),
48
+ detectGemini(),
49
+ detectOpenClaw(),
50
+ ]);
51
+ return results
52
+ .filter((r) => r.status === "fulfilled")
53
+ .map((r) => r.value)
54
+ .filter((t) => t !== null)
55
+ .sort((a, b) => a.priority - b.priority);
56
+ }
57
+ export async function detectBestCLI() {
58
+ const cliOverride = process.env.ZHIHAND_CLI;
59
+ const tools = await detectCLITools();
60
+ if (cliOverride) {
61
+ const match = tools.find((t) => t.name === cliOverride || t.command === cliOverride);
62
+ if (match)
63
+ return match;
64
+ }
65
+ // Return best available tool (logged in + highest priority)
66
+ return tools.find((t) => t.loggedIn) ?? tools[0] ?? null;
67
+ }
68
+ export function formatDetectedTools(tools) {
69
+ if (tools.length === 0)
70
+ return "No CLI tools detected.";
71
+ return [
72
+ "Detected CLI tools:",
73
+ ...tools.map((t) => ` ${t.loggedIn ? "✓" : "✗"} ${t.name} (${t.command} ${t.version})${t.loggedIn ? "" : " — not logged in"}`),
74
+ ].join("\n");
75
+ }
@@ -0,0 +1,6 @@
1
+ export declare function isZhiHandPluginInstalled(): Promise<boolean>;
2
+ export declare function installZhiHandPlugin(options?: {
3
+ timeoutMs?: number;
4
+ autoConfirm?: boolean;
5
+ }): Promise<boolean>;
6
+ export declare function detectAndSetupOpenClaw(): Promise<void>;
@@ -0,0 +1,47 @@
1
+ import { execSync } from "node:child_process";
2
+ function tryExec(cmd) {
3
+ try {
4
+ return execSync(cmd, { encoding: "utf8", timeout: 30_000, stdio: ["pipe", "pipe", "pipe"] }).trim();
5
+ }
6
+ catch {
7
+ return null;
8
+ }
9
+ }
10
+ function isCommandAvailable(cmd) {
11
+ return tryExec(`which ${cmd}`) !== null;
12
+ }
13
+ export async function isZhiHandPluginInstalled() {
14
+ const output = tryExec("openclaw plugins list");
15
+ if (!output)
16
+ return false;
17
+ return output.includes("zhihand") || output.includes("@zhihand/mcp");
18
+ }
19
+ export async function installZhiHandPlugin(options = {}) {
20
+ const timeout = options.timeoutMs ?? 30_000;
21
+ try {
22
+ execSync("openclaw plugins install @zhihand/mcp", {
23
+ encoding: "utf8",
24
+ timeout,
25
+ stdio: options.autoConfirm ? ["pipe", "pipe", "pipe"] : "inherit",
26
+ });
27
+ return true;
28
+ }
29
+ catch {
30
+ return false;
31
+ }
32
+ }
33
+ export async function detectAndSetupOpenClaw() {
34
+ if (!isCommandAvailable("openclaw"))
35
+ return;
36
+ const pluginInstalled = await isZhiHandPluginInstalled();
37
+ if (pluginInstalled)
38
+ return;
39
+ process.stderr.write("[zhihand] Detected OpenClaw without ZhiHand plugin. Installing...\n");
40
+ const success = await installZhiHandPlugin({ timeoutMs: 30_000, autoConfirm: true });
41
+ if (success) {
42
+ process.stderr.write("[zhihand] ZhiHand plugin installed to OpenClaw.\n");
43
+ }
44
+ else {
45
+ process.stderr.write("[zhihand] Failed to install ZhiHand plugin to OpenClaw.\n");
46
+ }
47
+ }
@@ -0,0 +1,2 @@
1
+ import type { CLITool } from "./detect.ts";
2
+ export declare function spawnCLITask(tool: CLITool, prompt: string): Promise<string>;
@@ -0,0 +1,31 @@
1
+ import { execSync } from "node:child_process";
2
+ function shellEscape(s) {
3
+ return `'${s.replace(/'/g, "'\\''")}'`;
4
+ }
5
+ export async function spawnCLITask(tool, prompt) {
6
+ const escaped = shellEscape(prompt);
7
+ switch (tool.name) {
8
+ case "claudecode":
9
+ return execSync(`${tool.command} -p ${escaped} --output-format json`, {
10
+ encoding: "utf8",
11
+ timeout: 300_000,
12
+ });
13
+ case "codex":
14
+ return execSync(`${tool.command} -q ${escaped} --json`, {
15
+ encoding: "utf8",
16
+ timeout: 300_000,
17
+ });
18
+ case "gemini":
19
+ return execSync(`${tool.command} -p ${escaped}`, {
20
+ encoding: "utf8",
21
+ timeout: 300_000,
22
+ });
23
+ case "openclaw":
24
+ return execSync(`${tool.command} run ${escaped}`, {
25
+ encoding: "utf8",
26
+ timeout: 300_000,
27
+ });
28
+ default:
29
+ throw new Error(`Unsupported CLI tool: ${tool.name}`);
30
+ }
31
+ }
@@ -0,0 +1,41 @@
1
+ import type { ZhiHandConfig } from "./config.ts";
2
+ export type ScrollDirection = "up" | "down" | "left" | "right";
3
+ export type ClipboardAction = "get" | "set";
4
+ export interface ControlParams {
5
+ action: string;
6
+ xRatio?: number;
7
+ yRatio?: number;
8
+ text?: string;
9
+ direction?: ScrollDirection;
10
+ amount?: number;
11
+ keys?: string;
12
+ clipboardAction?: ClipboardAction;
13
+ durationMs?: number;
14
+ startXRatio?: number;
15
+ startYRatio?: number;
16
+ endXRatio?: number;
17
+ endYRatio?: number;
18
+ }
19
+ export interface QueuedControlCommand {
20
+ type: string;
21
+ payload?: Record<string, unknown>;
22
+ messageId?: number;
23
+ }
24
+ export interface QueuedCommandRecord {
25
+ id: string;
26
+ credential_id: string;
27
+ status: string;
28
+ command: QueuedControlCommand;
29
+ created_at: string;
30
+ acked_at?: string;
31
+ ack_status?: string;
32
+ ack_result?: Record<string, unknown>;
33
+ }
34
+ export interface WaitForCommandAckResult {
35
+ acked: boolean;
36
+ command?: QueuedCommandRecord;
37
+ }
38
+ export declare function createControlCommand(params: ControlParams): QueuedControlCommand;
39
+ export declare function enqueueCommand(config: ZhiHandConfig, command: QueuedControlCommand): Promise<QueuedCommandRecord>;
40
+ export declare function getCommand(config: ZhiHandConfig, commandId: string): Promise<QueuedCommandRecord>;
41
+ export declare function formatAckSummary(action: string, result: WaitForCommandAckResult): string;
@@ -0,0 +1,84 @@
1
+ let messageCounter = 0;
2
+ function nextMessageId() {
3
+ messageCounter = (messageCounter + 1) % 1000;
4
+ return (Date.now() * 1000) + messageCounter;
5
+ }
6
+ export function createControlCommand(params) {
7
+ switch (params.action) {
8
+ case "click":
9
+ return { type: "receive_click", payload: { x: params.xRatio, y: params.yRatio } };
10
+ case "doubleclick":
11
+ return { type: "receive_doubleclick", payload: { x: params.xRatio, y: params.yRatio } };
12
+ case "rightclick":
13
+ return { type: "receive_rightclick", payload: { x: params.xRatio, y: params.yRatio } };
14
+ case "middleclick":
15
+ return { type: "receive_middleclick", payload: { x: params.xRatio, y: params.yRatio } };
16
+ case "type":
17
+ return { type: "receive_type", payload: { text: params.text } };
18
+ case "swipe":
19
+ return {
20
+ type: "receive_swipe",
21
+ payload: {
22
+ startX: params.startXRatio,
23
+ startY: params.startYRatio,
24
+ endX: params.endXRatio,
25
+ endY: params.endYRatio,
26
+ },
27
+ };
28
+ case "scroll":
29
+ return {
30
+ type: "receive_scroll",
31
+ payload: {
32
+ x: params.xRatio,
33
+ y: params.yRatio,
34
+ direction: params.direction,
35
+ amount: params.amount ?? 3,
36
+ },
37
+ };
38
+ case "keycombo":
39
+ return { type: "receive_keycombo", payload: { keys: params.keys } };
40
+ case "clipboard":
41
+ return {
42
+ type: "receive_clipboard",
43
+ payload: { action: params.clipboardAction, text: params.text },
44
+ };
45
+ case "screenshot":
46
+ return { type: "receive_screenshot", payload: {} };
47
+ default:
48
+ throw new Error(`Unsupported action: ${params.action}`);
49
+ }
50
+ }
51
+ export async function enqueueCommand(config, command) {
52
+ const response = await fetch(`${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/commands`, {
53
+ method: "POST",
54
+ headers: {
55
+ "Content-Type": "application/json",
56
+ "x-zhihand-controller-token": config.controllerToken,
57
+ },
58
+ body: JSON.stringify({
59
+ command: { ...command, message_id: command.messageId ?? nextMessageId() },
60
+ }),
61
+ });
62
+ if (!response.ok) {
63
+ throw new Error(`Enqueue command failed: ${response.status}`);
64
+ }
65
+ const payload = (await response.json());
66
+ return payload.command;
67
+ }
68
+ export async function getCommand(config, commandId) {
69
+ const response = await fetch(`${config.controlPlaneEndpoint}/v1/credentials/${encodeURIComponent(config.credentialId)}/commands/${encodeURIComponent(commandId)}`, {
70
+ headers: { "x-zhihand-controller-token": config.controllerToken },
71
+ });
72
+ if (!response.ok) {
73
+ throw new Error(`Get command failed: ${response.status}`);
74
+ }
75
+ const payload = (await response.json());
76
+ return payload.command;
77
+ }
78
+ export function formatAckSummary(action, result) {
79
+ if (!result.acked) {
80
+ return `Sent ${action}, waiting for ACK (timed out).`;
81
+ }
82
+ const ackStatus = result.command?.ack_status ?? "ok";
83
+ return `Sent ${action}. ACK: ${ackStatus}`;
84
+ }
@@ -0,0 +1,26 @@
1
+ export interface DeviceCredential {
2
+ credentialId: string;
3
+ controllerToken: string;
4
+ endpoint: string;
5
+ deviceName?: string;
6
+ pairedAt?: string;
7
+ }
8
+ export interface CredentialStore {
9
+ default: string;
10
+ devices: Record<string, DeviceCredential>;
11
+ }
12
+ export interface ZhiHandConfig {
13
+ controlPlaneEndpoint: string;
14
+ credentialId: string;
15
+ controllerToken: string;
16
+ edgeId?: string;
17
+ timeoutMs?: number;
18
+ }
19
+ export declare function resolveZhiHandDir(): string;
20
+ export declare function ensureZhiHandDir(): void;
21
+ export declare function loadCredentialStore(): CredentialStore | null;
22
+ export declare function loadDefaultCredential(): DeviceCredential | null;
23
+ export declare function saveCredential(name: string, cred: DeviceCredential, setDefault?: boolean): void;
24
+ export declare function resolveConfig(deviceName?: string): ZhiHandConfig;
25
+ export declare function loadState<T = unknown>(): T | null;
26
+ export declare function saveState(state: unknown): void;