chat-recall 0.5.16 → 0.5.18

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