@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 +97 -72
- package/bin/zhihand +175 -15
- package/dist/cli/detect.js +4 -2
- package/dist/cli/mcp-config.d.ts +9 -0
- package/dist/cli/mcp-config.js +71 -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/daemon/dispatcher.d.ts +13 -0
- package/dist/daemon/dispatcher.js +145 -0
- package/dist/daemon/heartbeat.d.ts +5 -0
- package/dist/daemon/heartbeat.js +65 -0
- package/dist/daemon/index.d.ts +6 -0
- package/dist/daemon/index.js +283 -0
- package/dist/daemon/prompt-listener.d.ts +32 -0
- package/dist/daemon/prompt-listener.js +152 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
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.
|
|
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
|
|
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
|
-
|
|
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) —
|
|
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.
|
|
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.
|
|
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
|
-
|
|
66
|
+
No manual MCP configuration needed — `zhihand setup` handles everything.
|
|
60
67
|
|
|
61
|
-
|
|
68
|
+
### 2. Start the daemon
|
|
62
69
|
|
|
63
70
|
```bash
|
|
64
|
-
|
|
71
|
+
zhihand start # Start daemon in foreground
|
|
72
|
+
zhihand start -d # Start daemon in background (detached)
|
|
65
73
|
```
|
|
66
74
|
|
|
67
|
-
|
|
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
|
|
127
|
-
zhihand
|
|
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 ←
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
194
|
+
AI Agent ←HTTP Streamable→ Daemon (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 (
|
|
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
|
|
30
|
-
zhihand
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
230
|
+
// 2. Detect tools
|
|
90
231
|
const tools = await detectCLITools();
|
|
91
232
|
console.log(formatDetectedTools(tools));
|
|
92
233
|
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
97
|
-
console.log(
|
|
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
|
|
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 (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
|
+
}
|
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;
|