noctrace 1.3.0 → 1.4.1
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-plugin/plugin.json +1 -1
- package/dist/client/assets/index-BBxFz4Ap.js +30 -0
- package/dist/client/index.html +1 -1
- package/dist/server/shared/parser.js +16 -3
- package/dist/server/shared/providers/codex.js +418 -0
- package/dist/server/shared/providers/index.js +4 -0
- package/package.json +1 -1
- package/dist/client/assets/index-BGW0xA7n.js +0 -30
package/dist/client/index.html
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
8
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
9
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600;700&display=swap" rel="stylesheet" />
|
|
10
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
+
<script type="module" crossorigin src="/assets/index-BBxFz4Ap.js"></script>
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/index-DlKrxvV-.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
@@ -736,6 +736,14 @@ export function parseJsonlContent(content) {
|
|
|
736
736
|
parentToolUseId: null,
|
|
737
737
|
});
|
|
738
738
|
}
|
|
739
|
+
// Chronological sort: tool rows, turn rows, api-errors, and hook rows are pushed
|
|
740
|
+
// in separate passes above. Without this sort, turn rows cluster at the end of
|
|
741
|
+
// the waterfall instead of interleaving with the tool calls they happened between.
|
|
742
|
+
top.sort((a, b) => {
|
|
743
|
+
if (a.startTime !== b.startTime)
|
|
744
|
+
return a.startTime - b.startTime;
|
|
745
|
+
return (a.sequence ?? 0) - (b.sequence ?? 0);
|
|
746
|
+
});
|
|
739
747
|
return top;
|
|
740
748
|
}
|
|
741
749
|
/**
|
|
@@ -935,10 +943,15 @@ export function parseSubAgentContent(content) {
|
|
|
935
943
|
parentToolUseId: null,
|
|
936
944
|
});
|
|
937
945
|
}
|
|
938
|
-
//
|
|
939
|
-
|
|
946
|
+
// Chronological sort — same rule as parseJsonlContent.
|
|
947
|
+
rows.sort((a, b) => {
|
|
948
|
+
if (a.startTime !== b.startTime)
|
|
949
|
+
return a.startTime - b.startTime;
|
|
950
|
+
return (a.sequence ?? 0) - (b.sequence ?? 0);
|
|
951
|
+
});
|
|
952
|
+
// Compute per-row token delta over the now-sorted rows.
|
|
940
953
|
let prevInput = 0;
|
|
941
|
-
for (const row of
|
|
954
|
+
for (const row of rows) {
|
|
942
955
|
row.tokenDelta = row.inputTokens > 0 ? Math.max(0, row.inputTokens - prevInput) : 0;
|
|
943
956
|
if (row.inputTokens > 0)
|
|
944
957
|
prevInput = row.inputTokens;
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex CLI provider implementation.
|
|
3
|
+
* Parses OpenAI Codex CLI session JSONL rollout files into WaterfallRow[].
|
|
4
|
+
*
|
|
5
|
+
* Session id format (rawSlug): the path from the `sessions/` directory onward.
|
|
6
|
+
* e.g. '2026/04/15/rollout-2026-04-15T09-00-00-abc123.jsonl'
|
|
7
|
+
*
|
|
8
|
+
* Default home: ~/.codex Override: CODEX_HOME env var or codexHome constructor param.
|
|
9
|
+
*
|
|
10
|
+
* Record types handled:
|
|
11
|
+
* SessionMeta, TurnContext, EventMsg (TurnStarted, TurnComplete, TokenCount,
|
|
12
|
+
* ExecCommandEnd), ResponseItem (FunctionCall, FunctionCallOutput, assistant message)
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs/promises';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
import chokidar from 'chokidar';
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Capability descriptor
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
const CODEX_CAPABILITIES = {
|
|
22
|
+
toolCallGranularity: 'full',
|
|
23
|
+
contextTracking: true,
|
|
24
|
+
subAgents: true,
|
|
25
|
+
realtime: true,
|
|
26
|
+
tokenAccounting: 'per-turn',
|
|
27
|
+
};
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Helpers
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
/** Resolve Codex home directory, respecting CODEX_HOME env var. */
|
|
32
|
+
function resolveCodexHome(override) {
|
|
33
|
+
if (override)
|
|
34
|
+
return override;
|
|
35
|
+
return process.env['CODEX_HOME'] ?? path.join(os.homedir(), '.codex');
|
|
36
|
+
}
|
|
37
|
+
/** Read the mtime of a file; returns null on error. */
|
|
38
|
+
async function safeStatMtime(filePath) {
|
|
39
|
+
try {
|
|
40
|
+
const stat = await fs.stat(filePath);
|
|
41
|
+
return stat.mtime;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
/** Convert cwd path to a display form, replacing home directory prefix with ~. */
|
|
48
|
+
function toProjectContext(cwd) {
|
|
49
|
+
if (!cwd)
|
|
50
|
+
return 'unknown';
|
|
51
|
+
const home = os.homedir();
|
|
52
|
+
if (cwd.startsWith(home))
|
|
53
|
+
return '~' + cwd.slice(home.length);
|
|
54
|
+
return cwd;
|
|
55
|
+
}
|
|
56
|
+
/** Parse a single line defensively; returns null on malformed input. */
|
|
57
|
+
function parseLine(line) {
|
|
58
|
+
const t = line.trim();
|
|
59
|
+
if (!t)
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
const parsed = JSON.parse(t);
|
|
63
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed))
|
|
64
|
+
return null;
|
|
65
|
+
return parsed;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
console.warn('[noctrace] codex provider: skipping malformed JSONL line:', t.slice(0, 80));
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Extract a nested value from an event object by key path. */
|
|
73
|
+
function getEventVariant(event, key) {
|
|
74
|
+
const v = event[key];
|
|
75
|
+
if (typeof v !== 'object' || v === null)
|
|
76
|
+
return null;
|
|
77
|
+
return v;
|
|
78
|
+
}
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Core parser
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
/**
|
|
83
|
+
* Parse a Codex rollout JSONL string into WaterfallRow[].
|
|
84
|
+
* Skips malformed lines with console.warn — never throws.
|
|
85
|
+
*/
|
|
86
|
+
export function parseCodexContent(content) {
|
|
87
|
+
const lines = content.split('\n');
|
|
88
|
+
const rows = [];
|
|
89
|
+
// State for pairing and timing
|
|
90
|
+
/** Pending FunctionCall rows awaiting their FunctionCallOutput. */
|
|
91
|
+
const callMap = new Map();
|
|
92
|
+
/** All rows by call_id (including completed ones), for retroactive ExecCommandEnd patching. */
|
|
93
|
+
const rowByCallId = new Map();
|
|
94
|
+
const turnTokens = new Map();
|
|
95
|
+
let latestTurnId = null;
|
|
96
|
+
let latestTurnStartMs = 0;
|
|
97
|
+
let latestTokenData = null;
|
|
98
|
+
let sequence = 0;
|
|
99
|
+
for (const line of lines) {
|
|
100
|
+
const rec = parseLine(line);
|
|
101
|
+
if (!rec)
|
|
102
|
+
continue;
|
|
103
|
+
const recType = rec['type'];
|
|
104
|
+
const timestamp = typeof rec['timestamp'] === 'string'
|
|
105
|
+
? new Date(rec['timestamp']).getTime()
|
|
106
|
+
: Date.now();
|
|
107
|
+
if (recType === 'EventMsg') {
|
|
108
|
+
const event = rec['event'];
|
|
109
|
+
if (typeof event !== 'object' || event === null)
|
|
110
|
+
continue;
|
|
111
|
+
const ev = event;
|
|
112
|
+
const turnStarted = getEventVariant(ev, 'TurnStarted');
|
|
113
|
+
if (turnStarted) {
|
|
114
|
+
latestTurnId = typeof turnStarted['turn_id'] === 'string' ? turnStarted['turn_id'] : null;
|
|
115
|
+
latestTurnStartMs = timestamp;
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const turnComplete = getEventVariant(ev, 'TurnComplete');
|
|
119
|
+
if (turnComplete) {
|
|
120
|
+
latestTurnId = typeof turnComplete['turn_id'] === 'string' ? turnComplete['turn_id'] : latestTurnId;
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
const tokenCount = getEventVariant(ev, 'TokenCount');
|
|
124
|
+
if (tokenCount) {
|
|
125
|
+
const info = tokenCount['info'];
|
|
126
|
+
if (info?.total_token_usage) {
|
|
127
|
+
const td = {
|
|
128
|
+
inputTokens: info.total_token_usage.input_tokens,
|
|
129
|
+
outputTokens: info.total_token_usage.output_tokens,
|
|
130
|
+
cachedInputTokens: info.total_token_usage.cached_input_tokens,
|
|
131
|
+
contextWindow: info.model_context_window,
|
|
132
|
+
};
|
|
133
|
+
latestTokenData = td;
|
|
134
|
+
if (latestTurnId)
|
|
135
|
+
turnTokens.set(latestTurnId, td);
|
|
136
|
+
}
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
const execEnd = getEventVariant(ev, 'ExecCommandEnd');
|
|
140
|
+
if (execEnd) {
|
|
141
|
+
const execData = execEnd;
|
|
142
|
+
const isFailure = execData.timed_out === true || execData.exit_code !== 0;
|
|
143
|
+
if (isFailure && execData.call_id) {
|
|
144
|
+
// Patch the row retroactively (ExecCommandEnd may arrive after FunctionCallOutput)
|
|
145
|
+
const target = rowByCallId.get(execData.call_id) ?? callMap.get(execData.call_id)?.row;
|
|
146
|
+
if (target) {
|
|
147
|
+
target.isFailure = true;
|
|
148
|
+
target.status = 'error';
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (recType === 'ResponseItem') {
|
|
156
|
+
const item = rec['item'];
|
|
157
|
+
if (typeof item !== 'object' || item === null)
|
|
158
|
+
continue;
|
|
159
|
+
const it = item;
|
|
160
|
+
// FunctionCallOutput: pairs with a pending FunctionCall
|
|
161
|
+
if (typeof it['call_id'] === 'string' && typeof it['output'] === 'string' && !it['name'] && !it['role']) {
|
|
162
|
+
const output = it;
|
|
163
|
+
const pending = callMap.get(output.call_id);
|
|
164
|
+
if (pending) {
|
|
165
|
+
const { row } = pending;
|
|
166
|
+
row.endTime = timestamp;
|
|
167
|
+
row.duration = timestamp - row.startTime;
|
|
168
|
+
row.output = output.output;
|
|
169
|
+
row.status = 'success';
|
|
170
|
+
callMap.delete(output.call_id);
|
|
171
|
+
// Keep in rowByCallId so ExecCommandEnd can patch retroactively
|
|
172
|
+
rowByCallId.set(output.call_id, row);
|
|
173
|
+
}
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
// FunctionCall: name + arguments + call_id
|
|
177
|
+
if (typeof it['name'] === 'string' && typeof it['call_id'] === 'string' && typeof it['arguments'] === 'string') {
|
|
178
|
+
const call = it;
|
|
179
|
+
let parsedArgs = {};
|
|
180
|
+
try {
|
|
181
|
+
const a = JSON.parse(call.arguments);
|
|
182
|
+
if (typeof a === 'object' && a !== null && !Array.isArray(a)) {
|
|
183
|
+
parsedArgs = a;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
catch { /* leave empty */ }
|
|
187
|
+
const tokens = latestTurnId ? (turnTokens.get(latestTurnId) ?? latestTokenData) : latestTokenData;
|
|
188
|
+
const contextWindow = tokens?.contextWindow ?? 128000;
|
|
189
|
+
const fillPct = tokens ? (tokens.inputTokens / contextWindow) * 100 : 0;
|
|
190
|
+
const toolLabel = buildLabel(call.name, parsedArgs);
|
|
191
|
+
const row = {
|
|
192
|
+
id: call.call_id,
|
|
193
|
+
type: 'tool',
|
|
194
|
+
toolName: call.name === 'shell' ? 'Bash' : call.name,
|
|
195
|
+
label: toolLabel,
|
|
196
|
+
startTime: timestamp,
|
|
197
|
+
endTime: null,
|
|
198
|
+
duration: null,
|
|
199
|
+
status: 'running',
|
|
200
|
+
parentAgentId: null,
|
|
201
|
+
input: parsedArgs,
|
|
202
|
+
output: null,
|
|
203
|
+
inputTokens: tokens?.inputTokens ?? 0,
|
|
204
|
+
outputTokens: tokens?.outputTokens ?? 0,
|
|
205
|
+
tokenDelta: 0,
|
|
206
|
+
contextFillPercent: fillPct,
|
|
207
|
+
isReread: false,
|
|
208
|
+
isFailure: false,
|
|
209
|
+
children: [],
|
|
210
|
+
tips: [],
|
|
211
|
+
modelName: null,
|
|
212
|
+
estimatedCost: null,
|
|
213
|
+
agentType: null,
|
|
214
|
+
agentColor: null,
|
|
215
|
+
sequence: sequence++,
|
|
216
|
+
isFastMode: false,
|
|
217
|
+
parentToolUseId: null,
|
|
218
|
+
};
|
|
219
|
+
rows.push(row);
|
|
220
|
+
callMap.set(call.call_id, { row, timestamp });
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Any tool calls still in callMap have no matching output (session truncated / running)
|
|
226
|
+
// Leave them as status: 'running'
|
|
227
|
+
return rows;
|
|
228
|
+
}
|
|
229
|
+
/** Build a human-readable label for a tool call. */
|
|
230
|
+
function buildLabel(toolName, args) {
|
|
231
|
+
const displayName = toolName === 'shell' ? 'Bash' : toolName;
|
|
232
|
+
if (toolName === 'shell') {
|
|
233
|
+
const cmd = typeof args['command'] === 'string' ? args['command'] : '';
|
|
234
|
+
const short = cmd.length > 60 ? cmd.slice(0, 57) + '...' : cmd;
|
|
235
|
+
return `Bash: ${short}`;
|
|
236
|
+
}
|
|
237
|
+
const first = Object.values(args)[0];
|
|
238
|
+
if (typeof first === 'string') {
|
|
239
|
+
const short = first.length > 60 ? first.slice(0, 57) + '...' : first;
|
|
240
|
+
return `${displayName}: ${short}`;
|
|
241
|
+
}
|
|
242
|
+
return displayName;
|
|
243
|
+
}
|
|
244
|
+
/** Extract the session-level metadata record from parsed lines. */
|
|
245
|
+
function extractCodexSessionMeta(lines) {
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
const rec = parseLine(line);
|
|
248
|
+
if (rec?.['type'] === 'SessionMeta')
|
|
249
|
+
return rec;
|
|
250
|
+
}
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
// ---------------------------------------------------------------------------
|
|
254
|
+
// listSessions helpers
|
|
255
|
+
// ---------------------------------------------------------------------------
|
|
256
|
+
/** Recursively collect all .jsonl rollout files under a directory. */
|
|
257
|
+
async function collectRolloutFiles(dir) {
|
|
258
|
+
const results = [];
|
|
259
|
+
let names;
|
|
260
|
+
try {
|
|
261
|
+
names = await fs.readdir(dir);
|
|
262
|
+
}
|
|
263
|
+
catch {
|
|
264
|
+
return results;
|
|
265
|
+
}
|
|
266
|
+
for (const name of names) {
|
|
267
|
+
const full = path.join(dir, name);
|
|
268
|
+
let stat;
|
|
269
|
+
try {
|
|
270
|
+
stat = await fs.stat(full);
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
if (stat.isDirectory()) {
|
|
276
|
+
const sub = await collectRolloutFiles(full);
|
|
277
|
+
results.push(...sub);
|
|
278
|
+
}
|
|
279
|
+
else if (stat.isFile() && name.endsWith('.jsonl') && name.startsWith('rollout-')) {
|
|
280
|
+
results.push(full);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return results;
|
|
284
|
+
}
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Provider factory
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
/**
|
|
289
|
+
* Create a Codex CLI provider instance.
|
|
290
|
+
*
|
|
291
|
+
* @param codexHome - Override path to the Codex home directory.
|
|
292
|
+
* Defaults to CODEX_HOME env var or ~/.codex.
|
|
293
|
+
*/
|
|
294
|
+
export function createCodexProvider(codexHome) {
|
|
295
|
+
const home = resolveCodexHome(codexHome);
|
|
296
|
+
const sessionsDir = path.join(home, 'sessions');
|
|
297
|
+
return {
|
|
298
|
+
id: 'codex',
|
|
299
|
+
displayName: 'Codex CLI',
|
|
300
|
+
capabilities: CODEX_CAPABILITIES,
|
|
301
|
+
async listSessions(window) {
|
|
302
|
+
const results = [];
|
|
303
|
+
const files = await collectRolloutFiles(sessionsDir);
|
|
304
|
+
for (const filePath of files) {
|
|
305
|
+
const mtime = await safeStatMtime(filePath);
|
|
306
|
+
if (!mtime)
|
|
307
|
+
continue;
|
|
308
|
+
const mtimeMs = mtime.getTime();
|
|
309
|
+
if (mtimeMs < window.startMs || mtimeMs >= window.endMs)
|
|
310
|
+
continue;
|
|
311
|
+
// rawSlug: path relative to sessionsDir
|
|
312
|
+
const rawSlug = path.relative(sessionsDir, filePath).split(path.sep).join('/');
|
|
313
|
+
// Read first 2048 bytes to get SessionMeta without loading full file
|
|
314
|
+
let metaRecord = null;
|
|
315
|
+
try {
|
|
316
|
+
const fh = await fs.open(filePath, 'r');
|
|
317
|
+
try {
|
|
318
|
+
const buf = Buffer.alloc(2048);
|
|
319
|
+
const { bytesRead } = await fh.read(buf, 0, 2048, 0);
|
|
320
|
+
const chunk = buf.slice(0, bytesRead).toString('utf8');
|
|
321
|
+
metaRecord = extractCodexSessionMeta(chunk.split('\n'));
|
|
322
|
+
}
|
|
323
|
+
finally {
|
|
324
|
+
await fh.close();
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
catch { /* leave metaRecord as null */ }
|
|
328
|
+
const startMs = metaRecord?.timestamp
|
|
329
|
+
? new Date(metaRecord.timestamp).getTime()
|
|
330
|
+
: mtimeMs;
|
|
331
|
+
const cwd = metaRecord?.cwd ?? null;
|
|
332
|
+
const sessionId = metaRecord?.id ?? rawSlug;
|
|
333
|
+
const forkedFrom = metaRecord?.forked_from_id ?? null;
|
|
334
|
+
const meta = {
|
|
335
|
+
provider: 'codex',
|
|
336
|
+
sessionId,
|
|
337
|
+
projectContext: toProjectContext(cwd),
|
|
338
|
+
rawSlug,
|
|
339
|
+
startMs,
|
|
340
|
+
endMs: mtimeMs,
|
|
341
|
+
...(metaRecord?.model_provider ? { modelHint: metaRecord.model_provider } : {}),
|
|
342
|
+
...(forkedFrom ? { parentSessionId: forkedFrom } : {}),
|
|
343
|
+
};
|
|
344
|
+
results.push(meta);
|
|
345
|
+
}
|
|
346
|
+
return results;
|
|
347
|
+
},
|
|
348
|
+
async readSession(id) {
|
|
349
|
+
// id is the rawSlug: relative path from sessions/, e.g. '2026/04/15/rollout-....jsonl'
|
|
350
|
+
const filePath = path.join(sessionsDir, ...id.split('/'));
|
|
351
|
+
let content;
|
|
352
|
+
try {
|
|
353
|
+
content = await fs.readFile(filePath, 'utf8');
|
|
354
|
+
}
|
|
355
|
+
catch {
|
|
356
|
+
throw new Error(`Codex session not found: ${id}`);
|
|
357
|
+
}
|
|
358
|
+
const lines = content.split('\n');
|
|
359
|
+
const metaRecord = extractCodexSessionMeta(lines);
|
|
360
|
+
const mtime = await safeStatMtime(filePath);
|
|
361
|
+
const startMs = metaRecord?.timestamp
|
|
362
|
+
? new Date(metaRecord.timestamp).getTime()
|
|
363
|
+
: (mtime?.getTime() ?? Date.now());
|
|
364
|
+
const cwd = metaRecord?.cwd ?? null;
|
|
365
|
+
const sessionId = metaRecord?.id ?? id;
|
|
366
|
+
const forkedFrom = metaRecord?.forked_from_id ?? null;
|
|
367
|
+
const meta = {
|
|
368
|
+
provider: 'codex',
|
|
369
|
+
sessionId,
|
|
370
|
+
projectContext: toProjectContext(cwd),
|
|
371
|
+
rawSlug: id,
|
|
372
|
+
startMs,
|
|
373
|
+
endMs: mtime?.getTime() ?? null,
|
|
374
|
+
...(metaRecord?.model_provider ? { modelHint: metaRecord.model_provider } : {}),
|
|
375
|
+
...(forkedFrom ? { parentSessionId: forkedFrom } : {}),
|
|
376
|
+
};
|
|
377
|
+
const rows = parseCodexContent(content);
|
|
378
|
+
return { meta, native: rows };
|
|
379
|
+
},
|
|
380
|
+
watch(onEvent) {
|
|
381
|
+
let watcher = null;
|
|
382
|
+
try {
|
|
383
|
+
watcher = chokidar.watch(sessionsDir, {
|
|
384
|
+
persistent: true,
|
|
385
|
+
ignoreInitial: true,
|
|
386
|
+
depth: 4,
|
|
387
|
+
});
|
|
388
|
+
watcher.on('add', (filePath) => {
|
|
389
|
+
if (!filePath.endsWith('.jsonl'))
|
|
390
|
+
return;
|
|
391
|
+
const rawSlug = path.relative(sessionsDir, filePath).split(path.sep).join('/');
|
|
392
|
+
onEvent({ kind: 'session-added', provider: 'codex', sessionId: rawSlug });
|
|
393
|
+
});
|
|
394
|
+
watcher.on('change', (filePath) => {
|
|
395
|
+
if (!filePath.endsWith('.jsonl'))
|
|
396
|
+
return;
|
|
397
|
+
const rawSlug = path.relative(sessionsDir, filePath).split(path.sep).join('/');
|
|
398
|
+
onEvent({ kind: 'session-updated', provider: 'codex', sessionId: rawSlug });
|
|
399
|
+
});
|
|
400
|
+
watcher.on('unlink', (filePath) => {
|
|
401
|
+
if (!filePath.endsWith('.jsonl'))
|
|
402
|
+
return;
|
|
403
|
+
const rawSlug = path.relative(sessionsDir, filePath).split(path.sep).join('/');
|
|
404
|
+
onEvent({ kind: 'session-removed', provider: 'codex', sessionId: rawSlug });
|
|
405
|
+
});
|
|
406
|
+
watcher.on('error', (err) => {
|
|
407
|
+
console.warn('[noctrace] codex provider watcher error:', err instanceof Error ? err.message : String(err));
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
catch (err) {
|
|
411
|
+
console.warn('[noctrace] codex provider: could not start watcher:', err instanceof Error ? err.message : String(err));
|
|
412
|
+
}
|
|
413
|
+
return () => {
|
|
414
|
+
watcher?.close().catch(() => { });
|
|
415
|
+
};
|
|
416
|
+
},
|
|
417
|
+
};
|
|
418
|
+
}
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Additional providers (Codex, Copilot, etc.) will be registered in Phase B/C.
|
|
5
5
|
*/
|
|
6
6
|
import { createClaudeCodeProvider } from './claude-code.js';
|
|
7
|
+
import { createCodexProvider } from './codex.js';
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Registry
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -35,3 +36,6 @@ export function listProviders() {
|
|
|
35
36
|
// Register the Claude Code provider with default settings.
|
|
36
37
|
// The claudeHome path is resolved from CLAUDE_HOME env var or ~/.claude.
|
|
37
38
|
registerProvider(createClaudeCodeProvider());
|
|
39
|
+
// Register the Codex CLI provider.
|
|
40
|
+
// The codexHome path is resolved from CODEX_HOME env var or ~/.codex.
|
|
41
|
+
registerProvider(createCodexProvider());
|
package/package.json
CHANGED