conare 0.2.1 → 0.2.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 +320 -126
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -285,6 +285,16 @@ import { createHash } from "node:crypto";
285
285
  import { join as join2 } from "node:path";
286
286
  import { homedir as homedir2 } from "node:os";
287
287
  var MANIFEST_PATH = join2(homedir2(), ".conare", "ingested.json");
288
+ var MIN_SUBSTANTIVE = 200;
289
+ 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))/;
290
+ function isNarration(text) {
291
+ const stripped = text.trim();
292
+ if (stripped.length < MIN_SUBSTANTIVE)
293
+ return true;
294
+ if (stripped.length <= 300 && NARRATION_RE.test(stripped))
295
+ return true;
296
+ return false;
297
+ }
288
298
  function cleanText(raw) {
289
299
  let text = raw;
290
300
  text = text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "");
@@ -344,8 +354,10 @@ function extractText(content) {
344
354
  `);
345
355
  }
346
356
  function parseSession(lines) {
347
- const messages = [];
357
+ const rounds = [];
348
358
  let date = null;
359
+ let currentUser = null;
360
+ let currentAssistant = [];
349
361
  for (const line of lines) {
350
362
  if (!line.trim())
351
363
  continue;
@@ -357,20 +369,31 @@ function parseSession(lines) {
357
369
  }
358
370
  if (!date && obj.timestamp)
359
371
  date = obj.timestamp.slice(0, 10);
360
- if (obj.type === "user" || obj.type === "assistant") {
372
+ if (obj.type === "user") {
361
373
  const text = cleanText(extractText(obj.message?.content));
362
374
  if (text.length >= MIN_TURN_LEN) {
363
- messages.push({ role: obj.type, text });
375
+ if (currentUser !== null && currentAssistant.length > 0) {
376
+ rounds.push({ user: currentUser, assistantParts: currentAssistant });
377
+ }
378
+ currentUser = text;
379
+ currentAssistant = [];
380
+ }
381
+ } else if (obj.type === "assistant") {
382
+ const text = cleanText(extractText(obj.message?.content));
383
+ if (text.length >= MIN_TURN_LEN) {
384
+ currentAssistant.push(text);
364
385
  }
365
386
  }
366
387
  }
367
- const turns = [];
368
- for (let i = 0;i < messages.length - 1; i++) {
369
- if (messages[i].role === "user" && messages[i + 1].role === "assistant") {
370
- turns.push({ user: messages[i].text, assistant: messages[i + 1].text });
371
- i++;
372
- }
388
+ if (currentUser !== null && currentAssistant.length > 0) {
389
+ rounds.push({ user: currentUser, assistantParts: currentAssistant });
373
390
  }
391
+ const turns = rounds.map((r) => ({
392
+ user: r.user,
393
+ assistant: r.assistantParts.filter((p) => !isNarration(p)).join(`
394
+
395
+ `)
396
+ })).filter((t) => t.assistant.length >= MIN_TURN_LEN);
374
397
  return { turns, date };
375
398
  }
376
399
  function ingestClaude() {
@@ -567,9 +590,11 @@ function walkCodexSessions(dir, memories, sessionIds, stats) {
567
590
  try {
568
591
  const lines = readFileSync3(join4(dir, entry.name), "utf-8").split(`
569
592
  `).filter(Boolean);
570
- const turns = [];
571
593
  let date = null;
572
594
  let project = null;
595
+ const rounds = [];
596
+ let currentUser = null;
597
+ let currentAssistant = [];
573
598
  for (const line of lines) {
574
599
  try {
575
600
  const obj = JSON.parse(line);
@@ -578,16 +603,33 @@ function walkCodexSessions(dir, memories, sessionIds, stats) {
578
603
  if (obj.type === "session_meta" && obj.payload?.cwd) {
579
604
  project = projectFromCwd(obj.payload.cwd);
580
605
  }
581
- if (obj.type === "response_item" && obj.payload?.role === "user") {
582
- const content2 = obj.payload.content;
583
- if (Array.isArray(content2)) {
584
- for (const block of content2) {
585
- if (block.type === "input_text" && block.text) {
586
- const text = cleanText(block.text);
587
- if (isCodexBoilerplate(text))
588
- continue;
589
- if (text.length > 50)
590
- turns.push(text);
606
+ if (obj.type !== "response_item" || obj.payload?.type !== "message")
607
+ continue;
608
+ const role = obj.payload.role;
609
+ const msgContent = obj.payload.content;
610
+ if (!Array.isArray(msgContent))
611
+ continue;
612
+ if (role === "user") {
613
+ for (const block of msgContent) {
614
+ if (block.type === "input_text" && block.text) {
615
+ const text = cleanText(block.text);
616
+ if (isCodexBoilerplate(text))
617
+ continue;
618
+ if (text.length >= 50) {
619
+ if (currentUser !== null && currentAssistant.length > 0) {
620
+ rounds.push({ user: currentUser, assistantParts: currentAssistant });
621
+ }
622
+ currentUser = text;
623
+ currentAssistant = [];
624
+ }
625
+ }
626
+ }
627
+ } else if (role === "assistant") {
628
+ for (const block of msgContent) {
629
+ if (block.type === "output_text" && block.text) {
630
+ const text = cleanText(block.text);
631
+ if (!isNarration(text)) {
632
+ currentAssistant.push(text);
591
633
  }
592
634
  }
593
635
  }
@@ -596,16 +638,27 @@ function walkCodexSessions(dir, memories, sessionIds, stats) {
596
638
  continue;
597
639
  }
598
640
  }
599
- if (turns.length === 0) {
641
+ if (currentUser !== null && currentAssistant.length > 0) {
642
+ rounds.push({ user: currentUser, assistantParts: currentAssistant });
643
+ }
644
+ if (rounds.length === 0) {
600
645
  stats.filtered++;
601
646
  continue;
602
647
  }
603
- const body = turns.map((t) => t.length > 500 ? t.slice(0, 500) + "..." : t).join(`
648
+ const body = rounds.map((r) => {
649
+ const q = r.user.length > 300 ? r.user.slice(0, 300) + "..." : r.user;
650
+ const assistant = r.assistantParts.join(`
651
+
652
+ `);
653
+ return `## Q: ${q}
654
+
655
+ ${assistant}`;
656
+ }).join(`
604
657
 
605
658
  ---
606
659
 
607
660
  `);
608
- let content = `# Codex Session | ${date || "unknown"}
661
+ let content = `# Codex Session${project ? `: ${project}` : ""} | ${date || "unknown"}
609
662
 
610
663
  ${body}`;
611
664
  if (content.length > MAX_CONTENT2)
@@ -1276,7 +1329,7 @@ function getSavedApiKey() {
1276
1329
  }
1277
1330
 
1278
1331
  // src/sync.ts
1279
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync8, chmodSync, cpSync, rmSync } from "node:fs";
1332
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4, unlinkSync, readFileSync as readFileSync8, chmodSync, cpSync, rmSync, symlinkSync, readlinkSync, appendFileSync } from "node:fs";
1280
1333
  import { join as join9, dirname as dirname2 } from "node:path";
1281
1334
  import { homedir as homedir7, platform as platform2 } from "node:os";
1282
1335
  import { execSync } from "node:child_process";
@@ -1351,7 +1404,7 @@ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) START sync" >> "$LOG"
1351
1404
  2>> "$LOG"
1352
1405
  echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) DONE sync (exit $?)" >> "$LOG"
1353
1406
  `;
1354
- function makePlist() {
1407
+ function makePlist(intervalMinutes) {
1355
1408
  const home = homedir7();
1356
1409
  return `<?xml version="1.0" encoding="UTF-8"?>
1357
1410
  <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
@@ -1366,7 +1419,7 @@ function makePlist() {
1366
1419
  <string>${home}/.conare/bin/run.sh</string>
1367
1420
  </array>
1368
1421
  <key>StartInterval</key>
1369
- <integer>600</integer>
1422
+ <integer>${intervalMinutes * 60}</integer>
1370
1423
  <key>StandardOutPath</key>
1371
1424
  <string>${home}/.conare/ingest.log</string>
1372
1425
  <key>StandardErrorPath</key>
@@ -1383,17 +1436,19 @@ Description=Conare Memory — background sync
1383
1436
  Type=oneshot
1384
1437
  ExecStart=%h/.conare/bin/run.sh
1385
1438
  `;
1386
- var SYSTEMD_TIMER_CONTENT = `[Unit]
1387
- Description=Conare Memory — sync every 10 minutes
1439
+ function makeSystemdTimer(intervalMinutes) {
1440
+ return `[Unit]
1441
+ Description=Conare Memory — sync every ${intervalMinutes} minutes
1388
1442
 
1389
1443
  [Timer]
1390
1444
  OnBootSec=2min
1391
- OnUnitActiveSec=10min
1445
+ OnUnitActiveSec=${intervalMinutes}min
1392
1446
  Persistent=true
1393
1447
 
1394
1448
  [Install]
1395
1449
  WantedBy=timers.target
1396
1450
  `;
1451
+ }
1397
1452
  function hasSystemd() {
1398
1453
  try {
1399
1454
  execSync("systemctl --user status 2>/dev/null", { stdio: "ignore" });
@@ -1458,10 +1513,10 @@ function findSqlJs() {
1458
1513
  }
1459
1514
  return null;
1460
1515
  }
1461
- function setupMacOS() {
1516
+ function setupMacOS(intervalMinutes) {
1462
1517
  const plistDir = dirname2(PLIST_PATH);
1463
1518
  mkdirSync4(plistDir, { recursive: true });
1464
- writeFileSync4(PLIST_PATH, makePlist());
1519
+ writeFileSync4(PLIST_PATH, makePlist(intervalMinutes));
1465
1520
  const id = uid();
1466
1521
  try {
1467
1522
  execSync(`launchctl bootout gui/${id} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
@@ -1476,16 +1531,16 @@ function setupMacOS() {
1476
1531
  }
1477
1532
  }
1478
1533
  }
1479
- function setupLinuxSystemd() {
1534
+ function setupLinuxSystemd(intervalMinutes) {
1480
1535
  mkdirSync4(SYSTEMD_DIR, { recursive: true });
1481
1536
  writeFileSync4(SYSTEMD_SERVICE, SYSTEMD_SERVICE_CONTENT);
1482
- writeFileSync4(SYSTEMD_TIMER, SYSTEMD_TIMER_CONTENT);
1537
+ writeFileSync4(SYSTEMD_TIMER, makeSystemdTimer(intervalMinutes));
1483
1538
  execSync("systemctl --user daemon-reload", { stdio: "ignore" });
1484
1539
  execSync("systemctl --user enable --now conare-sync.timer", { stdio: "ignore" });
1485
1540
  }
1486
- function setupLinuxCron() {
1541
+ function setupLinuxCron(intervalMinutes) {
1487
1542
  const cronCmd = `${homedir7()}/.conare/bin/run.sh`;
1488
- const cronLine = `*/10 * * * * ${cronCmd}`;
1543
+ const cronLine = `*/${intervalMinutes} * * * * ${cronCmd}`;
1489
1544
  try {
1490
1545
  const existing = execSync("crontab -l 2>/dev/null", { encoding: "utf-8" });
1491
1546
  const filtered = existing.split(`
@@ -1500,23 +1555,100 @@ function setupLinuxCron() {
1500
1555
  `, stdio: ["pipe", "ignore", "ignore"] });
1501
1556
  }
1502
1557
  }
1503
- function installSync(apiKey) {
1558
+ function installGlobalCommand() {
1559
+ const wrapper = join9(BIN_DIR, "conare");
1560
+ const content = `#!/bin/bash
1561
+ # Conare global command — runs the persisted CLI bundle
1562
+ CONARE_DIR="$HOME/.conare"
1563
+ NODE=$(command -v node || echo "")
1564
+ if [ -z "$NODE" ]; then
1565
+ if [ -s "$HOME/.nvm/nvm.sh" ]; then
1566
+ . "$HOME/.nvm/nvm.sh" >/dev/null 2>&1
1567
+ NODE=$(command -v node || echo "")
1568
+ fi
1569
+ fi
1570
+ if [ -z "$NODE" ]; then
1571
+ echo "Error: node not found. Install Node.js first." >&2
1572
+ exit 1
1573
+ fi
1574
+ exec "$NODE" "$CONARE_DIR/bin/conare-ingest.mjs" "$@"
1575
+ `;
1576
+ writeFileSync4(wrapper, content);
1577
+ chmodSync(wrapper, 493);
1578
+ const symlinkTarget = "/usr/local/bin/conare";
1579
+ try {
1580
+ if (existsSync7(symlinkTarget)) {
1581
+ try {
1582
+ const existing = readlinkSync(symlinkTarget);
1583
+ if (existing === wrapper)
1584
+ return "Global command: conare (already linked)";
1585
+ } catch {}
1586
+ unlinkSync(symlinkTarget);
1587
+ }
1588
+ symlinkSync(wrapper, symlinkTarget);
1589
+ return "Global command: conare (linked to /usr/local/bin)";
1590
+ } catch {
1591
+ const pathDirs = (process.env.PATH || "").split(":");
1592
+ if (pathDirs.includes(BIN_DIR)) {
1593
+ return "Global command: conare (via ~/.conare/bin in PATH)";
1594
+ }
1595
+ const shellProfile = getShellProfile();
1596
+ if (shellProfile) {
1597
+ try {
1598
+ const profileContent = existsSync7(shellProfile) ? readFileSync8(shellProfile, "utf-8") : "";
1599
+ const exportLine = `export PATH="$HOME/.conare/bin:$PATH"`;
1600
+ if (!profileContent.includes(".conare/bin")) {
1601
+ appendFileSync(shellProfile, `
1602
+ # Conare CLI
1603
+ ${exportLine}
1604
+ `);
1605
+ return `Global command: conare (added ~/.conare/bin to ${shellProfile} — restart shell)`;
1606
+ }
1607
+ return "Global command: conare (via ~/.conare/bin in PATH)";
1608
+ } catch {
1609
+ return `Global command: add ~/.conare/bin to your PATH manually`;
1610
+ }
1611
+ }
1612
+ return `Global command: add ~/.conare/bin to your PATH manually`;
1613
+ }
1614
+ }
1615
+ function getShellProfile() {
1616
+ const home = homedir7();
1617
+ const shell = process.env.SHELL || "";
1618
+ if (shell.includes("zsh"))
1619
+ return join9(home, ".zshrc");
1620
+ if (shell.includes("bash")) {
1621
+ const profile = join9(home, ".bash_profile");
1622
+ if (platform2() === "darwin" && existsSync7(profile))
1623
+ return profile;
1624
+ return join9(home, ".bashrc");
1625
+ }
1626
+ if (existsSync7(join9(home, ".zshrc")))
1627
+ return join9(home, ".zshrc");
1628
+ if (existsSync7(join9(home, ".bashrc")))
1629
+ return join9(home, ".bashrc");
1630
+ return null;
1631
+ }
1632
+ function installSync(apiKey, intervalMinutes = 10) {
1504
1633
  const messages = [];
1505
1634
  const os = platform2();
1506
1635
  persistBinary(apiKey);
1507
1636
  messages.push("Persisted CLI to ~/.conare/bin/");
1508
1637
  messages.push("Saved config to ~/.conare/config.json");
1638
+ const globalMsg = installGlobalCommand();
1639
+ if (globalMsg)
1640
+ messages.push(globalMsg);
1509
1641
  if (os === "darwin") {
1510
- setupMacOS();
1511
- messages.push("Installed launchd agent (every 10 min)");
1642
+ setupMacOS(intervalMinutes);
1643
+ messages.push(`Installed launchd agent (every ${intervalMinutes} min)`);
1512
1644
  messages.push(`Plist: ${PLIST_PATH}`);
1513
1645
  } else if (os === "linux") {
1514
1646
  if (hasSystemd()) {
1515
- setupLinuxSystemd();
1516
- messages.push("Installed systemd timer (every 10 min)");
1647
+ setupLinuxSystemd(intervalMinutes);
1648
+ messages.push(`Installed systemd timer (every ${intervalMinutes} min)`);
1517
1649
  } else {
1518
- setupLinuxCron();
1519
- messages.push("Installed cron job (every 10 min)");
1650
+ setupLinuxCron(intervalMinutes);
1651
+ messages.push(`Installed cron job (every ${intervalMinutes} min)`);
1520
1652
  }
1521
1653
  } else {
1522
1654
  messages.push(`Unsupported platform: ${os}. Run manually: ~/.conare/bin/run.sh`);
@@ -1582,8 +1714,9 @@ import { stripVTControlCharacters as S2 } from "node:util";
1582
1714
 
1583
1715
  // node_modules/@clack/core/dist/index.mjs
1584
1716
  var import_sisteransi = __toESM(require_src(), 1);
1585
- import { stdin as j, stdout as M } from "node:process";
1586
1717
  var import_picocolors = __toESM(require_picocolors(), 1);
1718
+ import { stdin as j, stdout as M } from "node:process";
1719
+ import * as g from "node:readline";
1587
1720
  import O from "node:readline";
1588
1721
  import { Writable as X } from "node:stream";
1589
1722
  function DD({ onlyFirst: e = false } = {}) {
@@ -1816,6 +1949,28 @@ function m(e, u) {
1816
1949
  const t = e;
1817
1950
  t.isTTY && t.setRawMode(u);
1818
1951
  }
1952
+ function fD({ input: e = j, output: u = M, overwrite: t = true, hideCursor: F = true } = {}) {
1953
+ const s = g.createInterface({ input: e, output: u, prompt: "", tabSize: 1 });
1954
+ g.emitKeypressEvents(e, s), e.isTTY && e.setRawMode(true);
1955
+ const i = (D, { name: C, sequence: n }) => {
1956
+ const E = String(D);
1957
+ if ($([E, C, n], "cancel")) {
1958
+ F && u.write(import_sisteransi.cursor.show), process.exit(0);
1959
+ return;
1960
+ }
1961
+ if (!t)
1962
+ return;
1963
+ const a = C === "return" ? 0 : -1, o = C === "return" ? -1 : 0;
1964
+ g.moveCursor(u, a, o, () => {
1965
+ g.clearLine(u, 1, () => {
1966
+ e.once("keypress", i);
1967
+ });
1968
+ });
1969
+ };
1970
+ return F && u.write(import_sisteransi.cursor.hide), e.once("keypress", i), () => {
1971
+ e.off("keypress", i), F && u.write(import_sisteransi.cursor.show), e.isTTY && !AD && e.setRawMode(false), s.terminal = false, s.close();
1972
+ };
1973
+ }
1819
1974
  var gD = Object.defineProperty;
1820
1975
  var vD = (e, u, t) => (u in e) ? gD(e, u, { enumerable: true, configurable: true, writable: true, value: t }) : e[u] = t;
1821
1976
  var h = (e, u, t) => (vD(e, typeof u != "symbol" ? u + "" : u, t), t);
@@ -2041,9 +2196,9 @@ var G2 = (t) => {
2041
2196
  const { cursor: n, options: r2, style: i } = t, s = t.maxItems ?? Number.POSITIVE_INFINITY, c = Math.max(process.stdout.rows - 4, 0), a = Math.min(c, Math.max(s, 5));
2042
2197
  let l2 = 0;
2043
2198
  n >= l2 + a - 3 ? l2 = Math.max(Math.min(n - a + 3, r2.length - a), 0) : n < l2 + 2 && (l2 = Math.max(n - 2, 0));
2044
- const $2 = a < r2.length && l2 > 0, g = a < r2.length && l2 + a < r2.length;
2199
+ const $2 = a < r2.length && l2 > 0, g2 = a < r2.length && l2 + a < r2.length;
2045
2200
  return r2.slice(l2, l2 + a).map((p2, v2, f) => {
2046
- const j2 = v2 === 0 && $2, E = v2 === f.length - 1 && g;
2201
+ const j2 = v2 === 0 && $2, E = v2 === f.length - 1 && g2;
2047
2202
  return j2 || E ? import_picocolors2.default.dim("...") : i(p2, v2 + l2 === n);
2048
2203
  });
2049
2204
  };
@@ -2159,6 +2314,57 @@ ${import_picocolors2.default.gray(d2)} ${t}
2159
2314
  `);
2160
2315
  };
2161
2316
  var J = `${import_picocolors2.default.gray(o)} `;
2317
+ var Y2 = ({ indicator: t = "dots" } = {}) => {
2318
+ const n = V2 ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"], r2 = V2 ? 80 : 120, i = process.env.CI === "true";
2319
+ let s, c, a = false, l2 = "", $2, g2 = performance.now();
2320
+ const p2 = (m2) => {
2321
+ const h2 = m2 > 1 ? "Something went wrong" : "Canceled";
2322
+ a && N2(h2, m2);
2323
+ }, v2 = () => p2(2), f = () => p2(1), j2 = () => {
2324
+ process.on("uncaughtExceptionMonitor", v2), process.on("unhandledRejection", v2), process.on("SIGINT", f), process.on("SIGTERM", f), process.on("exit", p2);
2325
+ }, E = () => {
2326
+ process.removeListener("uncaughtExceptionMonitor", v2), process.removeListener("unhandledRejection", v2), process.removeListener("SIGINT", f), process.removeListener("SIGTERM", f), process.removeListener("exit", p2);
2327
+ }, B2 = () => {
2328
+ if ($2 === undefined)
2329
+ return;
2330
+ i && process.stdout.write(`
2331
+ `);
2332
+ const m2 = $2.split(`
2333
+ `);
2334
+ process.stdout.write(import_sisteransi2.cursor.move(-999, m2.length - 1)), process.stdout.write(import_sisteransi2.erase.down(m2.length));
2335
+ }, R2 = (m2) => m2.replace(/\.+$/, ""), O2 = (m2) => {
2336
+ const h2 = (performance.now() - m2) / 1000, w2 = Math.floor(h2 / 60), I2 = Math.floor(h2 % 60);
2337
+ return w2 > 0 ? `[${w2}m ${I2}s]` : `[${I2}s]`;
2338
+ }, H2 = (m2 = "") => {
2339
+ a = true, s = fD(), l2 = R2(m2), g2 = performance.now(), process.stdout.write(`${import_picocolors2.default.gray(o)}
2340
+ `);
2341
+ let h2 = 0, w2 = 0;
2342
+ j2(), c = setInterval(() => {
2343
+ if (i && l2 === $2)
2344
+ return;
2345
+ B2(), $2 = l2;
2346
+ const I2 = import_picocolors2.default.magenta(n[h2]);
2347
+ if (i)
2348
+ process.stdout.write(`${I2} ${l2}...`);
2349
+ else if (t === "timer")
2350
+ process.stdout.write(`${I2} ${l2} ${O2(g2)}`);
2351
+ else {
2352
+ const z2 = ".".repeat(Math.floor(w2)).slice(0, 3);
2353
+ process.stdout.write(`${I2} ${l2}${z2}`);
2354
+ }
2355
+ h2 = h2 + 1 < n.length ? h2 + 1 : 0, w2 = w2 < n.length ? w2 + 0.125 : 0;
2356
+ }, r2);
2357
+ }, N2 = (m2 = "", h2 = 0) => {
2358
+ a = false, clearInterval(c), B2();
2359
+ const w2 = h2 === 0 ? import_picocolors2.default.green(C) : h2 === 1 ? import_picocolors2.default.red(L2) : import_picocolors2.default.red(W2);
2360
+ l2 = R2(m2 ?? l2), t === "timer" ? process.stdout.write(`${w2} ${l2} ${O2(g2)}
2361
+ `) : process.stdout.write(`${w2} ${l2}
2362
+ `), E(), s();
2363
+ };
2364
+ return { start: H2, stop: N2, message: (m2 = "") => {
2365
+ l2 = R2(m2 ?? l2);
2366
+ } };
2367
+ };
2162
2368
 
2163
2369
  // src/interactive.ts
2164
2370
  function formatDetectedCount(count) {
@@ -2298,6 +2504,7 @@ function parseArgs() {
2298
2504
  let quiet = false;
2299
2505
  let installSyncFlag = false;
2300
2506
  let uninstallSyncFlag = false;
2507
+ let syncInterval = 10;
2301
2508
  let source;
2302
2509
  let wasmDir;
2303
2510
  let indexPath;
@@ -2316,6 +2523,8 @@ function parseArgs() {
2316
2523
  installSyncFlag = true;
2317
2524
  } else if (args[i] === "--uninstall-sync") {
2318
2525
  uninstallSyncFlag = true;
2526
+ } else if (args[i] === "--sync-interval" && args[i + 1]) {
2527
+ syncInterval = parseInt(args[++i], 10);
2319
2528
  } else if (args[i] === "--source" && args[i + 1]) {
2320
2529
  source = args[++i];
2321
2530
  } else if (args[i] === "--wasm-dir" && args[i + 1]) {
@@ -2345,6 +2554,7 @@ Options:
2345
2554
  --quiet Suppress all stdout output (for background timer runs)
2346
2555
  --install-sync Set up automatic background sync (every 10 min)
2347
2556
  --uninstall-sync Remove background sync timer and persisted files
2557
+ --sync-interval <n> Sync interval in minutes (default: 10)
2348
2558
  --ingest-only Ingest memories without MCP configuration
2349
2559
  --config-only Configure MCP only, skip ingestion
2350
2560
  --interactive Run guided setup prompts
@@ -2364,7 +2574,7 @@ Get your API key at https://mcp.conare.ai
2364
2574
  console.error("Error: --ingest-only and --config-only cannot be used together");
2365
2575
  process.exit(1);
2366
2576
  }
2367
- return { key, configFile, dryRun, force, ingestOnly, configOnly, interactive, quiet, installSync: installSyncFlag, uninstallSync: uninstallSyncFlag, source, wasmDir, indexPath };
2577
+ return { key, configFile, dryRun, force, ingestOnly, configOnly, interactive, quiet, installSync: installSyncFlag, uninstallSync: uninstallSyncFlag, syncInterval, source, wasmDir, indexPath };
2368
2578
  }
2369
2579
  async function main() {
2370
2580
  const opts = parseArgs();
@@ -2435,38 +2645,25 @@ async function main() {
2435
2645
  process.exit(1);
2436
2646
  }
2437
2647
  if (opts.installSync) {
2438
- console.log("");
2439
- console.log("Setting up background sync...");
2440
- console.log("");
2441
2648
  const auth2 = await validateKey(apiKey);
2442
2649
  if (!auth2.valid) {
2443
- console.error("INVALID key. Check your key at https://mcp.conare.ai");
2650
+ console.error("Invalid API key. Check your key at https://mcp.conare.ai");
2444
2651
  process.exit(1);
2445
2652
  }
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");
2653
+ const syncSpinner = Y2();
2654
+ syncSpinner.start("Setting up background sync...");
2655
+ installSync(apiKey, opts.syncInterval);
2656
+ syncSpinner.stop(`Background sync active (every ${opts.syncInterval}min)`);
2453
2657
  console.log("");
2454
2658
  return;
2455
2659
  }
2456
2660
  log("");
2457
- log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2458
- log(" Conare Installer");
2459
- log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2460
- log("");
2461
- write("Validating API key... ");
2462
2661
  const auth = await validateKey(apiKey);
2463
2662
  if (!auth.valid) {
2464
- console.error("INVALID. Check your key at https://mcp.conare.ai");
2663
+ console.error("Invalid API key. Check your key at https://mcp.conare.ai");
2465
2664
  process.exit(1);
2466
2665
  }
2467
- log(auth.email ? `OK (${auth.email})` : "OK");
2468
2666
  saveApiKey(apiKey);
2469
- log();
2470
2667
  if (!opts.force && !opts.dryRun) {
2471
2668
  const remoteMemoryCount = await getRemoteMemoryCount(apiKey);
2472
2669
  const localManifest = getIngested();
@@ -2482,39 +2679,32 @@ async function main() {
2482
2679
  }
2483
2680
  if (effectiveConfigOnly) {
2484
2681
  if (!opts.dryRun) {
2485
- log("─── Configuring MCP ───");
2486
- log("");
2487
- for (const line of configureMcp(apiKey, selectedTargets)) {
2488
- log(` ${line}`);
2489
- }
2490
- log("");
2682
+ const mcpSpinner = Y2();
2683
+ mcpSpinner.start("Configuring MCP...");
2684
+ const lines = configureMcp(apiKey, selectedTargets);
2685
+ mcpSpinner.stop(`MCP configured: ${lines.join(", ")}`);
2491
2686
  }
2492
- log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2493
- log("Done! Conare MCP is configured.");
2494
- log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2687
+ log("");
2688
+ log("Done! Conare MCP is configured.");
2495
2689
  log("");
2496
2690
  return;
2497
2691
  }
2498
2692
  if (effectiveIndexPath !== undefined) {
2499
2693
  const { resolve: resolve2 } = await import("node:path");
2500
2694
  const absPath = resolve2(effectiveIndexPath);
2501
- log(`Indexing codebase: ${absPath}`);
2502
2695
  if (opts.force) {
2503
2696
  clearIngested("codebase");
2504
- write("Clearing existing codebase index... ");
2505
2697
  try {
2506
2698
  await fetch("https://mcp.conare.ai/api/containers/codebase", {
2507
2699
  method: "DELETE",
2508
2700
  headers: { Authorization: `Bearer ${apiKey}` }
2509
2701
  });
2510
- log("done");
2511
- } catch {
2512
- log("skipped (no existing index)");
2513
- }
2702
+ } catch {}
2514
2703
  }
2515
- write("Scanning files... ");
2704
+ const idxSpinner = Y2();
2705
+ idxSpinner.start("Scanning codebase...");
2516
2706
  const { memories, fileCount, skipped } = indexCodebase(absPath);
2517
- log(`${fileCount} files found, ${skipped} skipped`);
2707
+ idxSpinner.stop(`Codebase: ${fileCount} files found, ${skipped} skipped`);
2518
2708
  if (memories.length === 0) {
2519
2709
  log(`
2520
2710
  Nothing new to index.`);
@@ -2541,14 +2731,12 @@ Nothing new to index.`);
2541
2731
  if (!opts.quiet)
2542
2732
  printFailureSummary(results, memories);
2543
2733
  }
2544
- log();
2734
+ log("");
2545
2735
  if (!opts.dryRun && !effectiveIngestOnly) {
2546
- log("─── Configuring MCP ───");
2547
- log("");
2548
- for (const line of configureMcp(apiKey, selectedTargets)) {
2549
- log(` ${line}`);
2550
- }
2551
- log("");
2736
+ const mcpSpinner = Y2();
2737
+ mcpSpinner.start("Configuring MCP...");
2738
+ const mcpLines = configureMcp(apiKey, selectedTargets);
2739
+ mcpSpinner.stop(`MCP: ${mcpLines.join(", ")}`);
2552
2740
  }
2553
2741
  log("Done! Your codebase is now searchable via recall.");
2554
2742
  return;
@@ -2578,23 +2766,39 @@ Nothing new to index.`);
2578
2766
  const allMemories = [];
2579
2767
  const shouldIngest = (name) => selectedSources.includes(name);
2580
2768
  if (shouldIngest("claude") && tools.find((t) => t.name === "Claude Code")?.available) {
2581
- write("Ingesting Claude Code... ");
2582
- const { memories, filtered, deduped } = ingestClaude();
2583
- allMemories.push(...memories);
2584
- log(renderDiscoverySummary(memories.length, filtered, deduped));
2769
+ if (!opts.quiet) {
2770
+ const s = Y2();
2771
+ s.start("Scanning Claude Code chats...");
2772
+ const { memories, filtered, deduped } = ingestClaude();
2773
+ allMemories.push(...memories);
2774
+ s.stop(`Claude Code: ${renderDiscoverySummary(memories.length, filtered, deduped)}`);
2775
+ } else {
2776
+ allMemories.push(...ingestClaude().memories);
2777
+ }
2585
2778
  }
2586
2779
  if (shouldIngest("codex") && tools.find((t) => t.name === "Codex")?.available) {
2587
- write("Ingesting Codex... ");
2588
- const { memories, filtered, deduped } = ingestCodex();
2589
- allMemories.push(...memories);
2590
- log(renderDiscoverySummary(memories.length, filtered, deduped));
2780
+ if (!opts.quiet) {
2781
+ const s = Y2();
2782
+ s.start("Scanning Codex chats...");
2783
+ const { memories, filtered, deduped } = ingestCodex();
2784
+ allMemories.push(...memories);
2785
+ s.stop(`Codex: ${renderDiscoverySummary(memories.length, filtered, deduped)}`);
2786
+ } else {
2787
+ allMemories.push(...ingestCodex().memories);
2788
+ }
2591
2789
  }
2592
2790
  if (shouldIngest("cursor") && tools.find((t) => t.name === "Cursor")?.available) {
2593
- write("Ingesting Cursor... ");
2594
- const cursorTool = tools.find((t) => t.name === "Cursor");
2595
- const { memories, filtered, deduped } = await ingestCursor(cursorTool.path, opts.wasmDir);
2596
- allMemories.push(...memories);
2597
- log(renderDiscoverySummary(memories.length, filtered, deduped));
2791
+ if (!opts.quiet) {
2792
+ const s = Y2();
2793
+ s.start("Scanning Cursor chats...");
2794
+ const cursorTool = tools.find((t) => t.name === "Cursor");
2795
+ const { memories, filtered, deduped } = await ingestCursor(cursorTool.path, opts.wasmDir);
2796
+ allMemories.push(...memories);
2797
+ s.stop(`Cursor: ${renderDiscoverySummary(memories.length, filtered, deduped)}`);
2798
+ } else {
2799
+ const cursorTool = tools.find((t) => t.name === "Cursor");
2800
+ allMemories.push(...(await ingestCursor(cursorTool.path, opts.wasmDir)).memories);
2801
+ }
2598
2802
  }
2599
2803
  allMemories.sort((a, b3) => {
2600
2804
  const da = a.metadata?.date || "0000";
@@ -2670,21 +2874,18 @@ Nothing new to index.`);
2670
2874
  finishSetup();
2671
2875
  }
2672
2876
  if (!opts.dryRun && !effectiveIngestOnly) {
2673
- log("─── Configuring MCP ───");
2674
- log("");
2675
- for (const line of configureMcp(apiKey, selectedTargets)) {
2676
- log(` ${line}`);
2677
- }
2678
- log("");
2877
+ const mcpSpinner = Y2();
2878
+ mcpSpinner.start("Configuring MCP...");
2879
+ const mcpLines = configureMcp(apiKey, selectedTargets);
2880
+ mcpSpinner.stop(`MCP: ${mcpLines.join(", ")}`);
2679
2881
  }
2680
2882
  if (postIngestIndexPath) {
2681
2883
  const { resolve: resolve2 } = await import("node:path");
2682
2884
  const absPath = resolve2(postIngestIndexPath);
2683
- log("─── Indexing codebase ───");
2684
- log("");
2685
- write("Scanning files... ");
2885
+ const idxSpinner = Y2();
2886
+ idxSpinner.start("Indexing codebase...");
2686
2887
  const { memories, fileCount, skipped } = indexCodebase(absPath);
2687
- log(`${fileCount} files found, ${skipped} skipped`);
2888
+ idxSpinner.stop(`Codebase: ${fileCount} files found, ${skipped} skipped`);
2688
2889
  if (memories.length === 0) {
2689
2890
  log(`
2690
2891
  Nothing new to index.`);
@@ -2705,24 +2906,17 @@ Nothing new to index.`);
2705
2906
  log("");
2706
2907
  }
2707
2908
  if (!opts.dryRun && !opts.quiet) {
2708
- log("─── Background Sync ───");
2709
- log("");
2909
+ const syncSpinner = Y2();
2910
+ syncSpinner.start("Setting up background sync...");
2710
2911
  try {
2711
- const syncMessages = installSync(apiKey);
2712
- for (const msg of syncMessages)
2713
- log(` ✓ ${msg}`);
2714
- log("");
2715
- log(" New chats will be auto-ingested every 10 minutes.");
2716
- log(" Logs: ~/.conare/ingest.log | Remove: conare --uninstall-sync");
2912
+ installSync(apiKey, opts.syncInterval);
2913
+ syncSpinner.stop(`Background sync active (every ${opts.syncInterval}min)`);
2717
2914
  } catch (e2) {
2718
- log(`Could not set up background sync: ${e2.message}`);
2719
- log(" Run manually later: conare --install-sync");
2915
+ syncSpinner.stop(`Could not set up background sync: ${e2.message}`);
2720
2916
  }
2721
- log("");
2722
2917
  }
2723
- log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2724
- log(" Done! Every new conversation now starts with context.");
2725
- log("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
2918
+ log("");
2919
+ log(" \x1B[32m✓\x1B[0m Done! Every new conversation now starts with context.");
2726
2920
  log("");
2727
2921
  }
2728
2922
  main().catch((e2) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conare",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
4
4
  "description": "Conare CLI for ingesting AI chat history and configuring memory at conare.ai",
5
5
  "type": "module",
6
6
  "bin": {