@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.27",
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",
@@ -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
- const merged = mergeConfig(apiKey, baseUrlOpenAI, primaryModel)
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.25 / hs19] Hermes ACP PTY wrapper.
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
- 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'.
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[3] &= ~termios.ECHO
43
- attrs[1] &= ~termios.ONLCR
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
- try:
107
- os.write(master_fd, data)
108
- except OSError:
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-hs20.tar.gz'
63
+ 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs22.tar.gz'
64
64
  const DEFAULT_RUNTIME_SHA256 =
65
- '99ddb768cacd223a13c77d716413032e913c8be2850e7dbccaf7ae52d0797e37'
66
- const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs20'
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