@remnic/import-chatgpt 9.69.27 → 9.69.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -8
- package/dist/index.js +4 -12
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -91,14 +91,6 @@ declare function collectUserTurnsFromConversation(conversation: ChatGPTConversat
|
|
|
91
91
|
createdAt?: string;
|
|
92
92
|
}>;
|
|
93
93
|
|
|
94
|
-
/**
|
|
95
|
-
* Canonical `ImporterAdapter` exposed by `@remnic/import-chatgpt`.
|
|
96
|
-
*
|
|
97
|
-
* Loaded by `remnic-cli/optional-importer.ts` via a computed-specifier
|
|
98
|
-
* dynamic import. The CLI calls `adapter.parse` → `adapter.transform` →
|
|
99
|
-
* `adapter.writeTo` through the shared `runImporter` helper in
|
|
100
|
-
* `@remnic/core`.
|
|
101
|
-
*/
|
|
102
94
|
declare const adapter: ImporterAdapter<ParsedChatGPTExport>;
|
|
103
95
|
/** Alias kept for symmetry with other @remnic/import-* packages. */
|
|
104
96
|
declare const chatgptAdapter: ImporterAdapter<ParsedChatGPTExport>;
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// openclaw-engram: Local-first memory plugin
|
|
2
2
|
|
|
3
3
|
// src/adapter.ts
|
|
4
|
-
import {
|
|
4
|
+
import { defineFileImporterAdapter } from "@remnic/core";
|
|
5
5
|
|
|
6
6
|
// src/parser.ts
|
|
7
7
|
function parseChatGPTExport(input, options = {}) {
|
|
@@ -349,25 +349,17 @@ function firstTimestamp(turns) {
|
|
|
349
349
|
}
|
|
350
350
|
|
|
351
351
|
// src/adapter.ts
|
|
352
|
-
var adapter = {
|
|
352
|
+
var adapter = defineFileImporterAdapter({
|
|
353
353
|
name: "chatgpt",
|
|
354
354
|
sourceLabel: CHATGPT_SOURCE_LABEL,
|
|
355
|
-
parse
|
|
356
|
-
return parseChatGPTExport(input, {
|
|
357
|
-
...options?.strict !== void 0 ? { strict: options.strict } : {},
|
|
358
|
-
...options?.filePath !== void 0 ? { filePath: options.filePath } : {}
|
|
359
|
-
});
|
|
360
|
-
},
|
|
355
|
+
parse: parseChatGPTExport,
|
|
361
356
|
transform(parsed, options) {
|
|
362
357
|
return transformChatGPTExport(parsed, {
|
|
363
358
|
includeConversations: options?.includeConversations === true,
|
|
364
359
|
...options?.maxMemories !== void 0 ? { maxMemories: options.maxMemories } : {}
|
|
365
360
|
});
|
|
366
|
-
},
|
|
367
|
-
async writeTo(target, memories) {
|
|
368
|
-
return defaultWriteMemoriesToOrchestrator(target, memories);
|
|
369
361
|
}
|
|
370
|
-
};
|
|
362
|
+
});
|
|
371
363
|
var chatgptAdapter = adapter;
|
|
372
364
|
export {
|
|
373
365
|
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 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":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/adapter.ts","../src/parser.ts","../src/transform.ts"],"sourcesContent":["// ChatGPT importer adapter (issue #568 slice 2). The parse/transform/writeTo\n// shell comes from `defineFileImporterAdapter` in @remnic/core (issue #2794).\nimport type { ImporterAdapter } from \"@remnic/core\";\nimport { defineFileImporterAdapter } from \"@remnic/core\";\n\nimport { parseChatGPTExport, type ParsedChatGPTExport } from \"./parser.js\";\nimport { CHATGPT_SOURCE_LABEL, transformChatGPTExport } from \"./transform.js\";\n\nexport const adapter: ImporterAdapter<ParsedChatGPTExport> =\n defineFileImporterAdapter({\n name: \"chatgpt\",\n sourceLabel: CHATGPT_SOURCE_LABEL,\n parse: parseChatGPTExport,\n transform(parsed, options) {\n return transformChatGPTExport(parsed, {\n includeConversations: options?.includeConversations === true,\n ...(options?.maxMemories !== undefined\n ? { maxMemories: options.maxMemories }\n : {}),\n });\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":"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","names":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remnic/import-chatgpt",
|
|
3
|
-
"version": "9.69.
|
|
3
|
+
"version": "9.69.28",
|
|
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",
|
|
@@ -19,13 +19,13 @@
|
|
|
19
19
|
"provenance": true
|
|
20
20
|
},
|
|
21
21
|
"peerDependencies": {
|
|
22
|
-
"@remnic/core": "^9.69.
|
|
22
|
+
"@remnic/core": "^9.69.28"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"tsup": "^8.0.0",
|
|
26
26
|
"tsx": "^4.0.0",
|
|
27
27
|
"typescript": "^5.7.0",
|
|
28
|
-
"@remnic/core": "9.69.
|
|
28
|
+
"@remnic/core": "9.69.28"
|
|
29
29
|
},
|
|
30
30
|
"license": "MIT",
|
|
31
31
|
"repository": {
|