conare 0.3.1 → 0.3.3

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 +968 -753
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -29,6 +29,584 @@ var __export = (target, all) => {
29
29
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
30
30
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
31
31
 
32
+ // src/ingest/shared.ts
33
+ import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
34
+ import { createHash } from "node:crypto";
35
+ import { join as join2 } from "node:path";
36
+ import { homedir as homedir2 } from "node:os";
37
+ function isNarration(text) {
38
+ const stripped = text.trim();
39
+ if (stripped.length < MIN_SUBSTANTIVE)
40
+ return true;
41
+ if (stripped.length <= 300 && NARRATION_RE.test(stripped))
42
+ return true;
43
+ return false;
44
+ }
45
+ function cleanText(raw) {
46
+ let text = raw;
47
+ text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
48
+ text = text.replace(/<attached-context[\s\S]*?<\/attached-context>/g, "");
49
+ text = text.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, "");
50
+ text = text.replace(/^\s*Base directory for this skill:[\s\S]*/g, "");
51
+ text = text.replace(/\nBase directory for this skill:[\s\S]*/g, "");
52
+ text = text.replace(/<!-- vibe-rules Integration -->[\s\S]*?<!-- \/vibe-rules Integration -->/g, "");
53
+ text = text.replace(/<good-behaviour>[\s\S]*?<\/good-behaviour>/g, "");
54
+ text = text.replace(/<frontend-design>[\s\S]*?<\/frontend-design>/g, "");
55
+ text = text.replace(/<architecture>[\s\S]*?<\/architecture>/g, "");
56
+ text = text.replace(/<task-notification>[\s\S]*?<\/task-notification>/g, "");
57
+ text = text.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, "");
58
+ text = text.replace(/<system_instruction>[\s\S]*?<\/system_instruction>/g, "");
59
+ return text.trim();
60
+ }
61
+ function createContentHash(content) {
62
+ return createHash("sha256").update(content).digest("hex").slice(0, 16);
63
+ }
64
+ function getIngested() {
65
+ try {
66
+ if (existsSync2(MANIFEST_PATH)) {
67
+ return JSON.parse(readFileSync2(MANIFEST_PATH, "utf-8"));
68
+ }
69
+ } catch {}
70
+ return {};
71
+ }
72
+ function markIngested(source, sessionIds) {
73
+ const manifest = getIngested();
74
+ const existing = new Set(manifest[source] || []);
75
+ for (const id of sessionIds)
76
+ existing.add(id);
77
+ manifest[source] = [...existing];
78
+ const dir = join2(homedir2(), ".conare");
79
+ if (!existsSync2(dir))
80
+ mkdirSync(dir, { recursive: true });
81
+ writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
82
+ }
83
+ function isIngested(source, sessionId) {
84
+ const manifest = getIngested();
85
+ return (manifest[source] || []).includes(sessionId);
86
+ }
87
+ function clearIngested(source) {
88
+ const manifest = getIngested();
89
+ if (source) {
90
+ delete manifest[source];
91
+ } else {
92
+ for (const key of Object.keys(manifest))
93
+ delete manifest[key];
94
+ }
95
+ const dir = join2(homedir2(), ".conare");
96
+ if (!existsSync2(dir))
97
+ mkdirSync(dir, { recursive: true });
98
+ writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
99
+ }
100
+ var MANIFEST_PATH, MIN_SUBSTANTIVE = 200, NARRATION_RE;
101
+ var init_shared = __esm(() => {
102
+ MANIFEST_PATH = join2(homedir2(), ".conare", "ingested.json");
103
+ NARRATION_RE = /^[\s\n]*(Let me |Now let me |Now I['\u2019]|Now add |Now fix |Now replace |Now integrate |Now update |Now pass |Now clean |Now build|Update the |Builds clean|Deployed\.|Wait, I |Let['\u2019]s test |Good —|Great\.|Perfect\.|Alright|OK,? let me|I[''\u2019]ll |Starting |I need to |Need |I found |I read |I[''\u2019]ve (loaded|confirmed|verified)|Context loaded|Next (I[''\u2019]|step)|Deps confirm|Diff check|Still missing|I[''\u2019]ll (do|check|inspect|trace|run|grab|pull|read|verify))/;
104
+ });
105
+
106
+ // src/ingest/codebase.ts
107
+ var exports_codebase = {};
108
+ __export(exports_codebase, {
109
+ indexCodebase: () => indexCodebase,
110
+ getRemoteFilePath: () => getRemoteFilePath,
111
+ getChangedFiles: () => getChangedFiles,
112
+ detectProjectName: () => detectProjectName
113
+ });
114
+ import { createHash as createHash2 } from "node:crypto";
115
+ import { readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync2, existsSync as existsSync5 } from "node:fs";
116
+ import { join as join6, relative, extname, resolve, basename as basename3 } from "node:path";
117
+ import { execSync } from "node:child_process";
118
+ function parseGitignore(rootPath) {
119
+ const patterns = new Set;
120
+ const gitignorePath = join6(rootPath, ".gitignore");
121
+ if (!existsSync5(gitignorePath))
122
+ return patterns;
123
+ try {
124
+ const content = readFileSync6(gitignorePath, "utf-8");
125
+ for (const line of content.split(`
126
+ `)) {
127
+ const trimmed = line.trim();
128
+ if (!trimmed || trimmed.startsWith("#"))
129
+ continue;
130
+ patterns.add(trimmed.replace(/\/$/, ""));
131
+ }
132
+ } catch {}
133
+ return patterns;
134
+ }
135
+ function shouldIgnore(name, ignorePatterns) {
136
+ if (DEFAULT_IGNORE.has(name))
137
+ return true;
138
+ if (IGNORE_FILES.has(name))
139
+ return true;
140
+ if (name.startsWith(".env"))
141
+ return true;
142
+ if (ignorePatterns.has(name))
143
+ return true;
144
+ return false;
145
+ }
146
+ function langFromExt(ext) {
147
+ return EXT_TO_LANG[ext] || ext.slice(1);
148
+ }
149
+ function formatFile(relPath, content, ext) {
150
+ const lang = langFromExt(ext);
151
+ return `# File: ${relPath}
152
+
153
+ \`\`\`${lang}
154
+ ${content}
155
+ \`\`\``;
156
+ }
157
+ function detectProjectName(rootPath) {
158
+ const readers = [
159
+ () => {
160
+ const pkg = JSON.parse(readFileSync6(join6(rootPath, "package.json"), "utf-8"));
161
+ return typeof pkg.name === "string" ? pkg.name : null;
162
+ },
163
+ () => {
164
+ const content = readFileSync6(join6(rootPath, "Cargo.toml"), "utf-8");
165
+ return content.match(/^name\s*=\s*"([^"]+)"/m)?.[1] ?? null;
166
+ },
167
+ () => {
168
+ const content = readFileSync6(join6(rootPath, "pyproject.toml"), "utf-8");
169
+ return content.match(/^name\s*=\s*"([^"]+)"/m)?.[1] ?? null;
170
+ }
171
+ ];
172
+ for (const reader of readers) {
173
+ try {
174
+ const name = reader();
175
+ if (name)
176
+ return name;
177
+ } catch {}
178
+ }
179
+ return basename3(resolve(rootPath));
180
+ }
181
+ function getChangedFiles(rootPath) {
182
+ try {
183
+ const committed = execSync("git diff --name-only HEAD 2>/dev/null || true", {
184
+ cwd: rootPath,
185
+ encoding: "utf-8",
186
+ stdio: ["pipe", "pipe", "pipe"]
187
+ }).trim();
188
+ const staged = execSync("git diff --name-only --cached 2>/dev/null || true", {
189
+ cwd: rootPath,
190
+ encoding: "utf-8",
191
+ stdio: ["pipe", "pipe", "pipe"]
192
+ }).trim();
193
+ const all = new Set;
194
+ for (const line of [...committed.split(`
195
+ `), ...staged.split(`
196
+ `)]) {
197
+ const f = line.trim();
198
+ if (f.length > 0)
199
+ all.add(f);
200
+ }
201
+ return [...all];
202
+ } catch {
203
+ return null;
204
+ }
205
+ }
206
+ function getRemoteFilePath(memory) {
207
+ const meta = memory.metadata;
208
+ if (!meta || typeof meta !== "object")
209
+ return null;
210
+ const fp = meta.filePath;
211
+ return typeof fp === "string" ? fp : null;
212
+ }
213
+ function indexCodebase(rootPath, options = {}) {
214
+ const absRoot = resolve(rootPath);
215
+ const repoHash = createHash2("sha256").update(absRoot).digest("hex").slice(0, 12);
216
+ const gitignorePatterns = parseGitignore(absRoot);
217
+ const memories = [];
218
+ const localFilePaths = [];
219
+ let fileCount = 0;
220
+ let skipped = 0;
221
+ const project = options.project || detectProjectName(absRoot);
222
+ const changedFiles = options.changedOnly ? getChangedFiles(absRoot) : null;
223
+ const changedSet = changedFiles ? new Set(changedFiles) : null;
224
+ if (options.changedOnly && changedSet !== null && changedSet.size === 0) {
225
+ return { memories, fileCount: 0, skipped: 0, localFilePaths: [], project };
226
+ }
227
+ function walk(dir) {
228
+ let entries;
229
+ try {
230
+ entries = readdirSync4(dir, { withFileTypes: true });
231
+ } catch {
232
+ return;
233
+ }
234
+ for (const entry of entries) {
235
+ if (shouldIgnore(entry.name, gitignorePatterns))
236
+ continue;
237
+ const fullPath = join6(dir, entry.name);
238
+ if (entry.isDirectory()) {
239
+ walk(fullPath);
240
+ continue;
241
+ }
242
+ if (!entry.isFile())
243
+ continue;
244
+ const ext = extname(entry.name).toLowerCase();
245
+ if (!CODE_EXTENSIONS.has(ext) && !SPECIAL_FILES.has(entry.name))
246
+ continue;
247
+ const relPath = relative(absRoot, fullPath);
248
+ localFilePaths.push(relPath);
249
+ if (changedSet && !changedSet.has(relPath)) {
250
+ skipped++;
251
+ continue;
252
+ }
253
+ let stat;
254
+ try {
255
+ stat = statSync2(fullPath);
256
+ } catch {
257
+ continue;
258
+ }
259
+ if (stat.size > MAX_FILE_SIZE || stat.size === 0) {
260
+ skipped++;
261
+ continue;
262
+ }
263
+ let raw;
264
+ try {
265
+ raw = readFileSync6(fullPath, "utf-8");
266
+ } catch {
267
+ skipped++;
268
+ continue;
269
+ }
270
+ if (raw.includes("\x00")) {
271
+ skipped++;
272
+ continue;
273
+ }
274
+ const contentHash = createContentHash(raw);
275
+ const dedupKey = `codebase:${repoHash}:${relPath}`;
276
+ const fingerprint = `${dedupKey}:${contentHash}`;
277
+ if (isIngested("codebase", fingerprint)) {
278
+ skipped++;
279
+ continue;
280
+ }
281
+ const content = formatFile(relPath, raw, ext);
282
+ const metadata = {
283
+ dedupKey,
284
+ contentHash,
285
+ source: "codebase-index",
286
+ repoHash,
287
+ filePath: relPath,
288
+ fileHash: `${relPath}:${contentHash}`,
289
+ language: langFromExt(ext),
290
+ project
291
+ };
292
+ memories.push({ content, containerTag: "codebase", metadata });
293
+ fileCount++;
294
+ }
295
+ }
296
+ walk(absRoot);
297
+ return { memories, fileCount, skipped, localFilePaths, project };
298
+ }
299
+ var DEFAULT_IGNORE, IGNORE_FILES, CODE_EXTENSIONS, SPECIAL_FILES, MAX_FILE_SIZE = 1e5, EXT_TO_LANG;
300
+ var init_codebase = __esm(() => {
301
+ init_shared();
302
+ DEFAULT_IGNORE = new Set([
303
+ "node_modules",
304
+ ".git",
305
+ ".next",
306
+ ".nuxt",
307
+ ".output",
308
+ "dist",
309
+ "build",
310
+ ".cache",
311
+ "__pycache__",
312
+ ".venv",
313
+ "venv",
314
+ "vendor",
315
+ "target",
316
+ ".turbo",
317
+ ".DS_Store",
318
+ ".claude",
319
+ ".conare"
320
+ ]);
321
+ IGNORE_FILES = new Set([
322
+ "package-lock.json",
323
+ "bun.lockb",
324
+ "yarn.lock",
325
+ "pnpm-lock.yaml"
326
+ ]);
327
+ CODE_EXTENSIONS = new Set([
328
+ ".ts",
329
+ ".tsx",
330
+ ".js",
331
+ ".jsx",
332
+ ".mjs",
333
+ ".cjs",
334
+ ".vue",
335
+ ".svelte",
336
+ ".astro",
337
+ ".py",
338
+ ".rb",
339
+ ".go",
340
+ ".rs",
341
+ ".java",
342
+ ".kt",
343
+ ".swift",
344
+ ".c",
345
+ ".cpp",
346
+ ".h",
347
+ ".css",
348
+ ".scss",
349
+ ".less",
350
+ ".html",
351
+ ".json",
352
+ ".yaml",
353
+ ".yml",
354
+ ".toml",
355
+ ".md",
356
+ ".mdx",
357
+ ".txt",
358
+ ".sql",
359
+ ".graphql",
360
+ ".prisma",
361
+ ".sh",
362
+ ".bash",
363
+ ".zsh",
364
+ ".lua",
365
+ ".zig",
366
+ ".ex",
367
+ ".exs"
368
+ ]);
369
+ SPECIAL_FILES = new Set([
370
+ "Dockerfile",
371
+ "Makefile",
372
+ "Procfile",
373
+ "Justfile",
374
+ ".gitignore",
375
+ ".dockerignore",
376
+ "CLAUDE.md",
377
+ "AGENTS.md",
378
+ "README.md",
379
+ "ARCHITECTURE.md",
380
+ "wrangler.toml",
381
+ "wrangler.json"
382
+ ]);
383
+ EXT_TO_LANG = {
384
+ ".ts": "typescript",
385
+ ".tsx": "tsx",
386
+ ".js": "javascript",
387
+ ".jsx": "jsx",
388
+ ".mjs": "javascript",
389
+ ".cjs": "javascript",
390
+ ".vue": "vue",
391
+ ".svelte": "svelte",
392
+ ".astro": "astro",
393
+ ".py": "python",
394
+ ".rb": "ruby",
395
+ ".go": "go",
396
+ ".rs": "rust",
397
+ ".java": "java",
398
+ ".kt": "kotlin",
399
+ ".swift": "swift",
400
+ ".c": "c",
401
+ ".cpp": "cpp",
402
+ ".h": "c",
403
+ ".css": "css",
404
+ ".scss": "scss",
405
+ ".less": "less",
406
+ ".html": "html",
407
+ ".json": "json",
408
+ ".yaml": "yaml",
409
+ ".yml": "yaml",
410
+ ".toml": "toml",
411
+ ".md": "markdown",
412
+ ".mdx": "mdx",
413
+ ".sql": "sql",
414
+ ".graphql": "graphql",
415
+ ".prisma": "prisma",
416
+ ".sh": "bash",
417
+ ".bash": "bash",
418
+ ".zsh": "zsh",
419
+ ".lua": "lua",
420
+ ".zig": "zig",
421
+ ".ex": "elixir",
422
+ ".exs": "elixir"
423
+ };
424
+ });
425
+
426
+ // src/api.ts
427
+ var exports_api = {};
428
+ __export(exports_api, {
429
+ validateKey: () => validateKey,
430
+ uploadBulk: () => uploadBulk,
431
+ listRemoteMemories: () => listRemoteMemories,
432
+ getRemoteMemoryCount: () => getRemoteMemoryCount,
433
+ deleteMemory: () => deleteMemory,
434
+ deleteMemories: () => deleteMemories,
435
+ createUploadBatches: () => createUploadBatches,
436
+ ApiError: () => ApiError
437
+ });
438
+ async function apiRequest(path, apiKey, init) {
439
+ const res = await fetch(`${API_URL}${path}`, {
440
+ ...init,
441
+ headers: {
442
+ Authorization: `Bearer ${apiKey}`,
443
+ "Content-Type": "application/json",
444
+ ...init?.headers || {}
445
+ }
446
+ });
447
+ if (!res.ok) {
448
+ const body = await res.text().catch(() => "");
449
+ throw new ApiError(`${path} failed: HTTP ${res.status}${body ? ` — ${body.slice(0, 200)}` : ""}`, res.status, path);
450
+ }
451
+ return res.json();
452
+ }
453
+ async function listRemoteMemories(apiKey, containerTag) {
454
+ const all = [];
455
+ let offset = 0;
456
+ while (true) {
457
+ const data = await apiRequest(`/api/memories?containerTag=${encodeURIComponent(containerTag)}&limit=${PAGE_SIZE}&offset=${offset}`, apiKey);
458
+ const page = data.memories || [];
459
+ all.push(...page);
460
+ if (page.length < PAGE_SIZE)
461
+ break;
462
+ offset += PAGE_SIZE;
463
+ }
464
+ return all;
465
+ }
466
+ async function deleteMemory(apiKey, memoryId) {
467
+ try {
468
+ await apiRequest(`/api/memories/${memoryId}`, apiKey, { method: "DELETE" });
469
+ return true;
470
+ } catch (err) {
471
+ if (err instanceof ApiError && err.statusCode === 404)
472
+ return false;
473
+ throw err;
474
+ }
475
+ }
476
+ async function deleteMemories(apiKey, memoryIds, onProgress) {
477
+ let deleted = 0;
478
+ const total = memoryIds.length;
479
+ for (let i = 0;i < total; i += DELETE_CONCURRENCY) {
480
+ const batch = memoryIds.slice(i, i + DELETE_CONCURRENCY);
481
+ const results = await Promise.all(batch.map((id) => deleteMemory(apiKey, id)));
482
+ deleted += results.filter(Boolean).length;
483
+ onProgress?.(deleted, total);
484
+ }
485
+ return deleted;
486
+ }
487
+ function createUploadBatches(memories, maxItems = 10, maxChars = 500000) {
488
+ const batches = [];
489
+ let current = [];
490
+ let currentChars = 0;
491
+ let startIndex = 0;
492
+ for (let i = 0;i < memories.length; i++) {
493
+ const item = memories[i];
494
+ const itemChars = item.content.length;
495
+ const exceedsBatch = current.length > 0 && (current.length >= maxItems || currentChars + itemChars > maxChars);
496
+ if (exceedsBatch) {
497
+ batches.push({ startIndex, items: current });
498
+ current = [];
499
+ currentChars = 0;
500
+ startIndex = i;
501
+ }
502
+ current.push(item);
503
+ currentChars += itemChars;
504
+ }
505
+ if (current.length > 0) {
506
+ batches.push({ startIndex, items: current });
507
+ }
508
+ return batches;
509
+ }
510
+ async function validateKey(apiKey) {
511
+ try {
512
+ const data = await apiRequest("/api/auth/me", apiKey);
513
+ return { valid: true, email: data.user.email, name: data.user.name };
514
+ } catch {
515
+ return { valid: false };
516
+ }
517
+ }
518
+ async function getRemoteMemoryCount(apiKey) {
519
+ try {
520
+ const data = await apiRequest("/api/containers", apiKey);
521
+ if (!Array.isArray(data.containers))
522
+ return 0;
523
+ return data.containers.reduce((sum, c) => sum + (c.count || 0), 0);
524
+ } catch {
525
+ return null;
526
+ }
527
+ }
528
+ async function uploadItems(apiKey, items, asyncEmbed = false) {
529
+ let retries = 4;
530
+ while (retries > 0) {
531
+ try {
532
+ const data = await apiRequest("/api/memories/bulk", apiKey, {
533
+ method: "POST",
534
+ body: JSON.stringify(asyncEmbed ? { items, async: true } : items)
535
+ });
536
+ if (!Array.isArray(data.results) || data.results.length !== items.length) {
537
+ throw new Error("Unexpected bulk upload response");
538
+ }
539
+ return data.results;
540
+ } catch (error) {
541
+ if (error instanceof ApiError && error.statusCode === 429) {
542
+ retries--;
543
+ await new Promise((r) => setTimeout(r, 5000));
544
+ continue;
545
+ }
546
+ retries--;
547
+ if (retries === 0) {
548
+ return items.map(() => ({
549
+ success: false,
550
+ error: error instanceof Error ? error.message : String(error)
551
+ }));
552
+ }
553
+ await new Promise((r) => setTimeout(r, 2000));
554
+ }
555
+ }
556
+ return items.map(() => ({ success: false, error: "Upload failed" }));
557
+ }
558
+ async function uploadBulk(apiKey, memories, onProgress) {
559
+ let success = 0;
560
+ let failed = 0;
561
+ const total = memories.length;
562
+ const results = [];
563
+ const batches = createUploadBatches(memories);
564
+ for (const batch of batches) {
565
+ let batchResults = await uploadItems(apiKey, batch.items, true);
566
+ if (batch.items.length > 1 && batchResults.some((result) => !result.success)) {
567
+ const retriedResults = [];
568
+ for (let i = 0;i < batch.items.length; i++) {
569
+ const result = batchResults[i];
570
+ if (result.success) {
571
+ retriedResults.push(result);
572
+ continue;
573
+ }
574
+ const [singleResult] = await uploadItems(apiKey, [batch.items[i]], true);
575
+ retriedResults.push(singleResult);
576
+ }
577
+ batchResults = retriedResults;
578
+ }
579
+ for (let i = 0;i < batchResults.length; i++) {
580
+ const result = batchResults[i];
581
+ results.push({
582
+ index: batch.startIndex + i,
583
+ success: result.success,
584
+ deduped: result.deduped,
585
+ error: result.error
586
+ });
587
+ if (result.success)
588
+ success++;
589
+ else
590
+ failed++;
591
+ }
592
+ onProgress?.(success + failed, total, failed);
593
+ }
594
+ return { success, failed, results };
595
+ }
596
+ var API_URL = "https://mcp.conare.ai", ApiError, PAGE_SIZE = 200, DELETE_CONCURRENCY = 5;
597
+ var init_api = __esm(() => {
598
+ ApiError = class ApiError extends Error {
599
+ statusCode;
600
+ endpoint;
601
+ constructor(message, statusCode, endpoint) {
602
+ super(message);
603
+ this.statusCode = statusCode;
604
+ this.endpoint = endpoint;
605
+ this.name = "ApiError";
606
+ }
607
+ };
608
+ });
609
+
32
610
  // node_modules/sisteransi/src/index.js
33
611
  var require_src = __commonJS((exports, module) => {
34
612
  var ESC = "\x1B";
@@ -794,7 +1372,8 @@ __export(exports_interactive, {
794
1372
  selectChatSources: () => selectChatSources,
795
1373
  promptApiKey: () => promptApiKey,
796
1374
  finishSetup: () => finishSetup,
797
- confirmIndexCodebase: () => confirmIndexCodebase
1375
+ confirmIndexCodebase: () => confirmIndexCodebase,
1376
+ confirmBackgroundSync: () => confirmBackgroundSync
798
1377
  });
799
1378
  function formatDetectedCount(count) {
800
1379
  if (count === undefined)
@@ -866,6 +1445,12 @@ async function confirmIndexCodebase() {
866
1445
  initialValue: false
867
1446
  }));
868
1447
  }
1448
+ async function confirmBackgroundSync() {
1449
+ return ensureValue(await ye({
1450
+ message: "Auto-ingest new chats every 10 minutes?",
1451
+ initialValue: true
1452
+ }));
1453
+ }
869
1454
  var init_interactive = __esm(() => {
870
1455
  init_dist2();
871
1456
  });
@@ -875,10 +1460,10 @@ import { existsSync as existsSync9 } from "node:fs";
875
1460
  import { join as join10 } from "node:path";
876
1461
 
877
1462
  // src/detect.ts
878
- import { existsSync, readdirSync } from "node:fs";
879
- import { spawnSync } from "node:child_process";
1463
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
880
1464
  import { join } from "node:path";
881
1465
  import { homedir, platform } from "node:os";
1466
+ import { createRequire as createRequire2 } from "node:module";
882
1467
  function countJsonlFiles(dir) {
883
1468
  let count = 0;
884
1469
  try {
@@ -892,50 +1477,41 @@ function countJsonlFiles(dir) {
892
1477
  } catch {}
893
1478
  return count;
894
1479
  }
895
- function countCursorSessions(dbPath) {
1480
+ async function countCursorSessions(dbPath) {
896
1481
  try {
897
- const result = spawnSync("sqlite3", [
898
- dbPath,
899
- `
900
- WITH composer_rows AS (
901
- SELECT substr(key, 14) AS composer_id, value AS composer_json
902
- FROM cursorDiskKV
903
- WHERE key LIKE 'composerData:%'
904
- ),
905
- headers AS (
906
- SELECT
907
- composer_id,
908
- json_extract(j.value, '$.bubbleId') AS bubble_id,
909
- json_extract(j.value, '$.type') AS type
910
- FROM composer_rows, json_each(json_extract(composer_json, '$.fullConversationHeadersOnly')) AS j
911
- ),
912
- messages AS (
913
- SELECT h.composer_id, h.type
914
- FROM headers h
915
- JOIN cursorDiskKV kv
916
- ON kv.key = 'bubbleId:' || h.composer_id || ':' || h.bubble_id
917
- WHERE h.type IN (1, 2)
918
- AND length(COALESCE(json_extract(kv.value, '$.text'), '')) >= 50
919
- )
920
- SELECT count(*)
921
- FROM (
922
- SELECT composer_id
923
- FROM messages
924
- GROUP BY composer_id
925
- HAVING SUM(CASE WHEN type = 1 THEN 1 ELSE 0 END) > 0
926
- AND SUM(CASE WHEN type = 2 THEN 1 ELSE 0 END) > 0
927
- );
928
- `.trim()
929
- ], { encoding: "utf-8" });
930
- if (result.status !== 0)
931
- return 0;
932
- const count = Number.parseInt(result.stdout.trim(), 10);
933
- return Number.isFinite(count) ? count : 0;
1482
+ const require2 = createRequire2(import.meta.url);
1483
+ const initSqlJs = require2("sql.js");
1484
+ const SQL = await initSqlJs();
1485
+ const buffer = readFileSync(dbPath);
1486
+ const db = new SQL.Database(buffer);
1487
+ try {
1488
+ const results = db.exec("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
1489
+ if (results.length === 0)
1490
+ return 0;
1491
+ let count = 0;
1492
+ for (const [, value] of results[0].values) {
1493
+ try {
1494
+ const parsed = JSON.parse(value);
1495
+ const headers = parsed.fullConversationHeadersOnly;
1496
+ if (!Array.isArray(headers) || headers.length < 2)
1497
+ continue;
1498
+ const hasUser = headers.some((h) => h.type === 1);
1499
+ const hasAssistant = headers.some((h) => h.type === 2);
1500
+ if (hasUser && hasAssistant)
1501
+ count++;
1502
+ } catch {
1503
+ continue;
1504
+ }
1505
+ }
1506
+ return count;
1507
+ } finally {
1508
+ db.close();
1509
+ }
934
1510
  } catch {
935
1511
  return 0;
936
1512
  }
937
1513
  }
938
- function detect() {
1514
+ async function detect() {
939
1515
  const home = homedir();
940
1516
  const tools = [];
941
1517
  const claudeDir = join(home, ".claude", "projects");
@@ -956,135 +1532,74 @@ function detect() {
956
1532
  let sessionCount = 0;
957
1533
  if (existsSync(codexHistory)) {
958
1534
  try {
959
- const { readFileSync } = __require("node:fs");
960
- const lines = readFileSync(codexHistory, "utf-8").split(`
1535
+ const { readFileSync: readFileSync2 } = __require("node:fs");
1536
+ const lines = readFileSync2(codexHistory, "utf-8").split(`
961
1537
  `).filter(Boolean);
962
1538
  const sessions = new Set(lines.map((l) => {
963
1539
  try {
964
1540
  return JSON.parse(l).session_id;
965
1541
  } catch {
966
1542
  return null;
967
- }
968
- }));
969
- sessions.delete(null);
970
- sessionCount = sessions.size;
971
- } catch {}
972
- }
973
- tools.push({
974
- name: "Codex",
975
- available: true,
976
- path: existsSync(codexHistory) ? codexHistory : codexSessions,
977
- sessionCount
978
- });
979
- } else {
980
- tools.push({ name: "Codex", available: false, path: codexHistory, sessionCount: 0 });
981
- }
982
- const os = platform();
983
- let cursorDbPath;
984
- if (os === "darwin") {
985
- cursorDbPath = join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
986
- } else if (os === "win32") {
987
- cursorDbPath = join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb");
988
- } else {
989
- cursorDbPath = join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
990
- }
991
- tools.push({
992
- name: "Cursor",
993
- available: existsSync(cursorDbPath),
994
- path: cursorDbPath,
995
- sessionCount: existsSync(cursorDbPath) ? countCursorSessions(cursorDbPath) : 0
996
- });
997
- const openclawDir = join(home, ".openclaw");
998
- tools.push({
999
- name: "OpenClaw",
1000
- available: existsSync(openclawDir),
1001
- path: openclawDir,
1002
- sessionCount: 0
1003
- });
1004
- return tools;
1005
- }
1006
-
1007
- // src/ingest/claude.ts
1008
- import { readdirSync as readdirSync2, readFileSync as readFileSync2, existsSync as existsSync3 } from "node:fs";
1009
- import { join as join3, basename } from "node:path";
1010
- import { homedir as homedir3 } from "node:os";
1011
-
1012
- // src/ingest/shared.ts
1013
- import { existsSync as existsSync2, readFileSync, writeFileSync, mkdirSync } from "node:fs";
1014
- import { createHash } from "node:crypto";
1015
- import { join as join2 } from "node:path";
1016
- import { homedir as homedir2 } from "node:os";
1017
- var MANIFEST_PATH = join2(homedir2(), ".conare", "ingested.json");
1018
- var MIN_SUBSTANTIVE = 200;
1019
- var NARRATION_RE = /^[\s\n]*(Let me |Now let me |Now I['\u2019]|Now add |Now fix |Now replace |Now integrate |Now update |Now pass |Now clean |Now build|Update the |Builds clean|Deployed\.|Wait, I |Let['\u2019]s test |Good —|Great\.|Perfect\.|Alright|OK,? let me|I[''\u2019]ll |Starting |I need to |Need |I found |I read |I[''\u2019]ve (loaded|confirmed|verified)|Context loaded|Next (I[''\u2019]|step)|Deps confirm|Diff check|Still missing|I[''\u2019]ll (do|check|inspect|trace|run|grab|pull|read|verify))/;
1020
- function isNarration(text) {
1021
- const stripped = text.trim();
1022
- if (stripped.length < MIN_SUBSTANTIVE)
1023
- return true;
1024
- if (stripped.length <= 300 && NARRATION_RE.test(stripped))
1025
- return true;
1026
- return false;
1027
- }
1028
- function cleanText(raw) {
1029
- let text = raw;
1030
- text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
1031
- text = text.replace(/<attached-context[\s\S]*?<\/attached-context>/g, "");
1032
- text = text.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, "");
1033
- text = text.replace(/^\s*Base directory for this skill:[\s\S]*/g, "");
1034
- text = text.replace(/\nBase directory for this skill:[\s\S]*/g, "");
1035
- text = text.replace(/<!-- vibe-rules Integration -->[\s\S]*?<!-- \/vibe-rules Integration -->/g, "");
1036
- text = text.replace(/<good-behaviour>[\s\S]*?<\/good-behaviour>/g, "");
1037
- text = text.replace(/<frontend-design>[\s\S]*?<\/frontend-design>/g, "");
1038
- text = text.replace(/<architecture>[\s\S]*?<\/architecture>/g, "");
1039
- return text.trim();
1040
- }
1041
- function createContentHash(content) {
1042
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
1043
- }
1044
- function getIngested() {
1045
- try {
1046
- if (existsSync2(MANIFEST_PATH)) {
1047
- return JSON.parse(readFileSync(MANIFEST_PATH, "utf-8"));
1048
- }
1049
- } catch {}
1050
- return {};
1051
- }
1052
- function markIngested(source, sessionIds) {
1053
- const manifest = getIngested();
1054
- const existing = new Set(manifest[source] || []);
1055
- for (const id of sessionIds)
1056
- existing.add(id);
1057
- manifest[source] = [...existing];
1058
- const dir = join2(homedir2(), ".conare");
1059
- if (!existsSync2(dir))
1060
- mkdirSync(dir, { recursive: true });
1061
- writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
1062
- }
1063
- function isIngested(source, sessionId) {
1064
- const manifest = getIngested();
1065
- return (manifest[source] || []).includes(sessionId);
1066
- }
1067
- function clearIngested(source) {
1068
- const manifest = getIngested();
1069
- if (source) {
1070
- delete manifest[source];
1543
+ }
1544
+ }));
1545
+ sessions.delete(null);
1546
+ sessionCount = sessions.size;
1547
+ } catch {}
1548
+ }
1549
+ tools.push({
1550
+ name: "Codex",
1551
+ available: true,
1552
+ path: existsSync(codexHistory) ? codexHistory : codexSessions,
1553
+ sessionCount
1554
+ });
1071
1555
  } else {
1072
- for (const key of Object.keys(manifest))
1073
- delete manifest[key];
1556
+ tools.push({ name: "Codex", available: false, path: codexHistory, sessionCount: 0 });
1074
1557
  }
1075
- const dir = join2(homedir2(), ".conare");
1076
- if (!existsSync2(dir))
1077
- mkdirSync(dir, { recursive: true });
1078
- writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
1558
+ const os = platform();
1559
+ let cursorDbPath;
1560
+ if (os === "darwin") {
1561
+ cursorDbPath = join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
1562
+ } else if (os === "win32") {
1563
+ cursorDbPath = join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb");
1564
+ } else {
1565
+ cursorDbPath = join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
1566
+ }
1567
+ tools.push({
1568
+ name: "Cursor",
1569
+ available: existsSync(cursorDbPath),
1570
+ path: cursorDbPath,
1571
+ sessionCount: existsSync(cursorDbPath) ? await countCursorSessions(cursorDbPath) : 0
1572
+ });
1573
+ const openclawDir = join(home, ".openclaw");
1574
+ tools.push({
1575
+ name: "OpenClaw",
1576
+ available: existsSync(openclawDir),
1577
+ path: openclawDir,
1578
+ sessionCount: 0
1579
+ });
1580
+ return tools;
1079
1581
  }
1080
1582
 
1081
1583
  // src/ingest/claude.ts
1584
+ init_shared();
1585
+ import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
1586
+ import { join as join3, basename } from "node:path";
1587
+ import { homedir as homedir3, platform as platform2 } from "node:os";
1082
1588
  var MAX_CONTENT = 48000;
1083
1589
  var MIN_TURN_LEN = 50;
1084
1590
  function resolveProjectName(dirName) {
1085
1591
  const segments = dirName.replace(/^-/, "").split("-");
1086
- let resolved = "/";
1087
- let i = 0;
1592
+ const isWindows = platform2() === "win32";
1593
+ let resolved;
1594
+ let startIdx;
1595
+ if (isWindows && segments.length > 0 && /^[A-Za-z]$/.test(segments[0])) {
1596
+ resolved = segments[0].toUpperCase() + ":\\";
1597
+ startIdx = 1;
1598
+ } else {
1599
+ resolved = "/";
1600
+ startIdx = 0;
1601
+ }
1602
+ let i = startIdx;
1088
1603
  while (i < segments.length) {
1089
1604
  let found = false;
1090
1605
  for (let end = segments.length;end > i; end--) {
@@ -1103,10 +1618,11 @@ function resolveProjectName(dirName) {
1103
1618
  }
1104
1619
  }
1105
1620
  const home = homedir3();
1106
- if (resolved.startsWith(home + "/")) {
1107
- return resolved.slice(home.length + 1);
1621
+ const sep = isWindows ? "\\" : "/";
1622
+ if (resolved.startsWith(home + sep)) {
1623
+ return resolved.slice(home.length + 1).replace(/\\/g, "/");
1108
1624
  }
1109
- return resolved;
1625
+ return resolved.replace(/\\/g, "/");
1110
1626
  }
1111
1627
  function extractText(content) {
1112
1628
  if (typeof content === "string")
@@ -1182,7 +1698,7 @@ function ingestClaude() {
1182
1698
  }
1183
1699
  for (const file of files) {
1184
1700
  const sessionId = basename(file, ".jsonl");
1185
- const raw = readFileSync2(join3(projPath, file), "utf-8");
1701
+ const raw = readFileSync3(join3(projPath, file), "utf-8");
1186
1702
  const { turns, date } = parseSession(raw.split(`
1187
1703
  `));
1188
1704
  if (turns.length === 0) {
@@ -1191,8 +1707,7 @@ function ingestClaude() {
1191
1707
  }
1192
1708
  const header = `# Chat: ${project}${date ? ` | ${date}` : ""}`;
1193
1709
  const body = turns.map((t) => {
1194
- const q = t.user.length > 300 ? t.user.slice(0, 300) + "..." : t.user;
1195
- return `## Q: ${q}
1710
+ return `## Q: ${t.user}
1196
1711
 
1197
1712
  ${t.assistant}`;
1198
1713
  }).join(`
@@ -1233,103 +1748,32 @@ ${body}`;
1233
1748
  }
1234
1749
 
1235
1750
  // src/ingest/codex.ts
1236
- import { existsSync as existsSync4, readFileSync as readFileSync3, readdirSync as readdirSync3 } from "node:fs";
1751
+ init_shared();
1752
+ import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
1237
1753
  import { join as join4, basename as basename2 } from "node:path";
1238
1754
  import { homedir as homedir4 } from "node:os";
1239
1755
  var MAX_CONTENT2 = 48000;
1240
1756
  function isCodexBoilerplate(text) {
1241
1757
  return text.startsWith("# AGENTS.md instructions for") || text.startsWith("<INSTRUCTIONS>") || text.startsWith("<user_instructions>") || text.startsWith("<user_action>");
1242
1758
  }
1243
- function stripEnvironmentContext(text) {
1244
- let cwd = null;
1245
- const cwdMatch = text.match(/<cwd>([^<]+)<\/cwd>/);
1246
- if (cwdMatch)
1247
- cwd = cwdMatch[1];
1248
- const cleaned = text.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, "").trim();
1249
- return { text: cleaned, cwd };
1759
+ function extractCwd(text) {
1760
+ const match = text.match(/<cwd>([^<]+)<\/cwd>/);
1761
+ return match ? match[1] : null;
1250
1762
  }
1251
1763
  function projectFromCwd(cwd) {
1252
- return cwd.replace(/^\/Users\/[^/]+\//, "");
1764
+ const home = homedir4();
1765
+ const normalized = cwd.replace(/\\/g, "/");
1766
+ const normalizedHome = home.replace(/\\/g, "/");
1767
+ if (normalized.startsWith(normalizedHome + "/")) {
1768
+ return normalized.slice(normalizedHome.length + 1);
1769
+ }
1770
+ return normalized.replace(/^\/Users\/[^/]+\//, "").replace(/^\/home\/[^/]+\//, "").replace(/^[A-Za-z]:\/Users\/[^/]+\//, "");
1253
1771
  }
1254
1772
  function ingestCodex() {
1255
1773
  const memories = [];
1256
1774
  const sessionIds = [];
1257
1775
  let filtered = 0;
1258
1776
  let deduped = 0;
1259
- const historyPath = join4(homedir4(), ".codex", "history.jsonl");
1260
- if (existsSync4(historyPath)) {
1261
- try {
1262
- const lines = readFileSync3(historyPath, "utf-8").split(`
1263
- `).filter(Boolean);
1264
- const sessions = new Map;
1265
- for (const line of lines) {
1266
- try {
1267
- const obj = JSON.parse(line);
1268
- if (!obj.session_id || !obj.text)
1269
- continue;
1270
- if (!sessions.has(obj.session_id))
1271
- sessions.set(obj.session_id, []);
1272
- sessions.get(obj.session_id).push({ ts: obj.ts, text: obj.text });
1273
- } catch {
1274
- continue;
1275
- }
1276
- }
1277
- for (const [sessionId, entries] of sessions) {
1278
- entries.sort((a, b) => a.ts - b.ts);
1279
- const date = new Date(entries[0].ts * 1000).toISOString().slice(0, 10);
1280
- let project = null;
1281
- const cleanEntries = [];
1282
- for (const e of entries) {
1283
- let text = cleanText(e.text);
1284
- if (isCodexBoilerplate(text))
1285
- continue;
1286
- const env = stripEnvironmentContext(text);
1287
- text = env.text;
1288
- if (!project && env.cwd)
1289
- project = projectFromCwd(env.cwd);
1290
- if (text.length === 0)
1291
- continue;
1292
- cleanEntries.push(text.length > 300 ? text.slice(0, 300) + "..." : text);
1293
- }
1294
- const body = cleanEntries.filter(Boolean).join(`
1295
-
1296
- ---
1297
-
1298
- `);
1299
- let content = `# Codex Chat | ${date}
1300
-
1301
- ${body}`;
1302
- if (content.length > MAX_CONTENT2)
1303
- content = content.slice(0, MAX_CONTENT2) + `
1304
-
1305
- [truncated]`;
1306
- if (content.length < 100) {
1307
- filtered++;
1308
- continue;
1309
- }
1310
- const contentHash = createContentHash(content);
1311
- const dedupKey = `codex:${sessionId}`;
1312
- const fingerprint = `${dedupKey}:${contentHash}`;
1313
- if (isIngested("codex", fingerprint)) {
1314
- deduped++;
1315
- continue;
1316
- }
1317
- memories.push({
1318
- content,
1319
- containerTag: "codex-chats",
1320
- metadata: {
1321
- dedupKey,
1322
- contentHash,
1323
- source: "codex",
1324
- sessionId,
1325
- date,
1326
- ...project ? { project } : {}
1327
- }
1328
- });
1329
- sessionIds.push(sessionId);
1330
- }
1331
- } catch {}
1332
- }
1333
1777
  const sessionsDir = join4(homedir4(), ".codex", "sessions");
1334
1778
  if (existsSync4(sessionsDir)) {
1335
1779
  try {
@@ -1348,10 +1792,8 @@ function walkCodexSessions(dir, memories, sessionIds, stats) {
1348
1792
  walkCodexSessions(join4(dir, entry.name), memories, sessionIds, stats);
1349
1793
  } else if (entry.name.endsWith(".jsonl")) {
1350
1794
  const sessionId = basename2(entry.name, ".jsonl");
1351
- if (sessionIds.includes(sessionId))
1352
- continue;
1353
1795
  try {
1354
- const lines = readFileSync3(join4(dir, entry.name), "utf-8").split(`
1796
+ const lines = readFileSync4(join4(dir, entry.name), "utf-8").split(`
1355
1797
  `).filter(Boolean);
1356
1798
  let date = null;
1357
1799
  let project = null;
@@ -1362,19 +1804,29 @@ function walkCodexSessions(dir, memories, sessionIds, stats) {
1362
1804
  try {
1363
1805
  const obj = JSON.parse(line);
1364
1806
  if (!date && obj.timestamp)
1365
- date = obj.timestamp.slice(0, 10);
1807
+ date = typeof obj.timestamp === "string" ? obj.timestamp.slice(0, 10) : null;
1366
1808
  if (obj.type === "session_meta" && obj.payload?.cwd) {
1367
1809
  project = projectFromCwd(obj.payload.cwd);
1368
1810
  }
1369
- if (obj.type !== "response_item" || obj.payload?.type !== "message")
1370
- continue;
1371
- const role = obj.payload.role;
1372
- const msgContent = obj.payload.content;
1373
- if (!Array.isArray(msgContent))
1811
+ let role;
1812
+ let msgContent;
1813
+ if (obj.type === "response_item" && obj.payload?.type === "message") {
1814
+ role = obj.payload.role;
1815
+ msgContent = Array.isArray(obj.payload.content) ? obj.payload.content : undefined;
1816
+ } else if (obj.type === "message" && obj.role) {
1817
+ role = obj.role;
1818
+ msgContent = Array.isArray(obj.content) ? obj.content : undefined;
1819
+ }
1820
+ if (!role || !msgContent)
1374
1821
  continue;
1375
1822
  if (role === "user") {
1376
1823
  for (const block of msgContent) {
1377
1824
  if (block.type === "input_text" && block.text) {
1825
+ if (!project) {
1826
+ const cwd = extractCwd(block.text);
1827
+ if (cwd)
1828
+ project = projectFromCwd(cwd);
1829
+ }
1378
1830
  const text = cleanText(block.text);
1379
1831
  if (isCodexBoilerplate(text))
1380
1832
  continue;
@@ -1409,11 +1861,10 @@ function walkCodexSessions(dir, memories, sessionIds, stats) {
1409
1861
  continue;
1410
1862
  }
1411
1863
  const body = rounds.map((r) => {
1412
- const q = r.user.length > 300 ? r.user.slice(0, 300) + "..." : r.user;
1413
1864
  const assistant = r.assistantParts.join(`
1414
1865
 
1415
1866
  `);
1416
- return `## Q: ${q}
1867
+ return `## Q: ${r.user}
1417
1868
 
1418
1869
  ${assistant}`;
1419
1870
  }).join(`
@@ -1455,9 +1906,10 @@ ${body}`;
1455
1906
  }
1456
1907
 
1457
1908
  // src/ingest/cursor.ts
1458
- import { readFileSync as readFileSync4, statSync } from "node:fs";
1909
+ init_shared();
1910
+ import { readFileSync as readFileSync5, statSync } from "node:fs";
1459
1911
  import { join as join5 } from "node:path";
1460
- import { createRequire as createRequire2 } from "node:module";
1912
+ import { createRequire as createRequire3 } from "node:module";
1461
1913
  var MAX_CONTENT3 = 48000;
1462
1914
  var MAX_DB_SIZE = 2 * 1024 * 1024 * 1024;
1463
1915
  var WARN_DB_SIZE = 500 * 1024 * 1024;
@@ -1465,10 +1917,10 @@ var MIN_TURN_LEN2 = 50;
1465
1917
  function loadSqlJs(wasmDir) {
1466
1918
  try {
1467
1919
  if (wasmDir) {
1468
- const require2 = createRequire2(join5(wasmDir, "sql.js", "package.json"));
1920
+ const require2 = createRequire3(join5(wasmDir, "sql.js", "package.json"));
1469
1921
  return require2("sql.js");
1470
1922
  } else {
1471
- const require2 = createRequire2(import.meta.url);
1923
+ const require2 = createRequire3(import.meta.url);
1472
1924
  return require2("sql.js");
1473
1925
  }
1474
1926
  } catch {
@@ -1481,7 +1933,7 @@ function openDb(initSqlJs, dbPath, wasmDir) {
1481
1933
  locateOpts.locateFile = (file) => join5(wasmDir, "sql.js", "dist", file);
1482
1934
  }
1483
1935
  return initSqlJs(locateOpts).then((SQL) => {
1484
- const buffer = readFileSync4(dbPath);
1936
+ const buffer = readFileSync5(dbPath);
1485
1937
  return new SQL.Database(buffer);
1486
1938
  });
1487
1939
  }
@@ -1528,477 +1980,107 @@ async function ingestCursor(dbPath, wasmDir) {
1528
1980
  }
1529
1981
  if (fileSize > WARN_DB_SIZE) {
1530
1982
  console.log(` Warning: large database (${(fileSize / 1024 / 1024).toFixed(0)}MB), loading into memory...`);
1531
- }
1532
- const initSqlJs = loadSqlJs(wasmDir);
1533
- if (!initSqlJs) {
1534
- console.log(" Skipping Cursor: sql.js not available");
1535
- return { memories, sessionIds, skipped: 0, filtered, deduped };
1536
- }
1537
- let db;
1538
- try {
1539
- db = await openDb(initSqlJs, dbPath, wasmDir);
1540
- } catch (e) {
1541
- console.log(` Skipping Cursor: cannot open database (${e.message})`);
1542
- return { memories, sessionIds, skipped: 0, filtered, deduped };
1543
- }
1544
- try {
1545
- let rows = [];
1546
- try {
1547
- const results = db.exec("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
1548
- if (results.length > 0)
1549
- rows = results[0].values;
1550
- } catch {}
1551
- for (const [key, value] of rows) {
1552
- const composerId = key.replace("composerData:", "");
1553
- let parsed;
1554
- try {
1555
- parsed = JSON.parse(value);
1556
- if (!parsed || typeof parsed !== "object") {
1557
- filtered++;
1558
- continue;
1559
- }
1560
- } catch {
1561
- filtered++;
1562
- continue;
1563
- }
1564
- const bubbleHeaders = parsed.fullConversationHeadersOnly;
1565
- if (!Array.isArray(bubbleHeaders) || bubbleHeaders.length === 0) {
1566
- filtered++;
1567
- continue;
1568
- }
1569
- const turns = extractTurns(db, composerId, bubbleHeaders);
1570
- if (turns.length === 0) {
1571
- filtered++;
1572
- continue;
1573
- }
1574
- const sessionName = parsed.name || "Cursor Chat";
1575
- const date = parsed.createdAt ? new Date(parsed.createdAt).toISOString().slice(0, 10) : "unknown";
1576
- const header = `# ${sessionName} | ${date}`;
1577
- const body = turns.map((t) => {
1578
- const q = t.user.length > 300 ? t.user.slice(0, 300) + "..." : t.user;
1579
- return `## Q: ${q}
1580
-
1581
- ${t.assistant}`;
1582
- }).join(`
1583
-
1584
- ---
1585
-
1586
- `);
1587
- let content = `${header}
1588
-
1589
- ${body}`;
1590
- if (content.length > MAX_CONTENT3)
1591
- content = content.slice(0, MAX_CONTENT3) + `
1592
-
1593
- [truncated]`;
1594
- const contentHash = createContentHash(content);
1595
- const dedupKey = `cursor:${composerId}`;
1596
- const fingerprint = `${dedupKey}:${contentHash}`;
1597
- if (isIngested("cursor", fingerprint)) {
1598
- deduped++;
1599
- continue;
1600
- }
1601
- memories.push({
1602
- content,
1603
- containerTag: "cursor-chats",
1604
- metadata: {
1605
- dedupKey,
1606
- contentHash,
1607
- source: "cursor",
1608
- sessionId: composerId,
1609
- name: sessionName,
1610
- date
1611
- }
1612
- });
1613
- sessionIds.push(composerId);
1614
- }
1615
- } catch (e) {
1616
- console.log(` Cursor ingestion error: ${e.message}`);
1617
- } finally {
1618
- db.close();
1619
- }
1620
- return { memories, sessionIds, skipped: filtered + deduped, filtered, deduped };
1621
- }
1622
-
1623
- // src/ingest/codebase.ts
1624
- import { createHash as createHash2 } from "node:crypto";
1625
- import { readdirSync as readdirSync4, readFileSync as readFileSync5, statSync as statSync2, existsSync as existsSync5 } from "node:fs";
1626
- import { join as join6, relative, extname, resolve } from "node:path";
1627
- var DEFAULT_IGNORE = new Set([
1628
- "node_modules",
1629
- ".git",
1630
- ".next",
1631
- ".nuxt",
1632
- ".output",
1633
- "dist",
1634
- "build",
1635
- ".cache",
1636
- "__pycache__",
1637
- ".venv",
1638
- "venv",
1639
- "vendor",
1640
- "target",
1641
- ".turbo",
1642
- ".DS_Store",
1643
- ".claude",
1644
- ".conare"
1645
- ]);
1646
- var IGNORE_FILES = new Set([
1647
- "package-lock.json",
1648
- "bun.lockb",
1649
- "yarn.lock",
1650
- "pnpm-lock.yaml"
1651
- ]);
1652
- var CODE_EXTENSIONS = new Set([
1653
- ".ts",
1654
- ".tsx",
1655
- ".js",
1656
- ".jsx",
1657
- ".mjs",
1658
- ".cjs",
1659
- ".vue",
1660
- ".svelte",
1661
- ".astro",
1662
- ".py",
1663
- ".rb",
1664
- ".go",
1665
- ".rs",
1666
- ".java",
1667
- ".kt",
1668
- ".swift",
1669
- ".c",
1670
- ".cpp",
1671
- ".h",
1672
- ".css",
1673
- ".scss",
1674
- ".less",
1675
- ".html",
1676
- ".json",
1677
- ".yaml",
1678
- ".yml",
1679
- ".toml",
1680
- ".md",
1681
- ".mdx",
1682
- ".txt",
1683
- ".sql",
1684
- ".graphql",
1685
- ".prisma",
1686
- ".sh",
1687
- ".bash",
1688
- ".zsh",
1689
- ".lua",
1690
- ".zig",
1691
- ".ex",
1692
- ".exs"
1693
- ]);
1694
- var SPECIAL_FILES = new Set([
1695
- "Dockerfile",
1696
- "Makefile",
1697
- "Procfile",
1698
- "Justfile",
1699
- ".gitignore",
1700
- ".dockerignore",
1701
- "CLAUDE.md",
1702
- "AGENTS.md",
1703
- "README.md",
1704
- "ARCHITECTURE.md",
1705
- "wrangler.toml",
1706
- "wrangler.json"
1707
- ]);
1708
- var MAX_FILE_SIZE = 1e5;
1709
- function parseGitignore(rootPath) {
1710
- const patterns = new Set;
1711
- const gitignorePath = join6(rootPath, ".gitignore");
1712
- if (!existsSync5(gitignorePath))
1713
- return patterns;
1714
- try {
1715
- const content = readFileSync5(gitignorePath, "utf-8");
1716
- for (const line of content.split(`
1717
- `)) {
1718
- const trimmed = line.trim();
1719
- if (!trimmed || trimmed.startsWith("#"))
1720
- continue;
1721
- patterns.add(trimmed.replace(/\/$/, ""));
1722
- }
1723
- } catch {}
1724
- return patterns;
1725
- }
1726
- function shouldIgnore(name, ignorePatterns) {
1727
- if (DEFAULT_IGNORE.has(name))
1728
- return true;
1729
- if (IGNORE_FILES.has(name))
1730
- return true;
1731
- if (name.startsWith(".env"))
1732
- return true;
1733
- if (ignorePatterns.has(name))
1734
- return true;
1735
- return false;
1736
- }
1737
- function langFromExt(ext) {
1738
- const map = {
1739
- ".ts": "typescript",
1740
- ".tsx": "tsx",
1741
- ".js": "javascript",
1742
- ".jsx": "jsx",
1743
- ".mjs": "javascript",
1744
- ".cjs": "javascript",
1745
- ".vue": "vue",
1746
- ".svelte": "svelte",
1747
- ".astro": "astro",
1748
- ".py": "python",
1749
- ".rb": "ruby",
1750
- ".go": "go",
1751
- ".rs": "rust",
1752
- ".java": "java",
1753
- ".kt": "kotlin",
1754
- ".swift": "swift",
1755
- ".c": "c",
1756
- ".cpp": "cpp",
1757
- ".h": "c",
1758
- ".css": "css",
1759
- ".scss": "scss",
1760
- ".less": "less",
1761
- ".html": "html",
1762
- ".json": "json",
1763
- ".yaml": "yaml",
1764
- ".yml": "yaml",
1765
- ".toml": "toml",
1766
- ".md": "markdown",
1767
- ".mdx": "mdx",
1768
- ".sql": "sql",
1769
- ".graphql": "graphql",
1770
- ".prisma": "prisma",
1771
- ".sh": "bash",
1772
- ".bash": "bash",
1773
- ".zsh": "zsh",
1774
- ".lua": "lua",
1775
- ".zig": "zig",
1776
- ".ex": "elixir",
1777
- ".exs": "elixir"
1778
- };
1779
- return map[ext] || ext.slice(1);
1780
- }
1781
- function formatFile(relPath, content, ext) {
1782
- const lang = langFromExt(ext);
1783
- return `# File: ${relPath}
1784
-
1785
- \`\`\`${lang}
1786
- ${content}
1787
- \`\`\``;
1788
- }
1789
- function indexCodebase(rootPath) {
1790
- const absRoot = resolve(rootPath);
1791
- const repoHash = createHash2("sha256").update(absRoot).digest("hex").slice(0, 12);
1792
- const gitignorePatterns = parseGitignore(absRoot);
1793
- const memories = [];
1794
- let fileCount = 0;
1795
- let skipped = 0;
1796
- function walk(dir) {
1797
- let entries;
1983
+ }
1984
+ const initSqlJs = loadSqlJs(wasmDir);
1985
+ if (!initSqlJs) {
1986
+ console.log(" Skipping Cursor: sql.js not available");
1987
+ return { memories, sessionIds, skipped: 0, filtered, deduped };
1988
+ }
1989
+ let db;
1990
+ try {
1991
+ db = await openDb(initSqlJs, dbPath, wasmDir);
1992
+ } catch (e) {
1993
+ console.log(` Skipping Cursor: cannot open database (${e.message})`);
1994
+ return { memories, sessionIds, skipped: 0, filtered, deduped };
1995
+ }
1996
+ try {
1997
+ let rows = [];
1798
1998
  try {
1799
- entries = readdirSync4(dir, { withFileTypes: true });
1800
- } catch {
1801
- return;
1802
- }
1803
- for (const entry of entries) {
1804
- if (shouldIgnore(entry.name, gitignorePatterns))
1805
- continue;
1806
- const fullPath = join6(dir, entry.name);
1807
- if (entry.isDirectory()) {
1808
- walk(fullPath);
1809
- continue;
1810
- }
1811
- if (!entry.isFile())
1812
- continue;
1813
- const ext = extname(entry.name).toLowerCase();
1814
- if (!CODE_EXTENSIONS.has(ext) && !SPECIAL_FILES.has(entry.name))
1815
- continue;
1816
- let stat;
1999
+ const results = db.exec("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
2000
+ if (results.length > 0)
2001
+ rows = results[0].values;
2002
+ } catch {}
2003
+ for (const [key, value] of rows) {
2004
+ const composerId = key.replace("composerData:", "");
2005
+ let parsed;
1817
2006
  try {
1818
- stat = statSync2(fullPath);
2007
+ parsed = JSON.parse(value);
2008
+ if (!parsed || typeof parsed !== "object") {
2009
+ filtered++;
2010
+ continue;
2011
+ }
1819
2012
  } catch {
2013
+ filtered++;
1820
2014
  continue;
1821
2015
  }
1822
- if (stat.size > MAX_FILE_SIZE || stat.size === 0) {
1823
- skipped++;
1824
- continue;
1825
- }
1826
- let raw;
1827
- try {
1828
- raw = readFileSync5(fullPath, "utf-8");
1829
- } catch {
1830
- skipped++;
2016
+ const bubbleHeaders = parsed.fullConversationHeadersOnly;
2017
+ if (!Array.isArray(bubbleHeaders) || bubbleHeaders.length === 0) {
2018
+ filtered++;
1831
2019
  continue;
1832
2020
  }
1833
- if (raw.includes("\x00")) {
1834
- skipped++;
2021
+ const turns = extractTurns(db, composerId, bubbleHeaders);
2022
+ if (turns.length === 0) {
2023
+ filtered++;
1835
2024
  continue;
1836
2025
  }
1837
- const relPath = relative(absRoot, fullPath);
1838
- const contentHash = createContentHash(raw);
1839
- const dedupKey = `codebase:${repoHash}:${relPath}`;
2026
+ const sessionName = parsed.name || "Cursor Chat";
2027
+ const date = parsed.createdAt ? new Date(parsed.createdAt).toISOString().slice(0, 10) : "unknown";
2028
+ const header = `# ${sessionName} | ${date}`;
2029
+ const body = turns.map((t) => {
2030
+ return `## Q: ${t.user}
2031
+
2032
+ ${t.assistant}`;
2033
+ }).join(`
2034
+
2035
+ ---
2036
+
2037
+ `);
2038
+ let content = `${header}
2039
+
2040
+ ${body}`;
2041
+ if (content.length > MAX_CONTENT3)
2042
+ content = content.slice(0, MAX_CONTENT3) + `
2043
+
2044
+ [truncated]`;
2045
+ const contentHash = createContentHash(content);
2046
+ const dedupKey = `cursor:${composerId}`;
1840
2047
  const fingerprint = `${dedupKey}:${contentHash}`;
1841
- if (isIngested("codebase", fingerprint)) {
1842
- skipped++;
2048
+ if (isIngested("cursor", fingerprint)) {
2049
+ deduped++;
1843
2050
  continue;
1844
2051
  }
1845
- const content = formatFile(relPath, raw, ext);
1846
2052
  memories.push({
1847
2053
  content,
1848
- containerTag: "codebase",
2054
+ containerTag: "cursor-chats",
1849
2055
  metadata: {
1850
2056
  dedupKey,
1851
2057
  contentHash,
1852
- source: "codebase-index",
1853
- repoHash,
1854
- filePath: relPath,
1855
- fileHash: `${relPath}:${contentHash}`,
1856
- language: langFromExt(ext)
2058
+ source: "cursor",
2059
+ sessionId: composerId,
2060
+ name: sessionName,
2061
+ date
1857
2062
  }
1858
2063
  });
1859
- fileCount++;
2064
+ sessionIds.push(composerId);
1860
2065
  }
2066
+ } catch (e) {
2067
+ console.log(` Cursor ingestion error: ${e.message}`);
2068
+ } finally {
2069
+ db.close();
1861
2070
  }
1862
- walk(absRoot);
1863
- return { memories, fileCount, skipped };
2071
+ return { memories, sessionIds, skipped: filtered + deduped, filtered, deduped };
1864
2072
  }
1865
2073
 
1866
- // src/api.ts
1867
- var API_URL = "https://mcp.conare.ai";
1868
- function createUploadBatches(memories, maxItems = 50, maxChars = 1500000) {
1869
- const batches = [];
1870
- let current = [];
1871
- let currentChars = 0;
1872
- let startIndex = 0;
1873
- for (let i = 0;i < memories.length; i++) {
1874
- const item = memories[i];
1875
- const itemChars = item.content.length;
1876
- const exceedsBatch = current.length > 0 && (current.length >= maxItems || currentChars + itemChars > maxChars);
1877
- if (exceedsBatch) {
1878
- batches.push({ startIndex, items: current });
1879
- current = [];
1880
- currentChars = 0;
1881
- startIndex = i;
1882
- }
1883
- current.push(item);
1884
- currentChars += itemChars;
1885
- }
1886
- if (current.length > 0) {
1887
- batches.push({ startIndex, items: current });
1888
- }
1889
- return batches;
1890
- }
1891
- async function validateKey(apiKey) {
1892
- try {
1893
- const res = await fetch(`${API_URL}/api/auth/me`, {
1894
- headers: { Authorization: `Bearer ${apiKey}` }
1895
- });
1896
- if (!res.ok)
1897
- return { valid: false };
1898
- const data = await res.json();
1899
- return { valid: true, email: data.user.email, name: data.user.name };
1900
- } catch {
1901
- return { valid: false };
1902
- }
1903
- }
1904
- async function getRemoteMemoryCount(apiKey) {
1905
- try {
1906
- const res = await fetch(`${API_URL}/api/containers`, {
1907
- headers: { Authorization: `Bearer ${apiKey}` }
1908
- });
1909
- if (!res.ok)
1910
- return null;
1911
- const data = await res.json();
1912
- if (!Array.isArray(data.containers))
1913
- return 0;
1914
- return data.containers.reduce((sum, container) => sum + (container.count || 0), 0);
1915
- } catch {
1916
- return null;
1917
- }
1918
- }
1919
- async function uploadItems(apiKey, items, asyncEmbed = false) {
1920
- let retries = 4;
1921
- while (retries > 0) {
1922
- try {
1923
- const res = await fetch(`${API_URL}/api/memories/bulk`, {
1924
- method: "POST",
1925
- headers: {
1926
- "Content-Type": "application/json",
1927
- Authorization: `Bearer ${apiKey}`
1928
- },
1929
- body: JSON.stringify(asyncEmbed ? { items, async: true } : items)
1930
- });
1931
- if (res.status === 429) {
1932
- retries--;
1933
- await new Promise((r) => setTimeout(r, 5000));
1934
- continue;
1935
- }
1936
- if (!res.ok) {
1937
- const body = await res.text().catch(() => "");
1938
- throw new Error(`HTTP ${res.status}: ${body.slice(0, 120)}`);
1939
- }
1940
- const data = await res.json();
1941
- if (!Array.isArray(data.results) || data.results.length !== items.length) {
1942
- throw new Error("Unexpected bulk upload response");
1943
- }
1944
- return data.results;
1945
- } catch (error) {
1946
- retries--;
1947
- if (retries === 0) {
1948
- return items.map(() => ({
1949
- success: false,
1950
- error: error instanceof Error ? error.message : String(error)
1951
- }));
1952
- }
1953
- await new Promise((r) => setTimeout(r, 2000));
1954
- }
1955
- }
1956
- return items.map(() => ({ success: false, error: "Upload failed" }));
1957
- }
1958
- async function uploadBulk(apiKey, memories, onProgress) {
1959
- let success = 0;
1960
- let failed = 0;
1961
- const total = memories.length;
1962
- const results = [];
1963
- const batches = createUploadBatches(memories);
1964
- for (const batch of batches) {
1965
- let batchResults = await uploadItems(apiKey, batch.items, true);
1966
- if (batch.items.length > 1 && batchResults.some((result) => !result.success)) {
1967
- const retriedResults = [];
1968
- for (let i = 0;i < batch.items.length; i++) {
1969
- const result = batchResults[i];
1970
- if (result.success) {
1971
- retriedResults.push(result);
1972
- continue;
1973
- }
1974
- const [singleResult] = await uploadItems(apiKey, [batch.items[i]], true);
1975
- retriedResults.push(singleResult);
1976
- }
1977
- batchResults = retriedResults;
1978
- }
1979
- for (let i = 0;i < batchResults.length; i++) {
1980
- const result = batchResults[i];
1981
- results.push({
1982
- index: batch.startIndex + i,
1983
- success: result.success,
1984
- deduped: result.deduped,
1985
- error: result.error
1986
- });
1987
- if (result.success)
1988
- success++;
1989
- else
1990
- failed++;
1991
- }
1992
- onProgress?.(success + failed, total, failed);
1993
- }
1994
- return { success, failed, results };
1995
- }
2074
+ // src/index.ts
2075
+ init_codebase();
2076
+ init_shared();
2077
+ init_api();
1996
2078
 
1997
2079
  // src/configure.ts
1998
- import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync6, writeFileSync as writeFileSync2 } from "node:fs";
2080
+ import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
1999
2081
  import { dirname, join as join7 } from "node:path";
2000
- import { homedir as homedir5 } from "node:os";
2001
- import { spawnSync as spawnSync2 } from "node:child_process";
2082
+ import { homedir as homedir5, platform as platform4 } from "node:os";
2083
+ import { spawnSync } from "node:child_process";
2002
2084
  var CONARE_URL = "https://mcp.conare.ai";
2003
2085
  var SERVER_NAME = "conare-memory";
2004
2086
  var MCP_TARGETS = [
@@ -2009,7 +2091,7 @@ var MCP_TARGETS = [
2009
2091
  ];
2010
2092
  function readJsonFile(path) {
2011
2093
  try {
2012
- return JSON.parse(readFileSync6(path, "utf-8"));
2094
+ return JSON.parse(readFileSync7(path, "utf-8"));
2013
2095
  } catch {
2014
2096
  return {};
2015
2097
  }
@@ -2039,8 +2121,9 @@ function upsertMcpServer(path, apiKey) {
2039
2121
  function configureClaude(apiKey) {
2040
2122
  const claudeConfigPath = join7(homedir5(), ".claude.json");
2041
2123
  const claudeMcpPath = join7(homedir5(), ".claude", "mcp.json");
2042
- if (spawnSync2("claude", ["mcp", "add-json", SERVER_NAME, "--scope", "user", JSON.stringify(getServerConfig(apiKey))], {
2043
- stdio: "ignore"
2124
+ if (spawnSync("claude", ["mcp", "add-json", SERVER_NAME, "--scope", "user", JSON.stringify(getServerConfig(apiKey))], {
2125
+ stdio: "ignore",
2126
+ shell: platform4() === "win32"
2044
2127
  }).status === 0) {
2045
2128
  return "Claude Code configured via `claude mcp add-json`";
2046
2129
  }
@@ -2072,7 +2155,7 @@ function configureMcp(apiKey, targets = ["claude", "cursor", "codex"]) {
2072
2155
  }
2073
2156
 
2074
2157
  // src/config.ts
2075
- import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
2158
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
2076
2159
  import { join as join8 } from "node:path";
2077
2160
  import { homedir as homedir6 } from "node:os";
2078
2161
  var CONFIG_DIR = join8(homedir6(), ".conare");
@@ -2081,7 +2164,7 @@ function readConfig() {
2081
2164
  try {
2082
2165
  if (!existsSync7(CONFIG_PATH))
2083
2166
  return {};
2084
- return JSON.parse(readFileSync7(CONFIG_PATH, "utf-8"));
2167
+ return JSON.parse(readFileSync8(CONFIG_PATH, "utf-8"));
2085
2168
  } catch {
2086
2169
  return {};
2087
2170
  }
@@ -2096,10 +2179,10 @@ function getSavedApiKey() {
2096
2179
  }
2097
2180
 
2098
2181
  // src/sync.ts
2099
- import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync8, chmodSync, cpSync, rmSync, symlinkSync, readlinkSync, appendFileSync } from "node:fs";
2182
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync9, chmodSync, cpSync, rmSync, symlinkSync, readlinkSync, appendFileSync } from "node:fs";
2100
2183
  import { join as join9, dirname as dirname2 } from "node:path";
2101
- import { homedir as homedir7, platform as platform2 } from "node:os";
2102
- import { execSync } from "node:child_process";
2184
+ import { homedir as homedir7, platform as platform5 } from "node:os";
2185
+ import { execSync as execSync2 } from "node:child_process";
2103
2186
  var CONARE_DIR = join9(homedir7(), ".conare");
2104
2187
  var BIN_DIR = join9(CONARE_DIR, "bin");
2105
2188
  var CONFIG_PATH2 = join9(CONARE_DIR, "config.json");
@@ -2108,6 +2191,43 @@ var PLIST_PATH = join9(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.pl
2108
2191
  var SYSTEMD_DIR = join9(homedir7(), ".config", "systemd", "user");
2109
2192
  var SYSTEMD_SERVICE = join9(SYSTEMD_DIR, "conare-sync.service");
2110
2193
  var SYSTEMD_TIMER = join9(SYSTEMD_DIR, "conare-sync.timer");
2194
+ var TASK_NAME = "ConareMemorySync";
2195
+ var RUN_VBS = `Set WshShell = CreateObject("WScript.Shell")
2196
+ WshShell.Run """" & CreateObject("WScript.Shell").ExpandEnvironmentStrings("%USERPROFILE%") & "\\.conare\\bin\\run.cmd" & """", 0, True
2197
+ `;
2198
+ var RUN_CMD = `@echo off
2199
+ REM Conare Memory — background sync wrapper (Windows)
2200
+ setlocal
2201
+
2202
+ set "CONARE_DIR=%USERPROFILE%\\.conare"
2203
+ set "LOG=%CONARE_DIR%\\ingest.log"
2204
+ set "LOCKFILE=%CONARE_DIR%\\sync.lock.d"
2205
+
2206
+ REM File lock: prevent concurrent runs
2207
+ if exist "%LOCKFILE%" (
2208
+ echo %DATE% %TIME% SKIP: another instance running >> "%LOG%"
2209
+ exit /b 0
2210
+ )
2211
+ mkdir "%LOCKFILE%" 2>nul
2212
+ if errorlevel 1 (
2213
+ echo %DATE% %TIME% SKIP: lock failed >> "%LOG%"
2214
+ exit /b 0
2215
+ )
2216
+
2217
+ REM Resolve node
2218
+ where node >nul 2>nul
2219
+ if errorlevel 1 (
2220
+ echo %DATE% %TIME% ERROR: node not found >> "%LOG%"
2221
+ rmdir "%LOCKFILE%" 2>nul
2222
+ exit /b 1
2223
+ )
2224
+
2225
+ echo %DATE% %TIME% START sync >> "%LOG%"
2226
+ node "%CONARE_DIR%\\bin\\conare-ingest.mjs" --config-file "%CONARE_DIR%\\config.json" --ingest-only --quiet 2>> "%LOG%"
2227
+ echo %DATE% %TIME% DONE sync (exit %ERRORLEVEL%) >> "%LOG%"
2228
+
2229
+ rmdir "%LOCKFILE%" 2>nul
2230
+ `;
2111
2231
  var RUN_SH = `#!/bin/bash
2112
2232
  # Conare Memory — background sync wrapper
2113
2233
  # Resolves node at runtime to survive nvm/fnm upgrades
@@ -2218,7 +2338,7 @@ WantedBy=timers.target
2218
2338
  }
2219
2339
  function hasSystemd() {
2220
2340
  try {
2221
- execSync("systemctl --user status 2>/dev/null", { stdio: "ignore" });
2341
+ execSync2("systemctl --user status 2>/dev/null", { stdio: "ignore" });
2222
2342
  return true;
2223
2343
  } catch {
2224
2344
  return false;
@@ -2226,7 +2346,7 @@ function hasSystemd() {
2226
2346
  }
2227
2347
  function uid() {
2228
2348
  try {
2229
- return execSync("id -u", { encoding: "utf-8" }).trim();
2349
+ return execSync2("id -u", { encoding: "utf-8" }).trim();
2230
2350
  } catch {
2231
2351
  return "501";
2232
2352
  }
@@ -2238,7 +2358,7 @@ function persistBinary(apiKey) {
2238
2358
  throw new Error("Could not locate CLI bundle. Run from an installed conare package.");
2239
2359
  }
2240
2360
  const dest = join9(BIN_DIR, "conare-ingest.mjs");
2241
- const content = readFileSync8(cliEntry, "utf-8");
2361
+ const content = readFileSync9(cliEntry, "utf-8");
2242
2362
  writeFileSync4(dest, content);
2243
2363
  const sqlJsDir = findSqlJs();
2244
2364
  if (sqlJsDir) {
@@ -2250,7 +2370,13 @@ function persistBinary(apiKey) {
2250
2370
  }
2251
2371
  const runShPath = join9(BIN_DIR, "run.sh");
2252
2372
  writeFileSync4(runShPath, RUN_SH);
2253
- chmodSync(runShPath, 493);
2373
+ try {
2374
+ chmodSync(runShPath, 493);
2375
+ } catch {}
2376
+ const runCmdPath = join9(BIN_DIR, "run.cmd");
2377
+ writeFileSync4(runCmdPath, RUN_CMD);
2378
+ const runVbsPath = join9(BIN_DIR, "run.vbs");
2379
+ writeFileSync4(runVbsPath, RUN_VBS);
2254
2380
  writeFileSync4(CONFIG_PATH2, JSON.stringify({ apiKey }, null, 2) + `
2255
2381
  `);
2256
2382
  }
@@ -2286,13 +2412,13 @@ function setupMacOS(intervalMinutes) {
2286
2412
  writeFileSync4(PLIST_PATH, makePlist(intervalMinutes));
2287
2413
  const id = uid();
2288
2414
  try {
2289
- execSync(`launchctl bootout gui/${id} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
2415
+ execSync2(`launchctl bootout gui/${id} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
2290
2416
  } catch {}
2291
2417
  try {
2292
- execSync(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
2418
+ execSync2(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
2293
2419
  } catch {
2294
2420
  try {
2295
- execSync(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
2421
+ execSync2(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
2296
2422
  } catch {
2297
2423
  throw new Error("Failed to load launchd agent. Try manually: launchctl load " + PLIST_PATH);
2298
2424
  }
@@ -2302,27 +2428,51 @@ function setupLinuxSystemd(intervalMinutes) {
2302
2428
  mkdirSync4(SYSTEMD_DIR, { recursive: true });
2303
2429
  writeFileSync4(SYSTEMD_SERVICE, SYSTEMD_SERVICE_CONTENT);
2304
2430
  writeFileSync4(SYSTEMD_TIMER, makeSystemdTimer(intervalMinutes));
2305
- execSync("systemctl --user daemon-reload", { stdio: "ignore" });
2306
- execSync("systemctl --user enable --now conare-sync.timer", { stdio: "ignore" });
2431
+ execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
2432
+ execSync2("systemctl --user enable --now conare-sync.timer", { stdio: "ignore" });
2307
2433
  }
2308
2434
  function setupLinuxCron(intervalMinutes) {
2309
2435
  const cronCmd = `${homedir7()}/.conare/bin/run.sh`;
2310
2436
  const cronLine = `*/${intervalMinutes} * * * * ${cronCmd}`;
2311
2437
  try {
2312
- const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
2438
+ const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
2313
2439
  const filtered = existing.split(`
2314
2440
  `).filter((l) => !l.includes("conare")).join(`
2315
2441
  `);
2316
2442
  const newCrontab = (filtered.trim() ? filtered.trim() + `
2317
2443
  ` : "") + cronLine + `
2318
2444
  `;
2319
- execSync("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
2445
+ execSync2("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
2320
2446
  } catch {
2321
- execSync("crontab -", { input: cronLine + `
2447
+ execSync2("crontab -", { input: cronLine + `
2322
2448
  `, stdio: ["pipe", "ignore", "ignore"] });
2323
2449
  }
2324
2450
  }
2325
2451
  function installGlobalCommand() {
2452
+ const isWindows = platform5() === "win32";
2453
+ if (isWindows) {
2454
+ const wrapper2 = join9(BIN_DIR, "conare.cmd");
2455
+ const content2 = `@echo off\r
2456
+ where node >nul 2>nul\r
2457
+ if errorlevel 1 (\r
2458
+ echo Error: node not found. Install Node.js first. >&2\r
2459
+ exit /b 1\r
2460
+ )\r
2461
+ node "%USERPROFILE%\\.conare\\bin\\conare-ingest.mjs" %*\r
2462
+ `;
2463
+ writeFileSync4(wrapper2, content2);
2464
+ const pathDirs = (process.env.PATH || "").split(";");
2465
+ if (pathDirs.some((d) => d.toLowerCase() === BIN_DIR.toLowerCase())) {
2466
+ return "Global command: conare (via .conare\\bin in PATH)";
2467
+ }
2468
+ const binDirWin = BIN_DIR.replace(/\//g, "\\");
2469
+ try {
2470
+ execSync2(`powershell -NoProfile -Command "$p = [Environment]::GetEnvironmentVariable('PATH','User'); if ($p -and $p.ToLower().Contains('${binDirWin.toLowerCase().replace(/\\/g, "\\\\")}')) { exit 0 }; [Environment]::SetEnvironmentVariable('PATH', $(if($p){$p + ';'} else {''}) + '${binDirWin.replace(/\\/g, "\\\\")}', 'User')"`, { stdio: "ignore" });
2471
+ return `Global command: conare (added .conare\\bin to user PATH — restart terminal)`;
2472
+ } catch {
2473
+ return `Global command: add ${binDirWin} to your PATH manually`;
2474
+ }
2475
+ }
2326
2476
  const wrapper = join9(BIN_DIR, "conare");
2327
2477
  const content = `#!/bin/bash
2328
2478
  # Conare global command — runs the persisted CLI bundle
@@ -2362,7 +2512,7 @@ exec "$NODE" "$CONARE_DIR/bin/conare-ingest.mjs" "$@"
2362
2512
  const shellProfile = getShellProfile();
2363
2513
  if (shellProfile) {
2364
2514
  try {
2365
- const profileContent = existsSync8(shellProfile) ? readFileSync8(shellProfile, "utf-8") : "";
2515
+ const profileContent = existsSync8(shellProfile) ? readFileSync9(shellProfile, "utf-8") : "";
2366
2516
  const exportLine = `export PATH="$HOME/.conare/bin:$PATH"`;
2367
2517
  if (!profileContent.includes(".conare/bin")) {
2368
2518
  appendFileSync(shellProfile, `
@@ -2386,7 +2536,7 @@ function getShellProfile() {
2386
2536
  return join9(home, ".zshrc");
2387
2537
  if (shell.includes("bash")) {
2388
2538
  const profile = join9(home, ".bash_profile");
2389
- if (platform2() === "darwin" && existsSync8(profile))
2539
+ if (platform5() === "darwin" && existsSync8(profile))
2390
2540
  return profile;
2391
2541
  return join9(home, ".bashrc");
2392
2542
  }
@@ -2396,9 +2546,16 @@ function getShellProfile() {
2396
2546
  return join9(home, ".bashrc");
2397
2547
  return null;
2398
2548
  }
2549
+ function setupWindows(intervalMinutes) {
2550
+ const runVbs = join9(BIN_DIR, "run.vbs").replace(/\//g, "\\");
2551
+ try {
2552
+ execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2553
+ } catch {}
2554
+ execSync2(`schtasks /Create /TN "${TASK_NAME}" /TR "wscript.exe \\"${runVbs}\\"" /SC MINUTE /MO ${intervalMinutes} /F`, { stdio: "ignore" });
2555
+ }
2399
2556
  function installSync(apiKey, intervalMinutes = 10) {
2400
2557
  const messages = [];
2401
- const os = platform2();
2558
+ const os = platform5();
2402
2559
  persistBinary(apiKey);
2403
2560
  messages.push("Persisted CLI to ~/.conare/bin/");
2404
2561
  messages.push("Saved config to ~/.conare/config.json");
@@ -2409,6 +2566,9 @@ function installSync(apiKey, intervalMinutes = 10) {
2409
2566
  setupMacOS(intervalMinutes);
2410
2567
  messages.push(`Installed launchd agent (every ${intervalMinutes} min)`);
2411
2568
  messages.push(`Plist: ${PLIST_PATH}`);
2569
+ } else if (os === "win32") {
2570
+ setupWindows(intervalMinutes);
2571
+ messages.push(`Installed Windows Task Scheduler (every ${intervalMinutes} min)`);
2412
2572
  } else if (os === "linux") {
2413
2573
  if (hasSystemd()) {
2414
2574
  setupLinuxSystemd(intervalMinutes);
@@ -2424,35 +2584,40 @@ function installSync(apiKey, intervalMinutes = 10) {
2424
2584
  }
2425
2585
  function uninstallSync() {
2426
2586
  const messages = [];
2427
- const os = platform2();
2587
+ const os = platform5();
2428
2588
  if (os === "darwin") {
2429
2589
  try {
2430
- execSync(`launchctl bootout gui/${uid()} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
2590
+ execSync2(`launchctl bootout gui/${uid()} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
2431
2591
  } catch {}
2432
2592
  if (existsSync8(PLIST_PATH)) {
2433
2593
  unlinkSync(PLIST_PATH);
2434
2594
  messages.push("Removed launchd agent");
2435
2595
  }
2596
+ } else if (os === "win32") {
2597
+ try {
2598
+ execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2599
+ messages.push("Removed Windows scheduled task");
2600
+ } catch {}
2436
2601
  } else if (os === "linux") {
2437
2602
  if (hasSystemd()) {
2438
2603
  try {
2439
- execSync("systemctl --user disable --now conare-sync.timer 2>/dev/null", { stdio: "ignore" });
2604
+ execSync2("systemctl --user disable --now conare-sync.timer 2>/dev/null", { stdio: "ignore" });
2440
2605
  } catch {}
2441
2606
  if (existsSync8(SYSTEMD_SERVICE))
2442
2607
  unlinkSync(SYSTEMD_SERVICE);
2443
2608
  if (existsSync8(SYSTEMD_TIMER))
2444
2609
  unlinkSync(SYSTEMD_TIMER);
2445
2610
  try {
2446
- execSync("systemctl --user daemon-reload", { stdio: "ignore" });
2611
+ execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
2447
2612
  } catch {}
2448
2613
  messages.push("Removed systemd timer");
2449
2614
  } else {
2450
2615
  try {
2451
- const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
2616
+ const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
2452
2617
  const filtered = existing.split(`
2453
2618
  `).filter((l) => !l.includes("conare")).join(`
2454
2619
  `);
2455
- execSync("crontab -", { input: filtered.trim() + `
2620
+ execSync2("crontab -", { input: filtered.trim() + `
2456
2621
  `, stdio: ["pipe", "ignore", "ignore"] });
2457
2622
  messages.push("Removed cron job");
2458
2623
  } catch {}
@@ -2547,6 +2712,8 @@ function parseArgs() {
2547
2712
  let source;
2548
2713
  let wasmDir;
2549
2714
  let indexPath;
2715
+ let indexProject;
2716
+ let indexChanged = false;
2550
2717
  for (let i = 0;i < args.length; i++) {
2551
2718
  if (args[i] === "--key" && args[i + 1]) {
2552
2719
  key = args[++i];
@@ -2570,6 +2737,10 @@ function parseArgs() {
2570
2737
  wasmDir = args[++i];
2571
2738
  } else if (args[i] === "--index") {
2572
2739
  indexPath = args[i + 1] && !args[i + 1].startsWith("--") ? args[++i] : ".";
2740
+ } else if (args[i] === "--project" && args[i + 1]) {
2741
+ indexProject = args[++i];
2742
+ } else if (args[i] === "--changed") {
2743
+ indexChanged = true;
2573
2744
  } else if (args[i] === "--ingest-only") {
2574
2745
  ingestOnly = true;
2575
2746
  } else if (args[i] === "--config-only") {
@@ -2590,6 +2761,8 @@ Options:
2590
2761
  --key <key> Your Conare API key (required, starts with cmem_)
2591
2762
  --config-file <path> Read API key from JSON config file (e.g. ~/.conare/config.json)
2592
2763
  --index [path] Index codebase at path (default: current directory)
2764
+ --project <name> Project name for codebase indexing (auto-detected if omitted)
2765
+ --changed Only index files changed in the last git commit
2593
2766
  --dry-run Preview what would be uploaded
2594
2767
  --force Re-ingest all / re-index all (bypass dedup)
2595
2768
  --quiet Suppress all stdout output (for background timer runs)
@@ -2615,7 +2788,7 @@ Get your API key at https://mcp.conare.ai
2615
2788
  console.error("Error: --ingest-only and --config-only cannot be used together");
2616
2789
  process.exit(1);
2617
2790
  }
2618
- return { key, configFile, dryRun, force, ingestOnly, configOnly, interactive, quiet, installSync: installSyncFlag, uninstallSync: uninstallSyncFlag, syncInterval, source, wasmDir, indexPath };
2791
+ return { key, configFile, dryRun, force, ingestOnly, configOnly, interactive, quiet, installSync: installSyncFlag, uninstallSync: uninstallSyncFlag, syncInterval, source, wasmDir, indexPath, indexProject, indexChanged };
2619
2792
  }
2620
2793
  async function runInstall() {
2621
2794
  const args = process.argv.slice(3);
@@ -2658,8 +2831,8 @@ async function main() {
2658
2831
  let configFileKey;
2659
2832
  if (opts.configFile) {
2660
2833
  try {
2661
- const { readFileSync: readFileSync9 } = await import("node:fs");
2662
- const raw = JSON.parse(readFileSync9(opts.configFile, "utf-8"));
2834
+ const { readFileSync: readFileSync10 } = await import("node:fs");
2835
+ const raw = JSON.parse(readFileSync10(opts.configFile, "utf-8"));
2663
2836
  configFileKey = raw.apiKey || raw.key;
2664
2837
  if (!configFileKey) {
2665
2838
  console.error(`Error: no apiKey/key found in ${opts.configFile}`);
@@ -2691,7 +2864,7 @@ async function main() {
2691
2864
  }));
2692
2865
  let interactiveMode = false;
2693
2866
  if (shouldRunInteractive) {
2694
- const detectedTools = detect();
2867
+ const detectedTools = await detect();
2695
2868
  interactiveTargets = MCP_TARGETS.map((target) => {
2696
2869
  const detected = detectedTools.find((tool) => target.id === "claude" && tool.name === "Claude Code" || target.id === "cursor" && tool.name === "Cursor" || target.id === "codex" && tool.name === "Codex" || target.id === "openclaw" && tool.name === "OpenClaw");
2697
2870
  return {
@@ -2779,15 +2952,52 @@ async function main() {
2779
2952
  } catch {}
2780
2953
  }
2781
2954
  const idxSpinner = Y2();
2782
- idxSpinner.start("Scanning codebase...");
2783
- const { memories, fileCount, skipped } = indexCodebase(absPath);
2784
- idxSpinner.stop(`Codebase: ${fileCount} files found, ${skipped} skipped`);
2955
+ const scanLabel = opts.indexChanged ? "Scanning changed files..." : "Scanning codebase...";
2956
+ idxSpinner.start(scanLabel);
2957
+ const { memories, fileCount, skipped, localFilePaths, project } = indexCodebase(absPath, {
2958
+ changedOnly: opts.indexChanged,
2959
+ project: opts.indexProject
2960
+ });
2961
+ idxSpinner.stop(`Codebase: ${fileCount} files found, ${skipped} skipped (project: ${project})`);
2962
+ if (!opts.dryRun && !opts.indexChanged) {
2963
+ const cleanupSpinner = Y2();
2964
+ cleanupSpinner.start("Checking for stale/deleted files...");
2965
+ try {
2966
+ const { listRemoteMemories: listRemoteMemories2, deleteMemories: deleteMemories2 } = await Promise.resolve().then(() => (init_api(), exports_api));
2967
+ const { getRemoteFilePath: getRemoteFilePath2 } = await Promise.resolve().then(() => (init_codebase(), exports_codebase));
2968
+ const remoteMemories = await listRemoteMemories2(apiKey, "codebase");
2969
+ const localPathSet = new Set(localFilePaths);
2970
+ const uploadingPaths = new Set(memories.map((m2) => getRemoteFilePath2(m2)).filter(Boolean));
2971
+ const toDelete = [];
2972
+ for (const remote of remoteMemories) {
2973
+ const remotePath = getRemoteFilePath2(remote);
2974
+ if (!remotePath)
2975
+ continue;
2976
+ if (!localPathSet.has(remotePath) || uploadingPaths.has(remotePath)) {
2977
+ toDelete.push(remote.id);
2978
+ }
2979
+ }
2980
+ if (toDelete.length > 0) {
2981
+ cleanupSpinner.stop(`Found ${toDelete.length} stale/orphaned files`);
2982
+ const deleted = await deleteMemories2(apiKey, toDelete, (done, total) => {
2983
+ write(`\r Cleaning up ${done}/${total}...`);
2984
+ });
2985
+ write(`\r Cleaned up ${deleted}/${toDelete.length} old memories
2986
+ `);
2987
+ } else {
2988
+ cleanupSpinner.stop("No stale files found");
2989
+ }
2990
+ } catch (err) {
2991
+ cleanupSpinner.stop(`Cleanup failed: ${err instanceof Error ? err.message : String(err)}`);
2992
+ log(" Continuing with upload (stale files may remain)...");
2993
+ }
2994
+ }
2785
2995
  if (memories.length === 0) {
2786
2996
  log(`
2787
2997
  Nothing new to index.`);
2788
2998
  } else if (opts.dryRun) {
2789
2999
  log(`
2790
- [DRY RUN] Would upload ${memories.length} files`);
3000
+ [DRY RUN] Would upload ${memories.length} files (project: ${project})`);
2791
3001
  for (const m2 of memories.slice(0, 10)) {
2792
3002
  const meta = m2.metadata;
2793
3003
  log(` ${meta.language.padEnd(12)} ${meta.filePath}`);
@@ -2828,7 +3038,7 @@ Nothing new to index.`);
2828
3038
  }
2829
3039
  log();
2830
3040
  }
2831
- const tools = detect();
3041
+ const tools = await detect();
2832
3042
  if (!interactiveMode) {
2833
3043
  log("Detected AI tools:");
2834
3044
  for (const t of tools) {
@@ -2961,8 +3171,10 @@ Nothing new to index.`);
2961
3171
  const absPath = resolve2(postIngestIndexPath);
2962
3172
  const idxSpinner = Y2();
2963
3173
  idxSpinner.start("Indexing codebase...");
2964
- const { memories, fileCount, skipped } = indexCodebase(absPath);
2965
- idxSpinner.stop(`Codebase: ${fileCount} files found, ${skipped} skipped`);
3174
+ const { memories, fileCount, skipped, project } = indexCodebase(absPath, {
3175
+ project: opts.indexProject
3176
+ });
3177
+ idxSpinner.stop(`Codebase: ${fileCount} files found, ${skipped} skipped (project: ${project})`);
2966
3178
  if (memories.length === 0) {
2967
3179
  log(`
2968
3180
  Nothing new to index.`);
@@ -2983,13 +3195,16 @@ Nothing new to index.`);
2983
3195
  log("");
2984
3196
  }
2985
3197
  if (!opts.dryRun && !opts.quiet) {
2986
- const syncSpinner = Y2();
2987
- syncSpinner.start("Setting up background sync...");
2988
- try {
2989
- installSync(apiKey, opts.syncInterval);
2990
- syncSpinner.stop(`Background sync active (every ${opts.syncInterval}min)`);
2991
- } catch (e2) {
2992
- syncSpinner.stop(`Could not set up background sync: ${e2.message}`);
3198
+ const shouldSync = interactiveMode ? await confirmBackgroundSync() : true;
3199
+ if (shouldSync) {
3200
+ const syncSpinner = Y2();
3201
+ syncSpinner.start("Setting up background sync...");
3202
+ try {
3203
+ installSync(apiKey, opts.syncInterval);
3204
+ syncSpinner.stop(`Background sync active (every ${opts.syncInterval}min)`);
3205
+ } catch (e2) {
3206
+ syncSpinner.stop(`Could not set up background sync: ${e2.message}`);
3207
+ }
2993
3208
  }
2994
3209
  }
2995
3210
  log("");