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.
- package/dist/index.js +460 -91
- 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
|
|
152
|
-
import { join as
|
|
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 =
|
|
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
|
-
|
|
1148
|
-
|
|
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
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
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
|
-
|
|
2467
|
+
log(auth.email ? `OK (${auth.email})` : "OK");
|
|
2107
2468
|
saveApiKey(apiKey);
|
|
2108
|
-
|
|
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
|
-
|
|
2116
|
-
|
|
2476
|
+
log("Remote account is empty; cleared stale local ingestion history.");
|
|
2477
|
+
log("");
|
|
2117
2478
|
}
|
|
2118
2479
|
}
|
|
2119
|
-
if (!opts.wasmDir &&
|
|
2120
|
-
opts.wasmDir =
|
|
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
|
-
|
|
2125
|
-
|
|
2485
|
+
log("─── Configuring MCP ───");
|
|
2486
|
+
log("");
|
|
2126
2487
|
for (const line of configureMcp(apiKey, selectedTargets)) {
|
|
2127
|
-
|
|
2488
|
+
log(` ✓ ${line}`);
|
|
2128
2489
|
}
|
|
2129
|
-
|
|
2490
|
+
log("");
|
|
2130
2491
|
}
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
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
|
-
|
|
2501
|
+
log(`Indexing codebase: ${absPath}`);
|
|
2141
2502
|
if (opts.force) {
|
|
2142
2503
|
clearIngested("codebase");
|
|
2143
|
-
|
|
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
|
-
|
|
2510
|
+
log("done");
|
|
2150
2511
|
} catch {
|
|
2151
|
-
|
|
2512
|
+
log("skipped (no existing index)");
|
|
2152
2513
|
}
|
|
2153
2514
|
}
|
|
2154
|
-
|
|
2515
|
+
write("Scanning files... ");
|
|
2155
2516
|
const { memories, fileCount, skipped } = indexCodebase(absPath);
|
|
2156
|
-
|
|
2517
|
+
log(`${fileCount} files found, ${skipped} skipped`);
|
|
2157
2518
|
if (memories.length === 0) {
|
|
2158
|
-
|
|
2519
|
+
log(`
|
|
2159
2520
|
Nothing new to index.`);
|
|
2160
2521
|
} else if (opts.dryRun) {
|
|
2161
|
-
|
|
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
|
-
|
|
2526
|
+
log(` ${meta.language.padEnd(12)} ${meta.filePath}`);
|
|
2166
2527
|
}
|
|
2167
2528
|
if (memories.length > 10)
|
|
2168
|
-
|
|
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
|
-
|
|
2536
|
+
write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
|
|
2176
2537
|
});
|
|
2177
|
-
|
|
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
|
-
|
|
2541
|
+
if (!opts.quiet)
|
|
2542
|
+
printFailureSummary(results, memories);
|
|
2181
2543
|
}
|
|
2182
|
-
|
|
2544
|
+
log();
|
|
2183
2545
|
if (!opts.dryRun && !effectiveIngestOnly) {
|
|
2184
|
-
|
|
2185
|
-
|
|
2546
|
+
log("─── Configuring MCP ───");
|
|
2547
|
+
log("");
|
|
2186
2548
|
for (const line of configureMcp(apiKey, selectedTargets)) {
|
|
2187
|
-
|
|
2549
|
+
log(` ✓ ${line}`);
|
|
2188
2550
|
}
|
|
2189
|
-
|
|
2551
|
+
log("");
|
|
2190
2552
|
}
|
|
2191
|
-
|
|
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
|
-
|
|
2559
|
+
log(`Cleared ingestion history for ${opts.source}`);
|
|
2198
2560
|
} else {
|
|
2199
2561
|
clearIngested();
|
|
2200
|
-
|
|
2562
|
+
log("Cleared all ingestion history");
|
|
2201
2563
|
}
|
|
2202
|
-
|
|
2564
|
+
log();
|
|
2203
2565
|
}
|
|
2204
2566
|
const tools = detect();
|
|
2205
2567
|
if (!interactiveMode) {
|
|
2206
|
-
|
|
2568
|
+
log("Detected AI tools:");
|
|
2207
2569
|
for (const t of tools) {
|
|
2208
2570
|
if (t.available) {
|
|
2209
|
-
|
|
2571
|
+
log(` + ${t.name}${t.sessionCount || t.sessionCount === 0 ? ` — ${t.sessionCount} sessions` : ""}`);
|
|
2210
2572
|
} else {
|
|
2211
|
-
|
|
2573
|
+
log(` - ${t.name} (not found)`);
|
|
2212
2574
|
}
|
|
2213
2575
|
}
|
|
2214
|
-
|
|
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
|
-
|
|
2581
|
+
write("Ingesting Claude Code... ");
|
|
2220
2582
|
const { memories, filtered, deduped } = ingestClaude();
|
|
2221
2583
|
allMemories.push(...memories);
|
|
2222
|
-
|
|
2584
|
+
log(renderDiscoverySummary(memories.length, filtered, deduped));
|
|
2223
2585
|
}
|
|
2224
2586
|
if (shouldIngest("codex") && tools.find((t) => t.name === "Codex")?.available) {
|
|
2225
|
-
|
|
2587
|
+
write("Ingesting Codex... ");
|
|
2226
2588
|
const { memories, filtered, deduped } = ingestCodex();
|
|
2227
2589
|
allMemories.push(...memories);
|
|
2228
|
-
|
|
2590
|
+
log(renderDiscoverySummary(memories.length, filtered, deduped));
|
|
2229
2591
|
}
|
|
2230
2592
|
if (shouldIngest("cursor") && tools.find((t) => t.name === "Cursor")?.available) {
|
|
2231
|
-
|
|
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
|
-
|
|
2597
|
+
log(renderDiscoverySummary(memories.length, filtered, deduped));
|
|
2236
2598
|
}
|
|
2237
|
-
|
|
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
|
-
|
|
2606
|
+
log("Nothing new to upload.");
|
|
2240
2607
|
} else if (opts.dryRun) {
|
|
2241
|
-
|
|
2608
|
+
log(`[DRY RUN] Would upload ${allMemories.length} memories`);
|
|
2242
2609
|
for (const m2 of allMemories.slice(0, 5)) {
|
|
2243
|
-
|
|
2610
|
+
log(` ${m2.containerTag} | ${m2.content.length} chars | ${m2.metadata?.sessionId?.slice(0, 12) || "?"}`);
|
|
2244
2611
|
}
|
|
2245
2612
|
if (allMemories.length > 5)
|
|
2246
|
-
|
|
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
|
-
|
|
2620
|
+
write(`\r Uploading [\x1B[33m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
|
|
2254
2621
|
});
|
|
2255
|
-
|
|
2622
|
+
write(renderProgressSummary(success, failed, "uploaded"));
|
|
2256
2623
|
if (failed > 0) {
|
|
2257
|
-
|
|
2624
|
+
log(` Re-run with --force to retry failed memories.`);
|
|
2258
2625
|
}
|
|
2259
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2306
|
-
|
|
2673
|
+
log("─── Configuring MCP ───");
|
|
2674
|
+
log("");
|
|
2307
2675
|
for (const line of configureMcp(apiKey, selectedTargets)) {
|
|
2308
|
-
|
|
2676
|
+
log(` ✓ ${line}`);
|
|
2309
2677
|
}
|
|
2310
|
-
|
|
2678
|
+
log("");
|
|
2311
2679
|
}
|
|
2312
2680
|
if (postIngestIndexPath) {
|
|
2313
2681
|
const { resolve: resolve2 } = await import("node:path");
|
|
2314
2682
|
const absPath = resolve2(postIngestIndexPath);
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2683
|
+
log("─── Indexing codebase ───");
|
|
2684
|
+
log("");
|
|
2685
|
+
write("Scanning files... ");
|
|
2318
2686
|
const { memories, fileCount, skipped } = indexCodebase(absPath);
|
|
2319
|
-
|
|
2687
|
+
log(`${fileCount} files found, ${skipped} skipped`);
|
|
2320
2688
|
if (memories.length === 0) {
|
|
2321
|
-
|
|
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
|
-
|
|
2697
|
+
write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
|
|
2330
2698
|
});
|
|
2331
|
-
|
|
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
|
-
|
|
2702
|
+
if (!opts.quiet)
|
|
2703
|
+
printFailureSummary(results, memories);
|
|
2335
2704
|
}
|
|
2336
|
-
|
|
2705
|
+
log("");
|
|
2337
2706
|
}
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
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);
|