sintbot 1.0.16 → 1.3.0

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.
Files changed (137) hide show
  1. package/CHANGELOG.en.json +99 -0
  2. package/CHANGELOG.json +99 -0
  3. package/README.md +20 -158
  4. package/README.zh-CN.md +20 -166
  5. package/THIRD-PARTY-NOTICES.md +394 -0
  6. package/bin/sintbot-v2.js +363 -0
  7. package/docs/AI-ASSISTED-INSTALL.md +11 -35
  8. package/docs/install.md +61 -167
  9. package/docs/zh-CN/AI-ASSISTED-INSTALL.md +11 -35
  10. package/docs/zh-CN/install.md +61 -165
  11. package/install.sh +84 -0
  12. package/package.json +73 -37
  13. package/src/agent/claudecode.js +149 -108
  14. package/src/agent/codexapp.js +458 -0
  15. package/src/agent/codexcode.js +210 -107
  16. package/src/agent/errtag.js +34 -0
  17. package/src/agent/privatecore.js +790 -0
  18. package/src/agent/privatecore.wasm +0 -0
  19. package/src/agent/runner.js +10 -8
  20. package/src/agent/winexec.js +35 -5
  21. package/src/c2install.js +117 -0
  22. package/src/config.js +2 -1
  23. package/src/core/c2cardeffectbridge.js +51 -0
  24. package/src/core/c2cardterminalpath.js +216 -0
  25. package/src/core/c2durableingress.js +148 -0
  26. package/src/core/c2durablejournal.js +1560 -0
  27. package/src/core/c2durableruntime.js +2361 -0
  28. package/src/core/c2eventidentity.js +90 -0
  29. package/src/core/c2globalfault.js +401 -0
  30. package/src/core/c2localeffectexecutor.js +193 -0
  31. package/src/core/c2payloadhandles.js +333 -0
  32. package/src/core/c2persistrelation.js +179 -0
  33. package/src/core/c2persiststate.js +342 -0
  34. package/src/core/c2platformrecoverysender.js +76 -0
  35. package/src/core/c2recoverygate.js +385 -0
  36. package/src/core/c2runnerinput.js +63 -0
  37. package/src/core/c2servicestartgate.js +103 -0
  38. package/src/core/c2sessionbridge.js +72 -0
  39. package/src/core/c2sourcequarantine.js +30 -0
  40. package/src/core/c2startuprecovery.js +105 -0
  41. package/src/core/c2successorbridge.js +151 -0
  42. package/src/core/c2terminaleffectbridge.js +154 -0
  43. package/src/core/c2terminalprune.js +108 -0
  44. package/src/core/c2turnprojection.js +177 -0
  45. package/src/core/enginecheckpoint.js +211 -0
  46. package/src/core/engineeffectcursor.js +394 -0
  47. package/src/core/enginestepcoordinator.js +183 -0
  48. package/src/core/store.js +52 -8
  49. package/src/core/v2engine.js +1469 -0
  50. package/src/platform/feishu.js +324 -67
  51. package/src/plugins.js +1 -9
  52. package/assets/design-styles/acid-pop.md +0 -155
  53. package/assets/design-styles/archive-index.md +0 -161
  54. package/assets/design-styles/art-deco.md +0 -164
  55. package/assets/design-styles/cinematic-noir.md +0 -178
  56. package/assets/design-styles/editorial.md +0 -121
  57. package/assets/design-styles/extra/README.md +0 -23
  58. package/assets/design-styles/extra/constructivism.md +0 -146
  59. package/assets/design-styles/extra/neo-brutalism.md +0 -175
  60. package/assets/design-styles/extra/new-wave.md +0 -166
  61. package/assets/design-styles/extra/nordic-material.md +0 -162
  62. package/assets/design-styles/extra/paris-couture.md +0 -162
  63. package/assets/design-styles/extra/tropical-modern.md +0 -168
  64. package/assets/design-styles/extra/ulm-functional.md +0 -182
  65. package/assets/design-styles/extra/wa-shikimen.md +0 -160
  66. package/assets/design-styles/extra/y2k-chrome.md +0 -172
  67. package/assets/design-styles/geometric-pop.md +0 -122
  68. package/assets/design-styles/hand-drawn.md +0 -117
  69. package/assets/design-styles/islamic-geometry.md +0 -155
  70. package/assets/design-styles/liquid-glass.md +0 -183
  71. package/assets/design-styles/neo-chinese.md +0 -136
  72. package/assets/design-styles/precision-mono.md +0 -156
  73. package/assets/design-styles/quiet-luxury.md +0 -163
  74. package/assets/design-styles/riso-print.md +0 -116
  75. package/assets/design-styles/soft-friendly.md +0 -174
  76. package/assets/design-styles/swiss-grid.md +0 -156
  77. package/assets/design-styles/tech-dark.md +0 -182
  78. package/assets/favicon.svg +0 -6
  79. package/assets/logo.svg +0 -24
  80. package/bin/sbot.js +0 -4716
  81. package/docs/slack-app-manifest.yaml +0 -104
  82. package/sbot.config.example.json +0 -235
  83. package/src/agent/askonce.js +0 -76
  84. package/src/agent/claudemodels.js +0 -60
  85. package/src/agent/claudestatus.js +0 -74
  86. package/src/agent/claudeusage.js +0 -200
  87. package/src/agent/codexstatus.js +0 -181
  88. package/src/agent/imagen.js +0 -458
  89. package/src/agent/quotaerr.js +0 -44
  90. package/src/agent/transienterr.js +0 -32
  91. package/src/cdp.js +0 -254
  92. package/src/cli-secret.js +0 -36
  93. package/src/clilang.js +0 -31
  94. package/src/core/cron.js +0 -182
  95. package/src/core/cronfire.js +0 -120
  96. package/src/core/cronrun.js +0 -116
  97. package/src/core/engine.js +0 -2758
  98. package/src/core/interfaces.js +0 -114
  99. package/src/core/issues.js +0 -185
  100. package/src/core/ledger.js +0 -30
  101. package/src/core/opsalert.js +0 -214
  102. package/src/core/pending.js +0 -123
  103. package/src/core/proxy.js +0 -424
  104. package/src/core/quotaalert.js +0 -79
  105. package/src/core/quotaerr.js +0 -55
  106. package/src/core/scheduler.js +0 -59
  107. package/src/design.js +0 -5161
  108. package/src/gitrepo.js +0 -150
  109. package/src/health.js +0 -353
  110. package/src/installdeps.js +0 -72
  111. package/src/leftover.js +0 -174
  112. package/src/platform/boardnotice.js +0 -77
  113. package/src/platform/boardops.js +0 -36
  114. package/src/platform/boardwatch.js +0 -447
  115. package/src/platform/cli.js +0 -45
  116. package/src/platform/slack.js +0 -1289
  117. package/src/platform/takeovercard.js +0 -37
  118. package/src/platform/wecom.js +0 -938
  119. package/src/platform/wecomcli.js +0 -128
  120. package/src/platform/wecomgate.js +0 -108
  121. package/src/platform/wecomname.js +0 -43
  122. package/src/platform/wecompoll.js +0 -241
  123. package/src/preview.js +0 -581
  124. package/src/projectmemory.js +0 -175
  125. package/src/service.js +0 -317
  126. package/src/ui/auth.js +0 -31
  127. package/src/ui/browser.html +0 -392
  128. package/src/ui/browserroutes.js +0 -234
  129. package/src/ui/console.html +0 -5526
  130. package/src/ui/dashboard.js +0 -168
  131. package/src/ui/feishulogin.js +0 -136
  132. package/src/ui/server.js +0 -2314
  133. package/src/ui/sessions.js +0 -78
  134. package/src/ui/takeover.js +0 -108
  135. package/src/updater.js +0 -243
  136. package/src/upgrade.js +0 -35
  137. package/src/usage.js +0 -144
@@ -0,0 +1,363 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
4
+ // Restricted macOS Feishu single-group entry. No legacy daemon or terminal
5
+ // chat dispatch is reachable from this entry.
6
+ const fs = require('node:fs')
7
+ const path = require('node:path')
8
+ const { parseArgs } = require('node:util')
9
+ const { applyProxyEnv, chatPlatform } = require('../src/config')
10
+ const { secureDataDir } = require('../src/core/datadir')
11
+ const { StateStore } = require('../src/core/store')
12
+ const { createFeishuPlatform, feishuClient } = require('../src/platform/feishu')
13
+ const { createMultiPlatform } = require('../src/platform/multi')
14
+
15
+ async function startService(rest) {
16
+ const { values } = parseArgs({ args: rest, allowPositionals: false,
17
+ options: { config: { type: 'string' }, ui: { type: 'boolean' },
18
+ 'no-open': { type: 'boolean' }, 'ui-port': { type: 'string' },
19
+ 'ui-host': { type: 'string' }, preflight: { type: 'boolean' } } })
20
+ const preflightOnly = values.preflight === true
21
+ if (values.ui || values['no-open'] || values['ui-port'] || values['ui-host']) {
22
+ throw new Error('C2 v2 single-group service has no durable UI route; remove UI options')
23
+ }
24
+ const { loadInstallation, assertPrivateDirectory } = require('../src/c2install')
25
+ const { stateDir, file, cfg, groups, serviceChatId, provider } = loadInstallation(values.config)
26
+ assertPrivateDirectory(stateDir)
27
+ let journalStat
28
+ try { journalStat = fs.lstatSync(file) } catch {}
29
+ if (!journalStat?.isFile() || journalStat.isSymbolicLink()) {
30
+ throw new Error('No initialized journal; use sintbot init for a new installation. Existing state must be reviewed before migration.')
31
+ }
32
+ applyProxyEnv(cfg)
33
+ const { spawnSync } = require('node:child_process')
34
+ const probe = spawnSync(provider.bin || 'codex', ['--version'], {
35
+ encoding: 'utf8', timeout: 10_000, maxBuffer: 64 * 1024,
36
+ env: { ...process.env, ...(provider.env || {}) },
37
+ })
38
+ if (probe.error || probe.status !== 0 || !/codex/i.test(probe.stdout || '')) {
39
+ throw new Error('Codex executable preflight failed; install and sign in to Codex, then check the configured executable.')
40
+ }
41
+ const runner = require('../src/agent/runner')
42
+ secureDataDir(stateDir)
43
+ const lockFile = path.join(stateDir, 'daemon.lock')
44
+ const lock = require('../src/core/instancelock').acquire(lockFile)
45
+ if (!lock.ok) throw new Error('C2 v2 development service cannot hold the single-writer lock')
46
+ let running = false
47
+ try {
48
+ // The old daemon records interrupted work here and consumes it on restart.
49
+ // Starting v2 while such a file exists would leave that work invisible;
50
+ // never consume, delete, or reinterpret it without an explicit migration.
51
+ for (const name of ['pending-turns.json', 'engine-receipts.json']) {
52
+ try {
53
+ fs.lstatSync(path.join(stateDir, name))
54
+ } catch (error) {
55
+ if (error.code === 'ENOENT') continue
56
+ throw error
57
+ }
58
+ throw new Error(`C2 v2 cannot start with legacy ${name}; preserve it and drain with the old service or use a reviewed migration`)
59
+ }
60
+ const { createC2V2SourceQuarantine } = require('../src/core/c2sourcequarantine')
61
+ const requireQuarantine = process.env.SBOT_C2_V2_REQUIRE_BLOCK_SOURCE_ID
62
+ if (requireQuarantine !== undefined &&
63
+ requireQuarantine !== '0' && requireQuarantine !== '1') {
64
+ throw new Error('C2 v2 source quarantine requirement must be 0 or 1')
65
+ }
66
+ const sourceQuarantine = createC2V2SourceQuarantine([
67
+ process.env.SBOT_C2_V2_BLOCK_SOURCE_ID,
68
+ process.env.SBOT_C2_V2_BLOCK_SOURCE_ID_2],
69
+ { required: requireQuarantine === '1' })
70
+ const assertWriterLock = () => {
71
+ try { return fs.readFileSync(lockFile, 'utf8').trim() === String(process.pid) }
72
+ catch { return false }
73
+ }
74
+ const { C2DurableJournal } = require('../src/core/c2durablejournal')
75
+ const { validatePersistentState } = require('../src/core/c2persiststate')
76
+ const { validatePersistentReceipts, validatePersistentRelation } = require('../src/core/c2persistrelation')
77
+ const { createC2ServiceStartGate } = require('../src/core/c2servicestartgate')
78
+ const { C2PayloadHandles } = require('../src/core/c2payloadhandles')
79
+ const { createC2LocalEffectExecutor, resolveC2LocalEffectTurn } =
80
+ require('../src/core/c2localeffectexecutor')
81
+ const { createFeishuRecoverySender } = require('../src/core/c2platformrecoverysender')
82
+ const journal = C2DurableJournal.open(file, { validateState: validatePersistentState,
83
+ validateReceipts: validatePersistentReceipts,
84
+ validateRelation: validatePersistentRelation })
85
+ const beforeRecovery = journal.snapshot()
86
+ if (preflightOnly && (beforeRecovery.receipts.pending.length !== 0 ||
87
+ Object.keys(beforeRecovery.state.lanes).length !== 0 ||
88
+ [...beforeRecovery.effects.values()].some(status => status !== 'acked') ||
89
+ [...beforeRecovery.notices.values()].some(notice =>
90
+ notice.status === 'attempting' || notice.status === 'unknown'))) {
91
+ throw new Error('C2 v2 service preflight requires a settled journal; use recovery in a controlled service window')
92
+ }
93
+ const limits = { maxEventCount: 100_000,
94
+ maxJournalBytes: 64 * 1024 * 1024,
95
+ maxCompletionEvents: 1_000, maxCompletionBytes: 8 * 1024 * 1024 }
96
+ const { routeBackend } = require('../src/agent/privatecore')
97
+ const { C2DurableRuntime } = require('../src/core/c2durableruntime')
98
+ const { coordinateC2GlobalFault } = require('../src/core/c2globalfault')
99
+ const closeChecks = {
100
+ claude: require('../src/agent/claudecode').isClosedRunnerResult,
101
+ 'codex-exec': require('../src/agent/codexcode').isClosedRunnerResult,
102
+ 'codex-app': require('../src/agent/codexapp').isClosedRunnerResult,
103
+ }
104
+ const validators = { validateState: validatePersistentState,
105
+ validateReceipts: validatePersistentReceipts,
106
+ validateRelation: validatePersistentRelation }
107
+ const createTransport = () => {
108
+ const client = feishuClient(cfg.feishu.appId, cfg.feishu.appSecret)
109
+ const adapter = createFeishuPlatform({ appId: cfg.feishu.appId,
110
+ appSecret: cfg.feishu.appSecret, botOpenId: cfg.feishu.botOpenId,
111
+ onlyWhenMentioned: cfg.feishu.onlyWhenMentioned === true,
112
+ ws: cfg.feishu.ws || {}, card: cfg.feishu.card || {}, client })
113
+ const platform = createMultiPlatform({ adapters: { feishu: adapter },
114
+ resolve: chatId => chatPlatform(cfg, chatId) })
115
+ const sender = createFeishuRecoverySender({ cfg, platform, client })
116
+ for (const [chatId] of groups) sender.resolveTarget({ id: '0'.repeat(32),
117
+ chatId, source: 'message', sourceId: 'om_c2_service_probe' })
118
+ return { platform, sender, stopPlatform: () => adapter.stop({ strict: true }) }
119
+ }
120
+ const buildBinding = async authorityJournal => {
121
+ const { platform, sender, stopPlatform } = createTransport()
122
+ const gate = createC2ServiceStartGate({ enabled: true,
123
+ journal: authorityJournal, assertWriterLock,
124
+ resolveTarget: sender.resolveTarget, send: sender.send,
125
+ singleNoticePerGroup: true, ...limits })
126
+ try {
127
+ await gate.start()
128
+ const runtime = gate.runtime
129
+ if (!runtime || runtime.blocked || !assertWriterLock()) {
130
+ throw new Error('C2 v2 development service recovery did not open ingress')
131
+ }
132
+ const handles = new C2PayloadHandles({ journal: authorityJournal })
133
+ handles.reopenAfterRecovery()
134
+ if (handles.blocked || runtime.snapshot().cursor !== authorityJournal.snapshot().cursor) {
135
+ throw new Error('C2 v2 development service payload authority differs after recovery')
136
+ }
137
+ const store = new StateStore(path.join(stateDir, 'state.json'))
138
+ const closedResults = new WeakSet()
139
+ const startTurn = async input => {
140
+ const backend = routeBackend(input.provider?.runner, input.codexApp,
141
+ input.codexSteer, input.codexProjectDocFallback)
142
+ const result = await runner.runTurn(input)
143
+ if (closeChecks[backend]?.(result) === true) closedResults.add(result)
144
+ return result
145
+ }
146
+ let engine
147
+ const applyLocalEffect = createC2LocalEffectExecutor({
148
+ journal: authorityJournal, store,
149
+ verifyFile: path.join(stateDir, 'verify.json'), assertWriterLock,
150
+ resolveTurn: identity => resolveC2LocalEffectTurn(engine, identity) })
151
+ const { Engine } = require('../src/core/v2engine')
152
+ engine = new Engine(cfg, { runner, store, log: message =>
153
+ process.stderr.write(`C2 v2 · ${message}\n`),
154
+ durableV2Runtime: runtime, durableV2Payloads: handles,
155
+ durableV2StartTurn: startTurn, durableV2ApplyLocalEffect: applyLocalEffect,
156
+ durableV2VerifyRunnerClosed: ({ result }) => !!result &&
157
+ typeof result === 'object' && closedResults.has(result),
158
+ durableV2AssertWriterLock: assertWriterLock,
159
+ durableV2Journal: authorityJournal, durableV2NormalRoute: true })
160
+ return { journal: authorityJournal, platform, sender, stopPlatform, gate,
161
+ runtime, handles, store, engine }
162
+ } catch (error) { gate.stop(); throw error }
163
+ }
164
+ let active = await buildBinding(journal)
165
+ if (preflightOnly) {
166
+ active.gate.stop()
167
+ process.stdout.write('SintBot startup preflight passed; Feishu socket stayed disconnected\n')
168
+ return
169
+ }
170
+ let stopping = false
171
+ let rebuilds = 0
172
+ let finishing = false
173
+ const finish = (reason, code, platformStopped, drained) => {
174
+ if (finishing) return
175
+ finishing = true
176
+ lock.release()
177
+ process.stderr.write(`C2 v2 · ${reason}; platform stopped=${platformStopped}; runner drained=${drained}\n`,
178
+ () => process.exit(code))
179
+ }
180
+ const activeTurns = snapshot => {
181
+ const lanes = snapshot.state.lanes
182
+ const turns = []
183
+ for (const chatId of new Set([...Object.keys(lanes.turn || {}),
184
+ ...Object.keys(lanes.handoff || {}),
185
+ ...Object.keys(lanes.lifecycle || {})])) {
186
+ const turn = lanes.turn?.[chatId]
187
+ const handoff = lanes.handoff?.[chatId]
188
+ const lifecycle = lanes.lifecycle?.[chatId]
189
+ if (turn?.active != null || handoff?.phase && handoff.phase !== 'idle' ||
190
+ lifecycle?.busy === true || lifecycle?.controller === true) {
191
+ turns.push({ chatId, turnToken: turn?.active ?? handoff?.turnToken ?? null })
192
+ }
193
+ }
194
+ return turns.sort((a, b) => a.chatId.localeCompare(b.chatId))
195
+ }
196
+ const launch = async binding => {
197
+ const onFatal = createC2ReceiptIngressFatalHook({
198
+ log: message => process.stderr.write(`C2 v2 · ${message}\n`),
199
+ shutdown: reason => shutdown(binding, reason, 1) })
200
+ const unsupported = source => () => {
201
+ void onFatal(new Error(`C2 v2 unsupported Feishu ${source} source`))
202
+ }
203
+ await binding.platform.start((msg, sourceAdapter) => {
204
+ if (binding !== active || binding.gate.blocked || !assertWriterLock()) {
205
+ throw new Error('C2 v2 development service ingress lost durable authority')
206
+ }
207
+ if (msg?.chatId !== serviceChatId) {
208
+ throw new Error('C2 v2 development service received a message outside its single group')
209
+ }
210
+ if (sourceQuarantine?.rejects(msg)) {
211
+ process.stderr.write('C2 v2 · quarantined historical source message before receipt\n')
212
+ return { quarantined: true }
213
+ }
214
+ return binding.engine.runDurableV2Message(msg, sourceAdapter)
215
+ }, { onFatal, onRecall: unsupported('recall'),
216
+ onReaction: unsupported('reaction'),
217
+ onCardAction: unsupported('card action'),
218
+ onBoardUpdated: unsupported('board update') })
219
+ process.stderr.write('SintBot single-group Feishu ingress connected\n')
220
+ }
221
+ const shutdown = async (binding, reason, code = 1) => {
222
+ if (stopping || binding !== active) return
223
+ stopping = true
224
+ binding.gate.stop()
225
+ binding.engine.shutdown()
226
+ let stopTimer
227
+ const platformStopped = await Promise.race([
228
+ Promise.resolve().then(() => binding.stopPlatform()).then(value => value === true, () => false),
229
+ new Promise(resolve => {
230
+ stopTimer = setTimeout(() => resolve(false), 2_000)
231
+ }),
232
+ ])
233
+ clearTimeout(stopTimer)
234
+ const drained = await binding.engine.drainDurableV2(12_000).catch(() => false)
235
+ // A failed route may have already settled, leaving a closed child and
236
+ // active durable receipt for global-fault reconciliation. A still-live
237
+ // route or child cannot be replaced, even if the drain timer expired.
238
+ const quiescent = [...binding.engine.rts.values()].every(rt =>
239
+ !rt.v2Controller && !rt.v2NormalRouteActive)
240
+ if (code === 0 || !platformStopped || !quiescent || !assertWriterLock() ||
241
+ rebuilds >= 1) {
242
+ finish(reason, code === 0 && platformStopped && drained ? 0 : 1,
243
+ platformStopped, drained)
244
+ return
245
+ }
246
+ rebuilds++
247
+ try {
248
+ const current = binding.journal.snapshot()
249
+ const turns = activeTurns(current)
250
+ const proofs = turns.length
251
+ ? binding.engine.durableV2ActiveFaultProofs?.(turns) || null : null
252
+ const result = await coordinateC2GlobalFault({
253
+ runtime: binding.runtime, journal: binding.journal, assertWriterLock,
254
+ abortTurn: turn => binding.engine.abortDurableV2FaultTurn?.(turn) === true,
255
+ activeTurnProofs: proofs,
256
+ createAuthority: () => {
257
+ const nextJournal = C2DurableJournal.open(file, validators)
258
+ const nextRuntime = new C2DurableRuntime({ journal: nextJournal,
259
+ resolveTarget: binding.sender.resolveTarget,
260
+ send: binding.sender.send, assertWriterLock,
261
+ singleNoticePerGroup: true, ...limits })
262
+ return { journal: nextJournal, runtime: nextRuntime }
263
+ } })
264
+ if (result.status !== 'recovered' || !assertWriterLock()) {
265
+ finish(`C2 v2 recovery sealed: ${result.reason || 'writer lock lost'}`,
266
+ 1, platformStopped, drained)
267
+ return
268
+ }
269
+ result.runtime.close()
270
+ const next = await buildBinding(result.journal)
271
+ active = next
272
+ // A fatal callback may arrive synchronously while the fresh platform
273
+ // starts. It must not be swallowed by the old binding's stop guard.
274
+ stopping = false
275
+ await launch(next)
276
+ process.stderr.write('C2 v2 process-local authority rebuilt\n')
277
+ } catch (error) {
278
+ finish(`C2 v2 recovery failed: ${error?.message || String(error)}`,
279
+ 1, platformStopped, drained)
280
+ }
281
+ }
282
+ process.on('SIGINT', () => { void shutdown(active, 'SIGINT', 0) })
283
+ process.on('SIGTERM', () => { void shutdown(active, 'SIGTERM', 0) })
284
+ process.on('exit', lock.release)
285
+ try {
286
+ await launch(active)
287
+ running = true
288
+ } catch (error) {
289
+ active.gate.stop()
290
+ active.engine.shutdown()
291
+ let timer
292
+ await Promise.race([
293
+ Promise.resolve().then(() => active.stopPlatform()).catch(() => false),
294
+ new Promise(resolve => { timer = setTimeout(resolve, 2_000) }),
295
+ ])
296
+ clearTimeout(timer)
297
+ await active.engine.drainDurableV2(12_000).catch(() => false)
298
+ throw error
299
+ }
300
+ } finally { if (!running) lock.release() }
301
+ }
302
+
303
+ // The opt-in receipt authority cannot keep accepting Feishu messages after
304
+ // its ingress callback rejects. The adapter closes its socket first; the
305
+ // daemon then aborts running turns and exits nonzero under the supervisor.
306
+ function createC2ReceiptIngressFatalHook({ log, shutdown }) {
307
+ if (typeof log !== 'function' || typeof shutdown !== 'function') {
308
+ throw new TypeError('C2 receipt ingress fatal hook needs log and shutdown')
309
+ }
310
+ let fired = false
311
+ return error => {
312
+ if (fired) return
313
+ fired = true
314
+ log('C2 receipt ingress failed: ' + (error && error.message || String(error)))
315
+ return shutdown('C2 receipt ingress failed', 1)
316
+ }
317
+ }
318
+
319
+ function fail(message) {
320
+ process.stderr.write(`sintbot: ${message}\n`)
321
+ process.exitCode = 1
322
+ }
323
+
324
+ const HELP = `SintBot macOS Feishu single-group release
325
+ Usage:
326
+ sintbot init [--config <absolute path>]
327
+ sintbot start [--config <absolute path>] [--preflight]
328
+ sintbot version
329
+ sintbot help
330
+
331
+ Supports one explicit Feishu group using Codex. Start runs in the foreground.
332
+ Initialization is for a new installation only; existing journals are never reset.
333
+ Canvas, multiple groups, console, scheduled tasks, terminal chat and other platforms are unavailable.
334
+ `
335
+
336
+ async function main(argv = process.argv.slice(2)) {
337
+ const [sub, ...rest] = argv
338
+ if (sub === 'version' || sub === '--version' || sub === '-v') {
339
+ if (rest.length) throw new Error('version does not accept arguments')
340
+ process.stdout.write(require('../package.json').version + '\n')
341
+ return
342
+ }
343
+ if (!sub || sub === 'help' || sub === '--help' || sub === '-h' ||
344
+ ['init', 'start'].includes(sub) && rest.length === 1 && ['--help', '-h'].includes(rest[0])) {
345
+ process.stdout.write(HELP)
346
+ return
347
+ }
348
+ if (!['init', 'start'].includes(sub)) {
349
+ throw new Error(`${sub} is unavailable in this single-group release; use sintbot help`)
350
+ }
351
+ if (process.platform !== 'darwin') throw new Error('this release supports macOS only')
352
+ if (sub === 'init') {
353
+ const { values } = parseArgs({ args: rest, allowPositionals: false,
354
+ options: { config: { type: 'string' } } })
355
+ require('../src/c2install').initializeInstallation(values.config)
356
+ process.stdout.write('SintBot journal initialized; run sintbot start to connect the configured group.\n')
357
+ return
358
+ }
359
+ await startService(rest)
360
+ }
361
+
362
+ if (require.main === module) main().catch(error => fail(error?.message || String(error)))
363
+ module.exports = { main, createC2ReceiptIngressFatalHook }
@@ -1,43 +1,19 @@
1
- # Install and start using SintBot with an AI assistant
1
+ # Install SintBot 1.3.0 with an AI assistant
2
2
 
3
- [简体中文](./zh-CN/AI-ASSISTED-INSTALL.md) · English
3
+ English · [简体中文](./zh-CN/AI-ASSISTED-INSTALL.md)
4
4
 
5
- An AI assistant can install SintBot only if it can use a **local terminal on the computer where SintBot will run**. A chat-only session or cloud shell cannot install it on your Mac. If the assistant cannot reach that terminal, run the install command in Terminal on the target computer, then ask the assistant to continue with verification.
5
+ Use the terminal on the Mac that will run SintBot. A cloud shell cannot install it on the user's Mac. Follow the [installation guide](./install.md); its supported route is one Feishu group, Codex and a foreground process.
6
6
 
7
- The one-line installer supports macOS and Linux (Linux support is experimental). Windows users should follow the [manual install steps](./install.md#windows).
8
-
9
- ## Shortest path
10
-
11
- 1. **Inspect first.** Check the target OS, Node version, installed SintBot version and service state. Do not read or print configuration or credentials.
12
- 2. **Pick the right path.** For a fresh install, download the official installer to a temporary file, inspect it, and run its no-change preview; execute that same file after approval. For an upgrade, update only the package; do not repeat model setup or its trial. Restarting a running service can announce the upgrade to connected groups.
13
- 3. **Confirm once.** Before changing anything, give one short summary of Homebrew Node changes, the required Claude Code CLI if missing, the selected model and possible trial charge, and whether the background service will be installed or restarted. Ask only for choices that are still needed. After approval, answer installer prompts using those choices; do not ask for the same approval again. Stop and ask if an unexpected change appears.
14
- 4. **Run locally.** Use the target computer's interactive terminal. Let the user sign in themselves and type API keys directly at SintBot's masked terminal prompt. Never copy credentials into chat or the agent transcript, and do not use `sudo`.
15
- 5. **Complete the first use.** Check the version, health and service state. In the console, follow **Get started**: confirm a usable model, connect a chat platform if needed, bind one group to one project folder, then send a read-only test request. To test a model before connecting a platform, use **Playground**.
16
-
17
- If the user needs to create or approve a chat-platform app, guide them with the relevant [platform setup guide](./platforms/). The user enters credentials directly in the local console. Ask for only the next decision needed; do not make them repeat information already provided.
18
-
19
- ## If the assistant cannot use a local terminal
20
-
21
- Do not run a real installation on another computer or in a cloud shell. A no-change `--dry-run` preview is fine, but without an interactive terminal a real install stops when Node 22+ is missing; if Node is present, it may install only the package and report incomplete setup. Have the user run the [one-line install](./install.md#one-line-install) on the target computer, then continue verification.
7
+ 1. Check macOS, Node 22+, the actual Codex executable/login and any existing SintBot process. Do not print credentials. Preserve existing installations, configuration and state.
8
+ 2. Download and inspect the installer, run its `--dry-run`, then install the exact package into a new prefix. Continue within the user's authorization; ask only for missing decisions. The installer does not change Node, Codex, shell profiles or services.
9
+ 3. Use the executable under that prefix and verify it reports `1.3.0`. Do not substitute a globally installed command. A successful package install is not a successful group test.
10
+ 4. Prepare the single-group configuration in the install guide. Let the user enter secrets locally or use already authorized credential references without revealing them. Use an existing authorized Feishu app only when another process is not connected to it. If app setup or login requires the user, finish other independent preparation first.
11
+ 5. For fresh state only, run `init`, then `start --preflight`. Refusal due to old state or unresolved receipts is a stop condition: never delete files, fabricate a new authority or alter receipts to bypass it.
12
+ 6. Run the foreground service, send an authorized read-only group request and compare its response with the file. Stop cleanly and verify a restart. If no person is available, use an authorized test account to send the request; document the sender and do not call it independent human acceptance. A bot's own API message may be ignored and is not a substitute for an incoming user message.
13
+ 7. Stop temporary processes before ending the task. Report what was installed, what actually ran and any remaining setup. Do not register an automatic background service for this release.
22
14
 
23
15
  ## Copyable request
24
16
 
25
17
  ```text
26
- Help me install or upgrade SintBot on this computer and get through the first use. Use only the local terminal on the computer where it will run. First check the OS, Node version, SintBot version and service state; do not read configuration or credentials.
27
-
28
- For a fresh install, download the official installer to a temporary file, inspect it and run its no-change preview; execute that same file after approval. For an upgrade, update only the package; do not rerun model setup or its trial. Before changes, give me one concise summary covering Node / Claude Code installs, model choice and possible trial cost, and background-service install or restart (including any group notice). Ask once for the decisions needed. After I approve, answer matching installer prompts from my choices without asking again; stop if a new change is required.
29
-
30
- Use the target computer's interactive terminal. I will sign in and type any API key directly at the masked local prompt; never put credentials in chat. Do not use sudo or reset existing data. After installation, verify the version, health and service state, then guide me through the console's Get started steps. We can use Playground first, or connect a platform, bind one group to one project and send a read-only test. Ask only for the next required choice; stop on errors and explain the cause.
31
- ```
32
-
33
- ## Give an AI agent a clear task
34
-
35
- For coding work, include the goal, acceptance criteria and constraints. For example:
36
-
37
- ```text
38
- Goal: …
39
- Done means: …
40
- Constraints: …
41
-
42
- Read the project's instructions and current state first. If the request is clear, proceed; ask only when a missing fact blocks the work. Finish with a short summary of changes, checks and anything still open.
18
+ Install SintBot 1.3.0 on this Mac using the versioned, isolated installation guide. Preserve existing services, configuration, state and Codex account data. Do not expose credentials. Verify the actual installed executable and binary core, configure one Feishu group for Codex, and initialize only clean new state. Run the foreground preflight and an authorized read-only group test, check the result, then verify clean stop/restart. Do not use the old setup, console or service-install flow. Stop temporary processes when finished, and distinguish package installation from successful group acceptance.
43
19
  ```