linkshell-cli 0.3.9 → 0.3.10
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/cli/src/runtime/acp/claude-sessions.d.ts +32 -4
- package/dist/cli/src/runtime/acp/claude-sessions.js +175 -7
- package/dist/cli/src/runtime/acp/claude-sessions.js.map +1 -1
- package/dist/cli/src/runtime/acp/codex-sessions.d.ts +32 -4
- package/dist/cli/src/runtime/acp/codex-sessions.js +293 -14
- package/dist/cli/src/runtime/acp/codex-sessions.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/runtime/acp/claude-sessions.ts +218 -10
- package/src/runtime/acp/codex-sessions.ts +336 -17
|
@@ -18,10 +18,38 @@ export interface ClaudeStoredSession {
|
|
|
18
18
|
export interface StoredAgentTimelineItem {
|
|
19
19
|
id: string;
|
|
20
20
|
conversationId: string;
|
|
21
|
-
type: "message";
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
type: "message" | "tool_call";
|
|
22
|
+
kind?: "tool_activity" | "command_execution" | "file_change";
|
|
23
|
+
itemId?: string;
|
|
24
|
+
role?: "user" | "assistant" | "system";
|
|
25
|
+
content?: Array<{ type: "text"; text: string }>;
|
|
26
|
+
text?: string;
|
|
27
|
+
toolCall?: {
|
|
28
|
+
id: string;
|
|
29
|
+
name: string;
|
|
30
|
+
input?: string;
|
|
31
|
+
output?: string;
|
|
32
|
+
createdAt?: number;
|
|
33
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
34
|
+
};
|
|
35
|
+
commandExecution?: {
|
|
36
|
+
command?: string;
|
|
37
|
+
cwd?: string;
|
|
38
|
+
output?: string;
|
|
39
|
+
exitCode?: number | null;
|
|
40
|
+
status?: "pending" | "running" | "completed" | "failed";
|
|
41
|
+
};
|
|
42
|
+
fileChange?: {
|
|
43
|
+
entries: Array<{
|
|
44
|
+
path: string;
|
|
45
|
+
kind?: string;
|
|
46
|
+
added?: number;
|
|
47
|
+
removed?: number;
|
|
48
|
+
}>;
|
|
49
|
+
diff?: string;
|
|
50
|
+
summary?: string;
|
|
51
|
+
status?: "pending" | "running" | "completed" | "failed";
|
|
52
|
+
};
|
|
25
53
|
createdAt: number;
|
|
26
54
|
updatedAt?: number;
|
|
27
55
|
metadata?: Record<string, unknown>;
|
|
@@ -110,6 +138,35 @@ function extractHistoryText(value: unknown): string | undefined {
|
|
|
110
138
|
return extractHistoryText(record.content ?? record.text ?? record.message);
|
|
111
139
|
}
|
|
112
140
|
|
|
141
|
+
function extractVisibleClaudeText(value: unknown): string | undefined {
|
|
142
|
+
if (!Array.isArray(value)) return extractHistoryText(value);
|
|
143
|
+
const text = value
|
|
144
|
+
.map((part) => {
|
|
145
|
+
if (typeof part === "string") return part;
|
|
146
|
+
const record = asRecord(part);
|
|
147
|
+
if (!record || record.type === "tool_use" || record.type === "tool_result") return "";
|
|
148
|
+
return typeof record.text === "string"
|
|
149
|
+
? record.text
|
|
150
|
+
: typeof record.content === "string"
|
|
151
|
+
? record.content
|
|
152
|
+
: "";
|
|
153
|
+
})
|
|
154
|
+
.filter(Boolean)
|
|
155
|
+
.join("\n")
|
|
156
|
+
.trim();
|
|
157
|
+
return text || undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function stringifyHistoryValue(value: unknown): string | undefined {
|
|
161
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
162
|
+
if (typeof value === "string") return value;
|
|
163
|
+
try {
|
|
164
|
+
return JSON.stringify(value, null, 2);
|
|
165
|
+
} catch {
|
|
166
|
+
return String(value);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
113
170
|
function guessCwdFromProjectDir(projectDirName: string, fallbackCwd: string): string {
|
|
114
171
|
const trimmed = projectDirName.replace(/^-+/, "");
|
|
115
172
|
if (!trimmed) return resolve(fallbackCwd);
|
|
@@ -292,6 +349,127 @@ function historyMessage(
|
|
|
292
349
|
};
|
|
293
350
|
}
|
|
294
351
|
|
|
352
|
+
function claudeToolName(name: string | undefined): string {
|
|
353
|
+
if (!name) return "工具";
|
|
354
|
+
if (name === "Bash") return "命令";
|
|
355
|
+
if (isClaudeFileTool(name)) return "文件修改";
|
|
356
|
+
return name;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function isClaudeFileTool(name: string | undefined): boolean {
|
|
360
|
+
return name === "Edit" || name === "MultiEdit" || name === "Write" || name === "NotebookEdit";
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function claudeToolKind(name: string | undefined): "tool_activity" | "command_execution" | "file_change" {
|
|
364
|
+
if (name === "Bash") return "command_execution";
|
|
365
|
+
if (isClaudeFileTool(name)) return "file_change";
|
|
366
|
+
return "tool_activity";
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function claudeFileEntry(name: string | undefined, input: Record<string, unknown> | undefined): StoredAgentTimelineItem["fileChange"] | undefined {
|
|
370
|
+
if (!isClaudeFileTool(name) || !input) return undefined;
|
|
371
|
+
const rawPath = input.file_path ?? input.path ?? input.notebook_path;
|
|
372
|
+
if (typeof rawPath !== "string" || !rawPath.trim()) return undefined;
|
|
373
|
+
const path = rawPath.trim();
|
|
374
|
+
const kind = name === "Write" ? "create" : "update";
|
|
375
|
+
const added = typeof input.new_string === "string"
|
|
376
|
+
? input.new_string.split(/\r?\n/).filter((line) => line.length > 0).length
|
|
377
|
+
: typeof input.content === "string"
|
|
378
|
+
? input.content.split(/\r?\n/).filter((line) => line.length > 0).length
|
|
379
|
+
: Array.isArray(input.edits)
|
|
380
|
+
? input.edits.length
|
|
381
|
+
: undefined;
|
|
382
|
+
const removed = typeof input.old_string === "string"
|
|
383
|
+
? input.old_string.split(/\r?\n/).filter((line) => line.length > 0).length
|
|
384
|
+
: undefined;
|
|
385
|
+
const entry: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] = { path, kind };
|
|
386
|
+
if (added && added > 0) entry.added = added;
|
|
387
|
+
if (removed && removed > 0) entry.removed = removed;
|
|
388
|
+
return {
|
|
389
|
+
entries: [entry],
|
|
390
|
+
summary: [kind, path].filter(Boolean).join(" "),
|
|
391
|
+
status: "running",
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function claudeCommandExecution(input: Record<string, unknown> | undefined): StoredAgentTimelineItem["commandExecution"] | undefined {
|
|
396
|
+
if (!input) return undefined;
|
|
397
|
+
const command = typeof input.command === "string" ? input.command : undefined;
|
|
398
|
+
const cwd = typeof input.cwd === "string" ? input.cwd : undefined;
|
|
399
|
+
if (!command && !cwd) return undefined;
|
|
400
|
+
return { command, cwd, status: "running" };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
function createClaudeToolItem(
|
|
404
|
+
conversationId: string,
|
|
405
|
+
toolUseId: string,
|
|
406
|
+
name: string | undefined,
|
|
407
|
+
input: Record<string, unknown> | undefined,
|
|
408
|
+
createdAt: number,
|
|
409
|
+
): StoredAgentTimelineItem {
|
|
410
|
+
const kind = claudeToolKind(name);
|
|
411
|
+
const inputText = stringifyHistoryValue(input);
|
|
412
|
+
const fileChange = claudeFileEntry(name, input);
|
|
413
|
+
return {
|
|
414
|
+
id: `history-tool:${toolUseId}`,
|
|
415
|
+
conversationId,
|
|
416
|
+
type: "tool_call",
|
|
417
|
+
kind,
|
|
418
|
+
itemId: toolUseId,
|
|
419
|
+
toolCall: {
|
|
420
|
+
id: toolUseId,
|
|
421
|
+
name: claudeToolName(name),
|
|
422
|
+
input: inputText,
|
|
423
|
+
createdAt,
|
|
424
|
+
status: "running",
|
|
425
|
+
},
|
|
426
|
+
commandExecution: name === "Bash" ? claudeCommandExecution(input) : undefined,
|
|
427
|
+
fileChange,
|
|
428
|
+
text: fileChange ? `已编辑 ${fileChange.entries.length} 个文件` : undefined,
|
|
429
|
+
createdAt,
|
|
430
|
+
updatedAt: createdAt,
|
|
431
|
+
metadata: { source: "device-history", provider: "claude", toolName: name },
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function completeClaudeToolItem(
|
|
436
|
+
itemsById: Map<string, StoredAgentTimelineItem>,
|
|
437
|
+
conversationId: string,
|
|
438
|
+
toolUseId: string,
|
|
439
|
+
output: string | undefined,
|
|
440
|
+
failed: boolean,
|
|
441
|
+
createdAt: number,
|
|
442
|
+
): void {
|
|
443
|
+
const id = `history-tool:${toolUseId}`;
|
|
444
|
+
const existing = itemsById.get(id);
|
|
445
|
+
const status = failed ? "failed" : "completed";
|
|
446
|
+
itemsById.set(id, {
|
|
447
|
+
id,
|
|
448
|
+
conversationId,
|
|
449
|
+
type: "tool_call",
|
|
450
|
+
kind: existing?.kind ?? "tool_activity",
|
|
451
|
+
itemId: toolUseId,
|
|
452
|
+
toolCall: {
|
|
453
|
+
id: toolUseId,
|
|
454
|
+
name: existing?.toolCall?.name ?? "工具",
|
|
455
|
+
input: existing?.toolCall?.input,
|
|
456
|
+
output,
|
|
457
|
+
createdAt: existing?.toolCall?.createdAt ?? createdAt,
|
|
458
|
+
status,
|
|
459
|
+
},
|
|
460
|
+
commandExecution: existing?.commandExecution
|
|
461
|
+
? { ...existing.commandExecution, output, status }
|
|
462
|
+
: undefined,
|
|
463
|
+
fileChange: existing?.fileChange
|
|
464
|
+
? { ...existing.fileChange, summary: output ?? existing.fileChange.summary, status }
|
|
465
|
+
: undefined,
|
|
466
|
+
text: existing?.text,
|
|
467
|
+
createdAt: existing?.createdAt ?? createdAt,
|
|
468
|
+
updatedAt: createdAt,
|
|
469
|
+
metadata: existing?.metadata ?? { source: "device-history", provider: "claude" },
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
295
473
|
export function loadClaudeStoredTimeline(
|
|
296
474
|
sessionId: string,
|
|
297
475
|
conversationId: string,
|
|
@@ -308,7 +486,7 @@ export function loadClaudeStoredTimeline(
|
|
|
308
486
|
return { items: [] };
|
|
309
487
|
}
|
|
310
488
|
|
|
311
|
-
const
|
|
489
|
+
const itemsById = new Map<string, StoredAgentTimelineItem>();
|
|
312
490
|
let index = 0;
|
|
313
491
|
for (const line of readHistorySample(filePath, statSize).split(/\r?\n/)) {
|
|
314
492
|
const trimmed = line.trim();
|
|
@@ -326,20 +504,50 @@ export function loadClaudeStoredTimeline(
|
|
|
326
504
|
: undefined;
|
|
327
505
|
const role = rawRole === "assistant" ? "assistant" : rawRole === "user" ? "user" : undefined;
|
|
328
506
|
if (!role) continue;
|
|
329
|
-
const text = extractHistoryText(message?.content ?? record.content ?? record.text);
|
|
330
|
-
if (!text) continue;
|
|
331
507
|
const createdAt =
|
|
332
508
|
parseTimestamp(record.timestamp ?? record.createdAt ?? record.created_at) ??
|
|
333
509
|
statMtime + index;
|
|
334
|
-
|
|
335
|
-
|
|
510
|
+
const content = message?.content ?? record.content ?? record.text;
|
|
511
|
+
const text = extractVisibleClaudeText(content);
|
|
512
|
+
if (text) {
|
|
513
|
+
itemsById.set(
|
|
514
|
+
`history:${sessionId}:${index}`,
|
|
515
|
+
historyMessage(conversationId, index, role, text, createdAt, sessionId),
|
|
516
|
+
);
|
|
517
|
+
index += 1;
|
|
518
|
+
}
|
|
519
|
+
if (Array.isArray(content)) {
|
|
520
|
+
for (const part of content) {
|
|
521
|
+
const block = asRecord(part);
|
|
522
|
+
if (!block) continue;
|
|
523
|
+
if (role === "assistant" && block.type === "tool_use") {
|
|
524
|
+
const toolUseId = typeof block.id === "string" ? block.id : undefined;
|
|
525
|
+
if (!toolUseId) continue;
|
|
526
|
+
const name = typeof block.name === "string" ? block.name : undefined;
|
|
527
|
+
const input = asRecord(block.input);
|
|
528
|
+
const item = createClaudeToolItem(conversationId, toolUseId, name, input, createdAt);
|
|
529
|
+
itemsById.set(item.id, item);
|
|
530
|
+
} else if (role === "user" && block.type === "tool_result") {
|
|
531
|
+
const toolUseId = typeof block.tool_use_id === "string" ? block.tool_use_id : undefined;
|
|
532
|
+
if (!toolUseId) continue;
|
|
533
|
+
completeClaudeToolItem(
|
|
534
|
+
itemsById,
|
|
535
|
+
conversationId,
|
|
536
|
+
toolUseId,
|
|
537
|
+
extractHistoryText(block.content),
|
|
538
|
+
block.is_error === true,
|
|
539
|
+
createdAt,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
336
544
|
} catch {
|
|
337
545
|
// Ignore malformed or partial JSONL lines in the history window.
|
|
338
546
|
}
|
|
339
547
|
}
|
|
340
548
|
|
|
341
549
|
return {
|
|
342
|
-
items:
|
|
550
|
+
items: [...itemsById.values()]
|
|
343
551
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
344
552
|
.slice(-MAX_HISTORY_ITEMS),
|
|
345
553
|
};
|
|
@@ -19,10 +19,38 @@ export interface CodexStoredSession {
|
|
|
19
19
|
export interface StoredAgentTimelineItem {
|
|
20
20
|
id: string;
|
|
21
21
|
conversationId: string;
|
|
22
|
-
type: "message";
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
type: "message" | "tool_call";
|
|
23
|
+
kind?: "tool_activity" | "command_execution" | "file_change";
|
|
24
|
+
itemId?: string;
|
|
25
|
+
role?: "user" | "assistant" | "system";
|
|
26
|
+
content?: Array<{ type: "text"; text: string }>;
|
|
27
|
+
text?: string;
|
|
28
|
+
toolCall?: {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
input?: string;
|
|
32
|
+
output?: string;
|
|
33
|
+
createdAt?: number;
|
|
34
|
+
status: "pending" | "running" | "completed" | "failed";
|
|
35
|
+
};
|
|
36
|
+
commandExecution?: {
|
|
37
|
+
command?: string;
|
|
38
|
+
cwd?: string;
|
|
39
|
+
output?: string;
|
|
40
|
+
exitCode?: number | null;
|
|
41
|
+
status?: "pending" | "running" | "completed" | "failed";
|
|
42
|
+
};
|
|
43
|
+
fileChange?: {
|
|
44
|
+
entries: Array<{
|
|
45
|
+
path: string;
|
|
46
|
+
kind?: string;
|
|
47
|
+
added?: number;
|
|
48
|
+
removed?: number;
|
|
49
|
+
}>;
|
|
50
|
+
diff?: string;
|
|
51
|
+
summary?: string;
|
|
52
|
+
status?: "pending" | "running" | "completed" | "failed";
|
|
53
|
+
};
|
|
26
54
|
createdAt: number;
|
|
27
55
|
updatedAt?: number;
|
|
28
56
|
metadata?: Record<string, unknown>;
|
|
@@ -276,6 +304,40 @@ function normalizeHistoryText(value: unknown): string | undefined {
|
|
|
276
304
|
return normalizeHistoryText(record.content ?? record.text ?? record.message);
|
|
277
305
|
}
|
|
278
306
|
|
|
307
|
+
function stringifyHistoryValue(value: unknown): string | undefined {
|
|
308
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
309
|
+
if (typeof value === "string") return value;
|
|
310
|
+
try {
|
|
311
|
+
return JSON.stringify(value, null, 2);
|
|
312
|
+
} catch {
|
|
313
|
+
return String(value);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function visibleCodexRole(value: unknown): "user" | "assistant" | undefined {
|
|
318
|
+
return value === "user" || value === "assistant" ? value : undefined;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isInjectedCodexContext(text: string): boolean {
|
|
322
|
+
const trimmed = text.trimStart();
|
|
323
|
+
return (
|
|
324
|
+
trimmed.startsWith("<permissions instructions>") ||
|
|
325
|
+
trimmed.startsWith("<app-context>") ||
|
|
326
|
+
trimmed.startsWith("<environment_context>") ||
|
|
327
|
+
trimmed.startsWith("<skill>") ||
|
|
328
|
+
trimmed.startsWith("<turn_aborted>") ||
|
|
329
|
+
trimmed.startsWith("<developer") ||
|
|
330
|
+
trimmed.startsWith("# AGENTS.md instructions") ||
|
|
331
|
+
trimmed.startsWith("AGENTS.md instructions for ") ||
|
|
332
|
+
trimmed.includes("\n# AGENTS.md\n") ||
|
|
333
|
+
trimmed.includes("\n<INSTRUCTIONS>")
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function messageDedupeKey(role: "user" | "assistant" | "system", text: string): string {
|
|
338
|
+
return `${role}:${text.replace(/\s+/g, " ").trim().slice(0, 400)}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
279
341
|
function historyMessage(
|
|
280
342
|
conversationId: string,
|
|
281
343
|
index: number,
|
|
@@ -297,6 +359,207 @@ function historyMessage(
|
|
|
297
359
|
};
|
|
298
360
|
}
|
|
299
361
|
|
|
362
|
+
function historyToolName(name: string | undefined): string {
|
|
363
|
+
if (!name) return "工具";
|
|
364
|
+
if (name.endsWith("exec_command") || name.endsWith("write_stdin")) return "命令";
|
|
365
|
+
if (name === "apply_patch") return "文件修改";
|
|
366
|
+
return name;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function historyToolKind(name: string | undefined): "tool_activity" | "command_execution" {
|
|
370
|
+
return name?.endsWith("exec_command") || name?.endsWith("write_stdin") ? "command_execution" : "tool_activity";
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function commandFromCodexTool(name: string | undefined, rawInput: unknown, output?: string): StoredAgentTimelineItem["commandExecution"] {
|
|
374
|
+
if (!name?.endsWith("exec_command") && !name?.endsWith("write_stdin")) return undefined;
|
|
375
|
+
let input = asRecord(rawInput);
|
|
376
|
+
if (!input && typeof rawInput === "string" && rawInput.trim().startsWith("{")) {
|
|
377
|
+
try {
|
|
378
|
+
input = asRecord(JSON.parse(rawInput));
|
|
379
|
+
} catch {
|
|
380
|
+
input = undefined;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const command = typeof input?.cmd === "string"
|
|
384
|
+
? input.cmd
|
|
385
|
+
: typeof input?.chars === "string"
|
|
386
|
+
? input.chars
|
|
387
|
+
: undefined;
|
|
388
|
+
const cwd = typeof input?.workdir === "string" ? input.workdir : undefined;
|
|
389
|
+
if (!command && !cwd && !output) return undefined;
|
|
390
|
+
return { command, cwd, output, status: output === undefined ? "running" : "completed" };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function upsertHistoryTool(
|
|
394
|
+
itemsById: Map<string, StoredAgentTimelineItem>,
|
|
395
|
+
conversationId: string,
|
|
396
|
+
callId: string,
|
|
397
|
+
name: string | undefined,
|
|
398
|
+
rawInput: unknown,
|
|
399
|
+
input: string | undefined,
|
|
400
|
+
createdAt: number,
|
|
401
|
+
): void {
|
|
402
|
+
const id = `history-tool:${callId}`;
|
|
403
|
+
const existing = itemsById.get(id);
|
|
404
|
+
const commandExecution = commandFromCodexTool(name, rawInput, existing?.commandExecution?.output);
|
|
405
|
+
itemsById.set(id, {
|
|
406
|
+
id,
|
|
407
|
+
conversationId,
|
|
408
|
+
type: "tool_call",
|
|
409
|
+
kind: historyToolKind(name),
|
|
410
|
+
itemId: callId,
|
|
411
|
+
toolCall: {
|
|
412
|
+
id: callId,
|
|
413
|
+
name: historyToolName(name),
|
|
414
|
+
input: input ?? existing?.toolCall?.input,
|
|
415
|
+
output: existing?.toolCall?.output,
|
|
416
|
+
createdAt: existing?.toolCall?.createdAt ?? createdAt,
|
|
417
|
+
status: existing?.toolCall?.status ?? "running",
|
|
418
|
+
},
|
|
419
|
+
commandExecution: commandExecution ?? existing?.commandExecution,
|
|
420
|
+
createdAt: existing?.createdAt ?? createdAt,
|
|
421
|
+
updatedAt: createdAt,
|
|
422
|
+
metadata: { source: "device-history", provider: "codex" },
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function completeHistoryTool(
|
|
427
|
+
itemsById: Map<string, StoredAgentTimelineItem>,
|
|
428
|
+
conversationId: string,
|
|
429
|
+
callId: string,
|
|
430
|
+
output: string | undefined,
|
|
431
|
+
createdAt: number,
|
|
432
|
+
): void {
|
|
433
|
+
const id = `history-tool:${callId}`;
|
|
434
|
+
const existing = itemsById.get(id);
|
|
435
|
+
const commandExecution = existing?.commandExecution
|
|
436
|
+
? { ...existing.commandExecution, output, status: "completed" as const }
|
|
437
|
+
: undefined;
|
|
438
|
+
itemsById.set(id, {
|
|
439
|
+
id,
|
|
440
|
+
conversationId,
|
|
441
|
+
type: "tool_call",
|
|
442
|
+
kind: existing?.kind ?? "tool_activity",
|
|
443
|
+
itemId: callId,
|
|
444
|
+
toolCall: {
|
|
445
|
+
id: callId,
|
|
446
|
+
name: existing?.toolCall?.name ?? "工具",
|
|
447
|
+
input: existing?.toolCall?.input,
|
|
448
|
+
output,
|
|
449
|
+
createdAt: existing?.toolCall?.createdAt ?? createdAt,
|
|
450
|
+
status: "completed",
|
|
451
|
+
},
|
|
452
|
+
commandExecution,
|
|
453
|
+
createdAt: existing?.createdAt ?? createdAt,
|
|
454
|
+
updatedAt: createdAt,
|
|
455
|
+
metadata: { source: "device-history", provider: "codex" },
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function relativeHistoryPath(path: string, cwd: string): string {
|
|
460
|
+
const normalized = path.replace(/\\/g, "/").replace(/^["']|["']$/g, "");
|
|
461
|
+
const normalizedCwd = cwd.replace(/\\/g, "/").replace(/\/+$/, "");
|
|
462
|
+
return normalized.startsWith(`${normalizedCwd}/`)
|
|
463
|
+
? normalized.slice(normalizedCwd.length + 1)
|
|
464
|
+
: normalized;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function countDiffLines(diff: string | undefined): { added: number; removed: number } {
|
|
468
|
+
if (!diff) return { added: 0, removed: 0 };
|
|
469
|
+
let added = 0;
|
|
470
|
+
let removed = 0;
|
|
471
|
+
for (const line of diff.split("\n")) {
|
|
472
|
+
if (line.startsWith("+") && !line.startsWith("+++")) added += 1;
|
|
473
|
+
else if (line.startsWith("-") && !line.startsWith("---")) removed += 1;
|
|
474
|
+
}
|
|
475
|
+
return { added, removed };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function countContentLines(content: unknown): number {
|
|
479
|
+
if (typeof content !== "string") return 0;
|
|
480
|
+
if (!content) return 0;
|
|
481
|
+
return content.split(/\r?\n/).filter((line) => line.length > 0).length;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function changeKind(value: string | undefined): string | undefined {
|
|
485
|
+
if (value === "add") return "create";
|
|
486
|
+
if (value === "delete") return "delete";
|
|
487
|
+
if (value === "update" || value === "move") return "update";
|
|
488
|
+
return value;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function patchApplyFileChange(
|
|
492
|
+
conversationId: string,
|
|
493
|
+
payload: Record<string, unknown>,
|
|
494
|
+
cwd: string,
|
|
495
|
+
createdAt: number,
|
|
496
|
+
): StoredAgentTimelineItem | undefined {
|
|
497
|
+
const changes = asRecord(payload.changes);
|
|
498
|
+
if (!changes) return undefined;
|
|
499
|
+
const entries: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"] = [];
|
|
500
|
+
const diffParts: string[] = [];
|
|
501
|
+
for (const [absolutePath, rawChange] of Object.entries(changes)) {
|
|
502
|
+
const change = asRecord(rawChange);
|
|
503
|
+
if (!change) continue;
|
|
504
|
+
const path = relativeHistoryPath(absolutePath, cwd);
|
|
505
|
+
const kind = changeKind(typeof change.type === "string" ? change.type : undefined);
|
|
506
|
+
const diff = typeof change.unified_diff === "string" ? change.unified_diff : undefined;
|
|
507
|
+
const stats = countDiffLines(diff);
|
|
508
|
+
const added = stats.added || (kind === "create" ? countContentLines(change.content) : 0);
|
|
509
|
+
const removed = stats.removed;
|
|
510
|
+
const entry: NonNullable<StoredAgentTimelineItem["fileChange"]>["entries"][number] = { path };
|
|
511
|
+
if (kind) entry.kind = kind;
|
|
512
|
+
if (added > 0) entry.added = added;
|
|
513
|
+
if (removed > 0) entry.removed = removed;
|
|
514
|
+
entries.push(entry);
|
|
515
|
+
if (diff) {
|
|
516
|
+
diffParts.push([
|
|
517
|
+
`Path: ${path}`,
|
|
518
|
+
kind ? `Kind: ${kind}` : undefined,
|
|
519
|
+
`Totals: +${added} -${removed}`,
|
|
520
|
+
"",
|
|
521
|
+
"```diff",
|
|
522
|
+
diff.trimEnd(),
|
|
523
|
+
"```",
|
|
524
|
+
].filter((line): line is string => line !== undefined).join("\n"));
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (entries.length === 0) return undefined;
|
|
528
|
+
const callId = typeof payload.call_id === "string" ? payload.call_id : `patch-${createdAt}`;
|
|
529
|
+
const status = payload.success === false ? "failed" : "completed";
|
|
530
|
+
const totalAdded = entries.reduce((sum, entry) => sum + (entry.added ?? 0), 0);
|
|
531
|
+
const totalRemoved = entries.reduce((sum, entry) => sum + (entry.removed ?? 0), 0);
|
|
532
|
+
const summary = entries
|
|
533
|
+
.map((entry) => [entry.kind, entry.path].filter(Boolean).join(" "))
|
|
534
|
+
.join("\n");
|
|
535
|
+
const diff = diffParts.length > 0 ? diffParts.join("\n\n---\n\n") : undefined;
|
|
536
|
+
return {
|
|
537
|
+
id: `history-file-change:${callId}`,
|
|
538
|
+
conversationId,
|
|
539
|
+
type: "tool_call",
|
|
540
|
+
kind: "file_change",
|
|
541
|
+
itemId: callId,
|
|
542
|
+
toolCall: {
|
|
543
|
+
id: callId,
|
|
544
|
+
name: "文件修改",
|
|
545
|
+
input: summary,
|
|
546
|
+
output: diff ?? (typeof payload.stdout === "string" ? payload.stdout : undefined),
|
|
547
|
+
createdAt,
|
|
548
|
+
status,
|
|
549
|
+
},
|
|
550
|
+
fileChange: {
|
|
551
|
+
entries,
|
|
552
|
+
diff,
|
|
553
|
+
summary,
|
|
554
|
+
status,
|
|
555
|
+
},
|
|
556
|
+
text: `已编辑 ${entries.length} 个文件 +${totalAdded} -${totalRemoved}`,
|
|
557
|
+
createdAt,
|
|
558
|
+
updatedAt: createdAt,
|
|
559
|
+
metadata: { source: "device-history", provider: "codex" },
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
300
563
|
export function listCodexStoredSessions(inputCwd: string): { sessions: CodexStoredSession[] } {
|
|
301
564
|
const root = join(homedir(), ".codex");
|
|
302
565
|
if (!existsSync(root)) return { sessions: [] };
|
|
@@ -360,7 +623,8 @@ export function loadCodexStoredTimeline(
|
|
|
360
623
|
return { items: [] };
|
|
361
624
|
}
|
|
362
625
|
|
|
363
|
-
const
|
|
626
|
+
const itemsById = new Map<string, StoredAgentTimelineItem>();
|
|
627
|
+
const seenMessages = new Set<string>();
|
|
364
628
|
let index = 0;
|
|
365
629
|
for (const line of readHistorySample(file.path, statSize).split(/\r?\n/)) {
|
|
366
630
|
const trimmed = line.trim();
|
|
@@ -368,21 +632,76 @@ export function loadCodexStoredTimeline(
|
|
|
368
632
|
try {
|
|
369
633
|
const entry = asRecord(JSON.parse(trimmed));
|
|
370
634
|
const payload = asRecord(entry?.payload);
|
|
371
|
-
if (entry?.type !== "event_msg" || !payload) continue;
|
|
372
|
-
const eventType = typeof payload.type === "string" ? payload.type : undefined;
|
|
373
635
|
const createdAt =
|
|
374
|
-
parseTimestamp(entry
|
|
636
|
+
parseTimestamp(entry?.timestamp ?? payload?.created_at ?? payload?.started_at ?? payload?.completed_at) ??
|
|
375
637
|
statMtime + index;
|
|
376
|
-
if (
|
|
377
|
-
const
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
638
|
+
if (entry?.type === "event_msg" && payload) {
|
|
639
|
+
const eventType = typeof payload.type === "string" ? payload.type : undefined;
|
|
640
|
+
if (eventType === "user_message") {
|
|
641
|
+
const text = normalizeHistoryText(payload.message);
|
|
642
|
+
if (!text || isInjectedCodexContext(text)) continue;
|
|
643
|
+
const dedupeKey = messageDedupeKey("user", text);
|
|
644
|
+
if (seenMessages.has(dedupeKey)) continue;
|
|
645
|
+
seenMessages.add(dedupeKey);
|
|
646
|
+
itemsById.set(`history:${sessionId}:${index}`, historyMessage(conversationId, index, "user", text, createdAt, sessionId));
|
|
647
|
+
index += 1;
|
|
648
|
+
} else if (eventType === "agent_message") {
|
|
649
|
+
const text = normalizeHistoryText(payload.message);
|
|
650
|
+
if (!text || isInjectedCodexContext(text)) continue;
|
|
651
|
+
const dedupeKey = messageDedupeKey("assistant", text);
|
|
652
|
+
if (seenMessages.has(dedupeKey)) continue;
|
|
653
|
+
seenMessages.add(dedupeKey);
|
|
654
|
+
itemsById.set(`history:${sessionId}:${index}`, historyMessage(conversationId, index, "assistant", text, createdAt, sessionId));
|
|
655
|
+
index += 1;
|
|
656
|
+
} else if (eventType === "patch_apply_end") {
|
|
657
|
+
const item = patchApplyFileChange(conversationId, payload, inputCwd, createdAt);
|
|
658
|
+
if (item) itemsById.set(item.id, item);
|
|
659
|
+
}
|
|
660
|
+
continue;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
if (entry?.type !== "response_item" || !payload) continue;
|
|
664
|
+
const responseType = typeof payload.type === "string" ? payload.type : undefined;
|
|
665
|
+
if (responseType === "message") {
|
|
666
|
+
const role = visibleCodexRole(payload.role);
|
|
667
|
+
if (!role) continue;
|
|
382
668
|
const text = normalizeHistoryText(payload.message);
|
|
383
|
-
|
|
384
|
-
|
|
669
|
+
const contentText = text ?? normalizeHistoryText(payload.content);
|
|
670
|
+
if (!contentText || isInjectedCodexContext(contentText)) continue;
|
|
671
|
+
const dedupeKey = messageDedupeKey(role, contentText);
|
|
672
|
+
if (seenMessages.has(dedupeKey)) continue;
|
|
673
|
+
seenMessages.add(dedupeKey);
|
|
674
|
+
itemsById.set(`history:${sessionId}:${index}`, historyMessage(conversationId, index, role, contentText, createdAt, sessionId));
|
|
385
675
|
index += 1;
|
|
676
|
+
continue;
|
|
677
|
+
}
|
|
678
|
+
if (responseType === "function_call") {
|
|
679
|
+
const callId = typeof payload.call_id === "string"
|
|
680
|
+
? payload.call_id
|
|
681
|
+
: typeof payload.id === "string"
|
|
682
|
+
? payload.id
|
|
683
|
+
: undefined;
|
|
684
|
+
if (!callId) continue;
|
|
685
|
+
const name = typeof payload.name === "string" ? payload.name : undefined;
|
|
686
|
+
upsertHistoryTool(
|
|
687
|
+
itemsById,
|
|
688
|
+
conversationId,
|
|
689
|
+
callId,
|
|
690
|
+
name,
|
|
691
|
+
payload.arguments,
|
|
692
|
+
stringifyHistoryValue(payload.arguments),
|
|
693
|
+
createdAt,
|
|
694
|
+
);
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (responseType === "function_call_output") {
|
|
698
|
+
const callId = typeof payload.call_id === "string"
|
|
699
|
+
? payload.call_id
|
|
700
|
+
: typeof payload.id === "string"
|
|
701
|
+
? payload.id
|
|
702
|
+
: undefined;
|
|
703
|
+
if (!callId) continue;
|
|
704
|
+
completeHistoryTool(itemsById, conversationId, callId, stringifyHistoryValue(payload.output), createdAt);
|
|
386
705
|
}
|
|
387
706
|
} catch {
|
|
388
707
|
// Ignore malformed or partial JSONL lines in the history window.
|
|
@@ -390,7 +709,7 @@ export function loadCodexStoredTimeline(
|
|
|
390
709
|
}
|
|
391
710
|
|
|
392
711
|
return {
|
|
393
|
-
items:
|
|
712
|
+
items: [...itemsById.values()]
|
|
394
713
|
.sort((a, b) => a.createdAt - b.createdAt)
|
|
395
714
|
.slice(-MAX_HISTORY_ITEMS),
|
|
396
715
|
};
|