@simonyea/holysheep-cli 2.1.25 → 2.1.26
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/package.json +1 -1
- package/src/tools/pty-hermes-wrapper.py +122 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.26",
|
|
4
4
|
"description": "Claude Code/Cursor/Cline API relay for China \u2014 \u00a51=$1, WeChat/Alipay payment, no credit card, no VPN. One command setup for all AI coding tools.",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node tests/droid.test.js && node tests/workspace-store.test.js && node tests/runtime-stale-upgrade.test.js && node tests/hermes.test.js && node tests/preflight.test.js",
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
[HolySheep fork v2.1.25 / hs19] Hermes ACP PTY wrapper.
|
|
4
|
+
|
|
5
|
+
Why: bun 1.3.9's subprocess.spawn with stdio:['pipe','pipe','pipe']
|
|
6
|
+
passes sockets to the child. Python's asyncio.connect_write_pipe()
|
|
7
|
+
silently drops the 2nd+ message written under bun's event loop routing.
|
|
8
|
+
Reproduced: bun delivers id=1 response but not id=2+; node delivers both.
|
|
9
|
+
|
|
10
|
+
Fix: allocate a PTY master/slave pair. Hand the SLAVE to hermes as its
|
|
11
|
+
stdin+stdout, keep stderr on the wrapper's fd 2 so hs web log still
|
|
12
|
+
captures Python tracebacks separately. Master side pumps bytes between
|
|
13
|
+
our (bun-spawned) stdin/stdout and the PTY, so bun reads through kernel
|
|
14
|
+
PTY buffer which hermes's asyncio transport drives correctly.
|
|
15
|
+
|
|
16
|
+
Disable ECHO on slave so stdin bytes are not echoed back as stdout bytes
|
|
17
|
+
(the old BSD `script -q /dev/null` wrapper had this bug).
|
|
18
|
+
Disable ONLCR so '\\n' is not rewritten to '\\r\\n'.
|
|
19
|
+
|
|
20
|
+
Usage: python3 pty-hermes-wrapper.py
|
|
21
|
+
HERMES_BIN is auto-resolved from $PATH; override via $HOLYSHEEP_HERMES_BIN.
|
|
22
|
+
"""
|
|
23
|
+
import os, sys, pty, termios, select, signal
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _resolve_hermes_bin() -> str:
|
|
27
|
+
override = os.environ.get('HOLYSHEEP_HERMES_BIN')
|
|
28
|
+
if override and os.access(override, os.X_OK):
|
|
29
|
+
return override
|
|
30
|
+
for d in os.environ.get('PATH', '').split(os.pathsep):
|
|
31
|
+
candidate = os.path.join(d, 'hermes')
|
|
32
|
+
if os.access(candidate, os.X_OK):
|
|
33
|
+
return candidate
|
|
34
|
+
sys.stderr.write('[pty-hermes-wrapper] hermes binary not found in $PATH\n')
|
|
35
|
+
sys.exit(127)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def main() -> None:
|
|
39
|
+
hermes_bin = _resolve_hermes_bin()
|
|
40
|
+
master_fd, slave_fd = pty.openpty()
|
|
41
|
+
attrs = termios.tcgetattr(slave_fd)
|
|
42
|
+
attrs[3] &= ~termios.ECHO
|
|
43
|
+
attrs[1] &= ~termios.ONLCR
|
|
44
|
+
termios.tcsetattr(slave_fd, termios.TCSANOW, attrs)
|
|
45
|
+
|
|
46
|
+
pid = os.fork()
|
|
47
|
+
if pid == 0:
|
|
48
|
+
os.setsid()
|
|
49
|
+
os.dup2(slave_fd, 0)
|
|
50
|
+
os.dup2(slave_fd, 1)
|
|
51
|
+
os.close(master_fd)
|
|
52
|
+
os.close(slave_fd)
|
|
53
|
+
os.execvp(hermes_bin, [hermes_bin, 'acp'])
|
|
54
|
+
return
|
|
55
|
+
|
|
56
|
+
os.close(slave_fd)
|
|
57
|
+
|
|
58
|
+
def _forward(signum, _frame):
|
|
59
|
+
try:
|
|
60
|
+
os.kill(pid, signum)
|
|
61
|
+
except ProcessLookupError:
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
signal.signal(signal.SIGTERM, _forward)
|
|
65
|
+
signal.signal(signal.SIGINT, _forward)
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
while True:
|
|
69
|
+
try:
|
|
70
|
+
wpid, _ = os.waitpid(pid, os.WNOHANG)
|
|
71
|
+
if wpid != 0:
|
|
72
|
+
break
|
|
73
|
+
except ChildProcessError:
|
|
74
|
+
break
|
|
75
|
+
try:
|
|
76
|
+
r, _, _ = select.select([master_fd, 0], [], [], 0.5)
|
|
77
|
+
except (InterruptedError, OSError):
|
|
78
|
+
continue
|
|
79
|
+
if master_fd in r:
|
|
80
|
+
try:
|
|
81
|
+
data = os.read(master_fd, 65536)
|
|
82
|
+
except OSError:
|
|
83
|
+
break
|
|
84
|
+
if not data:
|
|
85
|
+
break
|
|
86
|
+
try:
|
|
87
|
+
sys.stdout.buffer.write(data)
|
|
88
|
+
sys.stdout.buffer.flush()
|
|
89
|
+
except (BrokenPipeError, OSError):
|
|
90
|
+
try:
|
|
91
|
+
os.kill(pid, signal.SIGTERM)
|
|
92
|
+
except ProcessLookupError:
|
|
93
|
+
pass
|
|
94
|
+
break
|
|
95
|
+
if 0 in r:
|
|
96
|
+
try:
|
|
97
|
+
data = os.read(0, 65536)
|
|
98
|
+
except OSError:
|
|
99
|
+
break
|
|
100
|
+
if not data:
|
|
101
|
+
try:
|
|
102
|
+
os.kill(pid, signal.SIGTERM)
|
|
103
|
+
except ProcessLookupError:
|
|
104
|
+
pass
|
|
105
|
+
break
|
|
106
|
+
try:
|
|
107
|
+
os.write(master_fd, data)
|
|
108
|
+
except OSError:
|
|
109
|
+
break
|
|
110
|
+
finally:
|
|
111
|
+
try:
|
|
112
|
+
os.close(master_fd)
|
|
113
|
+
except OSError:
|
|
114
|
+
pass
|
|
115
|
+
try:
|
|
116
|
+
os.waitpid(pid, 0)
|
|
117
|
+
except OSError:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
if __name__ == '__main__':
|
|
122
|
+
main()
|