@juspay/shooter 1.16.0 → 1.18.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.DEfoFaGR.css → 0.NV8k8wxG.css} +1 -1
- package/build/client/_app/immutable/assets/0.NV8k8wxG.css.br +0 -0
- package/build/client/_app/immutable/assets/0.NV8k8wxG.css.gz +0 -0
- package/build/client/_app/immutable/chunks/8lO1IL7u.js +1 -0
- package/build/client/_app/immutable/chunks/8lO1IL7u.js.br +0 -0
- package/build/client/_app/immutable/chunks/8lO1IL7u.js.gz +0 -0
- package/build/client/_app/immutable/chunks/B9WQy_3X.js +1 -0
- package/build/client/_app/immutable/chunks/B9WQy_3X.js.br +0 -0
- package/build/client/_app/immutable/chunks/B9WQy_3X.js.gz +0 -0
- package/build/client/_app/immutable/chunks/BdtLzPpO.js +1 -0
- package/build/client/_app/immutable/chunks/BdtLzPpO.js.br +0 -0
- package/build/client/_app/immutable/chunks/BdtLzPpO.js.gz +0 -0
- package/build/client/_app/immutable/chunks/{DlS3abGJ.js → DJvX78LW.js} +1 -1
- package/build/client/_app/immutable/chunks/DJvX78LW.js.br +0 -0
- package/build/client/_app/immutable/chunks/DJvX78LW.js.gz +0 -0
- package/build/client/_app/immutable/chunks/nWG9RHyB.js +3 -0
- package/build/client/_app/immutable/chunks/nWG9RHyB.js.br +0 -0
- package/build/client/_app/immutable/chunks/nWG9RHyB.js.gz +0 -0
- package/build/client/_app/immutable/entry/{app.CSJG7N9H.js → app.f46Ko1hu.js} +2 -2
- package/build/client/_app/immutable/entry/app.f46Ko1hu.js.br +0 -0
- package/build/client/_app/immutable/entry/app.f46Ko1hu.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.BVDjNnXt.js +1 -0
- package/build/client/_app/immutable/entry/start.BVDjNnXt.js.br +2 -0
- package/build/client/_app/immutable/entry/start.BVDjNnXt.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{0.qOL7xtFn.js → 0.D_9EwVmq.js} +1 -1
- package/build/client/_app/immutable/nodes/0.D_9EwVmq.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.D_9EwVmq.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{1.Di708Ago.js → 1.C4eFlqSB.js} +1 -1
- package/build/client/_app/immutable/nodes/1.C4eFlqSB.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.C4eFlqSB.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{2.DSM1znqa.js → 2.CdC092Za.js} +1 -1
- package/build/client/_app/immutable/nodes/2.CdC092Za.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.CdC092Za.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{3.BPa5fh75.js → 3.Dhf4ZWW0.js} +1 -1
- package/build/client/_app/immutable/nodes/3.Dhf4ZWW0.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.Dhf4ZWW0.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.B3SEB_li.js +1 -0
- package/build/client/_app/immutable/nodes/6.B3SEB_li.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.B3SEB_li.js.gz +0 -0
- package/build/client/_app/immutable/nodes/{7.B7UJd8GQ.js → 7.DV8cJ1lX.js} +3 -3
- package/build/client/_app/immutable/nodes/7.DV8cJ1lX.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.DV8cJ1lX.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.Bs362gyb.js +2 -0
- package/build/client/_app/immutable/nodes/8.Bs362gyb.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.Bs362gyb.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.Cf7_3uqT.js +2 -0
- package/build/client/_app/immutable/nodes/9.Cf7_3uqT.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.Cf7_3uqT.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-D8uPamNd.js → 0-Cd7jY0a7.js} +3 -3
- package/build/server/chunks/{0-D8uPamNd.js.map → 0-Cd7jY0a7.js.map} +1 -1
- package/build/server/chunks/{1-DhtioHbs.js → 1-C4BOGoJY.js} +2 -2
- package/build/server/chunks/{1-DhtioHbs.js.map → 1-C4BOGoJY.js.map} +1 -1
- package/build/server/chunks/{2-Cgh7ZFgE.js → 2-Ba0mNwJ6.js} +2 -2
- package/build/server/chunks/{2-Cgh7ZFgE.js.map → 2-Ba0mNwJ6.js.map} +1 -1
- package/build/server/chunks/{3-I6hnjssH.js → 3-Pg8t1uJU.js} +2 -2
- package/build/server/chunks/{3-I6hnjssH.js.map → 3-Pg8t1uJU.js.map} +1 -1
- package/build/server/chunks/{6-bPDbH1_W.js → 6-D8xbnTSo.js} +2 -2
- package/build/server/chunks/6-D8xbnTSo.js.map +1 -0
- package/build/server/chunks/{7-CpBrOkxQ.js → 7-CkVK06S0.js} +2 -2
- package/build/server/chunks/7-CkVK06S0.js.map +1 -0
- package/build/server/chunks/{8-BRGAVfze.js → 8-C8qVhrds.js} +2 -2
- package/build/server/chunks/8-C8qVhrds.js.map +1 -0
- package/build/server/chunks/{9-C6xuAb_Y.js → 9-fL5zqN0T.js} +2 -2
- package/build/server/chunks/9-fL5zqN0T.js.map +1 -0
- package/build/server/chunks/{_server.ts-BrRZXr-8.js → _server.ts-BA_uWcPw.js} +9 -9
- package/build/server/chunks/_server.ts-BA_uWcPw.js.map +1 -0
- package/build/server/chunks/{_server.ts-C6xbNz6d.js → _server.ts-Bu3s5hfv.js} +3 -3
- package/build/server/chunks/{_server.ts-C6xbNz6d.js.map → _server.ts-Bu3s5hfv.js.map} +1 -1
- package/build/server/chunks/{_server.ts-Cq9_scaV.js → _server.ts-CwAjt91u.js} +18 -18
- package/build/server/chunks/_server.ts-CwAjt91u.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/{_server.ts-Dekgb6Hx.js → _server.ts-DZP2lhaY.js} +3 -3
- package/build/server/chunks/{_server.ts-Dekgb6Hx.js.map → _server.ts-DZP2lhaY.js.map} +1 -1
- package/build/server/chunks/_server.ts-DZgfQKiH.js +81 -0
- package/build/server/chunks/_server.ts-DZgfQKiH.js.map +1 -0
- package/build/server/chunks/{_server.ts-CjK0g9dO.js → _server.ts-MbnroWEF.js} +25 -16
- package/build/server/chunks/_server.ts-MbnroWEF.js.map +1 -0
- package/build/server/chunks/{pty-manager-aFpChJah.js → pty-manager-DmNSCKAr.js} +99 -2
- package/build/server/chunks/pty-manager-DmNSCKAr.js.map +1 -0
- package/build/server/chunks/qwen-reader-DGfUbKaJ.js +2112 -0
- package/build/server/chunks/qwen-reader-DGfUbKaJ.js.map +1 -0
- package/build/server/chunks/{_server.ts-D--_NXt2.js → registry-Kcw2UCMv.js} +132 -106
- package/build/server/chunks/registry-Kcw2UCMv.js.map +1 -0
- package/build/server/index.js +1 -1
- package/build/server/index.js.map +1 -1
- package/build/server/manifest.js +16 -16
- package/build/server/manifest.js.map +1 -1
- package/package.json +2 -2
- package/scripts/e2e-all-features.sh +165 -0
- package/scripts/e2e-cross-terminal.sh +168 -0
- package/server.ts +12 -0
- package/src/lib/modules/client/common/index.ts +1 -1
- package/src/lib/modules/client/common/provider.ts +11 -0
- package/src/lib/modules/client/terminal/ChatView.svelte +9 -2
- package/src/lib/modules/client/terminal/LaunchSheet.svelte +4 -0
- package/src/lib/modules/server/sessions/amp-reader.ts +439 -0
- package/src/lib/modules/server/sessions/codex-reader.ts +34 -33
- package/src/lib/modules/server/sessions/copilot-reader.ts +542 -0
- package/src/lib/modules/server/sessions/cursor-reader.ts +634 -0
- package/src/lib/modules/server/sessions/gemini-reader.ts +594 -0
- package/src/lib/modules/server/sessions/opencode-db-path.ts +19 -10
- package/src/lib/modules/server/sessions/opencode-reader.ts +13 -12
- package/src/lib/modules/server/sessions/process-detector.ts +39 -18
- package/src/lib/modules/server/sessions/provider-paths.ts +173 -0
- package/src/lib/modules/server/sessions/qwen-reader.ts +336 -0
- package/src/lib/modules/server/sessions/registry.ts +178 -0
- package/src/lib/modules/server/terminal/codex-watcher.ts +4 -1
- package/src/lib/modules/server/terminal/generic-session-watcher.ts +163 -0
- package/src/lib/modules/server/terminal/pty-manager.ts +51 -0
- package/src/lib/modules/server/ws/session-handler.ts +34 -20
- package/src/lib/theme.css +32 -0
- package/src/lib/types/gemini.ts +100 -0
- package/src/lib/types/generated/Sessions.ts +17 -1
- package/src/lib/types/index.ts +1 -0
- package/src/lib/types/server.ts +23 -6
- package/src/lib/types/sessions.ts +14 -2
- package/src/routes/api/sessions/+server.ts +5 -52
- package/src/routes/api/sessions/connect/+server.ts +18 -11
- package/src/routes/api/terminals/+server.ts +7 -5
- package/src/routes/terminals/+page.svelte +7 -2
- package/src/routes/terminals/[id]/+page.svelte +1 -2
- package/build/client/_app/immutable/assets/0.DEfoFaGR.css.br +0 -0
- package/build/client/_app/immutable/assets/0.DEfoFaGR.css.gz +0 -0
- package/build/client/_app/immutable/chunks/Bkqjn62J.js +0 -1
- package/build/client/_app/immutable/chunks/Bkqjn62J.js.br +0 -1
- package/build/client/_app/immutable/chunks/Bkqjn62J.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DOHhmtDH.js +0 -3
- package/build/client/_app/immutable/chunks/DOHhmtDH.js.br +0 -0
- package/build/client/_app/immutable/chunks/DOHhmtDH.js.gz +0 -0
- package/build/client/_app/immutable/chunks/DlS3abGJ.js.br +0 -0
- package/build/client/_app/immutable/chunks/DlS3abGJ.js.gz +0 -0
- package/build/client/_app/immutable/chunks/Pw0jDB7M.js +0 -1
- package/build/client/_app/immutable/chunks/Pw0jDB7M.js.br +0 -0
- package/build/client/_app/immutable/chunks/Pw0jDB7M.js.gz +0 -0
- package/build/client/_app/immutable/entry/app.CSJG7N9H.js.br +0 -0
- package/build/client/_app/immutable/entry/app.CSJG7N9H.js.gz +0 -0
- package/build/client/_app/immutable/entry/start.CTt1901T.js +0 -1
- package/build/client/_app/immutable/entry/start.CTt1901T.js.br +0 -2
- package/build/client/_app/immutable/entry/start.CTt1901T.js.gz +0 -0
- package/build/client/_app/immutable/nodes/0.qOL7xtFn.js.br +0 -0
- package/build/client/_app/immutable/nodes/0.qOL7xtFn.js.gz +0 -0
- package/build/client/_app/immutable/nodes/1.Di708Ago.js.br +0 -0
- package/build/client/_app/immutable/nodes/1.Di708Ago.js.gz +0 -0
- package/build/client/_app/immutable/nodes/2.DSM1znqa.js.br +0 -0
- package/build/client/_app/immutable/nodes/2.DSM1znqa.js.gz +0 -0
- package/build/client/_app/immutable/nodes/3.BPa5fh75.js.br +0 -0
- package/build/client/_app/immutable/nodes/3.BPa5fh75.js.gz +0 -0
- package/build/client/_app/immutable/nodes/6.B1LwwEF-.js +0 -1
- package/build/client/_app/immutable/nodes/6.B1LwwEF-.js.br +0 -0
- package/build/client/_app/immutable/nodes/6.B1LwwEF-.js.gz +0 -0
- package/build/client/_app/immutable/nodes/7.B7UJd8GQ.js.br +0 -0
- package/build/client/_app/immutable/nodes/7.B7UJd8GQ.js.gz +0 -0
- package/build/client/_app/immutable/nodes/8.CG0mrgBU.js +0 -2
- package/build/client/_app/immutable/nodes/8.CG0mrgBU.js.br +0 -0
- package/build/client/_app/immutable/nodes/8.CG0mrgBU.js.gz +0 -0
- package/build/client/_app/immutable/nodes/9.KwzWaMHj.js +0 -2
- package/build/client/_app/immutable/nodes/9.KwzWaMHj.js.br +0 -0
- package/build/client/_app/immutable/nodes/9.KwzWaMHj.js.gz +0 -0
- package/build/server/chunks/6-bPDbH1_W.js.map +0 -1
- package/build/server/chunks/7-CpBrOkxQ.js.map +0 -1
- package/build/server/chunks/8-BRGAVfze.js.map +0 -1
- package/build/server/chunks/9-C6xuAb_Y.js.map +0 -1
- package/build/server/chunks/_server.ts-BrRZXr-8.js.map +0 -1
- package/build/server/chunks/_server.ts-CjK0g9dO.js.map +0 -1
- package/build/server/chunks/_server.ts-Cq9_scaV.js.map +0 -1
- package/build/server/chunks/_server.ts-D--_NXt2.js.map +0 -1
- package/build/server/chunks/opencode-db-path-CRgzBK5U.js +0 -402
- package/build/server/chunks/opencode-db-path-CRgzBK5U.js.map +0 -1
- package/build/server/chunks/pty-manager-aFpChJah.js.map +0 -1
|
@@ -170,7 +170,14 @@
|
|
|
170
170
|
}
|
|
171
171
|
|
|
172
172
|
function formatTime(ts: string): string {
|
|
173
|
-
|
|
173
|
+
if (!ts) {
|
|
174
|
+
return '';
|
|
175
|
+
}
|
|
176
|
+
const date = new Date(ts);
|
|
177
|
+
if (Number.isNaN(date.getTime())) {
|
|
178
|
+
return '';
|
|
179
|
+
}
|
|
180
|
+
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
function getToolDescriptionFromPart(part: ToolUsePart): string {
|
|
@@ -366,7 +373,7 @@
|
|
|
366
373
|
role="button"
|
|
367
374
|
tabindex="0"
|
|
368
375
|
>
|
|
369
|
-
💭 Thinking... {isThinkExpanded ? '
|
|
376
|
+
💭 Thinking... {isThinkExpanded ? '▼' : '▶'}
|
|
370
377
|
</div>
|
|
371
378
|
<Accordion expand={isThinkExpanded}>
|
|
372
379
|
{#if isThinkExpanded}
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
{ args: [], command: 'claude', label: 'Claude Code' },
|
|
12
12
|
{ args: [], command: 'codex', label: 'Codex' },
|
|
13
13
|
{ args: [], command: 'gemini', label: 'Gemini' },
|
|
14
|
+
{ args: [], command: 'qwen', label: 'Qwen' },
|
|
15
|
+
{ args: [], command: 'cursor-agent', label: 'Cursor' },
|
|
16
|
+
{ args: [], command: 'copilot', label: 'Copilot' },
|
|
17
|
+
{ args: [], command: 'amp', label: 'Amp' },
|
|
14
18
|
{ args: [], command: 'opencode', label: 'OpenCode' },
|
|
15
19
|
{ args: [], command: 'zsh', label: 'Shell / zsh' },
|
|
16
20
|
{ args: [], command: 'bash', label: 'Bash' },
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amp session reader — listing + conversation retrieval.
|
|
3
|
+
*
|
|
4
|
+
* Amp stores one JSON document per thread at:
|
|
5
|
+
* ~/.local/share/amp/threads/T-<id>.json
|
|
6
|
+
*
|
|
7
|
+
* Document shape:
|
|
8
|
+
* { id, title, created, messages: [{role, content}],
|
|
9
|
+
* meta: { traces: [{endTime}] },
|
|
10
|
+
* env: { initial: { trees: [{displayName}] } } }
|
|
11
|
+
*
|
|
12
|
+
* `content` is either a plain string or an array of Anthropic-style blocks
|
|
13
|
+
* (text / thinking / tool_use / tool_result), so we handle both paths.
|
|
14
|
+
* The file is a single JSON document — skip files larger than 64 MB.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import type { ConversationMessage, MessagePart, ProjectGroup, SessionInfo } from '$lib/types';
|
|
18
|
+
|
|
19
|
+
import * as crypto from 'crypto';
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import { homedir } from 'os';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Constants
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
const AMP_THREADS = path.join(homedir(), '.local', 'share', 'amp', 'threads');
|
|
29
|
+
|
|
30
|
+
/** Single JSON document — skip (do not truncate) if larger than this. */
|
|
31
|
+
const MAX_FILE_BYTES = 64 * 1024 * 1024;
|
|
32
|
+
|
|
33
|
+
/** Pattern for valid thread file names: T-<id>.json */
|
|
34
|
+
const THREAD_FILE_RE = /^T-(.+)\.json$/;
|
|
35
|
+
|
|
36
|
+
/** Only allow these chars in a sessionId used to build a path. */
|
|
37
|
+
const SESSION_ID_RE = /^[A-Za-z0-9_.-]+$/;
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Public API
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Return sessions whose thread file mtime is within `thresholdMs` of now —
|
|
45
|
+
* i.e. sessions that are currently (or very recently) active.
|
|
46
|
+
*/
|
|
47
|
+
export function detectActiveAmpSessions(
|
|
48
|
+
thresholdMs: number
|
|
49
|
+
): { cwd: string; id: string; startedAt: number }[] {
|
|
50
|
+
const cutoff = Date.now() - thresholdMs;
|
|
51
|
+
const out: { cwd: string; id: string; startedAt: number }[] = [];
|
|
52
|
+
for (const filePath of collectThreadFiles()) {
|
|
53
|
+
try {
|
|
54
|
+
const stat = fs.statSync(filePath);
|
|
55
|
+
if (stat.mtimeMs < cutoff) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const doc = readThreadDoc(filePath);
|
|
59
|
+
if (!doc) {
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const id = threadId(filePath, doc);
|
|
63
|
+
const cwd = projectCwd(doc);
|
|
64
|
+
if (id && cwd) {
|
|
65
|
+
out.push({ cwd, id, startedAt: stat.birthtimeMs });
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
// skip unreadable files
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Return the conversation messages for a given Amp thread ID.
|
|
76
|
+
* Mirrors the pagination pattern from codex-reader / qwen-reader.
|
|
77
|
+
*/
|
|
78
|
+
export function getAmpConversation(
|
|
79
|
+
sessionId: string,
|
|
80
|
+
offset = 0,
|
|
81
|
+
limit = 200
|
|
82
|
+
): ConversationMessage[] {
|
|
83
|
+
if (!SESSION_ID_RE.test(sessionId)) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const filePath = path.join(AMP_THREADS, `T-${sessionId}.json`);
|
|
87
|
+
if (!fs.existsSync(filePath)) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
try {
|
|
91
|
+
const doc = readThreadDoc(filePath);
|
|
92
|
+
if (!doc) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
const rawMessages = Array.isArray(doc.messages) ? (doc.messages as unknown[]) : [];
|
|
96
|
+
const messages: ConversationMessage[] = [];
|
|
97
|
+
let idx = 0;
|
|
98
|
+
for (const raw of rawMessages) {
|
|
99
|
+
const msg = ampMessageToConversationMessage(raw, idx);
|
|
100
|
+
if (msg) {
|
|
101
|
+
messages.push(msg);
|
|
102
|
+
idx++;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (offset === 0 && messages.length > limit) {
|
|
106
|
+
let startIdx = messages.length - limit;
|
|
107
|
+
while (startIdx > 0 && messages[startIdx]?.role !== 'user') {
|
|
108
|
+
startIdx--;
|
|
109
|
+
}
|
|
110
|
+
return messages.slice(startIdx);
|
|
111
|
+
}
|
|
112
|
+
return messages.slice(offset, offset + limit);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
console.error('[amp] Failed to read conversation:', err);
|
|
115
|
+
return [];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* List all Amp threads grouped by project (env.initial.trees[0].displayName),
|
|
121
|
+
* sorted by most-recently-modified first.
|
|
122
|
+
*/
|
|
123
|
+
export function listAmpProjects(): ProjectGroup[] {
|
|
124
|
+
const byCwd = new Map<string, SessionInfo[]>();
|
|
125
|
+
for (const filePath of collectThreadFiles()) {
|
|
126
|
+
let stat: fs.Stats;
|
|
127
|
+
try {
|
|
128
|
+
stat = fs.statSync(filePath);
|
|
129
|
+
} catch {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const doc = readThreadDoc(filePath);
|
|
133
|
+
if (!doc) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const id = threadId(filePath, doc);
|
|
137
|
+
if (!id) {
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
const cwd = projectCwd(doc) || id;
|
|
141
|
+
const modified = lastModified(doc, stat);
|
|
142
|
+
const created =
|
|
143
|
+
typeof doc.created === 'string' && doc.created ? doc.created : stat.birthtime.toISOString();
|
|
144
|
+
const rawMessages = Array.isArray(doc.messages) ? (doc.messages as unknown[]) : [];
|
|
145
|
+
const session: SessionInfo = {
|
|
146
|
+
created,
|
|
147
|
+
gitBranch: '',
|
|
148
|
+
id,
|
|
149
|
+
messageCount: rawMessages.length,
|
|
150
|
+
modified,
|
|
151
|
+
projectPath: cwd,
|
|
152
|
+
source: 'amp' as const,
|
|
153
|
+
summary: '',
|
|
154
|
+
title: cleanTitle(typeof doc.title === 'string' ? doc.title : ''),
|
|
155
|
+
};
|
|
156
|
+
const bucket = byCwd.get(cwd);
|
|
157
|
+
if (bucket) {
|
|
158
|
+
bucket.push(session);
|
|
159
|
+
} else {
|
|
160
|
+
byCwd.set(cwd, [session]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const projects: ProjectGroup[] = [];
|
|
165
|
+
for (const [cwd, sessions] of byCwd) {
|
|
166
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
167
|
+
const segments = cwd.split('/').filter(Boolean);
|
|
168
|
+
projects.push({
|
|
169
|
+
fullPath: cwd,
|
|
170
|
+
id: shortHash(cwd),
|
|
171
|
+
lastModified: sessions[0]?.modified ?? '',
|
|
172
|
+
name: segments.slice(-2).join('/') || cwd,
|
|
173
|
+
sessionCount: sessions.length,
|
|
174
|
+
sessions,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return projects.sort(
|
|
178
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Parse the Amp thread JSON file at `filePath` and return the full list of
|
|
184
|
+
* ConversationMessage — no pagination or tail-limit applied.
|
|
185
|
+
* Returns `[]` on any error (missing file, malformed JSON, oversized file).
|
|
186
|
+
*/
|
|
187
|
+
export function parseAmpSessionFile(filePath: string): ConversationMessage[] {
|
|
188
|
+
try {
|
|
189
|
+
const doc = readThreadDoc(filePath);
|
|
190
|
+
if (!doc) {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
const rawMessages = Array.isArray(doc.messages) ? (doc.messages as unknown[]) : [];
|
|
194
|
+
const messages: ConversationMessage[] = [];
|
|
195
|
+
let idx = 0;
|
|
196
|
+
for (const raw of rawMessages) {
|
|
197
|
+
const msg = ampMessageToConversationMessage(raw, idx);
|
|
198
|
+
if (msg) {
|
|
199
|
+
messages.push(msg);
|
|
200
|
+
idx++;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return messages;
|
|
204
|
+
} catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Return the absolute path of the Amp thread file for `sessionId`, or `null`
|
|
211
|
+
* if the session ID is invalid or the file does not exist.
|
|
212
|
+
*/
|
|
213
|
+
export function resolveAmpSessionFile(sessionId: string): null | string {
|
|
214
|
+
if (!SESSION_ID_RE.test(sessionId)) {
|
|
215
|
+
return null;
|
|
216
|
+
}
|
|
217
|
+
const filePath = path.join(AMP_THREADS, `T-${sessionId}.json`);
|
|
218
|
+
return fs.existsSync(filePath) ? filePath : null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// Block → MessagePart mapping (Anthropic-style blocks)
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
/** Map one Anthropic-style content block to a MessagePart, or null to skip. */
|
|
226
|
+
function ampBlockToPart(block: Record<string, unknown>): MessagePart | null {
|
|
227
|
+
switch (block.type) {
|
|
228
|
+
case 'text':
|
|
229
|
+
return { content: typeof block.text === 'string' ? block.text : '', type: 'text' };
|
|
230
|
+
case 'thinking':
|
|
231
|
+
return {
|
|
232
|
+
content: typeof block.thinking === 'string' ? block.thinking : '',
|
|
233
|
+
type: 'thinking',
|
|
234
|
+
};
|
|
235
|
+
case 'tool_result': {
|
|
236
|
+
const output = extractToolResultText(block.content);
|
|
237
|
+
return {
|
|
238
|
+
isError: typeof block.is_error === 'boolean' ? block.is_error : false,
|
|
239
|
+
output: output.slice(0, 2000),
|
|
240
|
+
toolUseId: typeof block.tool_use_id === 'string' ? block.tool_use_id : '',
|
|
241
|
+
type: 'tool_result',
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
case 'tool_use':
|
|
245
|
+
return {
|
|
246
|
+
id: typeof block.id === 'string' ? block.id : '',
|
|
247
|
+
input: isRecord(block.input) ? block.input : {},
|
|
248
|
+
toolName: typeof block.name === 'string' ? block.name : 'tool',
|
|
249
|
+
type: 'tool_use',
|
|
250
|
+
};
|
|
251
|
+
default:
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Map one raw Amp message object to a ConversationMessage, or null to skip. */
|
|
257
|
+
function ampMessageToConversationMessage(raw: unknown, index: number): ConversationMessage | null {
|
|
258
|
+
if (!isRecord(raw)) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
const role = raw.role;
|
|
262
|
+
if (role !== 'user' && role !== 'assistant') {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
const parts = contentToParts(raw.content);
|
|
266
|
+
if (parts.length === 0) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
const msgRole: 'assistant' | 'user' = role === 'user' ? 'user' : 'assistant';
|
|
270
|
+
return {
|
|
271
|
+
id: `amp-${msgRole}-${index}`,
|
|
272
|
+
parts,
|
|
273
|
+
role: msgRole,
|
|
274
|
+
timestamp: '',
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// File I/O helpers
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
function cleanTitle(raw: string): string {
|
|
283
|
+
const first = raw.replace(/\s+/g, ' ').trim().split('\n')[0]?.trim() ?? '';
|
|
284
|
+
if (!first) {
|
|
285
|
+
return 'Untitled Session';
|
|
286
|
+
}
|
|
287
|
+
return first.length > 80 ? `${first.slice(0, 77)}...` : first;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Collect all T-*.json files under the Amp threads directory, sorted by mtime desc. */
|
|
291
|
+
function collectThreadFiles(): string[] {
|
|
292
|
+
let entries: fs.Dirent[];
|
|
293
|
+
try {
|
|
294
|
+
entries = fs.readdirSync(AMP_THREADS, { withFileTypes: true });
|
|
295
|
+
} catch {
|
|
296
|
+
return [];
|
|
297
|
+
}
|
|
298
|
+
const files: { mtime: number; path: string }[] = [];
|
|
299
|
+
for (const entry of entries) {
|
|
300
|
+
if (!entry.isFile() || !THREAD_FILE_RE.test(entry.name)) {
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
const full = path.join(AMP_THREADS, entry.name);
|
|
304
|
+
try {
|
|
305
|
+
const stat = fs.statSync(full);
|
|
306
|
+
files.push({ mtime: stat.mtimeMs, path: full });
|
|
307
|
+
} catch {
|
|
308
|
+
// skip
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
files.sort((a, b) => b.mtime - a.mtime);
|
|
312
|
+
return files.map((f) => f.path);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Map a `content` field (string or block array) to MessagePart[]. */
|
|
316
|
+
function contentToParts(content: unknown): MessagePart[] {
|
|
317
|
+
if (typeof content === 'string' && content) {
|
|
318
|
+
return [{ content, type: 'text' }];
|
|
319
|
+
}
|
|
320
|
+
if (!Array.isArray(content)) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
const parts: MessagePart[] = [];
|
|
324
|
+
for (const item of content as unknown[]) {
|
|
325
|
+
if (!isRecord(item)) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const part = ampBlockToPart(item);
|
|
329
|
+
if (part) {
|
|
330
|
+
parts.push(part);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
return parts;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ---------------------------------------------------------------------------
|
|
337
|
+
// Metadata helpers
|
|
338
|
+
// ---------------------------------------------------------------------------
|
|
339
|
+
|
|
340
|
+
/** Extract plain text from a tool_result content field (string or text-block array). */
|
|
341
|
+
function extractToolResultText(content: unknown): string {
|
|
342
|
+
if (typeof content === 'string') {
|
|
343
|
+
return content;
|
|
344
|
+
}
|
|
345
|
+
if (!Array.isArray(content)) {
|
|
346
|
+
return '';
|
|
347
|
+
}
|
|
348
|
+
return (content as unknown[])
|
|
349
|
+
.filter((c): c is Record<string, unknown> => isRecord(c) && c.type === 'text')
|
|
350
|
+
.map((c) => (typeof c.text === 'string' ? c.text : ''))
|
|
351
|
+
.join('\n');
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Narrow an unknown value to a plain object with string keys. */
|
|
355
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
356
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Derive the modified timestamp: last trace endTime or file mtime.
|
|
361
|
+
* Amp records execution traces in meta.traces[].endTime (ISO string or ms number).
|
|
362
|
+
*/
|
|
363
|
+
function lastModified(doc: Record<string, unknown>, stat: fs.Stats): string {
|
|
364
|
+
if (isRecord(doc.meta)) {
|
|
365
|
+
const traces = doc.meta.traces;
|
|
366
|
+
if (Array.isArray(traces) && traces.length > 0) {
|
|
367
|
+
const last: unknown = traces[traces.length - 1];
|
|
368
|
+
if (isRecord(last)) {
|
|
369
|
+
const et = last.endTime;
|
|
370
|
+
if (typeof et === 'string' && et) {
|
|
371
|
+
return et;
|
|
372
|
+
}
|
|
373
|
+
if (typeof et === 'number' && et > 0) {
|
|
374
|
+
return new Date(et).toISOString();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return stat.mtime.toISOString();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Extract the project cwd/name from env.initial.trees[0].displayName.
|
|
384
|
+
* May be an absolute path or just a display name — surface it as-is.
|
|
385
|
+
*/
|
|
386
|
+
function projectCwd(doc: Record<string, unknown>): string {
|
|
387
|
+
if (isRecord(doc.env) && isRecord(doc.env.initial)) {
|
|
388
|
+
const trees = doc.env.initial.trees;
|
|
389
|
+
if (Array.isArray(trees) && trees.length > 0 && isRecord(trees[0])) {
|
|
390
|
+
const dn = trees[0].displayName;
|
|
391
|
+
if (typeof dn === 'string' && dn) {
|
|
392
|
+
return dn;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return '';
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
/**
|
|
400
|
+
* Read and parse a thread JSON file.
|
|
401
|
+
* Returns null if the file is too large or malformed.
|
|
402
|
+
*/
|
|
403
|
+
function readThreadDoc(filePath: string): null | Record<string, unknown> {
|
|
404
|
+
try {
|
|
405
|
+
if (fs.statSync(filePath).size > MAX_FILE_BYTES) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
409
|
+
return isRecord(parsed) ? parsed : null;
|
|
410
|
+
} catch {
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function shortHash(input: string): string {
|
|
416
|
+
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Derive the canonical session id from the filename stem (the part after
|
|
421
|
+
* "T-" in "T-<id>.json"). This must agree with what getAmpConversation
|
|
422
|
+
* expects: it reconstructs the file path as `T-${sessionId}.json`, so the
|
|
423
|
+
* id returned here must NOT include the leading "T-".
|
|
424
|
+
*
|
|
425
|
+
* The doc's `.id` field is accepted only as a last-resort fallback when the
|
|
426
|
+
* filename doesn't match the expected pattern — using doc.id first was the
|
|
427
|
+
* original bug (doc.id may contain or omit the "T-" prefix differently from
|
|
428
|
+
* the filename, causing a mismatch that produced zero messages on retrieval).
|
|
429
|
+
*/
|
|
430
|
+
function threadId(filePath: string, doc: Record<string, unknown>): string {
|
|
431
|
+
const m = THREAD_FILE_RE.exec(path.basename(filePath));
|
|
432
|
+
if (m?.[1]) {
|
|
433
|
+
return m[1];
|
|
434
|
+
}
|
|
435
|
+
if (typeof doc.id === 'string' && doc.id) {
|
|
436
|
+
return doc.id;
|
|
437
|
+
}
|
|
438
|
+
return '';
|
|
439
|
+
}
|
|
@@ -58,6 +58,15 @@ export function detectActiveCodexSessions(
|
|
|
58
58
|
return out;
|
|
59
59
|
}
|
|
60
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
|
+
|
|
61
70
|
/**
|
|
62
71
|
* Find the rollout file for a Codex session launched in `cwd` after `sinceMs`
|
|
63
72
|
* (used by pty-manager to link a freshly-launched `codex` terminal to its file).
|
|
@@ -92,13 +101,13 @@ export function getCodexConversation(
|
|
|
92
101
|
offset = 0,
|
|
93
102
|
limit = 200
|
|
94
103
|
): ConversationMessage[] {
|
|
95
|
-
const filePath =
|
|
104
|
+
const filePath = findCodexRolloutById(sessionId);
|
|
96
105
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
97
106
|
return [];
|
|
98
107
|
}
|
|
99
108
|
|
|
100
109
|
try {
|
|
101
|
-
const { messages } = parseCodexRollout(
|
|
110
|
+
const { messages } = parseCodexRollout(readBoundedRolloutText(filePath));
|
|
102
111
|
|
|
103
112
|
// Match the Claude reader: with no explicit offset, return the most recent
|
|
104
113
|
// `limit` messages, backing up to a user-message boundary so turns aren't clipped.
|
|
@@ -135,7 +144,8 @@ export function listCodexProjects(): ProjectGroup[] {
|
|
|
135
144
|
continue;
|
|
136
145
|
}
|
|
137
146
|
|
|
138
|
-
const
|
|
147
|
+
const nlIdx = prefix.indexOf('\n');
|
|
148
|
+
const firstLine = nlIdx === -1 ? prefix : prefix.slice(0, nlIdx);
|
|
139
149
|
const meta = parseCodexMeta(firstLine);
|
|
140
150
|
if (!meta) {
|
|
141
151
|
continue;
|
|
@@ -180,6 +190,27 @@ export function listCodexProjects(): ProjectGroup[] {
|
|
|
180
190
|
);
|
|
181
191
|
}
|
|
182
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
|
+
|
|
183
214
|
function cleanTitle(prompt: string): string {
|
|
184
215
|
const firstLine = prompt.replace(/\s+/g, ' ').trim().split('\n')[0]?.trim() ?? '';
|
|
185
216
|
if (!firstLine) {
|
|
@@ -218,15 +249,6 @@ function collectRolloutFiles(): string[] {
|
|
|
218
249
|
return out;
|
|
219
250
|
}
|
|
220
251
|
|
|
221
|
-
/** Locate a rollout file by its session UUID (embedded in the filename and session_meta.id). */
|
|
222
|
-
function findRolloutPath(sessionId: string): null | string {
|
|
223
|
-
if (!/^[0-9a-f-]+$/i.test(sessionId)) {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
const suffix = `-${sessionId}.jsonl`;
|
|
227
|
-
return collectRolloutFiles().find((p) => path.basename(p).endsWith(suffix)) ?? null;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
252
|
/** Pull the first genuine user prompt (skipping Codex's auto-injected wrappers) for a title. */
|
|
231
253
|
function firstUserPrompt(prefixText: string): string {
|
|
232
254
|
for (const line of prefixText.split('\n')) {
|
|
@@ -267,27 +289,6 @@ function readPrefix(filePath: string): string {
|
|
|
267
289
|
}
|
|
268
290
|
}
|
|
269
291
|
|
|
270
|
-
/** Read a rollout file's text, bounded to the tail for oversized files. */
|
|
271
|
-
function readRolloutText(filePath: string): string {
|
|
272
|
-
const stat = fs.statSync(filePath);
|
|
273
|
-
if (stat.size <= MAX_FULL_READ_BYTES) {
|
|
274
|
-
return fs.readFileSync(filePath, 'utf-8');
|
|
275
|
-
}
|
|
276
|
-
// Oversized: keep session_meta (first line) + the tail (most recent messages).
|
|
277
|
-
const head = readPrefix(filePath).split('\n')[0] ?? '';
|
|
278
|
-
const fd = fs.openSync(filePath, 'r');
|
|
279
|
-
try {
|
|
280
|
-
const start = stat.size - MAX_FULL_READ_BYTES;
|
|
281
|
-
const buf = Buffer.alloc(MAX_FULL_READ_BYTES);
|
|
282
|
-
const bytesRead = fs.readSync(fd, buf, 0, MAX_FULL_READ_BYTES, start);
|
|
283
|
-
const tail = buf.toString('utf-8', 0, bytesRead);
|
|
284
|
-
// Drop the first (likely partial) line of the tail.
|
|
285
|
-
return `${head}\n${tail.slice(tail.indexOf('\n') + 1)}`;
|
|
286
|
-
} finally {
|
|
287
|
-
fs.closeSync(fd);
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
292
|
function shortHash(input: string): string {
|
|
292
293
|
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
293
294
|
}
|