chat-recall 0.6.9 → 0.7.1

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
@@ -4549,6 +4549,25 @@ var init_paths = __esm({
4549
4549
  }
4550
4550
  });
4551
4551
 
4552
+ // ../engine/dist/parsers/banners.js
4553
+ function stripInjectedBanners(text) {
4554
+ let result = text;
4555
+ for (const re of INJECTED_BANNERS)
4556
+ result = result.replace(re, " ");
4557
+ return result;
4558
+ }
4559
+ var INJECTED_BANNERS;
4560
+ var init_banners = __esm({
4561
+ "../engine/dist/parsers/banners.js"() {
4562
+ "use strict";
4563
+ INJECTED_BANNERS = [
4564
+ /MCP issues detected\. ?Run \/mcp list for status\.?/g,
4565
+ /Context low[^\n]*Run \/compact[^\n]*/g,
4566
+ /API Error:[^\n]{0,120}/g
4567
+ ];
4568
+ }
4569
+ });
4570
+
4552
4571
  // ../engine/dist/core/project-path-match.js
4553
4572
  function canonicalProjectPath(p) {
4554
4573
  return (p || "").trim().replace(/\\/g, "/").replace(/[-_]/g, "/").replace(/\/{2,}/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -5083,866 +5102,247 @@ var init_settings = __esm({
5083
5102
  }
5084
5103
  });
5085
5104
 
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
5105
+ // ../engine/dist/core/home-approval.js
5106
+ var home_approval_exports = {};
5107
+ __export(home_approval_exports, {
5108
+ approveHome: () => approveHome,
5109
+ declineHome: () => declineHome,
5110
+ grandfatherLegacyHomes: () => grandfatherLegacyHomes,
5111
+ homeDecision: () => homeDecision,
5112
+ isHomeSynced: () => isHomeSynced,
5113
+ normalizeHomePath: () => normalizeHomePath,
5114
+ resetHomeDecision: () => resetHomeDecision
5105
5115
  });
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;
5115
- }
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;
5139
- }
5140
- ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
5141
- break;
5142
- }
5116
+ import { existsSync as existsSync3, readdirSync as readdirSync2, realpathSync } from "fs";
5117
+ import { homedir as homedir2 } from "os";
5118
+ import { join as join3, dirname as dirname3 } from "path";
5119
+ function normalizeHomePath(p) {
5120
+ const expanded = p.startsWith("~") ? join3(homedir2(), p.slice(1)) : p;
5121
+ const trimmed = expanded.replace(/[/\\]+$/, "");
5122
+ try {
5123
+ return realpathSync(trimmed);
5124
+ } catch {
5125
+ return trimmed;
5143
5126
  }
5144
- ranges.sort((a, b) => a.start - b.start);
5127
+ }
5128
+ function legacyAutoSyncedHomes() {
5129
+ const home = homedir2();
5145
5130
  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;
5131
+ const add = (dir, marker) => {
5132
+ if (!existsSync3(join3(dir, marker)))
5133
+ return;
5134
+ const n = normalizeHomePath(dir);
5135
+ if (!out.includes(n))
5136
+ out.push(n);
5137
+ };
5138
+ const bases = [
5139
+ { base: join3(home, ".claude"), marker: "projects" },
5140
+ { base: join3(home, ".gemini"), marker: "tmp" },
5141
+ { base: join3(home, ".codex"), marker: "sessions" },
5142
+ { base: join3(home, ".gemini", "antigravity-cli"), marker: "brain" },
5143
+ { base: join3(home, ".local", "share", "opencode"), marker: "opencode.db" },
5144
+ { base: join3(home, ".cursor"), marker: "chats" }
5145
+ ];
5146
+ for (const { base: base3, marker } of bases) {
5147
+ add(base3, marker);
5148
+ const parent = dirname3(base3);
5149
+ const leaf = base3.slice(parent.length + 1);
5150
+ try {
5151
+ for (const e of readdirSync2(parent, { withFileTypes: true })) {
5152
+ if (!e.isDirectory())
5153
+ continue;
5154
+ if (e.name === leaf || !e.name.startsWith(leaf + "-"))
5155
+ continue;
5156
+ if (e.name === ".claude-code")
5157
+ continue;
5158
+ add(join3(parent, e.name), marker);
5159
+ }
5160
+ } catch {
5151
5161
  }
5152
- out.push({ ...r });
5153
5162
  }
5154
5163
  return out;
5155
5164
  }
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;
5165
+ function grandfatherLegacyHomes() {
5166
+ let s;
5164
5167
  try {
5165
- mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5168
+ s = loadSettings();
5166
5169
  } catch {
5170
+ return { seeded: [] };
5167
5171
  }
5168
- if (cache && cache.mtimeMs === mtimeMs)
5169
- return cache;
5170
- let enabled = false;
5171
- const extra = [];
5172
- try {
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
- }
5172
+ if (s.sources.homesGrandfathered)
5173
+ return { seeded: [] };
5174
+ const legacy = legacyAutoSyncedHomes();
5175
+ const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
5176
+ const seeded = [];
5177
+ for (const p of legacy) {
5178
+ if (!approved.has(p)) {
5179
+ approved.add(p);
5180
+ seeded.push(p);
5180
5181
  }
5182
+ }
5183
+ s.sources.approvedHomes = [...approved];
5184
+ s.sources.homesGrandfathered = true;
5185
+ try {
5186
+ saveSettings(s);
5181
5187
  } catch {
5188
+ return { seeded: [] };
5182
5189
  }
5183
- cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
5184
- return cache;
5190
+ return { seeded };
5185
5191
  }
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;
5192
+ function primaryHomes() {
5193
+ const home = homedir2();
5194
+ const env = process.env;
5195
+ let s;
5193
5196
  try {
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}` };
5197
+ s = loadSettings().sources;
5198
+ } catch {
5199
+ s = void 0;
5198
5200
  }
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 };
5206
- }
5207
- function validateRedactionRule(spec) {
5208
- const out = compileServerRule(spec);
5209
- return "reason" in out ? { ok: false, reason: out.reason } : { ok: true };
5201
+ const opencodeDb = env.CHAT_RECALL_OPENCODE_DB || s?.opencodeDbPath || join3(home, ".local", "share", "opencode", "opencode.db");
5202
+ const candidates2 = [
5203
+ env.CHAT_RECALL_CLAUDE_HOME || s?.claudeHome || join3(home, ".claude"),
5204
+ env.CHAT_RECALL_GEMINI_HOME || s?.geminiHome || join3(home, ".gemini"),
5205
+ env.CHAT_RECALL_CODEX_HOME || s?.codexHome || join3(home, ".codex"),
5206
+ env.CHAT_RECALL_AGY_HOME || s?.agyHome || join3(home, ".gemini", "antigravity-cli"),
5207
+ env.CHAT_RECALL_CURSOR_HOME || s?.cursorHome || join3(home, ".cursor"),
5208
+ dirOf(opencodeDb)
5209
+ ];
5210
+ return candidates2.map(normalizeHomePath);
5210
5211
  }
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);
5212
+ function homeDecision(path) {
5213
+ const p = normalizeHomePath(path);
5214
+ if (primaryHomes().includes(p))
5215
+ return "primary";
5216
+ let s;
5217
+ try {
5218
+ s = loadSettings().sources;
5219
+ } catch {
5220
+ return "pending";
5224
5221
  }
5225
- const version2 = pack.version || "";
5226
- serverPack = { version: version2, rules };
5227
- return { version: version2, accepted: rules.length, rejected };
5228
- }
5229
- function serverRulePackVersion() {
5230
- return serverPack ? serverPack.version || "unversioned" : null;
5231
- }
5232
- function _clearServerRulePack() {
5233
- serverPack = null;
5222
+ if ((s.declinedHomes ?? []).map(normalizeHomePath).includes(p))
5223
+ return "declined";
5224
+ if ((s.approvedHomes ?? []).map(normalizeHomePath).includes(p))
5225
+ return "approved";
5226
+ if ((s.extraClaudeHomes ?? []).map(normalizeHomePath).includes(p))
5227
+ return "approved";
5228
+ return "pending";
5234
5229
  }
5235
- function activeRules() {
5236
- const base3 = settingsView().rules;
5237
- return serverPack && serverPack.rules.length > 0 ? [...base3, ...serverPack.rules] : base3;
5230
+ function isHomeSynced(path) {
5231
+ const d = homeDecision(path);
5232
+ return d === "primary" || d === "approved";
5238
5233
  }
5239
- function isRedactionEnabled() {
5240
- const env = isEnvRedactionRequested();
5241
- if (env !== null)
5242
- return env;
5243
- return settingsView().enabled;
5234
+ function setDecision(path, decision) {
5235
+ const p = normalizeHomePath(path);
5236
+ let s;
5237
+ try {
5238
+ s = loadSettings();
5239
+ } catch {
5240
+ return false;
5241
+ }
5242
+ const approved = new Set((s.sources.approvedHomes ?? []).map(normalizeHomePath));
5243
+ const declined = new Set((s.sources.declinedHomes ?? []).map(normalizeHomePath));
5244
+ approved.delete(p);
5245
+ declined.delete(p);
5246
+ (decision === "approved" ? approved : declined).add(p);
5247
+ s.sources.approvedHomes = [...approved];
5248
+ s.sources.declinedHomes = [...declined];
5249
+ try {
5250
+ saveSettings(s);
5251
+ } catch {
5252
+ return false;
5253
+ }
5254
+ return true;
5244
5255
  }
5245
- function _resetRedactorCache() {
5246
- cache = null;
5256
+ function approveHome(path) {
5257
+ return setDecision(path, "approved");
5247
5258
  }
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
- });
5259
+ function declineHome(path) {
5260
+ return setDecision(path, "declined");
5261
+ }
5262
+ function resetHomeDecision(path) {
5263
+ const p = normalizeHomePath(path);
5264
+ let s;
5265
+ try {
5266
+ s = loadSettings();
5267
+ } catch {
5268
+ return false;
5266
5269
  }
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);
5270
+ s.sources.approvedHomes = (s.sources.approvedHomes ?? []).filter((x) => normalizeHomePath(x) !== p);
5271
+ s.sources.declinedHomes = (s.sources.declinedHomes ?? []).filter((x) => normalizeHomePath(x) !== p);
5272
+ try {
5273
+ saveSettings(s);
5274
+ } catch {
5275
+ return false;
5273
5276
  }
5274
- return out;
5275
- }
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);
5277
+ return true;
5282
5278
  }
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);
5279
+ var dirOf;
5280
+ var init_home_approval = __esm({
5281
+ "../engine/dist/core/home-approval.js"() {
5282
+ "use strict";
5283
+ init_settings();
5284
+ dirOf = (filePath) => dirname3(filePath);
5290
5285
  }
5291
- return re;
5286
+ });
5287
+
5288
+ // ../engine/dist/core/tool-paths.js
5289
+ import { existsSync as existsSync4, readdirSync as readdirSync3 } from "fs";
5290
+ import { homedir as homedir3 } from "os";
5291
+ import { join as join4, dirname as dirname4, basename } from "path";
5292
+ function claudeHomeDir() {
5293
+ return process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome || join4(homedir3(), ".claude");
5292
5294
  }
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;
5295
+ function claudeProjectDirs(opts = {}) {
5296
+ const home = homedir3();
5297
+ const dirs = [];
5298
+ const addHome = (d) => {
5299
+ const p = join4(d, "projects");
5300
+ if (existsSync4(p) && !dirs.includes(p))
5301
+ dirs.push(p);
5307
5302
  };
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;
5326
- }
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) });
5303
+ addHome(claudeHomeDir());
5304
+ const homeOverridden = !!(process.env.CHAT_RECALL_CLAUDE_HOME || sources().claudeHome);
5305
+ if (!homeOverridden) {
5306
+ try {
5307
+ for (const entry of readdirSync3(home, { withFileTypes: true })) {
5308
+ if (entry.isDirectory() && entry.name.startsWith(".claude-") && entry.name !== ".claude-code") {
5309
+ addHome(join4(home, entry.name));
5310
+ }
5332
5311
  }
5333
- if (re.lastIndex === m.index)
5334
- re.lastIndex++;
5312
+ } catch {
5335
5313
  }
5336
5314
  }
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)) });
5315
+ if (process.env.CLAUDE_DIRS) {
5316
+ for (const d of process.env.CLAUDE_DIRS.split(",")) {
5317
+ const t = d.trim();
5318
+ if (t)
5319
+ addHome(t.startsWith("~/") ? join4(home, t.slice(2)) : t);
5320
+ }
5340
5321
  }
5341
- return findings;
5322
+ if (dirs.length === 0)
5323
+ dirs.push(join4(claudeHomeDir(), "projects"));
5324
+ return applyDecisions(dirs, opts.includeExcluded);
5342
5325
  }
5343
- function privacyView() {
5344
- let mtimeMs = -1;
5345
- try {
5346
- mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5347
- } catch {
5348
- }
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 {
5357
- }
5358
- privacyCache = { mtimeMs, redactToolOutputs, redactFilePaths: redactFilePaths2 };
5359
- return privacyCache;
5326
+ function _setSourceExclusionFilter(fn) {
5327
+ _sourceExclusionFilter = fn;
5360
5328
  }
5361
- function _resetPrivacyCache() {
5362
- privacyCache = null;
5329
+ function geminiHomeDir() {
5330
+ return process.env.CHAT_RECALL_GEMINI_HOME || sources().geminiHome || join4(homedir3(), ".gemini");
5363
5331
  }
5364
- function isToolOutputRedactionEnabled() {
5365
- return privacyView().redactToolOutputs;
5332
+ function opencodeDbPath() {
5333
+ return process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath || join4(homedir3(), ".local", "share", "opencode", "opencode.db");
5366
5334
  }
5367
- function isFilePathRedactionEnabled() {
5368
- return privacyView().redactFilePaths;
5335
+ function codexHomeDir() {
5336
+ return process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome || join4(homedir3(), ".codex");
5369
5337
  }
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
- });
5338
+ function agyHomeDir() {
5339
+ return process.env.CHAT_RECALL_AGY_HOME || sources().agyHome || join4(homedir3(), ".gemini", "antigravity-cli");
5379
5340
  }
5380
- function applyChunkPrivacy(text) {
5381
- let out = redactSecrets(text);
5382
- if (privacyView().redactFilePaths)
5383
- out = redactFilePaths(out);
5384
- return out;
5341
+ function cursorHomeDir() {
5342
+ return process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome || join4(homedir3(), ".cursor");
5385
5343
  }
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
- }
5643
- }
5644
- if (chunks.length >= maxChunks) {
5645
- return chunks.slice(0, maxChunks);
5646
- }
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
- }
5665
- }
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);
5691
- }
5692
- var INJECTED_BANNERS;
5693
- var init_chunker = __esm({
5694
- "../engine/dist/parsers/chunker.js"() {
5695
- "use strict";
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
- ];
5702
- }
5703
- });
5704
-
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
5715
- });
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;
5726
- }
5727
- }
5728
- function legacyAutoSyncedHomes() {
5729
- const home = homedir2();
5730
- const out = [];
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 {
5761
- }
5762
- }
5763
- return out;
5764
- }
5765
- function grandfatherLegacyHomes() {
5766
- let s;
5767
- try {
5768
- s = loadSettings();
5769
- } catch {
5770
- return { seeded: [] };
5771
- }
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);
5781
- }
5782
- }
5783
- s.sources.approvedHomes = [...approved];
5784
- s.sources.homesGrandfathered = true;
5785
- try {
5786
- saveSettings(s);
5787
- } catch {
5788
- return { seeded: [] };
5789
- }
5790
- return { seeded };
5791
- }
5792
- function primaryHomes() {
5793
- const home = homedir2();
5794
- const env = process.env;
5795
- let s;
5796
- try {
5797
- s = loadSettings().sources;
5798
- } catch {
5799
- s = void 0;
5800
- }
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);
5811
- }
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";
5821
- }
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";
5829
- }
5830
- function isHomeSynced(path) {
5831
- const d = homeDecision(path);
5832
- return d === "primary" || d === "approved";
5833
- }
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;
5855
- }
5856
- function approveHome(path) {
5857
- return setDecision(path, "approved");
5858
- }
5859
- function declineHome(path) {
5860
- return setDecision(path, "declined");
5861
- }
5862
- function resetHomeDecision(path) {
5863
- const p = normalizeHomePath(path);
5864
- let s;
5865
- try {
5866
- s = loadSettings();
5867
- } catch {
5868
- return false;
5869
- }
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;
5876
- }
5877
- return true;
5878
- }
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);
5885
- }
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");
5894
- }
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);
5902
- };
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
- }
5914
- }
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
- }
5921
- }
5922
- if (dirs.length === 0)
5923
- dirs.push(join4(claudeHomeDir(), "projects"));
5924
- return applyDecisions(dirs, opts.includeExcluded);
5925
- }
5926
- function _setSourceExclusionFilter(fn) {
5927
- _sourceExclusionFilter = fn;
5928
- }
5929
- function geminiHomeDir() {
5930
- return process.env.CHAT_RECALL_GEMINI_HOME || sources().geminiHome || join4(homedir3(), ".gemini");
5931
- }
5932
- function opencodeDbPath() {
5933
- return process.env.CHAT_RECALL_OPENCODE_DB || sources().opencodeDbPath || join4(homedir3(), ".local", "share", "opencode", "opencode.db");
5934
- }
5935
- function codexHomeDir() {
5936
- return process.env.CHAT_RECALL_CODEX_HOME || sources().codexHome || join4(homedir3(), ".codex");
5937
- }
5938
- function agyHomeDir() {
5939
- return process.env.CHAT_RECALL_AGY_HOME || sources().agyHome || join4(homedir3(), ".gemini", "antigravity-cli");
5940
- }
5941
- function cursorHomeDir() {
5942
- return process.env.CHAT_RECALL_CURSOR_HOME || sources().cursorHome || join4(homedir3(), ".cursor");
5943
- }
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"));
5344
+ function cursorIdeHomeDir() {
5345
+ return process.env.CHAT_RECALL_CURSOR_IDE_HOME || sources().cursorIdeHome || (process.platform === "darwin" ? join4(homedir3(), "Library", "Application Support", "Cursor") : join4(homedir3(), ".config", "Cursor"));
5946
5346
  }
5947
5347
  function siblingHomes(base3, overridden, suffix) {
5948
5348
  const roots = [];
@@ -6287,7 +5687,7 @@ var init_model_pricing = __esm({
6287
5687
  });
6288
5688
 
6289
5689
  // ../engine/dist/parsers/session.js
6290
- import { createReadStream, existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync2, statSync as statSync4, openSync, readSync, closeSync } from "fs";
5690
+ import { createReadStream, existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync2, statSync as statSync3, openSync, readSync, closeSync } from "fs";
6291
5691
  import { createInterface } from "readline";
6292
5692
  import { join as join5, basename as basename2 } from "path";
6293
5693
  function removeCodeBlocks(text) {
@@ -6334,44 +5734,6 @@ function isValuableToolResult(content, toolName = "") {
6334
5734
  }
6335
5735
  return true;
6336
5736
  }
6337
- function parseSessionsIndex(indexPath) {
6338
- if (!existsSync5(indexPath)) {
6339
- return [];
6340
- }
6341
- const data = JSON.parse(readFileSync2(indexPath, "utf-8"));
6342
- const entries = data.entries || [];
6343
- return entries.map((e) => ({
6344
- sessionId: e.sessionId || "",
6345
- fullPath: e.fullPath || "",
6346
- fileMtime: e.fileMtime || 0,
6347
- firstPrompt: e.firstPrompt || "",
6348
- messageCount: e.messageCount || 0,
6349
- created: e.created || "",
6350
- modified: e.modified || "",
6351
- gitBranch: e.gitBranch || "",
6352
- projectPath: e.projectPath || "",
6353
- isSidechain: e.isSidechain || false
6354
- }));
6355
- }
6356
- function* iterAllSessionIndices(claudeDir) {
6357
- const dir = claudeDir || claudeBackend.projectsDir();
6358
- if (!existsSync5(dir)) {
6359
- return;
6360
- }
6361
- const entries = readdirSync5(dir, { withFileTypes: true });
6362
- for (const entry of entries) {
6363
- if (!entry.isDirectory())
6364
- continue;
6365
- const projectDir = join5(dir, entry.name);
6366
- const indexPath = join5(projectDir, "sessions-index.json");
6367
- if (existsSync5(indexPath)) {
6368
- const sessions = parseSessionsIndex(indexPath);
6369
- if (sessions.length > 0) {
6370
- yield [projectDir, sessions];
6371
- }
6372
- }
6373
- }
6374
- }
6375
5737
  function hasSubagentsDir(sessionPath) {
6376
5738
  const dir = sessionPath.slice(0, -6);
6377
5739
  return existsSync5(join5(dir, "subagents"));
@@ -6382,40 +5744,6 @@ function getSubagentPaths(sessionPath) {
6382
5744
  return [];
6383
5745
  return readdirSync5(subagentsDir).filter((f) => f.endsWith(".jsonl")).sort().map((f) => join5(subagentsDir, f));
6384
5746
  }
6385
- function getSubagentProjectPath(sessionPath, folderDerivedPath) {
6386
- const subagentPaths = getSubagentPaths(sessionPath);
6387
- if (subagentPaths.length === 0)
6388
- return folderDerivedPath;
6389
- const cwdCounts = /* @__PURE__ */ new Map();
6390
- for (const filePath of subagentPaths) {
6391
- try {
6392
- const lines = readFileSync2(filePath, "utf-8").split("\n");
6393
- for (const line of lines) {
6394
- if (!line.trim())
6395
- continue;
6396
- try {
6397
- const entry = JSON.parse(line);
6398
- if (entry.type === "user" && typeof entry.cwd === "string" && entry.cwd) {
6399
- cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) ?? 0) + 1);
6400
- }
6401
- } catch {
6402
- }
6403
- }
6404
- } catch {
6405
- }
6406
- }
6407
- if (cwdCounts.size === 0)
6408
- return folderDerivedPath;
6409
- let best = folderDerivedPath;
6410
- let bestCount = 0;
6411
- for (const [cwd, count] of cwdCounts) {
6412
- if (count > bestCount) {
6413
- best = cwd;
6414
- bestCount = count;
6415
- }
6416
- }
6417
- return best;
6418
- }
6419
5747
  function readCwdFromJsonl(sessionPath) {
6420
5748
  try {
6421
5749
  if (!existsSync5(sessionPath))
@@ -6444,22 +5772,6 @@ function readCwdFromJsonl(sessionPath) {
6444
5772
  }
6445
5773
  return "";
6446
5774
  }
6447
- function getSessionMtime(sessionPath) {
6448
- const subagentPaths = getSubagentPaths(sessionPath);
6449
- if (subagentPaths.length === 0) {
6450
- return statSync4(sessionPath).mtimeMs;
6451
- }
6452
- let maxMtime = statSync4(sessionPath).mtimeMs;
6453
- for (const p of subagentPaths) {
6454
- try {
6455
- const m = statSync4(p).mtimeMs;
6456
- if (m > maxMtime)
6457
- maxMtime = m;
6458
- } catch {
6459
- }
6460
- }
6461
- return maxMtime;
6462
- }
6463
5775
  async function parseSessionFile(sessionPath, maxMessages = 50) {
6464
5776
  const sessionId = basename2(sessionPath, ".jsonl");
6465
5777
  const content = {
@@ -6697,76 +6009,10 @@ ${resultText}` : resultText;
6697
6009
  });
6698
6010
  return content;
6699
6011
  }
6700
- function* getAllSessions(claudeDir) {
6701
- const seenSessionIds = /* @__PURE__ */ new Set();
6702
- const projectDirs = claudeDir ? [claudeDir] : claudeProjectDirs();
6703
- for (const dir of projectDirs) {
6704
- for (const [, entries] of iterAllSessionIndices(dir)) {
6705
- for (const entry of entries) {
6706
- if (!seenSessionIds.has(entry.sessionId) && existsSync5(entry.fullPath)) {
6707
- seenSessionIds.add(entry.sessionId);
6708
- yield [entry, entry.fullPath];
6709
- }
6710
- }
6711
- }
6712
- if (!existsSync5(dir))
6713
- continue;
6714
- const dirEntries = readdirSync5(dir, { withFileTypes: true });
6715
- for (const projDirEntry of dirEntries) {
6716
- if (!projDirEntry.isDirectory())
6717
- continue;
6718
- const projectPath = join5(dir, projDirEntry.name);
6719
- const files = readdirSync5(projectPath);
6720
- for (const file of files) {
6721
- if (!file.endsWith(".jsonl"))
6722
- continue;
6723
- if (file.startsWith("agent-"))
6724
- continue;
6725
- if (file === "sessions-index.json")
6726
- continue;
6727
- const sessionId = basename2(file, ".jsonl");
6728
- if (seenSessionIds.has(sessionId))
6729
- continue;
6730
- const fullPath = join5(projectPath, file);
6731
- try {
6732
- const stat3 = statSync4(fullPath);
6733
- const derivedProjectPath = resolveProjectDirName(projDirEntry.name);
6734
- const effectiveMtime = getSessionMtime(fullPath);
6735
- let projectPath2 = "";
6736
- if (hasSubagentsDir(fullPath)) {
6737
- projectPath2 = getSubagentProjectPath(fullPath, derivedProjectPath);
6738
- } else {
6739
- const fromJsonl = readCwdFromJsonl(fullPath);
6740
- if (fromJsonl)
6741
- projectPath2 = fromJsonl;
6742
- }
6743
- if (!projectPath2)
6744
- projectPath2 = derivedProjectPath;
6745
- const entry = {
6746
- sessionId,
6747
- fullPath,
6748
- fileMtime: effectiveMtime,
6749
- firstPrompt: "",
6750
- messageCount: 0,
6751
- created: stat3.birthtime.toISOString(),
6752
- modified: new Date(effectiveMtime).toISOString(),
6753
- gitBranch: "",
6754
- projectPath: projectPath2,
6755
- isSidechain: false
6756
- };
6757
- seenSessionIds.add(sessionId);
6758
- yield [entry, fullPath];
6759
- } catch {
6760
- continue;
6761
- }
6762
- }
6763
- }
6764
- }
6765
- }
6766
6012
  var init_session = __esm({
6767
6013
  "../engine/dist/parsers/session.js"() {
6768
6014
  "use strict";
6769
- init_chunker();
6015
+ init_banners();
6770
6016
  init_claude2();
6771
6017
  init_tool_paths();
6772
6018
  init_project_dir_name();
@@ -7287,7 +6533,7 @@ __export(raw_exports, {
7287
6533
  parseTranscriptFromContainer: () => parseTranscriptFromContainer
7288
6534
  });
7289
6535
  import { gzipSync, gunzipSync } from "zlib";
7290
- import { createHash as createHash2 } from "crypto";
6536
+ import { createHash } from "crypto";
7291
6537
  import * as fsMod from "fs";
7292
6538
  import * as pathMod from "path";
7293
6539
  function buildRawContainer(exp) {
@@ -7299,7 +6545,7 @@ function buildRawContainer(exp) {
7299
6545
  };
7300
6546
  }
7301
6547
  function containerSrcHash(c) {
7302
- const h = createHash2("sha1");
6548
+ const h = createHash("sha1");
7303
6549
  h.update(c.tool);
7304
6550
  for (const f of c.files) {
7305
6551
  h.update("\0");
@@ -8846,17 +8092,17 @@ var init_opencode = __esm({
8846
8092
  });
8847
8093
 
8848
8094
  // ../engine/dist/core/backends/tail-read.js
8849
- import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2, statSync as statSync5 } from "node:fs";
8850
- function readTailFromOffset(path, offset) {
8095
+ import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2, statSync as statSync4 } from "node:fs";
8096
+ function readTailFromOffset(path, offset, maxBytes = TAIL_READ_MAX_BYTES) {
8851
8097
  let size = 0;
8852
8098
  try {
8853
- size = statSync5(path).size;
8099
+ size = statSync4(path).size;
8854
8100
  } catch {
8855
8101
  return { text: "", newOffset: offset };
8856
8102
  }
8857
8103
  if (size <= offset)
8858
8104
  return { text: "", newOffset: offset };
8859
- const len = Math.min(size - offset, 16 * 1024 * 1024);
8105
+ const len = Math.min(size - offset, Math.max(1, maxBytes));
8860
8106
  const buf = Buffer.allocUnsafe(len);
8861
8107
  let fd = null;
8862
8108
  try {
@@ -8872,11 +8118,16 @@ function readTailFromOffset(path, offset) {
8872
8118
  return { text: "", newOffset: offset };
8873
8119
  const slice = got < len ? buf.subarray(0, got) : buf;
8874
8120
  const lastNl = slice.lastIndexOf(10);
8875
- if (lastNl < 0) {
8876
- return { text: "", newOffset: offset };
8121
+ if (lastNl >= 0) {
8122
+ const text = slice.subarray(0, lastNl + 1).toString("utf-8");
8123
+ return { text, newOffset: offset + lastNl + 1 };
8877
8124
  }
8878
- const text = slice.subarray(0, lastNl + 1).toString("utf-8");
8879
- return { text, newOffset: offset + lastNl + 1 };
8125
+ if (offset + got >= size)
8126
+ return { text: "", newOffset: offset };
8127
+ const end = findNewline(fd, offset + got, size);
8128
+ if (end < 0)
8129
+ return { text: "", newOffset: offset };
8130
+ return { text: "", newOffset: end + 1, skippedBytes: end + 1 - offset };
8880
8131
  } catch {
8881
8132
  return { text: "", newOffset: offset };
8882
8133
  } finally {
@@ -8888,14 +8139,31 @@ function readTailFromOffset(path, offset) {
8888
8139
  }
8889
8140
  }
8890
8141
  }
8142
+ function findNewline(fd, from, size) {
8143
+ const block = Buffer.allocUnsafe(SKIP_SCAN_BYTES);
8144
+ let pos = from;
8145
+ while (pos < size) {
8146
+ const n = readSync2(fd, block, 0, Math.min(SKIP_SCAN_BYTES, size - pos), pos);
8147
+ if (n <= 0)
8148
+ return -1;
8149
+ const i = block.subarray(0, n).indexOf(10);
8150
+ if (i >= 0)
8151
+ return pos + i;
8152
+ pos += n;
8153
+ }
8154
+ return -1;
8155
+ }
8156
+ var TAIL_READ_MAX_BYTES, SKIP_SCAN_BYTES;
8891
8157
  var init_tail_read = __esm({
8892
8158
  "../engine/dist/core/backends/tail-read.js"() {
8893
8159
  "use strict";
8160
+ TAIL_READ_MAX_BYTES = 16 * 1024 * 1024;
8161
+ SKIP_SCAN_BYTES = 1024 * 1024;
8894
8162
  }
8895
8163
  });
8896
8164
 
8897
8165
  // ../engine/dist/core/backends/codex.js
8898
- import { readdirSync as readdirSync6, readFileSync as readFileSync3, statSync as statSync6, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
8166
+ import { readdirSync as readdirSync6, readFileSync as readFileSync3, statSync as statSync5, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
8899
8167
  import { basename as basename7, dirname as dirname9, join as join9 } from "path";
8900
8168
  function parseShellWrites(cmd) {
8901
8169
  const out = [];
@@ -9103,7 +8371,7 @@ var init_codex2 = __esm({
9103
8371
  return null;
9104
8372
  let mtime = 0;
9105
8373
  try {
9106
- mtime = statSync6(located.path).mtimeMs;
8374
+ mtime = statSync5(located.path).mtimeMs;
9107
8375
  } catch {
9108
8376
  }
9109
8377
  return {
@@ -9123,7 +8391,7 @@ var init_codex2 = __esm({
9123
8391
  for (const filePath of walkRollouts(root)) {
9124
8392
  let stat3;
9125
8393
  try {
9126
- stat3 = statSync6(filePath);
8394
+ stat3 = statSync5(filePath);
9127
8395
  } catch {
9128
8396
  continue;
9129
8397
  }
@@ -9319,7 +8587,7 @@ var init_codex2 = __esm({
9319
8587
  let mtime = 0;
9320
8588
  try {
9321
8589
  raw = readFileSync3(path, "utf-8");
9322
- mtime = statSync6(path).mtimeMs;
8590
+ mtime = statSync5(path).mtimeMs;
9323
8591
  } catch {
9324
8592
  return [];
9325
8593
  }
@@ -9419,7 +8687,7 @@ var init_codex2 = __esm({
9419
8687
  const events = this.readEvents(this.toRawId(id));
9420
8688
  let fileMtime = 0;
9421
8689
  try {
9422
- fileMtime = statSync6(located.path).mtimeMs;
8690
+ fileMtime = statSync5(located.path).mtimeMs;
9423
8691
  } catch {
9424
8692
  }
9425
8693
  return liveScanEditsFromEvents(events, this.fileToolMap, {
@@ -9483,7 +8751,7 @@ var init_codex2 = __esm({
9483
8751
  const p = join9(dir, entry.name);
9484
8752
  let st;
9485
8753
  try {
9486
- st = statSync6(p);
8754
+ st = statSync5(p);
9487
8755
  } catch {
9488
8756
  continue;
9489
8757
  }
@@ -9550,7 +8818,7 @@ var init_codex2 = __esm({
9550
8818
  let mtime = 0;
9551
8819
  const push = (path, name) => {
9552
8820
  try {
9553
- const st = statSync6(path);
8821
+ const st = statSync5(path);
9554
8822
  files.push({ name, bytes: readFileSync3(path) });
9555
8823
  if (st.mtimeMs > mtime)
9556
8824
  mtime = st.mtimeMs;
@@ -9592,16 +8860,16 @@ var init_codex2 = __esm({
9592
8860
  if (!loc)
9593
8861
  return 0;
9594
8862
  try {
9595
- return statSync6(loc.path).size;
8863
+ return statSync5(loc.path).size;
9596
8864
  } catch {
9597
8865
  return 0;
9598
8866
  }
9599
8867
  }
9600
- async readFromOffset(prefixedId, offset) {
8868
+ async readFromOffset(prefixedId, offset, maxBytes) {
9601
8869
  const loc = this.findSession(prefixedId);
9602
8870
  if (!loc)
9603
8871
  return { text: "", newOffset: offset };
9604
- return readTailFromOffset(loc.path, offset);
8872
+ return readTailFromOffset(loc.path, offset, maxBytes);
9605
8873
  }
9606
8874
  };
9607
8875
  codexBackend = new CodexBackend();
@@ -9609,7 +8877,7 @@ var init_codex2 = __esm({
9609
8877
  });
9610
8878
 
9611
8879
  // ../engine/dist/core/backends/agy.js
9612
- import { existsSync as existsSync8, readdirSync as readdirSync7, readFileSync as readFileSync4, statSync as statSync7 } from "fs";
8880
+ import { existsSync as existsSync8, readdirSync as readdirSync7, readFileSync as readFileSync4, statSync as statSync6 } from "fs";
9613
8881
  import { basename as basename8, dirname as dirname10, join as join10 } from "path";
9614
8882
  function agyTranscriptPath(logsDir) {
9615
8883
  const full = join10(logsDir, "transcript_full.jsonl");
@@ -9802,7 +9070,7 @@ var init_agy = __esm({
9802
9070
  return null;
9803
9071
  let mtime = 0;
9804
9072
  try {
9805
- mtime = statSync7(path).mtimeMs;
9073
+ mtime = statSync6(path).mtimeMs;
9806
9074
  } catch {
9807
9075
  }
9808
9076
  const projectPath = resolveAgyProject(dirname10(path), this.homeDir());
@@ -9830,7 +9098,7 @@ var init_agy = __esm({
9830
9098
  for (const rawId of sessionDirs) {
9831
9099
  const sessionPath = join10(brainDir, rawId);
9832
9100
  try {
9833
- if (!statSync7(sessionPath).isDirectory())
9101
+ if (!statSync6(sessionPath).isDirectory())
9834
9102
  continue;
9835
9103
  } catch {
9836
9104
  continue;
@@ -9840,7 +9108,7 @@ var init_agy = __esm({
9840
9108
  continue;
9841
9109
  let stat3;
9842
9110
  try {
9843
- stat3 = statSync7(filePath);
9111
+ stat3 = statSync6(filePath);
9844
9112
  } catch {
9845
9113
  continue;
9846
9114
  }
@@ -10105,7 +9373,7 @@ var init_agy = __esm({
10105
9373
  for (const rawId of sessionDirs) {
10106
9374
  const sessionPath = join10(brainDir, rawId);
10107
9375
  try {
10108
- if (!statSync7(sessionPath).isDirectory())
9376
+ if (!statSync6(sessionPath).isDirectory())
10109
9377
  continue;
10110
9378
  } catch {
10111
9379
  continue;
@@ -10114,7 +9382,7 @@ var init_agy = __esm({
10114
9382
  if (!filePath)
10115
9383
  continue;
10116
9384
  try {
10117
- const st = statSync7(filePath);
9385
+ const st = statSync6(filePath);
10118
9386
  if (st.mtimeMs < opts.sinceMs)
10119
9387
  continue;
10120
9388
  candidates2.push({ rawId, mtime: st.mtimeMs });
@@ -10142,7 +9410,7 @@ var init_agy = __esm({
10142
9410
  try {
10143
9411
  return {
10144
9412
  tool: "agy",
10145
- mtime: statSync7(loc.path).mtimeMs,
9413
+ mtime: statSync6(loc.path).mtimeMs,
10146
9414
  files: [{ name: basename8(loc.path), bytes: readFileSync4(loc.path) }]
10147
9415
  };
10148
9416
  } catch {
@@ -10157,16 +9425,16 @@ var init_agy = __esm({
10157
9425
  if (!loc)
10158
9426
  return 0;
10159
9427
  try {
10160
- return statSync7(loc.path).size;
9428
+ return statSync6(loc.path).size;
10161
9429
  } catch {
10162
9430
  return 0;
10163
9431
  }
10164
9432
  }
10165
- async readFromOffset(prefixedId, offset) {
9433
+ async readFromOffset(prefixedId, offset, maxBytes) {
10166
9434
  const loc = this.findSession(prefixedId);
10167
9435
  if (!loc)
10168
9436
  return { text: "", newOffset: offset };
10169
- return readTailFromOffset(loc.path, offset);
9437
+ return readTailFromOffset(loc.path, offset, maxBytes);
10170
9438
  }
10171
9439
  };
10172
9440
  agyBackend = new AgyBackend();
@@ -10174,7 +9442,7 @@ var init_agy = __esm({
10174
9442
  });
10175
9443
 
10176
9444
  // ../engine/dist/core/backends/cursor-store.js
10177
- import { existsSync as existsSync9, readdirSync as readdirSync8, readFileSync as readFileSync5, statSync as statSync8 } from "fs";
9445
+ import { existsSync as existsSync9, readdirSync as readdirSync8, readFileSync as readFileSync5, statSync as statSync7 } from "fs";
10178
9446
  import { join as join11 } from "path";
10179
9447
  function cursorProjectSlug(absPath) {
10180
9448
  return absPath.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -10191,7 +9459,7 @@ function listChatsIn(chatsRoot) {
10191
9459
  const hashPath3 = join11(chatsRoot, workspaceHash);
10192
9460
  let chatIds;
10193
9461
  try {
10194
- if (!statSync8(hashPath3).isDirectory())
9462
+ if (!statSync7(hashPath3).isDirectory())
10195
9463
  continue;
10196
9464
  chatIds = readdirSync8(hashPath3);
10197
9465
  } catch {
@@ -10213,7 +9481,7 @@ function listChatsIn(chatsRoot) {
10213
9481
  let created = meta.createdAtMs ?? 0;
10214
9482
  if (!mtime || !created) {
10215
9483
  try {
10216
- const st = statSync8(dbPath);
9484
+ const st = statSync7(dbPath);
10217
9485
  mtime = mtime || st.mtimeMs;
10218
9486
  created = created || st.birthtimeMs || st.mtimeMs;
10219
9487
  } catch {
@@ -10256,7 +9524,7 @@ function findChat(chatsRoots, chatId) {
10256
9524
  let created = meta.createdAtMs ?? 0;
10257
9525
  if (!mtime || !created) {
10258
9526
  try {
10259
- const st = statSync8(dbPath);
9527
+ const st = statSync7(dbPath);
10260
9528
  mtime = mtime || st.mtimeMs;
10261
9529
  created = created || st.birthtimeMs || st.mtimeMs;
10262
9530
  } catch {
@@ -10559,7 +9827,7 @@ var init_cursor_store = __esm({
10559
9827
  });
10560
9828
 
10561
9829
  // ../engine/dist/core/backends/cursor-ide.js
10562
- import { existsSync as existsSync10, readFileSync as readFileSync6, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
9830
+ import { existsSync as existsSync10, readFileSync as readFileSync6, readdirSync as readdirSync9, statSync as statSync8 } from "fs";
10563
9831
  import { dirname as dirname11, join as join12 } from "path";
10564
9832
  function jsonOf(value) {
10565
9833
  if (value == null)
@@ -10629,7 +9897,7 @@ function workspaceIndex(workspaceRoots) {
10629
9897
  for (const id of entries) {
10630
9898
  const dir = join12(root, id);
10631
9899
  try {
10632
- if (!statSync9(dir).isDirectory())
9900
+ if (!statSync8(dir).isDirectory())
10633
9901
  continue;
10634
9902
  } catch {
10635
9903
  continue;
@@ -10975,7 +10243,7 @@ var init_cursor_ide = __esm({
10975
10243
  });
10976
10244
 
10977
10245
  // ../engine/dist/core/backends/cursor.js
10978
- import { existsSync as existsSync11, readFileSync as readFileSync7, statSync as statSync10 } from "fs";
10246
+ import { existsSync as existsSync11, readFileSync as readFileSync7, statSync as statSync9 } from "fs";
10979
10247
  import { join as join13 } from "path";
10980
10248
  var PREFIX4, CursorBackend, cursorBackend;
10981
10249
  var init_cursor = __esm({
@@ -11028,7 +10296,7 @@ var init_cursor = __esm({
11028
10296
  if (r.surface === "cli") {
11029
10297
  let mtime2 = r.chat.updatedAtMs;
11030
10298
  try {
11031
- mtime2 = statSync10(r.chat.dbPath).mtimeMs || mtime2;
10299
+ mtime2 = statSync9(r.chat.dbPath).mtimeMs || mtime2;
11032
10300
  } catch {
11033
10301
  }
11034
10302
  return {
@@ -11041,7 +10309,7 @@ var init_cursor = __esm({
11041
10309
  }
11042
10310
  let mtime = r.composer.updatedAtMs;
11043
10311
  try {
11044
- mtime = statSync10(r.composer.globalDbPath).mtimeMs || mtime;
10312
+ mtime = statSync9(r.composer.globalDbPath).mtimeMs || mtime;
11045
10313
  } catch {
11046
10314
  }
11047
10315
  return {
@@ -11287,7 +10555,7 @@ var init_cursor = __esm({
11287
10555
  if (existsSync11(metaPath)) {
11288
10556
  files.push({ name: "meta.json", bytes: readFileSync7(metaPath) });
11289
10557
  }
11290
- return { tool: "cursor", mtime: statSync10(r.chat.dbPath).mtimeMs, files };
10558
+ return { tool: "cursor", mtime: statSync9(r.chat.dbPath).mtimeMs, files };
11291
10559
  }
11292
10560
  const events = readComposerEvents(r.composer);
11293
10561
  if (events.length === 0)
@@ -11295,7 +10563,7 @@ var init_cursor = __esm({
11295
10563
  const body = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
11296
10564
  return {
11297
10565
  tool: "cursor",
11298
- mtime: statSync10(r.composer.globalDbPath).mtimeMs,
10566
+ mtime: statSync9(r.composer.globalDbPath).mtimeMs,
11299
10567
  files: [{ name: `${r.composer.composerId}.jsonl`, bytes: Buffer.from(body, "utf8") }]
11300
10568
  };
11301
10569
  } catch {
@@ -11361,8 +10629,8 @@ var init_backends = __esm({
11361
10629
 
11362
10630
  // ../engine/dist/transcript/shadow.js
11363
10631
  import { gzipSync as gzipSync2, gunzipSync as gunzipSync2 } from "zlib";
11364
- import { createHash as createHash3 } from "crypto";
11365
- import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync2, renameSync as renameSync2, statSync as statSync11 } from "fs";
10632
+ import { createHash as createHash2 } from "crypto";
10633
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync2, renameSync as renameSync2, statSync as statSync10, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
11366
10634
  import { dirname as dirname12, join as join14 } from "path";
11367
10635
  function shadowRoot() {
11368
10636
  return join14(getDataDir(), "shadow");
@@ -11392,7 +10660,7 @@ function writeShadowContainer(tool, rawId, container) {
11392
10660
  renameSync2(tmp, path);
11393
10661
  }
11394
10662
  function sha1(s) {
11395
- return createHash3("sha1").update(s).digest("hex");
10663
+ return createHash2("sha1").update(s).digest("hex");
11396
10664
  }
11397
10665
  function lineKey(line) {
11398
10666
  try {
@@ -11570,7 +10838,7 @@ function liveFingerprint(backend, rawId) {
11570
10838
  const loc = backend.findSession(rawId);
11571
10839
  if (!loc?.path)
11572
10840
  return "";
11573
- const st = statSync11(loc.path);
10841
+ const st = statSync10(loc.path);
11574
10842
  return `${st.mtimeMs}:${st.size}`;
11575
10843
  } catch {
11576
10844
  return "";
@@ -11586,9 +10854,32 @@ function seedShadow(sessionId, container) {
11586
10854
  }
11587
10855
  return fullest;
11588
10856
  }
10857
+ function shadowUncompressedBytes(tool, rawId) {
10858
+ const path = shadowFileFor(tool, rawId);
10859
+ let fd = null;
10860
+ try {
10861
+ const size = statSync10(path).size;
10862
+ if (size < 18)
10863
+ return 0;
10864
+ fd = openSync4(path, "r");
10865
+ const tail = Buffer.alloc(4);
10866
+ if (readSync4(fd, tail, 0, 4, size - 4) !== 4)
10867
+ return 0;
10868
+ return tail.readUInt32LE(0);
10869
+ } catch {
10870
+ return 0;
10871
+ } finally {
10872
+ if (fd !== null) {
10873
+ try {
10874
+ closeSync4(fd);
10875
+ } catch {
10876
+ }
10877
+ }
10878
+ }
10879
+ }
11589
10880
  function shadowSizeOnDisk(tool, rawId) {
11590
10881
  try {
11591
- return statSync11(shadowFileFor(tool, rawId)).size;
10882
+ return statSync10(shadowFileFor(tool, rawId)).size;
11592
10883
  } catch {
11593
10884
  return 0;
11594
10885
  }
@@ -11606,7 +10897,7 @@ var init_shadow = __esm({
11606
10897
  });
11607
10898
 
11608
10899
  // ../engine/dist/core/live-session-scan.js
11609
- import { existsSync as existsSync13, readdirSync as readdirSync10, readFileSync as readFileSync9, statSync as statSync12, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
10900
+ import { existsSync as existsSync13, readdirSync as readdirSync10, readFileSync as readFileSync9, statSync as statSync11, openSync as openSync5, readSync as readSync5, closeSync as closeSync5 } from "fs";
11610
10901
  import { join as join15, basename as basename9 } from "path";
11611
10902
  function detectTool(sessionId) {
11612
10903
  return getBackendForId(sessionId)?.id ?? "claude";
@@ -11633,6 +10924,15 @@ function sessionFileIndex() {
11633
10924
  return indexCache;
11634
10925
  const byId = /* @__PURE__ */ new Map();
11635
10926
  const seenRoots = /* @__PURE__ */ new Set();
10927
+ const projectPaths = /* @__PURE__ */ new Map();
10928
+ const projectPathOf = (dirName) => {
10929
+ let p = projectPaths.get(dirName);
10930
+ if (p === void 0) {
10931
+ p = resolveProjectDirName(dirName);
10932
+ projectPaths.set(dirName, p);
10933
+ }
10934
+ return p;
10935
+ };
11636
10936
  for (const root of claudeProjectDirs()) {
11637
10937
  if (seenRoots.has(root) || !existsSync13(root))
11638
10938
  continue;
@@ -11660,7 +10960,7 @@ function sessionFileIndex() {
11660
10960
  const located = {
11661
10961
  path: join15(dir, name),
11662
10962
  projectDir: entry.name,
11663
- projectPath: resolveProjectDirName(entry.name)
10963
+ projectPath: projectPathOf(entry.name)
11664
10964
  };
11665
10965
  const prior = byId.get(id);
11666
10966
  if (prior)
@@ -11724,7 +11024,7 @@ function readSessionGroupText(group) {
11724
11024
  const parts = [];
11725
11025
  for (const p of group.paths) {
11726
11026
  try {
11727
- const st = statSync12(p);
11027
+ const st = statSync11(p);
11728
11028
  parts.push(`${p}:${st.mtimeMs}:${st.size}`);
11729
11029
  } catch {
11730
11030
  parts.push(`${p}:missing`);
@@ -11744,7 +11044,7 @@ function readSessionGroupText(group) {
11744
11044
  continue;
11745
11045
  }
11746
11046
  try {
11747
- mtime = Math.max(mtime, statSync12(p).mtimeMs);
11047
+ mtime = Math.max(mtime, statSync11(p).mtimeMs);
11748
11048
  } catch {
11749
11049
  }
11750
11050
  text = text ? mergeLineText(text, raw).text : raw;
@@ -11850,7 +11150,7 @@ var init_live_session_scan = __esm({
11850
11150
  });
11851
11151
 
11852
11152
  // ../engine/dist/core/first-prompt.js
11853
- import { createReadStream as createReadStream2, existsSync as existsSync14, openSync as openSync5, readSync as readSync5, closeSync as closeSync5 } from "fs";
11153
+ import { createReadStream as createReadStream2, existsSync as existsSync14, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
11854
11154
  import { StringDecoder } from "string_decoder";
11855
11155
  function stripBanners2(s) {
11856
11156
  let out = s;
@@ -11904,7 +11204,7 @@ function extractFirstUserPromptSync(filePath, options = {}) {
11904
11204
  const maxBytes = options.maxBytes ?? 4 * 1024 * 1024;
11905
11205
  let fd;
11906
11206
  try {
11907
- fd = openSync5(filePath, "r");
11207
+ fd = openSync6(filePath, "r");
11908
11208
  } catch {
11909
11209
  return "";
11910
11210
  }
@@ -11916,7 +11216,7 @@ function extractFirstUserPromptSync(filePath, options = {}) {
11916
11216
  let pos = 0;
11917
11217
  let lineNo = 0;
11918
11218
  for (; ; ) {
11919
- const n = readSync5(fd, buf, 0, CHUNK, pos);
11219
+ const n = readSync6(fd, buf, 0, CHUNK, pos);
11920
11220
  if (n <= 0)
11921
11221
  break;
11922
11222
  pos += n;
@@ -11942,7 +11242,7 @@ function extractFirstUserPromptSync(filePath, options = {}) {
11942
11242
  return "";
11943
11243
  } finally {
11944
11244
  try {
11945
- closeSync5(fd);
11245
+ closeSync6(fd);
11946
11246
  } catch {
11947
11247
  }
11948
11248
  }
@@ -11961,7 +11261,7 @@ var init_first_prompt = __esm({
11961
11261
  });
11962
11262
 
11963
11263
  // ../engine/dist/core/backends/claude.js
11964
- import { existsSync as existsSync15, readdirSync as readdirSync11, readFileSync as readFileSync10, statSync as statSync13, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
11264
+ import { existsSync as existsSync15, readdirSync as readdirSync11, readFileSync as readFileSync10, statSync as statSync12, openSync as openSync7, readSync as readSync7, closeSync as closeSync7 } from "fs";
11965
11265
  import { join as join16, basename as basename10 } from "path";
11966
11266
  function stripReminders(text) {
11967
11267
  return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
@@ -12058,13 +11358,13 @@ var init_claude2 = __esm({
12058
11358
  return null;
12059
11359
  let head;
12060
11360
  try {
12061
- const fd = openSync6(located.path, "r");
11361
+ const fd = openSync7(located.path, "r");
12062
11362
  try {
12063
11363
  const buf = Buffer.alloc(65536);
12064
- const n = readSync6(fd, buf, 0, buf.length, 0);
11364
+ const n = readSync7(fd, buf, 0, buf.length, 0);
12065
11365
  head = buf.toString("utf-8", 0, n);
12066
11366
  } finally {
12067
- closeSync6(fd);
11367
+ closeSync7(fd);
12068
11368
  }
12069
11369
  } catch {
12070
11370
  return null;
@@ -12091,7 +11391,7 @@ var init_claude2 = __esm({
12091
11391
  let mtime = 0;
12092
11392
  for (const c of copies) {
12093
11393
  try {
12094
- mtime = Math.max(mtime, statSync13(c.path).mtimeMs);
11394
+ mtime = Math.max(mtime, statSync12(c.path).mtimeMs);
12095
11395
  } catch {
12096
11396
  }
12097
11397
  }
@@ -12167,7 +11467,7 @@ var init_claude2 = __esm({
12167
11467
  const fullPath = join16(projPath, f);
12168
11468
  let stat3;
12169
11469
  try {
12170
- stat3 = statSync13(fullPath);
11470
+ stat3 = statSync12(fullPath);
12171
11471
  } catch {
12172
11472
  continue;
12173
11473
  }
@@ -12388,7 +11688,7 @@ var init_claude2 = __esm({
12388
11688
  const events = this.readEvents(rawId);
12389
11689
  let fileMtime = 0;
12390
11690
  try {
12391
- fileMtime = statSync13(located.path).mtimeMs;
11691
+ fileMtime = statSync12(located.path).mtimeMs;
12392
11692
  } catch {
12393
11693
  }
12394
11694
  return liveScanEditsFromEvents(events, this.fileToolMap, {
@@ -12443,13 +11743,13 @@ var init_claude2 = __esm({
12443
11743
  continue;
12444
11744
  const fp = join16(projPath, f);
12445
11745
  try {
12446
- const st = statSync13(fp);
11746
+ const st = statSync12(fp);
12447
11747
  let mtime = st.mtimeMs;
12448
11748
  const subDir = join16(projPath, f.slice(0, -6), "subagents");
12449
11749
  if (existsSync15(subDir)) {
12450
11750
  for (const sub of readdirSync11(subDir)) {
12451
11751
  try {
12452
- const subSt = statSync13(join16(subDir, sub));
11752
+ const subSt = statSync12(join16(subDir, sub));
12453
11753
  if (subSt.mtimeMs > mtime)
12454
11754
  mtime = subSt.mtimeMs;
12455
11755
  } catch {
@@ -12514,7 +11814,7 @@ var init_claude2 = __esm({
12514
11814
  let total = 0;
12515
11815
  for (const located of findSessionFiles(this.toRawId(prefixedId))) {
12516
11816
  try {
12517
- total += statSync13(located.path).size;
11817
+ total += statSync12(located.path).size;
12518
11818
  } catch {
12519
11819
  }
12520
11820
  }
@@ -12536,417 +11836,877 @@ var init_claude2 = __esm({
12536
11836
  spansMultipleSources(prefixedId) {
12537
11837
  return findSessionFiles(this.toRawId(prefixedId)).length > 1;
12538
11838
  }
12539
- async readFromOffset(prefixedId, offset) {
12540
- const rawId = this.toRawId(prefixedId);
12541
- const copies = findSessionFiles(rawId);
12542
- if (copies.length === 0)
12543
- return { text: "", newOffset: offset };
12544
- if (copies.length > 1)
12545
- return { text: "", newOffset: offset };
12546
- return readTailFromOffset(copies[0].path, offset);
11839
+ async readFromOffset(prefixedId, offset, maxBytes) {
11840
+ const rawId = this.toRawId(prefixedId);
11841
+ const copies = findSessionFiles(rawId);
11842
+ if (copies.length === 0)
11843
+ return { text: "", newOffset: offset };
11844
+ if (copies.length > 1)
11845
+ return { text: "", newOffset: offset };
11846
+ return readTailFromOffset(copies[0].path, offset, maxBytes);
11847
+ }
11848
+ };
11849
+ claudeBackend = new ClaudeBackend();
11850
+ }
11851
+ });
11852
+
11853
+ // ../engine/dist/core/project-resolver.js
11854
+ var project_resolver_exports = {};
11855
+ __export(project_resolver_exports, {
11856
+ _setProjectsConfigForTests: () => _setProjectsConfigForTests,
11857
+ getProjectsConfigPath: () => getProjectsConfigPath,
11858
+ globMatch: () => globMatch,
11859
+ loadProjectsConfig: () => loadProjectsConfig,
11860
+ parseGitRemote: () => parseGitRemote,
11861
+ resetProjectResolverCache: () => resetProjectResolverCache,
11862
+ resolveProjectId: () => resolveProjectId,
11863
+ resolveWorkspaceId: () => resolveWorkspaceId
11864
+ });
11865
+ import { execSync } from "child_process";
11866
+ import { createHash as createHash3 } from "crypto";
11867
+ import { existsSync as existsSync16, readFileSync as readFileSync11, realpathSync as realpathSync2, readdirSync as readdirSync12, statSync as statSync13 } from "fs";
11868
+ import { basename as basename11, dirname as dirname13, isAbsolute, join as join17, resolve, sep } from "path";
11869
+ function getProjectsConfigPath() {
11870
+ return join17(getDataDir(), "projects.json");
11871
+ }
11872
+ function loadProjectsConfig() {
11873
+ if (cachedConfig && cachedConfig.mtime === Number.MAX_SAFE_INTEGER) {
11874
+ return cachedConfig.cfg;
11875
+ }
11876
+ const path = getProjectsConfigPath();
11877
+ if (!existsSync16(path)) {
11878
+ cachedConfig = { mtime: 0, cfg: {} };
11879
+ return cachedConfig.cfg;
11880
+ }
11881
+ const mtime = statSync13(path).mtimeMs;
11882
+ if (cachedConfig && cachedConfig.mtime === mtime)
11883
+ return cachedConfig.cfg;
11884
+ try {
11885
+ const raw = readFileSync11(path, "utf-8");
11886
+ const cfg = JSON.parse(raw);
11887
+ cachedConfig = { mtime, cfg };
11888
+ return cfg;
11889
+ } catch {
11890
+ cachedConfig = { mtime, cfg: {} };
11891
+ return cachedConfig.cfg;
11892
+ }
11893
+ }
11894
+ function resetProjectResolverCache() {
11895
+ resolveCache.clear();
11896
+ gitCache.clear();
11897
+ workspaceCache.clear();
11898
+ wsIdCache.clear();
11899
+ cachedConfig = null;
11900
+ }
11901
+ function _setProjectsConfigForTests(cfg) {
11902
+ resetProjectResolverCache();
11903
+ cachedConfig = cfg === null ? null : { mtime: Number.MAX_SAFE_INTEGER, cfg };
11904
+ }
11905
+ function resolveProjectId(inputPath) {
11906
+ if (!inputPath) {
11907
+ return { id: "", displayName: "", source: "path" };
11908
+ }
11909
+ if (!isAbsolute(inputPath)) {
11910
+ return { id: `path:${inputPath}`, displayName: inputPath, source: "path" };
11911
+ }
11912
+ const realPath = safeRealpath(inputPath);
11913
+ const cached = resolveCache.get(realPath);
11914
+ if (cached)
11915
+ return cached;
11916
+ const result = doResolve(realPath);
11917
+ resolveCache.set(realPath, result);
11918
+ return result;
11919
+ }
11920
+ function resolveWorkspaceId(inputPath) {
11921
+ if (!inputPath || !isAbsolute(inputPath))
11922
+ return null;
11923
+ const realPath = safeRealpath(inputPath);
11924
+ const cached = wsIdCache.get(realPath);
11925
+ if (cached !== void 0)
11926
+ return cached;
11927
+ const result = doResolveWorkspace(realPath);
11928
+ wsIdCache.set(realPath, result);
11929
+ return result;
11930
+ }
11931
+ function doResolveWorkspace(realPath) {
11932
+ const cfg = loadProjectsConfig();
11933
+ const declaredWorkspaces = (cfg.projects || []).filter((p) => p.workspace).sort((a, b) => b.root.length - a.root.length);
11934
+ for (const proj of declaredWorkspaces) {
11935
+ if (isInside(realPath, safeRealpath(proj.root)))
11936
+ return proj.id;
11937
+ }
11938
+ const anchor = findGitToplevel(realPath) ?? realPath;
11939
+ const ws = resolveAutoWorkspace(anchor, cfg.autoWorkspaceMinRepos ?? DEFAULT_AUTO_WORKSPACE_MIN_REPOS);
11940
+ return ws ? ws.id : null;
11941
+ }
11942
+ function doResolve(realPath) {
11943
+ const cfg = loadProjectsConfig();
11944
+ if (matchesIgnore(realPath, cfg)) {
11945
+ return { id: `ignored:${realPath}`, displayName: basename11(realPath), source: "ignored" };
11946
+ }
11947
+ const declared = matchDeclaredProject(realPath, cfg.projects || []);
11948
+ if (declared)
11949
+ return declared;
11950
+ const bot = resolveBotWorktree(realPath);
11951
+ if (bot)
11952
+ return bot;
11953
+ const git2 = resolveGit(realPath);
11954
+ if (git2)
11955
+ return git2;
11956
+ const ws = resolveAutoWorkspace(realPath, cfg.autoWorkspaceMinRepos ?? DEFAULT_AUTO_WORKSPACE_MIN_REPOS);
11957
+ if (ws)
11958
+ return ws;
11959
+ return {
11960
+ id: `path:${realPath}`,
11961
+ displayName: basename11(realPath) || realPath,
11962
+ source: "path"
11963
+ };
11964
+ }
11965
+ function matchesIgnore(realPath, cfg) {
11966
+ const rules = cfg.ignore || [];
11967
+ for (const r of rules) {
11968
+ if (globMatch(r.match, realPath))
11969
+ return true;
11970
+ }
11971
+ return false;
11972
+ }
11973
+ function resolveBotWorktree(realPath) {
11974
+ const parts = realPath.split(sep);
11975
+ const idx = parts.findIndex((p) => BOT_WORKTREE_MARKERS.includes(p));
11976
+ if (idx === -1)
11977
+ return null;
11978
+ const botRoot = parts.slice(0, idx + 1).join(sep);
11979
+ return {
11980
+ id: `path:${botRoot}`,
11981
+ displayName: parts[idx],
11982
+ source: "path"
11983
+ };
11984
+ }
11985
+ function matchDeclaredProject(realPath, projects) {
11986
+ const candidates2 = [...projects].sort((a, b) => b.root.length - a.root.length);
11987
+ for (const proj of candidates2) {
11988
+ const root = safeRealpath(proj.root);
11989
+ if (!isInside(realPath, root))
11990
+ continue;
11991
+ if (!proj.workspace && proj.children?.length) {
11992
+ const child = matchChild(realPath, root, proj.children);
11993
+ if (child) {
11994
+ return {
11995
+ id: `${proj.id}/${child.id}`,
11996
+ displayName: child.name || child.id,
11997
+ source: "user",
11998
+ workspaceId: proj.id
11999
+ };
12000
+ }
12001
+ }
12002
+ return {
12003
+ id: proj.id,
12004
+ displayName: proj.name || proj.id,
12005
+ source: "user",
12006
+ workspaceId: proj.workspace ? void 0 : void 0
12007
+ };
12008
+ }
12009
+ return null;
12010
+ }
12011
+ function matchChild(realPath, parentRoot, children) {
12012
+ const sorted = [...children].sort((a, b) => b.path.length - a.path.length);
12013
+ for (const c of sorted) {
12014
+ const childRoot = safeRealpath(join17(parentRoot, c.path));
12015
+ if (isInside(realPath, childRoot))
12016
+ return c;
12017
+ }
12018
+ return null;
12019
+ }
12020
+ function resolveGit(realPath) {
12021
+ const toplevel = findGitToplevel(realPath);
12022
+ if (!toplevel)
12023
+ return null;
12024
+ const probe = getGitRemote(toplevel);
12025
+ if (probe.kind === "remote") {
12026
+ const parsed = parseGitRemote(probe.url);
12027
+ if (parsed) {
12028
+ return {
12029
+ id: `git:${parsed.host}/${parsed.owner}/${parsed.repo}`,
12030
+ displayName: parsed.repo,
12031
+ source: "git-remote"
12032
+ };
12033
+ }
12034
+ }
12035
+ if (probe.kind === "failed") {
12036
+ console.error(`[project] could not read git remotes in ${toplevel} \u2014 filing it as a local-only repo for now, which is probably wrong: ${probe.reason}`);
12037
+ }
12038
+ const hash = createHash3("sha1").update(toplevel).digest("hex").slice(0, 12);
12039
+ return {
12040
+ id: `git-local:${hash}`,
12041
+ displayName: basename11(toplevel),
12042
+ source: "git-local"
12043
+ };
12044
+ }
12045
+ function findGitToplevel(realPath) {
12046
+ let cur = realPath;
12047
+ let last = "";
12048
+ while (cur && cur !== last) {
12049
+ if (existsSync16(join17(cur, ".git")))
12050
+ return cur;
12051
+ last = cur;
12052
+ cur = dirname13(cur);
12053
+ }
12054
+ return null;
12055
+ }
12056
+ function git(toplevel, args) {
12057
+ return execSync(`git ${args}`, {
12058
+ cwd: toplevel,
12059
+ stdio: ["ignore", "pipe", "ignore"],
12060
+ encoding: "utf-8",
12061
+ timeout: GIT_TIMEOUT_MS
12062
+ });
12063
+ }
12064
+ function probeGitRemote(toplevel) {
12065
+ let names;
12066
+ try {
12067
+ names = git(toplevel, "remote").split("\n").map((n) => n.trim()).filter(Boolean);
12068
+ } catch (err) {
12069
+ return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
12070
+ }
12071
+ if (names.length === 0)
12072
+ return { kind: "none" };
12073
+ const pick = names.includes("origin") ? "origin" : names[0];
12074
+ try {
12075
+ const url = git(toplevel, `remote get-url ${pick}`).trim();
12076
+ return url ? { kind: "remote", url } : { kind: "none" };
12077
+ } catch (err) {
12078
+ return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
12079
+ }
12080
+ }
12081
+ function getGitRemote(toplevel) {
12082
+ const cached = gitCache.get(toplevel);
12083
+ if (cached)
12084
+ return cached;
12085
+ const probe = probeGitRemote(toplevel);
12086
+ if (probe.kind !== "failed")
12087
+ gitCache.set(toplevel, probe);
12088
+ return probe;
12089
+ }
12090
+ function parseGitRemote(url) {
12091
+ const trimmed = url.trim().replace(/\.git$/, "");
12092
+ if (!trimmed)
12093
+ return null;
12094
+ const ssh = /^[^@]+@([^:]+):(.+?)\/([^/]+)$/.exec(trimmed);
12095
+ if (ssh)
12096
+ return { host: normaliseHost(ssh[1]), owner: ssh[2], repo: ssh[3] };
12097
+ const url1 = /^[a-z+]+:\/\/(?:[^@]+@)?([^/:]+)(?::\d+)?\/(.+?)\/([^/]+)$/.exec(trimmed);
12098
+ if (url1)
12099
+ return { host: normaliseHost(url1[1]), owner: url1[2], repo: url1[3] };
12100
+ return null;
12101
+ }
12102
+ function normaliseHost(host) {
12103
+ for (const known of ["github.com", "gitlab.com", "bitbucket.org", "codeberg.org", "gitea.com"]) {
12104
+ if (host === known)
12105
+ return known;
12106
+ if (host.startsWith(known + "-") || host.startsWith(known + "."))
12107
+ return known;
12108
+ }
12109
+ return host;
12110
+ }
12111
+ function resolveAutoWorkspace(realPath, minRepos) {
12112
+ let cur = realPath;
12113
+ let last = "";
12114
+ let depth = 0;
12115
+ while (cur && cur !== last && depth < AUTO_WORKSPACE_MAX_DEPTH) {
12116
+ if (!existsSync16(join17(cur, ".git"))) {
12117
+ const cached = workspaceCache.get(cur);
12118
+ let qualifies;
12119
+ if (cached === void 0) {
12120
+ qualifies = countGitChildren(cur) >= minRepos;
12121
+ workspaceCache.set(cur, qualifies ? cur : null);
12122
+ } else {
12123
+ qualifies = cached !== null;
12124
+ }
12125
+ if (qualifies) {
12126
+ const name = basename11(cur);
12127
+ return { id: `ws:${name}`, displayName: name, source: "auto-workspace" };
12547
12128
  }
12548
- };
12549
- claudeBackend = new ClaudeBackend();
12129
+ }
12130
+ last = cur;
12131
+ cur = dirname13(cur);
12132
+ depth++;
12550
12133
  }
12551
- });
12552
-
12553
- // ../engine/dist/core/project-resolver.js
12554
- var project_resolver_exports = {};
12555
- __export(project_resolver_exports, {
12556
- _setProjectsConfigForTests: () => _setProjectsConfigForTests,
12557
- getProjectsConfigPath: () => getProjectsConfigPath,
12558
- globMatch: () => globMatch,
12559
- loadProjectsConfig: () => loadProjectsConfig,
12560
- parseGitRemote: () => parseGitRemote,
12561
- resetProjectResolverCache: () => resetProjectResolverCache,
12562
- resolveProjectId: () => resolveProjectId,
12563
- resolveWorkspaceId: () => resolveWorkspaceId
12564
- });
12565
- import { execSync } from "child_process";
12566
- import { createHash as createHash4 } from "crypto";
12567
- import { existsSync as existsSync16, readFileSync as readFileSync11, realpathSync as realpathSync2, readdirSync as readdirSync12, statSync as statSync14 } from "fs";
12568
- import { basename as basename11, dirname as dirname13, isAbsolute, join as join17, resolve, sep } from "path";
12569
- function getProjectsConfigPath() {
12570
- return join17(getDataDir(), "projects.json");
12134
+ return null;
12571
12135
  }
12572
- function loadProjectsConfig() {
12573
- if (cachedConfig && cachedConfig.mtime === Number.MAX_SAFE_INTEGER) {
12574
- return cachedConfig.cfg;
12136
+ function countGitChildren(dir) {
12137
+ let count = 0;
12138
+ let entries;
12139
+ try {
12140
+ entries = readdirSync12(dir);
12141
+ } catch {
12142
+ return 0;
12575
12143
  }
12576
- const path = getProjectsConfigPath();
12577
- if (!existsSync16(path)) {
12578
- cachedConfig = { mtime: 0, cfg: {} };
12579
- return cachedConfig.cfg;
12144
+ for (const name of entries) {
12145
+ if (name.startsWith("."))
12146
+ continue;
12147
+ const child = join17(dir, name);
12148
+ try {
12149
+ const st = statSync13(child);
12150
+ if (!st.isDirectory())
12151
+ continue;
12152
+ if (existsSync16(join17(child, ".git")))
12153
+ count++;
12154
+ } catch {
12155
+ }
12580
12156
  }
12581
- const mtime = statSync14(path).mtimeMs;
12582
- if (cachedConfig && cachedConfig.mtime === mtime)
12583
- return cachedConfig.cfg;
12157
+ return count;
12158
+ }
12159
+ function safeRealpath(p) {
12584
12160
  try {
12585
- const raw = readFileSync11(path, "utf-8");
12586
- const cfg = JSON.parse(raw);
12587
- cachedConfig = { mtime, cfg };
12588
- return cfg;
12161
+ return realpathSync2(p);
12589
12162
  } catch {
12590
- cachedConfig = { mtime, cfg: {} };
12591
- return cachedConfig.cfg;
12163
+ return resolve(p);
12592
12164
  }
12593
12165
  }
12594
- function resetProjectResolverCache() {
12595
- resolveCache.clear();
12596
- gitCache.clear();
12597
- workspaceCache.clear();
12598
- wsIdCache.clear();
12599
- cachedConfig = null;
12166
+ function isInside(child, parent) {
12167
+ if (!parent)
12168
+ return false;
12169
+ if (child === parent)
12170
+ return true;
12171
+ const c = child.endsWith(sep) ? child : child + sep;
12172
+ const p = parent.endsWith(sep) ? parent : parent + sep;
12173
+ return c.startsWith(p);
12600
12174
  }
12601
- function _setProjectsConfigForTests(cfg) {
12602
- resetProjectResolverCache();
12603
- cachedConfig = cfg === null ? null : { mtime: Number.MAX_SAFE_INTEGER, cfg };
12175
+ function globMatch(pattern, path) {
12176
+ const re = new RegExp("^" + globToRegex(toPosix(pattern)) + "$");
12177
+ return re.test(toPosix(path));
12604
12178
  }
12605
- function resolveProjectId(inputPath) {
12606
- if (!inputPath) {
12607
- return { id: "", displayName: "", source: "path" };
12179
+ function toPosix(p) {
12180
+ return p.replace(/\\/g, "/");
12181
+ }
12182
+ function globToRegex(glob) {
12183
+ let out = "";
12184
+ let i = 0;
12185
+ while (i < glob.length) {
12186
+ const c = glob[i];
12187
+ if (c === "*" && glob[i + 1] === "*") {
12188
+ out += ".*";
12189
+ i += 2;
12190
+ if (glob[i] === "/")
12191
+ i++;
12192
+ } else if (c === "*") {
12193
+ out += "[^/]*";
12194
+ i++;
12195
+ } else if (c === "?") {
12196
+ out += "[^/]";
12197
+ i++;
12198
+ } else if (c === "{") {
12199
+ const end = glob.indexOf("}", i);
12200
+ if (end === -1) {
12201
+ out += "\\{";
12202
+ i++;
12203
+ continue;
12204
+ }
12205
+ const opts = glob.slice(i + 1, end).split(",").map(escapeRe).join("|");
12206
+ out += "(?:" + opts + ")";
12207
+ i = end + 1;
12208
+ } else {
12209
+ out += escapeRe(c);
12210
+ i++;
12211
+ }
12608
12212
  }
12609
- if (!isAbsolute(inputPath)) {
12610
- return { id: `path:${inputPath}`, displayName: inputPath, source: "path" };
12213
+ return out;
12214
+ }
12215
+ function escapeRe(s) {
12216
+ return s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
12217
+ }
12218
+ var DEFAULT_AUTO_WORKSPACE_MIN_REPOS, BOT_WORKTREE_MARKERS, resolveCache, gitCache, workspaceCache, wsIdCache, cachedConfig, GIT_TIMEOUT_MS, AUTO_WORKSPACE_MAX_DEPTH;
12219
+ var init_project_resolver = __esm({
12220
+ "../engine/dist/core/project-resolver.js"() {
12221
+ "use strict";
12222
+ init_paths();
12223
+ DEFAULT_AUTO_WORKSPACE_MIN_REPOS = 3;
12224
+ BOT_WORKTREE_MARKERS = [".claude-pr-bot"];
12225
+ resolveCache = /* @__PURE__ */ new Map();
12226
+ gitCache = /* @__PURE__ */ new Map();
12227
+ workspaceCache = /* @__PURE__ */ new Map();
12228
+ wsIdCache = /* @__PURE__ */ new Map();
12229
+ cachedConfig = null;
12230
+ GIT_TIMEOUT_MS = 1e4;
12231
+ AUTO_WORKSPACE_MAX_DEPTH = 2;
12611
12232
  }
12612
- const realPath = safeRealpath(inputPath);
12613
- const cached = resolveCache.get(realPath);
12614
- if (cached)
12615
- return cached;
12616
- const result = doResolve(realPath);
12617
- resolveCache.set(realPath, result);
12618
- return result;
12233
+ });
12234
+
12235
+ // ../engine/dist/core/credentials-env.js
12236
+ function envCredentials(env = process.env) {
12237
+ const token = (env.CHAT_RECALL_TOKEN || "").trim();
12238
+ const server = (env.CHAT_RECALL_SERVER || "").trim();
12239
+ if (!token || !server)
12240
+ return null;
12241
+ if (!/^https?:\/\//i.test(server))
12242
+ return null;
12243
+ return { serverUrl: normalise(server), token };
12619
12244
  }
12620
- function resolveWorkspaceId(inputPath) {
12621
- if (!inputPath || !isAbsolute(inputPath))
12245
+ var normalise;
12246
+ var init_credentials_env = __esm({
12247
+ "../engine/dist/core/credentials-env.js"() {
12248
+ "use strict";
12249
+ normalise = (u) => u.trim().replace(/\/+$/, "");
12250
+ }
12251
+ });
12252
+
12253
+ // ../engine/dist/core/secret-redactor.js
12254
+ var secret_redactor_exports = {};
12255
+ __export(secret_redactor_exports, {
12256
+ DEFAULT_REDACTION_RULES: () => DEFAULT_REDACTION_RULES,
12257
+ _clearServerRulePack: () => _clearServerRulePack,
12258
+ _resetPrivacyCache: () => _resetPrivacyCache,
12259
+ _resetRedactorCache: () => _resetRedactorCache,
12260
+ applyChunkPrivacy: () => applyChunkPrivacy,
12261
+ findContextualSecrets: () => findContextualSecrets,
12262
+ installServerRulePack: () => installServerRulePack,
12263
+ isFilePathRedactionEnabled: () => isFilePathRedactionEnabled,
12264
+ isRedactionEnabled: () => isRedactionEnabled,
12265
+ isToolOutputRedactionEnabled: () => isToolOutputRedactionEnabled,
12266
+ redactFilePaths: () => redactFilePaths,
12267
+ redactSecrets: () => redactSecrets,
12268
+ redactToolResultBody: () => redactToolResultBody,
12269
+ scanTextForFindings: () => scanTextForFindings,
12270
+ serverRulePackVersion: () => serverRulePackVersion,
12271
+ validateRedactionRule: () => validateRedactionRule
12272
+ });
12273
+ import { statSync as statSync14 } from "fs";
12274
+ import { createHash as createHash4 } from "crypto";
12275
+ function secretOf(rule, m) {
12276
+ const raw = rule.secret != null ? m[rule.secret] : m[0];
12277
+ if (raw == null)
12622
12278
  return null;
12623
- const realPath = safeRealpath(inputPath);
12624
- const cached = wsIdCache.get(realPath);
12625
- if (cached !== void 0)
12626
- return cached;
12627
- const result = doResolveWorkspace(realPath);
12628
- wsIdCache.set(realPath, result);
12629
- return result;
12279
+ if (rule.where && !rule.where(raw))
12280
+ return null;
12281
+ return raw;
12630
12282
  }
12631
- function doResolveWorkspace(realPath) {
12632
- const cfg = loadProjectsConfig();
12633
- const declaredWorkspaces = (cfg.projects || []).filter((p) => p.workspace).sort((a, b) => b.root.length - a.root.length);
12634
- for (const proj of declaredWorkspaces) {
12635
- if (isInside(realPath, safeRealpath(proj.root)))
12636
- return proj.id;
12637
- }
12638
- const anchor = findGitToplevel(realPath) ?? realPath;
12639
- const ws = resolveAutoWorkspace(anchor, cfg.autoWorkspaceMinRepos ?? DEFAULT_AUTO_WORKSPACE_MIN_REPOS);
12640
- return ws ? ws.id : null;
12283
+ function looksLikeSecretToken(t) {
12284
+ if (t.length < 32)
12285
+ return false;
12286
+ if (/^[a-f0-9]+$/i.test(t))
12287
+ return true;
12288
+ return /[A-Z]/.test(t) && /[a-z]/.test(t) && /[0-9]/.test(t);
12641
12289
  }
12642
- function doResolve(realPath) {
12643
- const cfg = loadProjectsConfig();
12644
- if (matchesIgnore(realPath, cfg)) {
12645
- return { id: `ignored:${realPath}`, displayName: basename11(realPath), source: "ignored" };
12290
+ function findContextualSecrets(text) {
12291
+ if (!text)
12292
+ return [];
12293
+ const ranges = [];
12294
+ let m;
12295
+ SECRET_CONTEXT_RE.lastIndex = 0;
12296
+ while ((m = SECRET_CONTEXT_RE.exec(text)) !== null) {
12297
+ const winStart = m.index + m[0].length;
12298
+ const window = text.slice(winStart, winStart + 80);
12299
+ HIGH_ENTROPY_TOKEN_RE.lastIndex = 0;
12300
+ let tm;
12301
+ while ((tm = HIGH_ENTROPY_TOKEN_RE.exec(window)) !== null) {
12302
+ if (!looksLikeSecretToken(tm[0]))
12303
+ continue;
12304
+ 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))) {
12305
+ continue;
12306
+ }
12307
+ ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
12308
+ break;
12309
+ }
12646
12310
  }
12647
- const declared = matchDeclaredProject(realPath, cfg.projects || []);
12648
- if (declared)
12649
- return declared;
12650
- const bot = resolveBotWorktree(realPath);
12651
- if (bot)
12652
- return bot;
12653
- const git2 = resolveGit(realPath);
12654
- if (git2)
12655
- return git2;
12656
- const ws = resolveAutoWorkspace(realPath, cfg.autoWorkspaceMinRepos ?? DEFAULT_AUTO_WORKSPACE_MIN_REPOS);
12657
- if (ws)
12658
- return ws;
12659
- return {
12660
- id: `path:${realPath}`,
12661
- displayName: basename11(realPath) || realPath,
12662
- source: "path"
12663
- };
12664
- }
12665
- function matchesIgnore(realPath, cfg) {
12666
- const rules = cfg.ignore || [];
12667
- for (const r of rules) {
12668
- if (globMatch(r.match, realPath))
12669
- return true;
12311
+ ranges.sort((a, b) => a.start - b.start);
12312
+ const out = [];
12313
+ for (const r of ranges) {
12314
+ const last = out[out.length - 1];
12315
+ if (last && r.start < last.end) {
12316
+ last.end = Math.max(last.end, r.end);
12317
+ continue;
12318
+ }
12319
+ out.push({ ...r });
12670
12320
  }
12671
- return false;
12321
+ return out;
12672
12322
  }
12673
- function resolveBotWorktree(realPath) {
12674
- const parts = realPath.split(sep);
12675
- const idx = parts.findIndex((p) => BOT_WORKTREE_MARKERS.includes(p));
12676
- if (idx === -1)
12323
+ function isEnvRedactionRequested() {
12324
+ const v = process.env.CHAT_RECALL_REDACT_INDEX;
12325
+ if (v === void 0)
12677
12326
  return null;
12678
- const botRoot = parts.slice(0, idx + 1).join(sep);
12679
- return {
12680
- id: `path:${botRoot}`,
12681
- displayName: parts[idx],
12682
- source: "path"
12683
- };
12327
+ return v === "1" || v === "true" || v === "yes";
12684
12328
  }
12685
- function matchDeclaredProject(realPath, projects) {
12686
- const candidates2 = [...projects].sort((a, b) => b.root.length - a.root.length);
12687
- for (const proj of candidates2) {
12688
- const root = safeRealpath(proj.root);
12689
- if (!isInside(realPath, root))
12690
- continue;
12691
- if (!proj.workspace && proj.children?.length) {
12692
- const child = matchChild(realPath, root, proj.children);
12693
- if (child) {
12694
- return {
12695
- id: `${proj.id}/${child.id}`,
12696
- displayName: child.name || child.id,
12697
- source: "user",
12698
- workspaceId: proj.id
12699
- };
12329
+ function settingsView() {
12330
+ let mtimeMs = -1;
12331
+ try {
12332
+ mtimeMs = statSync14(settingsFilePath()).mtimeMs;
12333
+ } catch {
12334
+ }
12335
+ if (cache && cache.mtimeMs === mtimeMs)
12336
+ return cache;
12337
+ let enabled = false;
12338
+ const extra = [];
12339
+ try {
12340
+ const s = loadSettings();
12341
+ enabled = !!s.privacy?.redactIndex;
12342
+ for (const r of s.privacy?.redactionRules ?? []) {
12343
+ try {
12344
+ extra.push({ label: r.label, pattern: new RegExp(r.pattern, "g") });
12345
+ } catch {
12700
12346
  }
12701
12347
  }
12702
- return {
12703
- id: proj.id,
12704
- displayName: proj.name || proj.id,
12705
- source: "user",
12706
- workspaceId: proj.workspace ? void 0 : void 0
12707
- };
12348
+ } catch {
12708
12349
  }
12709
- return null;
12350
+ cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
12351
+ return cache;
12710
12352
  }
12711
- function matchChild(realPath, parentRoot, children) {
12712
- const sorted = [...children].sort((a, b) => b.path.length - a.path.length);
12713
- for (const c of sorted) {
12714
- const childRoot = safeRealpath(join17(parentRoot, c.path));
12715
- if (isInside(realPath, childRoot))
12716
- return c;
12353
+ function compileServerRule(spec) {
12354
+ const name = spec.name || "(unnamed)";
12355
+ if (!spec.regex)
12356
+ return { name, reason: "empty pattern" };
12357
+ if (spec.regex.length > MAX_PATTERN_LEN)
12358
+ return { name, reason: `pattern longer than ${MAX_PATTERN_LEN} chars` };
12359
+ let re;
12360
+ try {
12361
+ const flags = "g" + (spec.flags || "").replace(/[^ims]/g, "");
12362
+ re = new RegExp(spec.regex, flags);
12363
+ } catch (e) {
12364
+ return { name, reason: `invalid regex: ${e instanceof Error ? e.message : e}` };
12717
12365
  }
12718
- return null;
12366
+ if (re.test(""))
12367
+ return { name, reason: "matches the empty string (would redact everything)" };
12368
+ re.lastIndex = 0;
12369
+ if (re.test(CANARY))
12370
+ return { name, reason: "matches benign text \u2014 too broad" };
12371
+ re.lastIndex = 0;
12372
+ return { label: `${spec.source === "pack" ? "pack" : "tenant"}:${name}`, pattern: re };
12719
12373
  }
12720
- function resolveGit(realPath) {
12721
- const toplevel = findGitToplevel(realPath);
12722
- if (!toplevel)
12723
- return null;
12724
- const probe = getGitRemote(toplevel);
12725
- if (probe.kind === "remote") {
12726
- const parsed = parseGitRemote(probe.url);
12727
- if (parsed) {
12728
- return {
12729
- id: `git:${parsed.host}/${parsed.owner}/${parsed.repo}`,
12730
- displayName: parsed.repo,
12731
- source: "git-remote"
12732
- };
12374
+ function validateRedactionRule(spec) {
12375
+ const out = compileServerRule(spec);
12376
+ return "reason" in out ? { ok: false, reason: out.reason } : { ok: true };
12377
+ }
12378
+ function installServerRulePack(pack) {
12379
+ const rejected = [];
12380
+ const rules = [];
12381
+ for (const spec of pack.rules || []) {
12382
+ if (rules.length >= MAX_PACK_RULES) {
12383
+ rejected.push({ name: spec.name || "(unnamed)", reason: `pack exceeds ${MAX_PACK_RULES} rules` });
12384
+ continue;
12733
12385
  }
12386
+ const out = compileServerRule(spec);
12387
+ if ("reason" in out)
12388
+ rejected.push(out);
12389
+ else
12390
+ rules.push(out);
12734
12391
  }
12735
- if (probe.kind === "failed") {
12736
- console.error(`[project] could not read git remotes in ${toplevel} \u2014 filing it as a local-only repo for now, which is probably wrong: ${probe.reason}`);
12737
- }
12738
- const hash = createHash4("sha1").update(toplevel).digest("hex").slice(0, 12);
12739
- return {
12740
- id: `git-local:${hash}`,
12741
- displayName: basename11(toplevel),
12742
- source: "git-local"
12743
- };
12392
+ const version2 = pack.version || "";
12393
+ serverPack = { version: version2, rules };
12394
+ return { version: version2, accepted: rules.length, rejected };
12744
12395
  }
12745
- function findGitToplevel(realPath) {
12746
- let cur = realPath;
12747
- let last = "";
12748
- while (cur && cur !== last) {
12749
- if (existsSync16(join17(cur, ".git")))
12750
- return cur;
12751
- last = cur;
12752
- cur = dirname13(cur);
12753
- }
12754
- return null;
12396
+ function serverRulePackVersion() {
12397
+ return serverPack ? serverPack.version || "unversioned" : null;
12755
12398
  }
12756
- function git(toplevel, args) {
12757
- return execSync(`git ${args}`, {
12758
- cwd: toplevel,
12759
- stdio: ["ignore", "pipe", "ignore"],
12760
- encoding: "utf-8",
12761
- timeout: GIT_TIMEOUT_MS
12762
- });
12399
+ function _clearServerRulePack() {
12400
+ serverPack = null;
12763
12401
  }
12764
- function probeGitRemote(toplevel) {
12765
- let names;
12766
- try {
12767
- names = git(toplevel, "remote").split("\n").map((n) => n.trim()).filter(Boolean);
12768
- } catch (err) {
12769
- return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
12402
+ function activeRules() {
12403
+ const base3 = settingsView().rules;
12404
+ return serverPack && serverPack.rules.length > 0 ? [...base3, ...serverPack.rules] : base3;
12405
+ }
12406
+ function isRedactionEnabled() {
12407
+ const env = isEnvRedactionRequested();
12408
+ if (env !== null)
12409
+ return env;
12410
+ return settingsView().enabled;
12411
+ }
12412
+ function _resetRedactorCache() {
12413
+ cache = null;
12414
+ }
12415
+ function redactSecrets(text, opts = {}) {
12416
+ if (!opts.force && !isRedactionEnabled())
12417
+ return text;
12418
+ if (!text)
12419
+ return text;
12420
+ const rules = opts.rules || activeRules();
12421
+ let out = text;
12422
+ for (const r of rules) {
12423
+ out = out.replace(r.pattern, (...args) => {
12424
+ const whole = args[0];
12425
+ const m = args.slice(0, args.length);
12426
+ const secret = secretOf(r, m);
12427
+ if (secret == null)
12428
+ return whole;
12429
+ if (opts.count)
12430
+ opts.count.redactions++;
12431
+ return whole.slice(0, whole.length - secret.length) + `[REDACTED:${r.label}]`;
12432
+ });
12770
12433
  }
12771
- if (names.length === 0)
12772
- return { kind: "none" };
12773
- const pick = names.includes("origin") ? "origin" : names[0];
12774
- try {
12775
- const url = git(toplevel, `remote get-url ${pick}`).trim();
12776
- return url ? { kind: "remote", url } : { kind: "none" };
12777
- } catch (err) {
12778
- return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
12434
+ const ctx = findContextualSecrets(out);
12435
+ for (let i = ctx.length - 1; i >= 0; i--) {
12436
+ const { start, end } = ctx[i];
12437
+ if (opts.count)
12438
+ opts.count.redactions++;
12439
+ out = out.slice(0, start) + "[REDACTED:secret-context]" + out.slice(end);
12779
12440
  }
12441
+ return out;
12780
12442
  }
12781
- function getGitRemote(toplevel) {
12782
- const cached = gitCache.get(toplevel);
12783
- if (cached)
12784
- return cached;
12785
- const probe = probeGitRemote(toplevel);
12786
- if (probe.kind !== "failed")
12787
- gitCache.set(toplevel, probe);
12788
- return probe;
12789
- }
12790
- function parseGitRemote(url) {
12791
- const trimmed = url.trim().replace(/\.git$/, "");
12792
- if (!trimmed)
12793
- return null;
12794
- const ssh = /^[^@]+@([^:]+):(.+?)\/([^/]+)$/.exec(trimmed);
12795
- if (ssh)
12796
- return { host: normaliseHost(ssh[1]), owner: ssh[2], repo: ssh[3] };
12797
- const url1 = /^[a-z+]+:\/\/(?:[^@]+@)?([^/:]+)(?::\d+)?\/(.+?)\/([^/]+)$/.exec(trimmed);
12798
- if (url1)
12799
- return { host: normaliseHost(url1[1]), owner: url1[2], repo: url1[3] };
12800
- return null;
12443
+ function maskSecret(s) {
12444
+ if (!s)
12445
+ return "";
12446
+ if (s.length <= 4)
12447
+ return "*".repeat(s.length);
12448
+ return "*".repeat(Math.min(s.length - 4, 60)) + s.slice(-4);
12801
12449
  }
12802
- function normaliseHost(host) {
12803
- for (const known of ["github.com", "gitlab.com", "bitbucket.org", "codeberg.org", "gitea.com"]) {
12804
- if (host === known)
12805
- return known;
12806
- if (host.startsWith(known + "-") || host.startsWith(known + "."))
12807
- return known;
12450
+ function globalRegexFor(pattern) {
12451
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
12452
+ const key2 = pattern.source + "\0" + flags;
12453
+ let re = globalRegexCache.get(key2);
12454
+ if (!re) {
12455
+ re = new RegExp(pattern.source, flags);
12456
+ globalRegexCache.set(key2, re);
12808
12457
  }
12809
- return host;
12458
+ return re;
12810
12459
  }
12811
- function resolveAutoWorkspace(realPath, minRepos) {
12812
- let cur = realPath;
12813
- let last = "";
12814
- let depth = 0;
12815
- while (cur && cur !== last && depth < AUTO_WORKSPACE_MAX_DEPTH) {
12816
- if (!existsSync16(join17(cur, ".git"))) {
12817
- const cached = workspaceCache.get(cur);
12818
- let qualifies;
12819
- if (cached === void 0) {
12820
- qualifies = countGitChildren(cur) >= minRepos;
12821
- workspaceCache.set(cur, qualifies ? cur : null);
12822
- } else {
12823
- qualifies = cached !== null;
12460
+ function makeLineResolver(text) {
12461
+ const starts = [];
12462
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1))
12463
+ starts.push(i);
12464
+ return (index) => {
12465
+ let lo = 0, hi = starts.length;
12466
+ while (lo < hi) {
12467
+ const mid = lo + hi >> 1;
12468
+ if (starts[mid] < index)
12469
+ lo = mid + 1;
12470
+ else
12471
+ hi = mid;
12472
+ }
12473
+ return lo + 1;
12474
+ };
12475
+ }
12476
+ function scanTextForFindings(text, opts = {}) {
12477
+ if (!text)
12478
+ return [];
12479
+ const rules = opts.rules || activeRules();
12480
+ const findings = [];
12481
+ const seenSpans = /* @__PURE__ */ new Set();
12482
+ const lineAt = makeLineResolver(text);
12483
+ for (const r of rules) {
12484
+ const re = globalRegexFor(r.pattern);
12485
+ re.lastIndex = 0;
12486
+ let m;
12487
+ while ((m = re.exec(text)) !== null) {
12488
+ const secret = secretOf(r, m);
12489
+ if (secret == null) {
12490
+ if (re.lastIndex === m.index)
12491
+ re.lastIndex++;
12492
+ continue;
12824
12493
  }
12825
- if (qualifies) {
12826
- const name = basename11(cur);
12827
- return { id: `ws:${name}`, displayName: name, source: "auto-workspace" };
12494
+ const at = m.index + (m[0].length - secret.length);
12495
+ const span = `${at}:${secret.length}`;
12496
+ if (!seenSpans.has(span)) {
12497
+ seenSpans.add(span);
12498
+ findings.push({ rule: r.label, line: lineAt(at), preview: maskSecret(secret) });
12828
12499
  }
12500
+ if (re.lastIndex === m.index)
12501
+ re.lastIndex++;
12829
12502
  }
12830
- last = cur;
12831
- cur = dirname13(cur);
12832
- depth++;
12833
12503
  }
12834
- return null;
12504
+ for (const { start, end } of findContextualSecrets(text)) {
12505
+ const line = lineAt(start);
12506
+ findings.push({ rule: "secret-context", line, preview: maskSecret(text.slice(start, end)) });
12507
+ }
12508
+ return findings;
12835
12509
  }
12836
- function countGitChildren(dir) {
12837
- let count = 0;
12838
- let entries;
12510
+ function privacyView() {
12511
+ let mtimeMs = -1;
12839
12512
  try {
12840
- entries = readdirSync12(dir);
12513
+ mtimeMs = statSync14(settingsFilePath()).mtimeMs;
12841
12514
  } catch {
12842
- return 0;
12843
- }
12844
- for (const name of entries) {
12845
- if (name.startsWith("."))
12846
- continue;
12847
- const child = join17(dir, name);
12848
- try {
12849
- const st = statSync14(child);
12850
- if (!st.isDirectory())
12851
- continue;
12852
- if (existsSync16(join17(child, ".git")))
12853
- count++;
12854
- } catch {
12855
- }
12856
12515
  }
12857
- return count;
12858
- }
12859
- function safeRealpath(p) {
12516
+ if (privacyCache && privacyCache.mtimeMs === mtimeMs)
12517
+ return privacyCache;
12518
+ let redactToolOutputs = false, redactFilePaths2 = false;
12860
12519
  try {
12861
- return realpathSync2(p);
12520
+ const s = loadSettings();
12521
+ redactToolOutputs = !!s.privacy?.redactToolOutputs;
12522
+ redactFilePaths2 = !!s.privacy?.redactFilePaths;
12862
12523
  } catch {
12863
- return resolve(p);
12864
12524
  }
12525
+ privacyCache = { mtimeMs, redactToolOutputs, redactFilePaths: redactFilePaths2 };
12526
+ return privacyCache;
12865
12527
  }
12866
- function isInside(child, parent) {
12867
- if (!parent)
12868
- return false;
12869
- if (child === parent)
12870
- return true;
12871
- const c = child.endsWith(sep) ? child : child + sep;
12872
- const p = parent.endsWith(sep) ? parent : parent + sep;
12873
- return c.startsWith(p);
12528
+ function _resetPrivacyCache() {
12529
+ privacyCache = null;
12874
12530
  }
12875
- function globMatch(pattern, path) {
12876
- const re = new RegExp("^" + globToRegex(toPosix(pattern)) + "$");
12877
- return re.test(toPosix(path));
12531
+ function isToolOutputRedactionEnabled() {
12532
+ return privacyView().redactToolOutputs;
12878
12533
  }
12879
- function toPosix(p) {
12880
- return p.replace(/\\/g, "/");
12534
+ function isFilePathRedactionEnabled() {
12535
+ return privacyView().redactFilePaths;
12881
12536
  }
12882
- function globToRegex(glob) {
12883
- let out = "";
12884
- let i = 0;
12885
- while (i < glob.length) {
12886
- const c = glob[i];
12887
- if (c === "*" && glob[i + 1] === "*") {
12888
- out += ".*";
12889
- i += 2;
12890
- if (glob[i] === "/")
12891
- i++;
12892
- } else if (c === "*") {
12893
- out += "[^/]*";
12894
- i++;
12895
- } else if (c === "?") {
12896
- out += "[^/]";
12897
- i++;
12898
- } else if (c === "{") {
12899
- const end = glob.indexOf("}", i);
12900
- if (end === -1) {
12901
- out += "\\{";
12902
- i++;
12903
- continue;
12904
- }
12905
- const opts = glob.slice(i + 1, end).split(",").map(escapeRe).join("|");
12906
- out += "(?:" + opts + ")";
12907
- i = end + 1;
12908
- } else {
12909
- out += escapeRe(c);
12910
- i++;
12911
- }
12912
- }
12913
- return out;
12537
+ function redactFilePaths(text) {
12538
+ if (!text)
12539
+ return text;
12540
+ return text.replace(PATH_REGEX, (m) => {
12541
+ if (m.startsWith("[REDACTED:"))
12542
+ return m;
12543
+ const h = createHash4("sha256").update(m).digest("hex").slice(0, 12);
12544
+ return `[path:${h}]`;
12545
+ });
12914
12546
  }
12915
- function escapeRe(s) {
12916
- return s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
12547
+ function applyChunkPrivacy(text) {
12548
+ let out = redactSecrets(text);
12549
+ if (privacyView().redactFilePaths)
12550
+ out = redactFilePaths(out);
12551
+ return out;
12917
12552
  }
12918
- var DEFAULT_AUTO_WORKSPACE_MIN_REPOS, BOT_WORKTREE_MARKERS, resolveCache, gitCache, workspaceCache, wsIdCache, cachedConfig, GIT_TIMEOUT_MS, AUTO_WORKSPACE_MAX_DEPTH;
12919
- var init_project_resolver = __esm({
12920
- "../engine/dist/core/project-resolver.js"() {
12921
- "use strict";
12922
- init_paths();
12923
- DEFAULT_AUTO_WORKSPACE_MIN_REPOS = 3;
12924
- BOT_WORKTREE_MARKERS = [".claude-pr-bot"];
12925
- resolveCache = /* @__PURE__ */ new Map();
12926
- gitCache = /* @__PURE__ */ new Map();
12927
- workspaceCache = /* @__PURE__ */ new Map();
12928
- wsIdCache = /* @__PURE__ */ new Map();
12929
- cachedConfig = null;
12930
- GIT_TIMEOUT_MS = 1e4;
12931
- AUTO_WORKSPACE_MAX_DEPTH = 2;
12932
- }
12933
- });
12934
-
12935
- // ../engine/dist/core/credentials-env.js
12936
- function envCredentials(env = process.env) {
12937
- const token = (env.CHAT_RECALL_TOKEN || "").trim();
12938
- const server = (env.CHAT_RECALL_SERVER || "").trim();
12939
- if (!token || !server)
12940
- return null;
12941
- if (!/^https?:\/\//i.test(server))
12942
- return null;
12943
- return { serverUrl: normalise(server), token };
12553
+ function redactToolResultBody(toolName, body) {
12554
+ if (!body)
12555
+ return body;
12556
+ return `[redacted: ${toolName} output, ${body.length} chars]`;
12944
12557
  }
12945
- var normalise;
12946
- var init_credentials_env = __esm({
12947
- "../engine/dist/core/credentials-env.js"() {
12558
+ var DEFAULT_REDACTION_RULES, SECRET_CONTEXT_RE, HIGH_ENTROPY_TOKEN_RE, cache, CANARY, MAX_PACK_RULES, MAX_PATTERN_LEN, serverPack, globalRegexCache, privacyCache, PATH_REGEX;
12559
+ var init_secret_redactor = __esm({
12560
+ "../engine/dist/core/secret-redactor.js"() {
12948
12561
  "use strict";
12949
- normalise = (u) => u.trim().replace(/\/+$/, "");
12562
+ init_settings();
12563
+ DEFAULT_REDACTION_RULES = [
12564
+ // AWS access tokens. Prefix-anchored: AKIA, ASIA, ABIA (long-term,
12565
+ // STS, EC2). Always exactly 16 hex/alphanumeric after the prefix.
12566
+ // NOTE: this matches only the access-key-ID, NOT the secret key value —
12567
+ // the `env-secret` rule below covers AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN.
12568
+ { label: "aws-access-token", pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
12569
+ // AWS STS session tokens. These are long base64 blobs with NO `NAME=` context
12570
+ // when pasted bare into prose/code, so `env-secret` misses them — but they
12571
+ // reliably start with one of a handful of base64 prefixes (the encoded
12572
+ // `{"...` JSON header). Prefix-anchored → low false-positive. This is the
12573
+ // bare-value form that leaked (`search('IQoJEXAMPLEfakeSESSIONtokenNOTreal01234567890abcDEF==...')`).
12574
+ { label: "aws-session-token", pattern: /\b(?:IQoJ|FwoG|FwoH|FQoG|FQoH|FwoD)[A-Za-z0-9/+]{40,}={0,2}/g },
12575
+ // Env/shell-assignment secrets — a VAR whose name ends in KEY/SECRET/TOKEN/
12576
+ // PASSWORD/CREDENTIAL, then `=`/`:`, then the value. Zero-width lookbehind so
12577
+ // only the VALUE is redacted (the var name stays for context). This is the
12578
+ // dominant real-world leak vector (`export` dumps, .env files, `curl -H`,
12579
+ // tool output) and is exactly what leaked AWS_SECRET_ACCESS_KEY and
12580
+ // AWS_SESSION_TOKEN in cleartext while the access-key-ID rule above caught
12581
+ // only the (least-sensitive) ID.
12582
+ // Suffix list deliberately broad: DB_PASS (our own helm chart's name!),
12583
+ // NPM_AUTH, SENTRY_DSN, SESSION/COOKIE secrets and BEARER vars all leaked
12584
+ // past the original KEY/SECRET/TOKEN/PASSWORD-only list. Values under 12
12585
+ // chars don't redact, so flag-ish vars (BYPASS=true) stay readable.
12586
+ // Context CONSUMED, secret in group 1 — see RedactionRule.secret. As a
12587
+ // lookbehind this was the second most expensive rule in the collector.
12588
+ { 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 },
12589
+ // Credentials embedded in a connection URL: scheme://user:PASSWORD@host.
12590
+ // Redacts only the password segment (keeps scheme/user/host for context).
12591
+ //
12592
+ // Scheme-agnostic on purpose. The old rule allowlisted eight DB schemes, so
12593
+ // `https://user:pass@host`, `ssh://`, `ftp://`, `smtp://` and every other
12594
+ // userinfo URL walked through in cleartext — and a `jdbc:postgresql://…`
12595
+ // prefix broke the \b anchor for the one family it did cover. Any scheme
12596
+ // followed by userinfo is a credential, so match the shape, not a list.
12597
+ { label: "url-password", secret: 1, pattern: /\b[a-zA-Z][a-zA-Z0-9+.-]{1,31}:\/\/[^:@\s/]{1,64}:([^@\s/]{1,256})(?=@)/g },
12598
+ // Bare AWS secret-access-key VALUE: EXACTLY 40 base64 chars, word-bounded,
12599
+ // containing upper+lower+digit. This is the gitleaks heuristic — the mixed-case
12600
+ // requirement excludes 40-char lowercase-hex git SHAs, and the exact-40 bound
12601
+ // excludes longer blobs/tokens. It catches the value when it appears bare in
12602
+ // prose with NO `NAME=` and NO nearby context word (e.g. inside a markdown
12603
+ // table or quoted in analysis) — the residual leak path the env-secret and
12604
+ // contextual rules both miss. Placed AFTER env-secret/url-password so the
12605
+ // keyed/URL forms keep their more-specific labels.
12606
+ // The three stacked lookaheads each re-scanned the same 40 characters, which
12607
+ // made this the MOST expensive rule of all 106 the collector runs (13.1% of a
12608
+ // 29.5 MB scan). The mixed-case-plus-digit test is now one JS pass.
12609
+ {
12610
+ label: "aws-secret-key",
12611
+ secret: 1,
12612
+ pattern: /(?<![A-Za-z0-9/+])([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+])/g,
12613
+ where: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) && /[0-9]/.test(v)
12614
+ },
12615
+ // GitHub personal access tokens.
12616
+ { label: "github-pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
12617
+ // GitHub fine-grained PATs.
12618
+ { label: "github-fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
12619
+ // GitHub OAuth.
12620
+ { label: "github-oauth", pattern: /\bgho_[A-Za-z0-9]{36}\b/g },
12621
+ // GitHub app + refresh.
12622
+ { label: "github-app", pattern: /\b(?:ghs|ghu|ghr)_[A-Za-z0-9]{36}\b/g },
12623
+ // Slack bot/user/app tokens.
12624
+ { label: "slack-token", pattern: /\bxox[abopr]-[A-Za-z0-9-]{10,}\b/g },
12625
+ // Slack incoming webhook URL.
12626
+ { label: "slack-webhook", pattern: /\bhttps:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+\b/g },
12627
+ // Stripe live + test keys.
12628
+ { label: "stripe-key", pattern: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
12629
+ // Anthropic.
12630
+ { label: "anthropic-key", pattern: /\bsk-ant-[a-zA-Z0-9_-]{40,}\b/g },
12631
+ // OpenAI. Two shapes, deliberately kept apart. The classic key is
12632
+ // `sk-` + base62 with NO hyphens; the modern scoped keys carry an explicit
12633
+ // proj/svcacct/admin segment first. Allowing hyphens after a bare `sk-`
12634
+ // (which is what admitted sk-proj- in the first place) also matches ordinary
12635
+ // kebab-case text — `sk-fix-the-login-redirect-bug`, `.sk-button-primary`,
12636
+ // `sk-2024-01-01-backup` — and this corpus is full of branch names and CSS
12637
+ // classes. Requiring the segment keeps sk-proj- coverage without that.
12638
+ { label: "openai-key", pattern: /\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}\b|\bsk-[A-Za-z0-9]{20,}\b/g },
12639
+ // Google API key (AIza…).
12640
+ { label: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
12641
+ // Generic JWT — three base64url segments.
12642
+ { label: "jwt", pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
12643
+ // Private key blocks (PEM). The label is a free word run rather than a fixed
12644
+ // list, so ENCRYPTED PRIVATE KEY and PGP PRIVATE KEY BLOCK are covered
12645
+ // without another enumeration, and \1 forces END to name the same kind as
12646
+ // BEGIN.
12647
+ //
12648
+ // The body is [^-], NOT [\s\S]*?. That is a performance boundary, not a
12649
+ // style choice. An unanchored prefix with an unbounded lazy gap re-scans to
12650
+ // end-of-input from every candidate start, so text carrying many BEGIN
12651
+ // markers and no matching END costs O(n^2): 500KB of that measured 810ms
12652
+ // here, and this runs synchronously on the server's event loop over
12653
+ // tenant-supplied text (services/secret-rescan.ts). A PEM body is base64
12654
+ // plus headers and contains no '-', so excluding it makes the scan stop at
12655
+ // the next delimiter instead of at EOF — same matches, linear cost.
12656
+ { label: "private-key", pattern: /-----BEGIN ([A-Z0-9 ]{0,32})PRIVATE KEY(?: BLOCK)?-----[^-]{0,10000}?-----END \1PRIVATE KEY(?: BLOCK)?-----/g },
12657
+ // PuTTY keys are not PEM. Match the Private-Lines payload — the actual key
12658
+ // material — rather than spanning the whole file, which would need the same
12659
+ // unbounded gap the rule above just removed. Line-anchored and base64-only,
12660
+ // so it cannot run away.
12661
+ { label: "putty-key", pattern: /^Private-Lines:[ \t]*\d{1,4}[\r\n]+(?:[A-Za-z0-9+/=]{8,120}[\r\n]+){1,400}/gm },
12662
+ // npm tokens (uuid-shaped — overlap with our UUID filter; intentional).
12663
+ { label: "npm-token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/g },
12664
+ // .npmrc auth lines: //registry.npmjs.org/:_authToken=VALUE (the key is not
12665
+ // ALL-CAPS, so the env-secret rule above never matched it). The leading `:`
12666
+ // is required — it is what makes this npmrc syntax rather than any shell var
12667
+ // ending in _AUTH, which env-secret already owns and labels better.
12668
+ // [^\s"'`]+ so the closing quote is not swallowed, and a ${VAR} placeholder
12669
+ // is left alone — redacting it destroys a config example and hides nothing.
12670
+ // The rule the perf profile caught the wedged daemon inside: 98.94% of samples.
12671
+ { label: "npmrc-auth", secret: 1, pattern: /:_(?:authToken|password|auth)[ \t]*=[ \t]*(?!\$\{)([^\s"'`]{12,})/gi },
12672
+ // HTTP Authorization headers. env-secret requires an ALL-CAPS name, and
12673
+ // `Authorization` is mixed case, so both Bearer and Basic walked straight
12674
+ // through — including short opaque tokens the contextual pass also misses
12675
+ // (it needs 32+ chars AND mixed-case-plus-digit).
12676
+ // Same two guards as npmrc-auth: stop at a quote, and skip the obvious
12677
+ // placeholders that fill documentation (<YOUR_TOKEN>, ${TOKEN}, xxx…).
12678
+ { label: "auth-header", secret: 1, pattern: /\bauthorization[ \t]*:[ \t]*(?:bearer|basic|token)[ \t]+(?![<$])([^\s"'`<]{8,})/gi },
12679
+ // Vendor tokens with unambiguous prefixes. Each is a fixed literal, so a
12680
+ // false positive is essentially impossible and the cost of listing is low.
12681
+ { label: "gitlab-pat", pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
12682
+ { label: "slack-app-token", pattern: /\bxapp-\d-[A-Z0-9]{8,}-\d{10,}-[a-f0-9]{32,}\b/g },
12683
+ { label: "sendgrid-key", pattern: /\bSG\.[A-Za-z0-9_-]{20,24}\.[A-Za-z0-9_-]{40,50}\b/g },
12684
+ // (?<![-\w]) so `cache-key-<md5>` is not read as a mailgun key.
12685
+ { label: "mailgun-key", pattern: /(?<![-\w])key-[a-f0-9]{32}\b/g },
12686
+ { label: "digitalocean-pat", pattern: /\bdop_v1_[a-f0-9]{64}\b/g },
12687
+ { label: "huggingface-token", pattern: /\bhf_[A-Za-z0-9]{30,}\b/g },
12688
+ { label: "shopify-token", pattern: /\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}\b/g },
12689
+ { label: "pypi-token", pattern: /\bpypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{50,}\b/g },
12690
+ // No twilio-sid rule: AC…/SK… are Twilio SIDs, which are public account and
12691
+ // key identifiers, not the auth token. Redacting them hid nothing and
12692
+ // collided with 32-hex etags and MD5 sums.
12693
+ // No discord-bot-token rule either: the shape (word.word.word) matched
12694
+ // ordinary dotted symbols such as a.b.ApplicationContextProviderImpl.
12695
+ { label: "telegram-bot-token", pattern: /\b\d{8,10}:AA[A-Za-z0-9_-]{32,}\b/g },
12696
+ { label: "doppler-token", pattern: /\bdp\.(?:pt|st|sa|ct)\.[A-Za-z0-9_-]{20,}\b/g },
12697
+ // Azure Storage SAS signature — the `sig=` parameter is the actual secret.
12698
+ { label: "azure-sas", secret: 1, pattern: /[?&]sig=([A-Za-z0-9%/+=]{20,})/g }
12699
+ ];
12700
+ SECRET_CONTEXT_RE = /\b(?:secret|password|passwd|credential|access[ _-]?key|private[ _-]?key|api[ _-]?key|auth(?:oriz\w*)?|bearer|token)\b/gi;
12701
+ HIGH_ENTROPY_TOKEN_RE = /[A-Za-z0-9/+_-]{32,}/g;
12702
+ cache = null;
12703
+ CANARY = "the quick brown fox jumps over the lazy dog 0123456789 commit 8ba1f109551bd432803012645ac136ddd475d0fb src/index.ts:42";
12704
+ MAX_PACK_RULES = 200;
12705
+ MAX_PATTERN_LEN = 512;
12706
+ serverPack = null;
12707
+ globalRegexCache = /* @__PURE__ */ new Map();
12708
+ privacyCache = null;
12709
+ PATH_REGEX = /(?<![A-Za-z0-9_])(?:\/[^\s'"`<>]+|[A-Za-z]:\\[^\s'"`<>]+)/g;
12950
12710
  }
12951
12711
  });
12952
12712
 
@@ -14036,10 +13796,10 @@ var init_types = __esm({
14036
13796
 
14037
13797
  // ../engine/dist/transcript/opencode.js
14038
13798
  async function parseOpenCodeSubagents(parentSessionId) {
14039
- const { existsSync: existsSync57 } = await import("fs");
13799
+ const { existsSync: existsSync58 } = await import("fs");
14040
13800
  const { opencodeBackend: opencodeBackend2 } = await Promise.resolve().then(() => (init_opencode(), opencode_exports));
14041
13801
  const dbPath = opencodeBackend2.dbPath();
14042
- if (!existsSync57(dbPath))
13802
+ if (!existsSync58(dbPath))
14043
13803
  return [];
14044
13804
  const realParent = parentSessionId.replace("opencode_", "");
14045
13805
  const db = openSqliteReadonlyOrThrow(dbPath);
@@ -14080,10 +13840,10 @@ async function parseOpenCodeSubagents(parentSessionId) {
14080
13840
  }
14081
13841
  }
14082
13842
  async function parseOpenCodeTranscript(sessionId) {
14083
- const { existsSync: existsSync57 } = await import("fs");
13843
+ const { existsSync: existsSync58 } = await import("fs");
14084
13844
  const { opencodeBackend: opencodeBackend2 } = await Promise.resolve().then(() => (init_opencode(), opencode_exports));
14085
13845
  const dbPath = opencodeBackend2.dbPath();
14086
- if (!existsSync57(dbPath))
13846
+ if (!existsSync58(dbPath))
14087
13847
  return [];
14088
13848
  const realId = sessionId.replace("opencode_", "");
14089
13849
  const db = openSqliteReadonlyOrThrow(dbPath);
@@ -14160,6 +13920,7 @@ __export(transcript_exports, {
14160
13920
  shadowFileFor: () => shadowFileFor,
14161
13921
  shadowRoot: () => shadowRoot,
14162
13922
  shadowSizeOnDisk: () => shadowSizeOnDisk,
13923
+ shadowUncompressedBytes: () => shadowUncompressedBytes,
14163
13924
  snapshotShadow: () => snapshotShadow,
14164
13925
  trimTranscriptForSync: () => trimTranscriptForSync,
14165
13926
  updateShadow: () => updateShadow,
@@ -14929,111 +14690,6 @@ var init_source_registry = __esm({
14929
14690
  }
14930
14691
  });
14931
14692
 
14932
- // ../engine/dist/parsers/session-source.js
14933
- var SessionSource;
14934
- var init_session_source = __esm({
14935
- "../engine/dist/parsers/session-source.js"() {
14936
- "use strict";
14937
- init_session();
14938
- init_chunker();
14939
- init_settings();
14940
- SessionSource = class {
14941
- sourceType = "session";
14942
- claudeDir;
14943
- constructor(claudeDir) {
14944
- this.claudeDir = claudeDir;
14945
- }
14946
- async *discover() {
14947
- if (!isSourceEnabled("claude", "sessions"))
14948
- return;
14949
- for (const [entry, sessionPath] of getAllSessions(this.claudeDir)) {
14950
- yield this.entryToItem(entry, sessionPath);
14951
- }
14952
- }
14953
- async parse(item) {
14954
- const content = await parseSessionFile(item.filePath);
14955
- const entry = this.itemToEntry(item);
14956
- const sessionChunks = chunkSession(entry, content);
14957
- item.extra = {
14958
- ...item.extra || {},
14959
- ...content.metadata
14960
- };
14961
- return sessionChunks.map((sc) => ({
14962
- chunkId: sc.chunkId,
14963
- itemId: sc.sessionId,
14964
- sourceType: "session",
14965
- // Empty title is preferable to "Session abc12345" placeholder text:
14966
- // the latter pollutes search results and gets surfaced in the UI as
14967
- // a misleading heading. Downstream renderers fall back to
14968
- // project path / time when title is empty.
14969
- title: sc.firstPrompt.slice(0, 100),
14970
- text: sc.text,
14971
- chunkType: sc.chunkType,
14972
- projectPath: sc.projectPath,
14973
- filePath: sc.sourceFile,
14974
- mtime: sc.mtime
14975
- }));
14976
- }
14977
- async extractLinks(item) {
14978
- if (item.filePath) {
14979
- try {
14980
- const { detectForkPredecessor: detectForkPredecessor2 } = await Promise.resolve().then(() => (init_raw(), raw_exports));
14981
- const predecessor = detectForkPredecessor2(item.filePath);
14982
- if (predecessor) {
14983
- return [{
14984
- sourceType: "session",
14985
- sourceId: item.id,
14986
- targetType: "session",
14987
- targetId: predecessor,
14988
- linkType: "forked_from",
14989
- confidence: 1
14990
- }];
14991
- }
14992
- } catch {
14993
- }
14994
- }
14995
- return [];
14996
- }
14997
- entryToItem(entry, sessionPath) {
14998
- return {
14999
- id: entry.sessionId,
15000
- sourceType: "session",
15001
- // Empty title when there's no first prompt — the UI then knows to
15002
- // fall back to summary or project path instead of rendering the
15003
- // useless "Session abc12345" placeholder as a heading.
15004
- title: entry.firstPrompt ? entry.firstPrompt.replace(/\n/g, " ").trim().slice(0, 100) : "",
15005
- projectPath: entry.projectPath,
15006
- filePath: sessionPath,
15007
- mtime: entry.fileMtime,
15008
- contentPreview: entry.firstPrompt?.slice(0, 200),
15009
- extra: {
15010
- messageCount: entry.messageCount,
15011
- gitBranch: entry.gitBranch,
15012
- created: entry.created,
15013
- modified: entry.modified,
15014
- isSidechain: entry.isSidechain
15015
- }
15016
- };
15017
- }
15018
- itemToEntry(item) {
15019
- const extra = item.extra || {};
15020
- return {
15021
- sessionId: item.id,
15022
- fullPath: item.filePath,
15023
- fileMtime: item.mtime,
15024
- firstPrompt: item.contentPreview || "",
15025
- messageCount: extra.messageCount || 0,
15026
- created: extra.created || "",
15027
- modified: extra.modified || "",
15028
- gitBranch: extra.gitBranch || "",
15029
- projectPath: item.projectPath,
15030
- isSidechain: extra.isSidechain || false
15031
- };
15032
- }
15033
- };
15034
- }
15035
- });
15036
-
15037
14693
  // ../engine/dist/core/utils.js
15038
14694
  import { existsSync as existsSync21, readdirSync as readdirSync15 } from "fs";
15039
14695
  import { homedir as homedir6 } from "os";
@@ -18390,7 +18046,6 @@ var init_plugins_source = __esm({
18390
18046
 
18391
18047
  // ../engine/dist/parsers/all-sources.js
18392
18048
  function buildSourceRegistry(registry = new SourceRegistry()) {
18393
- registry.register(new SessionSource());
18394
18049
  registry.register(new PlanSource());
18395
18050
  registry.register(new TaskSource());
18396
18051
  registry.register(new ClaudeMdSource());
@@ -18414,7 +18069,6 @@ var init_all_sources = __esm({
18414
18069
  "../engine/dist/parsers/all-sources.js"() {
18415
18070
  "use strict";
18416
18071
  init_source_registry();
18417
- init_session_source();
18418
18072
  init_plan_source();
18419
18073
  init_task_source();
18420
18074
  init_claude_md_source();
@@ -18736,7 +18390,7 @@ var init_sync_fields = __esm({
18736
18390
  });
18737
18391
 
18738
18392
  // ../engine/dist/core/index-lock.js
18739
- import { existsSync as existsSync39, mkdirSync as mkdirSync7, openSync as openSync7, writeFileSync as writeFileSync6, readFileSync as readFileSync30, rmSync as rmSync4, statSync as statSync35, closeSync as closeSync7 } from "fs";
18393
+ import { existsSync as existsSync39, mkdirSync as mkdirSync7, openSync as openSync8, writeFileSync as writeFileSync6, readFileSync as readFileSync30, rmSync as rmSync4, statSync as statSync35, closeSync as closeSync8 } from "fs";
18740
18394
  import { dirname as dirname16, join as join39 } from "path";
18741
18395
  function acquireIndexLock(opts) {
18742
18396
  const lockPath = lockFilePath();
@@ -18748,7 +18402,7 @@ function acquireIndexLock(opts) {
18748
18402
  }
18749
18403
  let fd;
18750
18404
  try {
18751
- fd = openSync7(lockPath, "wx", 384);
18405
+ fd = openSync8(lockPath, "wx", 384);
18752
18406
  } catch (err) {
18753
18407
  if (err.code === "EEXIST")
18754
18408
  return null;
@@ -18758,7 +18412,7 @@ function acquireIndexLock(opts) {
18758
18412
  try {
18759
18413
  writeFileSync6(fd, JSON.stringify(holder));
18760
18414
  } finally {
18761
- closeSync7(fd);
18415
+ closeSync8(fd);
18762
18416
  }
18763
18417
  let released = false;
18764
18418
  return {
@@ -18858,7 +18512,7 @@ var init_http = __esm({
18858
18512
  CLI_VERSION = JSON.parse(readFileSync31(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
18859
18513
  } catch {
18860
18514
  try {
18861
- if (true) CLI_VERSION = "0.6.9";
18515
+ if (true) CLI_VERSION = "0.7.1";
18862
18516
  } catch {
18863
18517
  }
18864
18518
  }
@@ -19357,7 +19011,7 @@ var init_telemetry = __esm({
19357
19011
  init_sync_client();
19358
19012
  cliVersion2 = () => {
19359
19013
  try {
19360
- return true ? "0.6.9" : "dev";
19014
+ return true ? "0.7.1" : "dev";
19361
19015
  } catch {
19362
19016
  return "dev";
19363
19017
  }
@@ -19713,16 +19367,17 @@ __export(auto_update_exports, {
19713
19367
  noteUpdateFailure: () => noteUpdateFailure,
19714
19368
  planAutoUpdate: () => planAutoUpdate,
19715
19369
  readUpdateState: () => readUpdateState,
19370
+ resolveNpm: () => resolveNpm,
19716
19371
  runAutoUpdate: () => runAutoUpdate,
19717
19372
  runningPrefix: () => runningPrefix,
19718
19373
  sweepStaleStaging: () => sweepStaleStaging,
19719
19374
  tarballUrl: () => tarballUrl
19720
19375
  });
19721
- import { execSync as execSync2 } from "node:child_process";
19722
- import { readFileSync as readFileSync34 } from "node:fs";
19376
+ import { execSync as execSync2, execFileSync as execFileSync3 } from "node:child_process";
19377
+ import { readFileSync as readFileSync34, existsSync as existsSync41 } from "node:fs";
19723
19378
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19724
19379
  import { writeFileSync as writeFileSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync5, readdirSync as readdirSync31, statSync as statSync36, mkdirSync as mkdirSync9 } from "node:fs";
19725
- import { join as join41, dirname as dirname18, sep as sep2 } from "node:path";
19380
+ import { join as join41, dirname as dirname18, sep as sep2, posix as pathPosix, win32 as pathWin32 } from "node:path";
19726
19381
  import { tmpdir as tmpdir2, homedir as homedir15 } from "node:os";
19727
19382
  import { createHash as createHash8 } from "node:crypto";
19728
19383
  function compareVersions(a, b) {
@@ -19837,25 +19492,46 @@ function runningPrefix(moduleUrl = import.meta.url) {
19837
19492
  const beforeNodeModules = here.slice(0, at);
19838
19493
  return beforeNodeModules.endsWith(`${sep2}lib`) ? beforeNodeModules.slice(0, -4) : beforeNodeModules;
19839
19494
  }
19840
- function npmInstall(cmd) {
19495
+ function npmInstall(npm, args) {
19841
19496
  try {
19842
- execSync2(cmd, { stdio: ["ignore", "ignore", "pipe"] });
19497
+ const opts = { stdio: ["ignore", "ignore", "pipe"], env: npm.env };
19498
+ if (npm.shell) execSync2([npm.file, ...npm.args, ...args].map((a) => `"${a}"`).join(" "), opts);
19499
+ else execFileSync3(npm.file, [...npm.args, ...args], opts);
19843
19500
  } catch (e) {
19844
19501
  const err = e;
19845
19502
  const detail = String(err.stderr ?? "").trim().split("\n").filter((l) => l.trim() && !/^npm (notice|warn)\b/i.test(l)).slice(-4).join(" | ").slice(0, 400);
19846
19503
  throw new Error(detail ? `${err.message ?? "install failed"} \u2014 ${detail}` : err.message ?? "install failed");
19847
19504
  }
19848
19505
  }
19506
+ function resolveNpm(loc) {
19507
+ const win = loc.platform === "win32";
19508
+ const p = win ? pathWin32 : pathPosix;
19509
+ const nodeDir = p.dirname(loc.execPath);
19510
+ const cliUnder = (root) => win ? p.join(root, "node_modules", "npm", "bin", "npm-cli.js") : p.join(root, "lib", "node_modules", "npm", "bin", "npm-cli.js");
19511
+ const candidates2 = [
19512
+ cliUnder(win ? nodeDir : p.join(nodeDir, "..")),
19513
+ ...loc.prefix ? [cliUnder(loc.prefix)] : []
19514
+ ];
19515
+ const pathKey = Object.keys(loc.env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
19516
+ const binDirs = [nodeDir, ...loc.prefix ? [win ? loc.prefix : p.join(loc.prefix, "bin")] : []];
19517
+ const delimiter3 = win ? ";" : ":";
19518
+ const current = loc.env[pathKey] ?? "";
19519
+ const env = { ...loc.env, [pathKey]: [...binDirs, ...current ? [current] : []].join(delimiter3) };
19520
+ const cli = candidates2.find((c) => loc.exists(c));
19521
+ if (cli) return { file: loc.execPath, args: [cli], env, shell: false };
19522
+ return { file: "npm", args: [], env, shell: true };
19523
+ }
19849
19524
  function realInstall(tgz) {
19850
19525
  const prefix = runningPrefix();
19526
+ const npm = resolveNpm({ execPath: process.execPath, prefix, platform: process.platform, env: process.env, exists: existsSync41 });
19851
19527
  if (prefix) {
19852
- npmInstall(`npm install -g --prefix "${prefix}" "${tgz}"`);
19528
+ npmInstall(npm, ["install", "-g", "--prefix", prefix, tgz]);
19853
19529
  return;
19854
19530
  }
19855
19531
  try {
19856
- npmInstall(`npm install -g "${tgz}"`);
19532
+ npmInstall(npm, ["install", "-g", tgz]);
19857
19533
  } catch {
19858
- npmInstall(`npm install -g --prefix "${process.env.HOME}/.local" "${tgz}"`);
19534
+ npmInstall(npm, ["install", "-g", "--prefix", join41(homedir15(), ".local"), tgz]);
19859
19535
  }
19860
19536
  }
19861
19537
  function realRestart(platform2) {
@@ -19956,7 +19632,9 @@ var init_auto_update = __esm({
19956
19632
  var sync_client_exports = {};
19957
19633
  __export(sync_client_exports, {
19958
19634
  BATCH_SCAN_MAX_BYTES: () => BATCH_SCAN_MAX_BYTES,
19635
+ DERIVED_MAX_BYTES: () => DERIVED_MAX_BYTES,
19959
19636
  FULL_BUILD_MAX_BYTES: () => FULL_BUILD_MAX_BYTES,
19637
+ SYNC_CHUNK_BYTES: () => SYNC_CHUNK_BYTES,
19960
19638
  _acceptedUploads: () => _acceptedUploads,
19961
19639
  _fetchTenantSyncConfigForTests: () => _fetchTenantSyncConfigForTests,
19962
19640
  _resetCapsCacheForTests: () => _resetCapsCacheForTests,
@@ -19975,8 +19653,10 @@ __export(sync_client_exports, {
19975
19653
  reconcileFields: () => reconcileFields,
19976
19654
  removeCredentials: () => removeCredentials,
19977
19655
  saveCredentials: () => saveCredentials,
19656
+ scanTranscriptFindingsChunked: () => scanTranscriptFindingsChunked,
19978
19657
  serverAcceptsGzipBody: () => serverAcceptsGzipBody,
19979
19658
  serverIngestConcurrency: () => serverIngestConcurrency,
19659
+ sessionBuildBytes: () => sessionBuildBytes,
19980
19660
  sessionFileBytes: () => sessionFileBytes,
19981
19661
  setServerIngestConcurrency: () => setServerIngestConcurrency,
19982
19662
  syncIncremental: () => syncIncremental,
@@ -20748,16 +20428,53 @@ async function syncToTargetInScope(cred, opts = {}) {
20748
20428
  clearTimeout(timer);
20749
20429
  }
20750
20430
  };
20431
+ const postChunkedHead = async (conv, fileSize) => {
20432
+ const headers = { "content-type": "application/json" };
20433
+ if (cred.token) headers.authorization = `Bearer ${cred.token}`;
20434
+ const ac = new AbortController();
20435
+ const timer = setTimeout(() => ac.abort(new Error(`upload timed out after ${UPLOAD_TIMEOUT_MS}ms`)), UPLOAD_TIMEOUT_MS);
20436
+ try {
20437
+ const res = await fetch(`${base3}/api/sync`, {
20438
+ method: "POST",
20439
+ headers,
20440
+ body: JSON.stringify({ conversations: [conv] }),
20441
+ signal: ac.signal
20442
+ });
20443
+ if (!res.ok) throw new Error(`chunked head sync failed: HTTP ${res.status} ${await res.text().catch(() => "")}`);
20444
+ const body = await res.json().catch(() => ({}));
20445
+ const id = conv.session_id;
20446
+ const guarded = (body.shrink_guarded ?? []).find((g) => g.session_id === id);
20447
+ const mtime = conv.mtime;
20448
+ try {
20449
+ if (!guarded) {
20450
+ markSynced(base3, [{ id, mtime, offset: conv.from_offset, size: conv.from_offset, acked: true }]);
20451
+ } else if (typeof guarded.o === "number" && guarded.o > 0 && guarded.o <= fileSize) {
20452
+ markSynced(base3, [{ id, mtime, offset: guarded.o, size: guarded.o, acked: true }]);
20453
+ } else {
20454
+ markSynced(base3, [{ id, mtime }]);
20455
+ }
20456
+ } catch {
20457
+ }
20458
+ appendResults.uploaded++;
20459
+ } finally {
20460
+ clearTimeout(timer);
20461
+ }
20462
+ };
20751
20463
  let appendBatch = [];
20464
+ let appendBatchBytes = 0;
20752
20465
  const APPEND_BATCH_SIZE = 50;
20466
+ const APPEND_BATCH_MAX_BYTES = SYNC_CHUNK_BYTES;
20753
20467
  const appendFlush = async () => {
20754
20468
  if (appendBatch.length === 0) return;
20755
20469
  const batch = appendBatch;
20756
20470
  appendBatch = [];
20471
+ appendBatchBytes = 0;
20757
20472
  await postAppend(batch);
20758
20473
  };
20759
- const appendAdd = async (conv) => {
20474
+ const appendAdd = async (conv, bytes = 0) => {
20475
+ if (appendBatch.length > 0 && appendBatchBytes + bytes > APPEND_BATCH_MAX_BYTES) await appendFlush();
20760
20476
  appendBatch.push(conv);
20477
+ appendBatchBytes += bytes;
20761
20478
  if (appendBatch.length >= APPEND_BATCH_SIZE) await appendFlush();
20762
20479
  };
20763
20480
  const itemsBatch = makeBatcher("items", 100);
@@ -20836,7 +20553,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20836
20553
  let externalFindings = null;
20837
20554
  if (scanSecrets) {
20838
20555
  externalFindings = /* @__PURE__ */ new Map();
20839
- const toScan = slice.filter((ref) => modeOf(ref) === "full" && sessionFileBytes(ref) <= FULL_BUILD_MAX_BYTES);
20556
+ const toScan = slice.filter((ref) => modeOf(ref) === "full" && sessionBuildBytes(ref) <= FULL_BUILD_MAX_BYTES);
20840
20557
  trace?.(`batch secret scan start: ${toScan.length} session(s), \u2264${(BATCH_SCAN_MAX_BYTES / 1048576).toFixed(0)}MB per slice`);
20841
20558
  const batch = batchScanExternal(
20842
20559
  toScan.map((ref) => ({
@@ -20880,11 +20597,17 @@ async function syncToTargetInScope(cred, opts = {}) {
20880
20597
  const ack = ledger.get(ref.prefixedId);
20881
20598
  try {
20882
20599
  const tail = await buildConversationTail(ref, ack.o ?? 0, { mapPath, tenantRules });
20883
- if (tail && tail.findings.length > 0) {
20600
+ const chunked = sessionBuildBytes(ref) > FULL_BUILD_MAX_BYTES;
20601
+ if (tail && tail.findings.length > 0 && !chunked) {
20884
20602
  promotedForFindings = true;
20885
20603
  } else if (tail) {
20886
20604
  redactions += tail.redactions;
20887
- await appendAdd(tail.conv);
20605
+ await appendAdd(tail.conv, tail.bytes);
20606
+ if (tail.findings.length > 0) {
20607
+ await appendFlush();
20608
+ const all = await scanTranscriptFindingsChunked(ref, tenantRules);
20609
+ if (all.length > 0) await findingsBatch.add(all);
20610
+ }
20888
20611
  walked++;
20889
20612
  if (walked % 250 === 0) console.error(`[sync] ${walked}/${slice.length} sessions\u2026`);
20890
20613
  continue;
@@ -20932,12 +20655,13 @@ async function syncToTargetInScope(cred, opts = {}) {
20932
20655
  }
20933
20656
  redactions += built.redactions;
20934
20657
  if (built.srcHash) shippedHash.set(ref.prefixedId, built.srcHash);
20935
- await convBatch.add(built.conv);
20658
+ if (built.conv.chunked) await postChunkedHead(built.conv, sessionFileBytes(ref));
20659
+ else await convBatch.add(built.conv);
20936
20660
  if (built.findings.length > 0) await findingsBatch.add(built.findings);
20937
20661
  if (upload.sessionMeta) {
20938
20662
  await accumulateKg(extractEntities(built.kgText, { projectPath: built.projectPath, sourceType: "session", sessionId: ref.prefixedId, validFrom: ref.mtime ? new Date(ref.mtime).toISOString().slice(0, 10) : void 0 }), ref.prefixedId);
20939
20663
  }
20940
- if (upload.sessionMeta && sessionFileBytes(ref) <= FULL_BUILD_MAX_BYTES) {
20664
+ if (upload.sessionMeta && sessionFileBytes(ref) <= DERIVED_MAX_BYTES) {
20941
20665
  try {
20942
20666
  const count = { redactions: 0 };
20943
20667
  const offloaded = await scanPool().runDerived({
@@ -21117,35 +20841,70 @@ async function syncToTargetInScope(cred, opts = {}) {
21117
20841
  scanMs: Math.round(scanMs)
21118
20842
  };
21119
20843
  }
20844
+ async function scanTranscriptFindingsChunked(ref, tenantRules) {
20845
+ const backend = getBackendForId(ref.prefixedId);
20846
+ const findings = [];
20847
+ if (!backend?.readFromOffset) return findings;
20848
+ let offset = 0;
20849
+ let lineBase = 0;
20850
+ for (; ; ) {
20851
+ const chunk = await backend.readFromOffset(ref.prefixedId, offset, SYNC_CHUNK_BYTES);
20852
+ if (chunk.newOffset <= offset) break;
20853
+ offset = chunk.newOffset;
20854
+ if (chunk.skippedBytes) {
20855
+ lineBase += 1;
20856
+ continue;
20857
+ }
20858
+ try {
20859
+ for (const f of scanTextForFindings(chunk.text)) {
20860
+ findings.push({ session_id: ref.prefixedId, detector: "builtin", rule: f.rule, line: lineBase + f.line, preview: f.preview });
20861
+ }
20862
+ if (tenantRules && tenantRules.length > 0) {
20863
+ for (const f of scanTenantRules(chunk.text, tenantRules)) {
20864
+ findings.push({ session_id: ref.prefixedId, detector: f.detector, rule: f.rule, line: lineBase + f.line, preview: f.preview });
20865
+ }
20866
+ }
20867
+ } catch {
20868
+ }
20869
+ for (let i = chunk.text.indexOf("\n"); i >= 0; i = chunk.text.indexOf("\n", i + 1)) lineBase++;
20870
+ }
20871
+ return dropFuzzyFindings(findings, (f) => ({ detector: f.detector, rule: f.rule }));
20872
+ }
21120
20873
  function sessionFileBytes(ref) {
21121
20874
  const backend = getBackendForId(ref.prefixedId);
21122
20875
  if (!backend?.isAppendOnly?.()) return 0;
21123
20876
  return backend.fileSize?.(ref.prefixedId) ?? 0;
21124
20877
  }
20878
+ function sessionBuildBytes(ref) {
20879
+ const main = sessionFileBytes(ref);
20880
+ if (main === 0) return 0;
20881
+ return Math.max(main, shadowUncompressedBytes(ref.toolId, ref.rawId));
20882
+ }
21125
20883
  async function buildConversationSync(ref, mtime, opts = {}) {
21126
20884
  const mapPath = opts.mapPath ?? ((p) => p);
21127
20885
  let includeRaw = opts.includeRaw !== false;
21128
20886
  let includeMeta = opts.includeMeta !== false;
21129
20887
  let srcHash;
21130
- const fileBytes = sessionFileBytes(ref);
21131
- const oversized = fileBytes > FULL_BUILD_MAX_BYTES;
21132
- let tailText = "";
21133
- let tailOffsetEnd = 0;
20888
+ const oversized = sessionBuildBytes(ref) > FULL_BUILD_MAX_BYTES;
20889
+ let headOffsetEnd = 0;
20890
+ let oversizedFindings = null;
21134
20891
  let transcript = null;
21135
20892
  let fullestContainer = null;
21136
20893
  if (oversized) {
21137
20894
  const backend = getBackendForId(ref.prefixedId);
21138
20895
  if (!backend?.readFromOffset) return null;
21139
20896
  try {
21140
- const tail = await backend.readFromOffset(ref.prefixedId, fileBytes - FULL_BUILD_TAIL_BYTES);
21141
- tailText = tail.text;
21142
- tailOffsetEnd = tail.newOffset;
21143
- const messages = parseTailMessages(ref.toolId, tailText);
20897
+ let head = await backend.readFromOffset(ref.prefixedId, 0, SYNC_CHUNK_BYTES);
20898
+ for (let hops = 0; head.skippedBytes && hops < 8; hops++) {
20899
+ head = await backend.readFromOffset(ref.prefixedId, head.newOffset, SYNC_CHUNK_BYTES);
20900
+ }
20901
+ headOffsetEnd = head.newOffset;
20902
+ const messages = parseTailMessages(ref.toolId, head.text);
21144
20903
  if (messages.length > 0) transcript = { messages, subagents: [] };
21145
20904
  } catch {
21146
20905
  }
21147
20906
  includeRaw = false;
21148
- includeMeta = false;
20907
+ oversizedFindings = await scanTranscriptFindingsChunked(ref, opts.tenantRules);
21149
20908
  } else {
21150
20909
  try {
21151
20910
  const backend = getBackendForId(ref.prefixedId) ?? getBackend("claude");
@@ -21206,7 +20965,6 @@ async function buildConversationSync(ref, mtime, opts = {}) {
21206
20965
  );
21207
20966
  let projectPath = ref.projectPath;
21208
20967
  const meta = { messageCount: textMessages.length };
21209
- if (oversized) meta.truncated = true;
21210
20968
  if (textMessages.filter((m) => m.role === "user").length <= 1) meta.oneShot = true;
21211
20969
  try {
21212
20970
  const cwd = ref.fullPath ? readCwdFromJsonl(ref.fullPath) : "";
@@ -21320,21 +21078,7 @@ async function buildConversationSync(ref, mtime, opts = {}) {
21320
21078
  }
21321
21079
  scanMs = performance.now() - t0;
21322
21080
  }
21323
- if (oversized && tailText) {
21324
- const t0 = performance.now();
21325
- try {
21326
- for (const f of scanTextForFindings(tailText)) {
21327
- findings.push({ session_id: ref.prefixedId, detector: "builtin", rule: f.rule, line: f.line, preview: f.preview });
21328
- }
21329
- if (opts.tenantRules && opts.tenantRules.length > 0) {
21330
- for (const f of scanTenantRules(tailText, opts.tenantRules)) {
21331
- findings.push({ session_id: ref.prefixedId, detector: f.detector, rule: f.rule, line: f.line, preview: f.preview });
21332
- }
21333
- }
21334
- } catch {
21335
- }
21336
- scanMs = performance.now() - t0;
21337
- }
21081
+ if (oversizedFindings) findings.push(...oversizedFindings);
21338
21082
  const keptFindings = dropFuzzyFindings(findings, (f) => ({ detector: f.detector, rule: f.rule }));
21339
21083
  findings.length = 0;
21340
21084
  findings.push(...keptFindings);
@@ -21380,7 +21124,10 @@ async function buildConversationSync(ref, mtime, opts = {}) {
21380
21124
  // (convBatch onFlush uses this, not a recomputed size) so the next
21381
21125
  // append's base_offset matches exactly — no spurious continuity misses on
21382
21126
  // an actively-growing session. undefined for non-append-only backends.
21383
- from_offset: oversized ? tailOffsetEnd : getBackendForId(ref.prefixedId)?.fileSize?.(ref.rawId),
21127
+ from_offset: oversized ? headOffsetEnd : getBackendForId(ref.prefixedId)?.fileSize?.(ref.rawId),
21128
+ // The head of a chunked session. The server reports back when its shrink
21129
+ // guard keeps a fuller stored copy, so the client appends from there.
21130
+ ...oversized ? { chunked: true } : {},
21384
21131
  meta,
21385
21132
  mtime
21386
21133
  },
@@ -21402,12 +21149,30 @@ async function buildConversationTail(ref, fromOffset, opts = {}) {
21402
21149
  const backend = getBackendForId(ref.prefixedId);
21403
21150
  if (!backend?.readFromOffset) return null;
21404
21151
  const mapPath = opts.mapPath ?? ((p) => p);
21405
- const { text: tailText, newOffset } = await backend.readFromOffset(ref.prefixedId, fromOffset);
21406
- if (!tailText.trim()) return null;
21407
- const messages = parseTailMessages(ref.toolId, tailText);
21408
- if (messages.length === 0) return null;
21152
+ const { text: tailText, newOffset, skippedBytes } = await backend.readFromOffset(ref.prefixedId, fromOffset, SYNC_CHUNK_BYTES);
21153
+ if (newOffset <= fromOffset) return null;
21154
+ const messages = skippedBytes ? [] : parseTailMessages(ref.toolId, tailText);
21409
21155
  const textMessages = messages.filter((m) => m.role !== "summary" && m.content?.trim());
21410
- if (textMessages.length === 0 && !messages.some((m) => m.toolCalls?.length)) return null;
21156
+ const hasContent = textMessages.length > 0 || messages.some((m) => m.toolCalls?.length);
21157
+ if (!hasContent) {
21158
+ return {
21159
+ conv: {
21160
+ session_id: ref.prefixedId,
21161
+ tool: ref.toolId,
21162
+ project_path: mapPath(ref.projectPath),
21163
+ append: true,
21164
+ base_offset: fromOffset,
21165
+ from_offset: newOffset,
21166
+ redacted_text: "",
21167
+ envelope: { v: TRANSCRIPT_VERSION, messages: [], subagents: [] },
21168
+ mtime: Math.floor(ref.mtime) || 0
21169
+ },
21170
+ redactions: 0,
21171
+ newOffset,
21172
+ findings: [],
21173
+ bytes: 0
21174
+ };
21175
+ }
21411
21176
  const firstUser = textMessages.find((m) => m.role === "user");
21412
21177
  if (isInternalToolPrompt(firstUser?.content)) return null;
21413
21178
  const count = { redactions: 0 };
@@ -21450,7 +21215,8 @@ async function buildConversationTail(ref, fromOffset, opts = {}) {
21450
21215
  },
21451
21216
  redactions: count.redactions,
21452
21217
  newOffset,
21453
- findings
21218
+ findings,
21219
+ bytes: Buffer.byteLength(tailText)
21454
21220
  };
21455
21221
  }
21456
21222
  function isSyncSkip(r) {
@@ -21480,7 +21246,7 @@ async function syncIncremental(opts = {}) {
21480
21246
  lock.release();
21481
21247
  }
21482
21248
  }
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;
21249
+ 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, DERIVED_MAX_BYTES, SYNC_CHUNK_BYTES, BATCH_SCAN_MAX_BYTES;
21484
21250
  var init_sync_client = __esm({
21485
21251
  "src/sync-client.ts"() {
21486
21252
  "use strict";
@@ -21525,7 +21291,7 @@ var init_sync_client = __esm({
21525
21291
  ownVersion = JSON.parse(readFileSync35(join42(dir, "../package.json"), "utf-8")).version || ownVersion;
21526
21292
  } catch {
21527
21293
  try {
21528
- if (true) ownVersion = "0.6.9";
21294
+ if (true) ownVersion = "0.7.1";
21529
21295
  } catch {
21530
21296
  }
21531
21297
  }
@@ -21552,8 +21318,9 @@ var init_sync_client = __esm({
21552
21318
  capsOk = /* @__PURE__ */ new Map();
21553
21319
  advertisedIngestConc = /* @__PURE__ */ new Map();
21554
21320
  advertisedGzipBody = /* @__PURE__ */ new Set();
21555
- FULL_BUILD_MAX_BYTES = Math.max(8, parseInt(process.env.CHAT_RECALL_FULL_BUILD_MAX_MB || "64", 10)) * 1024 * 1024;
21556
- FULL_BUILD_TAIL_BYTES = 16 * 1024 * 1024;
21321
+ FULL_BUILD_MAX_BYTES = Math.max(8, parseInt(process.env.CHAT_RECALL_FULL_BUILD_MAX_MB || "24", 10)) * 1024 * 1024;
21322
+ DERIVED_MAX_BYTES = Math.max(8, parseInt(process.env.CHAT_RECALL_DERIVED_MAX_MB || "64", 10)) * 1024 * 1024;
21323
+ SYNC_CHUNK_BYTES = 8 * 1024 * 1024;
21557
21324
  BATCH_SCAN_MAX_BYTES = Math.max(1, parseInt(process.env.CHAT_RECALL_BATCHSCAN_MAX_MB || "64", 10)) * 1024 * 1024;
21558
21325
  }
21559
21326
  });
@@ -21674,7 +21441,7 @@ var init_update_notice = __esm({
21674
21441
  });
21675
21442
 
21676
21443
  // ../engine/dist/core/which.js
21677
- import { existsSync as existsSync41, statSync as statSync38, accessSync, constants } from "node:fs";
21444
+ import { existsSync as existsSync42, statSync as statSync38, accessSync, constants } from "node:fs";
21678
21445
  import { join as join44, isAbsolute as isAbsolute2, delimiter as delimiter2, sep as sep3 } from "node:path";
21679
21446
  function executable(p) {
21680
21447
  try {
@@ -21715,7 +21482,7 @@ function resolveOnPath(name) {
21715
21482
  continue;
21716
21483
  for (const c of candidates(name)) {
21717
21484
  const full = join44(dir, c);
21718
- if (existsSync41(full) && executable(full))
21485
+ if (existsSync42(full) && executable(full))
21719
21486
  return full;
21720
21487
  }
21721
21488
  }
@@ -21778,7 +21545,7 @@ __export(mcp_clients_exports, {
21778
21545
  inspectMcpClients: () => inspectMcpClients,
21779
21546
  registerMcpEverywhere: () => registerMcpEverywhere
21780
21547
  });
21781
- import { existsSync as existsSync43, mkdirSync as mkdirSync12, readFileSync as readFileSync37, writeFileSync as writeFileSync12 } from "fs";
21548
+ import { existsSync as existsSync44, mkdirSync as mkdirSync12, readFileSync as readFileSync37, writeFileSync as writeFileSync12 } from "fs";
21782
21549
  import { homedir as homedir16 } from "os";
21783
21550
  import { dirname as dirname21, join as join46 } from "path";
21784
21551
  function envHome(varName, fallback) {
@@ -21797,7 +21564,7 @@ function readJsonObject(path) {
21797
21564
  }
21798
21565
  function registerMcpServersJson(target, path, spec) {
21799
21566
  let cfg = {};
21800
- if (existsSync43(path)) {
21567
+ if (existsSync44(path)) {
21801
21568
  try {
21802
21569
  cfg = readJsonObject(path);
21803
21570
  } catch (err) {
@@ -21826,7 +21593,7 @@ function registerOpencodeJson(path, spec) {
21826
21593
  const id = "opencode";
21827
21594
  const label = "OpenCode";
21828
21595
  let cfg = {};
21829
- if (existsSync43(path)) {
21596
+ if (existsSync44(path)) {
21830
21597
  try {
21831
21598
  cfg = readJsonObject(path);
21832
21599
  } catch (err) {
@@ -21865,7 +21632,7 @@ function registerCodexToml(path, spec) {
21865
21632
  const id = "codex";
21866
21633
  const label = "Codex";
21867
21634
  const block = codexBlock(spec);
21868
- const prev = existsSync43(path) ? readFileSync37(path, "utf-8") : "";
21635
+ const prev = existsSync44(path) ? readFileSync37(path, "utf-8") : "";
21869
21636
  const lines = prev.length ? prev.split("\n") : [];
21870
21637
  const start = lines.findIndex((l) => l.trim() === CODEX_HEADER);
21871
21638
  if (start === -1) {
@@ -21906,7 +21673,7 @@ function inspectMcpClients(opts = {}) {
21906
21673
  return MCP_CLIENTS.map((c) => {
21907
21674
  const path = c.configPath(home);
21908
21675
  let registered = false;
21909
- if (existsSync43(path)) {
21676
+ if (existsSync44(path)) {
21910
21677
  try {
21911
21678
  const raw = readFileSync37(path, "utf-8");
21912
21679
  registered = c.id === "codex" ? raw.includes(CODEX_HEADER) : !!(JSON.parse(raw).mcpServers?.[MCP_SERVER_NAME] ?? JSON.parse(raw).mcp?.[MCP_SERVER_NAME]);
@@ -21943,7 +21710,7 @@ var init_mcp_clients = __esm({
21943
21710
  configPath: (home) => join46(envHome("CHAT_RECALL_CODEX_HOME", join46(home, ".codex")), "config.toml"),
21944
21711
  isPresent(home) {
21945
21712
  const p = this.configPath(home);
21946
- return existsSync43(p) || existsSync43(dirname21(p));
21713
+ return existsSync44(p) || existsSync44(dirname21(p));
21947
21714
  },
21948
21715
  register(spec, home) {
21949
21716
  return registerCodexToml(this.configPath(home), spec);
@@ -21956,7 +21723,7 @@ var init_mcp_clients = __esm({
21956
21723
  configPath: (home) => join46(envHome("XDG_CONFIG_HOME", join46(home, ".config")), "opencode", "opencode.json"),
21957
21724
  isPresent(home) {
21958
21725
  const p = this.configPath(home);
21959
- return existsSync43(p) || existsSync43(dirname21(p));
21726
+ return existsSync44(p) || existsSync44(dirname21(p));
21960
21727
  },
21961
21728
  register(spec, home) {
21962
21729
  return registerOpencodeJson(this.configPath(home), spec);
@@ -21970,7 +21737,7 @@ var init_mcp_clients = __esm({
21970
21737
  bin: "agy",
21971
21738
  configPath: (home) => join46(home, ".gemini", "config", "mcp_config.json"),
21972
21739
  isPresent(home) {
21973
- return existsSync43(join46(home, ".gemini", "antigravity-cli")) || existsSync43(this.configPath(home));
21740
+ return existsSync44(join46(home, ".gemini", "antigravity-cli")) || existsSync44(this.configPath(home));
21974
21741
  },
21975
21742
  register(spec, home) {
21976
21743
  return registerMcpServersJson(this, this.configPath(home), spec);
@@ -21983,7 +21750,7 @@ var init_mcp_clients = __esm({
21983
21750
  configPath: (home) => join46(home, ".cursor", "mcp.json"),
21984
21751
  isPresent(home) {
21985
21752
  const p = this.configPath(home);
21986
- return existsSync43(p) || existsSync43(dirname21(p));
21753
+ return existsSync44(p) || existsSync44(dirname21(p));
21987
21754
  },
21988
21755
  register(spec, home) {
21989
21756
  return registerMcpServersJson(this, this.configPath(home), spec);
@@ -22007,7 +21774,7 @@ __export(companions_exports, {
22007
21774
  uninstallCodeindex: () => uninstallCodeindex,
22008
21775
  unregisterCodeindexMcp: () => unregisterCodeindexMcp
22009
21776
  });
22010
- import { existsSync as existsSync44, mkdirSync as mkdirSync13, statSync as statSync39, chmodSync as chmodSync4, unlinkSync as unlinkSync3, renameSync as renameSync5, createWriteStream, readFileSync as readFileSync38, writeFileSync as writeFileSync13 } from "node:fs";
21777
+ import { existsSync as existsSync45, mkdirSync as mkdirSync13, statSync as statSync39, chmodSync as chmodSync4, unlinkSync as unlinkSync3, renameSync as renameSync5, createWriteStream, readFileSync as readFileSync38, writeFileSync as writeFileSync13 } from "node:fs";
22011
21778
  import { homedir as homedir17 } from "node:os";
22012
21779
  import { join as join47 } from "node:path";
22013
21780
  import { execSync as execSync3 } from "node:child_process";
@@ -22130,7 +21897,7 @@ Refusing to install. The release may have been re-uploaded, or something is serv
22130
21897
  renameSync5(tmpPath, CODEINDEX_BIN_PATH);
22131
21898
  } catch (err) {
22132
21899
  try {
22133
- if (existsSync44(tmpPath))
21900
+ if (existsSync45(tmpPath))
22134
21901
  unlinkSync3(tmpPath);
22135
21902
  } catch {
22136
21903
  }
@@ -22139,7 +21906,7 @@ Refusing to install. The release may have been re-uploaded, or something is serv
22139
21906
  return checkCodeindexStatus();
22140
21907
  }
22141
21908
  function uninstallCodeindex() {
22142
- if (existsSync44(CODEINDEX_BIN_PATH)) {
21909
+ if (existsSync45(CODEINDEX_BIN_PATH)) {
22143
21910
  unlinkSync3(CODEINDEX_BIN_PATH);
22144
21911
  return { removed: true, path: CODEINDEX_BIN_PATH };
22145
21912
  }
@@ -22147,7 +21914,7 @@ function uninstallCodeindex() {
22147
21914
  }
22148
21915
  function registerCodeindexMcp(mcpJsonPath, binaryPath) {
22149
21916
  let cfg = {};
22150
- if (existsSync44(mcpJsonPath)) {
21917
+ if (existsSync45(mcpJsonPath)) {
22151
21918
  try {
22152
21919
  cfg = JSON.parse(readFileSync38(mcpJsonPath, "utf-8"));
22153
21920
  } catch {
@@ -22162,7 +21929,7 @@ function registerCodeindexMcp(mcpJsonPath, binaryPath) {
22162
21929
  return { added: true };
22163
21930
  }
22164
21931
  function unregisterCodeindexMcp(mcpJsonPath) {
22165
- if (!existsSync44(mcpJsonPath))
21932
+ if (!existsSync45(mcpJsonPath))
22166
21933
  return { removed: false };
22167
21934
  let cfg;
22168
21935
  try {
@@ -22208,7 +21975,7 @@ __export(install_skills_exports, {
22208
21975
  skillsSourceDir: () => skillsSourceDir,
22209
21976
  uninstallSkills: () => uninstallSkills
22210
21977
  });
22211
- import { readdirSync as readdirSync33, existsSync as existsSync45, mkdirSync as mkdirSync14, rmSync as rmSync7, cpSync, writeFileSync as writeFileSync14, readFileSync as readFileSync39, statSync as statSync40, realpathSync as realpathSync4 } from "fs";
21978
+ import { readdirSync as readdirSync33, existsSync as existsSync46, mkdirSync as mkdirSync14, rmSync as rmSync7, cpSync, writeFileSync as writeFileSync14, readFileSync as readFileSync39, statSync as statSync40, realpathSync as realpathSync4 } from "fs";
22212
21979
  import { createHash as createHash11 } from "crypto";
22213
21980
  import { join as join48, basename as basename21 } from "path";
22214
21981
  function skillsSourceDir() {
@@ -22247,8 +22014,8 @@ function skillStamp() {
22247
22014
  }
22248
22015
  function bundledSkillNames() {
22249
22016
  const src = skillsSourceDir();
22250
- if (!existsSync45(src)) return [];
22251
- return readdirSync33(src, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync45(join48(src, e.name, "SKILL.md"))).map((e) => e.name).sort();
22017
+ if (!existsSync46(src)) return [];
22018
+ return readdirSync33(src, { withFileTypes: true }).filter((e) => e.isDirectory() && existsSync46(join48(src, e.name, "SKILL.md"))).map((e) => e.name).sort();
22252
22019
  }
22253
22020
  function realPathOrSelf(p) {
22254
22021
  try {
@@ -22272,7 +22039,7 @@ function claudeSkillTargets() {
22272
22039
  dir,
22273
22040
  // Judge each profile on ITS OWN transcripts, not the primary's: a home
22274
22041
  // with no projects/ is a directory someone made, not a Claude install.
22275
- available: existsSync45(join48(home, "projects"))
22042
+ available: existsSync46(join48(home, "projects"))
22276
22043
  });
22277
22044
  }
22278
22045
  if (targets.length === 0) {
@@ -22296,7 +22063,7 @@ function skillTargets() {
22296
22063
  ];
22297
22064
  }
22298
22065
  function isManaged(dir) {
22299
- return existsSync45(join48(dir, MARKER));
22066
+ return existsSync46(join48(dir, MARKER));
22300
22067
  }
22301
22068
  function installSkills(opts = {}) {
22302
22069
  const onlyAvailable = opts.onlyAvailable ?? true;
@@ -22314,7 +22081,7 @@ function installSkills(opts = {}) {
22314
22081
  }
22315
22082
  for (const name of names) {
22316
22083
  const dest = join48(t.dir, name);
22317
- if (existsSync45(dest) && !isManaged(dest)) {
22084
+ if (existsSync46(dest) && !isManaged(dest)) {
22318
22085
  skippedUserOwned.push(name);
22319
22086
  continue;
22320
22087
  }
@@ -22334,7 +22101,7 @@ function uninstallSkills() {
22334
22101
  const perTarget = [];
22335
22102
  for (const t of skillTargets()) {
22336
22103
  const removed = [];
22337
- if (!existsSync45(t.dir)) {
22104
+ if (!existsSync46(t.dir)) {
22338
22105
  perTarget.push({ id: t.id, label: t.label, removed });
22339
22106
  continue;
22340
22107
  }
@@ -22358,7 +22125,7 @@ function skillsNeedRefresh() {
22358
22125
  if (!t.available) continue;
22359
22126
  for (const name of names) {
22360
22127
  const marker = join48(t.dir, name, MARKER);
22361
- if (!existsSync45(marker)) return true;
22128
+ if (!existsSync46(marker)) return true;
22362
22129
  try {
22363
22130
  if (readFileSync39(marker, "utf-8").trim() !== cur) return true;
22364
22131
  } catch {
@@ -22376,7 +22143,7 @@ function skillStatus() {
22376
22143
  let ver = null;
22377
22144
  for (const name of names) {
22378
22145
  const marker = join48(t.dir, name, MARKER);
22379
- if (existsSync45(marker)) {
22146
+ if (existsSync46(marker)) {
22380
22147
  installed++;
22381
22148
  try {
22382
22149
  ver = readFileSync39(marker, "utf-8").trim();
@@ -22564,7 +22331,7 @@ __export(toolkit_sync_exports, {
22564
22331
  supportedTargetsFor: () => supportedTargetsFor,
22565
22332
  writeMcpEntry: () => writeMcpEntry
22566
22333
  });
22567
- import { existsSync as existsSync46, mkdirSync as mkdirSync15, readFileSync as readFileSync41, writeFileSync as writeFileSync15, cpSync as cpSync2 } from "fs";
22334
+ import { existsSync as existsSync47, mkdirSync as mkdirSync15, readFileSync as readFileSync41, writeFileSync as writeFileSync15, cpSync as cpSync2 } from "fs";
22568
22335
  import { dirname as dirname22, join as join50, basename as basename23 } from "path";
22569
22336
  import { homedir as homedir19 } from "os";
22570
22337
  function supportedTargetsFor(type) {
@@ -22619,11 +22386,11 @@ function skillsDirFor(tool) {
22619
22386
  }
22620
22387
  function copySkill(row, toTool) {
22621
22388
  const skillDir = row.extra.skillDir || dirname22(row.filePath);
22622
- if (!existsSync46(skillDir))
22389
+ if (!existsSync47(skillDir))
22623
22390
  return { ok: false, status: 404, error: `Source dir missing: ${skillDir}` };
22624
22391
  const name = row.extra.skillName || basename23(skillDir);
22625
22392
  const targetDir = join50(skillsDirFor(toTool), name);
22626
- if (existsSync46(targetDir))
22393
+ if (existsSync47(targetDir))
22627
22394
  return { ok: false, status: 409, error: `Already exists: ${targetDir}` };
22628
22395
  try {
22629
22396
  mkdirSync15(dirname22(targetDir), { recursive: true });
@@ -22655,7 +22422,7 @@ function readMcpEntry(tool, name) {
22655
22422
  return entry;
22656
22423
  }
22657
22424
  for (const t of tries) {
22658
- if (!existsSync46(t.path))
22425
+ if (!existsSync47(t.path))
22659
22426
  continue;
22660
22427
  try {
22661
22428
  const cfg = JSON.parse(readFileSync41(t.path, "utf-8"));
@@ -22668,7 +22435,7 @@ function readMcpEntry(tool, name) {
22668
22435
  return null;
22669
22436
  }
22670
22437
  function readCodexMcpEntry(path, target) {
22671
- if (!existsSync46(path))
22438
+ if (!existsSync47(path))
22672
22439
  return null;
22673
22440
  let content;
22674
22441
  try {
@@ -22722,7 +22489,7 @@ function readCodexMcpEntry(path, target) {
22722
22489
  }
22723
22490
  function writeCodexMcpEntry(path, name, entry) {
22724
22491
  let content = "";
22725
- if (existsSync46(path))
22492
+ if (existsSync47(path))
22726
22493
  content = readFileSync41(path, "utf-8");
22727
22494
  const header = `[mcp_servers.${name}]`;
22728
22495
  if (content.includes(header))
@@ -22780,7 +22547,7 @@ function writeMcpEntry(toTool, name, entry) {
22780
22547
  key2 = "mcp";
22781
22548
  }
22782
22549
  let cfg = {};
22783
- if (existsSync46(path)) {
22550
+ if (existsSync47(path)) {
22784
22551
  try {
22785
22552
  cfg = JSON.parse(readFileSync41(path, "utf-8"));
22786
22553
  } catch {
@@ -22844,7 +22611,7 @@ function copyCommandOrAgent(type, row, toTool) {
22844
22611
  return { ok: false, status: 404, error: `Source unreadable: ${e instanceof Error ? e.message : "failed"}` };
22845
22612
  }
22846
22613
  const out = emit(type, art, toTool);
22847
- if (existsSync46(out.path))
22614
+ if (existsSync47(out.path))
22848
22615
  return { ok: false, status: 409, error: `Already exists: ${out.path}` };
22849
22616
  try {
22850
22617
  mkdirSync15(dirname22(out.path), { recursive: true });
@@ -22864,7 +22631,7 @@ function copyInstructions(row, toTool) {
22864
22631
  return { ok: false, status: 404, error: `Source unreadable: ${e instanceof Error ? e.message : "failed"}` };
22865
22632
  }
22866
22633
  const out = emit("instructions", art, toTool, row.projectPath || void 0);
22867
- if (existsSync46(out.path))
22634
+ if (existsSync47(out.path))
22868
22635
  return { ok: false, status: 409, error: `Already exists: ${out.path}` };
22869
22636
  try {
22870
22637
  mkdirSync15(dirname22(out.path), { recursive: true });
@@ -23014,7 +22781,7 @@ __export(toolkit_pull_exports, {
23014
22781
  planPull: () => planPull,
23015
22782
  portableCommand: () => portableCommand
23016
22783
  });
23017
- import { existsSync as existsSync47, mkdirSync as mkdirSync16, statSync as statSync41, writeFileSync as writeFileSync16 } from "node:fs";
22784
+ import { existsSync as existsSync48, mkdirSync as mkdirSync16, statSync as statSync41, writeFileSync as writeFileSync16 } from "node:fs";
23018
22785
  import { dirname as dirname23, join as join51, basename as basename24, isAbsolute as isAbsolute3 } from "node:path";
23019
22786
  function isExecutableFile(p) {
23020
22787
  try {
@@ -23167,7 +22934,7 @@ function executePull(rows, opts = {}) {
23167
22934
  for (const tool of tools) {
23168
22935
  const dir = join51(skillsDirFor(tool), name);
23169
22936
  const file = join51(dir, "SKILL.md");
23170
- if (existsSync47(file)) {
22937
+ if (existsSync48(file)) {
23171
22938
  outcomes.push({ type: "skill", name, tool, status: "present" });
23172
22939
  continue;
23173
22940
  }
@@ -23212,7 +22979,7 @@ function executePull(rows, opts = {}) {
23212
22979
  outcomes.push({ type, name, tool, status: "failed", reason: e instanceof Error ? e.message : "emit failed" });
23213
22980
  continue;
23214
22981
  }
23215
- if (existsSync47(out.path)) {
22982
+ if (existsSync48(out.path)) {
23216
22983
  outcomes.push({ type, name, tool, status: "present" });
23217
22984
  continue;
23218
22985
  }
@@ -23370,7 +23137,7 @@ __export(board_hooks_exports, {
23370
23137
  removeBoardHooks: () => removeBoardHooks,
23371
23138
  resolveCliEntry: () => resolveCliEntry
23372
23139
  });
23373
- import { existsSync as existsSync48, mkdirSync as mkdirSync17, readFileSync as readFileSync42, writeFileSync as writeFileSync17 } from "node:fs";
23140
+ import { existsSync as existsSync49, mkdirSync as mkdirSync17, readFileSync as readFileSync42, writeFileSync as writeFileSync17 } from "node:fs";
23374
23141
  import { dirname as dirname24, join as join52 } from "node:path";
23375
23142
  function boardHookCommand(execPath, cliEntry, mode) {
23376
23143
  return `"${execPath}" "${cliEntry}" task-hook --${mode}`;
@@ -23410,7 +23177,7 @@ function removeBoardHooks(config2) {
23410
23177
  return removed;
23411
23178
  }
23412
23179
  function readConfig(file) {
23413
- if (!existsSync48(file)) return { hooks: {} };
23180
+ if (!existsSync49(file)) return { hooks: {} };
23414
23181
  try {
23415
23182
  const parsed = JSON.parse(readFileSync42(file, "utf-8"));
23416
23183
  if (!parsed.hooks || typeof parsed.hooks !== "object") parsed.hooks = {};
@@ -23422,7 +23189,7 @@ function readConfig(file) {
23422
23189
  function registerBoardHooks(files, execPath, cliEntry) {
23423
23190
  const written = [];
23424
23191
  for (const file of files) {
23425
- if (existsSync48(file)) {
23192
+ if (existsSync49(file)) {
23426
23193
  try {
23427
23194
  JSON.parse(readFileSync42(file, "utf-8"));
23428
23195
  } catch {
@@ -23442,7 +23209,7 @@ function registerBoardHooks(files, execPath, cliEntry) {
23442
23209
  }
23443
23210
  function resolveCliEntry(distDir) {
23444
23211
  const bundled = join52(distDir, "cli.js");
23445
- return existsSync48(bundled) ? bundled : join52(distDir, "cli.ts");
23212
+ return existsSync49(bundled) ? bundled : join52(distDir, "cli.ts");
23446
23213
  }
23447
23214
  var BOARD_HOOKS;
23448
23215
  var init_board_hooks = __esm({
@@ -23545,6 +23312,9 @@ async function repairSession(id, opts = {}) {
23545
23312
  note: "archive count is a lower bound; the rebuild also unions the on-disk tail"
23546
23313
  };
23547
23314
  }
23315
+ if (!opts.force && endpoints.every((ep) => (currentOnServer.get(ep.url) ?? 0) >= fullest.messages)) {
23316
+ return { sessionId: id, status: "already-full", fullestMessages: fullest.messages, fullestSource: fullest.source, pushed: [] };
23317
+ }
23548
23318
  seedShadow(rawId, fullest.container);
23549
23319
  const located = backend.findSession(rawId);
23550
23320
  const projectPath = located?.projectPath || cwdFromContainer(fullest.container);
@@ -23587,7 +23357,7 @@ async function repairSession(id, opts = {}) {
23587
23357
  try {
23588
23358
  const res = await fetchWithTimeout(`${ep.url}/api/sync`, { method: "POST", headers, body: JSON.stringify({ conversations: [built.conv] }) }, 6e4);
23589
23359
  if (!res.ok) {
23590
- pushed.push({ server: ep.url, before, after: before });
23360
+ pushed.push({ server: ep.url, before, after: before, error: `HTTP ${res.status}` });
23591
23361
  log6(`push to ${ep.url} FAILED: HTTP ${res.status}`);
23592
23362
  continue;
23593
23363
  }
@@ -23596,10 +23366,21 @@ async function repairSession(id, opts = {}) {
23596
23366
  pushed.push({ server: ep.url, before, after: afterN });
23597
23367
  log6(`pushed to ${ep.url}: ${before} \u2192 ${afterN} msg(s)`);
23598
23368
  } catch (e) {
23599
- pushed.push({ server: ep.url, before, after: before });
23600
- log6(`push to ${ep.url} errored: ${e instanceof Error ? e.message : e}`);
23369
+ const error = e instanceof Error ? e.message : String(e);
23370
+ pushed.push({ server: ep.url, before, after: before, error });
23371
+ log6(`push to ${ep.url} errored: ${error}`);
23601
23372
  }
23602
23373
  }
23374
+ if (pushed.every((p) => p.error)) {
23375
+ return {
23376
+ sessionId: id,
23377
+ status: "error",
23378
+ fullestMessages: builtCount,
23379
+ fullestSource: fullest.source,
23380
+ pushed,
23381
+ note: `every push failed: ${pushed.map((p) => `${p.server}: ${p.error}`).join("; ")}`
23382
+ };
23383
+ }
23603
23384
  return { sessionId: id, status: "repaired", fullestMessages: builtCount, fullestSource: fullest.source, pushed };
23604
23385
  }
23605
23386
  async function repairSessions(ids, opts = {}) {
@@ -24094,11 +23875,11 @@ var verify_repair_exports = {};
24094
23875
  __export(verify_repair_exports, {
24095
23876
  forceFullResync: () => forceFullResync
24096
23877
  });
24097
- import { copyFileSync as copyFileSync3, existsSync as existsSync49 } from "fs";
23878
+ import { copyFileSync as copyFileSync3, existsSync as existsSync50 } from "fs";
24098
23879
  function forceFullResync(server, sessionIds) {
24099
23880
  if (sessionIds.length === 0) return 0;
24100
23881
  const path = ledgerFilePath();
24101
- if (existsSync49(path)) {
23882
+ if (existsSync50(path)) {
24102
23883
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "").replace(/-/g, "");
24103
23884
  try {
24104
23885
  copyFileSync3(path, `${path}.bak-${stamp}`);
@@ -25252,7 +25033,7 @@ __export(project_tasks_exports, {
25252
25033
  pushProjectTaskStatuses: () => pushProjectTaskStatuses,
25253
25034
  renderTeamTasksMd: () => renderTeamTasksMd
25254
25035
  });
25255
- import { existsSync as existsSync50, readFileSync as readFileSync44, writeFileSync as writeFileSync18, mkdirSync as mkdirSync18, statSync as statSync42 } from "node:fs";
25036
+ import { existsSync as existsSync51, readFileSync as readFileSync44, writeFileSync as writeFileSync18, mkdirSync as mkdirSync18, statSync as statSync42 } from "node:fs";
25256
25037
  import { join as join55, dirname as dirname25 } from "node:path";
25257
25038
  import { homedir as homedir21 } from "node:os";
25258
25039
  function isTeamTaskStatus(v) {
@@ -25338,7 +25119,7 @@ async function pushProjectTaskStatuses(base3, authHeaders, opts = {}) {
25338
25119
  }
25339
25120
  for (const project of projects) {
25340
25121
  const file = join55(project, fk.filename);
25341
- if (!existsSync50(file)) continue;
25122
+ if (!existsSync51(file)) continue;
25342
25123
  let mtime;
25343
25124
  try {
25344
25125
  mtime = statSync42(file).mtimeMs;
@@ -25420,7 +25201,7 @@ __export(intent_drain_exports, {
25420
25201
  drainSyncIntents: () => drainSyncIntents,
25421
25202
  instructionTargets: () => instructionTargets
25422
25203
  });
25423
- import { existsSync as existsSync51, readFileSync as readFileSync45, writeFileSync as writeFileSync19, appendFileSync, mkdirSync as mkdirSync19 } from "node:fs";
25204
+ import { existsSync as existsSync52, readFileSync as readFileSync45, writeFileSync as writeFileSync19, appendFileSync, mkdirSync as mkdirSync19 } from "node:fs";
25424
25205
  import { join as join56 } from "node:path";
25425
25206
  import { fileURLToPath as fileURLToPath4 } from "node:url";
25426
25207
  import { dirname as dirname26 } from "node:path";
@@ -25435,7 +25216,7 @@ function instructionTargets(rootPath, isGlobal) {
25435
25216
  const tools = installed.length ? installed : [INSTRUCTION_TOOLS[0]];
25436
25217
  if (isGlobal) return [...new Set(tools.map((t) => instructionsPath(t.id)))];
25437
25218
  if (!rootPath) return [];
25438
- const present = INSTRUCTION_FILENAMES.map((f) => join56(rootPath, f)).filter((p) => existsSync51(p));
25219
+ const present = INSTRUCTION_FILENAMES.map((f) => join56(rootPath, f)).filter((p) => existsSync52(p));
25439
25220
  if (present.length) return present;
25440
25221
  return [...new Set(tools.map((t) => join56(rootPath, instructionsFilename(t.id))))];
25441
25222
  }
@@ -25463,7 +25244,7 @@ function applyCodeRecommendation(intent) {
25463
25244
  mkdirSync19(dir, { recursive: true });
25464
25245
  } catch {
25465
25246
  }
25466
- const existing = existsSync51(file) ? readFileSync45(file, "utf8") : "";
25247
+ const existing = existsSync52(file) ? readFileSync45(file, "utf8") : "";
25467
25248
  if (existing.includes(text)) {
25468
25249
  skipped.push(file);
25469
25250
  continue;
@@ -25641,7 +25422,7 @@ var init_intent_drain = __esm({
25641
25422
  ownVersion2 = JSON.parse(readFileSync45(join56(dir, "../package.json"), "utf-8")).version || ownVersion2;
25642
25423
  } catch {
25643
25424
  try {
25644
- if (true) ownVersion2 = "0.6.9";
25425
+ if (true) ownVersion2 = "0.7.1";
25645
25426
  } catch {
25646
25427
  }
25647
25428
  }
@@ -25842,7 +25623,7 @@ var team_merge_exports = {};
25842
25623
  __export(team_merge_exports, {
25843
25624
  mergePullResult: () => mergePullResult
25844
25625
  });
25845
- import { existsSync as existsSync52, mkdirSync as mkdirSync20, readFileSync as readFileSync46, rmSync as rmSync8, writeFileSync as writeFileSync20, statSync as statSync43 } from "fs";
25626
+ import { existsSync as existsSync53, mkdirSync as mkdirSync20, readFileSync as readFileSync46, rmSync as rmSync8, writeFileSync as writeFileSync20, statSync as statSync43 } from "fs";
25846
25627
  import { dirname as dirname27, join as join57 } from "path";
25847
25628
  import { createHash as createHash14 } from "crypto";
25848
25629
  function installPathFor(art, tool) {
@@ -25959,7 +25740,7 @@ async function mergePullResult(opts) {
25959
25740
  if (!path)
25960
25741
  continue;
25961
25742
  try {
25962
- if (existsSync52(path))
25743
+ if (existsSync53(path))
25963
25744
  rmSync8(path, { force: true });
25964
25745
  await teamInstallsForget(id, row.tool);
25965
25746
  report.removed.push({ artifactId: id, path });
@@ -25977,7 +25758,7 @@ async function mergePullResult(opts) {
25977
25758
  }
25978
25759
  function readFileIfExists(path) {
25979
25760
  try {
25980
- if (!existsSync52(path))
25761
+ if (!existsSync53(path))
25981
25762
  return null;
25982
25763
  const s = statSync43(path);
25983
25764
  if (!s.isFile())
@@ -26063,7 +25844,7 @@ var init_vault_crypto = __esm({
26063
25844
  });
26064
25845
 
26065
25846
  // ../engine/dist/core/vault-source.js
26066
- import { existsSync as existsSync53, readdirSync as readdirSync34, statSync as statSync44 } from "fs";
25847
+ import { existsSync as existsSync54, readdirSync as readdirSync34, statSync as statSync44 } from "fs";
26067
25848
  import { extname, join as join58 } from "path";
26068
25849
  function* walkVaultSources() {
26069
25850
  const t = loadSettings().team.vault;
@@ -26089,7 +25870,7 @@ function safeStat(path) {
26089
25870
  }
26090
25871
  function* walkClaude() {
26091
25872
  const root = claudeBackend.projectsDir();
26092
- if (!existsSync53(root))
25873
+ if (!existsSync54(root))
26093
25874
  return;
26094
25875
  let projectDirs = [];
26095
25876
  try {
@@ -26142,7 +25923,7 @@ function* walkCursor() {
26142
25923
  }
26143
25924
  function* walkCodex() {
26144
25925
  const sessionsRoot = join58(codexBackend.homeDir(), "sessions");
26145
- if (!existsSync53(sessionsRoot))
25926
+ if (!existsSync54(sessionsRoot))
26146
25927
  return;
26147
25928
  const stack = [sessionsRoot];
26148
25929
  while (stack.length) {
@@ -26171,7 +25952,7 @@ function* walkCodex() {
26171
25952
  }
26172
25953
  function* walkAgy() {
26173
25954
  const brainDir = join58(agyBackend.homeDir(), "brain");
26174
- if (!existsSync53(brainDir))
25955
+ if (!existsSync54(brainDir))
26175
25956
  return;
26176
25957
  let sessionDirs = [];
26177
25958
  try {
@@ -26188,10 +25969,10 @@ function* walkAgy() {
26188
25969
  continue;
26189
25970
  }
26190
25971
  let filePath = join58(sessionPath, ".system_generated", "logs", "transcript.jsonl");
26191
- if (!existsSync53(filePath)) {
25972
+ if (!existsSync54(filePath)) {
26192
25973
  filePath = join58(sessionPath, ".system_generated", "logs", "transcript_full.jsonl");
26193
25974
  }
26194
- if (!existsSync53(filePath))
25975
+ if (!existsSync54(filePath))
26195
25976
  continue;
26196
25977
  const s = safeStat(filePath);
26197
25978
  if (!s)
@@ -26227,7 +26008,7 @@ __export(vault_client_exports, {
26227
26008
  vaultStatus: () => vaultStatus,
26228
26009
  vaultSync: () => vaultSync
26229
26010
  });
26230
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync47, writeFileSync as writeFileSync21, existsSync as existsSync54 } from "fs";
26011
+ import { mkdirSync as mkdirSync21, readFileSync as readFileSync47, writeFileSync as writeFileSync21, existsSync as existsSync55 } from "fs";
26231
26012
  import { dirname as dirname28, join as join59 } from "path";
26232
26013
  function context2() {
26233
26014
  const s = loadSettings().team;
@@ -26440,7 +26221,7 @@ async function vaultRestore(passphrase, opts = {}) {
26440
26221
  }
26441
26222
  try {
26442
26223
  const path = importedPathFor(blob.tool, blob.sessionId);
26443
- if (existsSync54(path)) {
26224
+ if (existsSync55(path)) {
26444
26225
  const localStat = await import("fs").then((m) => m.statSync(path));
26445
26226
  if (Math.abs(localStat.mtimeMs - blob.mtimeSourceMs) < 1) {
26446
26227
  report.skipped.push({ sessionId: blob.sessionId, tool: blob.tool, reason: "unchanged" });
@@ -26523,7 +26304,7 @@ __export(keyring_exports, {
26523
26304
  psQuote: () => psQuote,
26524
26305
  shQuote: () => shQuote
26525
26306
  });
26526
- import { execFileSync as execFileSync3 } from "node:child_process";
26307
+ import { execFileSync as execFileSync4 } from "node:child_process";
26527
26308
  function probeKeyring(r = realRunner) {
26528
26309
  if (r.platform === "darwin") {
26529
26310
  try {
@@ -26635,7 +26416,7 @@ var init_keyring = __esm({
26635
26416
  ACCOUNT = "vault-passphrase";
26636
26417
  realRunner = {
26637
26418
  run(file, args, input) {
26638
- return execFileSync3(file, args, {
26419
+ return execFileSync4(file, args, {
26639
26420
  input,
26640
26421
  encoding: "utf-8",
26641
26422
  stdio: input === void 0 ? ["ignore", "pipe", "pipe"] : ["pipe", "pipe", "pipe"]
@@ -27070,7 +26851,7 @@ __export(service_installer_exports, {
27070
26851
  uninstallService: () => uninstallService
27071
26852
  });
27072
26853
  import { execSync as execSync5 } from "child_process";
27073
- import { existsSync as existsSync55, mkdirSync as mkdirSync22, readFileSync as readFileSync48, rmSync as rmSync9, writeFileSync as writeFileSync22 } from "fs";
26854
+ import { existsSync as existsSync56, mkdirSync as mkdirSync22, readFileSync as readFileSync48, rmSync as rmSync9, writeFileSync as writeFileSync22 } from "fs";
27074
26855
  import { homedir as homedir22 } from "os";
27075
26856
  import { join as join60 } from "path";
27076
26857
  import { fileURLToPath as fileURLToPath5 } from "url";
@@ -27097,7 +26878,7 @@ function isServiceRunning() {
27097
26878
  return /com\.chat-recall\.watch/.test(tryCmd("launchctl list 2>/dev/null"));
27098
26879
  }
27099
26880
  if (process.platform === "linux") {
27100
- return /active/.test(tryCmd("systemctl --user is-active chat-recall-watch.service 2>/dev/null"));
26881
+ return /^active$/m.test(tryCmd("systemctl --user is-active chat-recall-watch.service 2>/dev/null"));
27101
26882
  }
27102
26883
  return false;
27103
26884
  }
@@ -27106,7 +26887,7 @@ function refreshServiceDefinition() {
27106
26887
  try {
27107
26888
  const { watchJs, node, logFile } = resolveDaemonPaths();
27108
26889
  const unitPath = join60(homedir22(), ".config", "systemd", "user", "chat-recall-watch.service");
27109
- if (!existsSync55(unitPath)) return false;
26890
+ if (!existsSync56(unitPath)) return false;
27110
26891
  const current = readFileSync48(unitPath, "utf8");
27111
26892
  if (!current.includes(watchJs)) return false;
27112
26893
  const wanted = renderSystemdUnit(watchJs, node, logFile);
@@ -27173,7 +26954,12 @@ function renderSystemdUnit(watchJs, node, logFile) {
27173
26954
  // sawtooth. 1536MB keeps GC honest (768 OOMed on the largest subagent-fanout session) with plenty of headroom for the
27174
26955
  // largest transcripts.
27175
26956
  `ExecStart=${node} --max-old-space-size=1536 ${watchJs}`,
27176
- "Restart=on-failure",
26957
+ // ALWAYS: the collector never ends its own work, so any exit systemd did
26958
+ // not ask for is a fault. Under on-failure a clean exit left it dead: on
26959
+ // 2026-09-16 the unit went "inactive (dead) … status=0/SUCCESS" and stayed
26960
+ // down for 8 days, so nothing synced and the auto-update tick, which runs
26961
+ // inside the daemon, never ran. `systemctl stop` still stops it.
26962
+ "Restart=always",
27177
26963
  "RestartSec=10",
27178
26964
  "Nice=10",
27179
26965
  // NEVER STOP RETRYING. systemd's default start limiter gives up after a
@@ -27269,7 +27055,7 @@ function uninstallService() {
27269
27055
  } catch {
27270
27056
  }
27271
27057
  const unitPath = join60(homedir22(), ".config", "systemd", "user", "chat-recall-watch.service");
27272
- if (existsSync55(unitPath)) {
27058
+ if (existsSync56(unitPath)) {
27273
27059
  rmSync9(unitPath, { force: true });
27274
27060
  removed = true;
27275
27061
  }
@@ -27286,7 +27072,7 @@ function uninstallService() {
27286
27072
  } catch {
27287
27073
  }
27288
27074
  let removed = false;
27289
- if (existsSync55(plistPath)) {
27075
+ if (existsSync56(plistPath)) {
27290
27076
  rmSync9(plistPath, { force: true });
27291
27077
  removed = true;
27292
27078
  }
@@ -27699,7 +27485,7 @@ init_project_resolver();
27699
27485
  import { config } from "dotenv";
27700
27486
  import { Command } from "commander";
27701
27487
  import chalk2 from "chalk";
27702
- import { existsSync as existsSync56, readFileSync as readFileSync49, writeFileSync as writeFileSync23, realpathSync as realpathSync5 } from "fs";
27488
+ import { existsSync as existsSync57, readFileSync as readFileSync49, writeFileSync as writeFileSync23, realpathSync as realpathSync5 } from "fs";
27703
27489
  import { homedir as homedir23 } from "os";
27704
27490
  import { join as join61, resolve as resolve2, dirname as dirname29, basename as basename27 } from "path";
27705
27491
 
@@ -27808,7 +27594,7 @@ function renderEnvNeeds(needs, installedCount) {
27808
27594
 
27809
27595
  // src/star-nudge.ts
27810
27596
  init_paths();
27811
- import { existsSync as existsSync42, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "node:fs";
27597
+ import { existsSync as existsSync43, mkdirSync as mkdirSync11, writeFileSync as writeFileSync11 } from "node:fs";
27812
27598
  import { dirname as dirname20, join as join45 } from "node:path";
27813
27599
  var REPO = "https://github.com/munhq/chat-recall";
27814
27600
  function markerPath() {
@@ -27824,7 +27610,7 @@ function shouldAskForStar(resultCount) {
27824
27610
  if (suppressed()) return false;
27825
27611
  if (!process.stderr.isTTY) return false;
27826
27612
  try {
27827
- return !existsSync42(markerPath());
27613
+ return !existsSync43(markerPath());
27828
27614
  } catch {
27829
27615
  return false;
27830
27616
  }
@@ -27865,7 +27651,7 @@ function claudeHookConfigFiles() {
27865
27651
  const out = [];
27866
27652
  const seen = /* @__PURE__ */ new Set();
27867
27653
  for (const home of claudeHomeDirs()) {
27868
- if (!existsSync56(join61(home, "projects"))) continue;
27654
+ if (!existsSync57(join61(home, "projects"))) continue;
27869
27655
  const file = join61(home, "hooks.json");
27870
27656
  let key2;
27871
27657
  try {
@@ -28013,6 +27799,7 @@ var DEFAULT_ALLOW = [
28013
27799
  "recall_user_prompts",
28014
27800
  "recall_decision_record",
28015
27801
  "recall_decisions",
27802
+ "recall_decision_scan",
28016
27803
  "recall_analytics_summary",
28017
27804
  "recall_outcome_summary",
28018
27805
  "recall_regenerate_summary",
@@ -28650,7 +28437,7 @@ memory.command("wake-up").description("Generate wake-up context (high-importance
28650
28437
  try {
28651
28438
  const identityFile = getIdentityFilePath();
28652
28439
  let identity = "AI coding assistant";
28653
- if (existsSync56(identityFile)) {
28440
+ if (existsSync57(identityFile)) {
28654
28441
  identity = readFileSync49(identityFile, "utf-8").trim();
28655
28442
  }
28656
28443
  const lines = [
@@ -28751,7 +28538,7 @@ program.command("feedback").argument("<message...>", "what you want to say \u201
28751
28538
  }
28752
28539
  });
28753
28540
  program.command("doctor").description("Quick health check across login, server, credentials, hooks, MCP server, and codeindex").option("--purge-local", "Remove the legacy local index (cache.db + vector/KG files) left by pre-thin-collector versions \u2014 storage is server-side now. Requires login; keeps credentials, sync ledger, identity, and the audit log.").action(async (opts) => {
28754
- const { existsSync: existsSync57, readFileSync: readFileSync50, statSync: statSync45 } = await import("fs");
28541
+ const { existsSync: existsSync58, readFileSync: readFileSync50, statSync: statSync45 } = await import("fs");
28755
28542
  const { execSync: execSync6 } = await import("child_process");
28756
28543
  const rows = [];
28757
28544
  const note = (ok, label, detail) => rows.push({ ok, label, detail });
@@ -28815,7 +28602,7 @@ program.command("doctor").description("Quick health check across login, server,
28815
28602
  }
28816
28603
  }
28817
28604
  const credFile = join61(getDataDir(), "credentials.json");
28818
- if (!existsSync57(credFile)) {
28605
+ if (!existsSync58(credFile)) {
28819
28606
  note(false, "Credentials file", `missing \u2014 run \`chat-recall login <server-url>\``);
28820
28607
  } else {
28821
28608
  try {
@@ -28830,13 +28617,13 @@ program.command("doctor").description("Quick health check across login, server,
28830
28617
  for (const hooksJson of claudeHookConfigFiles()) {
28831
28618
  const profile = basename27(dirname29(hooksJson));
28832
28619
  const label = `Claude hooks (${profile})`;
28833
- if (!existsSync57(hooksJson)) {
28620
+ if (!existsSync58(hooksJson)) {
28834
28621
  note(false, label, `no ${hooksJson} \u2014 run \`chat-recall install-hooks\``);
28835
28622
  continue;
28836
28623
  }
28837
28624
  try {
28838
28625
  const readHooks = (file) => {
28839
- if (!existsSync57(file)) return {};
28626
+ if (!existsSync58(file)) return {};
28840
28627
  try {
28841
28628
  return JSON.parse(readFileSync50(file, "utf-8")).hooks || {};
28842
28629
  } catch {
@@ -28879,9 +28666,9 @@ program.command("doctor").description("Quick health check across login, server,
28879
28666
  try {
28880
28667
  const reg = await Promise.resolve().then(() => (init_guard_registration(), guard_registration_exports));
28881
28668
  for (const target2 of reg.guardTargets()) {
28882
- if (!existsSync57(target2.homeDir)) continue;
28669
+ if (!existsSync58(target2.homeDir)) continue;
28883
28670
  const label = `Decision guard \u2014 ${target2.label}`;
28884
- if (!existsSync57(target2.configPath)) {
28671
+ if (!existsSync58(target2.configPath)) {
28885
28672
  note(false, label, `not registered \u2014 run \`chat-recall install-hooks\``);
28886
28673
  continue;
28887
28674
  }
@@ -28906,7 +28693,7 @@ program.command("doctor").description("Quick health check across login, server,
28906
28693
  const want = bundledSkillNames2().length;
28907
28694
  for (const t of skillTargets2()) {
28908
28695
  if (!t.available) continue;
28909
- const have = want === 0 ? 0 : bundledSkillNames2().filter((n) => existsSync57(join61(t.dir, n, ".chat-recall-managed"))).length;
28696
+ const have = want === 0 ? 0 : bundledSkillNames2().filter((n) => existsSync58(join61(t.dir, n, ".chat-recall-managed"))).length;
28910
28697
  note(
28911
28698
  have === want,
28912
28699
  `Skills \u2014 ${t.label}`,
@@ -28976,7 +28763,7 @@ program.command("doctor").description("Quick health check across login, server,
28976
28763
  } else if (process.platform === "darwin") {
28977
28764
  watchRunning = /com\.chat-recall\.watch/.test(tryCmd2("launchctl list 2>/dev/null"));
28978
28765
  } else {
28979
- watchRunning = /active/.test(tryCmd2("systemctl --user is-active chat-recall-watch.service 2>/dev/null"));
28766
+ watchRunning = /^active$/m.test(tryCmd2("systemctl --user is-active chat-recall-watch.service 2>/dev/null"));
28980
28767
  }
28981
28768
  note(watchRunning, "Watch daemon", watchRunning ? "running" : "not running (run `chat-recall watch --install-service` to keep collection running)");
28982
28769
  const dataDir = getDataDir();
@@ -28988,7 +28775,7 @@ program.command("doctor").description("Quick health check across login, server,
28988
28775
  join61(dataDir, "index", "knowledge_graph.db"),
28989
28776
  join61(dataDir, "index", "metadata.db")
28990
28777
  ];
28991
- const legacyPresent = legacyPaths.filter((p) => existsSync57(p));
28778
+ const legacyPresent = legacyPaths.filter((p) => existsSync58(p));
28992
28779
  if (opts.purgeLocal) {
28993
28780
  if (!target) {
28994
28781
  note(false, "Purge legacy index", "refused \u2014 log in and `chat-recall sync` first, so your history is on the server before removing local copies");
@@ -29632,10 +29419,10 @@ tasks.command("pull [dir]").description("Write TEAM_TASKS.md for your tasks in t
29632
29419
  try {
29633
29420
  const { resolveProjectId: resolveProjectId2 } = await Promise.resolve().then(() => (init_project_resolver(), project_resolver_exports));
29634
29421
  const { renderTeamTasksMd: renderTeamTasksMd2, parseTeamTasksFile: parseTeamTasksFile2 } = await Promise.resolve().then(() => (init_project_tasks(), project_tasks_exports));
29635
- const { writeFileSync: writeFileSync24, readFileSync: readFileSync50, existsSync: existsSync57 } = await import("node:fs");
29422
+ const { writeFileSync: writeFileSync24, readFileSync: readFileSync50, existsSync: existsSync58 } = await import("node:fs");
29636
29423
  const { join: join62 } = await import("node:path");
29637
29424
  const file = join62(cwd, "TEAM_TASKS.md");
29638
- if (!opts.force && existsSync57(file) && parseTeamTasksFile2(readFileSync50(file, "utf8")).length > 0) {
29425
+ if (!opts.force && existsSync58(file) && parseTeamTasksFile2(readFileSync50(file, "utf8")).length > 0) {
29639
29426
  console.error(chalk2.red("TEAM_TASKS.md has unpushed edits."), "Run", chalk2.bold("chat-recall tasks push"), "first, or", chalk2.bold("tasks pull --force"), "to discard.");
29640
29427
  process.exit(1);
29641
29428
  }
@@ -29654,10 +29441,10 @@ tasks.command("push [dir]").description("Push TEAM_TASKS.md status edits back to
29654
29441
  const cwd = dir || process.cwd();
29655
29442
  try {
29656
29443
  const { parseTeamTasksFile: parseTeamTasksFile2 } = await Promise.resolve().then(() => (init_project_tasks(), project_tasks_exports));
29657
- const { readFileSync: readFileSync50, existsSync: existsSync57 } = await import("node:fs");
29444
+ const { readFileSync: readFileSync50, existsSync: existsSync58 } = await import("node:fs");
29658
29445
  const { join: join62 } = await import("node:path");
29659
29446
  const file = join62(cwd, "TEAM_TASKS.md");
29660
- if (!existsSync57(file)) {
29447
+ if (!existsSync58(file)) {
29661
29448
  console.error(chalk2.red("no TEAM_TASKS.md here \u2014"), "run", chalk2.bold("chat-recall tasks pull"), "first");
29662
29449
  process.exit(1);
29663
29450
  }
@@ -30099,10 +29886,10 @@ program.command("task-hook").description("Move a board card when a prompt or a c
30099
29886
  return;
30100
29887
  }
30101
29888
  if (!opts.close) return;
30102
- const { execFileSync: execFileSync4 } = await import("node:child_process");
29889
+ const { execFileSync: execFileSync5 } = await import("node:child_process");
30103
29890
  const git2 = (args) => {
30104
29891
  try {
30105
- return execFileSync4("git", args, {
29892
+ return execFileSync5("git", args, {
30106
29893
  cwd,
30107
29894
  encoding: "utf8",
30108
29895
  timeout: 5e3,
@@ -30144,7 +29931,7 @@ program.command("task-hook").description("Move a board card when a prompt or a c
30144
29931
  }
30145
29932
  });
30146
29933
  program.command("install-hooks").description("Install Claude Code hooks (Stop + PreCompact + UserPromptSubmit + SessionEnd + SessionStart) into every Claude profile").option("--uninstall", "Remove hooks instead of installing them").option("--no-resume-hint", "Don't install the UserPromptSubmit resume-hint hook").option("--no-escalate", "Don't install the SessionEnd learnings-escalation hook").option("--no-wakeup", "Don't install the SessionStart wake-up hook").option("--no-guard", "Don't install the PreToolUse decision guard").option("--no-task-board", "Don't install the hooks that claim and close board cards").action(async (opts) => {
30147
- const { mkdirSync: mkdirSync23, copyFileSync: copyFileSync4, chmodSync: chmodSync5, existsSync: existsSync57, readFileSync: readFileSync50, writeFileSync: writeFileSync24, statSync: statSync45, unlinkSync: unlinkSync4 } = await import("fs");
29934
+ const { mkdirSync: mkdirSync23, copyFileSync: copyFileSync4, chmodSync: chmodSync5, existsSync: existsSync58, readFileSync: readFileSync50, writeFileSync: writeFileSync24, statSync: statSync45, unlinkSync: unlinkSync4 } = await import("fs");
30148
29935
  const { fileURLToPath: fileURLToPath6 } = await import("url");
30149
29936
  const { boardHookEntries: boardHookEntries2, isBoardHookEntry: isBoardHookEntry2, registerBoardHooks: registerBoardHooks2, resolveCliEntry: resolveCliEntry2 } = await Promise.resolve().then(() => (init_board_hooks(), board_hooks_exports));
30150
29937
  const here = fileURLToPath6(new URL(".", import.meta.url));
@@ -30157,7 +29944,7 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
30157
29944
  // freshly added script is only at the root until that build runs.
30158
29945
  join61(here, "..", "..", "..", "hooks", name)
30159
29946
  ];
30160
- return candidates2.find((p) => existsSync57(p));
29947
+ return candidates2.find((p) => existsSync58(p));
30161
29948
  };
30162
29949
  const sourceSaveHook = findHook("chat_recall_save_hook.sh");
30163
29950
  const sourceGuardHook = findHook("chat_recall_guard_hook.sh");
@@ -30176,7 +29963,7 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
30176
29963
  const installedWakeupHook = join61(hooksDir, "chat_recall_wakeup_hook.sh");
30177
29964
  const wakeupWiredInSettings = (home) => {
30178
29965
  const settings2 = join61(home, "settings.json");
30179
- if (!existsSync57(settings2)) return false;
29966
+ if (!existsSync58(settings2)) return false;
30180
29967
  try {
30181
29968
  const cfg = JSON.parse(readFileSync50(settings2, "utf-8"));
30182
29969
  const arr = Array.isArray(cfg.hooks?.SessionStart) ? cfg.hooks.SessionStart : [];
@@ -30187,7 +29974,7 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
30187
29974
  };
30188
29975
  const hookConfigFiles = claudeHookConfigFiles();
30189
29976
  const readHookConfig = (file) => {
30190
- if (!existsSync57(file)) return { hooks: {} };
29977
+ if (!existsSync58(file)) return { hooks: {} };
30191
29978
  try {
30192
29979
  const c = JSON.parse(readFileSync50(file, "utf-8"));
30193
29980
  if (!c.hooks || typeof c.hooks !== "object") c.hooks = {};
@@ -30204,7 +29991,7 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
30204
29991
  };
30205
29992
  const HOOK_EVENTS = ["Stop", "PreCompact", "UserPromptSubmit", "SessionEnd", "SessionStart", "PreToolUse"];
30206
29993
  const readJson4 = (file) => {
30207
- if (!existsSync57(file)) return {};
29994
+ if (!existsSync58(file)) return {};
30208
29995
  try {
30209
29996
  return JSON.parse(readFileSync50(file, "utf-8"));
30210
29997
  } catch {
@@ -30215,7 +30002,7 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
30215
30002
  const reg = await Promise.resolve().then(() => (init_guard_registration(), guard_registration_exports));
30216
30003
  const notes = [];
30217
30004
  for (const target of reg.guardTargets()) {
30218
- if (!existsSync57(target.homeDir)) continue;
30005
+ if (!existsSync58(target.homeDir)) continue;
30219
30006
  const wrapper = join61(hooksDir, reg.guardWrapperName(target.tool));
30220
30007
  writeFileSync24(wrapper, reg.guardWrapperSource(target.tool, hookPath));
30221
30008
  chmodSync5(wrapper, 493);
@@ -30236,7 +30023,7 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
30236
30023
  const reg = await Promise.resolve().then(() => (init_guard_registration(), guard_registration_exports));
30237
30024
  let removed = 0;
30238
30025
  for (const target of reg.guardTargets()) {
30239
- if (!existsSync57(target.configPath)) continue;
30026
+ if (!existsSync58(target.configPath)) continue;
30240
30027
  if (target.tool === "opencode") {
30241
30028
  const body = readFileSync50(target.configPath, "utf-8");
30242
30029
  if (!body.includes("chat-recall install-hooks")) continue;
@@ -30264,7 +30051,7 @@ program.command("install-hooks").description("Install Claude Code hooks (Stop +
30264
30051
  if (filtered.length) config2.hooks[event] = filtered;
30265
30052
  else delete config2.hooks[event];
30266
30053
  }
30267
- if (removed === 0 && !existsSync57(hooksJson)) continue;
30054
+ if (removed === 0 && !existsSync58(hooksJson)) continue;
30268
30055
  writeFileSync24(hooksJson, JSON.stringify(config2, null, 2) + "\n");
30269
30056
  total += removed;
30270
30057
  console.log(chalk2.green(`\u2713 Removed ${removed} chat-recall hook entr${removed === 1 ? "y" : "ies"} from ${hooksJson}`));
@@ -31137,8 +30924,8 @@ npm has ${version2}; the server you sync to still serves ${check.serverVersion}.
31137
30924
  console.log(chalk2.dim(`
31138
30925
  Installing ${version2} from npm\u2026`));
31139
30926
  try {
31140
- const { execFileSync: execFileSync4 } = await import("node:child_process");
31141
- execFileSync4("npm", ["install", "-g", `chat-recall@${version2}`], { stdio: "inherit" });
30927
+ const { execFileSync: execFileSync5 } = await import("node:child_process");
30928
+ execFileSync5("npm", ["install", "-g", `chat-recall@${version2}`], { stdio: "inherit" });
31142
30929
  const { installedVersion: installedVersion2 } = await Promise.resolve().then(() => (init_auto_update(), auto_update_exports));
31143
30930
  const landed = installedVersion2();
31144
30931
  if (landed && compareVersions2(landed, version2) >= 0) {