@respan/cli 0.5.3 → 0.6.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/dist/commands/auth/login.js +2 -2
- package/dist/commands/integrate/claude-code.js +9 -28
- package/dist/commands/integrate/codex-cli.js +7 -27
- package/dist/commands/integrate/gemini-cli.js +99 -53
- package/dist/hooks/claude-code.cjs +951 -0
- package/dist/hooks/claude-code.d.ts +1 -0
- package/dist/hooks/claude-code.js +641 -0
- package/dist/hooks/codex-cli.cjs +793 -0
- package/dist/hooks/codex-cli.d.ts +1 -0
- package/dist/hooks/codex-cli.js +469 -0
- package/dist/hooks/gemini-cli.cjs +826 -0
- package/dist/hooks/gemini-cli.d.ts +1 -0
- package/dist/hooks/gemini-cli.js +563 -0
- package/dist/hooks/shared.d.ts +82 -0
- package/dist/hooks/shared.js +461 -0
- package/dist/lib/integrate.d.ts +3 -3
- package/dist/lib/integrate.js +4 -8
- package/oclif.manifest.json +466 -466
- package/package.json +6 -3
- package/dist/assets/codex_hook.py +0 -897
- package/dist/assets/hook.py +0 -1052
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,469 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Respan Hook for Codex CLI
|
|
3
|
+
*
|
|
4
|
+
* Sends Codex CLI conversation traces to Respan after each agent turn.
|
|
5
|
+
* Uses Codex CLI's notify hook to capture session JSONL files and convert
|
|
6
|
+
* them to Respan spans.
|
|
7
|
+
*
|
|
8
|
+
* Span tree per turn:
|
|
9
|
+
* Root: codex-cli
|
|
10
|
+
* ├── openai.chat (generation — model, tokens, messages)
|
|
11
|
+
* ├── Reasoning (if reasoning_output_tokens > 0)
|
|
12
|
+
* ├── Tool: Shell (if exec_command)
|
|
13
|
+
* ├── Tool: File Edit (if apply_patch)
|
|
14
|
+
* └── Tool: Web Search (if web_search_call)
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'node:fs';
|
|
17
|
+
import * as os from 'node:os';
|
|
18
|
+
import * as path from 'node:path';
|
|
19
|
+
import { initLogging, log, debug, resolveCredentials, loadRespanConfig, loadState, saveState, acquireLock, sendSpans, addDefaultsToAll, resolveSpanFields, buildMetadata, nowISO, latencySeconds, truncate, } from './shared.js';
|
|
20
|
+
// ── Config ────────────────────────────────────────────────────────
|
|
21
|
+
const STATE_DIR = path.join(os.homedir(), '.codex', 'state');
|
|
22
|
+
const LOG_FILE = path.join(STATE_DIR, 'respan_hook.log');
|
|
23
|
+
const STATE_FILE = path.join(STATE_DIR, 'respan_state.json');
|
|
24
|
+
const LOCK_PATH = path.join(STATE_DIR, 'respan_hook.lock');
|
|
25
|
+
const DEBUG_MODE = (process.env.CODEX_RESPAN_DEBUG ?? '').toLowerCase() === 'true';
|
|
26
|
+
const MAX_CHARS = parseInt(process.env.CODEX_RESPAN_MAX_CHARS ?? '4000', 10) || 4000;
|
|
27
|
+
initLogging(LOG_FILE, DEBUG_MODE);
|
|
28
|
+
// ── Tool display names ────────────────────────────────────────────
|
|
29
|
+
const TOOL_DISPLAY_NAMES = {
|
|
30
|
+
exec_command: 'Shell',
|
|
31
|
+
apply_patch: 'File Edit',
|
|
32
|
+
web_search: 'Web Search',
|
|
33
|
+
};
|
|
34
|
+
function toolDisplayName(name) {
|
|
35
|
+
return TOOL_DISPLAY_NAMES[name] ?? name;
|
|
36
|
+
}
|
|
37
|
+
function formatToolInput(toolName, args) {
|
|
38
|
+
if (!args)
|
|
39
|
+
return '';
|
|
40
|
+
try {
|
|
41
|
+
const parsed = typeof args === 'string' ? JSON.parse(args) : args;
|
|
42
|
+
if (toolName === 'exec_command' && typeof parsed === 'object' && parsed !== null) {
|
|
43
|
+
const cmd = parsed.cmd ?? '';
|
|
44
|
+
const workdir = parsed.workdir ?? '';
|
|
45
|
+
return truncate(workdir ? `[${workdir}] Command: ${cmd}` : `Command: ${cmd}`, MAX_CHARS);
|
|
46
|
+
}
|
|
47
|
+
if (toolName === 'apply_patch')
|
|
48
|
+
return truncate(args, MAX_CHARS);
|
|
49
|
+
if (typeof parsed === 'object')
|
|
50
|
+
return truncate(JSON.stringify(parsed, null, 2), MAX_CHARS);
|
|
51
|
+
}
|
|
52
|
+
catch { }
|
|
53
|
+
return truncate(String(args), MAX_CHARS);
|
|
54
|
+
}
|
|
55
|
+
function formatToolOutput(output) {
|
|
56
|
+
if (!output)
|
|
57
|
+
return '';
|
|
58
|
+
try {
|
|
59
|
+
const parsed = JSON.parse(output);
|
|
60
|
+
if (typeof parsed === 'object' && parsed !== null && 'output' in parsed) {
|
|
61
|
+
return truncate(String(parsed.output), MAX_CHARS);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
return truncate(output, MAX_CHARS);
|
|
66
|
+
}
|
|
67
|
+
// ── Session parsing ───────────────────────────────────────────────
|
|
68
|
+
function parseSession(lines) {
|
|
69
|
+
const events = [];
|
|
70
|
+
for (const line of lines) {
|
|
71
|
+
if (!line.trim())
|
|
72
|
+
continue;
|
|
73
|
+
try {
|
|
74
|
+
events.push(JSON.parse(line));
|
|
75
|
+
}
|
|
76
|
+
catch { }
|
|
77
|
+
}
|
|
78
|
+
return events;
|
|
79
|
+
}
|
|
80
|
+
function extractTurns(events) {
|
|
81
|
+
const turns = [];
|
|
82
|
+
let current = null;
|
|
83
|
+
for (const event of events) {
|
|
84
|
+
const evtType = String(event.type ?? '');
|
|
85
|
+
const payload = (event.payload ?? {});
|
|
86
|
+
const timestamp = String(event.timestamp ?? '');
|
|
87
|
+
if (evtType === 'event_msg') {
|
|
88
|
+
const msgType = String(payload.type ?? '');
|
|
89
|
+
if (msgType === 'task_started') {
|
|
90
|
+
current = {
|
|
91
|
+
turn_id: String(payload.turn_id ?? ''),
|
|
92
|
+
start_time: timestamp,
|
|
93
|
+
end_time: '',
|
|
94
|
+
model: '',
|
|
95
|
+
cwd: '',
|
|
96
|
+
user_message: '',
|
|
97
|
+
assistant_message: '',
|
|
98
|
+
commentary: [],
|
|
99
|
+
tool_calls: [],
|
|
100
|
+
tool_outputs: [],
|
|
101
|
+
reasoning: false,
|
|
102
|
+
reasoning_text: '',
|
|
103
|
+
token_usage: {},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
else if (msgType === 'task_complete' && current) {
|
|
107
|
+
current.end_time = timestamp;
|
|
108
|
+
turns.push(current);
|
|
109
|
+
current = null;
|
|
110
|
+
}
|
|
111
|
+
else if (msgType === 'user_message' && current) {
|
|
112
|
+
current.user_message = String(payload.message ?? '');
|
|
113
|
+
}
|
|
114
|
+
else if (msgType === 'agent_message' && current) {
|
|
115
|
+
const phase = String(payload.phase ?? '');
|
|
116
|
+
const message = String(payload.message ?? '');
|
|
117
|
+
if (phase === 'final_answer')
|
|
118
|
+
current.assistant_message = message;
|
|
119
|
+
else if (phase === 'commentary')
|
|
120
|
+
current.commentary.push(message);
|
|
121
|
+
}
|
|
122
|
+
else if (msgType === 'token_count' && current) {
|
|
123
|
+
const info = (payload.info ?? {});
|
|
124
|
+
const lastUsage = (info.last_token_usage ?? {});
|
|
125
|
+
if (Object.keys(lastUsage).length)
|
|
126
|
+
current.token_usage = lastUsage;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
else if (evtType === 'turn_context' && current) {
|
|
130
|
+
current.model = String(payload.model ?? '');
|
|
131
|
+
current.cwd = String(payload.cwd ?? '');
|
|
132
|
+
}
|
|
133
|
+
else if (evtType === 'response_item' && current) {
|
|
134
|
+
const itemType = String(payload.type ?? '');
|
|
135
|
+
if (itemType === 'function_call' || itemType === 'custom_tool_call') {
|
|
136
|
+
current.tool_calls.push({
|
|
137
|
+
name: String(payload.name ?? 'unknown'),
|
|
138
|
+
arguments: String(itemType === 'custom_tool_call' ? (payload.input ?? '') : (payload.arguments ?? '')),
|
|
139
|
+
call_id: String(payload.call_id ?? ''),
|
|
140
|
+
timestamp,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
else if (itemType === 'function_call_output' || itemType === 'custom_tool_call_output') {
|
|
144
|
+
current.tool_outputs.push({
|
|
145
|
+
call_id: String(payload.call_id ?? ''),
|
|
146
|
+
output: String(payload.output ?? ''),
|
|
147
|
+
timestamp,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else if (itemType === 'reasoning') {
|
|
151
|
+
if (current) {
|
|
152
|
+
current.reasoning = true;
|
|
153
|
+
const summary = String(payload.summary ?? payload.text ?? '');
|
|
154
|
+
if (summary)
|
|
155
|
+
current.reasoning_text += (current.reasoning_text ? '\n' : '') + summary;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
else if (itemType === 'web_search_call') {
|
|
159
|
+
const action = (payload.action ?? {});
|
|
160
|
+
const syntheticId = `web_search_${timestamp}`;
|
|
161
|
+
current.tool_calls.push({
|
|
162
|
+
name: 'web_search',
|
|
163
|
+
arguments: JSON.stringify({ query: action.query ?? '' }),
|
|
164
|
+
call_id: syntheticId,
|
|
165
|
+
timestamp,
|
|
166
|
+
});
|
|
167
|
+
// Web search has no separate output event; record query as output
|
|
168
|
+
current.tool_outputs.push({
|
|
169
|
+
call_id: syntheticId,
|
|
170
|
+
output: `Search: ${action.query ?? ''}`,
|
|
171
|
+
timestamp,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return turns;
|
|
177
|
+
}
|
|
178
|
+
// ── Span creation ─────────────────────────────────────────────────
|
|
179
|
+
function createSpans(sessionId, turnNum, turn, config) {
|
|
180
|
+
const spans = [];
|
|
181
|
+
const now = nowISO();
|
|
182
|
+
const startTimeStr = turn.start_time || now;
|
|
183
|
+
const endTimeStr = turn.end_time || now;
|
|
184
|
+
const lat = latencySeconds(startTimeStr, endTimeStr);
|
|
185
|
+
const promptMessages = [];
|
|
186
|
+
if (turn.user_message)
|
|
187
|
+
promptMessages.push({ role: 'user', content: turn.user_message });
|
|
188
|
+
const completionMessage = turn.assistant_message
|
|
189
|
+
? { role: 'assistant', content: turn.assistant_message }
|
|
190
|
+
: null;
|
|
191
|
+
const { workflowName, spanName, customerId } = resolveSpanFields(config, {
|
|
192
|
+
workflowName: 'codex-cli',
|
|
193
|
+
spanName: 'codex-cli',
|
|
194
|
+
});
|
|
195
|
+
const traceUniqueId = `${sessionId}_turn_${turnNum}`;
|
|
196
|
+
const threadId = `codexcli_${sessionId}`;
|
|
197
|
+
// Metadata
|
|
198
|
+
const baseMeta = { codex_cli_turn: turnNum };
|
|
199
|
+
if (turn.cwd)
|
|
200
|
+
baseMeta.cwd = turn.cwd;
|
|
201
|
+
if (turn.commentary.length)
|
|
202
|
+
baseMeta.commentary = turn.commentary.join('\n');
|
|
203
|
+
const metadata = buildMetadata(config, baseMeta);
|
|
204
|
+
// Token usage
|
|
205
|
+
const usageFields = {};
|
|
206
|
+
const tu = turn.token_usage;
|
|
207
|
+
if (Object.keys(tu).length) {
|
|
208
|
+
const pt = Number(tu.input_tokens ?? 0);
|
|
209
|
+
const ct = Number(tu.output_tokens ?? 0);
|
|
210
|
+
usageFields.prompt_tokens = pt;
|
|
211
|
+
usageFields.completion_tokens = ct;
|
|
212
|
+
usageFields.total_tokens = Number(tu.total_tokens ?? pt + ct) || pt + ct;
|
|
213
|
+
const cached = Number(tu.cached_input_tokens ?? 0);
|
|
214
|
+
if (cached > 0)
|
|
215
|
+
usageFields.prompt_tokens_details = { cached_tokens: cached };
|
|
216
|
+
const reasoning = Number(tu.reasoning_output_tokens ?? 0);
|
|
217
|
+
if (reasoning > 0)
|
|
218
|
+
metadata.reasoning_tokens = reasoning;
|
|
219
|
+
}
|
|
220
|
+
// Root span
|
|
221
|
+
const rootSpanId = `codexcli_${traceUniqueId}_root`;
|
|
222
|
+
spans.push({
|
|
223
|
+
trace_unique_id: traceUniqueId,
|
|
224
|
+
thread_identifier: threadId,
|
|
225
|
+
customer_identifier: customerId,
|
|
226
|
+
span_unique_id: rootSpanId,
|
|
227
|
+
span_name: spanName,
|
|
228
|
+
span_workflow_name: workflowName,
|
|
229
|
+
model: turn.model || 'gpt-5.4',
|
|
230
|
+
provider_id: '',
|
|
231
|
+
span_path: '',
|
|
232
|
+
input: promptMessages.length ? JSON.stringify(promptMessages) : '',
|
|
233
|
+
output: turn.assistant_message,
|
|
234
|
+
timestamp: endTimeStr,
|
|
235
|
+
start_time: startTimeStr,
|
|
236
|
+
metadata,
|
|
237
|
+
...(lat !== undefined ? { latency: lat } : {}),
|
|
238
|
+
});
|
|
239
|
+
// LLM generation child span
|
|
240
|
+
spans.push({
|
|
241
|
+
trace_unique_id: traceUniqueId,
|
|
242
|
+
span_unique_id: `codexcli_${traceUniqueId}_gen`,
|
|
243
|
+
span_parent_id: rootSpanId,
|
|
244
|
+
span_name: 'openai.chat',
|
|
245
|
+
span_workflow_name: workflowName,
|
|
246
|
+
span_path: 'openai_chat',
|
|
247
|
+
model: turn.model || 'gpt-5.4',
|
|
248
|
+
provider_id: 'openai',
|
|
249
|
+
metadata: {},
|
|
250
|
+
input: promptMessages.length ? JSON.stringify(promptMessages) : '',
|
|
251
|
+
output: turn.assistant_message,
|
|
252
|
+
prompt_messages: promptMessages,
|
|
253
|
+
completion_message: completionMessage,
|
|
254
|
+
timestamp: endTimeStr,
|
|
255
|
+
start_time: startTimeStr,
|
|
256
|
+
...(lat !== undefined ? { latency: lat } : {}),
|
|
257
|
+
...usageFields,
|
|
258
|
+
});
|
|
259
|
+
// Reasoning child span
|
|
260
|
+
const reasoningTokens = Number(tu.reasoning_output_tokens ?? 0);
|
|
261
|
+
if (turn.reasoning || reasoningTokens > 0) {
|
|
262
|
+
spans.push({
|
|
263
|
+
trace_unique_id: traceUniqueId,
|
|
264
|
+
span_unique_id: `codexcli_${traceUniqueId}_reasoning`,
|
|
265
|
+
span_parent_id: rootSpanId,
|
|
266
|
+
span_name: 'Reasoning',
|
|
267
|
+
span_workflow_name: workflowName,
|
|
268
|
+
span_path: 'reasoning',
|
|
269
|
+
provider_id: '',
|
|
270
|
+
metadata: reasoningTokens > 0 ? { reasoning_tokens: reasoningTokens } : {},
|
|
271
|
+
input: '',
|
|
272
|
+
output: turn.reasoning_text || (reasoningTokens > 0 ? `[Reasoning: ${reasoningTokens} tokens]` : '[Reasoning]'),
|
|
273
|
+
timestamp: endTimeStr,
|
|
274
|
+
start_time: startTimeStr,
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
// Tool child spans
|
|
278
|
+
const outputMap = new Map();
|
|
279
|
+
for (const to of turn.tool_outputs) {
|
|
280
|
+
if (to.call_id)
|
|
281
|
+
outputMap.set(to.call_id, to);
|
|
282
|
+
}
|
|
283
|
+
let toolNum = 0;
|
|
284
|
+
for (const tc of turn.tool_calls) {
|
|
285
|
+
toolNum++;
|
|
286
|
+
const display = toolDisplayName(tc.name);
|
|
287
|
+
const outputData = outputMap.get(tc.call_id);
|
|
288
|
+
const toolEnd = outputData?.timestamp ?? endTimeStr;
|
|
289
|
+
const toolLat = latencySeconds(tc.timestamp, toolEnd);
|
|
290
|
+
spans.push({
|
|
291
|
+
trace_unique_id: traceUniqueId,
|
|
292
|
+
span_unique_id: `codexcli_${traceUniqueId}_tool_${toolNum}`,
|
|
293
|
+
span_parent_id: rootSpanId,
|
|
294
|
+
span_name: `Tool: ${display}`,
|
|
295
|
+
span_workflow_name: workflowName,
|
|
296
|
+
span_path: `tool_${tc.name.toLowerCase()}`,
|
|
297
|
+
provider_id: '',
|
|
298
|
+
metadata: {},
|
|
299
|
+
input: formatToolInput(tc.name, tc.arguments),
|
|
300
|
+
output: formatToolOutput(outputData?.output ?? ''),
|
|
301
|
+
timestamp: toolEnd,
|
|
302
|
+
start_time: tc.timestamp || startTimeStr,
|
|
303
|
+
...(toolLat !== undefined ? { latency: toolLat } : {}),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return addDefaultsToAll(spans);
|
|
307
|
+
}
|
|
308
|
+
// ── Session file finding ──────────────────────────────────────────
|
|
309
|
+
function findSessionFile(sessionId) {
|
|
310
|
+
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
311
|
+
if (!fs.existsSync(sessionsDir))
|
|
312
|
+
return null;
|
|
313
|
+
// Search date dirs in reverse order (newest first)
|
|
314
|
+
const walk = (dir) => {
|
|
315
|
+
const entries = fs.readdirSync(dir).sort().reverse();
|
|
316
|
+
for (const entry of entries) {
|
|
317
|
+
const full = path.join(dir, entry);
|
|
318
|
+
if (fs.statSync(full).isDirectory()) {
|
|
319
|
+
const result = walk(full);
|
|
320
|
+
if (result)
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
else if (entry.endsWith('.jsonl') && entry.includes(sessionId)) {
|
|
324
|
+
return full;
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
};
|
|
329
|
+
return walk(sessionsDir);
|
|
330
|
+
}
|
|
331
|
+
function findLatestSessionFile() {
|
|
332
|
+
const sessionsDir = path.join(os.homedir(), '.codex', 'sessions');
|
|
333
|
+
if (!fs.existsSync(sessionsDir))
|
|
334
|
+
return null;
|
|
335
|
+
let latestFile = null;
|
|
336
|
+
let latestMtime = 0;
|
|
337
|
+
const walk = (dir) => {
|
|
338
|
+
for (const entry of fs.readdirSync(dir)) {
|
|
339
|
+
const full = path.join(dir, entry);
|
|
340
|
+
const stat = fs.statSync(full);
|
|
341
|
+
if (stat.isDirectory())
|
|
342
|
+
walk(full);
|
|
343
|
+
else if (entry.endsWith('.jsonl') && stat.mtimeMs > latestMtime) {
|
|
344
|
+
latestMtime = stat.mtimeMs;
|
|
345
|
+
latestFile = full;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
walk(sessionsDir);
|
|
350
|
+
if (!latestFile)
|
|
351
|
+
return null;
|
|
352
|
+
try {
|
|
353
|
+
const firstLine = fs.readFileSync(latestFile, 'utf-8').split('\n')[0];
|
|
354
|
+
if (!firstLine)
|
|
355
|
+
return null;
|
|
356
|
+
const firstMsg = JSON.parse(firstLine);
|
|
357
|
+
const payload = (firstMsg.payload ?? {});
|
|
358
|
+
const sessionId = String(payload.id ?? path.basename(latestFile, '.jsonl'));
|
|
359
|
+
return { sessionId, sessionFile: latestFile };
|
|
360
|
+
}
|
|
361
|
+
catch {
|
|
362
|
+
return null;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
// ── Main ──────────────────────────────────────────────────────────
|
|
366
|
+
async function main() {
|
|
367
|
+
const scriptStart = Date.now();
|
|
368
|
+
debug('Codex hook started');
|
|
369
|
+
// Parse notify payload from argv[2] (argv[0]=node, argv[1]=script)
|
|
370
|
+
if (process.argv.length < 3) {
|
|
371
|
+
debug('No argument provided (expected JSON payload in argv[2])');
|
|
372
|
+
process.exit(0);
|
|
373
|
+
}
|
|
374
|
+
let payload;
|
|
375
|
+
try {
|
|
376
|
+
payload = JSON.parse(process.argv[2]);
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
debug(`Invalid JSON in argv[2]: ${e}`);
|
|
380
|
+
process.exit(0);
|
|
381
|
+
}
|
|
382
|
+
const eventType = String(payload.type ?? '');
|
|
383
|
+
if (eventType !== 'agent-turn-complete') {
|
|
384
|
+
debug(`Ignoring event type: ${eventType}`);
|
|
385
|
+
process.exit(0);
|
|
386
|
+
}
|
|
387
|
+
let sessionId = String(payload['thread-id'] ?? '');
|
|
388
|
+
if (!sessionId) {
|
|
389
|
+
debug('No thread-id in notify payload');
|
|
390
|
+
process.exit(0);
|
|
391
|
+
}
|
|
392
|
+
debug(`Processing notify: type=${eventType}, session=${sessionId}`);
|
|
393
|
+
const creds = resolveCredentials();
|
|
394
|
+
if (!creds) {
|
|
395
|
+
log('ERROR', 'No API key found. Run: respan auth login');
|
|
396
|
+
process.exit(0);
|
|
397
|
+
}
|
|
398
|
+
// Find session file
|
|
399
|
+
let sessionFile = findSessionFile(sessionId);
|
|
400
|
+
if (!sessionFile) {
|
|
401
|
+
const latest = findLatestSessionFile();
|
|
402
|
+
if (latest) {
|
|
403
|
+
sessionId = latest.sessionId;
|
|
404
|
+
sessionFile = latest.sessionFile;
|
|
405
|
+
}
|
|
406
|
+
else {
|
|
407
|
+
debug('No session file found');
|
|
408
|
+
process.exit(0);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
// Load config
|
|
412
|
+
const cwd = String(payload.cwd ?? '');
|
|
413
|
+
const config = cwd ? loadRespanConfig(path.join(cwd, '.codex', 'respan.json')) : null;
|
|
414
|
+
if (config)
|
|
415
|
+
debug(`Loaded respan.json config from ${cwd}`);
|
|
416
|
+
// Process with retry
|
|
417
|
+
const maxAttempts = 3;
|
|
418
|
+
let turns = 0;
|
|
419
|
+
try {
|
|
420
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
421
|
+
const unlock = acquireLock(LOCK_PATH);
|
|
422
|
+
try {
|
|
423
|
+
const state = loadState(STATE_FILE);
|
|
424
|
+
const sessionState = (state[sessionId] ?? {});
|
|
425
|
+
const lastTurnCount = Number(sessionState.turn_count ?? 0);
|
|
426
|
+
const lines = fs.readFileSync(sessionFile, 'utf-8').trim().split('\n');
|
|
427
|
+
const events = parseSession(lines);
|
|
428
|
+
const allTurns = extractTurns(events);
|
|
429
|
+
if (allTurns.length > lastTurnCount) {
|
|
430
|
+
const newTurns = allTurns.slice(lastTurnCount);
|
|
431
|
+
for (const turn of newTurns) {
|
|
432
|
+
turns++;
|
|
433
|
+
const turnNum = lastTurnCount + turns;
|
|
434
|
+
const spans = createSpans(sessionId, turnNum, turn, config);
|
|
435
|
+
await sendSpans(spans, creds.apiKey, creds.baseUrl, `turn_${turnNum}`);
|
|
436
|
+
}
|
|
437
|
+
state[sessionId] = {
|
|
438
|
+
turn_count: lastTurnCount + turns,
|
|
439
|
+
updated: nowISO(),
|
|
440
|
+
};
|
|
441
|
+
saveState(STATE_FILE, state);
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
finally {
|
|
445
|
+
unlock?.();
|
|
446
|
+
}
|
|
447
|
+
if (turns > 0)
|
|
448
|
+
break;
|
|
449
|
+
if (attempt < maxAttempts - 1) {
|
|
450
|
+
const delay = 500 * (attempt + 1);
|
|
451
|
+
debug(`No turns processed (attempt ${attempt + 1}/${maxAttempts}), retrying in ${delay}ms...`);
|
|
452
|
+
await new Promise((r) => setTimeout(r, delay));
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const duration = (Date.now() - scriptStart) / 1000;
|
|
456
|
+
log('INFO', `Processed ${turns} turns in ${duration.toFixed(1)}s`);
|
|
457
|
+
if (duration > 180)
|
|
458
|
+
log('WARN', `Hook took ${duration.toFixed(1)}s (>3min)`);
|
|
459
|
+
}
|
|
460
|
+
catch (e) {
|
|
461
|
+
log('ERROR', `Failed to process session: ${e}`);
|
|
462
|
+
if (DEBUG_MODE)
|
|
463
|
+
debug(String(e.stack ?? e));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
main().catch((e) => {
|
|
467
|
+
log('ERROR', `Hook crashed: ${e}`);
|
|
468
|
+
process.exit(1);
|
|
469
|
+
});
|