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/cli.js CHANGED
@@ -5083,978 +5083,978 @@ var init_settings = __esm({
5083
5083
  }
5084
5084
  });
5085
5085
 
5086
- // ../engine/dist/core/home-approval.js
5087
- var home_approval_exports = {};
5088
- __export(home_approval_exports, {
5089
- approveHome: () => approveHome,
5090
- declineHome: () => declineHome,
5091
- grandfatherLegacyHomes: () => grandfatherLegacyHomes,
5092
- homeDecision: () => homeDecision,
5093
- isHomeSynced: () => isHomeSynced,
5094
- normalizeHomePath: () => normalizeHomePath,
5095
- resetHomeDecision: () => resetHomeDecision
5086
+ // ../engine/dist/core/secret-redactor.js
5087
+ var secret_redactor_exports = {};
5088
+ __export(secret_redactor_exports, {
5089
+ DEFAULT_REDACTION_RULES: () => DEFAULT_REDACTION_RULES,
5090
+ _clearServerRulePack: () => _clearServerRulePack,
5091
+ _resetPrivacyCache: () => _resetPrivacyCache,
5092
+ _resetRedactorCache: () => _resetRedactorCache,
5093
+ applyChunkPrivacy: () => applyChunkPrivacy,
5094
+ findContextualSecrets: () => findContextualSecrets,
5095
+ installServerRulePack: () => installServerRulePack,
5096
+ isFilePathRedactionEnabled: () => isFilePathRedactionEnabled,
5097
+ isRedactionEnabled: () => isRedactionEnabled,
5098
+ isToolOutputRedactionEnabled: () => isToolOutputRedactionEnabled,
5099
+ redactFilePaths: () => redactFilePaths,
5100
+ redactSecrets: () => redactSecrets,
5101
+ redactToolResultBody: () => redactToolResultBody,
5102
+ scanTextForFindings: () => scanTextForFindings,
5103
+ serverRulePackVersion: () => serverRulePackVersion,
5104
+ validateRedactionRule: () => validateRedactionRule
5096
5105
  });
5097
- import { existsSync as existsSync3, readdirSync as readdirSync2, realpathSync } from "fs";
5098
- import { homedir as homedir2 } from "os";
5099
- import { join as join3, dirname as dirname3 } from "path";
5100
- function normalizeHomePath(p) {
5101
- const expanded = p.startsWith("~") ? join3(homedir2(), p.slice(1)) : p;
5102
- const trimmed = expanded.replace(/[/\\]+$/, "");
5103
- try {
5104
- return realpathSync(trimmed);
5105
- } catch {
5106
- return trimmed;
5107
- }
5106
+ import { statSync as statSync3 } from "fs";
5107
+ import { createHash } from "crypto";
5108
+ function secretOf(rule, m) {
5109
+ const raw = rule.secret != null ? m[rule.secret] : m[0];
5110
+ if (raw == null)
5111
+ return null;
5112
+ if (rule.where && !rule.where(raw))
5113
+ return null;
5114
+ return raw;
5108
5115
  }
5109
- function legacyAutoSyncedHomes() {
5110
- const home = homedir2();
5111
- const out = [];
5112
- const add = (dir, marker) => {
5113
- if (!existsSync3(join3(dir, marker)))
5114
- return;
5115
- const n = normalizeHomePath(dir);
5116
- if (!out.includes(n))
5117
- out.push(n);
5118
- };
5119
- const bases = [
5120
- { base: join3(home, ".claude"), marker: "projects" },
5121
- { base: join3(home, ".gemini"), marker: "tmp" },
5122
- { base: join3(home, ".codex"), marker: "sessions" },
5123
- { base: join3(home, ".gemini", "antigravity-cli"), marker: "brain" },
5124
- { base: join3(home, ".local", "share", "opencode"), marker: "opencode.db" },
5125
- { base: join3(home, ".cursor"), marker: "chats" }
5126
- ];
5127
- for (const { base: base3, marker } of bases) {
5128
- add(base3, marker);
5129
- const parent = dirname3(base3);
5130
- const leaf = base3.slice(parent.length + 1);
5131
- try {
5132
- for (const e of readdirSync2(parent, { withFileTypes: true })) {
5133
- if (!e.isDirectory())
5134
- continue;
5135
- if (e.name === leaf || !e.name.startsWith(leaf + "-"))
5136
- continue;
5137
- if (e.name === ".claude-code")
5138
- continue;
5139
- add(join3(parent, e.name), marker);
5116
+ function looksLikeSecretToken(t) {
5117
+ if (t.length < 32)
5118
+ return false;
5119
+ if (/^[a-f0-9]+$/i.test(t))
5120
+ return true;
5121
+ return /[A-Z]/.test(t) && /[a-z]/.test(t) && /[0-9]/.test(t);
5122
+ }
5123
+ function findContextualSecrets(text) {
5124
+ if (!text)
5125
+ return [];
5126
+ const ranges = [];
5127
+ let m;
5128
+ SECRET_CONTEXT_RE.lastIndex = 0;
5129
+ while ((m = SECRET_CONTEXT_RE.exec(text)) !== null) {
5130
+ const winStart = m.index + m[0].length;
5131
+ const window = text.slice(winStart, winStart + 80);
5132
+ HIGH_ENTROPY_TOKEN_RE.lastIndex = 0;
5133
+ let tm;
5134
+ while ((tm = HIGH_ENTROPY_TOKEN_RE.exec(window)) !== null) {
5135
+ if (!looksLikeSecretToken(tm[0]))
5136
+ continue;
5137
+ 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))) {
5138
+ continue;
5140
5139
  }
5141
- } catch {
5140
+ ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
5141
+ break;
5142
5142
  }
5143
5143
  }
5144
- return out;
5145
- }
5146
- function grandfatherLegacyHomes() {
5147
- let s;
5148
- try {
5149
- s = loadSettings();
5150
- } catch {
5151
- return { seeded: [] };
5152
- }
5153
- if (s.sources.homesGrandfathered)
5154
- return { seeded: [] };
5155
- const legacy = legacyAutoSyncedHomes();
5156
- const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
5157
- const seeded = [];
5158
- for (const p of legacy) {
5159
- if (!approved.has(p)) {
5160
- approved.add(p);
5161
- seeded.push(p);
5144
+ ranges.sort((a, b) => a.start - b.start);
5145
+ const out = [];
5146
+ for (const r of ranges) {
5147
+ const last = out[out.length - 1];
5148
+ if (last && r.start < last.end) {
5149
+ last.end = Math.max(last.end, r.end);
5150
+ continue;
5162
5151
  }
5152
+ out.push({ ...r });
5163
5153
  }
5164
- s.sources.approvedHomes = [...approved];
5165
- s.sources.homesGrandfathered = true;
5154
+ return out;
5155
+ }
5156
+ function isEnvRedactionRequested() {
5157
+ const v = process.env.CHAT_RECALL_REDACT_INDEX;
5158
+ if (v === void 0)
5159
+ return null;
5160
+ return v === "1" || v === "true" || v === "yes";
5161
+ }
5162
+ function settingsView() {
5163
+ let mtimeMs = -1;
5166
5164
  try {
5167
- saveSettings(s);
5165
+ mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5168
5166
  } catch {
5169
- return { seeded: [] };
5170
5167
  }
5171
- return { seeded };
5172
- }
5173
- function primaryHomes() {
5174
- const home = homedir2();
5175
- const env = process.env;
5176
- let s;
5168
+ if (cache && cache.mtimeMs === mtimeMs)
5169
+ return cache;
5170
+ let enabled = false;
5171
+ const extra = [];
5177
5172
  try {
5178
- s = loadSettings().sources;
5173
+ const s = loadSettings();
5174
+ enabled = !!s.privacy?.redactIndex;
5175
+ for (const r of s.privacy?.redactionRules ?? []) {
5176
+ try {
5177
+ extra.push({ label: r.label, pattern: new RegExp(r.pattern, "g") });
5178
+ } catch {
5179
+ }
5180
+ }
5179
5181
  } catch {
5180
- s = void 0;
5181
5182
  }
5182
- const opencodeDb = env.CHAT_RECALL_OPENCODE_DB || s?.opencodeDbPath || join3(home, ".local", "share", "opencode", "opencode.db");
5183
- const candidates2 = [
5184
- env.CHAT_RECALL_CLAUDE_HOME || s?.claudeHome || join3(home, ".claude"),
5185
- env.CHAT_RECALL_GEMINI_HOME || s?.geminiHome || join3(home, ".gemini"),
5186
- env.CHAT_RECALL_CODEX_HOME || s?.codexHome || join3(home, ".codex"),
5187
- env.CHAT_RECALL_AGY_HOME || s?.agyHome || join3(home, ".gemini", "antigravity-cli"),
5188
- env.CHAT_RECALL_CURSOR_HOME || s?.cursorHome || join3(home, ".cursor"),
5189
- dirOf(opencodeDb)
5190
- ];
5191
- return candidates2.map(normalizeHomePath);
5183
+ cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
5184
+ return cache;
5192
5185
  }
5193
- function homeDecision(path) {
5194
- const p = normalizeHomePath(path);
5195
- if (primaryHomes().includes(p))
5196
- return "primary";
5197
- let s;
5186
+ function compileServerRule(spec) {
5187
+ const name = spec.name || "(unnamed)";
5188
+ if (!spec.regex)
5189
+ return { name, reason: "empty pattern" };
5190
+ if (spec.regex.length > MAX_PATTERN_LEN)
5191
+ return { name, reason: `pattern longer than ${MAX_PATTERN_LEN} chars` };
5192
+ let re;
5198
5193
  try {
5199
- s = loadSettings().sources;
5200
- } catch {
5201
- return "pending";
5194
+ const flags = "g" + (spec.flags || "").replace(/[^ims]/g, "");
5195
+ re = new RegExp(spec.regex, flags);
5196
+ } catch (e) {
5197
+ return { name, reason: `invalid regex: ${e instanceof Error ? e.message : e}` };
5202
5198
  }
5203
- if ((s.declinedHomes ?? []).map(normalizeHomePath).includes(p))
5204
- return "declined";
5205
- if ((s.approvedHomes ?? []).map(normalizeHomePath).includes(p))
5206
- return "approved";
5207
- if ((s.extraClaudeHomes ?? []).map(normalizeHomePath).includes(p))
5208
- return "approved";
5209
- return "pending";
5199
+ if (re.test(""))
5200
+ return { name, reason: "matches the empty string (would redact everything)" };
5201
+ re.lastIndex = 0;
5202
+ if (re.test(CANARY))
5203
+ return { name, reason: "matches benign text \u2014 too broad" };
5204
+ re.lastIndex = 0;
5205
+ return { label: `${spec.source === "pack" ? "pack" : "tenant"}:${name}`, pattern: re };
5210
5206
  }
5211
- function isHomeSynced(path) {
5212
- const d = homeDecision(path);
5213
- return d === "primary" || d === "approved";
5207
+ function validateRedactionRule(spec) {
5208
+ const out = compileServerRule(spec);
5209
+ return "reason" in out ? { ok: false, reason: out.reason } : { ok: true };
5214
5210
  }
5215
- function setDecision(path, decision) {
5216
- const p = normalizeHomePath(path);
5217
- let s;
5218
- try {
5219
- s = loadSettings();
5220
- } catch {
5221
- return false;
5222
- }
5223
- const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
5224
- const declined = new Set((s.sources.declinedHomes ?? []).map(normalizeHomePath));
5225
- approved.delete(p);
5226
- declined.delete(p);
5227
- (decision === "approved" ? approved : declined).add(p);
5228
- s.sources.approvedHomes = [...approved];
5229
- s.sources.declinedHomes = [...declined];
5230
- try {
5231
- saveSettings(s);
5232
- } catch {
5233
- return false;
5211
+ function installServerRulePack(pack) {
5212
+ const rejected = [];
5213
+ const rules = [];
5214
+ for (const spec of pack.rules || []) {
5215
+ if (rules.length >= MAX_PACK_RULES) {
5216
+ rejected.push({ name: spec.name || "(unnamed)", reason: `pack exceeds ${MAX_PACK_RULES} rules` });
5217
+ continue;
5218
+ }
5219
+ const out = compileServerRule(spec);
5220
+ if ("reason" in out)
5221
+ rejected.push(out);
5222
+ else
5223
+ rules.push(out);
5234
5224
  }
5235
- return true;
5225
+ const version2 = pack.version || "";
5226
+ serverPack = { version: version2, rules };
5227
+ return { version: version2, accepted: rules.length, rejected };
5236
5228
  }
5237
- function approveHome(path) {
5238
- return setDecision(path, "approved");
5229
+ function serverRulePackVersion() {
5230
+ return serverPack ? serverPack.version || "unversioned" : null;
5239
5231
  }
5240
- function declineHome(path) {
5241
- return setDecision(path, "declined");
5232
+ function _clearServerRulePack() {
5233
+ serverPack = null;
5242
5234
  }
5243
- function resetHomeDecision(path) {
5244
- const p = normalizeHomePath(path);
5245
- let s;
5246
- try {
5247
- s = loadSettings();
5248
- } catch {
5249
- return false;
5250
- }
5251
- s.sources.approvedHomes = (s.sources.approvedHomes ?? []).filter((x) => normalizeHomePath(x) !== p);
5252
- s.sources.declinedHomes = (s.sources.declinedHomes ?? []).filter((x) => normalizeHomePath(x) !== p);
5253
- try {
5254
- saveSettings(s);
5255
- } catch {
5256
- return false;
5257
- }
5258
- return true;
5235
+ function activeRules() {
5236
+ const base3 = settingsView().rules;
5237
+ return serverPack && serverPack.rules.length > 0 ? [...base3, ...serverPack.rules] : base3;
5259
5238
  }
5260
- var dirOf;
5261
- var init_home_approval = __esm({
5262
- "../engine/dist/core/home-approval.js"() {
5263
- "use strict";
5264
- init_settings();
5265
- dirOf = (filePath) => dirname3(filePath);
5266
- }
5267
- });
5268
-
5269
- // ../engine/dist/core/tool-paths.js
5270
- import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
5271
- import { homedir as homedir3 } from "os";
5272
- import { join as join4, dirname as dirname4, basename } from "path";
5273
- function claudeHomeDir() {
5274
- return process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome || join4(homedir3(), ".claude");
5239
+ function isRedactionEnabled() {
5240
+ const env = isEnvRedactionRequested();
5241
+ if (env !== null)
5242
+ return env;
5243
+ return settingsView().enabled;
5275
5244
  }
5276
- function claudeProjectDirs(opts = {}) {
5277
- const home = homedir3();
5278
- const dirs = [];
5279
- const addHome = (d) => {
5280
- const p = join4(d, "projects");
5281
- if (existsSync4(p) && !dirs.includes(p))
5282
- dirs.push(p);
5283
- };
5284
- addHome(claudeHomeDir());
5285
- const homeOverridden = !!(process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome);
5286
- if (!homeOverridden) {
5287
- try {
5288
- for (const entry of readdirSync3(home, { withFileTypes: true })) {
5289
- if (entry.isDirectory() && entry.name.startsWith(".claude-") && entry.name !== ".claude-code") {
5290
- addHome(join4(home, entry.name));
5291
- }
5292
- }
5293
- } catch {
5294
- }
5245
+ function _resetRedactorCache() {
5246
+ cache = null;
5247
+ }
5248
+ function redactSecrets(text, opts = {}) {
5249
+ if (!opts.force && !isRedactionEnabled())
5250
+ return text;
5251
+ if (!text)
5252
+ return text;
5253
+ const rules = opts.rules || activeRules();
5254
+ let out = text;
5255
+ for (const r of rules) {
5256
+ out = out.replace(r.pattern, (...args) => {
5257
+ const whole = args[0];
5258
+ const m = args.slice(0, args.length);
5259
+ const secret = secretOf(r, m);
5260
+ if (secret == null)
5261
+ return whole;
5262
+ if (opts.count)
5263
+ opts.count.redactions++;
5264
+ return whole.slice(0, whole.length - secret.length) + `[REDACTED:${r.label}]`;
5265
+ });
5295
5266
  }
5296
- if (process.env.CLAUDE_DIRS) {
5297
- for (const d of process.env.CLAUDE_DIRS.split(",")) {
5298
- const t = d.trim();
5299
- if (t)
5300
- addHome(t.startsWith("~/") ? join4(home, t.slice(2)) : t);
5301
- }
5267
+ const ctx = findContextualSecrets(out);
5268
+ for (let i = ctx.length - 1; i >= 0; i--) {
5269
+ const { start, end } = ctx[i];
5270
+ if (opts.count)
5271
+ opts.count.redactions++;
5272
+ out = out.slice(0, start) + "[REDACTED:secret-context]" + out.slice(end);
5302
5273
  }
5303
- if (dirs.length === 0)
5304
- dirs.push(join4(claudeHomeDir(), "projects"));
5305
- return applyDecisions(dirs, opts.includeExcluded);
5306
- }
5307
- function _setSourceExclusionFilter(fn) {
5308
- _sourceExclusionFilter = fn;
5309
- }
5310
- function geminiHomeDir() {
5311
- return process.env.CHAT_RECALL_GEMINI_HOME || sources().geminiHome || join4(homedir3(), ".gemini");
5312
- }
5313
- function opencodeDbPath() {
5314
- return process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath || join4(homedir3(), ".local", "share", "opencode", "opencode.db");
5315
- }
5316
- function codexHomeDir() {
5317
- return process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome || join4(homedir3(), ".codex");
5318
- }
5319
- function agyHomeDir() {
5320
- return process.env.CHAT_RECALL_AGY_HOME || sources().agyHome || join4(homedir3(), ".gemini", "antigravity-cli");
5274
+ return out;
5321
5275
  }
5322
- function cursorHomeDir() {
5323
- return process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome || join4(homedir3(), ".cursor");
5276
+ function maskSecret(s) {
5277
+ if (!s)
5278
+ return "";
5279
+ if (s.length <= 4)
5280
+ return "*".repeat(s.length);
5281
+ return "*".repeat(Math.min(s.length - 4, 60)) + s.slice(-4);
5324
5282
  }
5325
- function cursorIdeHomeDir() {
5326
- return process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome || (process.platform === "darwin" ? join4(homedir3(), "Library", "Application Support", "Cursor") : join4(homedir3(), ".config", "Cursor"));
5283
+ function globalRegexFor(pattern) {
5284
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
5285
+ const key2 = pattern.source + "\0" + flags;
5286
+ let re = globalRegexCache.get(key2);
5287
+ if (!re) {
5288
+ re = new RegExp(pattern.source, flags);
5289
+ globalRegexCache.set(key2, re);
5290
+ }
5291
+ return re;
5327
5292
  }
5328
- function siblingHomes(base3, overridden, suffix) {
5329
- const roots = [];
5330
- const add = (home) => {
5331
- const root = suffix ? join4(home, suffix) : home;
5332
- if (existsSync4(root) && !roots.includes(root))
5333
- roots.push(root);
5293
+ function makeLineResolver(text) {
5294
+ const starts = [];
5295
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1))
5296
+ starts.push(i);
5297
+ return (index) => {
5298
+ let lo = 0, hi = starts.length;
5299
+ while (lo < hi) {
5300
+ const mid = lo + hi >> 1;
5301
+ if (starts[mid] < index)
5302
+ lo = mid + 1;
5303
+ else
5304
+ hi = mid;
5305
+ }
5306
+ return lo + 1;
5334
5307
  };
5335
- add(base3);
5336
- if (!overridden) {
5337
- const parent = dirname4(base3);
5338
- const leaf = basename(base3);
5339
- try {
5340
- for (const entry of readdirSync3(parent, { withFileTypes: true })) {
5341
- if (!entry.isDirectory())
5342
- continue;
5343
- if (entry.name === leaf || !entry.name.startsWith(leaf + "-"))
5344
- continue;
5345
- add(join4(parent, entry.name));
5308
+ }
5309
+ function scanTextForFindings(text, opts = {}) {
5310
+ if (!text)
5311
+ return [];
5312
+ const rules = opts.rules || activeRules();
5313
+ const findings = [];
5314
+ const seenSpans = /* @__PURE__ */ new Set();
5315
+ const lineAt = makeLineResolver(text);
5316
+ for (const r of rules) {
5317
+ const re = globalRegexFor(r.pattern);
5318
+ re.lastIndex = 0;
5319
+ let m;
5320
+ while ((m = re.exec(text)) !== null) {
5321
+ const secret = secretOf(r, m);
5322
+ if (secret == null) {
5323
+ if (re.lastIndex === m.index)
5324
+ re.lastIndex++;
5325
+ continue;
5346
5326
  }
5347
- } catch {
5327
+ const at = m.index + (m[0].length - secret.length);
5328
+ const span = `${at}:${secret.length}`;
5329
+ if (!seenSpans.has(span)) {
5330
+ seenSpans.add(span);
5331
+ findings.push({ rule: r.label, line: lineAt(at), preview: maskSecret(secret) });
5332
+ }
5333
+ if (re.lastIndex === m.index)
5334
+ re.lastIndex++;
5348
5335
  }
5349
5336
  }
5350
- return roots;
5337
+ for (const { start, end } of findContextualSecrets(text)) {
5338
+ const line = lineAt(start);
5339
+ findings.push({ rule: "secret-context", line, preview: maskSecret(text.slice(start, end)) });
5340
+ }
5341
+ return findings;
5351
5342
  }
5352
- function applyDecisions(roots, includeExcluded) {
5353
- if (roots.length === 0)
5354
- return roots;
5355
- let kept = roots;
5356
- if (!includeExcluded) {
5357
- try {
5358
- grandfatherLegacyHomes();
5359
- const approved = kept.filter((r) => isHomeSynced(dirname4(r)));
5360
- if (approved.length > 0)
5361
- kept = approved;
5362
- } catch {
5363
- }
5343
+ function privacyView() {
5344
+ let mtimeMs = -1;
5345
+ try {
5346
+ mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5347
+ } catch {
5364
5348
  }
5365
- if (!includeExcluded && _sourceExclusionFilter) {
5366
- const notExcluded = kept.filter((r) => !_sourceExclusionFilter(r));
5367
- if (notExcluded.length > 0)
5368
- kept = notExcluded;
5349
+ if (privacyCache && privacyCache.mtimeMs === mtimeMs)
5350
+ return privacyCache;
5351
+ let redactToolOutputs = false, redactFilePaths2 = false;
5352
+ try {
5353
+ const s = loadSettings();
5354
+ redactToolOutputs = !!s.privacy?.redactToolOutputs;
5355
+ redactFilePaths2 = !!s.privacy?.redactFilePaths;
5356
+ } catch {
5369
5357
  }
5370
- return kept;
5371
- }
5372
- function applyExclusions(roots, includeExcluded) {
5373
- return applyDecisions(roots, includeExcluded);
5374
- }
5375
- function codexSessionDirs(opts = {}) {
5376
- const overridden = !!(process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome);
5377
- return applyExclusions(siblingHomes(codexHomeDir(), overridden, "sessions"), opts.includeExcluded);
5358
+ privacyCache = { mtimeMs, redactToolOutputs, redactFilePaths: redactFilePaths2 };
5359
+ return privacyCache;
5378
5360
  }
5379
- function agyBrainDirs(opts = {}) {
5380
- const overridden = !!(process.env.CHAT_RECALL_AGY_HOME || sources().agyHome);
5381
- return applyExclusions(siblingHomes(agyHomeDir(), overridden, "brain"), opts.includeExcluded);
5361
+ function _resetPrivacyCache() {
5362
+ privacyCache = null;
5382
5363
  }
5383
- function cursorChatDirs(opts = {}) {
5384
- const overridden = !!(process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome);
5385
- return applyExclusions(siblingHomes(cursorHomeDir(), overridden, "chats"), opts.includeExcluded);
5364
+ function isToolOutputRedactionEnabled() {
5365
+ return privacyView().redactToolOutputs;
5386
5366
  }
5387
- function cursorIdeWorkspaceDirs(opts = {}) {
5388
- const overridden = !!(process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome);
5389
- return applyExclusions(siblingHomes(cursorIdeHomeDir(), overridden, join4("User", "workspaceStorage")), opts.includeExcluded);
5367
+ function isFilePathRedactionEnabled() {
5368
+ return privacyView().redactFilePaths;
5390
5369
  }
5391
- function cursorIdeGlobalDbs(opts = {}) {
5392
- const overridden = !!(process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome);
5393
- const roots = applyExclusions(siblingHomes(cursorIdeHomeDir(), overridden, join4("User", "globalStorage")), opts.includeExcluded);
5394
- const dbs = [];
5395
- for (const r of roots) {
5396
- const p = join4(r, "state.vscdb");
5397
- if (existsSync4(p) && !dbs.includes(p))
5398
- dbs.push(p);
5370
+ function redactFilePaths(text) {
5371
+ if (!text)
5372
+ return text;
5373
+ return text.replace(PATH_REGEX, (m) => {
5374
+ if (m.startsWith("[REDACTED:"))
5375
+ return m;
5376
+ const h = createHash("sha256").update(m).digest("hex").slice(0, 12);
5377
+ return `[path:${h}]`;
5378
+ });
5379
+ }
5380
+ function applyChunkPrivacy(text) {
5381
+ let out = redactSecrets(text);
5382
+ if (privacyView().redactFilePaths)
5383
+ out = redactFilePaths(out);
5384
+ return out;
5385
+ }
5386
+ function redactToolResultBody(toolName, body) {
5387
+ if (!body)
5388
+ return body;
5389
+ return `[redacted: ${toolName} output, ${body.length} chars]`;
5390
+ }
5391
+ var DEFAULT_REDACTION_RULES, SECRET_CONTEXT_RE, HIGH_ENTROPY_TOKEN_RE, cache, CANARY, MAX_PACK_RULES, MAX_PATTERN_LEN, serverPack, globalRegexCache, privacyCache, PATH_REGEX;
5392
+ var init_secret_redactor = __esm({
5393
+ "../engine/dist/core/secret-redactor.js"() {
5394
+ "use strict";
5395
+ init_settings();
5396
+ DEFAULT_REDACTION_RULES = [
5397
+ // AWS access tokens. Prefix-anchored: AKIA, ASIA, ABIA (long-term,
5398
+ // STS, EC2). Always exactly 16 hex/alphanumeric after the prefix.
5399
+ // NOTE: this matches only the access-key-ID, NOT the secret key value —
5400
+ // the `env-secret` rule below covers AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN.
5401
+ { label: "aws-access-token", pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
5402
+ // AWS STS session tokens. These are long base64 blobs with NO `NAME=` context
5403
+ // when pasted bare into prose/code, so `env-secret` misses them — but they
5404
+ // reliably start with one of a handful of base64 prefixes (the encoded
5405
+ // `{"...` JSON header). Prefix-anchored → low false-positive. This is the
5406
+ // bare-value form that leaked (`search('IQoJEXAMPLEfakeSESSIONtokenNOTreal01234567890abcDEF==...')`).
5407
+ { label: "aws-session-token", pattern: /\b(?:IQoJ|FwoG|FwoH|FQoG|FQoH|FwoD)[A-Za-z0-9/+]{40,}={0,2}/g },
5408
+ // Env/shell-assignment secrets — a VAR whose name ends in KEY/SECRET/TOKEN/
5409
+ // PASSWORD/CREDENTIAL, then `=`/`:`, then the value. Zero-width lookbehind so
5410
+ // only the VALUE is redacted (the var name stays for context). This is the
5411
+ // dominant real-world leak vector (`export` dumps, .env files, `curl -H`,
5412
+ // tool output) and is exactly what leaked AWS_SECRET_ACCESS_KEY and
5413
+ // AWS_SESSION_TOKEN in cleartext while the access-key-ID rule above caught
5414
+ // only the (least-sensitive) ID.
5415
+ // Suffix list deliberately broad: DB_PASS (our own helm chart's name!),
5416
+ // NPM_AUTH, SENTRY_DSN, SESSION/COOKIE secrets and BEARER vars all leaked
5417
+ // past the original KEY/SECRET/TOKEN/PASSWORD-only list. Values under 12
5418
+ // chars don't redact, so flag-ish vars (BYPASS=true) stay readable.
5419
+ // Context CONSUMED, secret in group 1 — see RedactionRule.secret. As a
5420
+ // lookbehind this was the second most expensive rule in the collector.
5421
+ { label: "env-secret", secret: 1, pattern: /\b[A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL|AUTH|DSN|SESSION|COOKIE|BEARER|SIGNATURE)S?["']?[ \t]{0,3}[:=][ \t]{0,3}["']?([A-Za-z0-9/+_.~=:@-]{12,})/g },
5422
+ // Credentials embedded in a connection URL: scheme://user:PASSWORD@host.
5423
+ // Redacts only the password segment (keeps scheme/user/host for context).
5424
+ //
5425
+ // Scheme-agnostic on purpose. The old rule allowlisted eight DB schemes, so
5426
+ // `https://user:pass@host`, `ssh://`, `ftp://`, `smtp://` and every other
5427
+ // userinfo URL walked through in cleartext — and a `jdbc:postgresql://…`
5428
+ // prefix broke the \b anchor for the one family it did cover. Any scheme
5429
+ // followed by userinfo is a credential, so match the shape, not a list.
5430
+ { label: "url-password", secret: 1, pattern: /\b[a-zA-Z][a-zA-Z0-9+.-]{1,31}:\/\/[^:@\s/]{1,64}:([^@\s/]{1,256})(?=@)/g },
5431
+ // Bare AWS secret-access-key VALUE: EXACTLY 40 base64 chars, word-bounded,
5432
+ // containing upper+lower+digit. This is the gitleaks heuristic — the mixed-case
5433
+ // requirement excludes 40-char lowercase-hex git SHAs, and the exact-40 bound
5434
+ // excludes longer blobs/tokens. It catches the value when it appears bare in
5435
+ // prose with NO `NAME=` and NO nearby context word (e.g. inside a markdown
5436
+ // table or quoted in analysis) — the residual leak path the env-secret and
5437
+ // contextual rules both miss. Placed AFTER env-secret/url-password so the
5438
+ // keyed/URL forms keep their more-specific labels.
5439
+ // The three stacked lookaheads each re-scanned the same 40 characters, which
5440
+ // made this the MOST expensive rule of all 106 the collector runs (13.1% of a
5441
+ // 29.5 MB scan). The mixed-case-plus-digit test is now one JS pass.
5442
+ {
5443
+ label: "aws-secret-key",
5444
+ secret: 1,
5445
+ pattern: /(?<![A-Za-z0-9/+])([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+])/g,
5446
+ where: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) && /[0-9]/.test(v)
5447
+ },
5448
+ // GitHub personal access tokens.
5449
+ { label: "github-pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
5450
+ // GitHub fine-grained PATs.
5451
+ { label: "github-fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
5452
+ // GitHub OAuth.
5453
+ { label: "github-oauth", pattern: /\bgho_[A-Za-z0-9]{36}\b/g },
5454
+ // GitHub app + refresh.
5455
+ { label: "github-app", pattern: /\b(?:ghs|ghu|ghr)_[A-Za-z0-9]{36}\b/g },
5456
+ // Slack bot/user/app tokens.
5457
+ { label: "slack-token", pattern: /\bxox[abopr]-[A-Za-z0-9-]{10,}\b/g },
5458
+ // Slack incoming webhook URL.
5459
+ { label: "slack-webhook", pattern: /\bhttps:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+\b/g },
5460
+ // Stripe live + test keys.
5461
+ { label: "stripe-key", pattern: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
5462
+ // Anthropic.
5463
+ { label: "anthropic-key", pattern: /\bsk-ant-[a-zA-Z0-9_-]{40,}\b/g },
5464
+ // OpenAI. Two shapes, deliberately kept apart. The classic key is
5465
+ // `sk-` + base62 with NO hyphens; the modern scoped keys carry an explicit
5466
+ // proj/svcacct/admin segment first. Allowing hyphens after a bare `sk-`
5467
+ // (which is what admitted sk-proj- in the first place) also matches ordinary
5468
+ // kebab-case text — `sk-fix-the-login-redirect-bug`, `.sk-button-primary`,
5469
+ // `sk-2024-01-01-backup` — and this corpus is full of branch names and CSS
5470
+ // classes. Requiring the segment keeps sk-proj- coverage without that.
5471
+ { label: "openai-key", pattern: /\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}\b|\bsk-[A-Za-z0-9]{20,}\b/g },
5472
+ // Google API key (AIza…).
5473
+ { label: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
5474
+ // Generic JWT — three base64url segments.
5475
+ { label: "jwt", pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
5476
+ // Private key blocks (PEM). The label is a free word run rather than a fixed
5477
+ // list, so ENCRYPTED PRIVATE KEY and PGP PRIVATE KEY BLOCK are covered
5478
+ // without another enumeration, and \1 forces END to name the same kind as
5479
+ // BEGIN.
5480
+ //
5481
+ // The body is [^-], NOT [\s\S]*?. That is a performance boundary, not a
5482
+ // style choice. An unanchored prefix with an unbounded lazy gap re-scans to
5483
+ // end-of-input from every candidate start, so text carrying many BEGIN
5484
+ // markers and no matching END costs O(n^2): 500KB of that measured 810ms
5485
+ // here, and this runs synchronously on the server's event loop over
5486
+ // tenant-supplied text (services/secret-rescan.ts). A PEM body is base64
5487
+ // plus headers and contains no '-', so excluding it makes the scan stop at
5488
+ // the next delimiter instead of at EOF — same matches, linear cost.
5489
+ { label: "private-key", pattern: /-----BEGIN ([A-Z0-9 ]{0,32})PRIVATE KEY(?: BLOCK)?-----[^-]{0,10000}?-----END \1PRIVATE KEY(?: BLOCK)?-----/g },
5490
+ // PuTTY keys are not PEM. Match the Private-Lines payload — the actual key
5491
+ // material — rather than spanning the whole file, which would need the same
5492
+ // unbounded gap the rule above just removed. Line-anchored and base64-only,
5493
+ // so it cannot run away.
5494
+ { label: "putty-key", pattern: /^Private-Lines:[ \t]*\d{1,4}[\r\n]+(?:[A-Za-z0-9+/=]{8,120}[\r\n]+){1,400}/gm },
5495
+ // npm tokens (uuid-shaped — overlap with our UUID filter; intentional).
5496
+ { label: "npm-token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/g },
5497
+ // .npmrc auth lines: //registry.npmjs.org/:_authToken=VALUE (the key is not
5498
+ // ALL-CAPS, so the env-secret rule above never matched it). The leading `:`
5499
+ // is required — it is what makes this npmrc syntax rather than any shell var
5500
+ // ending in _AUTH, which env-secret already owns and labels better.
5501
+ // [^\s"'`]+ so the closing quote is not swallowed, and a ${VAR} placeholder
5502
+ // is left alone — redacting it destroys a config example and hides nothing.
5503
+ // The rule the perf profile caught the wedged daemon inside: 98.94% of samples.
5504
+ { label: "npmrc-auth", secret: 1, pattern: /:_(?:authToken|password|auth)[ \t]*=[ \t]*(?!\$\{)([^\s"'`]{12,})/gi },
5505
+ // HTTP Authorization headers. env-secret requires an ALL-CAPS name, and
5506
+ // `Authorization` is mixed case, so both Bearer and Basic walked straight
5507
+ // through — including short opaque tokens the contextual pass also misses
5508
+ // (it needs 32+ chars AND mixed-case-plus-digit).
5509
+ // Same two guards as npmrc-auth: stop at a quote, and skip the obvious
5510
+ // placeholders that fill documentation (<YOUR_TOKEN>, ${TOKEN}, xxx…).
5511
+ { label: "auth-header", secret: 1, pattern: /\bauthorization[ \t]*:[ \t]*(?:bearer|basic|token)[ \t]+(?![<$])([^\s"'`<]{8,})/gi },
5512
+ // Vendor tokens with unambiguous prefixes. Each is a fixed literal, so a
5513
+ // false positive is essentially impossible and the cost of listing is low.
5514
+ { label: "gitlab-pat", pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
5515
+ { label: "slack-app-token", pattern: /\bxapp-\d-[A-Z0-9]{8,}-\d{10,}-[a-f0-9]{32,}\b/g },
5516
+ { label: "sendgrid-key", pattern: /\bSG\.[A-Za-z0-9_-]{20,24}\.[A-Za-z0-9_-]{40,50}\b/g },
5517
+ // (?<![-\w]) so `cache-key-<md5>` is not read as a mailgun key.
5518
+ { label: "mailgun-key", pattern: /(?<![-\w])key-[a-f0-9]{32}\b/g },
5519
+ { label: "digitalocean-pat", pattern: /\bdop_v1_[a-f0-9]{64}\b/g },
5520
+ { label: "huggingface-token", pattern: /\bhf_[A-Za-z0-9]{30,}\b/g },
5521
+ { label: "shopify-token", pattern: /\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}\b/g },
5522
+ { label: "pypi-token", pattern: /\bpypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{50,}\b/g },
5523
+ // No twilio-sid rule: AC…/SK… are Twilio SIDs, which are public account and
5524
+ // key identifiers, not the auth token. Redacting them hid nothing and
5525
+ // collided with 32-hex etags and MD5 sums.
5526
+ // No discord-bot-token rule either: the shape (word.word.word) matched
5527
+ // ordinary dotted symbols such as a.b.ApplicationContextProviderImpl.
5528
+ { label: "telegram-bot-token", pattern: /\b\d{8,10}:AA[A-Za-z0-9_-]{32,}\b/g },
5529
+ { label: "doppler-token", pattern: /\bdp\.(?:pt|st|sa|ct)\.[A-Za-z0-9_-]{20,}\b/g },
5530
+ // Azure Storage SAS signature — the `sig=` parameter is the actual secret.
5531
+ { label: "azure-sas", secret: 1, pattern: /[?&]sig=([A-Za-z0-9%/+=]{20,})/g }
5532
+ ];
5533
+ SECRET_CONTEXT_RE = /\b(?:secret|password|passwd|credential|access[ _-]?key|private[ _-]?key|api[ _-]?key|auth(?:oriz\w*)?|bearer|token)\b/gi;
5534
+ HIGH_ENTROPY_TOKEN_RE = /[A-Za-z0-9/+_-]{32,}/g;
5535
+ cache = null;
5536
+ CANARY = "the quick brown fox jumps over the lazy dog 0123456789 commit 8ba1f109551bd432803012645ac136ddd475d0fb src/index.ts:42";
5537
+ MAX_PACK_RULES = 200;
5538
+ MAX_PATTERN_LEN = 512;
5539
+ serverPack = null;
5540
+ globalRegexCache = /* @__PURE__ */ new Map();
5541
+ privacyCache = null;
5542
+ PATH_REGEX = /(?<![A-Za-z0-9_])(?:\/[^\s'"`<>]+|[A-Za-z]:\\[^\s'"`<>]+)/g;
5543
+ }
5544
+ });
5545
+
5546
+ // ../engine/dist/parsers/chunker.js
5547
+ function stripInjectedBanners(text) {
5548
+ let result = text;
5549
+ for (const re of INJECTED_BANNERS)
5550
+ result = result.replace(re, " ");
5551
+ return result;
5552
+ }
5553
+ function cleanText(text) {
5554
+ let result = text.replace(/<system-reminder>.*?<\/system-reminder>/gs, "");
5555
+ result = stripInjectedBanners(result);
5556
+ result = result.replace(/\x1b\[[0-9;]*m/g, "");
5557
+ result = result.replace(/\s+/g, " ");
5558
+ return result.trim();
5559
+ }
5560
+ function windowText(text, maxChars, maxWindows = 12) {
5561
+ const clean = cleanText(text);
5562
+ if (clean.length <= maxChars)
5563
+ return clean ? [clean] : [];
5564
+ const out = [];
5565
+ const overlap = 80;
5566
+ const step = Math.max(1, maxChars - overlap);
5567
+ for (let i = 0; i < clean.length && out.length < maxWindows; i += step) {
5568
+ out.push(clean.slice(i, i + maxChars));
5569
+ }
5570
+ return out;
5571
+ }
5572
+ function chunkSession(entry, content, maxChunks = 80, maxTokensPerChunk = 500) {
5573
+ const chunks = [];
5574
+ const maxChars = maxTokensPerChunk * 4;
5575
+ const baseData = {
5576
+ sessionId: entry.sessionId,
5577
+ projectPath: entry.projectPath,
5578
+ created: entry.created,
5579
+ modified: entry.modified,
5580
+ mtime: entry.fileMtime,
5581
+ firstPrompt: entry.firstPrompt || content.firstPrompt,
5582
+ sourceFile: content.sessionPath
5583
+ };
5584
+ for (let i = 0; i < Math.min(2, content.summaries.length); i++) {
5585
+ const summary = content.summaries[i];
5586
+ if (!summary?.trim())
5587
+ continue;
5588
+ const chunkText = cleanText(summary.slice(0, maxChars));
5589
+ if (chunkText.length > 8e3) {
5590
+ chunks.push({
5591
+ chunkId: `${entry.sessionId}_summary_${i}`,
5592
+ chunkType: "summary",
5593
+ text: chunkText.slice(0, 8e3),
5594
+ sourceLine: 0,
5595
+ ...baseData
5596
+ });
5597
+ } else {
5598
+ chunks.push({
5599
+ chunkId: `${entry.sessionId}_summary_${i}`,
5600
+ chunkType: "summary",
5601
+ text: chunkText,
5602
+ sourceLine: 0,
5603
+ ...baseData
5604
+ });
5605
+ }
5606
+ }
5607
+ if (chunks.length >= maxChunks) {
5608
+ return chunks.slice(0, maxChunks);
5609
+ }
5610
+ const firstPrompt = entry.firstPrompt || content.firstPrompt;
5611
+ if (firstPrompt?.trim()) {
5612
+ const firstLine = content.userMessages[0]?.lineNumber || 0;
5613
+ chunks.push({
5614
+ chunkId: `${entry.sessionId}_first_prompt`,
5615
+ chunkType: "first_prompt",
5616
+ text: cleanText(firstPrompt.slice(0, maxChars)),
5617
+ sourceLine: firstLine,
5618
+ ...baseData
5619
+ });
5620
+ }
5621
+ if (chunks.length >= maxChunks) {
5622
+ return chunks.slice(0, maxChunks);
5623
+ }
5624
+ const userStart = firstPrompt ? 1 : 0;
5625
+ for (let i = userStart; i < content.userMessages.length; i++) {
5626
+ if (chunks.length >= maxChunks)
5627
+ break;
5628
+ const msg = content.userMessages[i];
5629
+ if (!msg.text.trim())
5630
+ continue;
5631
+ const windows = windowText(msg.text, maxChars);
5632
+ for (let w = 0; w < windows.length; w++) {
5633
+ if (chunks.length >= maxChunks)
5634
+ break;
5635
+ chunks.push({
5636
+ chunkId: `${entry.sessionId}_user_${i}_${w}`,
5637
+ chunkType: "user_context",
5638
+ text: windows[w],
5639
+ sourceLine: msg.lineNumber,
5640
+ ...baseData
5641
+ });
5642
+ }
5399
5643
  }
5400
- return dbs;
5401
- }
5402
- function opencodeDbPaths(opts = {}) {
5403
- const primary = opencodeDbPath();
5404
- const overridden = !!(process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath);
5405
- const dbFile = basename(primary);
5406
- const dirs = siblingHomes(dirname4(primary), overridden, "");
5407
- const dbs = [];
5408
- for (const d of dirs) {
5409
- const p = join4(d, dbFile);
5410
- if (existsSync4(p) && !dbs.includes(p))
5411
- dbs.push(p);
5644
+ if (chunks.length >= maxChunks) {
5645
+ return chunks.slice(0, maxChunks);
5412
5646
  }
5413
- if (dbs.length === 0)
5414
- dbs.push(primary);
5415
- return applyExclusions(dbs, opts.includeExcluded);
5416
- }
5417
- function extraClaudeHomeDirs() {
5418
- const extras = sources().extraClaudeHomes ?? [];
5419
- const primary = claudeHomeDir();
5420
- return Array.from(new Set(extras)).filter((p) => p && p !== primary);
5421
- }
5422
- function claudeHomeDirs() {
5423
- const overridden = !!(process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome);
5424
- const homes = siblingHomes(claudeHomeDir(), overridden, "").filter((h) => basename(h) !== ".claude-code");
5425
- for (const extra of extraClaudeHomeDirs()) {
5426
- if (existsSync4(extra) && !homes.includes(extra))
5427
- homes.push(extra);
5647
+ for (let i = 0; i < content.assistantMessages.length; i++) {
5648
+ if (chunks.length >= maxChunks)
5649
+ break;
5650
+ const msg = content.assistantMessages[i];
5651
+ if (!msg.text.trim() || msg.text.length <= 50)
5652
+ continue;
5653
+ const windows = windowText(msg.text, maxChars);
5654
+ for (let w = 0; w < windows.length; w++) {
5655
+ if (chunks.length >= maxChunks)
5656
+ break;
5657
+ chunks.push({
5658
+ chunkId: `${entry.sessionId}_assistant_${i}_${w}`,
5659
+ chunkType: "assistant",
5660
+ text: windows[w],
5661
+ sourceLine: msg.lineNumber,
5662
+ ...baseData
5663
+ });
5664
+ }
5428
5665
  }
5429
- return homes;
5666
+ if (chunks.length >= maxChunks) {
5667
+ return chunks.slice(0, maxChunks);
5668
+ }
5669
+ if (content.toolResults.length > 0) {
5670
+ const webSearches = content.toolResults.filter((r) => r.contentType === "web_search");
5671
+ const otherResults = content.toolResults.filter((r) => r.contentType !== "web_search");
5672
+ const resultsToProcess = [...webSearches.slice(0, 5), ...otherResults.slice(0, 10)];
5673
+ const stripBodies = isToolOutputRedactionEnabled();
5674
+ for (let i = 0; i < resultsToProcess.length; i++) {
5675
+ if (chunks.length >= maxChunks)
5676
+ break;
5677
+ const result = resultsToProcess[i];
5678
+ if (result.text.trim()) {
5679
+ const body = stripBodies && result.contentType !== "web_search" ? redactToolResultBody(result.contentType, result.text) : result.text.slice(0, maxChars);
5680
+ chunks.push({
5681
+ chunkId: `${entry.sessionId}_${result.contentType}_${i}`,
5682
+ chunkType: result.contentType,
5683
+ text: cleanText(body),
5684
+ sourceLine: result.lineNumber,
5685
+ ...baseData
5686
+ });
5687
+ }
5688
+ }
5689
+ }
5690
+ return chunks.slice(0, maxChunks);
5430
5691
  }
5431
- var sources, _sourceExclusionFilter;
5432
- var init_tool_paths = __esm({
5433
- "../engine/dist/core/tool-paths.js"() {
5692
+ var INJECTED_BANNERS;
5693
+ var init_chunker = __esm({
5694
+ "../engine/dist/parsers/chunker.js"() {
5434
5695
  "use strict";
5435
- init_home_approval();
5436
- init_settings();
5437
- sources = loadSourceSettings;
5438
- _sourceExclusionFilter = null;
5696
+ init_secret_redactor();
5697
+ INJECTED_BANNERS = [
5698
+ /MCP issues detected\. ?Run \/mcp list for status\.?/g,
5699
+ /Context low[^\n]*Run \/compact[^\n]*/g,
5700
+ /API Error:[^\n]{0,120}/g
5701
+ ];
5439
5702
  }
5440
5703
  });
5441
5704
 
5442
- // ../engine/dist/core/secret-redactor.js
5443
- var secret_redactor_exports = {};
5444
- __export(secret_redactor_exports, {
5445
- DEFAULT_REDACTION_RULES: () => DEFAULT_REDACTION_RULES,
5446
- _clearServerRulePack: () => _clearServerRulePack,
5447
- _resetPrivacyCache: () => _resetPrivacyCache,
5448
- _resetRedactorCache: () => _resetRedactorCache,
5449
- applyChunkPrivacy: () => applyChunkPrivacy,
5450
- findContextualSecrets: () => findContextualSecrets,
5451
- installServerRulePack: () => installServerRulePack,
5452
- isFilePathRedactionEnabled: () => isFilePathRedactionEnabled,
5453
- isRedactionEnabled: () => isRedactionEnabled,
5454
- isToolOutputRedactionEnabled: () => isToolOutputRedactionEnabled,
5455
- redactFilePaths: () => redactFilePaths,
5456
- redactSecrets: () => redactSecrets,
5457
- redactToolResultBody: () => redactToolResultBody,
5458
- scanTextForFindings: () => scanTextForFindings,
5459
- serverRulePackVersion: () => serverRulePackVersion,
5460
- validateRedactionRule: () => validateRedactionRule
5705
+ // ../engine/dist/core/home-approval.js
5706
+ var home_approval_exports = {};
5707
+ __export(home_approval_exports, {
5708
+ approveHome: () => approveHome,
5709
+ declineHome: () => declineHome,
5710
+ grandfatherLegacyHomes: () => grandfatherLegacyHomes,
5711
+ homeDecision: () => homeDecision,
5712
+ isHomeSynced: () => isHomeSynced,
5713
+ normalizeHomePath: () => normalizeHomePath,
5714
+ resetHomeDecision: () => resetHomeDecision
5461
5715
  });
5462
- import { statSync as statSync3 } from "fs";
5463
- import { createHash } from "crypto";
5464
- function secretOf(rule, m) {
5465
- const raw = rule.secret != null ? m[rule.secret] : m[0];
5466
- if (raw == null)
5467
- return null;
5468
- if (rule.where && !rule.where(raw))
5469
- return null;
5470
- return raw;
5471
- }
5472
- function looksLikeSecretToken(t) {
5473
- if (t.length < 32)
5474
- return false;
5475
- if (/^[a-f0-9]+$/i.test(t))
5476
- return true;
5477
- return /[A-Z]/.test(t) && /[a-z]/.test(t) && /[0-9]/.test(t);
5478
- }
5479
- function findContextualSecrets(text) {
5480
- if (!text)
5481
- return [];
5482
- const ranges = [];
5483
- let m;
5484
- SECRET_CONTEXT_RE.lastIndex = 0;
5485
- while ((m = SECRET_CONTEXT_RE.exec(text)) !== null) {
5486
- const winStart = m.index + m[0].length;
5487
- const window = text.slice(winStart, winStart + 80);
5488
- HIGH_ENTROPY_TOKEN_RE.lastIndex = 0;
5489
- let tm;
5490
- while ((tm = HIGH_ENTROPY_TOKEN_RE.exec(window)) !== null) {
5491
- if (!looksLikeSecretToken(tm[0]))
5492
- continue;
5493
- 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))) {
5494
- continue;
5495
- }
5496
- ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
5497
- break;
5498
- }
5716
+ import { existsSync as existsSync3, readdirSync as readdirSync2, realpathSync } from "fs";
5717
+ import { homedir as homedir2 } from "os";
5718
+ import { join as join3, dirname as dirname3 } from "path";
5719
+ function normalizeHomePath(p) {
5720
+ const expanded = p.startsWith("~") ? join3(homedir2(), p.slice(1)) : p;
5721
+ const trimmed = expanded.replace(/[/\\]+$/, "");
5722
+ try {
5723
+ return realpathSync(trimmed);
5724
+ } catch {
5725
+ return trimmed;
5499
5726
  }
5500
- ranges.sort((a, b) => a.start - b.start);
5727
+ }
5728
+ function legacyAutoSyncedHomes() {
5729
+ const home = homedir2();
5501
5730
  const out = [];
5502
- for (const r of ranges) {
5503
- const last = out[out.length - 1];
5504
- if (last && r.start < last.end) {
5505
- last.end = Math.max(last.end, r.end);
5506
- continue;
5731
+ const add = (dir, marker) => {
5732
+ if (!existsSync3(join3(dir, marker)))
5733
+ return;
5734
+ const n = normalizeHomePath(dir);
5735
+ if (!out.includes(n))
5736
+ out.push(n);
5737
+ };
5738
+ const bases = [
5739
+ { base: join3(home, ".claude"), marker: "projects" },
5740
+ { base: join3(home, ".gemini"), marker: "tmp" },
5741
+ { base: join3(home, ".codex"), marker: "sessions" },
5742
+ { base: join3(home, ".gemini", "antigravity-cli"), marker: "brain" },
5743
+ { base: join3(home, ".local", "share", "opencode"), marker: "opencode.db" },
5744
+ { base: join3(home, ".cursor"), marker: "chats" }
5745
+ ];
5746
+ for (const { base: base3, marker } of bases) {
5747
+ add(base3, marker);
5748
+ const parent = dirname3(base3);
5749
+ const leaf = base3.slice(parent.length + 1);
5750
+ try {
5751
+ for (const e of readdirSync2(parent, { withFileTypes: true })) {
5752
+ if (!e.isDirectory())
5753
+ continue;
5754
+ if (e.name === leaf || !e.name.startsWith(leaf + "-"))
5755
+ continue;
5756
+ if (e.name === ".claude-code")
5757
+ continue;
5758
+ add(join3(parent, e.name), marker);
5759
+ }
5760
+ } catch {
5507
5761
  }
5508
- out.push({ ...r });
5509
5762
  }
5510
5763
  return out;
5511
5764
  }
5512
- function isEnvRedactionRequested() {
5513
- const v = process.env.CHAT_RECALL_REDACT_INDEX;
5514
- if (v === void 0)
5515
- return null;
5516
- return v === "1" || v === "true" || v === "yes";
5517
- }
5518
- function settingsView() {
5519
- let mtimeMs = -1;
5765
+ function grandfatherLegacyHomes() {
5766
+ let s;
5520
5767
  try {
5521
- mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5768
+ s = loadSettings();
5522
5769
  } catch {
5770
+ return { seeded: [] };
5523
5771
  }
5524
- if (cache && cache.mtimeMs === mtimeMs)
5525
- return cache;
5526
- let enabled = false;
5527
- const extra = [];
5528
- try {
5529
- const s = loadSettings();
5530
- enabled = !!s.privacy?.redactIndex;
5531
- for (const r of s.privacy?.redactionRules ?? []) {
5532
- try {
5533
- extra.push({ label: r.label, pattern: new RegExp(r.pattern, "g") });
5534
- } catch {
5535
- }
5772
+ if (s.sources.homesGrandfathered)
5773
+ return { seeded: [] };
5774
+ const legacy = legacyAutoSyncedHomes();
5775
+ const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
5776
+ const seeded = [];
5777
+ for (const p of legacy) {
5778
+ if (!approved.has(p)) {
5779
+ approved.add(p);
5780
+ seeded.push(p);
5536
5781
  }
5782
+ }
5783
+ s.sources.approvedHomes = [...approved];
5784
+ s.sources.homesGrandfathered = true;
5785
+ try {
5786
+ saveSettings(s);
5537
5787
  } catch {
5788
+ return { seeded: [] };
5538
5789
  }
5539
- cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
5540
- return cache;
5790
+ return { seeded };
5541
5791
  }
5542
- function compileServerRule(spec) {
5543
- const name = spec.name || "(unnamed)";
5544
- if (!spec.regex)
5545
- return { name, reason: "empty pattern" };
5546
- if (spec.regex.length > MAX_PATTERN_LEN)
5547
- return { name, reason: `pattern longer than ${MAX_PATTERN_LEN} chars` };
5548
- let re;
5792
+ function primaryHomes() {
5793
+ const home = homedir2();
5794
+ const env = process.env;
5795
+ let s;
5549
5796
  try {
5550
- const flags = "g" + (spec.flags || "").replace(/[^ims]/g, "");
5551
- re = new RegExp(spec.regex, flags);
5552
- } catch (e) {
5553
- return { name, reason: `invalid regex: ${e instanceof Error ? e.message : e}` };
5797
+ s = loadSettings().sources;
5798
+ } catch {
5799
+ s = void 0;
5554
5800
  }
5555
- if (re.test(""))
5556
- return { name, reason: "matches the empty string (would redact everything)" };
5557
- re.lastIndex = 0;
5558
- if (re.test(CANARY))
5559
- return { name, reason: "matches benign text \u2014 too broad" };
5560
- re.lastIndex = 0;
5561
- return { label: `${spec.source === "pack" ? "pack" : "tenant"}:${name}`, pattern: re };
5562
- }
5563
- function validateRedactionRule(spec) {
5564
- const out = compileServerRule(spec);
5565
- return "reason" in out ? { ok: false, reason: out.reason } : { ok: true };
5801
+ const opencodeDb = env.CHAT_RECALL_OPENCODE_DB || s?.opencodeDbPath || join3(home, ".local", "share", "opencode", "opencode.db");
5802
+ const candidates2 = [
5803
+ env.CHAT_RECALL_CLAUDE_HOME || s?.claudeHome || join3(home, ".claude"),
5804
+ env.CHAT_RECALL_GEMINI_HOME || s?.geminiHome || join3(home, ".gemini"),
5805
+ env.CHAT_RECALL_CODEX_HOME || s?.codexHome || join3(home, ".codex"),
5806
+ env.CHAT_RECALL_AGY_HOME || s?.agyHome || join3(home, ".gemini", "antigravity-cli"),
5807
+ env.CHAT_RECALL_CURSOR_HOME || s?.cursorHome || join3(home, ".cursor"),
5808
+ dirOf(opencodeDb)
5809
+ ];
5810
+ return candidates2.map(normalizeHomePath);
5566
5811
  }
5567
- function installServerRulePack(pack) {
5568
- const rejected = [];
5569
- const rules = [];
5570
- for (const spec of pack.rules || []) {
5571
- if (rules.length >= MAX_PACK_RULES) {
5572
- rejected.push({ name: spec.name || "(unnamed)", reason: `pack exceeds ${MAX_PACK_RULES} rules` });
5573
- continue;
5574
- }
5575
- const out = compileServerRule(spec);
5576
- if ("reason" in out)
5577
- rejected.push(out);
5578
- else
5579
- rules.push(out);
5812
+ function homeDecision(path) {
5813
+ const p = normalizeHomePath(path);
5814
+ if (primaryHomes().includes(p))
5815
+ return "primary";
5816
+ let s;
5817
+ try {
5818
+ s = loadSettings().sources;
5819
+ } catch {
5820
+ return "pending";
5580
5821
  }
5581
- const version2 = pack.version || "";
5582
- serverPack = { version: version2, rules };
5583
- return { version: version2, accepted: rules.length, rejected };
5584
- }
5585
- function serverRulePackVersion() {
5586
- return serverPack ? serverPack.version || "unversioned" : null;
5822
+ if ((s.declinedHomes ?? []).map(normalizeHomePath).includes(p))
5823
+ return "declined";
5824
+ if ((s.approvedHomes ?? []).map(normalizeHomePath).includes(p))
5825
+ return "approved";
5826
+ if ((s.extraClaudeHomes ?? []).map(normalizeHomePath).includes(p))
5827
+ return "approved";
5828
+ return "pending";
5587
5829
  }
5588
- function _clearServerRulePack() {
5589
- serverPack = null;
5830
+ function isHomeSynced(path) {
5831
+ const d = homeDecision(path);
5832
+ return d === "primary" || d === "approved";
5590
5833
  }
5591
- function activeRules() {
5592
- const base3 = settingsView().rules;
5593
- return serverPack && serverPack.rules.length > 0 ? [...base3, ...serverPack.rules] : base3;
5834
+ function setDecision(path, decision) {
5835
+ const p = normalizeHomePath(path);
5836
+ let s;
5837
+ try {
5838
+ s = loadSettings();
5839
+ } catch {
5840
+ return false;
5841
+ }
5842
+ const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
5843
+ const declined = new Set((s.sources.declinedHomes ?? []).map(normalizeHomePath));
5844
+ approved.delete(p);
5845
+ declined.delete(p);
5846
+ (decision === "approved" ? approved : declined).add(p);
5847
+ s.sources.approvedHomes = [...approved];
5848
+ s.sources.declinedHomes = [...declined];
5849
+ try {
5850
+ saveSettings(s);
5851
+ } catch {
5852
+ return false;
5853
+ }
5854
+ return true;
5594
5855
  }
5595
- function isRedactionEnabled() {
5596
- const env = isEnvRedactionRequested();
5597
- if (env !== null)
5598
- return env;
5599
- return settingsView().enabled;
5856
+ function approveHome(path) {
5857
+ return setDecision(path, "approved");
5600
5858
  }
5601
- function _resetRedactorCache() {
5602
- cache = null;
5859
+ function declineHome(path) {
5860
+ return setDecision(path, "declined");
5603
5861
  }
5604
- function redactSecrets(text, opts = {}) {
5605
- if (!opts.force && !isRedactionEnabled())
5606
- return text;
5607
- if (!text)
5608
- return text;
5609
- const rules = opts.rules || activeRules();
5610
- let out = text;
5611
- for (const r of rules) {
5612
- out = out.replace(r.pattern, (...args) => {
5613
- const whole = args[0];
5614
- const m = args.slice(0, args.length);
5615
- const secret = secretOf(r, m);
5616
- if (secret == null)
5617
- return whole;
5618
- if (opts.count)
5619
- opts.count.redactions++;
5620
- return whole.slice(0, whole.length - secret.length) + `[REDACTED:${r.label}]`;
5621
- });
5862
+ function resetHomeDecision(path) {
5863
+ const p = normalizeHomePath(path);
5864
+ let s;
5865
+ try {
5866
+ s = loadSettings();
5867
+ } catch {
5868
+ return false;
5622
5869
  }
5623
- const ctx = findContextualSecrets(out);
5624
- for (let i = ctx.length - 1; i >= 0; i--) {
5625
- const { start, end } = ctx[i];
5626
- if (opts.count)
5627
- opts.count.redactions++;
5628
- out = out.slice(0, start) + "[REDACTED:secret-context]" + out.slice(end);
5870
+ s.sources.approvedHomes = (s.sources.approvedHomes ?? []).filter((x) => normalizeHomePath(x) !== p);
5871
+ s.sources.declinedHomes = (s.sources.declinedHomes ?? []).filter((x) => normalizeHomePath(x) !== p);
5872
+ try {
5873
+ saveSettings(s);
5874
+ } catch {
5875
+ return false;
5629
5876
  }
5630
- return out;
5631
- }
5632
- function maskSecret(s) {
5633
- if (!s)
5634
- return "";
5635
- if (s.length <= 4)
5636
- return "*".repeat(s.length);
5637
- return "*".repeat(Math.min(s.length - 4, 60)) + s.slice(-4);
5877
+ return true;
5638
5878
  }
5639
- function globalRegexFor(pattern) {
5640
- const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
5641
- const key2 = pattern.source + "\0" + flags;
5642
- let re = globalRegexCache.get(key2);
5643
- if (!re) {
5644
- re = new RegExp(pattern.source, flags);
5645
- globalRegexCache.set(key2, re);
5879
+ var dirOf;
5880
+ var init_home_approval = __esm({
5881
+ "../engine/dist/core/home-approval.js"() {
5882
+ "use strict";
5883
+ init_settings();
5884
+ dirOf = (filePath) => dirname3(filePath);
5646
5885
  }
5647
- return re;
5886
+ });
5887
+
5888
+ // ../engine/dist/core/tool-paths.js
5889
+ import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
5890
+ import { homedir as homedir3 } from "os";
5891
+ import { join as join4, dirname as dirname4, basename } from "path";
5892
+ function claudeHomeDir() {
5893
+ return process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome || join4(homedir3(), ".claude");
5648
5894
  }
5649
- function makeLineResolver(text) {
5650
- const starts = [];
5651
- for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1))
5652
- starts.push(i);
5653
- return (index) => {
5654
- let lo = 0, hi = starts.length;
5655
- while (lo < hi) {
5656
- const mid = lo + hi >> 1;
5657
- if (starts[mid] < index)
5658
- lo = mid + 1;
5659
- else
5660
- hi = mid;
5661
- }
5662
- return lo + 1;
5895
+ function claudeProjectDirs(opts = {}) {
5896
+ const home = homedir3();
5897
+ const dirs = [];
5898
+ const addHome = (d) => {
5899
+ const p = join4(d, "projects");
5900
+ if (existsSync4(p) && !dirs.includes(p))
5901
+ dirs.push(p);
5663
5902
  };
5664
- }
5665
- function scanTextForFindings(text, opts = {}) {
5666
- if (!text)
5667
- return [];
5668
- const rules = opts.rules || activeRules();
5669
- const findings = [];
5670
- const seenSpans = /* @__PURE__ */ new Set();
5671
- const lineAt = makeLineResolver(text);
5672
- for (const r of rules) {
5673
- const re = globalRegexFor(r.pattern);
5674
- re.lastIndex = 0;
5675
- let m;
5676
- while ((m = re.exec(text)) !== null) {
5677
- const secret = secretOf(r, m);
5678
- if (secret == null) {
5679
- if (re.lastIndex === m.index)
5680
- re.lastIndex++;
5681
- continue;
5682
- }
5683
- const at = m.index + (m[0].length - secret.length);
5684
- const span = `${at}:${secret.length}`;
5685
- if (!seenSpans.has(span)) {
5686
- seenSpans.add(span);
5687
- findings.push({ rule: r.label, line: lineAt(at), preview: maskSecret(secret) });
5688
- }
5689
- if (re.lastIndex === m.index)
5690
- re.lastIndex++;
5691
- }
5692
- }
5693
- for (const { start, end } of findContextualSecrets(text)) {
5694
- const line = lineAt(start);
5695
- findings.push({ rule: "secret-context", line, preview: maskSecret(text.slice(start, end)) });
5696
- }
5697
- return findings;
5698
- }
5699
- function privacyView() {
5700
- let mtimeMs = -1;
5701
- try {
5702
- mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5703
- } catch {
5903
+ addHome(claudeHomeDir());
5904
+ const homeOverridden = !!(process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome);
5905
+ if (!homeOverridden) {
5906
+ try {
5907
+ for (const entry of readdirSync3(home, { withFileTypes: true })) {
5908
+ if (entry.isDirectory() && entry.name.startsWith(".claude-") && entry.name !== ".claude-code") {
5909
+ addHome(join4(home, entry.name));
5910
+ }
5911
+ }
5912
+ } catch {
5913
+ }
5704
5914
  }
5705
- if (privacyCache && privacyCache.mtimeMs === mtimeMs)
5706
- return privacyCache;
5707
- let redactToolOutputs = false, redactFilePaths2 = false;
5708
- try {
5709
- const s = loadSettings();
5710
- redactToolOutputs = !!s.privacy?.redactToolOutputs;
5711
- redactFilePaths2 = !!s.privacy?.redactFilePaths;
5712
- } catch {
5915
+ if (process.env.CLAUDE_DIRS) {
5916
+ for (const d of process.env.CLAUDE_DIRS.split(",")) {
5917
+ const t = d.trim();
5918
+ if (t)
5919
+ addHome(t.startsWith("~/") ? join4(home, t.slice(2)) : t);
5920
+ }
5713
5921
  }
5714
- privacyCache = { mtimeMs, redactToolOutputs, redactFilePaths: redactFilePaths2 };
5715
- return privacyCache;
5716
- }
5717
- function _resetPrivacyCache() {
5718
- privacyCache = null;
5719
- }
5720
- function isToolOutputRedactionEnabled() {
5721
- return privacyView().redactToolOutputs;
5922
+ if (dirs.length === 0)
5923
+ dirs.push(join4(claudeHomeDir(), "projects"));
5924
+ return applyDecisions(dirs, opts.includeExcluded);
5722
5925
  }
5723
- function isFilePathRedactionEnabled() {
5724
- return privacyView().redactFilePaths;
5926
+ function _setSourceExclusionFilter(fn) {
5927
+ _sourceExclusionFilter = fn;
5725
5928
  }
5726
- function redactFilePaths(text) {
5727
- if (!text)
5728
- return text;
5729
- return text.replace(PATH_REGEX, (m) => {
5730
- if (m.startsWith("[REDACTED:"))
5731
- return m;
5732
- const h = createHash("sha256").update(m).digest("hex").slice(0, 12);
5733
- return `[path:${h}]`;
5734
- });
5929
+ function geminiHomeDir() {
5930
+ return process.env.CHAT_RECALL_GEMINI_HOME || sources().geminiHome || join4(homedir3(), ".gemini");
5735
5931
  }
5736
- function applyChunkPrivacy(text) {
5737
- let out = redactSecrets(text);
5738
- if (privacyView().redactFilePaths)
5739
- out = redactFilePaths(out);
5740
- return out;
5932
+ function opencodeDbPath() {
5933
+ return process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath || join4(homedir3(), ".local", "share", "opencode", "opencode.db");
5741
5934
  }
5742
- function redactToolResultBody(toolName, body) {
5743
- if (!body)
5744
- return body;
5745
- return `[redacted: ${toolName} output, ${body.length} chars]`;
5935
+ function codexHomeDir() {
5936
+ return process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome || join4(homedir3(), ".codex");
5746
5937
  }
5747
- var DEFAULT_REDACTION_RULES, SECRET_CONTEXT_RE, HIGH_ENTROPY_TOKEN_RE, cache, CANARY, MAX_PACK_RULES, MAX_PATTERN_LEN, serverPack, globalRegexCache, privacyCache, PATH_REGEX;
5748
- var init_secret_redactor = __esm({
5749
- "../engine/dist/core/secret-redactor.js"() {
5750
- "use strict";
5751
- init_settings();
5752
- DEFAULT_REDACTION_RULES = [
5753
- // AWS access tokens. Prefix-anchored: AKIA, ASIA, ABIA (long-term,
5754
- // STS, EC2). Always exactly 16 hex/alphanumeric after the prefix.
5755
- // NOTE: this matches only the access-key-ID, NOT the secret key value —
5756
- // the `env-secret` rule below covers AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN.
5757
- { label: "aws-access-token", pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
5758
- // AWS STS session tokens. These are long base64 blobs with NO `NAME=` context
5759
- // when pasted bare into prose/code, so `env-secret` misses them — but they
5760
- // reliably start with one of a handful of base64 prefixes (the encoded
5761
- // `{"...` JSON header). Prefix-anchored → low false-positive. This is the
5762
- // bare-value form that leaked (`search('IQoJEXAMPLEfakeSESSIONtokenNOTreal01234567890abcDEF==...')`).
5763
- { label: "aws-session-token", pattern: /\b(?:IQoJ|FwoG|FwoH|FQoG|FQoH|FwoD)[A-Za-z0-9/+]{40,}={0,2}/g },
5764
- // Env/shell-assignment secrets — a VAR whose name ends in KEY/SECRET/TOKEN/
5765
- // PASSWORD/CREDENTIAL, then `=`/`:`, then the value. Zero-width lookbehind so
5766
- // only the VALUE is redacted (the var name stays for context). This is the
5767
- // dominant real-world leak vector (`export` dumps, .env files, `curl -H`,
5768
- // tool output) and is exactly what leaked AWS_SECRET_ACCESS_KEY and
5769
- // AWS_SESSION_TOKEN in cleartext while the access-key-ID rule above caught
5770
- // only the (least-sensitive) ID.
5771
- // Suffix list deliberately broad: DB_PASS (our own helm chart's name!),
5772
- // NPM_AUTH, SENTRY_DSN, SESSION/COOKIE secrets and BEARER vars all leaked
5773
- // past the original KEY/SECRET/TOKEN/PASSWORD-only list. Values under 12
5774
- // chars don't redact, so flag-ish vars (BYPASS=true) stay readable.
5775
- // Context CONSUMED, secret in group 1 — see RedactionRule.secret. As a
5776
- // lookbehind this was the second most expensive rule in the collector.
5777
- { label: "env-secret", secret: 1, pattern: /\b[A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL|AUTH|DSN|SESSION|COOKIE|BEARER|SIGNATURE)S?["']?[ \t]{0,3}[:=][ \t]{0,3}["']?([A-Za-z0-9/+_.~=:@-]{12,})/g },
5778
- // Credentials embedded in a connection URL: scheme://user:PASSWORD@host.
5779
- // Redacts only the password segment (keeps scheme/user/host for context).
5780
- //
5781
- // Scheme-agnostic on purpose. The old rule allowlisted eight DB schemes, so
5782
- // `https://user:pass@host`, `ssh://`, `ftp://`, `smtp://` and every other
5783
- // userinfo URL walked through in cleartext — and a `jdbc:postgresql://…`
5784
- // prefix broke the \b anchor for the one family it did cover. Any scheme
5785
- // followed by userinfo is a credential, so match the shape, not a list.
5786
- { label: "url-password", secret: 1, pattern: /\b[a-zA-Z][a-zA-Z0-9+.-]{1,31}:\/\/[^:@\s/]{1,64}:([^@\s/]{1,256})(?=@)/g },
5787
- // Bare AWS secret-access-key VALUE: EXACTLY 40 base64 chars, word-bounded,
5788
- // containing upper+lower+digit. This is the gitleaks heuristic — the mixed-case
5789
- // requirement excludes 40-char lowercase-hex git SHAs, and the exact-40 bound
5790
- // excludes longer blobs/tokens. It catches the value when it appears bare in
5791
- // prose with NO `NAME=` and NO nearby context word (e.g. inside a markdown
5792
- // table or quoted in analysis) — the residual leak path the env-secret and
5793
- // contextual rules both miss. Placed AFTER env-secret/url-password so the
5794
- // keyed/URL forms keep their more-specific labels.
5795
- // The three stacked lookaheads each re-scanned the same 40 characters, which
5796
- // made this the MOST expensive rule of all 106 the collector runs (13.1% of a
5797
- // 29.5 MB scan). The mixed-case-plus-digit test is now one JS pass.
5798
- {
5799
- label: "aws-secret-key",
5800
- secret: 1,
5801
- pattern: /(?<![A-Za-z0-9/+])([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+])/g,
5802
- where: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) && /[0-9]/.test(v)
5803
- },
5804
- // GitHub personal access tokens.
5805
- { label: "github-pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
5806
- // GitHub fine-grained PATs.
5807
- { label: "github-fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
5808
- // GitHub OAuth.
5809
- { label: "github-oauth", pattern: /\bgho_[A-Za-z0-9]{36}\b/g },
5810
- // GitHub app + refresh.
5811
- { label: "github-app", pattern: /\b(?:ghs|ghu|ghr)_[A-Za-z0-9]{36}\b/g },
5812
- // Slack bot/user/app tokens.
5813
- { label: "slack-token", pattern: /\bxox[abopr]-[A-Za-z0-9-]{10,}\b/g },
5814
- // Slack incoming webhook URL.
5815
- { label: "slack-webhook", pattern: /\bhttps:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+\b/g },
5816
- // Stripe live + test keys.
5817
- { label: "stripe-key", pattern: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
5818
- // Anthropic.
5819
- { label: "anthropic-key", pattern: /\bsk-ant-[a-zA-Z0-9_-]{40,}\b/g },
5820
- // OpenAI. Two shapes, deliberately kept apart. The classic key is
5821
- // `sk-` + base62 with NO hyphens; the modern scoped keys carry an explicit
5822
- // proj/svcacct/admin segment first. Allowing hyphens after a bare `sk-`
5823
- // (which is what admitted sk-proj- in the first place) also matches ordinary
5824
- // kebab-case text — `sk-fix-the-login-redirect-bug`, `.sk-button-primary`,
5825
- // `sk-2024-01-01-backup` — and this corpus is full of branch names and CSS
5826
- // classes. Requiring the segment keeps sk-proj- coverage without that.
5827
- { label: "openai-key", pattern: /\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}\b|\bsk-[A-Za-z0-9]{20,}\b/g },
5828
- // Google API key (AIza…).
5829
- { label: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
5830
- // Generic JWT — three base64url segments.
5831
- { label: "jwt", pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
5832
- // Private key blocks (PEM). The label is a free word run rather than a fixed
5833
- // list, so ENCRYPTED PRIVATE KEY and PGP PRIVATE KEY BLOCK are covered
5834
- // without another enumeration, and \1 forces END to name the same kind as
5835
- // BEGIN.
5836
- //
5837
- // The body is [^-], NOT [\s\S]*?. That is a performance boundary, not a
5838
- // style choice. An unanchored prefix with an unbounded lazy gap re-scans to
5839
- // end-of-input from every candidate start, so text carrying many BEGIN
5840
- // markers and no matching END costs O(n^2): 500KB of that measured 810ms
5841
- // here, and this runs synchronously on the server's event loop over
5842
- // tenant-supplied text (services/secret-rescan.ts). A PEM body is base64
5843
- // plus headers and contains no '-', so excluding it makes the scan stop at
5844
- // the next delimiter instead of at EOF — same matches, linear cost.
5845
- { label: "private-key", pattern: /-----BEGIN ([A-Z0-9 ]{0,32})PRIVATE KEY(?: BLOCK)?-----[^-]{0,10000}?-----END \1PRIVATE KEY(?: BLOCK)?-----/g },
5846
- // PuTTY keys are not PEM. Match the Private-Lines payload — the actual key
5847
- // material — rather than spanning the whole file, which would need the same
5848
- // unbounded gap the rule above just removed. Line-anchored and base64-only,
5849
- // so it cannot run away.
5850
- { label: "putty-key", pattern: /^Private-Lines:[ \t]*\d{1,4}[\r\n]+(?:[A-Za-z0-9+/=]{8,120}[\r\n]+){1,400}/gm },
5851
- // npm tokens (uuid-shaped — overlap with our UUID filter; intentional).
5852
- { label: "npm-token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/g },
5853
- // .npmrc auth lines: //registry.npmjs.org/:_authToken=VALUE (the key is not
5854
- // ALL-CAPS, so the env-secret rule above never matched it). The leading `:`
5855
- // is required — it is what makes this npmrc syntax rather than any shell var
5856
- // ending in _AUTH, which env-secret already owns and labels better.
5857
- // [^\s"'`]+ so the closing quote is not swallowed, and a ${VAR} placeholder
5858
- // is left alone — redacting it destroys a config example and hides nothing.
5859
- // The rule the perf profile caught the wedged daemon inside: 98.94% of samples.
5860
- { label: "npmrc-auth", secret: 1, pattern: /:_(?:authToken|password|auth)[ \t]*=[ \t]*(?!\$\{)([^\s"'`]{12,})/gi },
5861
- // HTTP Authorization headers. env-secret requires an ALL-CAPS name, and
5862
- // `Authorization` is mixed case, so both Bearer and Basic walked straight
5863
- // through — including short opaque tokens the contextual pass also misses
5864
- // (it needs 32+ chars AND mixed-case-plus-digit).
5865
- // Same two guards as npmrc-auth: stop at a quote, and skip the obvious
5866
- // placeholders that fill documentation (<YOUR_TOKEN>, ${TOKEN}, xxx…).
5867
- { label: "auth-header", secret: 1, pattern: /\bauthorization[ \t]*:[ \t]*(?:bearer|basic|token)[ \t]+(?![<$])([^\s"'`<]{8,})/gi },
5868
- // Vendor tokens with unambiguous prefixes. Each is a fixed literal, so a
5869
- // false positive is essentially impossible and the cost of listing is low.
5870
- { label: "gitlab-pat", pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
5871
- { label: "slack-app-token", pattern: /\bxapp-\d-[A-Z0-9]{8,}-\d{10,}-[a-f0-9]{32,}\b/g },
5872
- { label: "sendgrid-key", pattern: /\bSG\.[A-Za-z0-9_-]{20,24}\.[A-Za-z0-9_-]{40,50}\b/g },
5873
- // (?<![-\w]) so `cache-key-<md5>` is not read as a mailgun key.
5874
- { label: "mailgun-key", pattern: /(?<![-\w])key-[a-f0-9]{32}\b/g },
5875
- { label: "digitalocean-pat", pattern: /\bdop_v1_[a-f0-9]{64}\b/g },
5876
- { label: "huggingface-token", pattern: /\bhf_[A-Za-z0-9]{30,}\b/g },
5877
- { label: "shopify-token", pattern: /\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}\b/g },
5878
- { label: "pypi-token", pattern: /\bpypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{50,}\b/g },
5879
- // No twilio-sid rule: AC…/SK… are Twilio SIDs, which are public account and
5880
- // key identifiers, not the auth token. Redacting them hid nothing and
5881
- // collided with 32-hex etags and MD5 sums.
5882
- // No discord-bot-token rule either: the shape (word.word.word) matched
5883
- // ordinary dotted symbols such as a.b.ApplicationContextProviderImpl.
5884
- { label: "telegram-bot-token", pattern: /\b\d{8,10}:AA[A-Za-z0-9_-]{32,}\b/g },
5885
- { label: "doppler-token", pattern: /\bdp\.(?:pt|st|sa|ct)\.[A-Za-z0-9_-]{20,}\b/g },
5886
- // Azure Storage SAS signature — the `sig=` parameter is the actual secret.
5887
- { label: "azure-sas", secret: 1, pattern: /[?&]sig=([A-Za-z0-9%/+=]{20,})/g }
5888
- ];
5889
- SECRET_CONTEXT_RE = /\b(?:secret|password|passwd|credential|access[ _-]?key|private[ _-]?key|api[ _-]?key|auth(?:oriz\w*)?|bearer|token)\b/gi;
5890
- HIGH_ENTROPY_TOKEN_RE = /[A-Za-z0-9/+_-]{32,}/g;
5891
- cache = null;
5892
- CANARY = "the quick brown fox jumps over the lazy dog 0123456789 commit 8ba1f109551bd432803012645ac136ddd475d0fb src/index.ts:42";
5893
- MAX_PACK_RULES = 200;
5894
- MAX_PATTERN_LEN = 512;
5895
- serverPack = null;
5896
- globalRegexCache = /* @__PURE__ */ new Map();
5897
- privacyCache = null;
5898
- PATH_REGEX = /(?<![A-Za-z0-9_])(?:\/[^\s'"`<>]+|[A-Za-z]:\\[^\s'"`<>]+)/g;
5899
- }
5900
- });
5901
-
5902
- // ../engine/dist/parsers/chunker.js
5903
- function stripInjectedBanners(text) {
5904
- let result = text;
5905
- for (const re of INJECTED_BANNERS)
5906
- result = result.replace(re, " ");
5907
- return result;
5938
+ function agyHomeDir() {
5939
+ return process.env.CHAT_RECALL_AGY_HOME || sources().agyHome || join4(homedir3(), ".gemini", "antigravity-cli");
5908
5940
  }
5909
- function cleanText(text) {
5910
- let result = text.replace(/<system-reminder>.*?<\/system-reminder>/gs, "");
5911
- result = stripInjectedBanners(result);
5912
- result = result.replace(/\x1b\[[0-9;]*m/g, "");
5913
- result = result.replace(/\s+/g, " ");
5914
- return result.trim();
5941
+ function cursorHomeDir() {
5942
+ return process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome || join4(homedir3(), ".cursor");
5915
5943
  }
5916
- function windowText(text, maxChars, maxWindows = 12) {
5917
- const clean = cleanText(text);
5918
- if (clean.length <= maxChars)
5919
- return clean ? [clean] : [];
5920
- const out = [];
5921
- const overlap = 80;
5922
- const step = Math.max(1, maxChars - overlap);
5923
- for (let i = 0; i < clean.length && out.length < maxWindows; i += step) {
5924
- out.push(clean.slice(i, i + maxChars));
5925
- }
5926
- return out;
5944
+ function cursorIdeHomeDir() {
5945
+ return process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome || (process.platform === "darwin" ? join4(homedir3(), "Library", "Application Support", "Cursor") : join4(homedir3(), ".config", "Cursor"));
5927
5946
  }
5928
- function chunkSession(entry, content, maxChunks = 80, maxTokensPerChunk = 500) {
5929
- const chunks = [];
5930
- const maxChars = maxTokensPerChunk * 4;
5931
- const baseData = {
5932
- sessionId: entry.sessionId,
5933
- projectPath: entry.projectPath,
5934
- created: entry.created,
5935
- modified: entry.modified,
5936
- mtime: entry.fileMtime,
5937
- firstPrompt: entry.firstPrompt || content.firstPrompt,
5938
- sourceFile: content.sessionPath
5947
+ function siblingHomes(base3, overridden, suffix) {
5948
+ const roots = [];
5949
+ const add = (home) => {
5950
+ const root = suffix ? join4(home, suffix) : home;
5951
+ if (existsSync4(root) && !roots.includes(root))
5952
+ roots.push(root);
5939
5953
  };
5940
- for (let i = 0; i < Math.min(2, content.summaries.length); i++) {
5941
- const summary = content.summaries[i];
5942
- if (!summary?.trim())
5943
- continue;
5944
- const chunkText = cleanText(summary.slice(0, maxChars));
5945
- if (chunkText.length > 8e3) {
5946
- chunks.push({
5947
- chunkId: `${entry.sessionId}_summary_${i}`,
5948
- chunkType: "summary",
5949
- text: chunkText.slice(0, 8e3),
5950
- sourceLine: 0,
5951
- ...baseData
5952
- });
5953
- } else {
5954
- chunks.push({
5955
- chunkId: `${entry.sessionId}_summary_${i}`,
5956
- chunkType: "summary",
5957
- text: chunkText,
5958
- sourceLine: 0,
5959
- ...baseData
5960
- });
5954
+ add(base3);
5955
+ if (!overridden) {
5956
+ const parent = dirname4(base3);
5957
+ const leaf = basename(base3);
5958
+ try {
5959
+ for (const entry of readdirSync3(parent, { withFileTypes: true })) {
5960
+ if (!entry.isDirectory())
5961
+ continue;
5962
+ if (entry.name === leaf || !entry.name.startsWith(leaf + "-"))
5963
+ continue;
5964
+ add(join4(parent, entry.name));
5965
+ }
5966
+ } catch {
5961
5967
  }
5962
5968
  }
5963
- if (chunks.length >= maxChunks) {
5964
- return chunks.slice(0, maxChunks);
5965
- }
5966
- const firstPrompt = entry.firstPrompt || content.firstPrompt;
5967
- if (firstPrompt?.trim()) {
5968
- const firstLine = content.userMessages[0]?.lineNumber || 0;
5969
- chunks.push({
5970
- chunkId: `${entry.sessionId}_first_prompt`,
5971
- chunkType: "first_prompt",
5972
- text: cleanText(firstPrompt.slice(0, maxChars)),
5973
- sourceLine: firstLine,
5974
- ...baseData
5975
- });
5976
- }
5977
- if (chunks.length >= maxChunks) {
5978
- return chunks.slice(0, maxChunks);
5979
- }
5980
- const userStart = firstPrompt ? 1 : 0;
5981
- for (let i = userStart; i < content.userMessages.length; i++) {
5982
- if (chunks.length >= maxChunks)
5983
- break;
5984
- const msg = content.userMessages[i];
5985
- if (!msg.text.trim())
5986
- continue;
5987
- const windows = windowText(msg.text, maxChars);
5988
- for (let w = 0; w < windows.length; w++) {
5989
- if (chunks.length >= maxChunks)
5990
- break;
5991
- chunks.push({
5992
- chunkId: `${entry.sessionId}_user_${i}_${w}`,
5993
- chunkType: "user_context",
5994
- text: windows[w],
5995
- sourceLine: msg.lineNumber,
5996
- ...baseData
5997
- });
5969
+ return roots;
5970
+ }
5971
+ function applyDecisions(roots, includeExcluded) {
5972
+ if (roots.length === 0)
5973
+ return roots;
5974
+ let kept = roots;
5975
+ if (!includeExcluded) {
5976
+ try {
5977
+ grandfatherLegacyHomes();
5978
+ const approved = kept.filter((r) => isHomeSynced(dirname4(r)));
5979
+ if (approved.length > 0)
5980
+ kept = approved;
5981
+ } catch {
5998
5982
  }
5999
5983
  }
6000
- if (chunks.length >= maxChunks) {
6001
- return chunks.slice(0, maxChunks);
5984
+ if (!includeExcluded && _sourceExclusionFilter) {
5985
+ const notExcluded = kept.filter((r) => !_sourceExclusionFilter(r));
5986
+ if (notExcluded.length > 0)
5987
+ kept = notExcluded;
6002
5988
  }
6003
- for (let i = 0; i < content.assistantMessages.length; i++) {
6004
- if (chunks.length >= maxChunks)
6005
- break;
6006
- const msg = content.assistantMessages[i];
6007
- if (!msg.text.trim() || msg.text.length <= 50)
6008
- continue;
6009
- const windows = windowText(msg.text, maxChars);
6010
- for (let w = 0; w < windows.length; w++) {
6011
- if (chunks.length >= maxChunks)
6012
- break;
6013
- chunks.push({
6014
- chunkId: `${entry.sessionId}_assistant_${i}_${w}`,
6015
- chunkType: "assistant",
6016
- text: windows[w],
6017
- sourceLine: msg.lineNumber,
6018
- ...baseData
6019
- });
6020
- }
5989
+ return kept;
5990
+ }
5991
+ function applyExclusions(roots, includeExcluded) {
5992
+ return applyDecisions(roots, includeExcluded);
5993
+ }
5994
+ function codexSessionDirs(opts = {}) {
5995
+ const overridden = !!(process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome);
5996
+ return applyExclusions(siblingHomes(codexHomeDir(), overridden, "sessions"), opts.includeExcluded);
5997
+ }
5998
+ function agyBrainDirs(opts = {}) {
5999
+ const overridden = !!(process.env.CHAT_RECALL_AGY_HOME || sources().agyHome);
6000
+ return applyExclusions(siblingHomes(agyHomeDir(), overridden, "brain"), opts.includeExcluded);
6001
+ }
6002
+ function cursorChatDirs(opts = {}) {
6003
+ const overridden = !!(process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome);
6004
+ return applyExclusions(siblingHomes(cursorHomeDir(), overridden, "chats"), opts.includeExcluded);
6005
+ }
6006
+ function cursorIdeWorkspaceDirs(opts = {}) {
6007
+ const overridden = !!(process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome);
6008
+ return applyExclusions(siblingHomes(cursorIdeHomeDir(), overridden, join4("User", "workspaceStorage")), opts.includeExcluded);
6009
+ }
6010
+ function cursorIdeGlobalDbs(opts = {}) {
6011
+ const overridden = !!(process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome);
6012
+ const roots = applyExclusions(siblingHomes(cursorIdeHomeDir(), overridden, join4("User", "globalStorage")), opts.includeExcluded);
6013
+ const dbs = [];
6014
+ for (const r of roots) {
6015
+ const p = join4(r, "state.vscdb");
6016
+ if (existsSync4(p) && !dbs.includes(p))
6017
+ dbs.push(p);
6021
6018
  }
6022
- if (chunks.length >= maxChunks) {
6023
- return chunks.slice(0, maxChunks);
6019
+ return dbs;
6020
+ }
6021
+ function opencodeDbPaths(opts = {}) {
6022
+ const primary = opencodeDbPath();
6023
+ const overridden = !!(process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath);
6024
+ const dbFile = basename(primary);
6025
+ const dirs = siblingHomes(dirname4(primary), overridden, "");
6026
+ const dbs = [];
6027
+ for (const d of dirs) {
6028
+ const p = join4(d, dbFile);
6029
+ if (existsSync4(p) && !dbs.includes(p))
6030
+ dbs.push(p);
6024
6031
  }
6025
- if (content.toolResults.length > 0) {
6026
- const webSearches = content.toolResults.filter((r) => r.contentType === "web_search");
6027
- const otherResults = content.toolResults.filter((r) => r.contentType !== "web_search");
6028
- const resultsToProcess = [...webSearches.slice(0, 5), ...otherResults.slice(0, 10)];
6029
- const stripBodies = isToolOutputRedactionEnabled();
6030
- for (let i = 0; i < resultsToProcess.length; i++) {
6031
- if (chunks.length >= maxChunks)
6032
- break;
6033
- const result = resultsToProcess[i];
6034
- if (result.text.trim()) {
6035
- const body = stripBodies && result.contentType !== "web_search" ? redactToolResultBody(result.contentType, result.text) : result.text.slice(0, maxChars);
6036
- chunks.push({
6037
- chunkId: `${entry.sessionId}_${result.contentType}_${i}`,
6038
- chunkType: result.contentType,
6039
- text: cleanText(body),
6040
- sourceLine: result.lineNumber,
6041
- ...baseData
6042
- });
6043
- }
6044
- }
6032
+ if (dbs.length === 0)
6033
+ dbs.push(primary);
6034
+ return applyExclusions(dbs, opts.includeExcluded);
6035
+ }
6036
+ function extraClaudeHomeDirs() {
6037
+ const extras = sources().extraClaudeHomes ?? [];
6038
+ const primary = claudeHomeDir();
6039
+ return Array.from(new Set(extras)).filter((p) => p && p !== primary);
6040
+ }
6041
+ function claudeHomeDirs() {
6042
+ const overridden = !!(process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome);
6043
+ const homes = siblingHomes(claudeHomeDir(), overridden, "").filter((h) => basename(h) !== ".claude-code");
6044
+ for (const extra of extraClaudeHomeDirs()) {
6045
+ if (existsSync4(extra) && !homes.includes(extra))
6046
+ homes.push(extra);
6045
6047
  }
6046
- return chunks.slice(0, maxChunks);
6048
+ return homes;
6047
6049
  }
6048
- var INJECTED_BANNERS;
6049
- var init_chunker = __esm({
6050
- "../engine/dist/parsers/chunker.js"() {
6050
+ var sources, _sourceExclusionFilter;
6051
+ var init_tool_paths = __esm({
6052
+ "../engine/dist/core/tool-paths.js"() {
6051
6053
  "use strict";
6052
- init_secret_redactor();
6053
- INJECTED_BANNERS = [
6054
- /MCP issues detected\. ?Run \/mcp list for status\.?/g,
6055
- /Context low[^\n]*Run \/compact[^\n]*/g,
6056
- /API Error:[^\n]{0,120}/g
6057
- ];
6054
+ init_home_approval();
6055
+ init_settings();
6056
+ sources = loadSourceSettings;
6057
+ _sourceExclusionFilter = null;
6058
6058
  }
6059
6059
  });
6060
6060
 
@@ -7821,6 +7821,28 @@ var init_session_git = __esm({
7821
7821
  }
7822
7822
  });
7823
7823
 
7824
+ // ../engine/dist/core/session-multi-tool.js
7825
+ function extractTurnsAny(sessionId, opts = {}) {
7826
+ const backend = getBackendForId(sessionId);
7827
+ if (!backend)
7828
+ return { sessionId, found: false, turns: [], startMs: 0, endMs: 0 };
7829
+ return memoInSessionScope(`turns:${sessionId}:${opts.maxTurns ?? ""}:${opts.assistantMax ?? ""}`, () => backend.extractTurns(sessionId, opts));
7830
+ }
7831
+ function replaySessionAny(sessionId) {
7832
+ const backend = getBackendForId(sessionId);
7833
+ if (!backend)
7834
+ return { sessionId, found: false, projectPath: "", files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
7835
+ return memoInSessionScope(`replay:${sessionId}`, () => backend.replay(sessionId));
7836
+ }
7837
+ var init_session_multi_tool = __esm({
7838
+ "../engine/dist/core/session-multi-tool.js"() {
7839
+ "use strict";
7840
+ init_tool_backend();
7841
+ init_live_session_scan();
7842
+ init_backends();
7843
+ }
7844
+ });
7845
+
7824
7846
  // ../engine/dist/core/session-outcome.js
7825
7847
  function extractDecisionsFromTurn(turn) {
7826
7848
  if (turn.kind !== "assistant_text" || !turn.text)
@@ -7917,7 +7939,7 @@ function computeOutcome(sessionId, opts = {}) {
7917
7939
  };
7918
7940
  if (!backend)
7919
7941
  return notFound;
7920
- const turnsRes = backend.extractTurns(sessionId, { assistantMax: 2e3 });
7942
+ const turnsRes = extractTurnsAny(sessionId, { assistantMax: 2e3 });
7921
7943
  if (!turnsRes.found)
7922
7944
  return notFound;
7923
7945
  const decisions = [];
@@ -7956,7 +7978,7 @@ function computeOutcome(sessionId, opts = {}) {
7956
7978
  totalLinesAdded = fast.totalLinesAdded;
7957
7979
  totalLinesRemoved = fast.totalLinesRemoved;
7958
7980
  } else {
7959
- const replay = backend.replay(sessionId);
7981
+ const replay = replaySessionAny(sessionId);
7960
7982
  filesChanged = replay.files.map((f) => f.file);
7961
7983
  totalLinesAdded = replay.totalLinesAdded;
7962
7984
  totalLinesRemoved = replay.totalLinesRemoved;
@@ -7995,6 +8017,7 @@ var init_session_outcome = __esm({
7995
8017
  init_session_sentiment();
7996
8018
  init_session_git();
7997
8019
  init_tool_backend();
8020
+ init_session_multi_tool();
7998
8021
  init_backends();
7999
8022
  DECISION_PATTERNS = [
8000
8023
  /\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,
@@ -11675,13 +11698,26 @@ function resolveSessionContentGroups(sessionId) {
11675
11698
  }
11676
11699
  function withSessionReadCache(fn) {
11677
11700
  readScopeDepth++;
11701
+ if (readScopeDepth === 1)
11702
+ derivedScope = /* @__PURE__ */ new Map();
11678
11703
  try {
11679
11704
  return fn();
11680
11705
  } finally {
11681
- if (--readScopeDepth === 0)
11706
+ if (--readScopeDepth === 0) {
11682
11707
  readScope = null;
11708
+ derivedScope = null;
11709
+ }
11683
11710
  }
11684
11711
  }
11712
+ function memoInSessionScope(key2, compute) {
11713
+ if (!derivedScope)
11714
+ return compute();
11715
+ if (derivedScope.has(key2))
11716
+ return derivedScope.get(key2);
11717
+ const value = compute();
11718
+ derivedScope.set(key2, value);
11719
+ return value;
11720
+ }
11685
11721
  function readSessionGroupText(group) {
11686
11722
  let key2 = "";
11687
11723
  if (readScopeDepth > 0) {
@@ -11793,7 +11829,7 @@ function findCodexSessionFileIn(root, sessionId) {
11793
11829
  }
11794
11830
  return scanDir(root);
11795
11831
  }
11796
- var scanScopeDepth, indexCache, readScope, readScopeDepth, geminiCache, MAX_EVENT_LINE_BYTES;
11832
+ var scanScopeDepth, indexCache, readScope, readScopeDepth, derivedScope, geminiCache, MAX_EVENT_LINE_BYTES;
11797
11833
  var init_live_session_scan = __esm({
11798
11834
  "../engine/dist/core/live-session-scan.js"() {
11799
11835
  "use strict";
@@ -11807,6 +11843,7 @@ var init_live_session_scan = __esm({
11807
11843
  indexCache = null;
11808
11844
  readScope = null;
11809
11845
  readScopeDepth = 0;
11846
+ derivedScope = null;
11810
11847
  geminiCache = null;
11811
11848
  MAX_EVENT_LINE_BYTES = 8 * 1024 * 1024;
11812
11849
  }
@@ -11937,6 +11974,7 @@ var UUID_RE, ClaudeBackend, claudeBackend;
11937
11974
  var init_claude2 = __esm({
11938
11975
  "../engine/dist/core/backends/claude.js"() {
11939
11976
  "use strict";
11977
+ init_live_session_scan();
11940
11978
  init_tool_paths();
11941
11979
  init_live_session_scan();
11942
11980
  init_session_outcome();
@@ -12199,6 +12237,9 @@ var init_claude2 = __esm({
12199
12237
  * splits show up alongside the parent.
12200
12238
  */
12201
12239
  readEvents(rawId) {
12240
+ return memoInSessionScope(`events:claude:${rawId}`, () => this.#readEventsUncached(rawId));
12241
+ }
12242
+ #readEventsUncached(rawId) {
12202
12243
  const groups = resolveSessionContentGroups(rawId);
12203
12244
  if (groups.length === 0)
12204
12245
  return [];
@@ -18817,7 +18858,7 @@ var init_http = __esm({
18817
18858
  CLI_VERSION = JSON.parse(readFileSync31(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
18818
18859
  } catch {
18819
18860
  try {
18820
- if (true) CLI_VERSION = "0.6.8";
18861
+ if (true) CLI_VERSION = "0.6.9";
18821
18862
  } catch {
18822
18863
  }
18823
18864
  }
@@ -18842,27 +18883,6 @@ var init_redact_deep = __esm({
18842
18883
  }
18843
18884
  });
18844
18885
 
18845
- // ../engine/dist/core/session-multi-tool.js
18846
- function extractTurnsAny(sessionId, opts = {}) {
18847
- const backend = getBackendForId(sessionId);
18848
- if (!backend)
18849
- return { sessionId, found: false, turns: [], startMs: 0, endMs: 0 };
18850
- return backend.extractTurns(sessionId, opts);
18851
- }
18852
- function replaySessionAny(sessionId) {
18853
- const backend = getBackendForId(sessionId);
18854
- if (!backend)
18855
- return { sessionId, found: false, projectPath: "", files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
18856
- return backend.replay(sessionId);
18857
- }
18858
- var init_session_multi_tool = __esm({
18859
- "../engine/dist/core/session-multi-tool.js"() {
18860
- "use strict";
18861
- init_tool_backend();
18862
- init_backends();
18863
- }
18864
- });
18865
-
18866
18886
  // src/derived.ts
18867
18887
  function collectDerivedRows(ref, mtime, count, maxRowBytes) {
18868
18888
  const id = ref.prefixedId;
@@ -19337,7 +19357,7 @@ var init_telemetry = __esm({
19337
19357
  init_sync_client();
19338
19358
  cliVersion2 = () => {
19339
19359
  try {
19340
- return true ? "0.6.8" : "dev";
19360
+ return true ? "0.6.9" : "dev";
19341
19361
  } catch {
19342
19362
  return "dev";
19343
19363
  }
@@ -19352,6 +19372,7 @@ var collector_health_exports = {};
19352
19372
  __export(collector_health_exports, {
19353
19373
  CRASHLOOP_RESTARTS: () => CRASHLOOP_RESTARTS,
19354
19374
  STALE_AFTER_MS: () => STALE_AFTER_MS,
19375
+ applySyncOutcome: () => applySyncOutcome,
19355
19376
  collectorHealthPath: () => collectorHealthPath,
19356
19377
  judgeHealth: () => judgeHealth,
19357
19378
  progressLine: () => progressLine,
@@ -19362,6 +19383,22 @@ __export(collector_health_exports, {
19362
19383
  });
19363
19384
  import { readFileSync as readFileSync32, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8 } from "node:fs";
19364
19385
  import { join as join40, dirname as dirname17 } from "node:path";
19386
+ function applySyncOutcome(prior, outcome, now = Date.now()) {
19387
+ const t = { lastOkAt: prior?.lastOkAt ?? null, failures: prior?.failures ?? 0 };
19388
+ if (prior?.lastError !== void 0)
19389
+ t.lastError = prior.lastError;
19390
+ if (outcome.ok) {
19391
+ t.lastOkAt = now;
19392
+ t.failures = 0;
19393
+ delete t.lastError;
19394
+ return t;
19395
+ }
19396
+ if ((outcome.accepted ?? 0) > 0)
19397
+ t.lastOkAt = now;
19398
+ t.failures += 1;
19399
+ t.lastError = (outcome.error ?? "unknown error").slice(0, 160);
19400
+ return t;
19401
+ }
19365
19402
  function progressLine(h) {
19366
19403
  const p = h?.progress;
19367
19404
  if (!p || p.complete || p.total <= 0)
@@ -19920,6 +19957,7 @@ var sync_client_exports = {};
19920
19957
  __export(sync_client_exports, {
19921
19958
  BATCH_SCAN_MAX_BYTES: () => BATCH_SCAN_MAX_BYTES,
19922
19959
  FULL_BUILD_MAX_BYTES: () => FULL_BUILD_MAX_BYTES,
19960
+ _acceptedUploads: () => _acceptedUploads,
19923
19961
  _fetchTenantSyncConfigForTests: () => _fetchTenantSyncConfigForTests,
19924
19962
  _resetCapsCacheForTests: () => _resetCapsCacheForTests,
19925
19963
  _resetTenantSecurityConfigCache: () => _resetTenantSecurityConfigCache,
@@ -20183,6 +20221,9 @@ function removeCredentials(serverUrl) {
20183
20221
  writeFileSync9(credPath(), JSON.stringify({ targets: kept }, null, 2));
20184
20222
  return true;
20185
20223
  }
20224
+ function _acceptedUploads(url) {
20225
+ return acceptedUploads.get(url) ?? 0;
20226
+ }
20186
20227
  async function syncSessions(opts = {}) {
20187
20228
  const targets = loadAllCredentials();
20188
20229
  if (targets.length === 0) throw new Error("Not logged in \u2014 run `chat-recall login <server-url> --token <token>`");
@@ -20206,10 +20247,11 @@ async function syncSessions(opts = {}) {
20206
20247
  };
20207
20248
  continue;
20208
20249
  }
20250
+ acceptedUploads.set(cred.serverUrl, 0);
20209
20251
  try {
20210
20252
  const r = await syncToTarget(cred, opts);
20211
20253
  noteTargetSuccess(cred.serverUrl);
20212
- perTarget[cred.serverUrl] = { ok: true };
20254
+ perTarget[cred.serverUrl] = { ok: true, accepted: acceptedUploads.get(cred.serverUrl) ?? 0 };
20213
20255
  agg = agg ? {
20214
20256
  uploaded: agg.uploaded + r.uploaded,
20215
20257
  skipped: agg.skipped + r.skipped,
@@ -20226,7 +20268,7 @@ async function syncSessions(opts = {}) {
20226
20268
  } catch (e) {
20227
20269
  const msg = e instanceof Error ? e.message : String(e);
20228
20270
  errors.push(`${cred.serverUrl}: ${msg}`);
20229
- perTarget[cred.serverUrl] = { ok: false, error: msg };
20271
+ perTarget[cred.serverUrl] = { ok: false, error: msg, accepted: acceptedUploads.get(cred.serverUrl) ?? 0 };
20230
20272
  const next = noteTargetFailure(cred.serverUrl, msg);
20231
20273
  const notice = breakerNotice(cred.serverUrl, next);
20232
20274
  if (notice) console.error(`[sync] ${notice}`);
@@ -20492,7 +20534,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20492
20534
  return [];
20493
20535
  }
20494
20536
  });
20495
- const MAX_BATCH_BYTES = 4 * 1024 * 1024;
20537
+ const MAX_BATCH_BYTES = Number(process.env.CHAT_RECALL_MAX_BATCH_BYTES) || (isLocalHost(cred.serverUrl) ? 4 * 1024 * 1024 : 1024 * 1024);
20496
20538
  const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
20497
20539
  const explicitThrottle = typeof opts.throttleMs === "number" && opts.throttleMs > 0;
20498
20540
  const throttleMs = explicitThrottle ? opts.throttleMs : 0;
@@ -20527,6 +20569,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20527
20569
  signal: ac.signal
20528
20570
  });
20529
20571
  if (res.ok) {
20572
+ acceptedUploads.set(cred.serverUrl, (acceptedUploads.get(cred.serverUrl) ?? 0) + 1);
20530
20573
  const body2 = await res.json().catch(() => ({}));
20531
20574
  if (typeof body2.telemetry === "boolean") {
20532
20575
  setTelemetryEligible(base3, body2.telemetry);
@@ -20577,7 +20620,8 @@ async function syncToTargetInScope(cred, opts = {}) {
20577
20620
  if (attempt >= RETRY_DELAYS_MS.length) throw new Error(`sync failed after ${attempt + 1} attempts: HTTP ${res.status} ${text}`);
20578
20621
  const retryAfterS = Number(res.headers.get("retry-after"));
20579
20622
  const waitMs = res.status === 429 && retryAfterS > 0 ? retryAfterS * 1e3 : RETRY_DELAYS_MS[attempt];
20580
- console.error(`[sync] HTTP ${res.status} from ${base3} \u2014 backing off ${Math.round(waitMs / 1e3)}s (attempt ${attempt + 1})`);
20623
+ const why = res.status === 524 ? ` \u2014 the proxy cut the request at its time limit; the batch is too big for one request` : "";
20624
+ console.error(`[sync] HTTP ${res.status} from ${base3}${why} \u2014 backing off ${Math.round(waitMs / 1e3)}s (attempt ${attempt + 1})`);
20581
20625
  await sleep2(waitMs);
20582
20626
  continue;
20583
20627
  } catch (err) {
@@ -21436,7 +21480,7 @@ async function syncIncremental(opts = {}) {
21436
21480
  lock.release();
21437
21481
  }
21438
21482
  }
21439
- 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;
21483
+ 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;
21440
21484
  var init_sync_client = __esm({
21441
21485
  "src/sync-client.ts"() {
21442
21486
  "use strict";
@@ -21481,7 +21525,7 @@ var init_sync_client = __esm({
21481
21525
  ownVersion = JSON.parse(readFileSync35(join42(dir, "../package.json"), "utf-8")).version || ownVersion;
21482
21526
  } catch {
21483
21527
  try {
21484
- if (true) ownVersion = "0.6.8";
21528
+ if (true) ownVersion = "0.6.9";
21485
21529
  } catch {
21486
21530
  }
21487
21531
  }
@@ -21501,6 +21545,7 @@ var init_sync_client = __esm({
21501
21545
  _fetchTenantSyncConfigForTests = fetchTenantSyncConfig;
21502
21546
  SYNC_RULES_REFUSAL = /* @__PURE__ */ Symbol.for("chat-recall.syncRulesRefusal");
21503
21547
  hashPath = (p) => p ? "p_" + createHash9("sha256").update(p).digest("hex").slice(0, 12) : "";
21548
+ acceptedUploads = /* @__PURE__ */ new Map();
21504
21549
  MAX_DERIVED_ROW_BYTES = 2 * 1024 * 1024;
21505
21550
  isLocalHost = isPrivateHost;
21506
21551
  CAPS_TTL_MS = 60 * 6e4;
@@ -25596,7 +25641,7 @@ var init_intent_drain = __esm({
25596
25641
  ownVersion2 = JSON.parse(readFileSync45(join56(dir, "../package.json"), "utf-8")).version || ownVersion2;
25597
25642
  } catch {
25598
25643
  try {
25599
- if (true) ownVersion2 = "0.6.8";
25644
+ if (true) ownVersion2 = "0.6.9";
25600
25645
  } catch {
25601
25646
  }
25602
25647
  }
@@ -29945,7 +29990,19 @@ program.command("guard").description("Check a pending tool call against the deci
29945
29990
  if (target.token) headers.authorization = `Bearer ${target.token}`;
29946
29991
  const res = await fetchWithTimeout2(
29947
29992
  `${target.base}/api/decisions/check`,
29948
- { method: "POST", headers, body: JSON.stringify({ names, via, project: opts.project }) },
29993
+ {
29994
+ method: "POST",
29995
+ headers,
29996
+ // The folder group is resolved HERE, from the directory the hook runs
29997
+ // in. The server has no checkout to read it from, and a lookup per
29998
+ // tool call is exactly the cost this guard cannot afford.
29999
+ body: JSON.stringify({
30000
+ names,
30001
+ via,
30002
+ project: opts.project,
30003
+ workspace: resolveWorkspaceId(process.cwd())?.slice(3)
30004
+ })
30005
+ },
29949
30006
  4e3
29950
30007
  );
29951
30008
  if (!res.ok) return;