@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.
- package/CHANGELOG.md +41 -0
- package/README.md +1 -1
- package/dashboard-dist/assets/{index-Rr1JlICa.js → index-BdoBoNtI.js} +154 -139
- package/dashboard-dist/assets/index-QzYeMRSf.css +1 -0
- package/dashboard-dist/index.html +2 -2
- package/dist/parser/titles.d.ts.map +1 -1
- package/dist/parser/titles.js +55 -7
- package/dist/parser/titles.js.map +1 -1
- package/dist/providers/codex.d.ts +5 -1
- package/dist/providers/codex.d.ts.map +1 -1
- package/dist/providers/codex.js +489 -258
- package/dist/providers/codex.js.map +1 -1
- package/dist/providers/copilot-cli.d.ts.map +1 -1
- package/dist/providers/copilot-cli.js +64 -6
- package/dist/providers/copilot-cli.js.map +1 -1
- package/dist/providers/copilot.d.ts.map +1 -1
- package/dist/providers/copilot.js +23 -0
- package/dist/providers/copilot.js.map +1 -1
- package/dist/providers/cursor.js +206 -22
- package/dist/providers/cursor.js.map +1 -1
- package/dist/types.d.ts +28 -4
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/telemetry.d.ts.map +1 -1
- package/dist/utils/telemetry.js +33 -1
- package/dist/utils/telemetry.js.map +1 -1
- package/package.json +1 -1
- package/server-dist/llm/analysis.d.ts +1 -0
- package/server-dist/llm/analysis.d.ts.map +1 -1
- package/server-dist/llm/analysis.js +35 -9
- package/server-dist/llm/analysis.js.map +1 -1
- package/server-dist/llm/prompts.d.ts +31 -7
- package/server-dist/llm/prompts.d.ts.map +1 -1
- package/server-dist/llm/prompts.js +161 -46
- package/server-dist/llm/prompts.js.map +1 -1
- package/server-dist/llm/providers/anthropic.js +1 -1
- package/server-dist/llm/providers/gemini.js +1 -1
- package/server-dist/llm/providers/openai.d.ts.map +1 -1
- package/server-dist/llm/providers/openai.js +1 -0
- package/server-dist/llm/providers/openai.js.map +1 -1
- package/server-dist/routes/analysis.d.ts.map +1 -1
- package/server-dist/routes/analysis.js +4 -0
- package/server-dist/routes/analysis.js.map +1 -1
- package/server-dist/routes/analytics.d.ts.map +1 -1
- package/server-dist/routes/analytics.js +10 -0
- package/server-dist/routes/analytics.js.map +1 -1
- package/dashboard-dist/assets/index-BMhL7wL8.css +0 -1
package/dist/providers/codex.js
CHANGED
|
@@ -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
|
|
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
|
-
*
|
|
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
|
-
|
|
96
|
-
if (lines.length === 0)
|
|
102
|
+
const content = fs.readFileSync(filePath, 'utf-8').trim();
|
|
103
|
+
if (!content)
|
|
97
104
|
return null;
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
if (
|
|
101
|
-
return
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
198
|
+
lastTimestamp = parseEnvelopeTimestamp(event) || lastTimestamp;
|
|
213
199
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
|
|
269
|
-
|
|
454
|
+
catch {
|
|
455
|
+
args = { raw: item.arguments };
|
|
270
456
|
}
|
|
271
|
-
break;
|
|
272
457
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
|
583
|
+
return extractContent(item);
|
|
386
584
|
return null;
|
|
387
585
|
}
|
|
388
|
-
|
|
389
|
-
|
|
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
|