@bububuger/spanory 0.1.18 → 0.1.19
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/index.js +5109 -1798
- package/package.json +6 -4
- package/dist/alert/evaluate.d.ts +0 -3
- package/dist/alert/evaluate.js +0 -348
- package/dist/env.d.ts +0 -6
- package/dist/env.js +0 -82
- package/dist/issue/state.d.ts +0 -39
- package/dist/issue/state.js +0 -151
- package/dist/otlp.d.ts +0 -5
- package/dist/otlp.js +0 -12
- package/dist/report/aggregate.d.ts +0 -22
- package/dist/report/aggregate.js +0 -371
- package/dist/runtime/claude/adapter.d.ts +0 -18
- package/dist/runtime/claude/adapter.js +0 -222
- package/dist/runtime/codex/adapter.d.ts +0 -18
- package/dist/runtime/codex/adapter.js +0 -493
- package/dist/runtime/codex/proxy.d.ts +0 -9
- package/dist/runtime/codex/proxy.js +0 -215
- package/dist/runtime/openclaw/adapter.d.ts +0 -18
- package/dist/runtime/openclaw/adapter.js +0 -380
- package/dist/runtime/shared/capabilities.d.ts +0 -30
- package/dist/runtime/shared/capabilities.js +0 -39
- package/dist/runtime/shared/file-settle.d.ts +0 -19
- package/dist/runtime/shared/file-settle.js +0 -44
- package/dist/runtime/shared/normalize.d.ts +0 -9
- package/dist/runtime/shared/normalize.js +0 -1016
|
@@ -1,215 +0,0 @@
|
|
|
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 spanoryHome = process.env.SPANORY_HOME ?? path.join(process.env.HOME || '', '.spanory');
|
|
121
|
-
const spoolDir = options?.spoolDir
|
|
122
|
-
?? process.env.SPANORY_CODEX_PROXY_SPOOL_DIR
|
|
123
|
-
?? path.join(spanoryHome, 'spool', 'codex-proxy');
|
|
124
|
-
const maxBodyBytes = Number(options?.maxBodyBytes ?? process.env.SPANORY_CODEX_CAPTURE_MAX_BYTES ?? 131072);
|
|
125
|
-
const logger = options?.logger ?? console;
|
|
126
|
-
const upstream = new URL(upstreamBaseUrl);
|
|
127
|
-
let seq = 0;
|
|
128
|
-
const server = createServer(async (req, res) => {
|
|
129
|
-
const startedAt = Date.now();
|
|
130
|
-
seq += 1;
|
|
131
|
-
const requestBodyBuffer = await readRequestBuffer(req);
|
|
132
|
-
const correlationKey = correlationKeyFromRequest(req, seq);
|
|
133
|
-
const method = req.method ?? 'GET';
|
|
134
|
-
const targetUrl = new URL(req.url ?? '/', upstream);
|
|
135
|
-
const requestHeaders = { ...req.headers };
|
|
136
|
-
delete requestHeaders.host;
|
|
137
|
-
delete requestHeaders['content-length'];
|
|
138
|
-
try {
|
|
139
|
-
const upstreamResponse = await fetch(targetUrl, {
|
|
140
|
-
method,
|
|
141
|
-
headers: requestHeaders,
|
|
142
|
-
body: ['GET', 'HEAD'].includes(method.toUpperCase()) ? undefined : requestBodyBuffer,
|
|
143
|
-
});
|
|
144
|
-
const responseBuffer = Buffer.from(await upstreamResponse.arrayBuffer());
|
|
145
|
-
const responseHeaders = Object.fromEntries(upstreamResponse.headers.entries());
|
|
146
|
-
const record = {
|
|
147
|
-
timestamp: new Date().toISOString(),
|
|
148
|
-
metadata: {
|
|
149
|
-
capture_mode: 'full_redacted',
|
|
150
|
-
correlation_key: correlationKey,
|
|
151
|
-
latency_ms: Date.now() - startedAt,
|
|
152
|
-
},
|
|
153
|
-
request: {
|
|
154
|
-
method,
|
|
155
|
-
url: req.url ?? '/',
|
|
156
|
-
headers: redactHeaders(req.headers),
|
|
157
|
-
body: parseBodyFromBuffer(requestBodyBuffer, req.headers['content-type'], maxBodyBytes),
|
|
158
|
-
},
|
|
159
|
-
response: {
|
|
160
|
-
status: upstreamResponse.status,
|
|
161
|
-
headers: redactHeaders(responseHeaders),
|
|
162
|
-
body: parseBodyFromBuffer(responseBuffer, upstreamResponse.headers.get('content-type'), maxBodyBytes),
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
await writeCaptureRecord(spoolDir, record, logger);
|
|
166
|
-
for (const [key, value] of Object.entries(responseHeaders)) {
|
|
167
|
-
if (value !== undefined)
|
|
168
|
-
res.setHeader(key, value);
|
|
169
|
-
}
|
|
170
|
-
res.statusCode = upstreamResponse.status;
|
|
171
|
-
res.end(responseBuffer);
|
|
172
|
-
}
|
|
173
|
-
catch (error) {
|
|
174
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
-
res.statusCode = 502;
|
|
176
|
-
res.setHeader('content-type', 'application/json');
|
|
177
|
-
res.end(JSON.stringify({ error: 'upstream_request_failed', message }));
|
|
178
|
-
await writeCaptureRecord(spoolDir, {
|
|
179
|
-
timestamp: new Date().toISOString(),
|
|
180
|
-
metadata: {
|
|
181
|
-
capture_mode: 'full_redacted',
|
|
182
|
-
correlation_key: correlationKey,
|
|
183
|
-
latency_ms: Date.now() - startedAt,
|
|
184
|
-
},
|
|
185
|
-
request: {
|
|
186
|
-
method,
|
|
187
|
-
url: req.url ?? '/',
|
|
188
|
-
headers: redactHeaders(req.headers),
|
|
189
|
-
body: parseBodyFromBuffer(requestBodyBuffer, req.headers['content-type'], maxBodyBytes),
|
|
190
|
-
},
|
|
191
|
-
response: {
|
|
192
|
-
status: 502,
|
|
193
|
-
error: message,
|
|
194
|
-
},
|
|
195
|
-
}, logger);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
return {
|
|
199
|
-
async start({ host = '127.0.0.1', port = 8787 } = {}) {
|
|
200
|
-
await new Promise((resolve) => server.listen(port, host, resolve));
|
|
201
|
-
},
|
|
202
|
-
async stop() {
|
|
203
|
-
if (!server.listening)
|
|
204
|
-
return;
|
|
205
|
-
await new Promise((resolve) => server.close(resolve));
|
|
206
|
-
},
|
|
207
|
-
url() {
|
|
208
|
-
const address = server.address();
|
|
209
|
-
if (!address || typeof address === 'string')
|
|
210
|
-
return '';
|
|
211
|
-
return `http://${address.address}:${address.port}`;
|
|
212
|
-
},
|
|
213
|
-
server,
|
|
214
|
-
};
|
|
215
|
-
}
|
|
@@ -1,18 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,380 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { readdir, readFile, stat } from 'node:fs/promises';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import { RUNTIME_CAPABILITIES } from '../shared/capabilities.js';
|
|
5
|
-
import { normalizeTranscriptMessages, parseProjectIdFromTranscriptPath, pickUsage } from '../shared/normalize.js';
|
|
6
|
-
const INFER_WINDOW_EPSILON_MS = 1200;
|
|
7
|
-
function parseTimestamp(entry) {
|
|
8
|
-
const raw = entry?.timestamp ?? entry?.created_at ?? entry?.createdAt;
|
|
9
|
-
const date = raw ? new Date(raw) : new Date();
|
|
10
|
-
if (Number.isNaN(date.getTime()))
|
|
11
|
-
return new Date();
|
|
12
|
-
return date;
|
|
13
|
-
}
|
|
14
|
-
function normalizeToolName(name) {
|
|
15
|
-
if (name === 'exec')
|
|
16
|
-
return 'Bash';
|
|
17
|
-
return name;
|
|
18
|
-
}
|
|
19
|
-
function normalizeContentBlocks(content) {
|
|
20
|
-
if (typeof content === 'string')
|
|
21
|
-
return content;
|
|
22
|
-
if (!Array.isArray(content))
|
|
23
|
-
return '';
|
|
24
|
-
const blocks = [];
|
|
25
|
-
for (const block of content) {
|
|
26
|
-
if (typeof block === 'string') {
|
|
27
|
-
blocks.push(block);
|
|
28
|
-
continue;
|
|
29
|
-
}
|
|
30
|
-
if (!block || typeof block !== 'object')
|
|
31
|
-
continue;
|
|
32
|
-
if (block.type === 'text') {
|
|
33
|
-
blocks.push({ type: 'text', text: block.text ?? '' });
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
if (block.type === 'toolCall') {
|
|
37
|
-
blocks.push({
|
|
38
|
-
type: 'tool_use',
|
|
39
|
-
id: block.id ?? block.toolCallId ?? '',
|
|
40
|
-
name: normalizeToolName(block.name ?? block.toolName ?? ''),
|
|
41
|
-
input: block.arguments ?? block.input ?? {},
|
|
42
|
-
});
|
|
43
|
-
continue;
|
|
44
|
-
}
|
|
45
|
-
if (block.type === 'toolResult') {
|
|
46
|
-
blocks.push({
|
|
47
|
-
type: 'tool_result',
|
|
48
|
-
tool_use_id: block.toolCallId ?? block.tool_call_id ?? block.id ?? '',
|
|
49
|
-
content: block.content ?? '',
|
|
50
|
-
});
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
if (block.type === 'tool_use' || block.type === 'tool_result') {
|
|
54
|
-
blocks.push(block);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return blocks;
|
|
59
|
-
}
|
|
60
|
-
function normalizeRole(entry) {
|
|
61
|
-
if (entry?.type === 'message') {
|
|
62
|
-
const role = entry?.message?.role;
|
|
63
|
-
if (role === 'toolResult')
|
|
64
|
-
return 'user';
|
|
65
|
-
if (role)
|
|
66
|
-
return role;
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
if (entry?.role)
|
|
70
|
-
return entry.role;
|
|
71
|
-
if (entry?.message?.role)
|
|
72
|
-
return entry.message.role;
|
|
73
|
-
if (entry?.payload?.role)
|
|
74
|
-
return entry.payload.role;
|
|
75
|
-
if (entry?.type === 'user' || entry?.type === 'assistant' || entry?.type === 'system')
|
|
76
|
-
return entry.type;
|
|
77
|
-
return null;
|
|
78
|
-
}
|
|
79
|
-
function normalizeContent(entry) {
|
|
80
|
-
if (entry?.type === 'message') {
|
|
81
|
-
const role = entry?.message?.role;
|
|
82
|
-
if (role === 'toolResult') {
|
|
83
|
-
return [
|
|
84
|
-
{
|
|
85
|
-
type: 'tool_result',
|
|
86
|
-
tool_use_id: entry?.message?.toolCallId ?? entry?.message?.tool_call_id ?? '',
|
|
87
|
-
content: entry?.message?.content ?? '',
|
|
88
|
-
},
|
|
89
|
-
];
|
|
90
|
-
}
|
|
91
|
-
return normalizeContentBlocks(entry?.message?.content ?? '');
|
|
92
|
-
}
|
|
93
|
-
return normalizeContentBlocks(entry?.message?.content ?? entry?.content ?? entry?.payload?.content ?? '');
|
|
94
|
-
}
|
|
95
|
-
function normalizeModel(entry) {
|
|
96
|
-
return entry?.message?.model ?? entry?.model ?? entry?.payload?.model;
|
|
97
|
-
}
|
|
98
|
-
function normalizeMessageId(entry) {
|
|
99
|
-
return entry?.message?.id ?? entry?.messageId ?? entry?.message_id ?? entry?.id;
|
|
100
|
-
}
|
|
101
|
-
function normalizeToolUseResult(entry) {
|
|
102
|
-
return entry?.toolUseResult ?? entry?.tool_use_result ?? entry?.tool_result;
|
|
103
|
-
}
|
|
104
|
-
function normalizeSourceToolUseId(entry) {
|
|
105
|
-
return (entry?.sourceToolUseID
|
|
106
|
-
?? entry?.sourceToolUseId
|
|
107
|
-
?? entry?.source_tool_use_id
|
|
108
|
-
?? entry?.message?.toolCallId
|
|
109
|
-
?? entry?.message?.tool_call_id);
|
|
110
|
-
}
|
|
111
|
-
function normalizeUsage(entry) {
|
|
112
|
-
const raw = entry?.message?.usage
|
|
113
|
-
?? entry?.usage
|
|
114
|
-
?? entry?.message_usage
|
|
115
|
-
?? entry?.token_usage
|
|
116
|
-
?? entry?.payload?.usage;
|
|
117
|
-
if (!raw || typeof raw !== 'object')
|
|
118
|
-
return undefined;
|
|
119
|
-
return pickUsage({
|
|
120
|
-
input_tokens: raw.input_tokens ?? raw.prompt_tokens ?? raw.input,
|
|
121
|
-
output_tokens: raw.output_tokens ?? raw.completion_tokens ?? raw.output,
|
|
122
|
-
total_tokens: raw.total_tokens ?? raw.totalTokens,
|
|
123
|
-
cache_read_input_tokens: raw.cache_read_input_tokens ?? raw.cacheRead,
|
|
124
|
-
cache_creation_input_tokens: raw.cache_creation_input_tokens ?? raw.cacheWrite,
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
function normalizeIsSidechain(entry) {
|
|
128
|
-
const raw = entry?.isSidechain
|
|
129
|
-
?? entry?.is_sidechain
|
|
130
|
-
?? entry?.message?.isSidechain
|
|
131
|
-
?? entry?.message?.is_sidechain
|
|
132
|
-
?? entry?.payload?.isSidechain
|
|
133
|
-
?? entry?.payload?.is_sidechain;
|
|
134
|
-
return raw === true;
|
|
135
|
-
}
|
|
136
|
-
function normalizeAgentId(entry) {
|
|
137
|
-
return (entry?.agentId
|
|
138
|
-
?? entry?.agent_id
|
|
139
|
-
?? entry?.message?.agentId
|
|
140
|
-
?? entry?.message?.agent_id
|
|
141
|
-
?? entry?.payload?.agentId
|
|
142
|
-
?? entry?.payload?.agent_id);
|
|
143
|
-
}
|
|
144
|
-
function extractToolUses(content) {
|
|
145
|
-
if (!Array.isArray(content))
|
|
146
|
-
return [];
|
|
147
|
-
return content.filter((block) => block && typeof block === 'object' && block.type === 'tool_use');
|
|
148
|
-
}
|
|
149
|
-
function extractToolResults(content) {
|
|
150
|
-
if (!Array.isArray(content))
|
|
151
|
-
return [];
|
|
152
|
-
return content.filter((block) => block && typeof block === 'object' && block.type === 'tool_result');
|
|
153
|
-
}
|
|
154
|
-
function isToolResultOnlyContent(content) {
|
|
155
|
-
return Array.isArray(content)
|
|
156
|
-
&& content.length > 0
|
|
157
|
-
&& content.every((block) => block && typeof block === 'object' && block.type === 'tool_result');
|
|
158
|
-
}
|
|
159
|
-
function isPromptUserMessage(message) {
|
|
160
|
-
if (!message || message.role !== 'user' || message.isMeta)
|
|
161
|
-
return false;
|
|
162
|
-
const { content } = message;
|
|
163
|
-
if (typeof content === 'string')
|
|
164
|
-
return content.trim().length > 0;
|
|
165
|
-
if (!Array.isArray(content))
|
|
166
|
-
return false;
|
|
167
|
-
if (isToolResultOnlyContent(content))
|
|
168
|
-
return false;
|
|
169
|
-
return content.length > 0;
|
|
170
|
-
}
|
|
171
|
-
function findChildSessionHints(messages) {
|
|
172
|
-
const hasSidechainSignal = messages.some((m) => m?.isSidechain === true || String(m?.agentId ?? '').trim().length > 0);
|
|
173
|
-
if (!hasSidechainSignal)
|
|
174
|
-
return null;
|
|
175
|
-
const hasParentLink = messages.some((m) => String(m?.parentSessionId ?? m?.parent_session_id ?? '').trim().length > 0
|
|
176
|
-
|| String(m?.parentTurnId ?? m?.parent_turn_id ?? '').trim().length > 0
|
|
177
|
-
|| String(m?.parentToolCallId ?? m?.parent_tool_call_id ?? '').trim().length > 0);
|
|
178
|
-
if (hasParentLink)
|
|
179
|
-
return null;
|
|
180
|
-
const sorted = [...messages].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
181
|
-
const childStartedAt = sorted[0]?.timestamp;
|
|
182
|
-
if (!childStartedAt)
|
|
183
|
-
return null;
|
|
184
|
-
return { childStartedAt };
|
|
185
|
-
}
|
|
186
|
-
function extractTaskWindows(messages, sessionId) {
|
|
187
|
-
const sorted = [...messages].sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
188
|
-
const windows = [];
|
|
189
|
-
const byCallId = new Map();
|
|
190
|
-
let turnIndex = 0;
|
|
191
|
-
let currentTurnId = 'turn-1';
|
|
192
|
-
for (const msg of sorted) {
|
|
193
|
-
if (isPromptUserMessage(msg)) {
|
|
194
|
-
turnIndex += 1;
|
|
195
|
-
currentTurnId = `turn-${turnIndex}`;
|
|
196
|
-
}
|
|
197
|
-
if (msg.role === 'assistant') {
|
|
198
|
-
for (const tu of extractToolUses(msg.content)) {
|
|
199
|
-
const toolName = String(tu.name ?? '').trim();
|
|
200
|
-
if (toolName !== 'Task')
|
|
201
|
-
continue;
|
|
202
|
-
const callId = String(tu.id ?? '').trim();
|
|
203
|
-
if (!callId)
|
|
204
|
-
continue;
|
|
205
|
-
const window = {
|
|
206
|
-
parentSessionId: sessionId,
|
|
207
|
-
parentTurnId: currentTurnId,
|
|
208
|
-
parentToolCallId: callId,
|
|
209
|
-
startedAtMs: msg.timestamp.getTime(),
|
|
210
|
-
endedAtMs: msg.timestamp.getTime(),
|
|
211
|
-
};
|
|
212
|
-
windows.push(window);
|
|
213
|
-
byCallId.set(callId, window);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
if (msg.role === 'user') {
|
|
217
|
-
for (const tr of extractToolResults(msg.content)) {
|
|
218
|
-
const callId = String(tr.tool_use_id ?? tr.toolUseId ?? '').trim();
|
|
219
|
-
if (!callId)
|
|
220
|
-
continue;
|
|
221
|
-
const window = byCallId.get(callId);
|
|
222
|
-
if (window) {
|
|
223
|
-
window.endedAtMs = Math.max(window.endedAtMs, msg.timestamp.getTime());
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
return windows;
|
|
229
|
-
}
|
|
230
|
-
async function inferParentLinkFromSiblingSessions({ transcriptPath, messages }) {
|
|
231
|
-
const hints = findChildSessionHints(messages);
|
|
232
|
-
if (!hints)
|
|
233
|
-
return messages;
|
|
234
|
-
const currentSessionId = path.basename(transcriptPath, '.jsonl');
|
|
235
|
-
const dir = path.dirname(transcriptPath);
|
|
236
|
-
let entries = [];
|
|
237
|
-
try {
|
|
238
|
-
entries = await readdir(dir, { withFileTypes: true });
|
|
239
|
-
}
|
|
240
|
-
catch {
|
|
241
|
-
return messages;
|
|
242
|
-
}
|
|
243
|
-
const candidates = [];
|
|
244
|
-
for (const entry of entries) {
|
|
245
|
-
if (!entry.isFile() || !entry.name.endsWith('.jsonl'))
|
|
246
|
-
continue;
|
|
247
|
-
const siblingSessionId = entry.name.slice(0, -'.jsonl'.length);
|
|
248
|
-
if (siblingSessionId === currentSessionId)
|
|
249
|
-
continue;
|
|
250
|
-
const siblingPath = path.join(dir, entry.name);
|
|
251
|
-
const siblingMessages = await readOpenclawTranscript(siblingPath);
|
|
252
|
-
const windows = extractTaskWindows(siblingMessages, siblingSessionId);
|
|
253
|
-
for (const window of windows) {
|
|
254
|
-
const childAtMs = hints.childStartedAt.getTime();
|
|
255
|
-
const lower = window.startedAtMs - INFER_WINDOW_EPSILON_MS;
|
|
256
|
-
const upper = window.endedAtMs + INFER_WINDOW_EPSILON_MS;
|
|
257
|
-
if (childAtMs < lower || childAtMs > upper)
|
|
258
|
-
continue;
|
|
259
|
-
candidates.push({
|
|
260
|
-
...window,
|
|
261
|
-
score: Math.abs(childAtMs - window.startedAtMs),
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (candidates.length === 0)
|
|
266
|
-
return messages;
|
|
267
|
-
candidates.sort((a, b) => a.score - b.score);
|
|
268
|
-
const best = candidates[0];
|
|
269
|
-
return messages.map((msg) => ({
|
|
270
|
-
...msg,
|
|
271
|
-
parentSessionId: best.parentSessionId,
|
|
272
|
-
parentTurnId: best.parentTurnId,
|
|
273
|
-
parentToolCallId: best.parentToolCallId,
|
|
274
|
-
parentLinkConfidence: 'inferred',
|
|
275
|
-
}));
|
|
276
|
-
}
|
|
277
|
-
async function readOpenclawTranscript(transcriptPath) {
|
|
278
|
-
const raw = await readFile(transcriptPath, 'utf-8');
|
|
279
|
-
const lines = raw.split('\n').map((line) => line.trim()).filter(Boolean);
|
|
280
|
-
const messages = [];
|
|
281
|
-
let runtimeVersion;
|
|
282
|
-
for (const line of lines) {
|
|
283
|
-
try {
|
|
284
|
-
const entry = JSON.parse(line);
|
|
285
|
-
if (entry?.type === 'session') {
|
|
286
|
-
runtimeVersion = entry?.runtimeVersion
|
|
287
|
-
?? entry?.runtime_version
|
|
288
|
-
?? entry?.openclawVersion
|
|
289
|
-
?? entry?.openclaw_version
|
|
290
|
-
?? entry?.version
|
|
291
|
-
?? runtimeVersion;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
const role = normalizeRole(entry);
|
|
295
|
-
if (!role)
|
|
296
|
-
continue;
|
|
297
|
-
messages.push({
|
|
298
|
-
role,
|
|
299
|
-
isMeta: entry?.isMeta ?? entry?.is_meta ?? false,
|
|
300
|
-
isSidechain: normalizeIsSidechain(entry),
|
|
301
|
-
agentId: normalizeAgentId(entry),
|
|
302
|
-
parentSessionId: entry?.parentSessionId ?? entry?.parent_session_id,
|
|
303
|
-
parentTurnId: entry?.parentTurnId ?? entry?.parent_turn_id,
|
|
304
|
-
parentToolCallId: entry?.parentToolCallId ?? entry?.parent_tool_call_id,
|
|
305
|
-
content: normalizeContent(entry),
|
|
306
|
-
model: normalizeModel(entry),
|
|
307
|
-
usage: normalizeUsage(entry),
|
|
308
|
-
messageId: normalizeMessageId(entry),
|
|
309
|
-
toolUseResult: normalizeToolUseResult(entry),
|
|
310
|
-
sourceToolUseId: normalizeSourceToolUseId(entry),
|
|
311
|
-
runtimeVersion: entry?.runtimeVersion
|
|
312
|
-
?? entry?.runtime_version
|
|
313
|
-
?? entry?.version
|
|
314
|
-
?? entry?.app_version
|
|
315
|
-
?? entry?.appVersion
|
|
316
|
-
?? runtimeVersion,
|
|
317
|
-
timestamp: parseTimestamp(entry),
|
|
318
|
-
});
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
321
|
-
// ignore malformed lines
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
messages.sort((a, b) => a.timestamp.getTime() - b.timestamp.getTime());
|
|
325
|
-
return messages;
|
|
326
|
-
}
|
|
327
|
-
function resolveRuntimeHome(context) {
|
|
328
|
-
return (context.runtimeHome
|
|
329
|
-
?? process.env.SPANORY_OPENCLOW_HOME
|
|
330
|
-
?? process.env.SPANORY_OPENCLAW_HOME
|
|
331
|
-
?? path.join(process.env.HOME || '', '.openclaw'));
|
|
332
|
-
}
|
|
333
|
-
function parseOpenclawProjectId(transcriptPath) {
|
|
334
|
-
return (parseProjectIdFromTranscriptPath(transcriptPath, '/.openclaw/projects/')
|
|
335
|
-
?? parseProjectIdFromTranscriptPath(transcriptPath, '/.openclaw/agents/'));
|
|
336
|
-
}
|
|
337
|
-
async function resolveTranscriptPath(context) {
|
|
338
|
-
if (context.transcriptPath)
|
|
339
|
-
return context.transcriptPath;
|
|
340
|
-
const runtimeHome = resolveRuntimeHome(context);
|
|
341
|
-
const candidates = [
|
|
342
|
-
path.join(runtimeHome, 'projects', context.projectId, `${context.sessionId}.jsonl`),
|
|
343
|
-
path.join(runtimeHome, 'agents', context.projectId, 'sessions', `${context.sessionId}.jsonl`),
|
|
344
|
-
];
|
|
345
|
-
for (const p of candidates) {
|
|
346
|
-
try {
|
|
347
|
-
await stat(p);
|
|
348
|
-
return p;
|
|
349
|
-
}
|
|
350
|
-
catch {
|
|
351
|
-
// try next candidate
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
return candidates[0];
|
|
355
|
-
}
|
|
356
|
-
export const openclawAdapter = {
|
|
357
|
-
runtimeName: 'openclaw',
|
|
358
|
-
capabilities: RUNTIME_CAPABILITIES.openclaw,
|
|
359
|
-
resolveContextFromHook(payload) {
|
|
360
|
-
const sessionId = payload.sessionId;
|
|
361
|
-
const transcriptPath = payload.transcriptPath;
|
|
362
|
-
if (!sessionId || !transcriptPath)
|
|
363
|
-
return null;
|
|
364
|
-
const projectId = parseOpenclawProjectId(transcriptPath);
|
|
365
|
-
if (!projectId)
|
|
366
|
-
return null;
|
|
367
|
-
return { projectId, sessionId, transcriptPath };
|
|
368
|
-
},
|
|
369
|
-
async collectEvents(context) {
|
|
370
|
-
const transcriptPath = await resolveTranscriptPath(context);
|
|
371
|
-
const loaded = await readOpenclawTranscript(transcriptPath);
|
|
372
|
-
const messages = await inferParentLinkFromSiblingSessions({ transcriptPath, messages: loaded });
|
|
373
|
-
return normalizeTranscriptMessages({
|
|
374
|
-
runtime: 'openclaw',
|
|
375
|
-
projectId: context.projectId,
|
|
376
|
-
sessionId: context.sessionId,
|
|
377
|
-
messages,
|
|
378
|
-
});
|
|
379
|
-
},
|
|
380
|
-
};
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
export declare const CAPABILITY_KEYS: string[];
|
|
2
|
-
export declare const RUNTIME_CAPABILITIES: {
|
|
3
|
-
'claude-code': {
|
|
4
|
-
turnDetection: boolean;
|
|
5
|
-
toolCallAttribution: boolean;
|
|
6
|
-
toolResultCorrelation: boolean;
|
|
7
|
-
modelName: boolean;
|
|
8
|
-
usageDetails: boolean;
|
|
9
|
-
slashCommandExtraction: boolean;
|
|
10
|
-
mcpServerExtraction: boolean;
|
|
11
|
-
};
|
|
12
|
-
openclaw: {
|
|
13
|
-
turnDetection: boolean;
|
|
14
|
-
toolCallAttribution: boolean;
|
|
15
|
-
toolResultCorrelation: boolean;
|
|
16
|
-
modelName: boolean;
|
|
17
|
-
usageDetails: boolean;
|
|
18
|
-
slashCommandExtraction: boolean;
|
|
19
|
-
mcpServerExtraction: boolean;
|
|
20
|
-
};
|
|
21
|
-
codex: {
|
|
22
|
-
turnDetection: boolean;
|
|
23
|
-
toolCallAttribution: boolean;
|
|
24
|
-
toolResultCorrelation: boolean;
|
|
25
|
-
modelName: boolean;
|
|
26
|
-
usageDetails: boolean;
|
|
27
|
-
slashCommandExtraction: boolean;
|
|
28
|
-
mcpServerExtraction: boolean;
|
|
29
|
-
};
|
|
30
|
-
};
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
export const CAPABILITY_KEYS = [
|
|
3
|
-
'turnDetection',
|
|
4
|
-
'toolCallAttribution',
|
|
5
|
-
'toolResultCorrelation',
|
|
6
|
-
'modelName',
|
|
7
|
-
'usageDetails',
|
|
8
|
-
'slashCommandExtraction',
|
|
9
|
-
'mcpServerExtraction',
|
|
10
|
-
];
|
|
11
|
-
export const RUNTIME_CAPABILITIES = {
|
|
12
|
-
'claude-code': {
|
|
13
|
-
turnDetection: true,
|
|
14
|
-
toolCallAttribution: true,
|
|
15
|
-
toolResultCorrelation: true,
|
|
16
|
-
modelName: true,
|
|
17
|
-
usageDetails: true,
|
|
18
|
-
slashCommandExtraction: true,
|
|
19
|
-
mcpServerExtraction: true,
|
|
20
|
-
},
|
|
21
|
-
openclaw: {
|
|
22
|
-
turnDetection: true,
|
|
23
|
-
toolCallAttribution: true,
|
|
24
|
-
toolResultCorrelation: true,
|
|
25
|
-
modelName: true,
|
|
26
|
-
usageDetails: true,
|
|
27
|
-
slashCommandExtraction: true,
|
|
28
|
-
mcpServerExtraction: true,
|
|
29
|
-
},
|
|
30
|
-
codex: {
|
|
31
|
-
turnDetection: true,
|
|
32
|
-
toolCallAttribution: true,
|
|
33
|
-
toolResultCorrelation: true,
|
|
34
|
-
modelName: true,
|
|
35
|
-
usageDetails: true,
|
|
36
|
-
slashCommandExtraction: true,
|
|
37
|
-
mcpServerExtraction: true,
|
|
38
|
-
},
|
|
39
|
-
};
|