thinkpool-pair 0.7.364 → 0.7.365

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 (116) hide show
  1. package/LICENSE +18 -0
  2. package/README.md +20 -0
  3. package/abort-turn-barrier.mjs +1 -23
  4. package/account.mjs +1 -1437
  5. package/acp-client.mjs +1 -140
  6. package/agent-detect.mjs +1 -28
  7. package/agent-notify.mjs +1 -142
  8. package/agent-visibility.mjs +1 -67
  9. package/auth-store.mjs +1 -120
  10. package/bridge.mjs +1 -6266
  11. package/byok-detect.mjs +1 -126
  12. package/claude-command-catalog.mjs +1 -91
  13. package/claude-session.mjs +1 -1519
  14. package/code-event-contract.mjs +1 -118
  15. package/codex-app-server.mjs +1 -340
  16. package/codex-commands.mjs +1 -83
  17. package/codex-event-mapper.mjs +1 -232
  18. package/codex-images.mjs +1 -69
  19. package/codex-mcp-http.mjs +1 -131
  20. package/codex-session.mjs +1 -1295
  21. package/command-catalog.mjs +1 -118
  22. package/command-guidance.mjs +1 -8
  23. package/context-contract.mjs +1 -95
  24. package/context-windows.mjs +1 -107
  25. package/cross-terminal.mjs +1 -789
  26. package/cumulative-event-relay.mjs +1 -53
  27. package/design-edit.mjs +1 -424
  28. package/design-source-contract.mjs +1 -4
  29. package/direct-pair-room.mjs +1 -57
  30. package/dispatch-lease.mjs +1 -37
  31. package/dispatch-permission-cleanup.mjs +1 -86
  32. package/edit-diff.mjs +1 -136
  33. package/error-recovery.mjs +1 -50
  34. package/event-bounds.mjs +1 -121
  35. package/event-delivery-queue.mjs +1 -60
  36. package/event-id.mjs +1 -549
  37. package/evidence-citations.mjs +1 -50
  38. package/evidence-compact.mjs +1 -11
  39. package/flow-assembly.mjs +1 -196
  40. package/flow-budget.mjs +1 -84
  41. package/flow-conductor.mjs +1 -259
  42. package/flow-context-store.mjs +1 -387
  43. package/flow-host-revert.mjs +1 -42
  44. package/flow-models.mjs +1 -139
  45. package/flow-preview.mjs +1 -148
  46. package/flow-receipt.mjs +1 -122
  47. package/flow-redispatch.mjs +1 -72
  48. package/flow-review-gate.mjs +1 -402
  49. package/flow-review-reflect.mjs +1 -115
  50. package/flow-review.mjs +1 -152
  51. package/flow-scope-evidence.mjs +1 -117
  52. package/flow-skill-registry.mjs +1 -140
  53. package/flow-task-graph.mjs +1 -562
  54. package/flow-worktree.mjs +1 -71
  55. package/git-diff-report.mjs +1 -121
  56. package/hermes-delegation-guard.mjs +1 -14
  57. package/hermes-event-mapper.mjs +1 -194
  58. package/hermes-isolation.mjs +1 -61
  59. package/hermes-model-cache.mjs +1 -54
  60. package/hermes-policy.mjs +1 -92
  61. package/hermes-probe.mjs +1 -57
  62. package/hermes-session.mjs +1 -683
  63. package/hermes-setup.mjs +1 -167
  64. package/host-memory.mjs +1 -116
  65. package/interrupted-resume.mjs +1 -95
  66. package/keep-awake.mjs +1 -148
  67. package/key-shape.mjs +1 -49
  68. package/lane-continuation.mjs +1 -83
  69. package/lane-lifecycle.mjs +1 -189
  70. package/lane-worktree.mjs +1 -77
  71. package/launcher.mjs +1 -354
  72. package/mcp-flight-recorder.mjs +1 -79
  73. package/mockup-delivery.mjs +1 -57
  74. package/model-prices.mjs +1 -113
  75. package/package.json +13 -4
  76. package/pair-bus.mjs +1 -98
  77. package/pair-control-authority.mjs +1 -89
  78. package/past-work-search.mjs +1 -105
  79. package/plan-meters.mjs +1 -144
  80. package/presence.mjs +1 -191
  81. package/privacy-report.mjs +1 -108
  82. package/provider-resilience.mjs +1 -356
  83. package/provider.mjs +1 -133
  84. package/providers.mjs +1 -491
  85. package/publish-guard.mjs +2 -0
  86. package/publish-manifest.json +129 -0
  87. package/question-response.mjs +1 -58
  88. package/reap-terminal.mjs +1 -68
  89. package/recap.mjs +1 -297
  90. package/replay-transport.mjs +1 -64
  91. package/repo-search.mjs +1 -190
  92. package/review-check.mjs +1 -182
  93. package/runtime-contract.mjs +1 -93
  94. package/runtime-registry.mjs +1 -64
  95. package/runtime-session.mjs +1 -20
  96. package/scheduled-run-admission.mjs +1 -364
  97. package/scheduled-runs.mjs +1 -268
  98. package/sdk-admission.mjs +1 -9
  99. package/sdk-smoke.mjs +1 -61
  100. package/serve-consent.mjs +1 -118
  101. package/serve-dir.mjs +1 -40
  102. package/service.mjs +1 -877
  103. package/session-store.mjs +1 -484
  104. package/side-lane.mjs +1 -63
  105. package/supabase-key.mjs +1 -176
  106. package/supervisor-ready.mjs +1 -57
  107. package/switch-provider.mjs +1 -84
  108. package/terminal-name.mjs +1 -359
  109. package/terminal-row-reconcile.mjs +1 -70
  110. package/thinkpool-prompt-contracts.mjs +1 -85
  111. package/thinkpool-room-prompt.mjs +1 -187
  112. package/transcript-sanitize.mjs +1 -332
  113. package/turn-stall.mjs +1 -61
  114. package/update-gate.mjs +1 -53
  115. package/viewport.mjs +1 -810
  116. package/worker-completion.mjs +1 -57
@@ -1,118 +1 @@
1
- /* Canonical ThinkPool Code event contract.
2
- Keep this module browser-safe: the bridge and src/pages/code both consume it. */
3
-
4
- import { recoveryCodeEvent } from './error-recovery.mjs'
5
-
6
- export const EVENT_CLASS = Object.freeze({
7
- VOLATILE_PRESENCE: 'volatile-presence',
8
- REPLAYABLE_PROGRESS: 'replayable-progress',
9
- REPLAYABLE_COMMAND: 'replayable-command',
10
- DURABLE_DELIVERY_EVIDENCE: 'durable-delivery-evidence',
11
- UNKNOWN: 'unknown',
12
- })
13
-
14
- const V = EVENT_CLASS.VOLATILE_PRESENCE
15
- const P = EVENT_CLASS.REPLAYABLE_PROGRESS
16
- const C = EVENT_CLASS.REPLAYABLE_COMMAND
17
- const E = EVENT_CLASS.DURABLE_DELIVERY_EVIDENCE
18
-
19
- // Exhaustive over every kind currently emitted into a Code room, its retained log,
20
- // or the durable __agent notification stream. `answer`/`control`/etc. are commands
21
- // replayed by the transcript; they are not durable decision records. Pair control
22
- // supplies actor-bound decision records and authoritative verification receipts.
23
- // Adding a producer without adding its kind here is a test/dev error and a
24
- // fail-closed production value.
25
- export const CODE_EVENT_REGISTRY = Object.freeze({
26
- system: V,
27
- thinking_tokens: V,
28
- effort: V,
29
- mode: V,
30
- usage: V,
31
- suggestion: V,
32
- stalled: V,
33
- compact: V,
34
- assistant_stream: V,
35
- models: V,
36
- 'model-switch': V,
37
- capabilities: V,
38
- 'update-available': V,
39
-
40
- you: P,
41
- assistant: P,
42
- tool_result: P,
43
- note: P,
44
- pool: P,
45
- chat: P,
46
- whisper: P,
47
- compaction: P,
48
- 'side-started': P,
49
- 'side-handback': P,
50
- answer: C,
51
- control: C,
52
- 'repo-search': C,
53
- clear: C,
54
- 'queue-add': C,
55
- 'queue-cancel': C,
56
- propose: C,
57
- 'propose-done': C,
58
-
59
- result: E,
60
- error: E,
61
- mockup: E,
62
- image: E,
63
- 'needs-input': E,
64
- 'needs-resolved': E,
65
- continuation: E,
66
- 'turn-done': E,
67
- 'flow-review-receipt': E,
68
- })
69
-
70
- export const CODE_EVENT_KINDS = Object.freeze(Object.keys(CODE_EVENT_REGISTRY))
71
-
72
- const strictByDefault = () =>
73
- typeof process !== 'undefined' && (process.env?.NODE_ENV === 'test' || process.env?.NODE_ENV === 'development')
74
-
75
- export function classifyCodeEvent (eventOrKind, { strict = strictByDefault() } = {}) {
76
- const kind = typeof eventOrKind === 'string' ? eventOrKind : eventOrKind?.kind
77
- const eventClass = typeof kind === 'string' ? CODE_EVENT_REGISTRY[kind] : null
78
- if (!eventClass) {
79
- if (strict) throw new TypeError(`Unknown ThinkPool Code event kind: ${String(kind)}`)
80
- return Object.freeze({ kind: kind ?? null, known: false, eventClass: EVENT_CLASS.UNKNOWN, durable: false, replayable: false, authorizes: false, completes: false })
81
- }
82
- return Object.freeze({
83
- kind,
84
- known: true,
85
- eventClass,
86
- // A replayable command is historical UI context, not a durable decision fact.
87
- // Only delivery evidence currently has the archive-level durability contract.
88
- durable: eventClass === E,
89
- replayable: eventClass !== V,
90
- // Pair control supplies actor-bound authorization. Classification alone never does.
91
- authorizes: false,
92
- // `completes` means an authority-verified outcome, not a runtime turn boundary.
93
- // A successful result can settle execution while remaining unverified.
94
- completes: false,
95
- })
96
- }
97
-
98
- // Preserve observability without copying an unregistered payload into the replay
99
- // or archive. This is a bridge-observed delivery failure, not a claim about what
100
- // the producer meant or a substitute for its event.
101
- export function unknownCodeEventNotice (eventOrKind) {
102
- const rawKind = typeof eventOrKind === 'string' ? eventOrKind : eventOrKind?.kind
103
- const unknownKind = typeof rawKind === 'string' ? rawKind.slice(0, 120) : null
104
- return {
105
- kind: 'error',
106
- subtype: 'unknown_event',
107
- message: `Unrecognized Code event${unknownKind ? ` "${unknownKind}"` : ''} was rejected; this lane needs recovery.`,
108
- unknownKind,
109
- }
110
- }
111
-
112
- export const isKnownCodeEventKind = (kind) => Object.hasOwn(CODE_EVENT_REGISTRY, kind)
113
-
114
- // Normalize recovery reporting before it enters the durable room log. This is a
115
- // projection only: adapters retain their existing retry and session behavior.
116
- export function boundedRecoveryCodeEvent(input) {
117
- return recoveryCodeEvent(input)
118
- }
1
+ import{recoveryCodeEvent as e}from"./error-recovery.mjs";export const EVENT_CLASS=Object.freeze({VOLATILE_PRESENCE:"volatile-presence",REPLAYABLE_PROGRESS:"replayable-progress",REPLAYABLE_COMMAND:"replayable-command",DURABLE_DELIVERY_EVIDENCE:"durable-delivery-evidence",UNKNOWN:"unknown"});const n=EVENT_CLASS.VOLATILE_PRESENCE,t=EVENT_CLASS.REPLAYABLE_PROGRESS,o=EVENT_CLASS.REPLAYABLE_COMMAND,r=EVENT_CLASS.DURABLE_DELIVERY_EVIDENCE;export const CODE_EVENT_REGISTRY=Object.freeze({system:n,thinking_tokens:n,effort:n,mode:n,usage:n,suggestion:n,stalled:n,compact:n,assistant_stream:n,models:n,"model-switch":n,capabilities:n,"update-available":n,you:t,assistant:t,tool_result:t,note:t,pool:t,chat:t,whisper:t,compaction:t,"side-started":t,"side-handback":t,answer:o,control:o,"repo-search":o,clear:o,"queue-add":o,"queue-cancel":o,propose:o,"propose-done":o,result:r,error:r,mockup:r,image:r,"needs-input":r,"needs-resolved":r,continuation:r,"turn-done":r,"flow-review-receipt":r});export const CODE_EVENT_KINDS=Object.freeze(Object.keys(CODE_EVENT_REGISTRY));const s=()=>"undefined"!=typeof process&&("test"===process.env?.NODE_ENV||"development"===process.env?.NODE_ENV);export function classifyCodeEvent(e,{strict:t=s()}={}){const o="string"==typeof e?e:e?.kind,E="string"==typeof o?CODE_EVENT_REGISTRY[o]:null;if(!E){if(t)throw new TypeError(`Unknown ThinkPool Code event kind: ${String(o)}`);return Object.freeze({kind:o??null,known:!1,eventClass:EVENT_CLASS.UNKNOWN,durable:!1,replayable:!1,authorizes:!1,completes:!1})}return Object.freeze({kind:o,known:!0,eventClass:E,durable:E===r,replayable:E!==n,authorizes:!1,completes:!1})}export function unknownCodeEventNotice(e){const n="string"==typeof e?e:e?.kind,t="string"==typeof n?n.slice(0,120):null;return{kind:"error",subtype:"unknown_event",message:`Unrecognized Code event${t?` "${t}"`:""} was rejected; this lane needs recovery.`,unknownKind:t}}export const isKnownCodeEventKind=e=>Object.hasOwn(CODE_EVENT_REGISTRY,e);export function boundedRecoveryCodeEvent(n){return e(n)}
@@ -1,340 +1 @@
1
- // codex-app-server.mjs — defensive JSONL/JSON-RPC transport for Codex App Server.
2
- //
3
- // App Server is still experimental, so the bridge enables it only for the exact
4
- // Codex version range whose protocol we have exercised. TP_CODEX_APP_SERVER=0 is
5
- // the kill switch; `codex exec --json` remains the safe fallback.
6
- import { execFileSync, spawn } from 'node:child_process'
7
- import readline from 'node:readline'
8
-
9
- export const CODEX_APP_SERVER_ENV = 'TP_CODEX_APP_SERVER'
10
- export const MIN_CODEX_APP_SERVER_VERSION = Object.freeze([0, 144, 1])
11
- export const MAX_CODEX_APP_SERVER_VERSION = Object.freeze([0, 145, 0])
12
-
13
- const parseVersion = (raw) => {
14
- const m = String(raw || '').match(/(?:codex-cli\s+)?(\d+)\.(\d+)\.(\d+)/i)
15
- return m ? m.slice(1, 4).map(Number) : null
16
- }
17
-
18
- const atLeast = (actual, minimum) => {
19
- for (let i = 0; i < 3; i++) {
20
- if (actual[i] > minimum[i]) return true
21
- if (actual[i] < minimum[i]) return false
22
- }
23
- return true
24
- }
25
-
26
- const before = (actual, maximum) => !atLeast(actual, maximum)
27
-
28
- const transportError = (message, delivery = 'unknown') => {
29
- const error = message instanceof Error ? message : new Error(String(message || 'Codex App Server transport failed'))
30
- error.delivery = delivery
31
- return error
32
- }
33
-
34
- export function codexAppServerGate({ env = process.env, execFile = execFileSync } = {}) {
35
- const setting = String(env?.[CODEX_APP_SERVER_ENV] || '').trim().toLowerCase()
36
- if (['0', 'false', 'off'].includes(setting)) {
37
- return { enabled: false, reason: `${CODEX_APP_SERVER_ENV} disables App Server`, version: null }
38
- }
39
- if (setting && !['1', 'true', 'on'].includes(setting)) {
40
- return { enabled: false, reason: `${CODEX_APP_SERVER_ENV} has an invalid value`, version: null }
41
- }
42
- try {
43
- const raw = execFile('codex', ['--version'], { encoding: 'utf8', env })
44
- const parsed = parseVersion(raw)
45
- if (!parsed) return { enabled: false, reason: `unrecognized Codex version: ${String(raw).trim()}`, version: null }
46
- const version = parsed.join('.')
47
- if (!atLeast(parsed, MIN_CODEX_APP_SERVER_VERSION)) {
48
- return { enabled: false, reason: `Codex ${version} is older than ${MIN_CODEX_APP_SERVER_VERSION.join('.')}`, version }
49
- }
50
- if (!before(parsed, MAX_CODEX_APP_SERVER_VERSION)) {
51
- return { enabled: false, reason: `Codex ${version} is newer than the tested App Server protocol (<${MAX_CODEX_APP_SERVER_VERSION.join('.')})`, version }
52
- }
53
- return { enabled: true, reason: null, version }
54
- } catch (error) {
55
- return { enabled: false, reason: `could not read Codex version: ${error?.message || error}`, version: null }
56
- }
57
- }
58
-
59
- export function buildCodexAppServerArgs({ providerConfig, mcpUrl, writableDirs = [] } = {}) {
60
- const args = ['app-server', '--stdio']
61
- if (providerConfig) args.push('-c', providerConfig)
62
- if (mcpUrl) {
63
- args.push('-c', `mcp_servers.thinkpool.url=${JSON.stringify(mcpUrl)}`)
64
- // The room prompt promises spawn/read/close_terminal as native room tools.
65
- // Fail startup instead of silently running a tool-less Codex turn when the
66
- // private loopback MCP handshake is unavailable. Existing MCP approval
67
- // policy still owns the room permission card for each call.
68
- args.push('-c', 'mcp_servers.thinkpool.required=true')
69
- }
70
- if (writableDirs.length) args.push('-c', `sandbox_workspace_write.writable_roots=${JSON.stringify(writableDirs)}`)
71
- return args
72
- }
73
-
74
- const rpcError = (message, code = -32000) => ({ code, message: String(message || 'App Server request failed') })
75
- const turnInput = (text, images = []) => [
76
- { type: 'text', text: String(text ?? '') },
77
- ...images.filter(Boolean).map((imagePath) => ({ type: 'localImage', path: String(imagePath) })),
78
- ]
79
-
80
- /**
81
- * Minimal App Server client. It deliberately accepts unknown notifications and
82
- * fields: protocol additions must not take down an otherwise usable lane.
83
- */
84
- export class CodexAppServerClient {
85
- constructor({ cwd, env, args = ['app-server', '--stdio'], spawnImpl = spawn, onNotification, onServerRequest, onClose, initializeTimeoutMs = 3000, threadTimeoutMs = 5000, turnStartTimeoutMs = 5000, controlTimeoutMs = 5000 } = {}) {
86
- this.cwd = cwd
87
- this.env = env
88
- this.args = args
89
- this.spawnImpl = spawnImpl
90
- this.onNotification = onNotification
91
- this.onServerRequest = onServerRequest
92
- this.onClose = onClose
93
- this.initializeTimeoutMs = initializeTimeoutMs
94
- this.threadTimeoutMs = threadTimeoutMs
95
- this.turnStartTimeoutMs = turnStartTimeoutMs
96
- this.controlTimeoutMs = controlTimeoutMs
97
- this.child = null
98
- this.nextId = 1
99
- this.pending = new Map()
100
- this.turnWaiters = new Map()
101
- this.completedTurns = new Map()
102
- this.mcpStartup = new Map()
103
- this.mcpWaiters = new Map()
104
- this.closedError = null
105
- this.stderrTail = ''
106
- }
107
-
108
- get alive() { return !!this.child && !this.closedError }
109
-
110
- async start() {
111
- if (this.child) return
112
- const child = this.spawnImpl('codex', this.args, { cwd: this.cwd, env: this.env, stdio: ['pipe', 'pipe', 'pipe'] })
113
- this.child = child
114
- const rl = readline.createInterface({ input: child.stdout, crlfDelay: Infinity })
115
- rl.on('line', (line) => this._onLine(line))
116
- child.stderr?.on?.('data', (chunk) => {
117
- this.stderrTail += String(chunk)
118
- if (this.stderrTail.length > 1200) this.stderrTail = this.stderrTail.slice(-1200)
119
- })
120
- child.stdin?.on?.('error', (error) => this._close(error))
121
- child.on('error', (error) => this._close(error))
122
- child.on('close', (code) => this._close(new Error(`codex app-server exited ${code ?? 'unknown'}${this.stderrTail ? `: ${this.stderrTail.trim().slice(-300)}` : ''}`)))
123
-
124
- await this.request('initialize', {
125
- clientInfo: { name: 'thinkpool-pair', title: 'ThinkPool Code', version: '1' },
126
- capabilities: { experimentalApi: true },
127
- }, this.initializeTimeoutMs)
128
- this.notify('initialized')
129
- }
130
-
131
- _write(message) {
132
- if (!this.child?.stdin || this.closedError) throw this.closedError || new Error('codex app-server is not running')
133
- this.child.stdin.write(`${JSON.stringify(message)}\n`)
134
- }
135
-
136
- request(method, params = {}, timeoutMs = 0) {
137
- const id = this.nextId++
138
- return new Promise((resolve, reject) => {
139
- let timer = null
140
- if (timeoutMs > 0) timer = setTimeout(() => {
141
- this.pending.delete(id)
142
- reject(transportError(`${method} timed out after ${timeoutMs}ms`, 'unknown'))
143
- }, timeoutMs)
144
- this.pending.set(id, { resolve, reject, timer, method })
145
- try { this._write({ id, method, params }) }
146
- catch (error) {
147
- this.pending.delete(id)
148
- if (timer) clearTimeout(timer)
149
- reject(transportError(error, 'not_sent'))
150
- }
151
- })
152
- }
153
-
154
- notify(method, params) {
155
- this._write(params === undefined ? { method } : { method, params })
156
- }
157
-
158
- respond(id, result) { this._write({ id, result }) }
159
- respondError(id, error) { this._write({ id, error: rpcError(error?.message || error) }) }
160
-
161
- _onLine(line) {
162
- if (!String(line).trim()) return
163
- let message
164
- try { message = JSON.parse(line) } catch { return }
165
- if (message.id != null && ('result' in message || 'error' in message) && !message.method) {
166
- const pending = this.pending.get(message.id)
167
- if (!pending) return
168
- this.pending.delete(message.id)
169
- if (pending.timer) clearTimeout(pending.timer)
170
- if (message.error) {
171
- const error = transportError(message.error.message || `${pending.method} failed`, 'rejected')
172
- error.code = message.error.code
173
- error.data = message.error.data
174
- pending.reject(error)
175
- }
176
- else pending.resolve(message.result)
177
- return
178
- }
179
- if (message.id != null && message.method) {
180
- Promise.resolve().then(() => {
181
- if (!this.onServerRequest) throw new Error(`Unsupported Codex App Server request: ${message.method}`)
182
- return this.onServerRequest(message.method, message.params || {}, message.id)
183
- })
184
- .then((result) => { try { this.respond(message.id, result ?? {}) } catch { /* peer closed */ } })
185
- .catch((error) => { try { this.respondError(message.id, error) } catch { /* peer closed */ } })
186
- return
187
- }
188
- if (!message.method) return
189
- if (message.method === 'turn/completed') {
190
- const turn = message.params?.turn
191
- const turnId = turn?.id
192
- if (turnId) {
193
- const waiter = this.turnWaiters.get(turnId)
194
- if (waiter) { this.turnWaiters.delete(turnId); waiter.resolve(message.params) }
195
- else {
196
- this.completedTurns.set(turnId, message.params)
197
- while (this.completedTurns.size > 32) this.completedTurns.delete(this.completedTurns.keys().next().value)
198
- }
199
- }
200
- }
201
- if (message.method === 'mcpServer/startupStatus/updated') {
202
- const params = message.params || {}
203
- const threadId = String(params.threadId || '')
204
- const name = String(params.name || '')
205
- if (threadId && name) {
206
- const key = `${threadId}\0${name}`
207
- this.mcpStartup.set(key, params)
208
- const waiter = this.mcpWaiters.get(key)
209
- if (waiter && params.status === 'ready') {
210
- this.mcpWaiters.delete(key)
211
- if (waiter.timer) clearTimeout(waiter.timer)
212
- waiter.resolve(params)
213
- } else if (waiter && (params.status === 'failed' || params.error || params.failureReason)) {
214
- this.mcpWaiters.delete(key)
215
- if (waiter.timer) clearTimeout(waiter.timer)
216
- waiter.reject(new Error(`Codex MCP server ${name} failed to start: ${params.error || params.failureReason || params.status}`))
217
- }
218
- }
219
- }
220
- try { this.onNotification?.(message.method, message.params || {}) } catch { /* consumer isolation */ }
221
- }
222
-
223
- _close(error) {
224
- if (this.closedError) return
225
- this.closedError = transportError(error instanceof Error ? error : String(error || 'codex app-server closed'), 'unknown')
226
- this.child = null
227
- for (const { reject, timer } of this.pending.values()) { if (timer) clearTimeout(timer); reject(this.closedError) }
228
- this.pending.clear()
229
- for (const { reject } of this.turnWaiters.values()) reject(this.closedError)
230
- this.turnWaiters.clear()
231
- this.completedTurns.clear()
232
- for (const { reject, timer } of this.mcpWaiters.values()) {
233
- if (timer) clearTimeout(timer)
234
- reject(this.closedError)
235
- }
236
- this.mcpWaiters.clear()
237
- this.mcpStartup.clear()
238
- try { this.onClose?.(this.closedError) } catch { /* consumer isolation */ }
239
- }
240
-
241
- async startThread({ threadId, cwd, model, sandbox, approvalPolicy, config } = {}) {
242
- const params = { cwd, model, sandbox, approvalPolicy, ...(config ? { config } : {}) }
243
- const result = threadId
244
- ? await this.request('thread/resume', { ...params, threadId }, this.threadTimeoutMs)
245
- : await this.request('thread/start', params, this.threadTimeoutMs)
246
- const id = result?.thread?.id || result?.threadId || threadId
247
- if (!id) throw new Error('Codex App Server did not return a thread id')
248
- return id
249
- }
250
-
251
- waitForMcpServer({ threadId, name, timeoutMs = this.threadTimeoutMs } = {}) {
252
- const safeThreadId = String(threadId || '')
253
- const safeName = String(name || '')
254
- if (!safeThreadId || !safeName) return Promise.reject(new Error('Codex MCP readiness needs a thread id and server name'))
255
- const key = `${safeThreadId}\0${safeName}`
256
- const current = this.mcpStartup.get(key)
257
- if (current?.status === 'ready') return Promise.resolve(current)
258
- if (current && (current.status === 'failed' || current.error || current.failureReason)) {
259
- return Promise.reject(new Error(`Codex MCP server ${safeName} failed to start: ${current.error || current.failureReason || current.status}`))
260
- }
261
- if (this.mcpWaiters.has(key)) return this.mcpWaiters.get(key).promise
262
- let timer = null
263
- let resolveWaiter
264
- let rejectWaiter
265
- const promise = new Promise((resolve, reject) => {
266
- resolveWaiter = resolve
267
- rejectWaiter = reject
268
- if (timeoutMs > 0) timer = setTimeout(() => {
269
- this.mcpWaiters.delete(key)
270
- reject(new Error(`Codex MCP server ${safeName} did not become ready within ${timeoutMs}ms`))
271
- }, timeoutMs)
272
- })
273
- this.mcpWaiters.set(key, { promise, resolve: resolveWaiter, reject: rejectWaiter, timer })
274
- return promise
275
- }
276
-
277
- async startTurn({ threadId, input, images, model, effort, approvalPolicy, collaborationMode } = {}) {
278
- const result = await this.request('turn/start', {
279
- threadId,
280
- input: turnInput(input, images),
281
- ...(model ? { model } : {}),
282
- ...(effort ? { effort } : {}),
283
- ...(approvalPolicy ? { approvalPolicy } : {}),
284
- ...(collaborationMode ? { collaborationMode } : {}),
285
- }, this.turnStartTimeoutMs)
286
- const turnId = result?.turn?.id || result?.turnId
287
- if (!turnId) throw new Error('Codex App Server did not return a turn id')
288
- return turnId
289
- }
290
-
291
- waitForTurn(turnId) {
292
- if (this.completedTurns.has(turnId)) {
293
- const result = this.completedTurns.get(turnId)
294
- this.completedTurns.delete(turnId)
295
- return Promise.resolve(result)
296
- }
297
- if (this.turnWaiters.has(turnId)) return Promise.reject(new Error(`Already waiting for Codex turn ${turnId}`))
298
- return new Promise((resolve, reject) => this.turnWaiters.set(turnId, { resolve, reject }))
299
- }
300
-
301
- async steer({ threadId, turnId, input, images } = {}) {
302
- return this.request('turn/steer', {
303
- threadId,
304
- expectedTurnId: turnId,
305
- input: turnInput(input, images),
306
- }, this.controlTimeoutMs)
307
- }
308
-
309
- interrupt({ threadId, turnId } = {}) {
310
- return this.request('turn/interrupt', { threadId, turnId }, this.controlTimeoutMs)
311
- }
312
-
313
- compact({ threadId } = {}) {
314
- return this.request('thread/compact/start', { threadId }, this.controlTimeoutMs)
315
- }
316
-
317
- accountUsage() {
318
- return this.request('account/usage/read', null, this.controlTimeoutMs)
319
- }
320
-
321
- accountRateLimits() {
322
- return this.request('account/rateLimits/read', null, this.controlTimeoutMs)
323
- }
324
-
325
- startReview({ threadId, target } = {}) {
326
- return this.request('review/start', {
327
- threadId,
328
- target: target || { type: 'uncommittedChanges' },
329
- delivery: 'inline',
330
- }, this.turnStartTimeoutMs)
331
- }
332
-
333
- end() {
334
- const child = this.child
335
- this._close(new Error('codex app-server ended'))
336
- if (child) { try { child.kill('SIGTERM') } catch { /* noop */ } }
337
- }
338
- }
339
-
340
- export function createCodexAppServer(options) { return new CodexAppServerClient(options) }
1
+ import{execFileSync as e,spawn as t}from"node:child_process";import r from"node:readline";export const CODEX_APP_SERVER_ENV="TP_CODEX_APP_SERVER";export const MIN_CODEX_APP_SERVER_VERSION=Object.freeze([0,144,1]);export const MAX_CODEX_APP_SERVER_VERSION=Object.freeze([0,145,0]);const i=(e,t)=>{for(let r=0;r<3;r++){if(e[r]>t[r])return!0;if(e[r]<t[r])return!1}return!0},s=(e,t="unknown")=>{const r=e instanceof Error?e:new Error(String(e||"Codex App Server transport failed"));return r.delivery=t,r};export function codexAppServerGate({env:t=process.env,execFile:r=e}={}){const s=String(t?.[CODEX_APP_SERVER_ENV]||"").trim().toLowerCase();if(["0","false","off"].includes(s))return{enabled:!1,reason:`${CODEX_APP_SERVER_ENV} disables App Server`,version:null};if(s&&!["1","true","on"].includes(s))return{enabled:!1,reason:`${CODEX_APP_SERVER_ENV} has an invalid value`,version:null};try{const e=r("codex",["--version"],{encoding:"utf8",env:t}),s=(e=>{const t=String(e||"").match(/(?:codex-cli\s+)?(\d+)\.(\d+)\.(\d+)/i);return t?t.slice(1,4).map(Number):null})(e);if(!s)return{enabled:!1,reason:`unrecognized Codex version: ${String(e).trim()}`,version:null};const o=s.join(".");return i(s,MIN_CODEX_APP_SERVER_VERSION)?i(s,MAX_CODEX_APP_SERVER_VERSION)?{enabled:!1,reason:`Codex ${o} is newer than the tested App Server protocol (<${MAX_CODEX_APP_SERVER_VERSION.join(".")})`,version:o}:{enabled:!0,reason:null,version:o}:{enabled:!1,reason:`Codex ${o} is older than ${MIN_CODEX_APP_SERVER_VERSION.join(".")}`,version:o}}catch(e){return{enabled:!1,reason:`could not read Codex version: ${e?.message||e}`,version:null}}}export function buildCodexAppServerArgs({providerConfig:e,mcpUrl:t,writableDirs:r=[]}={}){const i=["app-server","--stdio"];return e&&i.push("-c",e),t&&(i.push("-c",`mcp_servers.thinkpool.url=${JSON.stringify(t)}`),i.push("-c","mcp_servers.thinkpool.required=true")),r.length&&i.push("-c",`sandbox_workspace_write.writable_roots=${JSON.stringify(r)}`),i}const o=(e,t=-32e3)=>({code:t,message:String(e||"App Server request failed")}),n=(e,t=[])=>[{type:"text",text:String(e??"")},...t.filter(Boolean).map(e=>({type:"localImage",path:String(e)}))];export class CodexAppServerClient{constructor({cwd:e,env:r,args:i=["app-server","--stdio"],spawnImpl:s=t,onNotification:o,onServerRequest:n,onClose:a,initializeTimeoutMs:d=3e3,threadTimeoutMs:c=5e3,turnStartTimeoutMs:l=5e3,controlTimeoutMs:h=5e3}={}){this.cwd=e,this.env=r,this.args=i,this.spawnImpl=s,this.onNotification=o,this.onServerRequest=n,this.onClose=a,this.initializeTimeoutMs=d,this.threadTimeoutMs=c,this.turnStartTimeoutMs=l,this.controlTimeoutMs=h,this.child=null,this.nextId=1,this.pending=new Map,this.turnWaiters=new Map,this.completedTurns=new Map,this.mcpStartup=new Map,this.mcpWaiters=new Map,this.closedError=null,this.stderrTail=""}get alive(){return!!this.child&&!this.closedError}async start(){if(this.child)return;const e=this.spawnImpl("codex",this.args,{cwd:this.cwd,env:this.env,stdio:["pipe","pipe","pipe"]});this.child=e,r.createInterface({input:e.stdout,crlfDelay:1/0}).on("line",e=>this._onLine(e)),e.stderr?.on?.("data",e=>{this.stderrTail+=String(e),this.stderrTail.length>1200&&(this.stderrTail=this.stderrTail.slice(-1200))}),e.stdin?.on?.("error",e=>this._close(e)),e.on("error",e=>this._close(e)),e.on("close",e=>this._close(new Error(`codex app-server exited ${e??"unknown"}${this.stderrTail?`: ${this.stderrTail.trim().slice(-300)}`:""}`))),await this.request("initialize",{clientInfo:{name:"thinkpool-pair",title:"ThinkPool Code",version:"1"},capabilities:{experimentalApi:!0}},this.initializeTimeoutMs),this.notify("initialized")}_write(e){if(!this.child?.stdin||this.closedError)throw this.closedError||new Error("codex app-server is not running");this.child.stdin.write(`${JSON.stringify(e)}\n`)}request(e,t={},r=0){const i=this.nextId++;return new Promise((o,n)=>{let a=null;r>0&&(a=setTimeout(()=>{this.pending.delete(i),n(s(`${e} timed out after ${r}ms`,"unknown"))},r)),this.pending.set(i,{resolve:o,reject:n,timer:a,method:e});try{this._write({id:i,method:e,params:t})}catch(e){this.pending.delete(i),a&&clearTimeout(a),n(s(e,"not_sent"))}})}notify(e,t){this._write(void 0===t?{method:e}:{method:e,params:t})}respond(e,t){this._write({id:e,result:t})}respondError(e,t){this._write({id:e,error:o(t?.message||t)})}_onLine(e){if(!String(e).trim())return;let t;try{t=JSON.parse(e)}catch{return}if(null!=t.id&&("result"in t||"error"in t)&&!t.method){const e=this.pending.get(t.id);if(!e)return;if(this.pending.delete(t.id),e.timer&&clearTimeout(e.timer),t.error){const r=s(t.error.message||`${e.method} failed`,"rejected");r.code=t.error.code,r.data=t.error.data,e.reject(r)}else e.resolve(t.result);return}if(null!=t.id&&t.method)Promise.resolve().then(()=>{if(!this.onServerRequest)throw new Error(`Unsupported Codex App Server request: ${t.method}`);return this.onServerRequest(t.method,t.params||{},t.id)}).then(e=>{try{this.respond(t.id,e??{})}catch{}}).catch(e=>{try{this.respondError(t.id,e)}catch{}});else if(t.method){if("turn/completed"===t.method){const e=t.params?.turn,r=e?.id;if(r){const e=this.turnWaiters.get(r);if(e)this.turnWaiters.delete(r),e.resolve(t.params);else for(this.completedTurns.set(r,t.params);this.completedTurns.size>32;)this.completedTurns.delete(this.completedTurns.keys().next().value)}}if("mcpServer/startupStatus/updated"===t.method){const e=t.params||{},r=String(e.threadId||""),i=String(e.name||"");if(r&&i){const t=`${r}\0${i}`;this.mcpStartup.set(t,e);const s=this.mcpWaiters.get(t);s&&"ready"===e.status?(this.mcpWaiters.delete(t),s.timer&&clearTimeout(s.timer),s.resolve(e)):s&&("failed"===e.status||e.error||e.failureReason)&&(this.mcpWaiters.delete(t),s.timer&&clearTimeout(s.timer),s.reject(new Error(`Codex MCP server ${i} failed to start: ${e.error||e.failureReason||e.status}`)))}}try{this.onNotification?.(t.method,t.params||{})}catch{}}}_close(e){if(!this.closedError){this.closedError=s(e instanceof Error?e:String(e||"codex app-server closed"),"unknown"),this.child=null;for(const{reject:e,timer:t}of this.pending.values())t&&clearTimeout(t),e(this.closedError);this.pending.clear();for(const{reject:e}of this.turnWaiters.values())e(this.closedError);this.turnWaiters.clear(),this.completedTurns.clear();for(const{reject:e,timer:t}of this.mcpWaiters.values())t&&clearTimeout(t),e(this.closedError);this.mcpWaiters.clear(),this.mcpStartup.clear();try{this.onClose?.(this.closedError)}catch{}}}async startThread({threadId:e,cwd:t,model:r,sandbox:i,approvalPolicy:s,config:o}={}){const n={cwd:t,model:r,sandbox:i,approvalPolicy:s,...o?{config:o}:{}},a=e?await this.request("thread/resume",{...n,threadId:e},this.threadTimeoutMs):await this.request("thread/start",n,this.threadTimeoutMs),d=a?.thread?.id||a?.threadId||e;if(!d)throw new Error("Codex App Server did not return a thread id");return d}waitForMcpServer({threadId:e,name:t,timeoutMs:r=this.threadTimeoutMs}={}){const i=String(e||""),s=String(t||"");if(!i||!s)return Promise.reject(new Error("Codex MCP readiness needs a thread id and server name"));const o=`${i}\0${s}`,n=this.mcpStartup.get(o);if("ready"===n?.status)return Promise.resolve(n);if(n&&("failed"===n.status||n.error||n.failureReason))return Promise.reject(new Error(`Codex MCP server ${s} failed to start: ${n.error||n.failureReason||n.status}`));if(this.mcpWaiters.has(o))return this.mcpWaiters.get(o).promise;let a,d,c=null;const l=new Promise((e,t)=>{a=e,d=t,r>0&&(c=setTimeout(()=>{this.mcpWaiters.delete(o),t(new Error(`Codex MCP server ${s} did not become ready within ${r}ms`))},r))});return this.mcpWaiters.set(o,{promise:l,resolve:a,reject:d,timer:c}),l}async startTurn({threadId:e,input:t,images:r,model:i,effort:s,approvalPolicy:o,collaborationMode:a}={}){const d=await this.request("turn/start",{threadId:e,input:n(t,r),...i?{model:i}:{},...s?{effort:s}:{},...o?{approvalPolicy:o}:{},...a?{collaborationMode:a}:{}},this.turnStartTimeoutMs),c=d?.turn?.id||d?.turnId;if(!c)throw new Error("Codex App Server did not return a turn id");return c}waitForTurn(e){if(this.completedTurns.has(e)){const t=this.completedTurns.get(e);return this.completedTurns.delete(e),Promise.resolve(t)}return this.turnWaiters.has(e)?Promise.reject(new Error(`Already waiting for Codex turn ${e}`)):new Promise((t,r)=>this.turnWaiters.set(e,{resolve:t,reject:r}))}async steer({threadId:e,turnId:t,input:r,images:i}={}){return this.request("turn/steer",{threadId:e,expectedTurnId:t,input:n(r,i)},this.controlTimeoutMs)}interrupt({threadId:e,turnId:t}={}){return this.request("turn/interrupt",{threadId:e,turnId:t},this.controlTimeoutMs)}compact({threadId:e}={}){return this.request("thread/compact/start",{threadId:e},this.controlTimeoutMs)}accountUsage(){return this.request("account/usage/read",null,this.controlTimeoutMs)}accountRateLimits(){return this.request("account/rateLimits/read",null,this.controlTimeoutMs)}startReview({threadId:e,target:t}={}){return this.request("review/start",{threadId:e,target:t||{type:"uncommittedChanges"},delivery:"inline"},this.turnStartTimeoutMs)}end(){const e=this.child;if(this._close(new Error("codex app-server ended")),e)try{e.kill("SIGTERM")}catch{}}}export function createCodexAppServer(e){return new CodexAppServerClient(e)}
@@ -1,83 +1 @@
1
- // Codex Code-room command policy.
2
- //
3
- // This is intentionally NOT a transcription of the interactive `codex` TUI
4
- // palette. Codex CLI 0.144.1's App Server schema has no slash-command catalog
5
- // endpoint. Keep only a command which the room can actually execute, and keep
6
- // bridge-wide controls in bridge.mjs where their shared state semantics live.
7
-
8
- /**
9
- * Commands surfaced from the Codex runtime's system catalog.
10
- *
11
- * /compact is bridge-executed today: bridge.mjs clears the native Codex context
12
- * and carries a bounded room recap into the next fresh thread. App Server also
13
- * exposes thread/compact/start in 0.144.1, but the bridge path is the supported
14
- * cross-transport implementation, including exec fallback and the room's
15
- * durable recap semantics.
16
- */
17
- export const CODEX_COMMAND_CATALOG = Object.freeze([])
18
-
19
- /**
20
- * Evidence-backed command matrix for the installed Codex CLI (0.144.1).
21
- * `surface` means what ThinkPool intentionally advertises; it is not a claim
22
- * that an interactive terminal palette command works through App Server.
23
- */
24
- export const CODEX_COMMAND_MATRIX = Object.freeze([
25
- Object.freeze({ name: '/compact', surface: true, runtime: 'native', api: 'thread/compact/start', reason: 'Native App Server compaction; bounded-recap reset remains the exec fallback.' }),
26
- Object.freeze({ name: '/model', surface: true, runtime: 'bridge', api: 'model/list', reason: 'Shared picker owns the model argument.' }),
27
- Object.freeze({ name: '/clear', surface: true, runtime: 'bridge', api: null, reason: 'Shared reset clears visible lane history and starts a fresh native thread.' }),
28
- Object.freeze({ name: '/usage', surface: true, runtime: 'bridge+native', api: 'account/usage/read + account/rateLimits/read', reason: 'Reports lane usage plus redacted provider totals and limits.' }),
29
- Object.freeze({ name: '/credits', surface: true, runtime: 'native', api: 'account/rateLimits/read', reason: 'Reports balance/availability only; never account identity.' }),
30
- Object.freeze({ name: '/review', surface: true, runtime: 'native', api: 'review/start', reason: 'Ordinary lanes only; Flow and reviewer structural restrictions win.' }),
31
- ])
32
-
33
- /**
34
- * Explicit exclusions: present in an interactive CLI or App Server protocol,
35
- * but not safe/meaningful Code-room commands. Never add these by inference.
36
- */
37
- export const CODEX_COMMAND_EXCLUSIONS = Object.freeze([
38
- 'profile-global configuration, skills, plugins, marketplaces, and experimental features',
39
- 'updates, diagnostics/feedback upload, and external-agent import',
40
- 'credentials, login/logout, identity, and account mutation',
41
- 'thread archive/delete/fork/name/goal/rollback and hidden-agent controls',
42
- 'MCP OAuth, filesystem, shell, and interactive command-exec protocol methods',
43
- 'detached review and review from Flow/reviewer lanes, because visible structural restrictions win',
44
- ])
45
-
46
- export function isCodexCompactCommand(text) {
47
- return /^\s*\/compact\s*$/i.test(String(text || ''))
48
- }
49
-
50
- export function codexReviewTarget(text) {
51
- const body = String(text || '').replace(/^\s*\/review\b/i, '').trim()
52
- if (!body) return { type: 'uncommittedChanges' }
53
- const base = body.match(/^base\s*:\s*(\S+)$/i)
54
- if (base) return { type: 'baseBranch', branch: base[1] }
55
- const commit = body.match(/^commit\s*:\s*([a-f0-9]{7,64})$/i)
56
- if (commit) return { type: 'commit', sha: commit[1] }
57
- return { type: 'custom', instructions: body }
58
- }
59
-
60
- const pct = (value) => Number.isFinite(Number(value)) ? `${Math.round(Number(value))}%` : null
61
- const reset = (value) => Number.isFinite(Number(value)) ? new Date(Number(value) * 1000).toISOString().replace('T', ' ').slice(0, 16) + 'Z' : null
62
-
63
- export function codexLimitReportLine(snapshot) {
64
- const limits = snapshot?.rateLimits || null
65
- if (!limits) return 'Codex provider limits unavailable'
66
- const parts = []
67
- if (limits.primary?.usedPercent != null) parts.push(`primary ${pct(limits.primary.usedPercent)}${reset(limits.primary.resetsAt) ? ` · resets ${reset(limits.primary.resetsAt)}` : ''}`)
68
- if (limits.secondary?.usedPercent != null) parts.push(`secondary ${pct(limits.secondary.usedPercent)}${reset(limits.secondary.resetsAt) ? ` · resets ${reset(limits.secondary.resetsAt)}` : ''}`)
69
- return parts.length ? `Codex limits · ${parts.join(' · ')}` : 'Codex provider limits unavailable'
70
- }
71
-
72
- export function codexCreditsReportLine(snapshot) {
73
- const credits = snapshot?.rateLimits?.credits
74
- if (!credits) return 'Codex credits unavailable for this account'
75
- if (credits.unlimited) return 'Codex credits · unlimited'
76
- if (credits.balance != null) return `Codex credits · balance ${String(credits.balance)}`
77
- return `Codex credits · ${credits.hasCredits ? 'available' : 'none available'}`
78
- }
79
-
80
- export function codexAccountUsageLine(snapshot) {
81
- const total = snapshot?.summary?.lifetimeTokens
82
- return Number.isFinite(Number(total)) ? `Codex account usage · ${Number(total).toLocaleString('en-US')} lifetime tokens` : null
83
- }
1
+ export const CODEX_COMMAND_CATALOG=Object.freeze([]);export const CODEX_COMMAND_MATRIX=Object.freeze([Object.freeze({name:"/compact",surface:!0,runtime:"native",api:"thread/compact/start",reason:"Native App Server compaction; bounded-recap reset remains the exec fallback."}),Object.freeze({name:"/model",surface:!0,runtime:"bridge",api:"model/list",reason:"Shared picker owns the model argument."}),Object.freeze({name:"/clear",surface:!0,runtime:"bridge",api:null,reason:"Shared reset clears visible lane history and starts a fresh native thread."}),Object.freeze({name:"/usage",surface:!0,runtime:"bridge+native",api:"account/usage/read + account/rateLimits/read",reason:"Reports lane usage plus redacted provider totals and limits."}),Object.freeze({name:"/credits",surface:!0,runtime:"native",api:"account/rateLimits/read",reason:"Reports balance/availability only; never account identity."}),Object.freeze({name:"/review",surface:!0,runtime:"native",api:"review/start",reason:"Ordinary lanes only; Flow and reviewer structural restrictions win."})]);export const CODEX_COMMAND_EXCLUSIONS=Object.freeze(["profile-global configuration, skills, plugins, marketplaces, and experimental features","updates, diagnostics/feedback upload, and external-agent import","credentials, login/logout, identity, and account mutation","thread archive/delete/fork/name/goal/rollback and hidden-agent controls","MCP OAuth, filesystem, shell, and interactive command-exec protocol methods","detached review and review from Flow/reviewer lanes, because visible structural restrictions win"]);export function isCodexCompactCommand(e){return/^\s*\/compact\s*$/i.test(String(e||""))}export function codexReviewTarget(e){const t=String(e||"").replace(/^\s*\/review\b/i,"").trim();if(!t)return{type:"uncommittedChanges"};const r=t.match(/^base\s*:\s*(\S+)$/i);if(r)return{type:"baseBranch",branch:r[1]};const a=t.match(/^commit\s*:\s*([a-f0-9]{7,64})$/i);return a?{type:"commit",sha:a[1]}:{type:"custom",instructions:t}}const e=e=>Number.isFinite(Number(e))?`${Math.round(Number(e))}%`:null,t=e=>Number.isFinite(Number(e))?new Date(1e3*Number(e)).toISOString().replace("T"," ").slice(0,16)+"Z":null;export function codexLimitReportLine(r){const a=r?.rateLimits||null;if(!a)return"Codex provider limits unavailable";const n=[];return null!=a.primary?.usedPercent&&n.push(`primary ${e(a.primary.usedPercent)}${t(a.primary.resetsAt)?` · resets ${t(a.primary.resetsAt)}`:""}`),null!=a.secondary?.usedPercent&&n.push(`secondary ${e(a.secondary.usedPercent)}${t(a.secondary.resetsAt)?` · resets ${t(a.secondary.resetsAt)}`:""}`),n.length?`Codex limits · ${n.join(" · ")}`:"Codex provider limits unavailable"}export function codexCreditsReportLine(e){const t=e?.rateLimits?.credits;return t?t.unlimited?"Codex credits · unlimited":null!=t.balance?`Codex credits · balance ${String(t.balance)}`:"Codex credits · "+(t.hasCredits?"available":"none available"):"Codex credits unavailable for this account"}export function codexAccountUsageLine(e){const t=e?.summary?.lifetimeTokens;return Number.isFinite(Number(t))?`Codex account usage · ${Number(t).toLocaleString("en-US")} lifetime tokens`:null}