@zhihand/mcp 0.12.3 → 0.15.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/README.md +28 -60
- package/bin/zhihand +77 -6
- package/dist/cli/detect.js +4 -2
- package/dist/cli/mcp-config.d.ts +9 -0
- package/dist/cli/mcp-config.js +65 -0
- package/dist/cli/spawn.d.ts +22 -1
- package/dist/cli/spawn.js +86 -21
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +17 -2
- 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.15.0`
|
|
6
6
|
|
|
7
7
|
## What is this?
|
|
8
8
|
|
|
@@ -39,7 +39,7 @@ npx @zhihand/mcp serve
|
|
|
39
39
|
|
|
40
40
|
## Quick Start
|
|
41
41
|
|
|
42
|
-
### 1.
|
|
42
|
+
### 1. Setup and pair
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
zhihand setup
|
|
@@ -52,64 +52,11 @@ This runs the full interactive setup:
|
|
|
52
52
|
3. Waits for you to scan the QR code with the ZhiHand mobile app
|
|
53
53
|
4. Saves credentials to `~/.zhihand/credentials.json`
|
|
54
54
|
5. Detects installed CLI tools (Claude Code, Codex, Gemini CLI, OpenClaw)
|
|
55
|
-
6.
|
|
55
|
+
6. Auto-selects the best available tool and configures MCP automatically
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
No manual MCP configuration needed — `zhihand setup` handles everything.
|
|
58
58
|
|
|
59
|
-
|
|
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
|
|
59
|
+
### 2. Start using it
|
|
113
60
|
|
|
114
61
|
Once configured, your AI agent can use ZhiHand tools directly. For example, in Claude Code:
|
|
115
62
|
|
|
@@ -124,13 +71,32 @@ Once configured, your AI agent can use ZhiHand tools directly. For example, in C
|
|
|
124
71
|
|
|
125
72
|
```
|
|
126
73
|
zhihand serve Start MCP Server (stdio mode, called by AI tools)
|
|
127
|
-
zhihand setup Interactive setup: pair + detect
|
|
74
|
+
zhihand setup Interactive setup: pair + auto-detect + auto-configure
|
|
128
75
|
zhihand pair Pair with a phone (QR code in terminal)
|
|
129
|
-
zhihand status Show
|
|
76
|
+
zhihand status Show pairing status, device info, and active backend
|
|
130
77
|
zhihand detect List detected CLI tools and their login status
|
|
131
78
|
zhihand --help Show help
|
|
79
|
+
|
|
80
|
+
zhihand claude Switch backend to Claude Code (auto-configures MCP)
|
|
81
|
+
zhihand codex Switch backend to Codex CLI (auto-configures MCP)
|
|
82
|
+
zhihand gemini Switch backend to Gemini CLI (auto-configures MCP)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Switching Backends
|
|
86
|
+
|
|
87
|
+
Use `zhihand claude`, `zhihand codex`, or `zhihand gemini` to switch the active backend:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
zhihand gemini # Switch to Gemini CLI
|
|
91
|
+
zhihand claude # Switch to Claude Code
|
|
92
|
+
zhihand codex # Switch to Codex CLI
|
|
132
93
|
```
|
|
133
94
|
|
|
95
|
+
When you switch:
|
|
96
|
+
- MCP config is **automatically added** to the new backend
|
|
97
|
+
- MCP config is **automatically removed** from the previous backend
|
|
98
|
+
- If the tool is not installed, an error is shown
|
|
99
|
+
|
|
134
100
|
### Options
|
|
135
101
|
|
|
136
102
|
| Option | Description |
|
|
@@ -217,6 +183,7 @@ Pairing credentials are stored at:
|
|
|
217
183
|
```
|
|
218
184
|
~/.zhihand/
|
|
219
185
|
├── credentials.json # Device credentials (credentialId, controllerToken, endpoint)
|
|
186
|
+
├── backend.json # Active backend selection (claudecode/codex/gemini)
|
|
220
187
|
└── state.json # Current pairing session state
|
|
221
188
|
```
|
|
222
189
|
|
|
@@ -261,6 +228,7 @@ packages/mcp/
|
|
|
261
228
|
│ └── cli/
|
|
262
229
|
│ ├── detect.ts # CLI tool detection (Claude Code, Codex, Gemini, OpenClaw)
|
|
263
230
|
│ ├── spawn.ts # CLI process spawning (for mobile-initiated tasks)
|
|
231
|
+
│ ├── mcp-config.ts # MCP auto-configuration (add/remove per backend)
|
|
264
232
|
│ └── openclaw.ts # OpenClaw auto-detect & plugin install
|
|
265
233
|
├── dist/ # Compiled JavaScript (shipped in npm package)
|
|
266
234
|
├── package.json
|
package/bin/zhihand
CHANGED
|
@@ -5,15 +5,24 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { startStdioServer } from "../dist/index.js";
|
|
6
6
|
import { detectCLITools, formatDetectedTools } from "../dist/cli/detect.js";
|
|
7
7
|
import { detectAndSetupOpenClaw } from "../dist/cli/openclaw.js";
|
|
8
|
-
import { loadDefaultCredential } from "../dist/core/config.js";
|
|
8
|
+
import { loadDefaultCredential, loadBackendConfig, saveBackendConfig } from "../dist/core/config.js";
|
|
9
9
|
import { executePairing } from "../dist/core/pair.js";
|
|
10
|
+
import { configureMCP, displayName } from "../dist/cli/mcp-config.js";
|
|
10
11
|
|
|
11
12
|
const DEFAULT_ENDPOINT = "https://api.zhihand.com";
|
|
12
13
|
|
|
14
|
+
const CLI_TOOL_MAP = {
|
|
15
|
+
claude: "claudecode",
|
|
16
|
+
codex: "codex",
|
|
17
|
+
gemini: "gemini",
|
|
18
|
+
};
|
|
19
|
+
|
|
13
20
|
const { positionals, values } = parseArgs({
|
|
14
21
|
allowPositionals: true,
|
|
22
|
+
strict: false,
|
|
15
23
|
options: {
|
|
16
24
|
device: { type: "string" },
|
|
25
|
+
model: { type: "string" },
|
|
17
26
|
http: { type: "boolean", default: false },
|
|
18
27
|
help: { type: "boolean", short: "h", default: false },
|
|
19
28
|
},
|
|
@@ -31,7 +40,11 @@ Usage:
|
|
|
31
40
|
zhihand pair Pair with a phone device
|
|
32
41
|
zhihand status Show pairing status and device info
|
|
33
42
|
zhihand detect Detect available CLI tools
|
|
34
|
-
zhihand setup Interactive setup: pair + configure
|
|
43
|
+
zhihand setup Interactive setup: pair + auto-configure
|
|
44
|
+
|
|
45
|
+
zhihand claude Switch backend to Claude Code
|
|
46
|
+
zhihand codex Switch backend to Codex CLI
|
|
47
|
+
zhihand gemini Switch backend to Gemini CLI
|
|
35
48
|
|
|
36
49
|
Options:
|
|
37
50
|
--device <name> Use a specific paired device
|
|
@@ -40,6 +53,43 @@ Options:
|
|
|
40
53
|
process.exit(0);
|
|
41
54
|
}
|
|
42
55
|
|
|
56
|
+
// Handle CLI tool subcommands: claude, codex, gemini → switch backend
|
|
57
|
+
if (Object.prototype.hasOwnProperty.call(CLI_TOOL_MAP, command)) {
|
|
58
|
+
const backendName = CLI_TOOL_MAP[command];
|
|
59
|
+
const tools = await detectCLITools();
|
|
60
|
+
const tool = tools.find((t) => t.name === backendName);
|
|
61
|
+
|
|
62
|
+
if (!tool) {
|
|
63
|
+
console.error(`Error: ${command} is not installed or not found in PATH.`);
|
|
64
|
+
console.error(`Install it first, then try again.`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (!tool.loggedIn) {
|
|
69
|
+
console.error(`Warning: ${command} is installed but not logged in.`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const config = loadBackendConfig();
|
|
73
|
+
const previous = config.activeBackend;
|
|
74
|
+
|
|
75
|
+
if (previous === backendName) {
|
|
76
|
+
console.log(`Already using ${displayName(backendName)} as backend.`);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
console.log(`Switching backend to ${displayName(backendName)}...`);
|
|
81
|
+
const { configured, removed } = configureMCP(backendName, previous);
|
|
82
|
+
|
|
83
|
+
if (configured) {
|
|
84
|
+
saveBackendConfig({ activeBackend: backendName });
|
|
85
|
+
console.log(`\nBackend switched to ${displayName(backendName)}.`);
|
|
86
|
+
if (previous) {
|
|
87
|
+
console.log(`Previous backend: ${displayName(previous)} ${removed ? '(MCP config removed)' : '(MCP config removal skipped)'}.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
process.exit(0);
|
|
91
|
+
}
|
|
92
|
+
|
|
43
93
|
switch (command) {
|
|
44
94
|
case "serve": {
|
|
45
95
|
await startStdioServer(values.device ?? process.env.ZHIHAND_DEVICE);
|
|
@@ -55,6 +105,7 @@ switch (command) {
|
|
|
55
105
|
|
|
56
106
|
case "status": {
|
|
57
107
|
const cred = loadDefaultCredential();
|
|
108
|
+
const backend = loadBackendConfig();
|
|
58
109
|
if (cred) {
|
|
59
110
|
console.log(`Paired device: ${cred.deviceName}`);
|
|
60
111
|
console.log(`Credential ID: ${cred.credentialId}`);
|
|
@@ -63,6 +114,11 @@ switch (command) {
|
|
|
63
114
|
} else {
|
|
64
115
|
console.log("No paired device. Run: zhihand pair");
|
|
65
116
|
}
|
|
117
|
+
if (backend.activeBackend) {
|
|
118
|
+
console.log(`Active backend: ${displayName(backend.activeBackend)}`);
|
|
119
|
+
} else {
|
|
120
|
+
console.log("No active backend. Run: zhihand claude / zhihand gemini / zhihand codex");
|
|
121
|
+
}
|
|
66
122
|
break;
|
|
67
123
|
}
|
|
68
124
|
|
|
@@ -90,11 +146,26 @@ switch (command) {
|
|
|
90
146
|
const tools = await detectCLITools();
|
|
91
147
|
console.log(formatDetectedTools(tools));
|
|
92
148
|
|
|
93
|
-
|
|
94
|
-
|
|
149
|
+
if (tools.length === 0) {
|
|
150
|
+
console.log("\nNo CLI tools detected. Install one of: Claude Code, Codex CLI, Gemini CLI, OpenClaw.");
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 3. Auto-select best tool (logged in + highest priority) and configure
|
|
155
|
+
const best = tools.find((t) => t.loggedIn) ?? tools[0];
|
|
156
|
+
const config = loadBackendConfig();
|
|
157
|
+
|
|
158
|
+
console.log(`\nAuto-selecting backend: ${displayName(best.name)}...`);
|
|
159
|
+
|
|
160
|
+
if (best.name === "openclaw") {
|
|
161
|
+
await detectAndSetupOpenClaw();
|
|
162
|
+
} else {
|
|
163
|
+
configureMCP(best.name, config.activeBackend);
|
|
164
|
+
}
|
|
95
165
|
|
|
96
|
-
|
|
97
|
-
console.log(
|
|
166
|
+
saveBackendConfig({ activeBackend: best.name });
|
|
167
|
+
console.log(`\nSetup complete. Backend: ${displayName(best.name)}.`);
|
|
168
|
+
console.log(`To switch backend later: zhihand claude / zhihand gemini / zhihand codex`);
|
|
98
169
|
break;
|
|
99
170
|
}
|
|
100
171
|
|
package/dist/cli/detect.js
CHANGED
|
@@ -30,8 +30,10 @@ async function detectGemini() {
|
|
|
30
30
|
if (!isCommandAvailable("gemini"))
|
|
31
31
|
return null;
|
|
32
32
|
const version = tryExec("gemini --version") ?? "unknown";
|
|
33
|
-
// Check login:
|
|
34
|
-
const loggedIn =
|
|
33
|
+
// Check login: oauth_creds.json or GOOGLE_API_KEY env var
|
|
34
|
+
const loggedIn = !!process.env.GOOGLE_API_KEY
|
|
35
|
+
|| !!process.env.GEMINI_API_KEY
|
|
36
|
+
|| tryExec("ls ~/.gemini/oauth_creds.json") !== null;
|
|
35
37
|
return { name: "gemini", command: "gemini", version, loggedIn, priority: 3 };
|
|
36
38
|
}
|
|
37
39
|
async function detectOpenClaw() {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { BackendName } from "../core/config.ts";
|
|
2
|
+
/**
|
|
3
|
+
* Configure MCP for the selected backend and remove from others.
|
|
4
|
+
*/
|
|
5
|
+
export declare function configureMCP(backend: BackendName, previousBackend: BackendName | null): {
|
|
6
|
+
configured: boolean;
|
|
7
|
+
removed: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare function displayName(backend: BackendName): string;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
const MCP_COMMANDS = {
|
|
3
|
+
claudecode: {
|
|
4
|
+
add: "claude mcp add zhihand -- zhihand serve",
|
|
5
|
+
remove: "claude mcp remove zhihand",
|
|
6
|
+
},
|
|
7
|
+
codex: {
|
|
8
|
+
add: "codex mcp add zhihand -- zhihand serve",
|
|
9
|
+
remove: "codex mcp remove zhihand",
|
|
10
|
+
},
|
|
11
|
+
gemini: {
|
|
12
|
+
add: "gemini mcp add --scope user zhihand zhihand -- serve",
|
|
13
|
+
remove: "gemini mcp remove --scope user zhihand",
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
const DISPLAY_NAMES = {
|
|
17
|
+
claudecode: "Claude Code",
|
|
18
|
+
codex: "Codex CLI",
|
|
19
|
+
gemini: "Gemini CLI",
|
|
20
|
+
openclaw: "OpenClaw",
|
|
21
|
+
};
|
|
22
|
+
function tryRun(cmd) {
|
|
23
|
+
try {
|
|
24
|
+
execSync(cmd, { stdio: "pipe", timeout: 10_000 });
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Configure MCP for the selected backend and remove from others.
|
|
33
|
+
*/
|
|
34
|
+
export function configureMCP(backend, previousBackend) {
|
|
35
|
+
let removed = false;
|
|
36
|
+
let configured = false;
|
|
37
|
+
// Remove from previous backend if different
|
|
38
|
+
if (previousBackend && previousBackend !== backend && previousBackend !== "openclaw") {
|
|
39
|
+
const cmds = MCP_COMMANDS[previousBackend];
|
|
40
|
+
if (cmds) {
|
|
41
|
+
console.log(` Removing MCP config from ${DISPLAY_NAMES[previousBackend]}...`);
|
|
42
|
+
removed = tryRun(cmds.remove);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Add to new backend
|
|
46
|
+
if (backend === "openclaw") {
|
|
47
|
+
console.log(` OpenClaw uses plugin system. Run: openclaw plugins install @zhihand/mcp`);
|
|
48
|
+
configured = true;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
const cmds = MCP_COMMANDS[backend];
|
|
52
|
+
console.log(` Configuring MCP for ${DISPLAY_NAMES[backend]}...`);
|
|
53
|
+
try {
|
|
54
|
+
execSync(cmds.add, { stdio: "inherit", timeout: 10_000 });
|
|
55
|
+
configured = true;
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
console.error(` Failed to configure ${DISPLAY_NAMES[backend]}: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return { configured, removed };
|
|
62
|
+
}
|
|
63
|
+
export function displayName(backend) {
|
|
64
|
+
return DISPLAY_NAMES[backend];
|
|
65
|
+
}
|
package/dist/cli/spawn.d.ts
CHANGED
|
@@ -1,2 +1,23 @@
|
|
|
1
1
|
import type { CLITool } from "./detect.ts";
|
|
2
|
-
export
|
|
2
|
+
export interface SpawnOptions {
|
|
3
|
+
model?: string;
|
|
4
|
+
timeout?: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Spawn a CLI tool interactively, inheriting stdio.
|
|
8
|
+
* Returns the exit code.
|
|
9
|
+
*/
|
|
10
|
+
export declare function spawnInteractive(command: string, args: string[], options?: {
|
|
11
|
+
timeout?: number;
|
|
12
|
+
env?: Record<string, string>;
|
|
13
|
+
}): Promise<number>;
|
|
14
|
+
/**
|
|
15
|
+
* Launch a CLI tool with a prompt. For Gemini, uses interactive mode (-i).
|
|
16
|
+
* For others, uses their respective prompt flags.
|
|
17
|
+
*/
|
|
18
|
+
export declare function launchCLI(tool: CLITool, prompt: string, options?: SpawnOptions): Promise<number>;
|
|
19
|
+
/**
|
|
20
|
+
* Non-interactive spawn that captures output (for MCP-initiated tasks).
|
|
21
|
+
* Uses spawnSync with argument arrays to avoid shell injection.
|
|
22
|
+
*/
|
|
23
|
+
export declare function spawnCLITask(tool: CLITool, prompt: string): string;
|
package/dist/cli/spawn.js
CHANGED
|
@@ -1,31 +1,96 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Spawn a CLI tool interactively, inheriting stdio.
|
|
4
|
+
* Returns the exit code.
|
|
5
|
+
*/
|
|
6
|
+
export function spawnInteractive(command, args, options) {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const child = spawn(command, args, {
|
|
9
|
+
stdio: "inherit",
|
|
10
|
+
env: { ...process.env, ...options?.env },
|
|
11
|
+
});
|
|
12
|
+
const timer = options?.timeout
|
|
13
|
+
? setTimeout(() => {
|
|
14
|
+
child.kill("SIGTERM");
|
|
15
|
+
reject(new Error(`Process timed out after ${options.timeout}ms`));
|
|
16
|
+
}, options.timeout)
|
|
17
|
+
: undefined;
|
|
18
|
+
child.on("error", (err) => {
|
|
19
|
+
if (timer)
|
|
20
|
+
clearTimeout(timer);
|
|
21
|
+
reject(err);
|
|
22
|
+
});
|
|
23
|
+
child.on("close", (code) => {
|
|
24
|
+
if (timer)
|
|
25
|
+
clearTimeout(timer);
|
|
26
|
+
resolve(code ?? 1);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
4
29
|
}
|
|
5
|
-
|
|
6
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Launch a CLI tool with a prompt. For Gemini, uses interactive mode (-i).
|
|
32
|
+
* For others, uses their respective prompt flags.
|
|
33
|
+
*/
|
|
34
|
+
export async function launchCLI(tool, prompt, options) {
|
|
35
|
+
const timeout = options?.timeout ?? 300_000;
|
|
36
|
+
switch (tool.name) {
|
|
37
|
+
case "claudecode": {
|
|
38
|
+
const args = ["-p", prompt, "--output-format", "json"];
|
|
39
|
+
return spawnInteractive(tool.command, args, { timeout });
|
|
40
|
+
}
|
|
41
|
+
case "codex": {
|
|
42
|
+
const args = ["-q", prompt, "--json"];
|
|
43
|
+
return spawnInteractive(tool.command, args, { timeout });
|
|
44
|
+
}
|
|
45
|
+
case "gemini": {
|
|
46
|
+
const model = options?.model ?? process.env.CLAUDE_GEMINI_MODEL ?? "gemini-3.1-pro-preview";
|
|
47
|
+
const args = [
|
|
48
|
+
"--approval-mode", "yolo",
|
|
49
|
+
"--model", model,
|
|
50
|
+
"-i", prompt,
|
|
51
|
+
];
|
|
52
|
+
const env = {
|
|
53
|
+
GEMINI_SANDBOX: "false",
|
|
54
|
+
TERM: process.env.TERM ?? "xterm-256color",
|
|
55
|
+
COLORTERM: process.env.COLORTERM ?? "truecolor",
|
|
56
|
+
};
|
|
57
|
+
return spawnInteractive(tool.command, args, { timeout, env });
|
|
58
|
+
}
|
|
59
|
+
case "openclaw": {
|
|
60
|
+
const args = ["run", prompt];
|
|
61
|
+
return spawnInteractive(tool.command, args, { timeout });
|
|
62
|
+
}
|
|
63
|
+
default:
|
|
64
|
+
throw new Error(`Unsupported CLI tool: ${tool.name}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Non-interactive spawn that captures output (for MCP-initiated tasks).
|
|
69
|
+
* Uses spawnSync with argument arrays to avoid shell injection.
|
|
70
|
+
*/
|
|
71
|
+
export function spawnCLITask(tool, prompt) {
|
|
72
|
+
const { spawnSync } = require("node:child_process");
|
|
73
|
+
const opts = { encoding: "utf8", timeout: 300_000 };
|
|
74
|
+
let result;
|
|
7
75
|
switch (tool.name) {
|
|
8
76
|
case "claudecode":
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
timeout: 300_000,
|
|
12
|
-
});
|
|
77
|
+
result = spawnSync(tool.command, ["-p", prompt, "--output-format", "json"], opts);
|
|
78
|
+
break;
|
|
13
79
|
case "codex":
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
timeout: 300_000,
|
|
17
|
-
});
|
|
80
|
+
result = spawnSync(tool.command, ["-q", prompt, "--json"], opts);
|
|
81
|
+
break;
|
|
18
82
|
case "gemini":
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
timeout: 300_000,
|
|
22
|
-
});
|
|
83
|
+
result = spawnSync(tool.command, ["--approval-mode", "yolo", "-p", prompt], opts);
|
|
84
|
+
break;
|
|
23
85
|
case "openclaw":
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
timeout: 300_000,
|
|
27
|
-
});
|
|
86
|
+
result = spawnSync(tool.command, ["run", prompt], opts);
|
|
87
|
+
break;
|
|
28
88
|
default:
|
|
29
89
|
throw new Error(`Unsupported CLI tool: ${tool.name}`);
|
|
30
90
|
}
|
|
91
|
+
if (result.error)
|
|
92
|
+
throw result.error;
|
|
93
|
+
if (result.status !== 0)
|
|
94
|
+
throw new Error(result.stderr || `Process exited with code ${result.status}`);
|
|
95
|
+
return result.stdout;
|
|
31
96
|
}
|
package/dist/core/config.d.ts
CHANGED
|
@@ -16,6 +16,10 @@ export interface ZhiHandConfig {
|
|
|
16
16
|
edgeId?: string;
|
|
17
17
|
timeoutMs?: number;
|
|
18
18
|
}
|
|
19
|
+
export type BackendName = "claudecode" | "codex" | "gemini" | "openclaw";
|
|
20
|
+
export interface BackendConfig {
|
|
21
|
+
activeBackend: BackendName | null;
|
|
22
|
+
}
|
|
19
23
|
export declare function resolveZhiHandDir(): string;
|
|
20
24
|
export declare function ensureZhiHandDir(): void;
|
|
21
25
|
export declare function loadCredentialStore(): CredentialStore | null;
|
|
@@ -24,3 +28,5 @@ export declare function saveCredential(name: string, cred: DeviceCredential, set
|
|
|
24
28
|
export declare function resolveConfig(deviceName?: string): ZhiHandConfig;
|
|
25
29
|
export declare function loadState<T = unknown>(): T | null;
|
|
26
30
|
export declare function saveState(state: unknown): void;
|
|
31
|
+
export declare function loadBackendConfig(): BackendConfig;
|
|
32
|
+
export declare function saveBackendConfig(config: BackendConfig): void;
|
package/dist/core/config.js
CHANGED
|
@@ -4,11 +4,12 @@ import os from "node:os";
|
|
|
4
4
|
const ZHIHAND_DIR = path.join(os.homedir(), ".zhihand");
|
|
5
5
|
const CREDENTIALS_PATH = path.join(ZHIHAND_DIR, "credentials.json");
|
|
6
6
|
const STATE_PATH = path.join(ZHIHAND_DIR, "state.json");
|
|
7
|
+
const BACKEND_PATH = path.join(ZHIHAND_DIR, "backend.json");
|
|
7
8
|
export function resolveZhiHandDir() {
|
|
8
9
|
return ZHIHAND_DIR;
|
|
9
10
|
}
|
|
10
11
|
export function ensureZhiHandDir() {
|
|
11
|
-
fs.mkdirSync(ZHIHAND_DIR, { recursive: true });
|
|
12
|
+
fs.mkdirSync(ZHIHAND_DIR, { recursive: true, mode: 0o700 });
|
|
12
13
|
}
|
|
13
14
|
export function loadCredentialStore() {
|
|
14
15
|
if (!fs.existsSync(CREDENTIALS_PATH))
|
|
@@ -32,7 +33,7 @@ export function saveCredential(name, cred, setDefault = true) {
|
|
|
32
33
|
store.devices[name] = cred;
|
|
33
34
|
if (setDefault)
|
|
34
35
|
store.default = name;
|
|
35
|
-
fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(store, null, 2));
|
|
36
|
+
fs.writeFileSync(CREDENTIALS_PATH, JSON.stringify(store, null, 2), { mode: 0o600 });
|
|
36
37
|
}
|
|
37
38
|
export function resolveConfig(deviceName) {
|
|
38
39
|
const store = loadCredentialStore();
|
|
@@ -65,3 +66,17 @@ export function saveState(state) {
|
|
|
65
66
|
ensureZhiHandDir();
|
|
66
67
|
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
67
68
|
}
|
|
69
|
+
export function loadBackendConfig() {
|
|
70
|
+
if (!fs.existsSync(BACKEND_PATH))
|
|
71
|
+
return { activeBackend: null };
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(fs.readFileSync(BACKEND_PATH, "utf8"));
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return { activeBackend: null };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
export function saveBackendConfig(config) {
|
|
80
|
+
ensureZhiHandDir();
|
|
81
|
+
fs.writeFileSync(BACKEND_PATH, JSON.stringify(config, null, 2));
|
|
82
|
+
}
|
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
|
-
const PACKAGE_VERSION = "0.
|
|
8
|
+
const PACKAGE_VERSION = "0.15.0";
|
|
9
9
|
export function createServer(deviceName) {
|
|
10
10
|
const server = new McpServer({
|
|
11
11
|
name: "zhihand",
|