chat-recall 0.5.16 → 0.5.17

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/mcp.js CHANGED
@@ -4862,6 +4862,78 @@ var init_project_resolver = __esm({
4862
4862
  }
4863
4863
  });
4864
4864
 
4865
+ // ../engine/dist/mcp/login-prompt.js
4866
+ var login_prompt_exports = {};
4867
+ __export(login_prompt_exports, {
4868
+ _resetLoginPrompts: () => _resetLoginPrompts,
4869
+ defaultLoginServer: () => defaultLoginServer,
4870
+ hasLoginStarter: () => hasLoginStarter,
4871
+ loginInstruction: () => loginInstruction,
4872
+ setLoginStarter: () => setLoginStarter
4873
+ });
4874
+ function setLoginStarter(fn) {
4875
+ starter = fn;
4876
+ }
4877
+ function hasLoginStarter() {
4878
+ return starter !== null;
4879
+ }
4880
+ function _resetLoginPrompts() {
4881
+ pending.clear();
4882
+ }
4883
+ function defaultLoginServer() {
4884
+ return (process.env.CHAT_RECALL_SERVER || "https://chatrecall.dev").replace(/\/+$/, "");
4885
+ }
4886
+ function loginInstruction(server = defaultLoginServer()) {
4887
+ if (!starter) {
4888
+ return `chat-recall is not logged in, and this host cannot start a login. Install the CLI and log in: \`npm i -g chat-recall && chat-recall login ${server}\`. A free 7-day trial starts as soon as you sign up.`;
4889
+ }
4890
+ const known = pending.get(server);
4891
+ if (known && Date.now() - known.at < PROMPT_TTL_MS) {
4892
+ if (known.prompt)
4893
+ return signInText(known.prompt, server);
4894
+ if (known.error) {
4895
+ return `chat-recall could not start a login automatically (${known.error}). Sign up at ${server} and then run \`chat-recall login ${server}\`.`;
4896
+ }
4897
+ return "chat-recall is starting a sign-in \u2014 call this tool again in a moment and it will show you the link.";
4898
+ }
4899
+ pending.set(server, { at: Date.now(), prompt: null });
4900
+ void starter(server).then((prompt) => {
4901
+ pending.set(server, { at: Date.now(), prompt });
4902
+ }).catch((err) => {
4903
+ pending.set(server, { at: Date.now(), prompt: null, error: err instanceof Error ? err.message : String(err) });
4904
+ });
4905
+ return "chat-recall is not connected yet \u2014 starting a sign-in now. Call this tool again in a moment and it will show you the link to open.";
4906
+ }
4907
+ function signInText(p, server) {
4908
+ return [
4909
+ "**chat-recall needs one sign-in before it can recall anything.**",
4910
+ "",
4911
+ `Open: ${p.url}`,
4912
+ `Code: ${p.userCode}`,
4913
+ "",
4914
+ `(If the link does not carry the code, enter it at ${p.verificationUri}.)`,
4915
+ "",
4916
+ "Sign in with Google or GitHub and a free 7-day trial starts immediately \u2014 no card.",
4917
+ "",
4918
+ "Approving also finishes the setup for you: the chat-recall skills go into your AI",
4919
+ "tools, the MCP is registered with them, and a background service starts shipping new",
4920
+ "conversations. Your existing sessions then sync \u2014 the first pass reports progress, so",
4921
+ 'a tool that says "still syncing" is working rather than broken.',
4922
+ "",
4923
+ "Nothing is read or uploaded until you approve. To keep a repo out afterwards:",
4924
+ "`recall_exclude_path`, or `chat-recall exclude project <path>`."
4925
+ ].join("\n");
4926
+ }
4927
+ var starter, pending, PROMPT_TTL_MS;
4928
+ var init_login_prompt = __esm({
4929
+ "../engine/dist/mcp/login-prompt.js"() {
4930
+ "use strict";
4931
+ starter = null;
4932
+ pending = /* @__PURE__ */ new Map();
4933
+ PROMPT_TTL_MS = 9 * 60 * 1e3;
4934
+ }
4935
+ });
4936
+
4865
4937
  // ../engine/dist/core/tool-backend.js
4866
4938
  var tool_backend_exports = {};
4867
4939
  __export(tool_backend_exports, {
@@ -4938,7 +5010,56 @@ var init_tool_backend = __esm({
4938
5010
  }
4939
5011
  });
4940
5012
 
5013
+ // ../engine/dist/core/project-path-match.js
5014
+ function canonicalProjectPath(p) {
5015
+ return (p || "").trim().replace(/\\/g, "/").replace(/[-_]/g, "/").replace(/\/{2,}/g, "/").replace(/\/+$/, "").toLowerCase();
5016
+ }
5017
+ function projectPathIncludes(candidate, rule) {
5018
+ if (!candidate || !rule)
5019
+ return false;
5020
+ if (candidate.includes(rule))
5021
+ return true;
5022
+ const c = canonicalProjectPath(candidate);
5023
+ const r = canonicalProjectPath(rule);
5024
+ return !!r && c.includes(r);
5025
+ }
5026
+ function projectPathAtOrUnder(candidate, rule) {
5027
+ if (!candidate || !rule)
5028
+ return false;
5029
+ const strip = (s) => s.replace(/\/+$/, "");
5030
+ const cRaw = strip(candidate);
5031
+ const rRaw = strip(rule);
5032
+ if (cRaw === rRaw || cRaw.startsWith(`${rRaw}/`))
5033
+ return true;
5034
+ const c = canonicalProjectPath(candidate);
5035
+ const r = canonicalProjectPath(rule);
5036
+ return !!r && (c === r || c.startsWith(`${r}/`));
5037
+ }
5038
+ var init_project_path_match = __esm({
5039
+ "../engine/dist/core/project-path-match.js"() {
5040
+ "use strict";
5041
+ }
5042
+ });
5043
+
4941
5044
  // ../engine/dist/core/settings.js
5045
+ var settings_exports = {};
5046
+ __export(settings_exports, {
5047
+ PERSONAL_DIR_NAMES: () => PERSONAL_DIR_NAMES,
5048
+ PERSONAL_DIR_PATTERNS: () => PERSONAL_DIR_PATTERNS,
5049
+ _resetSecretsCacheForTests: () => _resetSecretsCacheForTests,
5050
+ _resetSettingsCacheForTests: () => _resetSettingsCacheForTests,
5051
+ _resetSourceSettingsCache: () => _resetSourceSettingsCache,
5052
+ applySettingsToEnv: () => applySettingsToEnv,
5053
+ isPersonalPath: () => isPersonalPath,
5054
+ isProjectSyncable: () => isProjectSyncable,
5055
+ isSourceEnabled: () => isSourceEnabled,
5056
+ loadSettings: () => loadSettings,
5057
+ loadSourceSettings: () => loadSourceSettings,
5058
+ mergeSettings: () => mergeSettings,
5059
+ redactSettings: () => redactSettings,
5060
+ saveSettings: () => saveSettings,
5061
+ settingsFilePath: () => settingsFilePath
5062
+ });
4942
5063
  import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync, chmodSync, statSync as statSync3 } from "fs";
4943
5064
  import { delimiter } from "path";
4944
5065
  import { dirname as dirname3, join as join3 } from "path";
@@ -4973,7 +5094,7 @@ function isProjectSyncable(projectId, projectPath, cfg) {
4973
5094
  if (projectId && cfg.syncOnly.has(projectId))
4974
5095
  return true;
4975
5096
  for (const x of cfg.syncOnly)
4976
- if (x && projectPath.includes(x))
5097
+ if (projectPathIncludes(projectPath, x))
4977
5098
  return true;
4978
5099
  return false;
4979
5100
  }
@@ -5043,6 +5164,12 @@ function defaultSync() {
5043
5164
  includeProjects: []
5044
5165
  };
5045
5166
  }
5167
+ function isPersonalPath(projectPath, includeProjects = []) {
5168
+ if (includeProjects.some((inc) => inc && projectPath.includes(inc)))
5169
+ return false;
5170
+ const norm = (projectPath.replace(/\\/g, "/") + "/").toLowerCase();
5171
+ return PERSONAL_DIR_NAMES.some((n) => norm.includes(`/${n.toLowerCase()}/`));
5172
+ }
5046
5173
  function defaultTeam() {
5047
5174
  return {
5048
5175
  enabled: false,
@@ -5158,9 +5285,15 @@ function isSourceEnabled(tool, source) {
5158
5285
  return true;
5159
5286
  return block[source] !== false;
5160
5287
  }
5288
+ function _resetSourceSettingsCache() {
5289
+ _sourceSettingsCache = null;
5290
+ }
5161
5291
  function secretsPath() {
5162
5292
  return join3(getDataDir(), "secrets.env");
5163
5293
  }
5294
+ function _resetSecretsCacheForTests() {
5295
+ _secretsCache = null;
5296
+ }
5164
5297
  function loadSecrets() {
5165
5298
  const out = {};
5166
5299
  const p = secretsPath();
@@ -5218,6 +5351,9 @@ function applySecretOverlay(result) {
5218
5351
  }
5219
5352
  return result;
5220
5353
  }
5354
+ function _resetSettingsCacheForTests() {
5355
+ _settingsCache = null;
5356
+ }
5221
5357
  function loadSettings() {
5222
5358
  const path = settingsPath();
5223
5359
  if (!existsSync3(path))
@@ -5277,10 +5413,101 @@ function saveSettings(settings2) {
5277
5413
  } catch {
5278
5414
  }
5279
5415
  }
5416
+ function applySettingsToEnv(s = loadSettings()) {
5417
+ if (s.embedding.provider && s.embedding.provider !== "none") {
5418
+ process.env.EMBEDDING_PROVIDER = s.embedding.provider;
5419
+ }
5420
+ if (s.embedding.ollamaHost)
5421
+ process.env.OLLAMA_HOST = s.embedding.ollamaHost;
5422
+ if (s.embedding.ollamaCloudApiKey)
5423
+ process.env.OLLAMA_API_KEY = s.embedding.ollamaCloudApiKey;
5424
+ if (s.embedding.geminiApiKey)
5425
+ process.env.GEMINI_API_KEY = s.embedding.geminiApiKey;
5426
+ if (s.embedding.openaiApiKey)
5427
+ process.env.OPENAI_API_KEY = s.embedding.openaiApiKey;
5428
+ if (s.embedding.nvidiaApiKey)
5429
+ process.env.NVIDIA_API_KEY = s.embedding.nvidiaApiKey;
5430
+ if (s.embedding.openaiCompatBaseUrl)
5431
+ process.env.OPENAI_COMPAT_BASE_URL = s.embedding.openaiCompatBaseUrl;
5432
+ if (s.embedding.openaiCompatModel)
5433
+ process.env.OPENAI_COMPAT_MODEL = s.embedding.openaiCompatModel;
5434
+ if (s.embedding.openaiCompatApiKey)
5435
+ process.env.OPENAI_COMPAT_API_KEY = s.embedding.openaiCompatApiKey;
5436
+ if (s.embedding.openaiCompatDimension)
5437
+ process.env.OPENAI_COMPAT_DIMENSION = String(s.embedding.openaiCompatDimension);
5438
+ if (s.summary.provider)
5439
+ process.env.SUMMARY_PROVIDER = s.summary.provider;
5440
+ if (s.summary.cliCommand)
5441
+ process.env.SUMMARY_CLI_CMD = s.summary.cliCommand;
5442
+ if (s.summary.cliPreset)
5443
+ process.env.SUMMARY_CLI_PRESET = s.summary.cliPreset;
5444
+ if (s.summary.cliTimeoutMs)
5445
+ process.env.SUMMARY_CLI_TIMEOUT_MS = String(s.summary.cliTimeoutMs);
5446
+ if (s.summary.anthropicApiKey)
5447
+ process.env.ANTHROPIC_API_KEY = s.summary.anthropicApiKey;
5448
+ if (s.summary.apiBaseUrl)
5449
+ process.env.SUMMARY_API_BASE_URL = s.summary.apiBaseUrl;
5450
+ if (s.summary.apiModel)
5451
+ process.env.SUMMARY_API_MODEL = s.summary.apiModel;
5452
+ if (s.summary.apiKey)
5453
+ process.env.SUMMARY_API_KEY = s.summary.apiKey;
5454
+ }
5455
+ function redactSettings(s) {
5456
+ const mask2 = (v) => v ? "\u2022\u2022\u2022\u2022" + v.slice(-4) : void 0;
5457
+ return {
5458
+ v: s.v,
5459
+ embedding: {
5460
+ ...s.embedding,
5461
+ geminiApiKey: mask2(s.embedding.geminiApiKey),
5462
+ openaiApiKey: mask2(s.embedding.openaiApiKey),
5463
+ nvidiaApiKey: mask2(s.embedding.nvidiaApiKey),
5464
+ ollamaCloudApiKey: mask2(s.embedding.ollamaCloudApiKey),
5465
+ openaiCompatApiKey: mask2(s.embedding.openaiCompatApiKey)
5466
+ },
5467
+ summary: {
5468
+ ...s.summary,
5469
+ anthropicApiKey: mask2(s.summary.anthropicApiKey),
5470
+ apiKey: mask2(s.summary.apiKey)
5471
+ },
5472
+ // sources/privacy/sync hold no secrets — the sync token is referenced
5473
+ // by env-var name only, never persisted as a value.
5474
+ sources: s.sources,
5475
+ privacy: s.privacy,
5476
+ sync: s.sync,
5477
+ team: s.team
5478
+ };
5479
+ }
5480
+ function mergeSettings(current, update) {
5481
+ const isMask = (v) => typeof v === "string" && v.startsWith("\u2022\u2022\u2022\u2022");
5482
+ const keepIfMasked = (currentVal, newVal) => {
5483
+ if (isMask(newVal))
5484
+ return currentVal;
5485
+ return newVal === void 0 ? currentVal : newVal;
5486
+ };
5487
+ const e = { ...current.embedding, ...update.embedding ?? {} };
5488
+ if (update.embedding) {
5489
+ e.geminiApiKey = keepIfMasked(current.embedding.geminiApiKey, update.embedding.geminiApiKey);
5490
+ e.openaiApiKey = keepIfMasked(current.embedding.openaiApiKey, update.embedding.openaiApiKey);
5491
+ e.nvidiaApiKey = keepIfMasked(current.embedding.nvidiaApiKey, update.embedding.nvidiaApiKey);
5492
+ e.ollamaCloudApiKey = keepIfMasked(current.embedding.ollamaCloudApiKey, update.embedding.ollamaCloudApiKey);
5493
+ e.openaiCompatApiKey = keepIfMasked(current.embedding.openaiCompatApiKey, update.embedding.openaiCompatApiKey);
5494
+ }
5495
+ const sm = { ...current.summary, ...update.summary ?? {} };
5496
+ if (update.summary) {
5497
+ sm.anthropicApiKey = keepIfMasked(current.summary.anthropicApiKey, update.summary.anthropicApiKey);
5498
+ sm.apiKey = keepIfMasked(current.summary.apiKey, update.summary.apiKey);
5499
+ }
5500
+ const sources2 = mergeSources(current.sources, update.sources);
5501
+ const privacy = mergePrivacy(current.privacy, update.privacy);
5502
+ const sync = mergeSync(current.sync, update.sync);
5503
+ const team = mergeTeam(current.team, update.team);
5504
+ return { v: SCHEMA_VERSION, embedding: e, summary: sm, sources: sources2, privacy, sync, team };
5505
+ }
5280
5506
  var CLI_DETECTION_ORDER, SCHEMA_VERSION, PERSONAL_DIR_NAMES, PERSONAL_DIR_PATTERNS, _sourceSettingsCache, SECRET_FIELDS, _secretsCache, _settingsCache;
5281
5507
  var init_settings = __esm({
5282
5508
  "../engine/dist/core/settings.js"() {
5283
5509
  "use strict";
5510
+ init_project_path_match();
5284
5511
  init_paths();
5285
5512
  CLI_DETECTION_ORDER = [
5286
5513
  { preset: "gemini", bin: "gemini", cmd: 'gemini -p " "' },
@@ -7209,13 +7436,13 @@ async function parseCodexSubagents(sessionPath) {
7209
7436
  if (meta?.type !== "session_meta")
7210
7437
  continue;
7211
7438
  const payload = meta.payload || {};
7212
- const spawn3 = payload.source?.subagent?.thread_spawn;
7213
- if (!spawn3)
7439
+ const spawn4 = payload.source?.subagent?.thread_spawn;
7440
+ if (!spawn4)
7214
7441
  continue;
7215
- if (spawn3.parent_thread_id !== parentId)
7442
+ if (spawn4.parent_thread_id !== parentId)
7216
7443
  continue;
7217
7444
  const id = fileName.replace(/\.jsonl$/i, "");
7218
- const role = String(spawn3.agent_role || payload.agent_role || "").toLowerCase();
7445
+ const role = String(spawn4.agent_role || payload.agent_role || "").toLowerCase();
7219
7446
  const kind = role.includes("compact") ? "compact" : role.includes("aside") ? "aside" : role.includes("worker") || role.includes("explore") ? "explore" : "other";
7220
7447
  let messages = [];
7221
7448
  try {
@@ -7227,8 +7454,8 @@ async function parseCodexSubagents(sessionPath) {
7227
7454
  subagents.push({
7228
7455
  id,
7229
7456
  kind,
7230
- agentType: spawn3.agent_role || payload.agent_role,
7231
- description: spawn3.agent_nickname || payload.agent_nickname,
7457
+ agentType: spawn4.agent_role || payload.agent_role,
7458
+ description: spawn4.agent_nickname || payload.agent_nickname,
7232
7459
  filePath,
7233
7460
  messageCount: messages.length,
7234
7461
  toolUseCount,
@@ -11160,8 +11387,8 @@ var init_codex2 = __esm({
11160
11387
  } catch {
11161
11388
  continue;
11162
11389
  }
11163
- const spawn3 = meta?.payload?.source?.subagent?.thread_spawn;
11164
- if (!spawn3 || spawn3.parent_thread_id !== parentUuid)
11390
+ const spawn4 = meta?.payload?.source?.subagent?.thread_spawn;
11391
+ if (!spawn4 || spawn4.parent_thread_id !== parentUuid)
11165
11392
  continue;
11166
11393
  events.push(...this.parseRollout(path));
11167
11394
  }
@@ -11354,10 +11581,10 @@ var init_codex2 = __esm({
11354
11581
  const meta = JSON.parse(first);
11355
11582
  const payload = meta?.payload || {};
11356
11583
  projectPath = payload.cwd || "";
11357
- const spawn3 = payload.source?.subagent?.thread_spawn;
11358
- if (spawn3 || payload.agent_role || payload.agent_nickname) {
11584
+ const spawn4 = payload.source?.subagent?.thread_spawn;
11585
+ if (spawn4 || payload.agent_role || payload.agent_nickname) {
11359
11586
  isSubagent = true;
11360
- parentId = spawn3?.parent_thread_id || null;
11587
+ parentId = spawn4?.parent_thread_id || null;
11361
11588
  }
11362
11589
  }
11363
11590
  } catch {
@@ -13339,12 +13566,55 @@ var init_collector_health = __esm({
13339
13566
  }
13340
13567
  });
13341
13568
 
13569
+ // ../engine/dist/types/code-intel.js
13570
+ import { createHash as createHash6 } from "crypto";
13571
+ function shortHash(s) {
13572
+ return createHash6("sha256").update(s).digest("hex").slice(0, 16);
13573
+ }
13574
+ function snippetKey(snippet) {
13575
+ return (snippet ?? "").replace(/\s+/g, " ").trim().slice(0, 200);
13576
+ }
13577
+ function codeFindingIds(projectId, findings) {
13578
+ const groups = /* @__PURE__ */ new Map();
13579
+ findings.forEach((f, i) => {
13580
+ const key2 = [f.category, f.file, f.rule, identityTitle(f.title ?? ""), snippetKey(f.snippet)].join("|");
13581
+ const g = groups.get(key2);
13582
+ if (g)
13583
+ g.push(i);
13584
+ else
13585
+ groups.set(key2, [i]);
13586
+ });
13587
+ const lineOf = (i) => findings[i].line ?? -1;
13588
+ const ids = new Array(findings.length);
13589
+ for (const [key2, idxs] of groups) {
13590
+ idxs.sort((a, b) => lineOf(a) - lineOf(b));
13591
+ let ordinal = -1;
13592
+ let prevLine = Number.NaN;
13593
+ for (const idx of idxs) {
13594
+ if (lineOf(idx) !== prevLine) {
13595
+ ordinal++;
13596
+ prevLine = lineOf(idx);
13597
+ }
13598
+ ids[idx] = "cf_" + shortHash([projectId, key2, ordinal].join("|"));
13599
+ }
13600
+ }
13601
+ return ids;
13602
+ }
13603
+ function identityTitle(title) {
13604
+ return title.replace(/\d+/g, "#");
13605
+ }
13606
+ var init_code_intel = __esm({
13607
+ "../engine/dist/types/code-intel.js"() {
13608
+ "use strict";
13609
+ }
13610
+ });
13611
+
13342
13612
  // ../engine/dist/core/companions.js
13343
13613
  import { existsSync as existsSync20, mkdirSync as mkdirSync6, statSync as statSync17, chmodSync as chmodSync3, unlinkSync, renameSync as renameSync3, createWriteStream, readFileSync as readFileSync14, writeFileSync as writeFileSync4 } from "node:fs";
13344
13614
  import { homedir as homedir5 } from "node:os";
13345
13615
  import { join as join22 } from "node:path";
13346
13616
  import { execSync as execSync2 } from "node:child_process";
13347
- import { createHash as createHash6 } from "node:crypto";
13617
+ import { createHash as createHash7 } from "node:crypto";
13348
13618
  function expectedCodeindexSha256(artifact) {
13349
13619
  return CODEINDEX_SHA256[artifact.replace(/^codeindex-/, "").replace(/\.exe$/, "")] ?? null;
13350
13620
  }
@@ -13418,7 +13688,7 @@ async function downloadArtifact(artifact, dest) {
13418
13688
  if (!res.body)
13419
13689
  throw new Error("Download returned empty body");
13420
13690
  const file = createWriteStream(dest);
13421
- await new Promise((resolve2, reject) => {
13691
+ await new Promise((resolve3, reject) => {
13422
13692
  const reader = res.body.getReader();
13423
13693
  const pump = async () => {
13424
13694
  try {
@@ -13429,7 +13699,7 @@ async function downloadArtifact(artifact, dest) {
13429
13699
  file.write(value);
13430
13700
  }
13431
13701
  file.end();
13432
- file.on("finish", () => resolve2());
13702
+ file.on("finish", () => resolve3());
13433
13703
  file.on("error", reject);
13434
13704
  } catch (e) {
13435
13705
  reject(e);
@@ -13452,7 +13722,7 @@ async function installCodeindex(opts = {}) {
13452
13722
  if (!expected) {
13453
13723
  throw new Error(`No pinned checksum for ${status.artifactName} \u2014 refusing to install an unverified binary. Add its sha256 to CODEINDEX_SHA256 in companions.ts.`);
13454
13724
  }
13455
- const actual = createHash6("sha256").update(readFileSync14(tmpPath)).digest("hex");
13725
+ const actual = createHash7("sha256").update(readFileSync14(tmpPath)).digest("hex");
13456
13726
  if (actual !== expected) {
13457
13727
  throw new Error(`Checksum mismatch for ${status.artifactName}.
13458
13728
  expected ${expected}
@@ -13493,7 +13763,7 @@ var init_companions = __esm({
13493
13763
  });
13494
13764
 
13495
13765
  // ../engine/dist/core/detector-install.js
13496
- import { createHash as createHash7 } from "crypto";
13766
+ import { createHash as createHash8 } from "crypto";
13497
13767
  import { existsSync as existsSync21, mkdirSync as mkdirSync7, mkdtempSync, chmodSync as chmodSync4, renameSync as renameSync4, rmSync as rmSync2, writeFileSync as writeFileSync5, readFileSync as readFileSync15, statSync as statSync18, copyFileSync as copyFileSync2 } from "fs";
13498
13768
  import { join as join23 } from "path";
13499
13769
  function managedDetectorPath(name) {
@@ -13503,7 +13773,7 @@ function receiptPath(name) {
13503
13773
  return join23(getDetectorBinDir(), `${name}.receipt.json`);
13504
13774
  }
13505
13775
  function sha256File(path) {
13506
- return createHash7("sha256").update(readFileSync15(path)).digest("hex");
13776
+ return createHash8("sha256").update(readFileSync15(path)).digest("hex");
13507
13777
  }
13508
13778
  function readReceipt(name) {
13509
13779
  try {
@@ -13957,10 +14227,10 @@ async function resolveCodeindexBin(autoInstall = true) {
13957
14227
  return st.path;
13958
14228
  }
13959
14229
  function runMcp(binPath, workspace, calls, timeoutMs = 18e4) {
13960
- return new Promise((resolve2, reject) => {
14230
+ return new Promise((resolve3, reject) => {
13961
14231
  const p = spawn(binPath, ["--mcp", "--workspace", workspace], { stdio: ["pipe", "pipe", "ignore"] });
13962
14232
  const res = /* @__PURE__ */ new Map();
13963
- let pending = "";
14233
+ let pending2 = "";
13964
14234
  let failed = false;
13965
14235
  const timer = setTimeout(() => {
13966
14236
  try {
@@ -13987,16 +14257,16 @@ function runMcp(binPath, workspace, calls, timeoutMs = 18e4) {
13987
14257
  p.stdout.on("data", (d) => {
13988
14258
  if (failed)
13989
14259
  return;
13990
- pending += d;
13991
- let nl = pending.indexOf("\n");
14260
+ pending2 += d;
14261
+ let nl = pending2.indexOf("\n");
13992
14262
  while (nl !== -1) {
13993
- takeLine(pending.slice(0, nl));
13994
- pending = pending.slice(nl + 1);
13995
- nl = pending.indexOf("\n");
14263
+ takeLine(pending2.slice(0, nl));
14264
+ pending2 = pending2.slice(nl + 1);
14265
+ nl = pending2.indexOf("\n");
13996
14266
  }
13997
- if (pending.length > MCP_OUT_MAX_BYTES) {
14267
+ if (pending2.length > MCP_OUT_MAX_BYTES) {
13998
14268
  failed = true;
13999
- pending = "";
14269
+ pending2 = "";
14000
14270
  clearTimeout(timer);
14001
14271
  try {
14002
14272
  p.kill("SIGKILL");
@@ -14013,9 +14283,9 @@ function runMcp(binPath, workspace, calls, timeoutMs = 18e4) {
14013
14283
  clearTimeout(timer);
14014
14284
  if (failed)
14015
14285
  return;
14016
- if (pending)
14017
- takeLine(pending);
14018
- resolve2(res);
14286
+ if (pending2)
14287
+ takeLine(pending2);
14288
+ resolve3(res);
14019
14289
  });
14020
14290
  p.stdin.write(calls.map((c) => JSON.stringify(c) + "\n").join(""));
14021
14291
  p.stdin.end();
@@ -14181,6 +14451,11 @@ async function collectCode(opts) {
14181
14451
  }
14182
14452
  }
14183
14453
  const findings = [];
14454
+ const coveredBySecurityRule = /* @__PURE__ */ new Map();
14455
+ const coveredByCloneGroup = /* @__PURE__ */ new Map();
14456
+ const coveredByDupCluster = /* @__PURE__ */ new Map();
14457
+ const coveredByGodModule = /* @__PURE__ */ new Map();
14458
+ const coveredByCycle = /* @__PURE__ */ new Map();
14184
14459
  const sevFromLiteral = (cat) => cat === "secret_suspect" ? "medium" : cat === "todo_marker" ? "info" : "low";
14185
14460
  const securityRows = [];
14186
14461
  const heuristicSec = [];
@@ -14230,7 +14505,7 @@ async function collectCode(opts) {
14230
14505
  log6(`security: repo secret scan unavailable (${e instanceof Error ? e.message : e}) \u2014 install gitleaks/trufflehog for secret coverage`);
14231
14506
  }
14232
14507
  for (const f of [...securityRows, ...heuristicSec]) {
14233
- findings.push({
14508
+ const finding = {
14234
14509
  category: "security",
14235
14510
  severity: f.severity,
14236
14511
  file: f.file,
@@ -14240,7 +14515,13 @@ async function collectCode(opts) {
14240
14515
  snippet: f.snippet,
14241
14516
  why: FINDING_WHY.security,
14242
14517
  agentPrompt: securityPrompt(f.rule, f.severity, f.file, f.line)
14243
- });
14518
+ };
14519
+ findings.push(finding);
14520
+ const byRuleList = coveredBySecurityRule.get(f.rule);
14521
+ if (byRuleList)
14522
+ byRuleList.push(finding);
14523
+ else
14524
+ coveredBySecurityRule.set(f.rule, [finding]);
14244
14525
  }
14245
14526
  for (const f of (lit?.findings ?? []).slice(0, 150)) {
14246
14527
  const file = rel(f.file);
@@ -14262,7 +14543,7 @@ async function collectCode(opts) {
14262
14543
  const names = [...new Set(fns.map((x) => x.name))];
14263
14544
  const fileList = fns.map((x) => rel(x.file)).slice(0, 6);
14264
14545
  const heavy = (g.lines ?? 0) * (g.count ?? 0) >= 150;
14265
- findings.push({
14546
+ const finding = {
14266
14547
  category: "clone",
14267
14548
  severity: heavy ? "high" : "medium",
14268
14549
  file: fileList[0] ?? "",
@@ -14272,10 +14553,12 @@ async function collectCode(opts) {
14272
14553
  snippet: `${g.lines} lines, ${g.count} copies`,
14273
14554
  why: FINDING_WHY.clone,
14274
14555
  agentPrompt: clonePrompt(names, g.lines ?? 0, g.count ?? 0, fileList)
14275
- });
14556
+ };
14557
+ findings.push(finding);
14558
+ coveredByCloneGroup.set(g, finding);
14276
14559
  }
14277
14560
  for (const c of (dup?.clusters ?? []).slice(0, 40)) {
14278
- findings.push({
14561
+ const finding = {
14279
14562
  category: "duplication",
14280
14563
  severity: "low",
14281
14564
  file: c.name ?? "",
@@ -14285,7 +14568,9 @@ async function collectCode(opts) {
14285
14568
  snippet: `${c.kind}, ${c.count} files`,
14286
14569
  why: FINDING_WHY.duplication,
14287
14570
  agentPrompt: duplicationPrompt(c.name, c.kind ?? "", c.count ?? 0)
14288
- });
14571
+ };
14572
+ findings.push(finding);
14573
+ coveredByDupCluster.set(c, finding);
14289
14574
  }
14290
14575
  for (const s of (dead?.symbols ?? []).slice(0, 120)) {
14291
14576
  const file = rel(s.file);
@@ -14303,7 +14588,7 @@ async function collectCode(opts) {
14303
14588
  }
14304
14589
  for (const m of (coup?.god_modules ?? []).slice(0, 15)) {
14305
14590
  const file = rel(m.file);
14306
- findings.push({
14591
+ const finding = {
14307
14592
  category: "coupling",
14308
14593
  severity: "medium",
14309
14594
  file,
@@ -14313,11 +14598,13 @@ async function collectCode(opts) {
14313
14598
  snippet: `fan-in ${m.fan_in}, fan-out ${m.fan_out}`,
14314
14599
  why: FINDING_WHY.coupling,
14315
14600
  agentPrompt: couplingPrompt(file, m.fan_in ?? 0, m.fan_out ?? 0, Math.round((m.instability ?? 0) * 100) / 100)
14316
- });
14601
+ };
14602
+ findings.push(finding);
14603
+ coveredByGodModule.set(file, finding);
14317
14604
  }
14318
14605
  for (const c of (cyc?.cycles ?? []).slice(0, 25)) {
14319
14606
  const chain = (c.files ?? []).map(rel);
14320
- findings.push({
14607
+ const finding = {
14321
14608
  category: "cycle",
14322
14609
  severity: "medium",
14323
14610
  file: chain[0] ?? "",
@@ -14327,7 +14614,9 @@ async function collectCode(opts) {
14327
14614
  snippet: chain.slice(0, 4).join(" \u2192 "),
14328
14615
  why: FINDING_WHY.cycle,
14329
14616
  agentPrompt: cyclePrompt(chain.slice(0, 6))
14330
- });
14617
+ };
14618
+ findings.push(finding);
14619
+ coveredByCycle.set(c, finding);
14331
14620
  }
14332
14621
  for (const f of (unwrap?.findings ?? []).slice(0, 150)) {
14333
14622
  const file = rel(f.file);
@@ -14556,7 +14845,13 @@ async function collectCode(opts) {
14556
14845
  }
14557
14846
  log6("synthesising action plan \u2026");
14558
14847
  const actions = [];
14559
- const pushAction = (pri, category, title, fix, loc, agentPrompt) => actions.push({ pri, category, title, fix, loc, agentPrompt });
14848
+ const findingIdOf = /* @__PURE__ */ new Map();
14849
+ {
14850
+ const ids = codeFindingIds(projectId, findings);
14851
+ findings.forEach((f, i) => findingIdOf.set(f, ids[i]));
14852
+ }
14853
+ const coversOf = (fs) => [...new Set(fs.map((f) => f ? findingIdOf.get(f) : void 0).filter((id) => !!id))];
14854
+ const pushAction = (pri, category, title, fix, loc, agentPrompt, covers = []) => actions.push({ pri, category, title, fix, loc, agentPrompt, covers });
14560
14855
  const byRule = /* @__PURE__ */ new Map();
14561
14856
  for (const f of securityRows) {
14562
14857
  if (isTestOrExampleFile(f.file))
@@ -14568,7 +14863,17 @@ async function collectCode(opts) {
14568
14863
  for (const [rule, hits] of byRule) {
14569
14864
  const sev = hits.some((h) => h.sev === "critical") ? "critical" : hits.some((h) => h.sev === "high") ? "high" : "medium";
14570
14865
  const pri = sev === "critical" ? 0 : sev === "high" ? 1 : 2;
14571
- pushAction(pri, "security", `${rule} \u2014 ${hits.length} occurrence(s)`, secFix(rule), hits.slice(0, 4).map((h) => ({ file: h.file, line: h.line })), securityPrompt(rule, sev, hits[0].file, hits[0].line));
14866
+ pushAction(
14867
+ pri,
14868
+ "security",
14869
+ `${rule} \u2014 ${hits.length} occurrence(s)`,
14870
+ secFix(rule),
14871
+ hits.slice(0, 4).map((h) => ({ file: h.file, line: h.line })),
14872
+ securityPrompt(rule, sev, hits[0].file, hits[0].line),
14873
+ // Every security finding on this rule. The card IS the roll-up, so the
14874
+ // members must not each file one of their own.
14875
+ coversOf(coveredBySecurityRule.get(rule) ?? [])
14876
+ );
14572
14877
  }
14573
14878
  for (const g of clones?.groups ?? []) {
14574
14879
  if ((g.count ?? 0) >= 3 || (g.lines ?? 0) >= 12) {
@@ -14577,7 +14882,7 @@ async function collectCode(opts) {
14577
14882
  continue;
14578
14883
  const names = [...new Set(fns.map((x) => x.name))];
14579
14884
  const locs = fns.map((x) => rel(x.file)).slice(0, 5);
14580
- pushAction((g.lines ?? 0) * (g.count ?? 0) >= 150 ? 1 : 2, "duplication", `${names.join(", ")} copy-pasted ${g.count}\xD7 (${g.lines} lines each)`, "Extract one shared implementation and replace the copies.", locs.map((f) => ({ file: f })), clonePrompt(names, g.lines ?? 0, g.count ?? 0, locs));
14885
+ pushAction((g.lines ?? 0) * (g.count ?? 0) >= 150 ? 1 : 2, "duplication", `${names.join(", ")} copy-pasted ${g.count}\xD7 (${g.lines} lines each)`, "Extract one shared implementation and replace the copies.", locs.map((f) => ({ file: f })), clonePrompt(names, g.lines ?? 0, g.count ?? 0, locs), coversOf([coveredByCloneGroup.get(g)]));
14581
14886
  }
14582
14887
  }
14583
14888
  for (const c of (dup?.clusters ?? []).slice(0, 12)) {
@@ -14589,19 +14894,19 @@ async function collectCode(opts) {
14589
14894
  continue;
14590
14895
  if (kind === "type_alias" || kind === "interface" || kind === "enum" || kind === "type")
14591
14896
  continue;
14592
- pushAction(2, "reuse", `${nm} (${c.kind}) reimplemented in ${c.count} files`, "Consolidate into a single shared utility/module.", [], duplicationPrompt(nm, c.kind ?? "", c.count));
14897
+ pushAction(2, "reuse", `${nm} (${c.kind}) reimplemented in ${c.count} files`, "Consolidate into a single shared utility/module.", [], duplicationPrompt(nm, c.kind ?? "", c.count), coversOf([coveredByDupCluster.get(c)]));
14593
14898
  }
14594
14899
  for (const m of (coup?.god_modules ?? []).slice(0, 5)) {
14595
14900
  const file = rel(m.file);
14596
14901
  if (isGenerated(file))
14597
14902
  continue;
14598
- pushAction(2, "modularity", `God module ${file} (fan-in ${m.fan_in}, fan-out ${m.fan_out})`, "Split into cohesive sub-modules; introduce interfaces to cut fan-out.", [{ file }], couplingPrompt(file, m.fan_in ?? 0, m.fan_out ?? 0, Math.round((m.instability ?? 0) * 100) / 100));
14903
+ pushAction(2, "modularity", `God module ${file} (fan-in ${m.fan_in}, fan-out ${m.fan_out})`, "Split into cohesive sub-modules; introduce interfaces to cut fan-out.", [{ file }], couplingPrompt(file, m.fan_in ?? 0, m.fan_out ?? 0, Math.round((m.instability ?? 0) * 100) / 100), coversOf([coveredByGodModule.get(file)]));
14599
14904
  }
14600
14905
  for (const c of (cyc?.cycles ?? []).slice(0, 5)) {
14601
14906
  const chain = (c.files ?? []).map(rel).filter((f) => !isGenerated(f));
14602
14907
  if (chain.length < 2)
14603
14908
  continue;
14604
- pushAction(2, "architecture", `Circular dependency (${chain.length} files)`, "Break the cycle: extract the shared type/interface into a separate module.", chain.slice(0, 4).map((f) => ({ file: f })), cyclePrompt(chain.slice(0, 6)));
14909
+ pushAction(2, "architecture", `Circular dependency (${chain.length} files)`, "Break the cycle: extract the shared type/interface into a separate module.", chain.slice(0, 4).map((f) => ({ file: f })), cyclePrompt(chain.slice(0, 6)), coversOf([coveredByCycle.get(c)]));
14605
14910
  }
14606
14911
  for (const h of hotspotsAll.slice(0, 5)) {
14607
14912
  pushAction(2, "stability", `Hotspot ${h.file} (${h.churn}\xD7 changes, complexity ${h.complexity})`, "Add regression tests and refactor into smaller functions before the next change.", [{ file: h.file }], hotspotPrompt(h.file, h.churn, h.complexity, h.suggestion));
@@ -14714,11 +15019,12 @@ var init_collector = __esm({
14714
15019
  "../engine/dist/core/code/collector.js"() {
14715
15020
  "use strict";
14716
15021
  init_project_resolver();
15022
+ init_code_intel();
14717
15023
  init_companions();
14718
15024
  init_secret_redactor();
14719
15025
  init_secret_scanner();
14720
15026
  init_prompts();
14721
- COLLECTOR_VERSION = 2;
15027
+ COLLECTOR_VERSION = 3;
14722
15028
  ANALYSES = [
14723
15029
  "health",
14724
15030
  "security",
@@ -14840,11 +15146,11 @@ var init_collector = __esm({
14840
15146
  });
14841
15147
 
14842
15148
  // ../engine/dist/core/source-discovery.js
14843
- import { createHash as createHash8 } from "crypto";
15149
+ import { createHash as createHash9 } from "crypto";
14844
15150
  import { existsSync as existsSync24, readdirSync as readdirSync13, statSync as statSync20 } from "fs";
14845
15151
  import { join as join27 } from "path";
14846
15152
  function sourceId(path) {
14847
- return "src_" + createHash8("sha256").update(path).digest("hex").slice(0, 12);
15153
+ return "src_" + createHash9("sha256").update(path).digest("hex").slice(0, 12);
14848
15154
  }
14849
15155
  function installSourceExclusions(ids) {
14850
15156
  excludedIds = new Set((ids || []).filter((s) => typeof s === "string" && SOURCE_ID_RE.test(s)));
@@ -14949,10 +15255,10 @@ var init_source_discovery = __esm({
14949
15255
  // ../engine/dist/core/home-discovery.js
14950
15256
  import { execFileSync as execFileSync2 } from "child_process";
14951
15257
  import { existsSync as existsSync25, readdirSync as readdirSync14, readFileSync as readFileSync19, realpathSync as realpathSync3, statSync as statSync21 } from "fs";
14952
- import { homedir as homedir6, platform as osPlatform } from "os";
15258
+ import { homedir as homedir7, platform as osPlatform } from "os";
14953
15259
  import { join as join28 } from "path";
14954
15260
  function candidateRoots(plat = osPlatform(), env = process.env) {
14955
- const home = homedir6();
15261
+ const home = homedir7();
14956
15262
  const roots = [home];
14957
15263
  const add = (p) => {
14958
15264
  if (p && !roots.includes(p))
@@ -15070,7 +15376,7 @@ function declaredHomes(env = process.env, plat = osPlatform()) {
15070
15376
  }
15071
15377
  return out;
15072
15378
  }
15073
- const home = homedir6();
15379
+ const home = homedir7();
15074
15380
  const rcs = [".bashrc", ".bash_profile", ".zshrc", ".zprofile", ".profile"].map((f) => join28(home, f)).concat([join28(home, ".config", "fish", "config.fish")]);
15075
15381
  for (const rc of rcs) {
15076
15382
  let text;
@@ -16040,7 +16346,7 @@ function matchesProjectList(projectPath, patterns) {
16040
16346
  const tail = projectPath.slice(parent.length + 1);
16041
16347
  if (projectPath.startsWith(parent + "/") && !tail.includes("/"))
16042
16348
  return true;
16043
- } else if (projectPath === p || projectPath.startsWith(p + "/")) {
16349
+ } else if (projectPathAtOrUnder(projectPath, p)) {
16044
16350
  return true;
16045
16351
  }
16046
16352
  }
@@ -16066,6 +16372,7 @@ var cache2, TOOLS;
16066
16372
  var init_source_policy = __esm({
16067
16373
  "../engine/dist/core/source-policy.js"() {
16068
16374
  "use strict";
16375
+ init_project_path_match();
16069
16376
  init_settings();
16070
16377
  cache2 = null;
16071
16378
  TOOLS = ["claude", "gemini", "opencode", "codex", "agy", "cursor"];
@@ -16269,7 +16576,7 @@ var init_session_source = __esm({
16269
16576
 
16270
16577
  // ../engine/dist/core/utils.js
16271
16578
  import { existsSync as existsSync26, readdirSync as readdirSync15 } from "fs";
16272
- import { homedir as homedir7 } from "os";
16579
+ import { homedir as homedir8 } from "os";
16273
16580
  import { join as join29 } from "path";
16274
16581
  function splitByHeaders(markdown) {
16275
16582
  const lines = markdown.split("\n");
@@ -16300,7 +16607,7 @@ function splitByHeaders(markdown) {
16300
16607
  return sections;
16301
16608
  }
16302
16609
  function getClaudeDirs() {
16303
- const home = homedir7();
16610
+ const home = homedir8();
16304
16611
  const dirs = [];
16305
16612
  const seen = /* @__PURE__ */ new Set();
16306
16613
  const add = (dir) => {
@@ -16340,7 +16647,7 @@ function discoverSubdirs(subdir) {
16340
16647
  result.push(path);
16341
16648
  }
16342
16649
  }
16343
- return result.length > 0 ? result : [join29(homedir7(), ".claude", subdir)];
16650
+ return result.length > 0 ? result : [join29(homedir8(), ".claude", subdir)];
16344
16651
  }
16345
16652
  var init_utils = __esm({
16346
16653
  "../engine/dist/core/utils.js"() {
@@ -16350,9 +16657,9 @@ var init_utils = __esm({
16350
16657
 
16351
16658
  // ../engine/dist/parsers/plan-source.js
16352
16659
  import { existsSync as existsSync27, readdirSync as readdirSync16, readFileSync as readFileSync20, statSync as statSync24 } from "fs";
16353
- import { homedir as homedir8 } from "os";
16660
+ import { homedir as homedir9 } from "os";
16354
16661
  import { join as join30, basename as basename14 } from "path";
16355
- import { createHash as createHash9 } from "crypto";
16662
+ import { createHash as createHash10 } from "crypto";
16356
16663
  function parseFrontmatter(content) {
16357
16664
  if (!content.startsWith("---"))
16358
16665
  return { body: content };
@@ -16528,7 +16835,7 @@ var init_plan_source = __esm({
16528
16835
  try {
16529
16836
  const data = JSON.parse(readFileSync20(projectsPath, "utf-8"));
16530
16837
  for (const path of Object.keys(data.projects || {})) {
16531
- projMap.set(createHash9("sha256").update(path).digest("hex"), path);
16838
+ projMap.set(createHash10("sha256").update(path).digest("hex"), path);
16532
16839
  }
16533
16840
  } catch {
16534
16841
  }
@@ -16592,7 +16899,7 @@ var init_plan_source = __esm({
16592
16899
  for (const match of content.matchAll(fullPathPattern)) {
16593
16900
  let path = (match[1] || "").trim();
16594
16901
  if (path.startsWith("~/")) {
16595
- path = join30(homedir8(), path.slice(2));
16902
+ path = join30(homedir9(), path.slice(2));
16596
16903
  }
16597
16904
  for (const projectDir of knownProjects) {
16598
16905
  if (path.startsWith(projectDir)) {
@@ -17051,9 +17358,9 @@ ${task.description}`.slice(0, MAX_CHUNK_CHARS2);
17051
17358
 
17052
17359
  // ../engine/dist/parsers/claude-md-source.js
17053
17360
  import { existsSync as existsSync29, readFileSync as readFileSync22, readdirSync as readdirSync18, statSync as statSync26 } from "fs";
17054
- import { homedir as homedir9 } from "os";
17361
+ import { homedir as homedir10 } from "os";
17055
17362
  import { join as join32, basename as basename15, dirname as dirname15 } from "path";
17056
- import { createHash as createHash10 } from "crypto";
17363
+ import { createHash as createHash11 } from "crypto";
17057
17364
  function discoverProjectDirs(claudeDir) {
17058
17365
  const dirs = [];
17059
17366
  const seen = /* @__PURE__ */ new Set();
@@ -17081,7 +17388,7 @@ function discoverProjectDirs(claudeDir) {
17081
17388
  scanProjectsDir(join32(claudeDir, "projects"));
17082
17389
  return dirs;
17083
17390
  }
17084
- const home = homedir9();
17391
+ const home = homedir10();
17085
17392
  scanProjectsDir(claudeBackend.projectsDir());
17086
17393
  try {
17087
17394
  for (const entry of readdirSync18(home, { withFileTypes: true })) {
@@ -17115,7 +17422,7 @@ function discoverProjectDirs(claudeDir) {
17115
17422
  } catch {
17116
17423
  }
17117
17424
  }
17118
- void createHash10;
17425
+ void createHash11;
17119
17426
  return dirs;
17120
17427
  }
17121
17428
  var MAX_CHUNK_CHARS3, NOTE_FILES, ClaudeMdSource;
@@ -17150,7 +17457,7 @@ var init_claude_md_source = __esm({
17150
17457
  const seen = /* @__PURE__ */ new Set();
17151
17458
  const seenKey = (path, tool) => `${tool}:${path}`;
17152
17459
  for (const note of NOTE_FILES) {
17153
- const p = join32(homedir9(), note.filename);
17460
+ const p = join32(homedir10(), note.filename);
17154
17461
  const key2 = seenKey(p, note.tool);
17155
17462
  if (existsSync29(p) && !seen.has(key2)) {
17156
17463
  seen.add(key2);
@@ -17277,7 +17584,7 @@ var init_claude_md_source = __esm({
17277
17584
 
17278
17585
  // ../engine/dist/parsers/agent-memory-source.js
17279
17586
  import { existsSync as existsSync30, readFileSync as readFileSync23, readdirSync as readdirSync19, statSync as statSync27 } from "fs";
17280
- import { homedir as homedir10 } from "os";
17587
+ import { homedir as homedir11 } from "os";
17281
17588
  import { join as join33, basename as basename16 } from "path";
17282
17589
  function parseFrontmatter2(content) {
17283
17590
  const fm = {};
@@ -17315,7 +17622,7 @@ function discoverMemoryDirs() {
17315
17622
  out.push({ memoryDir, projectHash });
17316
17623
  }
17317
17624
  };
17318
- const home = homedir10();
17625
+ const home = homedir11();
17319
17626
  const claudeRoots = [claudeBackend.projectsDir()];
17320
17627
  try {
17321
17628
  for (const entry of readdirSync19(home, { withFileTypes: true })) {
@@ -17464,7 +17771,7 @@ var init_agent_memory_source = __esm({
17464
17771
  import { existsSync as existsSync31, readdirSync as readdirSync20, statSync as statSync28 } from "fs";
17465
17772
  import { createReadStream as createReadStream3 } from "fs";
17466
17773
  import { createInterface as createInterface2 } from "readline";
17467
- import { homedir as homedir11 } from "os";
17774
+ import { homedir as homedir12 } from "os";
17468
17775
  import { join as join34 } from "path";
17469
17776
  var MAX_CHUNK_CHARS5, HistorySource;
17470
17777
  var init_history_source = __esm({
@@ -17480,7 +17787,7 @@ var init_history_source = __esm({
17480
17787
  if (historyPath) {
17481
17788
  this.historyPaths = [historyPath];
17482
17789
  } else {
17483
- const home = homedir11();
17790
+ const home = homedir12();
17484
17791
  const paths = [];
17485
17792
  const tryAdd = (p) => {
17486
17793
  if (existsSync31(p) && !paths.includes(p))
@@ -17646,7 +17953,7 @@ var init_history_source = __esm({
17646
17953
 
17647
17954
  // ../engine/dist/parsers/paste-source.js
17648
17955
  import { existsSync as existsSync32, readdirSync as readdirSync21, readFileSync as readFileSync24, statSync as statSync29 } from "fs";
17649
- import { homedir as homedir12 } from "os";
17956
+ import { homedir as homedir13 } from "os";
17650
17957
  import { join as join35, basename as basename17 } from "path";
17651
17958
  var MAX_CHUNK_CHARS6, MAX_FILE_SIZE, PasteSource;
17652
17959
  var init_paste_source = __esm({
@@ -17663,7 +17970,7 @@ var init_paste_source = __esm({
17663
17970
  if (cacheDir) {
17664
17971
  this.cacheDirs = [cacheDir];
17665
17972
  } else {
17666
- const home = homedir12();
17973
+ const home = homedir13();
17667
17974
  const dirs = [];
17668
17975
  const tryAdd = (p) => {
17669
17976
  if (existsSync32(p) && !dirs.includes(p))
@@ -17747,7 +18054,7 @@ var init_paste_source = __esm({
17747
18054
  // ../engine/dist/parsers/gemini-source.js
17748
18055
  import { existsSync as existsSync33, readdirSync as readdirSync22, readFileSync as readFileSync25, statSync as statSync30 } from "fs";
17749
18056
  import { join as join36, basename as basename18 } from "path";
17750
- import { createHash as createHash11 } from "crypto";
18057
+ import { createHash as createHash12 } from "crypto";
17751
18058
  function loadGeminiProjectMap2() {
17752
18059
  const map = /* @__PURE__ */ new Map();
17753
18060
  const projectsPath = geminiBackend.projectsJson();
@@ -17757,7 +18064,7 @@ function loadGeminiProjectMap2() {
17757
18064
  const data = JSON.parse(readFileSync25(projectsPath, "utf-8"));
17758
18065
  const projects = data.projects || {};
17759
18066
  for (const [path] of Object.entries(projects)) {
17760
- const hash = createHash11("sha256").update(path).digest("hex");
18067
+ const hash = createHash12("sha256").update(path).digest("hex");
17761
18068
  map.set(hash, path);
17762
18069
  }
17763
18070
  } catch {
@@ -18740,7 +19047,7 @@ var init_redact_inline = __esm({
18740
19047
 
18741
19048
  // ../engine/dist/parsers/skills-source.js
18742
19049
  import { existsSync as existsSync39, readdirSync as readdirSync26, readFileSync as readFileSync29, statSync as statSync34 } from "fs";
18743
- import { homedir as homedir13 } from "os";
19050
+ import { homedir as homedir14 } from "os";
18744
19051
  import { join as join40 } from "path";
18745
19052
  function rebuildBody(content) {
18746
19053
  const { text, redacted } = redactInlineSecrets(content);
@@ -18756,7 +19063,7 @@ function rebuildBody(content) {
18756
19063
  return out;
18757
19064
  }
18758
19065
  function resolveRoots() {
18759
- const home = homedir13();
19066
+ const home = homedir14();
18760
19067
  const all = [];
18761
19068
  if (isSourceEnabled("shared", "skills")) {
18762
19069
  all.push({ path: join40(home, ".agents", "skills"), tool: "shared" });
@@ -18939,7 +19246,7 @@ var init_skills_source = __esm({
18939
19246
 
18940
19247
  // ../engine/dist/parsers/mcps-source.js
18941
19248
  import { existsSync as existsSync40, readdirSync as readdirSync27, readFileSync as readFileSync30, statSync as statSync35 } from "fs";
18942
- import { homedir as homedir14 } from "os";
19249
+ import { homedir as homedir15 } from "os";
18943
19250
  import { join as join41 } from "path";
18944
19251
  function readJson(path) {
18945
19252
  if (!existsSync40(path))
@@ -19005,7 +19312,7 @@ var init_mcps_source = __esm({
19005
19312
  async *discover() {
19006
19313
  if (!isSourceEnabled("common", "mcps"))
19007
19314
  return;
19008
- const home = homedir14();
19315
+ const home = homedir15();
19009
19316
  yield* this.fromObject(readJson(join41(home, ".mcp.json"))?.mcpServers, {
19010
19317
  tool: "claude",
19011
19318
  filePath: join41(home, ".mcp.json"),
@@ -19678,7 +19985,7 @@ var init_hooks_source = __esm({
19678
19985
 
19679
19986
  // ../engine/dist/parsers/plugins-source.js
19680
19987
  import { existsSync as existsSync44, readFileSync as readFileSync34, readdirSync as readdirSync31, statSync as statSync39 } from "fs";
19681
- import { homedir as homedir15 } from "os";
19988
+ import { homedir as homedir16 } from "os";
19682
19989
  import { join as join45 } from "path";
19683
19990
  function readJson3(path) {
19684
19991
  if (!existsSync44(path))
@@ -19700,7 +20007,7 @@ var init_plugins_source = __esm({
19700
20007
  PluginsSource = class {
19701
20008
  sourceType = "plugin";
19702
20009
  async *discover() {
19703
- const home = homedir15();
20010
+ const home = homedir16();
19704
20011
  const claudePluginsManifest = claudeBackend.pluginsManifestPath();
19705
20012
  const claudeJson = isSourceEnabled("claude", "plugins") ? readJson3(claudePluginsManifest) : null;
19706
20013
  if (claudeJson?.plugins && typeof claudeJson.plugins === "object") {
@@ -20322,7 +20629,7 @@ var init_http = __esm({
20322
20629
  CLI_VERSION = JSON.parse(readFileSync37(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
20323
20630
  } catch {
20324
20631
  try {
20325
- if (true) CLI_VERSION = "0.5.16";
20632
+ if (true) CLI_VERSION = "0.5.17";
20326
20633
  } catch {
20327
20634
  }
20328
20635
  }
@@ -20509,17 +20816,17 @@ var init_scan_pool = __esm({
20509
20816
  });
20510
20817
  const slot = { worker, pending: null };
20511
20818
  worker.on("message", (r) => {
20512
- const resolve2 = slot.pending;
20819
+ const resolve3 = slot.pending;
20513
20820
  slot.pending = null;
20514
- resolve2?.(r);
20821
+ resolve3?.(r);
20515
20822
  this.pump();
20516
20823
  });
20517
20824
  const die = (err) => {
20518
- const resolve2 = slot.pending;
20825
+ const resolve3 = slot.pending;
20519
20826
  slot.pending = null;
20520
20827
  this.slots = this.slots.filter((s) => s !== slot);
20521
20828
  if (this.slots.length === 0) this.disabled = true;
20522
- resolve2?.({ id: -1, findings: [], redactions: 0, rows: null, error: `scan worker died: ${err}` });
20829
+ resolve3?.({ id: -1, findings: [], redactions: 0, rows: null, error: `scan worker died: ${err}` });
20523
20830
  this.pump();
20524
20831
  };
20525
20832
  worker.on("error", die);
@@ -20585,8 +20892,8 @@ var init_scan_pool = __esm({
20585
20892
  this.start();
20586
20893
  if (this.disabled) return null;
20587
20894
  task.id = this.nextId++;
20588
- const result = await new Promise((resolve2) => {
20589
- this.queue.push({ task, resolve: resolve2 });
20895
+ const result = await new Promise((resolve3) => {
20896
+ this.queue.push({ task, resolve: resolve3 });
20590
20897
  this.pump();
20591
20898
  });
20592
20899
  if (result.error) return null;
@@ -20819,8 +21126,8 @@ async function flush() {
20819
21126
  }
20820
21127
  function hashHost() {
20821
21128
  try {
20822
- const { createHash: createHash15 } = __require("node:crypto");
20823
- return createHash15("sha256").update(hostname()).digest("hex").slice(0, 12);
21129
+ const { createHash: createHash16 } = __require("node:crypto");
21130
+ return createHash16("sha256").update(hostname()).digest("hex").slice(0, 12);
20824
21131
  } catch {
20825
21132
  return "";
20826
21133
  }
@@ -20834,7 +21141,7 @@ var init_telemetry = __esm({
20834
21141
  init_sync_client();
20835
21142
  cliVersion2 = () => {
20836
21143
  try {
20837
- return true ? "0.5.16" : "dev";
21144
+ return true ? "0.5.17" : "dev";
20838
21145
  } catch {
20839
21146
  return "dev";
20840
21147
  }
@@ -20858,7 +21165,7 @@ import { execSync as execSync4 } from "node:child_process";
20858
21165
  import { writeFileSync as writeFileSync9, mkdtempSync as mkdtempSync3 } from "node:fs";
20859
21166
  import { join as join48 } from "node:path";
20860
21167
  import { tmpdir as tmpdir2 } from "node:os";
20861
- import { createHash as createHash12 } from "node:crypto";
21168
+ import { createHash as createHash13 } from "node:crypto";
20862
21169
  function compareVersions(a, b) {
20863
21170
  const pa = String(a).split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
20864
21171
  const pb = String(b).split("-")[0].split(".").map((n) => parseInt(n, 10) || 0);
@@ -20888,7 +21195,7 @@ async function executeAutoUpdate(plan, deps) {
20888
21195
  } catch (e) {
20889
21196
  return { updated: false, reason: `download failed: ${e instanceof Error ? e.message : e}` };
20890
21197
  }
20891
- const got = createHash12("sha256").update(buf).digest("hex");
21198
+ const got = createHash13("sha256").update(buf).digest("hex");
20892
21199
  if (got !== plan.sha256) return { updated: false, reason: `checksum mismatch (expected ${plan.sha256.slice(0, 12)}\u2026, got ${got.slice(0, 12)}\u2026) \u2014 NOT installing` };
20893
21200
  const dir = mkdtempSync3(join48(tmpdir2(), "cr-update-"));
20894
21201
  const tgz = join48(dir, "chat-recall.tgz");
@@ -20961,6 +21268,7 @@ var sync_client_exports = {};
20961
21268
  __export(sync_client_exports, {
20962
21269
  BATCH_SCAN_MAX_BYTES: () => BATCH_SCAN_MAX_BYTES,
20963
21270
  FULL_BUILD_MAX_BYTES: () => FULL_BUILD_MAX_BYTES,
21271
+ _fetchTenantSyncConfigForTests: () => _fetchTenantSyncConfigForTests,
20964
21272
  _resetCapsCacheForTests: () => _resetCapsCacheForTests,
20965
21273
  _resetTenantSecurityConfigCache: () => _resetTenantSecurityConfigCache,
20966
21274
  _resetTenantSyncConfigCache: () => _resetTenantSyncConfigCache,
@@ -20984,7 +21292,7 @@ __export(sync_client_exports, {
20984
21292
  import { join as join49, dirname as dirname18, basename as basename23 } from "node:path";
20985
21293
  import { readFileSync as readFileSync38, writeFileSync as writeFileSync10, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync4, rmSync as rmSync5, chmodSync as chmodSync5, unlinkSync as unlinkSync3, readdirSync as readdirSync32, statSync as statSync41 } from "node:fs";
20986
21294
  import { tmpdir as tmpdir3, hostname as hostname2 } from "node:os";
20987
- import { createHash as createHash13 } from "node:crypto";
21295
+ import { createHash as createHash14 } from "node:crypto";
20988
21296
  import { fileURLToPath as fileURLToPath2 } from "node:url";
20989
21297
  import { gzipSync as gzipSync3 } from "zlib";
20990
21298
  function classifyError(msg) {
@@ -21083,6 +21391,25 @@ async function fetchTenantSecurityConfig(cred) {
21083
21391
  function _resetTenantSecurityConfigCache() {
21084
21392
  _securityConfigCache.clear();
21085
21393
  }
21394
+ function syncConfigCachePath() {
21395
+ return join49(getDataDir(), "sync-config-cache.json");
21396
+ }
21397
+ function readPersistedSyncConfigs() {
21398
+ try {
21399
+ return JSON.parse(readFileSync38(syncConfigCachePath(), "utf-8"));
21400
+ } catch {
21401
+ return {};
21402
+ }
21403
+ }
21404
+ function persistSyncConfig(base, config2) {
21405
+ try {
21406
+ const all = readPersistedSyncConfigs();
21407
+ all[base] = { fetchedAt: Date.now(), config: config2 };
21408
+ mkdirSync11(dirname18(syncConfigCachePath()), { recursive: true });
21409
+ writeFileSync10(syncConfigCachePath(), JSON.stringify(all, null, 2));
21410
+ } catch {
21411
+ }
21412
+ }
21086
21413
  async function fetchTenantSyncConfig(cred) {
21087
21414
  const base = cred.serverUrl.replace(/\/+$/, "");
21088
21415
  const cached = _syncConfigCache.get(base);
@@ -21102,7 +21429,11 @@ async function fetchTenantSyncConfig(cred) {
21102
21429
  } catch {
21103
21430
  }
21104
21431
  const res = await fetchWithTimeout(`${base}/api/sync-config`, { headers });
21105
- if (!res.ok) return { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
21432
+ if (res.status === 404) {
21433
+ _syncConfigCache.set(base, { fetchedAt: Date.now(), config: EMPTY_SYNC_CONFIG });
21434
+ return EMPTY_SYNC_CONFIG;
21435
+ }
21436
+ if (!res.ok) return lastKnownSyncConfig(base, `HTTP ${res.status}`);
21106
21437
  const body = await res.json().catch(() => ({}));
21107
21438
  const config2 = {
21108
21439
  excludeTools: Array.isArray(body.excludeTools) ? body.excludeTools.filter((t) => typeof t === "string") : [],
@@ -21140,11 +21471,22 @@ async function fetchTenantSyncConfig(cred) {
21140
21471
  } catch {
21141
21472
  }
21142
21473
  _syncConfigCache.set(base, { fetchedAt: Date.now(), config: config2 });
21474
+ persistSyncConfig(base, config2);
21143
21475
  return config2;
21144
- } catch {
21145
- return { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
21476
+ } catch (err) {
21477
+ return lastKnownSyncConfig(base, err instanceof Error ? err.message : "request failed");
21146
21478
  }
21147
21479
  }
21480
+ function lastKnownSyncConfig(base, reason) {
21481
+ const remembered = _syncConfigCache.get(base) ?? readPersistedSyncConfigs()[base];
21482
+ if (remembered) {
21483
+ console.error(`[sync] could not read server-side sync rules (${reason}) \u2014 applying the last known set from ${new Date(remembered.fetchedAt).toISOString()}`);
21484
+ return remembered.config;
21485
+ }
21486
+ throw new Error(
21487
+ `Cannot read the server-side sync rules from ${base} (${reason}), and this machine has never successfully read them. Refusing to sync: uploading without knowing your exclusions could ship a project you told the dashboard to hold back. Fix the server or retry.`
21488
+ );
21489
+ }
21148
21490
  function deviceLabel() {
21149
21491
  try {
21150
21492
  return hostname2();
@@ -21434,7 +21776,7 @@ async function syncToTargetInScope(cred, opts = {}) {
21434
21776
  };
21435
21777
  const cleartext = opts.cleartextPaths ?? sync?.pathsCleartext === true;
21436
21778
  const mapPath = (p) => cleartext ? p : hashPath(p);
21437
- const excluded = (projectPath) => excludeProjects.some((x) => x && projectPath.includes(x));
21779
+ const excluded = (projectPath) => excludeProjects.some((x) => projectPathIncludes(projectPath, x));
21438
21780
  const syncOnly = /* @__PURE__ */ new Set([...sync?.syncOnlyProjects ?? [], ...serverCfg.syncOnlyProjects ?? []]);
21439
21781
  const includedProject = (projectPath) => {
21440
21782
  if ((sync?.syncMode ?? "all") !== "only") return true;
@@ -21690,7 +22032,7 @@ async function syncToTargetInScope(cred, opts = {}) {
21690
22032
  const kgSeen = /* @__PURE__ */ new Set();
21691
22033
  const kgEntityType = /* @__PURE__ */ new Map();
21692
22034
  const kgEntityNames = /* @__PURE__ */ new Set();
21693
- const kgKey = (subject, predicate, object, validFrom) => createHash13("sha1").update(`${subject}|${predicate}|${object}|${validFrom}`).digest("hex").slice(0, 16);
22035
+ const kgKey = (subject, predicate, object, validFrom) => createHash14("sha1").update(`${subject}|${predicate}|${object}|${validFrom}`).digest("hex").slice(0, 16);
21694
22036
  const accumulateKg = async (triples, sourceSession) => {
21695
22037
  for (const t of triples) {
21696
22038
  const key2 = kgKey(t.subject, t.predicate, t.object, t.validFrom ?? "");
@@ -21780,7 +22122,7 @@ async function syncToTargetInScope(cred, opts = {}) {
21780
22122
  let traceN = 0;
21781
22123
  let considered = 0;
21782
22124
  for (const ref of slice) {
21783
- await new Promise((resolve2) => setImmediate(resolve2));
22125
+ await new Promise((resolve3) => setImmediate(resolve3));
21784
22126
  considered++;
21785
22127
  const walkDone = considered >= slice.length;
21786
22128
  reportWalkProgress({ done: considered, total: slice.length, startedAt: walkStartedAt, complete: walkDone });
@@ -22394,7 +22736,7 @@ async function syncIncremental(opts = {}) {
22394
22736
  lock.release();
22395
22737
  }
22396
22738
  }
22397
- var ownVersion, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
22739
+ var ownVersion, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, _fetchTenantSyncConfigForTests, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
22398
22740
  var init_sync_client = __esm({
22399
22741
  "src/sync-client.ts"() {
22400
22742
  "use strict";
@@ -22416,6 +22758,7 @@ var init_sync_client = __esm({
22416
22758
  init_tool_backend();
22417
22759
  init_session();
22418
22760
  init_project_resolver();
22761
+ init_project_path_match();
22419
22762
  init_entity_extractor();
22420
22763
  init_all_sources();
22421
22764
  init_sync_ledger();
@@ -22439,7 +22782,7 @@ var init_sync_client = __esm({
22439
22782
  ownVersion = JSON.parse(readFileSync38(join49(dir, "../package.json"), "utf-8")).version || ownVersion;
22440
22783
  } catch {
22441
22784
  try {
22442
- if (true) ownVersion = "0.5.16";
22785
+ if (true) ownVersion = "0.5.17";
22443
22786
  } catch {
22444
22787
  }
22445
22788
  }
@@ -22449,7 +22792,9 @@ var init_sync_client = __esm({
22449
22792
  _securityConfigCache = /* @__PURE__ */ new Map();
22450
22793
  SECURITY_CONFIG_TTL_MS = 5 * 60 * 1e3;
22451
22794
  _syncConfigCache = /* @__PURE__ */ new Map();
22452
- hashPath = (p) => p ? "p_" + createHash13("sha256").update(p).digest("hex").slice(0, 12) : "";
22795
+ EMPTY_SYNC_CONFIG = { excludeTools: [], excludeProjects: [], excludeSources: [], approveSources: [] };
22796
+ _fetchTenantSyncConfigForTests = fetchTenantSyncConfig;
22797
+ hashPath = (p) => p ? "p_" + createHash14("sha256").update(p).digest("hex").slice(0, 12) : "";
22453
22798
  MAX_DERIVED_ROW_BYTES = 2 * 1024 * 1024;
22454
22799
  isLocalHost = isPrivateHost;
22455
22800
  CAPS_TTL_MS = 60 * 6e4;
@@ -22626,7 +22971,7 @@ __export(install_skills_exports, {
22626
22971
  uninstallSkills: () => uninstallSkills
22627
22972
  });
22628
22973
  import { readdirSync as readdirSync33, existsSync as existsSync47, mkdirSync as mkdirSync12, rmSync as rmSync6, cpSync, writeFileSync as writeFileSync12, readFileSync as readFileSync41, statSync as statSync42, realpathSync as realpathSync4 } from "fs";
22629
- import { createHash as createHash14 } from "crypto";
22974
+ import { createHash as createHash15 } from "crypto";
22630
22975
  import { join as join51, basename as basename24 } from "path";
22631
22976
  function skillsSourceDir() {
22632
22977
  return join51(import.meta.dirname, "..", "skills");
@@ -22640,7 +22985,7 @@ function version() {
22640
22985
  }
22641
22986
  function skillsFingerprint() {
22642
22987
  const src = skillsSourceDir();
22643
- const h = createHash14("sha256");
22988
+ const h = createHash15("sha256");
22644
22989
  for (const name of bundledSkillNames()) {
22645
22990
  for (const rel of filesUnder(join51(src, name)).sort()) {
22646
22991
  h.update(name + "/" + rel);
@@ -22836,13 +23181,13 @@ __export(service_installer_exports, {
22836
23181
  });
22837
23182
  import { execSync as execSync5 } from "child_process";
22838
23183
  import { existsSync as existsSync48, mkdirSync as mkdirSync13, rmSync as rmSync7, writeFileSync as writeFileSync13 } from "fs";
22839
- import { homedir as homedir16 } from "os";
23184
+ import { homedir as homedir17 } from "os";
22840
23185
  import { join as join52 } from "path";
22841
23186
  import { fileURLToPath as fileURLToPath3 } from "url";
22842
23187
  function resolveDaemonPaths() {
22843
23188
  const watchJs = fileURLToPath3(new URL("./watch.js", import.meta.url));
22844
23189
  const node = process.execPath;
22845
- const dataDir = join52(homedir16(), ".chat-recall");
23190
+ const dataDir = join52(homedir17(), ".chat-recall");
22846
23191
  const logFile = join52(dataDir, "watch.log");
22847
23192
  return { watchJs, node, logFile, dataDir };
22848
23193
  }
@@ -22884,7 +23229,7 @@ function installService() {
22884
23229
  );
22885
23230
  }
22886
23231
  function installSystemd(watchJs, node, logFile) {
22887
- const unitDir = join52(homedir16(), ".config", "systemd", "user");
23232
+ const unitDir = join52(homedir17(), ".config", "systemd", "user");
22888
23233
  const unitPath = join52(unitDir, "chat-recall-watch.service");
22889
23234
  const unit = renderSystemdUnit(watchJs, node, logFile);
22890
23235
  mkdirSync13(unitDir, { recursive: true });
@@ -22932,7 +23277,7 @@ function renderSystemdUnit(watchJs, node, logFile) {
22932
23277
  }
22933
23278
  function installLaunchd(watchJs, node, logFile) {
22934
23279
  const label = "com.chat-recall.watch";
22935
- const plistDir = join52(homedir16(), "Library", "LaunchAgents");
23280
+ const plistDir = join52(homedir17(), "Library", "LaunchAgents");
22936
23281
  const plistPath = join52(plistDir, `${label}.plist`);
22937
23282
  const plist = renderLaunchdPlist(watchJs, node, logFile, label);
22938
23283
  mkdirSync13(plistDir, { recursive: true });
@@ -22989,7 +23334,7 @@ function uninstallService() {
22989
23334
  removed = true;
22990
23335
  } catch {
22991
23336
  }
22992
- const unitPath = join52(homedir16(), ".config", "systemd", "user", "chat-recall-watch.service");
23337
+ const unitPath = join52(homedir17(), ".config", "systemd", "user", "chat-recall-watch.service");
22993
23338
  if (existsSync48(unitPath)) {
22994
23339
  rmSync7(unitPath, { force: true });
22995
23340
  removed = true;
@@ -23001,7 +23346,7 @@ function uninstallService() {
23001
23346
  return removed;
23002
23347
  }
23003
23348
  if (process.platform === "darwin") {
23004
- const plistPath = join52(homedir16(), "Library", "LaunchAgents", "com.chat-recall.watch.plist");
23349
+ const plistPath = join52(homedir17(), "Library", "LaunchAgents", "com.chat-recall.watch.plist");
23005
23350
  try {
23006
23351
  execSync5(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore" });
23007
23352
  } catch {
@@ -23045,9 +23390,13 @@ var init_service_installer = __esm({
23045
23390
 
23046
23391
  // src/mcp.ts
23047
23392
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
23048
- import { spawn as spawn2 } from "child_process";
23393
+ import { spawn as spawn3 } from "child_process";
23049
23394
  import { fileURLToPath as fileURLToPath4 } from "node:url";
23050
23395
 
23396
+ // ../engine/dist/mcp/tools.js
23397
+ import { homedir as homedir6 } from "node:os";
23398
+ import { resolve as resolve2 } from "node:path";
23399
+
23051
23400
  // ../engine/dist/core/resume-command.js
23052
23401
  function toolOfSessionId(sessionId) {
23053
23402
  if (sessionId.startsWith("codex_"))
@@ -23164,6 +23513,9 @@ function trialEndingBanner(state2, serverBase) {
23164
23513
  ].join(" ");
23165
23514
  }
23166
23515
 
23516
+ // ../engine/dist/mcp/tools.js
23517
+ init_login_prompt();
23518
+
23167
23519
  // ../engine/dist/mcp/credential-context.js
23168
23520
  import { AsyncLocalStorage } from "node:async_hooks";
23169
23521
  var store = new AsyncLocalStorage();
@@ -23480,6 +23832,9 @@ function setIndexRunner(fn) {
23480
23832
  function localOnlyRefusal(tool) {
23481
23833
  return `${tool} runs on YOUR machine, not on the server \u2014 this connection has no access to your files. Install the chat-recall CLI and run it there; everything it collects syncs here and stays searchable from this connection.`;
23482
23834
  }
23835
+ function emptyIndexHint() {
23836
+ return isMultiTenant() ? "No coding history in this account yet \u2014 normal for a new one, not an error. The account already exists and the free trial is running; there is nothing to sign up for. The transcripts live on the user's own machine, so they must be indexed there.\n\nIF YOU HAVE A SHELL, DO THIS NOW \u2014 it is the whole setup:\n 1. npm install -g chat-recall\n 2. chat-recall init\n Prints a URL and waits. Tell the user to open it and approve \u2014 one click, same account,\n not a second signup. Do not kill the command while it waits.\n 3. It then indexes this machine's Claude Code, Codex, Gemini CLI, OpenCode, Antigravity and\n Cursor sessions, redacts secrets locally, and syncs them.\n 4. Call recall_status to confirm, then retry what you were asked.\nIf you have no shell, give the user steps 1 and 2 verbatim.\n\nMeanwhile this connection already works for memory created here \u2014 facts, decisions, tasks and diary entries persist across conversations right now." : "Nothing has been indexed yet. Run `chat-recall init` (first time) or `chat-recall index` to collect this machine's sessions and sync them.";
23837
+ }
23483
23838
  async function runIndexChild(force) {
23484
23839
  if (!indexRunner) {
23485
23840
  return "Indexing runs on YOUR machine, not on the server \u2014 this connection has no access to your transcripts. Install the CLI and run `chat-recall index` (or `chat-recall init` for first-time setup) on the machine whose history you want searchable here.";
@@ -23595,10 +23950,22 @@ async function remotePatch(path, body) {
23595
23950
  throw await buildHttpError(path, res);
23596
23951
  return res.json();
23597
23952
  }
23953
+ async function remoteDelete(path) {
23954
+ const cred = remoteCredentials();
23955
+ if (!cred)
23956
+ throw new Error('scope "server" needs a login \u2014 run `chat-recall login <server-url>` first.');
23957
+ const res = await fetch(cred.base + path, {
23958
+ method: "DELETE",
23959
+ headers: { authorization: `Bearer ${cred.token}` }
23960
+ });
23961
+ if (!res.ok)
23962
+ throw await buildHttpError(path, res);
23963
+ return res.json();
23964
+ }
23598
23965
  function requireRemote() {
23599
23966
  const cred = remoteCredentials();
23600
23967
  if (!cred) {
23601
- throw new Error("chat-recall is not logged in. Run `chat-recall login <server-url>` to connect this machine to your chat-recall server (self-host or cloud), then retry.");
23968
+ throw new Error(loginInstruction());
23602
23969
  }
23603
23970
  return cred;
23604
23971
  }
@@ -23670,6 +24037,14 @@ var RecallRenameSchema = z.object({
23670
24037
  session_id: z.string().describe("Session ID to name (from recall_recent / recall_search)."),
23671
24038
  name: z.string().describe("The name to give this conversation. Pass an empty string to clear it and revert to the auto title.")
23672
24039
  });
24040
+ var RecallForgetSchema = z.object({
24041
+ session_id: z.string().describe("Session ID to delete everywhere (from recall_recent / recall_search)."),
24042
+ confirm: z.literal(true).describe("Must be true. Only pass it when the user has actually asked for this conversation to be deleted.")
24043
+ });
24044
+ var RecallExcludePathSchema = z.object({
24045
+ path: z.string().min(1).describe("Absolute path (or a distinctive part of one) to stop syncing."),
24046
+ confirm: z.literal(true).describe("Must be true. Only pass it when the user has actually asked to stop syncing this path.")
24047
+ });
23673
24048
  var RecallEditsTimelineSchema = z.object({
23674
24049
  since_hours: z.number().optional().default(24).describe("Time window in hours. Defaults to 24h."),
23675
24050
  limit: z.number().optional().default(100).describe("Maximum edit rows to return"),
@@ -23713,7 +24088,13 @@ var RecallMemorySearchSchema = z.object({
23713
24088
  semantic: z.boolean().optional().default(false).describe("Reserved and currently ignored: the semantic tier is switched off server-side, so every search runs keyword FTS.")
23714
24089
  });
23715
24090
  var RecallSmartResumeSchema = z.object({
23716
- session_id: z.string().describe("Session ID to get smart resume context for")
24091
+ // OPTIONAL, because the question this tool answers is "continue where we left
24092
+ // off" and whoever asks it does not have a session id. Requiring one made the
24093
+ // routing advice ("continue" -> recall_smart_resume) impossible to follow: the
24094
+ // agent had to call recall_recent first, and a bare call failed with a zod
24095
+ // error about a missing field rather than doing the obvious thing.
24096
+ session_id: z.string().optional().describe("Session to resume. Omit for the most recent one."),
24097
+ project_filter: z.string().optional().describe("When session_id is omitted, resume the latest session from this project (path or name substring).")
23717
24098
  });
23718
24099
  var RecallSubagentSearchSchema = z.object({
23719
24100
  query: z.string().describe("Text to find inside subagent transcripts (explore/compact/aside sub-tasks)"),
@@ -23865,7 +24246,7 @@ var RecallUserPromptsSchema = z.object({
23865
24246
  });
23866
24247
  var RecallDecisionRecordSchema = z.object({
23867
24248
  subject: z.string().describe('What the decision is about (e.g., "chat-recall", "auth strategy")'),
23868
- decision: z.string().describe('The decision itself in plain words (e.g., "use SQLite FTS5 as the default search backend")'),
24249
+ decision: z.string().describe('The decision itself in plain words (e.g., "use Postgres full-text search as the default backend")'),
23869
24250
  reason: z.string().optional().describe("Why this was decided \u2014 short rationale"),
23870
24251
  importance: z.number().min(1).max(5).optional().default(4).describe("1\u20135; the classifier surfaces 4+ in wake-up context"),
23871
24252
  session_id: z.string().optional().describe("Session this decision was made in (for traceability)"),
@@ -23984,12 +24365,28 @@ var LEAN_TOOLS = /* @__PURE__ */ new Set([
23984
24365
  // Health, and the one that pays out on day one.
23985
24366
  "recall_status",
23986
24367
  "recall_index",
23987
- "recall_security_summary"
24368
+ "recall_security_summary",
24369
+ // The two REMOVE tools. They are here for the same reason as the rest — they
24370
+ // answer something a person says mid-conversation ("forget that", "stop
24371
+ // syncing this repo") — and a privacy control the agent cannot see is a
24372
+ // dashboard feature wearing an MCP badge. They shipped outside this set and
24373
+ // the e2e harness proved them working under CHAT_RECALL_MCP_PROFILE=full,
24374
+ // which is the one profile no user runs.
24375
+ //
24376
+ // Cost: two names on a 25-name list. Brake: `confirm: true` is a
24377
+ // z.literal(true), and both are kept out of the alwaysAllow list `init`
24378
+ // writes, so the host prompts every time. Neither depends on the listing.
24379
+ "recall_forget",
24380
+ "recall_exclude_path"
23988
24381
  ]);
23989
24382
  function toolProfile() {
23990
24383
  return (process.env.CHAT_RECALL_MCP_PROFILE || "").toLowerCase() === "full" ? "full" : "lean";
23991
24384
  }
23992
24385
  var WRITE_TOOLS = /* @__PURE__ */ new Set([
24386
+ "recall_forget",
24387
+ // deletes a session everywhere, with a tombstone
24388
+ "recall_exclude_path",
24389
+ // changes what syncs, here and on the account
23993
24390
  "recall_index",
23994
24391
  // indexes and ships new rows
23995
24392
  "recall_code_index",
@@ -24017,6 +24414,10 @@ var WRITE_TOOLS = /* @__PURE__ */ new Set([
24017
24414
  // overwrites the session name
24018
24415
  ]);
24019
24416
  var DESTRUCTIVE_TOOLS = /* @__PURE__ */ new Set([
24417
+ // The two scope-narrowing tools. recall_forget has no undo at all; excluding a
24418
+ // path changes what leaves the machine until someone removes the rule by hand.
24419
+ "recall_forget",
24420
+ "recall_exclude_path",
24020
24421
  "recall_kg_invalidate",
24021
24422
  "recall_recommendation_dismiss",
24022
24423
  // retires advice; undo exists but it is a decision
@@ -24129,7 +24530,7 @@ when the user is asking "what did we change?" and the diffs/SQL/commands matter.
24129
24530
  },
24130
24531
  {
24131
24532
  name: "recall_recent",
24132
- description: `List recent Claude Code sessions.
24533
+ description: `List recent sessions across every AI tool chat-recall indexes.
24133
24534
 
24134
24535
  Shows your most recent conversations across all projects or filtered by project.
24135
24536
  Use this when the user says "continue our last conversation" or wants to see recent work.
@@ -24145,6 +24546,50 @@ Pass \`since_hours: N\` to restrict to sessions modified in the last N hours.`,
24145
24546
  }
24146
24547
  }
24147
24548
  },
24549
+ {
24550
+ name: "recall_forget",
24551
+ description: `Delete one conversation from chat-recall, everywhere, permanently.
24552
+
24553
+ Use it when the user says "forget this conversation", "delete that session", "remove what we discussed
24554
+ about X" \u2014 anything that asks for a conversation to stop existing on the server.
24555
+
24556
+ WHAT IT DOES: purges the session on every server this machine is logged in to, and tombstones the id so
24557
+ no later sync can bring it back. The AI tool's own transcript file on the user's disk is NOT touched \u2014
24558
+ tell them that, because it is usually what they mean to keep.
24559
+
24560
+ IRREVERSIBLE. There is no undo and no tool that restores it. Only pass \`confirm: true\` when the user
24561
+ has actually asked for this; do not infer it from a complaint about a conversation.`,
24562
+ inputSchema: {
24563
+ type: "object",
24564
+ properties: {
24565
+ session_id: { type: "string", description: "Session ID to delete (from recall_recent / recall_search)." },
24566
+ confirm: { type: "boolean", description: "Must be true. Only when the user actually asked to delete it." }
24567
+ },
24568
+ required: ["session_id", "confirm"]
24569
+ }
24570
+ },
24571
+ {
24572
+ name: "recall_exclude_path",
24573
+ description: `Stop syncing a project path. Nothing under it leaves the machine from the next sync on.
24574
+
24575
+ Use it when the user says "stop syncing this repo", "don't upload anything from ~/work/client", "keep this
24576
+ project out of chat-recall".
24577
+
24578
+ WHAT IT DOES: adds the path to the exclusion rules \u2014 on this machine, and on the account so every device
24579
+ the user syncs from honours it. Sessions ALREADY uploaded from that path stay until they are deleted; say
24580
+ so, and offer recall_forget for the ones that matter.
24581
+
24582
+ There is deliberately no tool to REMOVE an exclusion: an agent may narrow what is synced, never widen it.
24583
+ Undo it with \`chat-recall exclude remove <path>\` or in the dashboard.`,
24584
+ inputSchema: {
24585
+ type: "object",
24586
+ properties: {
24587
+ path: { type: "string", description: "Absolute path (or a distinctive part of one) to stop syncing." },
24588
+ confirm: { type: "boolean", description: "Must be true. Only when the user actually asked to stop syncing it." }
24589
+ },
24590
+ required: ["path", "confirm"]
24591
+ }
24592
+ },
24148
24593
  {
24149
24594
  name: "recall_rename_session",
24150
24595
  description: `Give a conversation a memorable name \u2014 or rename it \u2014 so you (and the user) can find and resume it later.
@@ -24166,7 +24611,7 @@ The name persists across re-syncs and summary regeneration (it's stored separate
24166
24611
  {
24167
24612
  name: "recall_edits_timeline",
24168
24613
  description: `Chronological list of file edits across recent sessions, spanning every AI tool
24169
- chat-recall indexes \u2014 Claude Code, Gemini CLI, OpenCode, Codex and Antigravity.
24614
+ chat-recall indexes \u2014 Claude Code, Gemini CLI, OpenCode, Codex, Antigravity and Cursor.
24170
24615
 
24171
24616
  Returns rows shaped like (timestamp, tool, session_id, project, file, op) sorted newest
24172
24617
  first. Pulls live from each tool's native session store \u2014 Claude JSONL, Gemini chat
@@ -24268,8 +24713,8 @@ by extension, with tools used) \u2014 "which files did session X actually touch?
24268
24713
  name: "recall_commits",
24269
24714
  description: `Find git commits that landed during a session's edit window, grouped by repo.
24270
24715
 
24271
- Multi-repo aware: a session that touched ~/code/personal/k8s_gpu and
24272
- ~/code/personal/munbot returns commits from each repo, with overlap shown
24716
+ Multi-repo aware: a session that touched ~/code/example/infra and
24717
+ ~/code/example/api returns commits from each repo, with overlap shown
24273
24718
  between commit-files and session-touched-files.
24274
24719
 
24275
24720
  Use this to verify "shipped" claims \u2014 if the session edited 18 files but no
@@ -24333,7 +24778,11 @@ with recall_show (pass the plan id).`,
24333
24778
  },
24334
24779
  {
24335
24780
  name: "recall_smart_resume",
24336
- description: `Get structured resume context for a session.
24781
+ description: `Get structured resume context for a session \u2014 by default, the most recent one.
24782
+
24783
+ Call it with NO arguments to resume the latest session. That is the common case:
24784
+ "continue", "pick up where we left off". Pass session_id only to resume a
24785
+ specific one, or project_filter to take the latest from one project.
24337
24786
 
24338
24787
  Returns:
24339
24788
  - What was done (completed work, decisions made)
@@ -24346,9 +24795,9 @@ Use this instead of recall_context for a more actionable summary when resuming w
24346
24795
  inputSchema: {
24347
24796
  type: "object",
24348
24797
  properties: {
24349
- session_id: { type: "string", description: "Session ID to resume" }
24350
- },
24351
- required: ["session_id"]
24798
+ session_id: { type: "string", description: "Session to resume. OMIT to resume the most recent session." },
24799
+ project_filter: { type: "string", description: "With session_id omitted, resume the latest session from this project." }
24800
+ }
24352
24801
  }
24353
24802
  },
24354
24803
  {
@@ -24360,9 +24809,9 @@ overview (from CLAUDE.md), tech stack (KG uses), architecture / deployment / sec
24360
24809
  sections (from CLAUDE.md), decisions log (KG chose/rejected), recent session activity
24361
24810
  with summaries, open tasks, plans, agent diary conclusions, and cost rollup.
24362
24811
 
24363
- Accepts EITHER a project path / name substring (e.g. "munbot", "chat-recall",
24364
- "/home/user/code/poly") OR a stable project_id:
24365
- - "git:<host>/<owner>/<repo>" (e.g. "git:github.com/me/munbot")
24812
+ Accepts EITHER a project path / name substring (e.g. "api", "example-app",
24813
+ "/home/user/code/example") OR a stable project_id:
24814
+ - "git:<host>/<owner>/<repo>" (e.g. "git:github.com/owner/repo")
24366
24815
  - "ws:<name>" (workspace rollup)
24367
24816
  - "git-local:<sha1>" (local-only git repo)
24368
24817
  - "user:<custom>" (declared in ~/.chat-recall/projects.json)
@@ -24508,7 +24957,7 @@ without editing the task itself. Pass the task id (t_\u2026) from recall_tasks.`
24508
24957
  name: "recall_kg_query",
24509
24958
  description: `Query the knowledge graph for an entity's relationships.
24510
24959
 
24511
- Returns typed facts with temporal validity. E.g. "chat-recall" \u2192 uses TypeScript, has source FTS5.
24960
+ Returns typed facts with temporal validity. E.g. "example-app" \u2192 uses TypeScript, deploys to Kubernetes.
24512
24961
  Filter by date with as_of to see what was true at a specific point in time.
24513
24962
  Use this to VERIFY facts before asserting them.`,
24514
24963
  inputSchema: {
@@ -24525,7 +24974,7 @@ Use this to VERIFY facts before asserting them.`,
24525
24974
  name: "recall_kg_add",
24526
24975
  description: `Add a fact to the knowledge graph. Subject \u2192 predicate \u2192 object with optional time window.
24527
24976
 
24528
- E.g. ("chat-recall", "uses", "LanceDB", valid_from="2024-01-15")`,
24977
+ E.g. ("example-app", "uses", "Postgres", valid_from="2026-01-15")`,
24529
24978
  inputSchema: {
24530
24979
  type: "object",
24531
24980
  properties: {
@@ -25098,7 +25547,12 @@ Complements recall_memory_search: search finds candidates, this reads the specif
25098
25547
  if (hidden > 0) {
25099
25548
  lean.push({
25100
25549
  name: "recall_help",
25101
- description: `List the ${hidden} additional chat-recall tools that are not registered in this profile (analytics, knowledge-graph navigation, code intelligence, sharing, security triage, maintenance). They all still WORK if called by name. Set CHAT_RECALL_MCP_PROFILE=full to register every tool up front.`,
25550
+ // The env-var advice only applies to a LOCAL server the user launches.
25551
+ // Over the remote endpoint the process is ours, so telling a connector
25552
+ // user to set CHAT_RECALL_MCP_PROFILE names a knob they cannot reach —
25553
+ // and the useful half of the sentence (they still work by name) got lost
25554
+ // behind it.
25555
+ description: `List the ${hidden} additional chat-recall tools that are not registered in this profile (analytics, knowledge-graph navigation, code intelligence, sharing, security triage, maintenance). Every one of them still WORKS if you call it by name \u2014 this profile shortens the LIST, not the surface, because tool choice degrades as the list grows.` + (isMultiTenant() ? "" : " Set CHAT_RECALL_MCP_PROFILE=full to register them all up front."),
25102
25556
  inputSchema: { type: "object", properties: {} }
25103
25557
  });
25104
25558
  }
@@ -25258,7 +25712,14 @@ async function dispatchTool(request) {
25258
25712
  const searchQuery = sanitized.cleanQuery;
25259
25713
  const remote = await remotePost("/api/search", { query: searchQuery, topK: params.top_k, projectFilter: params.project_filter });
25260
25714
  if (!remote.results?.length) {
25261
- return { content: [{ type: "text", text: `No matching sessions on the server for "${params.query}".` }] };
25715
+ let indexed = 1;
25716
+ try {
25717
+ const st = await remoteGet("/api/status");
25718
+ indexed = st.totalChunks ?? 1;
25719
+ } catch {
25720
+ }
25721
+ const text = indexed === 0 ? emptyIndexHint() : `No matching sessions on the server for "${params.query}".`;
25722
+ return { content: [{ type: "text", text }] };
25262
25723
  }
25263
25724
  const lines = [`# Server results for: "${params.query}"`, "_(synced history across your devices)_", ""];
25264
25725
  for (let i = 0; i < remote.results.length; i++) {
@@ -25302,7 +25763,7 @@ async function dispatchTool(request) {
25302
25763
  const lines = [
25303
25764
  "Chat-Recall Server Status",
25304
25765
  `Synced sessions: ${sync.sessions}`,
25305
- `FTS5 chunks: ${status.totalChunks}`,
25766
+ `Indexed chunks: ${status.totalChunks}`,
25306
25767
  `Raw archives: ${sync.rawArchived}`
25307
25768
  ];
25308
25769
  {
@@ -25444,7 +25905,7 @@ ${plan.data.content}` }] };
25444
25905
  qs.set("project", params.project_filter);
25445
25906
  const remote = await remoteGet(`/api/conversations/recent?${qs.toString()}`);
25446
25907
  if (!remote.sessions?.length) {
25447
- const empty = params.project_filter || params.since_hours ? `No sessions match${params.project_filter ? ` project filter "${params.project_filter}"` : ""}${params.since_hours ? ` in the last ${params.since_hours}h` : ""}. The project filter is a case-insensitive substring of the project path \u2014 try a broader term, or drop it.` : "No sessions on the server yet \u2014 run `chat-recall sync` on your machines.";
25908
+ const empty = params.project_filter || params.since_hours ? `No sessions match${params.project_filter ? ` project filter "${params.project_filter}"` : ""}${params.since_hours ? ` in the last ${params.since_hours}h` : ""}. The project filter is a case-insensitive substring of the project path \u2014 try a broader term, or drop it.` : emptyIndexHint();
25448
25909
  return { content: [{ type: "text", text: empty }] };
25449
25910
  }
25450
25911
  const lines = [`# Recent sessions (server \u2014 ${remote.total} total synced)
@@ -25460,6 +25921,61 @@ ${plan.data.content}` }] };
25460
25921
  }
25461
25922
  return { content: [{ type: "text", text: lines.join("\n") }] };
25462
25923
  }
25924
+ case "recall_forget": {
25925
+ const params = RecallForgetSchema.parse(args);
25926
+ getWAL().log("forget", { session_id: params.session_id });
25927
+ requireRemote();
25928
+ const r = await remoteDelete(`/api/conversations/${encodeURIComponent(params.session_id)}`);
25929
+ return {
25930
+ content: [{
25931
+ type: "text",
25932
+ text: `\u2713 Deleted session \`${r.deleted || params.session_id}\` from the server${r.tombstoned ? " and tombstoned it, so no later sync can bring it back" : ""}.
25933
+ The transcript on this machine is untouched \u2014 this only removed the copy chat-recall held.`
25934
+ }]
25935
+ };
25936
+ }
25937
+ case "recall_exclude_path": {
25938
+ const params = RecallExcludePathSchema.parse(args);
25939
+ getWAL().log("exclude_path", { path: params.path });
25940
+ const written = [];
25941
+ if (!isMultiTenant()) {
25942
+ const { loadSettings: loadSettings2, saveSettings: saveSettings2 } = await Promise.resolve().then(() => (init_settings(), settings_exports));
25943
+ const settings2 = loadSettings2();
25944
+ const abs = resolve2(params.path.replace(/^~(?=\/|$)/, homedir6()));
25945
+ if (!settings2.sync.excludeProjects.includes(abs)) {
25946
+ settings2.sync.excludeProjects.push(abs);
25947
+ saveSettings2(settings2);
25948
+ }
25949
+ written.push("this machine");
25950
+ }
25951
+ try {
25952
+ const current = await remoteGet("/api/sync-config");
25953
+ if (!current.excludeProjects.includes(params.path)) {
25954
+ await remotePost("/api/sync-config", {
25955
+ ...current,
25956
+ excludeProjects: [...current.excludeProjects, params.path]
25957
+ });
25958
+ }
25959
+ written.push("your account (every device)");
25960
+ } catch (err) {
25961
+ if (!written.length)
25962
+ throw err;
25963
+ return {
25964
+ content: [{
25965
+ type: "text",
25966
+ text: `\u2713 Stopped syncing \`${params.path}\` from this machine.
25967
+ \u26A0 Could not write the rule to your account (${err instanceof Error ? err.message : "request failed"}), so your other devices are NOT covered yet. Retry, or add it in the dashboard.`
25968
+ }]
25969
+ };
25970
+ }
25971
+ return {
25972
+ content: [{
25973
+ type: "text",
25974
+ text: `\u2713 Stopped syncing \`${params.path}\` \u2014 rule written to ${written.join(" and ")}.
25975
+ Sessions already uploaded from that path stay until they are deleted; use recall_forget for the ones that matter.`
25976
+ }]
25977
+ };
25978
+ }
25463
25979
  case "recall_rename_session": {
25464
25980
  const params = RecallRenameSchema.parse(args);
25465
25981
  requireRemote();
@@ -25802,7 +26318,18 @@ _Edits stayed local or window was off \u2014 try increasing buffer_minutes._` }]
25802
26318
  }
25803
26319
  case "recall_smart_resume": {
25804
26320
  const params = RecallSmartResumeSchema.parse(args);
25805
- const sid = params.session_id;
26321
+ requireRemote();
26322
+ let sid = params.session_id;
26323
+ if (!sid) {
26324
+ const qs = new URLSearchParams({ limit: "1" });
26325
+ if (params.project_filter)
26326
+ qs.set("project", params.project_filter);
26327
+ const recent = await remoteGet(`/api/conversations/recent?${qs.toString()}`);
26328
+ sid = recent.sessions?.[0]?.sessionId;
26329
+ if (!sid) {
26330
+ return { content: [{ type: "text", text: params.project_filter ? `No sessions found for project \`${params.project_filter}\` \u2014 nothing to resume.` : "No sessions have synced yet, so there is nothing to resume. Run `chat-recall index` first." }] };
26331
+ }
26332
+ }
25806
26333
  const enc = encodeURIComponent(sid);
25807
26334
  const metaSoft = await remoteGetSoft(`/api/conversations/${enc}/metadata`);
25808
26335
  if (!metaSoft.data) {
@@ -27307,6 +27834,34 @@ ${JSON.stringify(r, null, 2)}
27307
27834
  }
27308
27835
  }
27309
27836
 
27837
+ // src/mcp.ts
27838
+ init_login_prompt();
27839
+
27840
+ // src/open-browser.ts
27841
+ import { spawn as spawn2 } from "node:child_process";
27842
+ function isSafeUrl(url) {
27843
+ try {
27844
+ const u = new URL(url);
27845
+ return u.protocol === "https:" || u.protocol === "http:";
27846
+ } catch {
27847
+ return false;
27848
+ }
27849
+ }
27850
+ function openBrowser(url) {
27851
+ if (!isSafeUrl(url)) return false;
27852
+ if (process.env.CHAT_RECALL_NO_BROWSER === "1" || process.env.CI) return false;
27853
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
27854
+ try {
27855
+ const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
27856
+ child.on("error", () => {
27857
+ });
27858
+ child.unref();
27859
+ return true;
27860
+ } catch {
27861
+ return false;
27862
+ }
27863
+ }
27864
+
27310
27865
  // src/mcp.ts
27311
27866
  init_client_events();
27312
27867
  init_update_notice();
@@ -27317,13 +27872,74 @@ setServerVersion(JSON.parse(
27317
27872
  setEventReporter(reportClientEvent2);
27318
27873
  setIndexRunner(runIndexChild2);
27319
27874
  setUpdateNotice(updateNotice2);
27875
+ setLoginStarter(startLoginChild);
27876
+ function withVia(url) {
27877
+ try {
27878
+ const u = new URL(url);
27879
+ u.searchParams.set("via", "mcp");
27880
+ return u.toString();
27881
+ } catch {
27882
+ return url;
27883
+ }
27884
+ }
27885
+ function startLoginChild(server) {
27886
+ return new Promise((resolve3, reject) => {
27887
+ let cliPath;
27888
+ try {
27889
+ cliPath = fileURLToPath4(new URL("./cli.js", import.meta.url));
27890
+ } catch {
27891
+ reject(new Error("could not locate the CLI entry point"));
27892
+ return;
27893
+ }
27894
+ const child = spawn3(process.execPath, [cliPath, "init", "--server", server, "--prompt-json", "--yes"], {
27895
+ env: process.env,
27896
+ stdio: ["ignore", "pipe", "pipe"],
27897
+ // Outlives this call by design: it polls until the user approves. Detached
27898
+ // so a client that restarts the MCP does not abort a sign-in the user is
27899
+ // halfway through in their browser.
27900
+ detached: true
27901
+ });
27902
+ child.unref();
27903
+ let out = "";
27904
+ let settled = false;
27905
+ const fail = (msg) => {
27906
+ if (!settled) {
27907
+ settled = true;
27908
+ reject(new Error(msg));
27909
+ }
27910
+ };
27911
+ child.stdout?.on("data", (d) => {
27912
+ if (settled) return;
27913
+ out += d;
27914
+ for (const line of out.split("\n")) {
27915
+ const t = line.trim();
27916
+ if (!t.startsWith("{")) continue;
27917
+ try {
27918
+ const parsed = JSON.parse(t);
27919
+ if (parsed.prompt?.url) {
27920
+ settled = true;
27921
+ const tagged = { ...parsed.prompt, url: withVia(parsed.prompt.url) };
27922
+ openBrowser(tagged.url);
27923
+ resolve3(tagged);
27924
+ return;
27925
+ }
27926
+ } catch {
27927
+ }
27928
+ }
27929
+ });
27930
+ child.on("error", (e) => fail(e.message));
27931
+ child.on("exit", (code) => fail(`login helper exited (${code ?? "signal"}) before issuing a code`));
27932
+ const timer = setTimeout(() => fail("the server did not issue a device code in time"), 3e4);
27933
+ timer.unref?.();
27934
+ });
27935
+ }
27320
27936
  function runIndexChild2(force) {
27321
- return new Promise((resolve2) => {
27937
+ return new Promise((resolve3) => {
27322
27938
  let cliPath;
27323
27939
  try {
27324
27940
  cliPath = fileURLToPath4(new URL("./cli.js", import.meta.url));
27325
27941
  } catch {
27326
- resolve2("Index unavailable: could not locate the CLI entry point.");
27942
+ resolve3("Index unavailable: could not locate the CLI entry point.");
27327
27943
  return;
27328
27944
  }
27329
27945
  const args = [cliPath, "index"];
@@ -27333,10 +27949,10 @@ function runIndexChild2(force) {
27333
27949
  const finish = (msg) => {
27334
27950
  if (!done) {
27335
27951
  done = true;
27336
- resolve2(msg);
27952
+ resolve3(msg);
27337
27953
  }
27338
27954
  };
27339
- const child = spawn2(process.execPath, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
27955
+ const child = spawn3(process.execPath, args, { env: process.env, stdio: ["ignore", "pipe", "pipe"] });
27340
27956
  child.stdout?.on("data", (d) => {
27341
27957
  out += d;
27342
27958
  });
@@ -27422,6 +28038,14 @@ async function main() {
27422
28038
  } catch {
27423
28039
  }
27424
28040
  })();
28041
+ try {
28042
+ const { loadAllCredentials: loadAllCredentials2 } = await Promise.resolve().then(() => (init_sync_client(), sync_client_exports));
28043
+ if (loadAllCredentials2().length === 0 && !process.env.CHAT_RECALL_TOKEN) {
28044
+ const { loginInstruction: loginInstruction2 } = await Promise.resolve().then(() => (init_login_prompt(), login_prompt_exports));
28045
+ loginInstruction2();
28046
+ }
28047
+ } catch {
28048
+ }
27425
28049
  let daemonRunning = false;
27426
28050
  try {
27427
28051
  const { isServiceRunning: isServiceRunning2 } = await Promise.resolve().then(() => (init_service_installer(), service_installer_exports));