@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,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Qwen Code session reader.
|
|
3
|
+
*
|
|
4
|
+
* Qwen Code (a Gemini-CLI fork) writes a HYBRID format: a Claude-style JSONL
|
|
5
|
+
* envelope (uuid/parentUuid/type per line) carrying a Gemini-style message body
|
|
6
|
+
* (`message: { role, parts: [{text}|{thought}|{functionCall}|{functionResponse}] }`).
|
|
7
|
+
* Stored at ~/.qwen/projects/<encoded-cwd>/chats/<id>.jsonl. So we parse the
|
|
8
|
+
* envelope ourselves and map the Gemini-style parts (NOT the Claude parser,
|
|
9
|
+
* which expects message.content).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ConversationMessage, MessagePart, ProjectGroup, SessionInfo } from '$lib/types';
|
|
13
|
+
|
|
14
|
+
import * as crypto from 'crypto';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import { homedir } from 'os';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
|
|
19
|
+
const QWEN_PROJECTS = path.join(homedir(), '.qwen', 'projects');
|
|
20
|
+
const PREFIX_BYTES = 64 * 1024;
|
|
21
|
+
/** Cap conversation reads at 16 MB; oversized files are tail-read (matches the Codex reader). */
|
|
22
|
+
const MAX_QWEN_FILE_BYTES = 16 * 1024 * 1024;
|
|
23
|
+
const SYSTEM_TAG_PREFIXES = [
|
|
24
|
+
'<command-name>',
|
|
25
|
+
'<local-command',
|
|
26
|
+
'<system-reminder>',
|
|
27
|
+
'<task-notification>',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
/** Find Qwen sessions whose file changed within `thresholdMs` — i.e. currently or recently active. */
|
|
31
|
+
export function detectActiveQwenSessions(
|
|
32
|
+
thresholdMs: number
|
|
33
|
+
): { cwd: string; id: string; startedAt: number }[] {
|
|
34
|
+
const cutoff = Date.now() - thresholdMs;
|
|
35
|
+
const out: { cwd: string; id: string; startedAt: number }[] = [];
|
|
36
|
+
for (const filePath of collectQwenFiles()) {
|
|
37
|
+
try {
|
|
38
|
+
const stat = fs.statSync(filePath);
|
|
39
|
+
if (stat.mtimeMs < cutoff) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
const meta = readMeta(readPrefix(filePath));
|
|
43
|
+
if (meta) {
|
|
44
|
+
out.push({ cwd: meta.cwd, id: meta.id, startedAt: stat.birthtimeMs });
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// skip
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Return a page of a Qwen session's conversation. With `offset` 0 the most recent
|
|
55
|
+
* `limit` messages are returned, backed up to a user-message boundary so turns
|
|
56
|
+
* aren't clipped; otherwise the `offset`..`offset + limit` slice is returned.
|
|
57
|
+
*/
|
|
58
|
+
export function getQwenConversation(
|
|
59
|
+
sessionId: string,
|
|
60
|
+
offset = 0,
|
|
61
|
+
limit = 200
|
|
62
|
+
): ConversationMessage[] {
|
|
63
|
+
if (!/^[A-Za-z0-9_-]+$/.test(sessionId)) {
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
const filePath = collectQwenFiles().find((p) => path.basename(p) === `${sessionId}.jsonl`);
|
|
67
|
+
if (!filePath) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
try {
|
|
71
|
+
const messages: ConversationMessage[] = [];
|
|
72
|
+
for (const line of readQwenTextBounded(filePath).split('\n')) {
|
|
73
|
+
const trimmed = line.trim();
|
|
74
|
+
if (!trimmed) {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const msg = qwenLineToMessage(JSON.parse(trimmed) as Record<string, unknown>);
|
|
79
|
+
if (msg) {
|
|
80
|
+
messages.push(msg);
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
// skip malformed line
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (offset === 0 && messages.length > limit) {
|
|
87
|
+
let startIdx = messages.length - limit;
|
|
88
|
+
while (startIdx > 0 && messages[startIdx].role !== 'user') {
|
|
89
|
+
startIdx--;
|
|
90
|
+
}
|
|
91
|
+
return messages.slice(startIdx);
|
|
92
|
+
}
|
|
93
|
+
return messages.slice(offset, offset + limit);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('[qwen] Failed to read conversation:', error);
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** List all Qwen sessions grouped by working directory, most-recently-modified first. */
|
|
101
|
+
export function listQwenProjects(): ProjectGroup[] {
|
|
102
|
+
const byCwd = new Map<string, SessionInfo[]>();
|
|
103
|
+
for (const filePath of collectQwenFiles()) {
|
|
104
|
+
let stat: fs.Stats;
|
|
105
|
+
try {
|
|
106
|
+
stat = fs.statSync(filePath);
|
|
107
|
+
} catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
const meta = readMeta(readPrefix(filePath));
|
|
111
|
+
if (!meta) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const session: SessionInfo = {
|
|
115
|
+
created: meta.started || stat.birthtime.toISOString(),
|
|
116
|
+
gitBranch: meta.gitBranch,
|
|
117
|
+
id: meta.id,
|
|
118
|
+
messageCount: 0,
|
|
119
|
+
modified: stat.mtime.toISOString(),
|
|
120
|
+
projectPath: meta.cwd,
|
|
121
|
+
source: 'qwen' as const,
|
|
122
|
+
summary: '',
|
|
123
|
+
title: meta.title || 'Untitled Session',
|
|
124
|
+
};
|
|
125
|
+
const bucket = byCwd.get(meta.cwd);
|
|
126
|
+
if (bucket) {
|
|
127
|
+
bucket.push(session);
|
|
128
|
+
} else {
|
|
129
|
+
byCwd.set(meta.cwd, [session]);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const projects: ProjectGroup[] = [];
|
|
134
|
+
for (const [cwd, sessions] of byCwd) {
|
|
135
|
+
sessions.sort((a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime());
|
|
136
|
+
const segments = cwd.split('/').filter(Boolean);
|
|
137
|
+
projects.push({
|
|
138
|
+
fullPath: cwd,
|
|
139
|
+
id: shortHash(cwd),
|
|
140
|
+
lastModified: sessions[0]?.modified ?? '',
|
|
141
|
+
name: segments.slice(-2).join('/'),
|
|
142
|
+
sessionCount: sessions.length,
|
|
143
|
+
sessions,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return projects.sort(
|
|
147
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** All chats/*.jsonl files under ~/.qwen/projects/<encoded-cwd>/chats/. */
|
|
152
|
+
function collectQwenFiles(): string[] {
|
|
153
|
+
const out: string[] = [];
|
|
154
|
+
let projectDirs: fs.Dirent[];
|
|
155
|
+
try {
|
|
156
|
+
projectDirs = fs.readdirSync(QWEN_PROJECTS, { withFileTypes: true });
|
|
157
|
+
} catch {
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
for (const dir of projectDirs) {
|
|
161
|
+
if (!dir.isDirectory()) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const chatsDir = path.join(QWEN_PROJECTS, dir.name, 'chats');
|
|
165
|
+
try {
|
|
166
|
+
for (const f of fs.readdirSync(chatsDir)) {
|
|
167
|
+
if (f.endsWith('.jsonl')) {
|
|
168
|
+
out.push(path.join(chatsDir, f));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
} catch {
|
|
172
|
+
// no chats dir
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Map a Qwen JSONL line (Claude envelope + Gemini message.parts) to a ConversationMessage. */
|
|
179
|
+
function qwenLineToMessage(entry: Record<string, unknown>): ConversationMessage | null {
|
|
180
|
+
const type = entry.type;
|
|
181
|
+
if (type !== 'user' && type !== 'assistant') {
|
|
182
|
+
return null; // skip system/control records
|
|
183
|
+
}
|
|
184
|
+
const message = (entry.message ?? {}) as { content?: unknown; parts?: unknown };
|
|
185
|
+
const rawParts = Array.isArray(message.parts) ? message.parts : [];
|
|
186
|
+
const parts: MessagePart[] = [];
|
|
187
|
+
for (const raw of rawParts) {
|
|
188
|
+
if (typeof raw !== 'object' || raw === null) {
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const p = raw as Record<string, unknown>;
|
|
192
|
+
if (p.functionCall && typeof p.functionCall === 'object') {
|
|
193
|
+
const fc = p.functionCall as Record<string, unknown>;
|
|
194
|
+
const toolName = typeof fc.name === 'string' ? fc.name : 'tool';
|
|
195
|
+
parts.push({
|
|
196
|
+
id: typeof fc.id === 'string' ? fc.id : toolName,
|
|
197
|
+
input: (fc.args as Record<string, unknown>) ?? {},
|
|
198
|
+
toolName,
|
|
199
|
+
type: 'tool_use',
|
|
200
|
+
});
|
|
201
|
+
} else if (p.thought === true && typeof p.text === 'string') {
|
|
202
|
+
parts.push({ content: p.text, type: 'thinking' });
|
|
203
|
+
} else if (typeof p.text === 'string') {
|
|
204
|
+
parts.push({ content: p.text, type: 'text' });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
// Fallback for any Claude-style string content.
|
|
208
|
+
if (parts.length === 0 && typeof message.content === 'string' && message.content) {
|
|
209
|
+
parts.push({ content: message.content, type: 'text' });
|
|
210
|
+
}
|
|
211
|
+
if (parts.length === 0) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
id: typeof entry.uuid === 'string' ? entry.uuid : `qwen-${type}`,
|
|
216
|
+
parts,
|
|
217
|
+
role: type === 'user' ? 'user' : 'assistant',
|
|
218
|
+
timestamp: typeof entry.timestamp === 'string' ? entry.timestamp : '',
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Extract {cwd, sessionId, gitBranch, started} + first real user prompt from a Qwen session prefix. */
|
|
223
|
+
function readMeta(
|
|
224
|
+
prefix: string
|
|
225
|
+
): null | { cwd: string; gitBranch: string; id: string; started: string; title: string } {
|
|
226
|
+
let cwd = '';
|
|
227
|
+
let id = '';
|
|
228
|
+
let gitBranch = '';
|
|
229
|
+
let started = '';
|
|
230
|
+
let title = '';
|
|
231
|
+
for (const line of prefix.split('\n')) {
|
|
232
|
+
const trimmed = line.trim();
|
|
233
|
+
if (!trimmed) {
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
let entry: Record<string, unknown>;
|
|
237
|
+
try {
|
|
238
|
+
entry = JSON.parse(trimmed) as Record<string, unknown>;
|
|
239
|
+
} catch {
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
if (!cwd && typeof entry.cwd === 'string') {
|
|
243
|
+
cwd = entry.cwd;
|
|
244
|
+
}
|
|
245
|
+
if (!id && typeof entry.sessionId === 'string') {
|
|
246
|
+
id = entry.sessionId;
|
|
247
|
+
}
|
|
248
|
+
if (!gitBranch && typeof entry.gitBranch === 'string') {
|
|
249
|
+
gitBranch = entry.gitBranch;
|
|
250
|
+
}
|
|
251
|
+
if (!started && typeof entry.timestamp === 'string') {
|
|
252
|
+
started = entry.timestamp;
|
|
253
|
+
}
|
|
254
|
+
if (!title && entry.type === 'user') {
|
|
255
|
+
const msg = entry.message as undefined | { content?: unknown; parts?: unknown };
|
|
256
|
+
let text = typeof msg?.content === 'string' ? msg.content : '';
|
|
257
|
+
if (!text && Array.isArray(msg?.parts)) {
|
|
258
|
+
text = msg.parts
|
|
259
|
+
.map((p) =>
|
|
260
|
+
p && typeof p === 'object' && typeof (p as { text?: unknown }).text === 'string'
|
|
261
|
+
? (p as { text: string }).text
|
|
262
|
+
: ''
|
|
263
|
+
)
|
|
264
|
+
.join(' ')
|
|
265
|
+
.trim();
|
|
266
|
+
}
|
|
267
|
+
if (text && !SYSTEM_TAG_PREFIXES.some((p) => text.startsWith(p))) {
|
|
268
|
+
title = text.split('\n')[0]?.slice(0, 80) ?? '';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return id && cwd ? { cwd, gitBranch, id, started, title } : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Read the head of a file (complete lines only). */
|
|
276
|
+
function readPrefix(filePath: string): string {
|
|
277
|
+
const fd = fs.openSync(filePath, 'r');
|
|
278
|
+
try {
|
|
279
|
+
const buf = Buffer.alloc(PREFIX_BYTES);
|
|
280
|
+
const n = fs.readSync(fd, buf, 0, PREFIX_BYTES, 0);
|
|
281
|
+
const text = buf.toString('utf-8', 0, n);
|
|
282
|
+
const lastNl = text.lastIndexOf('\n');
|
|
283
|
+
return lastNl === -1 ? text : text.slice(0, lastNl);
|
|
284
|
+
} finally {
|
|
285
|
+
fs.closeSync(fd);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** Read a Qwen session file, bounded to the tail for oversized files to cap memory. */
|
|
290
|
+
function readQwenTextBounded(filePath: string): string {
|
|
291
|
+
const size = fs.statSync(filePath).size;
|
|
292
|
+
if (size <= MAX_QWEN_FILE_BYTES) {
|
|
293
|
+
return fs.readFileSync(filePath, 'utf-8');
|
|
294
|
+
}
|
|
295
|
+
// Oversized: read only the trailing window (most recent messages), dropping the
|
|
296
|
+
// first (likely partial) line so JSON.parse never sees a fragment.
|
|
297
|
+
const fd = fs.openSync(filePath, 'r');
|
|
298
|
+
try {
|
|
299
|
+
const buf = Buffer.alloc(MAX_QWEN_FILE_BYTES);
|
|
300
|
+
const n = fs.readSync(fd, buf, 0, MAX_QWEN_FILE_BYTES, size - MAX_QWEN_FILE_BYTES);
|
|
301
|
+
const tail = buf.toString('utf-8', 0, n);
|
|
302
|
+
return tail.slice(tail.indexOf('\n') + 1);
|
|
303
|
+
} finally {
|
|
304
|
+
fs.closeSync(fd);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function shortHash(input: string): string {
|
|
309
|
+
return crypto.createHash('sha256').update(input).digest('hex').slice(0, 8);
|
|
310
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider registry — the single place that knows every AI-agent provider.
|
|
3
|
+
*
|
|
4
|
+
* Inspired by wesm/agentsview's AgentDef registry: instead of branching on
|
|
5
|
+
* `command === 'claude'` across ~14 call sites, the session API, the connect
|
|
6
|
+
* route, and the terminal allowlist all derive from this list. Adding a
|
|
7
|
+
* provider is one entry here (+ its reader) rather than edits everywhere.
|
|
8
|
+
*
|
|
9
|
+
* Detection (process-detector) and live watching (server.ts adapter) stay
|
|
10
|
+
* provider-specific because their mechanisms differ too much to unify cheaply.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { ConversationMessage, ProjectGroup, ProviderDef } from '$lib/types';
|
|
14
|
+
|
|
15
|
+
import { getCodexConversation, listCodexProjects } from './codex-reader';
|
|
16
|
+
import { getGeminiConversation, listGeminiProjects } from './gemini-reader';
|
|
17
|
+
import { getSessionConversation, listProjectsWithSessions } from './jsonl-reader';
|
|
18
|
+
import { getOpenCodeConversation, listOpenCodeProjects } from './opencode-reader';
|
|
19
|
+
import { getQwenConversation, listQwenProjects } from './qwen-reader';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Every AI-agent provider, in merge order. `claude-code` MUST stay first: its
|
|
23
|
+
* projects seed the merge map so the canonical (decoded) project path/name wins
|
|
24
|
+
* over other providers' guesses.
|
|
25
|
+
*/
|
|
26
|
+
export const PROVIDERS: ProviderDef[] = [
|
|
27
|
+
{
|
|
28
|
+
command: 'claude',
|
|
29
|
+
getConversation: (id, offset, limit) => getSessionConversation(id, offset, limit),
|
|
30
|
+
isAI: true,
|
|
31
|
+
label: 'Claude Code',
|
|
32
|
+
listProjects: listProjectsWithSessions,
|
|
33
|
+
resumeArgs: (id) => ['--resume', id],
|
|
34
|
+
source: 'claude-code',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
command: 'opencode',
|
|
38
|
+
getConversation: getOpenCodeConversation,
|
|
39
|
+
isAI: true,
|
|
40
|
+
label: 'OpenCode',
|
|
41
|
+
listProjects: listOpenCodeProjects,
|
|
42
|
+
nameSuffix: ' (OpenCode)',
|
|
43
|
+
resumeArgs: (id) => ['--session', id],
|
|
44
|
+
source: 'opencode',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
command: 'codex',
|
|
48
|
+
getConversation: getCodexConversation,
|
|
49
|
+
isAI: true,
|
|
50
|
+
label: 'Codex',
|
|
51
|
+
listProjects: listCodexProjects,
|
|
52
|
+
resumeArgs: (id) => ['resume', id],
|
|
53
|
+
source: 'codex',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
command: 'gemini',
|
|
57
|
+
getConversation: getGeminiConversation,
|
|
58
|
+
isAI: true,
|
|
59
|
+
label: 'Gemini',
|
|
60
|
+
listProjects: listGeminiProjects,
|
|
61
|
+
resumeArgs: () => [], // Gemini CLI has no session-resume flag
|
|
62
|
+
source: 'gemini',
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
command: 'qwen',
|
|
66
|
+
getConversation: getQwenConversation,
|
|
67
|
+
isAI: true,
|
|
68
|
+
label: 'Qwen',
|
|
69
|
+
listProjects: listQwenProjects,
|
|
70
|
+
resumeArgs: () => [],
|
|
71
|
+
source: 'qwen',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
/** AI-agent binary names (for AI_COMMANDS-style checks). */
|
|
76
|
+
export const AI_COMMANDS: string[] = PROVIDERS.filter((p) => p.isAI).map((p) => p.command);
|
|
77
|
+
|
|
78
|
+
/** All provider binary names (for the terminal allowlist + connect validation). */
|
|
79
|
+
export const PROVIDER_COMMANDS: string[] = PROVIDERS.map((p) => p.command);
|
|
80
|
+
|
|
81
|
+
/** Resolve a session's conversation across providers (Claude first, with its project dir). */
|
|
82
|
+
export function getProviderConversation(
|
|
83
|
+
sessionId: string,
|
|
84
|
+
offset: number,
|
|
85
|
+
limit: number,
|
|
86
|
+
claudeProjectDir?: string
|
|
87
|
+
): ConversationMessage[] {
|
|
88
|
+
const claude = getSessionConversation(sessionId, offset, limit, claudeProjectDir);
|
|
89
|
+
if (claude.length > 0) {
|
|
90
|
+
return claude;
|
|
91
|
+
}
|
|
92
|
+
for (const provider of PROVIDERS) {
|
|
93
|
+
if (provider.source === 'claude-code') {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const messages = provider.getConversation(sessionId, offset, limit);
|
|
97
|
+
if (messages.length > 0) {
|
|
98
|
+
return messages;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Merge every provider's projects, deduplicating by absolute path. */
|
|
105
|
+
export function listAllProviderProjects(): ProjectGroup[] {
|
|
106
|
+
const byPath = new Map<string, ProjectGroup>();
|
|
107
|
+
for (const provider of PROVIDERS) {
|
|
108
|
+
let groups: ProjectGroup[];
|
|
109
|
+
try {
|
|
110
|
+
groups = provider.listProjects();
|
|
111
|
+
} catch {
|
|
112
|
+
continue; // a broken provider must not take down the whole listing
|
|
113
|
+
}
|
|
114
|
+
for (const group of groups) {
|
|
115
|
+
const name = provider.nameSuffix ? group.name.replace(provider.nameSuffix, '') : group.name;
|
|
116
|
+
const existing = byPath.get(group.fullPath);
|
|
117
|
+
if (existing) {
|
|
118
|
+
existing.sessions.push(...group.sessions);
|
|
119
|
+
existing.sessions.sort(
|
|
120
|
+
(a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime()
|
|
121
|
+
);
|
|
122
|
+
existing.sessionCount = existing.sessions.length;
|
|
123
|
+
existing.lastModified = existing.sessions[0]?.modified || existing.lastModified;
|
|
124
|
+
} else {
|
|
125
|
+
byPath.set(group.fullPath, { ...group, name });
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return [...byPath.values()].sort(
|
|
130
|
+
(a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime()
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Resume args for a launched command (e.g. `codex resume <id>`). */
|
|
135
|
+
export function resumeArgsForCommand(command: string, sessionId: string): string[] {
|
|
136
|
+
return PROVIDERS.find((p) => p.command === command)?.resumeArgs(sessionId) ?? [];
|
|
137
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex live session watcher.
|
|
3
|
+
*
|
|
4
|
+
* Codex rollout files are append-only JSONL (like Claude's), so this mirrors
|
|
5
|
+
* SessionWatcher: chokidar detects writes, only the newly-appended bytes are
|
|
6
|
+
* read, and lines are fed to a CodexStreamParser that emits messages as each
|
|
7
|
+
* conversation run completes. Because Codex's role-run grouping only flushes a
|
|
8
|
+
* run when the *next* run begins, an idle timer flushes the final open run when
|
|
9
|
+
* writing stops, so the last message isn't withheld.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ConversationMessage, CodexWatchState as WatchState } from '$lib/types';
|
|
13
|
+
|
|
14
|
+
import { watch as chokidarWatch } from 'chokidar';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
|
|
17
|
+
import { CodexStreamParser, parseCodexRollout } from '../sessions/codex-parser';
|
|
18
|
+
import { readBoundedRolloutText } from '../sessions/codex-reader';
|
|
19
|
+
|
|
20
|
+
/** Flush the open run after this many ms without a write. */
|
|
21
|
+
const IDLE_FLUSH_MS = 1500;
|
|
22
|
+
|
|
23
|
+
class CodexWatcher {
|
|
24
|
+
private watched = new Map<string, WatchState>();
|
|
25
|
+
|
|
26
|
+
/** One-shot full-history read of a rollout file (bypasses the live watch). */
|
|
27
|
+
getHistory(filePath: string): ConversationMessage[] {
|
|
28
|
+
if (!fs.existsSync(filePath)) {
|
|
29
|
+
return [];
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
// Bounded read — rollout files can be hundreds of MB; this is called on
|
|
33
|
+
// every WS session connection (mirrors getCodexConversation).
|
|
34
|
+
return parseCodexRollout(readBoundedRolloutText(filePath)).messages;
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`[codex-watcher] Failed to read history for ${filePath}:`, error);
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Detach one `callback` (closing the watcher only when none remain), or stop entirely if no callback is given. */
|
|
42
|
+
stop(filePath: string, callback?: (messages: ConversationMessage[]) => void): void {
|
|
43
|
+
const state = this.watched.get(filePath);
|
|
44
|
+
if (!state) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
if (callback) {
|
|
48
|
+
state.callbacks.delete(callback);
|
|
49
|
+
if (state.callbacks.size > 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (state.idleTimer) {
|
|
54
|
+
clearTimeout(state.idleTimer);
|
|
55
|
+
}
|
|
56
|
+
void state.watcher.close();
|
|
57
|
+
this.watched.delete(filePath);
|
|
58
|
+
console.log(`[codex-watcher] Stopped watching: ${filePath}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Stop watching every file. */
|
|
62
|
+
stopAll(): void {
|
|
63
|
+
for (const [filePath] of this.watched) {
|
|
64
|
+
this.stop(filePath);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Watch a rollout file for appended messages; returns an unsubscribe function. */
|
|
69
|
+
subscribe(filePath: string, callback: (messages: ConversationMessage[]) => void): () => void {
|
|
70
|
+
const existing = this.watched.get(filePath);
|
|
71
|
+
if (existing) {
|
|
72
|
+
existing.callbacks.add(callback);
|
|
73
|
+
return () => {
|
|
74
|
+
this.stop(filePath, callback);
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const watcher = chokidarWatch(filePath, {
|
|
79
|
+
awaitWriteFinish: { pollInterval: 100, stabilityThreshold: 200 },
|
|
80
|
+
ignoreInitial: true,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const state: WatchState = {
|
|
84
|
+
callbacks: new Set([callback]),
|
|
85
|
+
idleTimer: null,
|
|
86
|
+
lineBuffer: '',
|
|
87
|
+
offset: fs.existsSync(filePath) ? fs.statSync(filePath).size : 0,
|
|
88
|
+
parser: new CodexStreamParser(),
|
|
89
|
+
watcher,
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
watcher.on('change', () => {
|
|
93
|
+
this.readNew(filePath);
|
|
94
|
+
});
|
|
95
|
+
watcher.on('add', () => {
|
|
96
|
+
this.readNew(filePath);
|
|
97
|
+
});
|
|
98
|
+
watcher.on('error', (err) => {
|
|
99
|
+
console.error(`[codex-watcher] watch error ${filePath}:`, err);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
this.watched.set(filePath, state);
|
|
103
|
+
console.log(`[codex-watcher] Watching: ${filePath} (offset ${state.offset})`);
|
|
104
|
+
return () => {
|
|
105
|
+
this.stop(filePath, callback);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private emit(state: WatchState, messages: ConversationMessage[]): void {
|
|
110
|
+
if (messages.length === 0) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const cb of state.callbacks) {
|
|
114
|
+
try {
|
|
115
|
+
cb(messages);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
console.error('[codex-watcher] callback error:', err);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
private readNew(filePath: string): void {
|
|
123
|
+
const state = this.watched.get(filePath);
|
|
124
|
+
if (!state) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
let stat: fs.Stats;
|
|
129
|
+
try {
|
|
130
|
+
stat = fs.statSync(filePath);
|
|
131
|
+
} catch {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (stat.size < state.offset) {
|
|
135
|
+
// Truncated/rotated — reset.
|
|
136
|
+
state.offset = 0;
|
|
137
|
+
state.lineBuffer = '';
|
|
138
|
+
state.parser = new CodexStreamParser();
|
|
139
|
+
}
|
|
140
|
+
if (stat.size <= state.offset) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const fd = fs.openSync(filePath, 'r');
|
|
145
|
+
try {
|
|
146
|
+
const buf = Buffer.alloc(stat.size - state.offset);
|
|
147
|
+
fs.readSync(fd, buf, 0, buf.length, state.offset);
|
|
148
|
+
state.offset = stat.size;
|
|
149
|
+
|
|
150
|
+
const combined = state.lineBuffer + buf.toString('utf-8');
|
|
151
|
+
const segments = combined.split('\n');
|
|
152
|
+
state.lineBuffer = combined.endsWith('\n') ? '' : (segments.pop() ?? '');
|
|
153
|
+
|
|
154
|
+
const emitted: ConversationMessage[] = [];
|
|
155
|
+
for (const line of segments) {
|
|
156
|
+
if (line.trim()) {
|
|
157
|
+
emitted.push(...state.parser.pushLine(line));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
this.emit(state, emitted);
|
|
161
|
+
} finally {
|
|
162
|
+
fs.closeSync(fd);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Reset the idle timer; flush the final open run once writes stop.
|
|
166
|
+
if (state.idleTimer) {
|
|
167
|
+
clearTimeout(state.idleTimer);
|
|
168
|
+
}
|
|
169
|
+
state.idleTimer = setTimeout(() => {
|
|
170
|
+
const current = this.watched.get(filePath);
|
|
171
|
+
if (current) {
|
|
172
|
+
this.emit(current, current.parser.flushOpen());
|
|
173
|
+
}
|
|
174
|
+
}, IDLE_FLUSH_MS);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Single shared instance across module loaders (same pattern as the other watchers).
|
|
179
|
+
const CW_GLOBAL_KEY = '__shooter_codex_watcher';
|
|
180
|
+
export const codexWatcher: CodexWatcher =
|
|
181
|
+
((globalThis as Record<string, unknown>)[CW_GLOBAL_KEY] as CodexWatcher) || new CodexWatcher();
|
|
182
|
+
(globalThis as Record<string, unknown>)[CW_GLOBAL_KEY] = codexWatcher;
|
|
@@ -12,6 +12,7 @@ import { existsSync, readdirSync, readFileSync, statSync, unlinkSync } from 'fs'
|
|
|
12
12
|
import path from 'path';
|
|
13
13
|
import { fileURLToPath } from 'url';
|
|
14
14
|
|
|
15
|
+
import { findCodexRolloutForCwd } from '../sessions/codex-reader';
|
|
15
16
|
import { broadcastEvent } from '../ws/server.js';
|
|
16
17
|
import { HolderClient } from './holder-client';
|
|
17
18
|
import { openCodeWatcher } from './opencode-watcher';
|
|
@@ -964,6 +965,46 @@ class PtyManager {
|
|
|
964
965
|
5 * 60 * 1000
|
|
965
966
|
);
|
|
966
967
|
}
|
|
968
|
+
|
|
969
|
+
// For Codex: poll ~/.codex/sessions for the rollout file created after
|
|
970
|
+
// launch whose session_meta.cwd matches. Codex reuses the sessionFile
|
|
971
|
+
// field (a JSONL path, like Claude); the WS adapter routes it to the
|
|
972
|
+
// Codex watcher by its /.codex/ path.
|
|
973
|
+
if (command === 'codex') {
|
|
974
|
+
const launchTime = terminal.createdAt.getTime();
|
|
975
|
+
terminal.pollTimer = setInterval(() => {
|
|
976
|
+
if (terminal.status === 'exited' || terminal.sessionFile) {
|
|
977
|
+
if (terminal.pollTimer) {
|
|
978
|
+
clearInterval(terminal.pollTimer);
|
|
979
|
+
terminal.pollTimer = null;
|
|
980
|
+
}
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
try {
|
|
984
|
+
const rollout = findCodexRolloutForCwd(cwd, launchTime);
|
|
985
|
+
if (rollout) {
|
|
986
|
+
terminal.sessionFile = rollout;
|
|
987
|
+
if (terminal.pollTimer) {
|
|
988
|
+
clearInterval(terminal.pollTimer);
|
|
989
|
+
terminal.pollTimer = null;
|
|
990
|
+
}
|
|
991
|
+
terminalStore.update(id, { sessionFile: rollout });
|
|
992
|
+
}
|
|
993
|
+
} catch {
|
|
994
|
+
// ignore filesystem errors
|
|
995
|
+
}
|
|
996
|
+
}, 1500);
|
|
997
|
+
|
|
998
|
+
setTimeout(
|
|
999
|
+
() => {
|
|
1000
|
+
if (terminal.pollTimer) {
|
|
1001
|
+
clearInterval(terminal.pollTimer);
|
|
1002
|
+
terminal.pollTimer = null;
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
5 * 60 * 1000
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
967
1008
|
}
|
|
968
1009
|
|
|
969
1010
|
/** Wire up all HolderClient callbacks (activity, CWD, output, exit, disconnect). */
|