@juspay/shooter 1.15.0 → 1.17.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/.claude/hooks/codex-hooks.example.json +75 -0
- package/.claude/hooks/notifier.cjs +158 -8
- package/build/client/_app/immutable/assets/{0.BZLcOr5z.css → 0.B0O0vCnX.css} +1 -1
- package/build/client/_app/immutable/assets/0.B0O0vCnX.css.br +0 -0
- package/build/client/_app/immutable/assets/0.B0O0vCnX.css.gz +0 -0
- package/build/client/_app/immutable/chunks/{X-tVU_3P.js → BctvtE4d.js} +1 -1
- package/build/client/_app/immutable/chunks/BctvtE4d.js.br +0 -0
- package/build/client/_app/immutable/chunks/BctvtE4d.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BxFShcQO.js +1 -0
- package/build/client/_app/immutable/chunks/BxFShcQO.js.br +0 -0
- package/build/client/_app/immutable/chunks/BxFShcQO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{gxvWeAns.js → ByzqAuXw.js} +1 -1
- package/build/client/_app/immutable/chunks/ByzqAuXw.js.br +0 -0
- package/build/client/_app/immutable/chunks/ByzqAuXw.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{pMo6RVvN.js → CjfxuHdN.js} +1 -1
- package/build/client/_app/immutable/chunks/CjfxuHdN.js.br +0 -0
- package/build/client/_app/immutable/chunks/CjfxuHdN.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.B0PrrcUG.js → app.CNaTe-zm.js} +2 -2
- package/build/client/_app/immutable/entry/app.CNaTe-zm.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CNaTe-zm.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.hxYnjcDu.js +1 -0
- package/build/client/_app/immutable/entry/start.hxYnjcDu.js.br +0 -0
- package/build/client/_app/immutable/entry/start.hxYnjcDu.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.D4GLHqPM.js → 0.C3ELOf4c.js} +1 -1
- package/build/client/_app/immutable/nodes/0.C3ELOf4c.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.C3ELOf4c.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.nJde5z5O.js → 1.Fqso94b3.js} +1 -1
- package/build/client/_app/immutable/nodes/1.Fqso94b3.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Fqso94b3.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.CLtsjLeG.js → 2.BusCVJWk.js} +1 -1
- package/build/client/_app/immutable/nodes/2.BusCVJWk.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.BusCVJWk.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.CKTUHtnx.js → 3.DUlpocIc.js} +1 -1
- package/build/client/_app/immutable/nodes/3.DUlpocIc.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.DUlpocIc.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.CG4eKRH0.js +1 -0
- package/build/client/_app/immutable/nodes/6.CG4eKRH0.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.CG4eKRH0.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.DHilxD1o.js +4 -0
- package/build/client/_app/immutable/nodes/7.DHilxD1o.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DHilxD1o.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.BjKgvSie.js +2 -0
- package/build/client/_app/immutable/nodes/8.BjKgvSie.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.BjKgvSie.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.BRT6HOXB.js +2 -0
- package/build/client/_app/immutable/nodes/9.BRT6HOXB.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.BRT6HOXB.js.gz +0 -0
- package/build/client/_app/version.json +1 -1
- package/build/client/_app/version.json.br +0 -0
- package/build/client/_app/version.json.gz +0 -0
- package/build/server/chunks/{0-BUSWGJr9.js → 0-BWFSL107.js} +3 -3
- package/build/server/chunks/{0-BUSWGJr9.js.map → 0-BWFSL107.js.map} +1 -1
- package/build/server/chunks/{1-DjiQE1K0.js → 1-Bw5KlAjL.js} +2 -2
- package/build/server/chunks/{1-DjiQE1K0.js.map → 1-Bw5KlAjL.js.map} +1 -1
- package/build/server/chunks/{2-ThgVrRKa.js → 2-CQ3yYSVK.js} +2 -2
- package/build/server/chunks/{2-ThgVrRKa.js.map → 2-CQ3yYSVK.js.map} +1 -1
- package/build/server/chunks/{3-G5LiDFQ9.js → 3-DZ4H9hPs.js} +2 -2
- package/build/server/chunks/{3-G5LiDFQ9.js.map → 3-DZ4H9hPs.js.map} +1 -1
- package/build/server/chunks/{6--I7fF3Bx.js → 6-BZ0enR6b.js} +2 -2
- package/build/server/chunks/6-BZ0enR6b.js.map +1 -0
- package/build/server/chunks/{7-BwPLVwOR.js → 7-Lg8imTZn.js} +2 -2
- package/build/server/chunks/7-Lg8imTZn.js.map +1 -0
- package/build/server/chunks/{8-BwOMHaoQ.js → 8-DKs4yOL7.js} +2 -2
- package/build/server/chunks/8-DKs4yOL7.js.map +1 -0
- package/build/server/chunks/{9-DkO6aJIB.js → 9-UNmpUWDY.js} +2 -2
- package/build/server/chunks/9-UNmpUWDY.js.map +1 -0
- package/build/server/chunks/{_server.ts-BuYyCrnF.js → _server.ts-5wx4ZppI.js} +4 -3
- package/build/server/chunks/_server.ts-5wx4ZppI.js.map +1 -0
- package/build/server/chunks/{_server.ts-40c_epk8.js → _server.ts-B1z0q6qZ.js} +10 -8
- package/build/server/chunks/_server.ts-B1z0q6qZ.js.map +1 -0
- package/build/server/chunks/{_server.ts-ByPExYfO.js → _server.ts-BLNDdFWC.js} +3 -3
- package/build/server/chunks/_server.ts-BLNDdFWC.js.map +1 -0
- package/build/server/chunks/_server.ts-BMMTS86y.js +82 -0
- package/build/server/chunks/_server.ts-BMMTS86y.js.map +1 -0
- package/build/server/chunks/{_server.ts-CjpQ10xh.js → _server.ts-Bt7EAfjo.js} +50 -2
- package/build/server/chunks/_server.ts-Bt7EAfjo.js.map +1 -0
- package/build/server/chunks/{_server.ts-0Xr2fWaq.js → _server.ts-CKXVBbwb.js} +18 -8
- package/build/server/chunks/_server.ts-CKXVBbwb.js.map +1 -0
- package/build/server/chunks/{_server.ts-2ixC-X3K.js → _server.ts-CgHc1Zpx.js} +4 -3
- package/build/server/chunks/_server.ts-CgHc1Zpx.js.map +1 -0
- package/build/server/chunks/{_server.ts-CFX-S_8q.js → _server.ts-DZ5naqSL.js} +2 -2
- package/build/server/chunks/{_server.ts-CFX-S_8q.js.map → _server.ts-DZ5naqSL.js.map} +1 -1
- package/build/server/chunks/opencode-db-path-BwaPufWf.js +411 -0
- package/build/server/chunks/opencode-db-path-BwaPufWf.js.map +1 -0
- package/build/server/chunks/{pty-manager-TyMUpDA9.js → pty-manager-RmhVe2Ez.js} +35 -2
- package/build/server/chunks/pty-manager-RmhVe2Ez.js.map +1 -0
- package/build/server/chunks/qwen-reader-2fTFuC_D.js +622 -0
- package/build/server/chunks/qwen-reader-2fTFuC_D.js.map +1 -0
- package/build/server/chunks/{_server.ts-CilRds58.js → registry-DzJj2E6I.js} +95 -92
- package/build/server/chunks/registry-DzJj2E6I.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +17 -17
- package/build/server/manifest.js.map +1 -1
- package/package.json +2 -2
- package/server.ts +12 -0
- package/src/lib/modules/client/common/index.ts +1 -0
- package/src/lib/modules/client/common/provider.ts +43 -0
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +3 -0
- package/src/lib/modules/server/sessions/codex-parser.ts +286 -0
- package/src/lib/modules/server/sessions/codex-reader.ts +294 -0
- package/src/lib/modules/server/sessions/gemini-reader.ts +571 -0
- package/src/lib/modules/server/sessions/opencode-db-path.ts +19 -10
- package/src/lib/modules/server/sessions/process-detector.ts +67 -0
- package/src/lib/modules/server/sessions/qwen-reader.ts +310 -0
- package/src/lib/modules/server/sessions/registry.ts +137 -0
- package/src/lib/modules/server/terminal/codex-watcher.ts +182 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +41 -0
- package/src/lib/modules/server/ws/session-handler.ts +23 -19
- package/src/lib/theme.css +54 -1
- package/src/lib/types/codex.ts +21 -0
- package/src/lib/types/gemini.ts +100 -0
- package/src/lib/types/generated/Sessions.ts +24 -1
- package/src/lib/types/index.ts +2 -0
- package/src/lib/types/server.ts +18 -5
- package/src/lib/types/sessions.ts +23 -2
- package/src/routes/api/device-token/+server.ts +7 -3
- package/src/routes/api/sessions/+server.ts +5 -40
- package/src/routes/api/sessions/connect/+server.ts +22 -11
- package/src/routes/api/terminals/+server.ts +7 -5
- package/src/routes/project/+page.svelte +7 -23
- package/src/routes/session/[id]/+page.svelte +3 -3
- package/src/routes/terminals/+page.svelte +7 -2
- package/src/routes/terminals/[id]/+page.svelte +1 -2
- package/build/client/_app/immutable/assets/0.BZLcOr5z.css.br +0 -0
- package/build/client/_app/immutable/assets/0.BZLcOr5z.css.gz +0 -0
- package/build/client/_app/immutable/chunks/X-tVU_3P.js.br +0 -0
- package/build/client/_app/immutable/chunks/X-tVU_3P.js.gz +0 -0
- package/build/client/_app/immutable/chunks/gxvWeAns.js.br +0 -0
- package/build/client/_app/immutable/chunks/gxvWeAns.js.gz +0 -0
- package/build/client/_app/immutable/chunks/pMo6RVvN.js.br +0 -0
- package/build/client/_app/immutable/chunks/pMo6RVvN.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.B0PrrcUG.js.br +0 -0
- package/build/client/_app/immutable/entry/app.B0PrrcUG.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.B1obDjVk.js +0 -1
- package/build/client/_app/immutable/entry/start.B1obDjVk.js.br +0 -0
- package/build/client/_app/immutable/entry/start.B1obDjVk.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.D4GLHqPM.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D4GLHqPM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.nJde5z5O.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.nJde5z5O.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.CLtsjLeG.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CLtsjLeG.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.CKTUHtnx.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.CKTUHtnx.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js +0 -1
- package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.Cgc5iTlM.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.BXKvUopV.js +0 -4
- package/build/client/_app/immutable/nodes/7.BXKvUopV.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.BXKvUopV.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.Df0leW0d.js +0 -2
- package/build/client/_app/immutable/nodes/8.Df0leW0d.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.Df0leW0d.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.C4-N3geF.js +0 -2
- package/build/client/_app/immutable/nodes/9.C4-N3geF.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.C4-N3geF.js.gz +0 -0
- package/build/server/chunks/6--I7fF3Bx.js.map +0 -1
- package/build/server/chunks/7-BwPLVwOR.js.map +0 -1
- package/build/server/chunks/8-BwOMHaoQ.js.map +0 -1
- package/build/server/chunks/9-DkO6aJIB.js.map +0 -1
- package/build/server/chunks/_server.ts-0Xr2fWaq.js.map +0 -1
- package/build/server/chunks/_server.ts-2ixC-X3K.js.map +0 -1
- package/build/server/chunks/_server.ts-40c_epk8.js.map +0 -1
- package/build/server/chunks/_server.ts-BuYyCrnF.js.map +0 -1
- package/build/server/chunks/_server.ts-ByPExYfO.js.map +0 -1
- package/build/server/chunks/_server.ts-CilRds58.js.map +0 -1
- package/build/server/chunks/_server.ts-CjpQ10xh.js.map +0 -1
- package/build/server/chunks/opencode-db-path-DcfhJtJy.js +0 -15
- package/build/server/chunks/opencode-db-path-DcfhJtJy.js.map +0 -1
- package/build/server/chunks/pty-manager-TyMUpDA9.js.map +0 -1
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI session reader — listing + conversation retrieval.
|
|
3
|
+
*
|
|
4
|
+
* Mirrors jsonl-reader.ts (Claude) and opencode-reader.ts (OpenCode): produces
|
|
5
|
+
* the same SessionInfo / ProjectGroup / ConversationMessage shapes so the rest
|
|
6
|
+
* of the app is provider-agnostic.
|
|
7
|
+
*
|
|
8
|
+
* Codex sessions live at ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<uuid>.jsonl
|
|
9
|
+
* (and ~/.codex/archived_sessions/). These files can be very large (hundreds of
|
|
10
|
+
* MB), so listing only reads a bounded prefix of each file for metadata/title
|
|
11
|
+
* and estimates the message count from file size; the conversation reader bounds
|
|
12
|
+
* its read for oversized files.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ConversationMessage, ProjectGroup, SessionInfo } from '$lib/types';
|
|
16
|
+
|
|
17
|
+
import * as crypto from 'crypto';
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import { homedir } from 'os';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
|
|
22
|
+
import { parseCodexMeta, parseCodexRollout } from './codex-parser';
|
|
23
|
+
|
|
24
|
+
/** Bytes read from the head of a rollout file when listing (enough for meta + first prompts). */
|
|
25
|
+
const LIST_PREFIX_BYTES = 256 * 1024;
|
|
26
|
+
/** Above this size, the conversation reader reads only the tail to bound memory. */
|
|
27
|
+
const MAX_FULL_READ_BYTES = 16 * 1024 * 1024;
|
|
28
|
+
/** Rough average bytes per Codex message line, used to estimate message counts cheaply. */
|
|
29
|
+
const APPROX_BYTES_PER_MESSAGE = 3000;
|
|
30
|
+
|
|
31
|
+
/** User-message wrappers that Codex injects automatically — not real prompts. */
|
|
32
|
+
const SYNTHETIC_PROMPT_PREFIXES = ['<environment_context>', '<user_instructions>', '<permissions'];
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Find Codex sessions whose rollout file was written within `thresholdMs` —
|
|
36
|
+
* i.e. sessions that are currently (or very recently) active. Uses filesystem
|
|
37
|
+
* mtime rather than ~/.codex/state_5.sqlite, which is WAL-locked while Codex runs.
|
|
38
|
+
*/
|
|
39
|
+
export function detectActiveCodexSessions(
|
|
40
|
+
thresholdMs: number
|
|
41
|
+
): { cwd: string; id: string; startedAt: number }[] {
|
|
42
|
+
const cutoff = Date.now() - thresholdMs;
|
|
43
|
+
const out: { cwd: string; id: string; startedAt: number }[] = [];
|
|
44
|
+
for (const filePath of collectRolloutFiles()) {
|
|
45
|
+
try {
|
|
46
|
+
const stat = fs.statSync(filePath);
|
|
47
|
+
if (stat.mtimeMs < cutoff) {
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
const meta = parseCodexMeta(readPrefix(filePath).split('\n')[0] ?? '');
|
|
51
|
+
if (meta) {
|
|
52
|
+
out.push({ cwd: meta.cwd, id: meta.id, startedAt: stat.birthtimeMs });
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// skip unreadable files
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Locate a rollout file by its session UUID (embedded in the filename and session_meta.id). */
|
|
62
|
+
export function findCodexRolloutById(sessionId: string): null | string {
|
|
63
|
+
if (!/^[0-9a-f-]+$/i.test(sessionId)) {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
const suffix = `-${sessionId}.jsonl`;
|
|
67
|
+
return collectRolloutFiles().find((p) => path.basename(p).endsWith(suffix)) ?? null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Find the rollout file for a Codex session launched in `cwd` after `sinceMs`
|
|
72
|
+
* (used by pty-manager to link a freshly-launched `codex` terminal to its file).
|
|
73
|
+
* Picks the newest matching file by creation time.
|
|
74
|
+
*/
|
|
75
|
+
export function findCodexRolloutForCwd(cwd: string, sinceMs: number): null | string {
|
|
76
|
+
let best: null | { birthtime: number; path: string } = null;
|
|
77
|
+
for (const filePath of collectRolloutFiles()) {
|
|
78
|
+
try {
|
|
79
|
+
const stat = fs.statSync(filePath);
|
|
80
|
+
if (stat.birthtimeMs <= sinceMs) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const meta = parseCodexMeta(readPrefix(filePath).split('\n')[0] ?? '');
|
|
84
|
+
if (meta?.cwd === cwd && (!best || stat.birthtimeMs > best.birthtime)) {
|
|
85
|
+
best = { birthtime: stat.birthtimeMs, path: filePath };
|
|
86
|
+
}
|
|
87
|
+
} catch {
|
|
88
|
+
// skip unreadable files
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return best?.path ?? null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Return a page of a Codex session's conversation. With `offset` 0 the most
|
|
96
|
+
* recent `limit` messages are returned, backed up to a user-message boundary so
|
|
97
|
+
* turns aren't clipped; otherwise the `offset`..`offset + limit` slice is returned.
|
|
98
|
+
*/
|
|
99
|
+
export function getCodexConversation(
|
|
100
|
+
sessionId: string,
|
|
101
|
+
offset = 0,
|
|
102
|
+
limit = 200
|
|
103
|
+
): ConversationMessage[] {
|
|
104
|
+
const filePath = findCodexRolloutById(sessionId);
|
|
105
|
+
if (!filePath || !fs.existsSync(filePath)) {
|
|
106
|
+
return [];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const { messages } = parseCodexRollout(readBoundedRolloutText(filePath));
|
|
111
|
+
|
|
112
|
+
// Match the Claude reader: with no explicit offset, return the most recent
|
|
113
|
+
// `limit` messages, backing up to a user-message boundary so turns aren't clipped.
|
|
114
|
+
if (offset === 0 && messages.length > limit) {
|
|
115
|
+
let startIdx = messages.length - limit;
|
|
116
|
+
while (startIdx > 0 && messages[startIdx].role !== 'user') {
|
|
117
|
+
startIdx--;
|
|
118
|
+
}
|
|
119
|
+
return messages.slice(startIdx);
|
|
120
|
+
}
|
|
121
|
+
return messages.slice(offset, offset + limit);
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('[codex] Failed to read conversation:', error);
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** List all Codex sessions grouped by working directory, most-recently-modified first. */
|
|
129
|
+
export function listCodexProjects(): ProjectGroup[] {
|
|
130
|
+
const files = collectRolloutFiles();
|
|
131
|
+
const byCwd = new Map<string, SessionInfo[]>();
|
|
132
|
+
|
|
133
|
+
for (const filePath of files) {
|
|
134
|
+
let stat: fs.Stats;
|
|
135
|
+
try {
|
|
136
|
+
stat = fs.statSync(filePath);
|
|
137
|
+
} catch {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
let prefix: string;
|
|
141
|
+
try {
|
|
142
|
+
prefix = readPrefix(filePath);
|
|
143
|
+
} catch {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const nlIdx = prefix.indexOf('\n');
|
|
148
|
+
const firstLine = nlIdx === -1 ? prefix : prefix.slice(0, nlIdx);
|
|
149
|
+
const meta = parseCodexMeta(firstLine);
|
|
150
|
+
if (!meta) {
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const session: SessionInfo = {
|
|
155
|
+
created: meta.startedAt || stat.birthtime.toISOString(),
|
|
156
|
+
gitBranch: '',
|
|
157
|
+
id: meta.id,
|
|
158
|
+
messageCount: Math.max(1, Math.round(stat.size / APPROX_BYTES_PER_MESSAGE)),
|
|
159
|
+
modified: stat.mtime.toISOString(),
|
|
160
|
+
projectPath: meta.cwd,
|
|
161
|
+
source: 'codex' as const,
|
|
162
|
+
summary: '',
|
|
163
|
+
title: cleanTitle(firstUserPrompt(prefix)),
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const bucket = byCwd.get(meta.cwd);
|
|
167
|
+
if (bucket) {
|
|
168
|
+
bucket.push(session);
|
|
169
|
+
} else {
|
|
170
|
+
byCwd.set(meta.cwd, [session]);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const projects: ProjectGroup[] = [];
|
|
175
|
+
for (const [cwd, sessions] of byCwd) {
|
|
176
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
177
|
+
const segments = cwd.split('/').filter(Boolean);
|
|
178
|
+
projects.push({
|
|
179
|
+
fullPath: cwd,
|
|
180
|
+
id: shortHash(cwd),
|
|
181
|
+
lastModified: sessions[0]?.modified ?? '',
|
|
182
|
+
name: segments.slice(-2).join('/'),
|
|
183
|
+
sessionCount: sessions.length,
|
|
184
|
+
sessions,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return projects.sort(
|
|
189
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Read a rollout file's text, bounded to the tail for oversized files (Codex files can be 100s of MB). */
|
|
194
|
+
export function readBoundedRolloutText(filePath: string): string {
|
|
195
|
+
const stat = fs.statSync(filePath);
|
|
196
|
+
if (stat.size <= MAX_FULL_READ_BYTES) {
|
|
197
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
198
|
+
}
|
|
199
|
+
// Oversized: keep session_meta (first line) + the tail (most recent messages).
|
|
200
|
+
const head = readPrefix(filePath).split('\n')[0] ?? '';
|
|
201
|
+
const fd = fs.openSync(filePath, 'r');
|
|
202
|
+
try {
|
|
203
|
+
const start = stat.size - MAX_FULL_READ_BYTES;
|
|
204
|
+
const buf = Buffer.alloc(MAX_FULL_READ_BYTES);
|
|
205
|
+
const bytesRead = fs.readSync(fd, buf, 0, MAX_FULL_READ_BYTES, start);
|
|
206
|
+
const tail = buf.toString('utf-8', 0, bytesRead);
|
|
207
|
+
// Drop the first (likely partial) line of the tail.
|
|
208
|
+
return `${head}\n${tail.slice(tail.indexOf('\n') + 1)}`;
|
|
209
|
+
} finally {
|
|
210
|
+
fs.closeSync(fd);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function cleanTitle(prompt: string): string {
|
|
215
|
+
const firstLine = prompt.replace(/\s+/g, ' ').trim().split('\n')[0]?.trim() ?? '';
|
|
216
|
+
if (!firstLine) {
|
|
217
|
+
return 'Untitled Session';
|
|
218
|
+
}
|
|
219
|
+
return firstLine.length > 80 ? `${firstLine.slice(0, 77)}...` : firstLine;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function codexSessionsDirs(): string[] {
|
|
223
|
+
const home = homedir();
|
|
224
|
+
return [path.join(home, '.codex', 'sessions'), path.join(home, '.codex', 'archived_sessions')];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Recursively collect all rollout-*.jsonl file paths under the Codex session roots. */
|
|
228
|
+
function collectRolloutFiles(): string[] {
|
|
229
|
+
const out: string[] = [];
|
|
230
|
+
const walk = (dir: string): void => {
|
|
231
|
+
let entries: fs.Dirent[];
|
|
232
|
+
try {
|
|
233
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
234
|
+
} catch {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
for (const entry of entries) {
|
|
238
|
+
const full = path.join(dir, entry.name);
|
|
239
|
+
if (entry.isDirectory()) {
|
|
240
|
+
walk(full);
|
|
241
|
+
} else if (entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
|
|
242
|
+
out.push(full);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
for (const root of codexSessionsDirs()) {
|
|
247
|
+
walk(root);
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/** Pull the first genuine user prompt (skipping Codex's auto-injected wrappers) for a title. */
|
|
253
|
+
function firstUserPrompt(prefixText: string): string {
|
|
254
|
+
for (const line of prefixText.split('\n')) {
|
|
255
|
+
const trimmed = line.trim();
|
|
256
|
+
if (!trimmed || !trimmed.includes('"type":"message"') || !trimmed.includes('"role":"user"')) {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const entry = JSON.parse(trimmed) as {
|
|
261
|
+
payload?: { content?: { text?: string; type?: string }[] };
|
|
262
|
+
};
|
|
263
|
+
const text = (entry.payload?.content ?? [])
|
|
264
|
+
.filter((c) => c.type === 'input_text' && typeof c.text === 'string')
|
|
265
|
+
.map((c) => c.text ?? '')
|
|
266
|
+
.join('\n')
|
|
267
|
+
.trim();
|
|
268
|
+
if (text && !SYNTHETIC_PROMPT_PREFIXES.some((p) => text.startsWith(p))) {
|
|
269
|
+
return text;
|
|
270
|
+
}
|
|
271
|
+
} catch {
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return '';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Read the first `LIST_PREFIX_BYTES` of a file as UTF-8 (complete lines only). */
|
|
279
|
+
function readPrefix(filePath: string): string {
|
|
280
|
+
const fd = fs.openSync(filePath, 'r');
|
|
281
|
+
try {
|
|
282
|
+
const buf = Buffer.alloc(LIST_PREFIX_BYTES);
|
|
283
|
+
const bytesRead = fs.readSync(fd, buf, 0, LIST_PREFIX_BYTES, 0);
|
|
284
|
+
const text = buf.toString('utf-8', 0, bytesRead);
|
|
285
|
+
const lastNl = text.lastIndexOf('\n');
|
|
286
|
+
return lastNl === -1 ? text : text.slice(0, lastNl);
|
|
287
|
+
} finally {
|
|
288
|
+
fs.closeSync(fd);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function shortHash(input: string): string {
|
|
293
|
+
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
294
|
+
}
|