@sayknow-cli/coding-agent 0.5.2 → 0.5.8
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 +66 -16
- package/dist/types/cli/update-cli.d.ts +8 -1
- package/dist/types/config/atomic-yaml-patch.d.ts +20 -1
- package/dist/types/config/keybindings.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +1 -1
- package/dist/types/config/models-config-schema.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +73 -0
- package/dist/types/dap/client.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +5 -0
- package/dist/types/modes/components/welcome.d.ts +1 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
- package/dist/types/modes/interactive-mode.d.ts +13 -1
- package/dist/types/modes/types.d.ts +7 -1
- package/dist/types/runtime-mcp/transports/stdio.d.ts +2 -0
- package/dist/types/session/agent-session.d.ts +8 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +3 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +2 -0
- package/dist/types/session/session-manager.d.ts +9 -0
- package/dist/types/session/session-storage.d.ts +17 -0
- package/dist/types/session-import/claude.d.ts +26 -0
- package/dist/types/session-import/codex.d.ts +22 -0
- package/dist/types/session-import/command.d.ts +20 -0
- package/dist/types/session-import/detect.d.ts +22 -0
- package/dist/types/session-import/index.d.ts +7 -0
- package/dist/types/session-import/redact.d.ts +23 -0
- package/dist/types/session-import/service.d.ts +28 -0
- package/dist/types/session-import/types.d.ts +125 -0
- package/dist/types/skc-runtime/launch-worktree.d.ts +17 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +4 -0
- package/dist/types/slash-commands/types.d.ts +5 -0
- package/dist/types/system-prompt.d.ts +5 -0
- package/dist/types/tools/ask.d.ts +5 -0
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +13 -7
- package/scripts/compile-args.ts +8 -4
- package/scripts/generate-sdk-operation-inventory.ts +5 -6
- package/src/cli/config-cli.ts +23 -8
- package/src/cli/update-cli.ts +265 -21
- package/src/config/atomic-yaml-patch.ts +167 -30
- package/src/config/keybindings.ts +10 -0
- package/src/config/model-profiles.ts +84 -16
- package/src/config/model-registry.ts +60 -38
- package/src/config/models-config-schema.ts +1 -1
- package/src/config/settings-schema.ts +79 -0
- package/src/config/settings.ts +13 -2
- package/src/dap/client.ts +78 -30
- package/src/internal-urls/docs-index.generated.ts +5 -4
- package/src/lsp/client.ts +17 -29
- package/src/modes/action-registry.ts +2 -0
- package/src/modes/components/welcome.ts +5 -0
- package/src/modes/controllers/goal-mode-controller.ts +7 -2
- package/src/modes/controllers/input-controller.ts +71 -6
- package/src/modes/controllers/plan-mode-controller.ts +8 -1
- package/src/modes/controllers/runtime-mcp-command-controller.ts +14 -0
- package/src/modes/controllers/selector-controller.ts +47 -19
- package/src/modes/interactive-mode.ts +54 -4
- package/src/modes/types.ts +5 -1
- package/src/prompts/system/system-prompt.md +8 -0
- package/src/runtime-mcp/transports/stdio.ts +96 -33
- package/src/sdk/broker/lifecycle.ts +6 -5
- package/src/sdk/protocol/operation-inventory.generated.json +33 -0
- package/src/sdk/session.ts +27 -18
- package/src/session/agent-session.ts +155 -15
- package/src/session/internal/managed-session-scope.ts +41 -6
- package/src/session/internal/managed-session-storage.ts +14 -0
- package/src/session/session-manager.ts +113 -11
- package/src/session/session-storage.ts +70 -0
- package/src/session-import/claude.ts +382 -0
- package/src/session-import/codex.ts +458 -0
- package/src/session-import/command.ts +61 -0
- package/src/session-import/detect.ts +133 -0
- package/src/session-import/index.ts +27 -0
- package/src/session-import/redact.ts +125 -0
- package/src/session-import/service.ts +749 -0
- package/src/session-import/types.ts +163 -0
- package/src/skc-runtime/launch-worktree.ts +277 -4
- package/src/slash-commands/acp-builtins.ts +5 -1
- package/src/slash-commands/builtin-registry.ts +100 -1
- package/src/slash-commands/types.ts +5 -0
- package/src/system-prompt.ts +7 -0
- package/src/tools/ask.ts +5 -0
- package/src/tools/fetch.ts +7 -2
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/export/html/template.css +0 -1060
- package/src/export/html/template.html +0 -47
- package/src/export/html/template.js +0 -2348
- package/vendor/insane-search/engine/tests/test_hardening.py +0 -57
- package/vendor/insane-search/engine/tests/test_smoke.py +0 -152
- package/vendor/insane-search/engine/tests/test_u1.py +0 -200
- package/vendor/insane-search/engine/tests/test_u4.py +0 -131
- package/vendor/insane-search/engine/tests/test_u5.py +0 -163
- package/vendor/insane-search/engine/tests/test_u7.py +0 -124
- package/vendor/insane-search/engine/tests/test_u8.py +0 -216
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude adapter (issue #3709).
|
|
3
|
+
*
|
|
4
|
+
* Parses two first-party observable Claude transcript/export shapes:
|
|
5
|
+
*
|
|
6
|
+
* 1. `claude-code-jsonl` — Claude Code session transcripts
|
|
7
|
+
* (`~/.claude/projects/<project-slug>/<session-uuid>.jsonl`), one JSON object
|
|
8
|
+
* per line with `type: "user" | "assistant" | "summary" | "system" | …`.
|
|
9
|
+
* 2. `claude-export-json` — the claude.ai data export (`conversations.json`):
|
|
10
|
+
* a JSON array of conversations (or a single conversation object) whose
|
|
11
|
+
* `chat_messages` carry `sender: "human" | "assistant"` and `content` blocks.
|
|
12
|
+
*
|
|
13
|
+
* The same quarantine contract as the Codex adapter applies: unmappable
|
|
14
|
+
* records/messages are counted and digested, never silently dropped.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createHash } from "node:crypto";
|
|
18
|
+
import { redactImportedText } from "./redact";
|
|
19
|
+
import {
|
|
20
|
+
type ImportedConversation,
|
|
21
|
+
type ImportedMessage,
|
|
22
|
+
type ImportQuarantineRecord,
|
|
23
|
+
SESSION_IMPORT_QUARANTINE_MAX_RECORDS,
|
|
24
|
+
type SessionImportCounts,
|
|
25
|
+
} from "./types";
|
|
26
|
+
|
|
27
|
+
type JsonRecord = Record<string, unknown>;
|
|
28
|
+
|
|
29
|
+
export interface ClaudeParseResult {
|
|
30
|
+
conversation: ImportedConversation;
|
|
31
|
+
quarantine: ImportQuarantineRecord[];
|
|
32
|
+
counts: SessionImportCounts;
|
|
33
|
+
redactionKinds: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const MAX_EVIDENCE_LINE_CHARS = 400;
|
|
37
|
+
const MAX_EVIDENCE_LINES = 100;
|
|
38
|
+
const MAX_EVIDENCE_BYTES = 8 * 1024;
|
|
39
|
+
|
|
40
|
+
function isRecord(value: unknown): value is JsonRecord {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function sha256Hex(text: string): string {
|
|
45
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function boundEvidence(text: string): string {
|
|
49
|
+
const singleLine = text.replace(/\s+/g, " ").trim();
|
|
50
|
+
if (singleLine.length <= MAX_EVIDENCE_LINE_CHARS) return singleLine;
|
|
51
|
+
return `${singleLine.slice(0, MAX_EVIDENCE_LINE_CHARS - 1)}…`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
class ParseSink {
|
|
55
|
+
readonly messages: ImportedMessage[] = [];
|
|
56
|
+
readonly quarantine: ImportQuarantineRecord[] = [];
|
|
57
|
+
readonly counts: SessionImportCounts = { mapped: 0, quarantined: 0, redacted: 0, omitted: 0 };
|
|
58
|
+
readonly redactionKinds = new Set<string>();
|
|
59
|
+
#evidence: string[] = [];
|
|
60
|
+
#evidenceBytes = 0;
|
|
61
|
+
#current: ImportedMessage | undefined;
|
|
62
|
+
|
|
63
|
+
redact(input: string): string {
|
|
64
|
+
const result = redactImportedText(input);
|
|
65
|
+
this.counts.redacted += result.redacted;
|
|
66
|
+
for (const kind of result.kinds) this.redactionKinds.add(kind);
|
|
67
|
+
return result.value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
quarantineRecord(record: number, raw: string, reason: ImportQuarantineRecord["reason"]): void {
|
|
71
|
+
if (this.quarantine.length < SESSION_IMPORT_QUARANTINE_MAX_RECORDS) {
|
|
72
|
+
this.quarantine.push({
|
|
73
|
+
record,
|
|
74
|
+
byteLength: Buffer.byteLength(raw, "utf8"),
|
|
75
|
+
sha256: sha256Hex(raw),
|
|
76
|
+
reason,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
this.counts.quarantined++;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
addEvidence(line: string): boolean {
|
|
83
|
+
if (line.length === 0) return true;
|
|
84
|
+
if (this.#evidence.length >= MAX_EVIDENCE_LINES || this.#evidenceBytes + line.length > MAX_EVIDENCE_BYTES) {
|
|
85
|
+
this.counts.omitted++;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
this.#evidence.push(line);
|
|
89
|
+
this.#evidenceBytes += line.length;
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
closeEvidence(): void {
|
|
94
|
+
if (this.#evidence.length > 0) {
|
|
95
|
+
if (this.#current?.role !== "assistant") {
|
|
96
|
+
const toolOnly: ImportedMessage = { role: "assistant", text: "", toolEvidence: [...this.#evidence] };
|
|
97
|
+
this.#current = toolOnly;
|
|
98
|
+
this.messages.push(toolOnly);
|
|
99
|
+
} else {
|
|
100
|
+
this.#current.toolEvidence = [...(this.#current.toolEvidence ?? []), ...this.#evidence];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
this.#evidence = [];
|
|
104
|
+
this.#evidenceBytes = 0;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Assemble visible text into the running conversation. `mapped` is counted by the caller. */
|
|
108
|
+
pushMessage(role: "user" | "assistant", rawText: string, timestamp?: string): void {
|
|
109
|
+
const sanitized = this.redact(rawText).trim();
|
|
110
|
+
if (role === "user") {
|
|
111
|
+
this.closeEvidence();
|
|
112
|
+
if (!sanitized) {
|
|
113
|
+
this.#current = undefined;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
this.#current = { role, text: sanitized, ...(timestamp ? { timestamp } : {}) };
|
|
117
|
+
this.messages.push(this.#current);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!sanitized) {
|
|
121
|
+
this.closeEvidence();
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (this.#current?.role === "assistant") {
|
|
125
|
+
this.#current.text = this.#current.text ? `${this.#current.text}\n\n${sanitized}` : sanitized;
|
|
126
|
+
} else {
|
|
127
|
+
this.#current = { role, text: sanitized, ...(timestamp ? { timestamp } : {}) };
|
|
128
|
+
this.messages.push(this.#current);
|
|
129
|
+
}
|
|
130
|
+
this.closeEvidence();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function toolUseSummary(input: unknown): string {
|
|
135
|
+
if (!isRecord(input)) return "";
|
|
136
|
+
if (typeof input.command === "string") return input.command;
|
|
137
|
+
if (typeof input.file_path === "string") return input.file_path;
|
|
138
|
+
if (typeof input.path === "string") return input.path;
|
|
139
|
+
if (typeof input.pattern === "string") return input.pattern;
|
|
140
|
+
if (typeof input.query === "string") return input.query;
|
|
141
|
+
if (typeof input.url === "string") return input.url;
|
|
142
|
+
if (typeof input.prompt === "string") return boundEvidence(input.prompt);
|
|
143
|
+
if (typeof input.content === "string") return boundEvidence(input.content);
|
|
144
|
+
return boundEvidence(JSON.stringify(input));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function toolResultText(content: unknown): string {
|
|
148
|
+
if (typeof content === "string") return content;
|
|
149
|
+
if (!Array.isArray(content)) return "";
|
|
150
|
+
const parts: string[] = [];
|
|
151
|
+
for (const block of content) {
|
|
152
|
+
if (isRecord(block) && block.type === "text" && typeof block.text === "string") parts.push(block.text);
|
|
153
|
+
}
|
|
154
|
+
return parts.join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Parse a Claude Code session JSONL transcript. */
|
|
158
|
+
export function parseClaudeCodeTranscript(text: string): ClaudeParseResult {
|
|
159
|
+
const sink = new ParseSink();
|
|
160
|
+
let sourceSessionId: string | undefined;
|
|
161
|
+
let title: string | undefined;
|
|
162
|
+
let cwd: string | undefined;
|
|
163
|
+
|
|
164
|
+
const lines = text.split("\n");
|
|
165
|
+
for (let index = 0; index < lines.length; index++) {
|
|
166
|
+
const rawLine = lines[index]!;
|
|
167
|
+
const line = rawLine.trim();
|
|
168
|
+
if (line.length === 0) continue;
|
|
169
|
+
const recordNumber = index + 1;
|
|
170
|
+
let record: unknown;
|
|
171
|
+
try {
|
|
172
|
+
record = JSON.parse(line);
|
|
173
|
+
} catch {
|
|
174
|
+
sink.quarantineRecord(recordNumber, rawLine, "invalid_json");
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
if (!isRecord(record) || typeof record.type !== "string") {
|
|
178
|
+
sink.quarantineRecord(recordNumber, rawLine, "invalid_json");
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
if (typeof record.sessionId === "string" && !sourceSessionId)
|
|
182
|
+
sourceSessionId = sink.redact(record.sessionId).trim();
|
|
183
|
+
if (typeof record.cwd === "string" && !cwd) cwd = sink.redact(record.cwd).trim();
|
|
184
|
+
const timestamp = typeof record.timestamp === "string" ? record.timestamp : undefined;
|
|
185
|
+
|
|
186
|
+
switch (record.type) {
|
|
187
|
+
case "summary": {
|
|
188
|
+
if (typeof record.summary === "string" && record.summary.trim().length > 0) {
|
|
189
|
+
title = sink.redact(record.summary).trim();
|
|
190
|
+
}
|
|
191
|
+
sink.counts.mapped++;
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
case "user":
|
|
195
|
+
case "assistant": {
|
|
196
|
+
const message = record.message;
|
|
197
|
+
if (!isRecord(message)) {
|
|
198
|
+
sink.quarantineRecord(recordNumber, rawLine, "missing_fields");
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
if (message.role !== record.type) {
|
|
202
|
+
sink.quarantineRecord(recordNumber, rawLine, "missing_fields");
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
sink.counts.mapped++;
|
|
206
|
+
const isMeta = record.isMeta === true || record.isSidechain === true;
|
|
207
|
+
if (isMeta) break; // command/system bookkeeping rows are not conversation.
|
|
208
|
+
const role: "user" | "assistant" = record.type === "user" ? "user" : "assistant";
|
|
209
|
+
const content = message.content;
|
|
210
|
+
if (typeof content === "string") {
|
|
211
|
+
if (!content.trim()) sink.quarantineRecord(recordNumber, rawLine, "missing_fields");
|
|
212
|
+
sink.pushMessage(role, content, timestamp);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
if (!Array.isArray(content) || content.length === 0) {
|
|
216
|
+
sink.quarantineRecord(recordNumber, rawLine, "missing_fields");
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
let unsupportedContent = false;
|
|
220
|
+
for (const block of content) {
|
|
221
|
+
if (!isRecord(block) || typeof block.type !== "string") {
|
|
222
|
+
unsupportedContent = true;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
switch (block.type) {
|
|
226
|
+
case "text":
|
|
227
|
+
if (typeof block.text === "string") sink.pushMessage(role, block.text, timestamp);
|
|
228
|
+
else unsupportedContent = true;
|
|
229
|
+
break;
|
|
230
|
+
case "thinking":
|
|
231
|
+
// Model-internal reasoning is never imported (non-goal).
|
|
232
|
+
break;
|
|
233
|
+
case "tool_use": {
|
|
234
|
+
if (typeof block.name !== "string" || !block.name.trim() || !isRecord(block.input)) {
|
|
235
|
+
unsupportedContent = true;
|
|
236
|
+
break;
|
|
237
|
+
}
|
|
238
|
+
const name = sink.redact(boundEvidence(block.name));
|
|
239
|
+
const summary = sink.redact(boundEvidence(toolUseSummary(block.input)));
|
|
240
|
+
if (!sink.addEvidence(`$ ${name} ${summary}`)) unsupportedContent = true;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case "tool_result": {
|
|
244
|
+
const summary = sink.redact(boundEvidence(toolResultText(block.content)));
|
|
245
|
+
if (!summary || !sink.addEvidence(`→ ${summary}`)) unsupportedContent = true;
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
default:
|
|
249
|
+
if (block.type === "image" || block.type === "document") {
|
|
250
|
+
if (!sink.addEvidence(`[${block.type}]`)) unsupportedContent = true;
|
|
251
|
+
} else {
|
|
252
|
+
unsupportedContent = true;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
if (unsupportedContent) sink.quarantineRecord(recordNumber, rawLine, "unknown_record");
|
|
257
|
+
sink.closeEvidence();
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
case "system":
|
|
261
|
+
case "file-history-snapshot":
|
|
262
|
+
case "queue-operation":
|
|
263
|
+
// Provider hooks/bookkeeping; not conversation content.
|
|
264
|
+
sink.counts.mapped++;
|
|
265
|
+
break;
|
|
266
|
+
default:
|
|
267
|
+
sink.quarantineRecord(recordNumber, rawLine, "unknown_record");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
sink.closeEvidence();
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
conversation: {
|
|
274
|
+
provider: "claude",
|
|
275
|
+
format: "claude-code-jsonl",
|
|
276
|
+
...(sourceSessionId ? { sourceSessionId } : {}),
|
|
277
|
+
...(title ? { title } : {}),
|
|
278
|
+
...(cwd ? { cwd } : {}),
|
|
279
|
+
messages: sink.messages,
|
|
280
|
+
},
|
|
281
|
+
quarantine: sink.quarantine,
|
|
282
|
+
counts: sink.counts,
|
|
283
|
+
redactionKinds: [...sink.redactionKinds].sort(),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function claudeExportMessageText(message: JsonRecord): { text: string; unsupported: boolean } {
|
|
288
|
+
let unsupported = false;
|
|
289
|
+
if (Array.isArray(message.content)) {
|
|
290
|
+
const parts: string[] = [];
|
|
291
|
+
for (const block of message.content) {
|
|
292
|
+
if (isRecord(block) && block.type === "text" && typeof block.text === "string") {
|
|
293
|
+
parts.push(block.text);
|
|
294
|
+
} else if (isRecord(block) && (block.type === "image" || block.type === "document")) {
|
|
295
|
+
parts.push(`[${block.type}]`);
|
|
296
|
+
} else {
|
|
297
|
+
unsupported = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
if (parts.length > 0) return { text: parts.join("\n"), unsupported };
|
|
301
|
+
}
|
|
302
|
+
if (typeof message.text === "string") return { text: message.text, unsupported };
|
|
303
|
+
return { text: "", unsupported: true };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Parse a claude.ai data-export conversation (array or single object). */
|
|
307
|
+
export function parseClaudeExport(text: string): ClaudeParseResult {
|
|
308
|
+
const sink = new ParseSink();
|
|
309
|
+
let parsed: unknown;
|
|
310
|
+
try {
|
|
311
|
+
parsed = JSON.parse(text);
|
|
312
|
+
} catch {
|
|
313
|
+
sink.quarantineRecord(1, text, "invalid_json");
|
|
314
|
+
return emptyResult(sink);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const conversations = Array.isArray(parsed) ? parsed : [parsed];
|
|
318
|
+
if (conversations.length === 0 || !conversations.every(isRecord)) {
|
|
319
|
+
sink.quarantineRecord(1, text, "missing_fields");
|
|
320
|
+
return emptyResult(sink);
|
|
321
|
+
}
|
|
322
|
+
// The export's array and chat-message order are authoritative and stable.
|
|
323
|
+
const ordered = conversations;
|
|
324
|
+
|
|
325
|
+
let title: string | undefined;
|
|
326
|
+
let sourceSessionId: string | undefined;
|
|
327
|
+
let recordNumber = 0;
|
|
328
|
+
for (const conversation of ordered) {
|
|
329
|
+
if (typeof conversation.uuid === "string" && !sourceSessionId)
|
|
330
|
+
sourceSessionId = sink.redact(conversation.uuid).trim();
|
|
331
|
+
if (typeof conversation.name === "string" && conversation.name.trim().length > 0 && !title) {
|
|
332
|
+
title = sink.redact(conversation.name).trim();
|
|
333
|
+
}
|
|
334
|
+
const chatMessages = conversation.chat_messages;
|
|
335
|
+
if (!Array.isArray(chatMessages)) {
|
|
336
|
+
sink.quarantineRecord(++recordNumber, JSON.stringify(conversation), "missing_fields");
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
for (const message of chatMessages) {
|
|
340
|
+
recordNumber++;
|
|
341
|
+
if (!isRecord(message)) {
|
|
342
|
+
sink.quarantineRecord(recordNumber, String(message), "invalid_json");
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
const sender = message.sender;
|
|
346
|
+
if (sender !== "human" && sender !== "assistant") {
|
|
347
|
+
sink.quarantineRecord(recordNumber, JSON.stringify(message), "unknown_record");
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
sink.counts.mapped++;
|
|
351
|
+
const timestamp = typeof message.created_at === "string" ? message.created_at : undefined;
|
|
352
|
+
const mapped = claudeExportMessageText(message);
|
|
353
|
+
if (mapped.unsupported || !mapped.text.trim()) {
|
|
354
|
+
sink.quarantineRecord(recordNumber, JSON.stringify(message), "missing_fields");
|
|
355
|
+
}
|
|
356
|
+
if (mapped.text) sink.pushMessage(sender === "human" ? "user" : "assistant", mapped.text, timestamp);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
sink.closeEvidence();
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
conversation: {
|
|
363
|
+
provider: "claude",
|
|
364
|
+
format: "claude-export-json",
|
|
365
|
+
...(sourceSessionId ? { sourceSessionId } : {}),
|
|
366
|
+
...(title ? { title } : {}),
|
|
367
|
+
messages: sink.messages,
|
|
368
|
+
},
|
|
369
|
+
quarantine: sink.quarantine,
|
|
370
|
+
counts: sink.counts,
|
|
371
|
+
redactionKinds: [...sink.redactionKinds].sort(),
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function emptyResult(sink: ParseSink): ClaudeParseResult {
|
|
376
|
+
return {
|
|
377
|
+
conversation: { provider: "claude", format: "claude-export-json", messages: [] },
|
|
378
|
+
quarantine: sink.quarantine,
|
|
379
|
+
counts: sink.counts,
|
|
380
|
+
redactionKinds: [],
|
|
381
|
+
};
|
|
382
|
+
}
|