@simonyea/holysheep-cli 2.1.27 → 2.1.29
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.29",
|
|
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",
|
package/src/tools/hermes.js
CHANGED
|
@@ -24,6 +24,24 @@ const { commandExists } = require('../utils/which')
|
|
|
24
24
|
|
|
25
25
|
const CONFIG_DIR = path.join(os.homedir(), '.hermes')
|
|
26
26
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.toml')
|
|
27
|
+
// [HolySheep fork v2.1.28 / hs21] Hermes's agent runtime (the `hermes acp`
|
|
28
|
+
// process) reads ~/.hermes/config.yaml — NOT config.toml — for `model.base_url`
|
|
29
|
+
// and `providers.*`. The TOML is only used by the hermes CLI wrapper for
|
|
30
|
+
// provider selection; the actual outbound HTTP requests come out of config.yaml.
|
|
31
|
+
//
|
|
32
|
+
// Historically a user or earlier setup script wrote `model.base_url:
|
|
33
|
+
// http://127.0.0.1:18788/v1` into config.yaml (that's the OpenClaw local
|
|
34
|
+
// bridge port). If AionUi is running but the user isn't using OpenClaw,
|
|
35
|
+
// :18788 isn't listening, so every hermes prompt hangs on `provider=custom`
|
|
36
|
+
// connection until the AcpAgentManager fallback fires and fabricates an
|
|
37
|
+
// empty end_turn. User-visible symptom: hermes "thinks forever" then emits
|
|
38
|
+
// no response.
|
|
39
|
+
//
|
|
40
|
+
// Fix: on every hermes.configure(), also rewrite config.yaml's
|
|
41
|
+
// `model.base_url` + api_key + providers.custom to point at HolySheep
|
|
42
|
+
// directly. This is idempotent and preserves every other config.yaml
|
|
43
|
+
// section (toolsets, agent, memory, ...).
|
|
44
|
+
const CONFIG_YAML = path.join(CONFIG_DIR, 'config.yaml')
|
|
27
45
|
|
|
28
46
|
const PROVIDER_SECTION_RE = /^\[providers\.holysheep\]\s*$([\s\S]*?)(?=^\[|\Z)/m
|
|
29
47
|
const DEFAULT_PROVIDER_RE = /^default_provider\s*=\s*"[^"]*"\s*$/m
|
|
@@ -37,6 +55,163 @@ function readConfig() {
|
|
|
37
55
|
return ''
|
|
38
56
|
}
|
|
39
57
|
|
|
58
|
+
// [HolySheep fork v2.1.28 / hs21] Minimal YAML patcher for ~/.hermes/config.yaml.
|
|
59
|
+
//
|
|
60
|
+
// We don't want to pull a full YAML dependency into the holysheep-cli bundle,
|
|
61
|
+
// so we do targeted line-level rewrites:
|
|
62
|
+
// - model.base_url : overwritten
|
|
63
|
+
// - model.api_key : inserted if absent, else overwritten
|
|
64
|
+
// - model.default : overwritten (only if primaryModel provided)
|
|
65
|
+
// - providers : if currently `{}` or missing, write an explicit
|
|
66
|
+
// `custom` provider pointing at HolySheep
|
|
67
|
+
//
|
|
68
|
+
// Everything else in config.yaml (toolsets, agent, memory, summary_*, ...)
|
|
69
|
+
// is preserved verbatim.
|
|
70
|
+
function patchConfigYaml(apiKey, baseUrlOpenAI, primaryModel) {
|
|
71
|
+
if (!fs.existsSync(CONFIG_YAML)) {
|
|
72
|
+
// If the user never ran `hermes` (so no config.yaml), do nothing —
|
|
73
|
+
// hermes creates one on first launch; our next `hs setup` will patch it.
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
const cleanBase = String(baseUrlOpenAI || 'https://api.holysheep.ai/v1').replace(/\/+$/, '')
|
|
77
|
+
const model = primaryModel || 'claude-sonnet-4-6'
|
|
78
|
+
const src = fs.readFileSync(CONFIG_YAML, 'utf8')
|
|
79
|
+
const lines = src.replace(/\r\n/g, '\n').split('\n')
|
|
80
|
+
const out = []
|
|
81
|
+
let inModel = false
|
|
82
|
+
let modelBlockLines = []
|
|
83
|
+
let modelIndent = ' '
|
|
84
|
+
let modelBlockStart = -1
|
|
85
|
+
// First pass: find model: block
|
|
86
|
+
for (let i = 0; i < lines.length; i++) {
|
|
87
|
+
const line = lines[i]
|
|
88
|
+
if (/^model:\s*$/.test(line)) {
|
|
89
|
+
out.push(line)
|
|
90
|
+
inModel = true
|
|
91
|
+
modelBlockStart = out.length
|
|
92
|
+
modelBlockLines = []
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
if (inModel) {
|
|
96
|
+
// Model section ends at next top-level key (non-indented non-blank) or EOF
|
|
97
|
+
if (line.length > 0 && !/^\s/.test(line)) {
|
|
98
|
+
// finalize model block first
|
|
99
|
+
out.push(...rewriteModelBlock(modelBlockLines, cleanBase, apiKey, model, modelIndent))
|
|
100
|
+
inModel = false
|
|
101
|
+
out.push(line)
|
|
102
|
+
continue
|
|
103
|
+
}
|
|
104
|
+
modelBlockLines.push(line)
|
|
105
|
+
// detect indent from first key-line
|
|
106
|
+
const m = line.match(/^(\s+)[^\s]/)
|
|
107
|
+
if (m && modelBlockLines.length === 1) modelIndent = m[1]
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
out.push(line)
|
|
111
|
+
}
|
|
112
|
+
if (inModel) {
|
|
113
|
+
// File ended inside model block
|
|
114
|
+
out.push(...rewriteModelBlock(modelBlockLines, cleanBase, apiKey, model, modelIndent))
|
|
115
|
+
}
|
|
116
|
+
// Second pass: rewrite the `providers:` block so `custom` always points
|
|
117
|
+
// at HolySheep. We locate the block by line offset so we can replace the
|
|
118
|
+
// entire block regardless of whether it's inline `{}`, empty, or a
|
|
119
|
+
// pre-existing nested map (which would otherwise duplicate `custom:`).
|
|
120
|
+
const resultLines = out.join('\n').split('\n')
|
|
121
|
+
const providersBlock = [
|
|
122
|
+
'providers:',
|
|
123
|
+
' custom:',
|
|
124
|
+
` base_url: ${cleanBase}`,
|
|
125
|
+
` api_key: ${apiKey}`,
|
|
126
|
+
` default_model: ${model}`,
|
|
127
|
+
' type: openai',
|
|
128
|
+
]
|
|
129
|
+
let start = -1, end = -1
|
|
130
|
+
for (let i = 0; i < resultLines.length; i++) {
|
|
131
|
+
if (/^providers:\s*($|\{\s*\}\s*$)/.test(resultLines[i])) { start = i; break }
|
|
132
|
+
}
|
|
133
|
+
if (start >= 0) {
|
|
134
|
+
// Find end = next top-level key or EOF (preserve whitespace-only lines inside)
|
|
135
|
+
end = resultLines.length
|
|
136
|
+
for (let j = start + 1; j < resultLines.length; j++) {
|
|
137
|
+
const ln = resultLines[j]
|
|
138
|
+
if (ln.length > 0 && !/^\s/.test(ln)) { end = j; break }
|
|
139
|
+
}
|
|
140
|
+
resultLines.splice(start, end - start, ...providersBlock)
|
|
141
|
+
}
|
|
142
|
+
// Third pass: hermes 0.8 also has a `custom_providers:` LIST (different
|
|
143
|
+
// from the `providers:` map). Line-by-line patch each item's base_url/api_key
|
|
144
|
+
// whose `name:` includes "holysheep" (case-insensitive) or whose base_url
|
|
145
|
+
// pointed at the stale OpenClaw bridge :18788.
|
|
146
|
+
const finalLines = resultLines.slice()
|
|
147
|
+
let inCustom = false
|
|
148
|
+
let itemStart = -1
|
|
149
|
+
let itemLines = []
|
|
150
|
+
const flushItem = () => {
|
|
151
|
+
if (itemStart < 0) return
|
|
152
|
+
const blob = itemLines.join('\n')
|
|
153
|
+
const isHolySheep = /name:\s*holysheep/i.test(blob) || /base_url:\s*http:\/\/127\.0\.0\.1:18788/.test(blob)
|
|
154
|
+
if (isHolySheep) {
|
|
155
|
+
for (let i = 0; i < itemLines.length; i++) {
|
|
156
|
+
if (/^\s+-?\s*base_url:/.test(itemLines[i])) {
|
|
157
|
+
itemLines[i] = itemLines[i].replace(/base_url:.*$/, `base_url: ${cleanBase}`)
|
|
158
|
+
} else if (/^\s+api_key:/.test(itemLines[i])) {
|
|
159
|
+
itemLines[i] = itemLines[i].replace(/api_key:.*$/, `api_key: ${apiKey}`)
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
finalLines.splice(itemStart, itemLines.length, ...itemLines)
|
|
163
|
+
}
|
|
164
|
+
itemStart = -1
|
|
165
|
+
itemLines = []
|
|
166
|
+
}
|
|
167
|
+
for (let i = 0; i < finalLines.length; i++) {
|
|
168
|
+
const ln = finalLines[i]
|
|
169
|
+
if (/^custom_providers:\s*$/.test(ln)) { inCustom = true; continue }
|
|
170
|
+
if (inCustom) {
|
|
171
|
+
// End of custom_providers: next top-level key (non-dash, non-indent, non-blank)
|
|
172
|
+
if (ln.length > 0 && !/^\s/.test(ln) && !/^-/.test(ln)) { flushItem(); inCustom = false; continue }
|
|
173
|
+
// New list item starts with dash at any indentation
|
|
174
|
+
if (/^\s*-\s/.test(ln)) { flushItem(); itemStart = i; itemLines = [ln]; continue }
|
|
175
|
+
if (itemStart >= 0) itemLines.push(ln)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (inCustom) flushItem()
|
|
179
|
+
fs.writeFileSync(CONFIG_YAML, finalLines.join('\n'), { encoding: 'utf8', mode: 0o600 })
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function rewriteModelBlock(blockLines, cleanBase, apiKey, model, indent) {
|
|
183
|
+
let sawBase = false, sawKey = false, sawDefault = false
|
|
184
|
+
const rewritten = []
|
|
185
|
+
for (const line of blockLines) {
|
|
186
|
+
if (/^\s*base_url\s*:/.test(line)) {
|
|
187
|
+
rewritten.push(`${indent}base_url: ${cleanBase}`)
|
|
188
|
+
sawBase = true
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
if (/^\s*api_key\s*:/.test(line)) {
|
|
192
|
+
rewritten.push(`${indent}api_key: ${apiKey}`)
|
|
193
|
+
sawKey = true
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
if (/^\s*default\s*:/.test(line)) {
|
|
197
|
+
rewritten.push(`${indent}default: ${model}`)
|
|
198
|
+
sawDefault = true
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
rewritten.push(line)
|
|
202
|
+
}
|
|
203
|
+
// Append missing keys at the end of the model block
|
|
204
|
+
// (preserve trailing blank line if present)
|
|
205
|
+
let insertAt = rewritten.length
|
|
206
|
+
while (insertAt > 0 && rewritten[insertAt - 1].trim() === '') insertAt--
|
|
207
|
+
const toInsert = []
|
|
208
|
+
if (!sawBase) toInsert.push(`${indent}base_url: ${cleanBase}`)
|
|
209
|
+
if (!sawKey) toInsert.push(`${indent}api_key: ${apiKey}`)
|
|
210
|
+
if (!sawDefault) toInsert.push(`${indent}default: ${model}`)
|
|
211
|
+
if (toInsert.length) rewritten.splice(insertAt, 0, ...toInsert)
|
|
212
|
+
return rewritten
|
|
213
|
+
}
|
|
214
|
+
|
|
40
215
|
function writeConfig(content) {
|
|
41
216
|
if (!fs.existsSync(CONFIG_DIR)) {
|
|
42
217
|
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 0o700 })
|
|
@@ -130,8 +305,30 @@ module.exports = {
|
|
|
130
305
|
return isConfiguredInToml(readConfig())
|
|
131
306
|
},
|
|
132
307
|
configure(apiKey, _baseUrlAnthropic, baseUrlOpenAI, primaryModel /*, _selectedModels */) {
|
|
133
|
-
|
|
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)
|
|
134
322
|
writeConfig(merged)
|
|
323
|
+
// [HolySheep fork v2.1.28 / hs21] Also patch config.yaml so the actual
|
|
324
|
+
// agent runtime uses the right base_url. See CONFIG_YAML comment.
|
|
325
|
+
try {
|
|
326
|
+
patchConfigYaml(apiKey, baseUrlOpenAI, hermesPrimaryModel)
|
|
327
|
+
} catch (e) {
|
|
328
|
+
// Non-fatal: hermes can still run on config.toml alone if the user
|
|
329
|
+
// manually edits config.yaml. Surface via stderr for diagnosability.
|
|
330
|
+
try { process.stderr.write(`[hermes.configure] config.yaml patch skipped: ${e.message}\n`) } catch {}
|
|
331
|
+
}
|
|
135
332
|
return {
|
|
136
333
|
file: CONFIG_FILE,
|
|
137
334
|
hot: true, // next `hermes` run picks up the TOML on load
|
|
@@ -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
|