@remnic/import-chatgpt 0.1.0

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/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @remnic/import-chatgpt
2
+
3
+ Import memory from ChatGPT data exports into [Remnic](https://github.com/joshuaswarren/remnic).
4
+
5
+ This is an optional companion package for `@remnic/cli`. Install it when you
6
+ want `remnic import --adapter chatgpt` to work.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ npm install -g @remnic/cli @remnic/import-chatgpt
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ ```bash
17
+ # Dry-run: preview what would be imported without writing.
18
+ remnic import --adapter chatgpt --file ./chatgpt-export/memory.json --dry-run
19
+
20
+ # Actual import.
21
+ remnic import --adapter chatgpt --file ./chatgpt-export/memory.json
22
+
23
+ # Also import a conversation summary per chat (default: off).
24
+ remnic import --adapter chatgpt --file ./chatgpt-export/conversations.json \
25
+ --include-conversations
26
+ ```
27
+
28
+ ## What gets imported
29
+
30
+ - **Saved memories** (the "Memory" feature inside ChatGPT) — 1:1 mapping.
31
+ Every entry becomes one Remnic memory with `sourceLabel: "chatgpt"` and
32
+ full provenance (file path, timestamp, and the original memory id).
33
+ - **Conversation summaries** — only when `--include-conversations` is set.
34
+ Each conversation is reduced to a single memory consisting of the
35
+ user-side turns concatenated (assistant replies are not imported).
36
+
37
+ ## Export shape support
38
+
39
+ Accepts all three shapes seen in ChatGPT exports from 2024-2026:
40
+
41
+ - `{ "memory": [...] }` (2026 shape)
42
+ - `{ "memories": [...] }` (2024/2025 shape)
43
+ - Top-level array of memory records (legacy)
44
+ - `conversations.json` (mapping or inline-messages form)
45
+
46
+ ## Privacy
47
+
48
+ This package only reads the export file you point at. No network calls are
49
+ made. The CLI writes to your Remnic memory store per your local config.
@@ -0,0 +1,122 @@
1
+ import { ImporterAdapter, ImportedMemory } from '@remnic/core';
2
+
3
+ /**
4
+ * A single saved memory entry. OpenAI has changed the shape several times;
5
+ * we accept the union of fields observed across 2024-2026 exports.
6
+ */
7
+ interface ChatGPTSavedMemory {
8
+ /** Stable identifier — uuid in recent exports, numeric string in older ones. */
9
+ id?: string;
10
+ /** The memory body. Required. */
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;
16
+ /** Soft-delete flag seen in 2025+ exports. When true, we skip the record. */
17
+ deleted?: boolean;
18
+ /** Whether the memory is pinned / manually curated. */
19
+ pinned?: boolean;
20
+ /** Tags the user applied to the memory. */
21
+ tags?: string[];
22
+ }
23
+ interface ChatGPTConversationMessage {
24
+ /** Message id. */
25
+ id?: string;
26
+ /** Author role: user / assistant / system / tool. */
27
+ author?: {
28
+ role?: string;
29
+ name?: string | null;
30
+ };
31
+ /** Content envelope — we read `parts[]` for text content. */
32
+ content?: {
33
+ parts?: unknown[];
34
+ content_type?: string;
35
+ } | null;
36
+ create_time?: number | string | null;
37
+ update_time?: number | string | null;
38
+ parent?: string | null;
39
+ }
40
+ interface ChatGPTConversationNode {
41
+ id: string;
42
+ message?: ChatGPTConversationMessage | null;
43
+ parent?: string | null;
44
+ children?: string[];
45
+ }
46
+ interface ChatGPTConversation {
47
+ id?: string;
48
+ title?: string;
49
+ create_time?: number | string | null;
50
+ update_time?: number | string | null;
51
+ /** Messages by id. Present in 2023+ conversation exports. */
52
+ mapping?: Record<string, ChatGPTConversationNode> | null;
53
+ /** Some older exports inline messages as an array instead. */
54
+ messages?: ChatGPTConversationMessage[];
55
+ /** Root message id (for graph traversal). */
56
+ current_node?: string | null;
57
+ }
58
+ /**
59
+ * Unified parsed shape we pass into `transform()`. Holds both saved memories
60
+ * and (optionally) conversations; either may be empty.
61
+ */
62
+ interface ParsedChatGPTExport {
63
+ savedMemories: ChatGPTSavedMemory[];
64
+ conversations: ChatGPTConversation[];
65
+ /** Source path the export came from (for provenance). */
66
+ filePath?: string;
67
+ }
68
+ interface ChatGPTParseOptions {
69
+ /** When true, throw on any validation failure instead of skipping. */
70
+ strict?: boolean;
71
+ /** File path of the source — preserved for provenance in transformed memories. */
72
+ filePath?: string;
73
+ }
74
+ /**
75
+ * Parse a raw export payload. Accepts either:
76
+ * - A JSON string (common CLI path — the CLI reads the file contents).
77
+ * - An already-parsed object or array (`JSON.parse` result).
78
+ *
79
+ * Returns the unified `ParsedChatGPTExport`. Non-export payloads throw a
80
+ * descriptive error; silently ignoring them would mask user errors
81
+ * (CLAUDE.md rule 51).
82
+ */
83
+ declare function parseChatGPTExport(input: unknown, options?: ChatGPTParseOptions): ParsedChatGPTExport;
84
+ /**
85
+ * Walk a conversation's message graph and return only the user-authored text
86
+ * turns in chronological order. Exported so the transform layer can build
87
+ * conversation summaries from the same graph.
88
+ */
89
+ declare function collectUserTurnsFromConversation(conversation: ChatGPTConversation): Array<{
90
+ content: string;
91
+ createdAt?: string;
92
+ }>;
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
+ declare const adapter: ImporterAdapter<ParsedChatGPTExport>;
103
+ /** Alias kept for symmetry with other @remnic/import-* packages. */
104
+ declare const chatgptAdapter: ImporterAdapter<ParsedChatGPTExport>;
105
+
106
+ declare const CHATGPT_SOURCE_LABEL = "chatgpt";
107
+ interface ChatGPTTransformOptions {
108
+ /** When true, produce conversation-summary memories in addition to saved memories. */
109
+ includeConversations?: boolean;
110
+ /** Optional cap on total memories emitted — primarily for tests. */
111
+ maxMemories?: number;
112
+ /** Maximum characters for a conversation summary memory. */
113
+ maxConversationSummaryChars?: number;
114
+ }
115
+ /**
116
+ * Transform a parsed ChatGPT export into `ImportedMemory[]`.
117
+ * Saved memories are emitted first (in parse order), then conversation
118
+ * summaries when opted in.
119
+ */
120
+ declare function transformChatGPTExport(parsed: ParsedChatGPTExport, options?: ChatGPTTransformOptions): ImportedMemory[];
121
+
122
+ export { CHATGPT_SOURCE_LABEL, type ChatGPTConversation, type ChatGPTConversationMessage, type ChatGPTConversationNode, type ChatGPTParseOptions, type ChatGPTSavedMemory, type ChatGPTTransformOptions, type ParsedChatGPTExport, adapter, chatgptAdapter, collectUserTurnsFromConversation, parseChatGPTExport, transformChatGPTExport };
package/dist/index.js ADDED
@@ -0,0 +1,374 @@
1
+ // openclaw-engram: Local-first memory plugin
2
+
3
+ // src/adapter.ts
4
+ import { defaultWriteMemoriesToOrchestrator } from "@remnic/core";
5
+
6
+ // src/parser.ts
7
+ function parseChatGPTExport(input, options = {}) {
8
+ if (input === void 0 || input === null) {
9
+ throw new Error(
10
+ "The 'chatgpt' importer requires a file. Pass `--file <path>` pointing at your ChatGPT data-export `memory.json` or `conversations.json`."
11
+ );
12
+ }
13
+ const raw = coerceJson(input);
14
+ const result = {
15
+ savedMemories: [],
16
+ conversations: [],
17
+ ...options.filePath !== void 0 ? { filePath: options.filePath } : {}
18
+ };
19
+ if (Array.isArray(raw)) {
20
+ if (raw.length === 0) return result;
21
+ let classification;
22
+ for (const entry of raw) {
23
+ if (looksLikeConversation(entry)) {
24
+ classification = "conversation";
25
+ break;
26
+ }
27
+ if (looksLikeSavedMemory(entry)) {
28
+ classification = "memory";
29
+ break;
30
+ }
31
+ }
32
+ if (classification === "conversation") {
33
+ for (const entry of raw) {
34
+ if (looksLikeConversation(entry)) {
35
+ result.conversations.push(entry);
36
+ } else if (options.strict) {
37
+ throw new Error("Non-conversation entry in conversations array");
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+ if (classification === "memory") {
43
+ for (const entry of raw) {
44
+ const mem = normalizeSavedMemory(entry, options.strict);
45
+ if (mem) result.savedMemories.push(mem);
46
+ }
47
+ return result;
48
+ }
49
+ if (options.strict) {
50
+ throw new Error(
51
+ "Unknown ChatGPT export array shape (neither memories nor conversations)."
52
+ );
53
+ }
54
+ return result;
55
+ }
56
+ if (raw && typeof raw === "object") {
57
+ const obj = raw;
58
+ let sawKnownSection = false;
59
+ for (const key of ["memory", "memories"]) {
60
+ const v = obj[key];
61
+ if (Array.isArray(v)) {
62
+ sawKnownSection = true;
63
+ for (const entry of v) {
64
+ const mem = normalizeSavedMemory(entry, options.strict);
65
+ if (mem) result.savedMemories.push(mem);
66
+ }
67
+ }
68
+ }
69
+ if (obj.user && typeof obj.user === "object") {
70
+ const userMem = obj.user.memory;
71
+ if (Array.isArray(userMem)) {
72
+ sawKnownSection = true;
73
+ for (const entry of userMem) {
74
+ const mem = normalizeSavedMemory(entry, options.strict);
75
+ if (mem) result.savedMemories.push(mem);
76
+ }
77
+ }
78
+ }
79
+ const convs = obj.conversations;
80
+ if (Array.isArray(convs)) {
81
+ sawKnownSection = true;
82
+ for (const entry of convs) {
83
+ if (looksLikeConversation(entry)) {
84
+ result.conversations.push(entry);
85
+ } else if (options.strict) {
86
+ throw new Error("Non-conversation entry in conversations array");
87
+ }
88
+ }
89
+ }
90
+ if (!sawKnownSection && options.strict) {
91
+ throw new Error(
92
+ "Unknown ChatGPT export object shape: expected one of 'memory', 'memories', 'user.memory', or 'conversations' keys."
93
+ );
94
+ }
95
+ return result;
96
+ }
97
+ throw new Error(
98
+ "ChatGPT export must be a JSON array or object; received " + typeof raw
99
+ );
100
+ }
101
+ function coerceJson(input) {
102
+ if (typeof input === "string") {
103
+ try {
104
+ return JSON.parse(input);
105
+ } catch (err) {
106
+ throw new Error(
107
+ `ChatGPT export is not valid JSON: ${err instanceof Error ? err.message : String(err)}`
108
+ );
109
+ }
110
+ }
111
+ return input;
112
+ }
113
+ function looksLikeSavedMemory(value) {
114
+ if (!value || typeof value !== "object") return false;
115
+ const v = value;
116
+ if (typeof v.content === "string" && v.content.length > 0) return true;
117
+ if (typeof v.text === "string" && v.text.length > 0) return true;
118
+ return false;
119
+ }
120
+ function looksLikeConversation(value) {
121
+ if (!value || typeof value !== "object") return false;
122
+ const v = value;
123
+ if (v.mapping && typeof v.mapping === "object") return true;
124
+ if (Array.isArray(v.messages)) return true;
125
+ return false;
126
+ }
127
+ function normalizeSavedMemory(value, strict) {
128
+ if (!value || typeof value !== "object") {
129
+ if (strict) throw new Error("Saved memory entry must be an object");
130
+ return void 0;
131
+ }
132
+ const v = value;
133
+ const content = typeof v.content === "string" ? v.content : typeof v.text === "string" ? v.text : void 0;
134
+ if (!content || content.trim().length === 0) {
135
+ if (strict) throw new Error("Saved memory entry missing content");
136
+ return void 0;
137
+ }
138
+ if (v.deleted === true) {
139
+ return void 0;
140
+ }
141
+ const normalized = {
142
+ content: content.trim(),
143
+ ...typeof v.id === "string" ? { id: v.id } : {},
144
+ ...typeof v.created_at === "string" ? { created_at: v.created_at } : {},
145
+ ...typeof v.updated_at === "string" ? { updated_at: v.updated_at } : {},
146
+ ...v.pinned === true ? { pinned: true } : {},
147
+ ...Array.isArray(v.tags) ? { tags: v.tags.filter((t) => typeof t === "string") } : {}
148
+ };
149
+ return normalized;
150
+ }
151
+ function collectUserTurnsFromConversation(conversation) {
152
+ const collected = [];
153
+ if (Array.isArray(conversation.messages) && conversation.messages.length > 0) {
154
+ for (const msg of conversation.messages) {
155
+ const text = extractMessageText(msg);
156
+ if (text) {
157
+ collected.push({
158
+ content: text,
159
+ ...asIsoString(msg.create_time ?? msg.update_time) !== void 0 ? { createdAt: asIsoString(msg.create_time ?? msg.update_time) } : {}
160
+ });
161
+ }
162
+ }
163
+ return collected;
164
+ }
165
+ if (conversation.mapping && typeof conversation.mapping === "object") {
166
+ const mapping = conversation.mapping;
167
+ const chainNodes = followCurrentNodeChain(mapping, conversation.current_node);
168
+ const ordered = chainNodes.length > 0 ? chainNodes : [...Object.values(mapping)].sort((a, b) => {
169
+ const delta = toNumericTime(a.message?.create_time) - toNumericTime(b.message?.create_time);
170
+ if (delta !== 0) return delta;
171
+ const aid = typeof a.id === "string" ? a.id : "";
172
+ const bid = typeof b.id === "string" ? b.id : "";
173
+ if (aid < bid) return -1;
174
+ if (aid > bid) return 1;
175
+ return 0;
176
+ });
177
+ for (const node of ordered) {
178
+ const msg = node.message;
179
+ if (!msg) continue;
180
+ if (msg.author?.role !== "user") continue;
181
+ const text = extractMessageText(msg);
182
+ if (text) {
183
+ collected.push({
184
+ content: text,
185
+ ...asIsoString(msg.create_time ?? msg.update_time) !== void 0 ? { createdAt: asIsoString(msg.create_time ?? msg.update_time) } : {}
186
+ });
187
+ }
188
+ }
189
+ }
190
+ return collected;
191
+ }
192
+ function followCurrentNodeChain(mapping, currentNode) {
193
+ if (typeof currentNode !== "string" || currentNode.length === 0) return [];
194
+ const visited = /* @__PURE__ */ new Set();
195
+ const chain = [];
196
+ let cursor = currentNode;
197
+ while (cursor) {
198
+ if (visited.has(cursor)) {
199
+ return [];
200
+ }
201
+ visited.add(cursor);
202
+ const node = mapping[cursor];
203
+ if (!node) {
204
+ return [];
205
+ }
206
+ chain.push(node);
207
+ cursor = node.parent ?? null;
208
+ }
209
+ return chain.reverse();
210
+ }
211
+ function extractMessageText(msg) {
212
+ if (!msg) return void 0;
213
+ if (msg.author?.role !== "user") {
214
+ return void 0;
215
+ }
216
+ const parts = msg.content?.parts;
217
+ if (!Array.isArray(parts)) return void 0;
218
+ const text = parts.filter((p) => typeof p === "string" && p.length > 0).join("\n").trim();
219
+ return text.length > 0 ? text : void 0;
220
+ }
221
+ function toNumericTime(value) {
222
+ if (typeof value === "number" && Number.isFinite(value)) return value;
223
+ if (typeof value === "string") {
224
+ const n = Number(value);
225
+ if (Number.isFinite(n)) return n;
226
+ const parsed = Date.parse(value);
227
+ if (!Number.isNaN(parsed)) return parsed / 1e3;
228
+ }
229
+ return 0;
230
+ }
231
+ function asIsoString(value) {
232
+ if (value === null || value === void 0) return void 0;
233
+ if (typeof value === "string") {
234
+ if (/\d{4}-\d{2}-\d{2}T/.test(value)) return value;
235
+ const asNum = Number(value);
236
+ if (Number.isFinite(asNum)) return epochToIso(asNum);
237
+ return void 0;
238
+ }
239
+ if (Number.isFinite(value)) {
240
+ return epochToIso(value);
241
+ }
242
+ return void 0;
243
+ }
244
+ function epochToIso(value) {
245
+ const ms = Math.abs(value) > 1e12 ? value : value * 1e3;
246
+ const MAX_SAFE_MS = 864e13;
247
+ if (!Number.isFinite(ms) || Math.abs(ms) > MAX_SAFE_MS) return void 0;
248
+ const d = new Date(ms);
249
+ if (Number.isNaN(d.getTime())) return void 0;
250
+ try {
251
+ return d.toISOString();
252
+ } catch {
253
+ return void 0;
254
+ }
255
+ }
256
+
257
+ // src/transform.ts
258
+ var CHATGPT_SOURCE_LABEL = "chatgpt";
259
+ var DEFAULT_CONVERSATION_SUMMARY_CHARS = 2e3;
260
+ function transformChatGPTExport(parsed, options = {}) {
261
+ const out = [];
262
+ const cap = options.maxMemories;
263
+ for (const entry of parsed.savedMemories) {
264
+ if (cap !== void 0 && out.length >= cap) return out;
265
+ const memory = savedMemoryToImported(entry, parsed.filePath);
266
+ if (memory) out.push(memory);
267
+ }
268
+ if (options.includeConversations) {
269
+ const maxSummaryChars = options.maxConversationSummaryChars ?? DEFAULT_CONVERSATION_SUMMARY_CHARS;
270
+ for (const conversation of parsed.conversations) {
271
+ if (cap !== void 0 && out.length >= cap) return out;
272
+ const summary = conversationToSummary(
273
+ conversation,
274
+ parsed.filePath,
275
+ maxSummaryChars
276
+ );
277
+ if (summary) out.push(summary);
278
+ }
279
+ }
280
+ return out;
281
+ }
282
+ function savedMemoryToImported(entry, filePath) {
283
+ const content = entry.content.trim();
284
+ if (content.length === 0) return void 0;
285
+ const sourceTimestamp = entry.updated_at ?? entry.created_at;
286
+ return {
287
+ content,
288
+ sourceLabel: CHATGPT_SOURCE_LABEL,
289
+ ...entry.id !== void 0 ? { sourceId: entry.id } : {},
290
+ ...sourceTimestamp !== void 0 ? { sourceTimestamp } : {},
291
+ ...filePath !== void 0 ? { importedFromPath: filePath } : {},
292
+ metadata: buildMetadata(entry)
293
+ };
294
+ }
295
+ function buildMetadata(entry) {
296
+ const meta = { kind: "saved_memory" };
297
+ if (entry.pinned === true) meta.pinned = true;
298
+ if (Array.isArray(entry.tags) && entry.tags.length > 0) {
299
+ meta.tags = [...entry.tags];
300
+ }
301
+ return meta;
302
+ }
303
+ function conversationToSummary(conversation, filePath, maxChars) {
304
+ const userTurns = collectUserTurnsFromConversation(conversation);
305
+ if (userTurns.length === 0) return void 0;
306
+ const title = typeof conversation.title === "string" ? conversation.title.trim() : "";
307
+ const titleLine = title.length > 0 ? `Conversation: ${title}
308
+
309
+ ` : "";
310
+ const body = userTurns.map((t) => `- ${t.content}`).join("\n");
311
+ let content = titleLine + body;
312
+ if (content.length > maxChars) {
313
+ const suffix = "...";
314
+ if (titleLine.length + suffix.length >= maxChars) {
315
+ content = titleLine.slice(0, Math.max(0, maxChars - suffix.length)) + suffix;
316
+ } else {
317
+ const remaining = maxChars - titleLine.length - suffix.length;
318
+ const bodyTruncated = body.slice(0, Math.max(0, remaining));
319
+ content = titleLine + bodyTruncated + suffix;
320
+ }
321
+ }
322
+ const sourceTimestamp = firstTimestamp(userTurns);
323
+ return {
324
+ content,
325
+ sourceLabel: CHATGPT_SOURCE_LABEL,
326
+ ...typeof conversation.id === "string" ? { sourceId: conversation.id } : {},
327
+ ...sourceTimestamp !== void 0 ? { sourceTimestamp } : {},
328
+ ...filePath !== void 0 ? { importedFromPath: filePath } : {},
329
+ metadata: {
330
+ kind: "conversation_summary",
331
+ ...title.length > 0 ? { title } : {},
332
+ userTurns: userTurns.length
333
+ }
334
+ };
335
+ }
336
+ function firstTimestamp(turns) {
337
+ for (const turn of turns) {
338
+ if (typeof turn.createdAt === "string" && turn.createdAt.length > 0) {
339
+ return turn.createdAt;
340
+ }
341
+ }
342
+ return void 0;
343
+ }
344
+
345
+ // src/adapter.ts
346
+ var adapter = {
347
+ name: "chatgpt",
348
+ sourceLabel: CHATGPT_SOURCE_LABEL,
349
+ parse(input, options) {
350
+ return parseChatGPTExport(input, {
351
+ ...options?.strict !== void 0 ? { strict: options.strict } : {},
352
+ ...options?.filePath !== void 0 ? { filePath: options.filePath } : {}
353
+ });
354
+ },
355
+ transform(parsed, options) {
356
+ return transformChatGPTExport(parsed, {
357
+ includeConversations: options?.includeConversations === true,
358
+ ...options?.maxMemories !== void 0 ? { maxMemories: options.maxMemories } : {}
359
+ });
360
+ },
361
+ async writeTo(target, memories) {
362
+ return defaultWriteMemoriesToOrchestrator(target, memories);
363
+ }
364
+ };
365
+ var chatgptAdapter = adapter;
366
+ export {
367
+ CHATGPT_SOURCE_LABEL,
368
+ adapter,
369
+ chatgptAdapter,
370
+ collectUserTurnsFromConversation,
371
+ parseChatGPTExport,
372
+ transformChatGPTExport
373
+ };
374
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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":[]}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@remnic/import-chatgpt",
3
+ "version": "0.1.0",
4
+ "description": "Import memory and conversation summaries from ChatGPT data exports into Remnic (issue #568)",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": ["dist"],
15
+ "scripts": {
16
+ "build": "tsup src/index.ts --format esm --dts",
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
+ },
21
+ "publishConfig": {
22
+ "access": "public",
23
+ "provenance": true
24
+ },
25
+ "peerDependencies": {
26
+ "@remnic/core": "workspace:^"
27
+ },
28
+ "devDependencies": {
29
+ "@remnic/core": "workspace:*",
30
+ "tsup": "^8.0.0",
31
+ "tsx": "^4.0.0",
32
+ "typescript": "^5.7.0"
33
+ },
34
+ "license": "MIT",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "https://github.com/joshuaswarren/remnic.git",
38
+ "directory": "packages/import-chatgpt"
39
+ },
40
+ "keywords": ["remnic", "memory", "chatgpt", "openai", "import"]
41
+ }