@bububuger/spanory 0.1.14

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.
@@ -0,0 +1,457 @@
1
+ // @ts-nocheck
2
+ import { createHash } from 'node:crypto';
3
+ import { readFile, readdir } from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { RUNTIME_CAPABILITIES } from '../shared/capabilities.js';
6
+ import { normalizeTranscriptMessages, pickUsage } from '../shared/normalize.js';
7
+ function parseTimestamp(raw) {
8
+ const date = raw ? new Date(raw) : new Date();
9
+ if (Number.isNaN(date.getTime()))
10
+ return new Date();
11
+ return date;
12
+ }
13
+ function safeJsonParse(raw, fallback = {}) {
14
+ if (typeof raw !== 'string') {
15
+ if (raw && typeof raw === 'object')
16
+ return raw;
17
+ return fallback;
18
+ }
19
+ try {
20
+ const parsed = JSON.parse(raw);
21
+ if (parsed && typeof parsed === 'object')
22
+ return parsed;
23
+ }
24
+ catch {
25
+ // ignore parse errors
26
+ }
27
+ return fallback;
28
+ }
29
+ function normalizeToolCall(name, args, index) {
30
+ const normalizedName = String(name ?? '').trim();
31
+ const shellTools = new Set(['exec_command', 'shell_command', 'write_stdin']);
32
+ const agentTaskTools = new Set(['spawn_agent', 'wait', 'close_agent']);
33
+ if (shellTools.has(normalizedName)) {
34
+ const command = String(args.command ?? args.cmd ?? args.chars ?? '').trim();
35
+ return {
36
+ toolName: 'Bash',
37
+ input: command ? { command } : args,
38
+ callId: `call-shell-${index}`,
39
+ };
40
+ }
41
+ if (agentTaskTools.has(normalizedName)) {
42
+ return {
43
+ toolName: 'Task',
44
+ input: { name: normalizedName, ...args },
45
+ callId: `call-task-${index}`,
46
+ };
47
+ }
48
+ return {
49
+ toolName: normalizedName || 'tool',
50
+ input: args,
51
+ callId: `call-tool-${index}`,
52
+ };
53
+ }
54
+ function sanitizeProjectBase(name) {
55
+ const text = String(name ?? '').trim();
56
+ if (!text)
57
+ return 'codex';
58
+ const out = text.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '').toLowerCase();
59
+ return out || 'codex';
60
+ }
61
+ function deriveProjectIdFromCwd(cwd) {
62
+ const base = sanitizeProjectBase(path.basename(String(cwd ?? '').trim()) || 'codex');
63
+ const hash = createHash('sha1').update(String(cwd ?? '')).digest('hex').slice(0, 6);
64
+ return `${base}-${hash}`;
65
+ }
66
+ function usageFromTotals(start, end) {
67
+ if (!start || !end)
68
+ return undefined;
69
+ const input = Math.max(0, Number(end.input_tokens ?? 0) - Number(start.input_tokens ?? 0));
70
+ const output = Math.max(0, Number(end.output_tokens ?? 0) - Number(start.output_tokens ?? 0));
71
+ const total = Math.max(0, Number(end.total_tokens ?? 0) - Number(start.total_tokens ?? 0));
72
+ if (input === 0 && output === 0 && total === 0)
73
+ return undefined;
74
+ return pickUsage({
75
+ input_tokens: input,
76
+ output_tokens: output,
77
+ total_tokens: total || input + output,
78
+ });
79
+ }
80
+ function createTurn(turnId, startedAt) {
81
+ return {
82
+ turnId,
83
+ startedAt,
84
+ endedAt: startedAt,
85
+ completed: false,
86
+ userInput: '',
87
+ lastAgentMessage: '',
88
+ model: undefined,
89
+ cwd: undefined,
90
+ calls: [],
91
+ lastUsage: undefined,
92
+ totalUsageStart: undefined,
93
+ totalUsageEnd: undefined,
94
+ };
95
+ }
96
+ async function findSessionTranscript(sessionsRoot, sessionId) {
97
+ const targetName = `${sessionId}.jsonl`;
98
+ const stack = [sessionsRoot];
99
+ while (stack.length > 0) {
100
+ const dir = stack.pop();
101
+ let entries = [];
102
+ try {
103
+ entries = await readdir(dir, { withFileTypes: true });
104
+ }
105
+ catch {
106
+ continue;
107
+ }
108
+ for (const entry of entries) {
109
+ const full = path.join(dir, entry.name);
110
+ if (entry.isDirectory()) {
111
+ stack.push(full);
112
+ continue;
113
+ }
114
+ if (entry.isFile() && entry.name === targetName) {
115
+ return full;
116
+ }
117
+ }
118
+ }
119
+ throw new Error(`codex session transcript not found: ${targetName} under ${sessionsRoot}`);
120
+ }
121
+ function toIso(timestamp) {
122
+ return parseTimestamp(timestamp).toISOString();
123
+ }
124
+ function buildMessagesFromTurns(turns, runtimeVersion) {
125
+ const messages = [];
126
+ for (const turn of turns) {
127
+ const input = String(turn.userInput ?? '').trim() || '(no user message captured)';
128
+ messages.push({
129
+ role: 'user',
130
+ isMeta: false,
131
+ content: input,
132
+ runtimeVersion,
133
+ messageId: `${turn.turnId}:user`,
134
+ timestamp: parseTimestamp(turn.startedAt),
135
+ });
136
+ for (let i = 0; i < turn.calls.length; i += 1) {
137
+ const call = turn.calls[i];
138
+ const callId = String(call.callId ?? `call-${i + 1}`);
139
+ messages.push({
140
+ role: 'assistant',
141
+ isMeta: false,
142
+ content: [
143
+ {
144
+ type: 'tool_use',
145
+ id: callId,
146
+ name: call.toolName,
147
+ input: call.input ?? {},
148
+ },
149
+ ],
150
+ model: turn.model,
151
+ runtimeVersion,
152
+ messageId: `${turn.turnId}:tool-use:${i + 1}`,
153
+ timestamp: parseTimestamp(call.startedAt ?? turn.startedAt),
154
+ });
155
+ messages.push({
156
+ role: 'user',
157
+ isMeta: false,
158
+ content: [
159
+ {
160
+ type: 'tool_result',
161
+ tool_use_id: callId,
162
+ content: String(call.output ?? ''),
163
+ },
164
+ ],
165
+ sourceToolUseId: callId,
166
+ toolUseResult: {
167
+ stdout: String(call.output ?? ''),
168
+ },
169
+ runtimeVersion,
170
+ messageId: `${turn.turnId}:tool-result:${i + 1}`,
171
+ timestamp: parseTimestamp(call.endedAt ?? call.startedAt ?? turn.endedAt),
172
+ });
173
+ }
174
+ messages.push({
175
+ role: 'assistant',
176
+ isMeta: false,
177
+ content: [{ type: 'text', text: String(turn.lastAgentMessage ?? '') }],
178
+ model: turn.model,
179
+ usage: turn.lastUsage ?? usageFromTotals(turn.totalUsageStart, turn.totalUsageEnd),
180
+ runtimeVersion,
181
+ messageId: `${turn.turnId}:assistant`,
182
+ timestamp: parseTimestamp(turn.endedAt),
183
+ });
184
+ }
185
+ messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
186
+ return messages;
187
+ }
188
+ async function readCodexSession(transcriptPath) {
189
+ const raw = await readFile(transcriptPath, 'utf-8');
190
+ const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean);
191
+ const turns = [];
192
+ let currentTurn = null;
193
+ let pendingUserInput = '';
194
+ let sessionMeta = null;
195
+ let callCounter = 0;
196
+ const callIndex = new Map();
197
+ function finalizeCurrentTurn(at) {
198
+ if (!currentTurn)
199
+ return;
200
+ currentTurn.endedAt = at ? toIso(at) : currentTurn.endedAt;
201
+ turns.push(currentTurn);
202
+ currentTurn = null;
203
+ }
204
+ function ensureCurrentTurn(at) {
205
+ if (currentTurn)
206
+ return currentTurn;
207
+ currentTurn = createTurn(`turn-codex-${turns.length + 1}`, toIso(at));
208
+ if (pendingUserInput) {
209
+ currentTurn.userInput = pendingUserInput;
210
+ pendingUserInput = '';
211
+ }
212
+ return currentTurn;
213
+ }
214
+ for (const line of lines) {
215
+ let entry;
216
+ try {
217
+ entry = JSON.parse(line);
218
+ }
219
+ catch {
220
+ continue;
221
+ }
222
+ const isoAt = toIso(entry.timestamp);
223
+ if (entry.type === 'session_meta') {
224
+ sessionMeta = entry.payload ?? {};
225
+ continue;
226
+ }
227
+ if (entry.type === 'turn_context') {
228
+ const turn = ensureCurrentTurn(entry.timestamp);
229
+ const payload = entry.payload ?? {};
230
+ if (payload.turn_id && turn.turnId.startsWith('turn-codex-')) {
231
+ turn.turnId = payload.turn_id;
232
+ }
233
+ if (payload.model)
234
+ turn.model = payload.model;
235
+ if (payload.cwd)
236
+ turn.cwd = payload.cwd;
237
+ continue;
238
+ }
239
+ if (entry.type === 'event_msg') {
240
+ const payload = entry.payload ?? {};
241
+ if (payload.type === 'task_started') {
242
+ finalizeCurrentTurn(entry.timestamp);
243
+ currentTurn = createTurn(payload.turn_id ?? `turn-codex-${turns.length + 1}`, isoAt);
244
+ if (pendingUserInput) {
245
+ currentTurn.userInput = pendingUserInput;
246
+ pendingUserInput = '';
247
+ }
248
+ continue;
249
+ }
250
+ if (payload.type === 'user_message') {
251
+ const text = String(payload.message ?? '').trim();
252
+ if (!text)
253
+ continue;
254
+ if (currentTurn && !currentTurn.userInput) {
255
+ currentTurn.userInput = text;
256
+ }
257
+ else {
258
+ pendingUserInput = text;
259
+ }
260
+ continue;
261
+ }
262
+ if (payload.type === 'token_count') {
263
+ const turn = ensureCurrentTurn(entry.timestamp);
264
+ const info = payload.info ?? {};
265
+ const lastUsage = pickUsage(info.last_token_usage ?? info.lastTokenUsage);
266
+ if (lastUsage) {
267
+ turn.lastUsage = lastUsage;
268
+ }
269
+ const totalUsage = pickUsage(info.total_token_usage ?? info.totalTokenUsage);
270
+ if (totalUsage) {
271
+ if (!turn.totalUsageStart)
272
+ turn.totalUsageStart = totalUsage;
273
+ turn.totalUsageEnd = totalUsage;
274
+ }
275
+ continue;
276
+ }
277
+ if (payload.type === 'agent_message') {
278
+ const turn = ensureCurrentTurn(entry.timestamp);
279
+ if (payload.phase === 'final_answer') {
280
+ turn.lastAgentMessage = String(payload.message ?? turn.lastAgentMessage ?? '');
281
+ }
282
+ continue;
283
+ }
284
+ if (payload.type === 'task_complete' || payload.type === 'turn_aborted') {
285
+ const turn = ensureCurrentTurn(entry.timestamp);
286
+ if (payload.turn_id)
287
+ turn.turnId = payload.turn_id;
288
+ if (payload.last_agent_message) {
289
+ turn.lastAgentMessage = String(payload.last_agent_message);
290
+ }
291
+ turn.completed = true;
292
+ turn.endedAt = isoAt;
293
+ finalizeCurrentTurn(entry.timestamp);
294
+ continue;
295
+ }
296
+ continue;
297
+ }
298
+ if (entry.type !== 'response_item')
299
+ continue;
300
+ const payload = entry.payload ?? {};
301
+ if (payload.type === 'message' && payload.role === 'assistant' && Array.isArray(payload.content)) {
302
+ const turn = ensureCurrentTurn(entry.timestamp);
303
+ const text = payload.content
304
+ .map((block) => {
305
+ if (block?.type === 'output_text' || block?.type === 'input_text')
306
+ return String(block.text ?? '');
307
+ if (typeof block?.text === 'string')
308
+ return block.text;
309
+ if (typeof block?.output_text === 'string')
310
+ return block.output_text;
311
+ return '';
312
+ })
313
+ .filter(Boolean)
314
+ .join('\n')
315
+ .trim();
316
+ if (text)
317
+ turn.lastAgentMessage = text;
318
+ continue;
319
+ }
320
+ if (payload.type === 'function_call' || payload.type === 'custom_tool_call') {
321
+ const turn = ensureCurrentTurn(entry.timestamp);
322
+ callCounter += 1;
323
+ const rawName = payload.name ?? payload.tool_name ?? payload.toolName;
324
+ const rawInput = payload.type === 'custom_tool_call' ? payload.input : payload.arguments;
325
+ const args = safeJsonParse(rawInput, typeof rawInput === 'string' ? { raw: rawInput } : {});
326
+ const normalized = normalizeToolCall(rawName, args, callCounter);
327
+ const callId = String(payload.call_id ?? payload.callId ?? normalized.callId ?? `call-${callCounter}`);
328
+ const call = {
329
+ callId,
330
+ toolName: normalized.toolName,
331
+ input: normalized.input,
332
+ output: '',
333
+ startedAt: isoAt,
334
+ endedAt: isoAt,
335
+ };
336
+ turn.calls.push(call);
337
+ callIndex.set(callId, call);
338
+ continue;
339
+ }
340
+ if (payload.type === 'function_call_output' || payload.type === 'custom_tool_call_output') {
341
+ const callId = String(payload.call_id ?? payload.callId ?? '');
342
+ const call = callIndex.get(callId);
343
+ if (call) {
344
+ call.output = String(payload.output ?? '');
345
+ call.endedAt = isoAt;
346
+ }
347
+ continue;
348
+ }
349
+ if (payload.type === 'web_search_call') {
350
+ const turn = ensureCurrentTurn(entry.timestamp);
351
+ callCounter += 1;
352
+ const call = {
353
+ callId: `call-web-search-${callCounter}`,
354
+ toolName: 'WebSearch',
355
+ input: payload.action ?? {},
356
+ output: JSON.stringify({
357
+ status: payload.status ?? 'unknown',
358
+ query: payload.action?.query ?? '',
359
+ }),
360
+ startedAt: isoAt,
361
+ endedAt: isoAt,
362
+ };
363
+ turn.calls.push(call);
364
+ continue;
365
+ }
366
+ }
367
+ finalizeCurrentTurn(new Date().toISOString());
368
+ const runtimeVersion = String(sessionMeta?.cli_version ?? '').trim() || undefined;
369
+ const cwd = String(sessionMeta?.cwd ?? '').trim() || undefined;
370
+ return {
371
+ turns,
372
+ runtimeVersion,
373
+ cwd,
374
+ };
375
+ }
376
+ function remapTurnIds(events, turns) {
377
+ const generatedTurnIds = events
378
+ .filter((event) => event.category === 'turn')
379
+ .map((event) => event.turnId);
380
+ const map = new Map();
381
+ for (let i = 0; i < generatedTurnIds.length; i += 1) {
382
+ const generated = generatedTurnIds[i];
383
+ const rawTurnId = turns[i]?.turnId;
384
+ if (generated && rawTurnId)
385
+ map.set(generated, rawTurnId);
386
+ }
387
+ return events.map((event) => {
388
+ const mappedTurnId = map.get(event.turnId);
389
+ if (!mappedTurnId)
390
+ return event;
391
+ return {
392
+ ...event,
393
+ turnId: mappedTurnId,
394
+ name: event.category === 'turn' ? `codex - Turn ${mappedTurnId}` : event.name,
395
+ };
396
+ });
397
+ }
398
+ function attachCwdAttribute(events, cwd) {
399
+ if (!cwd)
400
+ return events;
401
+ return events.map((event) => ({
402
+ ...event,
403
+ attributes: {
404
+ ...(event.attributes ?? {}),
405
+ 'agentic.project.cwd': cwd,
406
+ },
407
+ }));
408
+ }
409
+ function attachTurnCompletionAttribute(events, turns) {
410
+ const completionByTurnId = new Map(turns.map((turn) => [turn.turnId, Boolean(turn.completed)]));
411
+ return events.map((event) => {
412
+ if (event.category !== 'turn')
413
+ return event;
414
+ return {
415
+ ...event,
416
+ attributes: {
417
+ ...(event.attributes ?? {}),
418
+ 'agentic.turn.completed': completionByTurnId.get(event.turnId) ?? false,
419
+ },
420
+ };
421
+ });
422
+ }
423
+ function resolveRuntimeHome(context) {
424
+ return context.runtimeHome ?? process.env.SPANORY_CODEX_HOME ?? path.join(process.env.HOME || '', '.codex');
425
+ }
426
+ export const codexAdapter = {
427
+ runtimeName: 'codex',
428
+ capabilities: RUNTIME_CAPABILITIES.codex,
429
+ resolveContextFromHook(payload) {
430
+ const sessionId = payload.sessionId;
431
+ if (!sessionId)
432
+ return null;
433
+ const projectId = payload.cwd ? deriveProjectIdFromCwd(payload.cwd) : 'codex';
434
+ return {
435
+ projectId,
436
+ sessionId,
437
+ ...(payload.transcriptPath ? { transcriptPath: payload.transcriptPath } : {}),
438
+ };
439
+ },
440
+ async collectEvents(context) {
441
+ const runtimeHome = resolveRuntimeHome(context);
442
+ const transcriptPath = context.transcriptPath
443
+ ?? await findSessionTranscript(path.join(runtimeHome, 'sessions'), context.sessionId);
444
+ const parsed = await readCodexSession(transcriptPath);
445
+ const projectId = context.projectId || deriveProjectIdFromCwd(parsed.cwd ?? '');
446
+ const messages = buildMessagesFromTurns(parsed.turns, parsed.runtimeVersion);
447
+ const normalized = normalizeTranscriptMessages({
448
+ runtime: 'codex',
449
+ projectId,
450
+ sessionId: context.sessionId,
451
+ messages,
452
+ });
453
+ const withRawTurnIds = remapTurnIds(normalized, parsed.turns);
454
+ const withCompletion = attachTurnCompletionAttribute(withRawTurnIds, parsed.turns);
455
+ return attachCwdAttribute(withCompletion, parsed.cwd);
456
+ },
457
+ };
@@ -0,0 +1,9 @@
1
+ export declare function createCodexProxyServer(options: any): {
2
+ start({ host, port }?: {
3
+ host?: string;
4
+ port?: number;
5
+ }): Promise<void>;
6
+ stop(): Promise<void>;
7
+ url(): string;
8
+ server: import("http").Server<typeof import("http").IncomingMessage, typeof import("http").ServerResponse>;
9
+ };
@@ -0,0 +1,212 @@
1
+ // @ts-nocheck
2
+ import { randomUUID } from 'node:crypto';
3
+ import { mkdir, writeFile } from 'node:fs/promises';
4
+ import { createServer } from 'node:http';
5
+ import path from 'node:path';
6
+ const REDACTED = '[REDACTED]';
7
+ const SENSITIVE_KEY_RE = /(authorization|cookie|set-cookie|x-api-key|api[-_]?key|token|password|secret)/i;
8
+ function isSensitiveKey(key) {
9
+ return SENSITIVE_KEY_RE.test(String(key ?? ''));
10
+ }
11
+ function normalizeHeaderValue(value) {
12
+ if (Array.isArray(value))
13
+ return value.join(', ');
14
+ if (value === undefined || value === null)
15
+ return '';
16
+ return String(value);
17
+ }
18
+ function redactHeaders(headers) {
19
+ const out = {};
20
+ for (const [key, value] of Object.entries(headers ?? {})) {
21
+ const lowerKey = String(key).toLowerCase();
22
+ out[lowerKey] = isSensitiveKey(lowerKey) ? REDACTED : normalizeHeaderValue(value);
23
+ }
24
+ return out;
25
+ }
26
+ function truncateText(text, maxBytes) {
27
+ const raw = String(text ?? '');
28
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0)
29
+ return raw;
30
+ if (Buffer.byteLength(raw, 'utf8') <= maxBytes)
31
+ return raw;
32
+ let end = raw.length;
33
+ while (end > 0 && Buffer.byteLength(raw.slice(0, end), 'utf8') > maxBytes)
34
+ end -= 1;
35
+ return `${raw.slice(0, Math.max(0, end))}...[truncated]`;
36
+ }
37
+ function redactBody(value, maxBytes) {
38
+ function walk(current, keyHint = '') {
39
+ if (current === null || current === undefined)
40
+ return current;
41
+ if (typeof current === 'string') {
42
+ if (isSensitiveKey(keyHint))
43
+ return REDACTED;
44
+ return truncateText(current, maxBytes);
45
+ }
46
+ if (typeof current === 'number' || typeof current === 'boolean') {
47
+ if (isSensitiveKey(keyHint))
48
+ return REDACTED;
49
+ return current;
50
+ }
51
+ if (Array.isArray(current)) {
52
+ return current.map((item) => walk(item, keyHint));
53
+ }
54
+ if (typeof current === 'object') {
55
+ const out = {};
56
+ for (const [key, val] of Object.entries(current)) {
57
+ if (isSensitiveKey(key)) {
58
+ out[key] = REDACTED;
59
+ }
60
+ else {
61
+ out[key] = walk(val, key);
62
+ }
63
+ }
64
+ return out;
65
+ }
66
+ return truncateText(String(current), maxBytes);
67
+ }
68
+ const redacted = walk(value);
69
+ if (!Number.isFinite(maxBytes) || maxBytes <= 0)
70
+ return redacted;
71
+ const serialized = JSON.stringify(redacted);
72
+ if (Buffer.byteLength(serialized, 'utf8') <= maxBytes)
73
+ return redacted;
74
+ return {
75
+ __truncated__: true,
76
+ preview: truncateText(serialized, maxBytes),
77
+ };
78
+ }
79
+ function parseBodyFromBuffer(buffer, contentType, maxBytes) {
80
+ if (!buffer || buffer.length === 0)
81
+ return '';
82
+ const text = buffer.toString('utf8');
83
+ if (String(contentType ?? '').toLowerCase().includes('application/json')) {
84
+ try {
85
+ return redactBody(JSON.parse(text), maxBytes);
86
+ }
87
+ catch {
88
+ return truncateText(text, maxBytes);
89
+ }
90
+ }
91
+ return truncateText(text, maxBytes);
92
+ }
93
+ async function readRequestBuffer(req) {
94
+ const chunks = [];
95
+ for await (const chunk of req)
96
+ chunks.push(chunk);
97
+ return Buffer.concat(chunks);
98
+ }
99
+ async function writeCaptureRecord(spoolDir, record, logger) {
100
+ try {
101
+ await mkdir(spoolDir, { recursive: true });
102
+ const filename = `${Date.now()}-${randomUUID()}.json`;
103
+ const file = path.join(spoolDir, filename);
104
+ await writeFile(file, JSON.stringify(record, null, 2), 'utf8');
105
+ }
106
+ catch (error) {
107
+ logger?.warn?.(`[spanory-codex-proxy] capture write failed: ${String(error)}`);
108
+ }
109
+ }
110
+ function correlationKeyFromRequest(req, seq) {
111
+ const headers = req.headers ?? {};
112
+ const threadId = headers['x-codex-thread-id'] ?? headers['x-thread-id'] ?? headers['x-session-id'] ?? '';
113
+ const turnId = headers['x-codex-turn-id'] ?? headers['x-turn-id'] ?? '';
114
+ if (threadId || turnId)
115
+ return `${threadId || 'na'}:${turnId || 'na'}:${seq}`;
116
+ return `unknown:unknown:${seq}`;
117
+ }
118
+ export function createCodexProxyServer(options) {
119
+ const upstreamBaseUrl = options?.upstreamBaseUrl ?? process.env.OPENAI_BASE_URL ?? 'https://api.openai.com';
120
+ const spoolDir = options?.spoolDir ?? process.env.SPANORY_CODEX_PROXY_SPOOL_DIR ?? path.join(process.cwd(), '.spanory', 'codex-proxy-spool');
121
+ const maxBodyBytes = Number(options?.maxBodyBytes ?? process.env.SPANORY_CODEX_CAPTURE_MAX_BYTES ?? 131072);
122
+ const logger = options?.logger ?? console;
123
+ const upstream = new URL(upstreamBaseUrl);
124
+ let seq = 0;
125
+ const server = createServer(async (req, res) => {
126
+ const startedAt = Date.now();
127
+ seq += 1;
128
+ const requestBodyBuffer = await readRequestBuffer(req);
129
+ const correlationKey = correlationKeyFromRequest(req, seq);
130
+ const method = req.method ?? 'GET';
131
+ const targetUrl = new URL(req.url ?? '/', upstream);
132
+ const requestHeaders = { ...req.headers };
133
+ delete requestHeaders.host;
134
+ delete requestHeaders['content-length'];
135
+ try {
136
+ const upstreamResponse = await fetch(targetUrl, {
137
+ method,
138
+ headers: requestHeaders,
139
+ body: ['GET', 'HEAD'].includes(method.toUpperCase()) ? undefined : requestBodyBuffer,
140
+ });
141
+ const responseBuffer = Buffer.from(await upstreamResponse.arrayBuffer());
142
+ const responseHeaders = Object.fromEntries(upstreamResponse.headers.entries());
143
+ const record = {
144
+ timestamp: new Date().toISOString(),
145
+ metadata: {
146
+ capture_mode: 'full_redacted',
147
+ correlation_key: correlationKey,
148
+ latency_ms: Date.now() - startedAt,
149
+ },
150
+ request: {
151
+ method,
152
+ url: req.url ?? '/',
153
+ headers: redactHeaders(req.headers),
154
+ body: parseBodyFromBuffer(requestBodyBuffer, req.headers['content-type'], maxBodyBytes),
155
+ },
156
+ response: {
157
+ status: upstreamResponse.status,
158
+ headers: redactHeaders(responseHeaders),
159
+ body: parseBodyFromBuffer(responseBuffer, upstreamResponse.headers.get('content-type'), maxBodyBytes),
160
+ },
161
+ };
162
+ await writeCaptureRecord(spoolDir, record, logger);
163
+ for (const [key, value] of Object.entries(responseHeaders)) {
164
+ if (value !== undefined)
165
+ res.setHeader(key, value);
166
+ }
167
+ res.statusCode = upstreamResponse.status;
168
+ res.end(responseBuffer);
169
+ }
170
+ catch (error) {
171
+ const message = error instanceof Error ? error.message : String(error);
172
+ res.statusCode = 502;
173
+ res.setHeader('content-type', 'application/json');
174
+ res.end(JSON.stringify({ error: 'upstream_request_failed', message }));
175
+ await writeCaptureRecord(spoolDir, {
176
+ timestamp: new Date().toISOString(),
177
+ metadata: {
178
+ capture_mode: 'full_redacted',
179
+ correlation_key: correlationKey,
180
+ latency_ms: Date.now() - startedAt,
181
+ },
182
+ request: {
183
+ method,
184
+ url: req.url ?? '/',
185
+ headers: redactHeaders(req.headers),
186
+ body: parseBodyFromBuffer(requestBodyBuffer, req.headers['content-type'], maxBodyBytes),
187
+ },
188
+ response: {
189
+ status: 502,
190
+ error: message,
191
+ },
192
+ }, logger);
193
+ }
194
+ });
195
+ return {
196
+ async start({ host = '127.0.0.1', port = 8787 } = {}) {
197
+ await new Promise((resolve) => server.listen(port, host, resolve));
198
+ },
199
+ async stop() {
200
+ if (!server.listening)
201
+ return;
202
+ await new Promise((resolve) => server.close(resolve));
203
+ },
204
+ url() {
205
+ const address = server.address();
206
+ if (!address || typeof address === 'string')
207
+ return '';
208
+ return `http://${address.address}:${address.port}`;
209
+ },
210
+ server,
211
+ };
212
+ }
@@ -0,0 +1,18 @@
1
+ export declare const openclawAdapter: {
2
+ runtimeName: string;
3
+ capabilities: {
4
+ turnDetection: boolean;
5
+ toolCallAttribution: boolean;
6
+ toolResultCorrelation: boolean;
7
+ modelName: boolean;
8
+ usageDetails: boolean;
9
+ slashCommandExtraction: boolean;
10
+ mcpServerExtraction: boolean;
11
+ };
12
+ resolveContextFromHook(payload: any): {
13
+ projectId: any;
14
+ sessionId: any;
15
+ transcriptPath: any;
16
+ };
17
+ collectEvents(context: any): Promise<any[]>;
18
+ };