@simonyea/holysheep-cli 2.1.28 → 2.1.30
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simonyea/holysheep-cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.30",
|
|
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",
|
|
@@ -205,6 +205,26 @@ function buildAuthHeaders(config, lease) {
|
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
// [HolySheep fork v2.1.29 / hs22] Rewrite User-Agent away from claude-cli/*.
|
|
209
|
+
// CRS enforces a "Claude Code CLI must use hs claude" policy keyed on UA —
|
|
210
|
+
// when it sees claude-cli/* or claude-code/*, it 403s even with valid bridge
|
|
211
|
+
// headers, under the assumption the user bypassed the wrapper. Every forward
|
|
212
|
+
// path (node-proxy + direct-https fallback) now normalises UA to
|
|
213
|
+
// holysheep-cli so the wrapper identity is preserved end-to-end.
|
|
214
|
+
function sanitizeClaudeClientHeaders(headers, config) {
|
|
215
|
+
const out = { ...(headers || {}) }
|
|
216
|
+
for (const k of Object.keys(out)) {
|
|
217
|
+
if (/^x-forwarded-|^x-real-ip$|^forwarded$|^via$/i.test(k)) {
|
|
218
|
+
delete out[k]
|
|
219
|
+
continue
|
|
220
|
+
}
|
|
221
|
+
if (/^user-agent$/i.test(k) && /claude-cli|claude-code/i.test(String(out[k] || ''))) {
|
|
222
|
+
out[k] = `holysheep-cli/${(config && config.cliVersion) || 'process-proxy'} (hs-claude-proxy)`
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return out
|
|
226
|
+
}
|
|
227
|
+
|
|
208
228
|
function deriveNodeProxyUrl(lease) {
|
|
209
229
|
if (process.env.HS_CLAUDE_NODE_PROXY_OVERRIDE) {
|
|
210
230
|
return String(process.env.HS_CLAUDE_NODE_PROXY_OVERRIDE).replace(/\/+$/, '')
|
|
@@ -244,12 +264,7 @@ function forwardDirectHttps({ config, lease, clientReq, clientRes, trace }) {
|
|
|
244
264
|
// (or a proxy earlier in the chain could tag the request) and leak the
|
|
245
265
|
// loopback origin / real client IP to api.holysheep.ai. The upstream
|
|
246
266
|
// doesn't need them — it authenticates off bridge headers + API key.
|
|
247
|
-
const sanitized =
|
|
248
|
-
for (const k of Object.keys(sanitized)) {
|
|
249
|
-
if (/^x-forwarded-|^x-real-ip$|^forwarded$|^via$/i.test(k)) {
|
|
250
|
-
delete sanitized[k]
|
|
251
|
-
}
|
|
252
|
-
}
|
|
267
|
+
const sanitized = sanitizeClaudeClientHeaders(clientReq.headers, config)
|
|
253
268
|
const headers = {
|
|
254
269
|
...sanitized,
|
|
255
270
|
...buildAuthHeaders(config, lease),
|
|
@@ -375,8 +390,14 @@ function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, ex
|
|
|
375
390
|
stallTimer = setTimeout(() => failWithAnthropicError('upstream stream stalled'), STALL_TIMEOUT_MS)
|
|
376
391
|
}
|
|
377
392
|
|
|
393
|
+
// [HolySheep fork v2.1.29 / hs22] Same UA-rewrite logic as forwardDirectHttps.
|
|
394
|
+
// Even when we go through node4:3129, the forward-proxy delivers the UA
|
|
395
|
+
// verbatim to upstream CRS; keeping claude-cli/* gets us 403.
|
|
396
|
+
// `extraHeaders` already contains bridge auth via buildAuthHeaders.
|
|
397
|
+
// `config` may be undefined here because forwardViaNodeProxy doesn't take
|
|
398
|
+
// one; use a minimal stub — cliVersion is optional.
|
|
378
399
|
const finalHeaders = {
|
|
379
|
-
...clientReq.headers,
|
|
400
|
+
...sanitizeClaudeClientHeaders(clientReq.headers, { cliVersion: undefined }),
|
|
380
401
|
...extraHeaders,
|
|
381
402
|
host: targetUrl.host,
|
|
382
403
|
connection: 'close',
|
package/src/tools/hermes.js
CHANGED
|
@@ -305,12 +305,25 @@ module.exports = {
|
|
|
305
305
|
return isConfiguredInToml(readConfig())
|
|
306
306
|
},
|
|
307
307
|
configure(apiKey, _baseUrlAnthropic, baseUrlOpenAI, primaryModel /*, _selectedModels */) {
|
|
308
|
-
|
|
308
|
+
// [HolySheep fork v2.1.29 / hs22] Hermes talks to its provider via the
|
|
309
|
+
// OpenAI-compatible Chat Completions API (`/v1/chat/completions`), which
|
|
310
|
+
// the HolySheep CRS relay does NOT support for `claude-*` model IDs at
|
|
311
|
+
// the moment (returns 503 "Service temporarily unavailable"). Only the
|
|
312
|
+
// anthropic native `/v1/messages` path accepts Claude. GPT-family models
|
|
313
|
+
// work on both paths.
|
|
314
|
+
//
|
|
315
|
+
// To give users a usable hermes out of the box we pick a safe default
|
|
316
|
+
// here: if the caller passes a `claude-*` model, silently downgrade to
|
|
317
|
+
// gpt-5.4 (HolySheep's default GPT model) for hermes only. The TOML
|
|
318
|
+
// keeps the real choice so `hermes --provider holysheep --model claude-*`
|
|
319
|
+
// still works if the user overrides it later (and CRS fixes the upstream).
|
|
320
|
+
const hermesPrimaryModel = /^claude-/i.test(primaryModel || '') ? 'gpt-5.4' : (primaryModel || 'gpt-5.4')
|
|
321
|
+
const merged = mergeConfig(apiKey, baseUrlOpenAI, hermesPrimaryModel)
|
|
309
322
|
writeConfig(merged)
|
|
310
323
|
// [HolySheep fork v2.1.28 / hs21] Also patch config.yaml so the actual
|
|
311
324
|
// agent runtime uses the right base_url. See CONFIG_YAML comment.
|
|
312
325
|
try {
|
|
313
|
-
patchConfigYaml(apiKey, baseUrlOpenAI,
|
|
326
|
+
patchConfigYaml(apiKey, baseUrlOpenAI, hermesPrimaryModel)
|
|
314
327
|
} catch (e) {
|
|
315
328
|
// Non-fatal: hermes can still run on config.toml alone if the user
|
|
316
329
|
// manually edits config.yaml. Surface via stderr for diagnosability.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
[HolySheep fork v2.1.
|
|
3
|
+
[HolySheep fork v2.1.29 / hs22] Hermes ACP PTY wrapper.
|
|
4
4
|
|
|
5
5
|
Why: bun 1.3.9's subprocess.spawn with stdio:['pipe','pipe','pipe']
|
|
6
6
|
passes sockets to the child. Python's asyncio.connect_write_pipe()
|
|
@@ -13,9 +13,22 @@ captures Python tracebacks separately. Master side pumps bytes between
|
|
|
13
13
|
our (bun-spawned) stdin/stdout and the PTY, so bun reads through kernel
|
|
14
14
|
PTY buffer which hermes's asyncio transport drives correctly.
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
CRITICAL: disable ICANON (canonical/line-buffered mode) on the slave.
|
|
17
|
+
Default macOS/Linux PTY slaves have ICANON on, which means:
|
|
18
|
+
1. Input is buffered line-by-line and each line is capped at MAX_CANON
|
|
19
|
+
(1024B on macOS, ~4096B on Linux). Longer lines get truncated.
|
|
20
|
+
2. Special chars (^C, ^D, ^Z, erase/kill) are interpreted instead of
|
|
21
|
+
passed through — breaks JSON containing random bytes.
|
|
22
|
+
|
|
23
|
+
ACP session/prompt JSON can exceed 4KB easily (system prompt + skill
|
|
24
|
+
list + assistant rules). Before hs22 we only disabled ECHO + ONLCR, so
|
|
25
|
+
id=1 (initialize, ~150B) and id=2/3 (newSession, setSessionMode, <400B)
|
|
26
|
+
went through, but id=4 (prompt, ~4-8KB) was truncated by MAX_CANON —
|
|
27
|
+
hermes got invalid JSON, returned an SDK error with empty data, our
|
|
28
|
+
AcpAgentManager 60s fallback masked it. User saw "hermes thinks forever".
|
|
29
|
+
|
|
30
|
+
Also disable ICRNL and IXON so we don't have \\r<->\\n rewriting or
|
|
31
|
+
flow-control pauses on large bursts.
|
|
19
32
|
|
|
20
33
|
Usage: python3 pty-hermes-wrapper.py
|
|
21
34
|
HERMES_BIN is auto-resolved from $PATH; override via $HOLYSHEEP_HERMES_BIN.
|
|
@@ -39,9 +52,30 @@ def main() -> None:
|
|
|
39
52
|
hermes_bin = _resolve_hermes_bin()
|
|
40
53
|
master_fd, slave_fd = pty.openpty()
|
|
41
54
|
attrs = termios.tcgetattr(slave_fd)
|
|
42
|
-
attrs[
|
|
43
|
-
attrs[
|
|
55
|
+
# attrs = [iflag, oflag, cflag, lflag, ispeed, ospeed, cc]
|
|
56
|
+
# lflag (attrs[3]): disable canonical-mode line buffering, ECHO*, signal chars
|
|
57
|
+
attrs[3] &= ~(termios.ICANON | termios.ECHO | termios.ECHOE | termios.ECHOK
|
|
58
|
+
| termios.ECHONL | termios.ISIG)
|
|
59
|
+
# iflag (attrs[0]): disable \r<->\n translation, XON/XOFF flow control,
|
|
60
|
+
# and parity checking (we carry raw bytes only).
|
|
61
|
+
attrs[0] &= ~(termios.ICRNL | termios.INLCR | termios.IGNCR | termios.IXON
|
|
62
|
+
| termios.IXOFF | termios.ISTRIP | termios.IGNBRK | termios.BRKINT
|
|
63
|
+
| termios.INPCK | termios.PARMRK)
|
|
64
|
+
# oflag (attrs[1]): disable post-processing (ONLCR \n -> \r\n, tabs, etc.)
|
|
65
|
+
attrs[1] &= ~termios.OPOST
|
|
66
|
+
# Non-blocking reads: return as soon as 1 byte is available.
|
|
67
|
+
attrs[6][termios.VMIN] = 1
|
|
68
|
+
attrs[6][termios.VTIME] = 0
|
|
44
69
|
termios.tcsetattr(slave_fd, termios.TCSANOW, attrs)
|
|
70
|
+
# Best-effort: print the applied flags once so future regressions are
|
|
71
|
+
# easy to diagnose from hs web logs.
|
|
72
|
+
try:
|
|
73
|
+
sys.stderr.write(
|
|
74
|
+
f'[pty-hermes-wrapper] slave configured raw: ICANON off, ECHO off, OPOST off\n'
|
|
75
|
+
)
|
|
76
|
+
sys.stderr.flush()
|
|
77
|
+
except OSError:
|
|
78
|
+
pass
|
|
45
79
|
|
|
46
80
|
pid = os.fork()
|
|
47
81
|
if pid == 0:
|
|
@@ -103,9 +137,28 @@ def main() -> None:
|
|
|
103
137
|
except ProcessLookupError:
|
|
104
138
|
pass
|
|
105
139
|
break
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
140
|
+
# [HolySheep fork v2.1.29 / hs22] Diagnostic trace when enabled —
|
|
141
|
+
# emits one line per stdin chunk so we can see exactly what bun
|
|
142
|
+
# pushed to us and verify hermes received the full payload. Only
|
|
143
|
+
# active when HOLYSHEEP_PTY_TRACE=1 is set to avoid log flood.
|
|
144
|
+
if os.environ.get('HOLYSHEEP_PTY_TRACE') == '1':
|
|
145
|
+
try:
|
|
146
|
+
sys.stderr.write(f'[pty-hermes-wrapper] stdin chunk len={len(data)}\n')
|
|
147
|
+
sys.stderr.flush()
|
|
148
|
+
except OSError:
|
|
149
|
+
pass
|
|
150
|
+
# Write in full — os.write may short-write on large buffers; loop.
|
|
151
|
+
total = 0
|
|
152
|
+
while total < len(data):
|
|
153
|
+
try:
|
|
154
|
+
n = os.write(master_fd, data[total:])
|
|
155
|
+
except OSError:
|
|
156
|
+
total = -1
|
|
157
|
+
break
|
|
158
|
+
if n <= 0:
|
|
159
|
+
break
|
|
160
|
+
total += n
|
|
161
|
+
if total < 0:
|
|
109
162
|
break
|
|
110
163
|
finally:
|
|
111
164
|
try:
|
|
@@ -60,10 +60,10 @@ const VENDOR_DIR = path.join(__dirname, 'vendor', 'aionui')
|
|
|
60
60
|
// new CLI release, the next `hs web` invocation on user machines will detect
|
|
61
61
|
// the version drift and upgrade the cache in place.
|
|
62
62
|
const DEFAULT_RUNTIME_URL =
|
|
63
|
-
'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-
|
|
63
|
+
'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs22.tar.gz'
|
|
64
64
|
const DEFAULT_RUNTIME_SHA256 =
|
|
65
|
-
'
|
|
66
|
-
const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-
|
|
65
|
+
'03e386ea83660b4074e29026dfc53be6ed01fcd51fa35a9379ed64a396e99f33'
|
|
66
|
+
const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs22'
|
|
67
67
|
|
|
68
68
|
function isValidRuntimeDir(dir) {
|
|
69
69
|
if (!dir) return false
|