@remnic/import-chatgpt 0.1.0 → 9.3.517
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/LICENSE +21 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +18 -12
- package/dist/index.js.map +1 -1
- package/package.json +21 -13
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Joshua Warren
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
CHANGED
|
@@ -9,10 +9,10 @@ interface ChatGPTSavedMemory {
|
|
|
9
9
|
id?: string;
|
|
10
10
|
/** The memory body. Required. */
|
|
11
11
|
content: string;
|
|
12
|
-
/** ISO 8601 create timestamp. */
|
|
13
|
-
created_at?: string;
|
|
14
|
-
/** ISO 8601 last-update timestamp, if different from created_at. */
|
|
15
|
-
updated_at?: string;
|
|
12
|
+
/** ISO 8601 or epoch create timestamp. */
|
|
13
|
+
created_at?: string | number;
|
|
14
|
+
/** ISO 8601 or epoch last-update timestamp, if different from created_at. */
|
|
15
|
+
updated_at?: string | number;
|
|
16
16
|
/** Soft-delete flag seen in 2025+ exports. When true, we skip the record. */
|
|
17
17
|
deleted?: boolean;
|
|
18
18
|
/** Whether the memory is pinned / manually curated. */
|
package/dist/index.js
CHANGED
|
@@ -141,13 +141,16 @@ function normalizeSavedMemory(value, strict) {
|
|
|
141
141
|
const normalized = {
|
|
142
142
|
content: content.trim(),
|
|
143
143
|
...typeof v.id === "string" ? { id: v.id } : {},
|
|
144
|
-
...
|
|
145
|
-
...
|
|
144
|
+
...isRawChatGPTTimestamp(v.created_at) ? { created_at: v.created_at } : {},
|
|
145
|
+
...isRawChatGPTTimestamp(v.updated_at) ? { updated_at: v.updated_at } : {},
|
|
146
146
|
...v.pinned === true ? { pinned: true } : {},
|
|
147
147
|
...Array.isArray(v.tags) ? { tags: v.tags.filter((t) => typeof t === "string") } : {}
|
|
148
148
|
};
|
|
149
149
|
return normalized;
|
|
150
150
|
}
|
|
151
|
+
function isRawChatGPTTimestamp(value) {
|
|
152
|
+
return typeof value === "string" || typeof value === "number";
|
|
153
|
+
}
|
|
151
154
|
function collectUserTurnsFromConversation(conversation) {
|
|
152
155
|
const collected = [];
|
|
153
156
|
if (Array.isArray(conversation.messages) && conversation.messages.length > 0) {
|
|
@@ -156,7 +159,7 @@ function collectUserTurnsFromConversation(conversation) {
|
|
|
156
159
|
if (text) {
|
|
157
160
|
collected.push({
|
|
158
161
|
content: text,
|
|
159
|
-
...
|
|
162
|
+
...normalizeChatGPTTimestamp(msg.create_time ?? msg.update_time) !== void 0 ? { createdAt: normalizeChatGPTTimestamp(msg.create_time ?? msg.update_time) } : {}
|
|
160
163
|
});
|
|
161
164
|
}
|
|
162
165
|
}
|
|
@@ -182,7 +185,7 @@ function collectUserTurnsFromConversation(conversation) {
|
|
|
182
185
|
if (text) {
|
|
183
186
|
collected.push({
|
|
184
187
|
content: text,
|
|
185
|
-
...
|
|
188
|
+
...normalizeChatGPTTimestamp(msg.create_time ?? msg.update_time) !== void 0 ? { createdAt: normalizeChatGPTTimestamp(msg.create_time ?? msg.update_time) } : {}
|
|
186
189
|
});
|
|
187
190
|
}
|
|
188
191
|
}
|
|
@@ -204,7 +207,8 @@ function followCurrentNodeChain(mapping, currentNode) {
|
|
|
204
207
|
return [];
|
|
205
208
|
}
|
|
206
209
|
chain.push(node);
|
|
207
|
-
|
|
210
|
+
const messageParent = node.message?.parent;
|
|
211
|
+
cursor = typeof node.parent === "string" && node.parent.length > 0 ? node.parent : typeof messageParent === "string" && messageParent.length > 0 ? messageParent : null;
|
|
208
212
|
}
|
|
209
213
|
return chain.reverse();
|
|
210
214
|
}
|
|
@@ -228,7 +232,7 @@ function toNumericTime(value) {
|
|
|
228
232
|
}
|
|
229
233
|
return 0;
|
|
230
234
|
}
|
|
231
|
-
function
|
|
235
|
+
function normalizeChatGPTTimestamp(value) {
|
|
232
236
|
if (value === null || value === void 0) return void 0;
|
|
233
237
|
if (typeof value === "string") {
|
|
234
238
|
if (/\d{4}-\d{2}-\d{2}T/.test(value)) return value;
|
|
@@ -282,7 +286,7 @@ function transformChatGPTExport(parsed, options = {}) {
|
|
|
282
286
|
function savedMemoryToImported(entry, filePath) {
|
|
283
287
|
const content = entry.content.trim();
|
|
284
288
|
if (content.length === 0) return void 0;
|
|
285
|
-
const sourceTimestamp = entry.updated_at ?? entry.created_at;
|
|
289
|
+
const sourceTimestamp = normalizeChatGPTTimestamp(entry.updated_at) ?? normalizeChatGPTTimestamp(entry.created_at);
|
|
286
290
|
return {
|
|
287
291
|
content,
|
|
288
292
|
sourceLabel: CHATGPT_SOURCE_LABEL,
|
|
@@ -310,16 +314,18 @@ function conversationToSummary(conversation, filePath, maxChars) {
|
|
|
310
314
|
const body = userTurns.map((t) => `- ${t.content}`).join("\n");
|
|
311
315
|
let content = titleLine + body;
|
|
312
316
|
if (content.length > maxChars) {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
317
|
+
const effectiveMaxChars = Math.max(0, Math.floor(maxChars));
|
|
318
|
+
const suffix = effectiveMaxChars >= 3 ? "..." : "";
|
|
319
|
+
const available = effectiveMaxChars - suffix.length;
|
|
320
|
+
if (titleLine.length + suffix.length >= effectiveMaxChars) {
|
|
321
|
+
content = titleLine.slice(0, Math.max(0, available)) + suffix;
|
|
316
322
|
} else {
|
|
317
|
-
const remaining =
|
|
323
|
+
const remaining = available - titleLine.length;
|
|
318
324
|
const bodyTruncated = body.slice(0, Math.max(0, remaining));
|
|
319
325
|
content = titleLine + bodyTruncated + suffix;
|
|
320
326
|
}
|
|
321
327
|
}
|
|
322
|
-
const sourceTimestamp = firstTimestamp(userTurns);
|
|
328
|
+
const sourceTimestamp = firstTimestamp(userTurns) ?? normalizeChatGPTTimestamp(conversation.update_time) ?? normalizeChatGPTTimestamp(conversation.create_time);
|
|
323
329
|
return {
|
|
324
330
|
content,
|
|
325
331
|
sourceLabel: CHATGPT_SOURCE_LABEL,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/adapter.ts","../src/parser.ts","../src/transform.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// ChatGPT importer adapter (issue #568 slice 2)\n// ---------------------------------------------------------------------------\n\nimport type {\n ImportedMemory,\n ImporterAdapter,\n ImporterParseOptions,\n ImporterTransformOptions,\n ImporterWriteResult,\n ImporterWriteTarget,\n} from \"@remnic/core\";\nimport { defaultWriteMemoriesToOrchestrator } from \"@remnic/core\";\n\nimport {\n parseChatGPTExport,\n type ParsedChatGPTExport,\n} from \"./parser.js\";\nimport {\n CHATGPT_SOURCE_LABEL,\n transformChatGPTExport,\n} from \"./transform.js\";\n\n/**\n * Canonical `ImporterAdapter` exposed by `@remnic/import-chatgpt`.\n *\n * Loaded by `remnic-cli/optional-importer.ts` via a computed-specifier\n * dynamic import. The CLI calls `adapter.parse` → `adapter.transform` →\n * `adapter.writeTo` through the shared `runImporter` helper in\n * `@remnic/core`.\n */\nexport const adapter: ImporterAdapter<ParsedChatGPTExport> = {\n name: \"chatgpt\",\n sourceLabel: CHATGPT_SOURCE_LABEL,\n\n parse(input: unknown, options?: ImporterParseOptions): ParsedChatGPTExport {\n return parseChatGPTExport(input, {\n ...(options?.strict !== undefined ? { strict: options.strict } : {}),\n ...(options?.filePath !== undefined ? { filePath: options.filePath } : {}),\n });\n },\n\n transform(\n parsed: ParsedChatGPTExport,\n options?: ImporterTransformOptions,\n ): ImportedMemory[] {\n return transformChatGPTExport(parsed, {\n includeConversations: options?.includeConversations === true,\n ...(options?.maxMemories !== undefined\n ? { maxMemories: options.maxMemories }\n : {}),\n });\n },\n\n async writeTo(\n target: ImporterWriteTarget,\n memories: ImportedMemory[],\n ): Promise<ImporterWriteResult> {\n return defaultWriteMemoriesToOrchestrator(target, memories);\n },\n};\n\n/** Alias kept for symmetry with other @remnic/import-* packages. */\nexport const chatgptAdapter = adapter;\n","// ---------------------------------------------------------------------------\n// ChatGPT data-export parser (issue #568 slice 2)\n// ---------------------------------------------------------------------------\n//\n// OpenAI publishes each account's export as a ZIP. Inside are several JSON\n// files; this parser understands the two that carry durable user-level\n// content:\n//\n// 1. Saved memories (\"memories\" in OpenAI parlance). Location varies by\n// export vintage — we accept any of:\n// - a top-level `memory` array (current 2026 shape)\n// - a top-level `memories` array (earlier 2024/2025 shapes)\n// - a `\"memory\"` section inside `user.json` (some exports embed it)\n// Each record is a single first-person fact the user confirmed.\n// 2. `conversations.json` — array of `Conversation` objects, each holding\n// a graph of messages keyed by id with parent links. Bulk messages are\n// NOT imported by default; a `--include-conversations` opt-in produces\n// one memory per conversation summarizing the user-side turns.\n//\n// The parser accepts either the raw JSON text of a specific file OR a bundle\n// object (already-parsed envelope) that contains the known keys. This lets\n// the CLI pass either the plain `conversations.json` contents or a combined\n// manifest when a future bundle-auto-detect (PR 7) lands.\n//\n// We do not read ZIP archives directly here — the CLI parses the JSON\n// contents of whichever file the user points at with `--file`. A follow-up\n// may add a ZIP-aware reader; until then, users unzip manually.\n\n// ---------------------------------------------------------------------------\n// Raw export shapes (subset we care about)\n// ---------------------------------------------------------------------------\n\n/**\n * A single saved memory entry. OpenAI has changed the shape several times;\n * we accept the union of fields observed across 2024-2026 exports.\n */\nexport interface ChatGPTSavedMemory {\n /** Stable identifier — uuid in recent exports, numeric string in older ones. */\n id?: string;\n /** The memory body. Required. */\n content: string;\n /** ISO 8601 create timestamp. */\n created_at?: string;\n /** ISO 8601 last-update timestamp, if different from created_at. */\n updated_at?: string;\n /** Soft-delete flag seen in 2025+ exports. When true, we skip the record. */\n deleted?: boolean;\n /** Whether the memory is pinned / manually curated. */\n pinned?: boolean;\n /** Tags the user applied to the memory. */\n tags?: string[];\n}\n\nexport interface ChatGPTConversationMessage {\n /** Message id. */\n id?: string;\n /** Author role: user / assistant / system / tool. */\n author?: { role?: string; name?: string | null };\n /** Content envelope — we read `parts[]` for text content. */\n content?: { parts?: unknown[]; content_type?: string } | null;\n create_time?: number | string | null;\n update_time?: number | string | null;\n parent?: string | null;\n}\n\nexport interface ChatGPTConversationNode {\n id: string;\n message?: ChatGPTConversationMessage | null;\n parent?: string | null;\n children?: string[];\n}\n\nexport interface ChatGPTConversation {\n id?: string;\n title?: string;\n create_time?: number | string | null;\n update_time?: number | string | null;\n /** Messages by id. Present in 2023+ conversation exports. */\n mapping?: Record<string, ChatGPTConversationNode> | null;\n /** Some older exports inline messages as an array instead. */\n messages?: ChatGPTConversationMessage[];\n /** Root message id (for graph traversal). */\n current_node?: string | null;\n}\n\n/**\n * Unified parsed shape we pass into `transform()`. Holds both saved memories\n * and (optionally) conversations; either may be empty.\n */\nexport interface ParsedChatGPTExport {\n savedMemories: ChatGPTSavedMemory[];\n conversations: ChatGPTConversation[];\n /** Source path the export came from (for provenance). */\n filePath?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Public parse API\n// ---------------------------------------------------------------------------\n\nexport interface ChatGPTParseOptions {\n /** When true, throw on any validation failure instead of skipping. */\n strict?: boolean;\n /** File path of the source — preserved for provenance in transformed memories. */\n filePath?: string;\n}\n\n/**\n * Parse a raw export payload. Accepts either:\n * - A JSON string (common CLI path — the CLI reads the file contents).\n * - An already-parsed object or array (`JSON.parse` result).\n *\n * Returns the unified `ParsedChatGPTExport`. Non-export payloads throw a\n * descriptive error; silently ignoring them would mask user errors\n * (CLAUDE.md rule 51).\n */\nexport function parseChatGPTExport(\n input: unknown,\n options: ChatGPTParseOptions = {},\n): ParsedChatGPTExport {\n // File-backed adapter contract: `runImportCommand` passes `undefined`\n // when `--file` is omitted. ChatGPT is a file-only importer — a\n // missing payload MUST surface as a user-facing error rather than\n // silently succeeding with 0 memories. Codex review on PR #595.\n if (input === undefined || input === null) {\n throw new Error(\n \"The 'chatgpt' importer requires a file. Pass `--file <path>` pointing at \" +\n \"your ChatGPT data-export `memory.json` or `conversations.json`.\",\n );\n }\n const raw = coerceJson(input);\n const result: ParsedChatGPTExport = {\n savedMemories: [],\n conversations: [],\n ...(options.filePath !== undefined ? { filePath: options.filePath } : {}),\n };\n\n // Shape 1: a top-level array. ChatGPT's `conversations.json` is literally\n // an array of conversations. Older memory exports are also arrays (each\n // entry a ChatGPTSavedMemory). A leading tombstone / malformed element\n // (null, {}, etc.) would defeat a `raw[0]`-only classifier and cause us\n // to silently drop every later valid entry. Scan the full array for the\n // first recognized shape instead. Codex review on PR #595.\n if (Array.isArray(raw)) {\n if (raw.length === 0) return result;\n let classification: \"conversation\" | \"memory\" | undefined;\n for (const entry of raw) {\n if (looksLikeConversation(entry)) {\n classification = \"conversation\";\n break;\n }\n if (looksLikeSavedMemory(entry)) {\n classification = \"memory\";\n break;\n }\n }\n if (classification === \"conversation\") {\n for (const entry of raw) {\n if (looksLikeConversation(entry)) {\n result.conversations.push(entry as ChatGPTConversation);\n } else if (options.strict) {\n throw new Error(\"Non-conversation entry in conversations array\");\n }\n }\n return result;\n }\n if (classification === \"memory\") {\n for (const entry of raw) {\n const mem = normalizeSavedMemory(entry, options.strict);\n if (mem) result.savedMemories.push(mem);\n }\n return result;\n }\n if (options.strict) {\n throw new Error(\n \"Unknown ChatGPT export array shape (neither memories nor conversations).\",\n );\n }\n return result;\n }\n\n // Shape 2: an object. We look for the known keys.\n if (raw && typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>;\n let sawKnownSection = false;\n // Saved memories — accept `memory`, `memories`, or nested `user.memory`.\n for (const key of [\"memory\", \"memories\"] as const) {\n const v = obj[key];\n if (Array.isArray(v)) {\n sawKnownSection = true;\n for (const entry of v) {\n const mem = normalizeSavedMemory(entry, options.strict);\n if (mem) result.savedMemories.push(mem);\n }\n }\n }\n if (obj.user && typeof obj.user === \"object\") {\n const userMem = (obj.user as Record<string, unknown>).memory;\n if (Array.isArray(userMem)) {\n sawKnownSection = true;\n for (const entry of userMem) {\n const mem = normalizeSavedMemory(entry, options.strict);\n if (mem) result.savedMemories.push(mem);\n }\n }\n }\n // Conversations — top-level `conversations` array is the canonical shape.\n const convs = obj.conversations;\n if (Array.isArray(convs)) {\n sawKnownSection = true;\n for (const entry of convs) {\n if (looksLikeConversation(entry)) {\n result.conversations.push(entry as ChatGPTConversation);\n } else if (options.strict) {\n throw new Error(\"Non-conversation entry in conversations array\");\n }\n }\n }\n // Strict mode: if the object has none of the recognized sections, bail\n // rather than silently returning an empty result. Non-strict mode keeps\n // the lenient \"returns empty struct\" behavior for future-shape safety.\n if (!sawKnownSection && options.strict) {\n throw new Error(\n \"Unknown ChatGPT export object shape: expected one of 'memory', \" +\n \"'memories', 'user.memory', or 'conversations' keys.\",\n );\n }\n return result;\n }\n\n // Primitive / unparseable payloads must NEVER return an empty success —\n // that would mask operator mistakes and allow automation to treat a\n // broken import as a clean \"0 memories\" import. Throw regardless of\n // strict mode. Codex review on PR #595.\n throw new Error(\n \"ChatGPT export must be a JSON array or object; received \" + typeof raw,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction coerceJson(input: unknown): unknown {\n if (typeof input === \"string\") {\n try {\n return JSON.parse(input);\n } catch (err) {\n throw new Error(\n `ChatGPT export is not valid JSON: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n return input;\n}\n\nfunction looksLikeSavedMemory(value: unknown): value is ChatGPTSavedMemory {\n if (!value || typeof value !== \"object\") return false;\n const v = value as Record<string, unknown>;\n // At minimum a memory has a content string. Some exports use `text`\n // instead — we normalize in `normalizeSavedMemory`.\n if (typeof v.content === \"string\" && v.content.length > 0) return true;\n if (typeof v.text === \"string\" && v.text.length > 0) return true;\n return false;\n}\n\nfunction looksLikeConversation(value: unknown): value is ChatGPTConversation {\n if (!value || typeof value !== \"object\") return false;\n const v = value as Record<string, unknown>;\n // A conversation always has either `mapping` (new shape) or `messages`\n // (legacy shape). Title alone is insufficient because saved-memory\n // exports may also carry a `title` field in some vintages.\n if (v.mapping && typeof v.mapping === \"object\") return true;\n if (Array.isArray(v.messages)) return true;\n return false;\n}\n\nfunction normalizeSavedMemory(\n value: unknown,\n strict: boolean | undefined,\n): ChatGPTSavedMemory | undefined {\n if (!value || typeof value !== \"object\") {\n if (strict) throw new Error(\"Saved memory entry must be an object\");\n return undefined;\n }\n const v = value as Record<string, unknown>;\n const content =\n typeof v.content === \"string\"\n ? v.content\n : typeof v.text === \"string\"\n ? v.text\n : undefined;\n if (!content || content.trim().length === 0) {\n if (strict) throw new Error(\"Saved memory entry missing content\");\n return undefined;\n }\n if (v.deleted === true) {\n // Soft-deleted; skip even in strict mode (it's a valid shape, just not a\n // memory the user wants imported).\n return undefined;\n }\n const normalized: ChatGPTSavedMemory = {\n content: content.trim(),\n ...(typeof v.id === \"string\" ? { id: v.id } : {}),\n ...(typeof v.created_at === \"string\" ? { created_at: v.created_at } : {}),\n ...(typeof v.updated_at === \"string\" ? { updated_at: v.updated_at } : {}),\n ...(v.pinned === true ? { pinned: true } : {}),\n ...(Array.isArray(v.tags)\n ? { tags: v.tags.filter((t): t is string => typeof t === \"string\") }\n : {}),\n };\n return normalized;\n}\n\n/**\n * Walk a conversation's message graph and return only the user-authored text\n * turns in chronological order. Exported so the transform layer can build\n * conversation summaries from the same graph.\n */\nexport function collectUserTurnsFromConversation(\n conversation: ChatGPTConversation,\n): Array<{ content: string; createdAt?: string }> {\n const collected: Array<{ content: string; createdAt?: string }> = [];\n\n if (Array.isArray(conversation.messages) && conversation.messages.length > 0) {\n for (const msg of conversation.messages) {\n const text = extractMessageText(msg);\n if (text) {\n collected.push({\n content: text,\n ...(asIsoString(msg.create_time ?? msg.update_time) !== undefined\n ? { createdAt: asIsoString(msg.create_time ?? msg.update_time) as string }\n : {}),\n });\n }\n }\n return collected;\n }\n\n if (conversation.mapping && typeof conversation.mapping === \"object\") {\n // Prefer walking the active chain from `current_node` up to the root via\n // `parent` links, then reverse so we visit in chronological order. This\n // mirrors how ChatGPT renders the conversation and excludes abandoned\n // branches the user backed out of. If `current_node` is missing, fall\n // back to a deterministic traversal of all nodes sorted by create_time\n // with node id as the stable secondary key.\n const mapping = conversation.mapping;\n const chainNodes = followCurrentNodeChain(mapping, conversation.current_node);\n const ordered =\n chainNodes.length > 0\n ? chainNodes\n : [...Object.values(mapping)].sort((a, b) => {\n const delta = toNumericTime(a.message?.create_time) -\n toNumericTime(b.message?.create_time);\n if (delta !== 0) return delta;\n // Stable secondary key: node id. Without this, nodes with equal\n // or missing timestamps order non-deterministically.\n const aid = typeof a.id === \"string\" ? a.id : \"\";\n const bid = typeof b.id === \"string\" ? b.id : \"\";\n if (aid < bid) return -1;\n if (aid > bid) return 1;\n return 0;\n });\n for (const node of ordered) {\n const msg = node.message;\n if (!msg) continue;\n if (msg.author?.role !== \"user\") continue;\n const text = extractMessageText(msg);\n if (text) {\n collected.push({\n content: text,\n ...(asIsoString(msg.create_time ?? msg.update_time) !== undefined\n ? { createdAt: asIsoString(msg.create_time ?? msg.update_time) as string }\n : {}),\n });\n }\n }\n }\n return collected;\n}\n\n/**\n * Walk the mapping graph from `current_node` back to the root via each\n * node's `parent` pointer, then reverse so the returned array is in\n * chronological order. Returns empty when `current_node` is missing or the\n * chain is broken so callers can fall back to timestamp sort.\n */\nfunction followCurrentNodeChain(\n mapping: Record<string, ChatGPTConversationNode>,\n currentNode: string | null | undefined,\n): ChatGPTConversationNode[] {\n if (typeof currentNode !== \"string\" || currentNode.length === 0) return [];\n const visited = new Set<string>();\n const chain: ChatGPTConversationNode[] = [];\n let cursor: string | null | undefined = currentNode;\n while (cursor) {\n if (visited.has(cursor)) {\n // Cycle — refuse to trust the chain. Fall back to timestamp sort.\n return [];\n }\n visited.add(cursor);\n const node: ChatGPTConversationNode | undefined = mapping[cursor];\n if (!node) {\n // Broken parent link (dangling reference). Don't return a partial\n // tail — it would silently omit the root and leave the caller with\n // an inconsistent view. Fall back to the timestamp-sorted walk.\n // Codex review on PR #595.\n return [];\n }\n chain.push(node);\n cursor = node.parent ?? null;\n }\n return chain.reverse();\n}\n\nfunction extractMessageText(msg: ChatGPTConversationMessage): string | undefined {\n if (!msg) return undefined;\n if (msg.author?.role !== \"user\") {\n // Even in inline-messages shape, only return user-authored text.\n return undefined;\n }\n const parts = msg.content?.parts;\n if (!Array.isArray(parts)) return undefined;\n const text = parts\n .filter((p): p is string => typeof p === \"string\" && p.length > 0)\n .join(\"\\n\")\n .trim();\n return text.length > 0 ? text : undefined;\n}\n\nfunction toNumericTime(value: number | string | null | undefined): number {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\") {\n const n = Number(value);\n if (Number.isFinite(n)) return n;\n const parsed = Date.parse(value);\n if (!Number.isNaN(parsed)) return parsed / 1000;\n }\n return 0;\n}\n\nfunction asIsoString(value: number | string | null | undefined): string | undefined {\n if (value === null || value === undefined) return undefined;\n if (typeof value === \"string\") {\n // Already ISO? Accept.\n if (/\\d{4}-\\d{2}-\\d{2}T/.test(value)) return value;\n const asNum = Number(value);\n if (Number.isFinite(asNum)) return epochToIso(asNum);\n return undefined;\n }\n if (Number.isFinite(value)) {\n return epochToIso(value);\n }\n return undefined;\n}\n\n/**\n * Convert an epoch value (seconds or ms) to an ISO string. Guards against\n * corrupted inputs that would crash `new Date(x).toISOString()` — the\n * `Date` constructor accepts arbitrarily large numbers but `toISOString`\n * throws RangeError for values beyond the platform's representable range\n * (±100,000,000 days from the epoch). Returns `undefined` for unusable\n * timestamps rather than propagating a crash.\n */\nfunction epochToIso(value: number): string | undefined {\n // ChatGPT exports store epoch seconds. Detect ms by magnitude.\n const ms = Math.abs(value) > 1e12 ? value : value * 1000;\n // JS Date range: ±8,640,000,000,000,000 ms. Clamp well inside that.\n const MAX_SAFE_MS = 8640000000000000;\n if (!Number.isFinite(ms) || Math.abs(ms) > MAX_SAFE_MS) return undefined;\n const d = new Date(ms);\n if (Number.isNaN(d.getTime())) return undefined;\n try {\n return d.toISOString();\n } catch {\n return undefined;\n }\n}\n","// ---------------------------------------------------------------------------\n// ChatGPT parsed → ImportedMemory transform (issue #568 slice 2)\n// ---------------------------------------------------------------------------\n//\n// Two shapes of memory come out of a ChatGPT export:\n//\n// 1. Saved memories — first-person facts the user confirmed. 1:1 mapping:\n// every entry becomes one `ImportedMemory`. This is the default.\n// 2. Conversation summaries — only produced when the caller opts in via\n// `includeConversations: true`. Each conversation is reduced to a\n// single \"user said X, Y, Z\" summary (user-side turns concatenated),\n// not uploaded verbatim. The rationale is that the conversation\n// bodies mix transient and durable content, and the extraction\n// pipeline downstream will score them — but we need SOMETHING\n// coherent to hand it. One memory per conversation keeps the import\n// footprint bounded when users have thousands of chats.\n//\n// The adapter runs `parse()` first, then `transform()` produces the final\n// `ImportedMemory[]`. Provenance (sourceLabel, importedFromPath, metadata)\n// is attached here; `runImporter` stamps `importedAt`.\n\nimport type { ImportedMemory } from \"@remnic/core\";\n\nimport type {\n ChatGPTConversation,\n ChatGPTSavedMemory,\n ParsedChatGPTExport,\n} from \"./parser.js\";\nimport { collectUserTurnsFromConversation } from \"./parser.js\";\n\nexport const CHATGPT_SOURCE_LABEL = \"chatgpt\";\n\nexport interface ChatGPTTransformOptions {\n /** When true, produce conversation-summary memories in addition to saved memories. */\n includeConversations?: boolean;\n /** Optional cap on total memories emitted — primarily for tests. */\n maxMemories?: number;\n /** Maximum characters for a conversation summary memory. */\n maxConversationSummaryChars?: number;\n}\n\nconst DEFAULT_CONVERSATION_SUMMARY_CHARS = 2000;\n\n/**\n * Transform a parsed ChatGPT export into `ImportedMemory[]`.\n * Saved memories are emitted first (in parse order), then conversation\n * summaries when opted in.\n */\nexport function transformChatGPTExport(\n parsed: ParsedChatGPTExport,\n options: ChatGPTTransformOptions = {},\n): ImportedMemory[] {\n const out: ImportedMemory[] = [];\n const cap = options.maxMemories;\n\n for (const entry of parsed.savedMemories) {\n if (cap !== undefined && out.length >= cap) return out;\n const memory = savedMemoryToImported(entry, parsed.filePath);\n if (memory) out.push(memory);\n }\n\n if (options.includeConversations) {\n const maxSummaryChars =\n options.maxConversationSummaryChars ?? DEFAULT_CONVERSATION_SUMMARY_CHARS;\n for (const conversation of parsed.conversations) {\n if (cap !== undefined && out.length >= cap) return out;\n const summary = conversationToSummary(\n conversation,\n parsed.filePath,\n maxSummaryChars,\n );\n if (summary) out.push(summary);\n }\n }\n return out;\n}\n\nfunction savedMemoryToImported(\n entry: ChatGPTSavedMemory,\n filePath: string | undefined,\n): ImportedMemory | undefined {\n const content = entry.content.trim();\n if (content.length === 0) return undefined;\n const sourceTimestamp = entry.updated_at ?? entry.created_at;\n return {\n content,\n sourceLabel: CHATGPT_SOURCE_LABEL,\n ...(entry.id !== undefined ? { sourceId: entry.id } : {}),\n ...(sourceTimestamp !== undefined ? { sourceTimestamp } : {}),\n ...(filePath !== undefined ? { importedFromPath: filePath } : {}),\n metadata: buildMetadata(entry),\n };\n}\n\nfunction buildMetadata(entry: ChatGPTSavedMemory): Record<string, unknown> {\n const meta: Record<string, unknown> = { kind: \"saved_memory\" };\n if (entry.pinned === true) meta.pinned = true;\n if (Array.isArray(entry.tags) && entry.tags.length > 0) {\n meta.tags = [...entry.tags];\n }\n return meta;\n}\n\nfunction conversationToSummary(\n conversation: ChatGPTConversation,\n filePath: string | undefined,\n maxChars: number,\n): ImportedMemory | undefined {\n const userTurns = collectUserTurnsFromConversation(conversation);\n if (userTurns.length === 0) return undefined;\n\n const title = typeof conversation.title === \"string\" ? conversation.title.trim() : \"\";\n const titleLine = title.length > 0 ? `Conversation: ${title}\\n\\n` : \"\";\n const body = userTurns.map((t) => `- ${t.content}`).join(\"\\n\");\n let content = titleLine + body;\n if (content.length > maxChars) {\n // Reserve 3 chars for the \"...\" suffix. If the titleLine alone already\n // exceeds maxChars (pathological — a very long title with a small cap),\n // truncate the titleLine itself rather than letting content exceed\n // maxChars.\n const suffix = \"...\";\n if (titleLine.length + suffix.length >= maxChars) {\n content = titleLine.slice(0, Math.max(0, maxChars - suffix.length)) + suffix;\n } else {\n const remaining = maxChars - titleLine.length - suffix.length;\n const bodyTruncated = body.slice(0, Math.max(0, remaining));\n content = titleLine + bodyTruncated + suffix;\n }\n }\n\n const sourceTimestamp = firstTimestamp(userTurns);\n return {\n content,\n sourceLabel: CHATGPT_SOURCE_LABEL,\n ...(typeof conversation.id === \"string\" ? { sourceId: conversation.id } : {}),\n ...(sourceTimestamp !== undefined ? { sourceTimestamp } : {}),\n ...(filePath !== undefined ? { importedFromPath: filePath } : {}),\n metadata: {\n kind: \"conversation_summary\",\n ...(title.length > 0 ? { title } : {}),\n userTurns: userTurns.length,\n },\n };\n}\n\nfunction firstTimestamp(\n turns: Array<{ content: string; createdAt?: string }>,\n): string | undefined {\n for (const turn of turns) {\n if (typeof turn.createdAt === \"string\" && turn.createdAt.length > 0) {\n return turn.createdAt;\n }\n }\n return undefined;\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/parser.ts","../src/transform.ts"],"sourcesContent":["// ---------------------------------------------------------------------------\n// ChatGPT importer adapter (issue #568 slice 2)\n// ---------------------------------------------------------------------------\n\nimport type {\n ImportedMemory,\n ImporterAdapter,\n ImporterParseOptions,\n ImporterTransformOptions,\n ImporterWriteResult,\n ImporterWriteTarget,\n} from \"@remnic/core\";\nimport { defaultWriteMemoriesToOrchestrator } from \"@remnic/core\";\n\nimport {\n parseChatGPTExport,\n type ParsedChatGPTExport,\n} from \"./parser.js\";\nimport {\n CHATGPT_SOURCE_LABEL,\n transformChatGPTExport,\n} from \"./transform.js\";\n\n/**\n * Canonical `ImporterAdapter` exposed by `@remnic/import-chatgpt`.\n *\n * Loaded by `remnic-cli/optional-importer.ts` via a computed-specifier\n * dynamic import. The CLI calls `adapter.parse` → `adapter.transform` →\n * `adapter.writeTo` through the shared `runImporter` helper in\n * `@remnic/core`.\n */\nexport const adapter: ImporterAdapter<ParsedChatGPTExport> = {\n name: \"chatgpt\",\n sourceLabel: CHATGPT_SOURCE_LABEL,\n\n parse(input: unknown, options?: ImporterParseOptions): ParsedChatGPTExport {\n return parseChatGPTExport(input, {\n ...(options?.strict !== undefined ? { strict: options.strict } : {}),\n ...(options?.filePath !== undefined ? { filePath: options.filePath } : {}),\n });\n },\n\n transform(\n parsed: ParsedChatGPTExport,\n options?: ImporterTransformOptions,\n ): ImportedMemory[] {\n return transformChatGPTExport(parsed, {\n includeConversations: options?.includeConversations === true,\n ...(options?.maxMemories !== undefined\n ? { maxMemories: options.maxMemories }\n : {}),\n });\n },\n\n async writeTo(\n target: ImporterWriteTarget,\n memories: ImportedMemory[],\n ): Promise<ImporterWriteResult> {\n return defaultWriteMemoriesToOrchestrator(target, memories);\n },\n};\n\n/** Alias kept for symmetry with other @remnic/import-* packages. */\nexport const chatgptAdapter = adapter;\n","// ---------------------------------------------------------------------------\n// ChatGPT data-export parser (issue #568 slice 2)\n// ---------------------------------------------------------------------------\n//\n// OpenAI publishes each account's export as a ZIP. Inside are several JSON\n// files; this parser understands the two that carry durable user-level\n// content:\n//\n// 1. Saved memories (\"memories\" in OpenAI parlance). Location varies by\n// export vintage — we accept any of:\n// - a top-level `memory` array (current 2026 shape)\n// - a top-level `memories` array (earlier 2024/2025 shapes)\n// - a `\"memory\"` section inside `user.json` (some exports embed it)\n// Each record is a single first-person fact the user confirmed.\n// 2. `conversations.json` — array of `Conversation` objects, each holding\n// a graph of messages keyed by id with parent links. Bulk messages are\n// NOT imported by default; a `--include-conversations` opt-in produces\n// one memory per conversation summarizing the user-side turns.\n//\n// The parser accepts either the raw JSON text of a specific file OR a bundle\n// object (already-parsed envelope) that contains the known keys. This lets\n// the CLI pass either the plain `conversations.json` contents or a combined\n// manifest when a future bundle-auto-detect (PR 7) lands.\n//\n// We do not read ZIP archives directly here — the CLI parses the JSON\n// contents of whichever file the user points at with `--file`. A follow-up\n// may add a ZIP-aware reader; until then, users unzip manually.\n\n// ---------------------------------------------------------------------------\n// Raw export shapes (subset we care about)\n// ---------------------------------------------------------------------------\n\n/**\n * A single saved memory entry. OpenAI has changed the shape several times;\n * we accept the union of fields observed across 2024-2026 exports.\n */\nexport interface ChatGPTSavedMemory {\n /** Stable identifier — uuid in recent exports, numeric string in older ones. */\n id?: string;\n /** The memory body. Required. */\n content: string;\n /** ISO 8601 or epoch create timestamp. */\n created_at?: string | number;\n /** ISO 8601 or epoch last-update timestamp, if different from created_at. */\n updated_at?: string | number;\n /** Soft-delete flag seen in 2025+ exports. When true, we skip the record. */\n deleted?: boolean;\n /** Whether the memory is pinned / manually curated. */\n pinned?: boolean;\n /** Tags the user applied to the memory. */\n tags?: string[];\n}\n\nexport interface ChatGPTConversationMessage {\n /** Message id. */\n id?: string;\n /** Author role: user / assistant / system / tool. */\n author?: { role?: string; name?: string | null };\n /** Content envelope — we read `parts[]` for text content. */\n content?: { parts?: unknown[]; content_type?: string } | null;\n create_time?: number | string | null;\n update_time?: number | string | null;\n parent?: string | null;\n}\n\nexport interface ChatGPTConversationNode {\n id: string;\n message?: ChatGPTConversationMessage | null;\n parent?: string | null;\n children?: string[];\n}\n\nexport interface ChatGPTConversation {\n id?: string;\n title?: string;\n create_time?: number | string | null;\n update_time?: number | string | null;\n /** Messages by id. Present in 2023+ conversation exports. */\n mapping?: Record<string, ChatGPTConversationNode> | null;\n /** Some older exports inline messages as an array instead. */\n messages?: ChatGPTConversationMessage[];\n /** Root message id (for graph traversal). */\n current_node?: string | null;\n}\n\n/**\n * Unified parsed shape we pass into `transform()`. Holds both saved memories\n * and (optionally) conversations; either may be empty.\n */\nexport interface ParsedChatGPTExport {\n savedMemories: ChatGPTSavedMemory[];\n conversations: ChatGPTConversation[];\n /** Source path the export came from (for provenance). */\n filePath?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Public parse API\n// ---------------------------------------------------------------------------\n\nexport interface ChatGPTParseOptions {\n /** When true, throw on any validation failure instead of skipping. */\n strict?: boolean;\n /** File path of the source — preserved for provenance in transformed memories. */\n filePath?: string;\n}\n\n/**\n * Parse a raw export payload. Accepts either:\n * - A JSON string (common CLI path — the CLI reads the file contents).\n * - An already-parsed object or array (`JSON.parse` result).\n *\n * Returns the unified `ParsedChatGPTExport`. Non-export payloads throw a\n * descriptive error; silently ignoring them would mask user errors\n * (CLAUDE.md rule 51).\n */\nexport function parseChatGPTExport(\n input: unknown,\n options: ChatGPTParseOptions = {},\n): ParsedChatGPTExport {\n // File-backed adapter contract: `runImportCommand` passes `undefined`\n // when `--file` is omitted. ChatGPT is a file-only importer — a\n // missing payload MUST surface as a user-facing error rather than\n // silently succeeding with 0 memories. Codex review on PR #595.\n if (input === undefined || input === null) {\n throw new Error(\n \"The 'chatgpt' importer requires a file. Pass `--file <path>` pointing at \" +\n \"your ChatGPT data-export `memory.json` or `conversations.json`.\",\n );\n }\n const raw = coerceJson(input);\n const result: ParsedChatGPTExport = {\n savedMemories: [],\n conversations: [],\n ...(options.filePath !== undefined ? { filePath: options.filePath } : {}),\n };\n\n // Shape 1: a top-level array. ChatGPT's `conversations.json` is literally\n // an array of conversations. Older memory exports are also arrays (each\n // entry a ChatGPTSavedMemory). A leading tombstone / malformed element\n // (null, {}, etc.) would defeat a `raw[0]`-only classifier and cause us\n // to silently drop every later valid entry. Scan the full array for the\n // first recognized shape instead. Codex review on PR #595.\n if (Array.isArray(raw)) {\n if (raw.length === 0) return result;\n let classification: \"conversation\" | \"memory\" | undefined;\n for (const entry of raw) {\n if (looksLikeConversation(entry)) {\n classification = \"conversation\";\n break;\n }\n if (looksLikeSavedMemory(entry)) {\n classification = \"memory\";\n break;\n }\n }\n if (classification === \"conversation\") {\n for (const entry of raw) {\n if (looksLikeConversation(entry)) {\n result.conversations.push(entry as ChatGPTConversation);\n } else if (options.strict) {\n throw new Error(\"Non-conversation entry in conversations array\");\n }\n }\n return result;\n }\n if (classification === \"memory\") {\n for (const entry of raw) {\n const mem = normalizeSavedMemory(entry, options.strict);\n if (mem) result.savedMemories.push(mem);\n }\n return result;\n }\n if (options.strict) {\n throw new Error(\n \"Unknown ChatGPT export array shape (neither memories nor conversations).\",\n );\n }\n return result;\n }\n\n // Shape 2: an object. We look for the known keys.\n if (raw && typeof raw === \"object\") {\n const obj = raw as Record<string, unknown>;\n let sawKnownSection = false;\n // Saved memories — accept `memory`, `memories`, or nested `user.memory`.\n for (const key of [\"memory\", \"memories\"] as const) {\n const v = obj[key];\n if (Array.isArray(v)) {\n sawKnownSection = true;\n for (const entry of v) {\n const mem = normalizeSavedMemory(entry, options.strict);\n if (mem) result.savedMemories.push(mem);\n }\n }\n }\n if (obj.user && typeof obj.user === \"object\") {\n const userMem = (obj.user as Record<string, unknown>).memory;\n if (Array.isArray(userMem)) {\n sawKnownSection = true;\n for (const entry of userMem) {\n const mem = normalizeSavedMemory(entry, options.strict);\n if (mem) result.savedMemories.push(mem);\n }\n }\n }\n // Conversations — top-level `conversations` array is the canonical shape.\n const convs = obj.conversations;\n if (Array.isArray(convs)) {\n sawKnownSection = true;\n for (const entry of convs) {\n if (looksLikeConversation(entry)) {\n result.conversations.push(entry as ChatGPTConversation);\n } else if (options.strict) {\n throw new Error(\"Non-conversation entry in conversations array\");\n }\n }\n }\n // Strict mode: if the object has none of the recognized sections, bail\n // rather than silently returning an empty result. Non-strict mode keeps\n // the lenient \"returns empty struct\" behavior for future-shape safety.\n if (!sawKnownSection && options.strict) {\n throw new Error(\n \"Unknown ChatGPT export object shape: expected one of 'memory', \" +\n \"'memories', 'user.memory', or 'conversations' keys.\",\n );\n }\n return result;\n }\n\n // Primitive / unparseable payloads must NEVER return an empty success —\n // that would mask operator mistakes and allow automation to treat a\n // broken import as a clean \"0 memories\" import. Throw regardless of\n // strict mode. Codex review on PR #595.\n throw new Error(\n \"ChatGPT export must be a JSON array or object; received \" + typeof raw,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\nfunction coerceJson(input: unknown): unknown {\n if (typeof input === \"string\") {\n try {\n return JSON.parse(input);\n } catch (err) {\n throw new Error(\n `ChatGPT export is not valid JSON: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n return input;\n}\n\nfunction looksLikeSavedMemory(value: unknown): value is ChatGPTSavedMemory {\n if (!value || typeof value !== \"object\") return false;\n const v = value as Record<string, unknown>;\n // At minimum a memory has a content string. Some exports use `text`\n // instead — we normalize in `normalizeSavedMemory`.\n if (typeof v.content === \"string\" && v.content.length > 0) return true;\n if (typeof v.text === \"string\" && v.text.length > 0) return true;\n return false;\n}\n\nfunction looksLikeConversation(value: unknown): value is ChatGPTConversation {\n if (!value || typeof value !== \"object\") return false;\n const v = value as Record<string, unknown>;\n // A conversation always has either `mapping` (new shape) or `messages`\n // (legacy shape). Title alone is insufficient because saved-memory\n // exports may also carry a `title` field in some vintages.\n if (v.mapping && typeof v.mapping === \"object\") return true;\n if (Array.isArray(v.messages)) return true;\n return false;\n}\n\nfunction normalizeSavedMemory(\n value: unknown,\n strict: boolean | undefined,\n): ChatGPTSavedMemory | undefined {\n if (!value || typeof value !== \"object\") {\n if (strict) throw new Error(\"Saved memory entry must be an object\");\n return undefined;\n }\n const v = value as Record<string, unknown>;\n const content =\n typeof v.content === \"string\"\n ? v.content\n : typeof v.text === \"string\"\n ? v.text\n : undefined;\n if (!content || content.trim().length === 0) {\n if (strict) throw new Error(\"Saved memory entry missing content\");\n return undefined;\n }\n if (v.deleted === true) {\n // Soft-deleted; skip even in strict mode (it's a valid shape, just not a\n // memory the user wants imported).\n return undefined;\n }\n const normalized: ChatGPTSavedMemory = {\n content: content.trim(),\n ...(typeof v.id === \"string\" ? { id: v.id } : {}),\n ...(isRawChatGPTTimestamp(v.created_at) ? { created_at: v.created_at } : {}),\n ...(isRawChatGPTTimestamp(v.updated_at) ? { updated_at: v.updated_at } : {}),\n ...(v.pinned === true ? { pinned: true } : {}),\n ...(Array.isArray(v.tags)\n ? { tags: v.tags.filter((t): t is string => typeof t === \"string\") }\n : {}),\n };\n return normalized;\n}\n\nfunction isRawChatGPTTimestamp(value: unknown): value is string | number {\n return typeof value === \"string\" || typeof value === \"number\";\n}\n\n/**\n * Walk a conversation's message graph and return only the user-authored text\n * turns in chronological order. Exported so the transform layer can build\n * conversation summaries from the same graph.\n */\nexport function collectUserTurnsFromConversation(\n conversation: ChatGPTConversation,\n): Array<{ content: string; createdAt?: string }> {\n const collected: Array<{ content: string; createdAt?: string }> = [];\n\n if (Array.isArray(conversation.messages) && conversation.messages.length > 0) {\n for (const msg of conversation.messages) {\n const text = extractMessageText(msg);\n if (text) {\n collected.push({\n content: text,\n ...(normalizeChatGPTTimestamp(msg.create_time ?? msg.update_time) !== undefined\n ? { createdAt: normalizeChatGPTTimestamp(msg.create_time ?? msg.update_time) as string }\n : {}),\n });\n }\n }\n return collected;\n }\n\n if (conversation.mapping && typeof conversation.mapping === \"object\") {\n // Prefer walking the active chain from `current_node` up to the root via\n // `parent` links, then reverse so we visit in chronological order. This\n // mirrors how ChatGPT renders the conversation and excludes abandoned\n // branches the user backed out of. If `current_node` is missing, fall\n // back to a deterministic traversal of all nodes sorted by create_time\n // with node id as the stable secondary key.\n const mapping = conversation.mapping;\n const chainNodes = followCurrentNodeChain(mapping, conversation.current_node);\n const ordered =\n chainNodes.length > 0\n ? chainNodes\n : [...Object.values(mapping)].sort((a, b) => {\n const delta = toNumericTime(a.message?.create_time) -\n toNumericTime(b.message?.create_time);\n if (delta !== 0) return delta;\n // Stable secondary key: node id. Without this, nodes with equal\n // or missing timestamps order non-deterministically.\n const aid = typeof a.id === \"string\" ? a.id : \"\";\n const bid = typeof b.id === \"string\" ? b.id : \"\";\n if (aid < bid) return -1;\n if (aid > bid) return 1;\n return 0;\n });\n for (const node of ordered) {\n const msg = node.message;\n if (!msg) continue;\n if (msg.author?.role !== \"user\") continue;\n const text = extractMessageText(msg);\n if (text) {\n collected.push({\n content: text,\n ...(normalizeChatGPTTimestamp(msg.create_time ?? msg.update_time) !== undefined\n ? { createdAt: normalizeChatGPTTimestamp(msg.create_time ?? msg.update_time) as string }\n : {}),\n });\n }\n }\n }\n return collected;\n}\n\n/**\n * Walk the mapping graph from `current_node` back to the root via each\n * node's `parent` pointer, then reverse so the returned array is in\n * chronological order. Returns empty when `current_node` is missing or the\n * chain is broken so callers can fall back to timestamp sort.\n */\nfunction followCurrentNodeChain(\n mapping: Record<string, ChatGPTConversationNode>,\n currentNode: string | null | undefined,\n): ChatGPTConversationNode[] {\n if (typeof currentNode !== \"string\" || currentNode.length === 0) return [];\n const visited = new Set<string>();\n const chain: ChatGPTConversationNode[] = [];\n let cursor: string | null | undefined = currentNode;\n while (cursor) {\n if (visited.has(cursor)) {\n // Cycle — refuse to trust the chain. Fall back to timestamp sort.\n return [];\n }\n visited.add(cursor);\n const node: ChatGPTConversationNode | undefined = mapping[cursor];\n if (!node) {\n // Broken parent link (dangling reference). Don't return a partial\n // tail — it would silently omit the root and leave the caller with\n // an inconsistent view. Fall back to the timestamp-sorted walk.\n // Codex review on PR #595.\n return [];\n }\n chain.push(node);\n const messageParent: string | null | undefined = node.message?.parent;\n cursor =\n typeof node.parent === \"string\" && node.parent.length > 0\n ? node.parent\n : typeof messageParent === \"string\" && messageParent.length > 0\n ? messageParent\n : null;\n }\n return chain.reverse();\n}\n\nfunction extractMessageText(msg: ChatGPTConversationMessage): string | undefined {\n if (!msg) return undefined;\n if (msg.author?.role !== \"user\") {\n // Even in inline-messages shape, only return user-authored text.\n return undefined;\n }\n const parts = msg.content?.parts;\n if (!Array.isArray(parts)) return undefined;\n const text = parts\n .filter((p): p is string => typeof p === \"string\" && p.length > 0)\n .join(\"\\n\")\n .trim();\n return text.length > 0 ? text : undefined;\n}\n\nfunction toNumericTime(value: number | string | null | undefined): number {\n if (typeof value === \"number\" && Number.isFinite(value)) return value;\n if (typeof value === \"string\") {\n const n = Number(value);\n if (Number.isFinite(n)) return n;\n const parsed = Date.parse(value);\n if (!Number.isNaN(parsed)) return parsed / 1000;\n }\n return 0;\n}\n\nexport function normalizeChatGPTTimestamp(value: number | string | null | undefined): string | undefined {\n if (value === null || value === undefined) return undefined;\n if (typeof value === \"string\") {\n // Already ISO? Accept.\n if (/\\d{4}-\\d{2}-\\d{2}T/.test(value)) return value;\n const asNum = Number(value);\n if (Number.isFinite(asNum)) return epochToIso(asNum);\n return undefined;\n }\n if (Number.isFinite(value)) {\n return epochToIso(value);\n }\n return undefined;\n}\n\n/**\n * Convert an epoch value (seconds or ms) to an ISO string. Guards against\n * corrupted inputs that would crash `new Date(x).toISOString()` — the\n * `Date` constructor accepts arbitrarily large numbers but `toISOString`\n * throws RangeError for values beyond the platform's representable range\n * (±100,000,000 days from the epoch). Returns `undefined` for unusable\n * timestamps rather than propagating a crash.\n */\nfunction epochToIso(value: number): string | undefined {\n // ChatGPT exports store epoch seconds. Detect ms by magnitude.\n const ms = Math.abs(value) > 1e12 ? value : value * 1000;\n // JS Date range: ±8,640,000,000,000,000 ms. Clamp well inside that.\n const MAX_SAFE_MS = 8640000000000000;\n if (!Number.isFinite(ms) || Math.abs(ms) > MAX_SAFE_MS) return undefined;\n const d = new Date(ms);\n if (Number.isNaN(d.getTime())) return undefined;\n try {\n return d.toISOString();\n } catch {\n return undefined;\n }\n}\n","// ---------------------------------------------------------------------------\n// ChatGPT parsed → ImportedMemory transform (issue #568 slice 2)\n// ---------------------------------------------------------------------------\n//\n// Two shapes of memory come out of a ChatGPT export:\n//\n// 1. Saved memories — first-person facts the user confirmed. 1:1 mapping:\n// every entry becomes one `ImportedMemory`. This is the default.\n// 2. Conversation summaries — only produced when the caller opts in via\n// `includeConversations: true`. Each conversation is reduced to a\n// single \"user said X, Y, Z\" summary (user-side turns concatenated),\n// not uploaded verbatim. The rationale is that the conversation\n// bodies mix transient and durable content, and the extraction\n// pipeline downstream will score them — but we need SOMETHING\n// coherent to hand it. One memory per conversation keeps the import\n// footprint bounded when users have thousands of chats.\n//\n// The adapter runs `parse()` first, then `transform()` produces the final\n// `ImportedMemory[]`. Provenance (sourceLabel, importedFromPath, metadata)\n// is attached here; `runImporter` stamps `importedAt`.\n\nimport type { ImportedMemory } from \"@remnic/core\";\n\nimport type {\n ChatGPTConversation,\n ChatGPTSavedMemory,\n ParsedChatGPTExport,\n} from \"./parser.js\";\nimport {\n collectUserTurnsFromConversation,\n normalizeChatGPTTimestamp,\n} from \"./parser.js\";\n\nexport const CHATGPT_SOURCE_LABEL = \"chatgpt\";\n\nexport interface ChatGPTTransformOptions {\n /** When true, produce conversation-summary memories in addition to saved memories. */\n includeConversations?: boolean;\n /** Optional cap on total memories emitted — primarily for tests. */\n maxMemories?: number;\n /** Maximum characters for a conversation summary memory. */\n maxConversationSummaryChars?: number;\n}\n\nconst DEFAULT_CONVERSATION_SUMMARY_CHARS = 2000;\n\n/**\n * Transform a parsed ChatGPT export into `ImportedMemory[]`.\n * Saved memories are emitted first (in parse order), then conversation\n * summaries when opted in.\n */\nexport function transformChatGPTExport(\n parsed: ParsedChatGPTExport,\n options: ChatGPTTransformOptions = {},\n): ImportedMemory[] {\n const out: ImportedMemory[] = [];\n const cap = options.maxMemories;\n\n for (const entry of parsed.savedMemories) {\n if (cap !== undefined && out.length >= cap) return out;\n const memory = savedMemoryToImported(entry, parsed.filePath);\n if (memory) out.push(memory);\n }\n\n if (options.includeConversations) {\n const maxSummaryChars =\n options.maxConversationSummaryChars ?? DEFAULT_CONVERSATION_SUMMARY_CHARS;\n for (const conversation of parsed.conversations) {\n if (cap !== undefined && out.length >= cap) return out;\n const summary = conversationToSummary(\n conversation,\n parsed.filePath,\n maxSummaryChars,\n );\n if (summary) out.push(summary);\n }\n }\n return out;\n}\n\nfunction savedMemoryToImported(\n entry: ChatGPTSavedMemory,\n filePath: string | undefined,\n): ImportedMemory | undefined {\n const content = entry.content.trim();\n if (content.length === 0) return undefined;\n const sourceTimestamp =\n normalizeChatGPTTimestamp(entry.updated_at) ??\n normalizeChatGPTTimestamp(entry.created_at);\n return {\n content,\n sourceLabel: CHATGPT_SOURCE_LABEL,\n ...(entry.id !== undefined ? { sourceId: entry.id } : {}),\n ...(sourceTimestamp !== undefined ? { sourceTimestamp } : {}),\n ...(filePath !== undefined ? { importedFromPath: filePath } : {}),\n metadata: buildMetadata(entry),\n };\n}\n\nfunction buildMetadata(entry: ChatGPTSavedMemory): Record<string, unknown> {\n const meta: Record<string, unknown> = { kind: \"saved_memory\" };\n if (entry.pinned === true) meta.pinned = true;\n if (Array.isArray(entry.tags) && entry.tags.length > 0) {\n meta.tags = [...entry.tags];\n }\n return meta;\n}\n\nfunction conversationToSummary(\n conversation: ChatGPTConversation,\n filePath: string | undefined,\n maxChars: number,\n): ImportedMemory | undefined {\n const userTurns = collectUserTurnsFromConversation(conversation);\n if (userTurns.length === 0) return undefined;\n\n const title = typeof conversation.title === \"string\" ? conversation.title.trim() : \"\";\n const titleLine = title.length > 0 ? `Conversation: ${title}\\n\\n` : \"\";\n const body = userTurns.map((t) => `- ${t.content}`).join(\"\\n\");\n let content = titleLine + body;\n if (content.length > maxChars) {\n const effectiveMaxChars = Math.max(0, Math.floor(maxChars));\n const suffix = effectiveMaxChars >= 3 ? \"...\" : \"\";\n const available = effectiveMaxChars - suffix.length;\n if (titleLine.length + suffix.length >= effectiveMaxChars) {\n content = titleLine.slice(0, Math.max(0, available)) + suffix;\n } else {\n const remaining = available - titleLine.length;\n const bodyTruncated = body.slice(0, Math.max(0, remaining));\n content = titleLine + bodyTruncated + suffix;\n }\n }\n\n const sourceTimestamp = firstTimestamp(userTurns)\n ?? normalizeChatGPTTimestamp(conversation.update_time)\n ?? normalizeChatGPTTimestamp(conversation.create_time);\n return {\n content,\n sourceLabel: CHATGPT_SOURCE_LABEL,\n ...(typeof conversation.id === \"string\" ? { sourceId: conversation.id } : {}),\n ...(sourceTimestamp !== undefined ? { sourceTimestamp } : {}),\n ...(filePath !== undefined ? { importedFromPath: filePath } : {}),\n metadata: {\n kind: \"conversation_summary\",\n ...(title.length > 0 ? { title } : {}),\n userTurns: userTurns.length,\n },\n };\n}\n\nfunction firstTimestamp(\n turns: Array<{ content: string; createdAt?: string }>,\n): string | undefined {\n for (const turn of turns) {\n if (typeof turn.createdAt === \"string\" && turn.createdAt.length > 0) {\n return turn.createdAt;\n }\n }\n return undefined;\n}\n"],"mappings":";;;AAYA,SAAS,0CAA0C;;;ACwG5C,SAAS,mBACd,OACA,UAA+B,CAAC,GACX;AAKrB,MAAI,UAAU,UAAa,UAAU,MAAM;AACzC,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AACA,QAAM,MAAM,WAAW,KAAK;AAC5B,QAAM,SAA8B;AAAA,IAClC,eAAe,CAAC;AAAA,IAChB,eAAe,CAAC;AAAA,IAChB,GAAI,QAAQ,aAAa,SAAY,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,EACzE;AAQA,MAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,QAAI,IAAI,WAAW,EAAG,QAAO;AAC7B,QAAI;AACJ,eAAW,SAAS,KAAK;AACvB,UAAI,sBAAsB,KAAK,GAAG;AAChC,yBAAiB;AACjB;AAAA,MACF;AACA,UAAI,qBAAqB,KAAK,GAAG;AAC/B,yBAAiB;AACjB;AAAA,MACF;AAAA,IACF;AACA,QAAI,mBAAmB,gBAAgB;AACrC,iBAAW,SAAS,KAAK;AACvB,YAAI,sBAAsB,KAAK,GAAG;AAChC,iBAAO,cAAc,KAAK,KAA4B;AAAA,QACxD,WAAW,QAAQ,QAAQ;AACzB,gBAAM,IAAI,MAAM,+CAA+C;AAAA,QACjE;AAAA,MACF;AACA,aAAO;AAAA,IACT;AACA,QAAI,mBAAmB,UAAU;AAC/B,iBAAW,SAAS,KAAK;AACvB,cAAM,MAAM,qBAAqB,OAAO,QAAQ,MAAM;AACtD,YAAI,IAAK,QAAO,cAAc,KAAK,GAAG;AAAA,MACxC;AACA,aAAO;AAAA,IACT;AACA,QAAI,QAAQ,QAAQ;AAClB,YAAM,IAAI;AAAA,QACR;AAAA,MACF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAGA,MAAI,OAAO,OAAO,QAAQ,UAAU;AAClC,UAAM,MAAM;AACZ,QAAI,kBAAkB;AAEtB,eAAW,OAAO,CAAC,UAAU,UAAU,GAAY;AACjD,YAAM,IAAI,IAAI,GAAG;AACjB,UAAI,MAAM,QAAQ,CAAC,GAAG;AACpB,0BAAkB;AAClB,mBAAW,SAAS,GAAG;AACrB,gBAAM,MAAM,qBAAqB,OAAO,QAAQ,MAAM;AACtD,cAAI,IAAK,QAAO,cAAc,KAAK,GAAG;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AACA,QAAI,IAAI,QAAQ,OAAO,IAAI,SAAS,UAAU;AAC5C,YAAM,UAAW,IAAI,KAAiC;AACtD,UAAI,MAAM,QAAQ,OAAO,GAAG;AAC1B,0BAAkB;AAClB,mBAAW,SAAS,SAAS;AAC3B,gBAAM,MAAM,qBAAqB,OAAO,QAAQ,MAAM;AACtD,cAAI,IAAK,QAAO,cAAc,KAAK,GAAG;AAAA,QACxC;AAAA,MACF;AAAA,IACF;AAEA,UAAM,QAAQ,IAAI;AAClB,QAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,wBAAkB;AAClB,iBAAW,SAAS,OAAO;AACzB,YAAI,sBAAsB,KAAK,GAAG;AAChC,iBAAO,cAAc,KAAK,KAA4B;AAAA,QACxD,WAAW,QAAQ,QAAQ;AACzB,gBAAM,IAAI,MAAM,+CAA+C;AAAA,QACjE;AAAA,MACF;AAAA,IACF;AAIA,QAAI,CAAC,mBAAmB,QAAQ,QAAQ;AACtC,YAAM,IAAI;AAAA,QACR;AAAA,MAEF;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAMA,QAAM,IAAI;AAAA,IACR,6DAA6D,OAAO;AAAA,EACtE;AACF;AAMA,SAAS,WAAW,OAAyB;AAC3C,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI;AACF,aAAO,KAAK,MAAM,KAAK;AAAA,IACzB,SAAS,KAAK;AACZ,YAAM,IAAI;AAAA,QACR,qCACE,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CACjD;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,qBAAqB,OAA6C;AACzE,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,IAAI;AAGV,MAAI,OAAO,EAAE,YAAY,YAAY,EAAE,QAAQ,SAAS,EAAG,QAAO;AAClE,MAAI,OAAO,EAAE,SAAS,YAAY,EAAE,KAAK,SAAS,EAAG,QAAO;AAC5D,SAAO;AACT;AAEA,SAAS,sBAAsB,OAA8C;AAC3E,MAAI,CAAC,SAAS,OAAO,UAAU,SAAU,QAAO;AAChD,QAAM,IAAI;AAIV,MAAI,EAAE,WAAW,OAAO,EAAE,YAAY,SAAU,QAAO;AACvD,MAAI,MAAM,QAAQ,EAAE,QAAQ,EAAG,QAAO;AACtC,SAAO;AACT;AAEA,SAAS,qBACP,OACA,QACgC;AAChC,MAAI,CAAC,SAAS,OAAO,UAAU,UAAU;AACvC,QAAI,OAAQ,OAAM,IAAI,MAAM,sCAAsC;AAClE,WAAO;AAAA,EACT;AACA,QAAM,IAAI;AACV,QAAM,UACJ,OAAO,EAAE,YAAY,WACjB,EAAE,UACF,OAAO,EAAE,SAAS,WAChB,EAAE,OACF;AACR,MAAI,CAAC,WAAW,QAAQ,KAAK,EAAE,WAAW,GAAG;AAC3C,QAAI,OAAQ,OAAM,IAAI,MAAM,oCAAoC;AAChE,WAAO;AAAA,EACT;AACA,MAAI,EAAE,YAAY,MAAM;AAGtB,WAAO;AAAA,EACT;AACA,QAAM,aAAiC;AAAA,IACrC,SAAS,QAAQ,KAAK;AAAA,IACtB,GAAI,OAAO,EAAE,OAAO,WAAW,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC;AAAA,IAC/C,GAAI,sBAAsB,EAAE,UAAU,IAAI,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IAC1E,GAAI,sBAAsB,EAAE,UAAU,IAAI,EAAE,YAAY,EAAE,WAAW,IAAI,CAAC;AAAA,IAC1E,GAAI,EAAE,WAAW,OAAO,EAAE,QAAQ,KAAK,IAAI,CAAC;AAAA,IAC5C,GAAI,MAAM,QAAQ,EAAE,IAAI,IACpB,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,MAAmB,OAAO,MAAM,QAAQ,EAAE,IACjE,CAAC;AAAA,EACP;AACA,SAAO;AACT;AAEA,SAAS,sBAAsB,OAA0C;AACvE,SAAO,OAAO,UAAU,YAAY,OAAO,UAAU;AACvD;AAOO,SAAS,iCACd,cACgD;AAChD,QAAM,YAA4D,CAAC;AAEnE,MAAI,MAAM,QAAQ,aAAa,QAAQ,KAAK,aAAa,SAAS,SAAS,GAAG;AAC5E,eAAW,OAAO,aAAa,UAAU;AACvC,YAAM,OAAO,mBAAmB,GAAG;AACnC,UAAI,MAAM;AACR,kBAAU,KAAK;AAAA,UACb,SAAS;AAAA,UACT,GAAI,0BAA0B,IAAI,eAAe,IAAI,WAAW,MAAM,SAClE,EAAE,WAAW,0BAA0B,IAAI,eAAe,IAAI,WAAW,EAAY,IACrF,CAAC;AAAA,QACP,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO;AAAA,EACT;AAEA,MAAI,aAAa,WAAW,OAAO,aAAa,YAAY,UAAU;AAOpE,UAAM,UAAU,aAAa;AAC7B,UAAM,aAAa,uBAAuB,SAAS,aAAa,YAAY;AAC5E,UAAM,UACJ,WAAW,SAAS,IAChB,aACA,CAAC,GAAG,OAAO,OAAO,OAAO,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM;AACzC,YAAM,QAAQ,cAAc,EAAE,SAAS,WAAW,IAChD,cAAc,EAAE,SAAS,WAAW;AACtC,UAAI,UAAU,EAAG,QAAO;AAGxB,YAAM,MAAM,OAAO,EAAE,OAAO,WAAW,EAAE,KAAK;AAC9C,YAAM,MAAM,OAAO,EAAE,OAAO,WAAW,EAAE,KAAK;AAC9C,UAAI,MAAM,IAAK,QAAO;AACtB,UAAI,MAAM,IAAK,QAAO;AACtB,aAAO;AAAA,IACT,CAAC;AACP,eAAW,QAAQ,SAAS;AAC1B,YAAM,MAAM,KAAK;AACjB,UAAI,CAAC,IAAK;AACV,UAAI,IAAI,QAAQ,SAAS,OAAQ;AACjC,YAAM,OAAO,mBAAmB,GAAG;AACnC,UAAI,MAAM;AACR,kBAAU,KAAK;AAAA,UACb,SAAS;AAAA,UACT,GAAI,0BAA0B,IAAI,eAAe,IAAI,WAAW,MAAM,SAClE,EAAE,WAAW,0BAA0B,IAAI,eAAe,IAAI,WAAW,EAAY,IACrF,CAAC;AAAA,QACP,CAAC;AAAA,MACH;AAAA,IACF;AAAA,EACF;AACA,SAAO;AACT;AAQA,SAAS,uBACP,SACA,aAC2B;AAC3B,MAAI,OAAO,gBAAgB,YAAY,YAAY,WAAW,EAAG,QAAO,CAAC;AACzE,QAAM,UAAU,oBAAI,IAAY;AAChC,QAAM,QAAmC,CAAC;AAC1C,MAAI,SAAoC;AACxC,SAAO,QAAQ;AACb,QAAI,QAAQ,IAAI,MAAM,GAAG;AAEvB,aAAO,CAAC;AAAA,IACV;AACA,YAAQ,IAAI,MAAM;AAClB,UAAM,OAA4C,QAAQ,MAAM;AAChE,QAAI,CAAC,MAAM;AAKT,aAAO,CAAC;AAAA,IACV;AACA,UAAM,KAAK,IAAI;AACf,UAAM,gBAA2C,KAAK,SAAS;AAC/D,aACE,OAAO,KAAK,WAAW,YAAY,KAAK,OAAO,SAAS,IACpD,KAAK,SACL,OAAO,kBAAkB,YAAY,cAAc,SAAS,IAC1D,gBACA;AAAA,EACV;AACA,SAAO,MAAM,QAAQ;AACvB;AAEA,SAAS,mBAAmB,KAAqD;AAC/E,MAAI,CAAC,IAAK,QAAO;AACjB,MAAI,IAAI,QAAQ,SAAS,QAAQ;AAE/B,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,IAAI,SAAS;AAC3B,MAAI,CAAC,MAAM,QAAQ,KAAK,EAAG,QAAO;AAClC,QAAM,OAAO,MACV,OAAO,CAAC,MAAmB,OAAO,MAAM,YAAY,EAAE,SAAS,CAAC,EAChE,KAAK,IAAI,EACT,KAAK;AACR,SAAO,KAAK,SAAS,IAAI,OAAO;AAClC;AAEA,SAAS,cAAc,OAAmD;AACxE,MAAI,OAAO,UAAU,YAAY,OAAO,SAAS,KAAK,EAAG,QAAO;AAChE,MAAI,OAAO,UAAU,UAAU;AAC7B,UAAM,IAAI,OAAO,KAAK;AACtB,QAAI,OAAO,SAAS,CAAC,EAAG,QAAO;AAC/B,UAAM,SAAS,KAAK,MAAM,KAAK;AAC/B,QAAI,CAAC,OAAO,MAAM,MAAM,EAAG,QAAO,SAAS;AAAA,EAC7C;AACA,SAAO;AACT;AAEO,SAAS,0BAA0B,OAA+D;AACvG,MAAI,UAAU,QAAQ,UAAU,OAAW,QAAO;AAClD,MAAI,OAAO,UAAU,UAAU;AAE7B,QAAI,qBAAqB,KAAK,KAAK,EAAG,QAAO;AAC7C,UAAM,QAAQ,OAAO,KAAK;AAC1B,QAAI,OAAO,SAAS,KAAK,EAAG,QAAO,WAAW,KAAK;AACnD,WAAO;AAAA,EACT;AACA,MAAI,OAAO,SAAS,KAAK,GAAG;AAC1B,WAAO,WAAW,KAAK;AAAA,EACzB;AACA,SAAO;AACT;AAUA,SAAS,WAAW,OAAmC;AAErD,QAAM,KAAK,KAAK,IAAI,KAAK,IAAI,OAAO,QAAQ,QAAQ;AAEpD,QAAM,cAAc;AACpB,MAAI,CAAC,OAAO,SAAS,EAAE,KAAK,KAAK,IAAI,EAAE,IAAI,YAAa,QAAO;AAC/D,QAAM,IAAI,IAAI,KAAK,EAAE;AACrB,MAAI,OAAO,MAAM,EAAE,QAAQ,CAAC,EAAG,QAAO;AACtC,MAAI;AACF,WAAO,EAAE,YAAY;AAAA,EACvB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;;;ACxcO,IAAM,uBAAuB;AAWpC,IAAM,qCAAqC;AAOpC,SAAS,uBACd,QACA,UAAmC,CAAC,GAClB;AAClB,QAAM,MAAwB,CAAC;AAC/B,QAAM,MAAM,QAAQ;AAEpB,aAAW,SAAS,OAAO,eAAe;AACxC,QAAI,QAAQ,UAAa,IAAI,UAAU,IAAK,QAAO;AACnD,UAAM,SAAS,sBAAsB,OAAO,OAAO,QAAQ;AAC3D,QAAI,OAAQ,KAAI,KAAK,MAAM;AAAA,EAC7B;AAEA,MAAI,QAAQ,sBAAsB;AAChC,UAAM,kBACJ,QAAQ,+BAA+B;AACzC,eAAW,gBAAgB,OAAO,eAAe;AAC/C,UAAI,QAAQ,UAAa,IAAI,UAAU,IAAK,QAAO;AACnD,YAAM,UAAU;AAAA,QACd;AAAA,QACA,OAAO;AAAA,QACP;AAAA,MACF;AACA,UAAI,QAAS,KAAI,KAAK,OAAO;AAAA,IAC/B;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,sBACP,OACA,UAC4B;AAC5B,QAAM,UAAU,MAAM,QAAQ,KAAK;AACnC,MAAI,QAAQ,WAAW,EAAG,QAAO;AACjC,QAAM,kBACJ,0BAA0B,MAAM,UAAU,KAC1C,0BAA0B,MAAM,UAAU;AAC5C,SAAO;AAAA,IACL;AAAA,IACA,aAAa;AAAA,IACb,GAAI,MAAM,OAAO,SAAY,EAAE,UAAU,MAAM,GAAG,IAAI,CAAC;AAAA,IACvD,GAAI,oBAAoB,SAAY,EAAE,gBAAgB,IAAI,CAAC;AAAA,IAC3D,GAAI,aAAa,SAAY,EAAE,kBAAkB,SAAS,IAAI,CAAC;AAAA,IAC/D,UAAU,cAAc,KAAK;AAAA,EAC/B;AACF;AAEA,SAAS,cAAc,OAAoD;AACzE,QAAM,OAAgC,EAAE,MAAM,eAAe;AAC7D,MAAI,MAAM,WAAW,KAAM,MAAK,SAAS;AACzC,MAAI,MAAM,QAAQ,MAAM,IAAI,KAAK,MAAM,KAAK,SAAS,GAAG;AACtD,SAAK,OAAO,CAAC,GAAG,MAAM,IAAI;AAAA,EAC5B;AACA,SAAO;AACT;AAEA,SAAS,sBACP,cACA,UACA,UAC4B;AAC5B,QAAM,YAAY,iCAAiC,YAAY;AAC/D,MAAI,UAAU,WAAW,EAAG,QAAO;AAEnC,QAAM,QAAQ,OAAO,aAAa,UAAU,WAAW,aAAa,MAAM,KAAK,IAAI;AACnF,QAAM,YAAY,MAAM,SAAS,IAAI,iBAAiB,KAAK;AAAA;AAAA,IAAS;AACpE,QAAM,OAAO,UAAU,IAAI,CAAC,MAAM,KAAK,EAAE,OAAO,EAAE,EAAE,KAAK,IAAI;AAC7D,MAAI,UAAU,YAAY;AAC1B,MAAI,QAAQ,SAAS,UAAU;AAC7B,UAAM,oBAAoB,KAAK,IAAI,GAAG,KAAK,MAAM,QAAQ,CAAC;AAC1D,UAAM,SAAS,qBAAqB,IAAI,QAAQ;AAChD,UAAM,YAAY,oBAAoB,OAAO;AAC7C,QAAI,UAAU,SAAS,OAAO,UAAU,mBAAmB;AACzD,gBAAU,UAAU,MAAM,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC,IAAI;AAAA,IACzD,OAAO;AACL,YAAM,YAAY,YAAY,UAAU;AACxC,YAAM,gBAAgB,KAAK,MAAM,GAAG,KAAK,IAAI,GAAG,SAAS,CAAC;AAC1D,gBAAU,YAAY,gBAAgB;AAAA,IACxC;AAAA,EACF;AAEA,QAAM,kBAAkB,eAAe,SAAS,KAC3C,0BAA0B,aAAa,WAAW,KAClD,0BAA0B,aAAa,WAAW;AACvD,SAAO;AAAA,IACL;AAAA,IACA,aAAa;AAAA,IACb,GAAI,OAAO,aAAa,OAAO,WAAW,EAAE,UAAU,aAAa,GAAG,IAAI,CAAC;AAAA,IAC3E,GAAI,oBAAoB,SAAY,EAAE,gBAAgB,IAAI,CAAC;AAAA,IAC3D,GAAI,aAAa,SAAY,EAAE,kBAAkB,SAAS,IAAI,CAAC;AAAA,IAC/D,UAAU;AAAA,MACR,MAAM;AAAA,MACN,GAAI,MAAM,SAAS,IAAI,EAAE,MAAM,IAAI,CAAC;AAAA,MACpC,WAAW,UAAU;AAAA,IACvB;AAAA,EACF;AACF;AAEA,SAAS,eACP,OACoB;AACpB,aAAW,QAAQ,OAAO;AACxB,QAAI,OAAO,KAAK,cAAc,YAAY,KAAK,UAAU,SAAS,GAAG;AACnE,aAAO,KAAK;AAAA,IACd;AAAA,EACF;AACA,SAAO;AACT;;;AFhIO,IAAM,UAAgD;AAAA,EAC3D,MAAM;AAAA,EACN,aAAa;AAAA,EAEb,MAAM,OAAgB,SAAqD;AACzE,WAAO,mBAAmB,OAAO;AAAA,MAC/B,GAAI,SAAS,WAAW,SAAY,EAAE,QAAQ,QAAQ,OAAO,IAAI,CAAC;AAAA,MAClE,GAAI,SAAS,aAAa,SAAY,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IAC1E,CAAC;AAAA,EACH;AAAA,EAEA,UACE,QACA,SACkB;AAClB,WAAO,uBAAuB,QAAQ;AAAA,MACpC,sBAAsB,SAAS,yBAAyB;AAAA,MACxD,GAAI,SAAS,gBAAgB,SACzB,EAAE,aAAa,QAAQ,YAAY,IACnC,CAAC;AAAA,IACP,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,QACJ,QACA,UAC8B;AAC9B,WAAO,mCAAmC,QAAQ,QAAQ;AAAA,EAC5D;AACF;AAGO,IAAM,iBAAiB;","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/import-chatgpt",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.3.517",
|
|
4
4
|
"description": "Import memory and conversation summaries from ChatGPT data exports into Remnic (issue #568)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -11,25 +11,21 @@
|
|
|
11
11
|
"import": "./dist/index.js"
|
|
12
12
|
}
|
|
13
13
|
},
|
|
14
|
-
"files": [
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
"check-types": "tsc --noEmit",
|
|
18
|
-
"test": "tsx --test src/adapter.test.ts src/parser.test.ts src/transform.test.ts",
|
|
19
|
-
"prepublishOnly": "npm run build"
|
|
20
|
-
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
21
17
|
"publishConfig": {
|
|
22
18
|
"access": "public",
|
|
23
19
|
"provenance": true
|
|
24
20
|
},
|
|
25
21
|
"peerDependencies": {
|
|
26
|
-
"@remnic/core": "
|
|
22
|
+
"@remnic/core": "^9.3.517"
|
|
27
23
|
},
|
|
28
24
|
"devDependencies": {
|
|
29
|
-
"@remnic/core": "workspace:*",
|
|
30
25
|
"tsup": "^8.0.0",
|
|
31
26
|
"tsx": "^4.0.0",
|
|
32
|
-
"typescript": "^5.7.0"
|
|
27
|
+
"typescript": "^5.7.0",
|
|
28
|
+
"@remnic/core": "9.3.517"
|
|
33
29
|
},
|
|
34
30
|
"license": "MIT",
|
|
35
31
|
"repository": {
|
|
@@ -37,5 +33,17 @@
|
|
|
37
33
|
"url": "https://github.com/joshuaswarren/remnic.git",
|
|
38
34
|
"directory": "packages/import-chatgpt"
|
|
39
35
|
},
|
|
40
|
-
"keywords": [
|
|
41
|
-
|
|
36
|
+
"keywords": [
|
|
37
|
+
"remnic",
|
|
38
|
+
"memory",
|
|
39
|
+
"chatgpt",
|
|
40
|
+
"openai",
|
|
41
|
+
"import"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsup src/index.ts --format esm --dts",
|
|
45
|
+
"precheck-types": "node ../../scripts/ensure-bench-build-deps.mjs",
|
|
46
|
+
"check-types": "tsc --noEmit",
|
|
47
|
+
"test": "tsx --test src/adapter.test.ts src/parser.test.ts src/transform.test.ts"
|
|
48
|
+
}
|
|
49
|
+
}
|