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.
- package/LICENSE +21 -0
- package/README.md +318 -0
- package/dist-web/assets/index-CMaXwixo.js +1234 -0
- package/dist-web/assets/index-DBHApzV1.css +32 -0
- package/dist-web/assets/inter-cyrillic-400-normal-HOLc17fK.woff +0 -0
- package/dist-web/assets/inter-cyrillic-400-normal-obahsSVq.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-BasfLYem.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-500-normal-CxZf_p3X.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-4D_pXhcN.woff +0 -0
- package/dist-web/assets/inter-cyrillic-600-normal-CWCymEST.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-CjBOestx.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-700-normal-DrXBdSj3.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-BQZuk6qB.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-400-normal-DQukG94-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-B0yAr1jD.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-500-normal-BmqWE9Dz.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Bcila6Z-.woff +0 -0
- package/dist-web/assets/inter-cyrillic-ext-600-normal-Dfes3d0z.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-BjwYoWNd.woff2 +0 -0
- package/dist-web/assets/inter-cyrillic-ext-700-normal-LO58E6JB.woff +0 -0
- package/dist-web/assets/inter-greek-400-normal-B4URO6DV.woff2 +0 -0
- package/dist-web/assets/inter-greek-400-normal-q2sYcFCs.woff +0 -0
- package/dist-web/assets/inter-greek-500-normal-BIZE56-Y.woff2 +0 -0
- package/dist-web/assets/inter-greek-500-normal-Xzm54t5V.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-BZpKdvQh.woff +0 -0
- package/dist-web/assets/inter-greek-600-normal-plRanbMR.woff2 +0 -0
- package/dist-web/assets/inter-greek-700-normal-BUv2fZ6O.woff +0 -0
- package/dist-web/assets/inter-greek-700-normal-C3JjAnD8.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-DGGRlc-M.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-400-normal-KugGGMne.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-2j5mBUwD.woff +0 -0
- package/dist-web/assets/inter-greek-ext-500-normal-C4iEst2y.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-B8X0CLgF.woff +0 -0
- package/dist-web/assets/inter-greek-ext-600-normal-DRtmH8MT.woff2 +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-BoQ6DsYi.woff +0 -0
- package/dist-web/assets/inter-greek-ext-700-normal-qfdV9bQt.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-C38fXH4l.woff2 +0 -0
- package/dist-web/assets/inter-latin-400-normal-CyCys3Eg.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-BL9OpVg8.woff +0 -0
- package/dist-web/assets/inter-latin-500-normal-Cerq10X2.woff2 +0 -0
- package/dist-web/assets/inter-latin-600-normal-CiBQ2DWP.woff +0 -0
- package/dist-web/assets/inter-latin-600-normal-LgqL8muc.woff2 +0 -0
- package/dist-web/assets/inter-latin-700-normal-BLAVimhd.woff +0 -0
- package/dist-web/assets/inter-latin-700-normal-Yt3aPRUw.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-77YHD8bZ.woff +0 -0
- package/dist-web/assets/inter-latin-ext-400-normal-C1nco2VV.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-BxGbmqWO.woff +0 -0
- package/dist-web/assets/inter-latin-ext-500-normal-CV4jyFjo.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-CIVaiw4L.woff +0 -0
- package/dist-web/assets/inter-latin-ext-600-normal-D2bJ5OIk.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-Ca8adRJv.woff2 +0 -0
- package/dist-web/assets/inter-latin-ext-700-normal-TidjK2hL.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-Bbgyi5SW.woff +0 -0
- package/dist-web/assets/inter-vietnamese-400-normal-DMkecbls.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-DOriooB6.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-500-normal-mJboJaSs.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-BuLX-rYi.woff +0 -0
- package/dist-web/assets/inter-vietnamese-600-normal-Cc8MFFhd.woff2 +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-BZaoP0fm.woff +0 -0
- package/dist-web/assets/inter-vietnamese-700-normal-DlLaEgI2.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist-web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist-web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist-web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist-web/assets/logo-D4DDtU-r.png +0 -0
- package/dist-web/favicon.png +0 -0
- package/dist-web/index.html +14 -0
- package/package.json +88 -0
- package/src/ask-user-buttons.js +142 -0
- package/src/claude-transcript.js +203 -0
- package/src/cli.js +1040 -0
- package/src/codex-event-emitter.js +111 -0
- package/src/codex-prompt-detector.js +53 -0
- package/src/codex-sidecar.js +52 -0
- package/src/codex-transcript.js +74 -0
- package/src/config.js +692 -0
- package/src/data/claude-code-commands.json +52 -0
- package/src/db.js +1503 -0
- package/src/dispatch.js +13 -0
- package/src/export/todoMarkdown.js +246 -0
- package/src/first-run-wizard.js +82 -0
- package/src/git/gitStatus.js +139 -0
- package/src/lark-api-client.js +205 -0
- package/src/lark-bot.js +510 -0
- package/src/lark-card.js +88 -0
- package/src/lark-config-service.js +16 -0
- package/src/lark-event-client.js +107 -0
- package/src/lark-image.js +99 -0
- package/src/lark-markdown.js +51 -0
- package/src/lark-video.js +163 -0
- package/src/mcp/audit.js +34 -0
- package/src/mcp/server.js +83 -0
- package/src/mcp/tools/destructive/index.js +252 -0
- package/src/mcp/tools/openclaw/index.js +405 -0
- package/src/mcp/tools/read/index.js +269 -0
- package/src/mcp/tools/write/index.js +157 -0
- package/src/openclaw-bridge.js +566 -0
- package/src/openclaw-hook-installer.js +338 -0
- package/src/openclaw-hook.js +908 -0
- package/src/openclaw-wizard.js +2442 -0
- package/src/pending-questions.js +297 -0
- package/src/pricing.js +45 -0
- package/src/prompt-render.js +36 -0
- package/src/pty.js +992 -0
- package/src/routes/ai-terminal.js +1228 -0
- package/src/routes/git.js +89 -0
- package/src/routes/openclaw-hook.js +67 -0
- package/src/routes/openclaw-inbound.js +36 -0
- package/src/routes/recurringRules.js +80 -0
- package/src/routes/reports.js +50 -0
- package/src/routes/search.js +46 -0
- package/src/routes/stats.js +31 -0
- package/src/routes/telegram-config.js +152 -0
- package/src/routes/telegram-sync.js +221 -0
- package/src/routes/templates.js +63 -0
- package/src/routes/todos.js +649 -0
- package/src/routes/transcripts.js +75 -0
- package/src/routes/uploads.js +107 -0
- package/src/routes/wiki.js +142 -0
- package/src/search/fts.js +209 -0
- package/src/search/index.js +199 -0
- package/src/search/transcripts.js +148 -0
- package/src/server.js +1791 -0
- package/src/session-input-dispatcher.js +256 -0
- package/src/stats/markdown.js +42 -0
- package/src/stats/report.js +207 -0
- package/src/summarize.js +84 -0
- package/src/system-rules.js +52 -0
- package/src/telegram-bot.js +875 -0
- package/src/telegram-commands.js +149 -0
- package/src/telegram-config-service.js +84 -0
- package/src/telegram-image.js +95 -0
- package/src/telegram-loading-status.js +112 -0
- package/src/telegram-markdown.js +82 -0
- package/src/telegram-reaction-tracker.js +69 -0
- package/src/telegram-video.js +75 -0
- package/src/templates/claude-hooks/notify.js +103 -0
- package/src/transcript.js +305 -0
- package/src/transcripts/blocks.js +56 -0
- package/src/transcripts/index.js +222 -0
- package/src/transcripts/indexer.js +34 -0
- package/src/transcripts/matcher.js +70 -0
- package/src/transcripts/scanner.js +259 -0
- package/src/usage-footer.js +170 -0
- package/src/usage-parser.js +132 -0
- package/src/wiki/guide.js +44 -0
- package/src/wiki/index.js +232 -0
- package/src/wiki/redact.js +34 -0
- 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
|
+
}
|