@zhihand/mcp 0.12.3 → 0.16.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 CHANGED
@@ -2,22 +2,30 @@
2
2
 
3
3
  ZhiHand MCP Server — let AI agents see and control your phone.
4
4
 
5
- Version: `0.12.1`
5
+ Version: `0.16.0`
6
6
 
7
7
  ## What is this?
8
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:
9
+ `@zhihand/mcp` is the core integration layer for ZhiHand. It runs as a **persistent daemon** that exposes phone control tools to any compatible AI agent via [MCP (Model Context Protocol)](https://modelcontextprotocol.io/), including:
10
10
 
11
11
  - **Claude Code**
12
12
  - **Codex CLI**
13
13
  - **Gemini CLI**
14
14
  - **OpenClaw**
15
15
 
16
- One npm package, two entry points:
16
+ The daemon is a single persistent process that bundles three subsystems:
17
+
18
+ | Subsystem | Purpose |
19
+ |---|---|
20
+ | **MCP Server** | HTTP Streamable transport on `localhost:18686/mcp` — serves tool calls to AI agents |
21
+ | **Relay** | Brain heartbeat (30s), prompt listener (phone-initiated tasks), CLI dispatch |
22
+ | **Config API** | IPC endpoint for `zhihand gemini/claude/codex` backend switching |
23
+
24
+ Legacy entry points (backward compatible):
17
25
 
18
26
  | Entry | Purpose |
19
27
  |---|---|
20
- | `zhihand serve` | MCP Server (stdio) — used by Claude Code, Codex, Gemini CLI |
28
+ | `zhihand serve` | MCP Server (stdio mode) — legacy, still works for direct CLI integration |
21
29
  | `zhihand.openclaw` | OpenClaw Plugin entry — thin wrapper calling the same core |
22
30
 
23
31
  ## Requirements
@@ -39,7 +47,7 @@ npx @zhihand/mcp serve
39
47
 
40
48
  ## Quick Start
41
49
 
42
- ### 1. Pair your phone
50
+ ### 1. Setup and pair
43
51
 
44
52
  ```bash
45
53
  zhihand setup
@@ -52,62 +60,19 @@ This runs the full interactive setup:
52
60
  3. Waits for you to scan the QR code with the ZhiHand mobile app
53
61
  4. Saves credentials to `~/.zhihand/credentials.json`
54
62
  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
63
+ 6. Auto-selects the best available tool and configures MCP automatically
64
+ 7. Starts the daemon (MCP Server + Relay + Config API)
58
65
 
59
- Add the ZhiHand MCP server to your tool's configuration:
66
+ No manual MCP configuration needed `zhihand setup` handles everything.
60
67
 
61
- **Claude Code** — Add to `.mcp.json` in your project root, or run:
68
+ ### 2. Start the daemon
62
69
 
63
70
  ```bash
64
- claude mcp add zhihand -- zhihand serve
71
+ zhihand start # Start daemon in foreground
72
+ zhihand start -d # Start daemon in background (detached)
65
73
  ```
66
74
 
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
- ```
75
+ The daemon runs the MCP Server on `localhost:18686/mcp` (HTTP Streamable transport), maintains a brain heartbeat every 30 seconds (keeps the phone Brain indicator green), and listens for phone-initiated prompts.
111
76
 
112
77
  ### 3. Start using it
113
78
 
@@ -123,14 +88,52 @@ Once configured, your AI agent can use ZhiHand tools directly. For example, in C
123
88
  ## CLI Commands
124
89
 
125
90
  ```
126
- zhihand serve Start MCP Server (stdio mode, called by AI tools)
127
- zhihand setup Interactive setup: pair + detect tools + print config
91
+ zhihand setup Interactive setup: pair + detect tools + auto-select + configure MCP + start daemon
92
+ zhihand start Start daemon (MCP Server + Relay + Config API)
93
+ zhihand start -d Start daemon in background (detached)
94
+ zhihand stop Stop the running daemon
95
+ zhihand status Show daemon status, pairing info, device, and active backend
96
+
128
97
  zhihand pair Pair with a phone (QR code in terminal)
129
- zhihand status Show current pairing status and device info
130
98
  zhihand detect List detected CLI tools and their login status
99
+ zhihand serve Start MCP Server (stdio mode, backward compatible)
131
100
  zhihand --help Show help
101
+
102
+ zhihand claude Switch backend to Claude Code (sends IPC to daemon, auto-configures MCP)
103
+ zhihand codex Switch backend to Codex CLI (sends IPC to daemon, auto-configures MCP)
104
+ zhihand gemini Switch backend to Gemini CLI (sends IPC to daemon, auto-configures MCP)
105
+ ```
106
+
107
+ ### Daemon Lifecycle
108
+
109
+ ```bash
110
+ zhihand start # Start daemon in foreground
111
+ zhihand start -d # Start daemon in background
112
+ zhihand stop # Stop the daemon
113
+ zhihand status # Check if daemon is running, show device & backend info
132
114
  ```
133
115
 
116
+ The daemon is a single persistent process that runs:
117
+ - **MCP Server** on `localhost:18686/mcp` (HTTP Streamable transport)
118
+ - **Relay**: brain heartbeat every 30s (keeps phone Brain indicator green), prompt listener (phone-initiated tasks dispatched to CLI), CLI dispatch
119
+ - **Config API**: IPC endpoint for backend switching
120
+
121
+ ### Switching Backends
122
+
123
+ Use `zhihand claude`, `zhihand codex`, or `zhihand gemini` to switch the active backend:
124
+
125
+ ```bash
126
+ zhihand gemini # Switch to Gemini CLI
127
+ zhihand claude # Switch to Claude Code
128
+ zhihand codex # Switch to Codex CLI
129
+ ```
130
+
131
+ When you switch:
132
+ - The command sends an **IPC message to the running daemon**
133
+ - MCP config is **automatically added** to the new backend
134
+ - MCP config is **automatically removed** from the previous backend
135
+ - If the tool is not installed, an error is shown
136
+
134
137
  ### Options
135
138
 
136
139
  | Option | Description |
@@ -188,26 +191,40 @@ Pair with a phone device. Returns a QR code and pairing URL.
188
191
  ## How It Works
189
192
 
190
193
  ```
191
- AI Agent ←stdiozhihand 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
194
+ AI Agent ←HTTP StreamableDaemon (localhost:18686/mcp)
195
+
196
+ ├── MCP Server ──→ ZhiHand Server ──→ Mobile App
197
+ │ (tool calls: control, screenshot, pair)
198
+
199
+ ├── Relay
200
+ ├── Brain heartbeat (30s) ──→ Server
201
+ │ ├── Prompt listener (SSE) ←── Server ←── Phone
202
+ └── CLI dispatch ──→ spawn claude/codex/gemini
203
+
204
+ └── Config API
205
+ └── IPC from zhihand claude/codex/gemini
203
206
  ```
204
207
 
208
+ ### Agent-initiated flow (tool calls)
209
+
205
210
  1. AI agent calls a tool (e.g. `zhihand_control` with `action: "click"`)
206
211
  2. MCP Server translates to a device command and enqueues it via the ZhiHand API
207
212
  3. Mobile app picks up the command, executes it, and sends an ACK
208
213
  4. MCP Server receives the ACK (via SSE or polling fallback)
209
214
  5. MCP Server fetches a fresh screenshot and returns it to the AI agent
210
215
 
216
+ ### Phone-initiated flow (prompt relay)
217
+
218
+ 1. User speaks or types a prompt on the phone
219
+ 2. Phone sends prompt to ZhiHand Server
220
+ 3. Daemon receives prompt via SSE
221
+ 4. Daemon spawns the active CLI tool (e.g. `claude`, `codex`, `gemini`) with the prompt
222
+ 5. CLI tool executes, result is sent back to the phone
223
+
224
+ ### Brain heartbeat
225
+
226
+ The daemon sends a heartbeat to the ZhiHand Server every 30 seconds. This keeps the **Brain indicator green** on the phone, showing the user that an AI backend is connected and ready.
227
+
211
228
  Screenshots are transferred as raw JPEG binary and only base64-encoded at the LLM API boundary, minimizing bandwidth.
212
229
 
213
230
  ## Credential Storage
@@ -217,6 +234,8 @@ Pairing credentials are stored at:
217
234
  ```
218
235
  ~/.zhihand/
219
236
  ├── credentials.json # Device credentials (credentialId, controllerToken, endpoint)
237
+ ├── backend.json # Active backend selection (claudecode/codex/gemini)
238
+ ├── daemon.pid # Daemon PID file (for zhihand stop)
220
239
  └── state.json # Current pairing session state
221
240
  ```
222
241
 
@@ -242,10 +261,10 @@ You can manage multiple devices. The `credentials.json` file stores a `default`
242
261
  ```
243
262
  packages/mcp/
244
263
  ├── bin/
245
- │ ├── zhihand # Main CLI entry (serve/setup/pair/status/detect)
264
+ │ ├── zhihand # Main CLI entry (start/stop/status/setup/serve/pair/detect)
246
265
  │ └── zhihand.openclaw # OpenClaw plugin entry
247
266
  ├── src/
248
- │ ├── index.ts # MCP Server (stdio transport)
267
+ │ ├── index.ts # MCP Server (stdio transport, legacy)
249
268
  │ ├── openclaw.adapter.ts # OpenClaw Plugin adapter (thin wrapper)
250
269
  │ ├── core/
251
270
  │ │ ├── config.ts # Credential & config management (~/.zhihand/)
@@ -253,6 +272,11 @@ packages/mcp/
253
272
  │ │ ├── screenshot.ts # Binary screenshot fetch (JPEG)
254
273
  │ │ ├── sse.ts # SSE client + hybrid ACK (SSE push + polling fallback)
255
274
  │ │ └── pair.ts # Plugin registration + device pairing flow
275
+ │ ├── daemon/
276
+ │ │ ├── index.ts # Daemon entry: HTTP server + MCP + Relay + Config API
277
+ │ │ ├── heartbeat.ts # Brain heartbeat loop (30s interval, 5s retry)
278
+ │ │ ├── prompt-listener.ts # SSE + polling prompt listener with dedup
279
+ │ │ └── dispatcher.ts # Async CLI dispatch (spawn + timeout + two-stage kill)
256
280
  │ ├── tools/
257
281
  │ │ ├── schemas.ts # Zod parameter schemas
258
282
  │ │ ├── control.ts # zhihand_control handler
@@ -261,6 +285,7 @@ packages/mcp/
261
285
  │ └── cli/
262
286
  │ ├── detect.ts # CLI tool detection (Claude Code, Codex, Gemini, OpenClaw)
263
287
  │ ├── spawn.ts # CLI process spawning (for mobile-initiated tasks)
288
+ │ ├── mcp-config.ts # MCP auto-configuration (add/remove per backend)
264
289
  │ └── openclaw.ts # OpenClaw auto-detect & plugin install
265
290
  ├── dist/ # Compiled JavaScript (shipped in npm package)
266
291
  ├── package.json
package/bin/zhihand CHANGED
@@ -3,19 +3,29 @@
3
3
  import os from "node:os";
4
4
  import { parseArgs } from "node:util";
5
5
  import { startStdioServer } from "../dist/index.js";
6
+ import { startDaemon, stopDaemon, isAlreadyRunning } from "../dist/daemon/index.js";
6
7
  import { detectCLITools, formatDetectedTools } from "../dist/cli/detect.js";
7
8
  import { detectAndSetupOpenClaw } from "../dist/cli/openclaw.js";
8
- import { loadDefaultCredential } from "../dist/core/config.js";
9
+ import { loadDefaultCredential, loadBackendConfig, saveBackendConfig } from "../dist/core/config.js";
9
10
  import { executePairing } from "../dist/core/pair.js";
11
+ import { configureMCP, displayName } from "../dist/cli/mcp-config.js";
10
12
 
11
13
  const DEFAULT_ENDPOINT = "https://api.zhihand.com";
12
14
 
15
+ const CLI_TOOL_MAP = {
16
+ claude: "claudecode",
17
+ codex: "codex",
18
+ gemini: "gemini",
19
+ };
20
+
13
21
  const { positionals, values } = parseArgs({
14
22
  allowPositionals: true,
23
+ strict: false,
15
24
  options: {
16
25
  device: { type: "string" },
17
- http: { type: "boolean", default: false },
18
26
  help: { type: "boolean", short: "h", default: false },
27
+ detach: { type: "boolean", short: "d", default: false },
28
+ port: { type: "string" },
19
29
  },
20
30
  });
21
31
 
@@ -23,25 +33,137 @@ const command = positionals[0] ?? "serve";
23
33
 
24
34
  if (values.help) {
25
35
  console.log(`
26
- zhihand — MCP Server for phone control
36
+ zhihand — MCP Server and Relay for phone control
27
37
 
28
38
  Usage:
29
- zhihand serve Start MCP Server (stdio mode)
30
- zhihand serve --http Start MCP Server (HTTP mode)
39
+ zhihand start Start daemon (MCP Server + Relay, foreground)
40
+ zhihand start -d Start daemon in background (detach)
41
+ zhihand stop Stop daemon
42
+ zhihand status Show status (pairing, backend, brain)
43
+
44
+ zhihand gemini Switch backend to Gemini CLI
45
+ zhihand claude Switch backend to Claude Code
46
+ zhihand codex Switch backend to Codex CLI
47
+
48
+ zhihand setup Interactive setup: pair + configure + start
31
49
  zhihand pair Pair with a phone device
32
- zhihand status Show pairing status and device info
33
50
  zhihand detect Detect available CLI tools
34
- zhihand setup Interactive setup: pair + configure
51
+
52
+ zhihand serve Start MCP Server (stdio mode, backward compat)
35
53
 
36
54
  Options:
37
55
  --device <name> Use a specific paired device
56
+ --port <port> Override daemon port (default: 18686)
57
+ -d, --detach Run daemon in background
38
58
  -h, --help Show this help
39
59
  `);
40
60
  process.exit(0);
41
61
  }
42
62
 
63
+ // ── Backend switch commands: claude, codex, gemini ─────────
64
+
65
+ if (Object.prototype.hasOwnProperty.call(CLI_TOOL_MAP, command)) {
66
+ const backendName = CLI_TOOL_MAP[command];
67
+ const tools = await detectCLITools();
68
+ const tool = tools.find((t) => t.name === backendName);
69
+
70
+ if (!tool) {
71
+ console.error(`Error: ${command} is not installed or not found in PATH.`);
72
+ console.error(`Install it first, then try again.`);
73
+ process.exit(1);
74
+ }
75
+
76
+ if (!tool.loggedIn) {
77
+ console.error(`Warning: ${command} is installed but not logged in.`);
78
+ }
79
+
80
+ // Check if daemon is running — if so, notify it via HTTP
81
+ const daemonPid = isAlreadyRunning();
82
+ const config = loadBackendConfig();
83
+ const previous = config.activeBackend;
84
+
85
+ if (previous === backendName) {
86
+ console.log(`Already using ${displayName(backendName)} as backend.`);
87
+ process.exit(0);
88
+ }
89
+
90
+ console.log(`Switching backend to ${displayName(backendName)}...`);
91
+
92
+ // Configure MCP (HTTP transport)
93
+ const { configured, removed } = configureMCP(backendName, previous);
94
+
95
+ if (configured) {
96
+ // Notify daemon if running
97
+ if (daemonPid) {
98
+ try {
99
+ const port = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
100
+ const res = await fetch(`http://127.0.0.1:${port}/internal/backend`, {
101
+ method: "POST",
102
+ headers: { "Content-Type": "application/json" },
103
+ body: JSON.stringify({ backend: backendName }),
104
+ signal: AbortSignal.timeout(5000),
105
+ });
106
+ if (res.ok) {
107
+ console.log(`\nDaemon notified. Backend switched to ${displayName(backendName)}.`);
108
+ }
109
+ } catch {
110
+ // Daemon not responding, just save config
111
+ saveBackendConfig({ activeBackend: backendName });
112
+ console.log(`\nBackend config saved. Daemon not responding — restart with 'zhihand start'.`);
113
+ }
114
+ } else {
115
+ saveBackendConfig({ activeBackend: backendName });
116
+ console.log(`\nBackend switched to ${displayName(backendName)}.`);
117
+ console.log(`Start the daemon to receive prompts: zhihand start`);
118
+ }
119
+
120
+ if (previous && removed) {
121
+ console.log(`Previous backend: ${displayName(previous)} (MCP config removed).`);
122
+ }
123
+ }
124
+ process.exit(0);
125
+ }
126
+
127
+ // ── Other commands ─────────────────────────────────────────
128
+
43
129
  switch (command) {
130
+ case "start":
131
+ case "relay": {
132
+ if (values.detach) {
133
+ const { spawn: spawnChild } = await import("node:child_process");
134
+ const args = [process.argv[1], "start"];
135
+ if (values.port) args.push("--port", values.port);
136
+ if (values.device) args.push("--device", values.device);
137
+
138
+ const child = spawnChild(process.execPath, args, {
139
+ detached: true,
140
+ stdio: "ignore",
141
+ env: { ...process.env },
142
+ });
143
+ child.unref();
144
+ console.log(`Daemon starting in background (PID ${child.pid}).`);
145
+ process.exit(0);
146
+ }
147
+ const port = values.port ? parseInt(values.port, 10) : undefined;
148
+ await startDaemon({
149
+ port,
150
+ deviceName: values.device ?? process.env.ZHIHAND_DEVICE,
151
+ });
152
+ break;
153
+ }
154
+
155
+ case "stop": {
156
+ const stopped = stopDaemon();
157
+ if (stopped) {
158
+ console.log("Daemon stopped.");
159
+ } else {
160
+ console.log("No daemon is running.");
161
+ }
162
+ break;
163
+ }
164
+
44
165
  case "serve": {
166
+ // Backward compatible: stdio MCP server (for old configs)
45
167
  await startStdioServer(values.device ?? process.env.ZHIHAND_DEVICE);
46
168
  break;
47
169
  }
@@ -55,13 +177,32 @@ switch (command) {
55
177
 
56
178
  case "status": {
57
179
  const cred = loadDefaultCredential();
180
+ const backend = loadBackendConfig();
181
+ const daemonPid = isAlreadyRunning();
182
+
58
183
  if (cred) {
59
184
  console.log(`Paired device: ${cred.deviceName}`);
60
185
  console.log(`Credential ID: ${cred.credentialId}`);
61
186
  console.log(`Endpoint: ${cred.endpoint}`);
62
- console.log(`Paired at: ${cred.pairedAt}`);
63
187
  } else {
64
- console.log("No paired device. Run: zhihand pair");
188
+ console.log("No paired device. Run: zhihand setup");
189
+ }
190
+ console.log(`Active backend: ${backend.activeBackend ? displayName(backend.activeBackend) : "(none)"}`);
191
+ console.log(`Daemon: ${daemonPid ? `running (PID ${daemonPid})` : "not running"}`);
192
+
193
+ // If daemon running, get live status
194
+ if (daemonPid) {
195
+ try {
196
+ const port = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || 18686;
197
+ const res = await fetch(`http://127.0.0.1:${port}/internal/status`, {
198
+ signal: AbortSignal.timeout(3000),
199
+ });
200
+ if (res.ok) {
201
+ const status = await res.json();
202
+ console.log(`Processing: ${status.processing ? "yes" : "idle"}`);
203
+ console.log(`Queue: ${status.queueLength} prompt(s)`);
204
+ }
205
+ } catch { /* daemon not responding */ }
65
206
  }
66
207
  break;
67
208
  }
@@ -73,7 +214,7 @@ switch (command) {
73
214
  }
74
215
 
75
216
  case "setup": {
76
- // 1. Check/create pairing
217
+ // 1. Pair
77
218
  let cred = loadDefaultCredential();
78
219
  if (!cred) {
79
220
  console.log("No paired device found. Starting pairing...\n");
@@ -86,15 +227,34 @@ switch (command) {
86
227
  console.log(`\nPaired: ${cred.deviceName} (${cred.credentialId})\n`);
87
228
  }
88
229
 
89
- // 2. Detect CLI tools
230
+ // 2. Detect tools
90
231
  const tools = await detectCLITools();
91
232
  console.log(formatDetectedTools(tools));
92
233
 
93
- // 3. Setup OpenClaw if present
94
- await detectAndSetupOpenClaw();
234
+ if (tools.length === 0) {
235
+ console.log("\nNo CLI tools detected. Install one of: Claude Code, Codex CLI, Gemini CLI.");
236
+ break;
237
+ }
238
+
239
+ // 3. Auto-select best tool and configure MCP (HTTP transport)
240
+ const best = tools.find((t) => t.loggedIn) ?? tools[0];
241
+ const config = loadBackendConfig();
242
+
243
+ console.log(`\nAuto-selecting backend: ${displayName(best.name)}...`);
244
+ // Ensure MCP URL uses correct port
245
+ if (values.port) {
246
+ process.env.ZHIHAND_PORT = values.port;
247
+ }
248
+ configureMCP(best.name, config.activeBackend);
249
+ saveBackendConfig({ activeBackend: best.name });
95
250
 
96
- console.log("\nSetup complete. Add to your CLI tool's MCP config:");
97
- console.log(' { "mcpServers": { "zhihand": { "command": "zhihand", "args": ["serve"] } } }');
251
+ // 4. Start daemon
252
+ console.log(`\nStarting daemon...\n`);
253
+ const port = values.port ? parseInt(values.port, 10) : undefined;
254
+ await startDaemon({
255
+ port,
256
+ deviceName: values.device ?? process.env.ZHIHAND_DEVICE,
257
+ });
98
258
  break;
99
259
  }
100
260
 
@@ -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: Google Cloud auth
34
- const loggedIn = tryExec("gemini auth status") !== null;
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 (HTTP transport) 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,71 @@
1
+ import { execSync } from "node:child_process";
2
+ const DEFAULT_PORT = 18686;
3
+ function mcpUrl() {
4
+ const port = parseInt(process.env.ZHIHAND_PORT ?? "", 10) || DEFAULT_PORT;
5
+ return `http://localhost:${port}/mcp`;
6
+ }
7
+ const MCP_COMMANDS = {
8
+ claudecode: {
9
+ add: () => `claude mcp add --transport http zhihand ${mcpUrl()}`,
10
+ remove: "claude mcp remove zhihand",
11
+ },
12
+ codex: {
13
+ add: () => `codex mcp add zhihand --url ${mcpUrl()}`,
14
+ remove: "codex mcp remove zhihand",
15
+ },
16
+ gemini: {
17
+ add: () => `gemini mcp add --transport http --scope user zhihand ${mcpUrl()}`,
18
+ remove: "gemini mcp remove --scope user zhihand",
19
+ },
20
+ };
21
+ const DISPLAY_NAMES = {
22
+ claudecode: "Claude Code",
23
+ codex: "Codex CLI",
24
+ gemini: "Gemini CLI",
25
+ openclaw: "OpenClaw",
26
+ };
27
+ function tryRun(cmd) {
28
+ try {
29
+ execSync(cmd, { stdio: "pipe", timeout: 10_000 });
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
36
+ /**
37
+ * Configure MCP (HTTP transport) for the selected backend and remove from others.
38
+ */
39
+ export function configureMCP(backend, previousBackend) {
40
+ let removed = false;
41
+ let configured = false;
42
+ // Remove from previous backend if different
43
+ if (previousBackend && previousBackend !== backend && previousBackend !== "openclaw") {
44
+ const cmds = MCP_COMMANDS[previousBackend];
45
+ if (cmds) {
46
+ console.log(` Removing MCP config from ${DISPLAY_NAMES[previousBackend]}...`);
47
+ removed = tryRun(cmds.remove);
48
+ }
49
+ }
50
+ // Add to new backend
51
+ if (backend === "openclaw") {
52
+ console.log(` OpenClaw uses plugin system. Run: openclaw plugins install @zhihand/mcp`);
53
+ configured = true;
54
+ }
55
+ else {
56
+ const cmds = MCP_COMMANDS[backend];
57
+ const addCmd = cmds.add();
58
+ console.log(` Configuring MCP for ${DISPLAY_NAMES[backend]} (HTTP transport)...`);
59
+ try {
60
+ execSync(addCmd, { stdio: "inherit", timeout: 10_000 });
61
+ configured = true;
62
+ }
63
+ catch (err) {
64
+ console.error(` Failed to configure ${DISPLAY_NAMES[backend]}: ${err.message}`);
65
+ }
66
+ }
67
+ return { configured, removed };
68
+ }
69
+ export function displayName(backend) {
70
+ return DISPLAY_NAMES[backend];
71
+ }
@@ -1,2 +1,23 @@
1
1
  import type { CLITool } from "./detect.ts";
2
- export declare function spawnCLITask(tool: CLITool, prompt: string): Promise<string>;
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;