mu-agent 0.1.0 → 0.1.3
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 +27 -0
- package/README.md +4 -1
- package/dist/bundle/chunks/anthropic.js +25 -14
- package/dist/bundle/chunks/{chunk-QB4HYZZE.js → chunk-AIR4ZKSM.js} +90 -6
- package/dist/bundle/chunks/chunk-WSSPR443.js +2 -0
- package/dist/bundle/chunks/github-copilot.js +1 -1
- package/dist/bundle/chunks/openai-codex.js +25 -14
- package/dist/bundle/chunks/openrouter.js +25 -14
- package/dist/bundle/chunks/radius.js +25 -14
- package/dist/bundle/chunks/{virtual-modules-ROULVYS3.js → virtual-modules-I4HUN7PP.js} +1 -1
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/judge/agents/browser.md +1 -0
- package/judge/agents/investigator.md +1 -0
- package/judge/agents/planner.md +1 -0
- package/judge/agents/reviewer.md +1 -0
- package/judge/agents/scout.md +1 -0
- package/judge/agents/worker.md +1 -0
- package/judge/dist/auth.js +1248 -0
- package/judge/dist/import.js +1785 -0
- package/judge/dist/kyrn-judge.js +35571 -4446
- package/judge/manifest.json +8050 -0
- package/kyrn/bin/mu.mjs +118 -1
- package/package.json +5 -5
- package/dist/bundle/chunks/chunk-Z2DUOAN4.js +0 -2
|
@@ -0,0 +1,1785 @@
|
|
|
1
|
+
import { createRequire as __muCreateRequire } from "node:module"; const require = __muCreateRequire(import.meta.url);
|
|
2
|
+
|
|
3
|
+
// packages/kyrn-judge/src/import/command.ts
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { homedir as homedir3 } from "node:os";
|
|
6
|
+
import { join as join3, sep } from "node:path";
|
|
7
|
+
|
|
8
|
+
// packages/kyrn-judge/src/import/index.ts
|
|
9
|
+
import { mkdirSync, readdirSync as readdirSync2, realpathSync, statSync as statSync4 } from "node:fs";
|
|
10
|
+
import { homedir as homedir2 } from "node:os";
|
|
11
|
+
import { join as join2 } from "node:path";
|
|
12
|
+
|
|
13
|
+
// packages/kyrn-judge/src/import/claude-code.ts
|
|
14
|
+
import { statSync as statSync2 } from "node:fs";
|
|
15
|
+
import { basename } from "node:path";
|
|
16
|
+
|
|
17
|
+
// packages/kyrn-judge/src/import/jsonl.ts
|
|
18
|
+
import { closeSync, createReadStream, openSync, readSync } from "node:fs";
|
|
19
|
+
var MAX_LINE_BYTES = 64 * 1024 * 1024;
|
|
20
|
+
function parse(bytes) {
|
|
21
|
+
const text = bytes.toString("utf8").trim();
|
|
22
|
+
if (!text) return { problem: "malformed", bytes: bytes.length };
|
|
23
|
+
try {
|
|
24
|
+
return { value: JSON.parse(text), bytes: bytes.length };
|
|
25
|
+
} catch {
|
|
26
|
+
return { problem: "malformed", bytes: bytes.length };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var blank = (bytes) => bytes.toString("utf8").trim() === "";
|
|
30
|
+
async function* readJsonLines(path, maxLineBytes = MAX_LINE_BYTES) {
|
|
31
|
+
let parts = [];
|
|
32
|
+
let size = 0;
|
|
33
|
+
let oversized = false;
|
|
34
|
+
for await (const chunk of createReadStream(path, { highWaterMark: 1024 * 1024 })) {
|
|
35
|
+
const buffer = chunk;
|
|
36
|
+
let start = 0;
|
|
37
|
+
for (let newline = buffer.indexOf(10, start); newline !== -1; newline = buffer.indexOf(10, start)) {
|
|
38
|
+
const piece = buffer.subarray(start, newline);
|
|
39
|
+
start = newline + 1;
|
|
40
|
+
if (oversized || size + piece.length > maxLineBytes) {
|
|
41
|
+
yield { problem: "oversized", bytes: size + piece.length };
|
|
42
|
+
} else if (size + piece.length > 0) {
|
|
43
|
+
const line = parts.length === 0 ? piece : Buffer.concat([...parts, piece]);
|
|
44
|
+
if (!blank(line)) yield parse(line);
|
|
45
|
+
}
|
|
46
|
+
parts = [];
|
|
47
|
+
size = 0;
|
|
48
|
+
oversized = false;
|
|
49
|
+
}
|
|
50
|
+
const rest = buffer.subarray(start);
|
|
51
|
+
if (rest.length === 0) continue;
|
|
52
|
+
if (oversized || size + rest.length > maxLineBytes) {
|
|
53
|
+
oversized = true;
|
|
54
|
+
parts = [];
|
|
55
|
+
size += rest.length;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
parts.push(Buffer.from(rest));
|
|
59
|
+
size += rest.length;
|
|
60
|
+
}
|
|
61
|
+
if (oversized) yield { problem: "oversized", bytes: size };
|
|
62
|
+
else if (size > 0) {
|
|
63
|
+
const line = Buffer.concat(parts);
|
|
64
|
+
if (!blank(line)) yield parse(line);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function readHead(path, visit, limit = 1024 * 1024) {
|
|
68
|
+
const fd = openSync(path, "r");
|
|
69
|
+
try {
|
|
70
|
+
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
71
|
+
let pending = Buffer.alloc(0);
|
|
72
|
+
let read = 0;
|
|
73
|
+
while (read < limit) {
|
|
74
|
+
const count = readSync(fd, chunk, 0, Math.min(chunk.length, limit - read), null);
|
|
75
|
+
if (count === 0) {
|
|
76
|
+
if (!blank(pending)) {
|
|
77
|
+
const last = parse(pending);
|
|
78
|
+
if ("value" in last) visit(last.value);
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
read += count;
|
|
83
|
+
pending = Buffer.concat([pending, chunk.subarray(0, count)]);
|
|
84
|
+
let start = 0;
|
|
85
|
+
for (let newline = pending.indexOf(10, start); newline !== -1; newline = pending.indexOf(10, start)) {
|
|
86
|
+
const line = pending.subarray(start, newline);
|
|
87
|
+
start = newline + 1;
|
|
88
|
+
if (blank(line)) continue;
|
|
89
|
+
const parsed = parse(line);
|
|
90
|
+
if ("value" in parsed && visit(parsed.value)) return;
|
|
91
|
+
}
|
|
92
|
+
pending = pending.subarray(start);
|
|
93
|
+
}
|
|
94
|
+
} finally {
|
|
95
|
+
closeSync(fd);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function record(value) {
|
|
99
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
100
|
+
}
|
|
101
|
+
function str(value) {
|
|
102
|
+
return typeof value === "string" ? value : void 0;
|
|
103
|
+
}
|
|
104
|
+
function list(value) {
|
|
105
|
+
return Array.isArray(value) ? value : [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// packages/kyrn-judge/src/import/session.ts
|
|
109
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
110
|
+
import { closeSync as closeSync2, openSync as openSync2, readdirSync, statSync, writeSync } from "node:fs";
|
|
111
|
+
import { homedir } from "node:os";
|
|
112
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
113
|
+
var TOOL_NAMES = { "claude-code": "Claude Code", codex: "Codex" };
|
|
114
|
+
var IMPORT_MARKER = "mu.import";
|
|
115
|
+
var IMPORT_CONTEXT = "mu.import.context";
|
|
116
|
+
var SESSION_VERSION = 3;
|
|
117
|
+
var MAX_RESULT_CHARS = 1e5;
|
|
118
|
+
var IMAGE_MARKER = "(image not imported)";
|
|
119
|
+
var NO_RESULT = "No result was recorded for this call.";
|
|
120
|
+
function emptyCounts() {
|
|
121
|
+
return {
|
|
122
|
+
records: 0,
|
|
123
|
+
entries: 0,
|
|
124
|
+
user: 0,
|
|
125
|
+
assistant: 0,
|
|
126
|
+
toolCalls: 0,
|
|
127
|
+
toolResults: 0,
|
|
128
|
+
context: 0,
|
|
129
|
+
compactions: 0,
|
|
130
|
+
missingResults: 0,
|
|
131
|
+
truncated: 0,
|
|
132
|
+
asText: 0,
|
|
133
|
+
dropped: {}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function uuidv7(ms = Date.now()) {
|
|
137
|
+
const bytes = randomBytes(16);
|
|
138
|
+
let time = Math.max(0, Math.floor(ms));
|
|
139
|
+
for (let index = 5; index >= 0; index--) {
|
|
140
|
+
bytes[index] = time % 256;
|
|
141
|
+
time = Math.floor(time / 256);
|
|
142
|
+
}
|
|
143
|
+
bytes[6] = bytes[6] & 15 | 112;
|
|
144
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
145
|
+
const hex = bytes.toString("hex");
|
|
146
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
147
|
+
}
|
|
148
|
+
function expand(path) {
|
|
149
|
+
if (path === "~") return homedir();
|
|
150
|
+
if (path.startsWith("~/")) return join(homedir(), path.slice(2));
|
|
151
|
+
return path;
|
|
152
|
+
}
|
|
153
|
+
function absolute(path, base = process.cwd()) {
|
|
154
|
+
const expanded = expand(path);
|
|
155
|
+
return isAbsolute(expanded) ? resolve(expanded) : resolve(base, expanded);
|
|
156
|
+
}
|
|
157
|
+
function defaultSessionDir(cwd, agentDir) {
|
|
158
|
+
const safe = `--${absolute(cwd).replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
159
|
+
return join(absolute(agentDir), "sessions", safe);
|
|
160
|
+
}
|
|
161
|
+
function sessionDirFor(cwd, store) {
|
|
162
|
+
return store.sessionDir ? absolute(store.sessionDir) : defaultSessionDir(cwd, store.agentDir);
|
|
163
|
+
}
|
|
164
|
+
function sessionsRoot(store) {
|
|
165
|
+
return store.sessionDir ? absolute(store.sessionDir) : join(absolute(store.agentDir), "sessions");
|
|
166
|
+
}
|
|
167
|
+
function sessionFileName(started, id) {
|
|
168
|
+
return `${started.replace(/[:.]/g, "-")}_${id}.jsonl`;
|
|
169
|
+
}
|
|
170
|
+
function readOrigin(file) {
|
|
171
|
+
let header;
|
|
172
|
+
let marker;
|
|
173
|
+
try {
|
|
174
|
+
readHead(
|
|
175
|
+
file,
|
|
176
|
+
(value) => {
|
|
177
|
+
if (!header) {
|
|
178
|
+
header = record(value);
|
|
179
|
+
return header?.type !== "session";
|
|
180
|
+
}
|
|
181
|
+
marker = record(value);
|
|
182
|
+
return true;
|
|
183
|
+
},
|
|
184
|
+
256 * 1024
|
|
185
|
+
);
|
|
186
|
+
} catch {
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
189
|
+
const details = record(marker?.details);
|
|
190
|
+
if (marker?.type !== "custom_message" || marker.customType !== IMPORT_MARKER || !details) return void 0;
|
|
191
|
+
const tool = details.tool;
|
|
192
|
+
if (tool !== "claude-code" && tool !== "codex") return void 0;
|
|
193
|
+
if (typeof details.source !== "string") return void 0;
|
|
194
|
+
return { sessionId: str(header?.id) ?? "", origin: details };
|
|
195
|
+
}
|
|
196
|
+
function findImports(store) {
|
|
197
|
+
const files = [];
|
|
198
|
+
const scan = (dir, depth) => {
|
|
199
|
+
let names;
|
|
200
|
+
try {
|
|
201
|
+
names = readdirSync(dir);
|
|
202
|
+
} catch {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const name of names) {
|
|
206
|
+
const path = join(dir, name);
|
|
207
|
+
if (name.endsWith(".jsonl")) files.push(path);
|
|
208
|
+
else if (depth > 0) {
|
|
209
|
+
try {
|
|
210
|
+
if (statSync(path).isDirectory()) scan(path, depth - 1);
|
|
211
|
+
} catch {
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
scan(sessionsRoot(store), 1);
|
|
217
|
+
const found = [];
|
|
218
|
+
for (const file of files) {
|
|
219
|
+
const read = readOrigin(file);
|
|
220
|
+
if (read) found.push({ sessionFile: file, sessionId: read.sessionId, origin: read.origin });
|
|
221
|
+
}
|
|
222
|
+
return found;
|
|
223
|
+
}
|
|
224
|
+
function findImport(imports, tool, sourceId, source) {
|
|
225
|
+
return imports.find(
|
|
226
|
+
(found) => found.origin.tool === tool && (sourceId !== "" && found.origin.sourceId === sourceId || found.origin.source === source)
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
function firstLine(text, max = 80) {
|
|
230
|
+
const line = text.split(/\r?\n/).map((part) => part.replace(/\s+/g, " ").trim()).find((part) => part.length > 0) ?? "";
|
|
231
|
+
const chars = Array.from(line);
|
|
232
|
+
return chars.length > max ? `${chars.slice(0, max - 1).join("")}\u2026` : line;
|
|
233
|
+
}
|
|
234
|
+
function clip(text, max = MAX_RESULT_CHARS) {
|
|
235
|
+
if (text.length <= max) return { text, cut: false };
|
|
236
|
+
const head = Math.floor(max * 0.6);
|
|
237
|
+
const tail = max - head;
|
|
238
|
+
const left = text.length - head - tail;
|
|
239
|
+
return {
|
|
240
|
+
text: `${text.slice(0, head)}
|
|
241
|
+
|
|
242
|
+
[\u2026 ${left} characters not imported \u2026]
|
|
243
|
+
|
|
244
|
+
${text.slice(text.length - tail)}`,
|
|
245
|
+
cut: true
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function commandText(text) {
|
|
249
|
+
const name = /<command-name>([\s\S]*?)<\/command-name>/.exec(text)?.[1]?.trim();
|
|
250
|
+
if (!name) return void 0;
|
|
251
|
+
const args = /<command-args>([\s\S]*?)<\/command-args>/.exec(text)?.[1]?.trim();
|
|
252
|
+
const command = name.startsWith("/") ? name : `/${name}`;
|
|
253
|
+
return args ? `${command} ${args}` : command;
|
|
254
|
+
}
|
|
255
|
+
function shellText(text) {
|
|
256
|
+
const command = /<bash-input>([\s\S]*?)<\/bash-input>/.exec(text)?.[1];
|
|
257
|
+
if (command === void 0) return void 0;
|
|
258
|
+
const output = text.replace(/<bash-input>[\s\S]*?<\/bash-input>/, "").trim();
|
|
259
|
+
return { command: command.trim(), output };
|
|
260
|
+
}
|
|
261
|
+
var SETUP_TAGS = /* @__PURE__ */ new Set([
|
|
262
|
+
"environment_context",
|
|
263
|
+
"user_instructions",
|
|
264
|
+
"in-app-browser-context",
|
|
265
|
+
"codex_internal_context",
|
|
266
|
+
"recommended_plugins",
|
|
267
|
+
"permissions instructions",
|
|
268
|
+
"collaboration_mode",
|
|
269
|
+
"multi_agent_mode",
|
|
270
|
+
"multi_agent_role",
|
|
271
|
+
"skills_instructions",
|
|
272
|
+
"plugins_instructions",
|
|
273
|
+
"apps_instructions",
|
|
274
|
+
"app-context",
|
|
275
|
+
"image_resize_notice",
|
|
276
|
+
"model_switch",
|
|
277
|
+
"INSTRUCTIONS"
|
|
278
|
+
]);
|
|
279
|
+
var CONTEXT_TAGS = /* @__PURE__ */ new Set([
|
|
280
|
+
"task-notification",
|
|
281
|
+
"subagent_notification",
|
|
282
|
+
"system-reminder",
|
|
283
|
+
"local-command-stdout",
|
|
284
|
+
"local-command-stderr",
|
|
285
|
+
"local-command-caveat",
|
|
286
|
+
"bash-stdout",
|
|
287
|
+
"bash-stderr",
|
|
288
|
+
"turn_aborted",
|
|
289
|
+
"heartbeat"
|
|
290
|
+
]);
|
|
291
|
+
function classifyUserText(text) {
|
|
292
|
+
if (/^\s*\[Request interrupted by user/.test(text)) return "context";
|
|
293
|
+
const tag = /^\s*<([A-Za-z][\w-]*(?: [\w-]+)?)>/.exec(text)?.[1];
|
|
294
|
+
if (!tag) return "user";
|
|
295
|
+
if (SETUP_TAGS.has(tag)) return "setup";
|
|
296
|
+
if (CONTEXT_TAGS.has(tag)) return "context";
|
|
297
|
+
return "user";
|
|
298
|
+
}
|
|
299
|
+
function toolArguments(raw) {
|
|
300
|
+
if (typeof raw === "string") {
|
|
301
|
+
try {
|
|
302
|
+
const parsed = JSON.parse(raw);
|
|
303
|
+
const object2 = record(parsed);
|
|
304
|
+
return object2 ? object2 : { value: parsed };
|
|
305
|
+
} catch {
|
|
306
|
+
return { input: raw };
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const object = record(raw);
|
|
310
|
+
if (object) return object;
|
|
311
|
+
return raw === void 0 || raw === null ? {} : { value: raw };
|
|
312
|
+
}
|
|
313
|
+
function unknownBlockText(block) {
|
|
314
|
+
const { type, ...rest } = block;
|
|
315
|
+
let json;
|
|
316
|
+
try {
|
|
317
|
+
json = JSON.stringify(rest);
|
|
318
|
+
} catch {
|
|
319
|
+
json = "";
|
|
320
|
+
}
|
|
321
|
+
return `[${typeof type === "string" ? type : "block"}] ${clip(json, 2e3).text}`.trim();
|
|
322
|
+
}
|
|
323
|
+
var ZERO_USAGE = {
|
|
324
|
+
input: 0,
|
|
325
|
+
output: 0,
|
|
326
|
+
cacheRead: 0,
|
|
327
|
+
cacheWrite: 0,
|
|
328
|
+
totalTokens: 0,
|
|
329
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
330
|
+
};
|
|
331
|
+
var IMAGE = { type: "image", data: "", mimeType: "image/png" };
|
|
332
|
+
var SessionBuilder = class {
|
|
333
|
+
constructor(tool) {
|
|
334
|
+
this.counts = emptyCounts();
|
|
335
|
+
this.items = [];
|
|
336
|
+
/** Messages that arrived while calls were still waiting for their results. */
|
|
337
|
+
this.held = [];
|
|
338
|
+
/** Calls of the last assistant message without a result yet, by id. */
|
|
339
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
340
|
+
/** Every call seen, by id: a result may name its call only by the id. */
|
|
341
|
+
this.calls = /* @__PURE__ */ new Map();
|
|
342
|
+
this.answered = /* @__PURE__ */ new Set();
|
|
343
|
+
this.tool = tool;
|
|
344
|
+
}
|
|
345
|
+
drop(reason, count = 1) {
|
|
346
|
+
if (count <= 0) return;
|
|
347
|
+
this.counts.dropped[reason] = (this.counts.dropped[reason] ?? 0) + count;
|
|
348
|
+
}
|
|
349
|
+
/** Whether anything a person said or a model answered is in it: a transcript without either is not imported. */
|
|
350
|
+
get hasConversation() {
|
|
351
|
+
return this.counts.user + this.counts.assistant > 0;
|
|
352
|
+
}
|
|
353
|
+
/** Images become a line that says so; everything else is kept as it was. */
|
|
354
|
+
withoutImages(content) {
|
|
355
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
356
|
+
const blocks = [];
|
|
357
|
+
for (const block of content) {
|
|
358
|
+
if (block.type === "image") {
|
|
359
|
+
this.drop("images");
|
|
360
|
+
blocks.push({ type: "text", text: IMAGE_MARKER });
|
|
361
|
+
} else blocks.push({ type: "text", text: block.text });
|
|
362
|
+
}
|
|
363
|
+
return blocks;
|
|
364
|
+
}
|
|
365
|
+
clipped(text) {
|
|
366
|
+
const cut = clip(text);
|
|
367
|
+
if (cut.cut) this.counts.truncated++;
|
|
368
|
+
return cut.text;
|
|
369
|
+
}
|
|
370
|
+
push(item) {
|
|
371
|
+
const waits = item.kind === "context" || item.kind === "message" && item.message.role === "user";
|
|
372
|
+
if (this.pending.size > 0 && waits) this.held.push(item);
|
|
373
|
+
else this.items.push(item);
|
|
374
|
+
}
|
|
375
|
+
lastTime() {
|
|
376
|
+
return this.items.at(-1)?.at ?? 0;
|
|
377
|
+
}
|
|
378
|
+
/** Calls still without a result get one that says so, and whatever waited behind them follows. */
|
|
379
|
+
settle() {
|
|
380
|
+
for (const [id, name] of this.pending) {
|
|
381
|
+
this.counts.missingResults++;
|
|
382
|
+
this.answered.add(id);
|
|
383
|
+
const at = this.lastTime();
|
|
384
|
+
this.items.push({
|
|
385
|
+
kind: "message",
|
|
386
|
+
at,
|
|
387
|
+
message: {
|
|
388
|
+
role: "toolResult",
|
|
389
|
+
toolCallId: id,
|
|
390
|
+
toolName: name,
|
|
391
|
+
content: [{ type: "text", text: NO_RESULT }],
|
|
392
|
+
isError: true,
|
|
393
|
+
timestamp: at
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
this.pending.clear();
|
|
398
|
+
this.items.push(...this.held);
|
|
399
|
+
this.held = [];
|
|
400
|
+
}
|
|
401
|
+
user(content, at, key) {
|
|
402
|
+
const blocks = this.withoutImages(content).filter((block) => block.text.length > 0);
|
|
403
|
+
if (blocks.length === 0) return;
|
|
404
|
+
this.counts.user++;
|
|
405
|
+
this.push({
|
|
406
|
+
kind: "message",
|
|
407
|
+
at,
|
|
408
|
+
key,
|
|
409
|
+
message: { role: "user", content: typeof content === "string" ? content : blocks, timestamp: at }
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
/** Something the other tool showed its model that nobody typed: kept for the model, hidden from the transcript. */
|
|
413
|
+
context(text, at, key) {
|
|
414
|
+
if (!text.trim()) return;
|
|
415
|
+
this.counts.context++;
|
|
416
|
+
this.push({ kind: "context", text: this.clipped(text), at, key });
|
|
417
|
+
}
|
|
418
|
+
assistant(input) {
|
|
419
|
+
this.settle();
|
|
420
|
+
const blocks = input.blocks.filter((block) => block.type === "toolCall" || block.text.length > 0);
|
|
421
|
+
if (blocks.length === 0 && !input.error) return;
|
|
422
|
+
const calls = blocks.filter((block) => block.type === "toolCall");
|
|
423
|
+
const message = {
|
|
424
|
+
role: "assistant",
|
|
425
|
+
content: input.error ? [] : blocks,
|
|
426
|
+
api: this.tool,
|
|
427
|
+
provider: this.tool,
|
|
428
|
+
model: input.model || "unknown",
|
|
429
|
+
usage: ZERO_USAGE,
|
|
430
|
+
stopReason: input.error ? "error" : input.stopReason ?? (calls.length > 0 ? "toolUse" : "stop"),
|
|
431
|
+
...input.error ? { errorMessage: input.error } : {},
|
|
432
|
+
timestamp: input.at
|
|
433
|
+
};
|
|
434
|
+
this.counts.assistant++;
|
|
435
|
+
this.items.push({ kind: "message", message, at: input.at, key: input.key });
|
|
436
|
+
if (input.error) return;
|
|
437
|
+
for (const call of calls) {
|
|
438
|
+
this.counts.toolCalls++;
|
|
439
|
+
this.calls.set(call.id, call.name);
|
|
440
|
+
this.pending.set(call.id, call.name);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
toolResult(input) {
|
|
444
|
+
const name = input.name || this.calls.get(input.id) || "unknown";
|
|
445
|
+
const blocks = this.withoutImages(input.content);
|
|
446
|
+
if (!this.pending.has(input.id)) {
|
|
447
|
+
if (this.answered.has(input.id)) {
|
|
448
|
+
this.drop("repeated tool results");
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
this.drop("tool results without their call, kept as context");
|
|
452
|
+
this.context(`[Result of an earlier ${name} call]
|
|
453
|
+
${blocks.map((block) => block.text).join("\n")}`, input.at);
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const content = blocks.map((block) => ({ type: "text", text: this.clipped(block.text) }));
|
|
457
|
+
this.pending.delete(input.id);
|
|
458
|
+
this.answered.add(input.id);
|
|
459
|
+
this.counts.toolResults++;
|
|
460
|
+
this.items.push({
|
|
461
|
+
kind: "message",
|
|
462
|
+
at: input.at,
|
|
463
|
+
key: input.key,
|
|
464
|
+
message: {
|
|
465
|
+
role: "toolResult",
|
|
466
|
+
toolCallId: input.id,
|
|
467
|
+
toolName: name,
|
|
468
|
+
content: content.length > 0 ? content : [{ type: "text", text: "" }],
|
|
469
|
+
isError: input.isError,
|
|
470
|
+
timestamp: input.at
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
if (this.pending.size === 0) {
|
|
474
|
+
this.items.push(...this.held);
|
|
475
|
+
this.held = [];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* The other tool compacted the conversation here: from now on its model saw the summary instead of what came
|
|
480
|
+
* before. `keepFrom` lists, in order, the items the tool kept word for word after its summary (Claude Code's
|
|
481
|
+
* preserved segment); the first of them that was imported is where pi's kept part starts.
|
|
482
|
+
*/
|
|
483
|
+
compaction(summary, tokensBefore, at, keepFrom = []) {
|
|
484
|
+
if (!summary.trim()) return;
|
|
485
|
+
this.settle();
|
|
486
|
+
this.counts.compactions++;
|
|
487
|
+
this.items.push({ kind: "compaction", summary, tokensBefore, at, keepFrom });
|
|
488
|
+
}
|
|
489
|
+
/** The session file's lines: header, marker, name, then the conversation as one chain. */
|
|
490
|
+
finish(input) {
|
|
491
|
+
this.settle();
|
|
492
|
+
const taken = /* @__PURE__ */ new Set();
|
|
493
|
+
const newId = () => {
|
|
494
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
495
|
+
const id2 = randomUUID().slice(0, 8);
|
|
496
|
+
if (!taken.has(id2)) {
|
|
497
|
+
taken.add(id2);
|
|
498
|
+
return id2;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const id = randomUUID();
|
|
502
|
+
taken.add(id);
|
|
503
|
+
return id;
|
|
504
|
+
};
|
|
505
|
+
const startedAt = Date.parse(input.started);
|
|
506
|
+
const iso = (at) => new Date(at > 0 ? at : startedAt).toISOString();
|
|
507
|
+
const ids = this.items.map(() => newId());
|
|
508
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
509
|
+
this.items.forEach((item, index) => {
|
|
510
|
+
if (item.kind !== "compaction" && item.key && !byKey.has(item.key)) byKey.set(item.key, index);
|
|
511
|
+
});
|
|
512
|
+
let lastCompaction = -1;
|
|
513
|
+
this.items.forEach((item, index) => {
|
|
514
|
+
if (item.kind === "compaction") lastCompaction = index;
|
|
515
|
+
});
|
|
516
|
+
this.counts.entries = this.items.length + 2 + (lastCompaction >= 0 ? 1 : 0);
|
|
517
|
+
const header = {
|
|
518
|
+
type: "session",
|
|
519
|
+
version: SESSION_VERSION,
|
|
520
|
+
id: input.sessionId,
|
|
521
|
+
timestamp: input.started,
|
|
522
|
+
cwd: input.cwd
|
|
523
|
+
};
|
|
524
|
+
const marker = {
|
|
525
|
+
type: "custom_message",
|
|
526
|
+
id: newId(),
|
|
527
|
+
parentId: null,
|
|
528
|
+
timestamp: input.started,
|
|
529
|
+
customType: IMPORT_MARKER,
|
|
530
|
+
content: input.note,
|
|
531
|
+
display: true,
|
|
532
|
+
details: { ...input.origin, counts: this.counts }
|
|
533
|
+
};
|
|
534
|
+
const info = {
|
|
535
|
+
type: "session_info",
|
|
536
|
+
id: newId(),
|
|
537
|
+
parentId: marker.id,
|
|
538
|
+
timestamp: input.started,
|
|
539
|
+
name: input.name.replace(/[\r\n]+/g, " ").trim()
|
|
540
|
+
};
|
|
541
|
+
const entries = [marker, info];
|
|
542
|
+
let parentId = info.id;
|
|
543
|
+
this.items.forEach((item, index) => {
|
|
544
|
+
const id = ids[index];
|
|
545
|
+
const timestamp = iso(item.at);
|
|
546
|
+
if (item.kind === "message") {
|
|
547
|
+
entries.push({ type: "message", id, parentId, timestamp, message: item.message });
|
|
548
|
+
} else if (item.kind === "context") {
|
|
549
|
+
const context = {
|
|
550
|
+
type: "custom_message",
|
|
551
|
+
id,
|
|
552
|
+
parentId,
|
|
553
|
+
timestamp,
|
|
554
|
+
customType: IMPORT_CONTEXT,
|
|
555
|
+
content: item.text,
|
|
556
|
+
display: false
|
|
557
|
+
};
|
|
558
|
+
entries.push(context);
|
|
559
|
+
} else {
|
|
560
|
+
const kept = item.keepFrom.map((key) => byKey.get(key)).find((at) => at !== void 0);
|
|
561
|
+
const firstKeptEntryId = kept !== void 0 && kept < index ? ids[kept] : index + 1 < ids.length ? ids[index + 1] : newId();
|
|
562
|
+
const compaction = {
|
|
563
|
+
type: "compaction",
|
|
564
|
+
id,
|
|
565
|
+
parentId,
|
|
566
|
+
timestamp,
|
|
567
|
+
summary: item.summary,
|
|
568
|
+
firstKeptEntryId,
|
|
569
|
+
tokensBefore: item.tokensBefore
|
|
570
|
+
};
|
|
571
|
+
entries.push(compaction);
|
|
572
|
+
}
|
|
573
|
+
parentId = id;
|
|
574
|
+
if (index === lastCompaction) {
|
|
575
|
+
const again = { ...marker, id: newId(), parentId, timestamp };
|
|
576
|
+
entries.push(again);
|
|
577
|
+
parentId = again.id;
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
return [header, ...entries];
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
function importNote(tool, source) {
|
|
584
|
+
const name = TOOL_NAMES[tool];
|
|
585
|
+
return [
|
|
586
|
+
`This conversation was imported from ${name} (${source}) and continues here.`,
|
|
587
|
+
`Its tool calls use ${name}'s tool names, which may differ from the tools available now.`,
|
|
588
|
+
"Thinking and images were not imported."
|
|
589
|
+
].join(" ");
|
|
590
|
+
}
|
|
591
|
+
function complete(builder, input) {
|
|
592
|
+
const started = Number.isNaN(Date.parse(input.started)) ? (/* @__PURE__ */ new Date()).toISOString() : input.started;
|
|
593
|
+
const sessionId = uuidv7(Date.parse(started));
|
|
594
|
+
const name = input.name?.trim() || `${TOOL_NAMES[builder.tool]} conversation`;
|
|
595
|
+
const entries = builder.finish({
|
|
596
|
+
sessionId,
|
|
597
|
+
cwd: input.cwd,
|
|
598
|
+
started,
|
|
599
|
+
name,
|
|
600
|
+
note: importNote(builder.tool, input.source),
|
|
601
|
+
origin: {
|
|
602
|
+
version: 1,
|
|
603
|
+
tool: builder.tool,
|
|
604
|
+
source: input.source,
|
|
605
|
+
sourceId: input.sourceId,
|
|
606
|
+
importedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString(),
|
|
607
|
+
...input.model ? { model: input.model } : {}
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
return {
|
|
611
|
+
tool: builder.tool,
|
|
612
|
+
source: input.source,
|
|
613
|
+
sourceId: input.sourceId,
|
|
614
|
+
sessionId,
|
|
615
|
+
cwd: input.cwd,
|
|
616
|
+
started,
|
|
617
|
+
name,
|
|
618
|
+
model: input.model,
|
|
619
|
+
counts: builder.counts,
|
|
620
|
+
entries,
|
|
621
|
+
hasConversation: builder.hasConversation
|
|
622
|
+
};
|
|
623
|
+
}
|
|
624
|
+
function writeSessionFile(file, entries) {
|
|
625
|
+
const fd = openSync2(file, "wx", 384);
|
|
626
|
+
try {
|
|
627
|
+
for (const entry of entries) writeSync(fd, `${JSON.stringify(entry)}
|
|
628
|
+
`);
|
|
629
|
+
} finally {
|
|
630
|
+
closeSync2(fd);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// packages/kyrn-judge/src/import/claude-code.ts
|
|
635
|
+
var TREE_TYPES = /* @__PURE__ */ new Set(["user", "assistant", "system", "attachment"]);
|
|
636
|
+
function slim(data) {
|
|
637
|
+
delete data.toolUseResult;
|
|
638
|
+
for (const block of list(record(data.message)?.content)) {
|
|
639
|
+
const outer = record(block);
|
|
640
|
+
if (outer?.type === "image") delete outer.source;
|
|
641
|
+
if (outer?.type === "tool_result") {
|
|
642
|
+
for (const inner of list(outer.content)) {
|
|
643
|
+
const image = record(inner);
|
|
644
|
+
if (image?.type === "image") delete image.source;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
const file = record(record(record(data.attachment)?.content)?.file);
|
|
649
|
+
if (file) delete file.base64;
|
|
650
|
+
}
|
|
651
|
+
function blocksOf(message) {
|
|
652
|
+
const content = message?.content;
|
|
653
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
654
|
+
return list(content).map(record).filter((block) => block !== void 0);
|
|
655
|
+
}
|
|
656
|
+
function messageOf(item) {
|
|
657
|
+
return record(item.data.message) ?? {};
|
|
658
|
+
}
|
|
659
|
+
function resultContent(content, counts) {
|
|
660
|
+
if (typeof content === "string") return [{ type: "text", text: content }];
|
|
661
|
+
const blocks = [];
|
|
662
|
+
for (const block of list(content).map(record)) {
|
|
663
|
+
if (!block) continue;
|
|
664
|
+
if (block.type === "text") blocks.push({ type: "text", text: str(block.text) ?? "" });
|
|
665
|
+
else if (block.type === "image") blocks.push(IMAGE);
|
|
666
|
+
else if (block.type === "tool_reference")
|
|
667
|
+
blocks.push({ type: "text", text: `[tool reference: ${str(block.tool_name) ?? "unknown"}]` });
|
|
668
|
+
else {
|
|
669
|
+
counts.asText++;
|
|
670
|
+
blocks.push({ type: "text", text: unknownBlockText(block) });
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
return blocks;
|
|
674
|
+
}
|
|
675
|
+
function attachmentText(attachment) {
|
|
676
|
+
const where = (...names) => names.map(str).find((name) => name !== void 0);
|
|
677
|
+
switch (attachment.type) {
|
|
678
|
+
case "file": {
|
|
679
|
+
const file = record(record(attachment.content)?.file);
|
|
680
|
+
const text = str(file?.content);
|
|
681
|
+
if (text === void 0) return void 0;
|
|
682
|
+
return `Contents of ${where(attachment.displayPath, attachment.filename, file?.filePath) ?? "a file"}:
|
|
683
|
+
${text}`;
|
|
684
|
+
}
|
|
685
|
+
case "edited_text_file":
|
|
686
|
+
return `${where(attachment.displayPath, attachment.filename) ?? "A file"} was changed:
|
|
687
|
+
${str(attachment.snippet) ?? ""}`;
|
|
688
|
+
case "directory":
|
|
689
|
+
return `Contents of ${where(attachment.displayPath, attachment.path) ?? "a folder"}:
|
|
690
|
+
${str(attachment.content) ?? ""}`;
|
|
691
|
+
case "nested_memory": {
|
|
692
|
+
const memory = record(attachment.content);
|
|
693
|
+
return `Contents of ${where(attachment.displayPath, memory?.path) ?? "a memory file"}:
|
|
694
|
+
${str(memory?.content) ?? ""}`;
|
|
695
|
+
}
|
|
696
|
+
case "plan_file_reference":
|
|
697
|
+
return `The plan in ${where(attachment.planFilePath) ?? "a plan file"}:
|
|
698
|
+
${str(attachment.planContent) ?? ""}`;
|
|
699
|
+
case "hook_additional_context":
|
|
700
|
+
return list(attachment.content).map(str).filter((text) => text !== void 0).join("\n");
|
|
701
|
+
default:
|
|
702
|
+
return void 0;
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
async function readClaudeCode(path, options = {}) {
|
|
706
|
+
const source = absolute(path);
|
|
707
|
+
const builder = new SessionBuilder("claude-code");
|
|
708
|
+
const { counts } = builder;
|
|
709
|
+
const records = [];
|
|
710
|
+
const byUuid = /* @__PURE__ */ new Map();
|
|
711
|
+
let sessionId;
|
|
712
|
+
for await (const line of readJsonLines(source)) {
|
|
713
|
+
counts.records++;
|
|
714
|
+
if (!("value" in line)) {
|
|
715
|
+
builder.drop(line.problem === "oversized" ? "lines too long to read" : "unreadable lines");
|
|
716
|
+
continue;
|
|
717
|
+
}
|
|
718
|
+
const data = record(line.value);
|
|
719
|
+
const type = str(data?.type);
|
|
720
|
+
if (!data || !type) {
|
|
721
|
+
builder.drop("records not understood");
|
|
722
|
+
continue;
|
|
723
|
+
}
|
|
724
|
+
sessionId ??= str(data.sessionId);
|
|
725
|
+
const uuid = str(data.uuid);
|
|
726
|
+
if (!TREE_TYPES.has(type) || !uuid) {
|
|
727
|
+
builder.drop(uuid ? "records not understood" : "Claude Code bookkeeping (titles, queue, file history)");
|
|
728
|
+
continue;
|
|
729
|
+
}
|
|
730
|
+
if (byUuid.has(uuid)) {
|
|
731
|
+
builder.drop("repeated records");
|
|
732
|
+
continue;
|
|
733
|
+
}
|
|
734
|
+
slim(data);
|
|
735
|
+
const item = {
|
|
736
|
+
uuid,
|
|
737
|
+
parent: str(data.parentUuid),
|
|
738
|
+
logicalParent: str(data.logicalParentUuid),
|
|
739
|
+
type,
|
|
740
|
+
sidechain: data.isSidechain === true,
|
|
741
|
+
data
|
|
742
|
+
};
|
|
743
|
+
records.push(item);
|
|
744
|
+
byUuid.set(uuid, item);
|
|
745
|
+
}
|
|
746
|
+
const hasMain = records.some((item) => !item.sidechain);
|
|
747
|
+
const inScope = (item) => !hasMain || !item.sidechain;
|
|
748
|
+
let leaf;
|
|
749
|
+
for (let index = records.length - 1; index >= 0 && !leaf; index--) {
|
|
750
|
+
const item = records[index];
|
|
751
|
+
if (inScope(item) && (item.type === "user" || item.type === "assistant")) leaf = item;
|
|
752
|
+
}
|
|
753
|
+
const chosen = /* @__PURE__ */ new Set();
|
|
754
|
+
const position = new Map(records.map((item, index) => [item.uuid, index]));
|
|
755
|
+
const messageBefore = (item) => {
|
|
756
|
+
for (let index = (position.get(item.uuid) ?? 0) - 1; index >= 0; index--) {
|
|
757
|
+
const earlier = records[index];
|
|
758
|
+
if (inScope(earlier) && (earlier.type === "user" || earlier.type === "assistant")) return earlier;
|
|
759
|
+
}
|
|
760
|
+
return void 0;
|
|
761
|
+
};
|
|
762
|
+
for (let current2 = leaf; current2 && !chosen.has(current2.uuid); ) {
|
|
763
|
+
chosen.add(current2.uuid);
|
|
764
|
+
const next = current2.parent ?? current2.logicalParent;
|
|
765
|
+
let found = next ? byUuid.get(next) : void 0;
|
|
766
|
+
if (next && !found) {
|
|
767
|
+
if (!current2.parent) found = messageBefore(current2);
|
|
768
|
+
if (!found) builder.drop("links to records that are not in the file");
|
|
769
|
+
}
|
|
770
|
+
current2 = found;
|
|
771
|
+
}
|
|
772
|
+
const answers = /* @__PURE__ */ new Set();
|
|
773
|
+
for (const item of records) {
|
|
774
|
+
const id = str(messageOf(item).id);
|
|
775
|
+
if (chosen.has(item.uuid) && item.type === "assistant" && id) answers.add(id);
|
|
776
|
+
}
|
|
777
|
+
for (const item of records) {
|
|
778
|
+
const id = str(messageOf(item).id);
|
|
779
|
+
if (!chosen.has(item.uuid) && item.type === "assistant" && inScope(item) && id && answers.has(id))
|
|
780
|
+
chosen.add(item.uuid);
|
|
781
|
+
}
|
|
782
|
+
const calls = /* @__PURE__ */ new Set();
|
|
783
|
+
for (const item of records) {
|
|
784
|
+
if (!chosen.has(item.uuid) || item.type !== "assistant") continue;
|
|
785
|
+
for (const block of blocksOf(messageOf(item))) {
|
|
786
|
+
const id = str(block.id);
|
|
787
|
+
if (block.type === "tool_use" && id) calls.add(id);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
for (const item of records) {
|
|
791
|
+
if (chosen.has(item.uuid) || item.type !== "user" || !inScope(item)) continue;
|
|
792
|
+
const answersCall = blocksOf(messageOf(item)).some(
|
|
793
|
+
(block) => block.type === "tool_result" && calls.has(str(block.tool_use_id) ?? "")
|
|
794
|
+
);
|
|
795
|
+
if (answersCall) chosen.add(item.uuid);
|
|
796
|
+
}
|
|
797
|
+
let subAgent = 0;
|
|
798
|
+
let offBranch = 0;
|
|
799
|
+
for (const item of records) {
|
|
800
|
+
if (chosen.has(item.uuid)) continue;
|
|
801
|
+
if (inScope(item)) offBranch++;
|
|
802
|
+
else subAgent++;
|
|
803
|
+
}
|
|
804
|
+
builder.drop("sub-agent records", subAgent);
|
|
805
|
+
builder.drop("records off the latest branch", offBranch);
|
|
806
|
+
const ordered = records.filter((item) => chosen.has(item.uuid));
|
|
807
|
+
const orderedIds = ordered.map((item) => item.uuid);
|
|
808
|
+
let at = 0;
|
|
809
|
+
let started;
|
|
810
|
+
let cwd;
|
|
811
|
+
let model;
|
|
812
|
+
let name;
|
|
813
|
+
let firstCommand;
|
|
814
|
+
let boundary;
|
|
815
|
+
const answerParts = /* @__PURE__ */ new Map();
|
|
816
|
+
const partOf = /* @__PURE__ */ new Map();
|
|
817
|
+
let current;
|
|
818
|
+
for (const item of ordered) {
|
|
819
|
+
const message = messageOf(item);
|
|
820
|
+
if (item.type === "assistant") {
|
|
821
|
+
if (item.data.isApiErrorMessage === true || message.model === "<synthetic>") {
|
|
822
|
+
current = void 0;
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const id = str(message.id) ?? item.uuid;
|
|
826
|
+
const continues = current !== void 0 && current.id === id && (!current.interrupted || current.waiting.size > 0);
|
|
827
|
+
if (!current || !continues) {
|
|
828
|
+
current = { id, first: item.uuid, waiting: /* @__PURE__ */ new Set(), interrupted: false };
|
|
829
|
+
answerParts.set(item.uuid, []);
|
|
830
|
+
}
|
|
831
|
+
answerParts.get(current.first)?.push(item);
|
|
832
|
+
partOf.set(item.uuid, current.first);
|
|
833
|
+
for (const block of blocksOf(message)) {
|
|
834
|
+
const callId = str(block.id);
|
|
835
|
+
if (block.type === "tool_use" && callId) current.waiting.add(callId);
|
|
836
|
+
}
|
|
837
|
+
} else if (item.type === "user" && current) {
|
|
838
|
+
const blocks = blocksOf(message);
|
|
839
|
+
for (const block of blocks)
|
|
840
|
+
if (block.type === "tool_result") current.waiting.delete(str(block.tool_use_id) ?? "");
|
|
841
|
+
current.interrupted = true;
|
|
842
|
+
if (item.data.isMeta !== true && blocks.some((block) => block.type !== "tool_result")) current = void 0;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
const assistantRecord = (item) => {
|
|
846
|
+
const message = messageOf(item);
|
|
847
|
+
const used = str(message.model) ?? "";
|
|
848
|
+
if (item.data.isApiErrorMessage === true) {
|
|
849
|
+
const text = blocksOf(message).map((block) => str(block.text) ?? "").join("\n").trim();
|
|
850
|
+
builder.assistant({ blocks: [], error: text || "The request failed.", model: used, at, key: item.uuid });
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
if (used === "<synthetic>") {
|
|
854
|
+
builder.drop("Claude Code's own placeholder answers");
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (partOf.get(item.uuid) !== item.uuid) return;
|
|
858
|
+
if (used) model = used;
|
|
859
|
+
const blocks = [];
|
|
860
|
+
const callIds = /* @__PURE__ */ new Set();
|
|
861
|
+
let stop;
|
|
862
|
+
for (const part of answerParts.get(item.uuid) ?? [item]) {
|
|
863
|
+
const partMessage = messageOf(part);
|
|
864
|
+
if (partMessage.stop_reason === "max_tokens") stop = "length";
|
|
865
|
+
for (const block of blocksOf(partMessage)) {
|
|
866
|
+
if (block.type === "text") blocks.push({ type: "text", text: str(block.text) ?? "" });
|
|
867
|
+
else if (block.type === "thinking" || block.type === "redacted_thinking") builder.drop("thinking");
|
|
868
|
+
else if (block.type === "tool_use") {
|
|
869
|
+
const callId = str(block.id) ?? part.uuid;
|
|
870
|
+
if (callIds.has(callId)) {
|
|
871
|
+
builder.drop("repeated tool calls");
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
callIds.add(callId);
|
|
875
|
+
blocks.push({
|
|
876
|
+
type: "toolCall",
|
|
877
|
+
id: callId,
|
|
878
|
+
name: str(block.name) ?? "unknown",
|
|
879
|
+
arguments: toolArguments(block.input)
|
|
880
|
+
});
|
|
881
|
+
} else if (block.type === "image") {
|
|
882
|
+
builder.drop("images");
|
|
883
|
+
blocks.push({ type: "text", text: IMAGE_MARKER });
|
|
884
|
+
} else {
|
|
885
|
+
counts.asText++;
|
|
886
|
+
blocks.push({ type: "text", text: unknownBlockText(block) });
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
builder.assistant({ blocks, model: used, at, key: item.uuid, stopReason: stop });
|
|
891
|
+
};
|
|
892
|
+
const userRecord = (item) => {
|
|
893
|
+
const blocks = blocksOf(messageOf(item));
|
|
894
|
+
if (item.data.isCompactSummary === true) {
|
|
895
|
+
const summary = blocks.map((block) => str(block.text) ?? "").join("\n");
|
|
896
|
+
builder.compaction(summary, boundary?.tokens ?? 0, boundary?.at ?? at, boundary?.keep);
|
|
897
|
+
boundary = void 0;
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const meta = item.data.isMeta === true;
|
|
901
|
+
const words = [];
|
|
902
|
+
const say = () => {
|
|
903
|
+
if (words.length === 0) return;
|
|
904
|
+
builder.user(words.splice(0), at, item.uuid);
|
|
905
|
+
};
|
|
906
|
+
for (const block of blocks) {
|
|
907
|
+
if (block.type === "tool_result") {
|
|
908
|
+
say();
|
|
909
|
+
builder.toolResult({
|
|
910
|
+
id: str(block.tool_use_id) ?? "",
|
|
911
|
+
content: resultContent(block.content, counts),
|
|
912
|
+
isError: block.is_error === true,
|
|
913
|
+
at,
|
|
914
|
+
key: item.uuid
|
|
915
|
+
});
|
|
916
|
+
continue;
|
|
917
|
+
}
|
|
918
|
+
if (block.type === "image") {
|
|
919
|
+
if (meta) builder.drop("images");
|
|
920
|
+
else words.push(IMAGE);
|
|
921
|
+
continue;
|
|
922
|
+
}
|
|
923
|
+
if (block.type !== "text") {
|
|
924
|
+
counts.asText++;
|
|
925
|
+
words.push({ type: "text", text: unknownBlockText(block) });
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const text = str(block.text) ?? "";
|
|
929
|
+
const kind = meta ? "context" : classifyUserText(text);
|
|
930
|
+
if (kind === "setup") {
|
|
931
|
+
builder.drop("Claude Code setup");
|
|
932
|
+
continue;
|
|
933
|
+
}
|
|
934
|
+
if (kind === "context") {
|
|
935
|
+
say();
|
|
936
|
+
builder.context(text, at, item.uuid);
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const shell = shellText(text);
|
|
940
|
+
if (shell) {
|
|
941
|
+
words.push({ type: "text", text: `! ${shell.command}` });
|
|
942
|
+
say();
|
|
943
|
+
builder.context(shell.output, at);
|
|
944
|
+
continue;
|
|
945
|
+
}
|
|
946
|
+
const command = commandText(text);
|
|
947
|
+
if (command !== void 0) {
|
|
948
|
+
firstCommand ??= command;
|
|
949
|
+
words.push({ type: "text", text: command });
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (name === void 0 && text.trim()) name = firstLine(text);
|
|
953
|
+
words.push({ type: "text", text });
|
|
954
|
+
}
|
|
955
|
+
say();
|
|
956
|
+
};
|
|
957
|
+
const systemRecord = (item, index) => {
|
|
958
|
+
const subtype = str(item.data.subtype);
|
|
959
|
+
if (subtype === "compact_boundary") {
|
|
960
|
+
const metadata = record(item.data.compactMetadata);
|
|
961
|
+
const head = str(record(metadata?.preservedSegment)?.headUuid);
|
|
962
|
+
const start = head ? orderedIds.indexOf(head) : -1;
|
|
963
|
+
const tokens = metadata?.preTokens;
|
|
964
|
+
boundary = {
|
|
965
|
+
tokens: typeof tokens === "number" ? tokens : 0,
|
|
966
|
+
at,
|
|
967
|
+
keep: start >= 0 && start < index ? orderedIds.slice(start, index) : []
|
|
968
|
+
};
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
if (subtype === "local_command") {
|
|
972
|
+
builder.context(str(item.data.content) ?? "", at, item.uuid);
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
builder.drop("Claude Code status records");
|
|
976
|
+
};
|
|
977
|
+
const attachmentRecord = (item) => {
|
|
978
|
+
const attachment = record(item.data.attachment) ?? {};
|
|
979
|
+
if (attachment.type === "queued_command") {
|
|
980
|
+
const prompt = attachment.prompt;
|
|
981
|
+
const words = typeof prompt === "string" ? [{ type: "text", text: prompt }] : list(prompt).map(record).filter((block) => block !== void 0).map(
|
|
982
|
+
(block) => block.type === "text" ? { type: "text", text: str(block.text) ?? "" } : IMAGE
|
|
983
|
+
);
|
|
984
|
+
const typed = attachment.commandMode === "prompt" && attachment.isMeta !== true;
|
|
985
|
+
if (typed) {
|
|
986
|
+
const text2 = words.map((block) => block.type === "text" ? block.text : "").join("\n");
|
|
987
|
+
if (name === void 0 && text2.trim()) name = firstLine(text2);
|
|
988
|
+
builder.user(words, at, item.uuid);
|
|
989
|
+
} else {
|
|
990
|
+
const text2 = words.map((block) => block.type === "text" ? block.text : IMAGE_MARKER).join("\n");
|
|
991
|
+
builder.context(text2, at, item.uuid);
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (attachment.type === "file" && record(attachment.content)?.type === "image") {
|
|
996
|
+
builder.drop("images");
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
const text = attachmentText(attachment);
|
|
1000
|
+
if (text === void 0) builder.drop("Claude Code reminders and state");
|
|
1001
|
+
else builder.context(text, at, item.uuid);
|
|
1002
|
+
};
|
|
1003
|
+
ordered.forEach((item, index) => {
|
|
1004
|
+
const time = Date.parse(str(item.data.timestamp) ?? "");
|
|
1005
|
+
if (!Number.isNaN(time)) at = time;
|
|
1006
|
+
if (started === void 0 && at > 0) started = at;
|
|
1007
|
+
cwd ??= str(item.data.cwd);
|
|
1008
|
+
if (item.type === "assistant") assistantRecord(item);
|
|
1009
|
+
else if (item.type === "user") userRecord(item);
|
|
1010
|
+
else if (item.type === "system") systemRecord(item, index);
|
|
1011
|
+
else attachmentRecord(item);
|
|
1012
|
+
});
|
|
1013
|
+
if (boundary) builder.drop("compactions without their summary");
|
|
1014
|
+
cwd ??= records.map((item) => str(item.data.cwd)).find((dir) => dir !== void 0);
|
|
1015
|
+
let startedIso = started === void 0 ? void 0 : new Date(started).toISOString();
|
|
1016
|
+
if (!startedIso) {
|
|
1017
|
+
try {
|
|
1018
|
+
startedIso = statSync2(source).mtime.toISOString();
|
|
1019
|
+
} catch {
|
|
1020
|
+
startedIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const agentId = hasMain ? void 0 : records.map((item) => str(item.data.agentId)).find((id) => id !== void 0);
|
|
1024
|
+
return complete(builder, {
|
|
1025
|
+
source,
|
|
1026
|
+
sourceId: agentId ? `agent-${agentId}` : sessionId ?? basename(source, ".jsonl"),
|
|
1027
|
+
cwd: cwd ?? options.cwd ?? process.cwd(),
|
|
1028
|
+
started: startedIso,
|
|
1029
|
+
name: name ?? firstCommand,
|
|
1030
|
+
model,
|
|
1031
|
+
now: options.now
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
// packages/kyrn-judge/src/import/codex.ts
|
|
1036
|
+
import { statSync as statSync3 } from "node:fs";
|
|
1037
|
+
import { basename as basename2 } from "node:path";
|
|
1038
|
+
var APP_RECORDS = /* @__PURE__ */ new Set([
|
|
1039
|
+
"event_msg",
|
|
1040
|
+
"world_state",
|
|
1041
|
+
"token_usage_record",
|
|
1042
|
+
"inter_agent_communication_metadata"
|
|
1043
|
+
]);
|
|
1044
|
+
function environmentCwd(text) {
|
|
1045
|
+
return /<cwd>([\s\S]*?)<\/cwd>/.exec(text)?.[1]?.trim() || void 0;
|
|
1046
|
+
}
|
|
1047
|
+
function outputOf(output, counts) {
|
|
1048
|
+
if (typeof output === "string") {
|
|
1049
|
+
if (output.startsWith("{")) {
|
|
1050
|
+
try {
|
|
1051
|
+
const parsed = record(JSON.parse(output));
|
|
1052
|
+
const text = str(parsed?.output);
|
|
1053
|
+
if (parsed && text !== void 0) {
|
|
1054
|
+
const exit = record(parsed.metadata)?.exit_code;
|
|
1055
|
+
return { content: text, isError: typeof exit === "number" && exit !== 0 };
|
|
1056
|
+
}
|
|
1057
|
+
} catch {
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
return { content: output, isError: false };
|
|
1061
|
+
}
|
|
1062
|
+
const object = record(output);
|
|
1063
|
+
if (object) {
|
|
1064
|
+
const text = str(object.content);
|
|
1065
|
+
return { content: text ?? unknownBlockText(object), isError: object.success === false };
|
|
1066
|
+
}
|
|
1067
|
+
const blocks = [];
|
|
1068
|
+
for (const block of list(output).map(record)) {
|
|
1069
|
+
if (!block) continue;
|
|
1070
|
+
if (block.type === "input_text" || block.type === "output_text" || block.type === "text")
|
|
1071
|
+
blocks.push({ type: "text", text: str(block.text) ?? "" });
|
|
1072
|
+
else if (block.type === "input_image") blocks.push(IMAGE);
|
|
1073
|
+
else {
|
|
1074
|
+
counts.asText++;
|
|
1075
|
+
blocks.push({ type: "text", text: unknownBlockText(block) });
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
return { content: blocks, isError: false };
|
|
1079
|
+
}
|
|
1080
|
+
function compactionText(summary, kept) {
|
|
1081
|
+
const parts = [summary || "Codex compacted the conversation here. Its summary cannot be read outside Codex."];
|
|
1082
|
+
if (kept.length > 0) {
|
|
1083
|
+
const messages = clip(kept.map((text) => clip(text, 4e3).text).join("\n\n---\n\n"), 4e4).text;
|
|
1084
|
+
parts.push(`The user's messages that Codex kept after compacting:
|
|
1085
|
+
|
|
1086
|
+
${messages}`);
|
|
1087
|
+
}
|
|
1088
|
+
return parts.join("\n\n");
|
|
1089
|
+
}
|
|
1090
|
+
async function readCodex(path, options = {}) {
|
|
1091
|
+
const source = absolute(path);
|
|
1092
|
+
const builder = new SessionBuilder("codex");
|
|
1093
|
+
const { counts } = builder;
|
|
1094
|
+
let sourceId = "";
|
|
1095
|
+
let sawMeta = false;
|
|
1096
|
+
let cwd;
|
|
1097
|
+
let started;
|
|
1098
|
+
let model;
|
|
1099
|
+
let lastModel;
|
|
1100
|
+
let at = 0;
|
|
1101
|
+
let tokens = 0;
|
|
1102
|
+
let name;
|
|
1103
|
+
let firstCommand;
|
|
1104
|
+
let open;
|
|
1105
|
+
const flush = () => {
|
|
1106
|
+
if (!open) return;
|
|
1107
|
+
const { blocks, at: when, model: used } = open;
|
|
1108
|
+
open = void 0;
|
|
1109
|
+
builder.assistant({ blocks, model: used, at: when });
|
|
1110
|
+
};
|
|
1111
|
+
const answer = () => {
|
|
1112
|
+
open ??= { blocks: [], at, model: model ?? "unknown" };
|
|
1113
|
+
if (model) lastModel = model;
|
|
1114
|
+
return open;
|
|
1115
|
+
};
|
|
1116
|
+
const userMessage = (blocks) => {
|
|
1117
|
+
flush();
|
|
1118
|
+
const words = [];
|
|
1119
|
+
const say = () => {
|
|
1120
|
+
if (words.length > 0) builder.user(words.splice(0), at);
|
|
1121
|
+
};
|
|
1122
|
+
for (const block of blocks) {
|
|
1123
|
+
if (block.type === "input_image") {
|
|
1124
|
+
words.push(IMAGE);
|
|
1125
|
+
continue;
|
|
1126
|
+
}
|
|
1127
|
+
if (block.type !== "input_text" && block.type !== "text") {
|
|
1128
|
+
counts.asText++;
|
|
1129
|
+
words.push({ type: "text", text: unknownBlockText(block) });
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
const text = str(block.text) ?? "";
|
|
1133
|
+
const kind = classifyUserText(text);
|
|
1134
|
+
if (kind === "setup") {
|
|
1135
|
+
cwd ??= environmentCwd(text);
|
|
1136
|
+
builder.drop("Codex setup");
|
|
1137
|
+
continue;
|
|
1138
|
+
}
|
|
1139
|
+
if (kind === "context") {
|
|
1140
|
+
say();
|
|
1141
|
+
builder.context(text, at);
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
const shell = shellText(text);
|
|
1145
|
+
if (shell) {
|
|
1146
|
+
words.push({ type: "text", text: `! ${shell.command}` });
|
|
1147
|
+
say();
|
|
1148
|
+
builder.context(shell.output, at);
|
|
1149
|
+
continue;
|
|
1150
|
+
}
|
|
1151
|
+
const command = commandText(text);
|
|
1152
|
+
if (command !== void 0) {
|
|
1153
|
+
firstCommand ??= command;
|
|
1154
|
+
words.push({ type: "text", text: command });
|
|
1155
|
+
continue;
|
|
1156
|
+
}
|
|
1157
|
+
if (name === void 0 && text.trim()) name = firstLine(text);
|
|
1158
|
+
words.push({ type: "text", text });
|
|
1159
|
+
}
|
|
1160
|
+
say();
|
|
1161
|
+
};
|
|
1162
|
+
const item = (payload) => {
|
|
1163
|
+
const callId = str(payload.call_id) ?? str(payload.id) ?? "";
|
|
1164
|
+
switch (payload.type) {
|
|
1165
|
+
case "message": {
|
|
1166
|
+
const blocks = list(payload.content).map(record).filter((block) => block !== void 0);
|
|
1167
|
+
if (payload.role === "assistant") {
|
|
1168
|
+
const target = answer();
|
|
1169
|
+
for (const block of blocks) {
|
|
1170
|
+
if (block.type === "output_text" || block.type === "text" || block.type === "input_text")
|
|
1171
|
+
target.blocks.push({ type: "text", text: str(block.text) ?? "" });
|
|
1172
|
+
else if (block.type === "refusal")
|
|
1173
|
+
target.blocks.push({ type: "text", text: str(block.refusal) ?? "" });
|
|
1174
|
+
else {
|
|
1175
|
+
counts.asText++;
|
|
1176
|
+
target.blocks.push({ type: "text", text: unknownBlockText(block) });
|
|
1177
|
+
}
|
|
1178
|
+
}
|
|
1179
|
+
} else if (payload.role === "user") userMessage(blocks);
|
|
1180
|
+
else builder.drop("Codex instructions");
|
|
1181
|
+
return;
|
|
1182
|
+
}
|
|
1183
|
+
case "reasoning":
|
|
1184
|
+
builder.drop("thinking");
|
|
1185
|
+
return;
|
|
1186
|
+
case "function_call":
|
|
1187
|
+
case "tool_search_call":
|
|
1188
|
+
answer().blocks.push({
|
|
1189
|
+
type: "toolCall",
|
|
1190
|
+
id: callId,
|
|
1191
|
+
name: str(payload.name) ?? (payload.type === "tool_search_call" ? "tool_search" : "unknown"),
|
|
1192
|
+
arguments: toolArguments(payload.arguments)
|
|
1193
|
+
});
|
|
1194
|
+
return;
|
|
1195
|
+
case "custom_tool_call":
|
|
1196
|
+
answer().blocks.push({
|
|
1197
|
+
type: "toolCall",
|
|
1198
|
+
id: callId,
|
|
1199
|
+
name: str(payload.name) ?? "unknown",
|
|
1200
|
+
arguments: { input: str(payload.input) ?? "" }
|
|
1201
|
+
});
|
|
1202
|
+
return;
|
|
1203
|
+
case "local_shell_call":
|
|
1204
|
+
answer().blocks.push({
|
|
1205
|
+
type: "toolCall",
|
|
1206
|
+
id: callId,
|
|
1207
|
+
name: "local_shell",
|
|
1208
|
+
arguments: toolArguments(payload.action)
|
|
1209
|
+
});
|
|
1210
|
+
return;
|
|
1211
|
+
case "web_search_call": {
|
|
1212
|
+
const query = str(record(payload.action)?.query);
|
|
1213
|
+
counts.asText++;
|
|
1214
|
+
answer().blocks.push({ type: "text", text: query ? `(Searched the web: ${query})` : "(Searched the web)" });
|
|
1215
|
+
return;
|
|
1216
|
+
}
|
|
1217
|
+
case "function_call_output":
|
|
1218
|
+
case "custom_tool_call_output":
|
|
1219
|
+
case "local_shell_call_output": {
|
|
1220
|
+
flush();
|
|
1221
|
+
const output = outputOf(payload.output, counts);
|
|
1222
|
+
builder.toolResult({ id: callId, content: output.content, isError: output.isError, at });
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
case "tool_search_output": {
|
|
1226
|
+
flush();
|
|
1227
|
+
const found = list(payload.tools).map((tool) => str(record(tool)?.name)).filter((toolName) => toolName !== void 0);
|
|
1228
|
+
builder.toolResult({
|
|
1229
|
+
id: callId,
|
|
1230
|
+
content: found.length > 0 ? `Tools found: ${found.join(", ")}` : "No tools found.",
|
|
1231
|
+
isError: false,
|
|
1232
|
+
at
|
|
1233
|
+
});
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
case "agent_message": {
|
|
1237
|
+
flush();
|
|
1238
|
+
const text = list(payload.content).map((block) => str(record(block)?.text) ?? "").join("\n");
|
|
1239
|
+
const from = str(payload.author) ?? "an agent";
|
|
1240
|
+
const to = str(payload.recipient) ?? "an agent";
|
|
1241
|
+
builder.context(`[Message from ${from} to ${to}]
|
|
1242
|
+
${text}`, at);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
default:
|
|
1246
|
+
builder.drop("records not understood");
|
|
1247
|
+
}
|
|
1248
|
+
};
|
|
1249
|
+
const compacted = (payload) => {
|
|
1250
|
+
flush();
|
|
1251
|
+
const summary = (str(payload.message) ?? "").trim();
|
|
1252
|
+
const kept = [];
|
|
1253
|
+
for (const entry of list(payload.replacement_history).map(record)) {
|
|
1254
|
+
if (entry?.type !== "message" || entry.role !== "user") continue;
|
|
1255
|
+
for (const block of list(entry.content)) {
|
|
1256
|
+
const text = str(record(block)?.text)?.trim();
|
|
1257
|
+
if (!text || classifyUserText(text) !== "user") continue;
|
|
1258
|
+
if (summary && text.includes(summary.slice(0, 200))) continue;
|
|
1259
|
+
kept.push(text);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
builder.compaction(compactionText(summary, kept), tokens, at);
|
|
1263
|
+
};
|
|
1264
|
+
for await (const line of readJsonLines(source)) {
|
|
1265
|
+
counts.records++;
|
|
1266
|
+
if (!("value" in line)) {
|
|
1267
|
+
builder.drop(line.problem === "oversized" ? "lines too long to read" : "unreadable lines");
|
|
1268
|
+
continue;
|
|
1269
|
+
}
|
|
1270
|
+
const data = record(line.value);
|
|
1271
|
+
if (!data) {
|
|
1272
|
+
builder.drop("records not understood");
|
|
1273
|
+
continue;
|
|
1274
|
+
}
|
|
1275
|
+
const payload = record(data.payload);
|
|
1276
|
+
const type = str(data.type);
|
|
1277
|
+
if (payload && type) {
|
|
1278
|
+
const time = Date.parse(str(data.timestamp) ?? "");
|
|
1279
|
+
if (!Number.isNaN(time)) at = time;
|
|
1280
|
+
if (type === "session_meta") {
|
|
1281
|
+
if (!sawMeta) {
|
|
1282
|
+
sawMeta = true;
|
|
1283
|
+
sourceId = str(payload.id) ?? "";
|
|
1284
|
+
cwd = str(payload.cwd) ?? cwd;
|
|
1285
|
+
started = str(payload.timestamp) ?? str(data.timestamp);
|
|
1286
|
+
}
|
|
1287
|
+
} else if (type === "turn_context") {
|
|
1288
|
+
model = str(payload.model) ?? model;
|
|
1289
|
+
cwd ??= str(payload.cwd);
|
|
1290
|
+
} else if (type === "response_item") item(payload);
|
|
1291
|
+
else if (type === "compacted") compacted(payload);
|
|
1292
|
+
else if (APP_RECORDS.has(type)) {
|
|
1293
|
+
if (type === "event_msg" && payload.type === "token_count") {
|
|
1294
|
+
const input = record(record(payload.info)?.last_token_usage)?.input_tokens;
|
|
1295
|
+
if (typeof input === "number") tokens = input;
|
|
1296
|
+
}
|
|
1297
|
+
builder.drop("Codex events and state");
|
|
1298
|
+
} else builder.drop("records not understood");
|
|
1299
|
+
continue;
|
|
1300
|
+
}
|
|
1301
|
+
if (data.record_type === "state") {
|
|
1302
|
+
builder.drop("Codex events and state");
|
|
1303
|
+
continue;
|
|
1304
|
+
}
|
|
1305
|
+
if (type) {
|
|
1306
|
+
item(data);
|
|
1307
|
+
continue;
|
|
1308
|
+
}
|
|
1309
|
+
const id = str(data.id);
|
|
1310
|
+
const stamp = str(data.timestamp);
|
|
1311
|
+
if (id && stamp) {
|
|
1312
|
+
if (!sourceId) sourceId = id;
|
|
1313
|
+
started ??= stamp;
|
|
1314
|
+
const time = Date.parse(stamp);
|
|
1315
|
+
if (!Number.isNaN(time)) at = time;
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
builder.drop("records not understood");
|
|
1319
|
+
}
|
|
1320
|
+
flush();
|
|
1321
|
+
let startedIso = started && !Number.isNaN(Date.parse(started)) ? new Date(Date.parse(started)).toISOString() : void 0;
|
|
1322
|
+
if (!startedIso) {
|
|
1323
|
+
try {
|
|
1324
|
+
startedIso = statSync3(source).mtime.toISOString();
|
|
1325
|
+
} catch {
|
|
1326
|
+
startedIso = (/* @__PURE__ */ new Date()).toISOString();
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const fromName = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(source)?.[1];
|
|
1330
|
+
return complete(builder, {
|
|
1331
|
+
source,
|
|
1332
|
+
sourceId: sourceId || fromName || basename2(source, ".jsonl"),
|
|
1333
|
+
cwd: cwd ?? options.cwd ?? process.cwd(),
|
|
1334
|
+
started: startedIso,
|
|
1335
|
+
name: name ?? firstCommand,
|
|
1336
|
+
model: lastModel ?? model,
|
|
1337
|
+
now: options.now
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
// packages/kyrn-judge/src/import/index.ts
|
|
1342
|
+
var CODEX_ITEMS = /* @__PURE__ */ new Set([
|
|
1343
|
+
"message",
|
|
1344
|
+
"function_call",
|
|
1345
|
+
"function_call_output",
|
|
1346
|
+
"custom_tool_call",
|
|
1347
|
+
"custom_tool_call_output",
|
|
1348
|
+
"local_shell_call",
|
|
1349
|
+
"reasoning"
|
|
1350
|
+
]);
|
|
1351
|
+
function detectTool(path) {
|
|
1352
|
+
let tool;
|
|
1353
|
+
let lines = 0;
|
|
1354
|
+
readHead(
|
|
1355
|
+
path,
|
|
1356
|
+
(value) => {
|
|
1357
|
+
lines++;
|
|
1358
|
+
const data = record(value);
|
|
1359
|
+
if (data) {
|
|
1360
|
+
if (record(data.payload) && typeof data.type === "string") tool = "codex";
|
|
1361
|
+
else if (typeof data.sessionId === "string" || typeof data.uuid === "string" || typeof data.leafUuid === "string")
|
|
1362
|
+
tool = "claude-code";
|
|
1363
|
+
else if (data.record_type === "state") tool = "codex";
|
|
1364
|
+
else if (typeof data.id === "string" && typeof data.timestamp === "string" && data.type === void 0)
|
|
1365
|
+
tool = "codex";
|
|
1366
|
+
else if (typeof data.type === "string" && CODEX_ITEMS.has(data.type)) tool = "codex";
|
|
1367
|
+
}
|
|
1368
|
+
return tool !== void 0 || lines >= 50;
|
|
1369
|
+
},
|
|
1370
|
+
4 * 1024 * 1024
|
|
1371
|
+
);
|
|
1372
|
+
return tool;
|
|
1373
|
+
}
|
|
1374
|
+
var TREE_TYPES2 = /* @__PURE__ */ new Set(["user", "assistant", "system", "attachment"]);
|
|
1375
|
+
function titleParts(text) {
|
|
1376
|
+
if (classifyUserText(text) !== "user") return {};
|
|
1377
|
+
const command = commandText(text);
|
|
1378
|
+
if (command !== void 0) return { command };
|
|
1379
|
+
if (/^\s*<bash-input>/.test(text)) return {};
|
|
1380
|
+
const line = firstLine(text);
|
|
1381
|
+
return line ? { title: line } : {};
|
|
1382
|
+
}
|
|
1383
|
+
function describeClaudeCode(path) {
|
|
1384
|
+
let id;
|
|
1385
|
+
let agentId;
|
|
1386
|
+
let cwd;
|
|
1387
|
+
let started;
|
|
1388
|
+
let title;
|
|
1389
|
+
let command;
|
|
1390
|
+
let named;
|
|
1391
|
+
let first;
|
|
1392
|
+
readHead(path, (value) => {
|
|
1393
|
+
const data = record(value);
|
|
1394
|
+
if (!data) return false;
|
|
1395
|
+
id ??= str(data.sessionId);
|
|
1396
|
+
const type = str(data.type);
|
|
1397
|
+
if (type === "custom-title") named ??= str(data.customTitle);
|
|
1398
|
+
else if (type === "ai-title") named ??= str(data.aiTitle);
|
|
1399
|
+
else if (type === "summary") named ??= str(data.summary);
|
|
1400
|
+
if (!type || !TREE_TYPES2.has(type)) return false;
|
|
1401
|
+
first ??= data.isSidechain === true;
|
|
1402
|
+
if (first) agentId ??= str(data.agentId);
|
|
1403
|
+
cwd ??= str(data.cwd);
|
|
1404
|
+
started ??= str(data.timestamp);
|
|
1405
|
+
const own = first || data.isSidechain !== true;
|
|
1406
|
+
if (type === "user" && own && data.isMeta !== true && data.isCompactSummary !== true) {
|
|
1407
|
+
const message = record(data.message);
|
|
1408
|
+
const content = message?.content;
|
|
1409
|
+
const texts = typeof content === "string" ? [content] : (Array.isArray(content) ? content : []).map((block) => record(block)).filter((block) => block?.type === "text").map((block) => str(block?.text) ?? "");
|
|
1410
|
+
for (const text of texts) {
|
|
1411
|
+
const parts = titleParts(text);
|
|
1412
|
+
title ??= parts.title;
|
|
1413
|
+
command ??= parts.command;
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return title !== void 0 && cwd !== void 0 && id !== void 0;
|
|
1417
|
+
});
|
|
1418
|
+
return {
|
|
1419
|
+
id: first && agentId ? `agent-${agentId}` : id ?? "",
|
|
1420
|
+
cwd,
|
|
1421
|
+
title: title ?? named ?? command ?? "",
|
|
1422
|
+
started,
|
|
1423
|
+
subAgent: first === true
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
function describeCodex(path) {
|
|
1427
|
+
let id;
|
|
1428
|
+
let cwd;
|
|
1429
|
+
let started;
|
|
1430
|
+
let title;
|
|
1431
|
+
let command;
|
|
1432
|
+
let subAgent = false;
|
|
1433
|
+
let sawMeta = false;
|
|
1434
|
+
const visit = (item) => {
|
|
1435
|
+
if (item.type !== "message" || item.role !== "user" || !Array.isArray(item.content)) return;
|
|
1436
|
+
for (const block of item.content) {
|
|
1437
|
+
const text = str(record(block)?.text);
|
|
1438
|
+
if (text === void 0) continue;
|
|
1439
|
+
if (classifyUserText(text) === "setup") cwd ??= environmentCwd(text);
|
|
1440
|
+
const parts = titleParts(text);
|
|
1441
|
+
title ??= parts.title;
|
|
1442
|
+
command ??= parts.command;
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
readHead(path, (value) => {
|
|
1446
|
+
const data = record(value);
|
|
1447
|
+
if (!data) return false;
|
|
1448
|
+
const payload = record(data.payload);
|
|
1449
|
+
const type = str(data.type);
|
|
1450
|
+
if (payload && type) {
|
|
1451
|
+
if (type === "session_meta" && !sawMeta) {
|
|
1452
|
+
sawMeta = true;
|
|
1453
|
+
id = str(payload.id) ?? id;
|
|
1454
|
+
cwd = str(payload.cwd) ?? cwd;
|
|
1455
|
+
started = str(payload.timestamp) ?? str(data.timestamp);
|
|
1456
|
+
subAgent = record(payload.source) !== void 0 || payload.thread_source === "subagent";
|
|
1457
|
+
} else if (type === "turn_context") cwd ??= str(payload.cwd);
|
|
1458
|
+
else if (type === "response_item") visit(payload);
|
|
1459
|
+
} else if (type) visit(data);
|
|
1460
|
+
else if (str(data.id) && str(data.timestamp)) {
|
|
1461
|
+
id ??= str(data.id);
|
|
1462
|
+
started ??= str(data.timestamp);
|
|
1463
|
+
}
|
|
1464
|
+
return title !== void 0 && cwd !== void 0 && id !== void 0;
|
|
1465
|
+
});
|
|
1466
|
+
const fromName = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i.exec(path)?.[1];
|
|
1467
|
+
return { id: id ?? fromName ?? "", cwd, title: title ?? command ?? "", started, subAgent };
|
|
1468
|
+
}
|
|
1469
|
+
function claudeProjectsDir(env = process.env, home = homedir2()) {
|
|
1470
|
+
return join2(env.CLAUDE_CONFIG_DIR ? absolute(env.CLAUDE_CONFIG_DIR) : join2(home, ".claude"), "projects");
|
|
1471
|
+
}
|
|
1472
|
+
function codexSessionsDir(env = process.env, home = homedir2()) {
|
|
1473
|
+
return join2(env.CODEX_HOME ? absolute(env.CODEX_HOME) : join2(home, ".codex"), "sessions");
|
|
1474
|
+
}
|
|
1475
|
+
function sameFolder(a, b) {
|
|
1476
|
+
const canonical = (dir) => {
|
|
1477
|
+
const path = absolute(dir);
|
|
1478
|
+
try {
|
|
1479
|
+
return realpathSync.native(path);
|
|
1480
|
+
} catch {
|
|
1481
|
+
return path;
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
return canonical(a) === canonical(b);
|
|
1485
|
+
}
|
|
1486
|
+
function jsonlFiles(dir, depth) {
|
|
1487
|
+
let names;
|
|
1488
|
+
try {
|
|
1489
|
+
names = readdirSync2(dir);
|
|
1490
|
+
} catch {
|
|
1491
|
+
return [];
|
|
1492
|
+
}
|
|
1493
|
+
const files = [];
|
|
1494
|
+
for (const name of names.sort()) {
|
|
1495
|
+
const path = join2(dir, name);
|
|
1496
|
+
if (name.endsWith(".jsonl")) files.push(path);
|
|
1497
|
+
else if (depth > 0 && !name.startsWith(".")) {
|
|
1498
|
+
try {
|
|
1499
|
+
if (statSync4(path).isDirectory()) files.push(...jsonlFiles(path, depth - 1));
|
|
1500
|
+
} catch {
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
return files;
|
|
1505
|
+
}
|
|
1506
|
+
function listConversations(options = {}) {
|
|
1507
|
+
const claudeProjects = options.claudeProjects ?? claudeProjectsDir();
|
|
1508
|
+
const codexSessions = options.codexSessions ?? codexSessionsDir();
|
|
1509
|
+
const candidates = [];
|
|
1510
|
+
let projects = [];
|
|
1511
|
+
try {
|
|
1512
|
+
projects = readdirSync2(claudeProjects).sort();
|
|
1513
|
+
} catch {
|
|
1514
|
+
}
|
|
1515
|
+
for (const project of projects) {
|
|
1516
|
+
for (const path of jsonlFiles(join2(claudeProjects, project), 0)) candidates.push({ tool: "claude-code", path });
|
|
1517
|
+
}
|
|
1518
|
+
for (const path of jsonlFiles(codexSessions, 4)) candidates.push({ tool: "codex", path });
|
|
1519
|
+
const imported = /* @__PURE__ */ new Map();
|
|
1520
|
+
if (options.store) {
|
|
1521
|
+
for (const found of findImports(options.store)) {
|
|
1522
|
+
imported.set(`${found.origin.tool}:id:${found.origin.sourceId}`, found.sessionFile);
|
|
1523
|
+
imported.set(`${found.origin.tool}:path:${found.origin.source}`, found.sessionFile);
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
const conversations = [];
|
|
1527
|
+
for (const { tool, path } of candidates) {
|
|
1528
|
+
let size;
|
|
1529
|
+
let modified;
|
|
1530
|
+
try {
|
|
1531
|
+
const stats = statSync4(path);
|
|
1532
|
+
size = stats.size;
|
|
1533
|
+
modified = stats.mtime.toISOString();
|
|
1534
|
+
} catch {
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
let described;
|
|
1538
|
+
try {
|
|
1539
|
+
described = tool === "codex" ? describeCodex(path) : describeClaudeCode(path);
|
|
1540
|
+
} catch {
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
if (described.subAgent && !options.subAgents) continue;
|
|
1544
|
+
if (options.cwd && !(described.cwd && sameFolder(described.cwd, options.cwd))) continue;
|
|
1545
|
+
const importedAs = (described.id ? imported.get(`${tool}:id:${described.id}`) : void 0) ?? imported.get(`${tool}:path:${path}`);
|
|
1546
|
+
conversations.push({ tool, path, ...described, modified, size, ...importedAs ? { importedAs } : {} });
|
|
1547
|
+
}
|
|
1548
|
+
return conversations.sort((a, b) => b.modified.localeCompare(a.modified));
|
|
1549
|
+
}
|
|
1550
|
+
async function importOne(path, options, imports) {
|
|
1551
|
+
const source = absolute(path);
|
|
1552
|
+
const failed = (error) => ({ status: "failed", source, error });
|
|
1553
|
+
try {
|
|
1554
|
+
if (!statSync4(source).isFile()) return failed("not a file");
|
|
1555
|
+
} catch {
|
|
1556
|
+
return failed("no such file");
|
|
1557
|
+
}
|
|
1558
|
+
const already = (tool, earlier) => ({
|
|
1559
|
+
status: "already-imported",
|
|
1560
|
+
source,
|
|
1561
|
+
tool,
|
|
1562
|
+
sessionFile: earlier.sessionFile,
|
|
1563
|
+
sessionId: earlier.sessionId
|
|
1564
|
+
});
|
|
1565
|
+
try {
|
|
1566
|
+
const tool = detectTool(source);
|
|
1567
|
+
if (!tool) return failed("not a Claude Code or Codex transcript");
|
|
1568
|
+
const quick = (tool === "codex" ? describeCodex(source) : describeClaudeCode(source)).id;
|
|
1569
|
+
const known = findImport(imports, tool, quick, source);
|
|
1570
|
+
if (known) return already(tool, known);
|
|
1571
|
+
const converted = tool === "codex" ? await readCodex(source, options) : await readClaudeCode(source, options);
|
|
1572
|
+
const earlier = findImport(imports, tool, converted.sourceId, source);
|
|
1573
|
+
if (earlier) return already(tool, earlier);
|
|
1574
|
+
if (!converted.hasConversation) return failed("no messages in this transcript");
|
|
1575
|
+
const dir = sessionDirFor(converted.cwd, options);
|
|
1576
|
+
mkdirSync(dir, { recursive: true });
|
|
1577
|
+
const sessionFile = join2(dir, sessionFileName(converted.started, converted.sessionId));
|
|
1578
|
+
writeSessionFile(sessionFile, converted.entries);
|
|
1579
|
+
imports.push({
|
|
1580
|
+
sessionFile,
|
|
1581
|
+
sessionId: converted.sessionId,
|
|
1582
|
+
origin: {
|
|
1583
|
+
version: 1,
|
|
1584
|
+
tool,
|
|
1585
|
+
source,
|
|
1586
|
+
sourceId: converted.sourceId,
|
|
1587
|
+
importedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1588
|
+
counts: converted.counts
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
return {
|
|
1592
|
+
status: "imported",
|
|
1593
|
+
source,
|
|
1594
|
+
tool,
|
|
1595
|
+
sessionFile,
|
|
1596
|
+
sessionId: converted.sessionId,
|
|
1597
|
+
name: converted.name,
|
|
1598
|
+
cwd: converted.cwd,
|
|
1599
|
+
counts: converted.counts
|
|
1600
|
+
};
|
|
1601
|
+
} catch (error) {
|
|
1602
|
+
return failed(error instanceof Error ? error.message : String(error));
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
async function importTranscripts(paths, options) {
|
|
1606
|
+
const imports = findImports(options);
|
|
1607
|
+
const results = [];
|
|
1608
|
+
for (const path of paths) results.push(await importOne(path, options, imports));
|
|
1609
|
+
return results;
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
// packages/kyrn-judge/src/import/command.ts
|
|
1613
|
+
var USAGE = [
|
|
1614
|
+
"mu import: bring Claude Code and Codex conversations into mu, to continue them here.",
|
|
1615
|
+
"",
|
|
1616
|
+
" mu import --list [--json] [--cwd <dir>] the conversations on this machine (only <dir>'s with --cwd)",
|
|
1617
|
+
" mu import <file>... [--json] import these transcripts; each becomes a mu session",
|
|
1618
|
+
"",
|
|
1619
|
+
" --agent-dir <dir> mu's agent folder (default: MU_CODING_AGENT_DIR, else ~/.mu/agent)",
|
|
1620
|
+
" --sub-agents also list the threads sub-agents ran",
|
|
1621
|
+
"",
|
|
1622
|
+
"Looks in ~/.claude/projects (CLAUDE_CONFIG_DIR) and ~/.codex/sessions (CODEX_HOME).",
|
|
1623
|
+
"A conversation that was imported before is not imported again."
|
|
1624
|
+
].join("\n");
|
|
1625
|
+
function parseArgs(argv) {
|
|
1626
|
+
const parsed = { help: false, list: false, json: false, subAgents: false, files: [] };
|
|
1627
|
+
for (let index = 0; index < argv.length; index++) {
|
|
1628
|
+
const arg = argv[index];
|
|
1629
|
+
const value = () => {
|
|
1630
|
+
const next = argv[index + 1];
|
|
1631
|
+
index++;
|
|
1632
|
+
return next === void 0 || next.startsWith("--") ? void 0 : next;
|
|
1633
|
+
};
|
|
1634
|
+
if (arg === "-h" || arg === "--help" || arg === "help") parsed.help = true;
|
|
1635
|
+
else if (arg === "--list" || arg === "-l") parsed.list = true;
|
|
1636
|
+
else if (arg === "--json") parsed.json = true;
|
|
1637
|
+
else if (arg === "--sub-agents") parsed.subAgents = true;
|
|
1638
|
+
else if (arg === "--cwd") {
|
|
1639
|
+
const dir = value();
|
|
1640
|
+
if (!dir) return { error: "--cwd needs a folder" };
|
|
1641
|
+
parsed.cwd = dir;
|
|
1642
|
+
} else if (arg === "--agent-dir") {
|
|
1643
|
+
const dir = value();
|
|
1644
|
+
if (!dir) return { error: "--agent-dir needs a folder" };
|
|
1645
|
+
parsed.agentDir = dir;
|
|
1646
|
+
} else if (arg.startsWith("--")) return { error: `unknown option ${arg}` };
|
|
1647
|
+
else parsed.files.push(arg);
|
|
1648
|
+
}
|
|
1649
|
+
if (parsed.list && parsed.files.length > 0) return { error: "--list takes no files" };
|
|
1650
|
+
return parsed;
|
|
1651
|
+
}
|
|
1652
|
+
function storeFor(agentDirArg, io) {
|
|
1653
|
+
const agentDir = absolute(
|
|
1654
|
+
agentDirArg ?? io.env.MU_CODING_AGENT_DIR ?? io.env.KYRN_CODING_AGENT_DIR ?? join3(io.home, ".mu", "agent"),
|
|
1655
|
+
io.cwd
|
|
1656
|
+
);
|
|
1657
|
+
const fromEnv = io.env.MU_CODING_AGENT_SESSION_DIR;
|
|
1658
|
+
if (fromEnv) return { agentDir, sessionDir: absolute(fromEnv, io.cwd) };
|
|
1659
|
+
try {
|
|
1660
|
+
const settings = JSON.parse(readFileSync(join3(agentDir, "settings.json"), "utf8"));
|
|
1661
|
+
const sessionDir = settings !== null && typeof settings === "object" && "sessionDir" in settings ? settings.sessionDir : void 0;
|
|
1662
|
+
if (typeof sessionDir === "string" && sessionDir.trim())
|
|
1663
|
+
return { agentDir, sessionDir: absolute(sessionDir, io.cwd) };
|
|
1664
|
+
} catch {
|
|
1665
|
+
}
|
|
1666
|
+
return { agentDir };
|
|
1667
|
+
}
|
|
1668
|
+
function formatSize(bytes) {
|
|
1669
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
1670
|
+
const units = ["KB", "MB", "GB", "TB"];
|
|
1671
|
+
let value = bytes / 1024;
|
|
1672
|
+
let unit = 0;
|
|
1673
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
1674
|
+
value /= 1024;
|
|
1675
|
+
unit++;
|
|
1676
|
+
}
|
|
1677
|
+
return `${value < 10 ? value.toFixed(1) : Math.round(value)} ${units[unit]}`;
|
|
1678
|
+
}
|
|
1679
|
+
function formatDate(iso) {
|
|
1680
|
+
const date = iso ? new Date(iso) : void 0;
|
|
1681
|
+
if (!date || Number.isNaN(date.getTime())) return "????-??-?? ??:??";
|
|
1682
|
+
const two = (value) => String(value).padStart(2, "0");
|
|
1683
|
+
return `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
|
|
1684
|
+
}
|
|
1685
|
+
function tilde(path, home) {
|
|
1686
|
+
return path === home ? "~" : path.startsWith(home + sep) ? `~${path.slice(home.length)}` : path;
|
|
1687
|
+
}
|
|
1688
|
+
function formatList(conversations, home) {
|
|
1689
|
+
if (conversations.length === 0) return "No Claude Code or Codex conversations found.";
|
|
1690
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1691
|
+
for (const conversation of conversations) {
|
|
1692
|
+
const key = conversation.cwd ?? "";
|
|
1693
|
+
const group = groups.get(key) ?? [];
|
|
1694
|
+
group.push(conversation);
|
|
1695
|
+
groups.set(key, group);
|
|
1696
|
+
}
|
|
1697
|
+
const lines = [];
|
|
1698
|
+
const toolWidth = Math.max(...Object.values(TOOL_NAMES).map((name) => name.length));
|
|
1699
|
+
for (const [cwd, group] of groups) {
|
|
1700
|
+
if (lines.length > 0) lines.push("");
|
|
1701
|
+
lines.push(cwd ? tilde(cwd, home) : "(folder unknown)");
|
|
1702
|
+
for (const conversation of group) {
|
|
1703
|
+
const title = conversation.title || "(no message found)";
|
|
1704
|
+
const mark = conversation.importedAs ? " (imported)" : "";
|
|
1705
|
+
lines.push(
|
|
1706
|
+
` ${formatDate(conversation.modified)} ${TOOL_NAMES[conversation.tool].padEnd(toolWidth)} ${formatSize(conversation.size).padStart(7)} ${title}${mark}`
|
|
1707
|
+
);
|
|
1708
|
+
lines.push(`${" ".repeat(20)}${tilde(conversation.path, home)}`);
|
|
1709
|
+
}
|
|
1710
|
+
}
|
|
1711
|
+
const count = conversations.length;
|
|
1712
|
+
lines.push("", `${count} conversation${count === 1 ? "" : "s"}. Import one with: mu import <file>`);
|
|
1713
|
+
return lines.join("\n");
|
|
1714
|
+
}
|
|
1715
|
+
function formatResult(result, home) {
|
|
1716
|
+
if (result.status === "failed") return `Could not import ${tilde(result.source, home)}: ${result.error}`;
|
|
1717
|
+
if (result.status === "already-imported")
|
|
1718
|
+
return [
|
|
1719
|
+
`Already imported: ${tilde(result.source, home)}`,
|
|
1720
|
+
` it is the session ${tilde(result.sessionFile, home)}`
|
|
1721
|
+
].join("\n");
|
|
1722
|
+
const { counts } = result;
|
|
1723
|
+
const left = Object.entries(counts.dropped).sort((a, b) => b[1] - a[1]).map(([reason, count]) => `${reason} ${count}`);
|
|
1724
|
+
return [
|
|
1725
|
+
`Imported from ${TOOL_NAMES[result.tool]}: "${result.name}"`,
|
|
1726
|
+
` ${tilde(result.sessionFile, home)}`,
|
|
1727
|
+
` ${counts.user} messages from you, ${counts.assistant} answers, ${counts.toolCalls} tool calls${counts.compactions > 0 ? `, ${counts.compactions} compactions` : ""}`,
|
|
1728
|
+
...left.length > 0 ? [` left out: ${left.join(", ")}`] : [],
|
|
1729
|
+
` continue it: mu --session ${result.sessionFile}`
|
|
1730
|
+
].join("\n");
|
|
1731
|
+
}
|
|
1732
|
+
async function main(argv, io) {
|
|
1733
|
+
const parsed = parseArgs(argv);
|
|
1734
|
+
if ("error" in parsed) {
|
|
1735
|
+
io.err(`mu import: ${parsed.error}
|
|
1736
|
+
|
|
1737
|
+
${USAGE}`);
|
|
1738
|
+
return 2;
|
|
1739
|
+
}
|
|
1740
|
+
if (parsed.help || !parsed.list && parsed.files.length === 0) {
|
|
1741
|
+
io.out(USAGE);
|
|
1742
|
+
return parsed.help ? 0 : 2;
|
|
1743
|
+
}
|
|
1744
|
+
const store = storeFor(parsed.agentDir, io);
|
|
1745
|
+
if (parsed.list) {
|
|
1746
|
+
const conversations = listConversations({
|
|
1747
|
+
cwd: parsed.cwd === void 0 ? void 0 : absolute(parsed.cwd, io.cwd),
|
|
1748
|
+
store,
|
|
1749
|
+
subAgents: parsed.subAgents
|
|
1750
|
+
});
|
|
1751
|
+
io.out(parsed.json ? JSON.stringify({ conversations }, null, 2) : formatList(conversations, io.home));
|
|
1752
|
+
return 0;
|
|
1753
|
+
}
|
|
1754
|
+
const results = await importTranscripts(
|
|
1755
|
+
parsed.files.map((file) => absolute(file, io.cwd)),
|
|
1756
|
+
{ ...store, cwd: io.cwd }
|
|
1757
|
+
);
|
|
1758
|
+
io.out(
|
|
1759
|
+
parsed.json ? JSON.stringify({ results }, null, 2) : results.map((result) => formatResult(result, io.home)).join("\n\n")
|
|
1760
|
+
);
|
|
1761
|
+
return results.some((result) => result.status === "failed") ? 1 : 0;
|
|
1762
|
+
}
|
|
1763
|
+
function processIo() {
|
|
1764
|
+
return {
|
|
1765
|
+
out: (text) => process.stdout.write(`${text}
|
|
1766
|
+
`),
|
|
1767
|
+
err: (text) => process.stderr.write(`${text}
|
|
1768
|
+
`),
|
|
1769
|
+
env: process.env,
|
|
1770
|
+
cwd: process.cwd(),
|
|
1771
|
+
home: homedir3()
|
|
1772
|
+
};
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// packages/kyrn-judge/src/import/cli.ts
|
|
1776
|
+
main(process.argv.slice(2), processIo()).then(
|
|
1777
|
+
(code) => {
|
|
1778
|
+
process.exitCode = code;
|
|
1779
|
+
},
|
|
1780
|
+
(error) => {
|
|
1781
|
+
process.stderr.write(`mu import: ${error instanceof Error ? error.message : String(error)}
|
|
1782
|
+
`);
|
|
1783
|
+
process.exitCode = 1;
|
|
1784
|
+
}
|
|
1785
|
+
);
|