@simonyea/holysheep-cli 2.1.25 → 2.1.27
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.27",
|
|
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",
|
|
@@ -220,6 +220,63 @@ function deriveNodeProxyUrl(lease) {
|
|
|
220
220
|
return upstream.toString().replace(/\/+$/, '')
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
// [HolySheep fork v2.1.27 / hs20] Direct HTTPS fallback.
|
|
224
|
+
//
|
|
225
|
+
// Background: the assigned node's forward-proxy at :3129 can be unreachable
|
|
226
|
+
// (we've seen node3's proxy offline while node3's /v1/messages at :3101 is
|
|
227
|
+
// up). When that happens, claude-agent-acp spawns get ECONNREFUSED on every
|
|
228
|
+
// prompt and the user sees the generic "Internal error: API Error: 400".
|
|
229
|
+
//
|
|
230
|
+
// Fix: when the node-proxy forward fails with ECONNREFUSED (or the proxy
|
|
231
|
+
// URL itself won't resolve), retry the same request directly against
|
|
232
|
+
// `config.baseUrlAnthropic` (usually https://api.holysheep.ai). We still
|
|
233
|
+
// send the bridge headers — the upstream CRS accepts them and reuses the
|
|
234
|
+
// same lease. This gives users a usable claude path even while HolySheep
|
|
235
|
+
// SRE fixes the assigned node.
|
|
236
|
+
function forwardDirectHttps({ config, lease, clientReq, clientRes, trace }) {
|
|
237
|
+
const https = require('https')
|
|
238
|
+
const crsBase = config.baseUrlAnthropic || 'https://api.holysheep.ai'
|
|
239
|
+
const target = new URL(clientReq.url && clientReq.url.startsWith('http') ? clientReq.url : clientReq.url, crsBase)
|
|
240
|
+
return new Promise((resolve, reject) => {
|
|
241
|
+
// [HolySheep fork v2.1.27 / hs20] Sanitize headers before the direct
|
|
242
|
+
// upstream hit. claude-agent-acp usually runs in a clean env so these
|
|
243
|
+
// should never be present, but a future client library could set them
|
|
244
|
+
// (or a proxy earlier in the chain could tag the request) and leak the
|
|
245
|
+
// loopback origin / real client IP to api.holysheep.ai. The upstream
|
|
246
|
+
// doesn't need them — it authenticates off bridge headers + API key.
|
|
247
|
+
const sanitized = { ...clientReq.headers }
|
|
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
|
+
}
|
|
253
|
+
const headers = {
|
|
254
|
+
...sanitized,
|
|
255
|
+
...buildAuthHeaders(config, lease),
|
|
256
|
+
host: target.host,
|
|
257
|
+
connection: 'close',
|
|
258
|
+
}
|
|
259
|
+
const upReq = https.request({
|
|
260
|
+
hostname: target.hostname,
|
|
261
|
+
port: target.port || 443,
|
|
262
|
+
path: target.pathname + target.search,
|
|
263
|
+
method: clientReq.method,
|
|
264
|
+
headers,
|
|
265
|
+
timeout: RESPONSE_TIMEOUT_MS + 5000,
|
|
266
|
+
}, (upRes) => {
|
|
267
|
+
clientRes.writeHead(upRes.statusCode, upRes.headers)
|
|
268
|
+
upRes.pipe(clientRes)
|
|
269
|
+
upRes.on('end', resolve)
|
|
270
|
+
upRes.on('error', reject)
|
|
271
|
+
})
|
|
272
|
+
upReq.on('error', reject)
|
|
273
|
+
upReq.on('timeout', () => {
|
|
274
|
+
upReq.destroy(new Error('direct-https upstream timeout'))
|
|
275
|
+
})
|
|
276
|
+
clientReq.pipe(upReq)
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
|
|
223
280
|
function forwardViaNodeProxy({ nodeProxyUrl, targetUrl, clientReq, clientRes, extraHeaders = {}, trace = null }) {
|
|
224
281
|
const upstream = new URL(nodeProxyUrl)
|
|
225
282
|
return new Promise((resolve, reject) => {
|
|
@@ -617,6 +674,36 @@ function createProcessProxyServer({ sessionId, configPath = CONFIG_PATH, allowAn
|
|
|
617
674
|
if (!isRetryableNodeLeaseError(err) && attempt > 0) break
|
|
618
675
|
}
|
|
619
676
|
}
|
|
677
|
+
// [HolySheep fork v2.1.27 / hs20] Direct HTTPS fallback.
|
|
678
|
+
// If EVERY node-proxy retry failed with a connection-level error
|
|
679
|
+
// (ECONNREFUSED, EHOSTUNREACH, ETIMEDOUT, or a generic "Proxy error"
|
|
680
|
+
// mentioning ECONNREFUSED in the body), fall back to a direct HTTPS
|
|
681
|
+
// POST against api.holysheep.ai. The relay on that side recognises
|
|
682
|
+
// bridge headers + cr_ keys and forwards to an account pool even when
|
|
683
|
+
// the per-device assigned node is dead. Keeps users productive while
|
|
684
|
+
// SRE re-balances nodes.
|
|
685
|
+
if (lastError && !clientRes.headersSent) {
|
|
686
|
+
const msg = String(lastError?.message || lastError?.body || '')
|
|
687
|
+
const isConnectionLevel = /ECONNREFUSED|EHOSTUNREACH|ETIMEDOUT|ENETUNREACH|connect/i.test(msg)
|
|
688
|
+
if (isConnectionLevel) {
|
|
689
|
+
try {
|
|
690
|
+
const config = readConfig(configPath)
|
|
691
|
+
const lease = getCachedLease(sessionId) || await fetchFreshLease(config, sessionId, {})
|
|
692
|
+
logProxyTiming('request.direct-fallback', {
|
|
693
|
+
sessionId,
|
|
694
|
+
originalError: msg.slice(0, 160),
|
|
695
|
+
})
|
|
696
|
+
await forwardDirectHttps({ config, lease, clientReq, clientRes, trace: null })
|
|
697
|
+
lastError = null
|
|
698
|
+
} catch (fallbackErr) {
|
|
699
|
+
lastError = fallbackErr
|
|
700
|
+
logProxyTiming('request.direct-fallback.fail', {
|
|
701
|
+
sessionId,
|
|
702
|
+
error: String(fallbackErr?.message || fallbackErr),
|
|
703
|
+
})
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
620
707
|
if (lastError && !clientRes.headersSent) {
|
|
621
708
|
const status = Number(lastError.statusCode || 502)
|
|
622
709
|
const body = String(lastError.body || lastError.message || 'Proxy error')
|
package/src/tools/opencode.js
CHANGED
|
@@ -109,6 +109,28 @@ module.exports = {
|
|
|
109
109
|
// 设置默认模型
|
|
110
110
|
config.model = `anthropic/${primaryModel || 'claude-sonnet-4-6'}`
|
|
111
111
|
|
|
112
|
+
// [HolySheep fork v2.1.27 / hs20] Disable the giant OpenCode tools that
|
|
113
|
+
// blow the Anthropic request body past ~25 KB. When bodies exceed ~40 KB
|
|
114
|
+
// against the HolySheep relay, Anthropic returns a plan-limit error:
|
|
115
|
+
// "Third-party apps now draw from your extra usage, not your plan limits"
|
|
116
|
+
// which OpenCode surfaces as a silent `stop_reason: end_turn` with
|
|
117
|
+
// zero tokens (class-C failure). Removing just the biggest schemas
|
|
118
|
+
// (todowrite ~8.8 KB, task ~4.4 KB, webfetch/skill smaller) brings
|
|
119
|
+
// bodies below the threshold while keeping bash/read/glob/grep/edit/write
|
|
120
|
+
// which are the core coding tools users actually need.
|
|
121
|
+
//
|
|
122
|
+
// Users who want these tools back can set HOLYSHEEP_OPENCODE_KEEP_ALL_TOOLS=1
|
|
123
|
+
// in their shell, or manually remove the entries from opencode.json.
|
|
124
|
+
if (!process.env.HOLYSHEEP_OPENCODE_KEEP_ALL_TOOLS) {
|
|
125
|
+
const tools = { ...(config.tools || {}) }
|
|
126
|
+
if (tools.todowrite === undefined) tools.todowrite = false
|
|
127
|
+
if (tools.todoread === undefined) tools.todoread = false
|
|
128
|
+
if (tools.task === undefined) tools.task = false
|
|
129
|
+
if (tools.skill === undefined) tools.skill = false
|
|
130
|
+
if (tools.webfetch === undefined) tools.webfetch = false
|
|
131
|
+
config.tools = tools
|
|
132
|
+
}
|
|
133
|
+
|
|
112
134
|
writeConfig(config)
|
|
113
135
|
return { file: getConfigFile(), hot: false }
|
|
114
136
|
},
|
|
@@ -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()
|
|
@@ -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-hs20.tar.gz'
|
|
64
64
|
const DEFAULT_RUNTIME_SHA256 =
|
|
65
|
-
'
|
|
66
|
-
const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-
|
|
65
|
+
'99ddb768cacd223a13c77d716413032e913c8be2850e7dbccaf7ae52d0797e37'
|
|
66
|
+
const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs20'
|
|
67
67
|
|
|
68
68
|
function isValidRuntimeDir(dir) {
|
|
69
69
|
if (!dir) return false
|