agentquad 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +318 -0
  3. package/dist-web/assets/index-CMaXwixo.js +1234 -0
  4. package/dist-web/assets/index-DBHApzV1.css +32 -0
  5. package/dist-web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
  6. package/dist-web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
  7. package/dist-web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
  8. package/dist-web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
  9. package/dist-web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
  10. package/dist-web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
  11. package/dist-web/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
  12. package/dist-web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
  13. package/dist-web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
  14. package/dist-web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
  15. package/dist-web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
  16. package/dist-web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
  17. package/dist-web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
  18. package/dist-web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
  19. package/dist-web/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
  20. package/dist-web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
  21. package/dist-web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
  22. package/dist-web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
  23. package/dist-web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
  24. package/dist-web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
  25. package/dist-web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
  26. package/dist-web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
  27. package/dist-web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
  28. package/dist-web/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
  29. package/dist-web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
  30. package/dist-web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
  31. package/dist-web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
  32. package/dist-web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
  33. package/dist-web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
  34. package/dist-web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
  35. package/dist-web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
  36. package/dist-web/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
  37. package/dist-web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
  38. package/dist-web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
  39. package/dist-web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
  40. package/dist-web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
  41. package/dist-web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
  42. package/dist-web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
  43. package/dist-web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
  44. package/dist-web/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
  45. package/dist-web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
  46. package/dist-web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
  47. package/dist-web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
  48. package/dist-web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
  49. package/dist-web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
  50. package/dist-web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
  51. package/dist-web/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
  52. package/dist-web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
  53. package/dist-web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
  54. package/dist-web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
  55. package/dist-web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
  56. package/dist-web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
  57. package/dist-web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
  58. package/dist-web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
  59. package/dist-web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
  60. package/dist-web/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
  61. package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  62. package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  63. package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
  64. package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
  65. package/dist-web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  66. package/dist-web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  67. package/dist-web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
  68. package/dist-web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
  69. package/dist-web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  70. package/dist-web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  71. package/dist-web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
  72. package/dist-web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
  73. package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  74. package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  75. package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
  76. package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
  77. package/dist-web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  78. package/dist-web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
  79. package/dist-web/assets/logo-D4DDtU-r.png +0 -0
  80. package/dist-web/favicon.png +0 -0
  81. package/dist-web/index.html +14 -0
  82. package/package.json +88 -0
  83. package/src/ask-user-buttons.js +142 -0
  84. package/src/claude-transcript.js +203 -0
  85. package/src/cli.js +1040 -0
  86. package/src/codex-event-emitter.js +111 -0
  87. package/src/codex-prompt-detector.js +53 -0
  88. package/src/codex-sidecar.js +52 -0
  89. package/src/codex-transcript.js +74 -0
  90. package/src/config.js +692 -0
  91. package/src/data/claude-code-commands.json +52 -0
  92. package/src/db.js +1503 -0
  93. package/src/dispatch.js +13 -0
  94. package/src/export/todoMarkdown.js +246 -0
  95. package/src/first-run-wizard.js +82 -0
  96. package/src/git/gitStatus.js +139 -0
  97. package/src/lark-api-client.js +205 -0
  98. package/src/lark-bot.js +510 -0
  99. package/src/lark-card.js +88 -0
  100. package/src/lark-config-service.js +16 -0
  101. package/src/lark-event-client.js +107 -0
  102. package/src/lark-image.js +99 -0
  103. package/src/lark-markdown.js +51 -0
  104. package/src/lark-video.js +163 -0
  105. package/src/mcp/audit.js +34 -0
  106. package/src/mcp/server.js +83 -0
  107. package/src/mcp/tools/destructive/index.js +252 -0
  108. package/src/mcp/tools/openclaw/index.js +405 -0
  109. package/src/mcp/tools/read/index.js +269 -0
  110. package/src/mcp/tools/write/index.js +157 -0
  111. package/src/openclaw-bridge.js +566 -0
  112. package/src/openclaw-hook-installer.js +338 -0
  113. package/src/openclaw-hook.js +908 -0
  114. package/src/openclaw-wizard.js +2442 -0
  115. package/src/pending-questions.js +297 -0
  116. package/src/pricing.js +45 -0
  117. package/src/prompt-render.js +36 -0
  118. package/src/pty.js +992 -0
  119. package/src/routes/ai-terminal.js +1228 -0
  120. package/src/routes/git.js +89 -0
  121. package/src/routes/openclaw-hook.js +67 -0
  122. package/src/routes/openclaw-inbound.js +36 -0
  123. package/src/routes/recurringRules.js +80 -0
  124. package/src/routes/reports.js +50 -0
  125. package/src/routes/search.js +46 -0
  126. package/src/routes/stats.js +31 -0
  127. package/src/routes/telegram-config.js +152 -0
  128. package/src/routes/telegram-sync.js +221 -0
  129. package/src/routes/templates.js +63 -0
  130. package/src/routes/todos.js +649 -0
  131. package/src/routes/transcripts.js +75 -0
  132. package/src/routes/uploads.js +107 -0
  133. package/src/routes/wiki.js +142 -0
  134. package/src/search/fts.js +209 -0
  135. package/src/search/index.js +199 -0
  136. package/src/search/transcripts.js +148 -0
  137. package/src/server.js +1791 -0
  138. package/src/session-input-dispatcher.js +256 -0
  139. package/src/stats/markdown.js +42 -0
  140. package/src/stats/report.js +207 -0
  141. package/src/summarize.js +84 -0
  142. package/src/system-rules.js +52 -0
  143. package/src/telegram-bot.js +875 -0
  144. package/src/telegram-commands.js +149 -0
  145. package/src/telegram-config-service.js +84 -0
  146. package/src/telegram-image.js +95 -0
  147. package/src/telegram-loading-status.js +112 -0
  148. package/src/telegram-markdown.js +82 -0
  149. package/src/telegram-reaction-tracker.js +69 -0
  150. package/src/telegram-video.js +75 -0
  151. package/src/templates/claude-hooks/notify.js +103 -0
  152. package/src/transcript.js +305 -0
  153. package/src/transcripts/blocks.js +56 -0
  154. package/src/transcripts/index.js +222 -0
  155. package/src/transcripts/indexer.js +34 -0
  156. package/src/transcripts/matcher.js +70 -0
  157. package/src/transcripts/scanner.js +259 -0
  158. package/src/usage-footer.js +170 -0
  159. package/src/usage-parser.js +132 -0
  160. package/src/wiki/guide.js +44 -0
  161. package/src/wiki/index.js +232 -0
  162. package/src/wiki/redact.js +34 -0
  163. package/src/wiki/sources.js +122 -0
@@ -0,0 +1,111 @@
1
+ import { watch, watchFile, unwatchFile, openSync, readSync, closeSync, statSync } from 'node:fs'
2
+
3
+ const ABORT_DEDUP_MS = 100
4
+
5
+ /**
6
+ * 监听 codex rollout-*.jsonl 文件增量并把关键事件抛给上层。
7
+ * - task_complete → Stop
8
+ * - turn_aborted → TurnAborted(与 <turn_aborted> 用户消息 100ms 内去重)
9
+ * - error → Error
10
+ * 同时记录最新一条 assistant 文本,给 getLatestAssistantContent() 用。
11
+ */
12
+ export function createCodexEventEmitter({ filePath, nativeId, onEvent, logger = console } = {}) {
13
+ if (!filePath || !nativeId || !onEvent) throw new Error('filePath, nativeId, onEvent required')
14
+
15
+ let pos = 0
16
+ let watcher = null
17
+ let pollTimer = null
18
+ let buffer = ''
19
+ let latestAssistantText = ''
20
+ let lastAbortTs = 0
21
+
22
+ function readNew() {
23
+ let stat
24
+ try { stat = statSync(filePath) } catch { return }
25
+ if (stat.size <= pos) return
26
+ const fd = openSync(filePath, 'r')
27
+ try {
28
+ const buf = Buffer.alloc(stat.size - pos)
29
+ readSync(fd, buf, 0, buf.length, pos)
30
+ pos = stat.size
31
+ buffer += buf.toString('utf8')
32
+ } finally {
33
+ closeSync(fd)
34
+ }
35
+ let idx
36
+ while ((idx = buffer.indexOf('\n')) >= 0) {
37
+ const line = buffer.slice(0, idx)
38
+ buffer = buffer.slice(idx + 1)
39
+ if (!line.trim()) continue
40
+ try { handleLine(JSON.parse(line)) }
41
+ catch (e) { logger.warn?.(`[codex-emitter] bad jsonl line ignored: ${e.message}`) }
42
+ }
43
+ }
44
+
45
+ function handleLine(j) {
46
+ const t = j?.type
47
+ const p = j?.payload
48
+ if (t === 'event_msg') {
49
+ const pt = p?.type
50
+ if (pt === 'task_complete') {
51
+ onEvent({ event: 'Stop', nativeId, rawEventPayload: p })
52
+ } else if (pt === 'turn_aborted') {
53
+ lastAbortTs = Date.now()
54
+ onEvent({ event: 'TurnAborted', nativeId, rawEventPayload: p })
55
+ } else if (pt === 'error') {
56
+ onEvent({ event: 'Error', nativeId, rawEventPayload: p })
57
+ }
58
+ } else if (t === 'response_item') {
59
+ const pt = p?.type
60
+ if (pt === 'message' && p?.role === 'assistant' && Array.isArray(p?.content)) {
61
+ const text = p.content.map(c => c?.text || '').join('')
62
+ if (text) latestAssistantText = text
63
+ } else if (pt === 'message' && p?.role === 'user' && Array.isArray(p?.content)) {
64
+ // Dedup with sibling event_msg/turn_aborted
65
+ const txt = p.content.map(c => c?.text || '').join('')
66
+ if (txt.includes('<turn_aborted>') && Date.now() - lastAbortTs < ABORT_DEDUP_MS) {
67
+ // suppress
68
+ }
69
+ }
70
+ }
71
+ }
72
+
73
+ function start() {
74
+ // 注意:开始时不要把 pos 推到当前文件末尾,否则 fs.watch 在 macOS 上对刚 append
75
+ // 进来的内容偶尔不触发,会丢首批事件。从 0 起读 + dedup 不必要 —— 文件是会话私有的。
76
+ pos = 0
77
+ readNew()
78
+ try { watcher = watch(filePath, () => readNew()) } catch {}
79
+ // fs.watch 在 APFS / 网络盘上偶尔不触发,watchFile 30ms 轮询做兜底
80
+ watchFile(filePath, { interval: 30, persistent: false }, () => readNew())
81
+ // setInterval 自轮询:watchFile 用 Node 的中央 polling 线程,并发跑很多 emitter
82
+ // 时会被压垮(实测 vitest fork pool 全套 batch 下 watchFile 3 秒不触发);这里
83
+ // 自己每 50ms statSync 一次,跟 OS 通知双保险,加 stat 廉价。
84
+ pollTimer = setInterval(readNew, 50)
85
+ if (pollTimer.unref) pollTimer.unref()
86
+ }
87
+
88
+ function stop() {
89
+ if (watcher) {
90
+ try { watcher.close() } catch {}
91
+ watcher = null
92
+ }
93
+ if (pollTimer) {
94
+ clearInterval(pollTimer)
95
+ pollTimer = null
96
+ }
97
+ try { unwatchFile(filePath) } catch {}
98
+ }
99
+
100
+ function getLatestAssistantContent() {
101
+ return latestAssistantText
102
+ }
103
+
104
+ // 给 PtyManager.onExit 用:codex 自己不会在 jsonl 里写 SessionEnd(它就只是
105
+ // 进程结束),所以由外层合成一条事件触发"会话整体结束"分支。
106
+ function emitSynthetic(evt) {
107
+ onEvent({ ...evt, nativeId: evt.nativeId ?? nativeId })
108
+ }
109
+
110
+ return { start, stop, getLatestAssistantContent, emitSynthetic }
111
+ }
@@ -0,0 +1,53 @@
1
+ const DEFAULT_DEBOUNCE_MS = 1500
2
+ const RING_MAX = 32
3
+
4
+ const PATTERNS = [
5
+ /(approve|allow|continue|proceed)\??\s*\(\s*y\/n\s*\)\s*$/i,
6
+ /\?\s*\[\s*y\/N\s*\]\s*$/i,
7
+ /\?\s*\[\s*Y\/n\s*\]\s*$/i,
8
+ /(允许|批准|授权).*\?\s*[((]\s*[yYnN][\//][nNyY][))]\s*$/,
9
+ /run this command\?\s*\[[^\]]*\]\s*$/i,
10
+ /apply patch\?\s*\[[^\]]*\]\s*$/i,
11
+ ]
12
+
13
+ function stripAnsi(s) {
14
+ return String(s || '').replace(/\x1b\[[0-9;?]*[A-Za-z~]/g, '').replace(/\x1b\][^\x07]*\x07/g, '')
15
+ }
16
+
17
+ export function createCodexPromptDetector({ pty, onMatch, debounceMs = DEFAULT_DEBOUNCE_MS, emitter = null } = {}) {
18
+ if (!pty || !onMatch) throw new Error('pty, onMatch required')
19
+ const ring = []
20
+ let timer = null
21
+ let stopped = false
22
+
23
+ function onData(chunk) {
24
+ if (stopped) return
25
+ ring.push({ ts: Date.now(), text: stripAnsi(String(chunk)) })
26
+ while (ring.length > RING_MAX) ring.shift()
27
+ if (timer) clearTimeout(timer)
28
+ timer = setTimeout(maybeMatch, debounceMs)
29
+ }
30
+
31
+ function maybeMatch() {
32
+ const tail = ring.slice(-4).map(c => c.text).join('')
33
+ let matchedPattern = null
34
+ for (const re of PATTERNS) {
35
+ if (re.test(tail)) { matchedPattern = re.source; break }
36
+ }
37
+ if (!matchedPattern) return
38
+ const resolvedEmitter = typeof emitter === 'function' ? emitter() : emitter
39
+ if (resolvedEmitter?.getLatestAssistantContent) {
40
+ const ai = resolvedEmitter.getLatestAssistantContent() || ''
41
+ const trimmed = tail.slice(-200).trim()
42
+ if (trimmed && (ai.includes(trimmed) || ai.endsWith(trimmed))) {
43
+ return // AI self-quoted prompt; not a real Codex permission ask
44
+ }
45
+ }
46
+ onMatch({ promptText: tail.slice(-200), matchedPattern })
47
+ }
48
+
49
+ function start() { pty.onData(onData) }
50
+ function stop() { stopped = true; if (timer) clearTimeout(timer) }
51
+
52
+ return { start, stop }
53
+ }
@@ -0,0 +1,52 @@
1
+ import { mkdirSync, writeFileSync, readFileSync, readdirSync, unlinkSync, existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { DEFAULT_ROOT_DIR } from './config.js'
4
+
5
+ const DEFAULT_DIR = join(DEFAULT_ROOT_DIR, 'codex-sessions')
6
+
7
+ export function createCodexSidecar({ baseDir = DEFAULT_DIR } = {}) {
8
+ mkdirSync(baseDir, { recursive: true })
9
+ const memory = new Map()
10
+
11
+ function fileFor(nativeId) {
12
+ return join(baseDir, `${nativeId}.json`)
13
+ }
14
+
15
+ function lookup(nativeId) {
16
+ if (!nativeId) return null
17
+ if (memory.has(nativeId)) return memory.get(nativeId)
18
+ const path = fileFor(nativeId)
19
+ if (!existsSync(path)) return null
20
+ try {
21
+ const j = JSON.parse(readFileSync(path, 'utf8'))
22
+ const v = { quadtodoSessionId: j.quadtodoSessionId, todoId: j.todoId, cwd: j.cwd }
23
+ memory.set(nativeId, v)
24
+ return v
25
+ } catch { return null }
26
+ }
27
+
28
+ async function write({ nativeId, quadtodoSessionId, todoId, cwd }) {
29
+ if (!nativeId) throw new Error('nativeId_required')
30
+ memory.set(nativeId, { quadtodoSessionId, todoId, cwd })
31
+ const payload = { nativeId, quadtodoSessionId, todoId, cwd, ts: Date.now() }
32
+ writeFileSync(fileFor(nativeId), JSON.stringify(payload), 'utf8')
33
+ }
34
+
35
+ function restoreFromDisk() {
36
+ if (!existsSync(baseDir)) return
37
+ for (const name of readdirSync(baseDir)) {
38
+ if (!name.endsWith('.json')) continue
39
+ try {
40
+ const j = JSON.parse(readFileSync(join(baseDir, name), 'utf8'))
41
+ if (j.nativeId) memory.set(j.nativeId, { quadtodoSessionId: j.quadtodoSessionId, todoId: j.todoId, cwd: j.cwd })
42
+ } catch {}
43
+ }
44
+ }
45
+
46
+ function clear(nativeId) {
47
+ memory.delete(nativeId)
48
+ try { unlinkSync(fileFor(nativeId)) } catch {}
49
+ }
50
+
51
+ return { write, lookup, restoreFromDisk, clear }
52
+ }
@@ -0,0 +1,74 @@
1
+ import { readFileSync } from 'node:fs'
2
+
3
+ function parseLines(filePath) {
4
+ return readFileSync(filePath, 'utf8').split('\n').filter(l => l.trim())
5
+ }
6
+
7
+ function blockText(content) {
8
+ if (!Array.isArray(content)) return ''
9
+ return content.map(c => c?.text || '').filter(Boolean).join('')
10
+ }
11
+
12
+ export function readLatestCodexTurn(filePath) {
13
+ const lines = parseLines(filePath)
14
+ for (let i = lines.length - 1; i >= 0; i--) {
15
+ try {
16
+ const j = JSON.parse(lines[i])
17
+ if (j.type !== 'response_item') continue
18
+ const p = j.payload
19
+ if (p?.type !== 'message' || p?.role !== 'assistant') continue
20
+ const text = blockText(p.content)
21
+ if (!text) continue
22
+ return { text, raw: p, timestamp: j.timestamp || null }
23
+ } catch {}
24
+ }
25
+ return null
26
+ }
27
+
28
+ export async function readLatestCodexTurnFresh(filePath, lastSeenText, { retries = 3, retryMs = 200 } = {}) {
29
+ for (let i = 0; i <= retries; i++) {
30
+ const turn = readLatestCodexTurn(filePath)
31
+ if (turn && turn.text !== lastSeenText) return turn
32
+ if (i < retries) await new Promise(r => setTimeout(r, retryMs))
33
+ }
34
+ return null
35
+ }
36
+
37
+ export function buildFullCodexTranscript(filePath) {
38
+ const lines = parseLines(filePath)
39
+ const out = []
40
+ let turnCount = 0
41
+ for (const line of lines) {
42
+ let j
43
+ try { j = JSON.parse(line) } catch { continue }
44
+ if (j.type !== 'response_item' || j.payload?.type !== 'message') continue
45
+ const role = j.payload.role
46
+ const text = blockText(j.payload.content)
47
+ if (!text) continue
48
+ if (role === 'assistant') turnCount++
49
+ out.push(`### ${role}\n\n${text}\n`)
50
+ }
51
+ const header = `# Codex Session Transcript\n\n_Generated: ${new Date().toISOString()}_\n_Source: ${filePath}_\n_Turns: ${turnCount}_\n\n---\n\n`
52
+ return { markdown: header + out.join('\n'), turnCount }
53
+ }
54
+
55
+ export function extractCodexTurnUsageFromLines(lines) {
56
+ let last = null
57
+ for (const line of lines) {
58
+ if (!line || !line.trim()) continue
59
+ try {
60
+ const j = JSON.parse(line)
61
+ if (j.type === 'event_msg' && j.payload?.type === 'token_count') {
62
+ const info = j.payload.info
63
+ if (info?.last_token_usage) last = info.last_token_usage
64
+ }
65
+ } catch {}
66
+ }
67
+ if (!last) return null
68
+ return {
69
+ input: Number(last.input_tokens) || 0,
70
+ output: Number(last.output_tokens) || 0,
71
+ cacheRead: Number(last.cached_input_tokens || last.cache_read_input_tokens) || 0,
72
+ cacheCreation: Number(last.cache_creation_input_tokens) || 0,
73
+ }
74
+ }