@simonyea/holysheep-cli 2.1.26 → 2.1.28

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.26",
3
+ "version": "2.1.28",
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')
@@ -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 })
@@ -132,6 +307,15 @@ module.exports = {
132
307
  configure(apiKey, _baseUrlAnthropic, baseUrlOpenAI, primaryModel /*, _selectedModels */) {
133
308
  const merged = mergeConfig(apiKey, baseUrlOpenAI, primaryModel)
134
309
  writeConfig(merged)
310
+ // [HolySheep fork v2.1.28 / hs21] Also patch config.yaml so the actual
311
+ // agent runtime uses the right base_url. See CONFIG_YAML comment.
312
+ try {
313
+ patchConfigYaml(apiKey, baseUrlOpenAI, primaryModel)
314
+ } catch (e) {
315
+ // Non-fatal: hermes can still run on config.toml alone if the user
316
+ // manually edits config.yaml. Surface via stderr for diagnosability.
317
+ try { process.stderr.write(`[hermes.configure] config.yaml patch skipped: ${e.message}\n`) } catch {}
318
+ }
135
319
  return {
136
320
  file: CONFIG_FILE,
137
321
  hot: true, // next `hermes` run picks up the TOML on load
@@ -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
  },
@@ -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-hs19.tar.gz'
63
+ 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs20.tar.gz'
64
64
  const DEFAULT_RUNTIME_SHA256 =
65
- '9c41c88dfcf1a836d61a5322d748e02653953b087323b7a9766b8d1459170d13'
66
- const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs19'
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