@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.
@@ -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
- const child = spawn(claudePath, ["-p", "-", "--model", model, "--output-format", "json"], {
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.0";
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.0";
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.0",
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())