conare 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +460 -91
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -148,8 +148,8 @@ var require_picocolors = __commonJS((exports, module) => {
148
148
  });
149
149
 
150
150
  // src/index.ts
151
- import { existsSync as existsSync7 } from "node:fs";
152
- import { join as join9 } from "node:path";
151
+ import { existsSync as existsSync8 } from "node:fs";
152
+ import { join as join10 } from "node:path";
153
153
 
154
154
  // src/detect.ts
155
155
  import { existsSync, readdirSync } from "node:fs";
@@ -1049,7 +1049,7 @@ function indexCodebase(rootPath) {
1049
1049
 
1050
1050
  // src/api.ts
1051
1051
  var API_URL = "https://mcp.conare.ai";
1052
- function createUploadBatches(memories, maxItems = 10, maxChars = 1500000) {
1052
+ function createUploadBatches(memories, maxItems = 50, maxChars = 1500000) {
1053
1053
  const batches = [];
1054
1054
  let current = [];
1055
1055
  let currentChars = 0;
@@ -1100,7 +1100,7 @@ async function getRemoteMemoryCount(apiKey) {
1100
1100
  return null;
1101
1101
  }
1102
1102
  }
1103
- async function uploadItems(apiKey, items) {
1103
+ async function uploadItems(apiKey, items, asyncEmbed = false) {
1104
1104
  let retries = 4;
1105
1105
  while (retries > 0) {
1106
1106
  try {
@@ -1110,7 +1110,7 @@ async function uploadItems(apiKey, items) {
1110
1110
  "Content-Type": "application/json",
1111
1111
  Authorization: `Bearer ${apiKey}`
1112
1112
  },
1113
- body: JSON.stringify(items)
1113
+ body: JSON.stringify(asyncEmbed ? { items, async: true } : items)
1114
1114
  });
1115
1115
  if (res.status === 429) {
1116
1116
  retries--;
@@ -1144,8 +1144,9 @@ async function uploadBulk(apiKey, memories, onProgress) {
1144
1144
  let failed = 0;
1145
1145
  const total = memories.length;
1146
1146
  const results = [];
1147
- for (const batch of createUploadBatches(memories)) {
1148
- let batchResults = await uploadItems(apiKey, batch.items);
1147
+ const batches = createUploadBatches(memories);
1148
+ for (const batch of batches) {
1149
+ let batchResults = await uploadItems(apiKey, batch.items, true);
1149
1150
  if (batch.items.length > 1 && batchResults.some((result) => !result.success)) {
1150
1151
  const retriedResults = [];
1151
1152
  for (let i = 0;i < batch.items.length; i++) {
@@ -1154,7 +1155,7 @@ async function uploadBulk(apiKey, memories, onProgress) {
1154
1155
  retriedResults.push(result);
1155
1156
  continue;
1156
1157
  }
1157
- const [singleResult] = await uploadItems(apiKey, [batch.items[i]]);
1158
+ const [singleResult] = await uploadItems(apiKey, [batch.items[i]], true);
1158
1159
  retriedResults.push(singleResult);
1159
1160
  }
1160
1161
  batchResults = retriedResults;
@@ -1274,6 +1275,308 @@ function getSavedApiKey() {
1274
1275
  return readConfig().apiKey;
1275
1276
  }
1276
1277
 
1278
+ // src/sync.ts
1279
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync8, chmodSync, cpSync, rmSync } from "node:fs";
1280
+ import { join as join9, dirname as dirname2 } from "node:path";
1281
+ import { homedir as homedir7, platform as platform2 } from "node:os";
1282
+ import { execSync } from "node:child_process";
1283
+ var CONARE_DIR = join9(homedir7(), ".conare");
1284
+ var BIN_DIR = join9(CONARE_DIR, "bin");
1285
+ var CONFIG_PATH2 = join9(CONARE_DIR, "config.json");
1286
+ var PLIST_LABEL = "ai.conare.ingest";
1287
+ var PLIST_PATH = join9(homedir7(), "Library", "LaunchAgents", `${PLIST_LABEL}.plist`);
1288
+ var SYSTEMD_DIR = join9(homedir7(), ".config", "systemd", "user");
1289
+ var SYSTEMD_SERVICE = join9(SYSTEMD_DIR, "conare-sync.service");
1290
+ var SYSTEMD_TIMER = join9(SYSTEMD_DIR, "conare-sync.timer");
1291
+ var RUN_SH = `#!/bin/bash
1292
+ # Conare Memory — background sync wrapper
1293
+ # Resolves node at runtime to survive nvm/fnm upgrades
1294
+
1295
+ CONARE_DIR="$HOME/.conare"
1296
+ LOCKFILE="$CONARE_DIR/sync.lock"
1297
+ LOG="$CONARE_DIR/ingest.log"
1298
+
1299
+ # Log rotation: truncate if > 1MB
1300
+ if [ -f "$LOG" ] && [ "$(wc -c < "$LOG" 2>/dev/null)" -gt 1048576 ]; then
1301
+ tail -100 "$LOG" > "$LOG.tmp" && mv "$LOG.tmp" "$LOG"
1302
+ fi
1303
+
1304
+ # File lock: prevent concurrent runs
1305
+ # macOS doesn't have flock — use mkdir as atomic lock
1306
+ if ! mkdir "$LOCKFILE.d" 2>/dev/null; then
1307
+ # Check if stale (older than 30 min)
1308
+ if [ "$(uname)" = "Darwin" ]; then
1309
+ LOCK_AGE=$(( $(date +%s) - $(stat -f %m "$LOCKFILE.d" 2>/dev/null || echo 0) ))
1310
+ else
1311
+ LOCK_AGE=$(( $(date +%s) - $(stat -c %Y "$LOCKFILE.d" 2>/dev/null || echo 0) ))
1312
+ fi
1313
+ if [ "$LOCK_AGE" -gt 1800 ]; then
1314
+ rm -rf "$LOCKFILE.d"
1315
+ mkdir "$LOCKFILE.d" 2>/dev/null || true
1316
+ else
1317
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) SKIP: another instance running" >> "$LOG"
1318
+ exit 0
1319
+ fi
1320
+ fi
1321
+ trap 'rm -rf "$LOCKFILE.d"' EXIT
1322
+
1323
+ # Resolve node binary
1324
+ resolve_node() {
1325
+ command -v node >/dev/null 2>&1 && { command -v node; return; }
1326
+ if [ -s "$HOME/.nvm/nvm.sh" ]; then
1327
+ . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1
1328
+ command -v node >/dev/null 2>&1 && { command -v node; return; }
1329
+ fi
1330
+ if command -v fnm >/dev/null 2>&1; then
1331
+ eval "$(fnm env)" >/dev/null 2>&1
1332
+ command -v node >/dev/null 2>&1 && { command -v node; return; }
1333
+ fi
1334
+ for p in /opt/homebrew/bin/node /usr/local/bin/node /usr/bin/node; do
1335
+ [ -x "$p" ] && { echo "$p"; return; }
1336
+ done
1337
+ return 1
1338
+ }
1339
+
1340
+ NODE=$(resolve_node)
1341
+ if [ -z "$NODE" ]; then
1342
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) ERROR: node not found" >> "$LOG"
1343
+ exit 1
1344
+ fi
1345
+
1346
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) START sync" >> "$LOG"
1347
+ "$NODE" "$CONARE_DIR/bin/conare-ingest.mjs" \\
1348
+ --config-file "$CONARE_DIR/config.json" \\
1349
+ --ingest-only \\
1350
+ --quiet \\
1351
+ 2>> "$LOG"
1352
+ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) DONE sync (exit $?)" >> "$LOG"
1353
+ `;
1354
+ function makePlist() {
1355
+ const home = homedir7();
1356
+ return `<?xml version="1.0" encoding="UTF-8"?>
1357
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
1358
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
1359
+ <plist version="1.0">
1360
+ <dict>
1361
+ <key>Label</key>
1362
+ <string>${PLIST_LABEL}</string>
1363
+ <key>ProgramArguments</key>
1364
+ <array>
1365
+ <string>/bin/bash</string>
1366
+ <string>${home}/.conare/bin/run.sh</string>
1367
+ </array>
1368
+ <key>StartInterval</key>
1369
+ <integer>600</integer>
1370
+ <key>StandardOutPath</key>
1371
+ <string>${home}/.conare/ingest.log</string>
1372
+ <key>StandardErrorPath</key>
1373
+ <string>${home}/.conare/ingest.log</string>
1374
+ <key>RunAtLoad</key>
1375
+ <true/>
1376
+ </dict>
1377
+ </plist>`;
1378
+ }
1379
+ var SYSTEMD_SERVICE_CONTENT = `[Unit]
1380
+ Description=Conare Memory — background sync
1381
+
1382
+ [Service]
1383
+ Type=oneshot
1384
+ ExecStart=%h/.conare/bin/run.sh
1385
+ `;
1386
+ var SYSTEMD_TIMER_CONTENT = `[Unit]
1387
+ Description=Conare Memory — sync every 10 minutes
1388
+
1389
+ [Timer]
1390
+ OnBootSec=2min
1391
+ OnUnitActiveSec=10min
1392
+ Persistent=true
1393
+
1394
+ [Install]
1395
+ WantedBy=timers.target
1396
+ `;
1397
+ function hasSystemd() {
1398
+ try {
1399
+ execSync("systemctl --user status 2>/dev/null", { stdio: "ignore" });
1400
+ return true;
1401
+ } catch {
1402
+ return false;
1403
+ }
1404
+ }
1405
+ function uid() {
1406
+ try {
1407
+ return execSync("id -u", { encoding: "utf-8" }).trim();
1408
+ } catch {
1409
+ return "501";
1410
+ }
1411
+ }
1412
+ function persistBinary(apiKey) {
1413
+ mkdirSync4(BIN_DIR, { recursive: true });
1414
+ const cliEntry = findCliBundle();
1415
+ if (!cliEntry) {
1416
+ throw new Error("Could not locate CLI bundle. Run from an installed conare package.");
1417
+ }
1418
+ const dest = join9(BIN_DIR, "conare-ingest.mjs");
1419
+ const content = readFileSync8(cliEntry, "utf-8");
1420
+ writeFileSync4(dest, content);
1421
+ const sqlJsDir = findSqlJs();
1422
+ if (sqlJsDir) {
1423
+ const targetDir = join9(BIN_DIR, "node_modules", "sql.js");
1424
+ mkdirSync4(targetDir, { recursive: true });
1425
+ try {
1426
+ cpSync(sqlJsDir, targetDir, { recursive: true });
1427
+ } catch {}
1428
+ }
1429
+ const runShPath = join9(BIN_DIR, "run.sh");
1430
+ writeFileSync4(runShPath, RUN_SH);
1431
+ chmodSync(runShPath, 493);
1432
+ writeFileSync4(CONFIG_PATH2, JSON.stringify({ apiKey }, null, 2) + `
1433
+ `);
1434
+ }
1435
+ function findCliBundle() {
1436
+ const entry = process.argv[1];
1437
+ if (entry && existsSync7(entry))
1438
+ return entry;
1439
+ const candidates = [
1440
+ join9(dirname2(new URL(import.meta.url).pathname), "index.js"),
1441
+ join9(dirname2(new URL(import.meta.url).pathname), "..", "dist", "index.js")
1442
+ ];
1443
+ for (const c of candidates) {
1444
+ if (existsSync7(c))
1445
+ return c;
1446
+ }
1447
+ return null;
1448
+ }
1449
+ function findSqlJs() {
1450
+ const candidates = [
1451
+ join9(process.cwd(), "node_modules", "sql.js"),
1452
+ join9(dirname2(new URL(import.meta.url).pathname), "..", "node_modules", "sql.js"),
1453
+ join9(dirname2(new URL(import.meta.url).pathname), "..", "..", "node_modules", "sql.js")
1454
+ ];
1455
+ for (const c of candidates) {
1456
+ if (existsSync7(c))
1457
+ return c;
1458
+ }
1459
+ return null;
1460
+ }
1461
+ function setupMacOS() {
1462
+ const plistDir = dirname2(PLIST_PATH);
1463
+ mkdirSync4(plistDir, { recursive: true });
1464
+ writeFileSync4(PLIST_PATH, makePlist());
1465
+ const id = uid();
1466
+ try {
1467
+ execSync(`launchctl bootout gui/${id} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
1468
+ } catch {}
1469
+ try {
1470
+ execSync(`launchctl bootstrap gui/${id} "${PLIST_PATH}"`, { stdio: "ignore" });
1471
+ } catch {
1472
+ try {
1473
+ execSync(`launchctl load "${PLIST_PATH}"`, { stdio: "ignore" });
1474
+ } catch {
1475
+ throw new Error("Failed to load launchd agent. Try manually: launchctl load " + PLIST_PATH);
1476
+ }
1477
+ }
1478
+ }
1479
+ function setupLinuxSystemd() {
1480
+ mkdirSync4(SYSTEMD_DIR, { recursive: true });
1481
+ writeFileSync4(SYSTEMD_SERVICE, SYSTEMD_SERVICE_CONTENT);
1482
+ writeFileSync4(SYSTEMD_TIMER, SYSTEMD_TIMER_CONTENT);
1483
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
1484
+ execSync("systemctl --user enable --now conare-sync.timer", { stdio: "ignore" });
1485
+ }
1486
+ function setupLinuxCron() {
1487
+ const cronCmd = `${homedir7()}/.conare/bin/run.sh`;
1488
+ const cronLine = `*/10 * * * * ${cronCmd}`;
1489
+ try {
1490
+ const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
1491
+ const filtered = existing.split(`
1492
+ `).filter((l) => !l.includes("conare")).join(`
1493
+ `);
1494
+ const newCrontab = (filtered.trim() ? filtered.trim() + `
1495
+ ` : "") + cronLine + `
1496
+ `;
1497
+ execSync("crontab -", { input: newCrontab, stdio: ["pipe", "ignore", "ignore"] });
1498
+ } catch {
1499
+ execSync("crontab -", { input: cronLine + `
1500
+ `, stdio: ["pipe", "ignore", "ignore"] });
1501
+ }
1502
+ }
1503
+ function installSync(apiKey) {
1504
+ const messages = [];
1505
+ const os = platform2();
1506
+ persistBinary(apiKey);
1507
+ messages.push("Persisted CLI to ~/.conare/bin/");
1508
+ messages.push("Saved config to ~/.conare/config.json");
1509
+ if (os === "darwin") {
1510
+ setupMacOS();
1511
+ messages.push("Installed launchd agent (every 10 min)");
1512
+ messages.push(`Plist: ${PLIST_PATH}`);
1513
+ } else if (os === "linux") {
1514
+ if (hasSystemd()) {
1515
+ setupLinuxSystemd();
1516
+ messages.push("Installed systemd timer (every 10 min)");
1517
+ } else {
1518
+ setupLinuxCron();
1519
+ messages.push("Installed cron job (every 10 min)");
1520
+ }
1521
+ } else {
1522
+ messages.push(`Unsupported platform: ${os}. Run manually: ~/.conare/bin/run.sh`);
1523
+ }
1524
+ return messages;
1525
+ }
1526
+ function uninstallSync() {
1527
+ const messages = [];
1528
+ const os = platform2();
1529
+ if (os === "darwin") {
1530
+ try {
1531
+ execSync(`launchctl bootout gui/${uid()} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
1532
+ } catch {}
1533
+ if (existsSync7(PLIST_PATH)) {
1534
+ unlinkSync(PLIST_PATH);
1535
+ messages.push("Removed launchd agent");
1536
+ }
1537
+ } else if (os === "linux") {
1538
+ if (hasSystemd()) {
1539
+ try {
1540
+ execSync("systemctl --user disable --now conare-sync.timer 2>/dev/null", { stdio: "ignore" });
1541
+ } catch {}
1542
+ if (existsSync7(SYSTEMD_SERVICE))
1543
+ unlinkSync(SYSTEMD_SERVICE);
1544
+ if (existsSync7(SYSTEMD_TIMER))
1545
+ unlinkSync(SYSTEMD_TIMER);
1546
+ try {
1547
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
1548
+ } catch {}
1549
+ messages.push("Removed systemd timer");
1550
+ } else {
1551
+ try {
1552
+ const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
1553
+ const filtered = existing.split(`
1554
+ `).filter((l) => !l.includes("conare")).join(`
1555
+ `);
1556
+ execSync("crontab -", { input: filtered.trim() + `
1557
+ `, stdio: ["pipe", "ignore", "ignore"] });
1558
+ messages.push("Removed cron job");
1559
+ } catch {}
1560
+ }
1561
+ }
1562
+ const filesToRemove = [
1563
+ join9(CONARE_DIR, "config.json"),
1564
+ join9(CONARE_DIR, "sync.lock")
1565
+ ];
1566
+ for (const f of filesToRemove) {
1567
+ if (existsSync7(f))
1568
+ unlinkSync(f);
1569
+ }
1570
+ if (existsSync7(BIN_DIR)) {
1571
+ rmSync(BIN_DIR, { recursive: true, force: true });
1572
+ messages.push("Removed ~/.conare/bin/");
1573
+ }
1574
+ if (messages.length === 0) {
1575
+ messages.push("Nothing to uninstall (no sync timer found)");
1576
+ }
1577
+ return messages;
1578
+ }
1579
+
1277
1580
  // node_modules/@clack/prompts/dist/index.mjs
1278
1581
  import { stripVTControlCharacters as S2 } from "node:util";
1279
1582
 
@@ -1986,21 +2289,33 @@ function renderDiscoverySummary(discovered, filtered, deduped) {
1986
2289
  function parseArgs() {
1987
2290
  const args = process.argv.slice(2);
1988
2291
  let key = "";
2292
+ let configFile;
1989
2293
  let dryRun = false;
1990
2294
  let force = false;
1991
2295
  let ingestOnly = false;
1992
2296
  let configOnly = false;
1993
2297
  let interactive = false;
2298
+ let quiet = false;
2299
+ let installSyncFlag = false;
2300
+ let uninstallSyncFlag = false;
1994
2301
  let source;
1995
2302
  let wasmDir;
1996
2303
  let indexPath;
1997
2304
  for (let i = 0;i < args.length; i++) {
1998
2305
  if (args[i] === "--key" && args[i + 1]) {
1999
2306
  key = args[++i];
2307
+ } else if (args[i] === "--config-file" && args[i + 1]) {
2308
+ configFile = args[++i];
2000
2309
  } else if (args[i] === "--dry-run") {
2001
2310
  dryRun = true;
2002
2311
  } else if (args[i] === "--force") {
2003
2312
  force = true;
2313
+ } else if (args[i] === "--quiet") {
2314
+ quiet = true;
2315
+ } else if (args[i] === "--install-sync") {
2316
+ installSyncFlag = true;
2317
+ } else if (args[i] === "--uninstall-sync") {
2318
+ uninstallSyncFlag = true;
2004
2319
  } else if (args[i] === "--source" && args[i + 1]) {
2005
2320
  source = args[++i];
2006
2321
  } else if (args[i] === "--wasm-dir" && args[i + 1]) {
@@ -2023,9 +2338,13 @@ Usage:
2023
2338
 
2024
2339
  Options:
2025
2340
  --key <key> Your Conare API key (required, starts with cmem_)
2341
+ --config-file <path> Read API key from JSON config file (e.g. ~/.conare/config.json)
2026
2342
  --index [path] Index codebase at path (default: current directory)
2027
2343
  --dry-run Preview what would be uploaded
2028
2344
  --force Re-ingest all / re-index all (bypass dedup)
2345
+ --quiet Suppress all stdout output (for background timer runs)
2346
+ --install-sync Set up automatic background sync (every 10 min)
2347
+ --uninstall-sync Remove background sync timer and persisted files
2029
2348
  --ingest-only Ingest memories without MCP configuration
2030
2349
  --config-only Configure MCP only, skip ingestion
2031
2350
  --interactive Run guided setup prompts
@@ -2045,20 +2364,37 @@ Get your API key at https://mcp.conare.ai
2045
2364
  console.error("Error: --ingest-only and --config-only cannot be used together");
2046
2365
  process.exit(1);
2047
2366
  }
2048
- return { key, dryRun, force, ingestOnly, configOnly, interactive, source, wasmDir, indexPath };
2367
+ return { key, configFile, dryRun, force, ingestOnly, configOnly, interactive, quiet, installSync: installSyncFlag, uninstallSync: uninstallSyncFlag, source, wasmDir, indexPath };
2049
2368
  }
2050
2369
  async function main() {
2051
2370
  const opts = parseArgs();
2371
+ let configFileKey;
2372
+ if (opts.configFile) {
2373
+ try {
2374
+ const { readFileSync: readFileSync9 } = await import("node:fs");
2375
+ const raw = JSON.parse(readFileSync9(opts.configFile, "utf-8"));
2376
+ configFileKey = raw.apiKey || raw.key;
2377
+ if (!configFileKey) {
2378
+ console.error(`Error: no apiKey/key found in ${opts.configFile}`);
2379
+ process.exit(1);
2380
+ }
2381
+ } catch (e2) {
2382
+ console.error(`Error reading config file: ${e2.message}`);
2383
+ process.exit(1);
2384
+ }
2385
+ }
2386
+ const log = opts.quiet ? (..._args) => {} : console.log.bind(console);
2387
+ const write = opts.quiet ? (_s) => {} : (s) => process.stdout.write(s);
2052
2388
  const savedApiKey = getSavedApiKey();
2053
2389
  const hasTty = !!process.stdin.isTTY && !!process.stdout.isTTY;
2054
- const shouldRunInteractive = hasTty && !opts.dryRun && (opts.interactive || !opts.force && !opts.source && !opts.indexPath && !opts.configOnly && !opts.ingestOnly && !opts.wasmDir);
2390
+ const shouldRunInteractive = hasTty && !opts.dryRun && !opts.quiet && (opts.interactive || !opts.force && !opts.source && !opts.indexPath && !opts.configOnly && !opts.ingestOnly && !opts.wasmDir);
2055
2391
  let selectedTargets = MCP_TARGETS.map((target) => target.id);
2056
2392
  let effectiveConfigOnly = opts.configOnly;
2057
2393
  let effectiveIngestOnly = opts.ingestOnly;
2058
2394
  let effectiveIndexPath = opts.indexPath;
2059
2395
  let postIngestIndexPath;
2060
2396
  let selectedSources = opts.source ? [opts.source] : ["claude", "cursor", "codex"];
2061
- let apiKey = opts.key || process.env.CONARE_API_KEY || savedApiKey;
2397
+ let apiKey = opts.key || configFileKey || process.env.CONARE_API_KEY || savedApiKey;
2062
2398
  let interactiveTargets = MCP_TARGETS.map((target) => ({
2063
2399
  id: target.id,
2064
2400
  label: target.label,
@@ -2088,175 +2424,207 @@ async function main() {
2088
2424
  selectedSources = await selectChatSources(interactiveTargets);
2089
2425
  interactiveMode = true;
2090
2426
  }
2427
+ if (opts.uninstallSync) {
2428
+ const messages = uninstallSync();
2429
+ for (const msg of messages)
2430
+ console.log(` ${msg}`);
2431
+ return;
2432
+ }
2091
2433
  if (!apiKey) {
2092
2434
  printMissingKeyError();
2093
2435
  process.exit(1);
2094
2436
  }
2095
- console.log("");
2096
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2097
- console.log(" Conare Installer");
2098
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2099
- console.log("");
2100
- process.stdout.write("Validating API key... ");
2437
+ if (opts.installSync) {
2438
+ console.log("");
2439
+ console.log("Setting up background sync...");
2440
+ console.log("");
2441
+ const auth2 = await validateKey(apiKey);
2442
+ if (!auth2.valid) {
2443
+ console.error("INVALID key. Check your key at https://mcp.conare.ai");
2444
+ process.exit(1);
2445
+ }
2446
+ const messages = installSync(apiKey);
2447
+ for (const msg of messages)
2448
+ console.log(` ✓ ${msg}`);
2449
+ console.log("");
2450
+ console.log("Background sync active. New chats will be ingested every 10 minutes.");
2451
+ console.log("Logs: ~/.conare/ingest.log");
2452
+ console.log("Remove with: conare --uninstall-sync");
2453
+ console.log("");
2454
+ return;
2455
+ }
2456
+ log("");
2457
+ log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2458
+ log(" Conare Installer");
2459
+ log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2460
+ log("");
2461
+ write("Validating API key... ");
2101
2462
  const auth = await validateKey(apiKey);
2102
2463
  if (!auth.valid) {
2103
2464
  console.error("INVALID. Check your key at https://mcp.conare.ai");
2104
2465
  process.exit(1);
2105
2466
  }
2106
- console.log(auth.email ? `OK (${auth.email})` : "OK");
2467
+ log(auth.email ? `OK (${auth.email})` : "OK");
2107
2468
  saveApiKey(apiKey);
2108
- console.log();
2469
+ log();
2109
2470
  if (!opts.force && !opts.dryRun) {
2110
2471
  const remoteMemoryCount = await getRemoteMemoryCount(apiKey);
2111
2472
  const localManifest = getIngested();
2112
2473
  const localManifestCount = Object.values(localManifest).reduce((sum, entries) => sum + entries.length, 0);
2113
2474
  if (remoteMemoryCount === 0 && localManifestCount > 0) {
2114
2475
  clearIngested();
2115
- console.log("Remote account is empty; cleared stale local ingestion history.");
2116
- console.log("");
2476
+ log("Remote account is empty; cleared stale local ingestion history.");
2477
+ log("");
2117
2478
  }
2118
2479
  }
2119
- if (!opts.wasmDir && existsSync7(join9(process.cwd(), "node_modules", "sql.js"))) {
2120
- opts.wasmDir = join9(process.cwd(), "node_modules");
2480
+ if (!opts.wasmDir && existsSync8(join10(process.cwd(), "node_modules", "sql.js"))) {
2481
+ opts.wasmDir = join10(process.cwd(), "node_modules");
2121
2482
  }
2122
2483
  if (effectiveConfigOnly) {
2123
2484
  if (!opts.dryRun) {
2124
- console.log("─── Configuring MCP ───");
2125
- console.log("");
2485
+ log("─── Configuring MCP ───");
2486
+ log("");
2126
2487
  for (const line of configureMcp(apiKey, selectedTargets)) {
2127
- console.log(` ✓ ${line}`);
2488
+ log(` ✓ ${line}`);
2128
2489
  }
2129
- console.log("");
2490
+ log("");
2130
2491
  }
2131
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2132
- console.log(" ✓ Done! Conare MCP is configured.");
2133
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2134
- console.log("");
2492
+ log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2493
+ log(" ✓ Done! Conare MCP is configured.");
2494
+ log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2495
+ log("");
2135
2496
  return;
2136
2497
  }
2137
2498
  if (effectiveIndexPath !== undefined) {
2138
2499
  const { resolve: resolve2 } = await import("node:path");
2139
2500
  const absPath = resolve2(effectiveIndexPath);
2140
- console.log(`Indexing codebase: ${absPath}`);
2501
+ log(`Indexing codebase: ${absPath}`);
2141
2502
  if (opts.force) {
2142
2503
  clearIngested("codebase");
2143
- process.stdout.write("Clearing existing codebase index... ");
2504
+ write("Clearing existing codebase index... ");
2144
2505
  try {
2145
2506
  await fetch("https://mcp.conare.ai/api/containers/codebase", {
2146
2507
  method: "DELETE",
2147
2508
  headers: { Authorization: `Bearer ${apiKey}` }
2148
2509
  });
2149
- console.log("done");
2510
+ log("done");
2150
2511
  } catch {
2151
- console.log("skipped (no existing index)");
2512
+ log("skipped (no existing index)");
2152
2513
  }
2153
2514
  }
2154
- process.stdout.write("Scanning files... ");
2515
+ write("Scanning files... ");
2155
2516
  const { memories, fileCount, skipped } = indexCodebase(absPath);
2156
- console.log(`${fileCount} files found, ${skipped} skipped`);
2517
+ log(`${fileCount} files found, ${skipped} skipped`);
2157
2518
  if (memories.length === 0) {
2158
- console.log(`
2519
+ log(`
2159
2520
  Nothing new to index.`);
2160
2521
  } else if (opts.dryRun) {
2161
- console.log(`
2522
+ log(`
2162
2523
  [DRY RUN] Would upload ${memories.length} files`);
2163
2524
  for (const m2 of memories.slice(0, 10)) {
2164
2525
  const meta = m2.metadata;
2165
- console.log(` ${meta.language.padEnd(12)} ${meta.filePath}`);
2526
+ log(` ${meta.language.padEnd(12)} ${meta.filePath}`);
2166
2527
  }
2167
2528
  if (memories.length > 10)
2168
- console.log(` ... and ${memories.length - 10} more`);
2529
+ log(` ... and ${memories.length - 10} more`);
2169
2530
  } else {
2170
2531
  const barWidth = 20;
2171
2532
  const { success, failed, results } = await uploadBulk(apiKey, memories, (uploaded, total, _failed) => {
2172
2533
  const pct = (uploaded / total * 100).toFixed(1);
2173
2534
  const filled = Math.round(uploaded / total * barWidth);
2174
2535
  const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
2175
- process.stdout.write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
2536
+ write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
2176
2537
  });
2177
- process.stdout.write(renderProgressSummary(success, failed, "indexed"));
2538
+ write(renderProgressSummary(success, failed, "indexed"));
2178
2539
  const fileHashes = results.filter((result) => result.success).map((result) => getManifestFingerprint(memories[result.index])).filter((key) => !!key);
2179
2540
  markIngested("codebase", fileHashes);
2180
- printFailureSummary(results, memories);
2541
+ if (!opts.quiet)
2542
+ printFailureSummary(results, memories);
2181
2543
  }
2182
- console.log();
2544
+ log();
2183
2545
  if (!opts.dryRun && !effectiveIngestOnly) {
2184
- console.log("─── Configuring MCP ───");
2185
- console.log("");
2546
+ log("─── Configuring MCP ───");
2547
+ log("");
2186
2548
  for (const line of configureMcp(apiKey, selectedTargets)) {
2187
- console.log(` ✓ ${line}`);
2549
+ log(` ✓ ${line}`);
2188
2550
  }
2189
- console.log("");
2551
+ log("");
2190
2552
  }
2191
- console.log("Done! Your codebase is now searchable via recall.");
2553
+ log("Done! Your codebase is now searchable via recall.");
2192
2554
  return;
2193
2555
  }
2194
2556
  if (opts.force) {
2195
2557
  if (opts.source) {
2196
2558
  clearIngested(opts.source);
2197
- console.log(`Cleared ingestion history for ${opts.source}`);
2559
+ log(`Cleared ingestion history for ${opts.source}`);
2198
2560
  } else {
2199
2561
  clearIngested();
2200
- console.log("Cleared all ingestion history");
2562
+ log("Cleared all ingestion history");
2201
2563
  }
2202
- console.log();
2564
+ log();
2203
2565
  }
2204
2566
  const tools = detect();
2205
2567
  if (!interactiveMode) {
2206
- console.log("Detected AI tools:");
2568
+ log("Detected AI tools:");
2207
2569
  for (const t of tools) {
2208
2570
  if (t.available) {
2209
- console.log(` + ${t.name}${t.sessionCount || t.sessionCount === 0 ? ` — ${t.sessionCount} sessions` : ""}`);
2571
+ log(` + ${t.name}${t.sessionCount || t.sessionCount === 0 ? ` — ${t.sessionCount} sessions` : ""}`);
2210
2572
  } else {
2211
- console.log(` - ${t.name} (not found)`);
2573
+ log(` - ${t.name} (not found)`);
2212
2574
  }
2213
2575
  }
2214
- console.log();
2576
+ log();
2215
2577
  }
2216
2578
  const allMemories = [];
2217
2579
  const shouldIngest = (name) => selectedSources.includes(name);
2218
2580
  if (shouldIngest("claude") && tools.find((t) => t.name === "Claude Code")?.available) {
2219
- process.stdout.write("Ingesting Claude Code... ");
2581
+ write("Ingesting Claude Code... ");
2220
2582
  const { memories, filtered, deduped } = ingestClaude();
2221
2583
  allMemories.push(...memories);
2222
- console.log(renderDiscoverySummary(memories.length, filtered, deduped));
2584
+ log(renderDiscoverySummary(memories.length, filtered, deduped));
2223
2585
  }
2224
2586
  if (shouldIngest("codex") && tools.find((t) => t.name === "Codex")?.available) {
2225
- process.stdout.write("Ingesting Codex... ");
2587
+ write("Ingesting Codex... ");
2226
2588
  const { memories, filtered, deduped } = ingestCodex();
2227
2589
  allMemories.push(...memories);
2228
- console.log(renderDiscoverySummary(memories.length, filtered, deduped));
2590
+ log(renderDiscoverySummary(memories.length, filtered, deduped));
2229
2591
  }
2230
2592
  if (shouldIngest("cursor") && tools.find((t) => t.name === "Cursor")?.available) {
2231
- process.stdout.write("Ingesting Cursor... ");
2593
+ write("Ingesting Cursor... ");
2232
2594
  const cursorTool = tools.find((t) => t.name === "Cursor");
2233
2595
  const { memories, filtered, deduped } = await ingestCursor(cursorTool.path, opts.wasmDir);
2234
2596
  allMemories.push(...memories);
2235
- console.log(renderDiscoverySummary(memories.length, filtered, deduped));
2597
+ log(renderDiscoverySummary(memories.length, filtered, deduped));
2236
2598
  }
2237
- console.log();
2599
+ allMemories.sort((a, b3) => {
2600
+ const da = a.metadata?.date || "0000";
2601
+ const db = b3.metadata?.date || "0000";
2602
+ return db.localeCompare(da);
2603
+ });
2604
+ log();
2238
2605
  if (allMemories.length === 0) {
2239
- console.log("Nothing new to upload.");
2606
+ log("Nothing new to upload.");
2240
2607
  } else if (opts.dryRun) {
2241
- console.log(`[DRY RUN] Would upload ${allMemories.length} memories`);
2608
+ log(`[DRY RUN] Would upload ${allMemories.length} memories`);
2242
2609
  for (const m2 of allMemories.slice(0, 5)) {
2243
- console.log(` ${m2.containerTag} | ${m2.content.length} chars | ${m2.metadata?.sessionId?.slice(0, 12) || "?"}`);
2610
+ log(` ${m2.containerTag} | ${m2.content.length} chars | ${m2.metadata?.sessionId?.slice(0, 12) || "?"}`);
2244
2611
  }
2245
2612
  if (allMemories.length > 5)
2246
- console.log(` ... and ${allMemories.length - 5} more`);
2613
+ log(` ... and ${allMemories.length - 5} more`);
2247
2614
  } else {
2248
2615
  const barWidth = 20;
2249
2616
  const { success, failed, results } = await uploadBulk(apiKey, allMemories, (uploaded, total, _failed) => {
2250
2617
  const pct = (uploaded / total * 100).toFixed(1);
2251
2618
  const filled = Math.round(uploaded / total * barWidth);
2252
2619
  const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
2253
- process.stdout.write(`\r Uploading [\x1B[33m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
2620
+ write(`\r Uploading [\x1B[33m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
2254
2621
  });
2255
- process.stdout.write(renderProgressSummary(success, failed, "uploaded"));
2622
+ write(renderProgressSummary(success, failed, "uploaded"));
2256
2623
  if (failed > 0) {
2257
- console.log(` Re-run with --force to retry failed memories.`);
2624
+ log(` Re-run with --force to retry failed memories.`);
2258
2625
  }
2259
- printFailureSummary(results, allMemories);
2626
+ if (!opts.quiet)
2627
+ printFailureSummary(results, allMemories);
2260
2628
  const successfulKeysBySource = {
2261
2629
  claude: [],
2262
2630
  codex: [],
@@ -2286,7 +2654,7 @@ Nothing new to index.`);
2286
2654
  markIngested(source, ids);
2287
2655
  }
2288
2656
  }
2289
- console.log();
2657
+ log();
2290
2658
  if (interactiveMode) {
2291
2659
  selectedTargets = await selectMcpTargets(interactiveTargets);
2292
2660
  const shouldIndexCurrentCodebase = await confirmIndexCodebase();
@@ -2302,23 +2670,23 @@ Nothing new to index.`);
2302
2670
  finishSetup();
2303
2671
  }
2304
2672
  if (!opts.dryRun && !effectiveIngestOnly) {
2305
- console.log("─── Configuring MCP ───");
2306
- console.log("");
2673
+ log("─── Configuring MCP ───");
2674
+ log("");
2307
2675
  for (const line of configureMcp(apiKey, selectedTargets)) {
2308
- console.log(` ✓ ${line}`);
2676
+ log(` ✓ ${line}`);
2309
2677
  }
2310
- console.log("");
2678
+ log("");
2311
2679
  }
2312
2680
  if (postIngestIndexPath) {
2313
2681
  const { resolve: resolve2 } = await import("node:path");
2314
2682
  const absPath = resolve2(postIngestIndexPath);
2315
- console.log("─── Indexing codebase ───");
2316
- console.log("");
2317
- process.stdout.write("Scanning files... ");
2683
+ log("─── Indexing codebase ───");
2684
+ log("");
2685
+ write("Scanning files... ");
2318
2686
  const { memories, fileCount, skipped } = indexCodebase(absPath);
2319
- console.log(`${fileCount} files found, ${skipped} skipped`);
2687
+ log(`${fileCount} files found, ${skipped} skipped`);
2320
2688
  if (memories.length === 0) {
2321
- console.log(`
2689
+ log(`
2322
2690
  Nothing new to index.`);
2323
2691
  } else {
2324
2692
  const barWidth = 20;
@@ -2326,20 +2694,21 @@ Nothing new to index.`);
2326
2694
  const pct = (uploaded / total * 100).toFixed(1);
2327
2695
  const filled = Math.round(uploaded / total * barWidth);
2328
2696
  const bar = "█".repeat(filled) + "░".repeat(barWidth - filled);
2329
- process.stdout.write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
2697
+ write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
2330
2698
  });
2331
- process.stdout.write(renderProgressSummary(success, failed, "indexed"));
2699
+ write(renderProgressSummary(success, failed, "indexed"));
2332
2700
  const fileHashes = results.filter((result) => result.success).map((result) => getManifestFingerprint(memories[result.index])).filter((key) => !!key);
2333
2701
  markIngested("codebase", fileHashes);
2334
- printFailureSummary(results, memories);
2702
+ if (!opts.quiet)
2703
+ printFailureSummary(results, memories);
2335
2704
  }
2336
- console.log("");
2705
+ log("");
2337
2706
  }
2338
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2339
- console.log(" ✓ Done! Every new conversation now starts with context.");
2340
- console.log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2341
- console.log(" Next time you can run `conare` after a global install.");
2342
- console.log("");
2707
+ log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2708
+ log(" ✓ Done! Every new conversation now starts with context.");
2709
+ log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2710
+ log(" Next time you can run `conare` after a global install.");
2711
+ log("");
2343
2712
  }
2344
2713
  main().catch((e2) => {
2345
2714
  console.error("Error:", e2.message || e2);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conare",
3
- "version": "0.1.5",
3
+ "version": "0.2.0",
4
4
  "description": "Conare CLI for ingesting AI chat history and configuring memory at conare.ai",
5
5
  "type": "module",
6
6
  "bin": {