conare 0.0.1

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.
Files changed (2) hide show
  1. package/dist/index.js +1094 -0
  2. package/package.json +22 -0
package/dist/index.js ADDED
@@ -0,0 +1,1094 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // src/detect.ts
6
+ import { existsSync, readdirSync } from "node:fs";
7
+ import { join } from "node:path";
8
+ import { homedir, platform } from "node:os";
9
+ function countJsonlFiles(dir) {
10
+ let count = 0;
11
+ try {
12
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
13
+ if (entry.isDirectory()) {
14
+ count += countJsonlFiles(join(dir, entry.name));
15
+ } else if (entry.name.endsWith(".jsonl")) {
16
+ count++;
17
+ }
18
+ }
19
+ } catch {}
20
+ return count;
21
+ }
22
+ function detect() {
23
+ const home = homedir();
24
+ const tools = [];
25
+ const claudeDir = join(home, ".claude", "projects");
26
+ if (existsSync(claudeDir)) {
27
+ const sessionCount = countJsonlFiles(claudeDir);
28
+ tools.push({
29
+ name: "Claude Code",
30
+ available: sessionCount > 0,
31
+ path: claudeDir,
32
+ sessionCount
33
+ });
34
+ } else {
35
+ tools.push({ name: "Claude Code", available: false, path: claudeDir, sessionCount: 0 });
36
+ }
37
+ const codexHistory = join(home, ".codex", "history.jsonl");
38
+ const codexSessions = join(home, ".codex", "sessions");
39
+ if (existsSync(codexHistory) || existsSync(codexSessions)) {
40
+ let sessionCount = 0;
41
+ if (existsSync(codexHistory)) {
42
+ try {
43
+ const { readFileSync } = __require("node:fs");
44
+ const lines = readFileSync(codexHistory, "utf-8").split(`
45
+ `).filter(Boolean);
46
+ const sessions = new Set(lines.map((l) => {
47
+ try {
48
+ return JSON.parse(l).session_id;
49
+ } catch {
50
+ return null;
51
+ }
52
+ }));
53
+ sessions.delete(null);
54
+ sessionCount = sessions.size;
55
+ } catch {}
56
+ }
57
+ tools.push({
58
+ name: "Codex",
59
+ available: true,
60
+ path: existsSync(codexHistory) ? codexHistory : codexSessions,
61
+ sessionCount
62
+ });
63
+ } else {
64
+ tools.push({ name: "Codex", available: false, path: codexHistory, sessionCount: 0 });
65
+ }
66
+ const os = platform();
67
+ let cursorDbPath;
68
+ if (os === "darwin") {
69
+ cursorDbPath = join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
70
+ } else if (os === "win32") {
71
+ cursorDbPath = join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb");
72
+ } else {
73
+ cursorDbPath = join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
74
+ }
75
+ tools.push({
76
+ name: "Cursor",
77
+ available: existsSync(cursorDbPath),
78
+ path: cursorDbPath,
79
+ sessionCount: 0
80
+ });
81
+ return tools;
82
+ }
83
+
84
+ // src/ingest/claude.ts
85
+ import { readdirSync as readdirSync2, readFileSync as readFileSync2 } from "node:fs";
86
+ import { join as join3, basename } from "node:path";
87
+ import { homedir as homedir3 } from "node:os";
88
+
89
+ // src/ingest/shared.ts
90
+ import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync } from "node:fs";
91
+ import { join as join2 } from "node:path";
92
+ import { homedir as homedir2 } from "node:os";
93
+ var MANIFEST_PATH = join2(homedir2(), ".conare", "ingested.json");
94
+ function cleanText(raw) {
95
+ let text = raw;
96
+ text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
97
+ text = text.replace(/<attached-context[\s\S]*?<\/attached-context>/g, "");
98
+ return text.trim();
99
+ }
100
+ function getIngested() {
101
+ try {
102
+ if (existsSync2(MANIFEST_PATH)) {
103
+ return JSON.parse(readFileSync(MANIFEST_PATH, "utf-8"));
104
+ }
105
+ } catch {}
106
+ return {};
107
+ }
108
+ function markIngested(source, sessionIds) {
109
+ const manifest = getIngested();
110
+ const existing = new Set(manifest[source] || []);
111
+ for (const id of sessionIds)
112
+ existing.add(id);
113
+ manifest[source] = [...existing];
114
+ const dir = join2(homedir2(), ".conare");
115
+ if (!existsSync2(dir))
116
+ mkdirSync(dir, { recursive: true });
117
+ writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
118
+ }
119
+ function isIngested(source, sessionId) {
120
+ const manifest = getIngested();
121
+ return (manifest[source] || []).includes(sessionId);
122
+ }
123
+ function clearIngested(source) {
124
+ const manifest = getIngested();
125
+ if (source) {
126
+ delete manifest[source];
127
+ } else {
128
+ for (const key of Object.keys(manifest))
129
+ delete manifest[key];
130
+ }
131
+ const dir = join2(homedir2(), ".conare");
132
+ if (!existsSync2(dir))
133
+ mkdirSync(dir, { recursive: true });
134
+ writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
135
+ }
136
+
137
+ // src/ingest/claude.ts
138
+ var MAX_CONTENT = 48000;
139
+ var MIN_TURN_LEN = 50;
140
+ function extractText(content) {
141
+ if (typeof content === "string")
142
+ return content;
143
+ if (!Array.isArray(content))
144
+ return "";
145
+ return content.filter((b) => b.type === "text" && b.text).map((b) => b.text).join(`
146
+ `);
147
+ }
148
+ function parseSession(lines) {
149
+ const messages = [];
150
+ let date = null;
151
+ for (const line of lines) {
152
+ if (!line.trim())
153
+ continue;
154
+ let obj;
155
+ try {
156
+ obj = JSON.parse(line);
157
+ } catch {
158
+ continue;
159
+ }
160
+ if (!date && obj.timestamp)
161
+ date = obj.timestamp.slice(0, 10);
162
+ if (obj.type === "user" || obj.type === "assistant") {
163
+ const text = cleanText(extractText(obj.message?.content));
164
+ if (text.length >= MIN_TURN_LEN) {
165
+ messages.push({ role: obj.type, text });
166
+ }
167
+ }
168
+ }
169
+ const turns = [];
170
+ for (let i = 0;i < messages.length - 1; i++) {
171
+ if (messages[i].role === "user" && messages[i + 1].role === "assistant") {
172
+ turns.push({ user: messages[i].text, assistant: messages[i + 1].text });
173
+ i++;
174
+ }
175
+ }
176
+ return { turns, date };
177
+ }
178
+ function ingestClaude() {
179
+ const projectsDir = join3(homedir3(), ".claude", "projects");
180
+ const memories = [];
181
+ const sessionIds = [];
182
+ let skipped = 0;
183
+ let projectDirs;
184
+ try {
185
+ projectDirs = readdirSync2(projectsDir);
186
+ } catch {
187
+ return { memories, sessionIds, skipped };
188
+ }
189
+ for (const projDir of projectDirs) {
190
+ const projPath = join3(projectsDir, projDir);
191
+ const project = projDir.replace(/^-Users-[^-]+-/, "").replace(/-/g, "/") || projDir;
192
+ let files;
193
+ try {
194
+ files = readdirSync2(projPath).filter((f) => f.endsWith(".jsonl"));
195
+ } catch {
196
+ continue;
197
+ }
198
+ for (const file of files) {
199
+ const sessionId = basename(file, ".jsonl");
200
+ if (isIngested("claude", sessionId)) {
201
+ skipped++;
202
+ continue;
203
+ }
204
+ const raw = readFileSync2(join3(projPath, file), "utf-8");
205
+ const { turns, date } = parseSession(raw.split(`
206
+ `));
207
+ if (turns.length === 0) {
208
+ skipped++;
209
+ continue;
210
+ }
211
+ const header = `# Chat: ${project}${date ? ` | ${date}` : ""}`;
212
+ const body = turns.map((t) => {
213
+ const q = t.user.length > 300 ? t.user.slice(0, 300) + "..." : t.user;
214
+ return `## Q: ${q}
215
+
216
+ ${t.assistant}`;
217
+ }).join(`
218
+
219
+ ---
220
+
221
+ `);
222
+ let content = `${header}
223
+
224
+ ${body}`;
225
+ if (content.length > MAX_CONTENT)
226
+ content = content.slice(0, MAX_CONTENT) + `
227
+
228
+ [truncated]`;
229
+ memories.push({
230
+ content,
231
+ containerTag: "claude-chats",
232
+ metadata: { source: "claude-code", sessionId, project, date: date || "unknown" }
233
+ });
234
+ sessionIds.push(sessionId);
235
+ }
236
+ }
237
+ return { memories, sessionIds, skipped };
238
+ }
239
+
240
+ // src/ingest/codex.ts
241
+ import { existsSync as existsSync3, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "node:fs";
242
+ import { join as join4, basename as basename2 } from "node:path";
243
+ import { homedir as homedir4 } from "node:os";
244
+ var MAX_CONTENT2 = 48000;
245
+ function ingestCodex() {
246
+ const memories = [];
247
+ const sessionIds = [];
248
+ let skipped = 0;
249
+ const historyPath = join4(homedir4(), ".codex", "history.jsonl");
250
+ if (existsSync3(historyPath)) {
251
+ try {
252
+ const lines = readFileSync3(historyPath, "utf-8").split(`
253
+ `).filter(Boolean);
254
+ const sessions = new Map;
255
+ for (const line of lines) {
256
+ try {
257
+ const obj = JSON.parse(line);
258
+ if (!obj.session_id || !obj.text)
259
+ continue;
260
+ if (!sessions.has(obj.session_id))
261
+ sessions.set(obj.session_id, []);
262
+ sessions.get(obj.session_id).push({ ts: obj.ts, text: obj.text });
263
+ } catch {
264
+ continue;
265
+ }
266
+ }
267
+ for (const [sessionId, entries] of sessions) {
268
+ if (isIngested("codex", sessionId)) {
269
+ skipped++;
270
+ continue;
271
+ }
272
+ entries.sort((a, b) => a.ts - b.ts);
273
+ const date = new Date(entries[0].ts * 1000).toISOString().slice(0, 10);
274
+ const body = entries.map((e) => {
275
+ const text = cleanText(e.text);
276
+ return text.length > 300 ? text.slice(0, 300) + "..." : text;
277
+ }).filter(Boolean).join(`
278
+
279
+ ---
280
+
281
+ `);
282
+ let content = `# Codex Chat | ${date}
283
+
284
+ ${body}`;
285
+ if (content.length > MAX_CONTENT2)
286
+ content = content.slice(0, MAX_CONTENT2) + `
287
+
288
+ [truncated]`;
289
+ if (content.length < 100) {
290
+ skipped++;
291
+ continue;
292
+ }
293
+ memories.push({
294
+ content,
295
+ containerTag: "codex-chats",
296
+ metadata: { source: "codex", sessionId, date }
297
+ });
298
+ sessionIds.push(sessionId);
299
+ }
300
+ } catch {}
301
+ }
302
+ const sessionsDir = join4(homedir4(), ".codex", "sessions");
303
+ if (existsSync3(sessionsDir)) {
304
+ try {
305
+ walkCodexSessions(sessionsDir, memories, sessionIds, skipped);
306
+ } catch {}
307
+ }
308
+ return { memories, sessionIds, skipped };
309
+ }
310
+ function walkCodexSessions(dir, memories, sessionIds, skipped) {
311
+ try {
312
+ for (const entry of readdirSync3(dir, { withFileTypes: true })) {
313
+ if (entry.isDirectory()) {
314
+ walkCodexSessions(join4(dir, entry.name), memories, sessionIds, skipped);
315
+ } else if (entry.name.endsWith(".jsonl")) {
316
+ const sessionId = basename2(entry.name, ".jsonl");
317
+ if (isIngested("codex", sessionId) || sessionIds.includes(sessionId))
318
+ continue;
319
+ try {
320
+ const lines = readFileSync3(join4(dir, entry.name), "utf-8").split(`
321
+ `).filter(Boolean);
322
+ const turns = [];
323
+ let date = null;
324
+ for (const line of lines) {
325
+ try {
326
+ const obj = JSON.parse(line);
327
+ if (!date && obj.timestamp)
328
+ date = obj.timestamp.slice(0, 10);
329
+ if (obj.type === "response_item" && obj.payload?.role === "user") {
330
+ const content2 = obj.payload.content;
331
+ if (Array.isArray(content2)) {
332
+ for (const block of content2) {
333
+ if (block.type === "input_text" && block.text) {
334
+ const text = cleanText(block.text);
335
+ if (text.length > 50)
336
+ turns.push(text);
337
+ }
338
+ }
339
+ }
340
+ }
341
+ } catch {
342
+ continue;
343
+ }
344
+ }
345
+ if (turns.length === 0)
346
+ continue;
347
+ const body = turns.map((t) => t.length > 500 ? t.slice(0, 500) + "..." : t).join(`
348
+
349
+ ---
350
+
351
+ `);
352
+ let content = `# Codex Session | ${date || "unknown"}
353
+
354
+ ${body}`;
355
+ if (content.length > MAX_CONTENT2)
356
+ content = content.slice(0, MAX_CONTENT2) + `
357
+
358
+ [truncated]`;
359
+ memories.push({
360
+ content,
361
+ containerTag: "codex-chats",
362
+ metadata: { source: "codex-session", sessionId, date: date || "unknown" }
363
+ });
364
+ sessionIds.push(sessionId);
365
+ } catch {}
366
+ }
367
+ }
368
+ } catch {}
369
+ }
370
+
371
+ // src/ingest/cursor.ts
372
+ import { readFileSync as readFileSync4, statSync } from "node:fs";
373
+ import { join as join5 } from "node:path";
374
+ import { createRequire as createRequire2 } from "node:module";
375
+ var MAX_CONTENT3 = 48000;
376
+ var MAX_DB_SIZE = 2 * 1024 * 1024 * 1024;
377
+ var WARN_DB_SIZE = 500 * 1024 * 1024;
378
+ var MIN_TURN_LEN2 = 50;
379
+ function loadSqlJs(wasmDir) {
380
+ try {
381
+ if (wasmDir) {
382
+ const require2 = createRequire2(join5(wasmDir, "sql.js", "package.json"));
383
+ return require2("sql.js");
384
+ } else {
385
+ const require2 = createRequire2(import.meta.url);
386
+ return require2("sql.js");
387
+ }
388
+ } catch {
389
+ return null;
390
+ }
391
+ }
392
+ function openDb(initSqlJs, dbPath, wasmDir) {
393
+ const locateOpts = {};
394
+ if (wasmDir) {
395
+ locateOpts.locateFile = (file) => join5(wasmDir, "sql.js", "dist", file);
396
+ }
397
+ return initSqlJs(locateOpts).then((SQL) => {
398
+ const buffer = readFileSync4(dbPath);
399
+ return new SQL.Database(buffer);
400
+ });
401
+ }
402
+ function extractTurns(db, composerId, bubbleHeaders) {
403
+ const turns = [];
404
+ let pendingUser = null;
405
+ for (const header of bubbleHeaders) {
406
+ let text = "";
407
+ try {
408
+ const result = db.exec(`SELECT value FROM cursorDiskKV WHERE key = 'bubbleId:${composerId}:${header.bubbleId}'`);
409
+ if (result.length > 0) {
410
+ const bubble = JSON.parse(result[0].values[0][0]);
411
+ text = cleanText(bubble.text || "");
412
+ }
413
+ } catch {
414
+ continue;
415
+ }
416
+ if (text.length < MIN_TURN_LEN2)
417
+ continue;
418
+ if (header.type === 1) {
419
+ pendingUser = text;
420
+ } else if (header.type === 2 && pendingUser) {
421
+ turns.push({ user: pendingUser, assistant: text });
422
+ pendingUser = null;
423
+ }
424
+ }
425
+ return turns;
426
+ }
427
+ async function ingestCursor(dbPath, wasmDir) {
428
+ const memories = [];
429
+ const sessionIds = [];
430
+ let skipped = 0;
431
+ let fileSize;
432
+ try {
433
+ fileSize = statSync(dbPath).size;
434
+ } catch {
435
+ console.log(" Skipping Cursor: database not accessible");
436
+ return { memories, sessionIds, skipped };
437
+ }
438
+ if (fileSize > MAX_DB_SIZE) {
439
+ console.log(` Skipping Cursor: database too large (${(fileSize / 1024 / 1024 / 1024).toFixed(1)}GB)`);
440
+ return { memories, sessionIds, skipped };
441
+ }
442
+ if (fileSize > WARN_DB_SIZE) {
443
+ console.log(` Warning: large database (${(fileSize / 1024 / 1024).toFixed(0)}MB), loading into memory...`);
444
+ }
445
+ const initSqlJs = loadSqlJs(wasmDir);
446
+ if (!initSqlJs) {
447
+ console.log(" Skipping Cursor: sql.js not available");
448
+ return { memories, sessionIds, skipped };
449
+ }
450
+ let db;
451
+ try {
452
+ db = await openDb(initSqlJs, dbPath, wasmDir);
453
+ } catch (e) {
454
+ console.log(` Skipping Cursor: cannot open database (${e.message})`);
455
+ return { memories, sessionIds, skipped };
456
+ }
457
+ try {
458
+ let rows = [];
459
+ try {
460
+ const results = db.exec("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
461
+ if (results.length > 0)
462
+ rows = results[0].values;
463
+ } catch {}
464
+ for (const [key, value] of rows) {
465
+ const composerId = key.replace("composerData:", "");
466
+ if (isIngested("cursor", composerId)) {
467
+ skipped++;
468
+ continue;
469
+ }
470
+ let parsed;
471
+ try {
472
+ parsed = JSON.parse(value);
473
+ if (!parsed || typeof parsed !== "object") {
474
+ skipped++;
475
+ continue;
476
+ }
477
+ } catch {
478
+ skipped++;
479
+ continue;
480
+ }
481
+ const bubbleHeaders = parsed.fullConversationHeadersOnly;
482
+ if (!Array.isArray(bubbleHeaders) || bubbleHeaders.length === 0) {
483
+ skipped++;
484
+ continue;
485
+ }
486
+ const turns = extractTurns(db, composerId, bubbleHeaders);
487
+ if (turns.length === 0) {
488
+ skipped++;
489
+ continue;
490
+ }
491
+ const sessionName = parsed.name || "Cursor Chat";
492
+ const date = parsed.createdAt ? new Date(parsed.createdAt).toISOString().slice(0, 10) : "unknown";
493
+ const header = `# ${sessionName} | ${date}`;
494
+ const body = turns.map((t) => {
495
+ const q = t.user.length > 300 ? t.user.slice(0, 300) + "..." : t.user;
496
+ return `## Q: ${q}
497
+
498
+ ${t.assistant}`;
499
+ }).join(`
500
+
501
+ ---
502
+
503
+ `);
504
+ let content = `${header}
505
+
506
+ ${body}`;
507
+ if (content.length > MAX_CONTENT3)
508
+ content = content.slice(0, MAX_CONTENT3) + `
509
+
510
+ [truncated]`;
511
+ memories.push({
512
+ content,
513
+ containerTag: "cursor-chats",
514
+ metadata: { source: "cursor", sessionId: composerId, name: sessionName, date }
515
+ });
516
+ sessionIds.push(composerId);
517
+ }
518
+ } catch (e) {
519
+ console.log(` Cursor ingestion error: ${e.message}`);
520
+ } finally {
521
+ db.close();
522
+ }
523
+ return { memories, sessionIds, skipped };
524
+ }
525
+
526
+ // src/ingest/codebase.ts
527
+ import { createHash } from "node:crypto";
528
+ import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync2, existsSync as existsSync4 } from "node:fs";
529
+ import { join as join6, relative, extname, resolve } from "node:path";
530
+ var DEFAULT_IGNORE = new Set([
531
+ "node_modules",
532
+ ".git",
533
+ ".next",
534
+ ".nuxt",
535
+ ".output",
536
+ "dist",
537
+ "build",
538
+ ".cache",
539
+ "__pycache__",
540
+ ".venv",
541
+ "venv",
542
+ "vendor",
543
+ "target",
544
+ ".turbo",
545
+ ".DS_Store",
546
+ ".claude",
547
+ ".conare"
548
+ ]);
549
+ var IGNORE_FILES = new Set([
550
+ "package-lock.json",
551
+ "bun.lockb",
552
+ "yarn.lock",
553
+ "pnpm-lock.yaml"
554
+ ]);
555
+ var CODE_EXTENSIONS = new Set([
556
+ ".ts",
557
+ ".tsx",
558
+ ".js",
559
+ ".jsx",
560
+ ".mjs",
561
+ ".cjs",
562
+ ".vue",
563
+ ".svelte",
564
+ ".astro",
565
+ ".py",
566
+ ".rb",
567
+ ".go",
568
+ ".rs",
569
+ ".java",
570
+ ".kt",
571
+ ".swift",
572
+ ".c",
573
+ ".cpp",
574
+ ".h",
575
+ ".css",
576
+ ".scss",
577
+ ".less",
578
+ ".html",
579
+ ".json",
580
+ ".yaml",
581
+ ".yml",
582
+ ".toml",
583
+ ".md",
584
+ ".mdx",
585
+ ".txt",
586
+ ".sql",
587
+ ".graphql",
588
+ ".prisma",
589
+ ".sh",
590
+ ".bash",
591
+ ".zsh",
592
+ ".lua",
593
+ ".zig",
594
+ ".ex",
595
+ ".exs"
596
+ ]);
597
+ var SPECIAL_FILES = new Set([
598
+ "Dockerfile",
599
+ "Makefile",
600
+ "Procfile",
601
+ "Justfile",
602
+ ".gitignore",
603
+ ".dockerignore",
604
+ "CLAUDE.md",
605
+ "AGENTS.md",
606
+ "README.md",
607
+ "ARCHITECTURE.md",
608
+ "wrangler.toml",
609
+ "wrangler.json"
610
+ ]);
611
+ var MAX_FILE_SIZE = 1e5;
612
+ function parseGitignore(rootPath) {
613
+ const patterns = new Set;
614
+ const gitignorePath = join6(rootPath, ".gitignore");
615
+ if (!existsSync4(gitignorePath))
616
+ return patterns;
617
+ try {
618
+ const content = readFileSync5(gitignorePath, "utf-8");
619
+ for (const line of content.split(`
620
+ `)) {
621
+ const trimmed = line.trim();
622
+ if (!trimmed || trimmed.startsWith("#"))
623
+ continue;
624
+ patterns.add(trimmed.replace(/\/$/, ""));
625
+ }
626
+ } catch {}
627
+ return patterns;
628
+ }
629
+ function shouldIgnore(name, ignorePatterns) {
630
+ if (DEFAULT_IGNORE.has(name))
631
+ return true;
632
+ if (IGNORE_FILES.has(name))
633
+ return true;
634
+ if (name.startsWith(".env"))
635
+ return true;
636
+ if (ignorePatterns.has(name))
637
+ return true;
638
+ return false;
639
+ }
640
+ function langFromExt(ext) {
641
+ const map = {
642
+ ".ts": "typescript",
643
+ ".tsx": "tsx",
644
+ ".js": "javascript",
645
+ ".jsx": "jsx",
646
+ ".mjs": "javascript",
647
+ ".cjs": "javascript",
648
+ ".vue": "vue",
649
+ ".svelte": "svelte",
650
+ ".astro": "astro",
651
+ ".py": "python",
652
+ ".rb": "ruby",
653
+ ".go": "go",
654
+ ".rs": "rust",
655
+ ".java": "java",
656
+ ".kt": "kotlin",
657
+ ".swift": "swift",
658
+ ".c": "c",
659
+ ".cpp": "cpp",
660
+ ".h": "c",
661
+ ".css": "css",
662
+ ".scss": "scss",
663
+ ".less": "less",
664
+ ".html": "html",
665
+ ".json": "json",
666
+ ".yaml": "yaml",
667
+ ".yml": "yaml",
668
+ ".toml": "toml",
669
+ ".md": "markdown",
670
+ ".mdx": "mdx",
671
+ ".sql": "sql",
672
+ ".graphql": "graphql",
673
+ ".prisma": "prisma",
674
+ ".sh": "bash",
675
+ ".bash": "bash",
676
+ ".zsh": "zsh",
677
+ ".lua": "lua",
678
+ ".zig": "zig",
679
+ ".ex": "elixir",
680
+ ".exs": "elixir"
681
+ };
682
+ return map[ext] || ext.slice(1);
683
+ }
684
+ function formatFile(relPath, content, ext) {
685
+ const lang = langFromExt(ext);
686
+ return `# File: ${relPath}
687
+
688
+ \`\`\`${lang}
689
+ ${content}
690
+ \`\`\``;
691
+ }
692
+ function indexCodebase(rootPath) {
693
+ const absRoot = resolve(rootPath);
694
+ const gitignorePatterns = parseGitignore(absRoot);
695
+ const memories = [];
696
+ let fileCount = 0;
697
+ let skipped = 0;
698
+ function walk(dir) {
699
+ let entries;
700
+ try {
701
+ entries = readdirSync4(dir, { withFileTypes: true });
702
+ } catch {
703
+ return;
704
+ }
705
+ for (const entry of entries) {
706
+ if (shouldIgnore(entry.name, gitignorePatterns))
707
+ continue;
708
+ const fullPath = join6(dir, entry.name);
709
+ if (entry.isDirectory()) {
710
+ walk(fullPath);
711
+ continue;
712
+ }
713
+ if (!entry.isFile())
714
+ continue;
715
+ const ext = extname(entry.name).toLowerCase();
716
+ if (!CODE_EXTENSIONS.has(ext) && !SPECIAL_FILES.has(entry.name))
717
+ continue;
718
+ let stat;
719
+ try {
720
+ stat = statSync2(fullPath);
721
+ } catch {
722
+ continue;
723
+ }
724
+ if (stat.size > MAX_FILE_SIZE || stat.size === 0) {
725
+ skipped++;
726
+ continue;
727
+ }
728
+ let raw;
729
+ try {
730
+ raw = readFileSync5(fullPath, "utf-8");
731
+ } catch {
732
+ skipped++;
733
+ continue;
734
+ }
735
+ if (raw.includes("\x00")) {
736
+ skipped++;
737
+ continue;
738
+ }
739
+ const relPath = relative(absRoot, fullPath);
740
+ const contentHash = createHash("sha256").update(raw).digest("hex").slice(0, 16);
741
+ const fileHash = `${relPath}:${contentHash}`;
742
+ if (isIngested("codebase", fileHash)) {
743
+ skipped++;
744
+ continue;
745
+ }
746
+ const content = formatFile(relPath, raw, ext);
747
+ memories.push({
748
+ content,
749
+ containerTag: "codebase",
750
+ metadata: {
751
+ source: "codebase-index",
752
+ filePath: relPath,
753
+ fileHash,
754
+ language: langFromExt(ext)
755
+ }
756
+ });
757
+ fileCount++;
758
+ }
759
+ }
760
+ walk(absRoot);
761
+ return { memories, fileCount, skipped };
762
+ }
763
+
764
+ // src/api.ts
765
+ var API_URL = "https://mcp.conare.ai";
766
+ function createUploadBatches(memories, maxItems = 5, maxChars = 120000) {
767
+ const batches = [];
768
+ let current = [];
769
+ let currentChars = 0;
770
+ let startIndex = 0;
771
+ for (let i = 0;i < memories.length; i++) {
772
+ const item = memories[i];
773
+ const itemChars = item.content.length;
774
+ const exceedsBatch = current.length > 0 && (current.length >= maxItems || currentChars + itemChars > maxChars);
775
+ if (exceedsBatch) {
776
+ batches.push({ startIndex, items: current });
777
+ current = [];
778
+ currentChars = 0;
779
+ startIndex = i;
780
+ }
781
+ current.push(item);
782
+ currentChars += itemChars;
783
+ }
784
+ if (current.length > 0) {
785
+ batches.push({ startIndex, items: current });
786
+ }
787
+ return batches;
788
+ }
789
+ async function validateKey(apiKey) {
790
+ try {
791
+ const res = await fetch(`${API_URL}/api/auth/me`, {
792
+ headers: { Authorization: `Bearer ${apiKey}` }
793
+ });
794
+ if (!res.ok)
795
+ return { valid: false };
796
+ const data = await res.json();
797
+ return { valid: true, email: data.user.email, name: data.user.name };
798
+ } catch {
799
+ return { valid: false };
800
+ }
801
+ }
802
+ async function uploadItems(apiKey, items) {
803
+ let retries = 4;
804
+ while (retries > 0) {
805
+ try {
806
+ const res = await fetch(`${API_URL}/api/memories/bulk`, {
807
+ method: "POST",
808
+ headers: {
809
+ "Content-Type": "application/json",
810
+ Authorization: `Bearer ${apiKey}`
811
+ },
812
+ body: JSON.stringify(items)
813
+ });
814
+ if (res.status === 429) {
815
+ retries--;
816
+ await new Promise((r) => setTimeout(r, 5000));
817
+ continue;
818
+ }
819
+ if (!res.ok) {
820
+ const body = await res.text().catch(() => "");
821
+ throw new Error(`HTTP ${res.status}: ${body.slice(0, 120)}`);
822
+ }
823
+ const data = await res.json();
824
+ if (!Array.isArray(data.results) || data.results.length !== items.length) {
825
+ throw new Error("Unexpected bulk upload response");
826
+ }
827
+ return data.results;
828
+ } catch (error) {
829
+ retries--;
830
+ if (retries === 0) {
831
+ return items.map(() => ({
832
+ success: false,
833
+ error: error instanceof Error ? error.message : String(error)
834
+ }));
835
+ }
836
+ await new Promise((r) => setTimeout(r, 2000));
837
+ }
838
+ }
839
+ return items.map(() => ({ success: false, error: "Upload failed" }));
840
+ }
841
+ async function uploadBulk(apiKey, memories, onProgress) {
842
+ let success = 0;
843
+ let failed = 0;
844
+ const total = memories.length;
845
+ const results = [];
846
+ for (const batch of createUploadBatches(memories)) {
847
+ let batchResults = await uploadItems(apiKey, batch.items);
848
+ if (batch.items.length > 1 && batchResults.some((result) => !result.success)) {
849
+ const retriedResults = [];
850
+ for (let i = 0;i < batch.items.length; i++) {
851
+ const result = batchResults[i];
852
+ if (result.success) {
853
+ retriedResults.push(result);
854
+ continue;
855
+ }
856
+ const [singleResult] = await uploadItems(apiKey, [batch.items[i]]);
857
+ retriedResults.push(singleResult);
858
+ }
859
+ batchResults = retriedResults;
860
+ }
861
+ for (let i = 0;i < batchResults.length; i++) {
862
+ const result = batchResults[i];
863
+ results.push({
864
+ index: batch.startIndex + i,
865
+ success: result.success,
866
+ deduped: result.deduped,
867
+ error: result.error
868
+ });
869
+ if (result.success)
870
+ success++;
871
+ else
872
+ failed++;
873
+ }
874
+ onProgress?.(success + failed, total, failed);
875
+ }
876
+ return { success, failed, results };
877
+ }
878
+
879
+ // src/index.ts
880
+ function getDedupKey(memory) {
881
+ const metadata = memory.metadata;
882
+ return metadata?.sessionId || metadata?.fileHash || null;
883
+ }
884
+ function parseArgs() {
885
+ const args = process.argv.slice(2);
886
+ let key = "";
887
+ let dryRun = false;
888
+ let force = false;
889
+ let source;
890
+ let wasmDir;
891
+ let indexPath;
892
+ for (let i = 0;i < args.length; i++) {
893
+ if (args[i] === "--key" && args[i + 1]) {
894
+ key = args[++i];
895
+ } else if (args[i] === "--dry-run") {
896
+ dryRun = true;
897
+ } else if (args[i] === "--force") {
898
+ force = true;
899
+ } else if (args[i] === "--source" && args[i + 1]) {
900
+ source = args[++i];
901
+ } else if (args[i] === "--wasm-dir" && args[i + 1]) {
902
+ wasmDir = args[++i];
903
+ } else if (args[i] === "--index") {
904
+ indexPath = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : ".";
905
+ } else if (args[i] === "--ingest-only") {} else if (args[i] === "--config-only") {} else if (args[i] === "--help" || args[i] === "-h") {
906
+ console.log(`
907
+ conare — Ingest AI chat history into Conare
908
+
909
+ Usage:
910
+ conare --key <api_key> Ingest chat history
911
+ conare --key <api_key> --index [path] Index codebase
912
+
913
+ Options:
914
+ --key <key> Your Conare API key (required, starts with cmem_)
915
+ --index [path] Index codebase at path (default: current directory)
916
+ --dry-run Preview what would be uploaded
917
+ --force Re-ingest all / re-index all (bypass dedup)
918
+ --source <name> Only ingest from: claude, codex, cursor
919
+ --wasm-dir <path> Path to sql.js module (for Cursor ingestion)
920
+
921
+ Get your API key at https://mcp.conare.ai
922
+ `);
923
+ process.exit(0);
924
+ }
925
+ }
926
+ if (!key) {
927
+ console.error("Error: --key is required. Get your API key at https://mcp.conare.ai");
928
+ process.exit(1);
929
+ }
930
+ if (!key.startsWith("cmem_")) {
931
+ console.error("Error: API key must start with cmem_");
932
+ process.exit(1);
933
+ }
934
+ return { key, dryRun, force, source, wasmDir, indexPath };
935
+ }
936
+ async function main() {
937
+ const opts = parseArgs();
938
+ process.stdout.write("Validating API key... ");
939
+ const auth = await validateKey(opts.key);
940
+ if (!auth.valid) {
941
+ console.error("INVALID. Check your key at https://mcp.conare.ai");
942
+ process.exit(1);
943
+ }
944
+ console.log(`OK (${auth.email})`);
945
+ console.log();
946
+ if (opts.indexPath !== undefined) {
947
+ const { resolve: resolve2 } = await import("node:path");
948
+ const absPath = resolve2(opts.indexPath);
949
+ console.log(`Indexing codebase: ${absPath}`);
950
+ if (opts.force) {
951
+ clearIngested("codebase");
952
+ process.stdout.write("Clearing existing codebase index... ");
953
+ try {
954
+ await fetch("https://mcp.conare.ai/api/containers/codebase", {
955
+ method: "DELETE",
956
+ headers: { Authorization: `Bearer ${opts.key}` }
957
+ });
958
+ console.log("done");
959
+ } catch {
960
+ console.log("skipped (no existing index)");
961
+ }
962
+ }
963
+ process.stdout.write("Scanning files... ");
964
+ const { memories, fileCount, skipped } = indexCodebase(absPath);
965
+ console.log(`${fileCount} files found, ${skipped} skipped`);
966
+ if (memories.length === 0) {
967
+ console.log(`
968
+ Nothing new to index.`);
969
+ } else if (opts.dryRun) {
970
+ console.log(`
971
+ [DRY RUN] Would upload ${memories.length} files`);
972
+ for (const m of memories.slice(0, 10)) {
973
+ const meta = m.metadata;
974
+ console.log(` ${meta.language.padEnd(12)} ${meta.filePath}`);
975
+ }
976
+ if (memories.length > 10)
977
+ console.log(` ... and ${memories.length - 10} more`);
978
+ } else {
979
+ const barWidth = 20;
980
+ const { success, failed, results } = await uploadBulk(opts.key, memories, (uploaded, total, _failed) => {
981
+ const pct = (uploaded / total * 100).toFixed(1);
982
+ const filled = Math.round(uploaded / total * barWidth);
983
+ const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
984
+ process.stdout.write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
985
+ });
986
+ process.stdout.write(`\r \x1B[32m✓\x1B[0m ${success} files indexed, ${failed} failed${" ".repeat(20)}
987
+ `);
988
+ const fileHashes = results.filter((result) => result.success).map((result) => getDedupKey(memories[result.index])).filter((key) => !!key);
989
+ markIngested("codebase", fileHashes);
990
+ }
991
+ console.log();
992
+ console.log("Done! Your codebase is now searchable via recall.");
993
+ return;
994
+ }
995
+ if (opts.force) {
996
+ if (opts.source) {
997
+ clearIngested(opts.source);
998
+ console.log(`Cleared ingestion history for ${opts.source}`);
999
+ } else {
1000
+ clearIngested();
1001
+ console.log("Cleared all ingestion history");
1002
+ }
1003
+ console.log();
1004
+ }
1005
+ const tools = detect();
1006
+ console.log("Detected AI tools:");
1007
+ for (const t of tools) {
1008
+ if (t.available) {
1009
+ console.log(` + ${t.name}${t.sessionCount ? ` — ${t.sessionCount} sessions` : ""}`);
1010
+ } else {
1011
+ console.log(` - ${t.name} (not found)`);
1012
+ }
1013
+ }
1014
+ console.log();
1015
+ const allMemories = [];
1016
+ const shouldIngest = (name) => !opts.source || opts.source === name;
1017
+ if (shouldIngest("claude") && tools.find((t) => t.name === "Claude Code")?.available) {
1018
+ process.stdout.write("Ingesting Claude Code... ");
1019
+ const { memories, skipped } = ingestClaude();
1020
+ allMemories.push(...memories);
1021
+ console.log(`${memories.length} new, ${skipped} skipped`);
1022
+ }
1023
+ if (shouldIngest("codex") && tools.find((t) => t.name === "Codex")?.available) {
1024
+ process.stdout.write("Ingesting Codex... ");
1025
+ const { memories, skipped } = ingestCodex();
1026
+ allMemories.push(...memories);
1027
+ console.log(`${memories.length} new, ${skipped} skipped`);
1028
+ }
1029
+ if (shouldIngest("cursor") && tools.find((t) => t.name === "Cursor")?.available) {
1030
+ process.stdout.write("Ingesting Cursor... ");
1031
+ const cursorTool = tools.find((t) => t.name === "Cursor");
1032
+ const { memories, skipped } = await ingestCursor(cursorTool.path, opts.wasmDir);
1033
+ allMemories.push(...memories);
1034
+ console.log(`${memories.length} new, ${skipped} skipped`);
1035
+ }
1036
+ console.log();
1037
+ if (allMemories.length === 0) {
1038
+ console.log("Nothing new to upload.");
1039
+ } else if (opts.dryRun) {
1040
+ console.log(`[DRY RUN] Would upload ${allMemories.length} memories`);
1041
+ for (const m of allMemories.slice(0, 5)) {
1042
+ console.log(` ${m.containerTag} | ${m.content.length} chars | ${m.metadata?.sessionId?.slice(0, 12) || "?"}`);
1043
+ }
1044
+ if (allMemories.length > 5)
1045
+ console.log(` ... and ${allMemories.length - 5} more`);
1046
+ } else {
1047
+ const barWidth = 20;
1048
+ const { success, failed, results } = await uploadBulk(opts.key, allMemories, (uploaded, total, _failed) => {
1049
+ const pct = (uploaded / total * 100).toFixed(1);
1050
+ const filled = Math.round(uploaded / total * barWidth);
1051
+ const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
1052
+ process.stdout.write(`\r Uploading [\x1B[33m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
1053
+ });
1054
+ process.stdout.write(`\r \x1B[32m✓\x1B[0m ${success} uploaded, ${failed} failed${" ".repeat(20)}
1055
+ `);
1056
+ if (failed > 0) {
1057
+ console.log(` Re-run with --force to retry failed memories.`);
1058
+ }
1059
+ const successfulKeysBySource = {
1060
+ claude: [],
1061
+ codex: [],
1062
+ cursor: []
1063
+ };
1064
+ for (const result of results) {
1065
+ if (!result.success)
1066
+ continue;
1067
+ const memory = allMemories[result.index];
1068
+ const key = getDedupKey(memory);
1069
+ if (!key)
1070
+ continue;
1071
+ switch (memory.containerTag) {
1072
+ case "claude-chats":
1073
+ successfulKeysBySource.claude.push(key);
1074
+ break;
1075
+ case "codex-chats":
1076
+ successfulKeysBySource.codex.push(key);
1077
+ break;
1078
+ case "cursor-chats":
1079
+ successfulKeysBySource.cursor.push(key);
1080
+ break;
1081
+ }
1082
+ }
1083
+ for (const [source, ids] of Object.entries(successfulKeysBySource)) {
1084
+ if (ids.length > 0)
1085
+ markIngested(source, ids);
1086
+ }
1087
+ }
1088
+ console.log();
1089
+ console.log("Done! MCP will be configured by the install script.");
1090
+ }
1091
+ main().catch((e) => {
1092
+ console.error("Error:", e.message || e);
1093
+ process.exit(1);
1094
+ });
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "conare",
3
+ "version": "0.0.1",
4
+ "description": "Conare CLI for ingesting AI chat history and configuring memory at conare.ai",
5
+ "type": "module",
6
+ "bin": {
7
+ "conare": "./dist/index.js"
8
+ },
9
+ "scripts": {
10
+ "build": "bun build src/index.ts --outdir dist --target node --format esm --external sql.js && cp dist/index.js ../public/install.mjs",
11
+ "prepublishOnly": "bun run build"
12
+ },
13
+ "files": [
14
+ "dist"
15
+ ],
16
+ "keywords": ["conare", "ai", "memory", "mcp", "claude", "cursor", "codex", "context"],
17
+ "homepage": "https://conare.ai",
18
+ "license": "MIT",
19
+ "dependencies": {
20
+ "sql.js": "^1.12.0"
21
+ }
22
+ }