conare 0.3.2 → 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 +810 -604
  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
  });
@@ -961,118 +1546,45 @@ async function detect() {
961
1546
  sessionCount = sessions.size;
962
1547
  } catch {}
963
1548
  }
964
- tools.push({
965
- name: "Codex",
966
- available: true,
967
- path: existsSync(codexHistory) ? codexHistory : codexSessions,
968
- sessionCount
969
- });
970
- } else {
971
- tools.push({ name: "Codex", available: false, path: codexHistory, sessionCount: 0 });
972
- }
973
- const os = platform();
974
- let cursorDbPath;
975
- if (os === "darwin") {
976
- cursorDbPath = join(home, "Library", "Application Support", "Cursor", "User", "globalStorage", "state.vscdb");
977
- } else if (os === "win32") {
978
- cursorDbPath = join(process.env.APPDATA || join(home, "AppData", "Roaming"), "Cursor", "User", "globalStorage", "state.vscdb");
979
- } else {
980
- cursorDbPath = join(home, ".config", "Cursor", "User", "globalStorage", "state.vscdb");
981
- }
982
- tools.push({
983
- name: "Cursor",
984
- available: existsSync(cursorDbPath),
985
- path: cursorDbPath,
986
- sessionCount: existsSync(cursorDbPath) ? await countCursorSessions(cursorDbPath) : 0
987
- });
988
- const openclawDir = join(home, ".openclaw");
989
- tools.push({
990
- name: "OpenClaw",
991
- available: existsSync(openclawDir),
992
- path: openclawDir,
993
- sessionCount: 0
994
- });
995
- return tools;
996
- }
997
-
998
- // src/ingest/claude.ts
999
- import { readdirSync as readdirSync2, readFileSync as readFileSync3, existsSync as existsSync3 } from "node:fs";
1000
- import { join as join3, basename } from "node:path";
1001
- import { homedir as homedir3, platform as platform2 } from "node:os";
1002
-
1003
- // src/ingest/shared.ts
1004
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync, mkdirSync } from "node:fs";
1005
- import { createHash } from "node:crypto";
1006
- import { join as join2 } from "node:path";
1007
- import { homedir as homedir2 } from "node:os";
1008
- var MANIFEST_PATH = join2(homedir2(), ".conare", "ingested.json");
1009
- var MIN_SUBSTANTIVE = 200;
1010
- 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))/;
1011
- function isNarration(text) {
1012
- const stripped = text.trim();
1013
- if (stripped.length < MIN_SUBSTANTIVE)
1014
- return true;
1015
- if (stripped.length <= 300 && NARRATION_RE.test(stripped))
1016
- return true;
1017
- return false;
1018
- }
1019
- function cleanText(raw) {
1020
- let text = raw;
1021
- text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
1022
- text = text.replace(/<attached-context[\s\S]*?<\/attached-context>/g, "");
1023
- text = text.replace(/<environment_context>[\s\S]*?<\/environment_context>/g, "");
1024
- text = text.replace(/^\s*Base directory for this skill:[\s\S]*/g, "");
1025
- text = text.replace(/\nBase directory for this skill:[\s\S]*/g, "");
1026
- text = text.replace(/<!-- vibe-rules Integration -->[\s\S]*?<!-- \/vibe-rules Integration -->/g, "");
1027
- text = text.replace(/<good-behaviour>[\s\S]*?<\/good-behaviour>/g, "");
1028
- text = text.replace(/<frontend-design>[\s\S]*?<\/frontend-design>/g, "");
1029
- text = text.replace(/<architecture>[\s\S]*?<\/architecture>/g, "");
1030
- text = text.replace(/<task-notification>[\s\S]*?<\/task-notification>/g, "");
1031
- text = text.replace(/<local-command-stdout>[\s\S]*?<\/local-command-stdout>/g, "");
1032
- text = text.replace(/<system_instruction>[\s\S]*?<\/system_instruction>/g, "");
1033
- return text.trim();
1034
- }
1035
- function createContentHash(content) {
1036
- return createHash("sha256").update(content).digest("hex").slice(0, 16);
1037
- }
1038
- function getIngested() {
1039
- try {
1040
- if (existsSync2(MANIFEST_PATH)) {
1041
- return JSON.parse(readFileSync2(MANIFEST_PATH, "utf-8"));
1042
- }
1043
- } catch {}
1044
- return {};
1045
- }
1046
- function markIngested(source, sessionIds) {
1047
- const manifest = getIngested();
1048
- const existing = new Set(manifest[source] || []);
1049
- for (const id of sessionIds)
1050
- existing.add(id);
1051
- manifest[source] = [...existing];
1052
- const dir = join2(homedir2(), ".conare");
1053
- if (!existsSync2(dir))
1054
- mkdirSync(dir, { recursive: true });
1055
- writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
1056
- }
1057
- function isIngested(source, sessionId) {
1058
- const manifest = getIngested();
1059
- return (manifest[source] || []).includes(sessionId);
1060
- }
1061
- function clearIngested(source) {
1062
- const manifest = getIngested();
1063
- if (source) {
1064
- delete manifest[source];
1549
+ tools.push({
1550
+ name: "Codex",
1551
+ available: true,
1552
+ path: existsSync(codexHistory) ? codexHistory : codexSessions,
1553
+ sessionCount
1554
+ });
1065
1555
  } else {
1066
- for (const key of Object.keys(manifest))
1067
- delete manifest[key];
1556
+ tools.push({ name: "Codex", available: false, path: codexHistory, sessionCount: 0 });
1068
1557
  }
1069
- const dir = join2(homedir2(), ".conare");
1070
- if (!existsSync2(dir))
1071
- mkdirSync(dir, { recursive: true });
1072
- 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;
1073
1581
  }
1074
1582
 
1075
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";
1076
1588
  var MAX_CONTENT = 48000;
1077
1589
  var MIN_TURN_LEN = 50;
1078
1590
  function resolveProjectName(dirName) {
@@ -1236,6 +1748,7 @@ ${body}`;
1236
1748
  }
1237
1749
 
1238
1750
  // src/ingest/codex.ts
1751
+ init_shared();
1239
1752
  import { existsSync as existsSync4, readFileSync as readFileSync4, readdirSync as readdirSync3 } from "node:fs";
1240
1753
  import { join as join4, basename as basename2 } from "node:path";
1241
1754
  import { homedir as homedir4 } from "node:os";
@@ -1393,6 +1906,7 @@ ${body}`;
1393
1906
  }
1394
1907
 
1395
1908
  // src/ingest/cursor.ts
1909
+ init_shared();
1396
1910
  import { readFileSync as readFileSync5, statSync } from "node:fs";
1397
1911
  import { join as join5 } from "node:path";
1398
1912
  import { createRequire as createRequire3 } from "node:module";
@@ -1446,490 +1960,121 @@ function extractTurns(db, composerId, bubbleHeaders) {
1446
1960
  pendingUser = null;
1447
1961
  }
1448
1962
  }
1449
- return turns;
1450
- }
1451
- async function ingestCursor(dbPath, wasmDir) {
1452
- const memories = [];
1453
- const sessionIds = [];
1454
- let filtered = 0;
1455
- let deduped = 0;
1456
- let fileSize;
1457
- try {
1458
- fileSize = statSync(dbPath).size;
1459
- } catch {
1460
- console.log(" Skipping Cursor: database not accessible");
1461
- return { memories, sessionIds, skipped: 0, filtered, deduped };
1462
- }
1463
- if (fileSize > MAX_DB_SIZE) {
1464
- console.log(` Skipping Cursor: database too large (${(fileSize / 1024 / 1024 / 1024).toFixed(1)}GB)`);
1465
- return { memories, sessionIds, skipped: 0, filtered, deduped };
1466
- }
1467
- if (fileSize > WARN_DB_SIZE) {
1468
- console.log(` Warning: large database (${(fileSize / 1024 / 1024).toFixed(0)}MB), loading into memory...`);
1469
- }
1470
- const initSqlJs = loadSqlJs(wasmDir);
1471
- if (!initSqlJs) {
1472
- console.log(" Skipping Cursor: sql.js not available");
1473
- return { memories, sessionIds, skipped: 0, filtered, deduped };
1474
- }
1475
- let db;
1476
- try {
1477
- db = await openDb(initSqlJs, dbPath, wasmDir);
1478
- } catch (e) {
1479
- console.log(` Skipping Cursor: cannot open database (${e.message})`);
1480
- return { memories, sessionIds, skipped: 0, filtered, deduped };
1481
- }
1482
- try {
1483
- let rows = [];
1484
- try {
1485
- const results = db.exec("SELECT key, value FROM cursorDiskKV WHERE key LIKE 'composerData:%'");
1486
- if (results.length > 0)
1487
- rows = results[0].values;
1488
- } catch {}
1489
- for (const [key, value] of rows) {
1490
- const composerId = key.replace("composerData:", "");
1491
- let parsed;
1492
- try {
1493
- parsed = JSON.parse(value);
1494
- if (!parsed || typeof parsed !== "object") {
1495
- filtered++;
1496
- continue;
1497
- }
1498
- } catch {
1499
- filtered++;
1500
- continue;
1501
- }
1502
- const bubbleHeaders = parsed.fullConversationHeadersOnly;
1503
- if (!Array.isArray(bubbleHeaders) || bubbleHeaders.length === 0) {
1504
- filtered++;
1505
- continue;
1506
- }
1507
- const turns = extractTurns(db, composerId, bubbleHeaders);
1508
- if (turns.length === 0) {
1509
- filtered++;
1510
- continue;
1511
- }
1512
- const sessionName = parsed.name || "Cursor Chat";
1513
- const date = parsed.createdAt ? new Date(parsed.createdAt).toISOString().slice(0, 10) : "unknown";
1514
- const header = `# ${sessionName} | ${date}`;
1515
- const body = turns.map((t) => {
1516
- return `## Q: ${t.user}
1517
-
1518
- ${t.assistant}`;
1519
- }).join(`
1520
-
1521
- ---
1522
-
1523
- `);
1524
- let content = `${header}
1525
-
1526
- ${body}`;
1527
- if (content.length > MAX_CONTENT3)
1528
- content = content.slice(0, MAX_CONTENT3) + `
1529
-
1530
- [truncated]`;
1531
- const contentHash = createContentHash(content);
1532
- const dedupKey = `cursor:${composerId}`;
1533
- const fingerprint = `${dedupKey}:${contentHash}`;
1534
- if (isIngested("cursor", fingerprint)) {
1535
- deduped++;
1536
- continue;
1537
- }
1538
- memories.push({
1539
- content,
1540
- containerTag: "cursor-chats",
1541
- metadata: {
1542
- dedupKey,
1543
- contentHash,
1544
- source: "cursor",
1545
- sessionId: composerId,
1546
- name: sessionName,
1547
- date
1548
- }
1549
- });
1550
- sessionIds.push(composerId);
1551
- }
1552
- } catch (e) {
1553
- console.log(` Cursor ingestion error: ${e.message}`);
1554
- } finally {
1555
- db.close();
1556
- }
1557
- return { memories, sessionIds, skipped: filtered + deduped, filtered, deduped };
1558
- }
1559
-
1560
- // src/ingest/codebase.ts
1561
- import { createHash as createHash2 } from "node:crypto";
1562
- import { readdirSync as readdirSync4, readFileSync as readFileSync6, statSync as statSync2, existsSync as existsSync5 } from "node:fs";
1563
- import { join as join6, relative, extname, resolve } from "node:path";
1564
- var DEFAULT_IGNORE = new Set([
1565
- "node_modules",
1566
- ".git",
1567
- ".next",
1568
- ".nuxt",
1569
- ".output",
1570
- "dist",
1571
- "build",
1572
- ".cache",
1573
- "__pycache__",
1574
- ".venv",
1575
- "venv",
1576
- "vendor",
1577
- "target",
1578
- ".turbo",
1579
- ".DS_Store",
1580
- ".claude",
1581
- ".conare"
1582
- ]);
1583
- var IGNORE_FILES = new Set([
1584
- "package-lock.json",
1585
- "bun.lockb",
1586
- "yarn.lock",
1587
- "pnpm-lock.yaml"
1588
- ]);
1589
- var CODE_EXTENSIONS = new Set([
1590
- ".ts",
1591
- ".tsx",
1592
- ".js",
1593
- ".jsx",
1594
- ".mjs",
1595
- ".cjs",
1596
- ".vue",
1597
- ".svelte",
1598
- ".astro",
1599
- ".py",
1600
- ".rb",
1601
- ".go",
1602
- ".rs",
1603
- ".java",
1604
- ".kt",
1605
- ".swift",
1606
- ".c",
1607
- ".cpp",
1608
- ".h",
1609
- ".css",
1610
- ".scss",
1611
- ".less",
1612
- ".html",
1613
- ".json",
1614
- ".yaml",
1615
- ".yml",
1616
- ".toml",
1617
- ".md",
1618
- ".mdx",
1619
- ".txt",
1620
- ".sql",
1621
- ".graphql",
1622
- ".prisma",
1623
- ".sh",
1624
- ".bash",
1625
- ".zsh",
1626
- ".lua",
1627
- ".zig",
1628
- ".ex",
1629
- ".exs"
1630
- ]);
1631
- var SPECIAL_FILES = new Set([
1632
- "Dockerfile",
1633
- "Makefile",
1634
- "Procfile",
1635
- "Justfile",
1636
- ".gitignore",
1637
- ".dockerignore",
1638
- "CLAUDE.md",
1639
- "AGENTS.md",
1640
- "README.md",
1641
- "ARCHITECTURE.md",
1642
- "wrangler.toml",
1643
- "wrangler.json"
1644
- ]);
1645
- var MAX_FILE_SIZE = 1e5;
1646
- function parseGitignore(rootPath) {
1647
- const patterns = new Set;
1648
- const gitignorePath = join6(rootPath, ".gitignore");
1649
- if (!existsSync5(gitignorePath))
1650
- return patterns;
1651
- try {
1652
- const content = readFileSync6(gitignorePath, "utf-8");
1653
- for (const line of content.split(`
1654
- `)) {
1655
- const trimmed = line.trim();
1656
- if (!trimmed || trimmed.startsWith("#"))
1657
- continue;
1658
- patterns.add(trimmed.replace(/\/$/, ""));
1659
- }
1660
- } catch {}
1661
- return patterns;
1662
- }
1663
- function shouldIgnore(name, ignorePatterns) {
1664
- if (DEFAULT_IGNORE.has(name))
1665
- return true;
1666
- if (IGNORE_FILES.has(name))
1667
- return true;
1668
- if (name.startsWith(".env"))
1669
- return true;
1670
- if (ignorePatterns.has(name))
1671
- return true;
1672
- return false;
1673
- }
1674
- function langFromExt(ext) {
1675
- const map = {
1676
- ".ts": "typescript",
1677
- ".tsx": "tsx",
1678
- ".js": "javascript",
1679
- ".jsx": "jsx",
1680
- ".mjs": "javascript",
1681
- ".cjs": "javascript",
1682
- ".vue": "vue",
1683
- ".svelte": "svelte",
1684
- ".astro": "astro",
1685
- ".py": "python",
1686
- ".rb": "ruby",
1687
- ".go": "go",
1688
- ".rs": "rust",
1689
- ".java": "java",
1690
- ".kt": "kotlin",
1691
- ".swift": "swift",
1692
- ".c": "c",
1693
- ".cpp": "cpp",
1694
- ".h": "c",
1695
- ".css": "css",
1696
- ".scss": "scss",
1697
- ".less": "less",
1698
- ".html": "html",
1699
- ".json": "json",
1700
- ".yaml": "yaml",
1701
- ".yml": "yaml",
1702
- ".toml": "toml",
1703
- ".md": "markdown",
1704
- ".mdx": "mdx",
1705
- ".sql": "sql",
1706
- ".graphql": "graphql",
1707
- ".prisma": "prisma",
1708
- ".sh": "bash",
1709
- ".bash": "bash",
1710
- ".zsh": "zsh",
1711
- ".lua": "lua",
1712
- ".zig": "zig",
1713
- ".ex": "elixir",
1714
- ".exs": "elixir"
1715
- };
1716
- return map[ext] || ext.slice(1);
1717
- }
1718
- function formatFile(relPath, content, ext) {
1719
- const lang = langFromExt(ext);
1720
- return `# File: ${relPath}
1721
-
1722
- \`\`\`${lang}
1723
- ${content}
1724
- \`\`\``;
1725
- }
1726
- function indexCodebase(rootPath) {
1727
- const absRoot = resolve(rootPath);
1728
- const repoHash = createHash2("sha256").update(absRoot).digest("hex").slice(0, 12);
1729
- const gitignorePatterns = parseGitignore(absRoot);
1963
+ return turns;
1964
+ }
1965
+ async function ingestCursor(dbPath, wasmDir) {
1730
1966
  const memories = [];
1731
- let fileCount = 0;
1732
- let skipped = 0;
1733
- function walk(dir) {
1734
- let entries;
1967
+ const sessionIds = [];
1968
+ let filtered = 0;
1969
+ let deduped = 0;
1970
+ let fileSize;
1971
+ try {
1972
+ fileSize = statSync(dbPath).size;
1973
+ } catch {
1974
+ console.log(" Skipping Cursor: database not accessible");
1975
+ return { memories, sessionIds, skipped: 0, filtered, deduped };
1976
+ }
1977
+ if (fileSize > MAX_DB_SIZE) {
1978
+ console.log(` Skipping Cursor: database too large (${(fileSize / 1024 / 1024 / 1024).toFixed(1)}GB)`);
1979
+ return { memories, sessionIds, skipped: 0, filtered, deduped };
1980
+ }
1981
+ if (fileSize > WARN_DB_SIZE) {
1982
+ console.log(` Warning: large database (${(fileSize / 1024 / 1024).toFixed(0)}MB), loading into memory...`);
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 = [];
1735
1998
  try {
1736
- entries = readdirSync4(dir, { withFileTypes: true });
1737
- } catch {
1738
- return;
1739
- }
1740
- for (const entry of entries) {
1741
- if (shouldIgnore(entry.name, gitignorePatterns))
1742
- continue;
1743
- const fullPath = join6(dir, entry.name);
1744
- if (entry.isDirectory()) {
1745
- walk(fullPath);
1746
- continue;
1747
- }
1748
- if (!entry.isFile())
1749
- continue;
1750
- const ext = extname(entry.name).toLowerCase();
1751
- if (!CODE_EXTENSIONS.has(ext) && !SPECIAL_FILES.has(entry.name))
1752
- continue;
1753
- 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;
1754
2006
  try {
1755
- stat = statSync2(fullPath);
2007
+ parsed = JSON.parse(value);
2008
+ if (!parsed || typeof parsed !== "object") {
2009
+ filtered++;
2010
+ continue;
2011
+ }
1756
2012
  } catch {
2013
+ filtered++;
1757
2014
  continue;
1758
2015
  }
1759
- if (stat.size > MAX_FILE_SIZE || stat.size === 0) {
1760
- skipped++;
1761
- continue;
1762
- }
1763
- let raw;
1764
- try {
1765
- raw = readFileSync6(fullPath, "utf-8");
1766
- } catch {
1767
- skipped++;
2016
+ const bubbleHeaders = parsed.fullConversationHeadersOnly;
2017
+ if (!Array.isArray(bubbleHeaders) || bubbleHeaders.length === 0) {
2018
+ filtered++;
1768
2019
  continue;
1769
2020
  }
1770
- if (raw.includes("\x00")) {
1771
- skipped++;
2021
+ const turns = extractTurns(db, composerId, bubbleHeaders);
2022
+ if (turns.length === 0) {
2023
+ filtered++;
1772
2024
  continue;
1773
2025
  }
1774
- const relPath = relative(absRoot, fullPath);
1775
- const contentHash = createContentHash(raw);
1776
- 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}`;
1777
2047
  const fingerprint = `${dedupKey}:${contentHash}`;
1778
- if (isIngested("codebase", fingerprint)) {
1779
- skipped++;
2048
+ if (isIngested("cursor", fingerprint)) {
2049
+ deduped++;
1780
2050
  continue;
1781
2051
  }
1782
- const content = formatFile(relPath, raw, ext);
1783
2052
  memories.push({
1784
2053
  content,
1785
- containerTag: "codebase",
2054
+ containerTag: "cursor-chats",
1786
2055
  metadata: {
1787
2056
  dedupKey,
1788
2057
  contentHash,
1789
- source: "codebase-index",
1790
- repoHash,
1791
- filePath: relPath,
1792
- fileHash: `${relPath}:${contentHash}`,
1793
- language: langFromExt(ext)
2058
+ source: "cursor",
2059
+ sessionId: composerId,
2060
+ name: sessionName,
2061
+ date
1794
2062
  }
1795
2063
  });
1796
- fileCount++;
2064
+ sessionIds.push(composerId);
1797
2065
  }
2066
+ } catch (e) {
2067
+ console.log(` Cursor ingestion error: ${e.message}`);
2068
+ } finally {
2069
+ db.close();
1798
2070
  }
1799
- walk(absRoot);
1800
- return { memories, fileCount, skipped };
2071
+ return { memories, sessionIds, skipped: filtered + deduped, filtered, deduped };
1801
2072
  }
1802
2073
 
1803
- // src/api.ts
1804
- var API_URL = "https://mcp.conare.ai";
1805
- function createUploadBatches(memories, maxItems = 50, maxChars = 1500000) {
1806
- const batches = [];
1807
- let current = [];
1808
- let currentChars = 0;
1809
- let startIndex = 0;
1810
- for (let i = 0;i < memories.length; i++) {
1811
- const item = memories[i];
1812
- const itemChars = item.content.length;
1813
- const exceedsBatch = current.length > 0 && (current.length >= maxItems || currentChars + itemChars > maxChars);
1814
- if (exceedsBatch) {
1815
- batches.push({ startIndex, items: current });
1816
- current = [];
1817
- currentChars = 0;
1818
- startIndex = i;
1819
- }
1820
- current.push(item);
1821
- currentChars += itemChars;
1822
- }
1823
- if (current.length > 0) {
1824
- batches.push({ startIndex, items: current });
1825
- }
1826
- return batches;
1827
- }
1828
- async function validateKey(apiKey) {
1829
- try {
1830
- const res = await fetch(`${API_URL}/api/auth/me`, {
1831
- headers: { Authorization: `Bearer ${apiKey}` }
1832
- });
1833
- if (!res.ok)
1834
- return { valid: false };
1835
- const data = await res.json();
1836
- return { valid: true, email: data.user.email, name: data.user.name };
1837
- } catch {
1838
- return { valid: false };
1839
- }
1840
- }
1841
- async function getRemoteMemoryCount(apiKey) {
1842
- try {
1843
- const res = await fetch(`${API_URL}/api/containers`, {
1844
- headers: { Authorization: `Bearer ${apiKey}` }
1845
- });
1846
- if (!res.ok)
1847
- return null;
1848
- const data = await res.json();
1849
- if (!Array.isArray(data.containers))
1850
- return 0;
1851
- return data.containers.reduce((sum, container) => sum + (container.count || 0), 0);
1852
- } catch {
1853
- return null;
1854
- }
1855
- }
1856
- async function uploadItems(apiKey, items, asyncEmbed = false) {
1857
- let retries = 4;
1858
- while (retries > 0) {
1859
- try {
1860
- const res = await fetch(`${API_URL}/api/memories/bulk`, {
1861
- method: "POST",
1862
- headers: {
1863
- "Content-Type": "application/json",
1864
- Authorization: `Bearer ${apiKey}`
1865
- },
1866
- body: JSON.stringify(asyncEmbed ? { items, async: true } : items)
1867
- });
1868
- if (res.status === 429) {
1869
- retries--;
1870
- await new Promise((r) => setTimeout(r, 5000));
1871
- continue;
1872
- }
1873
- if (!res.ok) {
1874
- const body = await res.text().catch(() => "");
1875
- throw new Error(`HTTP ${res.status}: ${body.slice(0, 120)}`);
1876
- }
1877
- const data = await res.json();
1878
- if (!Array.isArray(data.results) || data.results.length !== items.length) {
1879
- throw new Error("Unexpected bulk upload response");
1880
- }
1881
- return data.results;
1882
- } catch (error) {
1883
- retries--;
1884
- if (retries === 0) {
1885
- return items.map(() => ({
1886
- success: false,
1887
- error: error instanceof Error ? error.message : String(error)
1888
- }));
1889
- }
1890
- await new Promise((r) => setTimeout(r, 2000));
1891
- }
1892
- }
1893
- return items.map(() => ({ success: false, error: "Upload failed" }));
1894
- }
1895
- async function uploadBulk(apiKey, memories, onProgress) {
1896
- let success = 0;
1897
- let failed = 0;
1898
- const total = memories.length;
1899
- const results = [];
1900
- const batches = createUploadBatches(memories);
1901
- for (const batch of batches) {
1902
- let batchResults = await uploadItems(apiKey, batch.items, true);
1903
- if (batch.items.length > 1 && batchResults.some((result) => !result.success)) {
1904
- const retriedResults = [];
1905
- for (let i = 0;i < batch.items.length; i++) {
1906
- const result = batchResults[i];
1907
- if (result.success) {
1908
- retriedResults.push(result);
1909
- continue;
1910
- }
1911
- const [singleResult] = await uploadItems(apiKey, [batch.items[i]], true);
1912
- retriedResults.push(singleResult);
1913
- }
1914
- batchResults = retriedResults;
1915
- }
1916
- for (let i = 0;i < batchResults.length; i++) {
1917
- const result = batchResults[i];
1918
- results.push({
1919
- index: batch.startIndex + i,
1920
- success: result.success,
1921
- deduped: result.deduped,
1922
- error: result.error
1923
- });
1924
- if (result.success)
1925
- success++;
1926
- else
1927
- failed++;
1928
- }
1929
- onProgress?.(success + failed, total, failed);
1930
- }
1931
- return { success, failed, results };
1932
- }
2074
+ // src/index.ts
2075
+ init_codebase();
2076
+ init_shared();
2077
+ init_api();
1933
2078
 
1934
2079
  // src/configure.ts
1935
2080
  import { existsSync as existsSync6, mkdirSync as mkdirSync2, readFileSync as readFileSync7, writeFileSync as writeFileSync2 } from "node:fs";
@@ -2037,7 +2182,7 @@ function getSavedApiKey() {
2037
2182
  import { existsSync as existsSync8, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync9, chmodSync, cpSync, rmSync, symlinkSync, readlinkSync, appendFileSync } from "node:fs";
2038
2183
  import { join as join9, dirname as dirname2 } from "node:path";
2039
2184
  import { homedir as homedir7, platform as platform5 } from "node:os";
2040
- import { execSync } from "node:child_process";
2185
+ import { execSync as execSync2 } from "node:child_process";
2041
2186
  var CONARE_DIR = join9(homedir7(), ".conare");
2042
2187
  var BIN_DIR = join9(CONARE_DIR, "bin");
2043
2188
  var CONFIG_PATH2 = join9(CONARE_DIR, "config.json");
@@ -2047,6 +2192,9 @@ var SYSTEMD_DIR = join9(homedir7(), ".config", "systemd", "user");
2047
2192
  var SYSTEMD_SERVICE = join9(SYSTEMD_DIR, "conare-sync.service");
2048
2193
  var SYSTEMD_TIMER = join9(SYSTEMD_DIR, "conare-sync.timer");
2049
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
+ `;
2050
2198
  var RUN_CMD = `@echo off
2051
2199
  REM Conare Memory — background sync wrapper (Windows)
2052
2200
  setlocal
@@ -2190,7 +2338,7 @@ WantedBy=timers.target
2190
2338
  }
2191
2339
  function hasSystemd() {
2192
2340
  try {
2193
- execSync("systemctl --user status 2>/dev/null", { stdio: "ignore" });
2341
+ execSync2("systemctl --user status 2>/dev/null", { stdio: "ignore" });
2194
2342
  return true;
2195
2343
  } catch {
2196
2344
  return false;
@@ -2198,7 +2346,7 @@ function hasSystemd() {
2198
2346
  }
2199
2347
  function uid() {
2200
2348
  try {
2201
- return execSync("id -u", { encoding: "utf-8" }).trim();
2349
+ return execSync2("id -u", { encoding: "utf-8" }).trim();
2202
2350
  } catch {
2203
2351
  return "501";
2204
2352
  }
@@ -2227,6 +2375,8 @@ function persistBinary(apiKey) {
2227
2375
  } catch {}
2228
2376
  const runCmdPath = join9(BIN_DIR, "run.cmd");
2229
2377
  writeFileSync4(runCmdPath, RUN_CMD);
2378
+ const runVbsPath = join9(BIN_DIR, "run.vbs");
2379
+ writeFileSync4(runVbsPath, RUN_VBS);
2230
2380
  writeFileSync4(CONFIG_PATH2, JSON.stringify({ apiKey }, null, 2) + `
2231
2381
  `);
2232
2382
  }
@@ -2262,13 +2412,13 @@ function setupMacOS(intervalMinutes) {
2262
2412
  writeFileSync4(PLIST_PATH, makePlist(intervalMinutes));
2263
2413
  const id = uid();
2264
2414
  try {
2265
- 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" });
2266
2416
  } catch {}
2267
2417
  try {
2268
- execSync(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
2418
+ execSync2(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
2269
2419
  } catch {
2270
2420
  try {
2271
- execSync(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
2421
+ execSync2(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
2272
2422
  } catch {
2273
2423
  throw new Error("Failed to load launchd agent. Try manually: launchctl load " + PLIST_PATH);
2274
2424
  }
@@ -2278,23 +2428,23 @@ function setupLinuxSystemd(intervalMinutes) {
2278
2428
  mkdirSync4(SYSTEMD_DIR, { recursive: true });
2279
2429
  writeFileSync4(SYSTEMD_SERVICE, SYSTEMD_SERVICE_CONTENT);
2280
2430
  writeFileSync4(SYSTEMD_TIMER, makeSystemdTimer(intervalMinutes));
2281
- execSync("systemctl --user daemon-reload", { stdio: "ignore" });
2282
- 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" });
2283
2433
  }
2284
2434
  function setupLinuxCron(intervalMinutes) {
2285
2435
  const cronCmd = `${homedir7()}/.conare/bin/run.sh`;
2286
2436
  const cronLine = `*/${intervalMinutes} * * * * ${cronCmd}`;
2287
2437
  try {
2288
- const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
2438
+ const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
2289
2439
  const filtered = existing.split(`
2290
2440
  `).filter((l) => !l.includes("conare")).join(`
2291
2441
  `);
2292
2442
  const newCrontab = (filtered.trim() ? filtered.trim() + `
2293
2443
  ` : "") + cronLine + `
2294
2444
  `;
2295
- execSync("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
2445
+ execSync2("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
2296
2446
  } catch {
2297
- execSync("crontab -", { input: cronLine + `
2447
+ execSync2("crontab -", { input: cronLine + `
2298
2448
  `, stdio: ["pipe", "ignore", "ignore"] });
2299
2449
  }
2300
2450
  }
@@ -2303,6 +2453,11 @@ function installGlobalCommand() {
2303
2453
  if (isWindows) {
2304
2454
  const wrapper2 = join9(BIN_DIR, "conare.cmd");
2305
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
2306
2461
  node "%USERPROFILE%\\.conare\\bin\\conare-ingest.mjs" %*\r
2307
2462
  `;
2308
2463
  writeFileSync4(wrapper2, content2);
@@ -2310,11 +2465,12 @@ node "%USERPROFILE%\\.conare\\bin\\conare-ingest.mjs" %*\r
2310
2465
  if (pathDirs.some((d) => d.toLowerCase() === BIN_DIR.toLowerCase())) {
2311
2466
  return "Global command: conare (via .conare\\bin in PATH)";
2312
2467
  }
2468
+ const binDirWin = BIN_DIR.replace(/\//g, "\\");
2313
2469
  try {
2314
- execSync(`powershell -NoProfile -Command "[Environment]::SetEnvironmentVariable('PATH', [Environment]::GetEnvironmentVariable('PATH','User') + ';${BIN_DIR}', 'User')"`, { stdio: "ignore" });
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" });
2315
2471
  return `Global command: conare (added .conare\\bin to user PATH — restart terminal)`;
2316
2472
  } catch {
2317
- return `Global command: add ${BIN_DIR} to your PATH manually`;
2473
+ return `Global command: add ${binDirWin} to your PATH manually`;
2318
2474
  }
2319
2475
  }
2320
2476
  const wrapper = join9(BIN_DIR, "conare");
@@ -2391,11 +2547,11 @@ function getShellProfile() {
2391
2547
  return null;
2392
2548
  }
2393
2549
  function setupWindows(intervalMinutes) {
2394
- const runCmd = join9(BIN_DIR, "run.cmd").replace(/\//g, "\\");
2550
+ const runVbs = join9(BIN_DIR, "run.vbs").replace(/\//g, "\\");
2395
2551
  try {
2396
- execSync(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2552
+ execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2397
2553
  } catch {}
2398
- execSync(`schtasks /Create /TN "${TASK_NAME}" /TR "${runCmd}" /SC MINUTE /MO ${intervalMinutes} /F`, { stdio: "ignore" });
2554
+ execSync2(`schtasks /Create /TN "${TASK_NAME}" /TR "wscript.exe \\"${runVbs}\\"" /SC MINUTE /MO ${intervalMinutes} /F`, { stdio: "ignore" });
2399
2555
  }
2400
2556
  function installSync(apiKey, intervalMinutes = 10) {
2401
2557
  const messages = [];
@@ -2431,7 +2587,7 @@ function uninstallSync() {
2431
2587
  const os = platform5();
2432
2588
  if (os === "darwin") {
2433
2589
  try {
2434
- 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" });
2435
2591
  } catch {}
2436
2592
  if (existsSync8(PLIST_PATH)) {
2437
2593
  unlinkSync(PLIST_PATH);
@@ -2439,29 +2595,29 @@ function uninstallSync() {
2439
2595
  }
2440
2596
  } else if (os === "win32") {
2441
2597
  try {
2442
- execSync(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2598
+ execSync2(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
2443
2599
  messages.push("Removed Windows scheduled task");
2444
2600
  } catch {}
2445
2601
  } else if (os === "linux") {
2446
2602
  if (hasSystemd()) {
2447
2603
  try {
2448
- 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" });
2449
2605
  } catch {}
2450
2606
  if (existsSync8(SYSTEMD_SERVICE))
2451
2607
  unlinkSync(SYSTEMD_SERVICE);
2452
2608
  if (existsSync8(SYSTEMD_TIMER))
2453
2609
  unlinkSync(SYSTEMD_TIMER);
2454
2610
  try {
2455
- execSync("systemctl --user daemon-reload", { stdio: "ignore" });
2611
+ execSync2("systemctl --user daemon-reload", { stdio: "ignore" });
2456
2612
  } catch {}
2457
2613
  messages.push("Removed systemd timer");
2458
2614
  } else {
2459
2615
  try {
2460
- const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
2616
+ const existing = execSync2("crontab -l 2>/dev/null", { encoding: "utf-8" });
2461
2617
  const filtered = existing.split(`
2462
2618
  `).filter((l) => !l.includes("conare")).join(`
2463
2619
  `);
2464
- execSync("crontab -", { input: filtered.trim() + `
2620
+ execSync2("crontab -", { input: filtered.trim() + `
2465
2621
  `, stdio: ["pipe", "ignore", "ignore"] });
2466
2622
  messages.push("Removed cron job");
2467
2623
  } catch {}
@@ -2556,6 +2712,8 @@ function parseArgs() {
2556
2712
  let source;
2557
2713
  let wasmDir;
2558
2714
  let indexPath;
2715
+ let indexProject;
2716
+ let indexChanged = false;
2559
2717
  for (let i = 0;i < args.length; i++) {
2560
2718
  if (args[i] === "--key" && args[i + 1]) {
2561
2719
  key = args[++i];
@@ -2579,6 +2737,10 @@ function parseArgs() {
2579
2737
  wasmDir = args[++i];
2580
2738
  } else if (args[i] === "--index") {
2581
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;
2582
2744
  } else if (args[i] === "--ingest-only") {
2583
2745
  ingestOnly = true;
2584
2746
  } else if (args[i] === "--config-only") {
@@ -2599,6 +2761,8 @@ Options:
2599
2761
  --key <key> Your Conare API key (required, starts with cmem_)
2600
2762
  --config-file <path> Read API key from JSON config file (e.g. ~/.conare/config.json)
2601
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
2602
2766
  --dry-run Preview what would be uploaded
2603
2767
  --force Re-ingest all / re-index all (bypass dedup)
2604
2768
  --quiet Suppress all stdout output (for background timer runs)
@@ -2624,7 +2788,7 @@ Get your API key at https://mcp.conare.ai
2624
2788
  console.error("Error: --ingest-only and --config-only cannot be used together");
2625
2789
  process.exit(1);
2626
2790
  }
2627
- 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 };
2628
2792
  }
2629
2793
  async function runInstall() {
2630
2794
  const args = process.argv.slice(3);
@@ -2788,15 +2952,52 @@ async function main() {
2788
2952
  } catch {}
2789
2953
  }
2790
2954
  const idxSpinner = Y2();
2791
- idxSpinner.start("Scanning codebase...");
2792
- const { memories, fileCount, skipped } = indexCodebase(absPath);
2793
- 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
+ }
2794
2995
  if (memories.length === 0) {
2795
2996
  log(`
2796
2997
  Nothing new to index.`);
2797
2998
  } else if (opts.dryRun) {
2798
2999
  log(`
2799
- [DRY RUN] Would upload ${memories.length} files`);
3000
+ [DRY RUN] Would upload ${memories.length} files (project: ${project})`);
2800
3001
  for (const m2 of memories.slice(0, 10)) {
2801
3002
  const meta = m2.metadata;
2802
3003
  log(` ${meta.language.padEnd(12)} ${meta.filePath}`);
@@ -2970,8 +3171,10 @@ Nothing new to index.`);
2970
3171
  const absPath = resolve2(postIngestIndexPath);
2971
3172
  const idxSpinner = Y2();
2972
3173
  idxSpinner.start("Indexing codebase...");
2973
- const { memories, fileCount, skipped } = indexCodebase(absPath);
2974
- 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})`);
2975
3178
  if (memories.length === 0) {
2976
3179
  log(`
2977
3180
  Nothing new to index.`);
@@ -2992,13 +3195,16 @@ Nothing new to index.`);
2992
3195
  log("");
2993
3196
  }
2994
3197
  if (!opts.dryRun && !opts.quiet) {
2995
- const syncSpinner = Y2();
2996
- syncSpinner.start("Setting up background sync...");
2997
- try {
2998
- installSync(apiKey, opts.syncInterval);
2999
- syncSpinner.stop(`Background sync active (every ${opts.syncInterval}min)`);
3000
- } catch (e2) {
3001
- 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
+ }
3002
3208
  }
3003
3209
  }
3004
3210
  log("");