@simonyea/holysheep-cli 2.1.3 → 2.1.5

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",
3
+ "version": "2.1.5",
4
4
  "description": "Claude Code/Cursor/Cline API relay for China — ¥1=$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",
@@ -210,6 +210,12 @@ async function loginWithApiKey(port, apiKey) {
210
210
  }
211
211
 
212
212
  // ── Start patched AionUi server ──────────────────────────────────────────────
213
+ // Timeout is generous for Windows first launch: Defender scans 41MB server.mjs,
214
+ // bun JIT-compiles, AionUi initializes sqlite under ~/.aionui-home, and npm
215
+ // may still pull the renderer. 60s is the realistic upper bound on a cold box.
216
+ const AIONUI_STARTUP_TIMEOUT_MS = Number(process.env.HS_WEB_STARTUP_TIMEOUT_MS) || 60_000
217
+ const AIONUI_LOG_TAIL_BYTES = 4096
218
+
213
219
  function spawnAionUiServer({ bunPath, runtimeDir, port }) {
214
220
  return new Promise((resolve, reject) => {
215
221
  const entry = path.join(runtimeDir, 'dist-server', 'server.mjs')
@@ -231,21 +237,74 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
231
237
  stdio: ['ignore', 'pipe', 'pipe'],
232
238
  })
233
239
 
240
+ // ── Log capture ────────────────────────────────────────────────────────
241
+ // Always subscribe to stdout+stderr. In debug mode we ALSO print live; in
242
+ // normal mode we keep the last ~4KB in a ring buffer so that if startup
243
+ // fails (timeout OR exit-before-ready) we can surface the real error to
244
+ // the user instead of the previous silent "failed to start within 20s".
245
+ const debug = process.env.HS_WEB_DEBUG === '1'
246
+ let logTail = ''
247
+ const appendLog = (stream, chunk) => {
248
+ const s = Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk)
249
+ logTail += s
250
+ if (logTail.length > AIONUI_LOG_TAIL_BYTES) {
251
+ logTail = logTail.slice(logTail.length - AIONUI_LOG_TAIL_BYTES)
252
+ }
253
+ if (debug) {
254
+ const target = stream === 'err' ? process.stderr : process.stdout
255
+ target.write(chalk.gray(`[aionui] ${s}`))
256
+ }
257
+ }
258
+ child.stdout.on('data', (d) => appendLog('out', d))
259
+ child.stderr.on('data', (d) => appendLog('err', d))
260
+
261
+ // ── Progress + timeout ─────────────────────────────────────────────────
234
262
  let settled = false
263
+ const startedAt = Date.now()
235
264
  const onReady = () => {
236
265
  if (!settled) {
237
266
  settled = true
267
+ clearInterval(progressTick)
238
268
  resolve(child)
239
269
  }
240
270
  }
241
- const timer = setTimeout(() => {
242
- if (!settled) {
243
- settled = true
244
- reject(new Error('AionUi server failed to start within 20s'))
271
+ const fail = (reason) => {
272
+ if (settled) return
273
+ settled = true
274
+ clearInterval(progressTick)
275
+ const tail = logTail.trim()
276
+ let msg = reason
277
+ if (tail) {
278
+ msg += `\n\n --- last AionUi output (stderr+stdout tail) ---\n${tail
279
+ .split(/\r?\n/)
280
+ .map((line) => ` ${line}`)
281
+ .join('\n')}\n ------------------------------------------------`
282
+ } else {
283
+ msg += '\n (no output captured from AionUi — check ' +
284
+ (process.platform === 'win32' ? 'Windows Defender / antivirus quarantining bun.exe' : 'bun or runtime corruption') +
285
+ ')'
245
286
  }
246
- }, 20_000)
247
- // Poll /health-ish endpoint to detect readiness.
287
+ reject(new Error(msg))
288
+ }
289
+
290
+ // Progress log every 10s, so user knows we're not hung. On Windows first
291
+ // launch (Defender scanning bun + bun JIT + sqlite init), 30-50s is normal.
292
+ const progressTick = setInterval(() => {
293
+ if (settled) return
294
+ const waited = Math.round((Date.now() - startedAt) / 1000)
295
+ const hint = process.platform === 'win32'
296
+ ? ' (Windows first-launch can take up to 60s while Defender scans bun.exe + server.mjs)'
297
+ : ''
298
+ console.log(chalk.gray(` still starting AionUi — waited ${waited}s…${hint}`))
299
+ }, 10_000)
300
+
301
+ const timer = setTimeout(() => {
302
+ fail(`AionUi server failed to start within ${Math.round(AIONUI_STARTUP_TIMEOUT_MS / 1000)}s`)
303
+ }, AIONUI_STARTUP_TIMEOUT_MS)
304
+
305
+ // ── Readiness poll ─────────────────────────────────────────────────────
248
306
  const pollReady = () => {
307
+ if (settled) return
249
308
  const req = http.request(
250
309
  { host: '127.0.0.1', port, path: '/', method: 'HEAD', timeout: 500 },
251
310
  (res) => {
@@ -267,17 +326,17 @@ function spawnAionUiServer({ bunPath, runtimeDir, port }) {
267
326
  // Start polling after a short grace period so bun has time to boot.
268
327
  setTimeout(pollReady, 600)
269
328
 
270
- // Surface logs with prefix (muted by default; set HS_WEB_DEBUG=1 to see)
271
- const debug = process.env.HS_WEB_DEBUG === '1'
272
- if (debug) {
273
- child.stdout.on('data', (d) => process.stdout.write(chalk.gray(`[aionui] ${d}`)))
274
- child.stderr.on('data', (d) => process.stderr.write(chalk.gray(`[aionui] ${d}`)))
275
- }
276
- child.on('exit', (code) => {
277
- if (!settled) {
278
- settled = true
279
- reject(new Error(`AionUi server exited with code ${code} before becoming ready`))
280
- }
329
+ // ── Early exit ─────────────────────────────────────────────────────────
330
+ child.on('exit', (code, signal) => {
331
+ const reason = signal
332
+ ? `AionUi server exited with signal ${signal} before becoming ready`
333
+ : `AionUi server exited with code ${code} before becoming ready`
334
+ clearTimeout(timer)
335
+ fail(reason)
336
+ })
337
+ child.on('error', (err) => {
338
+ clearTimeout(timer)
339
+ fail(`AionUi server spawn error: ${err.message || err}`)
281
340
  })
282
341
  })
283
342
  }
@@ -372,7 +431,18 @@ async function startAionUiMode(opts) {
372
431
  try {
373
432
  aionuiProc = await spawnAionUiServer({ bunPath, runtimeDir: runtime.dir, port })
374
433
  } catch (e) {
375
- console.log(chalk.red(`✗ AionUi server failed to start: ${e.message}`))
434
+ // `e.message` may now be multi-line (includes bun/AionUi stderr tail).
435
+ // Print the first line in red (the reason), then any extra lines verbatim
436
+ // so the user can see the real bun/AionUi error and paste it back to us.
437
+ const [firstLine, ...rest] = String(e.message).split(/\r?\n/)
438
+ console.log(chalk.red(`✗ AionUi server failed to start: ${firstLine}`))
439
+ for (const line of rest) {
440
+ if (line.trim()) console.log(chalk.gray(line))
441
+ }
442
+ console.log()
443
+ console.log(chalk.gray(' Tip: run again with HS_WEB_DEBUG=1 to stream AionUi logs live.'))
444
+ console.log(chalk.gray(' If this is a first launch on Windows, try once more — Defender'))
445
+ console.log(chalk.gray(' will cache bun.exe + server.mjs after the first scan.'))
376
446
  if (opts.aionui) process.exit(1)
377
447
  console.log(chalk.yellow(' Falling back to legacy workspace.'))
378
448
  return startLegacyMode(opts)
@@ -759,6 +759,73 @@ function buildModelsResponse(config) {
759
759
  }
760
760
  }
761
761
 
762
+ // ── Live HolySheep model list for OpenClaw ───────────────────────────────────
763
+ // OpenClaw / AcpModelSelector hits `/v1/models` to populate its model dropdown.
764
+ // Historically this returned only the static `config.models` from
765
+ // `~/.openclaw/holysheep-bridge.json`, so users saw a stale hand-curated list.
766
+ // We now fetch the full live catalog from the HolySheep API once per bridge
767
+ // process (60s TTL) and merge it with the user's preferred models. The
768
+ // config.models always wins on ordering — new entries from upstream are
769
+ // appended afterwards so existing users don't see their default model jump.
770
+ //
771
+ // Env opt-out: HOLYSHEEP_BRIDGE_NO_LIVE_MODELS=1 keeps the old static behavior.
772
+ let _liveModelsCache = { at: 0, ids: null }
773
+ const LIVE_MODELS_TTL_MS = 60_000
774
+
775
+ async function fetchLiveHolySheepModels(config) {
776
+ if (process.env.HOLYSHEEP_BRIDGE_NO_LIVE_MODELS === '1') return null
777
+ const now = Date.now()
778
+ if (_liveModelsCache.ids && now - _liveModelsCache.at < LIVE_MODELS_TTL_MS) {
779
+ return _liveModelsCache.ids
780
+ }
781
+ const base = (config.baseUrlOpenAI || 'https://api.holysheep.ai/v1').replace(/\/+$/, '')
782
+ const url = `${base}/models`
783
+ const apiKey = config.apiKey
784
+ if (!apiKey) return null
785
+ try {
786
+ const resp = await upstreamFetch(url, {
787
+ method: 'GET',
788
+ headers: { authorization: `Bearer ${apiKey}` },
789
+ })
790
+ if (!resp.ok) return null
791
+ const body = await resp.json()
792
+ const ids = Array.isArray(body?.data)
793
+ ? body.data.map((m) => (m && typeof m.id === 'string' ? m.id : null)).filter(Boolean)
794
+ : null
795
+ if (ids && ids.length) {
796
+ _liveModelsCache = { at: now, ids }
797
+ return ids
798
+ }
799
+ } catch {
800
+ // Network error / upstream 5xx — fall back to static config.models.
801
+ }
802
+ return null
803
+ }
804
+
805
+ async function buildLiveModelsResponse(config) {
806
+ const live = await fetchLiveHolySheepModels(config)
807
+ const configured = config.models || []
808
+ if (!live) return buildModelsResponse(config)
809
+ // Merge: preserve user's preferred ordering from config.models (typically
810
+ // the models they actively use for OpenClaw), then append new live entries.
811
+ const seen = new Set(configured)
812
+ const merged = [...configured]
813
+ for (const id of live) {
814
+ if (!seen.has(id)) {
815
+ merged.push(id)
816
+ seen.add(id)
817
+ }
818
+ }
819
+ return {
820
+ object: 'list',
821
+ data: merged.map((model) => ({
822
+ id: model,
823
+ object: 'model',
824
+ owned_by: 'holysheep',
825
+ })),
826
+ }
827
+ }
828
+
762
829
  function isProcessAlive(pid) {
763
830
  if (!Number.isInteger(pid) || pid <= 0) return null
764
831
  try {
@@ -29,11 +29,14 @@ const VENDOR_DIR = path.join(__dirname, 'vendor', 'aionui')
29
29
 
30
30
  // Baked-in defaults — updated with every 2.x.0 release that bumps the bundle.
31
31
  // Override via HOLYSHEEP_AIONUI_RUNTIME_URL / HOLYSHEEP_AIONUI_RUNTIME_SHA256.
32
+ // hs2 = HolySheep rebrand + Guid CLI card (2.1.5, 2026-04-21). Previous
33
+ // revisions still live on the CDN for forensics, but all new installs of
34
+ // @simonyea/holysheep-cli >= 2.1.5 pull this tarball.
32
35
  const DEFAULT_RUNTIME_URL =
33
- 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep.tar.gz'
36
+ 'https://mail.holysheep.ai/app/cli/aionui-runtime-v1.9.18-holysheep-hs2.tar.gz'
34
37
  const DEFAULT_RUNTIME_SHA256 =
35
- '379ae2a523542c0be55a84abbec5cd1db31684300c66db8aa35c4a02d38e9cb1'
36
- const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep'
38
+ '869d9c49b1902d6305606212b8bb5af0f7b331b311f998f2d86ec38849c8ed5b'
39
+ const DEFAULT_RUNTIME_VERSION = '1.9.18-holysheep-hs2'
37
40
 
38
41
  function isValidRuntimeDir(dir) {
39
42
  if (!dir) return false