sintbot 1.4.16 → 1.4.17

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.
@@ -180,21 +180,206 @@ async function fetchRaw(token, { ttl = 60_000, now = Date.now, log = warn, curl
180
180
  return data
181
181
  }
182
182
 
183
+ // ── 令牌过期兜底(2026-11-12,cc-switch takeaways B3)──────────────────────────────
184
+ // 纪律 2 不变:我们**不刷新**令牌。过期时让 Claude CLI 自己在自己的锁里刷新 —— 跑一次
185
+ // `claude -p /usage`(官方的非交互用量面板),它顺手把额度写进 `~/.claude.json` 的
186
+ // `cachedUsageUtilization`,我们只读那份缓存,交给核心 `usage_parse('claude','cached')` 判
187
+ //(当前 CLI 使用 fetchedAtMs + utilization.limits;旧 limits[] 也保留支持,超过 645 秒判旧)。
188
+ // 约束:
189
+ // · **不阻塞调用方**:需要刷新时在后台起跑(进程内一次一个),本次立刻回 null(或在跑期间回
190
+ // 上一次解析出的值);之后的调用再读结果。/hud、/api/claude/verify 都是秒级回复。
191
+ // · **只信本进程跑成功后的文件**:退出码 0 才记一次「刷新完成时刻」,读文件以它为准;文件
192
+ // 里没有 fetchedAt 时就用这个完成时刻 —— 别的进程(交互式 CLI)写的旧缓存不当新数。
193
+ // 跑失败 / 超时 ⇒ null。
194
+ // · 同账号(按凭据文件路径)10 分钟最多跑一次 · 45 秒后 SIGTERM、再 5 秒 SIGKILL,子进程
195
+ // 没 close 之前不会起下一个 · 旗子探测合成一次(短超时、有缓存),老 CLI 不认 ⇒ 跳过。
196
+ // · env 走 buildEnv({}) 再去掉 ANTHROPIC_API_KEY(只认 OAuth 登录态,别让 /usage 打到 API key
197
+ // 或第三方网关)· cwd 用临时目录(别让它读到哪个项目的 CLAUDE.md / .mcp.json)· 输出不看不记。
198
+ const FALLBACK_TIMEOUT_MS = 45_000
199
+ const FALLBACK_KILL_GRACE_MS = 5_000
200
+ const FALLBACK_INTERVAL_MS = 10 * 60_000
201
+ const FALLBACK_FLAG_PROBE_MS = 2_000
202
+ const FALLBACK_ARGS = Object.freeze(['-p', '/usage', '--output-format', 'json', '--tools', '', '--strict-mcp-config', '--setting-sources', '', '--no-session-persistence'])
203
+ const FALLBACK_FLAGS = Object.freeze(['--tools', '--strict-mcp-config', '--setting-sources', '--no-session-persistence'])
204
+ const USAGE_HELP_INFLIGHT = new Map()
205
+
206
+ // Some native CLI builds exit before a long help page drains through a pipe.
207
+ // A private regular file receives the complete page. The four flag checks share
208
+ // one bounded process; this temporary file contains help text only.
209
+ function supportsUsageFlag(bin, flag) {
210
+ let probe = USAGE_HELP_INFLIGHT.get(bin)
211
+ if (!probe) {
212
+ probe = (async () => {
213
+ const dir = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'sintbot-usage-help-'))
214
+ const file = path.join(dir, 'help.txt')
215
+ let fd
216
+ try {
217
+ fd = fs.openSync(file, 'wx', 0o600)
218
+ const ok = await new Promise(resolve => {
219
+ let child, timer, force, settled = false
220
+ const finish = value => { if (settled) return; settled = true; clearTimeout(timer); clearTimeout(force); resolve(value) }
221
+ try {
222
+ const command = require('./winexec').resolveSpawn(bin, ['--help'])
223
+ child = require('node:child_process').spawn(command.file, command.spawnArgs,
224
+ { cwd: dir, stdio: ['ignore', fd, 'ignore'], windowsHide: true })
225
+ } catch { return finish(false) }
226
+ child.once('error', () => finish(false))
227
+ child.once('close', code => finish(code === 0))
228
+ timer = setTimeout(() => {
229
+ try { child.kill('SIGTERM') } catch {}
230
+ force = setTimeout(() => { try { child.kill('SIGKILL') } catch {} }, 1000)
231
+ force.unref?.()
232
+ }, 5000)
233
+ timer.unref?.()
234
+ })
235
+ if (!ok || fs.statSync(file).size > 1024 * 1024) return null
236
+ return await fs.promises.readFile(file, 'utf8')
237
+ } finally {
238
+ if (fd !== undefined) fs.closeSync(fd)
239
+ await fs.promises.rm(dir, { recursive: true, force: true })
240
+ }
241
+ })().catch(() => null)
242
+ USAGE_HELP_INFLIGHT.set(bin, probe)
243
+ probe.finally(() => { if (USAGE_HELP_INFLIGHT.get(bin) === probe) USAGE_HELP_INFLIGHT.delete(bin) })
244
+ }
245
+ return probe.then(text => text === null ? null : text.includes(flag))
246
+ }
247
+ const FALLBACK = {
248
+ inflight: null, // 正在跑的那次(Promise<boolean>);子进程 close 前不清
249
+ lastAt: new Map(), // 凭据文件路径 -> 上次起跑时刻(节流)
250
+ doneAt: new Map(), // 凭据文件路径 -> 本进程最近一次退出码 0 的完成时刻(没有 = 不信文件)
251
+ lastGood: new Map(), // 凭据文件路径 -> 上一次解析出的一行(只在刷新进行中回它)
252
+ flags: null, // { bin, until, p } 合成一次的旗子探测
253
+ file: null, // { path, mtimeMs, body } 按 mtime 缓存的 ~/.claude.json 解析
254
+ }
255
+
256
+ // `~/.claude.json` 原文 → 交给核心的 body(cachedUsageUtilization 原样)。不是对象 ⇒ null。
257
+ // 时效优先用对象自带的 fetchedAtMs/fetchedAt;两者都没有才用本进程刷新完成时刻。
258
+ function readCachedUtilization(raw) {
259
+ let j = null
260
+ try { j = JSON.parse(String(raw || '')) } catch { return null }
261
+ const u = j && j.cachedUsageUtilization
262
+ return u && typeof u === 'object' && !Array.isArray(u) ? u : null
263
+ }
264
+
265
+ // 按 mtime 缓存的异步读:同一份文件只解析一次;stat/读失败 ⇒ null。
266
+ async function readClaudeJson(file, { readFile, stat }) {
267
+ let st = null
268
+ try { st = await stat(file) } catch { return null }
269
+ const mtimeMs = st && typeof st.mtimeMs === 'number' ? st.mtimeMs : null
270
+ const c = FALLBACK.file
271
+ if (c && c.path === file && mtimeMs !== null && c.mtimeMs === mtimeMs) return c.body
272
+ let raw = null
273
+ try { raw = await readFile(file, 'utf8') } catch { return null }
274
+ const body = readCachedUtilization(raw)
275
+ FALLBACK.file = { path: file, mtimeMs, body }
276
+ return body
277
+ }
278
+
279
+ // 核心的 windows[] → parseLimits 的形状,formatClaudeUsage 原样可用。
280
+ const windowsToLimits = (windows) => (Array.isArray(windows) ? windows : [])
281
+ .map((w) => ({ kind: w.kind, percent: w.percentUsed, resetsAt: w.resetsAt ?? null, scopeName: w.scopeName || '' }))
282
+
283
+ // 四个旗子合成**一次**探测:默认共享同一份完整 --help 输出,测试可注入探测函数。
284
+ // 短超时兜底;结论 true/false 缓存 10 分钟、不知道(null)缓存 60 秒。
285
+ function probeFlags(bin, supportsFlag, now, timeoutMs = FALLBACK_FLAG_PROBE_MS) {
286
+ const hit = FALLBACK.flags
287
+ if (hit && hit.bin === bin && now() < hit.until) return hit.p
288
+ const all = Promise.all(FALLBACK_FLAGS.map((f) => Promise.resolve().then(() => supportsFlag(bin, f))))
289
+ .then((a) => (a.every((v) => v === true) ? true : a.some((v) => v === false) ? false : null))
290
+ const late = new Promise((resolve) => { const t = setTimeout(() => resolve(null), timeoutMs); t.unref?.() })
291
+ const p = Promise.race([all, late]).catch(() => null)
292
+ FALLBACK.flags = { bin, until: now() + 60_000, p }
293
+ p.then((v) => { if (FALLBACK.flags && FALLBACK.flags.p === p) FALLBACK.flags.until = now() + (v === null ? 60_000 : 10 * 60_000) })
294
+ return p
295
+ }
296
+
297
+ // 跑一次 `claude -p /usage`;只在子进程 close(或起不来)后才落定 ⇒ 卡住的子进程不会与下一次重叠。
298
+ // 超时:SIGTERM,再过 killGraceMs 还没 close 就 SIGKILL;两个计时器都 unref。
299
+ function runUsageRefresh({ bin, spawn, buildEnv, tmpdir, log, timeoutMs, killGraceMs = FALLBACK_KILL_GRACE_MS }) {
300
+ return new Promise((resolve) => {
301
+ let child, settled = false, timedOut = false
302
+ const timers = []
303
+ const done = (ok, why) => { if (settled) return; settled = true; for (const t of timers) clearTimeout(t); if (why) log(why); resolve(ok) }
304
+ const later = (ms, fn) => { const t = setTimeout(fn, ms); t.unref?.(); timers.push(t) }
305
+ try {
306
+ const env = buildEnv({})
307
+ delete env.ANTHROPIC_API_KEY // 只认 OAuth 登录态
308
+ const { file, spawnArgs } = require('./winexec').resolveSpawn(bin, [...FALLBACK_ARGS])
309
+ child = spawn(file, spawnArgs, { cwd: tmpdir(), env, stdio: ['ignore', 'ignore', 'ignore'], windowsHide: true })
310
+ } catch (e) { return done(false, '额度兜底:claude 起不来(' + ((e && e.message) || '未知错误') + ')') }
311
+ later(timeoutMs, () => {
312
+ timedOut = true
313
+ try { child.kill('SIGTERM') } catch {}
314
+ later(killGraceMs, () => { try { child.kill('SIGKILL') } catch {} })
315
+ })
316
+ child.on('error', (e) => done(false, '额度兜底:claude 起不来(' + ((e && e.code) || (e && e.message) || '未知错误') + ')'))
317
+ child.on('close', (code) => {
318
+ if (timedOut) return done(false, `额度兜底:claude -p /usage 超时(${timeoutMs}ms),已终止`)
319
+ done(code === 0, code === 0 ? null : `额度兜底:claude -p /usage 退出码 ${code}`)
320
+ })
321
+ })
322
+ }
323
+
324
+ // 需要就在后台起一次刷新(不 await):进程内一次一个,同账号 10 分钟一次。
325
+ function startRefreshIfDue(account, { bin, now, log, spawn, supportsFlag, buildEnv, tmpdir, timeoutMs, killGraceMs, intervalMs, flagProbeMs }) {
326
+ if (FALLBACK.inflight) return
327
+ const last = FALLBACK.lastAt.get(account) || 0
328
+ if (last && now() - last < intervalMs) return
329
+ FALLBACK.lastAt.set(account, now())
330
+ FALLBACK.inflight = (async () => {
331
+ const flags = await probeFlags(bin, supportsFlag, now, flagProbeMs)
332
+ if (flags !== true) { log('额度兜底:本机 claude 不认所需旗子,跳过'); return false }
333
+ const ok = await runUsageRefresh({ bin, spawn, buildEnv, tmpdir, log, timeoutMs, killGraceMs })
334
+ if (ok) FALLBACK.doneAt.set(account, now())
335
+ else FALLBACK.doneAt.delete(account)
336
+ return ok
337
+ })().catch(() => { FALLBACK.doneAt.delete(account); return false })
338
+ .finally(() => { FALLBACK.inflight = null })
339
+ }
340
+
341
+ // 过期/缺凭据时的那条腿:(必要时)后台让 CLI 刷新 → 读缓存(只信本进程刷成功后的)→ 核心判 →
342
+ // 一行人话;任何一步不行 ⇒ null。调用方不会被挡住:这里只 await 一次文件读。
343
+ async function cachedUsageFallback({ home = os.homedir(), bin = 'claude', now = Date.now, log = warn,
344
+ spawn = require('node:child_process').spawn, readFile = fs.promises.readFile, stat = fs.promises.stat, tmpdir = os.tmpdir,
345
+ supportsFlag = null, buildEnv = null, timeoutMs = FALLBACK_TIMEOUT_MS, killGraceMs = FALLBACK_KILL_GRACE_MS,
346
+ intervalMs = FALLBACK_INTERVAL_MS, flagProbeMs = FALLBACK_FLAG_PROBE_MS } = {}) {
347
+ const account = path.join(home, '.claude', '.credentials.json')
348
+ startRefreshIfDue(account, { bin, now, log, spawn, tmpdir, timeoutMs, killGraceMs, intervalMs, flagProbeMs,
349
+ supportsFlag: supportsFlag || supportsUsageFlag, buildEnv: buildEnv || require('./claudecode').buildEnv })
350
+ const doneAt = FALLBACK.doneAt.get(account)
351
+ if (!doneAt) return FALLBACK.inflight ? (FALLBACK.lastGood.get(account) || null) : null
352
+ const raw = await readClaudeJson(path.join(home, '.claude.json'), { readFile, stat })
353
+ if (!raw) return null
354
+ const body = raw.fetchedAtMs != null || raw.fetchedAt != null ? raw : { ...raw, fetchedAt: doneAt }
355
+ const parsed = require('./privatecore').usageParse('claude', 'cached', null, body, now())
356
+ if (!parsed.ok) return FALLBACK.inflight ? (FALLBACK.lastGood.get(account) || null) : null
357
+ const line = formatClaudeUsage(windowsToLimits(parsed.windows), now())
358
+ if (line) FALLBACK.lastGood.set(account, line)
359
+ return line
360
+ }
361
+ function resetFallback() { FALLBACK.inflight = null; FALLBACK.lastAt.clear(); FALLBACK.doneAt.clear(); FALLBACK.lastGood.clear(); FALLBACK.flags = null; FALLBACK.file = null }
362
+ // 单测用:等后台那次刷新落定(没有就立刻回)。
363
+ const settleFallback = () => (FALLBACK.inflight ? FALLBACK.inflight.then(() => undefined) : Promise.resolve())
364
+
183
365
  // 拉一次 → 一行人话;拿不到 null。走 fetchRaw ⇒ 与告警共享那份缓存。
184
366
  // 缓存口径与 fetchRaw 一致:成功 60s、失败 10s(两层都压死一分钟就白搭下层的重试了)。
367
+ // 凭据缺失/过期 ⇒ 走 cachedUsageFallback(见上),不再直接 null。
185
368
  const CACHE = { at: 0, val: null }
186
369
  const TTL = 60_000
187
- async function usageLine({ ttl = TTL, now = Date.now, readToken = readAccessToken, ...io } = {}) {
370
+ async function usageLine({ ttl = TTL, now = Date.now, readToken = readAccessToken, fallback = cachedUsageFallback, home, bin, ...io } = {}) {
188
371
  if (CACHE.at && now() - CACHE.at < (CACHE.val ? ttl : FAIL_TTL_MS)) return CACHE.val
189
372
  let val = null
190
373
  const token = readToken()
191
374
  if (token) {
192
- const data = await fetchRaw(token, { ttl, now, ...io })
375
+ const data = await fetchRaw(token, { ttl, now, log: io.log, curl: io.curl, sleep: io.sleep })
193
376
  val = formatClaudeUsage(parseLimits(data), now())
377
+ } else {
378
+ try { val = await fallback({ now, ...(home ? { home } : {}), ...(bin ? { bin } : {}), ...io }) } catch { val = null }
194
379
  }
195
380
  CACHE.at = now(); CACHE.val = val
196
381
  return val
197
382
  }
198
383
  function clearCache() { CACHE.at = 0; CACHE.val = null; RAW.at = 0; RAW.data = null }
199
384
 
200
- module.exports = { usageLine, fetchRaw, clearCache, parseCredentials, parseLimits, formatClaudeUsage, readAccessToken, fetchCurl, fetchNative, readCurlResult, fmtDurDecimal, QUOTA_SEP, TIMEOUT_MS, FAIL_TTL_MS, RETRY_WAIT_MS }
385
+ module.exports = { usageLine, fetchRaw, clearCache, parseCredentials, parseLimits, formatClaudeUsage, readAccessToken, fetchCurl, fetchNative, readCurlResult, fmtDurDecimal, cachedUsageFallback, readCachedUtilization, readClaudeJson, supportsUsageFlag, resetFallback, settleFallback, QUOTA_SEP, TIMEOUT_MS, FAIL_TTL_MS, RETRY_WAIT_MS, FALLBACK_ARGS, FALLBACK_FLAGS, FALLBACK_TIMEOUT_MS, FALLBACK_KILL_GRACE_MS, FALLBACK_INTERVAL_MS, FALLBACK_FLAG_PROBE_MS }
@@ -0,0 +1,74 @@
1
+ 'use strict'
2
+
3
+ // Environment conflicts (2026-11-12, cc-switch takeaways B5): things outside
4
+ // SintBot's configuration that can silently reroute the Claude / Codex CLI —
5
+ // daemon env (`ANTHROPIC_*`, `CLAUDE_CODE_*`, `AWS_*`, `VERTEX_*`, `OPENAI_*`),
6
+ // `~/.claude/settings.json` (`env`, top-level `model` …) and `~/.codex/config.toml`
7
+ // (`model_provider`, `[model_providers.*]`). The host only gathers the inputs; the
8
+ // rule set lives in the binary core (`env_conflicts`, ABI 36) and it returns key
9
+ // names only — values never leave the process. Warn-only: `buildEnv` keeps clearing
10
+ // exactly what it cleared before.
11
+
12
+ const fs = require('node:fs')
13
+ const os = require('node:os')
14
+ const path = require('node:path')
15
+ const { envConflicts } = require('./privatecore')
16
+
17
+ // JSON file → parsed object; missing / invalid / not an object ⇒ null.
18
+ function readClaudeSettings(home, readFile) {
19
+ try {
20
+ const j = JSON.parse(String(readFile(path.join(home, '.claude', 'settings.json'), 'utf8')))
21
+ return j && typeof j === 'object' && !Array.isArray(j) ? j : null
22
+ } catch { return null }
23
+ }
24
+
25
+ // TOML text as-is (the core matches lines; no TOML parser); missing ⇒ null.
26
+ function readCodexConfig(home, readFile) {
27
+ try { return String(readFile(path.join(home, '.codex', 'config.toml'), 'utf8')) } catch { return null }
28
+ }
29
+
30
+ // Only string-valued entries travel to the core (process.env can hold undefined).
31
+ const snapshotEnv = (env) => Object.fromEntries(Object.entries(env || {}).filter(([, v]) => typeof v === 'string'))
32
+
33
+ // → [{ where: 'env'|'claude-settings'|'codex-config', key, reason }]. `provider` is a
34
+ // resolved provider ({ baseUrl, apiKey }) or an explicit { baseUrl, hasApiKey }.
35
+ function collectEnvConflicts({ runner = 'claude', provider = {}, env = process.env, home = os.homedir(), readFile = fs.readFileSync } = {}) {
36
+ const p = provider || {}
37
+ const input = {
38
+ runner: runner === 'codex' ? 'codex' : 'claude',
39
+ provider: { baseUrl: p.baseUrl || null, hasApiKey: typeof p.hasApiKey === 'boolean' ? p.hasApiKey : !!p.apiKey },
40
+ env: snapshotEnv(env),
41
+ claudeSettings: readClaudeSettings(home, readFile),
42
+ codexConfig: readCodexConfig(home, readFile),
43
+ }
44
+ return envConflicts(input).warnings.map((w) => ({ where: w.where, key: w.key, reason: w.reason }))
45
+ }
46
+
47
+ // Preflight / log line: `<key> — <reason>`; never a value.
48
+ const describeWarning = (w) => `${w.key} — ${w.reason}`
49
+
50
+ // `sintbot start --preflight`: one line per key for each provider an active route
51
+ // uses. The snapshot is this process's environment — a launchd job inherits a
52
+ // different one, so the block is prefixed with that caveat. Never fatal.
53
+ const PREFLIGHT_SCOPE_LINE = 'preflight · 环境冲突检查基于当前终端环境(launchd 任务的环境可能不同)\n'
54
+ function reportEnvConflicts({ cfg, routes } = {}, write = (message) => process.stderr.write(message), { collect = collectEnvConflicts } = {}) {
55
+ const seen = new Set()
56
+ let prefixed = false
57
+ for (const route of routes || []) {
58
+ if (!route || route.paused) continue
59
+ const name = route.group?.provider || cfg?.defaultProvider || ''
60
+ if (!name || seen.has(name)) continue
61
+ seen.add(name)
62
+ if (!prefixed) { prefixed = true; write(PREFLIGHT_SCOPE_LINE) }
63
+ try {
64
+ for (const warning of collect({ runner: route.provider?.runner, provider: route.provider })) {
65
+ write(`preflight · ${name}: ${describeWarning(warning)}\n`)
66
+ }
67
+ } catch (error) {
68
+ write(`preflight · ${name}: environment check skipped (${error?.message || String(error)})\n`)
69
+ }
70
+ }
71
+ return [...seen]
72
+ }
73
+
74
+ module.exports = { collectEnvConflicts, describeWarning, reportEnvConflicts, PREFLIGHT_SCOPE_LINE }
@@ -1,9 +1,9 @@
1
- // ABI v35 adapter for the private binary decision core. Manifest assembly still stays in JS.
1
+ // ABI v36 adapter for the private binary decision core. Manifest assembly still stays in JS.
2
2
  const fs = require('node:fs')
3
3
  const path = require('node:path')
4
4
  const { createHash } = require('node:crypto')
5
5
 
6
- const EXPECTED_SHA256 = 'a7ad2bd9210906ee6f5d7893ed3d9632769b6e2dbec285c1102492e3ba1257b5'
6
+ const EXPECTED_SHA256 = 'c79f12ceb1ef79aceb0ab54cf87052127325a1d8fdfd5c02e3509fef39f6f511'
7
7
  let bytes
8
8
  try {
9
9
  bytes = fs.readFileSync(path.join(__dirname, 'privatecore.wasm'))
@@ -26,7 +26,7 @@ try {
26
26
  } catch (cause) {
27
27
  throw new Error('SintBot binary core cannot start on this runtime', { cause })
28
28
  }
29
- if (core.abi_version() !== 35) throw new Error('SintBot binary core ABI mismatch')
29
+ if (core.abi_version() !== 36) throw new Error('SintBot binary core ABI mismatch')
30
30
  const encoder = new TextEncoder()
31
31
  const decoder = new TextDecoder('utf-8', { fatal: true })
32
32
 
@@ -771,6 +771,67 @@ function manifestPolicy(permissionMode, isCodex, allowedTools, disallowedTools)
771
771
  return value
772
772
  }
773
773
 
774
+ // ABI 36 JSON decisions (sintbot-core docs/ABI36_CONTRACTS.md). Each wrapper only
775
+ // checks the envelope shape the host relies on; the core owns the rules and wording.
776
+ const isObject = value => !!value && typeof value === 'object' && !Array.isArray(value)
777
+
778
+ function connectManifest(basic, userName, version) {
779
+ const value = coreJson('connect_manifest', { basic: !!basic, userName: String(userName || ''), version: String(version || '') }, 'build the Feishu connect manifest')
780
+ if (!isObject(value) || !isObject(value.appPreset) || !isObject(value.addons) ||
781
+ !Array.isArray(value.addons.scopes) || !Array.isArray(value.addons.events) ||
782
+ !Array.isArray(value.addons.callbacks) || typeof value.hint !== 'string') {
783
+ throw new Error('SintBot binary core returned an invalid connect manifest')
784
+ }
785
+ return value
786
+ }
787
+
788
+ function connectPlan(step, { basic = false, status = null, code = null, msg = '', observed = {} } = {}) {
789
+ const value = coreJson('connect_plan', { step, basic: !!basic, status, code, msg: String(msg || ''), observed: isObject(observed) ? observed : {} }, `decide the connect step ${step}`)
790
+ if (!isObject(value) || !['ok', 'stop', 'wait', 'warn'].includes(value.verdict) || typeof value.notice !== 'string' ||
791
+ !(value.next === null || typeof value.next === 'string') ||
792
+ !(value.retryAfterMs === null || Number.isSafeInteger(value.retryAfterMs))) {
793
+ throw new Error('SintBot binary core returned an invalid connect decision')
794
+ }
795
+ return value
796
+ }
797
+
798
+ function connectConfig(input) {
799
+ const value = coreJson('connect_config', input, 'build the Feishu connect configuration')
800
+ if (!isObject(value) || typeof value.ok !== 'boolean' ||
801
+ (value.ok && (!isObject(value.config) || typeof value.readme !== 'string')) ||
802
+ (!value.ok && typeof value.error !== 'string')) {
803
+ throw new Error('SintBot binary core returned an invalid connect configuration')
804
+ }
805
+ return value
806
+ }
807
+
808
+ function qrMatrix(text, ec = 'M') {
809
+ const value = coreJson('qr_matrix', { text: String(text), ec }, 'encode a QR code')
810
+ if (!isObject(value) || !Number.isInteger(value.size) || !Array.isArray(value.rows) ||
811
+ value.rows.length !== value.size || value.rows.some(row => typeof row !== 'string' || row.length !== value.size)) {
812
+ throw new Error('SintBot binary core returned an invalid QR matrix')
813
+ }
814
+ return value
815
+ }
816
+
817
+ function usageParse(provider, source, status, body, now = Date.now()) {
818
+ const value = coreJson('usage_parse', { provider, source, status, body, now }, `parse ${provider} ${source} usage`)
819
+ if (!isObject(value) || typeof value.ok !== 'boolean' ||
820
+ (value.ok && !Array.isArray(value.windows)) || (!value.ok && typeof value.error !== 'string')) {
821
+ throw new Error('SintBot binary core returned an invalid usage result')
822
+ }
823
+ return value
824
+ }
825
+
826
+ function envConflicts(input) {
827
+ const value = coreJson('env_conflicts', input, 'check environment conflicts')
828
+ if (!isObject(value) || !Array.isArray(value.warnings) ||
829
+ value.warnings.some(w => !isObject(w) || typeof w.key !== 'string' || typeof w.where !== 'string' || typeof w.reason !== 'string')) {
830
+ throw new Error('SintBot binary core returned an invalid environment check')
831
+ }
832
+ return value
833
+ }
834
+
774
835
  // A trap or exhausted stack can leave the instance's own stack pointer wrong, and
775
836
  // C1 calls hold no state, so the next call simply gets a fresh instance.
776
837
  function guarded(fn) {
@@ -798,7 +859,7 @@ function followupCall(name, value) {
798
859
  function followupEnabled(group, runner) { return followupCall('followup_policy', { group, runner }) }
799
860
  function claudeFollowupStep(state, event) { return followupCall('claude_followup_step', { state, event }) }
800
861
 
801
- const entries = { followupEnabled, claudeFollowupStep, classifyError, routeBackend, sandboxTier, appErrorOverride, appNormalizeItem, appStreamTail, appNoticeAction, appDeltaNext, appLandedAction, appCompletionAction, appCompletionNoticeAction, manifestFeatures, manifestHeader, manifestContext, manifestSkills, manifestStyleBoard, manifestImagenAsset, manifestRuntime, manifestPolicy, systemAppendix, codexEffortRejection, wecomGatePrompt, imagenPrompt, turnPrompt, compactEvent, engineQueueStep, engineSlotsStep, engineTurnStep, engineSessionStep, engineHandoffStep, engineIntakeStep, engineLifecycleStep, engineTerminalStep, engineSteerStep, engineRecoveryStep, engineFailoverStep, parseResetFields, chooseResetEpoch, codexBuildPrompt, codexBuildArgs, appUsageFromTotal, settleUsage, appBuildRequest, claudeBaseSkillAllows, claudeBuildArgs, claudePluginAllowRules, claudeNormalizeEvent, codexNormalizeEvent, codexParseError }
862
+ const entries = { followupEnabled, claudeFollowupStep, classifyError, routeBackend, sandboxTier, appErrorOverride, appNormalizeItem, appStreamTail, appNoticeAction, appDeltaNext, appLandedAction, appCompletionAction, appCompletionNoticeAction, manifestFeatures, manifestHeader, manifestContext, manifestSkills, manifestStyleBoard, manifestImagenAsset, manifestRuntime, manifestPolicy, systemAppendix, codexEffortRejection, wecomGatePrompt, imagenPrompt, turnPrompt, compactEvent, engineQueueStep, engineSlotsStep, engineTurnStep, engineSessionStep, engineHandoffStep, engineIntakeStep, engineLifecycleStep, engineTerminalStep, engineSteerStep, engineRecoveryStep, engineFailoverStep, parseResetFields, chooseResetEpoch, codexBuildPrompt, codexBuildArgs, appUsageFromTotal, settleUsage, appBuildRequest, claudeBaseSkillAllows, claudeBuildArgs, claudePluginAllowRules, claudeNormalizeEvent, codexNormalizeEvent, codexParseError, connectManifest, connectPlan, connectConfig, qrMatrix, usageParse, envConflicts }
802
863
  for (const name of Object.keys(entries)) entries[name] = guarded(entries[name])
803
864
 
804
865
  module.exports = { ...entries, engineStep, resetBinaryCore, HUMAN_BY_KIND: HUMAN }
Binary file
package/src/c2install.js CHANGED
@@ -323,6 +323,6 @@ function registerAddedGroups(installation) {
323
323
  return addedRoutes.map(route => route.chatId)
324
324
  }
325
325
 
326
- module.exports = { registerAddedGroups, loadInstallation, loadInstallationFromRaw, initializeInstallation, assertPrivateDirectory,
326
+ module.exports = { registerAddedGroups, loadInstallation, loadInstallationFromRaw, initializeInstallation, installationStateDir, assertPrivateDirectory,
327
327
  assertJournalScope, assertInstallationJournals, assertRunnerExecutable, SUPPORTED_RUNNERS, MANIFEST,
328
328
  VALIDATORS }