@x-otto/cli 0.0.1-alpha.43 → 0.0.1-alpha.44
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/dist/build-id-D_JURZdd.js +3 -0
- package/dist/build-id-D_JURZdd.js.map +1 -0
- package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/meta.json +1 -1
- package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/plugin.cjs +2 -1
- package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-responses.ts +5 -1
- package/dist/{command-dispatcher-DhcLUPR5.js → command-dispatcher-Dxj41r5P.js} +4 -4
- package/dist/{command-dispatcher-DhcLUPR5.js.map → command-dispatcher-Dxj41r5P.js.map} +1 -1
- package/dist/{daemon-base-Bc9p51Um.js → daemon-base-Y1XI61JY.js} +1 -1
- package/dist/{daemon-base-Bc9p51Um.js.map → daemon-base-Y1XI61JY.js.map} +1 -1
- package/dist/{doctor-lT2QhG2q.js → doctor-DXBLkbkP.js} +9 -9
- package/dist/doctor-DXBLkbkP.js.map +1 -0
- package/dist/{doctor-attach-_zeIJewx.js → doctor-attach-DEMuyBOk.js} +2 -2
- package/dist/{doctor-attach-_zeIJewx.js.map → doctor-attach-DEMuyBOk.js.map} +1 -1
- package/dist/{doctor-cdp-K_mIFRfA.js → doctor-cdp-CnJUVNOB.js} +1 -1
- package/dist/{doctor-cdp-K_mIFRfA.js.map → doctor-cdp-CnJUVNOB.js.map} +1 -1
- package/dist/{doctor-heap-Rr2nNvcw.js → doctor-heap-Eu3LZj-F.js} +2 -2
- package/dist/{doctor-heap-Rr2nNvcw.js.map → doctor-heap-Eu3LZj-F.js.map} +1 -1
- package/dist/{doctor-ps-OL3FGQYu.js → doctor-ps-JBgCH77n.js} +2 -2
- package/dist/{doctor-ps-OL3FGQYu.js.map → doctor-ps-JBgCH77n.js.map} +1 -1
- package/dist/export-trajectory-D-BcH0Wt.js +14 -0
- package/dist/export-trajectory-D-BcH0Wt.js.map +1 -0
- package/dist/{feedback-BQ_oz5nh.js → feedback-CYFY2MC4.js} +2 -2
- package/dist/{feedback-BQ_oz5nh.js.map → feedback-CYFY2MC4.js.map} +1 -1
- package/dist/feedback-D7Po4mlE.js +2 -0
- package/dist/{feishu-setup-B6ETf-8Y.js → feishu-setup-5ZoxtH0J.js} +1 -1
- package/dist/{feishu-setup-B6ETf-8Y.js.map → feishu-setup-5ZoxtH0J.js.map} +1 -1
- package/dist/{ftue-doctor-recovery-DeAf8jVo.js → ftue-doctor-recovery-8FeDBFOc.js} +1 -1
- package/dist/{ftue-doctor-recovery-DeAf8jVo.js.map → ftue-doctor-recovery-8FeDBFOc.js.map} +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/{mcp-9P_AmnDk.js → mcp-Cm4-OhXI.js} +1 -1
- package/dist/{mcp-9P_AmnDk.js.map → mcp-Cm4-OhXI.js.map} +1 -1
- package/dist/{mcp-server-BK4L-YBN.js → mcp-server-Dpza3yn2.js} +1 -1
- package/dist/{mcp-server-BK4L-YBN.js.map → mcp-server-Dpza3yn2.js.map} +1 -1
- package/dist/{migrate-DDNkik_0.js → migrate-RlBJqJnB.js} +1 -1
- package/dist/{migrate-DDNkik_0.js.map → migrate-RlBJqJnB.js.map} +1 -1
- package/dist/{observe-CzIv09hO.js → observe-Dxfud-t7.js} +1 -1
- package/dist/{observe-CzIv09hO.js.map → observe-Dxfud-t7.js.map} +1 -1
- package/dist/parse-cli-BSJEQtBo.js +3 -0
- package/dist/parse-cli-BSJEQtBo.js.map +1 -0
- package/dist/{persistence-DOaizHMA.js → persistence-CRFhOYx6.js} +2 -2
- package/dist/{persistence-DOaizHMA.js.map → persistence-CRFhOYx6.js.map} +1 -1
- package/dist/{plugin-assembly-check-Ov17ra7G.js → plugin-assembly-check-DEozmcJ7.js} +1 -1
- package/dist/{plugin-assembly-check-Ov17ra7G.js.map → plugin-assembly-check-DEozmcJ7.js.map} +1 -1
- package/dist/{ps-BovE9XMQ.js → ps-DiKCLKbs.js} +1 -1
- package/dist/{ps-BovE9XMQ.js.map → ps-DiKCLKbs.js.map} +1 -1
- package/dist/{run-attach-z7YECztf.js → run-attach-DDpMk2_N.js} +1 -1
- package/dist/{run-attach-z7YECztf.js.map → run-attach-DDpMk2_N.js.map} +1 -1
- package/dist/{serve-DaxogAZ5.js → serve-Ke0nwN9Y.js} +2 -2
- package/dist/{serve-DaxogAZ5.js.map → serve-Ke0nwN9Y.js.map} +1 -1
- package/dist/{version-Eh8zZKgk.js → version-BKPdYoHG.js} +2 -2
- package/dist/{version-Eh8zZKgk.js.map → version-BKPdYoHG.js.map} +1 -1
- package/package.json +12 -12
- package/dist/build-id-ClyMIKLy.js +0 -3
- package/dist/build-id-ClyMIKLy.js.map +0 -1
- package/dist/doctor-lT2QhG2q.js.map +0 -1
- package/dist/feedback-C2Hdxp1y.js +0 -2
- package/dist/parse-cli-3fWEIMWc.js +0 -3
- package/dist/parse-cli-3fWEIMWc.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ps-BovE9XMQ.js","names":[],"sources":["../src/commands/fleet-scan.ts","../src/commands/ps.ts"],"sourcesContent":["/**\n * 进程舰队全量扫描工具(ps/kill/signal + 按会话分代)。纯模块——不持状态、不新建 child。\n */\n\nimport { OTTO_SESSION_LOCKS_DIR, isContainerEnvironment, listOttoProcesses } from '@x-otto/env'\nimport { createSessionWriteLeaseManager } from '@x-otto/session'\n\nimport type { OttoProcessInfo } from '@x-otto/env'\n\nexport type OrphanVerdict = 'strong' | 'weak' | 'none'\n\nexport interface FleetProcess extends OttoProcessInfo {\n readonly verdict: OrphanVerdict\n /** 判定理由(展示用,如 `lease-stale`/`lease-other-owner`/`lease-self-heartbeat`)。 */\n readonly reason: string\n}\n\n/** 弱孤儿的最小运行时长(无租约可核时的辅助信号)。 */\nconst WEAK_ORPHAN_MIN_ETIME_MS = 60 * 60 * 1000\n\n/**\n * serve 类常驻子命令——合法 detach 形态,永不判孤儿。\n *\n * 关于 `daemon`(RFC-335 M2):`otto daemon start` 拉起的**后台进程命令行是 `serve`**\n * (见 `daemon.ts` 的 `spawnDetached`),因此它实际由上面的 `'serve'` 一项保护;\n * `daemon` 自身只是前台短命进程(发完 spawn 即退出),不会被扫描为常驻。\n * 这里仍列出 `daemon` 只为表达意图,**不是**该保护生效的原因——真正的防护是 `serve`。\n */\nconst RESIDENT_SUBCOMMANDS = ['serve', 'daemon', 'persistenced', 'remote-persistence-server', 'observe']\n\nexport interface FleetScanDeps {\n /** 进程枚举(测试注入)。默认 `listOttoProcesses()`。 */\n listProcesses?: () => OttoProcessInfo[]\n /** 租约检视(测试注入)。默认按 OTTO_HOME/sessions/locks 组装只读 lease manager。 */\n inspectLease?: (sessionId: string) => { locked: boolean; pid?: number; alive?: boolean; mtimeAgoMs?: number }\n /** 容器环境探测(测试注入)。默认 `isContainerEnvironment()`。 */\n isContainer?: () => boolean\n /** 租约 stale 阈值(对齐 session-write-lease DEFAULT_STALE_MS)。 */\n staleMs?: number\n}\n\nfunction isResidentCommand(command: string): boolean {\n // 子命令是 bin/otto.js 之后的第一个非 flag 参数\n const afterEntry = command.split('bin/otto.js')[1] ?? ''\n const firstArg = afterEntry.trim().split(/\\s+/)[0] ?? ''\n return RESIDENT_SUBCOMMANDS.includes(firstArg)\n}\n\n/** 扫描全机 otto 主进程并逐个判定孤儿三态。 */\nexport function scanFleet(deps: FleetScanDeps = {}): FleetProcess[] {\n const listProcesses = deps.listProcesses ?? listOttoProcesses\n const isContainer = deps.isContainer ?? isContainerEnvironment\n const staleMs = deps.staleMs ?? 60_000\n // review P2 修复:lease manager 提到判定循环外一次组装(此前每进程 new 一个,含\n // 重复 mkdirSync)。只读组装:installExitHook:false 无退出钩子副作用;inspect() 无\n // acquire/touch。lockDir 与 cli.ts 的会话存储组装同源 OTTO_HOME(P0 修复后一致)。\n const defaultManager = deps.inspectLease\n ? undefined\n : createSessionWriteLeaseManager({\n lockDir: OTTO_SESSION_LOCKS_DIR,\n installExitHook: false,\n })\n const inspectLease = deps.inspectLease ?? ((sessionId: string) => defaultManager!.inspect(sessionId))\n\n const container = isContainer()\n const processes = listProcesses()\n\n return processes.map((p): FleetProcess => {\n // 安全不变式:有 TTY 一律非孤儿。\n if (p.tty !== '??') return { ...p, verdict: 'none', reason: 'has-tty' }\n if (p.ppid !== 1) return { ...p, verdict: 'none', reason: 'has-parent' }\n if (isResidentCommand(p.command)) return { ...p, verdict: 'none', reason: 'resident-service' }\n // 容器环境:ppid==1 是常态,强判定失效(F6 豁免)。\n if (container) {\n return p.etimeMs > WEAK_ORPHAN_MIN_ETIME_MS\n ? { ...p, verdict: 'weak', reason: 'container-ppid1-longrun' }\n : { ...p, verdict: 'none', reason: 'container-ppid1' }\n }\n\n if (!p.sessionId) {\n // 无 --continue:无租约可核 → 弱孤儿(>1h 辅助信号)。\n return p.etimeMs > WEAK_ORPHAN_MIN_ETIME_MS\n ? { ...p, verdict: 'weak', reason: 'no-session-longrun' }\n : { ...p, verdict: 'none', reason: 'no-session-young' }\n }\n\n const lease = inspectLease(p.sessionId)\n if (!lease.locked) {\n // 锁文件不存在——会话早已正常释放,进程却还挂着 → 强孤儿。\n return { ...p, verdict: 'strong', reason: 'lease-released' }\n }\n if (lease.pid === p.pid) {\n // (c) 自持租约的活孤儿——心跳还在但不可交互(见文件头语义说明)。\n return { ...p, verdict: 'strong', reason: 'lease-self-heartbeat' }\n }\n if (lease.mtimeAgoMs !== undefined && lease.mtimeAgoMs > staleMs) {\n // (b) 租约 stale——持有者已死。\n return { ...p, verdict: 'strong', reason: 'lease-stale' }\n }\n // (a) 租约被其他进程持有且 fresh——本候选是废弃副本。\n return { ...p, verdict: 'strong', reason: 'lease-other-owner' }\n })\n}\n","/**\n * /ps 命令:进程舰队列表视图(按会话/TTY 分组、代际计数/内存)。\n */\nimport { isProcessAlive } from '@x-otto/env'\nimport { scanFleet } from './fleet-scan'\n\nimport type { FleetProcess } from './fleet-scan'\n\nexport interface PsCommandIo {\n stdout: (s: string) => void\n stderr: (s: string) => void\n /** SIGTERM 发送(测试注入)。默认 `process.kill(pid, 'SIGTERM')`。 */\n kill?: (pid: number) => void\n /** 存活探测(测试注入)。默认 `isProcessAlive`(@x-otto/env,kill(pid,0) 语义)。 */\n probeAlive?: (pid: number) => boolean\n /** sweep 后存活复查的等待毫秒(测试注入缩短)。默认 3000。 */\n verifyDelayMs?: number\n /** 延迟原语(测试注入 fake)。默认真实 setTimeout。 */\n sleep?: (ms: number) => Promise<void>\n}\n\nfunction formatRss(rssKb: number): string {\n return rssKb >= 1024 ? `${(rssKb / 1024).toFixed(0)}MB` : `${rssKb}KB`\n}\n\nfunction formatEtime(ms: number): string {\n const h = Math.floor(ms / 3_600_000)\n if (h >= 24) return `${Math.floor(h / 24)}d${h % 24}h`\n const m = Math.floor((ms % 3_600_000) / 60_000)\n return h > 0 ? `${h}h${m}m` : `${m}m`\n}\n\nfunction verdictLabel(p: FleetProcess): string {\n if (p.verdict === 'strong') return `ORPHAN(${p.reason})`\n if (p.verdict === 'weak') return `orphan?(${p.reason})`\n return '-'\n}\n\nexport async function runPsCommand(args: string[], io: PsCommandIo): Promise<number> {\n const sweep = args.includes('--sweep')\n const includeWeak = args.includes('--include-weak')\n const json = args.includes('--json')\n const kill = io.kill ?? ((pid: number) => process.kill(pid, 'SIGTERM'))\n\n const fleet = scanFleet()\n const self = process.pid\n\n if (json) {\n io.stdout(JSON.stringify(fleet, null, 2) + '\\n')\n return 0\n }\n\n if (fleet.length === 0) {\n io.stdout('no otto processes found\\n')\n return 0\n }\n\n // 列表输出\n const totalRssKb = fleet.reduce((acc, p) => acc + p.rssKb, 0)\n const strong = fleet.filter((p) => p.verdict === 'strong')\n const weak = fleet.filter((p) => p.verdict === 'weak')\n\n const header = ['PID', 'PPID', 'RSS', 'TTY', 'UP', 'GEN', 'SESSION', 'VERDICT'].join('\\t')\n io.stdout(header + '\\n')\n for (const p of fleet) {\n const selfMark = p.pid === self ? '*' : ''\n io.stdout(\n [\n `${p.pid}${selfMark}`,\n p.ppid,\n formatRss(p.rssKb),\n p.tty,\n formatEtime(p.etimeMs),\n p.generation ?? '-',\n p.sessionId ? p.sessionId.slice(0, 8) : '-',\n verdictLabel(p),\n ].join('\\t') + '\\n',\n )\n }\n io.stdout(\n `\\n${fleet.length} processes, total RSS ${formatRss(totalRssKb)}; ` +\n `${strong.length} strong orphan(s), ${weak.length} weak orphan(s)\\n`,\n )\n\n if (!sweep) {\n if (strong.length > 0 || weak.length > 0) {\n io.stdout('run `otto ps --sweep` to SIGTERM strong orphans' +\n (weak.length > 0 ? ' (--include-weak for weak ones)' : '') + '\\n')\n }\n return 0\n }\n\n // sweep:SIGTERM only;自进程/有 TTY 双保险排除(判定层已保证有 TTY 不入选,此处复核)。\n const targets = [...strong, ...(includeWeak ? weak : [])].filter(\n (p) => p.pid !== self && p.tty === '??',\n )\n if (targets.length === 0) {\n io.stdout('nothing to sweep\\n')\n return 0\n }\n let signalled = 0\n const signalledPids: FleetProcess[] = []\n for (const p of targets) {\n try {\n kill(p.pid)\n signalled++\n signalledPids.push(p)\n io.stdout(`SIGTERM ${p.pid} (${verdictLabel(p)}, ${formatRss(p.rssKb)})\\n`)\n } catch (err) {\n io.stderr(`failed to signal ${p.pid}: ${err instanceof Error ? err.message : String(err)}\\n`)\n }\n }\n\n // review P1-a 修复:SIGTERM 送达 ≠ 进程退出——chaperone 态孤儿装着空函数 signal guard\n // (cli.ts chaperoneSignalGuard,防前台组误杀的既有设计)会吞掉 SIGTERM,进程纹丝不动。\n // 送信号后延迟复查存活,把\"未退出\"名单如实报告并提示手动升级(不自动 SIGKILL——保持\n // RFC-196 \"SIGTERM only\" 不变式,kill -9 决策权留给用户)。\n const probeAlive = io.probeAlive ?? isProcessAlive\n const sleep = io.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n await sleep(io.verifyDelayMs ?? 3000)\n const survivors = signalledPids.filter((p) => probeAlive(p.pid))\n const exited = signalled - survivors.length\n io.stdout(`swept ${exited}/${targets.length}\\n`)\n if (survivors.length > 0) {\n io.stdout(\n `${survivors.length} process(es) ignored SIGTERM (likely chaperone signal guard): ` +\n survivors.map((p) => p.pid).join(', ') +\n `\\nescalate manually if needed: kill -9 ${survivors.map((p) => p.pid).join(' ')}\\n`,\n )\n return 1\n }\n return signalled === targets.length ? 0 : 1\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ps-DiKCLKbs.js","names":[],"sources":["../src/commands/fleet-scan.ts","../src/commands/ps.ts"],"sourcesContent":["/**\n * 进程舰队全量扫描工具(ps/kill/signal + 按会话分代)。纯模块——不持状态、不新建 child。\n */\n\nimport { OTTO_SESSION_LOCKS_DIR, isContainerEnvironment, listOttoProcesses } from '@x-otto/env'\nimport { createSessionWriteLeaseManager } from '@x-otto/session'\n\nimport type { OttoProcessInfo } from '@x-otto/env'\n\nexport type OrphanVerdict = 'strong' | 'weak' | 'none'\n\nexport interface FleetProcess extends OttoProcessInfo {\n readonly verdict: OrphanVerdict\n /** 判定理由(展示用,如 `lease-stale`/`lease-other-owner`/`lease-self-heartbeat`)。 */\n readonly reason: string\n}\n\n/** 弱孤儿的最小运行时长(无租约可核时的辅助信号)。 */\nconst WEAK_ORPHAN_MIN_ETIME_MS = 60 * 60 * 1000\n\n/**\n * serve 类常驻子命令——合法 detach 形态,永不判孤儿。\n *\n * 关于 `daemon`(RFC-335 M2):`otto daemon start` 拉起的**后台进程命令行是 `serve`**\n * (见 `daemon.ts` 的 `spawnDetached`),因此它实际由上面的 `'serve'` 一项保护;\n * `daemon` 自身只是前台短命进程(发完 spawn 即退出),不会被扫描为常驻。\n * 这里仍列出 `daemon` 只为表达意图,**不是**该保护生效的原因——真正的防护是 `serve`。\n */\nconst RESIDENT_SUBCOMMANDS = ['serve', 'daemon', 'persistenced', 'remote-persistence-server', 'observe']\n\nexport interface FleetScanDeps {\n /** 进程枚举(测试注入)。默认 `listOttoProcesses()`。 */\n listProcesses?: () => OttoProcessInfo[]\n /** 租约检视(测试注入)。默认按 OTTO_HOME/sessions/locks 组装只读 lease manager。 */\n inspectLease?: (sessionId: string) => { locked: boolean; pid?: number; alive?: boolean; mtimeAgoMs?: number }\n /** 容器环境探测(测试注入)。默认 `isContainerEnvironment()`。 */\n isContainer?: () => boolean\n /** 租约 stale 阈值(对齐 session-write-lease DEFAULT_STALE_MS)。 */\n staleMs?: number\n}\n\nfunction isResidentCommand(command: string): boolean {\n // 子命令是 bin/otto.js 之后的第一个非 flag 参数\n const afterEntry = command.split('bin/otto.js')[1] ?? ''\n const firstArg = afterEntry.trim().split(/\\s+/)[0] ?? ''\n return RESIDENT_SUBCOMMANDS.includes(firstArg)\n}\n\n/** 扫描全机 otto 主进程并逐个判定孤儿三态。 */\nexport function scanFleet(deps: FleetScanDeps = {}): FleetProcess[] {\n const listProcesses = deps.listProcesses ?? listOttoProcesses\n const isContainer = deps.isContainer ?? isContainerEnvironment\n const staleMs = deps.staleMs ?? 60_000\n // review P2 修复:lease manager 提到判定循环外一次组装(此前每进程 new 一个,含\n // 重复 mkdirSync)。只读组装:installExitHook:false 无退出钩子副作用;inspect() 无\n // acquire/touch。lockDir 与 cli.ts 的会话存储组装同源 OTTO_HOME(P0 修复后一致)。\n const defaultManager = deps.inspectLease\n ? undefined\n : createSessionWriteLeaseManager({\n lockDir: OTTO_SESSION_LOCKS_DIR,\n installExitHook: false,\n })\n const inspectLease = deps.inspectLease ?? ((sessionId: string) => defaultManager!.inspect(sessionId))\n\n const container = isContainer()\n const processes = listProcesses()\n\n return processes.map((p): FleetProcess => {\n // 安全不变式:有 TTY 一律非孤儿。\n if (p.tty !== '??') return { ...p, verdict: 'none', reason: 'has-tty' }\n if (p.ppid !== 1) return { ...p, verdict: 'none', reason: 'has-parent' }\n if (isResidentCommand(p.command)) return { ...p, verdict: 'none', reason: 'resident-service' }\n // 容器环境:ppid==1 是常态,强判定失效(F6 豁免)。\n if (container) {\n return p.etimeMs > WEAK_ORPHAN_MIN_ETIME_MS\n ? { ...p, verdict: 'weak', reason: 'container-ppid1-longrun' }\n : { ...p, verdict: 'none', reason: 'container-ppid1' }\n }\n\n if (!p.sessionId) {\n // 无 --continue:无租约可核 → 弱孤儿(>1h 辅助信号)。\n return p.etimeMs > WEAK_ORPHAN_MIN_ETIME_MS\n ? { ...p, verdict: 'weak', reason: 'no-session-longrun' }\n : { ...p, verdict: 'none', reason: 'no-session-young' }\n }\n\n const lease = inspectLease(p.sessionId)\n if (!lease.locked) {\n // 锁文件不存在——会话早已正常释放,进程却还挂着 → 强孤儿。\n return { ...p, verdict: 'strong', reason: 'lease-released' }\n }\n if (lease.pid === p.pid) {\n // (c) 自持租约的活孤儿——心跳还在但不可交互(见文件头语义说明)。\n return { ...p, verdict: 'strong', reason: 'lease-self-heartbeat' }\n }\n if (lease.mtimeAgoMs !== undefined && lease.mtimeAgoMs > staleMs) {\n // (b) 租约 stale——持有者已死。\n return { ...p, verdict: 'strong', reason: 'lease-stale' }\n }\n // (a) 租约被其他进程持有且 fresh——本候选是废弃副本。\n return { ...p, verdict: 'strong', reason: 'lease-other-owner' }\n })\n}\n","/**\n * /ps 命令:进程舰队列表视图(按会话/TTY 分组、代际计数/内存)。\n */\nimport { isProcessAlive } from '@x-otto/env'\nimport { scanFleet } from './fleet-scan'\n\nimport type { FleetProcess } from './fleet-scan'\n\nexport interface PsCommandIo {\n stdout: (s: string) => void\n stderr: (s: string) => void\n /** SIGTERM 发送(测试注入)。默认 `process.kill(pid, 'SIGTERM')`。 */\n kill?: (pid: number) => void\n /** 存活探测(测试注入)。默认 `isProcessAlive`(@x-otto/env,kill(pid,0) 语义)。 */\n probeAlive?: (pid: number) => boolean\n /** sweep 后存活复查的等待毫秒(测试注入缩短)。默认 3000。 */\n verifyDelayMs?: number\n /** 延迟原语(测试注入 fake)。默认真实 setTimeout。 */\n sleep?: (ms: number) => Promise<void>\n}\n\nfunction formatRss(rssKb: number): string {\n return rssKb >= 1024 ? `${(rssKb / 1024).toFixed(0)}MB` : `${rssKb}KB`\n}\n\nfunction formatEtime(ms: number): string {\n const h = Math.floor(ms / 3_600_000)\n if (h >= 24) return `${Math.floor(h / 24)}d${h % 24}h`\n const m = Math.floor((ms % 3_600_000) / 60_000)\n return h > 0 ? `${h}h${m}m` : `${m}m`\n}\n\nfunction verdictLabel(p: FleetProcess): string {\n if (p.verdict === 'strong') return `ORPHAN(${p.reason})`\n if (p.verdict === 'weak') return `orphan?(${p.reason})`\n return '-'\n}\n\nexport async function runPsCommand(args: string[], io: PsCommandIo): Promise<number> {\n const sweep = args.includes('--sweep')\n const includeWeak = args.includes('--include-weak')\n const json = args.includes('--json')\n const kill = io.kill ?? ((pid: number) => process.kill(pid, 'SIGTERM'))\n\n const fleet = scanFleet()\n const self = process.pid\n\n if (json) {\n io.stdout(JSON.stringify(fleet, null, 2) + '\\n')\n return 0\n }\n\n if (fleet.length === 0) {\n io.stdout('no otto processes found\\n')\n return 0\n }\n\n // 列表输出\n const totalRssKb = fleet.reduce((acc, p) => acc + p.rssKb, 0)\n const strong = fleet.filter((p) => p.verdict === 'strong')\n const weak = fleet.filter((p) => p.verdict === 'weak')\n\n const header = ['PID', 'PPID', 'RSS', 'TTY', 'UP', 'GEN', 'SESSION', 'VERDICT'].join('\\t')\n io.stdout(header + '\\n')\n for (const p of fleet) {\n const selfMark = p.pid === self ? '*' : ''\n io.stdout(\n [\n `${p.pid}${selfMark}`,\n p.ppid,\n formatRss(p.rssKb),\n p.tty,\n formatEtime(p.etimeMs),\n p.generation ?? '-',\n p.sessionId ? p.sessionId.slice(0, 8) : '-',\n verdictLabel(p),\n ].join('\\t') + '\\n',\n )\n }\n io.stdout(\n `\\n${fleet.length} processes, total RSS ${formatRss(totalRssKb)}; ` +\n `${strong.length} strong orphan(s), ${weak.length} weak orphan(s)\\n`,\n )\n\n if (!sweep) {\n if (strong.length > 0 || weak.length > 0) {\n io.stdout('run `otto ps --sweep` to SIGTERM strong orphans' +\n (weak.length > 0 ? ' (--include-weak for weak ones)' : '') + '\\n')\n }\n return 0\n }\n\n // sweep:SIGTERM only;自进程/有 TTY 双保险排除(判定层已保证有 TTY 不入选,此处复核)。\n const targets = [...strong, ...(includeWeak ? weak : [])].filter(\n (p) => p.pid !== self && p.tty === '??',\n )\n if (targets.length === 0) {\n io.stdout('nothing to sweep\\n')\n return 0\n }\n let signalled = 0\n const signalledPids: FleetProcess[] = []\n for (const p of targets) {\n try {\n kill(p.pid)\n signalled++\n signalledPids.push(p)\n io.stdout(`SIGTERM ${p.pid} (${verdictLabel(p)}, ${formatRss(p.rssKb)})\\n`)\n } catch (err) {\n io.stderr(`failed to signal ${p.pid}: ${err instanceof Error ? err.message : String(err)}\\n`)\n }\n }\n\n // review P1-a 修复:SIGTERM 送达 ≠ 进程退出——chaperone 态孤儿装着空函数 signal guard\n // (cli.ts chaperoneSignalGuard,防前台组误杀的既有设计)会吞掉 SIGTERM,进程纹丝不动。\n // 送信号后延迟复查存活,把\"未退出\"名单如实报告并提示手动升级(不自动 SIGKILL——保持\n // RFC-196 \"SIGTERM only\" 不变式,kill -9 决策权留给用户)。\n const probeAlive = io.probeAlive ?? isProcessAlive\n const sleep = io.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)))\n await sleep(io.verifyDelayMs ?? 3000)\n const survivors = signalledPids.filter((p) => probeAlive(p.pid))\n const exited = signalled - survivors.length\n io.stdout(`swept ${exited}/${targets.length}\\n`)\n if (survivors.length > 0) {\n io.stdout(\n `${survivors.length} process(es) ignored SIGTERM (likely chaperone signal guard): ` +\n survivors.map((p) => p.pid).join(', ') +\n `\\nescalate manually if needed: kill -9 ${survivors.map((p) => p.pid).join(' ')}\\n`,\n )\n return 1\n }\n return signalled === targets.length ? 0 : 1\n}\n"],"mappings":"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"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import{r as e}from"./service-client-Cr4sxDVM.js";import{createAttachSessionDataSource as t,createSessionEventReduceBridge as n,createStoreTuiSink as r,startTui as i}from"@x-otto/tui";const a=e=>typeof e==`object`&&!!e&&!Array.isArray(e);function o(e,t){let n;n=e.startsWith(`https://`)?`wss://${e.slice(8)}`:e.startsWith(`http://`)?`ws://${e.slice(7)}`:e.startsWith(`localhost`)||e.startsWith(`127.`)?`ws://${e}`:`wss://${e}`;let r=`${n.replace(/\/$/,``)}/ws`;return t?`${r}?token=${encodeURIComponent(t)}`:r}function s(e,t,n){let r=n.WebSocketImpl??globalThis.WebSocket,i=n.setTimeoutImpl??setTimeout,s=n.clearTimeoutImpl??clearTimeout,c=n.initialBackoffMs??500,l=n.maxBackoffMs??1e4,u=null,d=c,f=null,p=!1,m=0,h=null,g=null,_=1,v=new Map,y=null,b=e=>{e!==y&&(y=e,n.onStateChange?.(e))},x=(e=u)=>{!e||e.readyState!==e.OPEN||e.send(JSON.stringify({method:`Session.subscribe`,params:{sessionId:t,since:m}}))},S=()=>{p||f!==null||(f=i(()=>{f=null,b(`connecting`),w()},d),d=Math.min(d*2,l))},C=e=>{e.onopen=null,e.onmessage=null,e.onclose=null,e.onerror=null;try{e.close()}catch{}u===e&&(u=null)},w=()=>{if(p)return;b(`connecting`);let i;try{i=new r(o(e))}catch{S();return}u=i,i.onopen=()=>{d=c,b(`connected`),x(i)},i.onmessage=e=>{let r;try{r=JSON.parse(String(e.data))}catch{return}if(r.type===`Session.event`&&a(r.data)){let e=r.data;if(typeof e.sessionId!=`string`||e.sessionId!==t||!Number.isSafeInteger(e.seq)||e.seq<0)return;let n=e.seq;if(n<_||v.has(n))return;let{seq:i,...a}=e;for(v.set(n,{seq:n,payload:a});v.has(_);){let e=v.get(_);v.delete(_),h?.(e),m=_,_+=1}return}if(r.type===`Runtime.writeLease`&&a(r.data)){let e=r.data;n.onWriteLease?.({sessionId:typeof e.sessionId==`string`?e.sessionId:t,state:e.state??`not-held`,reason:typeof e.reason==`string`?e.reason:void 0,leaseOwner:typeof e.leaseOwner==`string`?e.leaseOwner:void 0});return}if(r.type===`Runtime.error`&&a(r.data)){let e=r.data,t=typeof e.message==`string`?e.message:``;if(e.code===`EVENT_LOG_GAP`){g?.({reason:`gap`,oldestAvailable:_}),C(i),S();return}n.onNotify?.(`[service] ${e.code??`runtime`}: ${t}`)}},i.onclose=()=>{u===i&&(u=null),p?b(`closed`):(b(`reconnecting`),S())},i.onerror=()=>{}};return{fetchSnapshot:e=>n.fetchSnapshot(e),subscribe(e,t,n,r){m=t,h=n,g=r,m=Math.max(m,t),_=m+1;for(let e of v.keys())e<=m&&v.delete(e);return u?u.readyState===u.OPEN&&x():w(),()=>{p=!0,f!==null&&(s(f),f=null),b(`closed`),u&&C(u),h=null,g=null}},sendVerb(e,t){p||!u||u.readyState!==u.OPEN||u.send(JSON.stringify({method:e,params:t}))}}}async function c(a){let o=a.stdout??process.stdout,c=a.stdin??process.stdin,l=a.fetchImpl??globalThis.fetch,u=!0,d,f=e(a.baseUrl,l,{token:a.token}),p;try{p=await f.fetchSnapshot(a.sessionId)}catch(e){throw o.write(`otto --attach: 无法连接 serve 上的会话 ${a.sessionId}:${e instanceof Error?e.message:String(e)}\n`),e}let m=p.session.id,h,g=e=>{if(!h)return;let t={role:`assistant`,content:[{type:`text`,text:e}]};h.messages.pushMessage(t)};h=await i({stdin:c,stdout:o,columns:o.columns??100,needsSetup:!1,welcome:{cwd:void 0},onSubmit:e=>{let t=e.trimStart();if(t.startsWith(`/`)){g(`[attach] \`${t.split(/\s+/)[0]}\` 为 attach 暂未支持命令。\n仅支持纯文本提交(经 Chat.query 下发到 serve);如需本地命令请退出 attach 后运行。`);return}if(h){if(!u){g(`[attach] 会话的写权已被另一 client 持有(${d??`?`})——本端变只读观察者。请另选 session 或让对方退出 attach。`);return}(async()=>{try{await f.sendPrompt(m,e)}catch(e){g(`[attach] 提交失败:${e instanceof Error?e.message:String(e)}`)}})()}}});let _=s(a.baseUrl,m,{fetchSnapshot:async e=>await f.fetchSnapshot(e),onNotify:e=>g(`[attach] ${e}`),onStateChange:e=>{e===`connected`?(g(`[attach] 已连接 serve。`),a.forceWriteLease?(g(`[attach] --force-write-lease:向服务端发起 force_acquire(kill -9 解锁通道)……`),_.sendVerb(`Session.writeLease.force_acquire`,{sessionId:m})):_.sendVerb(`Session.writeLease.acquire`,{sessionId:m})):e===`reconnecting`?(u=!1,g(`[attach] 连接断开,正在重连……`)):e===`closed`&&(u=!1,g(`[attach] 连接已关闭。`))},onWriteLease:e=>{e.state===`acquired`?(u=!0,d=e.leaseOwner,g(`[attach] 获会话写权——纯文本会走 Chat.query 到 service。`)):e.state===`denied`?(u=!1,d=e.leaseOwner,g(`[attach] 会话写权被另一 client 持有(${e.leaseOwner??`?`})——本端变只读观察者态;提交纯文本会被本端予拦截。\n若对方是 kill -9 残留,可重启带 \`--force-write-lease\` 强抢写权。`)):e.state===`released`&&(u=!1,d=e.leaseOwner,e.reason===`forced_by_other_client`?g(`[attach] 写权被另一 client 强行剥夺(force_acquire by ${e.leaseOwner??`?`})——本端变只读观察者态。`):g(`[attach] 写权已释放——重新 conn 上可申请。`))}}),v=n(r({pushMessage:h.messages.pushMessage,echoUserInput:h.messages.echoUserInput,pushEngineEvent:h.engine.pushEngineEvent,getNotifyStats:h.info.getNotifyStats,setTasks:h.stores.tasks.setTasks,addTurnBoundary:h.stores.tasks.addTurnBoundary})),y=t({transport:_,sessionId:m,onSessionEvent:v.handle,hydrateMessages:(e,t)=>{e.messageStore.setState(()=>({messages:[]})),t.length>0&&e.messageStore.setState(()=>({messages:[...t]})),v.reset(t.length)},onGap:()=>{g(`[attach] 会话事件日志 gap(cursor 被 service 逐出)——已重新水合 messages 快照。`)}});await h.lifecycle.attachSessionSource(y),g(`[attach] 已连接会话 ${m}(host=${p.authority.hostname} pid=${p.authority.pid})。\nsnapshot messages 已灌入 store;后续 Session.event 经 WS 实时应用;纯文本走 Chat.query;/xxx 斜杠命令暂未支持。`)}export{c as runAttach};
|
|
3
|
-
//# sourceMappingURL=run-attach-
|
|
3
|
+
//# sourceMappingURL=run-attach-DDpMk2_N.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-attach-z7YECztf.js","names":[],"sources":["../src/commands/remote-transport.ts","../src/commands/run-attach.ts"],"sourcesContent":["/**\n * remote-transport.ts — RFC-335 T335-07 S2:`otto serve` 的 WS 传输端口实现。\n *\n * web-ui `otto-client` 的 cli 对应物:走同一 `ws://<host>/ws`、同一 JSON-RPC 出站\n * 信封 `{method, params}`、同一 `Session.subscribe{sessionId, since}` 接续协议——\n * Node 22 全局 `WebSocket`(不新增依赖、**不**引 `ws` 包)。\n *\n * 职责与边界(严格对齐 RFC-335 §6 + grill 决议 B):\n * - **重连**归本模块(指数退避,上限 10s);重连 = 新连接,按 subscribe 时记住的\n * since 自动 `Session.subscribe{sessionId, since}` 补发断线期间事件。\n * - **本地合成 seq**:线上 `Session.event` envelope 不含 seq(`coding-service.attachSession`\n * 核实——`append()` 只入服务端 log、扇出 envelope 原样)。本模块对到达事件按序自增\n * 编号、写入 `RemoteSessionEvent.seq`;**不去重不重排**(那是 dataSource 的职责——\n * 它持有 snapshot.cursor 起步的 lastSeq,调 `event.seq <= lastSeq → drop`)。\n * 只一处该表:本模块保证**到达递交顺序 = 服务端发送顺序**(单 ws 连接事件按序抵达)。\n * - **gap(EVENT_LOG_GAP)→ onGap 显式上报 + 断开**:重连续接由 dataSource 的 onGap\n * 回调决策(重水合/重订阅新 cursor,RFC-335 §6:绝不静默补发缺失段)。\n *\n * 不做的事:不 fetch(HTTP 归 service-client)、不做 store 写入(归 dataSource+reduce\n * bridge)、不解释事件 kind、不消费 seq(只是分配透明编号)。\n */\n\nimport { ServiceUnreachableError } from './service-client'\n\n/**\n * 与 tui 契约(`packages/tui/src/bootstrap/session-data-source.ts`)对应的结构镜像。\n * 直接深导 `@x-otto/tui/session-data-source` 会破 package.json exports 边界(只暴露\n * `./`、`./compat`、`./standalone`);故本文件自声明最小类型面,消费方(adapter 装配\n * 代码)将本类型 cast 进 tui 的对应类型——字段名一致即成立。\n *\n * **同步纪律**:tui `session-data-source.ts` 增删 SessionTransport/RemoteSessionEvent\n * 字段时本文件必须同步。\n */\nexport interface RemoteSessionEvent {\n /** 本地合成单调序号(快照后首个到达事件 = snapshot.cursor + 1)。 */\n seq: number\n /** 服务端 Session.event.envelope.data 原样(含 sessionId/kind/... 字段)。 */\n payload: unknown\n}\n\nexport interface RemoteSessionSnapshot {\n session: { id: string; status: string; messages: ReadonlyArray<unknown> }\n cursor: number\n}\n\nexport interface SessionTransport {\n fetchSnapshot(sessionId: string): Promise<RemoteSessionSnapshot>\n subscribe(\n sessionId: string,\n since: number,\n onEvent: (event: RemoteSessionEvent) => void,\n onGap: (info: { reason: 'gap'; oldestAvailable: number }) => void,\n ): () => void\n /**\n * RFC-335 S4 协议面:发任意 WS verb(Session.writeLease.acquire/release 等)。\n * 仅 service 加入 WS 面向服务端的「直接命令语」时调(不遇 Session.event)。\n * 连上时不送(重连中 silently queue——最 recent subscribe 联速 prefer not\n * user-initiated verb race with disconnect)。\n */\n sendVerb(verb: string, data: Record<string, unknown>): void\n}\n\nconst INITIAL_BACKOFF_MS = 500\nconst MAX_BACKOFF_MS = 10_000\nconst WS_PATH = '/ws'\n\ninterface SessionEventEnvelope {\n type: 'Session.event'\n data: Record<string, unknown> & { sessionId?: string }\n}\ninterface RuntimeErrorEnvelope {\n type: 'Runtime.error'\n data: { code?: string; sessionId?: string; message?: string }\n}\ntype WireEnvelope = SessionEventEnvelope | RuntimeErrorEnvelope | { type: string; data?: unknown }\n\n/** 连接生命周期状态(S4 用户可感知——不区分 reconnecting-first/second,统一对两(代)发)。 */\nexport type AttachConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'closed'\n\nexport interface ServiceWsTransportDeps {\n /** snapshot HTTP 读取(复用 service-client,不另开 HTTP client)。 */\n fetchSnapshot: (sessionId: string) => Promise<RemoteSessionSnapshot>\n /** WebSocket 构造器注入(单测注入 fake 避免真网络;生产全局)。 */\n WebSocketImpl?: new (url: string) => WebSocket\n /** 非 gap 的 Runtime.error(PROMPT_FAILED 等)/连接异常通知(上层 toast)。 */\n onNotify?: (msg: string) => void\n /** 连接生命周期通知——connect/disconnect/reconnect/gap-oneway。重复事件不发送(去抖)。 */\n onStateChange?: (state: AttachConnectionState) => void\n /** RFC-335 S4:Runtime.writeLease envelope 到达——acquired/denied/released/not-held。 */\n onWriteLease?: (ev: {\n sessionId: string\n state: 'acquired' | 'denied' | 'released' | 'not-held'\n reason?: string\n leaseOwner?: string\n }) => void\n setTimeoutImpl?: typeof setTimeout\n clearTimeoutImpl?: typeof clearTimeout\n /** 退避常数注入(单测加速)。 */\n initialBackoffMs?: number\n maxBackoffMs?: number\n}\n\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v)\n\n/** `http(s)://host[:port]` → `ws(s)://host[:port]`(与 web-ui otto-client 一致)。 */\nexport function httpToWsUrl(baseUrl: string, token?: string): string {\n let wsBase: string\n if (baseUrl.startsWith('https://')) wsBase = `wss://${baseUrl.slice('https://'.length)}`\n else if (baseUrl.startsWith('http://')) wsBase = `ws://${baseUrl.slice('http://'.length)}`\n else if (baseUrl.startsWith('localhost') || baseUrl.startsWith('127.')) wsBase = `ws://${baseUrl}`\n else wsBase = `wss://${baseUrl}`\n const url = `${wsBase.replace(/\\/$/, '')}${WS_PATH}`\n return token ? `${url}?token=${encodeURIComponent(token)}` : url\n}\n\nexport function createServiceWsTransport(\n baseUrl: string,\n sessionId: string,\n deps: ServiceWsTransportDeps,\n): SessionTransport {\n const WebSocketImpl =\n deps.WebSocketImpl ??\n (globalThis as unknown as { WebSocket: new (u: string) => WebSocket }).WebSocket\n const setTimeoutImpl = deps.setTimeoutImpl ?? setTimeout\n const clearTimeoutImpl = deps.clearTimeoutImpl ?? clearTimeout\n const initialBackoff = deps.initialBackoffMs ?? INITIAL_BACKOFF_MS\n const maxBackoff = deps.maxBackoffMs ?? MAX_BACKOFF_MS\n\n let ws: WebSocket | null = null\n let backoff = initialBackoff\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n let disposed = false\n\n let subscribedSince = 0\n let onEventCb: ((event: RemoteSessionEvent) => void) | null = null\n let onGapCb: ((info: { reason: 'gap'; oldestAvailable: number }) => void) | null = null\n // The last contiguous authority sequence delivered to the data source. A replay\n // and a live event may arrive in either order, so do not advance this over a gap.\n let nextSeq = 1\n const pendingEvents = new Map<number, RemoteSessionEvent>()\n /** 当前会话连接生命周期状态(onStateChange 的发火边——防止重复) */\n let lastState: AttachConnectionState | null = null\n\n const emitState = (next: AttachConnectionState) => {\n if (next === lastState) return\n lastState = next\n deps.onStateChange?.(next)\n }\n\n const sendSubscribe = (socket: WebSocket | null = ws) => {\n if (!socket || socket.readyState !== socket.OPEN) return\n socket.send(\n JSON.stringify({\n method: 'Session.subscribe',\n params: { sessionId, since: subscribedSince },\n }),\n )\n }\n\n const scheduleReconnect = () => {\n if (disposed || reconnectTimer !== null) return\n reconnectTimer = setTimeoutImpl(() => {\n reconnectTimer = null\n emitState('connecting')\n connect()\n }, backoff)\n backoff = Math.min(backoff * 2, maxBackoff)\n }\n\n const closeSocket = (socket: WebSocket) => {\n socket.onopen = null\n socket.onmessage = null\n socket.onclose = null\n socket.onerror = null\n try { socket.close() } catch { /* best-effort */ }\n if (ws === socket) ws = null\n }\n\n const connect = () => {\n if (disposed) return\n emitState('connecting')\n let socket: WebSocket\n try {\n socket = new WebSocketImpl(httpToWsUrl(baseUrl))\n } catch {\n scheduleReconnect()\n return\n }\n ws = socket\n\n socket.onopen = () => {\n backoff = initialBackoff\n emitState('connected')\n sendSubscribe(socket)\n }\n socket.onmessage = (ev: MessageEvent) => {\n let envelope: WireEnvelope\n try {\n envelope = JSON.parse(String(ev.data)) as WireEnvelope\n } catch {\n return // 非 JSON:忽略(同 web-ui)\n }\n\n if (envelope.type === 'Session.event' && isRecord(envelope.data)) {\n const data = envelope.data as Record<string, unknown> & { sessionId?: string; seq?: unknown }\n if (typeof data.sessionId !== 'string' || data.sessionId !== sessionId) return\n // RFC-352 D1:replay and live delivery can interleave. Only the service's\n // event-log sequence preserves causal order; client arrival order does not.\n if (!Number.isSafeInteger(data.seq) || (data.seq as number) < 0) return\n const seq = data.seq as number\n if (seq < nextSeq || pendingEvents.has(seq)) return\n const { seq: _seq, ...payload } = data\n pendingEvents.set(seq, { seq, payload })\n while (pendingEvents.has(nextSeq)) {\n const next = pendingEvents.get(nextSeq)!\n pendingEvents.delete(nextSeq)\n onEventCb?.(next)\n subscribedSince = nextSeq\n nextSeq += 1\n }\n return\n }\n\n if (envelope.type === 'Runtime.writeLease' && isRecord(envelope.data)) {\n // RFC-335 S4:write-lease 通知——run-attach 应能感知 acquired/denied 状态,\n // 以便在 denied 时拒绝本地提交、在 acquired 时允许提交。\n const d = envelope.data as {\n sessionId?: string\n state?: string\n reason?: string\n leaseOwner?: string\n }\n deps.onWriteLease?.({\n sessionId: typeof d.sessionId === 'string' ? d.sessionId : sessionId,\n state: (d.state as 'acquired' | 'denied' | 'released' | 'not-held') ?? 'not-held',\n reason: typeof d.reason === 'string' ? d.reason : undefined,\n leaseOwner: typeof d.leaseOwner === 'string' ? d.leaseOwner : undefined,\n })\n return\n }\n\n if (envelope.type === 'Runtime.error' && isRecord(envelope.data)) {\n const d = envelope.data as { code?: string; message?: string; sessionId?: string }\n const message = typeof d.message === 'string' ? d.message : ''\n if (d.code === 'EVENT_LOG_GAP') {\n // onGap 回调先跑(dataSource 里处理重水合),然后断开——避免继续消费断序流。\n // gap 算 'reconnecting' 中加入(正次 connect.onopen 得为 'connected')。\n onGapCb?.({ reason: 'gap', oldestAvailable: nextSeq })\n closeSocket(socket)\n scheduleReconnect()\n return\n }\n deps.onNotify?.(`[service] ${d.code ?? 'runtime'}: ${message}`)\n }\n }\n socket.onclose = () => {\n if (ws === socket) ws = null\n if (!disposed) {\n // 从 'connected'/'connecting' 均退到 'reconnecting',下轮 socket.onopen 才回 'connected'。\n emitState('reconnecting')\n scheduleReconnect()\n } else {\n emitState('closed')\n }\n }\n socket.onerror = () => {\n // onclose 紧随其后统一走重连路径(对齐 web-ui)。\n }\n }\n\n return {\n fetchSnapshot: (sid) => deps.fetchSnapshot(sid),\n subscribe(_sid, since, onEvent, onGap) {\n subscribedSince = since\n onEventCb = onEvent\n onGapCb = onGap\n // Begin only at the snapshot cursor. Reconnect keeps contiguous progress and\n // any buffered out-of-order replay/live events, so subscribe resumes exactly\n // after the last applied authority sequence.\n subscribedSince = Math.max(subscribedSince, since)\n nextSeq = subscribedSince + 1\n for (const seq of pendingEvents.keys()) {\n if (seq <= subscribedSince) pendingEvents.delete(seq)\n }\n if (!ws) {\n connect()\n } else if (ws.readyState === ws.OPEN) {\n sendSubscribe()\n }\n return () => {\n disposed = true\n if (reconnectTimer !== null) { clearTimeoutImpl(reconnectTimer); reconnectTimer = null }\n // 手动发 'closed'——closeSocket 已清 onclose handler,socket.close() 不再代发状态。\n emitState('closed')\n if (ws) closeSocket(ws)\n onEventCb = null\n onGapCb = null\n }\n },\n\n sendVerb(verb: string, data: Record<string, unknown>): void {\n // RFC-335 S4:直接 verb(Session.writeLease.acquire/release)供应商。\n // 未连接 silently drop(重连 deliver-first-subscribe 不 stick 之前的下达)。\n if (disposed || !ws || ws.readyState !== ws.OPEN) return\n ws.send(JSON.stringify({ method: verb, params: data }))\n },\n }\n}\n\n/** attach 入口快捷形式:复用 service-client 的 fetchSnapshot。 */\nexport function createAttachTransport(args: {\n baseUrl: string\n sessionId: string\n fetchSnapshot: (sessionId: string) => Promise<RemoteSessionSnapshot>\n token?: string\n onNotify?: (msg: string) => void\n}): SessionTransport {\n return createServiceWsTransport(args.baseUrl, args.sessionId, {\n fetchSnapshot: args.fetchSnapshot,\n onNotify: args.onNotify,\n })\n}\n\n// 供 attach 入口报错区分。\nexport { ServiceUnreachableError }\n","/**\n * run-attach.ts — RFC-335 T335-07 S2 装配:`otto --attach <sessionId>` attach 模式。\n *\n * 装配链:\n * service snapshot → ServiceWsTransport(WebSocket) → createAttachSessionDataSource\n * → handle.lifecycle.attachSessionSource → 本地 messageStore(TUI 唯一 writer)\n *\n * 关键取舍(RFC-335 §5/§6 + grill 决议 A):\n * - **writer 唯一**:dataSource 的 hydrateMessages/append 都经 handle 出口 → 本地 zustand\n * messageStore 由 TUI 本体写,cli 这层从不手拿 `messageStore` 句柄。\n * - **事件流不再经 streamController**:attach 模式没 live-stream 流水线(streamText\n * Coalescer/agent sync 管线那都是本地 agent 跑起来的产物),事件逐条经 dataSource\n * 按 seq 应用进 store。\n * - **gap 行为可感知**:EVENT_LOG_GAP → dataSource 重水合(拉新 snapshot + messages\n * 整灌 + 重订阅新 cursor)——用户看到「消息重新出现」,绝不静默断一段。\n * - **写**:纯文本经 Chat.query 下发;任何斜杠命令 fail-fast 提示「attach 暂未支持」。\n */\nimport { startTui, type TuiHandle } from '@x-otto/tui'\nimport type { ChatMessage } from '@x-otto/tui'\nimport {\n createAttachSessionDataSource,\n createSessionEventReduceBridge,\n createStoreTuiSink,\n} from '@x-otto/tui'\n\nimport { createServiceClient } from './service-client'\nimport {\n createServiceWsTransport,\n type RemoteSessionSnapshot,\n type SessionTransport,\n} from './remote-transport'\n\nexport interface RunAttachOptions {\n sessionId: string\n baseUrl: string\n token?: string\n stdin?: NodeJS.ReadStream\n stdout?: NodeJS.WriteStream\n /** 单测注入;生产缺省全局 fetch。 */\n fetchImpl?: typeof globalThis.fetch\n /** RFC-335 S4 补丁:强抢写权——kill -9 造成 stuck lease 的唯一正式解锁口。\n * `otto --attach <id> --force-write-lease`。服务端 audit logger.warn 记录,\n * 原 owner 若仍订阅将收「写权被剥夺」通知。 */\n forceWriteLease?: boolean\n}\n\nexport async function runAttach(opts: RunAttachOptions): Promise<void> {\n const stdout = opts.stdout ?? process.stdout\n const stdin = opts.stdin ?? process.stdin\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch\n\n // RFC-335 S4 写租约状态:false 时 onSubmit 最纯文本被拒(只读观察者态)。初始为 true;\n // transport.onWriteLease 会按 Runtime.writeLease envelope 更新。\n let writeLeaseGranted = true\n let leaseOwnerId: string | undefined\n\n const client = createServiceClient(opts.baseUrl, fetchImpl, { token: opts.token })\n\n // 1) snapshot 先行:确认 session 存在 + 拿 cursor。失败 = 显式报错退出(RFC-335 §6:无 local fallback)。\n let snapshot\n try {\n snapshot = await client.fetchSnapshot(opts.sessionId)\n } catch (err) {\n stdout.write(`otto --attach: 无法连接 serve 上的会话 ${opts.sessionId}:${err instanceof Error ? err.message : String(err)}\\n`)\n throw err\n }\n const sessionId = snapshot.session.id\n\n // 2) 起 TUI(不开本地 session、不起 agent workflow——attach 模式的写全走 Chat.query/slash 灰门)\n let tuiRef: TuiHandle | undefined\n const pushText = (text: string): void => {\n if (!tuiRef) return\n const msg: ChatMessage = { role: 'assistant', content: [{ type: 'text', text }] }\n tuiRef.messages.pushMessage(msg)\n }\n\n tuiRef = await startTui({\n stdin,\n stdout,\n columns: stdout.columns ?? 100,\n needsSetup: false,\n welcome: { cwd: undefined },\n onSubmit: (text: string) => {\n // 灰门:任何斜杠命令 attach 期不消费(steer/pause/resume/setTodoList 归下期;RFC-335 §5)。\n const trimmed = text.trimStart()\n if (trimmed.startsWith('/')) {\n pushText(\n `[attach] \\`${trimmed.split(/\\s+/)[0]}\\` 为 attach 暂未支持命令。\\n` +\n `仅支持纯文本提交(经 Chat.query 下发到 serve);如需本地命令请退出 attach 后运行。`,\n )\n return\n }\n if (!tuiRef) return\n // RFC-335 S4 协议面:写租约被另一 client 占有时,本地预拦截(不发 Chat.query\n // 给服务端制造 PROMPT_FAILED spam)——TUI 提示只读观察者语义。\n if (!writeLeaseGranted) {\n pushText(\n `[attach] 会话的写权已被另一 client 持有(${leaseOwnerId ?? '?'})——本端变只读观察者。` +\n `请另选 session 或让对方退出 attach。`,\n )\n return\n }\n void (async () => {\n try {\n await client.sendPrompt(sessionId, text)\n // 响应经 WS 事件流实时回流到 dataSource → messageStore(无需手动同步)。\n } catch (err) {\n pushText(`[attach] 提交失败:${err instanceof Error ? err.message : String(err)}`)\n }\n })()\n },\n })\n\n // 3) transport + dataSource:service socket → 本地 messageStore\n const transport: SessionTransport = createServiceWsTransport(opts.baseUrl, sessionId, {\n fetchSnapshot: async (sid) => {\n const snap = await client.fetchSnapshot(sid)\n return snap as unknown as RemoteSessionSnapshot\n },\n onNotify: (msg) => pushText(`[attach] ${msg}`),\n onStateChange: (state) => {\n // S4 连接生命周期可感知(去抖已由 transport 管的)。\n if (state === 'connected') {\n pushText('[attach] 已连接 serve。')\n // RFC-335 S4:每次连接立刻尝试 acquire 写租约——酸同 clientId;\n // 服务端返回 acquired/denied 由 onWriteLease 通知。\n // RFC-335 S4 补丁:forceWriteLease 时改走 force_acquire(显式人工强抢)。\n if (opts.forceWriteLease) {\n pushText('[attach] --force-write-lease:向服务端发起 force_acquire(kill -9 解锁通道)……')\n transport.sendVerb('Session.writeLease.force_acquire', { sessionId })\n } else {\n transport.sendVerb('Session.writeLease.acquire', { sessionId })\n }\n } else if (state === 'reconnecting') {\n // RFC-335 S4:重连期间打 write-leases 不确定——服务端在断开时释放;保险状\n // 本端 reactive 防出被 rub:下状态 acquired 前一正在拒绝(更安全默认)。\n writeLeaseGranted = false\n pushText('[attach] 连接断开,正在重连……')\n } else if (state === 'closed') {\n writeLeaseGranted = false\n pushText('[attach] 连接已关闭。')\n }\n },\n onWriteLease: (ev) => {\n if (ev.state === 'acquired') {\n writeLeaseGranted = true\n leaseOwnerId = ev.leaseOwner\n pushText('[attach] 获会话写权——纯文本会走 Chat.query 到 service。')\n } else if (ev.state === 'denied') {\n writeLeaseGranted = false\n leaseOwnerId = ev.leaseOwner\n pushText(\n `[attach] 会话写权被另一 client 持有(${ev.leaseOwner ?? '?'})——` +\n `本端变只读观察者态;提交纯文本会被本端予拦截。\\n` +\n `若对方是 kill -9 残留,可重启带 \\`--force-write-lease\\` 强抢写权。`,\n )\n } else if (ev.state === 'released') {\n writeLeaseGranted = false\n leaseOwnerId = ev.leaseOwner\n if (ev.reason === 'forced_by_other_client') {\n pushText(\n `[attach] 写权被另一 client 强行剥夺(force_acquire by ${ev.leaseOwner ?? '?'})——` +\n `本端变只读观察者态。`,\n )\n } else {\n pushText('[attach] 写权已释放——重新 conn 上可申请。')\n }\n }\n },\n })\n\n // `reduceToTui` is the only event→view projection. It shares the same TuiHandle\n // store actions as local mode; snapshot hydration resets its derived state first.\n const eventBridge = createSessionEventReduceBridge(\n createStoreTuiSink({\n pushMessage: tuiRef.messages.pushMessage,\n echoUserInput: tuiRef.messages.echoUserInput,\n pushEngineEvent: tuiRef.engine.pushEngineEvent,\n getNotifyStats: tuiRef.info.getNotifyStats,\n setTasks: tuiRef.stores.tasks.setTasks,\n addTurnBoundary: tuiRef.stores.tasks.addTurnBoundary,\n }),\n )\n\n const dataSource = createAttachSessionDataSource({\n transport: transport as never,\n sessionId,\n onSessionEvent: eventBridge.handle,\n hydrateMessages: (target, messages) => {\n // Snapshot is authoritative after initial connect and EVENT_LOG_GAP. Reset the\n // reducer before accepting subsequent deltas so turn-boundary indices start at\n // the hydrated message count instead of an obsolete stream.\n target.messageStore.setState(() => ({ messages: [] }))\n if (messages.length > 0) {\n target.messageStore.setState(() => ({\n messages: [...(messages as readonly ChatMessage[]) as never],\n }))\n }\n eventBridge.reset(messages.length)\n },\n onGap: () => {\n pushText(\n `[attach] 会话事件日志 gap(cursor 被 service 逐出)——已重新水合 messages 快照。`,\n )\n },\n })\n\n // 4) 挂载:TuiHandle.lifecycle.attachSessionSource(RFC-347 收敛面内)——TUI 本体握 store;\n // 重复挂载会 throw(attach 一个数据源是硬约束)。stop 由调用方跟 TUI lifecycles 结。\n await tuiRef.lifecycle.attachSessionSource(dataSource)\n\n // 5) attach 概述 = 这一条句话(恰好是唯一由 cli 经 messages.pushMessage 写的位置)\n pushText(\n `[attach] 已连接会话 ${sessionId}(host=${snapshot.authority.hostname} pid=${snapshot.authority.pid})。\\n` +\n 'snapshot messages 已灌入 store;后续 Session.event 经 WS 实时应用;' +\n '纯文本走 Chat.query;/xxx 斜杠命令暂未支持。',\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"run-attach-DDpMk2_N.js","names":[],"sources":["../src/commands/remote-transport.ts","../src/commands/run-attach.ts"],"sourcesContent":["/**\n * remote-transport.ts — RFC-335 T335-07 S2:`otto serve` 的 WS 传输端口实现。\n *\n * web-ui `otto-client` 的 cli 对应物:走同一 `ws://<host>/ws`、同一 JSON-RPC 出站\n * 信封 `{method, params}`、同一 `Session.subscribe{sessionId, since}` 接续协议——\n * Node 22 全局 `WebSocket`(不新增依赖、**不**引 `ws` 包)。\n *\n * 职责与边界(严格对齐 RFC-335 §6 + grill 决议 B):\n * - **重连**归本模块(指数退避,上限 10s);重连 = 新连接,按 subscribe 时记住的\n * since 自动 `Session.subscribe{sessionId, since}` 补发断线期间事件。\n * - **本地合成 seq**:线上 `Session.event` envelope 不含 seq(`coding-service.attachSession`\n * 核实——`append()` 只入服务端 log、扇出 envelope 原样)。本模块对到达事件按序自增\n * 编号、写入 `RemoteSessionEvent.seq`;**不去重不重排**(那是 dataSource 的职责——\n * 它持有 snapshot.cursor 起步的 lastSeq,调 `event.seq <= lastSeq → drop`)。\n * 只一处该表:本模块保证**到达递交顺序 = 服务端发送顺序**(单 ws 连接事件按序抵达)。\n * - **gap(EVENT_LOG_GAP)→ onGap 显式上报 + 断开**:重连续接由 dataSource 的 onGap\n * 回调决策(重水合/重订阅新 cursor,RFC-335 §6:绝不静默补发缺失段)。\n *\n * 不做的事:不 fetch(HTTP 归 service-client)、不做 store 写入(归 dataSource+reduce\n * bridge)、不解释事件 kind、不消费 seq(只是分配透明编号)。\n */\n\nimport { ServiceUnreachableError } from './service-client'\n\n/**\n * 与 tui 契约(`packages/tui/src/bootstrap/session-data-source.ts`)对应的结构镜像。\n * 直接深导 `@x-otto/tui/session-data-source` 会破 package.json exports 边界(只暴露\n * `./`、`./compat`、`./standalone`);故本文件自声明最小类型面,消费方(adapter 装配\n * 代码)将本类型 cast 进 tui 的对应类型——字段名一致即成立。\n *\n * **同步纪律**:tui `session-data-source.ts` 增删 SessionTransport/RemoteSessionEvent\n * 字段时本文件必须同步。\n */\nexport interface RemoteSessionEvent {\n /** 本地合成单调序号(快照后首个到达事件 = snapshot.cursor + 1)。 */\n seq: number\n /** 服务端 Session.event.envelope.data 原样(含 sessionId/kind/... 字段)。 */\n payload: unknown\n}\n\nexport interface RemoteSessionSnapshot {\n session: { id: string; status: string; messages: ReadonlyArray<unknown> }\n cursor: number\n}\n\nexport interface SessionTransport {\n fetchSnapshot(sessionId: string): Promise<RemoteSessionSnapshot>\n subscribe(\n sessionId: string,\n since: number,\n onEvent: (event: RemoteSessionEvent) => void,\n onGap: (info: { reason: 'gap'; oldestAvailable: number }) => void,\n ): () => void\n /**\n * RFC-335 S4 协议面:发任意 WS verb(Session.writeLease.acquire/release 等)。\n * 仅 service 加入 WS 面向服务端的「直接命令语」时调(不遇 Session.event)。\n * 连上时不送(重连中 silently queue——最 recent subscribe 联速 prefer not\n * user-initiated verb race with disconnect)。\n */\n sendVerb(verb: string, data: Record<string, unknown>): void\n}\n\nconst INITIAL_BACKOFF_MS = 500\nconst MAX_BACKOFF_MS = 10_000\nconst WS_PATH = '/ws'\n\ninterface SessionEventEnvelope {\n type: 'Session.event'\n data: Record<string, unknown> & { sessionId?: string }\n}\ninterface RuntimeErrorEnvelope {\n type: 'Runtime.error'\n data: { code?: string; sessionId?: string; message?: string }\n}\ntype WireEnvelope = SessionEventEnvelope | RuntimeErrorEnvelope | { type: string; data?: unknown }\n\n/** 连接生命周期状态(S4 用户可感知——不区分 reconnecting-first/second,统一对两(代)发)。 */\nexport type AttachConnectionState = 'connecting' | 'connected' | 'reconnecting' | 'closed'\n\nexport interface ServiceWsTransportDeps {\n /** snapshot HTTP 读取(复用 service-client,不另开 HTTP client)。 */\n fetchSnapshot: (sessionId: string) => Promise<RemoteSessionSnapshot>\n /** WebSocket 构造器注入(单测注入 fake 避免真网络;生产全局)。 */\n WebSocketImpl?: new (url: string) => WebSocket\n /** 非 gap 的 Runtime.error(PROMPT_FAILED 等)/连接异常通知(上层 toast)。 */\n onNotify?: (msg: string) => void\n /** 连接生命周期通知——connect/disconnect/reconnect/gap-oneway。重复事件不发送(去抖)。 */\n onStateChange?: (state: AttachConnectionState) => void\n /** RFC-335 S4:Runtime.writeLease envelope 到达——acquired/denied/released/not-held。 */\n onWriteLease?: (ev: {\n sessionId: string\n state: 'acquired' | 'denied' | 'released' | 'not-held'\n reason?: string\n leaseOwner?: string\n }) => void\n setTimeoutImpl?: typeof setTimeout\n clearTimeoutImpl?: typeof clearTimeout\n /** 退避常数注入(单测加速)。 */\n initialBackoffMs?: number\n maxBackoffMs?: number\n}\n\nconst isRecord = (v: unknown): v is Record<string, unknown> =>\n typeof v === 'object' && v !== null && !Array.isArray(v)\n\n/** `http(s)://host[:port]` → `ws(s)://host[:port]`(与 web-ui otto-client 一致)。 */\nexport function httpToWsUrl(baseUrl: string, token?: string): string {\n let wsBase: string\n if (baseUrl.startsWith('https://')) wsBase = `wss://${baseUrl.slice('https://'.length)}`\n else if (baseUrl.startsWith('http://')) wsBase = `ws://${baseUrl.slice('http://'.length)}`\n else if (baseUrl.startsWith('localhost') || baseUrl.startsWith('127.')) wsBase = `ws://${baseUrl}`\n else wsBase = `wss://${baseUrl}`\n const url = `${wsBase.replace(/\\/$/, '')}${WS_PATH}`\n return token ? `${url}?token=${encodeURIComponent(token)}` : url\n}\n\nexport function createServiceWsTransport(\n baseUrl: string,\n sessionId: string,\n deps: ServiceWsTransportDeps,\n): SessionTransport {\n const WebSocketImpl =\n deps.WebSocketImpl ??\n (globalThis as unknown as { WebSocket: new (u: string) => WebSocket }).WebSocket\n const setTimeoutImpl = deps.setTimeoutImpl ?? setTimeout\n const clearTimeoutImpl = deps.clearTimeoutImpl ?? clearTimeout\n const initialBackoff = deps.initialBackoffMs ?? INITIAL_BACKOFF_MS\n const maxBackoff = deps.maxBackoffMs ?? MAX_BACKOFF_MS\n\n let ws: WebSocket | null = null\n let backoff = initialBackoff\n let reconnectTimer: ReturnType<typeof setTimeout> | null = null\n let disposed = false\n\n let subscribedSince = 0\n let onEventCb: ((event: RemoteSessionEvent) => void) | null = null\n let onGapCb: ((info: { reason: 'gap'; oldestAvailable: number }) => void) | null = null\n // The last contiguous authority sequence delivered to the data source. A replay\n // and a live event may arrive in either order, so do not advance this over a gap.\n let nextSeq = 1\n const pendingEvents = new Map<number, RemoteSessionEvent>()\n /** 当前会话连接生命周期状态(onStateChange 的发火边——防止重复) */\n let lastState: AttachConnectionState | null = null\n\n const emitState = (next: AttachConnectionState) => {\n if (next === lastState) return\n lastState = next\n deps.onStateChange?.(next)\n }\n\n const sendSubscribe = (socket: WebSocket | null = ws) => {\n if (!socket || socket.readyState !== socket.OPEN) return\n socket.send(\n JSON.stringify({\n method: 'Session.subscribe',\n params: { sessionId, since: subscribedSince },\n }),\n )\n }\n\n const scheduleReconnect = () => {\n if (disposed || reconnectTimer !== null) return\n reconnectTimer = setTimeoutImpl(() => {\n reconnectTimer = null\n emitState('connecting')\n connect()\n }, backoff)\n backoff = Math.min(backoff * 2, maxBackoff)\n }\n\n const closeSocket = (socket: WebSocket) => {\n socket.onopen = null\n socket.onmessage = null\n socket.onclose = null\n socket.onerror = null\n try { socket.close() } catch { /* best-effort */ }\n if (ws === socket) ws = null\n }\n\n const connect = () => {\n if (disposed) return\n emitState('connecting')\n let socket: WebSocket\n try {\n socket = new WebSocketImpl(httpToWsUrl(baseUrl))\n } catch {\n scheduleReconnect()\n return\n }\n ws = socket\n\n socket.onopen = () => {\n backoff = initialBackoff\n emitState('connected')\n sendSubscribe(socket)\n }\n socket.onmessage = (ev: MessageEvent) => {\n let envelope: WireEnvelope\n try {\n envelope = JSON.parse(String(ev.data)) as WireEnvelope\n } catch {\n return // 非 JSON:忽略(同 web-ui)\n }\n\n if (envelope.type === 'Session.event' && isRecord(envelope.data)) {\n const data = envelope.data as Record<string, unknown> & { sessionId?: string; seq?: unknown }\n if (typeof data.sessionId !== 'string' || data.sessionId !== sessionId) return\n // RFC-352 D1:replay and live delivery can interleave. Only the service's\n // event-log sequence preserves causal order; client arrival order does not.\n if (!Number.isSafeInteger(data.seq) || (data.seq as number) < 0) return\n const seq = data.seq as number\n if (seq < nextSeq || pendingEvents.has(seq)) return\n const { seq: _seq, ...payload } = data\n pendingEvents.set(seq, { seq, payload })\n while (pendingEvents.has(nextSeq)) {\n const next = pendingEvents.get(nextSeq)!\n pendingEvents.delete(nextSeq)\n onEventCb?.(next)\n subscribedSince = nextSeq\n nextSeq += 1\n }\n return\n }\n\n if (envelope.type === 'Runtime.writeLease' && isRecord(envelope.data)) {\n // RFC-335 S4:write-lease 通知——run-attach 应能感知 acquired/denied 状态,\n // 以便在 denied 时拒绝本地提交、在 acquired 时允许提交。\n const d = envelope.data as {\n sessionId?: string\n state?: string\n reason?: string\n leaseOwner?: string\n }\n deps.onWriteLease?.({\n sessionId: typeof d.sessionId === 'string' ? d.sessionId : sessionId,\n state: (d.state as 'acquired' | 'denied' | 'released' | 'not-held') ?? 'not-held',\n reason: typeof d.reason === 'string' ? d.reason : undefined,\n leaseOwner: typeof d.leaseOwner === 'string' ? d.leaseOwner : undefined,\n })\n return\n }\n\n if (envelope.type === 'Runtime.error' && isRecord(envelope.data)) {\n const d = envelope.data as { code?: string; message?: string; sessionId?: string }\n const message = typeof d.message === 'string' ? d.message : ''\n if (d.code === 'EVENT_LOG_GAP') {\n // onGap 回调先跑(dataSource 里处理重水合),然后断开——避免继续消费断序流。\n // gap 算 'reconnecting' 中加入(正次 connect.onopen 得为 'connected')。\n onGapCb?.({ reason: 'gap', oldestAvailable: nextSeq })\n closeSocket(socket)\n scheduleReconnect()\n return\n }\n deps.onNotify?.(`[service] ${d.code ?? 'runtime'}: ${message}`)\n }\n }\n socket.onclose = () => {\n if (ws === socket) ws = null\n if (!disposed) {\n // 从 'connected'/'connecting' 均退到 'reconnecting',下轮 socket.onopen 才回 'connected'。\n emitState('reconnecting')\n scheduleReconnect()\n } else {\n emitState('closed')\n }\n }\n socket.onerror = () => {\n // onclose 紧随其后统一走重连路径(对齐 web-ui)。\n }\n }\n\n return {\n fetchSnapshot: (sid) => deps.fetchSnapshot(sid),\n subscribe(_sid, since, onEvent, onGap) {\n subscribedSince = since\n onEventCb = onEvent\n onGapCb = onGap\n // Begin only at the snapshot cursor. Reconnect keeps contiguous progress and\n // any buffered out-of-order replay/live events, so subscribe resumes exactly\n // after the last applied authority sequence.\n subscribedSince = Math.max(subscribedSince, since)\n nextSeq = subscribedSince + 1\n for (const seq of pendingEvents.keys()) {\n if (seq <= subscribedSince) pendingEvents.delete(seq)\n }\n if (!ws) {\n connect()\n } else if (ws.readyState === ws.OPEN) {\n sendSubscribe()\n }\n return () => {\n disposed = true\n if (reconnectTimer !== null) { clearTimeoutImpl(reconnectTimer); reconnectTimer = null }\n // 手动发 'closed'——closeSocket 已清 onclose handler,socket.close() 不再代发状态。\n emitState('closed')\n if (ws) closeSocket(ws)\n onEventCb = null\n onGapCb = null\n }\n },\n\n sendVerb(verb: string, data: Record<string, unknown>): void {\n // RFC-335 S4:直接 verb(Session.writeLease.acquire/release)供应商。\n // 未连接 silently drop(重连 deliver-first-subscribe 不 stick 之前的下达)。\n if (disposed || !ws || ws.readyState !== ws.OPEN) return\n ws.send(JSON.stringify({ method: verb, params: data }))\n },\n }\n}\n\n/** attach 入口快捷形式:复用 service-client 的 fetchSnapshot。 */\nexport function createAttachTransport(args: {\n baseUrl: string\n sessionId: string\n fetchSnapshot: (sessionId: string) => Promise<RemoteSessionSnapshot>\n token?: string\n onNotify?: (msg: string) => void\n}): SessionTransport {\n return createServiceWsTransport(args.baseUrl, args.sessionId, {\n fetchSnapshot: args.fetchSnapshot,\n onNotify: args.onNotify,\n })\n}\n\n// 供 attach 入口报错区分。\nexport { ServiceUnreachableError }\n","/**\n * run-attach.ts — RFC-335 T335-07 S2 装配:`otto --attach <sessionId>` attach 模式。\n *\n * 装配链:\n * service snapshot → ServiceWsTransport(WebSocket) → createAttachSessionDataSource\n * → handle.lifecycle.attachSessionSource → 本地 messageStore(TUI 唯一 writer)\n *\n * 关键取舍(RFC-335 §5/§6 + grill 决议 A):\n * - **writer 唯一**:dataSource 的 hydrateMessages/append 都经 handle 出口 → 本地 zustand\n * messageStore 由 TUI 本体写,cli 这层从不手拿 `messageStore` 句柄。\n * - **事件流不再经 streamController**:attach 模式没 live-stream 流水线(streamText\n * Coalescer/agent sync 管线那都是本地 agent 跑起来的产物),事件逐条经 dataSource\n * 按 seq 应用进 store。\n * - **gap 行为可感知**:EVENT_LOG_GAP → dataSource 重水合(拉新 snapshot + messages\n * 整灌 + 重订阅新 cursor)——用户看到「消息重新出现」,绝不静默断一段。\n * - **写**:纯文本经 Chat.query 下发;任何斜杠命令 fail-fast 提示「attach 暂未支持」。\n */\nimport { startTui, type TuiHandle } from '@x-otto/tui'\nimport type { ChatMessage } from '@x-otto/tui'\nimport {\n createAttachSessionDataSource,\n createSessionEventReduceBridge,\n createStoreTuiSink,\n} from '@x-otto/tui'\n\nimport { createServiceClient } from './service-client'\nimport {\n createServiceWsTransport,\n type RemoteSessionSnapshot,\n type SessionTransport,\n} from './remote-transport'\n\nexport interface RunAttachOptions {\n sessionId: string\n baseUrl: string\n token?: string\n stdin?: NodeJS.ReadStream\n stdout?: NodeJS.WriteStream\n /** 单测注入;生产缺省全局 fetch。 */\n fetchImpl?: typeof globalThis.fetch\n /** RFC-335 S4 补丁:强抢写权——kill -9 造成 stuck lease 的唯一正式解锁口。\n * `otto --attach <id> --force-write-lease`。服务端 audit logger.warn 记录,\n * 原 owner 若仍订阅将收「写权被剥夺」通知。 */\n forceWriteLease?: boolean\n}\n\nexport async function runAttach(opts: RunAttachOptions): Promise<void> {\n const stdout = opts.stdout ?? process.stdout\n const stdin = opts.stdin ?? process.stdin\n const fetchImpl = opts.fetchImpl ?? globalThis.fetch\n\n // RFC-335 S4 写租约状态:false 时 onSubmit 最纯文本被拒(只读观察者态)。初始为 true;\n // transport.onWriteLease 会按 Runtime.writeLease envelope 更新。\n let writeLeaseGranted = true\n let leaseOwnerId: string | undefined\n\n const client = createServiceClient(opts.baseUrl, fetchImpl, { token: opts.token })\n\n // 1) snapshot 先行:确认 session 存在 + 拿 cursor。失败 = 显式报错退出(RFC-335 §6:无 local fallback)。\n let snapshot\n try {\n snapshot = await client.fetchSnapshot(opts.sessionId)\n } catch (err) {\n stdout.write(`otto --attach: 无法连接 serve 上的会话 ${opts.sessionId}:${err instanceof Error ? err.message : String(err)}\\n`)\n throw err\n }\n const sessionId = snapshot.session.id\n\n // 2) 起 TUI(不开本地 session、不起 agent workflow——attach 模式的写全走 Chat.query/slash 灰门)\n let tuiRef: TuiHandle | undefined\n const pushText = (text: string): void => {\n if (!tuiRef) return\n const msg: ChatMessage = { role: 'assistant', content: [{ type: 'text', text }] }\n tuiRef.messages.pushMessage(msg)\n }\n\n tuiRef = await startTui({\n stdin,\n stdout,\n columns: stdout.columns ?? 100,\n needsSetup: false,\n welcome: { cwd: undefined },\n onSubmit: (text: string) => {\n // 灰门:任何斜杠命令 attach 期不消费(steer/pause/resume/setTodoList 归下期;RFC-335 §5)。\n const trimmed = text.trimStart()\n if (trimmed.startsWith('/')) {\n pushText(\n `[attach] \\`${trimmed.split(/\\s+/)[0]}\\` 为 attach 暂未支持命令。\\n` +\n `仅支持纯文本提交(经 Chat.query 下发到 serve);如需本地命令请退出 attach 后运行。`,\n )\n return\n }\n if (!tuiRef) return\n // RFC-335 S4 协议面:写租约被另一 client 占有时,本地预拦截(不发 Chat.query\n // 给服务端制造 PROMPT_FAILED spam)——TUI 提示只读观察者语义。\n if (!writeLeaseGranted) {\n pushText(\n `[attach] 会话的写权已被另一 client 持有(${leaseOwnerId ?? '?'})——本端变只读观察者。` +\n `请另选 session 或让对方退出 attach。`,\n )\n return\n }\n void (async () => {\n try {\n await client.sendPrompt(sessionId, text)\n // 响应经 WS 事件流实时回流到 dataSource → messageStore(无需手动同步)。\n } catch (err) {\n pushText(`[attach] 提交失败:${err instanceof Error ? err.message : String(err)}`)\n }\n })()\n },\n })\n\n // 3) transport + dataSource:service socket → 本地 messageStore\n const transport: SessionTransport = createServiceWsTransport(opts.baseUrl, sessionId, {\n fetchSnapshot: async (sid) => {\n const snap = await client.fetchSnapshot(sid)\n return snap as unknown as RemoteSessionSnapshot\n },\n onNotify: (msg) => pushText(`[attach] ${msg}`),\n onStateChange: (state) => {\n // S4 连接生命周期可感知(去抖已由 transport 管的)。\n if (state === 'connected') {\n pushText('[attach] 已连接 serve。')\n // RFC-335 S4:每次连接立刻尝试 acquire 写租约——酸同 clientId;\n // 服务端返回 acquired/denied 由 onWriteLease 通知。\n // RFC-335 S4 补丁:forceWriteLease 时改走 force_acquire(显式人工强抢)。\n if (opts.forceWriteLease) {\n pushText('[attach] --force-write-lease:向服务端发起 force_acquire(kill -9 解锁通道)……')\n transport.sendVerb('Session.writeLease.force_acquire', { sessionId })\n } else {\n transport.sendVerb('Session.writeLease.acquire', { sessionId })\n }\n } else if (state === 'reconnecting') {\n // RFC-335 S4:重连期间打 write-leases 不确定——服务端在断开时释放;保险状\n // 本端 reactive 防出被 rub:下状态 acquired 前一正在拒绝(更安全默认)。\n writeLeaseGranted = false\n pushText('[attach] 连接断开,正在重连……')\n } else if (state === 'closed') {\n writeLeaseGranted = false\n pushText('[attach] 连接已关闭。')\n }\n },\n onWriteLease: (ev) => {\n if (ev.state === 'acquired') {\n writeLeaseGranted = true\n leaseOwnerId = ev.leaseOwner\n pushText('[attach] 获会话写权——纯文本会走 Chat.query 到 service。')\n } else if (ev.state === 'denied') {\n writeLeaseGranted = false\n leaseOwnerId = ev.leaseOwner\n pushText(\n `[attach] 会话写权被另一 client 持有(${ev.leaseOwner ?? '?'})——` +\n `本端变只读观察者态;提交纯文本会被本端予拦截。\\n` +\n `若对方是 kill -9 残留,可重启带 \\`--force-write-lease\\` 强抢写权。`,\n )\n } else if (ev.state === 'released') {\n writeLeaseGranted = false\n leaseOwnerId = ev.leaseOwner\n if (ev.reason === 'forced_by_other_client') {\n pushText(\n `[attach] 写权被另一 client 强行剥夺(force_acquire by ${ev.leaseOwner ?? '?'})——` +\n `本端变只读观察者态。`,\n )\n } else {\n pushText('[attach] 写权已释放——重新 conn 上可申请。')\n }\n }\n },\n })\n\n // `reduceToTui` is the only event→view projection. It shares the same TuiHandle\n // store actions as local mode; snapshot hydration resets its derived state first.\n const eventBridge = createSessionEventReduceBridge(\n createStoreTuiSink({\n pushMessage: tuiRef.messages.pushMessage,\n echoUserInput: tuiRef.messages.echoUserInput,\n pushEngineEvent: tuiRef.engine.pushEngineEvent,\n getNotifyStats: tuiRef.info.getNotifyStats,\n setTasks: tuiRef.stores.tasks.setTasks,\n addTurnBoundary: tuiRef.stores.tasks.addTurnBoundary,\n }),\n )\n\n const dataSource = createAttachSessionDataSource({\n transport: transport as never,\n sessionId,\n onSessionEvent: eventBridge.handle,\n hydrateMessages: (target, messages) => {\n // Snapshot is authoritative after initial connect and EVENT_LOG_GAP. Reset the\n // reducer before accepting subsequent deltas so turn-boundary indices start at\n // the hydrated message count instead of an obsolete stream.\n target.messageStore.setState(() => ({ messages: [] }))\n if (messages.length > 0) {\n target.messageStore.setState(() => ({\n messages: [...(messages as readonly ChatMessage[]) as never],\n }))\n }\n eventBridge.reset(messages.length)\n },\n onGap: () => {\n pushText(\n `[attach] 会话事件日志 gap(cursor 被 service 逐出)——已重新水合 messages 快照。`,\n )\n },\n })\n\n // 4) 挂载:TuiHandle.lifecycle.attachSessionSource(RFC-347 收敛面内)——TUI 本体握 store;\n // 重复挂载会 throw(attach 一个数据源是硬约束)。stop 由调用方跟 TUI lifecycles 结。\n await tuiRef.lifecycle.attachSessionSource(dataSource)\n\n // 5) attach 概述 = 这一条句话(恰好是唯一由 cli 经 messages.pushMessage 写的位置)\n pushText(\n `[attach] 已连接会话 ${sessionId}(host=${snapshot.authority.hostname} pid=${snapshot.authority.pid})。\\n` +\n 'snapshot messages 已灌入 store;后续 Session.event 经 WS 实时应用;' +\n '纯文本走 Chat.query;/xxx 斜杠命令暂未支持。',\n )\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{t as e}from"./create-host-app-BXQ8bU-B.js";import{t}from"./daemon-base-
|
|
2
|
+
import{t as e}from"./create-host-app-BXQ8bU-B.js";import{t}from"./daemon-base-Y1XI61JY.js";import{SERVICE_PORT as n}from"@x-otto/env";import{resolve as r}from"node:path";import{createCodingService as i}from"@x-otto/service";async function a(n,r={}){let a=r.stdout??(e=>process.stdout.write(e)),c=r.stderr??(e=>process.stderr.write(e));if(n[0]===`help`||n[0]===`--help`)return s(a),0;let l,u,d,f;try{({port:l,inspect:u,staticDir:d,token:f}=o(n))}catch(e){return c(`otto serve: ${e instanceof Error?e.message:String(e)}\n`),1}let p=r.app??e({logger:{name:`otto-serve`,level:r.verbose?`debug`:`info`,destination:`stderr`},inspect:u,workspaceDir:r.projectDir,projectConfigPath:r.configPath,modelId:r.modelId});try{await p.start()}catch(e){return c(`otto serve: failed to start app: ${e instanceof Error?e.message:String(e)}\n`),1}let m=i(p,{port:l,host:`127.0.0.1`,staticDir:d,apiToken:f,wsAuthToken:f,authorityId:process.env.OTTO_AUTHORITY_ID||void 0});try{await m.start()}catch(e){return c(`otto serve: failed to listen on port ${l}: ${e instanceof Error?e.message:String(e)}\n`),await p.stop(),1}let h=m.httpServer.address().port;a(`otto service listening on http://127.0.0.1:${h}${f?` (auth: token required)`:``}
|
|
3
3
|
${d?` web: http://127.0.0.1:${h}/${f?` (打开时附加 ?token=<你的 token>)`:``} (静态页面同源托管 ${d})\n`:``} panels: otto observe summary <session-id>
|
|
4
4
|
otto time-travel checkpoints <session-id>
|
|
5
5
|
otto debug breakpoints list${u?``:" (debugger disabled — restart with `otto serve --inspect`)"}
|
|
@@ -18,4 +18,4 @@ ${d?` web: http://127.0.0.1:${h}/${f?` (打开时附加 ?token=<你的 to
|
|
|
18
18
|
|
|
19
19
|
面板命令默认连 http://127.0.0.1:${n}(可 --remote 覆盖)
|
|
20
20
|
`)}export{a as runServeCommand};
|
|
21
|
-
//# sourceMappingURL=serve-
|
|
21
|
+
//# sourceMappingURL=serve-Ke0nwN9Y.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve-
|
|
1
|
+
{"version":3,"file":"serve-Ke0nwN9Y.js","names":[],"sources":["../src/commands/serve.ts"],"sourcesContent":["import type { AddressInfo } from 'node:net'\nimport { resolve } from 'node:path'\nimport { SERVICE_PORT } from '@x-otto/env'\nimport { createHostApp } from '../create-host-app'\nimport { createCodingService } from '@x-otto/service'\nimport type { AppContext } from '@x-otto/coding'\n\nimport { waitForShutdown } from './daemon-base'\n\nexport interface ServeDeps {\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n /** 全局 CLI 选项透传(与 `otto run` 同源装配)。 */\n projectDir?: string\n configPath?: string\n modelId?: string\n verbose?: boolean\n /** 测试注入:替代生产装配的 app(结构 = AppContext + start/stop)。 */\n app?: AppContext & { start(): Promise<void>; stop(): Promise<void> }\n /** 测试注入:优雅退出信号(生产 = SIGINT/SIGTERM)。 */\n signal?: AbortSignal\n}\n\nexport async function runServeCommand(args: string[], deps: ServeDeps = {}): Promise<number> {\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n\n if (args[0] === 'help' || args[0] === '--help') {\n printHelp(stdout)\n return 0\n }\n\n let port: number\n let inspect: boolean\n let staticDir: string | undefined\n let token: string | undefined\n try {\n ;({ port, inspect, staticDir, token } = parseServeArgs(args))\n } catch (error) {\n stderr(`otto serve: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n const app =\n deps.app ??\n createHostApp({\n logger: {\n name: 'otto-serve',\n level: deps.verbose ? 'debug' : 'info',\n destination: 'stderr',\n },\n // `--inspect` 必须下传:`AppOptions.inspect` 是 `buildInspectDevtools` 的唯一开关\n // (devtools-setup.ts:19 `if (!inspect) return { devtools: null }`)。此前 serve 解析了\n // 该 flag、也据它打印\"调试器已启用\"提示,却从未传给 createApp —— 于是 `app.devtools`\n // 恒为 null,`/api/debugger/*` 全部 503、DebugBridge 也不会被构造。即 `otto serve\n // --inspect` 实际从不启用调试器(提示与事实相反)。\n inspect,\n workspaceDir: deps.projectDir,\n projectConfigPath: deps.configPath,\n modelId: deps.modelId,\n })\n\n try {\n await app.start()\n } catch (error) {\n stderr(`otto serve: failed to start app: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n\n // --token 同时启用 HTTP Bearer(apiToken)与 WS ?token=(wsAuthToken);页面用 URL `?token=` 对上。\n const service = createCodingService(app, {\n port,\n host: '127.0.0.1',\n staticDir,\n apiToken: token,\n wsAuthToken: token,\n // RFC-335 M2:由 `otto daemon start` 下传,使 AuthorityDescriptor.instanceId 与\n // daemon state 记录一致(客户端据此确认\"连上的 authority 就是 state 里那个 daemon\")。\n // 前台直接 `otto serve` 时缺省,由 service 自行生成。\n authorityId: process.env['OTTO_AUTHORITY_ID'] || undefined,\n })\n\n try {\n await service.start()\n } catch (error) {\n stderr(`otto serve: failed to listen on port ${port}: ${error instanceof Error ? error.message : String(error)}\\n`)\n await app.stop()\n return 1\n }\n\n const actualPort = (service.httpServer.address() as AddressInfo).port\n // 不把 token 明文打进 stdout(会落 scrollback/CI 日志)——只提示需附加 ?token=。\n const webHint = token ? ' (打开时附加 ?token=<你的 token>)' : ''\n stdout(`otto service listening on http://127.0.0.1:${actualPort}${token ? ' (auth: token required)' : ''}\n${staticDir ? ` web: http://127.0.0.1:${actualPort}/${webHint} (静态页面同源托管 ${staticDir})\\n` : ''} panels: otto observe summary <session-id>\n otto time-travel checkpoints <session-id>\n otto debug breakpoints list${inspect ? '' : ' (debugger disabled — restart with `otto serve --inspect`)'}\n stop: Ctrl-C\n`)\n\n await waitForShutdown(deps.signal)\n\n stdout('otto serve: shutting down...\\n')\n await service.stop()\n await app.stop()\n // 硬退出:service.stop()/app.stop() 可能留下孤儿 handle(worker_threads port、\n // DB pool、trace tail),吊住事件循环导致 Ctrl+C 后进程不退。与 interactive 路径\n // (cli.ts 末尾 process.exit)对齐。stdio 已同步写,无截断风险。\n process.exit(0)\n}\n\nfunction parseServeArgs(args: string[]): {\n port: number\n inspect: boolean\n staticDir?: string\n token?: string\n} {\n let port = SERVICE_PORT\n let inspect = false\n let staticDir: string | undefined\n let token: string | undefined\n\n // 取下一个值;缺失或形如 --flag(误把下个选项当值)即报错,避免静默吞掉选项。\n const takeValue = (i: number, name: string): string => {\n const raw = args[i + 1]\n if (raw === undefined || raw.startsWith('--')) {\n throw new Error(`${name} requires a value`)\n }\n return raw\n }\n\n for (let i = 0; i < args.length; i++) {\n const arg = args[i]\n if (arg === '--port' || arg === '-p') {\n const raw = takeValue(i, '--port')\n i++\n const n = Number(raw)\n if (!Number.isInteger(n) || n < 0 || n > 65535) {\n throw new Error(`invalid --port value: ${raw}`)\n }\n port = n\n } else if (arg === '--inspect') {\n inspect = true\n } else if (arg === '--static') {\n staticDir = resolve(takeValue(i, '--static'))\n i++\n } else if (arg === '--token') {\n token = takeValue(i, '--token')\n i++\n }\n // 其它(含全局 flag --verbose/--model 等,经 ctx.args 透传进来)忽略——\n // 不能 throw,否则 `otto serve --verbose` 会误伤(全局 flag 由 parseCLI 已解析)。\n }\n\n return { port, inspect, staticDir, token }\n}\n\nfunction printHelp(stdout: (s: string) => void): void {\n stdout(`otto serve — 启动 HTTP/WS service(观测/调试/时间旅行面板的对端)\n\n用法:\n otto serve [--port <n>] [--inspect] [--static <dir>] [--token <t>]\n\n选项:\n --port, -p <n> 监听端口(默认 ${SERVICE_PORT},可经 OTTO_SERVICE_PORT 覆盖;0 = 随机)\n --inspect 启用调试器(断点/暂停面板需要)\n --static <dir> 同源托管一个已构建的前端目录(SPA),页面与 /api、/ws 同端口免 CORS\n --token <t> 启用鉴权:HTTP /api/* 需 Bearer,WS 需 ?token=;页面用 URL ?token=<t> 对上\n\n面板命令默认连 http://127.0.0.1:${SERVICE_PORT}(可 --remote 覆盖)\n`)\n}\n"],"mappings":";gOAuBA,eAAsB,EAAgB,EAAgB,EAAkB,EAAE,CAAmB,CAC3F,IAAM,EAAS,EAAK,SAAY,GAAM,QAAQ,OAAO,MAAM,EAAE,EACvD,EAAS,EAAK,SAAY,GAAM,QAAQ,OAAO,MAAM,EAAE,EAE7D,GAAI,EAAK,KAAO,QAAU,EAAK,KAAO,SAEpC,OADA,EAAU,EAAO,CACV,EAGT,IAAI,EACA,EACA,EACA,EACJ,GAAI,EACA,uCAAsC,EAAe,EAAK,QACrD,EAAO,CAEd,OADA,EAAO,eAAe,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CAC1E,EAGT,IAAM,EACJ,EAAK,KACL,EAAc,CACZ,OAAQ,CACN,KAAM,aACN,MAAO,EAAK,QAAU,QAAU,OAChC,YAAa,SACd,CAMD,UACA,aAAc,EAAK,WACnB,kBAAmB,EAAK,WACxB,QAAS,EAAK,QACf,CAAC,CAEJ,GAAI,CACF,MAAM,EAAI,OAAO,OACV,EAAO,CAEd,OADA,EAAO,oCAAoC,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CAC/F,EAIT,IAAM,EAAU,EAAoB,EAAK,CACvC,OACA,KAAM,YACN,YACA,SAAU,EACV,YAAa,EAIb,YAAa,QAAQ,IAAI,mBAAwB,IAAA,GAClD,CAAC,CAEF,GAAI,CACF,MAAM,EAAQ,OAAO,OACd,EAAO,CAGd,OAFA,EAAO,wCAAwC,EAAK,IAAI,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CACnH,MAAM,EAAI,MAAM,CACT,EAGT,IAAM,EAAc,EAAQ,WAAW,SAAS,CAAiB,KAGjE,EAAO,8CAA8C,IAAa,EAAQ,4BAA8B,GAAG;EAC3G,EAAY,+BAA+B,EAAW,GAFtC,EAAQ,6BAA+B,GAEU,eAAe,EAAU,KAAO,GAAG;;wCAE9D,EAAU,GAAK,+DAA+D;;EAEpH,CAEA,MAAM,EAAgB,EAAK,OAAO,CAElC,EAAO;EAAiC,CACxC,MAAM,EAAQ,MAAM,CACpB,MAAM,EAAI,MAAM,CAIhB,QAAQ,KAAK,EAAE,CAGjB,SAAS,EAAe,EAKtB,CACA,IAAI,EAAO,EACP,EAAU,GACV,EACA,EAGE,GAAa,EAAW,IAAyB,CACrD,IAAM,EAAM,EAAK,EAAI,GACrB,GAAI,IAAQ,IAAA,IAAa,EAAI,WAAW,KAAK,CAC3C,MAAU,MAAM,GAAG,EAAK,mBAAmB,CAE7C,OAAO,GAGT,IAAK,IAAI,EAAI,EAAG,EAAI,EAAK,OAAQ,IAAK,CACpC,IAAM,EAAM,EAAK,GACjB,GAAI,IAAQ,UAAY,IAAQ,KAAM,CACpC,IAAM,EAAM,EAAU,EAAG,SAAS,CAClC,IACA,IAAM,EAAI,OAAO,EAAI,CACrB,GAAI,CAAC,OAAO,UAAU,EAAE,EAAI,EAAI,GAAK,EAAI,MACvC,MAAU,MAAM,yBAAyB,IAAM,CAEjD,EAAO,OACE,IAAQ,YACjB,EAAU,GACD,IAAQ,YACjB,EAAY,EAAQ,EAAU,EAAG,WAAW,CAAC,CAC7C,KACS,IAAQ,YACjB,EAAQ,EAAU,EAAG,UAAU,CAC/B,KAMJ,MAAO,CAAE,OAAM,UAAS,YAAW,QAAO,CAG5C,SAAS,EAAU,EAAmC,CACpD,EAAO;;;;;;6BAMoB,EAAa;;;;;2BAKf,EAAa;EACtC"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{n as e}from"./build-id-
|
|
3
|
-
//# sourceMappingURL=version-
|
|
2
|
+
import{n as e}from"./build-id-D_JURZdd.js";import{createRequire as t}from"node:module";const n=t(import.meta.url);function r(){return`${n(`../package.json`).version??`0.0.0`} (build ${e})`}export{r as t};
|
|
3
|
+
//# sourceMappingURL=version-BKPdYoHG.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version-
|
|
1
|
+
{"version":3,"file":"version-BKPdYoHG.js","names":[],"sources":["../src/version.ts"],"sourcesContent":["import { createRequire } from 'node:module'\n\nimport { BUILD_ID } from './build-id'\n\nconst require = createRequire(import.meta.url)\n\n/**\n * 版本号 + build ID。\n * 格式:`0.1.0 (build 2072)`。build ID = git rev-list --count HEAD,随每次构建自增。\n */\nexport function getVersion(): string {\n const pkg = require('../package.json') as { version?: string }\n const ver = pkg.version ?? '0.0.0'\n return `${ver} (build ${BUILD_ID})`\n}\n"],"mappings":";uFAIA,MAAM,EAAU,EAAc,OAAO,KAAK,IAAI,CAM9C,SAAgB,GAAqB,CAGnC,MAAO,GAFK,EAAQ,kBAAkB,CACtB,SAAW,QACb,UAAU,EAAS"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x-otto/cli",
|
|
3
|
-
"version": "0.0.1-alpha.
|
|
3
|
+
"version": "0.0.1-alpha.44",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -18,26 +18,26 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"react": "^19.2.4",
|
|
21
|
-
"@x-otto/ai": "0.0.1-alpha.2",
|
|
22
|
-
"@x-otto/schedule": "0.0.1-alpha.2",
|
|
23
21
|
"@x-otto/agent": "0.0.1-alpha.2",
|
|
22
|
+
"@x-otto/ai": "0.0.1-alpha.2",
|
|
24
23
|
"@x-otto/coding": "0.0.1-alpha.16",
|
|
24
|
+
"@x-otto/schedule": "0.0.1-alpha.2",
|
|
25
|
+
"@x-otto/persistence": "0.0.1-alpha.3",
|
|
25
26
|
"@x-otto/env": "0.1.0-alpha.6",
|
|
26
|
-
"@x-otto/mcp": "0.0.1-alpha.2",
|
|
27
27
|
"@x-otto/memory": "0.0.1-alpha.2",
|
|
28
|
-
"@x-otto/
|
|
29
|
-
"@x-otto/persistence": "0.0.1-alpha.3",
|
|
30
|
-
"@x-otto/runtime": "0.0.1-alpha.6",
|
|
31
|
-
"@x-otto/interchange": "0.1.0-alpha.4",
|
|
28
|
+
"@x-otto/mcp": "0.0.1-alpha.2",
|
|
32
29
|
"@x-otto/remote-persistence-server": "0.0.1-alpha.3",
|
|
30
|
+
"@x-otto/interchange": "0.1.0-alpha.5",
|
|
31
|
+
"@x-otto/service": "0.0.1-alpha.2",
|
|
33
32
|
"@x-otto/session-contract": "0.0.1-alpha.2",
|
|
33
|
+
"@x-otto/hooks": "0.0.1-alpha.2",
|
|
34
|
+
"@x-otto/runtime": "0.0.1-alpha.7",
|
|
35
|
+
"@x-otto/shared": "0.1.0-alpha.6",
|
|
34
36
|
"@x-otto/session": "0.0.1-alpha.4",
|
|
37
|
+
"@x-otto/tools": "0.0.1-alpha.3",
|
|
35
38
|
"@x-otto/setting": "0.0.1-alpha.3",
|
|
36
39
|
"@x-otto/trace-view": "0.0.1-alpha.2",
|
|
37
|
-
"@x-otto/
|
|
38
|
-
"@x-otto/shared": "0.1.0-alpha.6",
|
|
39
|
-
"@x-otto/tui": "0.0.1-alpha.15",
|
|
40
|
-
"@x-otto/tools": "0.0.1-alpha.3"
|
|
40
|
+
"@x-otto/tui": "0.0.1-alpha.16"
|
|
41
41
|
},
|
|
42
42
|
"publishConfig": {
|
|
43
43
|
"access": "public",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"build-id-ClyMIKLy.js","names":[],"sources":["../src/build-id.ts"],"sourcesContent":["// auto-generated by scripts/gen-build-id.mjs — DO NOT EDIT\nexport const BUILD_ID = 4996\nexport const BUILD_BRANCH = 'dev'\nexport const BUILD_HASH = '07be7b2bd'\n"],"mappings":";AACA,MAAa,EAAW,KAEX,EAAa"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"doctor-lT2QhG2q.js","names":[],"sources":["../src/commands/doctor.ts"],"sourcesContent":["import { homedir, release, platform, arch, cpus, totalmem, freemem } from 'node:os'\nimport { existsSync, statSync, accessSync } from 'node:fs'\nimport { join } from 'node:path'\nimport {\n SESSION_TOTAL_RESIDENCY_BUDGET,\n SESSION_MIN_PER_SESSION_BYTES,\n SESSION_MAX_PER_SESSION_BYTES,\n SESSION_MAX_HISTORY_MESSAGES,\n SESSION_RESUME_WINDOW_MESSAGES,\n} from '@x-otto/session-contract'\nexport interface DoctorDeps {\n stdout: (s: string) => void\n stderr: (s: string) => void\n}\n\nexport interface DoctorReport {\n environment: EnvironmentSection\n configuration: ConfigurationSection\n state?: StateSection\n connectivity: ConnectivitySection\n /** RFC-364 M2:插件装配一致性——发现得到却未装载的缺口。 */\n plugins?: PluginsSection\n /** RFC-326 M6-01:FTUE 环境自检(廉价事实推导,非权威 readiness 判定,见模块头注释)。 */\n ftue?: import('./ftue/ftue-doctor-recovery').DoctorFtueGuidance\n}\n\ninterface PluginsSection {\n discovered: number\n /** 装配缺口的可执行诊断行(空数组 = 无缺口)。 */\n gaps: string[]\n /** 缺口涉及的插件 id(供 --json 消费方精确处理)。 */\n missing: Array<{ id: string; scope?: string }>\n}\n\ninterface EnvironmentSection {\n runtime: string\n installPath: string\n shell: string\n terminal: string\n cpu: string\n memory: { total: number; free: number }\n platform: string\n release: string\n}\n\ninterface ConfigurationSection {\n workspaceDir: string\n storageDir: string\n hasRipgrep: boolean\n hasGit: boolean\n authMode: string\n /** RFC-323:驻留预算配置(只读展示,使基线估算偏差可被校准) */\n residencyBudget?: {\n totalBytes: number\n minPerSessionBytes: number\n maxPerSessionBytes: number\n historyMessages: number\n resumeWindowMessages: number\n }\n}\n\ninterface StateSection {\n dbPath: string\n dbExists: boolean\n dbSize?: number\n walEnabled?: boolean\n snapshots?: number\n changeLogEntries?: number\n sessions?: number\n integrity?: string\n}\n\ninterface ConnectivitySection {\n providerHost: string\n providerReachable: boolean\n providerLatency?: number\n storageHostConfigured: boolean\n}\n\nconst OTTO_DIR = join(homedir(), '.otto')\n\nexport async function runDoctorCommand(\n args: string[],\n deps: DoctorDeps,\n): Promise<number> {\n const stdout = deps.stdout\n const stderr = deps.stderr\n\n // RFC-200 D1.0:子命令路由——首个位置参数(非 -- 开头)分发到诊断子命令;\n // 无子命令时走既有健康检查(行为零变化)。\n const sub = args.find((a) => !a.startsWith('-'))\n if (sub === 'ps' || sub === 'attach' || sub === 'heap') {\n const rest = args.filter((a) => a !== sub)\n switch (sub) {\n case 'ps': {\n const { runDoctorPs } = await import('./handlers/doctor-ps')\n return runDoctorPs(rest, deps)\n }\n case 'attach': {\n const { runDoctorAttach } = await import('./handlers/doctor-attach')\n return runDoctorAttach(rest, deps)\n }\n case 'heap': {\n const { runDoctorHeap } = await import('./handlers/doctor-heap')\n return runDoctorHeap(rest, deps)\n }\n }\n }\n if (sub !== undefined) {\n stderr(`otto doctor: unknown subcommand \"${sub}\" (available: ps, attach, heap)\\n`)\n return 1\n }\n\n if (args.includes('--help') || args.includes('-h')) {\n printHelp(stdout)\n return 0\n }\n\n const outputFormat = args.includes('--json') ? 'json' : args.includes('--summary') ? 'summary' : 'detailed'\n\n try {\n const report = await collectReport()\n\n if (outputFormat === 'json') {\n stdout(JSON.stringify(report, null, 2) + '\\n')\n } else if (outputFormat === 'summary') {\n printSummary(stdout, report)\n } else {\n printDetailed(stdout, report)\n }\n\n return 0\n } catch (error) {\n stderr(`otto doctor: ${error instanceof Error ? error.message : String(error)}\\n`)\n return 1\n }\n}\n\n/** 收集结构化诊断报告(TUI /doctor 折叠档复用;不打印)。 */\nexport async function collectReport(): Promise<DoctorReport> {\n const env = await collectEnvironment()\n const config = await collectConfiguration()\n const state = await collectState()\n const connectivity = await collectConnectivity()\n const { plugins, providerPluginDiscovered } = await collectPluginsAndProviderFlag()\n const ftue = await collectFtueGuidance(providerPluginDiscovered)\n\n return { environment: env, configuration: config, state, connectivity, plugins, ftue }\n}\n\nasync function collectEnvironment(): Promise<EnvironmentSection> {\n return {\n runtime: `Node.js ${process.version}`,\n installPath: process.execPath,\n shell: process.env.SHELL ?? '(unknown)',\n terminal: process.env.TERM ?? '(unknown)',\n cpu: `${cpus().length}×${arch()}`,\n memory: { total: totalmem(), free: freemem() },\n platform: platform(),\n release: release(),\n }\n}\n\nasync function collectConfiguration(): Promise<ConfigurationSection> {\n const authMode = process.env.ANTHROPIC_API_KEY\n ? 'api-key (ANTHROPIC_API_KEY)'\n : process.env.OTTO_API_KEY\n ? 'api-key (OTTO_API_KEY)'\n : process.env.AWS_ACCESS_KEY_ID\n ? 'aws-bedrock (env keys)'\n : 'not configured'\n\n return {\n workspaceDir: process.cwd(),\n storageDir: OTTO_DIR,\n hasRipgrep: await checkBinary('rg'),\n hasGit: await checkBinary('git'),\n authMode,\n // RFC-323 M3:暴露驻留预算配置,使\"基线 1500MB 估算偏差\"(R-2)可被真机数据校准。\n // 纯静态读常量,不接运行时状态——运行时有效分配值取决于活跃会话数 N,在 ps/doctor-ps 展示。\n residencyBudget: {\n totalBytes: SESSION_TOTAL_RESIDENCY_BUDGET,\n minPerSessionBytes: SESSION_MIN_PER_SESSION_BYTES,\n maxPerSessionBytes: SESSION_MAX_PER_SESSION_BYTES,\n historyMessages: SESSION_MAX_HISTORY_MESSAGES,\n resumeWindowMessages: SESSION_RESUME_WINDOW_MESSAGES,\n },\n }\n}\n\n/**\n * RFC-364 M2:插件装配一致性自检。\n *\n * 判据是「cli 侧完整发现(含 bundled 层)」与「App 实际装载」的差集。但 doctor 是\n * 轻量诊断命令(RFC-150 D3 明确要求不承担重依赖),构造真实 App 代价过高——\n * 故此处只做**发现侧**的自检:确认 bundled 目录可解析且其中的扩展能被发现。\n *\n * 真正的「发现 vs 装载」全量比对由运行时侧承担(App 启动路径),本处提供的是\n * 用户可主动运行的诊断入口:当用户报告\"插件不见了\"时,`otto doctor` 能立刻回答\n * \"otto 这一侧到底看见了哪些\"。\n */\n/**\n * RFC-326 M6-01:与 `providerPluginDiscovered` 标志合并计算——`discoverPluginsWithBundled`\n * 是本函数唯一的重量级调用,`collectFtueGuidance` 需要同一份发现结果的 `provider`\n * capability 过滤,若各自重新扫描一次会重复触发文件系统遍历。`providerPluginDiscovered`\n * 在扫描失败时缺省 `false`(诊断失败不阻断其余部分,与既有 fail-soft 语义一致;\n * FTUE 分支据此会诚实报告\"未发现 provider 插件\"而非静默跳过整个 ftue 字段)。\n */\nasync function collectPluginsAndProviderFlag(): Promise<{\n plugins: PluginsSection | undefined\n providerPluginDiscovered: boolean\n}> {\n try {\n const { discoverPluginsWithBundled } = await import('../discover-with-bundled')\n const { hostSelfDescribingOptions } = await import('../create-host-app')\n const { checkPluginAssembly, formatAssemblyGap } = await import('../plugin-assembly-check')\n\n const discovered = discoverPluginsWithBundled({ cwd: process.cwd(), homedir: homedir() })\n const bundledDirs = hostSelfDescribingOptions().pluginBundledDirs ?? []\n const providerPluginDiscovered = discovered.some((p) => p.manifest.capabilities?.includes('provider'))\n\n // 宿主自描述参数解析不出目录,但发现层却报告了 bundled 作用域插件(或反之)——\n // 这正是 M1 修的那类装配漂移,在此处显式暴露。\n const bundledSeen = discovered.filter((p) => p.scope === 'bundled')\n const assemblyBroken = bundledSeen.length > 0 && bundledDirs.length === 0\n\n const result = checkPluginAssembly({\n discovered: discovered.map((p) => ({ id: p.id, scope: p.scope })),\n // 发现侧自检:把「宿主参数解析失败」建模为\"全部 bundled 未装载\"\n active: assemblyBroken\n ? discovered.filter((p) => p.scope !== 'bundled').map((p) => ({ id: p.id, scope: p.scope }))\n : discovered.map((p) => ({ id: p.id, scope: p.scope })),\n })\n\n return {\n plugins: {\n discovered: discovered.length,\n gaps: formatAssemblyGap(result),\n missing: result.missingFromRuntime,\n },\n providerPluginDiscovered,\n }\n } catch {\n // 诊断失败不阻断 doctor 其余部分(与既有 collectState 的 fail-soft 一致)。\n return { plugins: undefined, providerPluginDiscovered: false }\n }\n}\n\n/**\n * RFC-326 M6-01:FTUE 环境自检——三个廉价事实推导诊断状态(见\n * `ftue-doctor-recovery.ts` 头注释的判据边界)。`AuthStore` 构造零 App 依赖\n * (默认读全局 `AUTH_PATH`),`listProviders()` 只读本地文件/env,不发网络请求。\n */\nasync function collectFtueGuidance(\n providerPluginDiscovered: boolean,\n): Promise<import('./ftue/ftue-doctor-recovery').DoctorFtueGuidance | undefined> {\n try {\n const { createDefaultAuthStore } = await import('@x-otto/ai')\n const { deriveDoctorFtueGuidance } = await import('./ftue/ftue-doctor-recovery')\n const { SettingsManager } = await import('@x-otto/setting')\n const { OTTO_CONFIG_FILENAME } = await import('@x-otto/env')\n\n // watchForExternalChanges:false —— doctor 是一次性命令,跑完即退出,不需要\n // fs.watch(同 auth-command.ts 的既有约定,见其头注释)。\n const authStore = createDefaultAuthStore({ watchForExternalChanges: false })\n // listProviders() 读内存缓存(this.cache),不像 getAuthSource/hasCredential 那样\n // 自带 ensureInitialized() 前置——首次调用前必须显式触发一次磁盘加载,否则读到空态。\n await authStore.ensureInitialized()\n const hasAnyCredential = authStore.listProviders().length > 0\n\n const settings = new SettingsManager({ projectConfigPath: join(OTTO_DIR, OTTO_CONFIG_FILENAME) })\n await settings.load()\n const firstPromptSubmitted = typeof settings.get('first_prompt_submitted_at') === 'number'\n\n return deriveDoctorFtueGuidance({ providerPluginDiscovered, hasAnyCredential, firstPromptSubmitted })\n } catch {\n // 诊断失败不阻断 doctor 其余部分。\n return undefined\n }\n}\n\nasync function collectState(): Promise<StateSection | undefined> {\n const dbPath = join(OTTO_DIR, 'storage.db')\n if (!existsSync(dbPath)) {\n return { dbPath, dbExists: false }\n }\n\n try {\n const { SqlitePersistence } = await import('@x-otto/persistence')\n const store = new SqlitePersistence({ dbPath, namespace: 'doctor', wal: false })\n try {\n const info = store.getInfo() as {\n walEnabled: boolean\n snapshots: number\n changeLogEntries: number\n sessions: number\n integrity: string\n }\n\n return {\n dbPath,\n dbExists: true,\n dbSize: getFileSizeSync(dbPath),\n ...info,\n }\n } finally {\n store.close()\n }\n } catch {\n return { dbPath, dbExists: true, integrity: 'error (unable to open)' }\n }\n}\n\nasync function collectConnectivity(): Promise<ConnectivitySection> {\n let providerHost = 'api.anthropic.com'\n let providerReachable = false\n let providerLatency: number | undefined\n\n const start = Date.now()\n try {\n const controller = new AbortController()\n const timeout = setTimeout(() => controller.abort(), 5000)\n await fetch(`https://${providerHost}`, {\n method: 'HEAD',\n signal: controller.signal,\n })\n clearTimeout(timeout)\n providerReachable = true\n providerLatency = Date.now() - start\n } catch {\n providerReachable = false\n }\n\n return {\n providerHost,\n providerReachable,\n providerLatency,\n storageHostConfigured: false,\n }\n}\n\nasync function checkBinary(name: string): Promise<boolean> {\n try {\n accessSync(join('/usr/local/bin', name), 1)\n return true\n } catch {\n try {\n accessSync(join('/usr/bin', name), 1)\n return true\n } catch {\n return false\n }\n }\n}\n\nfunction getFileSizeSync(path: string): number {\n try {\n return statSync(path).size\n } catch {\n return 0\n }\n}\n\nfunction printSummary(stdout: (s: string) => void, report: DoctorReport): void {\n const { environment: env, configuration: config, state, connectivity: conn } = report\n const dbOk = state?.dbExists && state.integrity === 'ok' ? '✅' : '❌'\n const netOk = conn.providerReachable ? '✅' : '❌'\n const pluginGap = report.plugins && report.plugins.gaps.length > 0\n ? ` ⚠ 插件装配缺口 ${report.plugins.missing.length} 项(otto doctor 查看详情)`\n : ''\n // RFC-326 M6-01:只在未配置(非 looks-configured)时提示——已配置用户不需要看\n // 这行噪音,未配置用户能立刻看到下一步命令,不必翻到 detailed 输出。\n const ftueHint = report.ftue && report.ftue.status !== 'looks-configured'\n ? ` ⚠ ${report.ftue.summary} → ${report.ftue.recoveryAction}`\n : ''\n stdout([\n `otto doctor — ${env.runtime} | ${config.storageDir} | DB:${dbOk} ${netOk}`,\n state ? ` ${state.snapshots ?? '?'} snapshots, ${state.changeLogEntries ?? '?'} change_log` : '',\n pluginGap,\n ftueHint,\n ].filter(Boolean).join('\\n') + '\\n')\n}\n\nfunction printDetailed(stdout: (s: string) => void, report: DoctorReport): void {\n const { environment: env, configuration: config, state, connectivity: conn } = report\n\n stdout('\\nEnvironment\\n')\n stdout(` Runtime: ${env.runtime}\\n`)\n stdout(` Install: ${env.installPath}\\n`)\n stdout(` Shell: ${env.shell}\\n`)\n stdout(` Terminal: ${env.terminal} (${env.release})\\n`)\n stdout(` CPU: ${env.cpu}\\n`)\n stdout(` Memory: ${formatBytes(env.memory.free)} / ${formatBytes(env.memory.total)} free\\n`)\n if (config.residencyBudget) {\n const rb = config.residencyBudget\n stdout(` Residency: budget=${formatBytes(rb.totalBytes)} per-session=${formatBytes(rb.minPerSessionBytes)}~${formatBytes(rb.maxPerSessionBytes)} history=${rb.historyMessages} resume=${rb.resumeWindowMessages}\\n`)\n }\n\n stdout('\\nConfiguration\\n')\n stdout(` Workspace: ${config.workspaceDir}\\n`)\n stdout(` Storage: ${config.storageDir} ${existsSync(config.storageDir) ? '✅' : '❌'}\\n`)\n stdout(` ripgrep: ${config.hasRipgrep ? '✅' : '❌'}\\n`)\n stdout(` git: ${config.hasGit ? '✅' : '❌'}\\n`)\n stdout(` Auth: ${config.authMode}\\n`)\n\n // RFC-364 M2:插件装配一致性——有缺口才打印(无缺口时不制造噪音)。\n if (report.plugins) {\n stdout('\\nPlugins\\n')\n stdout(` Discovered: ${report.plugins.discovered}\\n`)\n if (report.plugins.gaps.length > 0) {\n stdout(' Assembly: ❌ 检测到装配缺口\\n')\n for (const line of report.plugins.gaps) stdout(` ${line}\\n`)\n } else {\n stdout(' Assembly: ✅\\n')\n }\n }\n\n if (state) {\n stdout('\\nState\\n')\n stdout(` SQLite DB: ${state.dbPath}\\n`)\n if (state.dbExists) {\n stdout(` size: ${formatBytes(state.dbSize ?? 0)}\\n`)\n stdout(` integrity: ${state.integrity === 'ok' ? 'ok ✅' : state.integrity ?? 'N/A'}\\n`)\n if (state.snapshots !== undefined) stdout(` snapshots: ${state.snapshots}\\n`)\n if (state.changeLogEntries !== undefined) stdout(` change_log: ${state.changeLogEntries} entries\\n`)\n if (state.sessions !== undefined) stdout(` sessions: ${state.sessions}\\n`)\n if (state.walEnabled !== undefined) stdout(` WAL: ${state.walEnabled ? 'enabled' : 'disabled'}\\n`)\n } else {\n stdout(` (no storage.db found — start otto to create one)\\n`)\n }\n }\n\n stdout('\\nConnectivity\\n')\n stdout(` ${conn.providerHost}: ${conn.providerReachable ? `reachable (${conn.providerLatency}ms) ✅` : 'unreachable ❌'}\\n`)\n stdout(` storage host: ${conn.storageHostConfigured ? 'configured' : 'not configured (local only)'}\\n`)\n\n // RFC-326 M6-01:FTUE 环境自检——廉价事实推导,非权威 readiness 判定\n // (见 ftue-doctor-recovery.ts 头注释;权威判定只在 TUI 运行时的 FtueController)。\n if (report.ftue) {\n const icon = report.ftue.status === 'looks-configured' ? '✅' : '⚠'\n stdout('\\nFirst-time setup\\n')\n stdout(` ${icon} ${report.ftue.summary}\\n`)\n stdout(` → ${report.ftue.recoveryAction}\\n`)\n }\n\n stdout('\\n')\n}\n\nfunction printHelp(stdout: (s: string) => void): void {\n stdout(`otto doctor — 环境健康诊断\n\n用法:\n otto doctor [--summary|--json]\n\n选项:\n --summary 紧凑输出(一行摘要)\n --json 结构化 JSON(redacted)\n --help, -h 帮助信息\n\n示例:\n otto doctor\n otto doctor --summary\n otto doctor --json\n`)\n}\n\nfunction formatBytes(bytes: number): string {\n if (bytes === 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB', 'TB']\n const i = Math.floor(Math.log(bytes) / Math.log(1024))\n return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`\n}\n"],"mappings":";saA+EA,MAAM,EAAW,EAAK,GAAS,CAAE,QAAQ,CAEzC,eAAsB,EACpB,EACA,EACiB,CACjB,IAAM,EAAS,EAAK,OACd,EAAS,EAAK,OAId,EAAM,EAAK,KAAM,GAAM,CAAC,EAAE,WAAW,IAAI,CAAC,CAChD,GAAI,IAAQ,MAAQ,IAAQ,UAAY,IAAQ,OAAQ,CACtD,IAAM,EAAO,EAAK,OAAQ,GAAM,IAAM,EAAI,CAC1C,OAAQ,EAAR,CACE,IAAK,KAAM,CACT,GAAM,CAAE,eAAgB,MAAM,OAAO,2BACrC,OAAO,EAAY,EAAM,EAAK,CAEhC,IAAK,SAAU,CACb,GAAM,CAAE,mBAAoB,MAAM,OAAO,+BACzC,OAAO,EAAgB,EAAM,EAAK,CAEpC,IAAK,OAAQ,CACX,GAAM,CAAE,iBAAkB,MAAM,OAAO,6BACvC,OAAO,EAAc,EAAM,EAAK,GAItC,GAAI,IAAQ,IAAA,GAEV,OADA,EAAO,oCAAoC,EAAI,mCAAmC,CAC3E,EAGT,GAAI,EAAK,SAAS,SAAS,EAAI,EAAK,SAAS,KAAK,CAEhD,OADA,EAAU,EAAO,CACV,EAGT,IAAM,EAAe,EAAK,SAAS,SAAS,CAAG,OAAS,EAAK,SAAS,YAAY,CAAG,UAAY,WAEjG,GAAI,CACF,IAAM,EAAS,MAAM,GAAe,CAUpC,OARI,IAAiB,OACnB,EAAO,KAAK,UAAU,EAAQ,KAAM,EAAE,CAAG;EAAK,CACrC,IAAiB,UAC1B,EAAa,EAAQ,EAAO,CAE5B,EAAc,EAAQ,EAAO,CAGxB,QACA,EAAO,CAEd,OADA,EAAO,gBAAgB,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CAAC,IAAI,CAC3E,GAKX,eAAsB,GAAuC,CAC3D,IAAM,EAAM,MAAM,GAAoB,CAChC,EAAS,MAAM,GAAsB,CACrC,EAAQ,MAAM,GAAc,CAC5B,EAAe,MAAM,GAAqB,CAC1C,CAAE,UAAS,4BAA6B,MAAM,GAA+B,CAGnF,MAAO,CAAE,YAAa,EAAK,cAAe,EAAQ,QAAO,eAAc,UAAS,KAFnE,MAAM,EAAoB,EAAyB,CAEsB,CAGxF,eAAe,GAAkD,CAC/D,MAAO,CACL,QAAS,WAAW,QAAQ,UAC5B,YAAa,QAAQ,SACrB,MAAO,QAAQ,IAAI,OAAS,YAC5B,SAAU,QAAQ,IAAI,MAAQ,YAC9B,IAAK,GAAG,GAAM,CAAC,OAAO,GAAG,GAAM,GAC/B,OAAQ,CAAE,MAAO,GAAU,CAAE,KAAM,GAAS,CAAE,CAC9C,SAAU,GAAU,CACpB,QAAS,GAAS,CACnB,CAGH,eAAe,GAAsD,CACnE,IAAM,EAAW,QAAQ,IAAI,kBACzB,8BACA,QAAQ,IAAI,aACV,yBACA,QAAQ,IAAI,kBACV,yBACA,iBAER,MAAO,CACL,aAAc,QAAQ,KAAK,CAC3B,WAAY,EACZ,WAAY,MAAM,EAAY,KAAK,CACnC,OAAQ,MAAM,EAAY,MAAM,CAChC,WAGA,gBAAiB,CACf,WAAY,EACZ,mBAAoB,EACpB,mBAAoB,EACpB,gBAAiB,EACjB,qBAAsB,EACvB,CACF,CAqBH,eAAe,GAGZ,CACD,GAAI,CACF,GAAM,CAAE,8BAA+B,MAAM,OAAO,uCAC9C,CAAE,6BAA8B,MAAM,OAAO,iCAC7C,CAAE,sBAAqB,qBAAsB,MAAM,OAAO,uCAE1D,EAAa,EAA2B,CAAE,IAAK,QAAQ,KAAK,CAAE,QAAS,GAAS,CAAE,CAAC,CACnF,EAAc,GAA2B,CAAC,mBAAqB,EAAE,CACjE,EAA2B,EAAW,KAAM,GAAM,EAAE,SAAS,cAAc,SAAS,WAAW,CAAC,CAKhG,EADc,EAAW,OAAQ,GAAM,EAAE,QAAU,UAAU,CAChC,OAAS,GAAK,EAAY,SAAW,EAElE,EAAS,EAAoB,CACjC,WAAY,EAAW,IAAK,IAAO,CAAE,GAAI,EAAE,GAAI,MAAO,EAAE,MAAO,EAAE,CAEjE,OAAQ,EACJ,EAAW,OAAQ,GAAM,EAAE,QAAU,UAAU,CAAC,IAAK,IAAO,CAAE,GAAI,EAAE,GAAI,MAAO,EAAE,MAAO,EAAE,CAC1F,EAAW,IAAK,IAAO,CAAE,GAAI,EAAE,GAAI,MAAO,EAAE,MAAO,EAAE,CAC1D,CAAC,CAEF,MAAO,CACL,QAAS,CACP,WAAY,EAAW,OACvB,KAAM,EAAkB,EAAO,CAC/B,QAAS,EAAO,mBACjB,CACD,2BACD,MACK,CAEN,MAAO,CAAE,QAAS,IAAA,GAAW,yBAA0B,GAAO,EASlE,eAAe,EACb,EAC+E,CAC/E,GAAI,CACF,GAAM,CAAE,0BAA2B,MAAM,OAAO,cAC1C,CAAE,4BAA6B,MAAM,OAAO,sCAC5C,CAAE,mBAAoB,MAAM,OAAO,mBACnC,CAAE,wBAAyB,MAAM,OAAO,eAIxC,EAAY,EAAuB,CAAE,wBAAyB,GAAO,CAAC,CAG5E,MAAM,EAAU,mBAAmB,CACnC,IAAM,EAAmB,EAAU,eAAe,CAAC,OAAS,EAEtD,EAAW,IAAI,EAAgB,CAAE,kBAAmB,EAAK,EAAU,EAAqB,CAAE,CAAC,CAIjG,OAHA,MAAM,EAAS,MAAM,CAGd,EAAyB,CAAE,2BAA0B,mBAAkB,qBAFjD,OAAO,EAAS,IAAI,4BAA4B,EAAK,SAEkB,CAAC,MAC/F,CAEN,QAIJ,eAAe,GAAkD,CAC/D,IAAM,EAAS,EAAK,EAAU,aAAa,CAC3C,GAAI,CAAC,EAAW,EAAO,CACrB,MAAO,CAAE,SAAQ,SAAU,GAAO,CAGpC,GAAI,CACF,GAAM,CAAE,qBAAsB,MAAM,OAAO,uBACrC,EAAQ,IAAI,EAAkB,CAAE,SAAQ,UAAW,SAAU,IAAK,GAAO,CAAC,CAChF,GAAI,CACF,IAAM,EAAO,EAAM,SAAS,CAQ5B,MAAO,CACL,SACA,SAAU,GACV,OAAQ,EAAgB,EAAO,CAC/B,GAAG,EACJ,QACO,CACR,EAAM,OAAO,OAET,CACN,MAAO,CAAE,SAAQ,SAAU,GAAM,UAAW,yBAA0B,EAI1E,eAAe,GAAoD,CACjE,IAAI,EAAe,oBACf,EAAoB,GACpB,EAEE,EAAQ,KAAK,KAAK,CACxB,GAAI,CACF,IAAM,EAAa,IAAI,gBACjB,EAAU,eAAiB,EAAW,OAAO,CAAE,IAAK,CAC1D,MAAM,MAAM,WAAW,IAAgB,CACrC,OAAQ,OACR,OAAQ,EAAW,OACpB,CAAC,CACF,aAAa,EAAQ,CACrB,EAAoB,GACpB,EAAkB,KAAK,KAAK,CAAG,OACzB,CACN,EAAoB,GAGtB,MAAO,CACL,eACA,oBACA,kBACA,sBAAuB,GACxB,CAGH,eAAe,EAAY,EAAgC,CACzD,GAAI,CAEF,OADA,EAAW,EAAK,iBAAkB,EAAK,CAAE,EAAE,CACpC,QACD,CACN,GAAI,CAEF,OADA,EAAW,EAAK,WAAY,EAAK,CAAE,EAAE,CAC9B,QACD,CACN,MAAO,KAKb,SAAS,EAAgB,EAAsB,CAC7C,GAAI,CACF,OAAO,EAAS,EAAK,CAAC,UAChB,CACN,MAAO,IAIX,SAAS,EAAa,EAA6B,EAA4B,CAC7E,GAAM,CAAE,YAAa,EAAK,cAAe,EAAQ,QAAO,aAAc,GAAS,EACzE,EAAO,GAAO,UAAY,EAAM,YAAc,KAAO,IAAM,IAC3D,EAAQ,EAAK,kBAAoB,IAAM,IACvC,EAAY,EAAO,SAAW,EAAO,QAAQ,KAAK,OAAS,EAC7D,cAAc,EAAO,QAAQ,QAAQ,OAAO,sBAC5C,GAGE,EAAW,EAAO,MAAQ,EAAO,KAAK,SAAW,mBACnD,OAAO,EAAO,KAAK,QAAQ,KAAK,EAAO,KAAK,iBAC5C,GACJ,EAAO,CACL,iBAAiB,EAAI,QAAQ,KAAK,EAAO,WAAW,QAAQ,EAAK,GAAG,IACpE,EAAQ,KAAK,EAAM,WAAa,IAAI,cAAc,EAAM,kBAAoB,IAAI,aAAe,GAC/F,EACA,EACD,CAAC,OAAO,QAAQ,CAAC,KAAK;EAAK,CAAG;EAAK,CAGtC,SAAS,EAAc,EAA6B,EAA4B,CAC9E,GAAM,CAAE,YAAa,EAAK,cAAe,EAAQ,QAAO,aAAc,GAAS,EAS/E,GAPA,EAAO;;EAAkB,CACzB,EAAO,kBAAkB,EAAI,QAAQ,IAAI,CACzC,EAAO,kBAAkB,EAAI,YAAY,IAAI,CAC7C,EAAO,kBAAkB,EAAI,MAAM,IAAI,CACvC,EAAO,kBAAkB,EAAI,SAAS,IAAI,EAAI,QAAQ,KAAK,CAC3D,EAAO,kBAAkB,EAAI,IAAI,IAAI,CACrC,EAAO,kBAAkB,EAAY,EAAI,OAAO,KAAK,CAAC,KAAK,EAAY,EAAI,OAAO,MAAM,CAAC,SAAS,CAC9F,EAAO,gBAAiB,CAC1B,IAAM,EAAK,EAAO,gBAClB,EAAO,yBAAyB,EAAY,EAAG,WAAW,CAAC,gBAAgB,EAAY,EAAG,mBAAmB,CAAC,GAAG,EAAY,EAAG,mBAAmB,CAAC,YAAY,EAAG,gBAAgB,WAAW,EAAG,qBAAqB,IAAI,CAW5N,GARA,EAAO;;EAAoB,CAC3B,EAAO,kBAAkB,EAAO,aAAa,IAAI,CACjD,EAAO,kBAAkB,EAAO,WAAW,GAAG,EAAW,EAAO,WAAW,CAAG,IAAM,IAAI,IAAI,CAC5F,EAAO,kBAAkB,EAAO,WAAa,IAAM,IAAI,IAAI,CAC3D,EAAO,kBAAkB,EAAO,OAAS,IAAM,IAAI,IAAI,CACvD,EAAO,kBAAkB,EAAO,SAAS,IAAI,CAGzC,EAAO,QAGT,GAFA,EAAO;;EAAc,CACrB,EAAO,kBAAkB,EAAO,QAAQ,WAAW,IAAI,CACnD,EAAO,QAAQ,KAAK,OAAS,EAAG,CAClC,EAAO;EAA6B,CACpC,IAAK,IAAM,KAAQ,EAAO,QAAQ,KAAM,EAAO,OAAO,EAAK,IAAI,MAE/D,EAAO;EAAqB,CAyBhC,GArBI,IACF,EAAO;;EAAY,CACnB,EAAO,kBAAkB,EAAM,OAAO,IAAI,CACtC,EAAM,UACR,EAAO,kBAAkB,EAAY,EAAM,QAAU,EAAE,CAAC,IAAI,CAC5D,EAAO,kBAAkB,EAAM,YAAc,KAAO,OAAS,EAAM,WAAa,MAAM,IAAI,CACtF,EAAM,YAAc,IAAA,IAAW,EAAO,kBAAkB,EAAM,UAAU,IAAI,CAC5E,EAAM,mBAAqB,IAAA,IAAW,EAAO,mBAAmB,EAAM,iBAAiB,YAAY,CACnG,EAAM,WAAa,IAAA,IAAW,EAAO,mBAAmB,EAAM,SAAS,IAAI,CAC3E,EAAM,aAAe,IAAA,IAAW,EAAO,kBAAkB,EAAM,WAAa,UAAY,WAAW,IAAI,EAE3G,EAAO;EAAyD,EAIpE,EAAO;;EAAmB,CAC1B,EAAO,KAAK,EAAK,aAAa,IAAI,EAAK,kBAAoB,cAAc,EAAK,gBAAgB,OAAS,gBAAgB,IAAI,CAC3H,EAAO,mBAAmB,EAAK,sBAAwB,aAAe,8BAA8B,IAAI,CAIpG,EAAO,KAAM,CACf,IAAM,EAAO,EAAO,KAAK,SAAW,mBAAqB,IAAM,IAC/D,EAAO;;EAAuB,CAC9B,EAAO,KAAK,EAAK,GAAG,EAAO,KAAK,QAAQ,IAAI,CAC5C,EAAO,OAAO,EAAO,KAAK,eAAe,IAAI,CAG/C,EAAO;EAAK,CAGd,SAAS,EAAU,EAAmC,CACpD,EAAO;;;;;;;;;;;;;;EAcP,CAGF,SAAS,EAAY,EAAuB,CAC1C,GAAI,IAAU,EAAG,MAAO,MACxB,IAAM,EAAQ,CAAC,IAAK,KAAM,KAAM,KAAM,KAAK,CACrC,EAAI,KAAK,MAAM,KAAK,IAAI,EAAM,CAAG,KAAK,IAAI,KAAK,CAAC,CACtD,MAAO,IAAI,EAAiB,MAAM,GAAI,QAAQ,EAAE,CAAC,GAAG,EAAM"}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import{ot as e}from"./command-dispatcher-DhcLUPR5.js";import{t}from"./version-Eh8zZKgk.js";import{resolve as n}from"node:path";var r=e({SUB_COMMAND_NAMES:()=>i,getHelpLines:()=>c,parseCLI:()=>s});const i=new Set([`run`,`serve`,`daemon`,`doctor`,`config`,`migrate`,`auth`,`observe`,`debug`,`time-travel`,`mcp`,`mcp-server`,`persistenced`,`remote-persistence-server`,`persistence`,`extension`,`ps`,`feedback`]),a=new Map([{flags:[`--print`,`-p`],type:`mode`,value:`print`},{flags:[`--interactive`,`-i`],type:`mode`,value:`interactive`},{flags:[`--json`],type:`mode`,value:`json`},{flags:[`--model`,`-m`],type:`string`,key:`model`},{flags:[`--config`,`-c`],type:`string`,key:`configPath`},{flags:[`--project`,`-d`],type:`path`,key:`projectDir`},{flags:[`--quiet`,`-q`],type:`boolean`,key:`quiet`},{flags:[`--verbose`],type:`boolean`,key:`verbose`},{flags:[`--max-tool-turns`],type:`number`,key:`maxToolTurns`},{flags:[`--max-tool-turn-extensions`],type:`number`,key:`maxToolTurnExtensions`},{flags:[`--prompt-token-budget`],type:`number`,key:`promptTokenBudget`},{flags:[`--prompt-time-budget`],type:`number`,key:`promptTimeBudgetMin`},{flags:[`--session-cost-budget`],type:`number`,key:`sessionCostBudget`},{flags:[`--no-stall-detection`],type:`boolean`,key:`noStallDetection`},{flags:[`--stall-window-turns`],type:`number`,key:`stallWindowTurns`},{flags:[`--stall-repeat-threshold`],type:`number`,key:`stallRepeatThreshold`},{flags:[`--continue`],type:`string`,key:`continueSession`},{flags:[`--attach`],type:`string`,key:`attachSession`},{flags:[`--force-write-lease`],type:`boolean`,key:`forceWriteLease`},{flags:[`--memory-restart-grace`],type:`boolean`,key:`memoryRestartGrace`},{flags:[`--resume-prompt`],type:`string`,key:`resumePrompt`},{flags:[`--session-url`],type:`string`,key:`sessionUrl`},{flags:[`--session-token`],type:`string`,key:`sessionToken`},{flags:[`--id`],type:`string`,key:`workspaceId`},{flags:[`--shadow`],type:`boolean`,key:`shadow`},{flags:[`--help`,`-h`],type:`boolean`,key:`showHelp`},{flags:[`--version`,`-v`],type:`action`,handler:l}].flatMap(e=>e.flags.map(t=>[t,e])));var o=class{i;constructor(e){this.args=e,this.i=0}current(){return this.args[this.i]}next(){return this.args[++this.i]}advance(){this.i++}skip(e){this.i+=e}pos(){return this.i}};function s(e){let t=e.slice(2),r=new o(t),s={mode:`interactive`,projectDir:process.cwd(),verbose:!1,quiet:!1},c=r.current(),l,u=``,d,f;c&&i.has(c)?(l=c,u=t.slice(1).join(` `),r.skip(1),l===`run`&&(s.prompt=t.slice(1).filter(e=>!e.startsWith(`-`)).join(` `),s.mode=`print`)):c&&!c.startsWith(`-`)&&(d=c,f=t.slice(1));let p=s.prompt?[s.prompt]:[];for(;r.current()!==void 0;){let e=r.current();if(e===`--inspect`){s.inspect=!0,r.advance();continue}if(e?.startsWith(`--inspect=`)){let t=Number(e.slice(10));s.inspect=Number.isInteger(t)&&t>0?t:!0,r.advance();continue}if(e?.startsWith(`--restart-generation=`)){let t=Number(e.slice(21));Number.isInteger(t)&&t>0&&(s.restartGeneration=t),r.advance();continue}let t=a.get(e);if(t){switch(r.advance(),t.type){case`boolean`:s[t.key]=!0;break;case`mode`:s.mode=t.value;break;case`string`:s[t.key]=r.current(),r.advance();break;case`number`:s[t.key]=Number(r.current()),r.advance();break;case`path`:s[t.key]=n(r.current()??process.cwd()),r.advance();break;case`action`:t.handler();break}continue}e&&!e.startsWith(`-`)&&!l&&(s.prompt===void 0&&s.mode===`interactive`&&(s.mode=`print`),p.push(e)),r.advance()}return p.length>0&&(s.prompt=p.join(` `)),{options:s,subCommand:l,subCommandArgs:u,unresolvedCommandName:d,unresolvedCommandArgs:f}}function c(){return[``,`otto - AI 助理命令行工具`,``,`用法:`,` otto [提示词] 使用提示词启动(print 模式)`,` otto 进入交互式 TUI 模式`,` otto --json "query" JSON 输出模式`,``,`选项:`,` -i, --interactive Interactive 模式(默认)`,` -p, --print Print 模式(非交互)`,` --json JSON 输出模式`,` -m, --model <id> 指定模型`,` -c, --config <path> 配置文件路径`,` -d, --project <dir> 项目目录`,` --verbose 输出详细日志`,` -q, --quiet 只输出 error 日志`,` --shadow 影子模式:otto 自有状态(凭据/会话/记忆/教训/配置/trace/日志)本次会话零磁盘写入(不保证 provider 服务端日志与 shell/OS 层痕迹;RFC-345)`,` --max-tool-turns <n> 单个用户回合最大工具回合数(默认 120)`,` --max-tool-turn-extensions <n> 触顶后压缩续跑的扩展次数(默认主会话 3 / 子 agent 0)`,` --prompt-token-budget <n> 单回合输出 token 预算(配置后轮次上限 ×4 退位安全阀;RFC-104)`,` --prompt-time-budget <min> 单回合墙钟时间预算(分钟)`,` --session-cost-budget <usd> 会话累计成本软预算(美元,超过一次性提醒不阻断;RFC-204)`,` --no-stall-detection 关闭早期停滞检测(默认开启;RFC-203)`,` --stall-window-turns <n> 停滞检测滑动窗口大小(需与 --stall-repeat-threshold 同时提供,默认 8)`,` --stall-repeat-threshold <n> 停滞检测同签名重复阈值(需与 --stall-window-turns 同时提供,默认 4)`,` --continue <id> 继续已有会话`,` --session-url <url> 远端会话持久化地址(如 http://host:3001/sessions)`,` --session-token <token> 远端会话持久化 Bearer token`,` --inspect[=port] 启用 Node.js inspector(默认: 9229)`,` -h, --help 显示帮助`,` --version, -v 显示版本`,``,`命令:`,` otto run <prompt> 执行一次性任务`,` otto serve [--port N] [--inspect] [--static <dir>] [--token <t>] 启动 HTTP/WS service(--static 同源托管前端 SPA,--token 启用鉴权)`,` otto doctor [--summary|--json] 环境健康检查(环境/配置/存储状态/连通性)`,` otto auth login <provider> 登录(cursor / anthropic / openai-codex / github-copilot)`,` otto auth status 查看各 provider 凭据来源`,` otto auth logout <provider> 注销并清除凭据`,` otto config export [--project] [-o <path>] 导出配置(RFC-219)`,` otto config import <file> [--dry-run] [--scope global|project] [--no-plugins] [--plugins-only] [--yes] 导入配置`,` otto config restore [--list] 撤销最近一次导入`,` otto migrate export [--no-sessions] [-o <path>] 导出配置+插件清单+会话数据为迁移包(换电脑用)`,` otto migrate import <archive> [--no-sessions] 导入迁移包(目标已有会话数据时跳过会话部分)`,` otto observe <action> <id> 观测面板(summary / timeline / spans)`,` otto observe debug <action> [...] 调试面板(breakpoints / points / state)`,` otto observe replay <action> <id> 时间旅行(checkpoints / scrubber / restore / fork / replay)`,` otto debug <action> [...] (alias: otto observe debug)`,` otto time-travel <action> <id> (alias: otto observe replay)`,` otto mcp login <name> MCP server OAuth 登录`,` otto mcp logout <name> 登出 MCP server`,` otto mcp list 列出已认证的 MCP server`,` otto extension create <name> 生成插件骨架(命令 + 面板 + manifest,开箱即用)`,` otto extension install <name> [--yes] 安装首方/自定义插件(anthropic-provider / deepseek-provider / browser-use / …)`,` otto extension update [name] 检查并重装已过期的已安装 skill/plugin`,` otto persistence local [...flags] 启动独立持久化服务(storage + events + sync)`,` otto persistence remote [...flags] 启动最小化远程持久化服务(5 REST 端点)`,` otto persistenced [...flags] (alias: otto persistence local)`,` otto remote-persistence-server [...flags] (alias: otto persistence remote)`,` otto ps [--sweep] [--include-weak] [--json] 进程群列表与孤儿清扫(SIGTERM only,RFC-196)`,``,"面板命令默认连 `otto serve` 实例(http://127.0.0.1:8417,OTTO_SERVICE_URL/--remote 可覆盖)。"]}function l(){process.stdout.write(`otto ${t()}\n`),process.exit(0)}export{s as n,r,i as t};
|
|
3
|
-
//# sourceMappingURL=parse-cli-3fWEIMWc.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"parse-cli-3fWEIMWc.js","names":[],"sources":["../src/parse-cli.ts"],"sourcesContent":["import { resolve } from 'node:path'\n\nimport type { CLIOptions, RunMode } from './types'\nimport { getVersion } from './version'\n\nexport type SubCommand = 'run' | 'serve' | 'daemon' | 'doctor' | 'config' | 'migrate' | 'auth' | 'observe' | 'debug' | 'time-travel' | 'mcp' | 'mcp-server' | 'persistenced' | 'remote-persistence-server' | 'persistence' | 'extension' | 'ps' | 'feedback'\n\nexport interface ParsedCLI {\n options: CLIOptions\n /** 未命中任何内建子命令时为 `undefined`(而非 `null` 哨兵——与 SUB_COMMAND_NAMES 的\n * 元素类型保持一致,消费方无需 `.filter(n => n !== null)` 剔除哨兵值)。 */\n subCommand: SubCommand | undefined\n subCommandArgs: string\n /**\n * RFC-112 D2:内建 `SUB_COMMAND_NAMES` 未命中的第一个位置参数(候选插件 CLI 命令名,\n * 如 `otto feishu ...` 里的 `\"feishu\"`)。`undefined` 表示第一个参数已命中内建命令,\n * 或压根没有位置参数。**不改变** `options.prompt`/`subCommand` 的既有构建逻辑——\n * 这两者仍然按\"当作 prompt\"的旧行为完整计算好,供插件命令名查无匹配时零改动回退。\n */\n unresolvedCommandName?: string\n /**\n * 与 `unresolvedCommandName` 配对:该候选名之后的**原始** args(未做 flag 过滤,含 `-h`\n * 等字面量),只在插件命令表命中该名字时使用(透传给插件可执行文件)。\n */\n unresolvedCommandArgs?: string[]\n}\n\n/** RFC-112 D2:导出供 `cli.ts` 构建插件 CLI 命令表时用作保留字集合(插件不得覆盖)。 */\nexport const SUB_COMMAND_NAMES = new Set<SubCommand>([\n 'run', 'serve', 'daemon', 'doctor', 'config', 'migrate', 'auth', 'observe', 'debug', 'time-travel', 'mcp', 'mcp-server', 'persistenced', 'remote-persistence-server', 'persistence', 'extension', 'ps', 'feedback',\n])\n\ntype OptionEntry =\n | { flags: string[]; type: 'boolean'; key: keyof CLIOptions }\n | { flags: string[]; type: 'mode'; value: RunMode }\n | { flags: string[]; type: 'string'; key: keyof CLIOptions }\n | { flags: string[]; type: 'number'; key: keyof CLIOptions }\n | { flags: string[]; type: 'path'; key: keyof CLIOptions }\n | { flags: string[]; type: 'action'; handler: () => void }\n\nconst OPTIONS: OptionEntry[] = [\n { flags: ['--print', '-p'], type: 'mode', value: 'print' },\n { flags: ['--interactive', '-i'], type: 'mode', value: 'interactive' },\n { flags: ['--json'], type: 'mode', value: 'json' },\n { flags: ['--model', '-m'], type: 'string', key: 'model' },\n { flags: ['--config', '-c'], type: 'string', key: 'configPath' },\n { flags: ['--project', '-d'], type: 'path', key: 'projectDir' },\n { flags: ['--quiet', '-q'], type: 'boolean', key: 'quiet' },\n { flags: ['--verbose'], type: 'boolean', key: 'verbose' },\n { flags: ['--max-tool-turns'], type: 'number', key: 'maxToolTurns' },\n { flags: ['--max-tool-turn-extensions'], type: 'number', key: 'maxToolTurnExtensions' },\n { flags: ['--prompt-token-budget'], type: 'number', key: 'promptTokenBudget' },\n { flags: ['--prompt-time-budget'], type: 'number', key: 'promptTimeBudgetMin' },\n { flags: ['--session-cost-budget'], type: 'number', key: 'sessionCostBudget' },\n { flags: ['--no-stall-detection'], type: 'boolean', key: 'noStallDetection' },\n { flags: ['--stall-window-turns'], type: 'number', key: 'stallWindowTurns' },\n { flags: ['--stall-repeat-threshold'], type: 'number', key: 'stallRepeatThreshold' },\n { flags: ['--continue'], type: 'string', key: 'continueSession' },\n { flags: ['--attach'], type: 'string', key: 'attachSession' },\n { flags: ['--force-write-lease'], type: 'boolean', key: 'forceWriteLease' },\n // RFC-115 M3(P1-1 修正):仅由 cli.ts 的内存治理触发重启 chaperone 附加,不对外文档化——\n // 区分\"内存压力触发的自动重启\"与\"用户手动 otto --continue\",前者才应起算 D5 宽限期。\n { flags: ['--memory-restart-grace'], type: 'boolean', key: 'memoryRestartGrace' },\n // RFC-115 M4:仅由 cli.ts 的内存治理触发重启 chaperone 附加,不对外文档化——重启前\n // 会话正忙时的续跑原文,子进程启动后自动重新提交为续跑轮。\n { flags: ['--resume-prompt'], type: 'string', key: 'resumePrompt' },\n { flags: ['--session-url'], type: 'string', key: 'sessionUrl' },\n { flags: ['--session-token'], type: 'string', key: 'sessionToken' },\n { flags: ['--id'], type: 'string', key: 'workspaceId' },\n { flags: ['--shadow'], type: 'boolean', key: 'shadow' },\n { flags: ['--help', '-h'], type: 'boolean', key: 'showHelp' },\n { flags: ['--version', '-v'], type: 'action', handler: printVersion },\n]\n\nconst OPTION_LOOKUP: Map<string, OptionEntry> = new Map(\n OPTIONS.flatMap(e => e.flags.map(f => [f, e] as const)),\n)\n\nclass ArgCursor {\n private i: number\n constructor(private args: string[]) {\n this.i = 0\n }\n current(): string | undefined { return this.args[this.i] }\n next(): string | undefined { return this.args[++this.i] }\n advance(): void { this.i++ }\n skip(count: number): void { this.i += count }\n pos(): number { return this.i }\n}\n\nexport function parseCLI(argv: string[]): ParsedCLI {\n const args = argv.slice(2)\n const cursor = new ArgCursor(args)\n\n const options: CLIOptions = {\n mode: 'interactive',\n projectDir: process.cwd(),\n verbose: false,\n quiet: false,\n }\n\n const first = cursor.current()\n let subCommand: SubCommand | undefined\n let subCommandArgs = ''\n let unresolvedCommandName: string | undefined\n let unresolvedCommandArgs: string[] | undefined\n if (first && SUB_COMMAND_NAMES.has(first as SubCommand)) {\n subCommand = first as SubCommand\n subCommandArgs = args.slice(1).join(' ')\n cursor.skip(1)\n\n if (subCommand === 'run') {\n options.prompt = args.slice(1).filter(a => !a.startsWith('-')).join(' ')\n options.mode = 'print'\n }\n } else if (first && !first.startsWith('-')) {\n // RFC-112 D2:第一个位置参数不是内建命令,也不以 `-` 开头(不是 flag)——\n // 记录为候选插件 CLI 命令名,供 cli.ts 查插件表二次判定;不改变下方现有的\n // \"当作 prompt\" 逻辑(该逻辑仍会照常把 first 纳入 promptParts,回退路径零改动)。\n unresolvedCommandName = first\n unresolvedCommandArgs = args.slice(1)\n }\n\n let promptParts: string[] = options.prompt ? [options.prompt] : []\n\n while (cursor.current() !== undefined) {\n const arg = cursor.current()!\n\n if (arg === '--inspect') {\n options.inspect = true\n cursor.advance()\n continue\n }\n if (arg?.startsWith('--inspect=')) {\n const port = Number(arg.slice('--inspect='.length))\n options.inspect = Number.isInteger(port) && port > 0 ? port : true\n cursor.advance()\n continue\n }\n // RFC-196 M3:重启代际计数(chaperone 重启 spawn 注入,不文档化——内部可观测标记,\n // 供 ≥3 代告警与 otto ps GEN 列展示;不驱动任何行为分支)。\n if (arg?.startsWith('--restart-generation=')) {\n const gen = Number(arg.slice('--restart-generation='.length))\n if (Number.isInteger(gen) && gen > 0) options.restartGeneration = gen\n cursor.advance()\n continue\n }\n\n const entry = OPTION_LOOKUP.get(arg)\n if (entry) {\n cursor.advance()\n switch (entry.type) {\n case 'boolean':\n ;(options as unknown as Record<string, unknown>)[entry.key] = true\n break\n case 'mode':\n options.mode = entry.value\n break\n case 'string':\n ;(options as unknown as Record<string, unknown>)[entry.key] = cursor.current()\n cursor.advance()\n break\n case 'number':\n ;(options as unknown as Record<string, unknown>)[entry.key] = Number(cursor.current())\n cursor.advance()\n break\n case 'path':\n ;(options as unknown as Record<string, unknown>)[entry.key] = resolve(cursor.current() ?? process.cwd())\n cursor.advance()\n break\n case 'action':\n entry.handler()\n break\n }\n continue\n }\n\n if (arg && !arg.startsWith('-') && !subCommand) {\n if (options.prompt === undefined && options.mode === 'interactive') {\n options.mode = 'print'\n }\n promptParts.push(arg)\n }\n cursor.advance()\n }\n\n if (promptParts.length > 0) {\n options.prompt = promptParts.join(' ')\n }\n\n return { options, subCommand, subCommandArgs, unresolvedCommandName, unresolvedCommandArgs }\n}\n\n/** 帮助文本行(供 plain-text printHelp 与 TUI InfoDialog 共用)。 */\nexport function getHelpLines(): string[] {\n return [\n '',\n 'otto - AI 助理命令行工具',\n '',\n '用法:',\n ' otto [提示词] 使用提示词启动(print 模式)',\n ' otto 进入交互式 TUI 模式',\n ' otto --json \"query\" JSON 输出模式',\n '',\n '选项:',\n ' -i, --interactive Interactive 模式(默认)',\n ' -p, --print Print 模式(非交互)',\n ' --json JSON 输出模式',\n ' -m, --model <id> 指定模型',\n ' -c, --config <path> 配置文件路径',\n ' -d, --project <dir> 项目目录',\n ' --verbose 输出详细日志',\n ' -q, --quiet 只输出 error 日志',\n ' --shadow 影子模式:otto 自有状态(凭据/会话/记忆/教训/配置/trace/日志)本次会话零磁盘写入(不保证 provider 服务端日志与 shell/OS 层痕迹;RFC-345)',\n ' --max-tool-turns <n> 单个用户回合最大工具回合数(默认 120)',\n ' --max-tool-turn-extensions <n> 触顶后压缩续跑的扩展次数(默认主会话 3 / 子 agent 0)',\n ' --prompt-token-budget <n> 单回合输出 token 预算(配置后轮次上限 ×4 退位安全阀;RFC-104)',\n ' --prompt-time-budget <min> 单回合墙钟时间预算(分钟)',\n ' --session-cost-budget <usd> 会话累计成本软预算(美元,超过一次性提醒不阻断;RFC-204)',\n ' --no-stall-detection 关闭早期停滞检测(默认开启;RFC-203)',\n ' --stall-window-turns <n> 停滞检测滑动窗口大小(需与 --stall-repeat-threshold 同时提供,默认 8)',\n ' --stall-repeat-threshold <n> 停滞检测同签名重复阈值(需与 --stall-window-turns 同时提供,默认 4)',\n ' --continue <id> 继续已有会话',\n ' --session-url <url> 远端会话持久化地址(如 http://host:3001/sessions)',\n ' --session-token <token> 远端会话持久化 Bearer token',\n ' --inspect[=port] 启用 Node.js inspector(默认: 9229)',\n ' -h, --help 显示帮助',\n ' --version, -v 显示版本',\n '',\n '命令:',\n ' otto run <prompt> 执行一次性任务',\n ' otto serve [--port N] [--inspect] [--static <dir>] [--token <t>] 启动 HTTP/WS service(--static 同源托管前端 SPA,--token 启用鉴权)',\n ' otto doctor [--summary|--json] 环境健康检查(环境/配置/存储状态/连通性)',\n ' otto auth login <provider> 登录(cursor / anthropic / openai-codex / github-copilot)',\n ' otto auth status 查看各 provider 凭据来源',\n ' otto auth logout <provider> 注销并清除凭据',\n ' otto config export [--project] [-o <path>] 导出配置(RFC-219)',\n ' otto config import <file> [--dry-run] [--scope global|project] [--no-plugins] [--plugins-only] [--yes] 导入配置',\n ' otto config restore [--list] 撤销最近一次导入',\n ' otto migrate export [--no-sessions] [-o <path>] 导出配置+插件清单+会话数据为迁移包(换电脑用)',\n ' otto migrate import <archive> [--no-sessions] 导入迁移包(目标已有会话数据时跳过会话部分)',\n ' otto observe <action> <id> 观测面板(summary / timeline / spans)',\n ' otto observe debug <action> [...] 调试面板(breakpoints / points / state)',\n ' otto observe replay <action> <id> 时间旅行(checkpoints / scrubber / restore / fork / replay)',\n ' otto debug <action> [...] (alias: otto observe debug)',\n ' otto time-travel <action> <id> (alias: otto observe replay)',\n ' otto mcp login <name> MCP server OAuth 登录',\n ' otto mcp logout <name> 登出 MCP server',\n ' otto mcp list 列出已认证的 MCP server',\n ' otto extension create <name> 生成插件骨架(命令 + 面板 + manifest,开箱即用)',\n ' otto extension install <name> [--yes] 安装首方/自定义插件(anthropic-provider / deepseek-provider / browser-use / …)',\n ' otto extension update [name] 检查并重装已过期的已安装 skill/plugin',\n ' otto persistence local [...flags] 启动独立持久化服务(storage + events + sync)',\n ' otto persistence remote [...flags] 启动最小化远程持久化服务(5 REST 端点)',\n ' otto persistenced [...flags] (alias: otto persistence local)',\n ' otto remote-persistence-server [...flags] (alias: otto persistence remote)',\n ' otto ps [--sweep] [--include-weak] [--json] 进程群列表与孤儿清扫(SIGTERM only,RFC-196)',\n '',\n '面板命令默认连 `otto serve` 实例(http://127.0.0.1:8417,OTTO_SERVICE_URL/--remote 可覆盖)。',\n ]\n}\n\nfunction printVersion(): void {\n process.stdout.write(`otto ${getVersion()}\\n`)\n process.exit(0)\n}\n"],"mappings":"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"}
|