@x-otto/cli 0.0.1-alpha.8 → 0.1.0-alpha.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/README.md +72 -65
  2. package/bin/otto.js +6 -0
  3. package/dist/{auth-command-CXAB3_hs.js → auth-command-BdIvuKxx.js} +3 -3
  4. package/dist/auth-command-BdIvuKxx.js.map +1 -0
  5. package/dist/backend-handler-DUafuNWw.js +3 -0
  6. package/dist/backend-handler-DUafuNWw.js.map +1 -0
  7. package/dist/build-id-CWNug-3b.js +3 -0
  8. package/dist/build-id-CWNug-3b.js.map +1 -0
  9. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/meta.json +1 -1
  10. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin-dist/plugin.cjs +1056 -43191
  11. package/dist/bundled-extensions/plugin-otto-wire-protocols/plugin.ts +4 -0
  12. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/anthropic-messages.ts +14 -24
  13. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-completions.ts +27 -10
  14. package/dist/bundled-extensions/plugin-otto-wire-protocols/src/openai-responses.ts +20 -17
  15. package/dist/bundled-extensions-C3n8jg20.js +3 -0
  16. package/dist/bundled-extensions-C3n8jg20.js.map +1 -0
  17. package/dist/command-dispatcher-B2Xx3NP7.js +18 -0
  18. package/dist/command-dispatcher-B2Xx3NP7.js.map +1 -0
  19. package/dist/config-migration-DatflIxs.js.map +1 -1
  20. package/dist/create-host-app-iA84KNcI.js +3 -0
  21. package/dist/create-host-app-iA84KNcI.js.map +1 -0
  22. package/dist/{daemon-base-YOWgYYN7.js → daemon-base-D015ew5y.js} +1 -1
  23. package/dist/{daemon-base-YOWgYYN7.js.map → daemon-base-D015ew5y.js.map} +1 -1
  24. package/dist/discover-with-bundled-B5d4q___.js +2 -0
  25. package/dist/discover-with-bundled-CF8qFP41.js +3 -0
  26. package/dist/discover-with-bundled-CF8qFP41.js.map +1 -0
  27. package/dist/doctor-DMj8d4KD.js +36 -0
  28. package/dist/doctor-DMj8d4KD.js.map +1 -0
  29. package/dist/{doctor-attach-_zeIJewx.js → doctor-attach-DEoE7eFx.js} +2 -2
  30. package/dist/{doctor-attach-_zeIJewx.js.map → doctor-attach-DEoE7eFx.js.map} +1 -1
  31. package/dist/{doctor-cdp-K_mIFRfA.js → doctor-cdp-Cj9IFpEP.js} +1 -1
  32. package/dist/{doctor-cdp-K_mIFRfA.js.map → doctor-cdp-Cj9IFpEP.js.map} +1 -1
  33. package/dist/{doctor-heap-Rr2nNvcw.js → doctor-heap-SIxiRvL8.js} +2 -2
  34. package/dist/{doctor-heap-Rr2nNvcw.js.map → doctor-heap-SIxiRvL8.js.map} +1 -1
  35. package/dist/{doctor-ps-OL3FGQYu.js → doctor-ps-DNVOF6yY.js} +2 -2
  36. package/dist/{doctor-ps-OL3FGQYu.js.map → doctor-ps-DNVOF6yY.js.map} +1 -1
  37. package/dist/export-trajectory-D-BcH0Wt.js +14 -0
  38. package/dist/export-trajectory-D-BcH0Wt.js.map +1 -0
  39. package/dist/extension-CoJqqktf.js +2 -0
  40. package/dist/extension-Dpn8gltG.js +179 -0
  41. package/dist/extension-Dpn8gltG.js.map +1 -0
  42. package/dist/feedback-YXsFBBvG.js +30 -0
  43. package/dist/feedback-YXsFBBvG.js.map +1 -0
  44. package/dist/feedback-backend-methods-iVB3y7XG.js +3 -0
  45. package/dist/feedback-backend-methods-iVB3y7XG.js.map +1 -0
  46. package/dist/{feishu-setup-D6vnuNLw.js → feishu-setup-DbAq_BRH.js} +2 -2
  47. package/dist/feishu-setup-DbAq_BRH.js.map +1 -0
  48. package/dist/ftue-doctor-recovery-CKTMnxM8.js +3 -0
  49. package/dist/ftue-doctor-recovery-CKTMnxM8.js.map +1 -0
  50. package/dist/index.d.ts.map +1 -1
  51. package/dist/index.js +11 -13
  52. package/dist/index.js.map +1 -1
  53. package/dist/install-CEPczuet.js +11 -0
  54. package/dist/install-CEPczuet.js.map +1 -0
  55. package/dist/install-CG6b6fox.js +2 -0
  56. package/dist/interactive-schedule-DqvuQWjX.js +3 -0
  57. package/dist/interactive-schedule-DqvuQWjX.js.map +1 -0
  58. package/dist/{mcp-DbTYT7hB.js → mcp-DjReRjuu.js} +1 -1
  59. package/dist/{mcp-DbTYT7hB.js.map → mcp-DjReRjuu.js.map} +1 -1
  60. package/dist/mcp-server-CZtQjAbW.js +5 -0
  61. package/dist/mcp-server-CZtQjAbW.js.map +1 -0
  62. package/dist/{migrate-boDXhEyV.js → migrate-BBi3Txu2.js} +1 -1
  63. package/dist/{migrate-boDXhEyV.js.map → migrate-BBi3Txu2.js.map} +1 -1
  64. package/dist/observe-BqLCpmFD.js +83 -0
  65. package/dist/observe-BqLCpmFD.js.map +1 -0
  66. package/dist/parse-cli-CSd_QYJj.js +3 -0
  67. package/dist/parse-cli-CSd_QYJj.js.map +1 -0
  68. package/dist/persistence-BvLRnpfi.js +85 -0
  69. package/dist/persistence-BvLRnpfi.js.map +1 -0
  70. package/dist/plugin-assembly-check-BntT5W6H.js +3 -0
  71. package/dist/plugin-assembly-check-BntT5W6H.js.map +1 -0
  72. package/dist/{proxy-evg7ZvoD.js → proxy-CFZDVIwI.js} +1 -1
  73. package/dist/proxy-CFZDVIwI.js.map +1 -0
  74. package/dist/proxy-CkSf8ziQ.js +2 -0
  75. package/dist/{ps-DKrluEuy.js → ps-BVKv6rAo.js} +1 -1
  76. package/dist/{ps-DKrluEuy.js.map → ps-BVKv6rAo.js.map} +1 -1
  77. package/dist/{registry-D0NONjLv.js → registry-BUvu-es1.js} +4 -4
  78. package/dist/registry-BUvu-es1.js.map +1 -0
  79. package/dist/registry-DYHR-ONu.js +2 -0
  80. package/dist/{run-attach-Ci5jYyCa.js → run-attach-X35puJ_n.js} +2 -2
  81. package/dist/{run-attach-Ci5jYyCa.js.map → run-attach-X35puJ_n.js.map} +1 -1
  82. package/dist/schedule-C_uHI2M8.js +5 -0
  83. package/dist/schedule-C_uHI2M8.js.map +1 -0
  84. package/dist/serve-BIyu4x46.js +21 -0
  85. package/dist/serve-BIyu4x46.js.map +1 -0
  86. package/dist/{service-client-BLNeugM9.js → service-client-cfUUP4EE.js} +1 -1
  87. package/dist/{service-client-BLNeugM9.js.map → service-client-cfUUP4EE.js.map} +1 -1
  88. package/dist/{version-DaMJKLwj.js → version-Bt1T37Md.js} +2 -2
  89. package/dist/{version-DaMJKLwj.js.map → version-Bt1T37Md.js.map} +1 -1
  90. package/package.json +28 -25
  91. package/dist/auth-command-CXAB3_hs.js.map +0 -1
  92. package/dist/build-id-DMyumG97.js +0 -3
  93. package/dist/build-id-DMyumG97.js.map +0 -1
  94. package/dist/bundled-extensions/plugin-skill-inductor/otto-plugin.json +0 -8
  95. package/dist/bundled-extensions/plugin-skill-inductor/plugin-dist/meta.json +0 -1
  96. package/dist/bundled-extensions/plugin-skill-inductor/plugin-dist/plugin.cjs +0 -52781
  97. package/dist/bundled-extensions/plugin-skill-inductor/plugin.ts +0 -77
  98. package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-detector.ts +0 -210
  99. package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-digest.ts +0 -146
  100. package/dist/bundled-extensions/plugin-skill-inductor/src/candidate-store.ts +0 -87
  101. package/dist/bundled-extensions/plugin-skill-inductor/src/episode-store.ts +0 -122
  102. package/dist/bundled-extensions/plugin-skill-inductor/src/episode.ts +0 -125
  103. package/dist/bundled-extensions/plugin-skill-inductor/src/extraction-prompt.ts +0 -97
  104. package/dist/bundled-extensions/plugin-skill-inductor/src/inductor-service.ts +0 -183
  105. package/dist/bundled-extensions/plugin-skill-inductor/src/similarity.ts +0 -58
  106. package/dist/command-dispatcher-CR7RCzUl.js +0 -13
  107. package/dist/command-dispatcher-CR7RCzUl.js.map +0 -1
  108. package/dist/debug-CiosFwme.js +0 -34
  109. package/dist/debug-CiosFwme.js.map +0 -1
  110. package/dist/discover-with-bundled-CznLjkkw.js +0 -3
  111. package/dist/discover-with-bundled-CznLjkkw.js.map +0 -1
  112. package/dist/doctor-C84-hhEr.js +0 -30
  113. package/dist/doctor-C84-hhEr.js.map +0 -1
  114. package/dist/extension-BKXIr86F.js +0 -170
  115. package/dist/extension-BKXIr86F.js.map +0 -1
  116. package/dist/extension-CnON6Xum.js +0 -2
  117. package/dist/extension-plugin-B6WF8HzF.js +0 -2
  118. package/dist/extension-plugin-DH-4D0tt.js +0 -7
  119. package/dist/extension-plugin-DH-4D0tt.js.map +0 -1
  120. package/dist/feedback-7qQsiabD.js +0 -21
  121. package/dist/feedback-7qQsiabD.js.map +0 -1
  122. package/dist/feedback-Cc1eBsqy.js +0 -2
  123. package/dist/feishu-setup-D6vnuNLw.js.map +0 -1
  124. package/dist/install-DlVJRHRT.js +0 -11
  125. package/dist/install-DlVJRHRT.js.map +0 -1
  126. package/dist/install-WCcQ9k9M.js +0 -2
  127. package/dist/model-auth-7WCx1j-B.js +0 -2
  128. package/dist/model-auth-ylinoVLo.js +0 -3
  129. package/dist/model-auth-ylinoVLo.js.map +0 -1
  130. package/dist/observe-CVg-MRM2.js +0 -32
  131. package/dist/observe-CVg-MRM2.js.map +0 -1
  132. package/dist/parse-cli-2Cs-ZcSC.js +0 -3
  133. package/dist/parse-cli-2Cs-ZcSC.js.map +0 -1
  134. package/dist/persistenced-CCwHu1Ww.js +0 -36
  135. package/dist/persistenced-CCwHu1Ww.js.map +0 -1
  136. package/dist/proxy-BvdBsB1O.js +0 -2
  137. package/dist/proxy-evg7ZvoD.js.map +0 -1
  138. package/dist/registry-B3nfN62q.js +0 -2
  139. package/dist/registry-D0NONjLv.js.map +0 -1
  140. package/dist/remote-persistence-server-B_hONoVR.js +0 -35
  141. package/dist/remote-persistence-server-B_hONoVR.js.map +0 -1
  142. package/dist/serve-pVR6UIT9.js +0 -21
  143. package/dist/serve-pVR6UIT9.js.map +0 -1
  144. package/dist/time-travel-ZozuPPI-.js +0 -21
  145. package/dist/time-travel-ZozuPPI-.js.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"run-attach-Ci5jYyCa.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-X35puJ_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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"}
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import{OTTO_HOME as e}from"@x-otto/env";import{resolve as t}from"node:path";import{readFileSync as n,readdirSync as r}from"node:fs";import{parseCron as i}from"@x-otto/schedule";let a;function o(e){a=e}const s=async(e,t)=>{let n=a;if(!n){t.tui.info.showToast(`Schedule`,`Schedule system not available`);return}let r=e[0];switch(r){case void 0:return c(t);case`list`:return l(n,t);case`add`:return d(e.slice(1),n,t);case`remove`:return f(e[1],n,t);case`run`:return p(e[1],n,t);case`enable`:return h(e[1],!0,n,t);case`disable`:return h(e[1],!1,n,t);case`logs`:return g(e[1],t);case`cancel`:return m(e[1],t);default:t.tui.info.showToast(`Schedule`,`/schedule ${r} — unknown subcommand. Use: add|list|remove|run|enable|disable|cancel|logs`)}};async function c(e){let t=(e.app.getPluginPanelRegistrations?.()??[]).find(e=>e.pluginId===`plugin-schedule`);if(!t){e.tui.info.showToast(`Schedule`,`Schedule panel not installed — run "otto extension install plugin-schedule"`);return}e.tui.modal.openModal({kind:`pluginPanel`,panelInfo:{title:t.title,modulePath:t.modulePath,exportName:t.exportName,pluginId:t.pluginId},onClose:()=>{}})}async function l(e,t){let n=e.list(),r=e.isFireOwner()?[]:e.fireOwnerFailureReason()===`network-error`?[`[read-only — could not reach the remote schedule service; check network/persistenced connectivity]`]:[`[read-only — another terminal holds the fire-ownership lease for this workspace]`];if(n.length===0){t.tui.info.showToast(`Schedule`,r.length>0?`No scheduled tasks. ${r[0]}`:`No scheduled tasks`);return}let i=[];for(let e of n){let t=e.lastFireFailed?`✕`:e.enabled?`◉`:`○`,n=e.recurring?e.cronExpression:`one-shot`,r=new Date(e.nextFireAt).toLocaleString(),a=e.lastFireFailed&&e.lastFireError?` (failed: ${e.lastFireError})`:``,o=e.modelId?` [model: ${e.modelId}${e.thinkingLevel?` ${e.thinkingLevel}`:``}]`:``;i.push(`${t} ${e.id} ${e.name}${a}${o}`),i.push(` ${n} next: ${r}`)}t.tui.messages.pushMessage({role:`command_echo`,command:`/schedule`,status:r.length>0?`warn`:`ok`,summary:r.length>0?r[0]:`${n.length} 个已排定任务`,detail:i.join(`
3
+ `)}),t.tui.panes.togglePane?.(`schedulePane`)}function u(e){if(e.length>=5){let t=e.slice(0,5).join(` `);try{return i(t),{cron:t,prompt:e.slice(5).join(` `)}}catch{}}return{cron:e[0]??``,prompt:e.slice(1).join(` `)}}async function d(e,t,n){if(e.length<2){n.tui.info.showToast(`Schedule`,`/schedule add [--once] [--session] <cron 5 fields> <prompt>`);return}let r=!1,i,a=[...e],o=a.indexOf(`--once`);o!==-1&&(r=!0,a.splice(o,1));let s=a.indexOf(`--session`);s!==-1&&(i=`session`,a.splice(s,1));let l,d,f=a.indexOf(`--model`);f!==-1&&a[f+1]!==void 0&&(l=a[f+1],a.splice(f,2));let p=a.indexOf(`--thinking-level`);if(p!==-1&&a[p+1]!==void 0){let e=a[p+1];if(![`low`,`medium`,`high`,`xhigh`,`max`].includes(e)){n.tui.info.showToast(`Schedule`,`--thinking-level must be low|medium|high|xhigh|max (got "${e}")`);return}d=e,a.splice(p,2)}if(a.length<2){n.tui.info.showToast(`Schedule`,`Opening schedule panel in create mode`),c(n);return}let{cron:m,prompt:h}=u(a);try{let e=t.add({name:h.slice(0,40),prompt:h,cronExpression:m,recurring:!r,origin:`user`,target:i,...l?{modelId:l}:{},...d?{thinkingLevel:d}:{}}),a=r?`one-shot`:`recurring`,o=new Date(e.nextFireAt).toLocaleString(),s=e.target===`session`?` [fires into this conversation]`:``,c=e.modelId?` [model: ${e.modelId}${e.thinkingLevel?` ${e.thinkingLevel}`:``}]`:``;n.tui.info.showToast(`Schedule`,`${e.id}: ${e.name} (${a})${s}${c} next: ${o}`)}catch(e){n.tui.info.showToast(`Schedule`,`Failed: ${e instanceof Error?e.message:String(e)}`)}}async function f(e,t,n){if(!e){n.tui.info.showToast(`Schedule`,`/schedule remove <id>`);return}let r=t.remove(e);n.tui.info.showToast(`Schedule`,r?`Removed ${e}`:`Schedule "${e}" not found`)}async function p(e,t,n){if(!e){n.tui.info.showToast(`Schedule`,`/schedule run <id>`);return}try{t.runNow(e),n.tui.info.showToast(`Schedule`,`Triggering ${e}...`)}catch(e){n.tui.info.showToast(`Schedule`,`Failed: ${e instanceof Error?e.message:String(e)}`)}}async function m(e,t){let n=a;if(!n){t.tui.info.showToast(`Schedule`,`Schedule system not available`);return}if(!e){t.tui.info.showToast(`Schedule`,`/schedule cancel <id>`);return}let r=n.cancel(e);r.ok?t.tui.info.showToast(`Schedule`,`Canceled "${e}"`):t.tui.info.showToast(`Schedule`,`Cannot cancel "${e}": ${r.reason??`unknown`}`)}async function h(e,t,n,r){if(!e){r.tui.info.showToast(`Schedule`,`/schedule ${t?`enable`:`disable`} <id>`);return}let i=n.update(e,{enabled:t});r.tui.info.showToast(`Schedule`,i?`Schedule ${e} ${t?`enabled`:`disabled`}`:`Schedule "${e}" not found`)}async function g(i,a){let o=t(e,`schedule-logs`),s;try{s=r(o)}catch{a.tui.info.showToast(`Schedule`,`No schedule logs found`);return}if(i&&(s=s.filter(e=>e.startsWith(i+`-`))),s.length===0){a.tui.info.showToast(`Schedule`,i?`No logs for schedule "${i}"`:`No schedule logs found`);return}s.sort().reverse();let c=s.slice(0,5),l=[];for(let e of c)try{let r=n(t(o,e),`utf-8`).trim(),i=e.indexOf(`-`),a=i>0?e.slice(0,i):e;l.push(`${a} ${r}`)}catch{l.push(`${e} (unreadable)`)}let u=s.length-c.length;u>0&&l.push(`... and ${u} more entries`),a.tui.messages.pushMessage({role:`command_echo`,command:`/schedule logs`,summary:`${s.length} 条日志文件`,detail:l.join(`
4
+ `)})}export{o as n,s as t};
5
+ //# sourceMappingURL=schedule-C_uHI2M8.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schedule-C_uHI2M8.js","names":[],"sources":["../src/commands/handlers/schedule.ts"],"sourcesContent":["/**\n * /schedule 命令:周期性 Agent Job 管理(增删改查/手动执行)。\n */\n\nimport { readdirSync, readFileSync } from 'node:fs'\nimport { resolve } from 'node:path'\n\n// RFC-161 M2:SchedulerService 已迁至 @x-otto/schedule,直连新真源。\nimport type { SchedulerService } from '@x-otto/schedule'\nimport { parseCron } from '@x-otto/schedule'\nimport { OTTO_HOME } from '@x-otto/env'\n\nimport type { CliContext } from '../../command-dispatcher'\n\n/** RFC-426 M3:/schedule 定时任务管理——app 仅 getPluginPanelRegistrations(打开面板)+ tui。 */\nexport type ScheduleCtx = Pick<CliContext, 'tui'> & {\n app: Pick<CliContext['app'], 'getPluginPanelRegistrations'>\n}\n\n// ── Module-level scheduler ref ──\n\nlet schedulerRef: SchedulerService | undefined\n\nexport function setScheduleService(s: SchedulerService | undefined): void {\n schedulerRef = s\n}\n\n// ── Main handler ──\n\nexport const handleSchedule = async (args: string[], ctx: ScheduleCtx): Promise<void> => {\n const scheduler = schedulerRef\n if (!scheduler) {\n ctx.tui.info.showToast('Schedule', 'Schedule system not available')\n return\n }\n\n const sub = args[0]\n switch (sub) {\n case undefined:\n // RFC-229 M2: open the schedule plugin panel\n return handleOpenPanel(ctx)\n case 'list':\n return handleList(scheduler, ctx)\n case 'add':\n return handleAdd(args.slice(1), scheduler, ctx)\n case 'remove':\n return handleRemove(args[1], scheduler, ctx)\n case 'run':\n return handleRun(args[1], scheduler, ctx)\n case 'enable':\n return handleToggle(args[1], true, scheduler, ctx)\n case 'disable':\n return handleToggle(args[1], false, scheduler, ctx)\n case 'logs':\n return handleLogs(args[1], ctx)\n case 'cancel':\n return handleCancel(args[1], ctx)\n default:\n ctx.tui.info.showToast('Schedule', `/schedule ${sub} — unknown subcommand. Use: add|list|remove|run|enable|disable|cancel|logs`)\n }\n}\n\n// ── Subcommand handlers ──\n\n// open panel (RFC-229 M2: when /schedule is called without subcommand)\n//\n// 2026-07-25 终局修复:不再用 resolveOttoRepoRoot() 猜测 extensions/plugin-schedule/panels-dist/\n// 的磁盘路径——该函数向上找 `.git` 定位仓库根,只在本仓开发环境有效,`@x-otto/cli` 一旦真正\n// 发布到 npm(生产分发只含 `dist/`,无 `.git`),该函数恒返回 undefined,面板永远打不开。\n// schedule 现已物化为独立 npm 包 `@x-otto/plugin-schedule-panel`(对齐 browser-use/mobile-use 等既有\n// 内置扩展形态),走 `otto extension install schedule` 标准安装流程软链进 `~/.otto/plugins/\n// schedule`,面板路径改为查已注册的 `PanelRegistration`(`app.getPluginPanelRegistrations()`,\n// 内部用真实插件目录 `p.dir` 拼接,不依赖任何路径猜测)。\nasync function handleOpenPanel(ctx: ScheduleCtx): Promise<void> {\n const panel = (ctx.app.getPluginPanelRegistrations?.() ?? []).find(\n (p) => p.pluginId === 'plugin-schedule',\n )\n if (!panel) {\n ctx.tui.info.showToast(\n 'Schedule',\n 'Schedule panel not installed — run \"otto extension install plugin-schedule\"',\n )\n return\n }\n ctx.tui.modal.openModal({\n kind: 'pluginPanel',\n panelInfo: {\n title: panel.title,\n modulePath: panel.modulePath,\n exportName: panel.exportName,\n pluginId: panel.pluginId,\n },\n onClose: () => {},\n } as any)\n}\n\nasync function handleList(scheduler: SchedulerService, ctx: ScheduleCtx): Promise<void> {\n const tasks = scheduler.list()\n // RFC-169 D3d: surface read-only state — the user can see the shared task list but\n // this process isn't the one that will fire them (another terminal holds the lease).\n // F2 (RFC-169 终局 review 2026-07-15): distinguish \"another terminal genuinely holds the\n // lease\" from \"we couldn't reach the remote lease service at all\" — showing the wrong\n // specific reason is worse than a vague one; a network-error hint tells the user to check\n // connectivity instead of hunting for a nonexistent second terminal.\n const readOnlyHint = scheduler.isFireOwner()\n ? []\n : scheduler.fireOwnerFailureReason() === 'network-error'\n ? ['[read-only — could not reach the remote schedule service; check network/persistenced connectivity]']\n : ['[read-only — another terminal holds the fire-ownership lease for this workspace]']\n\n if (tasks.length === 0) {\n ctx.tui.info.showToast('Schedule', readOnlyHint.length > 0 ? `No scheduled tasks. ${readOnlyHint[0]}` : 'No scheduled tasks')\n return\n }\n const lines: string[] = []\n for (const t of tasks) {\n const status = t.lastFireFailed ? '✕' : t.enabled ? '◉' : '○'\n const cron = t.recurring ? t.cronExpression : 'one-shot'\n const next = new Date(t.nextFireAt).toLocaleString()\n const failedHint = t.lastFireFailed && t.lastFireError ? ` (failed: ${t.lastFireError})` : ''\n const modelHint = t.modelId ? ` [model: ${t.modelId}${t.thinkingLevel ? ` ${t.thinkingLevel}` : ''}]` : ''\n lines.push(`${status} ${t.id} ${t.name}${failedHint}${modelHint}`)\n lines.push(` ${cron} next: ${next}`)\n }\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/schedule',\n status: readOnlyHint.length > 0 ? 'warn' : 'ok',\n // readOnlyHint 优先展示(降级功能提醒,比\"N 个已排定任务\"更需要用户先看到)。\n summary: readOnlyHint.length > 0 ? readOnlyHint[0] : `${tasks.length} 个已排定任务`,\n detail: lines.join('\\n'),\n })\n ctx.tui.panes.togglePane?.('schedulePane')\n}\n\n/**\n * 从 token 化的命令行参数里解算 cron 表达式与 prompt(纯函数,单测覆盖)。\n *\n * 背景(2026-08-14 真 PTY 验收发现):cron 的自然书写是 5 个空格分隔字段\n * (`0 0 1 1 *`),但命令分词后散成 5 个独立 token——此前 handleAdd 只取\n * `positional[0]` 当 cron,用户自然写法必然报 \"expected 5 fields, got 1 (0)\",\n * 且用法提示从未提过引号包裹,等于自然语法全废。tokenizer 虽支持引号\n * (`\"0 0 1 1 *\"` 合成单 token),但把负担推给用户是 UX 缺陷而非答案。\n *\n * 解算规则(贪心、确定性):\n * - 前 5 个 token 拼接后能通过 parseCron 校验 → 认作 cron,其余归 prompt\n * (自然写法 `add --once 0 0 1 1 * probe` 命中此支);\n * - 否则沿用单 token 形态(引号包裹的完整表达式,或用户输入本就非法——\n * 由 scheduler.add 的 parseCron 报错兜底,错误文案不变)。\n */\nexport function splitCronArgs(positional: readonly string[]): { cron: string; prompt: string } {\n if (positional.length >= 5) {\n const joined = positional.slice(0, 5).join(' ')\n try {\n parseCron(joined)\n return { cron: joined, prompt: positional.slice(5).join(' ') }\n } catch {\n // 前 5 token 不构成合法 cron → 单 token 形态(引号包裹)继续。\n }\n }\n return { cron: positional[0] ?? '', prompt: positional.slice(1).join(' ') }\n}\n\nasync function handleAdd(args: string[], scheduler: SchedulerService, ctx: ScheduleCtx): Promise<void> {\n if (args.length < 2) {\n ctx.tui.info.showToast('Schedule', '/schedule add [--once] [--session] <cron 5 fields> <prompt>')\n return\n }\n\n let once = false\n // RFC-357 D3 (M2): --session opts a user-created task into firing back into the\n // current conversation (via followUp) instead of dispatching an isolated worktree job.\n let target: 'job' | 'session' | undefined\n const positional = [...args]\n const onceIdx = positional.indexOf('--once')\n if (onceIdx !== -1) {\n once = true\n positional.splice(onceIdx, 1)\n }\n const sessionIdx = positional.indexOf('--session')\n if (sessionIdx !== -1) {\n target = 'session'\n positional.splice(sessionIdx, 1)\n }\n\n // RFC-438 M2: --model <id> [--thinking-level <L>] —— per-job 模型指定。\n // 参照 --once/--session 模式;值取下一个 token。\n let modelId: string | undefined\n let thinkingLevel: 'low' | 'medium' | 'high' | 'xhigh' | 'max' | undefined\n const modelIdx = positional.indexOf('--model')\n if (modelIdx !== -1 && positional[modelIdx + 1] !== undefined) {\n modelId = positional[modelIdx + 1]\n positional.splice(modelIdx, 2)\n }\n const tlIdx = positional.indexOf('--thinking-level')\n if (tlIdx !== -1 && positional[tlIdx + 1] !== undefined) {\n const v = positional[tlIdx + 1] as 'low' | 'medium' | 'high' | 'xhigh' | 'max'\n if (!['low', 'medium', 'high', 'xhigh', 'max'].includes(v)) {\n ctx.tui.info.showToast('Schedule', `--thinking-level must be low|medium|high|xhigh|max (got \"${v}\")`)\n return\n }\n thinkingLevel = v\n positional.splice(tlIdx, 2)\n }\n\n if (positional.length < 2) {\n // RFC-229 M4: open the panel in create view mode\n ctx.tui.info.showToast('Schedule', 'Opening schedule panel in create mode')\n void handleOpenPanel(ctx)\n return\n }\n\n const { cron: cronExpr, prompt } = splitCronArgs(positional)\n\n try {\n const task = scheduler.add({\n name: prompt.slice(0, 40),\n prompt,\n cronExpression: cronExpr,\n recurring: !once,\n origin: 'user',\n target,\n ...(modelId ? { modelId } : {}),\n ...(thinkingLevel ? { thinkingLevel } : {}),\n })\n const kind = once ? 'one-shot' : 'recurring'\n const next = new Date(task.nextFireAt).toLocaleString()\n const targetNote = task.target === 'session' ? ' [fires into this conversation]' : ''\n const modelNote = task.modelId ? ` [model: ${task.modelId}${task.thinkingLevel ? ` ${task.thinkingLevel}` : ''}]` : ''\n ctx.tui.info.showToast('Schedule', `${task.id}: ${task.name} (${kind})${targetNote}${modelNote} next: ${next}`)\n } catch (err) {\n ctx.tui.info.showToast('Schedule', `Failed: ${err instanceof Error ? err.message : String(err)}`)\n }\n}\n\nasync function handleRemove(id: string | undefined, scheduler: SchedulerService, ctx: ScheduleCtx): Promise<void> {\n if (!id) {\n ctx.tui.info.showToast('Schedule', '/schedule remove <id>')\n return\n }\n const removed = scheduler.remove(id)\n ctx.tui.info.showToast('Schedule', removed ? `Removed ${id}` : `Schedule \"${id}\" not found`)\n}\n\nasync function handleRun(id: string | undefined, scheduler: SchedulerService, ctx: ScheduleCtx): Promise<void> {\n if (!id) {\n ctx.tui.info.showToast('Schedule', '/schedule run <id>')\n return\n }\n try {\n scheduler.runNow(id)\n ctx.tui.info.showToast('Schedule', `Triggering ${id}...`)\n } catch (err) {\n ctx.tui.info.showToast('Schedule', `Failed: ${err instanceof Error ? err.message : String(err)}`)\n }\n}\n\nasync function handleCancel(id: string | undefined, ctx: ScheduleCtx): Promise<void> {\n const scheduler = schedulerRef\n if (!scheduler) {\n ctx.tui.info.showToast('Schedule', 'Schedule system not available')\n return\n }\n if (!id) {\n ctx.tui.info.showToast('Schedule', '/schedule cancel <id>')\n return\n }\n const result = scheduler.cancel(id)\n if (result.ok) {\n ctx.tui.info.showToast('Schedule', `Canceled \"${id}\"`)\n } else {\n ctx.tui.info.showToast('Schedule', `Cannot cancel \"${id}\": ${result.reason ?? 'unknown'}`)\n }\n}\n\nasync function handleToggle(\n id: string | undefined,\n enabled: boolean,\n scheduler: SchedulerService,\n ctx: ScheduleCtx,\n): Promise<void> {\n if (!id) {\n ctx.tui.info.showToast('Schedule', `/schedule ${enabled ? 'enable' : 'disable'} <id>`)\n return\n }\n const ok = scheduler.update(id, { enabled })\n ctx.tui.info.showToast('Schedule', ok ? `Schedule ${id} ${enabled ? 'enabled' : 'disabled'}` : `Schedule \"${id}\" not found`)\n}\n\nasync function handleLogs(taskId: string | undefined, ctx: ScheduleCtx): Promise<void> {\n const logDir = resolve(OTTO_HOME, 'schedule-logs')\n\n let logFiles: string[]\n try {\n logFiles = readdirSync(logDir)\n } catch {\n ctx.tui.info.showToast('Schedule', 'No schedule logs found')\n return\n }\n\n if (taskId) {\n logFiles = logFiles.filter(f => f.startsWith(taskId + '-'))\n }\n\n if (logFiles.length === 0) {\n ctx.tui.info.showToast('Schedule', taskId ? `No logs for schedule \"${taskId}\"` : 'No schedule logs found')\n return\n }\n\n logFiles.sort().reverse()\n\n const recent = logFiles.slice(0, 5)\n const lines: string[] = []\n for (const filename of recent) {\n try {\n const content = readFileSync(resolve(logDir, filename), 'utf-8').trim()\n const dashIdx = filename.indexOf('-')\n const tid = dashIdx > 0 ? filename.slice(0, dashIdx) : filename\n lines.push(`${tid} ${content}`)\n } catch {\n lines.push(`${filename} (unreadable)`)\n }\n }\n\n const remaining = logFiles.length - recent.length\n if (remaining > 0) {\n lines.push(`... and ${remaining} more entries`)\n }\n\n ctx.tui.messages.pushMessage({\n role: 'command_echo',\n command: '/schedule logs',\n summary: `${logFiles.length} 条日志文件`,\n detail: lines.join('\\n'),\n })\n}\n"],"mappings":";iLAqBA,IAAI,EAEJ,SAAgB,EAAmB,EAAuC,CACxE,EAAe,EAKjB,MAAa,EAAiB,MAAO,EAAgB,IAAoC,CACvF,IAAM,EAAY,EAClB,GAAI,CAAC,EAAW,CACd,EAAI,IAAI,KAAK,UAAU,WAAY,gCAAgC,CACnE,OAGF,IAAM,EAAM,EAAK,GACjB,OAAQ,EAAR,CACE,KAAK,IAAA,GAEH,OAAO,EAAgB,EAAI,CAC7B,IAAK,OACH,OAAO,EAAW,EAAW,EAAI,CACnC,IAAK,MACH,OAAO,EAAU,EAAK,MAAM,EAAE,CAAE,EAAW,EAAI,CACjD,IAAK,SACH,OAAO,EAAa,EAAK,GAAI,EAAW,EAAI,CAC9C,IAAK,MACH,OAAO,EAAU,EAAK,GAAI,EAAW,EAAI,CAC3C,IAAK,SACH,OAAO,EAAa,EAAK,GAAI,GAAM,EAAW,EAAI,CACpD,IAAK,UACH,OAAO,EAAa,EAAK,GAAI,GAAO,EAAW,EAAI,CACrD,IAAK,OACH,OAAO,EAAW,EAAK,GAAI,EAAI,CACjC,IAAK,SACH,OAAO,EAAa,EAAK,GAAI,EAAI,CACnC,QACE,EAAI,IAAI,KAAK,UAAU,WAAY,aAAa,EAAI,4EAA4E,GAetI,eAAe,EAAgB,EAAiC,CAC9D,IAAM,GAAS,EAAI,IAAI,+BAA+B,EAAI,EAAE,EAAE,KAC3D,GAAM,EAAE,WAAa,kBACvB,CACD,GAAI,CAAC,EAAO,CACV,EAAI,IAAI,KAAK,UACX,WACA,8EACD,CACD,OAEF,EAAI,IAAI,MAAM,UAAU,CACtB,KAAM,cACN,UAAW,CACT,MAAO,EAAM,MACb,WAAY,EAAM,WAClB,WAAY,EAAM,WAClB,SAAU,EAAM,SACjB,CACD,YAAe,GAChB,CAAQ,CAGX,eAAe,EAAW,EAA6B,EAAiC,CACtF,IAAM,EAAQ,EAAU,MAAM,CAOxB,EAAe,EAAU,aAAa,CACxC,EAAE,CACF,EAAU,wBAAwB,GAAK,gBACrC,CAAC,qGAAqG,CACtG,CAAC,mFAAmF,CAE1F,GAAI,EAAM,SAAW,EAAG,CACtB,EAAI,IAAI,KAAK,UAAU,WAAY,EAAa,OAAS,EAAI,uBAAuB,EAAa,KAAO,qBAAqB,CAC7H,OAEF,IAAM,EAAkB,EAAE,CAC1B,IAAK,IAAM,KAAK,EAAO,CACrB,IAAM,EAAS,EAAE,eAAiB,IAAM,EAAE,QAAU,IAAM,IACpD,EAAO,EAAE,UAAY,EAAE,eAAiB,WACxC,EAAO,IAAI,KAAK,EAAE,WAAW,CAAC,gBAAgB,CAC9C,EAAa,EAAE,gBAAkB,EAAE,cAAgB,cAAc,EAAE,cAAc,GAAK,GACtF,EAAY,EAAE,QAAU,aAAa,EAAE,UAAU,EAAE,cAAgB,IAAI,EAAE,gBAAkB,GAAG,GAAK,GACzG,EAAM,KAAK,GAAG,EAAO,GAAG,EAAE,GAAG,IAAI,EAAE,OAAO,IAAa,IAAY,CACnE,EAAM,KAAK,MAAM,EAAK,UAAU,IAAO,CAEzC,EAAI,IAAI,SAAS,YAAY,CAC3B,KAAM,eACN,QAAS,YACT,OAAQ,EAAa,OAAS,EAAI,OAAS,KAE3C,QAAS,EAAa,OAAS,EAAI,EAAa,GAAK,GAAG,EAAM,OAAO,SACrE,OAAQ,EAAM,KAAK;EAAK,CACzB,CAAC,CACF,EAAI,IAAI,MAAM,aAAa,eAAe,CAkB5C,SAAgB,EAAc,EAAiE,CAC7F,GAAI,EAAW,QAAU,EAAG,CAC1B,IAAM,EAAS,EAAW,MAAM,EAAG,EAAE,CAAC,KAAK,IAAI,CAC/C,GAAI,CAEF,OADA,EAAU,EAAO,CACV,CAAE,KAAM,EAAQ,OAAQ,EAAW,MAAM,EAAE,CAAC,KAAK,IAAI,CAAE,MACxD,GAIV,MAAO,CAAE,KAAM,EAAW,IAAM,GAAI,OAAQ,EAAW,MAAM,EAAE,CAAC,KAAK,IAAI,CAAE,CAG7E,eAAe,EAAU,EAAgB,EAA6B,EAAiC,CACrG,GAAI,EAAK,OAAS,EAAG,CACnB,EAAI,IAAI,KAAK,UAAU,WAAY,8DAA8D,CACjG,OAGF,IAAI,EAAO,GAGP,EACE,EAAa,CAAC,GAAG,EAAK,CACtB,EAAU,EAAW,QAAQ,SAAS,CACxC,IAAY,KACd,EAAO,GACP,EAAW,OAAO,EAAS,EAAE,EAE/B,IAAM,EAAa,EAAW,QAAQ,YAAY,CAC9C,IAAe,KACjB,EAAS,UACT,EAAW,OAAO,EAAY,EAAE,EAKlC,IAAI,EACA,EACE,EAAW,EAAW,QAAQ,UAAU,CAC1C,IAAa,IAAM,EAAW,EAAW,KAAO,IAAA,KAClD,EAAU,EAAW,EAAW,GAChC,EAAW,OAAO,EAAU,EAAE,EAEhC,IAAM,EAAQ,EAAW,QAAQ,mBAAmB,CACpD,GAAI,IAAU,IAAM,EAAW,EAAQ,KAAO,IAAA,GAAW,CACvD,IAAM,EAAI,EAAW,EAAQ,GAC7B,GAAI,CAAC,CAAC,MAAO,SAAU,OAAQ,QAAS,MAAM,CAAC,SAAS,EAAE,CAAE,CAC1D,EAAI,IAAI,KAAK,UAAU,WAAY,4DAA4D,EAAE,IAAI,CACrG,OAEF,EAAgB,EAChB,EAAW,OAAO,EAAO,EAAE,CAG7B,GAAI,EAAW,OAAS,EAAG,CAEzB,EAAI,IAAI,KAAK,UAAU,WAAY,wCAAwC,CACtE,EAAgB,EAAI,CACzB,OAGF,GAAM,CAAE,KAAM,EAAU,UAAW,EAAc,EAAW,CAE5D,GAAI,CACF,IAAM,EAAO,EAAU,IAAI,CACzB,KAAM,EAAO,MAAM,EAAG,GAAG,CACzB,SACA,eAAgB,EAChB,UAAW,CAAC,EACZ,OAAQ,OACR,SACA,GAAI,EAAU,CAAE,UAAS,CAAG,EAAE,CAC9B,GAAI,EAAgB,CAAE,gBAAe,CAAG,EAAE,CAC3C,CAAC,CACI,EAAO,EAAO,WAAa,YAC3B,EAAO,IAAI,KAAK,EAAK,WAAW,CAAC,gBAAgB,CACjD,EAAa,EAAK,SAAW,UAAY,kCAAoC,GAC7E,EAAY,EAAK,QAAU,YAAY,EAAK,UAAU,EAAK,cAAgB,IAAI,EAAK,gBAAkB,GAAG,GAAK,GACpH,EAAI,IAAI,KAAK,UAAU,WAAY,GAAG,EAAK,GAAG,IAAI,EAAK,KAAK,IAAI,EAAK,GAAG,IAAa,EAAU,SAAS,IAAO,OACxG,EAAK,CACZ,EAAI,IAAI,KAAK,UAAU,WAAY,WAAW,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,GAAG,EAIrG,eAAe,EAAa,EAAwB,EAA6B,EAAiC,CAChH,GAAI,CAAC,EAAI,CACP,EAAI,IAAI,KAAK,UAAU,WAAY,wBAAwB,CAC3D,OAEF,IAAM,EAAU,EAAU,OAAO,EAAG,CACpC,EAAI,IAAI,KAAK,UAAU,WAAY,EAAU,WAAW,IAAO,aAAa,EAAG,aAAa,CAG9F,eAAe,EAAU,EAAwB,EAA6B,EAAiC,CAC7G,GAAI,CAAC,EAAI,CACP,EAAI,IAAI,KAAK,UAAU,WAAY,qBAAqB,CACxD,OAEF,GAAI,CACF,EAAU,OAAO,EAAG,CACpB,EAAI,IAAI,KAAK,UAAU,WAAY,cAAc,EAAG,KAAK,OAClD,EAAK,CACZ,EAAI,IAAI,KAAK,UAAU,WAAY,WAAW,aAAe,MAAQ,EAAI,QAAU,OAAO,EAAI,GAAG,EAIrG,eAAe,EAAa,EAAwB,EAAiC,CACnF,IAAM,EAAY,EAClB,GAAI,CAAC,EAAW,CACd,EAAI,IAAI,KAAK,UAAU,WAAY,gCAAgC,CACnE,OAEF,GAAI,CAAC,EAAI,CACP,EAAI,IAAI,KAAK,UAAU,WAAY,wBAAwB,CAC3D,OAEF,IAAM,EAAS,EAAU,OAAO,EAAG,CAC/B,EAAO,GACT,EAAI,IAAI,KAAK,UAAU,WAAY,aAAa,EAAG,GAAG,CAEtD,EAAI,IAAI,KAAK,UAAU,WAAY,kBAAkB,EAAG,KAAK,EAAO,QAAU,YAAY,CAI9F,eAAe,EACb,EACA,EACA,EACA,EACe,CACf,GAAI,CAAC,EAAI,CACP,EAAI,IAAI,KAAK,UAAU,WAAY,aAAa,EAAU,SAAW,UAAU,OAAO,CACtF,OAEF,IAAM,EAAK,EAAU,OAAO,EAAI,CAAE,UAAS,CAAC,CAC5C,EAAI,IAAI,KAAK,UAAU,WAAY,EAAK,YAAY,EAAG,GAAG,EAAU,UAAY,aAAe,aAAa,EAAG,aAAa,CAG9H,eAAe,EAAW,EAA4B,EAAiC,CACrF,IAAM,EAAS,EAAQ,EAAW,gBAAgB,CAE9C,EACJ,GAAI,CACF,EAAW,EAAY,EAAO,MACxB,CACN,EAAI,IAAI,KAAK,UAAU,WAAY,yBAAyB,CAC5D,OAOF,GAJI,IACF,EAAW,EAAS,OAAO,GAAK,EAAE,WAAW,EAAS,IAAI,CAAC,EAGzD,EAAS,SAAW,EAAG,CACzB,EAAI,IAAI,KAAK,UAAU,WAAY,EAAS,yBAAyB,EAAO,GAAK,yBAAyB,CAC1G,OAGF,EAAS,MAAM,CAAC,SAAS,CAEzB,IAAM,EAAS,EAAS,MAAM,EAAG,EAAE,CAC7B,EAAkB,EAAE,CAC1B,IAAK,IAAM,KAAY,EACrB,GAAI,CACF,IAAM,EAAU,EAAa,EAAQ,EAAQ,EAAS,CAAE,QAAQ,CAAC,MAAM,CACjE,EAAU,EAAS,QAAQ,IAAI,CAC/B,EAAM,EAAU,EAAI,EAAS,MAAM,EAAG,EAAQ,CAAG,EACvD,EAAM,KAAK,GAAG,EAAI,IAAI,IAAU,MAC1B,CACN,EAAM,KAAK,GAAG,EAAS,gBAAgB,CAI3C,IAAM,EAAY,EAAS,OAAS,EAAO,OACvC,EAAY,GACd,EAAM,KAAK,WAAW,EAAU,eAAe,CAGjD,EAAI,IAAI,SAAS,YAAY,CAC3B,KAAM,eACN,QAAS,iBACT,QAAS,GAAG,EAAS,OAAO,QAC5B,OAAQ,EAAM,KAAK;EAAK,CACzB,CAAC"}
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import{t as e}from"./create-host-app-iA84KNcI.js";import{t}from"./daemon-base-D015ew5y.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,p;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 m=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 m.start()}catch(e){return c(`otto serve: failed to start app: ${e instanceof Error?e.message:String(e)}\n`),1}if(`agentJobs`in m&&typeof m.start==`function`){let{setupScheduleService:e}=await import(`./interactive-schedule-DqvuQWjX.js`).then(e=>e.t),{FireLogger:t,DEFAULT_SCHEDULE_CONFIG:n}=await import(`@x-otto/schedule`),r=new t(n);try{let{scheduler:t}=e(m,{});p=t,r.write(`daemon-scheduler`,`headless scheduler assembled (job-only)`)}catch(e){let t=e instanceof Error?e.message:String(e);m.logger?.warn?.({err:t},`[schedule] serve headless scheduler setup failed (non-fatal)`),r.write(`daemon-scheduler`,`headless scheduler setup FAILED: ${t}`)}}let h=i(m,{port:l,host:`127.0.0.1`,staticDir:d,apiToken:f,wsAuthToken:f,authorityId:process.env.OTTO_AUTHORITY_ID||void 0});try{await h.start()}catch(e){return c(`otto serve: failed to listen on port ${l}: ${e instanceof Error?e.message:String(e)}\n`),await m.stop(),1}let g=h.httpServer.address().port;if(a(`otto service listening on http://127.0.0.1:${g}${f?` (auth: token required)`:``}
3
+ ${d?` web: http://127.0.0.1:${g}/${f?` (打开时附加 ?token=<你的 token>)`:``} (静态页面同源托管 ${d})\n`:``} panels: otto observe summary <session-id>
4
+ otto time-travel checkpoints <session-id>
5
+ otto debug breakpoints list${u?``:" (debugger disabled — restart with `otto serve --inspect`)"}
6
+ stop: Ctrl-C
7
+ `),await t(r.signal),a(`otto serve: shutting down...
8
+ `),p)try{await p.stop()}catch(e){m.logger?.warn?.({err:e instanceof Error?e.message:String(e)},`[schedule] serve scheduler stop failed`)}await h.stop(),await m.stop(),process.exit(0)}function o(e){let t=n,i=!1,a,o,s=(t,n)=>{let r=e[t+1];if(r===void 0||r.startsWith(`--`))throw Error(`${n} requires a value`);return r};for(let n=0;n<e.length;n++){let c=e[n];if(c===`--port`||c===`-p`){let e=s(n,`--port`);n++;let r=Number(e);if(!Number.isInteger(r)||r<0||r>65535)throw Error(`invalid --port value: ${e}`);t=r}else c===`--inspect`?i=!0:c===`--static`?(a=r(s(n,`--static`)),n++):c===`--token`&&(o=s(n,`--token`),n++)}return{port:t,inspect:i,staticDir:a,token:o}}function s(e){e(`otto serve — 启动 HTTP/WS service(观测/调试/时间旅行面板的对端)
9
+
10
+ 用法:
11
+ otto serve [--port <n>] [--inspect] [--static <dir>] [--token <t>]
12
+
13
+ 选项:
14
+ --port, -p <n> 监听端口(默认 ${n},可经 OTTO_SERVICE_PORT 覆盖;0 = 随机)
15
+ --inspect 启用调试器(断点/暂停面板需要)
16
+ --static <dir> 同源托管一个已构建的前端目录(SPA),页面与 /api、/ws 同端口免 CORS
17
+ --token <t> 启用鉴权:HTTP /api/* 需 Bearer,WS 需 ?token=;页面用 URL ?token=<t> 对上
18
+
19
+ 面板命令默认连 http://127.0.0.1:${n}(可 --remote 覆盖)
20
+ `)}export{a as runServeCommand};
21
+ //# sourceMappingURL=serve-BIyu4x46.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-BIyu4x46.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 /** RFC-435 D2:serve headless scheduler(无人值守定时任务)。退出时 stop 释放 ownership。 */\n let appScheduler: { stop(): Promise<void> } | 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 // RFC-435 D2:serve/daemon 无人值守装配 headless SchedulerService——job/subscriber 类\n // cron 任务在交互会话关闭后仍到点执行(用户原始诉求\"每天凌晨自动维护文档\")。\n // 无 sessionRef/tui → job-only 形态(session 类任务跳过 + fire log 记录,RFC-435 D2)。\n // fail-soft:装配失败不阻断 serve 主功能(调度是附加能力,R3)。\n // 仅完整 App(createHostApp 生产路径)装配——测试注入的 AppContext 桩无 agentJobs/\n // panelBackendController 等调度依赖,跳过(T435-5 用 fake app 直接验证装配调用)。\n if ('agentJobs' in app && typeof app.start === 'function') {\n const { setupScheduleService } = await import('./interactive-schedule')\n const { FireLogger, DEFAULT_SCHEDULE_CONFIG } = await import('@x-otto/schedule')\n const serveFireLogger = new FireLogger(DEFAULT_SCHEDULE_CONFIG)\n try {\n const { scheduler } = setupScheduleService(app, {})\n // serve 生命周期:优雅退出时释放 ownership(stop),避免锁残留 5min stale 窗口。\n appScheduler = scheduler\n // D5:无人值守装配成功落盘可查(daemon 壳 stdio:'ignore',stderr 日志不可见)。\n serveFireLogger.write('daemon-scheduler', 'headless scheduler assembled (job-only)')\n } catch (error) {\n const msg = error instanceof Error ? error.message : String(error)\n app.logger?.warn?.({ err: msg }, '[schedule] serve headless scheduler setup failed (non-fatal)')\n // D5:装配失败落盘——无人值守时用户第二天必须能查到调度未起。\n serveFireLogger.write('daemon-scheduler', `headless scheduler setup FAILED: ${msg}`)\n }\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 // RFC-435 D4:先 stop scheduler(release fire ownership,避免锁残留 stale 窗口,\n // 交互会话/其他 daemon 立即可接管),再停 service/app。\n if (appScheduler) {\n try {\n await appScheduler.stop()\n } catch (error) {\n app.logger?.warn?.(\n { err: error instanceof Error ? error.message : String(error) },\n '[schedule] serve scheduler stop failed',\n )\n }\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":"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"}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import{SERVICE_URL as e}from"@x-otto/env";var t=class extends Error{constructor(e){super(`cannot reach otto service at ${e} — start it with \`otto serve\` or pass --remote <url>`),this.name=`ServiceUnreachableError`}},n=class extends Error{constructor(e,t){super(t?`${t} (HTTP ${e})`:`request failed (HTTP ${e})`),this.status=e,this.name=`ServiceRequestError`}};function r(t){let n=[...t],r=n.indexOf(`--remote`),i=e;if(r>=0){let e=n[r+1];if(!e||e.startsWith(`-`))throw Error(`--remote requires a url (e.g. --remote http://127.0.0.1:8417)`);i=e,n.splice(r,2)}return{baseUrl:i.replace(/\/$/,``),rest:n}}function i(e,r=globalThis.fetch,i={}){async function a(a,o){let s,c={"content-type":`application/json`,...i.token?{Authorization:`Bearer ${i.token}`}:{},...o?.headers};try{s=await r(`${e}${a}`,{...o,headers:c})}catch{throw new t(e)}if(!s.ok){let e=await s.json().catch(()=>({}));throw new n(s.status,typeof e.error==`string`?e.error:void 0)}let l;try{l=await s.json()}catch{throw new n(s.status,`response was not valid JSON`)}return l}return{baseUrl:e,get:e=>a(e),post:(e,t)=>a(e,{method:`POST`,body:JSON.stringify(t??{})}),put:(e,t)=>a(e,{method:`PUT`,body:JSON.stringify(t??{})}),fetchSnapshot(e){return a(`/api/sessions/${encodeURIComponent(e)}/snapshot`)},async sendPrompt(e,t){return a(`/api/chat/query`,{method:`POST`,body:JSON.stringify({message:t,sessionId:e})})}}}function a(e,t,n){return n(`otto ${e}: ${t instanceof Error?t.message:String(t)}\n`),1}export{r as a,a as i,t as n,i as r,n as t};
3
- //# sourceMappingURL=service-client-BLNeugM9.js.map
3
+ //# sourceMappingURL=service-client-cfUUP4EE.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"service-client-BLNeugM9.js","names":[],"sources":["../src/commands/service-client.ts"],"sourcesContent":["import { SERVICE_URL } from '@x-otto/env'\n\n/** service 进程不可达(连接被拒/DNS 失败)。命令层 catch 后输出可操作提示。 */\nexport class ServiceUnreachableError extends Error {\n constructor(baseUrl: string) {\n super(\n `cannot reach otto service at ${baseUrl} — start it with \\`otto serve\\` or pass --remote <url>`,\n )\n this.name = 'ServiceUnreachableError'\n }\n}\n\nexport class ServiceRequestError extends Error {\n constructor(\n readonly status: number,\n serverMessage: string | undefined,\n ) {\n super(serverMessage ? `${serverMessage} (HTTP ${status})` : `request failed (HTTP ${status})`)\n this.name = 'ServiceRequestError'\n }\n}\n\nexport interface ServiceClient {\n readonly baseUrl: string\n get<T>(path: string): Promise<T>\n post<T>(path: string, body?: unknown): Promise<T>\n put<T>(path: string, body?: unknown): Promise<T>\n /** RFC-335 T335-07 S3:attach 快照(含 cursor——serve 给出的 subscribe.since 起点)。 */\n fetchSnapshot(sessionId: string): Promise<SessionSnapshotDto>\n /** RFC-335 T335-07 S3:attach 首期唯一写入面——经 POST /api/chat/query 发送 prompt。 */\n sendPrompt(sessionId: string, message: string): Promise<ChatQueryResponse>\n}\n\n/** service `GET /api/sessions/:id/snapshot` 响应(service/src/types.ts:141 SessionSnapshot)。 */\nexport interface SessionSnapshotDto {\n session: {\n id: string\n status: string\n messageCount: number\n messages: Array<unknown>\n }\n authority: { hostname: string; pid: number; sessionId: string }\n cursor: number\n}\n\n/** service `POST /api/chat/query` 响应。 */\nexport interface ChatQueryResponse {\n sessionId: string\n}\n\nexport function resolveRemote(args: string[]): { baseUrl: string; rest: string[] } {\n const rest = [...args]\n const idx = rest.indexOf('--remote')\n let baseUrl = SERVICE_URL\n if (idx >= 0) {\n const url = rest[idx + 1]\n if (!url || url.startsWith('-')) {\n throw new Error('--remote requires a url (e.g. --remote http://127.0.0.1:8417)')\n }\n baseUrl = url\n rest.splice(idx, 2)\n }\n return { baseUrl: baseUrl.replace(/\\/$/, ''), rest }\n}\n\nexport interface ServiceClientOptions {\n /** `otto serve --token <t>` 时的鉴权头(Authorization: Bearer <t>)。 */\n token?: string\n}\n\nexport function createServiceClient(\n baseUrl: string,\n fetchImpl: typeof globalThis.fetch = globalThis.fetch,\n options: ServiceClientOptions = {},\n): ServiceClient {\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n let res: Response\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n ...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),\n ...(init?.headers as Record<string, string> | undefined),\n }\n try {\n res = await fetchImpl(`${baseUrl}${path}`, { ...init, headers })\n } catch {\n throw new ServiceUnreachableError(baseUrl)\n }\n\n if (!res.ok) {\n const errBody = (await res.json().catch(() => ({}))) as Record<string, unknown>\n throw new ServiceRequestError(\n res.status,\n typeof errBody.error === 'string' ? errBody.error : undefined,\n )\n }\n\n let body: unknown\n try {\n body = await res.json()\n } catch {\n throw new ServiceRequestError(res.status, 'response was not valid JSON')\n }\n return body as T\n }\n\n return {\n baseUrl,\n get: (path) => request(path),\n post: (path, body) =>\n request(path, {\n method: 'POST',\n body: JSON.stringify(body ?? {}),\n }),\n put: (path, body) =>\n request(path, {\n method: 'PUT',\n body: JSON.stringify(body ?? {}),\n }),\n fetchSnapshot(sessionId: string): Promise<SessionSnapshotDto> {\n return request<SessionSnapshotDto>(`/api/sessions/${encodeURIComponent(sessionId)}/snapshot`)\n },\n async sendPrompt(sessionId: string, message: string): Promise<ChatQueryResponse> {\n return request<ChatQueryResponse>('/api/chat/query', {\n method: 'POST',\n body: JSON.stringify({ message, sessionId }),\n })\n },\n }\n}\n\nexport function reportServiceError(\n command: string,\n error: unknown,\n stderr: (s: string) => void,\n): number {\n const message = error instanceof Error ? error.message : String(error)\n stderr(`otto ${command}: ${message}\\n`)\n return 1\n}\n"],"mappings":";0CAGA,IAAa,EAAb,cAA6C,KAAM,CACjD,YAAY,EAAiB,CAC3B,MACE,gCAAgC,EAAQ,wDACzC,CACD,KAAK,KAAO,4BAIH,EAAb,cAAyC,KAAM,CAC7C,YACE,EACA,EACA,CACA,MAAM,EAAgB,GAAG,EAAc,SAAS,EAAO,GAAK,wBAAwB,EAAO,GAAG,CAHrF,KAAA,OAAA,EAIT,KAAK,KAAO,wBAgChB,SAAgB,EAAc,EAAqD,CACjF,IAAM,EAAO,CAAC,GAAG,EAAK,CAChB,EAAM,EAAK,QAAQ,WAAW,CAChC,EAAU,EACd,GAAI,GAAO,EAAG,CACZ,IAAM,EAAM,EAAK,EAAM,GACvB,GAAI,CAAC,GAAO,EAAI,WAAW,IAAI,CAC7B,MAAU,MAAM,gEAAgE,CAElF,EAAU,EACV,EAAK,OAAO,EAAK,EAAE,CAErB,MAAO,CAAE,QAAS,EAAQ,QAAQ,MAAO,GAAG,CAAE,OAAM,CAQtD,SAAgB,EACd,EACA,EAAqC,WAAW,MAChD,EAAgC,EAAE,CACnB,CACf,eAAe,EAAW,EAAc,EAAgC,CACtE,IAAI,EACE,EAAkC,CACtC,eAAgB,mBAChB,GAAI,EAAQ,MAAQ,CAAE,cAAe,UAAU,EAAQ,QAAS,CAAG,EAAE,CACrE,GAAI,GAAM,QACX,CACD,GAAI,CACF,EAAM,MAAM,EAAU,GAAG,IAAU,IAAQ,CAAE,GAAG,EAAM,UAAS,CAAC,MAC1D,CACN,MAAM,IAAI,EAAwB,EAAQ,CAG5C,GAAI,CAAC,EAAI,GAAI,CACX,IAAM,EAAW,MAAM,EAAI,MAAM,CAAC,WAAa,EAAE,EAAE,CACnD,MAAM,IAAI,EACR,EAAI,OACJ,OAAO,EAAQ,OAAU,SAAW,EAAQ,MAAQ,IAAA,GACrD,CAGH,IAAI,EACJ,GAAI,CACF,EAAO,MAAM,EAAI,MAAM,MACjB,CACN,MAAM,IAAI,EAAoB,EAAI,OAAQ,8BAA8B,CAE1E,OAAO,EAGT,MAAO,CACL,UACA,IAAM,GAAS,EAAQ,EAAK,CAC5B,MAAO,EAAM,IACX,EAAQ,EAAM,CACZ,OAAQ,OACR,KAAM,KAAK,UAAU,GAAQ,EAAE,CAAC,CACjC,CAAC,CACJ,KAAM,EAAM,IACV,EAAQ,EAAM,CACZ,OAAQ,MACR,KAAM,KAAK,UAAU,GAAQ,EAAE,CAAC,CACjC,CAAC,CACJ,cAAc,EAAgD,CAC5D,OAAO,EAA4B,iBAAiB,mBAAmB,EAAU,CAAC,WAAW,EAE/F,MAAM,WAAW,EAAmB,EAA6C,CAC/E,OAAO,EAA2B,kBAAmB,CACnD,OAAQ,OACR,KAAM,KAAK,UAAU,CAAE,UAAS,YAAW,CAAC,CAC7C,CAAC,EAEL,CAGH,SAAgB,EACd,EACA,EACA,EACQ,CAGR,OADA,EAAO,QAAQ,EAAQ,IADP,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CACnC,IAAI,CAChC"}
1
+ {"version":3,"file":"service-client-cfUUP4EE.js","names":[],"sources":["../src/commands/service-client.ts"],"sourcesContent":["import { SERVICE_URL } from '@x-otto/env'\n\n/** service 进程不可达(连接被拒/DNS 失败)。命令层 catch 后输出可操作提示。 */\nexport class ServiceUnreachableError extends Error {\n constructor(baseUrl: string) {\n super(\n `cannot reach otto service at ${baseUrl} — start it with \\`otto serve\\` or pass --remote <url>`,\n )\n this.name = 'ServiceUnreachableError'\n }\n}\n\nexport class ServiceRequestError extends Error {\n constructor(\n readonly status: number,\n serverMessage: string | undefined,\n ) {\n super(serverMessage ? `${serverMessage} (HTTP ${status})` : `request failed (HTTP ${status})`)\n this.name = 'ServiceRequestError'\n }\n}\n\nexport interface ServiceClient {\n readonly baseUrl: string\n get<T>(path: string): Promise<T>\n post<T>(path: string, body?: unknown): Promise<T>\n put<T>(path: string, body?: unknown): Promise<T>\n /** RFC-335 T335-07 S3:attach 快照(含 cursor——serve 给出的 subscribe.since 起点)。 */\n fetchSnapshot(sessionId: string): Promise<SessionSnapshotDto>\n /** RFC-335 T335-07 S3:attach 首期唯一写入面——经 POST /api/chat/query 发送 prompt。 */\n sendPrompt(sessionId: string, message: string): Promise<ChatQueryResponse>\n}\n\n/** service `GET /api/sessions/:id/snapshot` 响应(service/src/types.ts:141 SessionSnapshot)。 */\nexport interface SessionSnapshotDto {\n session: {\n id: string\n status: string\n messageCount: number\n messages: Array<unknown>\n }\n authority: { hostname: string; pid: number; sessionId: string }\n cursor: number\n}\n\n/** service `POST /api/chat/query` 响应。 */\nexport interface ChatQueryResponse {\n sessionId: string\n}\n\nexport function resolveRemote(args: string[]): { baseUrl: string; rest: string[] } {\n const rest = [...args]\n const idx = rest.indexOf('--remote')\n let baseUrl = SERVICE_URL\n if (idx >= 0) {\n const url = rest[idx + 1]\n if (!url || url.startsWith('-')) {\n throw new Error('--remote requires a url (e.g. --remote http://127.0.0.1:8417)')\n }\n baseUrl = url\n rest.splice(idx, 2)\n }\n return { baseUrl: baseUrl.replace(/\\/$/, ''), rest }\n}\n\nexport interface ServiceClientOptions {\n /** `otto serve --token <t>` 时的鉴权头(Authorization: Bearer <t>)。 */\n token?: string\n}\n\nexport function createServiceClient(\n baseUrl: string,\n fetchImpl: typeof globalThis.fetch = globalThis.fetch,\n options: ServiceClientOptions = {},\n): ServiceClient {\n async function request<T>(path: string, init?: RequestInit): Promise<T> {\n let res: Response\n const headers: Record<string, string> = {\n 'content-type': 'application/json',\n ...(options.token ? { Authorization: `Bearer ${options.token}` } : {}),\n ...(init?.headers as Record<string, string> | undefined),\n }\n try {\n res = await fetchImpl(`${baseUrl}${path}`, { ...init, headers })\n } catch {\n throw new ServiceUnreachableError(baseUrl)\n }\n\n if (!res.ok) {\n const errBody = (await res.json().catch(() => ({}))) as Record<string, unknown>\n throw new ServiceRequestError(\n res.status,\n typeof errBody.error === 'string' ? errBody.error : undefined,\n )\n }\n\n let body: unknown\n try {\n body = await res.json()\n } catch {\n throw new ServiceRequestError(res.status, 'response was not valid JSON')\n }\n return body as T\n }\n\n return {\n baseUrl,\n get: (path) => request(path),\n post: (path, body) =>\n request(path, {\n method: 'POST',\n body: JSON.stringify(body ?? {}),\n }),\n put: (path, body) =>\n request(path, {\n method: 'PUT',\n body: JSON.stringify(body ?? {}),\n }),\n fetchSnapshot(sessionId: string): Promise<SessionSnapshotDto> {\n return request<SessionSnapshotDto>(`/api/sessions/${encodeURIComponent(sessionId)}/snapshot`)\n },\n async sendPrompt(sessionId: string, message: string): Promise<ChatQueryResponse> {\n return request<ChatQueryResponse>('/api/chat/query', {\n method: 'POST',\n body: JSON.stringify({ message, sessionId }),\n })\n },\n }\n}\n\nexport function reportServiceError(\n command: string,\n error: unknown,\n stderr: (s: string) => void,\n): number {\n const message = error instanceof Error ? error.message : String(error)\n stderr(`otto ${command}: ${message}\\n`)\n return 1\n}\n"],"mappings":";0CAGA,IAAa,EAAb,cAA6C,KAAM,CACjD,YAAY,EAAiB,CAC3B,MACE,gCAAgC,EAAQ,wDACzC,CACD,KAAK,KAAO,4BAIH,EAAb,cAAyC,KAAM,CAC7C,YACE,EACA,EACA,CACA,MAAM,EAAgB,GAAG,EAAc,SAAS,EAAO,GAAK,wBAAwB,EAAO,GAAG,CAHrF,KAAA,OAAA,EAIT,KAAK,KAAO,wBAgChB,SAAgB,EAAc,EAAqD,CACjF,IAAM,EAAO,CAAC,GAAG,EAAK,CAChB,EAAM,EAAK,QAAQ,WAAW,CAChC,EAAU,EACd,GAAI,GAAO,EAAG,CACZ,IAAM,EAAM,EAAK,EAAM,GACvB,GAAI,CAAC,GAAO,EAAI,WAAW,IAAI,CAC7B,MAAU,MAAM,gEAAgE,CAElF,EAAU,EACV,EAAK,OAAO,EAAK,EAAE,CAErB,MAAO,CAAE,QAAS,EAAQ,QAAQ,MAAO,GAAG,CAAE,OAAM,CAQtD,SAAgB,EACd,EACA,EAAqC,WAAW,MAChD,EAAgC,EAAE,CACnB,CACf,eAAe,EAAW,EAAc,EAAgC,CACtE,IAAI,EACE,EAAkC,CACtC,eAAgB,mBAChB,GAAI,EAAQ,MAAQ,CAAE,cAAe,UAAU,EAAQ,QAAS,CAAG,EAAE,CACrE,GAAI,GAAM,QACX,CACD,GAAI,CACF,EAAM,MAAM,EAAU,GAAG,IAAU,IAAQ,CAAE,GAAG,EAAM,UAAS,CAAC,MAC1D,CACN,MAAM,IAAI,EAAwB,EAAQ,CAG5C,GAAI,CAAC,EAAI,GAAI,CACX,IAAM,EAAW,MAAM,EAAI,MAAM,CAAC,WAAa,EAAE,EAAE,CACnD,MAAM,IAAI,EACR,EAAI,OACJ,OAAO,EAAQ,OAAU,SAAW,EAAQ,MAAQ,IAAA,GACrD,CAGH,IAAI,EACJ,GAAI,CACF,EAAO,MAAM,EAAI,MAAM,MACjB,CACN,MAAM,IAAI,EAAoB,EAAI,OAAQ,8BAA8B,CAE1E,OAAO,EAGT,MAAO,CACL,UACA,IAAM,GAAS,EAAQ,EAAK,CAC5B,MAAO,EAAM,IACX,EAAQ,EAAM,CACZ,OAAQ,OACR,KAAM,KAAK,UAAU,GAAQ,EAAE,CAAC,CACjC,CAAC,CACJ,KAAM,EAAM,IACV,EAAQ,EAAM,CACZ,OAAQ,MACR,KAAM,KAAK,UAAU,GAAQ,EAAE,CAAC,CACjC,CAAC,CACJ,cAAc,EAAgD,CAC5D,OAAO,EAA4B,iBAAiB,mBAAmB,EAAU,CAAC,WAAW,EAE/F,MAAM,WAAW,EAAmB,EAA6C,CAC/E,OAAO,EAA2B,kBAAmB,CACnD,OAAQ,OACR,KAAM,KAAK,UAAU,CAAE,UAAS,YAAW,CAAC,CAC7C,CAAC,EAEL,CAGH,SAAgB,EACd,EACA,EACA,EACQ,CAGR,OADA,EAAO,QAAQ,EAAQ,IADP,aAAiB,MAAQ,EAAM,QAAU,OAAO,EAAM,CACnC,IAAI,CAChC"}
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
- import{n as e}from"./build-id-DMyumG97.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-DaMJKLwj.js.map
2
+ import{r as e}from"./build-id-CWNug-3b.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-Bt1T37Md.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"version-DaMJKLwj.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"}
1
+ {"version":3,"file":"version-Bt1T37Md.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.8",
3
+ "version": "0.1.0-alpha.10",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "types": "./dist/index.d.ts",
@@ -17,27 +17,30 @@
17
17
  "otto": "./bin/otto.js"
18
18
  },
19
19
  "dependencies": {
20
- "@x-otto/agent": "0.0.1-alpha.0",
21
- "@x-otto/ai": "0.0.1-alpha.0",
22
- "@x-otto/coding": "0.0.1-alpha.1",
23
- "@x-otto/schedule": "0.0.1-alpha.0",
24
- "@x-otto/env": "0.1.0-alpha.1",
25
- "@x-otto/hooks": "0.0.1-alpha.0",
26
- "@x-otto/mcp": "0.0.1-alpha.0",
27
- "@x-otto/memory": "0.0.1-alpha.0",
28
- "@x-otto/persistence": "0.0.1-alpha.0",
29
- "@x-otto/interchange": "0.1.0-alpha.1",
30
- "@x-otto/remote-persistence-server": "0.0.1-alpha.0",
31
- "@x-otto/runtime": "0.0.1-alpha.0",
32
- "@x-otto/service": "0.0.1-alpha.0",
33
- "@x-otto/session": "0.0.1-alpha.0",
34
- "@x-otto/session-contract": "0.0.1-alpha.0",
35
- "@x-otto/setting": "0.0.1-alpha.1",
36
- "@x-otto/shared": "0.1.0-alpha.1",
37
- "@x-otto/tools": "0.0.1-alpha.0",
38
- "@x-otto/trace-view": "0.0.1-alpha.0",
39
- "@x-otto/tui": "0.0.1-alpha.2",
40
- "react": "^19.2.4"
20
+ "react": "^19.2.4",
21
+ "zod": "4.3.6",
22
+ "@x-otto/agent": "0.1.0-alpha.10",
23
+ "@x-otto/guard": "0.1.0-alpha.10",
24
+ "@x-otto/env": "0.1.0-alpha.10",
25
+ "@x-otto/coding": "0.1.0-alpha.10",
26
+ "@x-otto/ai": "0.1.0-alpha.10",
27
+ "@x-otto/mcp": "0.1.0-alpha.10",
28
+ "@x-otto/hooks": "0.1.0-alpha.10",
29
+ "@x-otto/memory": "0.1.0-alpha.10",
30
+ "@x-otto/interchange": "0.1.0-alpha.10",
31
+ "@x-otto/persistence": "0.1.0-alpha.10",
32
+ "@x-otto/plugin": "0.1.0-alpha.10",
33
+ "@x-otto/remote-persistence-server": "0.1.0-alpha.10",
34
+ "@x-otto/runtime": "0.1.0-alpha.10",
35
+ "@x-otto/service": "0.1.0-alpha.10",
36
+ "@x-otto/schedule": "0.1.0-alpha.10",
37
+ "@x-otto/session-contract": "0.1.0-alpha.10",
38
+ "@x-otto/session": "0.1.0-alpha.10",
39
+ "@x-otto/setting": "0.1.0-alpha.10",
40
+ "@x-otto/shared": "0.1.0-alpha.10",
41
+ "@x-otto/tools": "0.1.0-alpha.10",
42
+ "@x-otto/tui": "0.1.0-alpha.10",
43
+ "@x-otto/trace-view": "0.1.0-alpha.10"
41
44
  },
42
45
  "publishConfig": {
43
46
  "access": "public",
@@ -45,12 +48,12 @@
45
48
  "tag": "alpha"
46
49
  },
47
50
  "devDependencies": {
48
- "node-pty": "^1.1.0",
49
- "@types/react": "^19.2.2"
51
+ "@types/react": "^19.2.2",
52
+ "node-pty": "^1.1.0"
50
53
  },
51
54
  "private": false,
52
55
  "scripts": {
53
- "build": "tsdown && node scripts/bundle-builtin-extensions.mjs",
56
+ "build": "node ../../scripts/gen-build-id.mjs && tsdown && node scripts/bundle-builtin-extensions.mjs",
54
57
  "typecheck:project": "tsc -p tsconfig.json --noEmit",
55
58
  "typecheck:file": "tsc src/index.ts --noEmit --target ES2024 --module ESNext --moduleResolution bundler --esModuleInterop --skipLibCheck",
56
59
  "typecheck": "tsc --noEmit",
@@ -1 +0,0 @@
1
- {"version":3,"file":"auth-command-CXAB3_hs.js","names":[],"sources":["../src/auth-command.ts"],"sourcesContent":["import { createInterface } from 'node:readline/promises'\n\nimport { createApp } from '@x-otto/coding'\nimport { createDefaultAuthStore } from '@x-otto/ai'\nimport type { AuthStore, OAuthLoginOptions } from '@x-otto/ai'\n\n/**\n * `otto auth login|status|logout` 子命令(M8-07)。\n *\n * 驱动 AuthStore.login 的 onAuth/onPrompt/onManualCode 回调:\n * - Anthropic(usesCallbackServer=false)→ loopback(54545) 优先自动回填,\n * 端口占用时回退托管回调 + 用户回贴 code(onManualCode 必须提供);\n * - loopback provider(OpenAI/Codex,usesCallbackServer=true)→ 浏览器重定向自动完成。\n * - 插件 OAuth(cursor/anthropic 等)→ 须 bootstrap App 装载插件后再查 oauth 表(RFC-130 M131-1)。\n */\nexport interface AuthBootstrapOptions {\n workspaceDir: string\n projectConfigPath?: string\n verbose?: boolean\n}\n\nexport interface AuthCommandDeps {\n authStore?: AuthStore\n /** 未注入 authStore 时,经 App.start() 装载插件 OAuth(对齐 TUI `/auth`)。 */\n bootstrap?: AuthBootstrapOptions\n stdout?: (s: string) => void\n stderr?: (s: string) => void\n /** 单行读取器(注入便于测试);未提供时按需创建 readline。 */\n prompt?: (question: string) => Promise<string>\n}\n\nexport async function runAuthCommand(args: string[], deps: AuthCommandDeps = {}): Promise<number> {\n if (!deps.authStore && deps.bootstrap) {\n const app = createApp({\n workspaceDir: deps.bootstrap.workspaceDir,\n projectConfigPath: deps.bootstrap.projectConfigPath,\n logger: {\n name: 'otto-auth',\n level: deps.bootstrap.verbose ? 'debug' : 'info',\n destination: 'stderr',\n },\n })\n try {\n await app.start()\n return await runAuthCommand(args, {\n ...deps,\n authStore: app.authStore,\n bootstrap: undefined,\n })\n } finally {\n await app.stop()\n }\n }\n\n const stdout = deps.stdout ?? ((s) => process.stdout.write(s))\n const stderr = deps.stderr ?? ((s) => process.stderr.write(s))\n // watchForExternalChanges:false —— `otto auth *` 是一次性命令,跑完即退出,不需要跨进程凭据\n // 实时同步(那是给 TUI/serve 长驻进程用的)。AuthStore 的 fs.watch 句柄本身已 unref(不阻塞\n // 事件循环清空,见 auth-store.ts setupWatcher 注释),此处显式关闭仍是更准确的语义表达——\n // 避免为一个秒退的脚本无谓建立/拆除一个不会被用到的 watcher。\n const authStore = deps.authStore ?? createDefaultAuthStore({ watchForExternalChanges: false })\n\n const [action, provider] = args\n\n switch (action) {\n case 'login':\n return authLogin(authStore, provider, { stdout, stderr, prompt: deps.prompt })\n case 'logout':\n return authLogout(authStore, provider, { stdout, stderr })\n case 'status':\n case undefined:\n return authStatus(authStore, { stdout })\n default:\n stderr(`otto auth: unknown action '${action}'. Use login | status | logout.\\n`)\n return 1\n }\n}\n\nfunction loginableProviders(authStore: AuthStore): string[] {\n return authStore.oauth.list().map((p) => p.id)\n}\n\nasync function authLogin(\n authStore: AuthStore,\n provider: string | undefined,\n io: { stdout: (s: string) => void; stderr: (s: string) => void; prompt?: (q: string) => Promise<string> },\n): Promise<number> {\n const providers = loginableProviders(authStore)\n // UX(round 6):零 provider 插件时 providers 为空数组——\"One of: \"后面是空串,\n // 用户面对死端。指回引导路径而不是展示空名单。\n if (providers.length === 0) {\n io.stderr(\n 'otto auth login: no OAuth-capable providers available yet.\\n' +\n 'Install a provider plugin first — run `otto` (interactive) for guided setup,\\n' +\n 'or: otto extension install anthropic-provider\\n',\n )\n return 1\n }\n if (!provider) {\n io.stderr(`otto auth login: missing provider. One of: ${providers.join(', ')}\\n`)\n return 1\n }\n\n const oauth = authStore.oauth.get(provider)\n if (!oauth) {\n io.stderr(`otto auth login: unknown provider '${provider}'. One of: ${providers.join(', ')}\\n`)\n io.stderr(\n 'Hint: plugin providers (cursor, anthropic, …) need the plugin installed with provider capability granted.\\n' +\n ' otto extension install --link extensions/plugin-cursor --yes\\n' +\n ' # user-scope install auto-trusts; then otto auth login cursor\\n',\n )\n return 1\n }\n\n const rl = io.prompt ? undefined : createInterface({ input: process.stdin, output: process.stdout })\n const ask = io.prompt ?? ((q: string) => rl!.question(q))\n\n const controller = new AbortController()\n const onSigint = (): void => controller.abort()\n process.on('SIGINT', onSigint)\n\n try {\n const options: OAuthLoginOptions = {\n onAuth: (info) => {\n io.stdout(`\\nOpen this URL in your browser to authorize:\\n ${info.url}\\n`)\n if (info.code) {\n io.stdout(`Code: ${info.code}\\n`)\n }\n if (info.instructions) {\n io.stdout(`${info.instructions}\\n`)\n }\n },\n onPrompt: async (p) => {\n const suffix = p.placeholder ? ` (${p.placeholder})` : ''\n return (await ask(`${p.message}${suffix}: `)).trim()\n },\n // loopback 回调先完成时经 signal 放行。readline 本身无法中断,由 finally 的\n // rl.close() 收尾——close 会 reject 悬挂的 question,故 catch 归一为空串防 unhandled。\n onManualCode: async (signal?: AbortSignal) => {\n const question = ask('\\nPaste the code here: ').then(\n (s) => s.trim(),\n () => '',\n )\n if (!signal) return question\n if (signal.aborted) return ''\n return Promise.race([\n question,\n new Promise<string>((resolve) => {\n signal.addEventListener('abort', () => resolve(''), { once: true })\n }),\n ])\n },\n onProgress: (m) => io.stdout(`${m}\\n`),\n signal: controller.signal,\n }\n\n await authStore.login(provider, options)\n io.stdout(`\\n✓ Logged in to ${oauth.name}.\\n`)\n return 0\n } catch (error) {\n io.stderr(`\\notto auth login failed: ${(error as Error).message}\\n`)\n return 1\n } finally {\n process.off('SIGINT', onSigint)\n rl?.close()\n }\n}\n\nasync function authLogout(\n authStore: AuthStore,\n provider: string | undefined,\n io: { stdout: (s: string) => void; stderr: (s: string) => void },\n): Promise<number> {\n if (!provider) {\n io.stderr(\n `otto auth logout: missing provider. One of: ${authStore.listProviders().join(', ')}\\n`,\n )\n return 1\n }\n\n const had = await authStore.hasCredential(provider)\n await authStore.logout(provider)\n io.stdout(had ? `✓ Logged out of ${provider}.\\n` : `${provider} was not logged in.\\n`)\n return 0\n}\n\nconst SOURCE_LABEL: Record<string, string> = {\n oauth: 'OAuth',\n api_key: 'API key (stored)',\n env: 'API key (env)',\n}\n\nasync function authStatus(\n authStore: AuthStore,\n io: { stdout: (s: string) => void },\n): Promise<number> {\n io.stdout('Authentication status:\\n')\n for (const provider of authStore.listProviders()) {\n const source = await authStore.getAuthSource(provider)\n const label = source ? SOURCE_LABEL[source] : 'not configured'\n io.stdout(` ${provider.padEnd(16)} ${label}\\n`)\n }\n return 0\n}\n"],"mappings":"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"}
@@ -1,3 +0,0 @@
1
- #!/usr/bin/env node
2
- const e=4570,t=`e60abd89a`;export{e as n,t};
3
- //# sourceMappingURL=build-id-DMyumG97.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"build-id-DMyumG97.js","names":[],"sources":["../src/build-id.ts"],"sourcesContent":["// auto-generated by scripts/gen-build-id.mjs — DO NOT EDIT\nexport const BUILD_ID = 4570\nexport const BUILD_BRANCH = 'dev'\nexport const BUILD_HASH = 'e60abd89a'\n"],"mappings":";AACA,MAAa,EAAW,KAEX,EAAa"}
@@ -1,8 +0,0 @@
1
- {
2
- "id": "plugin-skill-inductor",
3
- "name": "Skill Inductor",
4
- "version": "0.1.0",
5
- "description": "Observes completed task trajectories and distills them into structured episodes — the data source for auto-induced skills (RFC-318 skill loop).",
6
- "capabilities": ["service", "storage", "session.read", "llm.complete", "user.notify"],
7
- "scripts": { "postinstall": "otto extension build" }
8
- }
@@ -1 +0,0 @@
1
- {"apiVersion":1,"compilerVersion":"0.1.0-alpha.1","sourceHash":"5b28454cfded236868d7031600579b7aa59f72785a003a28034b3cb81c5b70ae"}