conare 0.1.0 → 0.1.2

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 +144 -55
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -281,6 +281,7 @@ import { homedir as homedir3 } from "node:os";
281
281
 
282
282
  // src/ingest/shared.ts
283
283
  import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync } from "node:fs";
284
+ import { createHash } from "node:crypto";
284
285
  import { join as join2 } from "node:path";
285
286
  import { homedir as homedir2 } from "node:os";
286
287
  var MANIFEST_PATH = join2(homedir2(), ".conare", "ingested.json");
@@ -290,6 +291,9 @@ function cleanText(raw) {
290
291
  text = text.replace(/<attached-context[\s\S]*?<\/attached-context>/g, "");
291
292
  return text.trim();
292
293
  }
294
+ function createContentHash(content) {
295
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
296
+ }
293
297
  function getIngested() {
294
298
  try {
295
299
  if (existsSync2(MANIFEST_PATH)) {
@@ -372,12 +376,13 @@ function ingestClaude() {
372
376
  const projectsDir = join3(homedir3(), ".claude", "projects");
373
377
  const memories = [];
374
378
  const sessionIds = [];
375
- let skipped = 0;
379
+ let filtered = 0;
380
+ let deduped = 0;
376
381
  let projectDirs;
377
382
  try {
378
383
  projectDirs = readdirSync2(projectsDir);
379
384
  } catch {
380
- return { memories, sessionIds, skipped };
385
+ return { memories, sessionIds, skipped: 0, filtered, deduped };
381
386
  }
382
387
  for (const projDir of projectDirs) {
383
388
  const projPath = join3(projectsDir, projDir);
@@ -390,15 +395,11 @@ function ingestClaude() {
390
395
  }
391
396
  for (const file of files) {
392
397
  const sessionId = basename(file, ".jsonl");
393
- if (isIngested("claude", sessionId)) {
394
- skipped++;
395
- continue;
396
- }
397
398
  const raw = readFileSync2(join3(projPath, file), "utf-8");
398
399
  const { turns, date } = parseSession(raw.split(`
399
400
  `));
400
401
  if (turns.length === 0) {
401
- skipped++;
402
+ filtered++;
402
403
  continue;
403
404
  }
404
405
  const header = `# Chat: ${project}${date ? ` | ${date}` : ""}`;
@@ -419,15 +420,29 @@ ${body}`;
419
420
  content = content.slice(0, MAX_CONTENT) + `
420
421
 
421
422
  [truncated]`;
423
+ const contentHash = createContentHash(content);
424
+ const dedupKey = `claude:${sessionId}`;
425
+ const fingerprint = `${dedupKey}:${contentHash}`;
426
+ if (isIngested("claude", fingerprint)) {
427
+ deduped++;
428
+ continue;
429
+ }
422
430
  memories.push({
423
431
  content,
424
432
  containerTag: "claude-chats",
425
- metadata: { source: "claude-code", sessionId, project, date: date || "unknown" }
433
+ metadata: {
434
+ dedupKey,
435
+ contentHash,
436
+ source: "claude-code",
437
+ sessionId,
438
+ project,
439
+ date: date || "unknown"
440
+ }
426
441
  });
427
442
  sessionIds.push(sessionId);
428
443
  }
429
444
  }
430
- return { memories, sessionIds, skipped };
445
+ return { memories, sessionIds, skipped: filtered + deduped, filtered, deduped };
431
446
  }
432
447
 
433
448
  // src/ingest/codex.ts
@@ -438,7 +453,8 @@ var MAX_CONTENT2 = 48000;
438
453
  function ingestCodex() {
439
454
  const memories = [];
440
455
  const sessionIds = [];
441
- let skipped = 0;
456
+ let filtered = 0;
457
+ let deduped = 0;
442
458
  const historyPath = join4(homedir4(), ".codex", "history.jsonl");
443
459
  if (existsSync3(historyPath)) {
444
460
  try {
@@ -458,10 +474,6 @@ function ingestCodex() {
458
474
  }
459
475
  }
460
476
  for (const [sessionId, entries] of sessions) {
461
- if (isIngested("codex", sessionId)) {
462
- skipped++;
463
- continue;
464
- }
465
477
  entries.sort((a, b) => a.ts - b.ts);
466
478
  const date = new Date(entries[0].ts * 1000).toISOString().slice(0, 10);
467
479
  const body = entries.map((e) => {
@@ -480,13 +492,20 @@ ${body}`;
480
492
 
481
493
  [truncated]`;
482
494
  if (content.length < 100) {
483
- skipped++;
495
+ filtered++;
496
+ continue;
497
+ }
498
+ const contentHash = createContentHash(content);
499
+ const dedupKey = `codex:${sessionId}`;
500
+ const fingerprint = `${dedupKey}:${contentHash}`;
501
+ if (isIngested("codex", fingerprint)) {
502
+ deduped++;
484
503
  continue;
485
504
  }
486
505
  memories.push({
487
506
  content,
488
507
  containerTag: "codex-chats",
489
- metadata: { source: "codex", sessionId, date }
508
+ metadata: { dedupKey, contentHash, source: "codex", sessionId, date }
490
509
  });
491
510
  sessionIds.push(sessionId);
492
511
  }
@@ -495,19 +514,22 @@ ${body}`;
495
514
  const sessionsDir = join4(homedir4(), ".codex", "sessions");
496
515
  if (existsSync3(sessionsDir)) {
497
516
  try {
498
- walkCodexSessions(sessionsDir, memories, sessionIds, skipped);
517
+ const stats = { filtered: 0, deduped: 0 };
518
+ walkCodexSessions(sessionsDir, memories, sessionIds, stats);
519
+ filtered += stats.filtered;
520
+ deduped += stats.deduped;
499
521
  } catch {}
500
522
  }
501
- return { memories, sessionIds, skipped };
523
+ return { memories, sessionIds, skipped: filtered + deduped, filtered, deduped };
502
524
  }
503
- function walkCodexSessions(dir, memories, sessionIds, skipped) {
525
+ function walkCodexSessions(dir, memories, sessionIds, stats) {
504
526
  try {
505
527
  for (const entry of readdirSync3(dir, { withFileTypes: true })) {
506
528
  if (entry.isDirectory()) {
507
- walkCodexSessions(join4(dir, entry.name), memories, sessionIds, skipped);
529
+ walkCodexSessions(join4(dir, entry.name), memories, sessionIds, stats);
508
530
  } else if (entry.name.endsWith(".jsonl")) {
509
531
  const sessionId = basename2(entry.name, ".jsonl");
510
- if (isIngested("codex", sessionId) || sessionIds.includes(sessionId))
532
+ if (sessionIds.includes(sessionId))
511
533
  continue;
512
534
  try {
513
535
  const lines = readFileSync3(join4(dir, entry.name), "utf-8").split(`
@@ -535,8 +557,10 @@ function walkCodexSessions(dir, memories, sessionIds, skipped) {
535
557
  continue;
536
558
  }
537
559
  }
538
- if (turns.length === 0)
560
+ if (turns.length === 0) {
561
+ stats.filtered++;
539
562
  continue;
563
+ }
540
564
  const body = turns.map((t) => t.length > 500 ? t.slice(0, 500) + "..." : t).join(`
541
565
 
542
566
  ---
@@ -549,10 +573,23 @@ ${body}`;
549
573
  content = content.slice(0, MAX_CONTENT2) + `
550
574
 
551
575
  [truncated]`;
576
+ const contentHash = createContentHash(content);
577
+ const dedupKey = `codex:${sessionId}`;
578
+ const fingerprint = `${dedupKey}:${contentHash}`;
579
+ if (isIngested("codex", fingerprint)) {
580
+ stats.deduped++;
581
+ continue;
582
+ }
552
583
  memories.push({
553
584
  content,
554
585
  containerTag: "codex-chats",
555
- metadata: { source: "codex-session", sessionId, date: date || "unknown" }
586
+ metadata: {
587
+ dedupKey,
588
+ contentHash,
589
+ source: "codex-session",
590
+ sessionId,
591
+ date: date || "unknown"
592
+ }
556
593
  });
557
594
  sessionIds.push(sessionId);
558
595
  } catch {}
@@ -620,17 +657,18 @@ function extractTurns(db, composerId, bubbleHeaders) {
620
657
  async function ingestCursor(dbPath, wasmDir) {
621
658
  const memories = [];
622
659
  const sessionIds = [];
623
- let skipped = 0;
660
+ let filtered = 0;
661
+ let deduped = 0;
624
662
  let fileSize;
625
663
  try {
626
664
  fileSize = statSync(dbPath).size;
627
665
  } catch {
628
666
  console.log(" Skipping Cursor: database not accessible");
629
- return { memories, sessionIds, skipped };
667
+ return { memories, sessionIds, skipped: 0, filtered, deduped };
630
668
  }
631
669
  if (fileSize > MAX_DB_SIZE) {
632
670
  console.log(` Skipping Cursor: database too large (${(fileSize / 1024 / 1024 / 1024).toFixed(1)}GB)`);
633
- return { memories, sessionIds, skipped };
671
+ return { memories, sessionIds, skipped: 0, filtered, deduped };
634
672
  }
635
673
  if (fileSize > WARN_DB_SIZE) {
636
674
  console.log(` Warning: large database (${(fileSize / 1024 / 1024).toFixed(0)}MB), loading into memory...`);
@@ -638,14 +676,14 @@ async function ingestCursor(dbPath, wasmDir) {
638
676
  const initSqlJs = loadSqlJs(wasmDir);
639
677
  if (!initSqlJs) {
640
678
  console.log(" Skipping Cursor: sql.js not available");
641
- return { memories, sessionIds, skipped };
679
+ return { memories, sessionIds, skipped: 0, filtered, deduped };
642
680
  }
643
681
  let db;
644
682
  try {
645
683
  db = await openDb(initSqlJs, dbPath, wasmDir);
646
684
  } catch (e) {
647
685
  console.log(` Skipping Cursor: cannot open database (${e.message})`);
648
- return { memories, sessionIds, skipped };
686
+ return { memories, sessionIds, skipped: 0, filtered, deduped };
649
687
  }
650
688
  try {
651
689
  let rows = [];
@@ -656,29 +694,25 @@ async function ingestCursor(dbPath, wasmDir) {
656
694
  } catch {}
657
695
  for (const [key, value] of rows) {
658
696
  const composerId = key.replace("composerData:", "");
659
- if (isIngested("cursor", composerId)) {
660
- skipped++;
661
- continue;
662
- }
663
697
  let parsed;
664
698
  try {
665
699
  parsed = JSON.parse(value);
666
700
  if (!parsed || typeof parsed !== "object") {
667
- skipped++;
701
+ filtered++;
668
702
  continue;
669
703
  }
670
704
  } catch {
671
- skipped++;
705
+ filtered++;
672
706
  continue;
673
707
  }
674
708
  const bubbleHeaders = parsed.fullConversationHeadersOnly;
675
709
  if (!Array.isArray(bubbleHeaders) || bubbleHeaders.length === 0) {
676
- skipped++;
710
+ filtered++;
677
711
  continue;
678
712
  }
679
713
  const turns = extractTurns(db, composerId, bubbleHeaders);
680
714
  if (turns.length === 0) {
681
- skipped++;
715
+ filtered++;
682
716
  continue;
683
717
  }
684
718
  const sessionName = parsed.name || "Cursor Chat";
@@ -701,10 +735,24 @@ ${body}`;
701
735
  content = content.slice(0, MAX_CONTENT3) + `
702
736
 
703
737
  [truncated]`;
738
+ const contentHash = createContentHash(content);
739
+ const dedupKey = `cursor:${composerId}`;
740
+ const fingerprint = `${dedupKey}:${contentHash}`;
741
+ if (isIngested("cursor", fingerprint)) {
742
+ deduped++;
743
+ continue;
744
+ }
704
745
  memories.push({
705
746
  content,
706
747
  containerTag: "cursor-chats",
707
- metadata: { source: "cursor", sessionId: composerId, name: sessionName, date }
748
+ metadata: {
749
+ dedupKey,
750
+ contentHash,
751
+ source: "cursor",
752
+ sessionId: composerId,
753
+ name: sessionName,
754
+ date
755
+ }
708
756
  });
709
757
  sessionIds.push(composerId);
710
758
  }
@@ -713,11 +761,11 @@ ${body}`;
713
761
  } finally {
714
762
  db.close();
715
763
  }
716
- return { memories, sessionIds, skipped };
764
+ return { memories, sessionIds, skipped: filtered + deduped, filtered, deduped };
717
765
  }
718
766
 
719
767
  // src/ingest/codebase.ts
720
- import { createHash } from "node:crypto";
768
+ import { createHash as createHash2 } from "node:crypto";
721
769
  import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync2, existsSync as existsSync4 } from "node:fs";
722
770
  import { join as join6, relative, extname, resolve } from "node:path";
723
771
  var DEFAULT_IGNORE = new Set([
@@ -884,6 +932,7 @@ ${content}
884
932
  }
885
933
  function indexCodebase(rootPath) {
886
934
  const absRoot = resolve(rootPath);
935
+ const repoHash = createHash2("sha256").update(absRoot).digest("hex").slice(0, 12);
887
936
  const gitignorePatterns = parseGitignore(absRoot);
888
937
  const memories = [];
889
938
  let fileCount = 0;
@@ -930,9 +979,10 @@ function indexCodebase(rootPath) {
930
979
  continue;
931
980
  }
932
981
  const relPath = relative(absRoot, fullPath);
933
- const contentHash = createHash("sha256").update(raw).digest("hex").slice(0, 16);
934
- const fileHash = `${relPath}:${contentHash}`;
935
- if (isIngested("codebase", fileHash)) {
982
+ const contentHash = createContentHash(raw);
983
+ const dedupKey = `codebase:${repoHash}:${relPath}`;
984
+ const fingerprint = `${dedupKey}:${contentHash}`;
985
+ if (isIngested("codebase", fingerprint)) {
936
986
  skipped++;
937
987
  continue;
938
988
  }
@@ -941,9 +991,12 @@ function indexCodebase(rootPath) {
941
991
  content,
942
992
  containerTag: "codebase",
943
993
  metadata: {
994
+ dedupKey,
995
+ contentHash,
944
996
  source: "codebase-index",
997
+ repoHash,
945
998
  filePath: relPath,
946
- fileHash,
999
+ fileHash: `${relPath}:${contentHash}`,
947
1000
  language: langFromExt(ext)
948
1001
  }
949
1002
  });
@@ -956,7 +1009,7 @@ function indexCodebase(rootPath) {
956
1009
 
957
1010
  // src/api.ts
958
1011
  var API_URL = "https://mcp.conare.ai";
959
- function createUploadBatches(memories, maxItems = 5, maxChars = 120000) {
1012
+ function createUploadBatches(memories, maxItems = 20, maxChars = 400000) {
960
1013
  const batches = [];
961
1014
  let current = [];
962
1015
  let currentChars = 0;
@@ -992,6 +1045,21 @@ async function validateKey(apiKey) {
992
1045
  return { valid: false };
993
1046
  }
994
1047
  }
1048
+ async function getRemoteMemoryCount(apiKey) {
1049
+ try {
1050
+ const res = await fetch(`${API_URL}/api/containers`, {
1051
+ headers: { Authorization: `Bearer ${apiKey}` }
1052
+ });
1053
+ if (!res.ok)
1054
+ return null;
1055
+ const data = await res.json();
1056
+ if (!Array.isArray(data.containers))
1057
+ return 0;
1058
+ return data.containers.reduce((sum, container) => sum + (container.count || 0), 0);
1059
+ } catch {
1060
+ return null;
1061
+ }
1062
+ }
995
1063
  async function uploadItems(apiKey, items) {
996
1064
  let retries = 4;
997
1065
  while (retries > 0) {
@@ -1822,9 +1890,12 @@ async function confirmIndexCodebase() {
1822
1890
  }
1823
1891
 
1824
1892
  // src/index.ts
1825
- function getDedupKey(memory) {
1893
+ function getManifestFingerprint(memory) {
1826
1894
  const metadata = memory.metadata;
1827
- return metadata?.sessionId || metadata?.fileHash || null;
1895
+ if (metadata?.dedupKey && metadata?.contentHash) {
1896
+ return `${metadata.dedupKey}:${metadata.contentHash}`;
1897
+ }
1898
+ return metadata?.dedupKey || metadata?.sessionId || metadata?.fileHash || null;
1828
1899
  }
1829
1900
  function printMissingKeyError() {
1830
1901
  console.error("Error: no API key configured.");
@@ -1864,6 +1935,14 @@ function renderProgressSummary(success, failed, noun) {
1864
1935
  return `\r \x1B[32m✓\x1B[0m [\x1B[36m${bar}\x1B[0m] ${success} ${noun}, ${failed} failed (${pct}%)${" ".repeat(12)}
1865
1936
  `;
1866
1937
  }
1938
+ function renderDiscoverySummary(discovered, filtered, deduped) {
1939
+ const parts = [`${discovered} ingestible`];
1940
+ if (filtered > 0)
1941
+ parts.push(`${filtered} filtered`);
1942
+ if (deduped > 0)
1943
+ parts.push(`${deduped} already imported`);
1944
+ return parts.join(", ");
1945
+ }
1867
1946
  function parseArgs() {
1868
1947
  const args = process.argv.slice(2);
1869
1948
  let key = "";
@@ -1987,6 +2066,16 @@ async function main() {
1987
2066
  console.log(auth.email ? `OK (${auth.email})` : "OK");
1988
2067
  saveApiKey(apiKey);
1989
2068
  console.log();
2069
+ if (!opts.force && !opts.dryRun) {
2070
+ const remoteMemoryCount = await getRemoteMemoryCount(apiKey);
2071
+ const localManifest = getIngested();
2072
+ const localManifestCount = Object.values(localManifest).reduce((sum, entries) => sum + entries.length, 0);
2073
+ if (remoteMemoryCount === 0 && localManifestCount > 0) {
2074
+ clearIngested();
2075
+ console.log("Remote account is empty; cleared stale local ingestion history.");
2076
+ console.log("");
2077
+ }
2078
+ }
1990
2079
  if (!opts.wasmDir && existsSync7(join9(process.cwd(), "node_modules", "sql.js"))) {
1991
2080
  opts.wasmDir = join9(process.cwd(), "node_modules");
1992
2081
  }
@@ -2046,7 +2135,7 @@ Nothing new to index.`);
2046
2135
  process.stdout.write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
2047
2136
  });
2048
2137
  process.stdout.write(renderProgressSummary(success, failed, "indexed"));
2049
- const fileHashes = results.filter((result) => result.success).map((result) => getDedupKey(memories[result.index])).filter((key) => !!key);
2138
+ const fileHashes = results.filter((result) => result.success).map((result) => getManifestFingerprint(memories[result.index])).filter((key) => !!key);
2050
2139
  markIngested("codebase", fileHashes);
2051
2140
  printFailureSummary(results, memories);
2052
2141
  }
@@ -2088,22 +2177,22 @@ Nothing new to index.`);
2088
2177
  const shouldIngest = (name) => selectedSources.includes(name);
2089
2178
  if (shouldIngest("claude") && tools.find((t) => t.name === "Claude Code")?.available) {
2090
2179
  process.stdout.write("Ingesting Claude Code... ");
2091
- const { memories, skipped } = ingestClaude();
2180
+ const { memories, filtered, deduped } = ingestClaude();
2092
2181
  allMemories.push(...memories);
2093
- console.log(`${memories.length} new, ${skipped} skipped`);
2182
+ console.log(renderDiscoverySummary(memories.length, filtered, deduped));
2094
2183
  }
2095
2184
  if (shouldIngest("codex") && tools.find((t) => t.name === "Codex")?.available) {
2096
2185
  process.stdout.write("Ingesting Codex... ");
2097
- const { memories, skipped } = ingestCodex();
2186
+ const { memories, filtered, deduped } = ingestCodex();
2098
2187
  allMemories.push(...memories);
2099
- console.log(`${memories.length} new, ${skipped} skipped`);
2188
+ console.log(renderDiscoverySummary(memories.length, filtered, deduped));
2100
2189
  }
2101
2190
  if (shouldIngest("cursor") && tools.find((t) => t.name === "Cursor")?.available) {
2102
2191
  process.stdout.write("Ingesting Cursor... ");
2103
2192
  const cursorTool = tools.find((t) => t.name === "Cursor");
2104
- const { memories, skipped } = await ingestCursor(cursorTool.path, opts.wasmDir);
2193
+ const { memories, filtered, deduped } = await ingestCursor(cursorTool.path, opts.wasmDir);
2105
2194
  allMemories.push(...memories);
2106
- console.log(`${memories.length} new, ${skipped} skipped`);
2195
+ console.log(renderDiscoverySummary(memories.length, filtered, deduped));
2107
2196
  }
2108
2197
  console.log();
2109
2198
  if (allMemories.length === 0) {
@@ -2137,7 +2226,7 @@ Nothing new to index.`);
2137
2226
  if (!result.success)
2138
2227
  continue;
2139
2228
  const memory = allMemories[result.index];
2140
- const key = getDedupKey(memory);
2229
+ const key = getManifestFingerprint(memory);
2141
2230
  if (!key)
2142
2231
  continue;
2143
2232
  switch (memory.containerTag) {
@@ -2200,7 +2289,7 @@ Nothing new to index.`);
2200
2289
  process.stdout.write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
2201
2290
  });
2202
2291
  process.stdout.write(renderProgressSummary(success, failed, "indexed"));
2203
- const fileHashes = results.filter((result) => result.success).map((result) => getDedupKey(memories[result.index])).filter((key) => !!key);
2292
+ const fileHashes = results.filter((result) => result.success).map((result) => getManifestFingerprint(memories[result.index])).filter((key) => !!key);
2204
2293
  markIngested("codebase", fileHashes);
2205
2294
  printFailureSummary(results, memories);
2206
2295
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conare",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Conare CLI for ingesting AI chat history and configuring memory at conare.ai",
5
5
  "type": "module",
6
6
  "bin": {