@zhihand/mcp 0.22.0 → 0.22.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.
- package/dist/daemon/dispatcher.js +7 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -2
- package/scripts/pty-wrap.py +143 -0
|
@@ -539,7 +539,13 @@ async function dispatchClaudeWithHistory(prompt, startTime, log, model) {
|
|
|
539
539
|
const claudePath = resolveClaude();
|
|
540
540
|
log(`[claude] One-shot dispatch (history: ${conversationHistory.length} turns)`);
|
|
541
541
|
// Pass prompt via stdin (-p -) to avoid ARG_MAX limit with long conversation history
|
|
542
|
-
|
|
542
|
+
// --permission-mode bypassPermissions: auto-approve all tool calls (like gemini's --approval-mode yolo)
|
|
543
|
+
const child = spawn(claudePath, [
|
|
544
|
+
"-p", "-",
|
|
545
|
+
"--model", model,
|
|
546
|
+
"--output-format", "json",
|
|
547
|
+
"--permission-mode", "bypassPermissions",
|
|
548
|
+
], {
|
|
543
549
|
env: process.env,
|
|
544
550
|
stdio: ["pipe", "pipe", "pipe"],
|
|
545
551
|
detached: false,
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
export declare const PACKAGE_VERSION = "0.22.
|
|
2
|
+
export declare const PACKAGE_VERSION = "0.22.1";
|
|
3
3
|
export declare function createServer(deviceName?: string): McpServer;
|
|
4
4
|
export declare function startStdioServer(deviceName?: string): Promise<void>;
|
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
|
-
export const PACKAGE_VERSION = "0.22.
|
|
8
|
+
export const PACKAGE_VERSION = "0.22.1";
|
|
9
9
|
export function createServer(deviceName) {
|
|
10
10
|
const server = new McpServer({
|
|
11
11
|
name: "zhihand",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhihand/mcp",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "ZhiHand MCP Server — phone control tools for Claude Code, Codex, Gemini CLI, and OpenClaw",
|
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"README.md",
|
|
24
24
|
"bin/",
|
|
25
|
-
"dist/"
|
|
25
|
+
"dist/",
|
|
26
|
+
"scripts/"
|
|
26
27
|
],
|
|
27
28
|
"publishConfig": {
|
|
28
29
|
"access": "public"
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Thin PTY wrapper — runs argv[1:] inside a pseudo-terminal so that tools
|
|
3
|
+
requiring isatty(stdin)==True (e.g. ``gemini -i``) work from a daemon.
|
|
4
|
+
|
|
5
|
+
Output is forwarded to this process's stdout in real time.
|
|
6
|
+
Stdin from the parent process is forwarded to the child's PTY input,
|
|
7
|
+
enabling persistent interactive sessions (send new prompts after startup).
|
|
8
|
+
Exit code matches the child's exit code.
|
|
9
|
+
|
|
10
|
+
Signals (SIGTERM, SIGINT) are forwarded to the child process group so that
|
|
11
|
+
killing this wrapper also kills the tool underneath — no orphaned processes.
|
|
12
|
+
|
|
13
|
+
Usage: python3 pty-wrap.py gemini --approval-mode yolo --model flash -i "prompt"
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
import pty
|
|
18
|
+
import select
|
|
19
|
+
import signal
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import time
|
|
23
|
+
|
|
24
|
+
SHUTDOWN_GRACE_SECONDS = 3
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> int:
|
|
28
|
+
if len(sys.argv) < 2:
|
|
29
|
+
sys.stderr.write("usage: pty-wrap.py COMMAND [ARGS...]\n")
|
|
30
|
+
return 1
|
|
31
|
+
|
|
32
|
+
master_fd, slave_fd = pty.openpty()
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
proc = subprocess.Popen(
|
|
36
|
+
sys.argv[1:],
|
|
37
|
+
stdin=slave_fd,
|
|
38
|
+
stdout=slave_fd,
|
|
39
|
+
stderr=slave_fd,
|
|
40
|
+
start_new_session=True,
|
|
41
|
+
close_fds=True,
|
|
42
|
+
)
|
|
43
|
+
except OSError as exc:
|
|
44
|
+
os.close(master_fd)
|
|
45
|
+
os.close(slave_fd)
|
|
46
|
+
sys.stderr.write(f"pty-wrap: exec failed: {exc}\n")
|
|
47
|
+
return 127
|
|
48
|
+
|
|
49
|
+
os.close(slave_fd)
|
|
50
|
+
os.set_blocking(master_fd, False)
|
|
51
|
+
|
|
52
|
+
# Set up stdin forwarding (parent → PTY master → child stdin)
|
|
53
|
+
stdin_fd = sys.stdin.fileno()
|
|
54
|
+
stdin_open = True
|
|
55
|
+
try:
|
|
56
|
+
os.set_blocking(stdin_fd, False)
|
|
57
|
+
except OSError:
|
|
58
|
+
stdin_open = False
|
|
59
|
+
|
|
60
|
+
# Forward SIGTERM/SIGINT to the child's process group
|
|
61
|
+
def _forward_signal(signum: int, _frame: object) -> None:
|
|
62
|
+
try:
|
|
63
|
+
os.killpg(proc.pid, signum)
|
|
64
|
+
except OSError:
|
|
65
|
+
pass
|
|
66
|
+
|
|
67
|
+
signal.signal(signal.SIGTERM, _forward_signal)
|
|
68
|
+
signal.signal(signal.SIGINT, _forward_signal)
|
|
69
|
+
|
|
70
|
+
# Drain PTY master while child is alive, forward stdin to child
|
|
71
|
+
while proc.poll() is None:
|
|
72
|
+
fds = [master_fd]
|
|
73
|
+
if stdin_open:
|
|
74
|
+
fds.append(stdin_fd)
|
|
75
|
+
try:
|
|
76
|
+
ready, _, _ = select.select(fds, [], [], 1.0)
|
|
77
|
+
except (OSError, InterruptedError):
|
|
78
|
+
break
|
|
79
|
+
|
|
80
|
+
if master_fd in ready:
|
|
81
|
+
try:
|
|
82
|
+
data = os.read(master_fd, 8192)
|
|
83
|
+
if data:
|
|
84
|
+
sys.stdout.buffer.write(data)
|
|
85
|
+
sys.stdout.buffer.flush()
|
|
86
|
+
except OSError:
|
|
87
|
+
break
|
|
88
|
+
|
|
89
|
+
if stdin_open and stdin_fd in ready:
|
|
90
|
+
try:
|
|
91
|
+
data = os.read(stdin_fd, 8192)
|
|
92
|
+
if data:
|
|
93
|
+
# Write all bytes, handling partial writes
|
|
94
|
+
offset = 0
|
|
95
|
+
while offset < len(data):
|
|
96
|
+
try:
|
|
97
|
+
written = os.write(master_fd, data[offset:])
|
|
98
|
+
offset += written
|
|
99
|
+
except BlockingIOError:
|
|
100
|
+
# PTY buffer full — wait briefly and retry
|
|
101
|
+
time.sleep(0.01)
|
|
102
|
+
else:
|
|
103
|
+
stdin_open = False # EOF on stdin
|
|
104
|
+
except BlockingIOError:
|
|
105
|
+
# read() got EAGAIN — no data yet, not an error
|
|
106
|
+
pass
|
|
107
|
+
except OSError:
|
|
108
|
+
stdin_open = False
|
|
109
|
+
|
|
110
|
+
# Final drain after child exits
|
|
111
|
+
try:
|
|
112
|
+
while True:
|
|
113
|
+
data = os.read(master_fd, 8192)
|
|
114
|
+
if not data:
|
|
115
|
+
break
|
|
116
|
+
sys.stdout.buffer.write(data)
|
|
117
|
+
sys.stdout.buffer.flush()
|
|
118
|
+
except OSError:
|
|
119
|
+
pass
|
|
120
|
+
|
|
121
|
+
os.close(master_fd)
|
|
122
|
+
|
|
123
|
+
# Ensure the entire process group is dead
|
|
124
|
+
if proc.poll() is None:
|
|
125
|
+
try:
|
|
126
|
+
os.killpg(proc.pid, signal.SIGTERM)
|
|
127
|
+
except OSError:
|
|
128
|
+
pass
|
|
129
|
+
deadline = time.monotonic() + SHUTDOWN_GRACE_SECONDS
|
|
130
|
+
while proc.poll() is None and time.monotonic() < deadline:
|
|
131
|
+
time.sleep(0.1)
|
|
132
|
+
if proc.poll() is None:
|
|
133
|
+
try:
|
|
134
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
135
|
+
except OSError:
|
|
136
|
+
pass
|
|
137
|
+
proc.wait(timeout=2)
|
|
138
|
+
|
|
139
|
+
return proc.returncode or 0
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
sys.exit(main())
|