@code-insights/cli 3.3.1 → 3.4.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.
Files changed (46) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +1 -1
  3. package/dashboard-dist/assets/{index-Rr1JlICa.js → index-BdoBoNtI.js} +154 -139
  4. package/dashboard-dist/assets/index-QzYeMRSf.css +1 -0
  5. package/dashboard-dist/index.html +2 -2
  6. package/dist/parser/titles.d.ts.map +1 -1
  7. package/dist/parser/titles.js +55 -7
  8. package/dist/parser/titles.js.map +1 -1
  9. package/dist/providers/codex.d.ts +5 -1
  10. package/dist/providers/codex.d.ts.map +1 -1
  11. package/dist/providers/codex.js +489 -258
  12. package/dist/providers/codex.js.map +1 -1
  13. package/dist/providers/copilot-cli.d.ts.map +1 -1
  14. package/dist/providers/copilot-cli.js +64 -6
  15. package/dist/providers/copilot-cli.js.map +1 -1
  16. package/dist/providers/copilot.d.ts.map +1 -1
  17. package/dist/providers/copilot.js +23 -0
  18. package/dist/providers/copilot.js.map +1 -1
  19. package/dist/providers/cursor.js +206 -22
  20. package/dist/providers/cursor.js.map +1 -1
  21. package/dist/types.d.ts +28 -4
  22. package/dist/types.d.ts.map +1 -1
  23. package/dist/utils/telemetry.d.ts.map +1 -1
  24. package/dist/utils/telemetry.js +33 -1
  25. package/dist/utils/telemetry.js.map +1 -1
  26. package/package.json +1 -1
  27. package/server-dist/llm/analysis.d.ts +1 -0
  28. package/server-dist/llm/analysis.d.ts.map +1 -1
  29. package/server-dist/llm/analysis.js +35 -9
  30. package/server-dist/llm/analysis.js.map +1 -1
  31. package/server-dist/llm/prompts.d.ts +31 -7
  32. package/server-dist/llm/prompts.d.ts.map +1 -1
  33. package/server-dist/llm/prompts.js +161 -46
  34. package/server-dist/llm/prompts.js.map +1 -1
  35. package/server-dist/llm/providers/anthropic.js +1 -1
  36. package/server-dist/llm/providers/gemini.js +1 -1
  37. package/server-dist/llm/providers/openai.d.ts.map +1 -1
  38. package/server-dist/llm/providers/openai.js +1 -0
  39. package/server-dist/llm/providers/openai.js.map +1 -1
  40. package/server-dist/routes/analysis.d.ts.map +1 -1
  41. package/server-dist/routes/analysis.js +4 -0
  42. package/server-dist/routes/analysis.js.map +1 -1
  43. package/server-dist/routes/analytics.d.ts.map +1 -1
  44. package/server-dist/routes/analytics.js +10 -0
  45. package/server-dist/routes/analytics.js.map +1 -1
  46. package/dashboard-dist/assets/index-BMhL7wL8.css +0 -1
@@ -4,7 +4,11 @@ import * as os from 'os';
4
4
  import { generateTitle, detectSessionCharacter } from '../parser/titles.js';
5
5
  /**
6
6
  * OpenAI Codex CLI session provider.
7
- * Discovers and parses rollout JSONL files from ~/.codex/sessions/
7
+ * Discovers and parses rollout files from ~/.codex/sessions/
8
+ *
9
+ * Supports two formats:
10
+ * Format A: JSONL (v0.104.0+, 2026) — envelope/payload structure with response_item/event_msg
11
+ * Format B: Single JSON object (pre-2025) — bare items array with no envelope
8
12
  */
9
13
  export class CodexProvider {
10
14
  getProviderName() {
@@ -44,17 +48,21 @@ function getCodexHome() {
44
48
  return fs.existsSync(defaultDir) ? defaultDir : null;
45
49
  }
46
50
  /**
47
- * Recursively collect rollout-*.jsonl files from date-organized directories.
48
- * Structure: sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl
51
+ * Recursively collect rollout-*.jsonl and rollout-*.json files from date-organized directories.
52
+ * Format A lives at: sessions/YYYY/MM/DD/rollout-<timestamp>-<uuid>.jsonl
53
+ * Format B lives at: sessions/rollout-<date>-<uuid>.json (flat)
49
54
  */
50
- function collectRolloutFiles(dir, files) {
55
+ function collectRolloutFiles(dir, files, depth = 0) {
56
+ if (depth > 10)
57
+ return; // Guard against symlink loops
51
58
  const entries = fs.readdirSync(dir, { withFileTypes: true });
52
59
  for (const entry of entries) {
53
60
  const fullPath = path.join(dir, entry.name);
54
61
  if (entry.isDirectory()) {
55
- collectRolloutFiles(fullPath, files);
62
+ collectRolloutFiles(fullPath, files, depth + 1);
56
63
  }
57
- else if (entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
64
+ else if ((entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) ||
65
+ (entry.name.startsWith('rollout-') && entry.name.endsWith('.json'))) {
58
66
  files.push(fullPath);
59
67
  }
60
68
  }
@@ -73,8 +81,8 @@ function filterByProject(files, projectFilter) {
73
81
  fs.closeSync(fd);
74
82
  const firstLine = buf.toString('utf-8', 0, bytesRead).split('\n')[0];
75
83
  const meta = JSON.parse(firstLine);
76
- // session_meta has cwd field
77
- const cwd = meta.cwd || meta.payload?.cwd || '';
84
+ // session_meta has cwd field (Format A); Format B has session.cwd
85
+ const cwd = meta.cwd || meta.payload?.cwd || meta.session?.cwd || '';
78
86
  if (cwd.toLowerCase().includes(lowerFilter)) {
79
87
  filtered.push(filePath);
80
88
  }
@@ -87,266 +95,450 @@ function filterByProject(files, projectFilter) {
87
95
  return filtered;
88
96
  }
89
97
  // ---------------------------------------------------------------------------
90
- // Parser
98
+ // Parser entry point
91
99
  // ---------------------------------------------------------------------------
92
100
  function parseCodexSession(filePath) {
93
101
  try {
94
- const content = fs.readFileSync(filePath, 'utf-8');
95
- const lines = content.split('\n').filter(line => line.trim());
96
- if (lines.length === 0)
102
+ const content = fs.readFileSync(filePath, 'utf-8').trim();
103
+ if (!content)
97
104
  return null;
98
- // Parse first linesession metadata
99
- const meta = parseSessionMeta(lines[0]);
100
- if (!meta)
101
- return null;
102
- const sessionId = `codex:${meta.id}`;
103
- // Parse remaining lines — events
104
- const messages = [];
105
- const usageEntries = [];
106
- let model = meta.model || '';
107
- let lastTimestamp = new Date(meta.timestamp);
108
- // Accumulator for current assistant turn
109
- let currentAssistantText = '';
110
- let currentToolCalls = [];
111
- let currentToolResults = [];
112
- let currentThinking = null;
113
- let turnUsage = null;
114
- let toolCounter = 0;
115
- function flushAssistantTurn() {
116
- const text = currentAssistantText.trim();
117
- if (!text && currentToolCalls.length === 0)
118
- return;
119
- const msgUsage = turnUsage ? {
120
- inputTokens: turnUsage.input_tokens || 0,
121
- outputTokens: turnUsage.output_tokens || 0,
122
- cacheCreationTokens: 0,
123
- cacheReadTokens: turnUsage.cached_input_tokens || 0,
124
- model: model || 'unknown',
125
- estimatedCostUsd: 0,
126
- } : null;
127
- messages.push({
128
- id: `codex-assistant-${messages.length}`,
129
- sessionId: sessionId,
130
- type: 'assistant',
131
- content: text.slice(0, 10000),
132
- thinking: currentThinking,
133
- toolCalls: [...currentToolCalls],
134
- toolResults: [...currentToolResults],
135
- usage: msgUsage,
136
- timestamp: lastTimestamp,
137
- parentId: null,
138
- });
139
- // Reset accumulators
140
- currentAssistantText = '';
141
- currentToolCalls = [];
142
- currentToolResults = [];
143
- currentThinking = null;
144
- turnUsage = null;
105
+ // Detect format by file extension content-sniffing is unreliable because
106
+ // JSONL files also start with '{' on line 1 (the session_meta object).
107
+ if (filePath.endsWith('.json')) {
108
+ return parseFormatB(content);
145
109
  }
146
- for (let i = 1; i < lines.length; i++) {
147
- const line = lines[i];
148
- let event;
149
- try {
150
- event = JSON.parse(line);
151
- }
152
- catch {
153
- continue;
154
- }
155
- // Unwrap RolloutLine envelope if present
156
- const eventType = event.type;
157
- const payload = (event.payload || event);
158
- // Handle different event types
159
- const innerType = payload.type || eventType;
160
- switch (innerType) {
161
- case 'message': {
162
- // response_item events: payload.type = "message", payload.role = "user"|"assistant"|"developer"
163
- const role = payload.role;
164
- if (role === 'user') {
165
- flushAssistantTurn();
166
- const userContent = extractUserContent(payload);
167
- if (userContent) {
168
- messages.push({
169
- id: payload.id || `codex-user-${messages.length}`,
170
- sessionId: sessionId,
171
- type: 'user',
172
- content: userContent.slice(0, 10000),
173
- thinking: null,
174
- toolCalls: [],
175
- toolResults: [],
176
- usage: null,
177
- timestamp: parseTimestamp(payload) || lastTimestamp,
178
- parentId: null,
179
- });
180
- lastTimestamp = messages[messages.length - 1].timestamp;
181
- }
182
- }
183
- else if (role === 'assistant') {
184
- const assistantContent = extractUserContent(payload);
185
- if (assistantContent) {
186
- currentAssistantText += assistantContent + '\n';
187
- }
188
- }
189
- // role === 'developer' = system/context messages — skip
110
+ return parseFormatA(content);
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ // ---------------------------------------------------------------------------
117
+ // Format A parser (v0.104.0+ JSONL with envelope/payload structure)
118
+ // ---------------------------------------------------------------------------
119
+ function parseFormatA(content) {
120
+ const lines = content.split('\n').filter(line => line.trim());
121
+ if (lines.length === 0)
122
+ return null;
123
+ // Parse first line session metadata
124
+ const meta = parseSessionMeta(lines[0]);
125
+ if (!meta)
126
+ return null;
127
+ const sessionId = `codex:${meta.id}`;
128
+ const messages = [];
129
+ const usageEntries = [];
130
+ let model = meta.model || '';
131
+ let lastTimestamp = new Date(meta.timestamp);
132
+ // Accumulator for current assistant turn
133
+ let currentAssistantText = '';
134
+ let currentToolCalls = [];
135
+ let currentToolResults = [];
136
+ let currentThinking = null;
137
+ let turnUsage = null;
138
+ let toolCounter = 0;
139
+ function flushAssistantTurn() {
140
+ const text = currentAssistantText.trim();
141
+ if (!text && currentToolCalls.length === 0)
142
+ return;
143
+ const msgUsage = turnUsage ? {
144
+ inputTokens: turnUsage.input_tokens || 0,
145
+ outputTokens: turnUsage.output_tokens || 0,
146
+ cacheCreationTokens: 0,
147
+ cacheReadTokens: turnUsage.cached_input_tokens || 0,
148
+ model: model || 'unknown',
149
+ estimatedCostUsd: 0,
150
+ } : null;
151
+ messages.push({
152
+ id: `codex-assistant-${messages.length}`,
153
+ sessionId: sessionId,
154
+ type: 'assistant',
155
+ content: text.slice(0, 10000),
156
+ thinking: currentThinking,
157
+ toolCalls: [...currentToolCalls],
158
+ toolResults: [...currentToolResults],
159
+ usage: msgUsage,
160
+ timestamp: lastTimestamp,
161
+ parentId: null,
162
+ });
163
+ // Reset accumulators
164
+ currentAssistantText = '';
165
+ currentToolCalls = [];
166
+ currentToolResults = [];
167
+ currentThinking = null;
168
+ turnUsage = null;
169
+ }
170
+ for (let i = 1; i < lines.length; i++) {
171
+ const line = lines[i];
172
+ let event;
173
+ try {
174
+ event = JSON.parse(line);
175
+ }
176
+ catch {
177
+ continue;
178
+ }
179
+ // Unwrap RolloutLine envelope if present
180
+ const eventType = event.type;
181
+ const payload = (event.payload || event);
182
+ // innerType is the meaningful event discriminant after unwrapping the envelope
183
+ const innerType = payload.type || eventType;
184
+ switch (innerType) {
185
+ case 'message': {
186
+ // response_item envelope: payload.type = "message", payload.role = "user"|"assistant"|"developer"
187
+ const role = payload.role;
188
+ if (role === 'developer') {
189
+ // System prompts, permissions, collaboration mode — not real user messages
190
190
  break;
191
191
  }
192
- case 'user_message':
193
- case 'userMessage': {
194
- // Flush any pending assistant turn
195
- flushAssistantTurn();
196
- const userContent = extractUserContent(payload);
197
- if (userContent) {
198
- messages.push({
199
- id: payload.id || `codex-user-${messages.length}`,
200
- sessionId: sessionId,
201
- type: 'user',
202
- content: userContent.slice(0, 10000),
203
- thinking: null,
204
- toolCalls: [],
205
- toolResults: [],
206
- usage: null,
207
- timestamp: parseTimestamp(payload) || lastTimestamp,
208
- parentId: null,
209
- });
210
- lastTimestamp = messages[messages.length - 1].timestamp;
192
+ if (role === 'assistant') {
193
+ // response_item assistant message: content has output_text items
194
+ const assistantContent = extractContent(payload);
195
+ if (assistantContent) {
196
+ currentAssistantText += assistantContent + '\n';
211
197
  }
212
- break;
198
+ lastTimestamp = parseEnvelopeTimestamp(event) || lastTimestamp;
213
199
  }
214
- case 'agent_message':
215
- case 'agentMessage':
216
- case 'item.completed': {
217
- const item = (payload.item || payload);
218
- const itemType = item.type || innerType;
219
- if (itemType === 'agent_message' || itemType === 'agentMessage') {
220
- currentAssistantText += (item.text || payload.text || '') + '\n';
221
- }
222
- else if (itemType === 'command_execution' || itemType === 'commandExecution') {
223
- toolCounter++;
224
- currentToolCalls.push({
225
- id: item.id || `codex-tool-${toolCounter}`,
226
- name: 'shell',
227
- input: { command: item.command || '', cwd: item.cwd || '' },
228
- });
229
- if (item.aggregatedOutput) {
230
- currentToolResults.push({
231
- toolUseId: item.id || `codex-tool-${toolCounter}`,
232
- output: (item.aggregatedOutput || '').slice(0, 1000),
233
- });
234
- }
235
- }
236
- else if (itemType === 'file_change' || itemType === 'fileChange') {
237
- if (item.changes) {
238
- for (const change of item.changes) {
239
- toolCounter++;
240
- currentToolCalls.push({
241
- id: `codex-file-${toolCounter}`,
242
- name: 'apply_patch',
243
- input: { path: change.path, kind: change.kind },
244
- });
245
- if (change.diff) {
246
- currentToolResults.push({
247
- toolUseId: `codex-file-${toolCounter}`,
248
- output: change.diff.slice(0, 1000),
249
- });
250
- }
251
- }
252
- }
200
+ // Skip role === 'user' — handled by event_msg/user_message case.
201
+ // Both response_item/message(role=user) and event_msg/user_message fire for
202
+ // every user prompt, so only capturing from one source avoids doubling the count.
203
+ break;
204
+ }
205
+ case 'user_message':
206
+ case 'userMessage': {
207
+ // event_msg/user_message: the real user prompt (payload.message = text string)
208
+ flushAssistantTurn();
209
+ // event_msg user_message stores the text directly in payload.message
210
+ const msgText = payload.message || '';
211
+ if (msgText) {
212
+ messages.push({
213
+ id: payload.id || `codex-user-${messages.length}`,
214
+ sessionId: sessionId,
215
+ type: 'user',
216
+ content: msgText.slice(0, 10000),
217
+ thinking: null,
218
+ toolCalls: [],
219
+ toolResults: [],
220
+ usage: null,
221
+ timestamp: parseEnvelopeTimestamp(event) || lastTimestamp,
222
+ parentId: null,
223
+ });
224
+ lastTimestamp = messages[messages.length - 1].timestamp;
225
+ }
226
+ break;
227
+ }
228
+ case 'agent_message': {
229
+ // event_msg/agent_message fires alongside response_item/message(role=assistant).
230
+ // Text is already captured via that handler — only update timestamp here to
231
+ // avoid duplicating assistant content.
232
+ lastTimestamp = parseEnvelopeTimestamp(event) || lastTimestamp;
233
+ break;
234
+ }
235
+ case 'function_call': {
236
+ // response_item/function_call: tool invocation (exec_command, etc.)
237
+ // payload: { type, name, arguments (JSON string), call_id, status? }
238
+ toolCounter++;
239
+ const callId = payload.call_id || `codex-tool-${toolCounter}`;
240
+ let args = {};
241
+ try {
242
+ args = JSON.parse(payload.arguments);
243
+ }
244
+ catch {
245
+ // arguments not valid JSON — store raw
246
+ args = { raw: payload.arguments };
247
+ }
248
+ currentToolCalls.push({
249
+ id: callId,
250
+ name: payload.name || 'unknown',
251
+ input: args,
252
+ });
253
+ lastTimestamp = parseEnvelopeTimestamp(event) || lastTimestamp;
254
+ break;
255
+ }
256
+ case 'function_call_output': {
257
+ // response_item/function_call_output: tool result
258
+ // payload: { type, call_id, output (string) }
259
+ const fcoCallId = payload.call_id;
260
+ const output = (payload.output || '').slice(0, 1000);
261
+ if (fcoCallId) {
262
+ currentToolResults.push({ toolUseId: fcoCallId, output });
263
+ }
264
+ break;
265
+ }
266
+ case 'custom_tool_call': {
267
+ // response_item/custom_tool_call: apply_patch and similar custom tools
268
+ // payload: { type, name, call_id, input (string), status? }
269
+ toolCounter++;
270
+ const ctcCallId = payload.call_id || `codex-custom-${toolCounter}`;
271
+ currentToolCalls.push({
272
+ id: ctcCallId,
273
+ name: payload.name || 'custom_tool',
274
+ input: { raw: (payload.input || '').slice(0, 2000) },
275
+ });
276
+ lastTimestamp = parseEnvelopeTimestamp(event) || lastTimestamp;
277
+ break;
278
+ }
279
+ case 'custom_tool_call_output': {
280
+ // response_item/custom_tool_call_output: apply_patch result (output is JSON string)
281
+ // payload: { type, call_id, output (JSON string with nested .output field) }
282
+ const ctcoCallId = payload.call_id;
283
+ let ctcoOutput = payload.output || '';
284
+ try {
285
+ // output field is often {"output":"...","metadata":{...}} — unwrap it
286
+ const parsed = JSON.parse(ctcoOutput);
287
+ if (typeof parsed.output === 'string') {
288
+ ctcoOutput = parsed.output;
253
289
  }
254
- else if (itemType === 'mcp_tool_call' || itemType === 'mcpToolCall') {
255
- toolCounter++;
256
- currentToolCalls.push({
257
- id: item.id || `codex-mcp-${toolCounter}`,
258
- name: item.tool || 'mcp_tool',
259
- input: item.arguments || {},
260
- });
261
- if (item.result) {
262
- currentToolResults.push({
263
- toolUseId: item.id || `codex-mcp-${toolCounter}`,
264
- output: (item.result || '').slice(0, 1000),
265
- });
266
- }
290
+ }
291
+ catch {
292
+ // not JSON — use raw
293
+ }
294
+ if (ctcoCallId) {
295
+ currentToolResults.push({ toolUseId: ctcoCallId, output: ctcoOutput.slice(0, 1000) });
296
+ }
297
+ break;
298
+ }
299
+ case 'reasoning': {
300
+ // response_item/reasoning: model's internal reasoning summary
301
+ // payload.summary is an array of { type: "summary_text", text: "..." }
302
+ const summary = payload.summary;
303
+ if (Array.isArray(summary)) {
304
+ const reasoningText = summary
305
+ .filter(s => s.type === 'summary_text')
306
+ .map(s => s.text)
307
+ .join('\n');
308
+ if (reasoningText)
309
+ currentThinking = (currentThinking || '') + reasoningText + '\n';
310
+ }
311
+ break;
312
+ }
313
+ case 'agent_reasoning': {
314
+ // event_msg/agent_reasoning: streaming thinking text
315
+ const reasoningText = payload.text || '';
316
+ if (reasoningText)
317
+ currentThinking = (currentThinking || '') + reasoningText + '\n';
318
+ break;
319
+ }
320
+ case 'task_complete': {
321
+ // event_msg/task_complete: turn boundary — replaces the non-existent "turn.completed"
322
+ // Capture usage if present
323
+ const usageRaw = payload.usage;
324
+ if (usageRaw?.input_tokens) {
325
+ turnUsage = usageRaw;
326
+ usageEntries.push(usageRaw);
327
+ }
328
+ if (payload.model) {
329
+ model = payload.model;
330
+ }
331
+ flushAssistantTurn();
332
+ break;
333
+ }
334
+ case 'turn.completed': {
335
+ // Legacy event name — handle in case some versions use it
336
+ const usage = (payload.usage || payload);
337
+ if (usage.input_tokens) {
338
+ turnUsage = usage;
339
+ usageEntries.push(usage);
340
+ }
341
+ if (payload.model) {
342
+ model = payload.model;
343
+ }
344
+ flushAssistantTurn();
345
+ break;
346
+ }
347
+ case 'turn.started':
348
+ case 'thread.started':
349
+ case 'session_meta':
350
+ case 'task_started':
351
+ case 'token_count':
352
+ case 'turn_context':
353
+ // Lifecycle/telemetry events — skip
354
+ break;
355
+ default:
356
+ break;
357
+ }
358
+ }
359
+ // Flush any remaining assistant content after all lines processed
360
+ flushAssistantTurn();
361
+ return buildSession(sessionId, meta.cwd || 'codex://unknown', meta.cli_version || null, meta.timestamp, messages, usageEntries, model);
362
+ }
363
+ // ---------------------------------------------------------------------------
364
+ // Format B parser (pre-2025 single JSON object: { session, items })
365
+ // ---------------------------------------------------------------------------
366
+ function parseFormatB(content) {
367
+ let parsed;
368
+ try {
369
+ parsed = JSON.parse(content);
370
+ }
371
+ catch {
372
+ return null;
373
+ }
374
+ if (!parsed.session?.id || !Array.isArray(parsed.items))
375
+ return null;
376
+ const meta = parsed.session;
377
+ const sessionId = `codex:${meta.id}`;
378
+ const sessionTimestamp = meta.timestamp;
379
+ const messages = [];
380
+ const usageEntries = [];
381
+ const model = meta.model || '';
382
+ // Accumulators for current assistant turn
383
+ let currentToolCalls = [];
384
+ let currentToolResults = [];
385
+ let currentThinking = null;
386
+ let toolCounter = 0;
387
+ // Format B has no per-item timestamps — use session timestamp for all
388
+ const sessionDate = new Date(sessionTimestamp);
389
+ function flushAssistantTurn() {
390
+ if (currentToolCalls.length === 0 && !currentThinking)
391
+ return;
392
+ messages.push({
393
+ id: `codex-assistant-${messages.length}`,
394
+ sessionId: sessionId,
395
+ type: 'assistant',
396
+ content: '',
397
+ thinking: currentThinking,
398
+ toolCalls: [...currentToolCalls],
399
+ toolResults: [...currentToolResults],
400
+ usage: null,
401
+ timestamp: sessionDate,
402
+ parentId: null,
403
+ });
404
+ currentToolCalls = [];
405
+ currentToolResults = [];
406
+ currentThinking = null;
407
+ }
408
+ for (const item of parsed.items) {
409
+ if (!item.type)
410
+ continue;
411
+ if (item.role === 'user' && item.type === 'message') {
412
+ // Flush pending assistant turn before new user message
413
+ flushAssistantTurn();
414
+ const userContent = extractFormatBContent(item.content);
415
+ if (userContent && !isSystemContextMessage(userContent)) {
416
+ messages.push({
417
+ id: `codex-user-${messages.length}`,
418
+ sessionId: sessionId,
419
+ type: 'user',
420
+ content: userContent.slice(0, 10000),
421
+ thinking: null,
422
+ toolCalls: [],
423
+ toolResults: [],
424
+ usage: null,
425
+ timestamp: sessionDate,
426
+ parentId: null,
427
+ });
428
+ }
429
+ continue;
430
+ }
431
+ switch (item.type) {
432
+ case 'reasoning': {
433
+ // summary is array of { type: "summary_text", text: "..." }
434
+ // In older sessions summary may be empty []
435
+ if (Array.isArray(item.summary)) {
436
+ const reasoningText = item.summary
437
+ .filter(s => s.type === 'summary_text')
438
+ .map(s => s.text)
439
+ .join('\n');
440
+ if (reasoningText)
441
+ currentThinking = (currentThinking || '') + reasoningText + '\n';
442
+ }
443
+ break;
444
+ }
445
+ case 'function_call': {
446
+ // item: { type, id, name, arguments (JSON string), call_id, status }
447
+ toolCounter++;
448
+ const callId = item.call_id || item.id || `codex-tool-${toolCounter}`;
449
+ let args = {};
450
+ if (item.arguments) {
451
+ try {
452
+ args = JSON.parse(item.arguments);
267
453
  }
268
- else if (itemType === 'reasoning') {
269
- currentThinking = item.summary || item.text || null;
454
+ catch {
455
+ args = { raw: item.arguments };
270
456
  }
271
- break;
272
457
  }
273
- case 'turn.completed': {
274
- const usage = (payload.usage || payload);
275
- if (usage.input_tokens) {
276
- turnUsage = usage;
277
- usageEntries.push(usage);
278
- }
279
- if (payload.model) {
280
- model = payload.model;
458
+ currentToolCalls.push({
459
+ id: callId,
460
+ name: item.name || 'unknown',
461
+ input: args,
462
+ });
463
+ break;
464
+ }
465
+ case 'function_call_output': {
466
+ // item: { type, call_id, output (JSON string) }
467
+ const fcoCallId = item.call_id;
468
+ let fcoOutput = item.output || '';
469
+ try {
470
+ // output is often {"output":"...","metadata":{...}}
471
+ const outputParsed = JSON.parse(fcoOutput);
472
+ if (typeof outputParsed.output === 'string') {
473
+ fcoOutput = outputParsed.output;
281
474
  }
282
- // Flush assistant turn at turn boundary
283
- flushAssistantTurn();
284
- break;
285
475
  }
286
- case 'turn.started':
287
- case 'thread.started':
288
- case 'session_meta':
289
- // Lifecycle events — skip
290
- break;
476
+ catch {
477
+ // not JSON — use raw
478
+ }
479
+ if (fcoCallId) {
480
+ currentToolResults.push({ toolUseId: fcoCallId, output: fcoOutput.slice(0, 1000) });
481
+ }
482
+ break;
291
483
  }
292
484
  }
293
- // Flush any remaining assistant content
294
- flushAssistantTurn();
295
- if (messages.length === 0)
296
- return null;
297
- // Build session
298
- const userMessages = messages.filter(m => m.type === 'user');
299
- const assistantMessages = messages.filter(m => m.type === 'assistant');
300
- const toolCallCount = messages.reduce((sum, m) => sum + m.toolCalls.length, 0);
301
- const timestamps = messages.map(m => m.timestamp.getTime()).filter(t => t > 0);
302
- const startedAt = timestamps.length > 0 ? new Date(Math.min(...timestamps)) : new Date(meta.timestamp);
303
- const endedAt = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : lastTimestamp;
304
- // Build session usage from accumulated turn usage
305
- const totalInput = usageEntries.reduce((s, u) => s + (u.input_tokens || 0), 0);
306
- const totalOutput = usageEntries.reduce((s, u) => s + (u.output_tokens || 0), 0);
307
- const totalCached = usageEntries.reduce((s, u) => s + (u.cached_input_tokens || 0), 0);
308
- const usage = totalInput > 0 ? {
309
- totalInputTokens: totalInput,
310
- totalOutputTokens: totalOutput,
311
- cacheCreationTokens: 0,
312
- cacheReadTokens: totalCached,
313
- estimatedCostUsd: 0, // TODO: Codex pricing not public
314
- modelsUsed: model ? [model] : [],
315
- primaryModel: model || 'unknown',
316
- usageSource: 'jsonl',
317
- } : undefined;
318
- const projectPath = meta.cwd || 'codex://unknown';
319
- const projectName = path.basename(projectPath);
320
- const session = {
321
- id: sessionId,
322
- projectPath,
323
- projectName,
324
- summary: null,
325
- generatedTitle: null,
326
- titleSource: null,
327
- sessionCharacter: null,
328
- startedAt,
329
- endedAt,
330
- messageCount: messages.length,
331
- userMessageCount: userMessages.length,
332
- assistantMessageCount: assistantMessages.length,
333
- toolCallCount,
334
- gitBranch: null,
335
- claudeVersion: meta.cli_version || null,
336
- sourceTool: 'codex-cli',
337
- usage,
338
- messages,
339
- };
340
- // Generate title and character
341
- const titleResult = generateTitle(session);
342
- session.generatedTitle = titleResult.title;
343
- session.titleSource = titleResult.source;
344
- session.sessionCharacter = titleResult.character || detectSessionCharacter(session);
345
- return session;
346
485
  }
347
- catch {
486
+ // Flush any remaining assistant content
487
+ flushAssistantTurn();
488
+ const projectPath = parsed.session.cwd || 'codex://unknown';
489
+ return buildSession(sessionId, projectPath, null, sessionTimestamp, messages, usageEntries, model);
490
+ }
491
+ // ---------------------------------------------------------------------------
492
+ // Shared session builder
493
+ // ---------------------------------------------------------------------------
494
+ function buildSession(sessionId, projectPath, cliVersion, metaTimestamp, messages, usageEntries, model) {
495
+ if (messages.length === 0)
348
496
  return null;
349
- }
497
+ const userMessages = messages.filter(m => m.type === 'user');
498
+ const assistantMessages = messages.filter(m => m.type === 'assistant');
499
+ const toolCallCount = messages.reduce((sum, m) => sum + m.toolCalls.length, 0);
500
+ const timestamps = messages.map(m => m.timestamp.getTime()).filter(t => t > 0);
501
+ const startedAt = timestamps.length > 0 ? new Date(Math.min(...timestamps)) : new Date(metaTimestamp);
502
+ const endedAt = timestamps.length > 0 ? new Date(Math.max(...timestamps)) : new Date(metaTimestamp);
503
+ const totalInput = usageEntries.reduce((s, u) => s + (u.input_tokens || 0), 0);
504
+ const totalOutput = usageEntries.reduce((s, u) => s + (u.output_tokens || 0), 0);
505
+ const totalCached = usageEntries.reduce((s, u) => s + (u.cached_input_tokens || 0), 0);
506
+ const usage = totalInput > 0 ? {
507
+ totalInputTokens: totalInput,
508
+ totalOutputTokens: totalOutput,
509
+ cacheCreationTokens: 0,
510
+ cacheReadTokens: totalCached,
511
+ estimatedCostUsd: 0, // Codex pricing not public
512
+ modelsUsed: model ? [model] : [],
513
+ primaryModel: model || 'unknown',
514
+ usageSource: 'jsonl',
515
+ } : undefined;
516
+ const projectName = path.basename(projectPath);
517
+ const session = {
518
+ id: sessionId,
519
+ projectPath,
520
+ projectName,
521
+ summary: null,
522
+ generatedTitle: null,
523
+ titleSource: null,
524
+ sessionCharacter: null,
525
+ startedAt,
526
+ endedAt,
527
+ messageCount: messages.length,
528
+ userMessageCount: userMessages.length,
529
+ assistantMessageCount: assistantMessages.length,
530
+ toolCallCount,
531
+ gitBranch: null,
532
+ claudeVersion: cliVersion,
533
+ sourceTool: 'codex-cli',
534
+ usage,
535
+ messages,
536
+ };
537
+ const titleResult = generateTitle(session);
538
+ session.generatedTitle = titleResult.title;
539
+ session.titleSource = titleResult.source;
540
+ session.sessionCharacter = titleResult.character || detectSessionCharacter(session);
541
+ return session;
350
542
  }
351
543
  // ---------------------------------------------------------------------------
352
544
  // Parsing helpers
@@ -354,7 +546,7 @@ function parseCodexSession(filePath) {
354
546
  function parseSessionMeta(line) {
355
547
  try {
356
548
  const parsed = JSON.parse(line);
357
- // Handle RolloutLine envelope
549
+ // Handle RolloutLine envelope: { type: "session_meta", payload: { id, cwd, ... } }
358
550
  if (parsed.payload && parsed.type === 'session_meta') {
359
551
  return parsed.payload;
360
552
  }
@@ -368,28 +560,67 @@ function parseSessionMeta(line) {
368
560
  return null;
369
561
  }
370
562
  }
371
- function extractUserContent(payload) {
563
+ /**
564
+ * Extract text content from a Format A payload.
565
+ * Handles: plain text, content arrays with input_text/output_text/text types,
566
+ * and nested item wrappers.
567
+ */
568
+ function extractContent(payload) {
372
569
  if (typeof payload.text === 'string')
373
570
  return payload.text;
374
571
  if (typeof payload.content === 'string')
375
572
  return payload.content;
376
573
  if (Array.isArray(payload.content)) {
377
- return payload.content
378
- .filter(c => c.type === 'text' || c.type === 'input_text')
574
+ const parts = payload.content
575
+ .filter(c => c.type === 'text' || c.type === 'input_text' || c.type === 'output_text')
379
576
  .map(c => c.text)
380
- .join('\n');
577
+ .filter(Boolean);
578
+ return parts.length > 0 ? parts.join('\n') : null;
381
579
  }
382
- // Nested in item
580
+ // Nested in item wrapper
383
581
  const item = payload.item;
384
582
  if (item)
385
- return extractUserContent(item);
583
+ return extractContent(item);
386
584
  return null;
387
585
  }
388
- function parseTimestamp(payload) {
389
- const ts = payload.timestamp || payload.createdAt;
586
+ /**
587
+ * Extract text content from Format B item content array.
588
+ */
589
+ function extractFormatBContent(content) {
590
+ if (!Array.isArray(content))
591
+ return null;
592
+ const parts = content
593
+ .filter(c => c.type === 'text' || c.type === 'input_text' || c.type === 'output_text')
594
+ .map(c => c.text)
595
+ .filter(Boolean);
596
+ return parts.length > 0 ? parts.join('\n') : null;
597
+ }
598
+ /**
599
+ * Parse the envelope-level timestamp from a Format A RolloutLine.
600
+ * Every line in Format A has a top-level `timestamp` ISO 8601 string.
601
+ */
602
+ function parseEnvelopeTimestamp(event) {
603
+ const ts = event.timestamp;
390
604
  if (!ts)
391
605
  return null;
392
606
  const d = new Date(ts);
393
607
  return isNaN(d.getTime()) ? null : d;
394
608
  }
609
+ /**
610
+ * Detect system context injection messages that should not be treated as user prompts.
611
+ * Codex CLI injects AGENTS.md, environment context, permissions, etc. as role="user" messages
612
+ * before the actual user prompt. We filter these out to avoid polluting the message list.
613
+ */
614
+ function isSystemContextMessage(content) {
615
+ const trimmed = content.trimStart();
616
+ return (trimmed.startsWith('<permissions') ||
617
+ trimmed.startsWith('<environment_context') ||
618
+ trimmed.startsWith('<collaboration_mode') ||
619
+ trimmed.startsWith('# AGENTS.md') ||
620
+ trimmed.startsWith('## Apps') ||
621
+ trimmed.startsWith('## Tools') ||
622
+ trimmed.startsWith('<system') ||
623
+ trimmed.startsWith('## Shell') ||
624
+ trimmed.startsWith('## Current working directory'));
625
+ }
395
626
  //# sourceMappingURL=codex.js.map