chat-recall 0.6.8 → 0.6.9

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
@@ -4593,6 +4593,28 @@ function resolveProjectId(inputPath) {
4593
4593
  resolveCache.set(realPath, result);
4594
4594
  return result;
4595
4595
  }
4596
+ function resolveWorkspaceId(inputPath) {
4597
+ if (!inputPath || !isAbsolute(inputPath))
4598
+ return null;
4599
+ const realPath = safeRealpath(inputPath);
4600
+ const cached = wsIdCache.get(realPath);
4601
+ if (cached !== void 0)
4602
+ return cached;
4603
+ const result = doResolveWorkspace(realPath);
4604
+ wsIdCache.set(realPath, result);
4605
+ return result;
4606
+ }
4607
+ function doResolveWorkspace(realPath) {
4608
+ const cfg = loadProjectsConfig();
4609
+ const declaredWorkspaces = (cfg.projects || []).filter((p) => p.workspace).sort((a, b) => b.root.length - a.root.length);
4610
+ for (const proj of declaredWorkspaces) {
4611
+ if (isInside(realPath, safeRealpath(proj.root)))
4612
+ return proj.id;
4613
+ }
4614
+ const anchor = findGitToplevel(realPath) ?? realPath;
4615
+ const ws = resolveAutoWorkspace(anchor, cfg.autoWorkspaceMinRepos ?? DEFAULT_AUTO_WORKSPACE_MIN_REPOS);
4616
+ return ws ? ws.id : null;
4617
+ }
4596
4618
  function doResolve(realPath) {
4597
4619
  const cfg = loadProjectsConfig();
4598
4620
  if (matchesIgnore(realPath, cfg)) {
@@ -4869,7 +4891,7 @@ function globToRegex(glob) {
4869
4891
  function escapeRe(s) {
4870
4892
  return s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
4871
4893
  }
4872
- var DEFAULT_AUTO_WORKSPACE_MIN_REPOS, BOT_WORKTREE_MARKERS, resolveCache, gitCache, workspaceCache, cachedConfig, GIT_TIMEOUT_MS, AUTO_WORKSPACE_MAX_DEPTH;
4894
+ var DEFAULT_AUTO_WORKSPACE_MIN_REPOS, BOT_WORKTREE_MARKERS, resolveCache, gitCache, workspaceCache, wsIdCache, cachedConfig, GIT_TIMEOUT_MS, AUTO_WORKSPACE_MAX_DEPTH;
4873
4895
  var init_project_resolver = __esm({
4874
4896
  "../engine/dist/core/project-resolver.js"() {
4875
4897
  "use strict";
@@ -4879,6 +4901,7 @@ var init_project_resolver = __esm({
4879
4901
  resolveCache = /* @__PURE__ */ new Map();
4880
4902
  gitCache = /* @__PURE__ */ new Map();
4881
4903
  workspaceCache = /* @__PURE__ */ new Map();
4904
+ wsIdCache = /* @__PURE__ */ new Map();
4882
4905
  cachedConfig = null;
4883
4906
  GIT_TIMEOUT_MS = 1e4;
4884
4907
  AUTO_WORKSPACE_MAX_DEPTH = 2;
@@ -5568,591 +5591,262 @@ var init_settings = __esm({
5568
5591
  }
5569
5592
  });
5570
5593
 
5571
- // ../engine/dist/core/home-approval.js
5572
- import { existsSync as existsSync4, readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
5573
- import { homedir as homedir2 } from "os";
5574
- import { join as join5, dirname as dirname5 } from "path";
5575
- function normalizeHomePath(p) {
5576
- const expanded = p.startsWith("~") ? join5(homedir2(), p.slice(1)) : p;
5577
- const trimmed = expanded.replace(/[/\\]+$/, "");
5578
- try {
5579
- return realpathSync2(trimmed);
5580
- } catch {
5581
- return trimmed;
5582
- }
5594
+ // ../engine/dist/core/secret-redactor.js
5595
+ import { statSync as statSync5 } from "fs";
5596
+ function secretOf(rule, m) {
5597
+ const raw = rule.secret != null ? m[rule.secret] : m[0];
5598
+ if (raw == null)
5599
+ return null;
5600
+ if (rule.where && !rule.where(raw))
5601
+ return null;
5602
+ return raw;
5583
5603
  }
5584
- function legacyAutoSyncedHomes() {
5585
- const home = homedir2();
5586
- const out = [];
5587
- const add = (dir, marker) => {
5588
- if (!existsSync4(join5(dir, marker)))
5589
- return;
5590
- const n = normalizeHomePath(dir);
5591
- if (!out.includes(n))
5592
- out.push(n);
5593
- };
5594
- const bases = [
5595
- { base: join5(home, ".claude"), marker: "projects" },
5596
- { base: join5(home, ".gemini"), marker: "tmp" },
5597
- { base: join5(home, ".codex"), marker: "sessions" },
5598
- { base: join5(home, ".gemini", "antigravity-cli"), marker: "brain" },
5599
- { base: join5(home, ".local", "share", "opencode"), marker: "opencode.db" },
5600
- { base: join5(home, ".cursor"), marker: "chats" }
5601
- ];
5602
- for (const { base, marker } of bases) {
5603
- add(base, marker);
5604
- const parent = dirname5(base);
5605
- const leaf = base.slice(parent.length + 1);
5606
- try {
5607
- for (const e of readdirSync3(parent, { withFileTypes: true })) {
5608
- if (!e.isDirectory())
5609
- continue;
5610
- if (e.name === leaf || !e.name.startsWith(leaf + "-"))
5611
- continue;
5612
- if (e.name === ".claude-code")
5613
- continue;
5614
- add(join5(parent, e.name), marker);
5604
+ function looksLikeSecretToken(t) {
5605
+ if (t.length < 32)
5606
+ return false;
5607
+ if (/^[a-f0-9]+$/i.test(t))
5608
+ return true;
5609
+ return /[A-Z]/.test(t) && /[a-z]/.test(t) && /[0-9]/.test(t);
5610
+ }
5611
+ function findContextualSecrets(text) {
5612
+ if (!text)
5613
+ return [];
5614
+ const ranges = [];
5615
+ let m;
5616
+ SECRET_CONTEXT_RE.lastIndex = 0;
5617
+ while ((m = SECRET_CONTEXT_RE.exec(text)) !== null) {
5618
+ const winStart = m.index + m[0].length;
5619
+ const window = text.slice(winStart, winStart + 80);
5620
+ HIGH_ENTROPY_TOKEN_RE.lastIndex = 0;
5621
+ let tm;
5622
+ while ((tm = HIGH_ENTROPY_TOKEN_RE.exec(window)) !== null) {
5623
+ if (!looksLikeSecretToken(tm[0]))
5624
+ continue;
5625
+ if (/^[a-f0-9]+$/i.test(tm[0]) && /\b(?:commit|sha-?\d*|hash|digest|checksum|uuid|guid|etag|blob|ref)\b/i.test(window.slice(0, tm.index))) {
5626
+ continue;
5615
5627
  }
5616
- } catch {
5628
+ ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
5629
+ break;
5630
+ }
5631
+ }
5632
+ ranges.sort((a, b) => a.start - b.start);
5633
+ const out = [];
5634
+ for (const r of ranges) {
5635
+ const last = out[out.length - 1];
5636
+ if (last && r.start < last.end) {
5637
+ last.end = Math.max(last.end, r.end);
5638
+ continue;
5617
5639
  }
5640
+ out.push({ ...r });
5618
5641
  }
5619
5642
  return out;
5620
5643
  }
5621
- function grandfatherLegacyHomes() {
5622
- let s;
5644
+ function isEnvRedactionRequested() {
5645
+ const v = process.env.CHAT_RECALL_REDACT_INDEX;
5646
+ if (v === void 0)
5647
+ return null;
5648
+ return v === "1" || v === "true" || v === "yes";
5649
+ }
5650
+ function settingsView() {
5651
+ let mtimeMs = -1;
5623
5652
  try {
5624
- s = loadSettings();
5653
+ mtimeMs = statSync5(settingsFilePath()).mtimeMs;
5625
5654
  } catch {
5626
- return { seeded: [] };
5627
- }
5628
- if (s.sources.homesGrandfathered)
5629
- return { seeded: [] };
5630
- const legacy = legacyAutoSyncedHomes();
5631
- const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
5632
- const seeded = [];
5633
- for (const p of legacy) {
5634
- if (!approved.has(p)) {
5635
- approved.add(p);
5636
- seeded.push(p);
5637
- }
5638
5655
  }
5639
- s.sources.approvedHomes = [...approved];
5640
- s.sources.homesGrandfathered = true;
5656
+ if (cache && cache.mtimeMs === mtimeMs)
5657
+ return cache;
5658
+ let enabled = false;
5659
+ const extra = [];
5641
5660
  try {
5642
- saveSettings(s);
5661
+ const s = loadSettings();
5662
+ enabled = !!s.privacy?.redactIndex;
5663
+ for (const r of s.privacy?.redactionRules ?? []) {
5664
+ try {
5665
+ extra.push({ label: r.label, pattern: new RegExp(r.pattern, "g") });
5666
+ } catch {
5667
+ }
5668
+ }
5643
5669
  } catch {
5644
- return { seeded: [] };
5645
5670
  }
5646
- return { seeded };
5671
+ cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
5672
+ return cache;
5647
5673
  }
5648
- function primaryHomes() {
5649
- const home = homedir2();
5650
- const env = process.env;
5651
- let s;
5674
+ function compileServerRule(spec) {
5675
+ const name = spec.name || "(unnamed)";
5676
+ if (!spec.regex)
5677
+ return { name, reason: "empty pattern" };
5678
+ if (spec.regex.length > MAX_PATTERN_LEN)
5679
+ return { name, reason: `pattern longer than ${MAX_PATTERN_LEN} chars` };
5680
+ let re;
5652
5681
  try {
5653
- s = loadSettings().sources;
5654
- } catch {
5655
- s = void 0;
5682
+ const flags = "g" + (spec.flags || "").replace(/[^ims]/g, "");
5683
+ re = new RegExp(spec.regex, flags);
5684
+ } catch (e) {
5685
+ return { name, reason: `invalid regex: ${e instanceof Error ? e.message : e}` };
5656
5686
  }
5657
- const opencodeDb = env.CHAT_RECALL_OPENCODE_DB || s?.opencodeDbPath || join5(home, ".local", "share", "opencode", "opencode.db");
5658
- const candidates2 = [
5659
- env.CHAT_RECALL_CLAUDE_HOME || s?.claudeHome || join5(home, ".claude"),
5660
- env.CHAT_RECALL_GEMINI_HOME || s?.geminiHome || join5(home, ".gemini"),
5661
- env.CHAT_RECALL_CODEX_HOME || s?.codexHome || join5(home, ".codex"),
5662
- env.CHAT_RECALL_AGY_HOME || s?.agyHome || join5(home, ".gemini", "antigravity-cli"),
5663
- env.CHAT_RECALL_CURSOR_HOME || s?.cursorHome || join5(home, ".cursor"),
5664
- dirOf(opencodeDb)
5665
- ];
5666
- return candidates2.map(normalizeHomePath);
5687
+ if (re.test(""))
5688
+ return { name, reason: "matches the empty string (would redact everything)" };
5689
+ re.lastIndex = 0;
5690
+ if (re.test(CANARY))
5691
+ return { name, reason: "matches benign text \u2014 too broad" };
5692
+ re.lastIndex = 0;
5693
+ return { label: `${spec.source === "pack" ? "pack" : "tenant"}:${name}`, pattern: re };
5667
5694
  }
5668
- function homeDecision(path) {
5669
- const p = normalizeHomePath(path);
5670
- if (primaryHomes().includes(p))
5671
- return "primary";
5672
- let s;
5673
- try {
5674
- s = loadSettings().sources;
5675
- } catch {
5676
- return "pending";
5695
+ function installServerRulePack(pack) {
5696
+ const rejected = [];
5697
+ const rules = [];
5698
+ for (const spec of pack.rules || []) {
5699
+ if (rules.length >= MAX_PACK_RULES) {
5700
+ rejected.push({ name: spec.name || "(unnamed)", reason: `pack exceeds ${MAX_PACK_RULES} rules` });
5701
+ continue;
5702
+ }
5703
+ const out = compileServerRule(spec);
5704
+ if ("reason" in out)
5705
+ rejected.push(out);
5706
+ else
5707
+ rules.push(out);
5677
5708
  }
5678
- if ((s.declinedHomes ?? []).map(normalizeHomePath).includes(p))
5679
- return "declined";
5680
- if ((s.approvedHomes ?? []).map(normalizeHomePath).includes(p))
5681
- return "approved";
5682
- if ((s.extraClaudeHomes ?? []).map(normalizeHomePath).includes(p))
5683
- return "approved";
5684
- return "pending";
5685
- }
5686
- function isHomeSynced(path) {
5687
- const d = homeDecision(path);
5688
- return d === "primary" || d === "approved";
5709
+ const version2 = pack.version || "";
5710
+ serverPack = { version: version2, rules };
5711
+ return { version: version2, accepted: rules.length, rejected };
5689
5712
  }
5690
- function setDecision(path, decision) {
5691
- const p = normalizeHomePath(path);
5692
- let s;
5693
- try {
5694
- s = loadSettings();
5695
- } catch {
5696
- return false;
5697
- }
5698
- const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
5699
- const declined = new Set((s.sources.declinedHomes ?? []).map(normalizeHomePath));
5700
- approved.delete(p);
5701
- declined.delete(p);
5702
- (decision === "approved" ? approved : declined).add(p);
5703
- s.sources.approvedHomes = [...approved];
5704
- s.sources.declinedHomes = [...declined];
5705
- try {
5706
- saveSettings(s);
5707
- } catch {
5708
- return false;
5709
- }
5710
- return true;
5713
+ function serverRulePackVersion() {
5714
+ return serverPack ? serverPack.version || "unversioned" : null;
5711
5715
  }
5712
- function approveHome(path) {
5713
- return setDecision(path, "approved");
5716
+ function activeRules() {
5717
+ const base = settingsView().rules;
5718
+ return serverPack && serverPack.rules.length > 0 ? [...base, ...serverPack.rules] : base;
5714
5719
  }
5715
- function declineHome(path) {
5716
- return setDecision(path, "declined");
5720
+ function isRedactionEnabled() {
5721
+ const env = isEnvRedactionRequested();
5722
+ if (env !== null)
5723
+ return env;
5724
+ return settingsView().enabled;
5717
5725
  }
5718
- var dirOf;
5719
- var init_home_approval = __esm({
5720
- "../engine/dist/core/home-approval.js"() {
5721
- "use strict";
5722
- init_settings();
5723
- dirOf = (filePath) => dirname5(filePath);
5724
- }
5725
- });
5726
-
5727
- // ../engine/dist/core/tool-paths.js
5728
- import { existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
5729
- import { homedir as homedir3 } from "os";
5730
- import { join as join6, dirname as dirname6, basename as basename2 } from "path";
5731
- function claudeHomeDir() {
5732
- return process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome || join6(homedir3(), ".claude");
5733
- }
5734
- function claudeProjectDirs(opts = {}) {
5735
- const home = homedir3();
5736
- const dirs = [];
5737
- const addHome = (d) => {
5738
- const p = join6(d, "projects");
5739
- if (existsSync5(p) && !dirs.includes(p))
5740
- dirs.push(p);
5741
- };
5742
- addHome(claudeHomeDir());
5743
- const homeOverridden = !!(process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome);
5744
- if (!homeOverridden) {
5745
- try {
5746
- for (const entry of readdirSync4(home, { withFileTypes: true })) {
5747
- if (entry.isDirectory() && entry.name.startsWith(".claude-") && entry.name !== ".claude-code") {
5748
- addHome(join6(home, entry.name));
5749
- }
5750
- }
5751
- } catch {
5752
- }
5726
+ function redactSecrets(text, opts = {}) {
5727
+ if (!opts.force && !isRedactionEnabled())
5728
+ return text;
5729
+ if (!text)
5730
+ return text;
5731
+ const rules = opts.rules || activeRules();
5732
+ let out = text;
5733
+ for (const r of rules) {
5734
+ out = out.replace(r.pattern, (...args) => {
5735
+ const whole = args[0];
5736
+ const m = args.slice(0, args.length);
5737
+ const secret = secretOf(r, m);
5738
+ if (secret == null)
5739
+ return whole;
5740
+ if (opts.count)
5741
+ opts.count.redactions++;
5742
+ return whole.slice(0, whole.length - secret.length) + `[REDACTED:${r.label}]`;
5743
+ });
5753
5744
  }
5754
- if (process.env.CLAUDE_DIRS) {
5755
- for (const d of process.env.CLAUDE_DIRS.split(",")) {
5756
- const t = d.trim();
5757
- if (t)
5758
- addHome(t.startsWith("~/") ? join6(home, t.slice(2)) : t);
5759
- }
5745
+ const ctx = findContextualSecrets(out);
5746
+ for (let i = ctx.length - 1; i >= 0; i--) {
5747
+ const { start, end } = ctx[i];
5748
+ if (opts.count)
5749
+ opts.count.redactions++;
5750
+ out = out.slice(0, start) + "[REDACTED:secret-context]" + out.slice(end);
5760
5751
  }
5761
- if (dirs.length === 0)
5762
- dirs.push(join6(claudeHomeDir(), "projects"));
5763
- return applyDecisions(dirs, opts.includeExcluded);
5764
- }
5765
- function _setSourceExclusionFilter(fn) {
5766
- _sourceExclusionFilter = fn;
5767
- }
5768
- function geminiHomeDir() {
5769
- return process.env.CHAT_RECALL_GEMINI_HOME || sources().geminiHome || join6(homedir3(), ".gemini");
5770
- }
5771
- function opencodeDbPath() {
5772
- return process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath || join6(homedir3(), ".local", "share", "opencode", "opencode.db");
5773
- }
5774
- function codexHomeDir() {
5775
- return process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome || join6(homedir3(), ".codex");
5776
- }
5777
- function agyHomeDir() {
5778
- return process.env.CHAT_RECALL_AGY_HOME || sources().agyHome || join6(homedir3(), ".gemini", "antigravity-cli");
5752
+ return out;
5779
5753
  }
5780
- function cursorHomeDir() {
5781
- return process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome || join6(homedir3(), ".cursor");
5754
+ function maskSecret(s) {
5755
+ if (!s)
5756
+ return "";
5757
+ if (s.length <= 4)
5758
+ return "*".repeat(s.length);
5759
+ return "*".repeat(Math.min(s.length - 4, 60)) + s.slice(-4);
5782
5760
  }
5783
- function cursorIdeHomeDir() {
5784
- return process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome || (process.platform === "darwin" ? join6(homedir3(), "Library", "Application Support", "Cursor") : join6(homedir3(), ".config", "Cursor"));
5761
+ function globalRegexFor(pattern) {
5762
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
5763
+ const key2 = pattern.source + "\0" + flags;
5764
+ let re = globalRegexCache.get(key2);
5765
+ if (!re) {
5766
+ re = new RegExp(pattern.source, flags);
5767
+ globalRegexCache.set(key2, re);
5768
+ }
5769
+ return re;
5785
5770
  }
5786
- function siblingHomes(base, overridden, suffix) {
5787
- const roots = [];
5788
- const add = (home) => {
5789
- const root = suffix ? join6(home, suffix) : home;
5790
- if (existsSync5(root) && !roots.includes(root))
5791
- roots.push(root);
5792
- };
5793
- add(base);
5794
- if (!overridden) {
5795
- const parent = dirname6(base);
5796
- const leaf = basename2(base);
5797
- try {
5798
- for (const entry of readdirSync4(parent, { withFileTypes: true })) {
5799
- if (!entry.isDirectory())
5800
- continue;
5801
- if (entry.name === leaf || !entry.name.startsWith(leaf + "-"))
5802
- continue;
5803
- add(join6(parent, entry.name));
5804
- }
5805
- } catch {
5771
+ function makeLineResolver(text) {
5772
+ const starts = [];
5773
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1))
5774
+ starts.push(i);
5775
+ return (index) => {
5776
+ let lo = 0, hi = starts.length;
5777
+ while (lo < hi) {
5778
+ const mid = lo + hi >> 1;
5779
+ if (starts[mid] < index)
5780
+ lo = mid + 1;
5781
+ else
5782
+ hi = mid;
5806
5783
  }
5807
- }
5808
- return roots;
5784
+ return lo + 1;
5785
+ };
5809
5786
  }
5810
- function applyDecisions(roots, includeExcluded) {
5811
- if (roots.length === 0)
5812
- return roots;
5813
- let kept = roots;
5814
- if (!includeExcluded) {
5815
- try {
5816
- grandfatherLegacyHomes();
5817
- const approved = kept.filter((r) => isHomeSynced(dirname6(r)));
5818
- if (approved.length > 0)
5819
- kept = approved;
5820
- } catch {
5787
+ function scanTextForFindings(text, opts = {}) {
5788
+ if (!text)
5789
+ return [];
5790
+ const rules = opts.rules || activeRules();
5791
+ const findings = [];
5792
+ const seenSpans = /* @__PURE__ */ new Set();
5793
+ const lineAt = makeLineResolver(text);
5794
+ for (const r of rules) {
5795
+ const re = globalRegexFor(r.pattern);
5796
+ re.lastIndex = 0;
5797
+ let m;
5798
+ while ((m = re.exec(text)) !== null) {
5799
+ const secret = secretOf(r, m);
5800
+ if (secret == null) {
5801
+ if (re.lastIndex === m.index)
5802
+ re.lastIndex++;
5803
+ continue;
5804
+ }
5805
+ const at = m.index + (m[0].length - secret.length);
5806
+ const span = `${at}:${secret.length}`;
5807
+ if (!seenSpans.has(span)) {
5808
+ seenSpans.add(span);
5809
+ findings.push({ rule: r.label, line: lineAt(at), preview: maskSecret(secret) });
5810
+ }
5811
+ if (re.lastIndex === m.index)
5812
+ re.lastIndex++;
5821
5813
  }
5822
5814
  }
5823
- if (!includeExcluded && _sourceExclusionFilter) {
5824
- const notExcluded = kept.filter((r) => !_sourceExclusionFilter(r));
5825
- if (notExcluded.length > 0)
5826
- kept = notExcluded;
5815
+ for (const { start, end } of findContextualSecrets(text)) {
5816
+ const line = lineAt(start);
5817
+ findings.push({ rule: "secret-context", line, preview: maskSecret(text.slice(start, end)) });
5827
5818
  }
5828
- return kept;
5829
- }
5830
- function applyExclusions(roots, includeExcluded) {
5831
- return applyDecisions(roots, includeExcluded);
5832
- }
5833
- function codexSessionDirs(opts = {}) {
5834
- const overridden = !!(process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome);
5835
- return applyExclusions(siblingHomes(codexHomeDir(), overridden, "sessions"), opts.includeExcluded);
5836
- }
5837
- function agyBrainDirs(opts = {}) {
5838
- const overridden = !!(process.env.CHAT_RECALL_AGY_HOME || sources().agyHome);
5839
- return applyExclusions(siblingHomes(agyHomeDir(), overridden, "brain"), opts.includeExcluded);
5840
- }
5841
- function cursorChatDirs(opts = {}) {
5842
- const overridden = !!(process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome);
5843
- return applyExclusions(siblingHomes(cursorHomeDir(), overridden, "chats"), opts.includeExcluded);
5844
- }
5845
- function cursorIdeWorkspaceDirs(opts = {}) {
5846
- const overridden = !!(process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome);
5847
- return applyExclusions(siblingHomes(cursorIdeHomeDir(), overridden, join6("User", "workspaceStorage")), opts.includeExcluded);
5819
+ return findings;
5848
5820
  }
5849
- function cursorIdeGlobalDbs(opts = {}) {
5850
- const overridden = !!(process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome);
5851
- const roots = applyExclusions(siblingHomes(cursorIdeHomeDir(), overridden, join6("User", "globalStorage")), opts.includeExcluded);
5852
- const dbs = [];
5853
- for (const r of roots) {
5854
- const p = join6(r, "state.vscdb");
5855
- if (existsSync5(p) && !dbs.includes(p))
5856
- dbs.push(p);
5821
+ function privacyView() {
5822
+ let mtimeMs = -1;
5823
+ try {
5824
+ mtimeMs = statSync5(settingsFilePath()).mtimeMs;
5825
+ } catch {
5857
5826
  }
5858
- return dbs;
5859
- }
5860
- function opencodeDbPaths(opts = {}) {
5861
- const primary = opencodeDbPath();
5862
- const overridden = !!(process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath);
5863
- const dbFile = basename2(primary);
5864
- const dirs = siblingHomes(dirname6(primary), overridden, "");
5865
- const dbs = [];
5866
- for (const d of dirs) {
5867
- const p = join6(d, dbFile);
5868
- if (existsSync5(p) && !dbs.includes(p))
5869
- dbs.push(p);
5827
+ if (privacyCache && privacyCache.mtimeMs === mtimeMs)
5828
+ return privacyCache;
5829
+ let redactToolOutputs = false, redactFilePaths = false;
5830
+ try {
5831
+ const s = loadSettings();
5832
+ redactToolOutputs = !!s.privacy?.redactToolOutputs;
5833
+ redactFilePaths = !!s.privacy?.redactFilePaths;
5834
+ } catch {
5870
5835
  }
5871
- if (dbs.length === 0)
5872
- dbs.push(primary);
5873
- return applyExclusions(dbs, opts.includeExcluded);
5836
+ privacyCache = { mtimeMs, redactToolOutputs, redactFilePaths };
5837
+ return privacyCache;
5874
5838
  }
5875
- function extraClaudeHomeDirs() {
5876
- const extras = sources().extraClaudeHomes ?? [];
5877
- const primary = claudeHomeDir();
5878
- return Array.from(new Set(extras)).filter((p) => p && p !== primary);
5839
+ function isToolOutputRedactionEnabled() {
5840
+ return privacyView().redactToolOutputs;
5879
5841
  }
5880
- function claudeHomeDirs() {
5881
- const overridden = !!(process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome);
5882
- const homes = siblingHomes(claudeHomeDir(), overridden, "").filter((h) => basename2(h) !== ".claude-code");
5883
- for (const extra of extraClaudeHomeDirs()) {
5884
- if (existsSync5(extra) && !homes.includes(extra))
5885
- homes.push(extra);
5886
- }
5887
- return homes;
5842
+ function redactToolResultBody(toolName, body) {
5843
+ if (!body)
5844
+ return body;
5845
+ return `[redacted: ${toolName} output, ${body.length} chars]`;
5888
5846
  }
5889
- var sources, _sourceExclusionFilter;
5890
- var init_tool_paths = __esm({
5891
- "../engine/dist/core/tool-paths.js"() {
5892
- "use strict";
5893
- init_home_approval();
5894
- init_settings();
5895
- sources = loadSourceSettings;
5896
- _sourceExclusionFilter = null;
5897
- }
5898
- });
5899
-
5900
- // ../engine/dist/core/secret-redactor.js
5901
- import { statSync as statSync5 } from "fs";
5902
- function secretOf(rule, m) {
5903
- const raw = rule.secret != null ? m[rule.secret] : m[0];
5904
- if (raw == null)
5905
- return null;
5906
- if (rule.where && !rule.where(raw))
5907
- return null;
5908
- return raw;
5909
- }
5910
- function looksLikeSecretToken(t) {
5911
- if (t.length < 32)
5912
- return false;
5913
- if (/^[a-f0-9]+$/i.test(t))
5914
- return true;
5915
- return /[A-Z]/.test(t) && /[a-z]/.test(t) && /[0-9]/.test(t);
5916
- }
5917
- function findContextualSecrets(text) {
5918
- if (!text)
5919
- return [];
5920
- const ranges = [];
5921
- let m;
5922
- SECRET_CONTEXT_RE.lastIndex = 0;
5923
- while ((m = SECRET_CONTEXT_RE.exec(text)) !== null) {
5924
- const winStart = m.index + m[0].length;
5925
- const window = text.slice(winStart, winStart + 80);
5926
- HIGH_ENTROPY_TOKEN_RE.lastIndex = 0;
5927
- let tm;
5928
- while ((tm = HIGH_ENTROPY_TOKEN_RE.exec(window)) !== null) {
5929
- if (!looksLikeSecretToken(tm[0]))
5930
- continue;
5931
- if (/^[a-f0-9]+$/i.test(tm[0]) && /\b(?:commit|sha-?\d*|hash|digest|checksum|uuid|guid|etag|blob|ref)\b/i.test(window.slice(0, tm.index))) {
5932
- continue;
5933
- }
5934
- ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
5935
- break;
5936
- }
5937
- }
5938
- ranges.sort((a, b) => a.start - b.start);
5939
- const out = [];
5940
- for (const r of ranges) {
5941
- const last = out[out.length - 1];
5942
- if (last && r.start < last.end) {
5943
- last.end = Math.max(last.end, r.end);
5944
- continue;
5945
- }
5946
- out.push({ ...r });
5947
- }
5948
- return out;
5949
- }
5950
- function isEnvRedactionRequested() {
5951
- const v = process.env.CHAT_RECALL_REDACT_INDEX;
5952
- if (v === void 0)
5953
- return null;
5954
- return v === "1" || v === "true" || v === "yes";
5955
- }
5956
- function settingsView() {
5957
- let mtimeMs = -1;
5958
- try {
5959
- mtimeMs = statSync5(settingsFilePath()).mtimeMs;
5960
- } catch {
5961
- }
5962
- if (cache && cache.mtimeMs === mtimeMs)
5963
- return cache;
5964
- let enabled = false;
5965
- const extra = [];
5966
- try {
5967
- const s = loadSettings();
5968
- enabled = !!s.privacy?.redactIndex;
5969
- for (const r of s.privacy?.redactionRules ?? []) {
5970
- try {
5971
- extra.push({ label: r.label, pattern: new RegExp(r.pattern, "g") });
5972
- } catch {
5973
- }
5974
- }
5975
- } catch {
5976
- }
5977
- cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
5978
- return cache;
5979
- }
5980
- function compileServerRule(spec) {
5981
- const name = spec.name || "(unnamed)";
5982
- if (!spec.regex)
5983
- return { name, reason: "empty pattern" };
5984
- if (spec.regex.length > MAX_PATTERN_LEN)
5985
- return { name, reason: `pattern longer than ${MAX_PATTERN_LEN} chars` };
5986
- let re;
5987
- try {
5988
- const flags = "g" + (spec.flags || "").replace(/[^ims]/g, "");
5989
- re = new RegExp(spec.regex, flags);
5990
- } catch (e) {
5991
- return { name, reason: `invalid regex: ${e instanceof Error ? e.message : e}` };
5992
- }
5993
- if (re.test(""))
5994
- return { name, reason: "matches the empty string (would redact everything)" };
5995
- re.lastIndex = 0;
5996
- if (re.test(CANARY))
5997
- return { name, reason: "matches benign text \u2014 too broad" };
5998
- re.lastIndex = 0;
5999
- return { label: `${spec.source === "pack" ? "pack" : "tenant"}:${name}`, pattern: re };
6000
- }
6001
- function installServerRulePack(pack) {
6002
- const rejected = [];
6003
- const rules = [];
6004
- for (const spec of pack.rules || []) {
6005
- if (rules.length >= MAX_PACK_RULES) {
6006
- rejected.push({ name: spec.name || "(unnamed)", reason: `pack exceeds ${MAX_PACK_RULES} rules` });
6007
- continue;
6008
- }
6009
- const out = compileServerRule(spec);
6010
- if ("reason" in out)
6011
- rejected.push(out);
6012
- else
6013
- rules.push(out);
6014
- }
6015
- const version2 = pack.version || "";
6016
- serverPack = { version: version2, rules };
6017
- return { version: version2, accepted: rules.length, rejected };
6018
- }
6019
- function serverRulePackVersion() {
6020
- return serverPack ? serverPack.version || "unversioned" : null;
6021
- }
6022
- function activeRules() {
6023
- const base = settingsView().rules;
6024
- return serverPack && serverPack.rules.length > 0 ? [...base, ...serverPack.rules] : base;
6025
- }
6026
- function isRedactionEnabled() {
6027
- const env = isEnvRedactionRequested();
6028
- if (env !== null)
6029
- return env;
6030
- return settingsView().enabled;
6031
- }
6032
- function redactSecrets(text, opts = {}) {
6033
- if (!opts.force && !isRedactionEnabled())
6034
- return text;
6035
- if (!text)
6036
- return text;
6037
- const rules = opts.rules || activeRules();
6038
- let out = text;
6039
- for (const r of rules) {
6040
- out = out.replace(r.pattern, (...args) => {
6041
- const whole = args[0];
6042
- const m = args.slice(0, args.length);
6043
- const secret = secretOf(r, m);
6044
- if (secret == null)
6045
- return whole;
6046
- if (opts.count)
6047
- opts.count.redactions++;
6048
- return whole.slice(0, whole.length - secret.length) + `[REDACTED:${r.label}]`;
6049
- });
6050
- }
6051
- const ctx = findContextualSecrets(out);
6052
- for (let i = ctx.length - 1; i >= 0; i--) {
6053
- const { start, end } = ctx[i];
6054
- if (opts.count)
6055
- opts.count.redactions++;
6056
- out = out.slice(0, start) + "[REDACTED:secret-context]" + out.slice(end);
6057
- }
6058
- return out;
6059
- }
6060
- function maskSecret(s) {
6061
- if (!s)
6062
- return "";
6063
- if (s.length <= 4)
6064
- return "*".repeat(s.length);
6065
- return "*".repeat(Math.min(s.length - 4, 60)) + s.slice(-4);
6066
- }
6067
- function globalRegexFor(pattern) {
6068
- const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
6069
- const key2 = pattern.source + "\0" + flags;
6070
- let re = globalRegexCache.get(key2);
6071
- if (!re) {
6072
- re = new RegExp(pattern.source, flags);
6073
- globalRegexCache.set(key2, re);
6074
- }
6075
- return re;
6076
- }
6077
- function makeLineResolver(text) {
6078
- const starts = [];
6079
- for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1))
6080
- starts.push(i);
6081
- return (index) => {
6082
- let lo = 0, hi = starts.length;
6083
- while (lo < hi) {
6084
- const mid = lo + hi >> 1;
6085
- if (starts[mid] < index)
6086
- lo = mid + 1;
6087
- else
6088
- hi = mid;
6089
- }
6090
- return lo + 1;
6091
- };
6092
- }
6093
- function scanTextForFindings(text, opts = {}) {
6094
- if (!text)
6095
- return [];
6096
- const rules = opts.rules || activeRules();
6097
- const findings = [];
6098
- const seenSpans = /* @__PURE__ */ new Set();
6099
- const lineAt = makeLineResolver(text);
6100
- for (const r of rules) {
6101
- const re = globalRegexFor(r.pattern);
6102
- re.lastIndex = 0;
6103
- let m;
6104
- while ((m = re.exec(text)) !== null) {
6105
- const secret = secretOf(r, m);
6106
- if (secret == null) {
6107
- if (re.lastIndex === m.index)
6108
- re.lastIndex++;
6109
- continue;
6110
- }
6111
- const at = m.index + (m[0].length - secret.length);
6112
- const span = `${at}:${secret.length}`;
6113
- if (!seenSpans.has(span)) {
6114
- seenSpans.add(span);
6115
- findings.push({ rule: r.label, line: lineAt(at), preview: maskSecret(secret) });
6116
- }
6117
- if (re.lastIndex === m.index)
6118
- re.lastIndex++;
6119
- }
6120
- }
6121
- for (const { start, end } of findContextualSecrets(text)) {
6122
- const line = lineAt(start);
6123
- findings.push({ rule: "secret-context", line, preview: maskSecret(text.slice(start, end)) });
6124
- }
6125
- return findings;
6126
- }
6127
- function privacyView() {
6128
- let mtimeMs = -1;
6129
- try {
6130
- mtimeMs = statSync5(settingsFilePath()).mtimeMs;
6131
- } catch {
6132
- }
6133
- if (privacyCache && privacyCache.mtimeMs === mtimeMs)
6134
- return privacyCache;
6135
- let redactToolOutputs = false, redactFilePaths = false;
6136
- try {
6137
- const s = loadSettings();
6138
- redactToolOutputs = !!s.privacy?.redactToolOutputs;
6139
- redactFilePaths = !!s.privacy?.redactFilePaths;
6140
- } catch {
6141
- }
6142
- privacyCache = { mtimeMs, redactToolOutputs, redactFilePaths };
6143
- return privacyCache;
6144
- }
6145
- function isToolOutputRedactionEnabled() {
6146
- return privacyView().redactToolOutputs;
6147
- }
6148
- function redactToolResultBody(toolName, body) {
6149
- if (!body)
6150
- return body;
6151
- return `[redacted: ${toolName} output, ${body.length} chars]`;
6152
- }
6153
- var DEFAULT_REDACTION_RULES, SECRET_CONTEXT_RE, HIGH_ENTROPY_TOKEN_RE, cache, CANARY, MAX_PACK_RULES, MAX_PATTERN_LEN, serverPack, globalRegexCache, privacyCache;
6154
- var init_secret_redactor = __esm({
6155
- "../engine/dist/core/secret-redactor.js"() {
5847
+ var DEFAULT_REDACTION_RULES, SECRET_CONTEXT_RE, HIGH_ENTROPY_TOKEN_RE, cache, CANARY, MAX_PACK_RULES, MAX_PATTERN_LEN, serverPack, globalRegexCache, privacyCache;
5848
+ var init_secret_redactor = __esm({
5849
+ "../engine/dist/core/secret-redactor.js"() {
6156
5850
  "use strict";
6157
5851
  init_settings();
6158
5852
  DEFAULT_REDACTION_RULES = [
@@ -6302,164 +5996,493 @@ var init_secret_redactor = __esm({
6302
5996
  globalRegexCache = /* @__PURE__ */ new Map();
6303
5997
  privacyCache = null;
6304
5998
  }
6305
- });
6306
-
6307
- // ../engine/dist/parsers/chunker.js
6308
- function stripInjectedBanners(text) {
6309
- let result = text;
6310
- for (const re of INJECTED_BANNERS)
6311
- result = result.replace(re, " ");
6312
- return result;
5999
+ });
6000
+
6001
+ // ../engine/dist/parsers/chunker.js
6002
+ function stripInjectedBanners(text) {
6003
+ let result = text;
6004
+ for (const re of INJECTED_BANNERS)
6005
+ result = result.replace(re, " ");
6006
+ return result;
6007
+ }
6008
+ function cleanText(text) {
6009
+ let result = text.replace(/<system-reminder>.*?<\/system-reminder>/gs, "");
6010
+ result = stripInjectedBanners(result);
6011
+ result = result.replace(/\x1b\[[0-9;]*m/g, "");
6012
+ result = result.replace(/\s+/g, " ");
6013
+ return result.trim();
6014
+ }
6015
+ function windowText(text, maxChars, maxWindows = 12) {
6016
+ const clean = cleanText(text);
6017
+ if (clean.length <= maxChars)
6018
+ return clean ? [clean] : [];
6019
+ const out = [];
6020
+ const overlap = 80;
6021
+ const step = Math.max(1, maxChars - overlap);
6022
+ for (let i = 0; i < clean.length && out.length < maxWindows; i += step) {
6023
+ out.push(clean.slice(i, i + maxChars));
6024
+ }
6025
+ return out;
6026
+ }
6027
+ function chunkSession(entry, content, maxChunks = 80, maxTokensPerChunk = 500) {
6028
+ const chunks = [];
6029
+ const maxChars = maxTokensPerChunk * 4;
6030
+ const baseData = {
6031
+ sessionId: entry.sessionId,
6032
+ projectPath: entry.projectPath,
6033
+ created: entry.created,
6034
+ modified: entry.modified,
6035
+ mtime: entry.fileMtime,
6036
+ firstPrompt: entry.firstPrompt || content.firstPrompt,
6037
+ sourceFile: content.sessionPath
6038
+ };
6039
+ for (let i = 0; i < Math.min(2, content.summaries.length); i++) {
6040
+ const summary = content.summaries[i];
6041
+ if (!summary?.trim())
6042
+ continue;
6043
+ const chunkText = cleanText(summary.slice(0, maxChars));
6044
+ if (chunkText.length > 8e3) {
6045
+ chunks.push({
6046
+ chunkId: `${entry.sessionId}_summary_${i}`,
6047
+ chunkType: "summary",
6048
+ text: chunkText.slice(0, 8e3),
6049
+ sourceLine: 0,
6050
+ ...baseData
6051
+ });
6052
+ } else {
6053
+ chunks.push({
6054
+ chunkId: `${entry.sessionId}_summary_${i}`,
6055
+ chunkType: "summary",
6056
+ text: chunkText,
6057
+ sourceLine: 0,
6058
+ ...baseData
6059
+ });
6060
+ }
6061
+ }
6062
+ if (chunks.length >= maxChunks) {
6063
+ return chunks.slice(0, maxChunks);
6064
+ }
6065
+ const firstPrompt = entry.firstPrompt || content.firstPrompt;
6066
+ if (firstPrompt?.trim()) {
6067
+ const firstLine = content.userMessages[0]?.lineNumber || 0;
6068
+ chunks.push({
6069
+ chunkId: `${entry.sessionId}_first_prompt`,
6070
+ chunkType: "first_prompt",
6071
+ text: cleanText(firstPrompt.slice(0, maxChars)),
6072
+ sourceLine: firstLine,
6073
+ ...baseData
6074
+ });
6075
+ }
6076
+ if (chunks.length >= maxChunks) {
6077
+ return chunks.slice(0, maxChunks);
6078
+ }
6079
+ const userStart = firstPrompt ? 1 : 0;
6080
+ for (let i = userStart; i < content.userMessages.length; i++) {
6081
+ if (chunks.length >= maxChunks)
6082
+ break;
6083
+ const msg = content.userMessages[i];
6084
+ if (!msg.text.trim())
6085
+ continue;
6086
+ const windows = windowText(msg.text, maxChars);
6087
+ for (let w = 0; w < windows.length; w++) {
6088
+ if (chunks.length >= maxChunks)
6089
+ break;
6090
+ chunks.push({
6091
+ chunkId: `${entry.sessionId}_user_${i}_${w}`,
6092
+ chunkType: "user_context",
6093
+ text: windows[w],
6094
+ sourceLine: msg.lineNumber,
6095
+ ...baseData
6096
+ });
6097
+ }
6098
+ }
6099
+ if (chunks.length >= maxChunks) {
6100
+ return chunks.slice(0, maxChunks);
6101
+ }
6102
+ for (let i = 0; i < content.assistantMessages.length; i++) {
6103
+ if (chunks.length >= maxChunks)
6104
+ break;
6105
+ const msg = content.assistantMessages[i];
6106
+ if (!msg.text.trim() || msg.text.length <= 50)
6107
+ continue;
6108
+ const windows = windowText(msg.text, maxChars);
6109
+ for (let w = 0; w < windows.length; w++) {
6110
+ if (chunks.length >= maxChunks)
6111
+ break;
6112
+ chunks.push({
6113
+ chunkId: `${entry.sessionId}_assistant_${i}_${w}`,
6114
+ chunkType: "assistant",
6115
+ text: windows[w],
6116
+ sourceLine: msg.lineNumber,
6117
+ ...baseData
6118
+ });
6119
+ }
6120
+ }
6121
+ if (chunks.length >= maxChunks) {
6122
+ return chunks.slice(0, maxChunks);
6123
+ }
6124
+ if (content.toolResults.length > 0) {
6125
+ const webSearches = content.toolResults.filter((r) => r.contentType === "web_search");
6126
+ const otherResults = content.toolResults.filter((r) => r.contentType !== "web_search");
6127
+ const resultsToProcess = [...webSearches.slice(0, 5), ...otherResults.slice(0, 10)];
6128
+ const stripBodies = isToolOutputRedactionEnabled();
6129
+ for (let i = 0; i < resultsToProcess.length; i++) {
6130
+ if (chunks.length >= maxChunks)
6131
+ break;
6132
+ const result = resultsToProcess[i];
6133
+ if (result.text.trim()) {
6134
+ const body = stripBodies && result.contentType !== "web_search" ? redactToolResultBody(result.contentType, result.text) : result.text.slice(0, maxChars);
6135
+ chunks.push({
6136
+ chunkId: `${entry.sessionId}_${result.contentType}_${i}`,
6137
+ chunkType: result.contentType,
6138
+ text: cleanText(body),
6139
+ sourceLine: result.lineNumber,
6140
+ ...baseData
6141
+ });
6142
+ }
6143
+ }
6144
+ }
6145
+ return chunks.slice(0, maxChunks);
6146
+ }
6147
+ var INJECTED_BANNERS;
6148
+ var init_chunker = __esm({
6149
+ "../engine/dist/parsers/chunker.js"() {
6150
+ "use strict";
6151
+ init_secret_redactor();
6152
+ INJECTED_BANNERS = [
6153
+ /MCP issues detected\. ?Run \/mcp list for status\.?/g,
6154
+ /Context low[^\n]*Run \/compact[^\n]*/g,
6155
+ /API Error:[^\n]{0,120}/g
6156
+ ];
6157
+ }
6158
+ });
6159
+
6160
+ // ../engine/dist/core/home-approval.js
6161
+ import { existsSync as existsSync4, readdirSync as readdirSync3, realpathSync as realpathSync2 } from "fs";
6162
+ import { homedir as homedir2 } from "os";
6163
+ import { join as join5, dirname as dirname5 } from "path";
6164
+ function normalizeHomePath(p) {
6165
+ const expanded = p.startsWith("~") ? join5(homedir2(), p.slice(1)) : p;
6166
+ const trimmed = expanded.replace(/[/\\]+$/, "");
6167
+ try {
6168
+ return realpathSync2(trimmed);
6169
+ } catch {
6170
+ return trimmed;
6171
+ }
6172
+ }
6173
+ function legacyAutoSyncedHomes() {
6174
+ const home = homedir2();
6175
+ const out = [];
6176
+ const add = (dir, marker) => {
6177
+ if (!existsSync4(join5(dir, marker)))
6178
+ return;
6179
+ const n = normalizeHomePath(dir);
6180
+ if (!out.includes(n))
6181
+ out.push(n);
6182
+ };
6183
+ const bases = [
6184
+ { base: join5(home, ".claude"), marker: "projects" },
6185
+ { base: join5(home, ".gemini"), marker: "tmp" },
6186
+ { base: join5(home, ".codex"), marker: "sessions" },
6187
+ { base: join5(home, ".gemini", "antigravity-cli"), marker: "brain" },
6188
+ { base: join5(home, ".local", "share", "opencode"), marker: "opencode.db" },
6189
+ { base: join5(home, ".cursor"), marker: "chats" }
6190
+ ];
6191
+ for (const { base, marker } of bases) {
6192
+ add(base, marker);
6193
+ const parent = dirname5(base);
6194
+ const leaf = base.slice(parent.length + 1);
6195
+ try {
6196
+ for (const e of readdirSync3(parent, { withFileTypes: true })) {
6197
+ if (!e.isDirectory())
6198
+ continue;
6199
+ if (e.name === leaf || !e.name.startsWith(leaf + "-"))
6200
+ continue;
6201
+ if (e.name === ".claude-code")
6202
+ continue;
6203
+ add(join5(parent, e.name), marker);
6204
+ }
6205
+ } catch {
6206
+ }
6207
+ }
6208
+ return out;
6313
6209
  }
6314
- function cleanText(text) {
6315
- let result = text.replace(/<system-reminder>.*?<\/system-reminder>/gs, "");
6316
- result = stripInjectedBanners(result);
6317
- result = result.replace(/\x1b\[[0-9;]*m/g, "");
6318
- result = result.replace(/\s+/g, " ");
6319
- return result.trim();
6210
+ function grandfatherLegacyHomes() {
6211
+ let s;
6212
+ try {
6213
+ s = loadSettings();
6214
+ } catch {
6215
+ return { seeded: [] };
6216
+ }
6217
+ if (s.sources.homesGrandfathered)
6218
+ return { seeded: [] };
6219
+ const legacy = legacyAutoSyncedHomes();
6220
+ const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
6221
+ const seeded = [];
6222
+ for (const p of legacy) {
6223
+ if (!approved.has(p)) {
6224
+ approved.add(p);
6225
+ seeded.push(p);
6226
+ }
6227
+ }
6228
+ s.sources.approvedHomes = [...approved];
6229
+ s.sources.homesGrandfathered = true;
6230
+ try {
6231
+ saveSettings(s);
6232
+ } catch {
6233
+ return { seeded: [] };
6234
+ }
6235
+ return { seeded };
6320
6236
  }
6321
- function windowText(text, maxChars, maxWindows = 12) {
6322
- const clean = cleanText(text);
6323
- if (clean.length <= maxChars)
6324
- return clean ? [clean] : [];
6325
- const out = [];
6326
- const overlap = 80;
6327
- const step = Math.max(1, maxChars - overlap);
6328
- for (let i = 0; i < clean.length && out.length < maxWindows; i += step) {
6329
- out.push(clean.slice(i, i + maxChars));
6237
+ function primaryHomes() {
6238
+ const home = homedir2();
6239
+ const env = process.env;
6240
+ let s;
6241
+ try {
6242
+ s = loadSettings().sources;
6243
+ } catch {
6244
+ s = void 0;
6330
6245
  }
6331
- return out;
6246
+ const opencodeDb = env.CHAT_RECALL_OPENCODE_DB || s?.opencodeDbPath || join5(home, ".local", "share", "opencode", "opencode.db");
6247
+ const candidates2 = [
6248
+ env.CHAT_RECALL_CLAUDE_HOME || s?.claudeHome || join5(home, ".claude"),
6249
+ env.CHAT_RECALL_GEMINI_HOME || s?.geminiHome || join5(home, ".gemini"),
6250
+ env.CHAT_RECALL_CODEX_HOME || s?.codexHome || join5(home, ".codex"),
6251
+ env.CHAT_RECALL_AGY_HOME || s?.agyHome || join5(home, ".gemini", "antigravity-cli"),
6252
+ env.CHAT_RECALL_CURSOR_HOME || s?.cursorHome || join5(home, ".cursor"),
6253
+ dirOf(opencodeDb)
6254
+ ];
6255
+ return candidates2.map(normalizeHomePath);
6332
6256
  }
6333
- function chunkSession(entry, content, maxChunks = 80, maxTokensPerChunk = 500) {
6334
- const chunks = [];
6335
- const maxChars = maxTokensPerChunk * 4;
6336
- const baseData = {
6337
- sessionId: entry.sessionId,
6338
- projectPath: entry.projectPath,
6339
- created: entry.created,
6340
- modified: entry.modified,
6341
- mtime: entry.fileMtime,
6342
- firstPrompt: entry.firstPrompt || content.firstPrompt,
6343
- sourceFile: content.sessionPath
6257
+ function homeDecision(path) {
6258
+ const p = normalizeHomePath(path);
6259
+ if (primaryHomes().includes(p))
6260
+ return "primary";
6261
+ let s;
6262
+ try {
6263
+ s = loadSettings().sources;
6264
+ } catch {
6265
+ return "pending";
6266
+ }
6267
+ if ((s.declinedHomes ?? []).map(normalizeHomePath).includes(p))
6268
+ return "declined";
6269
+ if ((s.approvedHomes ?? []).map(normalizeHomePath).includes(p))
6270
+ return "approved";
6271
+ if ((s.extraClaudeHomes ?? []).map(normalizeHomePath).includes(p))
6272
+ return "approved";
6273
+ return "pending";
6274
+ }
6275
+ function isHomeSynced(path) {
6276
+ const d = homeDecision(path);
6277
+ return d === "primary" || d === "approved";
6278
+ }
6279
+ function setDecision(path, decision) {
6280
+ const p = normalizeHomePath(path);
6281
+ let s;
6282
+ try {
6283
+ s = loadSettings();
6284
+ } catch {
6285
+ return false;
6286
+ }
6287
+ const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
6288
+ const declined = new Set((s.sources.declinedHomes ?? []).map(normalizeHomePath));
6289
+ approved.delete(p);
6290
+ declined.delete(p);
6291
+ (decision === "approved" ? approved : declined).add(p);
6292
+ s.sources.approvedHomes = [...approved];
6293
+ s.sources.declinedHomes = [...declined];
6294
+ try {
6295
+ saveSettings(s);
6296
+ } catch {
6297
+ return false;
6298
+ }
6299
+ return true;
6300
+ }
6301
+ function approveHome(path) {
6302
+ return setDecision(path, "approved");
6303
+ }
6304
+ function declineHome(path) {
6305
+ return setDecision(path, "declined");
6306
+ }
6307
+ var dirOf;
6308
+ var init_home_approval = __esm({
6309
+ "../engine/dist/core/home-approval.js"() {
6310
+ "use strict";
6311
+ init_settings();
6312
+ dirOf = (filePath) => dirname5(filePath);
6313
+ }
6314
+ });
6315
+
6316
+ // ../engine/dist/core/tool-paths.js
6317
+ import { existsSync as existsSync5, readdirSync as readdirSync4 } from "fs";
6318
+ import { homedir as homedir3 } from "os";
6319
+ import { join as join6, dirname as dirname6, basename as basename2 } from "path";
6320
+ function claudeHomeDir() {
6321
+ return process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome || join6(homedir3(), ".claude");
6322
+ }
6323
+ function claudeProjectDirs(opts = {}) {
6324
+ const home = homedir3();
6325
+ const dirs = [];
6326
+ const addHome = (d) => {
6327
+ const p = join6(d, "projects");
6328
+ if (existsSync5(p) && !dirs.includes(p))
6329
+ dirs.push(p);
6344
6330
  };
6345
- for (let i = 0; i < Math.min(2, content.summaries.length); i++) {
6346
- const summary = content.summaries[i];
6347
- if (!summary?.trim())
6348
- continue;
6349
- const chunkText = cleanText(summary.slice(0, maxChars));
6350
- if (chunkText.length > 8e3) {
6351
- chunks.push({
6352
- chunkId: `${entry.sessionId}_summary_${i}`,
6353
- chunkType: "summary",
6354
- text: chunkText.slice(0, 8e3),
6355
- sourceLine: 0,
6356
- ...baseData
6357
- });
6358
- } else {
6359
- chunks.push({
6360
- chunkId: `${entry.sessionId}_summary_${i}`,
6361
- chunkType: "summary",
6362
- text: chunkText,
6363
- sourceLine: 0,
6364
- ...baseData
6365
- });
6331
+ addHome(claudeHomeDir());
6332
+ const homeOverridden = !!(process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome);
6333
+ if (!homeOverridden) {
6334
+ try {
6335
+ for (const entry of readdirSync4(home, { withFileTypes: true })) {
6336
+ if (entry.isDirectory() && entry.name.startsWith(".claude-") && entry.name !== ".claude-code") {
6337
+ addHome(join6(home, entry.name));
6338
+ }
6339
+ }
6340
+ } catch {
6366
6341
  }
6367
6342
  }
6368
- if (chunks.length >= maxChunks) {
6369
- return chunks.slice(0, maxChunks);
6370
- }
6371
- const firstPrompt = entry.firstPrompt || content.firstPrompt;
6372
- if (firstPrompt?.trim()) {
6373
- const firstLine = content.userMessages[0]?.lineNumber || 0;
6374
- chunks.push({
6375
- chunkId: `${entry.sessionId}_first_prompt`,
6376
- chunkType: "first_prompt",
6377
- text: cleanText(firstPrompt.slice(0, maxChars)),
6378
- sourceLine: firstLine,
6379
- ...baseData
6380
- });
6343
+ if (process.env.CLAUDE_DIRS) {
6344
+ for (const d of process.env.CLAUDE_DIRS.split(",")) {
6345
+ const t = d.trim();
6346
+ if (t)
6347
+ addHome(t.startsWith("~/") ? join6(home, t.slice(2)) : t);
6348
+ }
6381
6349
  }
6382
- if (chunks.length >= maxChunks) {
6383
- return chunks.slice(0, maxChunks);
6350
+ if (dirs.length === 0)
6351
+ dirs.push(join6(claudeHomeDir(), "projects"));
6352
+ return applyDecisions(dirs, opts.includeExcluded);
6353
+ }
6354
+ function _setSourceExclusionFilter(fn) {
6355
+ _sourceExclusionFilter = fn;
6356
+ }
6357
+ function geminiHomeDir() {
6358
+ return process.env.CHAT_RECALL_GEMINI_HOME || sources().geminiHome || join6(homedir3(), ".gemini");
6359
+ }
6360
+ function opencodeDbPath() {
6361
+ return process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath || join6(homedir3(), ".local", "share", "opencode", "opencode.db");
6362
+ }
6363
+ function codexHomeDir() {
6364
+ return process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome || join6(homedir3(), ".codex");
6365
+ }
6366
+ function agyHomeDir() {
6367
+ return process.env.CHAT_RECALL_AGY_HOME || sources().agyHome || join6(homedir3(), ".gemini", "antigravity-cli");
6368
+ }
6369
+ function cursorHomeDir() {
6370
+ return process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome || join6(homedir3(), ".cursor");
6371
+ }
6372
+ function cursorIdeHomeDir() {
6373
+ return process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome || (process.platform === "darwin" ? join6(homedir3(), "Library", "Application Support", "Cursor") : join6(homedir3(), ".config", "Cursor"));
6374
+ }
6375
+ function siblingHomes(base, overridden, suffix) {
6376
+ const roots = [];
6377
+ const add = (home) => {
6378
+ const root = suffix ? join6(home, suffix) : home;
6379
+ if (existsSync5(root) && !roots.includes(root))
6380
+ roots.push(root);
6381
+ };
6382
+ add(base);
6383
+ if (!overridden) {
6384
+ const parent = dirname6(base);
6385
+ const leaf = basename2(base);
6386
+ try {
6387
+ for (const entry of readdirSync4(parent, { withFileTypes: true })) {
6388
+ if (!entry.isDirectory())
6389
+ continue;
6390
+ if (entry.name === leaf || !entry.name.startsWith(leaf + "-"))
6391
+ continue;
6392
+ add(join6(parent, entry.name));
6393
+ }
6394
+ } catch {
6395
+ }
6384
6396
  }
6385
- const userStart = firstPrompt ? 1 : 0;
6386
- for (let i = userStart; i < content.userMessages.length; i++) {
6387
- if (chunks.length >= maxChunks)
6388
- break;
6389
- const msg = content.userMessages[i];
6390
- if (!msg.text.trim())
6391
- continue;
6392
- const windows = windowText(msg.text, maxChars);
6393
- for (let w = 0; w < windows.length; w++) {
6394
- if (chunks.length >= maxChunks)
6395
- break;
6396
- chunks.push({
6397
- chunkId: `${entry.sessionId}_user_${i}_${w}`,
6398
- chunkType: "user_context",
6399
- text: windows[w],
6400
- sourceLine: msg.lineNumber,
6401
- ...baseData
6402
- });
6397
+ return roots;
6398
+ }
6399
+ function applyDecisions(roots, includeExcluded) {
6400
+ if (roots.length === 0)
6401
+ return roots;
6402
+ let kept = roots;
6403
+ if (!includeExcluded) {
6404
+ try {
6405
+ grandfatherLegacyHomes();
6406
+ const approved = kept.filter((r) => isHomeSynced(dirname6(r)));
6407
+ if (approved.length > 0)
6408
+ kept = approved;
6409
+ } catch {
6403
6410
  }
6404
6411
  }
6405
- if (chunks.length >= maxChunks) {
6406
- return chunks.slice(0, maxChunks);
6412
+ if (!includeExcluded && _sourceExclusionFilter) {
6413
+ const notExcluded = kept.filter((r) => !_sourceExclusionFilter(r));
6414
+ if (notExcluded.length > 0)
6415
+ kept = notExcluded;
6407
6416
  }
6408
- for (let i = 0; i < content.assistantMessages.length; i++) {
6409
- if (chunks.length >= maxChunks)
6410
- break;
6411
- const msg = content.assistantMessages[i];
6412
- if (!msg.text.trim() || msg.text.length <= 50)
6413
- continue;
6414
- const windows = windowText(msg.text, maxChars);
6415
- for (let w = 0; w < windows.length; w++) {
6416
- if (chunks.length >= maxChunks)
6417
- break;
6418
- chunks.push({
6419
- chunkId: `${entry.sessionId}_assistant_${i}_${w}`,
6420
- chunkType: "assistant",
6421
- text: windows[w],
6422
- sourceLine: msg.lineNumber,
6423
- ...baseData
6424
- });
6425
- }
6417
+ return kept;
6418
+ }
6419
+ function applyExclusions(roots, includeExcluded) {
6420
+ return applyDecisions(roots, includeExcluded);
6421
+ }
6422
+ function codexSessionDirs(opts = {}) {
6423
+ const overridden = !!(process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome);
6424
+ return applyExclusions(siblingHomes(codexHomeDir(), overridden, "sessions"), opts.includeExcluded);
6425
+ }
6426
+ function agyBrainDirs(opts = {}) {
6427
+ const overridden = !!(process.env.CHAT_RECALL_AGY_HOME || sources().agyHome);
6428
+ return applyExclusions(siblingHomes(agyHomeDir(), overridden, "brain"), opts.includeExcluded);
6429
+ }
6430
+ function cursorChatDirs(opts = {}) {
6431
+ const overridden = !!(process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome);
6432
+ return applyExclusions(siblingHomes(cursorHomeDir(), overridden, "chats"), opts.includeExcluded);
6433
+ }
6434
+ function cursorIdeWorkspaceDirs(opts = {}) {
6435
+ const overridden = !!(process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome);
6436
+ return applyExclusions(siblingHomes(cursorIdeHomeDir(), overridden, join6("User", "workspaceStorage")), opts.includeExcluded);
6437
+ }
6438
+ function cursorIdeGlobalDbs(opts = {}) {
6439
+ const overridden = !!(process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome);
6440
+ const roots = applyExclusions(siblingHomes(cursorIdeHomeDir(), overridden, join6("User", "globalStorage")), opts.includeExcluded);
6441
+ const dbs = [];
6442
+ for (const r of roots) {
6443
+ const p = join6(r, "state.vscdb");
6444
+ if (existsSync5(p) && !dbs.includes(p))
6445
+ dbs.push(p);
6426
6446
  }
6427
- if (chunks.length >= maxChunks) {
6428
- return chunks.slice(0, maxChunks);
6447
+ return dbs;
6448
+ }
6449
+ function opencodeDbPaths(opts = {}) {
6450
+ const primary = opencodeDbPath();
6451
+ const overridden = !!(process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath);
6452
+ const dbFile = basename2(primary);
6453
+ const dirs = siblingHomes(dirname6(primary), overridden, "");
6454
+ const dbs = [];
6455
+ for (const d of dirs) {
6456
+ const p = join6(d, dbFile);
6457
+ if (existsSync5(p) && !dbs.includes(p))
6458
+ dbs.push(p);
6429
6459
  }
6430
- if (content.toolResults.length > 0) {
6431
- const webSearches = content.toolResults.filter((r) => r.contentType === "web_search");
6432
- const otherResults = content.toolResults.filter((r) => r.contentType !== "web_search");
6433
- const resultsToProcess = [...webSearches.slice(0, 5), ...otherResults.slice(0, 10)];
6434
- const stripBodies = isToolOutputRedactionEnabled();
6435
- for (let i = 0; i < resultsToProcess.length; i++) {
6436
- if (chunks.length >= maxChunks)
6437
- break;
6438
- const result = resultsToProcess[i];
6439
- if (result.text.trim()) {
6440
- const body = stripBodies && result.contentType !== "web_search" ? redactToolResultBody(result.contentType, result.text) : result.text.slice(0, maxChars);
6441
- chunks.push({
6442
- chunkId: `${entry.sessionId}_${result.contentType}_${i}`,
6443
- chunkType: result.contentType,
6444
- text: cleanText(body),
6445
- sourceLine: result.lineNumber,
6446
- ...baseData
6447
- });
6448
- }
6449
- }
6460
+ if (dbs.length === 0)
6461
+ dbs.push(primary);
6462
+ return applyExclusions(dbs, opts.includeExcluded);
6463
+ }
6464
+ function extraClaudeHomeDirs() {
6465
+ const extras = sources().extraClaudeHomes ?? [];
6466
+ const primary = claudeHomeDir();
6467
+ return Array.from(new Set(extras)).filter((p) => p && p !== primary);
6468
+ }
6469
+ function claudeHomeDirs() {
6470
+ const overridden = !!(process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome);
6471
+ const homes = siblingHomes(claudeHomeDir(), overridden, "").filter((h) => basename2(h) !== ".claude-code");
6472
+ for (const extra of extraClaudeHomeDirs()) {
6473
+ if (existsSync5(extra) && !homes.includes(extra))
6474
+ homes.push(extra);
6450
6475
  }
6451
- return chunks.slice(0, maxChunks);
6476
+ return homes;
6452
6477
  }
6453
- var INJECTED_BANNERS;
6454
- var init_chunker = __esm({
6455
- "../engine/dist/parsers/chunker.js"() {
6478
+ var sources, _sourceExclusionFilter;
6479
+ var init_tool_paths = __esm({
6480
+ "../engine/dist/core/tool-paths.js"() {
6456
6481
  "use strict";
6457
- init_secret_redactor();
6458
- INJECTED_BANNERS = [
6459
- /MCP issues detected\. ?Run \/mcp list for status\.?/g,
6460
- /Context low[^\n]*Run \/compact[^\n]*/g,
6461
- /API Error:[^\n]{0,120}/g
6462
- ];
6482
+ init_home_approval();
6483
+ init_settings();
6484
+ sources = loadSourceSettings;
6485
+ _sourceExclusionFilter = null;
6463
6486
  }
6464
6487
  });
6465
6488
 
@@ -8114,13 +8137,26 @@ function resolveSessionContentGroups(sessionId) {
8114
8137
  }
8115
8138
  function withSessionReadCache(fn) {
8116
8139
  readScopeDepth++;
8140
+ if (readScopeDepth === 1)
8141
+ derivedScope = /* @__PURE__ */ new Map();
8117
8142
  try {
8118
8143
  return fn();
8119
8144
  } finally {
8120
- if (--readScopeDepth === 0)
8145
+ if (--readScopeDepth === 0) {
8121
8146
  readScope = null;
8147
+ derivedScope = null;
8148
+ }
8122
8149
  }
8123
8150
  }
8151
+ function memoInSessionScope(key2, compute) {
8152
+ if (!derivedScope)
8153
+ return compute();
8154
+ if (derivedScope.has(key2))
8155
+ return derivedScope.get(key2);
8156
+ const value = compute();
8157
+ derivedScope.set(key2, value);
8158
+ return value;
8159
+ }
8124
8160
  function readSessionGroupText(group) {
8125
8161
  let key2 = "";
8126
8162
  if (readScopeDepth > 0) {
@@ -8232,7 +8268,7 @@ function findCodexSessionFileIn(root, sessionId) {
8232
8268
  }
8233
8269
  return scanDir(root);
8234
8270
  }
8235
- var scanScopeDepth, indexCache, readScope, readScopeDepth, geminiCache, MAX_EVENT_LINE_BYTES;
8271
+ var scanScopeDepth, indexCache, readScope, readScopeDepth, derivedScope, geminiCache, MAX_EVENT_LINE_BYTES;
8236
8272
  var init_live_session_scan = __esm({
8237
8273
  "../engine/dist/core/live-session-scan.js"() {
8238
8274
  "use strict";
@@ -8246,6 +8282,7 @@ var init_live_session_scan = __esm({
8246
8282
  indexCache = null;
8247
8283
  readScope = null;
8248
8284
  readScopeDepth = 0;
8285
+ derivedScope = null;
8249
8286
  geminiCache = null;
8250
8287
  MAX_EVENT_LINE_BYTES = 8 * 1024 * 1024;
8251
8288
  }
@@ -8523,6 +8560,28 @@ var init_session_git = __esm({
8523
8560
  }
8524
8561
  });
8525
8562
 
8563
+ // ../engine/dist/core/session-multi-tool.js
8564
+ function extractTurnsAny(sessionId, opts = {}) {
8565
+ const backend = getBackendForId(sessionId);
8566
+ if (!backend)
8567
+ return { sessionId, found: false, turns: [], startMs: 0, endMs: 0 };
8568
+ return memoInSessionScope(`turns:${sessionId}:${opts.maxTurns ?? ""}:${opts.assistantMax ?? ""}`, () => backend.extractTurns(sessionId, opts));
8569
+ }
8570
+ function replaySessionAny(sessionId) {
8571
+ const backend = getBackendForId(sessionId);
8572
+ if (!backend)
8573
+ return { sessionId, found: false, projectPath: "", files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
8574
+ return memoInSessionScope(`replay:${sessionId}`, () => backend.replay(sessionId));
8575
+ }
8576
+ var init_session_multi_tool = __esm({
8577
+ "../engine/dist/core/session-multi-tool.js"() {
8578
+ "use strict";
8579
+ init_tool_backend();
8580
+ init_live_session_scan();
8581
+ init_backends();
8582
+ }
8583
+ });
8584
+
8526
8585
  // ../engine/dist/core/session-outcome.js
8527
8586
  function extractDecisionsFromTurn(turn) {
8528
8587
  if (turn.kind !== "assistant_text" || !turn.text)
@@ -8619,7 +8678,7 @@ function computeOutcome(sessionId, opts = {}) {
8619
8678
  };
8620
8679
  if (!backend)
8621
8680
  return notFound;
8622
- const turnsRes = backend.extractTurns(sessionId, { assistantMax: 2e3 });
8681
+ const turnsRes = extractTurnsAny(sessionId, { assistantMax: 2e3 });
8623
8682
  if (!turnsRes.found)
8624
8683
  return notFound;
8625
8684
  const decisions = [];
@@ -8658,7 +8717,7 @@ function computeOutcome(sessionId, opts = {}) {
8658
8717
  totalLinesAdded = fast.totalLinesAdded;
8659
8718
  totalLinesRemoved = fast.totalLinesRemoved;
8660
8719
  } else {
8661
- const replay = backend.replay(sessionId);
8720
+ const replay = replaySessionAny(sessionId);
8662
8721
  filesChanged = replay.files.map((f) => f.file);
8663
8722
  totalLinesAdded = replay.totalLinesAdded;
8664
8723
  totalLinesRemoved = replay.totalLinesRemoved;
@@ -8697,6 +8756,7 @@ var init_session_outcome = __esm({
8697
8756
  init_session_sentiment();
8698
8757
  init_session_git();
8699
8758
  init_tool_backend();
8759
+ init_session_multi_tool();
8700
8760
  init_backends();
8701
8761
  DECISION_PATTERNS = [
8702
8762
  /\b(?:I'?ll|I will|I'?m going to|Going to|Let me|We'?ll|We will|We should|Let'?s|I'?m about to|I'?m going to)\b[^\n.!?]*[.!?]/i,
@@ -9166,6 +9226,7 @@ var UUID_RE, ClaudeBackend, claudeBackend;
9166
9226
  var init_claude2 = __esm({
9167
9227
  "../engine/dist/core/backends/claude.js"() {
9168
9228
  "use strict";
9229
+ init_live_session_scan();
9169
9230
  init_tool_paths();
9170
9231
  init_live_session_scan();
9171
9232
  init_session_outcome();
@@ -9428,6 +9489,9 @@ var init_claude2 = __esm({
9428
9489
  * splits show up alongside the parent.
9429
9490
  */
9430
9491
  readEvents(rawId) {
9492
+ return memoInSessionScope(`events:claude:${rawId}`, () => this.#readEventsUncached(rawId));
9493
+ }
9494
+ #readEventsUncached(rawId) {
9431
9495
  const groups = resolveSessionContentGroups(rawId);
9432
9496
  if (groups.length === 0)
9433
9497
  return [];
@@ -15237,15 +15301,15 @@ function extractEntities(text, context = {}) {
15237
15301
  seen.add(key2);
15238
15302
  triples.push({ subject: s, predicate: p, object: o, confidence: conf, validFrom: context.validFrom });
15239
15303
  };
15240
- const projectName = context.projectPath ? context.projectPath.split("/").filter(Boolean).pop() || "" : "";
15304
+ const projectName2 = context.projectPath ? context.projectPath.split("/").filter(Boolean).pop() || "" : "";
15241
15305
  for (const { pattern, countPattern, contextPattern, name, category } of TOOL_PATTERNS) {
15242
15306
  if (!pattern.test(text))
15243
15307
  continue;
15244
15308
  countPattern.lastIndex = 0;
15245
15309
  const mentions = (text.match(countPattern) || []).length;
15246
15310
  const contextual = contextPattern.test(text);
15247
- if (projectName && (mentions >= 2 || contextual)) {
15248
- addTriple(projectName, "uses", canonTool(name), Math.min(0.9, 0.5 + 0.1 * Math.min(mentions, 4) + (contextual ? 0.1 : 0)));
15311
+ if (projectName2 && (mentions >= 2 || contextual)) {
15312
+ addTriple(projectName2, "uses", canonTool(name), Math.min(0.9, 0.5 + 0.1 * Math.min(mentions, 4) + (contextual ? 0.1 : 0)));
15249
15313
  }
15250
15314
  void category;
15251
15315
  }
@@ -15258,13 +15322,13 @@ function extractEntities(text, context = {}) {
15258
15322
  if (rejected) {
15259
15323
  if (chosen && looksLikeStrictTool(chosen) && looksLikeStrictTool(rejected)) {
15260
15324
  const c = canonTool(chosen), r = canonTool(rejected);
15261
- if (projectName)
15262
- addTriple(projectName, "chose", c, DECISION_GUESS_CONFIDENCE);
15263
- addTriple(projectName || "project", "rejected", r, DECISION_GUESS_CONFIDENCE);
15325
+ if (projectName2)
15326
+ addTriple(projectName2, "chose", c, DECISION_GUESS_CONFIDENCE);
15327
+ addTriple(projectName2 || "project", "rejected", r, DECISION_GUESS_CONFIDENCE);
15264
15328
  addTriple(c, "chosen_over", r, DECISION_GUESS_CONFIDENCE);
15265
15329
  }
15266
- } else if (chosen && looksLikeDecisionObject(chosen) && projectName) {
15267
- addTriple(projectName, "chose", canonTool(chosen), DECISION_GUESS_CONFIDENCE);
15330
+ } else if (chosen && looksLikeDecisionObject(chosen) && projectName2) {
15331
+ addTriple(projectName2, "chose", canonTool(chosen), DECISION_GUESS_CONFIDENCE);
15268
15332
  }
15269
15333
  }
15270
15334
  }
@@ -15294,8 +15358,8 @@ function extractEntities(text, context = {}) {
15294
15358
  if (match[0].startsWith("@") && (after === "/" || after === "." || after === "("))
15295
15359
  continue;
15296
15360
  addTriple(name, "is_a", "person", 0.6);
15297
- if (projectName) {
15298
- addTriple(name, "works_on", projectName, 0.5);
15361
+ if (projectName2) {
15362
+ addTriple(name, "works_on", projectName2, 0.5);
15299
15363
  }
15300
15364
  }
15301
15365
  }
@@ -16331,13 +16395,13 @@ var init_plan_source = __esm({
16331
16395
  });
16332
16396
  }
16333
16397
  if (item.projectPath) {
16334
- const projectName = basename13(item.projectPath);
16335
- if (projectName) {
16398
+ const projectName2 = basename13(item.projectPath);
16399
+ if (projectName2) {
16336
16400
  links.push({
16337
16401
  sourceType: "plan",
16338
16402
  sourceId: planName,
16339
16403
  targetType: "claude_md",
16340
- targetId: projectName,
16404
+ targetId: projectName2,
16341
16405
  linkType: "plan_for_project",
16342
16406
  confidence: sessionId ? 1 : 0.8
16343
16407
  });
@@ -16837,15 +16901,15 @@ var init_claude_md_source = __esm({
16837
16901
  try {
16838
16902
  const stat3 = statSync26(filePath);
16839
16903
  const projectDir = dirname16(filePath);
16840
- const projectName = basename14(projectDir);
16904
+ const projectName2 = basename14(projectDir);
16841
16905
  const projectPath = projectDir;
16842
16906
  const content = readFileSync21(filePath, "utf-8");
16843
- const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"))?.trim()?.slice(0, 200) || `${filename} for ${projectName}`;
16844
- const id = tool === "claude" ? projectName : `${projectName}_${tool}`;
16907
+ const firstLine = content.split("\n").find((l) => l.trim() && !l.startsWith("#"))?.trim()?.slice(0, 200) || `${filename} for ${projectName2}`;
16908
+ const id = tool === "claude" ? projectName2 : `${projectName2}_${tool}`;
16845
16909
  return {
16846
16910
  id,
16847
16911
  sourceType: "claude_md",
16848
- title: `${filename} - ${projectName}`,
16912
+ title: `${filename} - ${projectName2}`,
16849
16913
  projectPath,
16850
16914
  filePath,
16851
16915
  mtime: stat3.mtimeMs,
@@ -19728,7 +19792,7 @@ var init_http = __esm({
19728
19792
  CLI_VERSION = JSON.parse(readFileSync35(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
19729
19793
  } catch {
19730
19794
  try {
19731
- if (true) CLI_VERSION = "0.6.8";
19795
+ if (true) CLI_VERSION = "0.6.9";
19732
19796
  } catch {
19733
19797
  }
19734
19798
  }
@@ -19753,27 +19817,6 @@ var init_redact_deep = __esm({
19753
19817
  }
19754
19818
  });
19755
19819
 
19756
- // ../engine/dist/core/session-multi-tool.js
19757
- function extractTurnsAny(sessionId, opts = {}) {
19758
- const backend = getBackendForId(sessionId);
19759
- if (!backend)
19760
- return { sessionId, found: false, turns: [], startMs: 0, endMs: 0 };
19761
- return backend.extractTurns(sessionId, opts);
19762
- }
19763
- function replaySessionAny(sessionId) {
19764
- const backend = getBackendForId(sessionId);
19765
- if (!backend)
19766
- return { sessionId, found: false, projectPath: "", files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
19767
- return backend.replay(sessionId);
19768
- }
19769
- var init_session_multi_tool = __esm({
19770
- "../engine/dist/core/session-multi-tool.js"() {
19771
- "use strict";
19772
- init_tool_backend();
19773
- init_backends();
19774
- }
19775
- });
19776
-
19777
19820
  // src/derived.ts
19778
19821
  function collectDerivedRows(ref, mtime, count, maxRowBytes) {
19779
19822
  const id = ref.prefixedId;
@@ -20248,7 +20291,7 @@ var init_telemetry = __esm({
20248
20291
  init_sync_client();
20249
20292
  cliVersion2 = () => {
20250
20293
  try {
20251
- return true ? "0.6.8" : "dev";
20294
+ return true ? "0.6.9" : "dev";
20252
20295
  } catch {
20253
20296
  return "dev";
20254
20297
  }
@@ -20652,6 +20695,7 @@ var sync_client_exports = {};
20652
20695
  __export(sync_client_exports, {
20653
20696
  BATCH_SCAN_MAX_BYTES: () => BATCH_SCAN_MAX_BYTES,
20654
20697
  FULL_BUILD_MAX_BYTES: () => FULL_BUILD_MAX_BYTES,
20698
+ _acceptedUploads: () => _acceptedUploads,
20655
20699
  _fetchTenantSyncConfigForTests: () => _fetchTenantSyncConfigForTests,
20656
20700
  _resetCapsCacheForTests: () => _resetCapsCacheForTests,
20657
20701
  _resetTenantSecurityConfigCache: () => _resetTenantSecurityConfigCache,
@@ -20915,6 +20959,9 @@ function removeCredentials(serverUrl) {
20915
20959
  writeFileSync10(credPath(), JSON.stringify({ targets: kept }, null, 2));
20916
20960
  return true;
20917
20961
  }
20962
+ function _acceptedUploads(url) {
20963
+ return acceptedUploads.get(url) ?? 0;
20964
+ }
20918
20965
  async function syncSessions(opts = {}) {
20919
20966
  const targets = loadAllCredentials();
20920
20967
  if (targets.length === 0) throw new Error("Not logged in \u2014 run `chat-recall login <server-url> --token <token>`");
@@ -20938,10 +20985,11 @@ async function syncSessions(opts = {}) {
20938
20985
  };
20939
20986
  continue;
20940
20987
  }
20988
+ acceptedUploads.set(cred.serverUrl, 0);
20941
20989
  try {
20942
20990
  const r = await syncToTarget(cred, opts);
20943
20991
  noteTargetSuccess(cred.serverUrl);
20944
- perTarget[cred.serverUrl] = { ok: true };
20992
+ perTarget[cred.serverUrl] = { ok: true, accepted: acceptedUploads.get(cred.serverUrl) ?? 0 };
20945
20993
  agg = agg ? {
20946
20994
  uploaded: agg.uploaded + r.uploaded,
20947
20995
  skipped: agg.skipped + r.skipped,
@@ -20958,7 +21006,7 @@ async function syncSessions(opts = {}) {
20958
21006
  } catch (e) {
20959
21007
  const msg = e instanceof Error ? e.message : String(e);
20960
21008
  errors.push(`${cred.serverUrl}: ${msg}`);
20961
- perTarget[cred.serverUrl] = { ok: false, error: msg };
21009
+ perTarget[cred.serverUrl] = { ok: false, error: msg, accepted: acceptedUploads.get(cred.serverUrl) ?? 0 };
20962
21010
  const next = noteTargetFailure(cred.serverUrl, msg);
20963
21011
  const notice = breakerNotice(cred.serverUrl, next);
20964
21012
  if (notice) console.error(`[sync] ${notice}`);
@@ -21224,7 +21272,7 @@ async function syncToTargetInScope(cred, opts = {}) {
21224
21272
  return [];
21225
21273
  }
21226
21274
  });
21227
- const MAX_BATCH_BYTES = 4 * 1024 * 1024;
21275
+ const MAX_BATCH_BYTES = Number(process.env.CHAT_RECALL_MAX_BATCH_BYTES) || (isLocalHost(cred.serverUrl) ? 4 * 1024 * 1024 : 1024 * 1024);
21228
21276
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21229
21277
  const explicitThrottle = typeof opts.throttleMs === "number" && opts.throttleMs > 0;
21230
21278
  const throttleMs = explicitThrottle ? opts.throttleMs : 0;
@@ -21259,6 +21307,7 @@ async function syncToTargetInScope(cred, opts = {}) {
21259
21307
  signal: ac.signal
21260
21308
  });
21261
21309
  if (res.ok) {
21310
+ acceptedUploads.set(cred.serverUrl, (acceptedUploads.get(cred.serverUrl) ?? 0) + 1);
21262
21311
  const body2 = await res.json().catch(() => ({}));
21263
21312
  if (typeof body2.telemetry === "boolean") {
21264
21313
  setTelemetryEligible(base, body2.telemetry);
@@ -21309,7 +21358,8 @@ async function syncToTargetInScope(cred, opts = {}) {
21309
21358
  if (attempt >= RETRY_DELAYS_MS.length) throw new Error(`sync failed after ${attempt + 1} attempts: HTTP ${res.status} ${text}`);
21310
21359
  const retryAfterS = Number(res.headers.get("retry-after"));
21311
21360
  const waitMs = res.status === 429 && retryAfterS > 0 ? retryAfterS * 1e3 : RETRY_DELAYS_MS[attempt];
21312
- console.error(`[sync] HTTP ${res.status} from ${base} \u2014 backing off ${Math.round(waitMs / 1e3)}s (attempt ${attempt + 1})`);
21361
+ const why = res.status === 524 ? ` \u2014 the proxy cut the request at its time limit; the batch is too big for one request` : "";
21362
+ console.error(`[sync] HTTP ${res.status} from ${base}${why} \u2014 backing off ${Math.round(waitMs / 1e3)}s (attempt ${attempt + 1})`);
21313
21363
  await sleep(waitMs);
21314
21364
  continue;
21315
21365
  } catch (err) {
@@ -22168,7 +22218,7 @@ async function syncIncremental(opts = {}) {
22168
22218
  lock.release();
22169
22219
  }
22170
22220
  }
22171
- var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, _fetchTenantSyncConfigForTests, SYNC_RULES_REFUSAL, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
22221
+ var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, _fetchTenantSyncConfigForTests, SYNC_RULES_REFUSAL, hashPath, acceptedUploads, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
22172
22222
  var init_sync_client = __esm({
22173
22223
  "src/sync-client.ts"() {
22174
22224
  "use strict";
@@ -22213,7 +22263,7 @@ var init_sync_client = __esm({
22213
22263
  ownVersion = JSON.parse(readFileSync37(join48(dir, "../package.json"), "utf-8")).version || ownVersion;
22214
22264
  } catch {
22215
22265
  try {
22216
- if (true) ownVersion = "0.6.8";
22266
+ if (true) ownVersion = "0.6.9";
22217
22267
  } catch {
22218
22268
  }
22219
22269
  }
@@ -22233,6 +22283,7 @@ var init_sync_client = __esm({
22233
22283
  _fetchTenantSyncConfigForTests = fetchTenantSyncConfig;
22234
22284
  SYNC_RULES_REFUSAL = /* @__PURE__ */ Symbol.for("chat-recall.syncRulesRefusal");
22235
22285
  hashPath = (p) => p ? "p_" + createHash11("sha256").update(p).digest("hex").slice(0, 12) : "";
22286
+ acceptedUploads = /* @__PURE__ */ new Map();
22236
22287
  MAX_DERIVED_ROW_BYTES = 2 * 1024 * 1024;
22237
22288
  isLocalHost = isPrivateHost;
22238
22289
  CAPS_TTL_MS = 60 * 6e4;
@@ -23065,6 +23116,128 @@ function resumeCommandFor(sessionId, tool) {
23065
23116
  // ../engine/dist/mcp/tools.js
23066
23117
  init_project_resolver();
23067
23118
 
23119
+ // ../engine/dist/mcp/resume-digest.js
23120
+ function splitSummary(summary) {
23121
+ const out = {};
23122
+ if (!summary)
23123
+ return out;
23124
+ const heads = [
23125
+ ["request", /\*\*Request:?\*\*/i],
23126
+ ["plan", /\*\*Plan:?\*\*/i],
23127
+ ["done", /\*\*What was done:?\*\*/i],
23128
+ ["remaining", /\*\*Remaining\s*\/?\s*Not done:?\*\*/i]
23129
+ ];
23130
+ const marks = [];
23131
+ for (const [key2, re] of heads) {
23132
+ const m = re.exec(summary);
23133
+ if (m)
23134
+ marks.push({ key: key2, start: m.index + m[0].length, end: m.index });
23135
+ }
23136
+ marks.sort((a, b) => a.start - b.start);
23137
+ for (let i = 0; i < marks.length; i++) {
23138
+ const stop = i + 1 < marks.length ? marks[i + 1].end : summary.length;
23139
+ const body = summary.slice(marks[i].start, stop).trim();
23140
+ if (body)
23141
+ out[marks[i].key] = body;
23142
+ }
23143
+ return out;
23144
+ }
23145
+ function isLabelOnly(text) {
23146
+ const bare = text.replace(/\*\*/g, "").trim();
23147
+ return bare.endsWith(":");
23148
+ }
23149
+ function bulletLines(section, max, chars = 160) {
23150
+ if (!section)
23151
+ return [];
23152
+ const out = [];
23153
+ for (const raw of section.split("\n")) {
23154
+ const line = raw.trim();
23155
+ if (!line.startsWith("-") && !line.startsWith("*"))
23156
+ continue;
23157
+ const text = line.replace(/^[-*]\s*/, "").replace(/\s+/g, " ").trim();
23158
+ if (!text || isLabelOnly(text))
23159
+ continue;
23160
+ out.push(text.length > chars ? `${text.slice(0, chars)}\u2026` : text);
23161
+ if (out.length >= max)
23162
+ break;
23163
+ }
23164
+ return out;
23165
+ }
23166
+ function statusEmoji(status) {
23167
+ if (status === "shipped")
23168
+ return "\u{1F6A2}";
23169
+ if (status === "interrupted")
23170
+ return "\u23F8";
23171
+ if (status === "abandoned")
23172
+ return "\u{1FAA6}";
23173
+ if (status === "in_progress")
23174
+ return "\u{1F7E1}";
23175
+ return "\u2754";
23176
+ }
23177
+ function projectName(projectPath) {
23178
+ if (!projectPath)
23179
+ return "";
23180
+ return projectPath.split("/").pop() || projectPath;
23181
+ }
23182
+ function digestTitle(row, max = 90) {
23183
+ const t = row.userTitle?.trim() || row.toolTitle?.trim() || row.firstPrompt || "(no prompt)";
23184
+ const flat = t.replace(/\s+/g, " ").trim();
23185
+ return flat.length > max ? flat.slice(0, max) : flat;
23186
+ }
23187
+ function formatDigest(input, summaryChars = 400) {
23188
+ const { row, outcome, related, resumeCmd } = input;
23189
+ const lines = [];
23190
+ lines.push(`### ${digestTitle(row)}`);
23191
+ const when = (row.modified || "").slice(0, 16).replace("T", " ");
23192
+ lines.push(`**Project:** ${projectName(row.projectPath) || "(unknown)"} \xB7 **Tool:** ${row.tool || "claude"}` + (when ? ` \xB7 **Modified:** ${when}` : "") + ` \xB7 \`${row.sessionId}\``);
23193
+ if (outcome) {
23194
+ const edits = outcome.fileCount > 0 ? ` \xB7 ${outcome.fileCount} file(s) +${outcome.totalLinesAdded}/\u2212${outcome.totalLinesRemoved}` : "";
23195
+ const commits = outcome.commits && outcome.commits.totalCommits > 0 ? ` \xB7 ${outcome.commits.totalCommits} commit(s)` : "";
23196
+ lines.push(`${statusEmoji(outcome.status)} **${outcome.status}** \u2014 ${outcome.reason}${edits}${commits}`);
23197
+ }
23198
+ const sections = splitSummary(row.summary || "");
23199
+ const did = bulletLines(sections.done, 4);
23200
+ const left = bulletLines(sections.remaining, 4);
23201
+ if (did.length) {
23202
+ lines.push("**Did:**");
23203
+ for (const d of did)
23204
+ lines.push(`- ${d}`);
23205
+ }
23206
+ if (left.length) {
23207
+ lines.push("**Still open:**");
23208
+ for (const l of left)
23209
+ lines.push(`- ${l}`);
23210
+ }
23211
+ if (!did.length && !left.length) {
23212
+ const gist = (row.summary || "").replace(/\s+/g, " ").trim();
23213
+ if (gist)
23214
+ lines.push(gist.length > summaryChars ? `${gist.slice(0, summaryChars)}\u2026` : gist);
23215
+ }
23216
+ if (outcome?.decisions?.length) {
23217
+ lines.push("**Decisions:**");
23218
+ for (const d of outcome.decisions.slice(0, 3))
23219
+ lines.push(`- ${d.text.slice(0, 160)}`);
23220
+ }
23221
+ const tasks = (related?.links ?? []).filter((l) => l.sourceType === "task");
23222
+ if (tasks.length > 0) {
23223
+ lines.push("**Task lists:**");
23224
+ for (const t of tasks.slice(0, 5))
23225
+ lines.push(`- ${t.title}`);
23226
+ }
23227
+ if (resumeCmd)
23228
+ lines.push(`**Resume:** \`${resumeCmd}\``);
23229
+ lines.push("");
23230
+ return lines;
23231
+ }
23232
+ function crossProjectNote(headProject, priorRows, hasProjectFilter) {
23233
+ if (hasProjectFilter)
23234
+ return null;
23235
+ const names = new Set([headProject, ...priorRows.map((r) => projectName(r.projectPath))].filter(Boolean));
23236
+ if (names.size < 2)
23237
+ return null;
23238
+ return `_These sessions span ${names.size} projects (${[...names].join(", ")}). Pass \`project_filter\` to scope the resume to one._`;
23239
+ }
23240
+
23068
23241
  // ../engine/dist/core/decision-areas.js
23069
23242
  var DECISION_AREAS = [
23070
23243
  "auth",
@@ -23169,10 +23342,11 @@ function canonArea(raw) {
23169
23342
  function isKnownArea(key2) {
23170
23343
  return !!key2 && AREA_SET.has(key2);
23171
23344
  }
23172
- var ACCOUNT_SCOPE = "*";
23173
- function decisionSubject(project, area) {
23174
- const p = (project || "").trim() || ACCOUNT_SCOPE;
23175
- return `${p}:${area}`;
23345
+ function parseDecisionSubject(subject) {
23346
+ const i = subject.lastIndexOf(":");
23347
+ if (i <= 0 || i === subject.length - 1)
23348
+ return null;
23349
+ return { project: subject.slice(0, i), area: subject.slice(i + 1) };
23176
23350
  }
23177
23351
 
23178
23352
  // ../engine/dist/mcp/tools.js
@@ -23845,7 +24019,15 @@ var RecallSmartResumeSchema = z.object({
23845
24019
  // agent had to call recall_recent first, and a bare call failed with a zod
23846
24020
  // error about a missing field rather than doing the obvious thing.
23847
24021
  session_id: z.string().optional().describe("Session to resume. Omit for the most recent one."),
23848
- project_filter: z.string().optional().describe("When session_id is omitted, resume the latest session from this project (path or name substring).")
24022
+ project_filter: z.string().optional().describe("When session_id is omitted, resume the latest session from this project (path or name substring)."),
24023
+ // "Continue" rarely means one session. Work spans an evening across three of
24024
+ // them, and the newest alone reports a fragment: the tail of a task whose
24025
+ // decision was taken two sessions back. A bare call also takes the globally
24026
+ // newest session, so when the last thing on the machine belonged to another
24027
+ // project, the single-session answer was confidently about the wrong work
24028
+ // with nothing on screen to show it. The trailing digests carry their own
24029
+ // project, which makes that visible instead of silent.
24030
+ depth: z.number().int().min(1).max(5).default(3).describe("How many recent sessions to cover: the newest in full, the rest as digests. Ignored when session_id is set.")
23849
24031
  });
23850
24032
  var RecallSubagentSearchSchema = z.object({
23851
24033
  query: z.string().describe("Text to find inside subagent transcripts (explore/compact/aside sub-tasks)"),
@@ -24020,6 +24202,30 @@ var RecallUserPromptsSchema = z.object({
24020
24202
  limit: z.number().optional().default(50).describe("Maximum prompts to return"),
24021
24203
  with_markers: z.boolean().optional().default(true).describe("Tag each prompt with sentiment / corrective markers (interrupt, frustrated, correction, approval, \u2026). Set false to revert to legacy text-only output.")
24022
24204
  });
24205
+ function resolveRecordScope(params) {
24206
+ const fromSubject = parseDecisionSubject(params.subject);
24207
+ const subjectArea = fromSubject && isKnownArea(canonArea(fromSubject.area) ?? "") ? canonArea(fromSubject.area) : null;
24208
+ const area = canonArea(params.area) ?? subjectArea;
24209
+ const subjectScope = subjectArea ? fromSubject.project : null;
24210
+ const subjectProject = subjectScope && subjectScope !== "*" && !subjectScope.startsWith("ws:") ? subjectScope : void 0;
24211
+ const subjectWorkspace = subjectScope?.startsWith("ws:") ? subjectScope.slice(3) : void 0;
24212
+ return {
24213
+ area,
24214
+ project: params.project ?? subjectProject,
24215
+ workspace: params.workspace ?? subjectWorkspace ?? (params.cwdWorkspace || void 0),
24216
+ // A subject that named `*` asked for the account and still gets it. The
24217
+ // response says which scope it landed on either way.
24218
+ scope: params.scope ?? (subjectScope === "*" ? "account" : void 0)
24219
+ };
24220
+ }
24221
+ function describeWriteScope(subject) {
24222
+ const key2 = parseDecisionSubject(subject)?.project ?? "*";
24223
+ if (key2 === "*")
24224
+ return "account-wide \u2014 every project with no decision of its own inherits this";
24225
+ if (key2.startsWith("ws:"))
24226
+ return `the ${key2.slice(3)} folder group \u2014 every repository in it inherits this`;
24227
+ return `${key2} only`;
24228
+ }
24023
24229
  var RecallDecisionRecordSchema = z.object({
24024
24230
  subject: z.string().describe('What the decision is about (e.g., "chat-recall", "auth strategy")'),
24025
24231
  decision: z.string().describe('The decision itself in plain words (e.g., "use Postgres full-text search as the default backend")'),
@@ -24028,7 +24234,9 @@ var RecallDecisionRecordSchema = z.object({
24028
24234
  // "auth", "authentication" and "auth setup" are three separate facts and a
24029
24235
  // later decision never closes an earlier one — see core/decision-areas.ts.
24030
24236
  area: z.string().optional().describe(`The area this decides, so a later decision about the SAME area supersedes it. Use one of: ${DECISION_AREAS.join(", ")} \u2014 or any short slug for an area not listed. Without it the decision is recorded but nothing it contradicts is closed.`),
24031
- project: z.string().optional().describe("Project this decision applies to. Omit for an account-wide decision that applies everywhere."),
24237
+ project: z.string().optional().describe('Project this decision applies to \u2014 one repository, e.g. "example-app". The narrowest scope, and the one to prefer when the decision came out of one codebase.'),
24238
+ workspace: z.string().optional().describe('Folder group this decision applies to, e.g. "personal" for everything under code/personal. Use it for a decision that binds a group of repositories but not the whole account. Resolved from the current directory when omitted.'),
24239
+ scope: z.enum(["project", "workspace", "account"]).optional().describe('Which tier to record at. Omit and the narrowest scope you named is used: project, else workspace, else account. Pass "account" ONLY for something true of every project you own \u2014 an account decision is inherited by every project that has no opinion of its own.'),
24032
24240
  importance: z.number().min(1).max(5).optional().default(4).describe("1\u20135; the classifier surfaces 4+ in wake-up context"),
24033
24241
  session_id: z.string().optional().describe("Session this decision was made in (for traceability)"),
24034
24242
  agent_name: z.string().optional().default("agent").describe("Who recorded the decision (for diary linkage)")
@@ -24036,6 +24244,7 @@ var RecallDecisionRecordSchema = z.object({
24036
24244
  var RecallDecisionsSchema = z.object({
24037
24245
  area: z.string().optional().describe("Only this area"),
24038
24246
  project: z.string().optional().describe("Resolve for this project"),
24247
+ workspace: z.string().optional().describe('Folder group to resolve through, e.g. "personal". Resolved from the current directory when omitted.'),
24039
24248
  include_candidates: z.boolean().optional().default(false)
24040
24249
  });
24041
24250
  var RecallAnalyticsSummarySchema = z.object({
@@ -24608,25 +24817,32 @@ with recall_show (pass the plan id).`,
24608
24817
  },
24609
24818
  {
24610
24819
  name: "recall_smart_resume",
24611
- description: `Get structured resume context for a session \u2014 by default, the most recent one.
24820
+ description: `Get structured resume context for recent work \u2014 by default the last 3 sessions.
24612
24821
 
24613
- Call it with NO arguments to resume the latest session. That is the common case:
24614
- "continue", "pick up where we left off". Pass session_id only to resume a
24615
- specific one, or project_filter to take the latest from one project.
24822
+ Call it with NO arguments to resume. That is the common case: "continue", "pick
24823
+ up where we left off". Pass session_id to resume one specific session, or
24824
+ project_filter to scope to one project.
24616
24825
 
24617
- Returns:
24826
+ Returns, for the newest session:
24618
24827
  - What was done (completed work, decisions made)
24619
24828
  - What's pending (unfinished tasks, TODOs mentioned)
24620
24829
  - Files modified with change summary
24621
24830
  - Token/cost budget used
24622
24831
  - Resume command
24623
24832
 
24833
+ Then a digest of each earlier session: its project, outcome, summary, the
24834
+ decisions it took and the task lists it left open. Set depth to change how many.
24835
+
24836
+ A bare call takes the globally newest sessions, which may belong to different
24837
+ projects \u2014 the output says so when they do. Pass project_filter to scope it.
24838
+
24624
24839
  Use this instead of recall_context for a more actionable summary when resuming work.`,
24625
24840
  inputSchema: {
24626
24841
  type: "object",
24627
24842
  properties: {
24628
24843
  session_id: { type: "string", description: "Session to resume. OMIT to resume the most recent session." },
24629
- project_filter: { type: "string", description: "With session_id omitted, resume the latest session from this project." }
24844
+ project_filter: { type: "string", description: "With session_id omitted, resume the latest sessions from this project." },
24845
+ depth: { type: "number", default: 3, minimum: 1, maximum: 5, description: "How many recent sessions to cover: the newest in full, the rest as digests. Ignored when session_id is set." }
24630
24846
  }
24631
24847
  }
24632
24848
  },
@@ -25987,7 +26203,7 @@ Sessions already uploaded from that path stay until they are deleted; use recall
25987
26203
  return { content: [{ type: "text", text: soft.message || (soft.status === 404 ? `Session not found: ${params.session_id}` : `Summary not synced yet for ${params.session_id}.`) }] };
25988
26204
  }
25989
26205
  const outcome = soft.data;
25990
- const statusEmoji = outcome.status === "shipped" ? "\u{1F6A2}" : outcome.status === "interrupted" ? "\u23F8" : outcome.status === "abandoned" ? "\u{1FAA6}" : outcome.status === "in_progress" ? "\u{1F7E1}" : "\u2754";
26206
+ const statusEmoji2 = outcome.status === "shipped" ? "\u{1F6A2}" : outcome.status === "interrupted" ? "\u23F8" : outcome.status === "abandoned" ? "\u{1FAA6}" : outcome.status === "in_progress" ? "\u{1F7E1}" : "\u2754";
25991
26207
  if (!params.rich) {
25992
26208
  return { content: [{ type: "text", text: [
25993
26209
  `# \u{1F4CB} Summary`,
@@ -25995,7 +26211,7 @@ Sessions already uploaded from that path stay until they are deleted; use recall
25995
26211
  `**Session:** ${params.session_id.substring(0, 8)}...`,
25996
26212
  aiSummary ? `
25997
26213
  ${aiSummary}
25998
- ` : `**Status:** ${statusEmoji} ${outcome.status} \u2014 ${outcome.reason}`,
26214
+ ` : `**Status:** ${statusEmoji2} ${outcome.status} \u2014 ${outcome.reason}`,
25999
26215
  "",
26000
26216
  "---",
26001
26217
  "",
@@ -26009,7 +26225,7 @@ ${aiSummary}
26009
26225
  lines.push(aiSummary);
26010
26226
  lines.push("");
26011
26227
  }
26012
- lines.push(`**Status:** ${statusEmoji} ${outcome.status} \u2014 ${outcome.reason}`);
26228
+ lines.push(`**Status:** ${statusEmoji2} ${outcome.status} \u2014 ${outcome.reason}`);
26013
26229
  if (outcome.startMs && outcome.endMs) {
26014
26230
  const mins = Math.round((outcome.endMs - outcome.startMs) / 6e4);
26015
26231
  lines.push(`**Window:** ${new Date(outcome.startMs).toISOString().slice(0, 16).replace("T", " ")} \u2192 ${new Date(outcome.endMs).toISOString().slice(0, 16).replace("T", " ")} (${mins} min)`);
@@ -26241,13 +26457,15 @@ _Edits stayed local or window was off \u2014 try increasing buffer_minutes._` }]
26241
26457
  case "recall_smart_resume": {
26242
26458
  const params = RecallSmartResumeSchema.parse(args);
26243
26459
  requireRemote();
26460
+ let priorRows = [];
26244
26461
  let sid = params.session_id;
26245
26462
  if (!sid) {
26246
- const qs = new URLSearchParams({ limit: "1" });
26463
+ const qs = new URLSearchParams({ limit: String(params.depth) });
26247
26464
  if (params.project_filter)
26248
26465
  qs.set("project", params.project_filter);
26249
26466
  const recent = await remoteGet(`/api/conversations/recent?${qs.toString()}`);
26250
26467
  sid = recent.sessions?.[0]?.sessionId;
26468
+ priorRows = (recent.sessions ?? []).slice(1);
26251
26469
  if (!sid) {
26252
26470
  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." }] };
26253
26471
  }
@@ -26371,6 +26589,29 @@ _Edits stayed local or window was off \u2014 try increasing buffer_minutes._` }]
26371
26589
  if (rc)
26372
26590
  lines.push(`**Resume:** \`${rc}\``);
26373
26591
  }
26592
+ if (priorRows.length > 0) {
26593
+ const digests = await Promise.all(priorRows.map(async (row) => {
26594
+ const renc = encodeURIComponent(row.sessionId);
26595
+ const [o, r] = await Promise.all([
26596
+ remoteGetSoft(`/api/conversations/${renc}/outcome`),
26597
+ remoteGetSoft(`/api/conversations/${renc}/related`)
26598
+ ]);
26599
+ return {
26600
+ row,
26601
+ outcome: o.data,
26602
+ related: r.data,
26603
+ resumeCmd: resumeCommandFor(row.sessionId, row.tool)
26604
+ };
26605
+ }));
26606
+ lines.push("");
26607
+ lines.push(`## Before that (${digests.length} earlier session${digests.length === 1 ? "" : "s"})`);
26608
+ lines.push("");
26609
+ for (const d of digests)
26610
+ lines.push(...formatDigest(d));
26611
+ const note = crossProjectNote(projName, priorRows, !!params.project_filter);
26612
+ if (note)
26613
+ lines.push(note);
26614
+ }
26374
26615
  return { content: [{ type: "text", text: lines.join("\n") }] };
26375
26616
  }
26376
26617
  case "recall_project_context": {
@@ -27232,24 +27473,40 @@ ${JSON.stringify(r, null, 2)}
27232
27473
  const wal = getWAL();
27233
27474
  wal.log("decision_record", { subject: params.subject, importance: params.importance });
27234
27475
  const confidence = Math.min(1, params.importance / 5);
27235
- const area = canonArea(params.area);
27236
- const kgSubject = area ? decisionSubject(params.project, area) : params.subject;
27237
- await remotePost("/api/kg/add", {
27238
- subject: kgSubject,
27239
- predicate: "decided",
27240
- object: params.decision,
27241
- confidence,
27242
- source_session: params.session_id
27476
+ const { area, project, workspace, scope } = resolveRecordScope({
27477
+ ...params,
27478
+ cwdWorkspace: resolveWorkspaceId(process.cwd())?.slice(3) ?? null
27243
27479
  });
27244
- if (params.reason) {
27480
+ let kgSubject = params.subject;
27481
+ if (area) {
27482
+ const rec = await remotePost("/api/decisions", {
27483
+ area,
27484
+ value: params.decision,
27485
+ reason: params.reason,
27486
+ project,
27487
+ workspace,
27488
+ scope,
27489
+ session_id: params.session_id
27490
+ });
27491
+ kgSubject = rec.subject;
27492
+ } else {
27245
27493
  await remotePost("/api/kg/add", {
27246
27494
  subject: kgSubject,
27247
- predicate: "because",
27248
- object: params.reason,
27495
+ predicate: "decided",
27496
+ object: params.decision,
27249
27497
  confidence,
27250
- source_session: params.session_id,
27251
- supersede: false
27498
+ source_session: params.session_id
27252
27499
  });
27500
+ if (params.reason) {
27501
+ await remotePost("/api/kg/add", {
27502
+ subject: kgSubject,
27503
+ predicate: "because",
27504
+ object: params.reason,
27505
+ confidence,
27506
+ source_session: params.session_id,
27507
+ supersede: false
27508
+ });
27509
+ }
27253
27510
  }
27254
27511
  const diaryText = params.reason ? `Decided: ${params.decision}
27255
27512
  Subject: ${params.subject}
@@ -27268,8 +27525,9 @@ Subject: ${params.subject}`;
27268
27525
  "Decision recorded.",
27269
27526
  `- KG: ${kgSubject} \u2192 decided \u2192 ${params.decision}`,
27270
27527
  area ? `- Area: ${area}${isKnownArea(area) ? "" : " (not a standard area \u2014 kept as a slug)"} \xB7 a later decision on this area supersedes this one` : "- No area given, so nothing this contradicts was closed. Pass `area` to make reversals work.",
27528
+ area ? `- Scope: ${describeWriteScope(kgSubject)}` : "",
27271
27529
  `- Diary entry: ${diary.id} (importance ${params.importance})`
27272
- ].join("\n")
27530
+ ].filter(Boolean).join("\n")
27273
27531
  }]
27274
27532
  };
27275
27533
  }
@@ -27279,27 +27537,37 @@ Subject: ${params.subject}`;
27279
27537
  requireRemote();
27280
27538
  const r = await remoteGetQS("/api/decisions", {
27281
27539
  project: p.project,
27540
+ // Resolved HERE: the folder group comes from the checkout, and the
27541
+ // checkout is on this machine.
27542
+ workspace: p.workspace ?? resolveWorkspaceId(process.cwd())?.slice(3),
27282
27543
  include_candidates: p.include_candidates ? "1" : "0"
27283
27544
  });
27284
27545
  const rows = p.area ? r.decisions.filter((d) => d.area === canonArea(p.area)) : r.decisions;
27285
27546
  const lines = [];
27286
27547
  lines.push(r.project ? `Decisions for ${r.project}` : "Decisions \u2014 account-wide");
27548
+ if (r.project && r.workspace) {
27549
+ lines.push(`Resolved through the ${r.workspace.replace(/^ws:/, "")} folder group, then the account.`);
27550
+ }
27287
27551
  if (rows.length === 0) {
27288
27552
  lines.push("", p.area ? `Nothing decided for "${p.area}". You are free to choose \u2014 record it afterwards with recall_decision_record.` : "Nothing decided yet.");
27289
27553
  } else {
27290
27554
  lines.push("");
27291
27555
  for (const d of rows) {
27556
+ const from = d.scope === "workspace" ? `the ${d.scope_key.replace(/^ws:/, "")} folder group` : d.scope === "account" ? "the account" : d.scope_key;
27292
27557
  const tags = [
27293
- d.override ? "OVERRIDES ACCOUNT" : "",
27294
- d.inherited ? "inherited from account" : "",
27558
+ d.override ? `OVERRIDES a broader decision` : "",
27559
+ d.inherited ? `inherited from ${from}` : "",
27295
27560
  d.advisory ? "advisory \u2014 a personal preference, not binding" : ""
27296
27561
  ].filter(Boolean).join(" \xB7 ");
27297
27562
  lines.push(`${d.area}: ${d.value}${d.since ? ` (since ${d.since})` : ""}${tags ? ` [${tags}]` : ""}`);
27298
27563
  if (d.why)
27299
27564
  lines.push(` why: ${d.why}`);
27565
+ if (d.by)
27566
+ lines.push(` decided by ${d.by}`);
27300
27567
  const replaced = d.history.filter((h) => !h.current);
27301
- for (const h of replaced)
27302
- lines.push(` replaced: ${h.value} (ended ${h.to})`);
27568
+ for (const h of replaced) {
27569
+ lines.push(` replaced: ${h.value} (ended ${h.to}${h.by ? `, by ${h.by}` : ""})`);
27570
+ }
27303
27571
  if (d.source_session)
27304
27572
  lines.push(` from session ${d.source_session}`);
27305
27573
  }
@@ -28061,7 +28329,7 @@ function installCrashGuards() {
28061
28329
  reportClientEvent2("mcp_crash", { message: err.stack || err.message });
28062
28330
  });
28063
28331
  }
28064
- var RELAY_VERSION = true ? "0.6.8" : "0.0.0";
28332
+ var RELAY_VERSION = true ? "0.6.9" : "0.0.0";
28065
28333
  function daemonIdleSecs() {
28066
28334
  const raw = process.env.CHAT_RECALL_DAEMON_IDLE_SECS;
28067
28335
  if (raw === void 0) return 900;