conare 0.5.20 → 0.5.21

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 +363 -37
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -48,10 +48,37 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
48
48
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
49
49
 
50
50
  // src/ingest/shared.ts
51
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
51
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
52
52
  import { createHash } from "node:crypto";
53
53
  import { join } from "node:path";
54
54
  import { homedir } from "node:os";
55
+ function conareDir() {
56
+ const override = process.env.CONARE_HOME;
57
+ return override ? join(override, ".conare") : join(homedir(), ".conare");
58
+ }
59
+ function globalManifestPath() {
60
+ return join(conareDir(), "ingested.json");
61
+ }
62
+ function scopedManifestPath(userId) {
63
+ const slug = createHash("sha256").update(userId).digest("hex").slice(0, 16);
64
+ return join(conareDir(), `ingested.${slug}.json`);
65
+ }
66
+ function manifestPath() {
67
+ return activeUserId ? scopedManifestPath(activeUserId) : globalManifestPath();
68
+ }
69
+ function setManifestAccount(userId) {
70
+ activeUserId = userId || null;
71
+ if (!userId)
72
+ return;
73
+ const scoped = scopedManifestPath(userId);
74
+ if (!existsSync(scoped) && existsSync(globalManifestPath())) {
75
+ try {
76
+ if (!existsSync(conareDir()))
77
+ mkdirSync(conareDir(), { recursive: true });
78
+ renameSync(globalManifestPath(), scoped);
79
+ } catch {}
80
+ }
81
+ }
55
82
  function hardCapContent(content) {
56
83
  if (content.length <= MAX_MEMORY_CONTENT)
57
84
  return content;
@@ -80,12 +107,22 @@ ${body}`;
80
107
  return full;
81
108
  return hardCapContent(buildContent(TRUNCATED_USER_MSG));
82
109
  }
110
+ function normalizeEpochMs(value) {
111
+ return value < 10000000000 ? value * 1000 : value;
112
+ }
83
113
  function parseTimestampMs(value) {
84
114
  if (typeof value === "number" && Number.isFinite(value))
85
- return value;
86
- if (typeof value !== "string" || value.trim().length === 0)
115
+ return normalizeEpochMs(value);
116
+ if (typeof value !== "string")
117
+ return null;
118
+ const trimmed = value.trim();
119
+ if (trimmed.length === 0)
87
120
  return null;
88
- const parsed = Date.parse(value);
121
+ if (/^\d+$/.test(trimmed)) {
122
+ const n = Number(trimmed);
123
+ return Number.isFinite(n) ? normalizeEpochMs(n) : null;
124
+ }
125
+ const parsed = Date.parse(trimmed);
89
126
  return Number.isFinite(parsed) ? parsed : null;
90
127
  }
91
128
  function isNarration(text) {
@@ -115,10 +152,16 @@ function cleanText(raw) {
115
152
  function createContentHash(content) {
116
153
  return createHash("sha256").update(content).digest("hex").slice(0, 16);
117
154
  }
155
+ function writeManifest(manifest) {
156
+ if (!existsSync(conareDir()))
157
+ mkdirSync(conareDir(), { recursive: true });
158
+ writeFileSync(manifestPath(), JSON.stringify(manifest, null, 2));
159
+ }
118
160
  function getIngested() {
119
161
  try {
120
- if (existsSync(MANIFEST_PATH)) {
121
- return JSON.parse(readFileSync(MANIFEST_PATH, "utf-8"));
162
+ const path = manifestPath();
163
+ if (existsSync(path)) {
164
+ return JSON.parse(readFileSync(path, "utf-8"));
122
165
  }
123
166
  } catch {}
124
167
  return {};
@@ -129,10 +172,22 @@ function markIngested(source, sessionIds) {
129
172
  for (const id of sessionIds)
130
173
  existing.add(id);
131
174
  manifest[source] = [...existing];
132
- const dir = join(homedir(), ".conare");
133
- if (!existsSync(dir))
134
- mkdirSync(dir, { recursive: true });
135
- writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
175
+ writeManifest(manifest);
176
+ }
177
+ function mergeIngested(bySource) {
178
+ const manifest = getIngested();
179
+ let added = 0;
180
+ for (const [source, fingerprints] of Object.entries(bySource)) {
181
+ const existing = new Set(manifest[source] || []);
182
+ const before = existing.size;
183
+ for (const fp of fingerprints)
184
+ existing.add(fp);
185
+ added += existing.size - before;
186
+ manifest[source] = [...existing];
187
+ }
188
+ if (added > 0)
189
+ writeManifest(manifest);
190
+ return added;
136
191
  }
137
192
  function isIngested(source, sessionId) {
138
193
  const manifest = getIngested();
@@ -146,16 +201,12 @@ function clearIngested(source) {
146
201
  for (const key of Object.keys(manifest))
147
202
  delete manifest[key];
148
203
  }
149
- const dir = join(homedir(), ".conare");
150
- if (!existsSync(dir))
151
- mkdirSync(dir, { recursive: true });
152
- writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
204
+ writeManifest(manifest);
153
205
  }
154
- var MANIFEST_PATH, MAX_MEMORY_CONTENT = 200000, TRUNCATED_USER_MSG = 3000, TRUNCATED_MARKER = `
206
+ var activeUserId = null, MAX_MEMORY_CONTENT = 200000, TRUNCATED_USER_MSG = 3000, TRUNCATED_MARKER = `
155
207
 
156
208
  ...[truncated to fit Conare upload limit]`, MIN_SUBSTANTIVE = 200, NARRATION_RE;
157
209
  var init_shared = __esm(() => {
158
- MANIFEST_PATH = join(homedir(), ".conare", "ingested.json");
159
210
  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))/;
160
211
  });
161
212
 
@@ -484,10 +535,12 @@ var exports_api = {};
484
535
  __export(exports_api, {
485
536
  validateKey: () => validateKey,
486
537
  uploadBulk: () => uploadBulk,
538
+ recordSyncCheckIn: () => recordSyncCheckIn,
487
539
  listRemoteMemories: () => listRemoteMemories,
488
540
  getRemoteMemoryCount: () => getRemoteMemoryCount,
489
541
  getRemoteChatMemoryCount: () => getRemoteChatMemoryCount,
490
542
  getBillingStatus: () => getBillingStatus,
543
+ fetchServerDedupFingerprints: () => fetchServerDedupFingerprints,
491
544
  deleteMemory: () => deleteMemory,
492
545
  deleteMemories: () => deleteMemories,
493
546
  createUploadBatches: () => createUploadBatches,
@@ -568,11 +621,23 @@ function createUploadBatches(memories, maxItems = 10, maxChars = 500000) {
568
621
  async function validateKey(apiKey) {
569
622
  try {
570
623
  const data = await apiRequest("/api/auth/me", apiKey);
571
- return { valid: true, email: data.user.email, name: data.user.name };
624
+ return { valid: true, id: data.user.id, email: data.user.email, name: data.user.name };
572
625
  } catch {
573
626
  return { valid: false };
574
627
  }
575
628
  }
629
+ async function fetchServerDedupFingerprints(apiKey) {
630
+ const bySource = { claude: [], codex: [], cursor: [] };
631
+ await Promise.all(Object.entries(CONTAINER_TO_SOURCE).map(async ([containerTag, source]) => {
632
+ const data = await apiRequest(`/api/memories/dedup-keys?containerTag=${encodeURIComponent(containerTag)}`, apiKey);
633
+ for (const k of data.keys || []) {
634
+ if (!k.dedupKey)
635
+ continue;
636
+ bySource[source].push(k.contentHash ? `${k.dedupKey}:${k.contentHash}` : k.dedupKey);
637
+ }
638
+ }));
639
+ return bySource;
640
+ }
576
641
  async function getRemoteMemoryCount(apiKey) {
577
642
  try {
578
643
  const data = await apiRequest("/api/containers", apiKey);
@@ -601,6 +666,17 @@ async function getBillingStatus(apiKey) {
601
666
  return null;
602
667
  }
603
668
  }
669
+ async function recordSyncCheckIn(apiKey, source = "cli", memoryDelta = 0) {
670
+ try {
671
+ const data = await apiRequest("/api/sync/check-in", apiKey, {
672
+ method: "POST",
673
+ body: JSON.stringify({ source, memoryDelta })
674
+ });
675
+ return typeof data.lastSyncAt === "number" ? data.lastSyncAt : null;
676
+ } catch {
677
+ return null;
678
+ }
679
+ }
604
680
  async function uploadItems(apiKey, items) {
605
681
  let retries = 4;
606
682
  while (retries > 0) {
@@ -618,7 +694,7 @@ async function uploadItems(apiKey, items) {
618
694
  const message = error.message || "Plan limit reached";
619
695
  return items.map(() => ({
620
696
  success: false,
621
- error: message.includes("conare.ai") ? message : `${message}. Upgrade at https://conare.ai/pricing`
697
+ error: message.includes("conare.ai") ? message : `${message}. Upgrade at ${UPGRADE_URL}`
622
698
  }));
623
699
  }
624
700
  if (error instanceof ApiError && error.statusCode === 429) {
@@ -679,7 +755,7 @@ async function uploadBulk(apiKey, memories, onProgress) {
679
755
  }
680
756
  return { success, failed, results };
681
757
  }
682
- var API_URL2 = "https://conare.ai", ApiError, PAGE_SIZE = 200, DELETE_CONCURRENCY = 5;
758
+ var API_URL2 = "https://conare.ai", UPGRADE_URL = "https://conare.ai/#pricing", ApiError, PAGE_SIZE = 200, DELETE_CONCURRENCY = 5, CONTAINER_TO_SOURCE;
683
759
  var init_api = __esm(() => {
684
760
  ApiError = class ApiError extends Error {
685
761
  statusCode;
@@ -691,6 +767,11 @@ var init_api = __esm(() => {
691
767
  this.name = "ApiError";
692
768
  }
693
769
  };
770
+ CONTAINER_TO_SOURCE = {
771
+ "claude-chats": "claude",
772
+ "codex-chats": "codex",
773
+ "cursor-chats": "cursor"
774
+ };
694
775
  });
695
776
 
696
777
  // node_modules/sisteransi/src/index.js
@@ -3728,13 +3809,72 @@ function copyNodeModule(name) {
3728
3809
  mkdirSync4(dirname3(targetDir), { recursive: true });
3729
3810
  cpSync(sourceDir, targetDir, { recursive: true });
3730
3811
  }
3731
- function cleanupOldLaunchAgent() {
3812
+ function bootoutLaunchAgent() {
3732
3813
  try {
3733
- execSync3(`launchctl bootout gui/${uid()} "${PLIST_PATH}" 2>/dev/null`, { stdio: "ignore" });
3814
+ execSync3(`launchctl bootout gui/${uid()}/${PLIST_LABEL} 2>/dev/null`, { stdio: "ignore" });
3815
+ } catch {}
3816
+ }
3817
+ function removeLegacyCron() {
3818
+ try {
3819
+ const existing = execSync3("crontab -l 2>/dev/null", { encoding: "utf-8" });
3820
+ const lines = existing.split(`
3821
+ `);
3822
+ const filtered = lines.filter((l) => !l.endsWith("# conare-sync") && !l.includes(".conare/bin/run.sh"));
3823
+ if (filtered.length === lines.length)
3824
+ return;
3825
+ const next = filtered.join(`
3826
+ `).trim();
3827
+ execSync3("crontab -", { input: next ? next + `
3828
+ ` : `
3829
+ `, stdio: ["pipe", "ignore", "ignore"] });
3734
3830
  } catch {}
3735
- if (existsSync9(PLIST_PATH)) {
3736
- unlinkSync2(PLIST_PATH);
3831
+ }
3832
+ function makeLaunchdPlist(intervalSeconds) {
3833
+ return `<?xml version="1.0" encoding="UTF-8"?>
3834
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3835
+ <plist version="1.0">
3836
+ <dict>
3837
+ <key>Label</key>
3838
+ <string>${PLIST_LABEL}</string>
3839
+ <key>ProgramArguments</key>
3840
+ <array>
3841
+ <string>/bin/bash</string>
3842
+ <string>${join9(BIN_DIR, "run.sh")}</string>
3843
+ </array>
3844
+ <key>StartInterval</key>
3845
+ <integer>${intervalSeconds}</integer>
3846
+ <key>RunAtLoad</key>
3847
+ <true/>
3848
+ <key>ProcessType</key>
3849
+ <string>Background</string>
3850
+ <key>StandardErrorPath</key>
3851
+ <string>${join9(CONARE_DIR, "ingest.log")}</string>
3852
+ </dict>
3853
+ </plist>
3854
+ `;
3855
+ }
3856
+ function setupLaunchd(intervalMinutes) {
3857
+ removeLegacyCron();
3858
+ const dir = dirname3(PLIST_PATH);
3859
+ mkdirSync4(dir, { recursive: true });
3860
+ writeFileSync4(PLIST_PATH, makeLaunchdPlist(intervalMinutes * 60));
3861
+ bootoutLaunchAgent();
3862
+ let registered = false;
3863
+ try {
3864
+ execSync3(`launchctl bootstrap gui/${uid()} "${PLIST_PATH}"`, { stdio: "ignore" });
3865
+ registered = true;
3866
+ } catch {
3867
+ try {
3868
+ execSync3(`launchctl load -w "${PLIST_PATH}"`, { stdio: "ignore" });
3869
+ registered = true;
3870
+ } catch {}
3737
3871
  }
3872
+ if (!registered) {
3873
+ throw new Error(`launchd registration failed — plist written to ${PLIST_PATH} but the agent could not be loaded. Try: launchctl bootstrap gui/${uid()} "${PLIST_PATH}"`);
3874
+ }
3875
+ try {
3876
+ execSync3(`launchctl kickstart gui/${uid()}/${PLIST_LABEL}`, { stdio: "ignore" });
3877
+ } catch {}
3738
3878
  }
3739
3879
  function setupLinuxSystemd(intervalMinutes) {
3740
3880
  mkdirSync4(SYSTEMD_DIR, { recursive: true });
@@ -3924,10 +4064,8 @@ function installSync(apiKey, intervalMinutes = 10) {
3924
4064
  const os = platform6();
3925
4065
  intervalMinutes = sanitizeInterval(intervalMinutes);
3926
4066
  if (os === "darwin") {
3927
- cleanupOldLaunchAgent();
3928
- const actual = clampCronInterval(intervalMinutes);
3929
- setupCron(intervalMinutes);
3930
- messages.push(`Installed cron job (every ${actual} min)`);
4067
+ setupLaunchd(intervalMinutes);
4068
+ messages.push(`Installed launchd agent (every ${intervalMinutes} min)`);
3931
4069
  } else if (os === "win32") {
3932
4070
  setupWindows(intervalMinutes);
3933
4071
  messages.push(`Installed Windows Task Scheduler (every ${intervalMinutes} min)`);
@@ -3946,12 +4084,126 @@ function installSync(apiKey, intervalMinutes = 10) {
3946
4084
  messages.push("First sync scheduled");
3947
4085
  return messages;
3948
4086
  }
4087
+ function cronHasConareEntry() {
4088
+ try {
4089
+ const cron = execSync3("crontab -l 2>/dev/null", { encoding: "utf-8" });
4090
+ return /# conare-sync|\.conare\/bin\/run\.sh/.test(cron);
4091
+ } catch {
4092
+ return false;
4093
+ }
4094
+ }
4095
+ function syncTimerInstalled() {
4096
+ const os = platform6();
4097
+ if (os === "darwin") {
4098
+ try {
4099
+ execSync3(`launchctl print gui/${uid()}/${PLIST_LABEL}`, { stdio: "ignore" });
4100
+ return true;
4101
+ } catch {}
4102
+ return cronHasConareEntry();
4103
+ }
4104
+ if (os === "win32") {
4105
+ try {
4106
+ execSync3(`schtasks /Query /TN "${TASK_NAME}"`, { stdio: "ignore" });
4107
+ return true;
4108
+ } catch {
4109
+ return false;
4110
+ }
4111
+ }
4112
+ if (os === "linux") {
4113
+ try {
4114
+ execSync3("systemctl --user is-enabled conare-sync.timer", { stdio: "ignore" });
4115
+ execSync3("systemctl --user is-active conare-sync.timer", { stdio: "ignore" });
4116
+ return true;
4117
+ } catch {}
4118
+ return cronHasConareEntry();
4119
+ }
4120
+ return false;
4121
+ }
4122
+ function ensureSyncInstalled(apiKey, intervalMinutes = 10) {
4123
+ if (syncTimerInstalled())
4124
+ return false;
4125
+ installSync(apiKey, intervalMinutes);
4126
+ return true;
4127
+ }
4128
+ function parseLastSync(logText) {
4129
+ const lines = logText.trim().split(`
4130
+ `).filter(Boolean);
4131
+ let lastSyncIso = null;
4132
+ for (let i = lines.length - 1;i >= 0; i--) {
4133
+ const m = lines[i].match(/^(\d{4}-\d{2}-\d{2}T[\d:]+Z)\s+(START|DONE)/);
4134
+ if (m) {
4135
+ lastSyncIso = m[1];
4136
+ break;
4137
+ }
4138
+ }
4139
+ let lastStartIdx = -1;
4140
+ for (let i = lines.length - 1;i >= 0; i--) {
4141
+ if (/\bSTART sync\b/.test(lines[i])) {
4142
+ lastStartIdx = i;
4143
+ break;
4144
+ }
4145
+ }
4146
+ let lastSyncError = null;
4147
+ if (lastStartIdx >= 0) {
4148
+ const errMatch = lines.slice(lastStartIdx).join(`
4149
+ `).match(/.*(Invalid API key|ERROR:).*/);
4150
+ if (errMatch)
4151
+ lastSyncError = errMatch[0].trim();
4152
+ }
4153
+ return { lastSyncIso, lastSyncError };
4154
+ }
4155
+ function detectMechanism() {
4156
+ const os = platform6();
4157
+ if (os === "win32")
4158
+ return "Task Scheduler";
4159
+ if (os === "darwin") {
4160
+ try {
4161
+ execSync3(`launchctl print gui/${uid()}/${PLIST_LABEL}`, { stdio: "ignore" });
4162
+ return "launchd";
4163
+ } catch {}
4164
+ if (cronHasConareEntry())
4165
+ return "cron (legacy)";
4166
+ return "launchd";
4167
+ }
4168
+ if (os === "linux") {
4169
+ try {
4170
+ execSync3("systemctl --user is-active conare-sync.timer", { stdio: "ignore" });
4171
+ return "systemd";
4172
+ } catch {}
4173
+ if (cronHasConareEntry())
4174
+ return "cron";
4175
+ return hasSystemd() ? "systemd" : "cron";
4176
+ }
4177
+ return "unsupported";
4178
+ }
4179
+ function syncStatus() {
4180
+ const mechanism = detectMechanism();
4181
+ const logPath = join9(CONARE_DIR, "ingest.log");
4182
+ let parsed = { lastSyncIso: null, lastSyncError: null };
4183
+ if (existsSync9(logPath)) {
4184
+ try {
4185
+ parsed = parseLastSync(readFileSync8(logPath, "utf-8"));
4186
+ } catch {}
4187
+ }
4188
+ return {
4189
+ timerInstalled: syncTimerInstalled(),
4190
+ mechanism,
4191
+ binaryPersisted: existsSync9(join9(BIN_DIR, "conare-ingest.mjs")),
4192
+ configPresent: existsSync9(CONFIG_PATH),
4193
+ lastSyncIso: parsed.lastSyncIso,
4194
+ lastSyncError: parsed.lastSyncError
4195
+ };
4196
+ }
3949
4197
  function removeSyncTimer() {
3950
4198
  const messages = [];
3951
4199
  const os = platform6();
3952
4200
  if (os === "darwin") {
3953
- removeCronEntry() && messages.push("Removed cron job");
3954
- cleanupOldLaunchAgent();
4201
+ bootoutLaunchAgent();
4202
+ if (existsSync9(PLIST_PATH)) {
4203
+ unlinkSync2(PLIST_PATH);
4204
+ messages.push("Removed launchd agent");
4205
+ }
4206
+ removeCronEntry() && messages.push("Removed legacy cron job");
3955
4207
  } else if (os === "win32") {
3956
4208
  try {
3957
4209
  execSync3(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: "ignore" });
@@ -4140,6 +4392,7 @@ Usage:
4140
4392
  conare Interactive setup with browser auth
4141
4393
  conare install Just install the MCP (all detected clients)
4142
4394
  conare logout Clear saved API key and local hashes; keep conare installed
4395
+ conare doctor Check sync/auth health and repair a missing timer
4143
4396
  conare --key <api_key> Index chat history (key optional with browser auth)
4144
4397
  conare --key <api_key> --index [path] Index codebase
4145
4398
 
@@ -4237,8 +4490,6 @@ async function runLogout() {
4237
4490
  clearSyncLocks();
4238
4491
  const hadApiKey = clearSavedApiKey();
4239
4492
  messages.push(hadApiKey ? "Cleared saved API key" : "No saved API key found");
4240
- clearIngested();
4241
- messages.push("Reset local hashes");
4242
4493
  console.log("");
4243
4494
  for (const msg of messages)
4244
4495
  console.log(` ${msg}`);
@@ -4246,6 +4497,47 @@ async function runLogout() {
4246
4497
  console.log(" \x1B[32m✓\x1B[0m Logged out. The conare command is still installed.");
4247
4498
  console.log("");
4248
4499
  }
4500
+ async function runDoctor() {
4501
+ const ok = "\x1B[32m✓\x1B[0m";
4502
+ const bad = "\x1B[31m✗\x1B[0m";
4503
+ const dim = (s) => `\x1B[2m${s}\x1B[0m`;
4504
+ const apiKey = getSavedApiKey() || process.env.CONARE_API_KEY;
4505
+ const status = syncStatus();
4506
+ console.log("");
4507
+ console.log(" Conare doctor");
4508
+ console.log("");
4509
+ const auth = apiKey ? await validateKey(apiKey) : { valid: false };
4510
+ if (apiKey) {
4511
+ console.log(` ${auth.valid ? ok : bad} API key: ${auth.valid ? `valid (${auth.email})` : "INVALID — run `conare` to sign in again"}`);
4512
+ } else {
4513
+ console.log(` ${bad} API key: not configured — run \`conare\` to sign in`);
4514
+ }
4515
+ console.log(` ${status.timerInstalled ? ok : bad} Background sync: ${status.timerInstalled ? `installed (${status.mechanism})` : "NOT installed"}`);
4516
+ console.log(` ${status.binaryPersisted ? ok : bad} CLI binary: ${status.binaryPersisted ? "persisted (~/.conare/bin/)" : "missing"}`);
4517
+ if (status.lastSyncIso) {
4518
+ console.log(` ${dim(`Last sync run: ${status.lastSyncIso}`)}`);
4519
+ }
4520
+ if (status.lastSyncError) {
4521
+ console.log(` ${bad} Last logged error: ${status.lastSyncError}`);
4522
+ }
4523
+ if (!status.timerInstalled) {
4524
+ console.log("");
4525
+ if (!auth.valid) {
4526
+ console.log(` ${bad} Not repairing background sync: ${apiKey ? "API key is invalid" : "no API key"}. Run \`conare\` to sign in, which reinstalls sync.`);
4527
+ } else {
4528
+ console.log(" Repairing: installing background sync...");
4529
+ try {
4530
+ ensureSyncInstalled(apiKey);
4531
+ console.log(` ${ok} Background sync installed.`);
4532
+ } catch (e2) {
4533
+ console.log(` ${bad} Could not install background sync: ${e2 instanceof Error ? e2.message : String(e2)}`);
4534
+ }
4535
+ }
4536
+ }
4537
+ console.log("");
4538
+ console.log(` ${dim(`Dashboard: ${CONARE_URL2}/dashboard`)}`);
4539
+ console.log("");
4540
+ }
4249
4541
  async function main() {
4250
4542
  if (process.argv[2] === "install") {
4251
4543
  return runInstall();
@@ -4253,6 +4545,9 @@ async function main() {
4253
4545
  if (process.argv[2] === "logout") {
4254
4546
  return runLogout();
4255
4547
  }
4548
+ if (process.argv[2] === "doctor") {
4549
+ return runDoctor();
4550
+ }
4256
4551
  const opts = parseArgs();
4257
4552
  let configFileKey;
4258
4553
  if (opts.configFile) {
@@ -4372,6 +4667,8 @@ async function main() {
4372
4667
  process.exit(1);
4373
4668
  }
4374
4669
  saveApiKey(apiKey);
4670
+ if (auth.id)
4671
+ setManifestAccount(auth.id);
4375
4672
  if (!opts.force && !opts.dryRun) {
4376
4673
  const remoteMemoryCount = await getRemoteMemoryCount(apiKey);
4377
4674
  const localManifest = getIngested();
@@ -4380,6 +4677,17 @@ async function main() {
4380
4677
  clearIngested();
4381
4678
  log("Remote account is empty; cleared stale local index history.");
4382
4679
  log("");
4680
+ } else if (remoteMemoryCount && remoteMemoryCount > 0) {
4681
+ const localChatCount = (localManifest.claude?.length || 0) + (localManifest.codex?.length || 0) + (localManifest.cursor?.length || 0);
4682
+ const remoteChatCount = localChatCount === 0 ? await getRemoteChatMemoryCount(apiKey) : 0;
4683
+ if (remoteChatCount && remoteChatCount > 0) {
4684
+ try {
4685
+ const serverFingerprints = await fetchServerDedupFingerprints(apiKey);
4686
+ const added = mergeIngested(serverFingerprints);
4687
+ if (added > 0)
4688
+ log(`Synced ${added.toLocaleString()} already-indexed chats from your account.`);
4689
+ } catch {}
4690
+ }
4383
4691
  }
4384
4692
  }
4385
4693
  if (!opts.wasmDir && existsSync10(join10(process.cwd(), "node_modules", "sql.js"))) {
@@ -4452,6 +4760,7 @@ async function main() {
4452
4760
  log(" Continuing with upload (stale files may remain)...");
4453
4761
  }
4454
4762
  }
4763
+ let indexedCount = 0;
4455
4764
  if (memories.length === 0) {
4456
4765
  log(`
4457
4766
  Nothing new to index.`);
@@ -4473,11 +4782,15 @@ Nothing new to index.`);
4473
4782
  write(`\r Uploading [\x1B[36m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
4474
4783
  });
4475
4784
  write(renderProgressSummary(success, failed, "indexed"));
4785
+ indexedCount = results.filter((result) => result.success && !result.deduped).length;
4476
4786
  const fileHashes = results.filter((result) => result.success).map((result) => getManifestFingerprint(memories[result.index])).filter((key) => !!key);
4477
4787
  markIngested("codebase", fileHashes);
4478
4788
  if (!opts.quiet)
4479
4789
  printFailureSummary(results, memories);
4480
4790
  }
4791
+ if (!opts.dryRun) {
4792
+ await recordSyncCheckIn(apiKey, "codebase", indexedCount);
4793
+ }
4481
4794
  log("");
4482
4795
  if (!opts.dryRun && !effectiveIngestOnly) {
4483
4796
  const mcpSpinner = Y2();
@@ -4575,11 +4888,17 @@ Nothing new to index.`);
4575
4888
  } else {
4576
4889
  log(`\x1B[33m⚠\x1B[0m Free plan: chat upload limit already reached (${knownRemoteChatCount}/${chatLimit}); ${skipped} new local sessions skipped`);
4577
4890
  }
4578
- log(` Upgrade to Pro for unlimited uploads → \x1B[4mhttps://conare.ai/pricing\x1B[0m`);
4891
+ log(` Upgrade to Pro for unlimited uploads → \x1B[4m${CONARE_URL2}/#pricing\x1B[0m`);
4579
4892
  log();
4580
4893
  }
4581
4894
  }
4582
4895
  log();
4896
+ if (apiKey && !opts.dryRun && !interactiveMode) {
4897
+ try {
4898
+ ensureSyncInstalled(apiKey, opts.syncInterval);
4899
+ } catch {}
4900
+ }
4901
+ let skipUpload = false;
4583
4902
  if (allMemories.length > 2000 && !opts.dryRun && !opts.quiet) {
4584
4903
  if (hasTty) {
4585
4904
  const { confirm: confirm2 } = await Promise.resolve().then(() => (init_dist2(), exports_dist));
@@ -4587,17 +4906,20 @@ Nothing new to index.`);
4587
4906
  message: `That's ${allMemories.length.toLocaleString()} memories — this may take a while and use significant resources. Continue?`
4588
4907
  });
4589
4908
  if (!proceed || typeof proceed === "symbol") {
4590
- log("Aborted.");
4591
- process.exit(0);
4909
+ log("Skipping upload for now. You can re-run `conare` anytime to upload.");
4910
+ log();
4911
+ skipUpload = true;
4592
4912
  }
4593
4913
  } else {
4594
4914
  log(`Warning: ${allMemories.length.toLocaleString()} memories detected. Skipping large upload in non-interactive mode.`);
4595
4915
  log("Re-run interactively or with --force to proceed.");
4596
- process.exit(0);
4916
+ skipUpload = true;
4597
4917
  }
4598
4918
  }
4599
- if (allMemories.length === 0) {
4600
- log("Nothing new to upload.");
4919
+ let uploadedCount = 0;
4920
+ if (allMemories.length === 0 || skipUpload) {
4921
+ if (allMemories.length === 0)
4922
+ log("Nothing new to upload.");
4601
4923
  } else if (opts.dryRun) {
4602
4924
  log(`[DRY RUN] Would upload ${allMemories.length} memories`);
4603
4925
  for (const m2 of allMemories.slice(0, 5)) {
@@ -4614,6 +4936,7 @@ Nothing new to index.`);
4614
4936
  write(`\r Uploading [\x1B[33m${bar}\x1B[0m] ${uploaded}/${total} (${pct}%)`);
4615
4937
  });
4616
4938
  write(renderProgressSummary(success, failed, "uploaded"));
4939
+ uploadedCount = results.filter((result) => result.success && !result.deduped).length;
4617
4940
  if (failed > 0) {
4618
4941
  log(` Re-run with --force to retry failed memories.`);
4619
4942
  }
@@ -4648,6 +4971,9 @@ Nothing new to index.`);
4648
4971
  markIngested(source, ids);
4649
4972
  }
4650
4973
  }
4974
+ if (!opts.dryRun) {
4975
+ await recordSyncCheckIn(apiKey, "cli", uploadedCount);
4976
+ }
4651
4977
  log();
4652
4978
  if (interactiveMode) {
4653
4979
  selectedTargets = await selectMcpTargets(interactiveTargets);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "conare",
3
- "version": "0.5.20",
3
+ "version": "0.5.21",
4
4
  "description": "Conare CLI for indexing AI chat history and configuring memory at conare.ai",
5
5
  "type": "module",
6
6
  "bin": {