chat-recall 0.6.8 → 0.7.0

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();
@@ -5439,718 +5458,99 @@ var init_tool_paths = __esm({
5439
5458
  }
5440
5459
  });
5441
5460
 
5442
- // ../engine/dist/core/secret-redactor.js
5443
- var secret_redactor_exports = {};
5444
- __export(secret_redactor_exports, {
5445
- DEFAULT_REDACTION_RULES: () => DEFAULT_REDACTION_RULES,
5446
- _clearServerRulePack: () => _clearServerRulePack,
5447
- _resetPrivacyCache: () => _resetPrivacyCache,
5448
- _resetRedactorCache: () => _resetRedactorCache,
5449
- applyChunkPrivacy: () => applyChunkPrivacy,
5450
- findContextualSecrets: () => findContextualSecrets,
5451
- installServerRulePack: () => installServerRulePack,
5452
- isFilePathRedactionEnabled: () => isFilePathRedactionEnabled,
5453
- isRedactionEnabled: () => isRedactionEnabled,
5454
- isToolOutputRedactionEnabled: () => isToolOutputRedactionEnabled,
5455
- redactFilePaths: () => redactFilePaths,
5456
- redactSecrets: () => redactSecrets,
5457
- redactToolResultBody: () => redactToolResultBody,
5458
- scanTextForFindings: () => scanTextForFindings,
5459
- serverRulePackVersion: () => serverRulePackVersion,
5460
- validateRedactionRule: () => validateRedactionRule
5461
- });
5462
- import { statSync as statSync3 } from "fs";
5463
- import { createHash } from "crypto";
5464
- function secretOf(rule, m) {
5465
- const raw = rule.secret != null ? m[rule.secret] : m[0];
5466
- if (raw == null)
5467
- return null;
5468
- if (rule.where && !rule.where(raw))
5469
- return null;
5470
- return raw;
5461
+ // ../engine/dist/core/project-dir-name.js
5462
+ import { readdirSync as readdirSync4 } from "node:fs";
5463
+ function looksWindowsEncoded(dirName) {
5464
+ return /^[A-Za-z]--/.test(dirName);
5471
5465
  }
5472
- function looksLikeSecretToken(t) {
5473
- if (t.length < 32)
5474
- return false;
5475
- if (/^[a-f0-9]+$/i.test(t))
5476
- return true;
5477
- return /[A-Z]/.test(t) && /[a-z]/.test(t) && /[0-9]/.test(t);
5466
+ function decodeProjectDirName(dirName) {
5467
+ if (!dirName)
5468
+ return "";
5469
+ if (looksWindowsEncoded(dirName)) {
5470
+ const drive = dirName[0].toUpperCase();
5471
+ const rest = dirName.slice(3).replace(/-/g, "\\");
5472
+ return `${drive}:\\${rest}`;
5473
+ }
5474
+ return "/" + dirName.replace(/^-/, "").replace(/-/g, "/");
5478
5475
  }
5479
- function findContextualSecrets(text) {
5480
- if (!text)
5481
- return [];
5482
- const ranges = [];
5483
- let m;
5484
- SECRET_CONTEXT_RE.lastIndex = 0;
5485
- while ((m = SECRET_CONTEXT_RE.exec(text)) !== null) {
5486
- const winStart = m.index + m[0].length;
5487
- const window = text.slice(winStart, winStart + 80);
5488
- HIGH_ENTROPY_TOKEN_RE.lastIndex = 0;
5489
- let tm;
5490
- while ((tm = HIGH_ENTROPY_TOKEN_RE.exec(window)) !== null) {
5491
- if (!looksLikeSecretToken(tm[0]))
5492
- continue;
5493
- if (/^[a-f0-9]+$/i.test(tm[0]) && /\b(?:commit|sha-?\d*|hash|digest|checksum|uuid|guid|etag|blob|ref)\b/i.test(window.slice(0, tm.index))) {
5494
- continue;
5495
- }
5496
- ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
5476
+ function resolveProjectDirName(dirName, deps = {}) {
5477
+ const structural = decodeProjectDirName(dirName);
5478
+ if (!dirName)
5479
+ return structural;
5480
+ if (looksWindowsEncoded(dirName))
5481
+ return structural;
5482
+ const readdir3 = deps.readdir ?? ((p) => readdirSync4(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name));
5483
+ const encodeName = (name) => name.replace(/[-_.]/g, "-");
5484
+ let remaining = dirName.replace(/^-/, "");
5485
+ let current = "/";
5486
+ while (remaining) {
5487
+ let children;
5488
+ try {
5489
+ children = readdir3(current);
5490
+ } catch {
5497
5491
  break;
5498
5492
  }
5499
- }
5500
- ranges.sort((a, b) => a.start - b.start);
5501
- const out = [];
5502
- for (const r of ranges) {
5503
- const last = out[out.length - 1];
5504
- if (last && r.start < last.end) {
5505
- last.end = Math.max(last.end, r.end);
5506
- continue;
5493
+ let best = "";
5494
+ for (const child of children) {
5495
+ const encoded = encodeName(child);
5496
+ if (!encoded || encoded.length < best.length)
5497
+ continue;
5498
+ if (!remaining.startsWith(encoded))
5499
+ continue;
5500
+ const next = remaining[encoded.length];
5501
+ if (next !== void 0 && next !== "-")
5502
+ continue;
5503
+ if (encoded.length > best.length)
5504
+ best = child;
5507
5505
  }
5508
- out.push({ ...r });
5506
+ if (!best)
5507
+ break;
5508
+ current = current === "/" ? `/${best}` : `${current}/${best}`;
5509
+ remaining = remaining.slice(encodeName(best).length).replace(/^-/, "");
5509
5510
  }
5510
- return out;
5511
- }
5512
- function isEnvRedactionRequested() {
5513
- const v = process.env.CHAT_RECALL_REDACT_INDEX;
5514
- if (v === void 0)
5515
- return null;
5516
- return v === "1" || v === "true" || v === "yes";
5511
+ return remaining ? structural : current;
5517
5512
  }
5518
- function settingsView() {
5519
- let mtimeMs = -1;
5520
- try {
5521
- mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5522
- } catch {
5513
+ var init_project_dir_name = __esm({
5514
+ "../engine/dist/core/project-dir-name.js"() {
5515
+ "use strict";
5523
5516
  }
5524
- if (cache && cache.mtimeMs === mtimeMs)
5525
- return cache;
5526
- let enabled = false;
5527
- const extra = [];
5528
- try {
5529
- const s = loadSettings();
5530
- enabled = !!s.privacy?.redactIndex;
5531
- for (const r of s.privacy?.redactionRules ?? []) {
5532
- try {
5533
- extra.push({ label: r.label, pattern: new RegExp(r.pattern, "g") });
5534
- } catch {
5535
- }
5517
+ });
5518
+
5519
+ // ../engine/dist/core/model-pricing.js
5520
+ function pricingFor(modelId) {
5521
+ if (!modelId)
5522
+ return null;
5523
+ const lower = modelId.toLowerCase();
5524
+ let best = null;
5525
+ let bestLen = -1;
5526
+ for (const [prefix, entry] of Object.entries(TABLE)) {
5527
+ if (lower.startsWith(prefix.toLowerCase()) && prefix.length > bestLen) {
5528
+ best = entry;
5529
+ bestLen = prefix.length;
5536
5530
  }
5537
- } catch {
5538
- }
5539
- cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
5540
- return cache;
5541
- }
5542
- function compileServerRule(spec) {
5543
- const name = spec.name || "(unnamed)";
5544
- if (!spec.regex)
5545
- return { name, reason: "empty pattern" };
5546
- if (spec.regex.length > MAX_PATTERN_LEN)
5547
- return { name, reason: `pattern longer than ${MAX_PATTERN_LEN} chars` };
5548
- let re;
5549
- try {
5550
- const flags = "g" + (spec.flags || "").replace(/[^ims]/g, "");
5551
- re = new RegExp(spec.regex, flags);
5552
- } catch (e) {
5553
- return { name, reason: `invalid regex: ${e instanceof Error ? e.message : e}` };
5554
5531
  }
5555
- if (re.test(""))
5556
- return { name, reason: "matches the empty string (would redact everything)" };
5557
- re.lastIndex = 0;
5558
- if (re.test(CANARY))
5559
- return { name, reason: "matches benign text \u2014 too broad" };
5560
- re.lastIndex = 0;
5561
- return { label: `${spec.source === "pack" ? "pack" : "tenant"}:${name}`, pattern: re };
5562
- }
5563
- function validateRedactionRule(spec) {
5564
- const out = compileServerRule(spec);
5565
- return "reason" in out ? { ok: false, reason: out.reason } : { ok: true };
5532
+ return best;
5566
5533
  }
5567
- function installServerRulePack(pack) {
5568
- const rejected = [];
5569
- const rules = [];
5570
- for (const spec of pack.rules || []) {
5571
- if (rules.length >= MAX_PACK_RULES) {
5572
- rejected.push({ name: spec.name || "(unnamed)", reason: `pack exceeds ${MAX_PACK_RULES} rules` });
5534
+ function estimateCostUsdOrNull(modelsUsed, tokens) {
5535
+ let best = null;
5536
+ let bestRate = -1;
5537
+ for (const m of modelsUsed) {
5538
+ if (!m || m === "<synthetic>")
5573
5539
  continue;
5540
+ const p = pricingFor(m);
5541
+ if (p && p.output > bestRate) {
5542
+ best = p;
5543
+ bestRate = p.output;
5574
5544
  }
5575
- const out = compileServerRule(spec);
5576
- if ("reason" in out)
5577
- rejected.push(out);
5578
- else
5579
- rules.push(out);
5580
5545
  }
5581
- const version2 = pack.version || "";
5582
- serverPack = { version: version2, rules };
5583
- return { version: version2, accepted: rules.length, rejected };
5584
- }
5585
- function serverRulePackVersion() {
5586
- return serverPack ? serverPack.version || "unversioned" : null;
5587
- }
5588
- function _clearServerRulePack() {
5589
- serverPack = null;
5590
- }
5591
- function activeRules() {
5592
- const base3 = settingsView().rules;
5593
- return serverPack && serverPack.rules.length > 0 ? [...base3, ...serverPack.rules] : base3;
5594
- }
5595
- function isRedactionEnabled() {
5596
- const env = isEnvRedactionRequested();
5597
- if (env !== null)
5598
- return env;
5599
- return settingsView().enabled;
5546
+ if (!best)
5547
+ return null;
5548
+ const nonCachedInput = Math.max(0, tokens.inputTokens - tokens.cacheReadTokens - tokens.cacheCreationTokens);
5549
+ const cost = nonCachedInput / 1e6 * best.input + tokens.outputTokens / 1e6 * best.output + tokens.cacheReadTokens / 1e6 * best.cacheRead + tokens.cacheCreationTokens / 1e6 * best.cacheCreate;
5550
+ return Math.round(cost * 100) / 100;
5600
5551
  }
5601
- function _resetRedactorCache() {
5602
- cache = null;
5603
- }
5604
- function redactSecrets(text, opts = {}) {
5605
- if (!opts.force && !isRedactionEnabled())
5606
- return text;
5607
- if (!text)
5608
- return text;
5609
- const rules = opts.rules || activeRules();
5610
- let out = text;
5611
- for (const r of rules) {
5612
- out = out.replace(r.pattern, (...args) => {
5613
- const whole = args[0];
5614
- const m = args.slice(0, args.length);
5615
- const secret = secretOf(r, m);
5616
- if (secret == null)
5617
- return whole;
5618
- if (opts.count)
5619
- opts.count.redactions++;
5620
- return whole.slice(0, whole.length - secret.length) + `[REDACTED:${r.label}]`;
5621
- });
5622
- }
5623
- const ctx = findContextualSecrets(out);
5624
- for (let i = ctx.length - 1; i >= 0; i--) {
5625
- const { start, end } = ctx[i];
5626
- if (opts.count)
5627
- opts.count.redactions++;
5628
- out = out.slice(0, start) + "[REDACTED:secret-context]" + out.slice(end);
5629
- }
5630
- return out;
5631
- }
5632
- function maskSecret(s) {
5633
- if (!s)
5634
- return "";
5635
- if (s.length <= 4)
5636
- return "*".repeat(s.length);
5637
- return "*".repeat(Math.min(s.length - 4, 60)) + s.slice(-4);
5638
- }
5639
- function globalRegexFor(pattern) {
5640
- const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
5641
- const key2 = pattern.source + "\0" + flags;
5642
- let re = globalRegexCache.get(key2);
5643
- if (!re) {
5644
- re = new RegExp(pattern.source, flags);
5645
- globalRegexCache.set(key2, re);
5646
- }
5647
- return re;
5648
- }
5649
- function makeLineResolver(text) {
5650
- const starts = [];
5651
- for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1))
5652
- starts.push(i);
5653
- return (index) => {
5654
- let lo = 0, hi = starts.length;
5655
- while (lo < hi) {
5656
- const mid = lo + hi >> 1;
5657
- if (starts[mid] < index)
5658
- lo = mid + 1;
5659
- else
5660
- hi = mid;
5661
- }
5662
- return lo + 1;
5663
- };
5664
- }
5665
- function scanTextForFindings(text, opts = {}) {
5666
- if (!text)
5667
- return [];
5668
- const rules = opts.rules || activeRules();
5669
- const findings = [];
5670
- const seenSpans = /* @__PURE__ */ new Set();
5671
- const lineAt = makeLineResolver(text);
5672
- for (const r of rules) {
5673
- const re = globalRegexFor(r.pattern);
5674
- re.lastIndex = 0;
5675
- let m;
5676
- while ((m = re.exec(text)) !== null) {
5677
- const secret = secretOf(r, m);
5678
- if (secret == null) {
5679
- if (re.lastIndex === m.index)
5680
- re.lastIndex++;
5681
- continue;
5682
- }
5683
- const at = m.index + (m[0].length - secret.length);
5684
- const span = `${at}:${secret.length}`;
5685
- if (!seenSpans.has(span)) {
5686
- seenSpans.add(span);
5687
- findings.push({ rule: r.label, line: lineAt(at), preview: maskSecret(secret) });
5688
- }
5689
- if (re.lastIndex === m.index)
5690
- re.lastIndex++;
5691
- }
5692
- }
5693
- for (const { start, end } of findContextualSecrets(text)) {
5694
- const line = lineAt(start);
5695
- findings.push({ rule: "secret-context", line, preview: maskSecret(text.slice(start, end)) });
5696
- }
5697
- return findings;
5698
- }
5699
- function privacyView() {
5700
- let mtimeMs = -1;
5701
- try {
5702
- mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5703
- } catch {
5704
- }
5705
- if (privacyCache && privacyCache.mtimeMs === mtimeMs)
5706
- return privacyCache;
5707
- let redactToolOutputs = false, redactFilePaths2 = false;
5708
- try {
5709
- const s = loadSettings();
5710
- redactToolOutputs = !!s.privacy?.redactToolOutputs;
5711
- redactFilePaths2 = !!s.privacy?.redactFilePaths;
5712
- } catch {
5713
- }
5714
- privacyCache = { mtimeMs, redactToolOutputs, redactFilePaths: redactFilePaths2 };
5715
- return privacyCache;
5716
- }
5717
- function _resetPrivacyCache() {
5718
- privacyCache = null;
5719
- }
5720
- function isToolOutputRedactionEnabled() {
5721
- return privacyView().redactToolOutputs;
5722
- }
5723
- function isFilePathRedactionEnabled() {
5724
- return privacyView().redactFilePaths;
5725
- }
5726
- function redactFilePaths(text) {
5727
- if (!text)
5728
- return text;
5729
- return text.replace(PATH_REGEX, (m) => {
5730
- if (m.startsWith("[REDACTED:"))
5731
- return m;
5732
- const h = createHash("sha256").update(m).digest("hex").slice(0, 12);
5733
- return `[path:${h}]`;
5734
- });
5735
- }
5736
- function applyChunkPrivacy(text) {
5737
- let out = redactSecrets(text);
5738
- if (privacyView().redactFilePaths)
5739
- out = redactFilePaths(out);
5740
- return out;
5741
- }
5742
- function redactToolResultBody(toolName, body) {
5743
- if (!body)
5744
- return body;
5745
- return `[redacted: ${toolName} output, ${body.length} chars]`;
5746
- }
5747
- var DEFAULT_REDACTION_RULES, SECRET_CONTEXT_RE, HIGH_ENTROPY_TOKEN_RE, cache, CANARY, MAX_PACK_RULES, MAX_PATTERN_LEN, serverPack, globalRegexCache, privacyCache, PATH_REGEX;
5748
- var init_secret_redactor = __esm({
5749
- "../engine/dist/core/secret-redactor.js"() {
5750
- "use strict";
5751
- init_settings();
5752
- DEFAULT_REDACTION_RULES = [
5753
- // AWS access tokens. Prefix-anchored: AKIA, ASIA, ABIA (long-term,
5754
- // STS, EC2). Always exactly 16 hex/alphanumeric after the prefix.
5755
- // NOTE: this matches only the access-key-ID, NOT the secret key value —
5756
- // the `env-secret` rule below covers AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN.
5757
- { label: "aws-access-token", pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
5758
- // AWS STS session tokens. These are long base64 blobs with NO `NAME=` context
5759
- // when pasted bare into prose/code, so `env-secret` misses them — but they
5760
- // reliably start with one of a handful of base64 prefixes (the encoded
5761
- // `{"...` JSON header). Prefix-anchored → low false-positive. This is the
5762
- // bare-value form that leaked (`search('IQoJEXAMPLEfakeSESSIONtokenNOTreal01234567890abcDEF==...')`).
5763
- { label: "aws-session-token", pattern: /\b(?:IQoJ|FwoG|FwoH|FQoG|FQoH|FwoD)[A-Za-z0-9/+]{40,}={0,2}/g },
5764
- // Env/shell-assignment secrets — a VAR whose name ends in KEY/SECRET/TOKEN/
5765
- // PASSWORD/CREDENTIAL, then `=`/`:`, then the value. Zero-width lookbehind so
5766
- // only the VALUE is redacted (the var name stays for context). This is the
5767
- // dominant real-world leak vector (`export` dumps, .env files, `curl -H`,
5768
- // tool output) and is exactly what leaked AWS_SECRET_ACCESS_KEY and
5769
- // AWS_SESSION_TOKEN in cleartext while the access-key-ID rule above caught
5770
- // only the (least-sensitive) ID.
5771
- // Suffix list deliberately broad: DB_PASS (our own helm chart's name!),
5772
- // NPM_AUTH, SENTRY_DSN, SESSION/COOKIE secrets and BEARER vars all leaked
5773
- // past the original KEY/SECRET/TOKEN/PASSWORD-only list. Values under 12
5774
- // chars don't redact, so flag-ish vars (BYPASS=true) stay readable.
5775
- // Context CONSUMED, secret in group 1 — see RedactionRule.secret. As a
5776
- // lookbehind this was the second most expensive rule in the collector.
5777
- { label: "env-secret", secret: 1, pattern: /\b[A-Z][A-Z0-9_]*(?:KEY|SECRET|TOKEN|PASSWORD|PASSWD|PASS|PWD|CREDENTIAL|AUTH|DSN|SESSION|COOKIE|BEARER|SIGNATURE)S?["']?[ \t]{0,3}[:=][ \t]{0,3}["']?([A-Za-z0-9/+_.~=:@-]{12,})/g },
5778
- // Credentials embedded in a connection URL: scheme://user:PASSWORD@host.
5779
- // Redacts only the password segment (keeps scheme/user/host for context).
5780
- //
5781
- // Scheme-agnostic on purpose. The old rule allowlisted eight DB schemes, so
5782
- // `https://user:pass@host`, `ssh://`, `ftp://`, `smtp://` and every other
5783
- // userinfo URL walked through in cleartext — and a `jdbc:postgresql://…`
5784
- // prefix broke the \b anchor for the one family it did cover. Any scheme
5785
- // followed by userinfo is a credential, so match the shape, not a list.
5786
- { label: "url-password", secret: 1, pattern: /\b[a-zA-Z][a-zA-Z0-9+.-]{1,31}:\/\/[^:@\s/]{1,64}:([^@\s/]{1,256})(?=@)/g },
5787
- // Bare AWS secret-access-key VALUE: EXACTLY 40 base64 chars, word-bounded,
5788
- // containing upper+lower+digit. This is the gitleaks heuristic — the mixed-case
5789
- // requirement excludes 40-char lowercase-hex git SHAs, and the exact-40 bound
5790
- // excludes longer blobs/tokens. It catches the value when it appears bare in
5791
- // prose with NO `NAME=` and NO nearby context word (e.g. inside a markdown
5792
- // table or quoted in analysis) — the residual leak path the env-secret and
5793
- // contextual rules both miss. Placed AFTER env-secret/url-password so the
5794
- // keyed/URL forms keep their more-specific labels.
5795
- // The three stacked lookaheads each re-scanned the same 40 characters, which
5796
- // made this the MOST expensive rule of all 106 the collector runs (13.1% of a
5797
- // 29.5 MB scan). The mixed-case-plus-digit test is now one JS pass.
5798
- {
5799
- label: "aws-secret-key",
5800
- secret: 1,
5801
- pattern: /(?<![A-Za-z0-9/+])([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+])/g,
5802
- where: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) && /[0-9]/.test(v)
5803
- },
5804
- // GitHub personal access tokens.
5805
- { label: "github-pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
5806
- // GitHub fine-grained PATs.
5807
- { label: "github-fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
5808
- // GitHub OAuth.
5809
- { label: "github-oauth", pattern: /\bgho_[A-Za-z0-9]{36}\b/g },
5810
- // GitHub app + refresh.
5811
- { label: "github-app", pattern: /\b(?:ghs|ghu|ghr)_[A-Za-z0-9]{36}\b/g },
5812
- // Slack bot/user/app tokens.
5813
- { label: "slack-token", pattern: /\bxox[abopr]-[A-Za-z0-9-]{10,}\b/g },
5814
- // Slack incoming webhook URL.
5815
- { label: "slack-webhook", pattern: /\bhttps:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+\b/g },
5816
- // Stripe live + test keys.
5817
- { label: "stripe-key", pattern: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
5818
- // Anthropic.
5819
- { label: "anthropic-key", pattern: /\bsk-ant-[a-zA-Z0-9_-]{40,}\b/g },
5820
- // OpenAI. Two shapes, deliberately kept apart. The classic key is
5821
- // `sk-` + base62 with NO hyphens; the modern scoped keys carry an explicit
5822
- // proj/svcacct/admin segment first. Allowing hyphens after a bare `sk-`
5823
- // (which is what admitted sk-proj- in the first place) also matches ordinary
5824
- // kebab-case text — `sk-fix-the-login-redirect-bug`, `.sk-button-primary`,
5825
- // `sk-2024-01-01-backup` — and this corpus is full of branch names and CSS
5826
- // classes. Requiring the segment keeps sk-proj- coverage without that.
5827
- { label: "openai-key", pattern: /\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}\b|\bsk-[A-Za-z0-9]{20,}\b/g },
5828
- // Google API key (AIza…).
5829
- { label: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
5830
- // Generic JWT — three base64url segments.
5831
- { label: "jwt", pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
5832
- // Private key blocks (PEM). The label is a free word run rather than a fixed
5833
- // list, so ENCRYPTED PRIVATE KEY and PGP PRIVATE KEY BLOCK are covered
5834
- // without another enumeration, and \1 forces END to name the same kind as
5835
- // BEGIN.
5836
- //
5837
- // The body is [^-], NOT [\s\S]*?. That is a performance boundary, not a
5838
- // style choice. An unanchored prefix with an unbounded lazy gap re-scans to
5839
- // end-of-input from every candidate start, so text carrying many BEGIN
5840
- // markers and no matching END costs O(n^2): 500KB of that measured 810ms
5841
- // here, and this runs synchronously on the server's event loop over
5842
- // tenant-supplied text (services/secret-rescan.ts). A PEM body is base64
5843
- // plus headers and contains no '-', so excluding it makes the scan stop at
5844
- // the next delimiter instead of at EOF — same matches, linear cost.
5845
- { label: "private-key", pattern: /-----BEGIN ([A-Z0-9 ]{0,32})PRIVATE KEY(?: BLOCK)?-----[^-]{0,10000}?-----END \1PRIVATE KEY(?: BLOCK)?-----/g },
5846
- // PuTTY keys are not PEM. Match the Private-Lines payload — the actual key
5847
- // material — rather than spanning the whole file, which would need the same
5848
- // unbounded gap the rule above just removed. Line-anchored and base64-only,
5849
- // so it cannot run away.
5850
- { label: "putty-key", pattern: /^Private-Lines:[ \t]*\d{1,4}[\r\n]+(?:[A-Za-z0-9+/=]{8,120}[\r\n]+){1,400}/gm },
5851
- // npm tokens (uuid-shaped — overlap with our UUID filter; intentional).
5852
- { label: "npm-token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/g },
5853
- // .npmrc auth lines: //registry.npmjs.org/:_authToken=VALUE (the key is not
5854
- // ALL-CAPS, so the env-secret rule above never matched it). The leading `:`
5855
- // is required — it is what makes this npmrc syntax rather than any shell var
5856
- // ending in _AUTH, which env-secret already owns and labels better.
5857
- // [^\s"'`]+ so the closing quote is not swallowed, and a ${VAR} placeholder
5858
- // is left alone — redacting it destroys a config example and hides nothing.
5859
- // The rule the perf profile caught the wedged daemon inside: 98.94% of samples.
5860
- { label: "npmrc-auth", secret: 1, pattern: /:_(?:authToken|password|auth)[ \t]*=[ \t]*(?!\$\{)([^\s"'`]{12,})/gi },
5861
- // HTTP Authorization headers. env-secret requires an ALL-CAPS name, and
5862
- // `Authorization` is mixed case, so both Bearer and Basic walked straight
5863
- // through — including short opaque tokens the contextual pass also misses
5864
- // (it needs 32+ chars AND mixed-case-plus-digit).
5865
- // Same two guards as npmrc-auth: stop at a quote, and skip the obvious
5866
- // placeholders that fill documentation (<YOUR_TOKEN>, ${TOKEN}, xxx…).
5867
- { label: "auth-header", secret: 1, pattern: /\bauthorization[ \t]*:[ \t]*(?:bearer|basic|token)[ \t]+(?![<$])([^\s"'`<]{8,})/gi },
5868
- // Vendor tokens with unambiguous prefixes. Each is a fixed literal, so a
5869
- // false positive is essentially impossible and the cost of listing is low.
5870
- { label: "gitlab-pat", pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
5871
- { label: "slack-app-token", pattern: /\bxapp-\d-[A-Z0-9]{8,}-\d{10,}-[a-f0-9]{32,}\b/g },
5872
- { label: "sendgrid-key", pattern: /\bSG\.[A-Za-z0-9_-]{20,24}\.[A-Za-z0-9_-]{40,50}\b/g },
5873
- // (?<![-\w]) so `cache-key-<md5>` is not read as a mailgun key.
5874
- { label: "mailgun-key", pattern: /(?<![-\w])key-[a-f0-9]{32}\b/g },
5875
- { label: "digitalocean-pat", pattern: /\bdop_v1_[a-f0-9]{64}\b/g },
5876
- { label: "huggingface-token", pattern: /\bhf_[A-Za-z0-9]{30,}\b/g },
5877
- { label: "shopify-token", pattern: /\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}\b/g },
5878
- { label: "pypi-token", pattern: /\bpypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{50,}\b/g },
5879
- // No twilio-sid rule: AC…/SK… are Twilio SIDs, which are public account and
5880
- // key identifiers, not the auth token. Redacting them hid nothing and
5881
- // collided with 32-hex etags and MD5 sums.
5882
- // No discord-bot-token rule either: the shape (word.word.word) matched
5883
- // ordinary dotted symbols such as a.b.ApplicationContextProviderImpl.
5884
- { label: "telegram-bot-token", pattern: /\b\d{8,10}:AA[A-Za-z0-9_-]{32,}\b/g },
5885
- { label: "doppler-token", pattern: /\bdp\.(?:pt|st|sa|ct)\.[A-Za-z0-9_-]{20,}\b/g },
5886
- // Azure Storage SAS signature — the `sig=` parameter is the actual secret.
5887
- { label: "azure-sas", secret: 1, pattern: /[?&]sig=([A-Za-z0-9%/+=]{20,})/g }
5888
- ];
5889
- SECRET_CONTEXT_RE = /\b(?:secret|password|passwd|credential|access[ _-]?key|private[ _-]?key|api[ _-]?key|auth(?:oriz\w*)?|bearer|token)\b/gi;
5890
- HIGH_ENTROPY_TOKEN_RE = /[A-Za-z0-9/+_-]{32,}/g;
5891
- cache = null;
5892
- CANARY = "the quick brown fox jumps over the lazy dog 0123456789 commit 8ba1f109551bd432803012645ac136ddd475d0fb src/index.ts:42";
5893
- MAX_PACK_RULES = 200;
5894
- MAX_PATTERN_LEN = 512;
5895
- serverPack = null;
5896
- globalRegexCache = /* @__PURE__ */ new Map();
5897
- privacyCache = null;
5898
- PATH_REGEX = /(?<![A-Za-z0-9_])(?:\/[^\s'"`<>]+|[A-Za-z]:\\[^\s'"`<>]+)/g;
5899
- }
5900
- });
5901
-
5902
- // ../engine/dist/parsers/chunker.js
5903
- function stripInjectedBanners(text) {
5904
- let result = text;
5905
- for (const re of INJECTED_BANNERS)
5906
- result = result.replace(re, " ");
5907
- return result;
5908
- }
5909
- function cleanText(text) {
5910
- let result = text.replace(/<system-reminder>.*?<\/system-reminder>/gs, "");
5911
- result = stripInjectedBanners(result);
5912
- result = result.replace(/\x1b\[[0-9;]*m/g, "");
5913
- result = result.replace(/\s+/g, " ");
5914
- return result.trim();
5915
- }
5916
- function windowText(text, maxChars, maxWindows = 12) {
5917
- const clean = cleanText(text);
5918
- if (clean.length <= maxChars)
5919
- return clean ? [clean] : [];
5920
- const out = [];
5921
- const overlap = 80;
5922
- const step = Math.max(1, maxChars - overlap);
5923
- for (let i = 0; i < clean.length && out.length < maxWindows; i += step) {
5924
- out.push(clean.slice(i, i + maxChars));
5925
- }
5926
- return out;
5927
- }
5928
- function chunkSession(entry, content, maxChunks = 80, maxTokensPerChunk = 500) {
5929
- const chunks = [];
5930
- const maxChars = maxTokensPerChunk * 4;
5931
- const baseData = {
5932
- sessionId: entry.sessionId,
5933
- projectPath: entry.projectPath,
5934
- created: entry.created,
5935
- modified: entry.modified,
5936
- mtime: entry.fileMtime,
5937
- firstPrompt: entry.firstPrompt || content.firstPrompt,
5938
- sourceFile: content.sessionPath
5939
- };
5940
- for (let i = 0; i < Math.min(2, content.summaries.length); i++) {
5941
- const summary = content.summaries[i];
5942
- if (!summary?.trim())
5943
- continue;
5944
- const chunkText = cleanText(summary.slice(0, maxChars));
5945
- if (chunkText.length > 8e3) {
5946
- chunks.push({
5947
- chunkId: `${entry.sessionId}_summary_${i}`,
5948
- chunkType: "summary",
5949
- text: chunkText.slice(0, 8e3),
5950
- sourceLine: 0,
5951
- ...baseData
5952
- });
5953
- } else {
5954
- chunks.push({
5955
- chunkId: `${entry.sessionId}_summary_${i}`,
5956
- chunkType: "summary",
5957
- text: chunkText,
5958
- sourceLine: 0,
5959
- ...baseData
5960
- });
5961
- }
5962
- }
5963
- if (chunks.length >= maxChunks) {
5964
- return chunks.slice(0, maxChunks);
5965
- }
5966
- const firstPrompt = entry.firstPrompt || content.firstPrompt;
5967
- if (firstPrompt?.trim()) {
5968
- const firstLine = content.userMessages[0]?.lineNumber || 0;
5969
- chunks.push({
5970
- chunkId: `${entry.sessionId}_first_prompt`,
5971
- chunkType: "first_prompt",
5972
- text: cleanText(firstPrompt.slice(0, maxChars)),
5973
- sourceLine: firstLine,
5974
- ...baseData
5975
- });
5976
- }
5977
- if (chunks.length >= maxChunks) {
5978
- return chunks.slice(0, maxChunks);
5979
- }
5980
- const userStart = firstPrompt ? 1 : 0;
5981
- for (let i = userStart; i < content.userMessages.length; i++) {
5982
- if (chunks.length >= maxChunks)
5983
- break;
5984
- const msg = content.userMessages[i];
5985
- if (!msg.text.trim())
5986
- continue;
5987
- const windows = windowText(msg.text, maxChars);
5988
- for (let w = 0; w < windows.length; w++) {
5989
- if (chunks.length >= maxChunks)
5990
- break;
5991
- chunks.push({
5992
- chunkId: `${entry.sessionId}_user_${i}_${w}`,
5993
- chunkType: "user_context",
5994
- text: windows[w],
5995
- sourceLine: msg.lineNumber,
5996
- ...baseData
5997
- });
5998
- }
5999
- }
6000
- if (chunks.length >= maxChunks) {
6001
- return chunks.slice(0, maxChunks);
6002
- }
6003
- for (let i = 0; i < content.assistantMessages.length; i++) {
6004
- if (chunks.length >= maxChunks)
6005
- break;
6006
- const msg = content.assistantMessages[i];
6007
- if (!msg.text.trim() || msg.text.length <= 50)
6008
- continue;
6009
- const windows = windowText(msg.text, maxChars);
6010
- for (let w = 0; w < windows.length; w++) {
6011
- if (chunks.length >= maxChunks)
6012
- break;
6013
- chunks.push({
6014
- chunkId: `${entry.sessionId}_assistant_${i}_${w}`,
6015
- chunkType: "assistant",
6016
- text: windows[w],
6017
- sourceLine: msg.lineNumber,
6018
- ...baseData
6019
- });
6020
- }
6021
- }
6022
- if (chunks.length >= maxChunks) {
6023
- return chunks.slice(0, maxChunks);
6024
- }
6025
- if (content.toolResults.length > 0) {
6026
- const webSearches = content.toolResults.filter((r) => r.contentType === "web_search");
6027
- const otherResults = content.toolResults.filter((r) => r.contentType !== "web_search");
6028
- const resultsToProcess = [...webSearches.slice(0, 5), ...otherResults.slice(0, 10)];
6029
- const stripBodies = isToolOutputRedactionEnabled();
6030
- for (let i = 0; i < resultsToProcess.length; i++) {
6031
- if (chunks.length >= maxChunks)
6032
- break;
6033
- const result = resultsToProcess[i];
6034
- if (result.text.trim()) {
6035
- const body = stripBodies && result.contentType !== "web_search" ? redactToolResultBody(result.contentType, result.text) : result.text.slice(0, maxChars);
6036
- chunks.push({
6037
- chunkId: `${entry.sessionId}_${result.contentType}_${i}`,
6038
- chunkType: result.contentType,
6039
- text: cleanText(body),
6040
- sourceLine: result.lineNumber,
6041
- ...baseData
6042
- });
6043
- }
6044
- }
6045
- }
6046
- return chunks.slice(0, maxChunks);
6047
- }
6048
- var INJECTED_BANNERS;
6049
- var init_chunker = __esm({
6050
- "../engine/dist/parsers/chunker.js"() {
6051
- "use strict";
6052
- init_secret_redactor();
6053
- INJECTED_BANNERS = [
6054
- /MCP issues detected\. ?Run \/mcp list for status\.?/g,
6055
- /Context low[^\n]*Run \/compact[^\n]*/g,
6056
- /API Error:[^\n]{0,120}/g
6057
- ];
6058
- }
6059
- });
6060
-
6061
- // ../engine/dist/core/project-dir-name.js
6062
- import { readdirSync as readdirSync4 } from "node:fs";
6063
- function looksWindowsEncoded(dirName) {
6064
- return /^[A-Za-z]--/.test(dirName);
6065
- }
6066
- function decodeProjectDirName(dirName) {
6067
- if (!dirName)
6068
- return "";
6069
- if (looksWindowsEncoded(dirName)) {
6070
- const drive = dirName[0].toUpperCase();
6071
- const rest = dirName.slice(3).replace(/-/g, "\\");
6072
- return `${drive}:\\${rest}`;
6073
- }
6074
- return "/" + dirName.replace(/^-/, "").replace(/-/g, "/");
6075
- }
6076
- function resolveProjectDirName(dirName, deps = {}) {
6077
- const structural = decodeProjectDirName(dirName);
6078
- if (!dirName)
6079
- return structural;
6080
- if (looksWindowsEncoded(dirName))
6081
- return structural;
6082
- const readdir3 = deps.readdir ?? ((p) => readdirSync4(p, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name));
6083
- const encodeName = (name) => name.replace(/[-_.]/g, "-");
6084
- let remaining = dirName.replace(/^-/, "");
6085
- let current = "/";
6086
- while (remaining) {
6087
- let children;
6088
- try {
6089
- children = readdir3(current);
6090
- } catch {
6091
- break;
6092
- }
6093
- let best = "";
6094
- for (const child of children) {
6095
- const encoded = encodeName(child);
6096
- if (!encoded || encoded.length < best.length)
6097
- continue;
6098
- if (!remaining.startsWith(encoded))
6099
- continue;
6100
- const next = remaining[encoded.length];
6101
- if (next !== void 0 && next !== "-")
6102
- continue;
6103
- if (encoded.length > best.length)
6104
- best = child;
6105
- }
6106
- if (!best)
6107
- break;
6108
- current = current === "/" ? `/${best}` : `${current}/${best}`;
6109
- remaining = remaining.slice(encodeName(best).length).replace(/^-/, "");
6110
- }
6111
- return remaining ? structural : current;
6112
- }
6113
- var init_project_dir_name = __esm({
6114
- "../engine/dist/core/project-dir-name.js"() {
6115
- "use strict";
6116
- }
6117
- });
6118
-
6119
- // ../engine/dist/core/model-pricing.js
6120
- function pricingFor(modelId) {
6121
- if (!modelId)
6122
- return null;
6123
- const lower = modelId.toLowerCase();
6124
- let best = null;
6125
- let bestLen = -1;
6126
- for (const [prefix, entry] of Object.entries(TABLE)) {
6127
- if (lower.startsWith(prefix.toLowerCase()) && prefix.length > bestLen) {
6128
- best = entry;
6129
- bestLen = prefix.length;
6130
- }
6131
- }
6132
- return best;
6133
- }
6134
- function estimateCostUsdOrNull(modelsUsed, tokens) {
6135
- let best = null;
6136
- let bestRate = -1;
6137
- for (const m of modelsUsed) {
6138
- if (!m || m === "<synthetic>")
6139
- continue;
6140
- const p = pricingFor(m);
6141
- if (p && p.output > bestRate) {
6142
- best = p;
6143
- bestRate = p.output;
6144
- }
6145
- }
6146
- if (!best)
6147
- return null;
6148
- const nonCachedInput = Math.max(0, tokens.inputTokens - tokens.cacheReadTokens - tokens.cacheCreationTokens);
6149
- const cost = nonCachedInput / 1e6 * best.input + tokens.outputTokens / 1e6 * best.output + tokens.cacheReadTokens / 1e6 * best.cacheRead + tokens.cacheCreationTokens / 1e6 * best.cacheCreate;
6150
- return Math.round(cost * 100) / 100;
6151
- }
6152
- function estimateCostUsd(modelsUsed, tokens) {
6153
- return estimateCostUsdOrNull(modelsUsed, tokens) ?? 0;
5552
+ function estimateCostUsd(modelsUsed, tokens) {
5553
+ return estimateCostUsdOrNull(modelsUsed, tokens) ?? 0;
6154
5554
  }
6155
5555
  var TABLE, METADATA_VERSION;
6156
5556
  var init_model_pricing = __esm({
@@ -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,87 +5734,15 @@ 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"));
6378
5740
  }
6379
5741
  function getSubagentPaths(sessionPath) {
6380
5742
  const subagentsDir = join5(sessionPath.slice(0, -6), "subagents");
6381
- if (!existsSync5(subagentsDir))
6382
- return [];
6383
- return readdirSync5(subagentsDir).filter((f) => f.endsWith(".jsonl")).sort().map((f) => join5(subagentsDir, f));
6384
- }
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;
5743
+ if (!existsSync5(subagentsDir))
5744
+ return [];
5745
+ return readdirSync5(subagentsDir).filter((f) => f.endsWith(".jsonl")).sort().map((f) => join5(subagentsDir, f));
6418
5746
  }
6419
5747
  function readCwdFromJsonl(sessionPath) {
6420
5748
  try {
@@ -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");
@@ -7821,6 +7067,28 @@ var init_session_git = __esm({
7821
7067
  }
7822
7068
  });
7823
7069
 
7070
+ // ../engine/dist/core/session-multi-tool.js
7071
+ function extractTurnsAny(sessionId, opts = {}) {
7072
+ const backend = getBackendForId(sessionId);
7073
+ if (!backend)
7074
+ return { sessionId, found: false, turns: [], startMs: 0, endMs: 0 };
7075
+ return memoInSessionScope(`turns:${sessionId}:${opts.maxTurns ?? ""}:${opts.assistantMax ?? ""}`, () => backend.extractTurns(sessionId, opts));
7076
+ }
7077
+ function replaySessionAny(sessionId) {
7078
+ const backend = getBackendForId(sessionId);
7079
+ if (!backend)
7080
+ return { sessionId, found: false, projectPath: "", files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
7081
+ return memoInSessionScope(`replay:${sessionId}`, () => backend.replay(sessionId));
7082
+ }
7083
+ var init_session_multi_tool = __esm({
7084
+ "../engine/dist/core/session-multi-tool.js"() {
7085
+ "use strict";
7086
+ init_tool_backend();
7087
+ init_live_session_scan();
7088
+ init_backends();
7089
+ }
7090
+ });
7091
+
7824
7092
  // ../engine/dist/core/session-outcome.js
7825
7093
  function extractDecisionsFromTurn(turn) {
7826
7094
  if (turn.kind !== "assistant_text" || !turn.text)
@@ -7917,7 +7185,7 @@ function computeOutcome(sessionId, opts = {}) {
7917
7185
  };
7918
7186
  if (!backend)
7919
7187
  return notFound;
7920
- const turnsRes = backend.extractTurns(sessionId, { assistantMax: 2e3 });
7188
+ const turnsRes = extractTurnsAny(sessionId, { assistantMax: 2e3 });
7921
7189
  if (!turnsRes.found)
7922
7190
  return notFound;
7923
7191
  const decisions = [];
@@ -7956,7 +7224,7 @@ function computeOutcome(sessionId, opts = {}) {
7956
7224
  totalLinesAdded = fast.totalLinesAdded;
7957
7225
  totalLinesRemoved = fast.totalLinesRemoved;
7958
7226
  } else {
7959
- const replay = backend.replay(sessionId);
7227
+ const replay = replaySessionAny(sessionId);
7960
7228
  filesChanged = replay.files.map((f) => f.file);
7961
7229
  totalLinesAdded = replay.totalLinesAdded;
7962
7230
  totalLinesRemoved = replay.totalLinesRemoved;
@@ -7995,6 +7263,7 @@ var init_session_outcome = __esm({
7995
7263
  init_session_sentiment();
7996
7264
  init_session_git();
7997
7265
  init_tool_backend();
7266
+ init_session_multi_tool();
7998
7267
  init_backends();
7999
7268
  DECISION_PATTERNS = [
8000
7269
  /\b(?:I'?ll|I will|I'?m going to|Going to|Let me|We'?ll|We will|We should|Let'?s|I'?m about to|I'?m going to)\b[^\n.!?]*[.!?]/i,
@@ -8823,11 +8092,11 @@ var init_opencode = __esm({
8823
8092
  });
8824
8093
 
8825
8094
  // ../engine/dist/core/backends/tail-read.js
8826
- import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2, statSync as statSync5 } from "node:fs";
8095
+ import { openSync as openSync2, readSync as readSync2, closeSync as closeSync2, statSync as statSync4 } from "node:fs";
8827
8096
  function readTailFromOffset(path, offset) {
8828
8097
  let size = 0;
8829
8098
  try {
8830
- size = statSync5(path).size;
8099
+ size = statSync4(path).size;
8831
8100
  } catch {
8832
8101
  return { text: "", newOffset: offset };
8833
8102
  }
@@ -8872,7 +8141,7 @@ var init_tail_read = __esm({
8872
8141
  });
8873
8142
 
8874
8143
  // ../engine/dist/core/backends/codex.js
8875
- import { readdirSync as readdirSync6, readFileSync as readFileSync3, statSync as statSync6, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
8144
+ import { readdirSync as readdirSync6, readFileSync as readFileSync3, statSync as statSync5, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
8876
8145
  import { basename as basename7, dirname as dirname9, join as join9 } from "path";
8877
8146
  function parseShellWrites(cmd) {
8878
8147
  const out = [];
@@ -9080,7 +8349,7 @@ var init_codex2 = __esm({
9080
8349
  return null;
9081
8350
  let mtime = 0;
9082
8351
  try {
9083
- mtime = statSync6(located.path).mtimeMs;
8352
+ mtime = statSync5(located.path).mtimeMs;
9084
8353
  } catch {
9085
8354
  }
9086
8355
  return {
@@ -9100,7 +8369,7 @@ var init_codex2 = __esm({
9100
8369
  for (const filePath of walkRollouts(root)) {
9101
8370
  let stat3;
9102
8371
  try {
9103
- stat3 = statSync6(filePath);
8372
+ stat3 = statSync5(filePath);
9104
8373
  } catch {
9105
8374
  continue;
9106
8375
  }
@@ -9296,7 +8565,7 @@ var init_codex2 = __esm({
9296
8565
  let mtime = 0;
9297
8566
  try {
9298
8567
  raw = readFileSync3(path, "utf-8");
9299
- mtime = statSync6(path).mtimeMs;
8568
+ mtime = statSync5(path).mtimeMs;
9300
8569
  } catch {
9301
8570
  return [];
9302
8571
  }
@@ -9396,7 +8665,7 @@ var init_codex2 = __esm({
9396
8665
  const events = this.readEvents(this.toRawId(id));
9397
8666
  let fileMtime = 0;
9398
8667
  try {
9399
- fileMtime = statSync6(located.path).mtimeMs;
8668
+ fileMtime = statSync5(located.path).mtimeMs;
9400
8669
  } catch {
9401
8670
  }
9402
8671
  return liveScanEditsFromEvents(events, this.fileToolMap, {
@@ -9460,7 +8729,7 @@ var init_codex2 = __esm({
9460
8729
  const p = join9(dir, entry.name);
9461
8730
  let st;
9462
8731
  try {
9463
- st = statSync6(p);
8732
+ st = statSync5(p);
9464
8733
  } catch {
9465
8734
  continue;
9466
8735
  }
@@ -9527,7 +8796,7 @@ var init_codex2 = __esm({
9527
8796
  let mtime = 0;
9528
8797
  const push = (path, name) => {
9529
8798
  try {
9530
- const st = statSync6(path);
8799
+ const st = statSync5(path);
9531
8800
  files.push({ name, bytes: readFileSync3(path) });
9532
8801
  if (st.mtimeMs > mtime)
9533
8802
  mtime = st.mtimeMs;
@@ -9569,7 +8838,7 @@ var init_codex2 = __esm({
9569
8838
  if (!loc)
9570
8839
  return 0;
9571
8840
  try {
9572
- return statSync6(loc.path).size;
8841
+ return statSync5(loc.path).size;
9573
8842
  } catch {
9574
8843
  return 0;
9575
8844
  }
@@ -9586,7 +8855,7 @@ var init_codex2 = __esm({
9586
8855
  });
9587
8856
 
9588
8857
  // ../engine/dist/core/backends/agy.js
9589
- import { existsSync as existsSync8, readdirSync as readdirSync7, readFileSync as readFileSync4, statSync as statSync7 } from "fs";
8858
+ import { existsSync as existsSync8, readdirSync as readdirSync7, readFileSync as readFileSync4, statSync as statSync6 } from "fs";
9590
8859
  import { basename as basename8, dirname as dirname10, join as join10 } from "path";
9591
8860
  function agyTranscriptPath(logsDir) {
9592
8861
  const full = join10(logsDir, "transcript_full.jsonl");
@@ -9779,7 +9048,7 @@ var init_agy = __esm({
9779
9048
  return null;
9780
9049
  let mtime = 0;
9781
9050
  try {
9782
- mtime = statSync7(path).mtimeMs;
9051
+ mtime = statSync6(path).mtimeMs;
9783
9052
  } catch {
9784
9053
  }
9785
9054
  const projectPath = resolveAgyProject(dirname10(path), this.homeDir());
@@ -9807,7 +9076,7 @@ var init_agy = __esm({
9807
9076
  for (const rawId of sessionDirs) {
9808
9077
  const sessionPath = join10(brainDir, rawId);
9809
9078
  try {
9810
- if (!statSync7(sessionPath).isDirectory())
9079
+ if (!statSync6(sessionPath).isDirectory())
9811
9080
  continue;
9812
9081
  } catch {
9813
9082
  continue;
@@ -9817,7 +9086,7 @@ var init_agy = __esm({
9817
9086
  continue;
9818
9087
  let stat3;
9819
9088
  try {
9820
- stat3 = statSync7(filePath);
9089
+ stat3 = statSync6(filePath);
9821
9090
  } catch {
9822
9091
  continue;
9823
9092
  }
@@ -10082,7 +9351,7 @@ var init_agy = __esm({
10082
9351
  for (const rawId of sessionDirs) {
10083
9352
  const sessionPath = join10(brainDir, rawId);
10084
9353
  try {
10085
- if (!statSync7(sessionPath).isDirectory())
9354
+ if (!statSync6(sessionPath).isDirectory())
10086
9355
  continue;
10087
9356
  } catch {
10088
9357
  continue;
@@ -10091,7 +9360,7 @@ var init_agy = __esm({
10091
9360
  if (!filePath)
10092
9361
  continue;
10093
9362
  try {
10094
- const st = statSync7(filePath);
9363
+ const st = statSync6(filePath);
10095
9364
  if (st.mtimeMs < opts.sinceMs)
10096
9365
  continue;
10097
9366
  candidates2.push({ rawId, mtime: st.mtimeMs });
@@ -10119,7 +9388,7 @@ var init_agy = __esm({
10119
9388
  try {
10120
9389
  return {
10121
9390
  tool: "agy",
10122
- mtime: statSync7(loc.path).mtimeMs,
9391
+ mtime: statSync6(loc.path).mtimeMs,
10123
9392
  files: [{ name: basename8(loc.path), bytes: readFileSync4(loc.path) }]
10124
9393
  };
10125
9394
  } catch {
@@ -10134,7 +9403,7 @@ var init_agy = __esm({
10134
9403
  if (!loc)
10135
9404
  return 0;
10136
9405
  try {
10137
- return statSync7(loc.path).size;
9406
+ return statSync6(loc.path).size;
10138
9407
  } catch {
10139
9408
  return 0;
10140
9409
  }
@@ -10151,7 +9420,7 @@ var init_agy = __esm({
10151
9420
  });
10152
9421
 
10153
9422
  // ../engine/dist/core/backends/cursor-store.js
10154
- import { existsSync as existsSync9, readdirSync as readdirSync8, readFileSync as readFileSync5, statSync as statSync8 } from "fs";
9423
+ import { existsSync as existsSync9, readdirSync as readdirSync8, readFileSync as readFileSync5, statSync as statSync7 } from "fs";
10155
9424
  import { join as join11 } from "path";
10156
9425
  function cursorProjectSlug(absPath) {
10157
9426
  return absPath.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -10168,7 +9437,7 @@ function listChatsIn(chatsRoot) {
10168
9437
  const hashPath3 = join11(chatsRoot, workspaceHash);
10169
9438
  let chatIds;
10170
9439
  try {
10171
- if (!statSync8(hashPath3).isDirectory())
9440
+ if (!statSync7(hashPath3).isDirectory())
10172
9441
  continue;
10173
9442
  chatIds = readdirSync8(hashPath3);
10174
9443
  } catch {
@@ -10190,7 +9459,7 @@ function listChatsIn(chatsRoot) {
10190
9459
  let created = meta.createdAtMs ?? 0;
10191
9460
  if (!mtime || !created) {
10192
9461
  try {
10193
- const st = statSync8(dbPath);
9462
+ const st = statSync7(dbPath);
10194
9463
  mtime = mtime || st.mtimeMs;
10195
9464
  created = created || st.birthtimeMs || st.mtimeMs;
10196
9465
  } catch {
@@ -10233,7 +9502,7 @@ function findChat(chatsRoots, chatId) {
10233
9502
  let created = meta.createdAtMs ?? 0;
10234
9503
  if (!mtime || !created) {
10235
9504
  try {
10236
- const st = statSync8(dbPath);
9505
+ const st = statSync7(dbPath);
10237
9506
  mtime = mtime || st.mtimeMs;
10238
9507
  created = created || st.birthtimeMs || st.mtimeMs;
10239
9508
  } catch {
@@ -10536,7 +9805,7 @@ var init_cursor_store = __esm({
10536
9805
  });
10537
9806
 
10538
9807
  // ../engine/dist/core/backends/cursor-ide.js
10539
- import { existsSync as existsSync10, readFileSync as readFileSync6, readdirSync as readdirSync9, statSync as statSync9 } from "fs";
9808
+ import { existsSync as existsSync10, readFileSync as readFileSync6, readdirSync as readdirSync9, statSync as statSync8 } from "fs";
10540
9809
  import { dirname as dirname11, join as join12 } from "path";
10541
9810
  function jsonOf(value) {
10542
9811
  if (value == null)
@@ -10606,7 +9875,7 @@ function workspaceIndex(workspaceRoots) {
10606
9875
  for (const id of entries) {
10607
9876
  const dir = join12(root, id);
10608
9877
  try {
10609
- if (!statSync9(dir).isDirectory())
9878
+ if (!statSync8(dir).isDirectory())
10610
9879
  continue;
10611
9880
  } catch {
10612
9881
  continue;
@@ -10952,7 +10221,7 @@ var init_cursor_ide = __esm({
10952
10221
  });
10953
10222
 
10954
10223
  // ../engine/dist/core/backends/cursor.js
10955
- import { existsSync as existsSync11, readFileSync as readFileSync7, statSync as statSync10 } from "fs";
10224
+ import { existsSync as existsSync11, readFileSync as readFileSync7, statSync as statSync9 } from "fs";
10956
10225
  import { join as join13 } from "path";
10957
10226
  var PREFIX4, CursorBackend, cursorBackend;
10958
10227
  var init_cursor = __esm({
@@ -11005,7 +10274,7 @@ var init_cursor = __esm({
11005
10274
  if (r.surface === "cli") {
11006
10275
  let mtime2 = r.chat.updatedAtMs;
11007
10276
  try {
11008
- mtime2 = statSync10(r.chat.dbPath).mtimeMs || mtime2;
10277
+ mtime2 = statSync9(r.chat.dbPath).mtimeMs || mtime2;
11009
10278
  } catch {
11010
10279
  }
11011
10280
  return {
@@ -11018,7 +10287,7 @@ var init_cursor = __esm({
11018
10287
  }
11019
10288
  let mtime = r.composer.updatedAtMs;
11020
10289
  try {
11021
- mtime = statSync10(r.composer.globalDbPath).mtimeMs || mtime;
10290
+ mtime = statSync9(r.composer.globalDbPath).mtimeMs || mtime;
11022
10291
  } catch {
11023
10292
  }
11024
10293
  return {
@@ -11264,7 +10533,7 @@ var init_cursor = __esm({
11264
10533
  if (existsSync11(metaPath)) {
11265
10534
  files.push({ name: "meta.json", bytes: readFileSync7(metaPath) });
11266
10535
  }
11267
- return { tool: "cursor", mtime: statSync10(r.chat.dbPath).mtimeMs, files };
10536
+ return { tool: "cursor", mtime: statSync9(r.chat.dbPath).mtimeMs, files };
11268
10537
  }
11269
10538
  const events = readComposerEvents(r.composer);
11270
10539
  if (events.length === 0)
@@ -11272,7 +10541,7 @@ var init_cursor = __esm({
11272
10541
  const body = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
11273
10542
  return {
11274
10543
  tool: "cursor",
11275
- mtime: statSync10(r.composer.globalDbPath).mtimeMs,
10544
+ mtime: statSync9(r.composer.globalDbPath).mtimeMs,
11276
10545
  files: [{ name: `${r.composer.composerId}.jsonl`, bytes: Buffer.from(body, "utf8") }]
11277
10546
  };
11278
10547
  } catch {
@@ -11338,8 +10607,8 @@ var init_backends = __esm({
11338
10607
 
11339
10608
  // ../engine/dist/transcript/shadow.js
11340
10609
  import { gzipSync as gzipSync2, gunzipSync as gunzipSync2 } from "zlib";
11341
- import { createHash as createHash3 } from "crypto";
11342
- import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync2, renameSync as renameSync2, statSync as statSync11 } from "fs";
10610
+ import { createHash as createHash2 } from "crypto";
10611
+ import { existsSync as existsSync12, mkdirSync as mkdirSync3, readFileSync as readFileSync8, writeFileSync as writeFileSync2, renameSync as renameSync2, statSync as statSync10 } from "fs";
11343
10612
  import { dirname as dirname12, join as join14 } from "path";
11344
10613
  function shadowRoot() {
11345
10614
  return join14(getDataDir(), "shadow");
@@ -11369,7 +10638,7 @@ function writeShadowContainer(tool, rawId, container) {
11369
10638
  renameSync2(tmp, path);
11370
10639
  }
11371
10640
  function sha1(s) {
11372
- return createHash3("sha1").update(s).digest("hex");
10641
+ return createHash2("sha1").update(s).digest("hex");
11373
10642
  }
11374
10643
  function lineKey(line) {
11375
10644
  try {
@@ -11547,7 +10816,7 @@ function liveFingerprint(backend, rawId) {
11547
10816
  const loc = backend.findSession(rawId);
11548
10817
  if (!loc?.path)
11549
10818
  return "";
11550
- const st = statSync11(loc.path);
10819
+ const st = statSync10(loc.path);
11551
10820
  return `${st.mtimeMs}:${st.size}`;
11552
10821
  } catch {
11553
10822
  return "";
@@ -11565,7 +10834,7 @@ function seedShadow(sessionId, container) {
11565
10834
  }
11566
10835
  function shadowSizeOnDisk(tool, rawId) {
11567
10836
  try {
11568
- return statSync11(shadowFileFor(tool, rawId)).size;
10837
+ return statSync10(shadowFileFor(tool, rawId)).size;
11569
10838
  } catch {
11570
10839
  return 0;
11571
10840
  }
@@ -11583,7 +10852,7 @@ var init_shadow = __esm({
11583
10852
  });
11584
10853
 
11585
10854
  // ../engine/dist/core/live-session-scan.js
11586
- import { existsSync as existsSync13, readdirSync as readdirSync10, readFileSync as readFileSync9, statSync as statSync12, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
10855
+ import { existsSync as existsSync13, readdirSync as readdirSync10, readFileSync as readFileSync9, statSync as statSync11, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
11587
10856
  import { join as join15, basename as basename9 } from "path";
11588
10857
  function detectTool(sessionId) {
11589
10858
  return getBackendForId(sessionId)?.id ?? "claude";
@@ -11675,20 +10944,33 @@ function resolveSessionContentGroups(sessionId) {
11675
10944
  }
11676
10945
  function withSessionReadCache(fn) {
11677
10946
  readScopeDepth++;
10947
+ if (readScopeDepth === 1)
10948
+ derivedScope = /* @__PURE__ */ new Map();
11678
10949
  try {
11679
10950
  return fn();
11680
10951
  } finally {
11681
- if (--readScopeDepth === 0)
10952
+ if (--readScopeDepth === 0) {
11682
10953
  readScope = null;
10954
+ derivedScope = null;
10955
+ }
11683
10956
  }
11684
10957
  }
10958
+ function memoInSessionScope(key2, compute) {
10959
+ if (!derivedScope)
10960
+ return compute();
10961
+ if (derivedScope.has(key2))
10962
+ return derivedScope.get(key2);
10963
+ const value = compute();
10964
+ derivedScope.set(key2, value);
10965
+ return value;
10966
+ }
11685
10967
  function readSessionGroupText(group) {
11686
10968
  let key2 = "";
11687
10969
  if (readScopeDepth > 0) {
11688
10970
  const parts = [];
11689
10971
  for (const p of group.paths) {
11690
10972
  try {
11691
- const st = statSync12(p);
10973
+ const st = statSync11(p);
11692
10974
  parts.push(`${p}:${st.mtimeMs}:${st.size}`);
11693
10975
  } catch {
11694
10976
  parts.push(`${p}:missing`);
@@ -11708,7 +10990,7 @@ function readSessionGroupText(group) {
11708
10990
  continue;
11709
10991
  }
11710
10992
  try {
11711
- mtime = Math.max(mtime, statSync12(p).mtimeMs);
10993
+ mtime = Math.max(mtime, statSync11(p).mtimeMs);
11712
10994
  } catch {
11713
10995
  }
11714
10996
  text = text ? mergeLineText(text, raw).text : raw;
@@ -11793,7 +11075,7 @@ function findCodexSessionFileIn(root, sessionId) {
11793
11075
  }
11794
11076
  return scanDir(root);
11795
11077
  }
11796
- var scanScopeDepth, indexCache, readScope, readScopeDepth, geminiCache, MAX_EVENT_LINE_BYTES;
11078
+ var scanScopeDepth, indexCache, readScope, readScopeDepth, derivedScope, geminiCache, MAX_EVENT_LINE_BYTES;
11797
11079
  var init_live_session_scan = __esm({
11798
11080
  "../engine/dist/core/live-session-scan.js"() {
11799
11081
  "use strict";
@@ -11807,6 +11089,7 @@ var init_live_session_scan = __esm({
11807
11089
  indexCache = null;
11808
11090
  readScope = null;
11809
11091
  readScopeDepth = 0;
11092
+ derivedScope = null;
11810
11093
  geminiCache = null;
11811
11094
  MAX_EVENT_LINE_BYTES = 8 * 1024 * 1024;
11812
11095
  }
@@ -11924,7 +11207,7 @@ var init_first_prompt = __esm({
11924
11207
  });
11925
11208
 
11926
11209
  // ../engine/dist/core/backends/claude.js
11927
- import { existsSync as existsSync15, readdirSync as readdirSync11, readFileSync as readFileSync10, statSync as statSync13, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
11210
+ import { existsSync as existsSync15, readdirSync as readdirSync11, readFileSync as readFileSync10, statSync as statSync12, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
11928
11211
  import { join as join16, basename as basename10 } from "path";
11929
11212
  function stripReminders(text) {
11930
11213
  return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
@@ -11937,6 +11220,7 @@ var UUID_RE, ClaudeBackend, claudeBackend;
11937
11220
  var init_claude2 = __esm({
11938
11221
  "../engine/dist/core/backends/claude.js"() {
11939
11222
  "use strict";
11223
+ init_live_session_scan();
11940
11224
  init_tool_paths();
11941
11225
  init_live_session_scan();
11942
11226
  init_session_outcome();
@@ -12053,7 +11337,7 @@ var init_claude2 = __esm({
12053
11337
  let mtime = 0;
12054
11338
  for (const c of copies) {
12055
11339
  try {
12056
- mtime = Math.max(mtime, statSync13(c.path).mtimeMs);
11340
+ mtime = Math.max(mtime, statSync12(c.path).mtimeMs);
12057
11341
  } catch {
12058
11342
  }
12059
11343
  }
@@ -12129,7 +11413,7 @@ var init_claude2 = __esm({
12129
11413
  const fullPath = join16(projPath, f);
12130
11414
  let stat3;
12131
11415
  try {
12132
- stat3 = statSync13(fullPath);
11416
+ stat3 = statSync12(fullPath);
12133
11417
  } catch {
12134
11418
  continue;
12135
11419
  }
@@ -12199,6 +11483,9 @@ var init_claude2 = __esm({
12199
11483
  * splits show up alongside the parent.
12200
11484
  */
12201
11485
  readEvents(rawId) {
11486
+ return memoInSessionScope(`events:claude:${rawId}`, () => this.#readEventsUncached(rawId));
11487
+ }
11488
+ #readEventsUncached(rawId) {
12202
11489
  const groups = resolveSessionContentGroups(rawId);
12203
11490
  if (groups.length === 0)
12204
11491
  return [];
@@ -12347,7 +11634,7 @@ var init_claude2 = __esm({
12347
11634
  const events = this.readEvents(rawId);
12348
11635
  let fileMtime = 0;
12349
11636
  try {
12350
- fileMtime = statSync13(located.path).mtimeMs;
11637
+ fileMtime = statSync12(located.path).mtimeMs;
12351
11638
  } catch {
12352
11639
  }
12353
11640
  return liveScanEditsFromEvents(events, this.fileToolMap, {
@@ -12402,13 +11689,13 @@ var init_claude2 = __esm({
12402
11689
  continue;
12403
11690
  const fp = join16(projPath, f);
12404
11691
  try {
12405
- const st = statSync13(fp);
11692
+ const st = statSync12(fp);
12406
11693
  let mtime = st.mtimeMs;
12407
11694
  const subDir = join16(projPath, f.slice(0, -6), "subagents");
12408
11695
  if (existsSync15(subDir)) {
12409
11696
  for (const sub of readdirSync11(subDir)) {
12410
11697
  try {
12411
- const subSt = statSync13(join16(subDir, sub));
11698
+ const subSt = statSync12(join16(subDir, sub));
12412
11699
  if (subSt.mtimeMs > mtime)
12413
11700
  mtime = subSt.mtimeMs;
12414
11701
  } catch {
@@ -12473,7 +11760,7 @@ var init_claude2 = __esm({
12473
11760
  let total = 0;
12474
11761
  for (const located of findSessionFiles(this.toRawId(prefixedId))) {
12475
11762
  try {
12476
- total += statSync13(located.path).size;
11763
+ total += statSync12(located.path).size;
12477
11764
  } catch {
12478
11765
  }
12479
11766
  }
@@ -12495,417 +11782,877 @@ var init_claude2 = __esm({
12495
11782
  spansMultipleSources(prefixedId) {
12496
11783
  return findSessionFiles(this.toRawId(prefixedId)).length > 1;
12497
11784
  }
12498
- async readFromOffset(prefixedId, offset) {
12499
- const rawId = this.toRawId(prefixedId);
12500
- const copies = findSessionFiles(rawId);
12501
- if (copies.length === 0)
12502
- return { text: "", newOffset: offset };
12503
- if (copies.length > 1)
12504
- return { text: "", newOffset: offset };
12505
- return readTailFromOffset(copies[0].path, offset);
11785
+ async readFromOffset(prefixedId, offset) {
11786
+ const rawId = this.toRawId(prefixedId);
11787
+ const copies = findSessionFiles(rawId);
11788
+ if (copies.length === 0)
11789
+ return { text: "", newOffset: offset };
11790
+ if (copies.length > 1)
11791
+ return { text: "", newOffset: offset };
11792
+ return readTailFromOffset(copies[0].path, offset);
11793
+ }
11794
+ };
11795
+ claudeBackend = new ClaudeBackend();
11796
+ }
11797
+ });
11798
+
11799
+ // ../engine/dist/core/project-resolver.js
11800
+ var project_resolver_exports = {};
11801
+ __export(project_resolver_exports, {
11802
+ _setProjectsConfigForTests: () => _setProjectsConfigForTests,
11803
+ getProjectsConfigPath: () => getProjectsConfigPath,
11804
+ globMatch: () => globMatch,
11805
+ loadProjectsConfig: () => loadProjectsConfig,
11806
+ parseGitRemote: () => parseGitRemote,
11807
+ resetProjectResolverCache: () => resetProjectResolverCache,
11808
+ resolveProjectId: () => resolveProjectId,
11809
+ resolveWorkspaceId: () => resolveWorkspaceId
11810
+ });
11811
+ import { execSync } from "child_process";
11812
+ import { createHash as createHash3 } from "crypto";
11813
+ import { existsSync as existsSync16, readFileSync as readFileSync11, realpathSync as realpathSync2, readdirSync as readdirSync12, statSync as statSync13 } from "fs";
11814
+ import { basename as basename11, dirname as dirname13, isAbsolute, join as join17, resolve, sep } from "path";
11815
+ function getProjectsConfigPath() {
11816
+ return join17(getDataDir(), "projects.json");
11817
+ }
11818
+ function loadProjectsConfig() {
11819
+ if (cachedConfig && cachedConfig.mtime === Number.MAX_SAFE_INTEGER) {
11820
+ return cachedConfig.cfg;
11821
+ }
11822
+ const path = getProjectsConfigPath();
11823
+ if (!existsSync16(path)) {
11824
+ cachedConfig = { mtime: 0, cfg: {} };
11825
+ return cachedConfig.cfg;
11826
+ }
11827
+ const mtime = statSync13(path).mtimeMs;
11828
+ if (cachedConfig && cachedConfig.mtime === mtime)
11829
+ return cachedConfig.cfg;
11830
+ try {
11831
+ const raw = readFileSync11(path, "utf-8");
11832
+ const cfg = JSON.parse(raw);
11833
+ cachedConfig = { mtime, cfg };
11834
+ return cfg;
11835
+ } catch {
11836
+ cachedConfig = { mtime, cfg: {} };
11837
+ return cachedConfig.cfg;
11838
+ }
11839
+ }
11840
+ function resetProjectResolverCache() {
11841
+ resolveCache.clear();
11842
+ gitCache.clear();
11843
+ workspaceCache.clear();
11844
+ wsIdCache.clear();
11845
+ cachedConfig = null;
11846
+ }
11847
+ function _setProjectsConfigForTests(cfg) {
11848
+ resetProjectResolverCache();
11849
+ cachedConfig = cfg === null ? null : { mtime: Number.MAX_SAFE_INTEGER, cfg };
11850
+ }
11851
+ function resolveProjectId(inputPath) {
11852
+ if (!inputPath) {
11853
+ return { id: "", displayName: "", source: "path" };
11854
+ }
11855
+ if (!isAbsolute(inputPath)) {
11856
+ return { id: `path:${inputPath}`, displayName: inputPath, source: "path" };
11857
+ }
11858
+ const realPath = safeRealpath(inputPath);
11859
+ const cached = resolveCache.get(realPath);
11860
+ if (cached)
11861
+ return cached;
11862
+ const result = doResolve(realPath);
11863
+ resolveCache.set(realPath, result);
11864
+ return result;
11865
+ }
11866
+ function resolveWorkspaceId(inputPath) {
11867
+ if (!inputPath || !isAbsolute(inputPath))
11868
+ return null;
11869
+ const realPath = safeRealpath(inputPath);
11870
+ const cached = wsIdCache.get(realPath);
11871
+ if (cached !== void 0)
11872
+ return cached;
11873
+ const result = doResolveWorkspace(realPath);
11874
+ wsIdCache.set(realPath, result);
11875
+ return result;
11876
+ }
11877
+ function doResolveWorkspace(realPath) {
11878
+ const cfg = loadProjectsConfig();
11879
+ const declaredWorkspaces = (cfg.projects || []).filter((p) => p.workspace).sort((a, b) => b.root.length - a.root.length);
11880
+ for (const proj of declaredWorkspaces) {
11881
+ if (isInside(realPath, safeRealpath(proj.root)))
11882
+ return proj.id;
11883
+ }
11884
+ const anchor = findGitToplevel(realPath) ?? realPath;
11885
+ const ws = resolveAutoWorkspace(anchor, cfg.autoWorkspaceMinRepos ?? DEFAULT_AUTO_WORKSPACE_MIN_REPOS);
11886
+ return ws ? ws.id : null;
11887
+ }
11888
+ function doResolve(realPath) {
11889
+ const cfg = loadProjectsConfig();
11890
+ if (matchesIgnore(realPath, cfg)) {
11891
+ return { id: `ignored:${realPath}`, displayName: basename11(realPath), source: "ignored" };
11892
+ }
11893
+ const declared = matchDeclaredProject(realPath, cfg.projects || []);
11894
+ if (declared)
11895
+ return declared;
11896
+ const bot = resolveBotWorktree(realPath);
11897
+ if (bot)
11898
+ return bot;
11899
+ const git2 = resolveGit(realPath);
11900
+ if (git2)
11901
+ return git2;
11902
+ const ws = resolveAutoWorkspace(realPath, cfg.autoWorkspaceMinRepos ?? DEFAULT_AUTO_WORKSPACE_MIN_REPOS);
11903
+ if (ws)
11904
+ return ws;
11905
+ return {
11906
+ id: `path:${realPath}`,
11907
+ displayName: basename11(realPath) || realPath,
11908
+ source: "path"
11909
+ };
11910
+ }
11911
+ function matchesIgnore(realPath, cfg) {
11912
+ const rules = cfg.ignore || [];
11913
+ for (const r of rules) {
11914
+ if (globMatch(r.match, realPath))
11915
+ return true;
11916
+ }
11917
+ return false;
11918
+ }
11919
+ function resolveBotWorktree(realPath) {
11920
+ const parts = realPath.split(sep);
11921
+ const idx = parts.findIndex((p) => BOT_WORKTREE_MARKERS.includes(p));
11922
+ if (idx === -1)
11923
+ return null;
11924
+ const botRoot = parts.slice(0, idx + 1).join(sep);
11925
+ return {
11926
+ id: `path:${botRoot}`,
11927
+ displayName: parts[idx],
11928
+ source: "path"
11929
+ };
11930
+ }
11931
+ function matchDeclaredProject(realPath, projects) {
11932
+ const candidates2 = [...projects].sort((a, b) => b.root.length - a.root.length);
11933
+ for (const proj of candidates2) {
11934
+ const root = safeRealpath(proj.root);
11935
+ if (!isInside(realPath, root))
11936
+ continue;
11937
+ if (!proj.workspace && proj.children?.length) {
11938
+ const child = matchChild(realPath, root, proj.children);
11939
+ if (child) {
11940
+ return {
11941
+ id: `${proj.id}/${child.id}`,
11942
+ displayName: child.name || child.id,
11943
+ source: "user",
11944
+ workspaceId: proj.id
11945
+ };
11946
+ }
11947
+ }
11948
+ return {
11949
+ id: proj.id,
11950
+ displayName: proj.name || proj.id,
11951
+ source: "user",
11952
+ workspaceId: proj.workspace ? void 0 : void 0
11953
+ };
11954
+ }
11955
+ return null;
11956
+ }
11957
+ function matchChild(realPath, parentRoot, children) {
11958
+ const sorted = [...children].sort((a, b) => b.path.length - a.path.length);
11959
+ for (const c of sorted) {
11960
+ const childRoot = safeRealpath(join17(parentRoot, c.path));
11961
+ if (isInside(realPath, childRoot))
11962
+ return c;
11963
+ }
11964
+ return null;
11965
+ }
11966
+ function resolveGit(realPath) {
11967
+ const toplevel = findGitToplevel(realPath);
11968
+ if (!toplevel)
11969
+ return null;
11970
+ const probe = getGitRemote(toplevel);
11971
+ if (probe.kind === "remote") {
11972
+ const parsed = parseGitRemote(probe.url);
11973
+ if (parsed) {
11974
+ return {
11975
+ id: `git:${parsed.host}/${parsed.owner}/${parsed.repo}`,
11976
+ displayName: parsed.repo,
11977
+ source: "git-remote"
11978
+ };
11979
+ }
11980
+ }
11981
+ if (probe.kind === "failed") {
11982
+ 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}`);
11983
+ }
11984
+ const hash = createHash3("sha1").update(toplevel).digest("hex").slice(0, 12);
11985
+ return {
11986
+ id: `git-local:${hash}`,
11987
+ displayName: basename11(toplevel),
11988
+ source: "git-local"
11989
+ };
11990
+ }
11991
+ function findGitToplevel(realPath) {
11992
+ let cur = realPath;
11993
+ let last = "";
11994
+ while (cur && cur !== last) {
11995
+ if (existsSync16(join17(cur, ".git")))
11996
+ return cur;
11997
+ last = cur;
11998
+ cur = dirname13(cur);
11999
+ }
12000
+ return null;
12001
+ }
12002
+ function git(toplevel, args) {
12003
+ return execSync(`git ${args}`, {
12004
+ cwd: toplevel,
12005
+ stdio: ["ignore", "pipe", "ignore"],
12006
+ encoding: "utf-8",
12007
+ timeout: GIT_TIMEOUT_MS
12008
+ });
12009
+ }
12010
+ function probeGitRemote(toplevel) {
12011
+ let names;
12012
+ try {
12013
+ names = git(toplevel, "remote").split("\n").map((n) => n.trim()).filter(Boolean);
12014
+ } catch (err) {
12015
+ return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
12016
+ }
12017
+ if (names.length === 0)
12018
+ return { kind: "none" };
12019
+ const pick = names.includes("origin") ? "origin" : names[0];
12020
+ try {
12021
+ const url = git(toplevel, `remote get-url ${pick}`).trim();
12022
+ return url ? { kind: "remote", url } : { kind: "none" };
12023
+ } catch (err) {
12024
+ return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
12025
+ }
12026
+ }
12027
+ function getGitRemote(toplevel) {
12028
+ const cached = gitCache.get(toplevel);
12029
+ if (cached)
12030
+ return cached;
12031
+ const probe = probeGitRemote(toplevel);
12032
+ if (probe.kind !== "failed")
12033
+ gitCache.set(toplevel, probe);
12034
+ return probe;
12035
+ }
12036
+ function parseGitRemote(url) {
12037
+ const trimmed = url.trim().replace(/\.git$/, "");
12038
+ if (!trimmed)
12039
+ return null;
12040
+ const ssh = /^[^@]+@([^:]+):(.+?)\/([^/]+)$/.exec(trimmed);
12041
+ if (ssh)
12042
+ return { host: normaliseHost(ssh[1]), owner: ssh[2], repo: ssh[3] };
12043
+ const url1 = /^[a-z+]+:\/\/(?:[^@]+@)?([^/:]+)(?::\d+)?\/(.+?)\/([^/]+)$/.exec(trimmed);
12044
+ if (url1)
12045
+ return { host: normaliseHost(url1[1]), owner: url1[2], repo: url1[3] };
12046
+ return null;
12047
+ }
12048
+ function normaliseHost(host) {
12049
+ for (const known of ["github.com", "gitlab.com", "bitbucket.org", "codeberg.org", "gitea.com"]) {
12050
+ if (host === known)
12051
+ return known;
12052
+ if (host.startsWith(known + "-") || host.startsWith(known + "."))
12053
+ return known;
12054
+ }
12055
+ return host;
12056
+ }
12057
+ function resolveAutoWorkspace(realPath, minRepos) {
12058
+ let cur = realPath;
12059
+ let last = "";
12060
+ let depth = 0;
12061
+ while (cur && cur !== last && depth < AUTO_WORKSPACE_MAX_DEPTH) {
12062
+ if (!existsSync16(join17(cur, ".git"))) {
12063
+ const cached = workspaceCache.get(cur);
12064
+ let qualifies;
12065
+ if (cached === void 0) {
12066
+ qualifies = countGitChildren(cur) >= minRepos;
12067
+ workspaceCache.set(cur, qualifies ? cur : null);
12068
+ } else {
12069
+ qualifies = cached !== null;
12070
+ }
12071
+ if (qualifies) {
12072
+ const name = basename11(cur);
12073
+ return { id: `ws:${name}`, displayName: name, source: "auto-workspace" };
12506
12074
  }
12507
- };
12508
- claudeBackend = new ClaudeBackend();
12075
+ }
12076
+ last = cur;
12077
+ cur = dirname13(cur);
12078
+ depth++;
12509
12079
  }
12510
- });
12511
-
12512
- // ../engine/dist/core/project-resolver.js
12513
- var project_resolver_exports = {};
12514
- __export(project_resolver_exports, {
12515
- _setProjectsConfigForTests: () => _setProjectsConfigForTests,
12516
- getProjectsConfigPath: () => getProjectsConfigPath,
12517
- globMatch: () => globMatch,
12518
- loadProjectsConfig: () => loadProjectsConfig,
12519
- parseGitRemote: () => parseGitRemote,
12520
- resetProjectResolverCache: () => resetProjectResolverCache,
12521
- resolveProjectId: () => resolveProjectId,
12522
- resolveWorkspaceId: () => resolveWorkspaceId
12523
- });
12524
- import { execSync } from "child_process";
12525
- import { createHash as createHash4 } from "crypto";
12526
- import { existsSync as existsSync16, readFileSync as readFileSync11, realpathSync as realpathSync2, readdirSync as readdirSync12, statSync as statSync14 } from "fs";
12527
- import { basename as basename11, dirname as dirname13, isAbsolute, join as join17, resolve, sep } from "path";
12528
- function getProjectsConfigPath() {
12529
- return join17(getDataDir(), "projects.json");
12080
+ return null;
12530
12081
  }
12531
- function loadProjectsConfig() {
12532
- if (cachedConfig && cachedConfig.mtime === Number.MAX_SAFE_INTEGER) {
12533
- return cachedConfig.cfg;
12082
+ function countGitChildren(dir) {
12083
+ let count = 0;
12084
+ let entries;
12085
+ try {
12086
+ entries = readdirSync12(dir);
12087
+ } catch {
12088
+ return 0;
12534
12089
  }
12535
- const path = getProjectsConfigPath();
12536
- if (!existsSync16(path)) {
12537
- cachedConfig = { mtime: 0, cfg: {} };
12538
- return cachedConfig.cfg;
12090
+ for (const name of entries) {
12091
+ if (name.startsWith("."))
12092
+ continue;
12093
+ const child = join17(dir, name);
12094
+ try {
12095
+ const st = statSync13(child);
12096
+ if (!st.isDirectory())
12097
+ continue;
12098
+ if (existsSync16(join17(child, ".git")))
12099
+ count++;
12100
+ } catch {
12101
+ }
12539
12102
  }
12540
- const mtime = statSync14(path).mtimeMs;
12541
- if (cachedConfig && cachedConfig.mtime === mtime)
12542
- return cachedConfig.cfg;
12103
+ return count;
12104
+ }
12105
+ function safeRealpath(p) {
12543
12106
  try {
12544
- const raw = readFileSync11(path, "utf-8");
12545
- const cfg = JSON.parse(raw);
12546
- cachedConfig = { mtime, cfg };
12547
- return cfg;
12107
+ return realpathSync2(p);
12548
12108
  } catch {
12549
- cachedConfig = { mtime, cfg: {} };
12550
- return cachedConfig.cfg;
12109
+ return resolve(p);
12551
12110
  }
12552
12111
  }
12553
- function resetProjectResolverCache() {
12554
- resolveCache.clear();
12555
- gitCache.clear();
12556
- workspaceCache.clear();
12557
- wsIdCache.clear();
12558
- cachedConfig = null;
12112
+ function isInside(child, parent) {
12113
+ if (!parent)
12114
+ return false;
12115
+ if (child === parent)
12116
+ return true;
12117
+ const c = child.endsWith(sep) ? child : child + sep;
12118
+ const p = parent.endsWith(sep) ? parent : parent + sep;
12119
+ return c.startsWith(p);
12559
12120
  }
12560
- function _setProjectsConfigForTests(cfg) {
12561
- resetProjectResolverCache();
12562
- cachedConfig = cfg === null ? null : { mtime: Number.MAX_SAFE_INTEGER, cfg };
12121
+ function globMatch(pattern, path) {
12122
+ const re = new RegExp("^" + globToRegex(toPosix(pattern)) + "$");
12123
+ return re.test(toPosix(path));
12563
12124
  }
12564
- function resolveProjectId(inputPath) {
12565
- if (!inputPath) {
12566
- return { id: "", displayName: "", source: "path" };
12567
- }
12568
- if (!isAbsolute(inputPath)) {
12569
- return { id: `path:${inputPath}`, displayName: inputPath, source: "path" };
12125
+ function toPosix(p) {
12126
+ return p.replace(/\\/g, "/");
12127
+ }
12128
+ function globToRegex(glob) {
12129
+ let out = "";
12130
+ let i = 0;
12131
+ while (i < glob.length) {
12132
+ const c = glob[i];
12133
+ if (c === "*" && glob[i + 1] === "*") {
12134
+ out += ".*";
12135
+ i += 2;
12136
+ if (glob[i] === "/")
12137
+ i++;
12138
+ } else if (c === "*") {
12139
+ out += "[^/]*";
12140
+ i++;
12141
+ } else if (c === "?") {
12142
+ out += "[^/]";
12143
+ i++;
12144
+ } else if (c === "{") {
12145
+ const end = glob.indexOf("}", i);
12146
+ if (end === -1) {
12147
+ out += "\\{";
12148
+ i++;
12149
+ continue;
12150
+ }
12151
+ const opts = glob.slice(i + 1, end).split(",").map(escapeRe).join("|");
12152
+ out += "(?:" + opts + ")";
12153
+ i = end + 1;
12154
+ } else {
12155
+ out += escapeRe(c);
12156
+ i++;
12157
+ }
12570
12158
  }
12571
- const realPath = safeRealpath(inputPath);
12572
- const cached = resolveCache.get(realPath);
12573
- if (cached)
12574
- return cached;
12575
- const result = doResolve(realPath);
12576
- resolveCache.set(realPath, result);
12577
- return result;
12159
+ return out;
12578
12160
  }
12579
- function resolveWorkspaceId(inputPath) {
12580
- if (!inputPath || !isAbsolute(inputPath))
12161
+ function escapeRe(s) {
12162
+ return s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
12163
+ }
12164
+ var DEFAULT_AUTO_WORKSPACE_MIN_REPOS, BOT_WORKTREE_MARKERS, resolveCache, gitCache, workspaceCache, wsIdCache, cachedConfig, GIT_TIMEOUT_MS, AUTO_WORKSPACE_MAX_DEPTH;
12165
+ var init_project_resolver = __esm({
12166
+ "../engine/dist/core/project-resolver.js"() {
12167
+ "use strict";
12168
+ init_paths();
12169
+ DEFAULT_AUTO_WORKSPACE_MIN_REPOS = 3;
12170
+ BOT_WORKTREE_MARKERS = [".claude-pr-bot"];
12171
+ resolveCache = /* @__PURE__ */ new Map();
12172
+ gitCache = /* @__PURE__ */ new Map();
12173
+ workspaceCache = /* @__PURE__ */ new Map();
12174
+ wsIdCache = /* @__PURE__ */ new Map();
12175
+ cachedConfig = null;
12176
+ GIT_TIMEOUT_MS = 1e4;
12177
+ AUTO_WORKSPACE_MAX_DEPTH = 2;
12178
+ }
12179
+ });
12180
+
12181
+ // ../engine/dist/core/credentials-env.js
12182
+ function envCredentials(env = process.env) {
12183
+ const token = (env.CHAT_RECALL_TOKEN || "").trim();
12184
+ const server = (env.CHAT_RECALL_SERVER || "").trim();
12185
+ if (!token || !server)
12581
12186
  return null;
12582
- const realPath = safeRealpath(inputPath);
12583
- const cached = wsIdCache.get(realPath);
12584
- if (cached !== void 0)
12585
- return cached;
12586
- const result = doResolveWorkspace(realPath);
12587
- wsIdCache.set(realPath, result);
12588
- return result;
12187
+ if (!/^https?:\/\//i.test(server))
12188
+ return null;
12189
+ return { serverUrl: normalise(server), token };
12589
12190
  }
12590
- function doResolveWorkspace(realPath) {
12591
- const cfg = loadProjectsConfig();
12592
- const declaredWorkspaces = (cfg.projects || []).filter((p) => p.workspace).sort((a, b) => b.root.length - a.root.length);
12593
- for (const proj of declaredWorkspaces) {
12594
- if (isInside(realPath, safeRealpath(proj.root)))
12595
- return proj.id;
12191
+ var normalise;
12192
+ var init_credentials_env = __esm({
12193
+ "../engine/dist/core/credentials-env.js"() {
12194
+ "use strict";
12195
+ normalise = (u) => u.trim().replace(/\/+$/, "");
12596
12196
  }
12597
- const anchor = findGitToplevel(realPath) ?? realPath;
12598
- const ws = resolveAutoWorkspace(anchor, cfg.autoWorkspaceMinRepos ?? DEFAULT_AUTO_WORKSPACE_MIN_REPOS);
12599
- return ws ? ws.id : null;
12197
+ });
12198
+
12199
+ // ../engine/dist/core/secret-redactor.js
12200
+ var secret_redactor_exports = {};
12201
+ __export(secret_redactor_exports, {
12202
+ DEFAULT_REDACTION_RULES: () => DEFAULT_REDACTION_RULES,
12203
+ _clearServerRulePack: () => _clearServerRulePack,
12204
+ _resetPrivacyCache: () => _resetPrivacyCache,
12205
+ _resetRedactorCache: () => _resetRedactorCache,
12206
+ applyChunkPrivacy: () => applyChunkPrivacy,
12207
+ findContextualSecrets: () => findContextualSecrets,
12208
+ installServerRulePack: () => installServerRulePack,
12209
+ isFilePathRedactionEnabled: () => isFilePathRedactionEnabled,
12210
+ isRedactionEnabled: () => isRedactionEnabled,
12211
+ isToolOutputRedactionEnabled: () => isToolOutputRedactionEnabled,
12212
+ redactFilePaths: () => redactFilePaths,
12213
+ redactSecrets: () => redactSecrets,
12214
+ redactToolResultBody: () => redactToolResultBody,
12215
+ scanTextForFindings: () => scanTextForFindings,
12216
+ serverRulePackVersion: () => serverRulePackVersion,
12217
+ validateRedactionRule: () => validateRedactionRule
12218
+ });
12219
+ import { statSync as statSync14 } from "fs";
12220
+ import { createHash as createHash4 } from "crypto";
12221
+ function secretOf(rule, m) {
12222
+ const raw = rule.secret != null ? m[rule.secret] : m[0];
12223
+ if (raw == null)
12224
+ return null;
12225
+ if (rule.where && !rule.where(raw))
12226
+ return null;
12227
+ return raw;
12600
12228
  }
12601
- function doResolve(realPath) {
12602
- const cfg = loadProjectsConfig();
12603
- if (matchesIgnore(realPath, cfg)) {
12604
- return { id: `ignored:${realPath}`, displayName: basename11(realPath), source: "ignored" };
12605
- }
12606
- const declared = matchDeclaredProject(realPath, cfg.projects || []);
12607
- if (declared)
12608
- return declared;
12609
- const bot = resolveBotWorktree(realPath);
12610
- if (bot)
12611
- return bot;
12612
- const git2 = resolveGit(realPath);
12613
- if (git2)
12614
- return git2;
12615
- const ws = resolveAutoWorkspace(realPath, cfg.autoWorkspaceMinRepos ?? DEFAULT_AUTO_WORKSPACE_MIN_REPOS);
12616
- if (ws)
12617
- return ws;
12618
- return {
12619
- id: `path:${realPath}`,
12620
- displayName: basename11(realPath) || realPath,
12621
- source: "path"
12622
- };
12229
+ function looksLikeSecretToken(t) {
12230
+ if (t.length < 32)
12231
+ return false;
12232
+ if (/^[a-f0-9]+$/i.test(t))
12233
+ return true;
12234
+ return /[A-Z]/.test(t) && /[a-z]/.test(t) && /[0-9]/.test(t);
12623
12235
  }
12624
- function matchesIgnore(realPath, cfg) {
12625
- const rules = cfg.ignore || [];
12626
- for (const r of rules) {
12627
- if (globMatch(r.match, realPath))
12628
- return true;
12236
+ function findContextualSecrets(text) {
12237
+ if (!text)
12238
+ return [];
12239
+ const ranges = [];
12240
+ let m;
12241
+ SECRET_CONTEXT_RE.lastIndex = 0;
12242
+ while ((m = SECRET_CONTEXT_RE.exec(text)) !== null) {
12243
+ const winStart = m.index + m[0].length;
12244
+ const window = text.slice(winStart, winStart + 80);
12245
+ HIGH_ENTROPY_TOKEN_RE.lastIndex = 0;
12246
+ let tm;
12247
+ while ((tm = HIGH_ENTROPY_TOKEN_RE.exec(window)) !== null) {
12248
+ if (!looksLikeSecretToken(tm[0]))
12249
+ continue;
12250
+ 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))) {
12251
+ continue;
12252
+ }
12253
+ ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
12254
+ break;
12255
+ }
12629
12256
  }
12630
- return false;
12257
+ ranges.sort((a, b) => a.start - b.start);
12258
+ const out = [];
12259
+ for (const r of ranges) {
12260
+ const last = out[out.length - 1];
12261
+ if (last && r.start < last.end) {
12262
+ last.end = Math.max(last.end, r.end);
12263
+ continue;
12264
+ }
12265
+ out.push({ ...r });
12266
+ }
12267
+ return out;
12631
12268
  }
12632
- function resolveBotWorktree(realPath) {
12633
- const parts = realPath.split(sep);
12634
- const idx = parts.findIndex((p) => BOT_WORKTREE_MARKERS.includes(p));
12635
- if (idx === -1)
12269
+ function isEnvRedactionRequested() {
12270
+ const v = process.env.CHAT_RECALL_REDACT_INDEX;
12271
+ if (v === void 0)
12636
12272
  return null;
12637
- const botRoot = parts.slice(0, idx + 1).join(sep);
12638
- return {
12639
- id: `path:${botRoot}`,
12640
- displayName: parts[idx],
12641
- source: "path"
12642
- };
12273
+ return v === "1" || v === "true" || v === "yes";
12643
12274
  }
12644
- function matchDeclaredProject(realPath, projects) {
12645
- const candidates2 = [...projects].sort((a, b) => b.root.length - a.root.length);
12646
- for (const proj of candidates2) {
12647
- const root = safeRealpath(proj.root);
12648
- if (!isInside(realPath, root))
12649
- continue;
12650
- if (!proj.workspace && proj.children?.length) {
12651
- const child = matchChild(realPath, root, proj.children);
12652
- if (child) {
12653
- return {
12654
- id: `${proj.id}/${child.id}`,
12655
- displayName: child.name || child.id,
12656
- source: "user",
12657
- workspaceId: proj.id
12658
- };
12275
+ function settingsView() {
12276
+ let mtimeMs = -1;
12277
+ try {
12278
+ mtimeMs = statSync14(settingsFilePath()).mtimeMs;
12279
+ } catch {
12280
+ }
12281
+ if (cache && cache.mtimeMs === mtimeMs)
12282
+ return cache;
12283
+ let enabled = false;
12284
+ const extra = [];
12285
+ try {
12286
+ const s = loadSettings();
12287
+ enabled = !!s.privacy?.redactIndex;
12288
+ for (const r of s.privacy?.redactionRules ?? []) {
12289
+ try {
12290
+ extra.push({ label: r.label, pattern: new RegExp(r.pattern, "g") });
12291
+ } catch {
12659
12292
  }
12660
12293
  }
12661
- return {
12662
- id: proj.id,
12663
- displayName: proj.name || proj.id,
12664
- source: "user",
12665
- workspaceId: proj.workspace ? void 0 : void 0
12666
- };
12294
+ } catch {
12667
12295
  }
12668
- return null;
12296
+ cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
12297
+ return cache;
12669
12298
  }
12670
- function matchChild(realPath, parentRoot, children) {
12671
- const sorted = [...children].sort((a, b) => b.path.length - a.path.length);
12672
- for (const c of sorted) {
12673
- const childRoot = safeRealpath(join17(parentRoot, c.path));
12674
- if (isInside(realPath, childRoot))
12675
- return c;
12299
+ function compileServerRule(spec) {
12300
+ const name = spec.name || "(unnamed)";
12301
+ if (!spec.regex)
12302
+ return { name, reason: "empty pattern" };
12303
+ if (spec.regex.length > MAX_PATTERN_LEN)
12304
+ return { name, reason: `pattern longer than ${MAX_PATTERN_LEN} chars` };
12305
+ let re;
12306
+ try {
12307
+ const flags = "g" + (spec.flags || "").replace(/[^ims]/g, "");
12308
+ re = new RegExp(spec.regex, flags);
12309
+ } catch (e) {
12310
+ return { name, reason: `invalid regex: ${e instanceof Error ? e.message : e}` };
12676
12311
  }
12677
- return null;
12312
+ if (re.test(""))
12313
+ return { name, reason: "matches the empty string (would redact everything)" };
12314
+ re.lastIndex = 0;
12315
+ if (re.test(CANARY))
12316
+ return { name, reason: "matches benign text \u2014 too broad" };
12317
+ re.lastIndex = 0;
12318
+ return { label: `${spec.source === "pack" ? "pack" : "tenant"}:${name}`, pattern: re };
12678
12319
  }
12679
- function resolveGit(realPath) {
12680
- const toplevel = findGitToplevel(realPath);
12681
- if (!toplevel)
12682
- return null;
12683
- const probe = getGitRemote(toplevel);
12684
- if (probe.kind === "remote") {
12685
- const parsed = parseGitRemote(probe.url);
12686
- if (parsed) {
12687
- return {
12688
- id: `git:${parsed.host}/${parsed.owner}/${parsed.repo}`,
12689
- displayName: parsed.repo,
12690
- source: "git-remote"
12691
- };
12320
+ function validateRedactionRule(spec) {
12321
+ const out = compileServerRule(spec);
12322
+ return "reason" in out ? { ok: false, reason: out.reason } : { ok: true };
12323
+ }
12324
+ function installServerRulePack(pack) {
12325
+ const rejected = [];
12326
+ const rules = [];
12327
+ for (const spec of pack.rules || []) {
12328
+ if (rules.length >= MAX_PACK_RULES) {
12329
+ rejected.push({ name: spec.name || "(unnamed)", reason: `pack exceeds ${MAX_PACK_RULES} rules` });
12330
+ continue;
12692
12331
  }
12332
+ const out = compileServerRule(spec);
12333
+ if ("reason" in out)
12334
+ rejected.push(out);
12335
+ else
12336
+ rules.push(out);
12693
12337
  }
12694
- if (probe.kind === "failed") {
12695
- 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}`);
12696
- }
12697
- const hash = createHash4("sha1").update(toplevel).digest("hex").slice(0, 12);
12698
- return {
12699
- id: `git-local:${hash}`,
12700
- displayName: basename11(toplevel),
12701
- source: "git-local"
12702
- };
12338
+ const version2 = pack.version || "";
12339
+ serverPack = { version: version2, rules };
12340
+ return { version: version2, accepted: rules.length, rejected };
12703
12341
  }
12704
- function findGitToplevel(realPath) {
12705
- let cur = realPath;
12706
- let last = "";
12707
- while (cur && cur !== last) {
12708
- if (existsSync16(join17(cur, ".git")))
12709
- return cur;
12710
- last = cur;
12711
- cur = dirname13(cur);
12712
- }
12713
- return null;
12342
+ function serverRulePackVersion() {
12343
+ return serverPack ? serverPack.version || "unversioned" : null;
12714
12344
  }
12715
- function git(toplevel, args) {
12716
- return execSync(`git ${args}`, {
12717
- cwd: toplevel,
12718
- stdio: ["ignore", "pipe", "ignore"],
12719
- encoding: "utf-8",
12720
- timeout: GIT_TIMEOUT_MS
12721
- });
12345
+ function _clearServerRulePack() {
12346
+ serverPack = null;
12722
12347
  }
12723
- function probeGitRemote(toplevel) {
12724
- let names;
12725
- try {
12726
- names = git(toplevel, "remote").split("\n").map((n) => n.trim()).filter(Boolean);
12727
- } catch (err) {
12728
- return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
12348
+ function activeRules() {
12349
+ const base3 = settingsView().rules;
12350
+ return serverPack && serverPack.rules.length > 0 ? [...base3, ...serverPack.rules] : base3;
12351
+ }
12352
+ function isRedactionEnabled() {
12353
+ const env = isEnvRedactionRequested();
12354
+ if (env !== null)
12355
+ return env;
12356
+ return settingsView().enabled;
12357
+ }
12358
+ function _resetRedactorCache() {
12359
+ cache = null;
12360
+ }
12361
+ function redactSecrets(text, opts = {}) {
12362
+ if (!opts.force && !isRedactionEnabled())
12363
+ return text;
12364
+ if (!text)
12365
+ return text;
12366
+ const rules = opts.rules || activeRules();
12367
+ let out = text;
12368
+ for (const r of rules) {
12369
+ out = out.replace(r.pattern, (...args) => {
12370
+ const whole = args[0];
12371
+ const m = args.slice(0, args.length);
12372
+ const secret = secretOf(r, m);
12373
+ if (secret == null)
12374
+ return whole;
12375
+ if (opts.count)
12376
+ opts.count.redactions++;
12377
+ return whole.slice(0, whole.length - secret.length) + `[REDACTED:${r.label}]`;
12378
+ });
12729
12379
  }
12730
- if (names.length === 0)
12731
- return { kind: "none" };
12732
- const pick = names.includes("origin") ? "origin" : names[0];
12733
- try {
12734
- const url = git(toplevel, `remote get-url ${pick}`).trim();
12735
- return url ? { kind: "remote", url } : { kind: "none" };
12736
- } catch (err) {
12737
- return { kind: "failed", reason: err instanceof Error ? err.message : String(err) };
12380
+ const ctx = findContextualSecrets(out);
12381
+ for (let i = ctx.length - 1; i >= 0; i--) {
12382
+ const { start, end } = ctx[i];
12383
+ if (opts.count)
12384
+ opts.count.redactions++;
12385
+ out = out.slice(0, start) + "[REDACTED:secret-context]" + out.slice(end);
12738
12386
  }
12387
+ return out;
12739
12388
  }
12740
- function getGitRemote(toplevel) {
12741
- const cached = gitCache.get(toplevel);
12742
- if (cached)
12743
- return cached;
12744
- const probe = probeGitRemote(toplevel);
12745
- if (probe.kind !== "failed")
12746
- gitCache.set(toplevel, probe);
12747
- return probe;
12748
- }
12749
- function parseGitRemote(url) {
12750
- const trimmed = url.trim().replace(/\.git$/, "");
12751
- if (!trimmed)
12752
- return null;
12753
- const ssh = /^[^@]+@([^:]+):(.+?)\/([^/]+)$/.exec(trimmed);
12754
- if (ssh)
12755
- return { host: normaliseHost(ssh[1]), owner: ssh[2], repo: ssh[3] };
12756
- const url1 = /^[a-z+]+:\/\/(?:[^@]+@)?([^/:]+)(?::\d+)?\/(.+?)\/([^/]+)$/.exec(trimmed);
12757
- if (url1)
12758
- return { host: normaliseHost(url1[1]), owner: url1[2], repo: url1[3] };
12759
- return null;
12389
+ function maskSecret(s) {
12390
+ if (!s)
12391
+ return "";
12392
+ if (s.length <= 4)
12393
+ return "*".repeat(s.length);
12394
+ return "*".repeat(Math.min(s.length - 4, 60)) + s.slice(-4);
12760
12395
  }
12761
- function normaliseHost(host) {
12762
- for (const known of ["github.com", "gitlab.com", "bitbucket.org", "codeberg.org", "gitea.com"]) {
12763
- if (host === known)
12764
- return known;
12765
- if (host.startsWith(known + "-") || host.startsWith(known + "."))
12766
- return known;
12396
+ function globalRegexFor(pattern) {
12397
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
12398
+ const key2 = pattern.source + "\0" + flags;
12399
+ let re = globalRegexCache.get(key2);
12400
+ if (!re) {
12401
+ re = new RegExp(pattern.source, flags);
12402
+ globalRegexCache.set(key2, re);
12767
12403
  }
12768
- return host;
12404
+ return re;
12769
12405
  }
12770
- function resolveAutoWorkspace(realPath, minRepos) {
12771
- let cur = realPath;
12772
- let last = "";
12773
- let depth = 0;
12774
- while (cur && cur !== last && depth < AUTO_WORKSPACE_MAX_DEPTH) {
12775
- if (!existsSync16(join17(cur, ".git"))) {
12776
- const cached = workspaceCache.get(cur);
12777
- let qualifies;
12778
- if (cached === void 0) {
12779
- qualifies = countGitChildren(cur) >= minRepos;
12780
- workspaceCache.set(cur, qualifies ? cur : null);
12781
- } else {
12782
- qualifies = cached !== null;
12406
+ function makeLineResolver(text) {
12407
+ const starts = [];
12408
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1))
12409
+ starts.push(i);
12410
+ return (index) => {
12411
+ let lo = 0, hi = starts.length;
12412
+ while (lo < hi) {
12413
+ const mid = lo + hi >> 1;
12414
+ if (starts[mid] < index)
12415
+ lo = mid + 1;
12416
+ else
12417
+ hi = mid;
12418
+ }
12419
+ return lo + 1;
12420
+ };
12421
+ }
12422
+ function scanTextForFindings(text, opts = {}) {
12423
+ if (!text)
12424
+ return [];
12425
+ const rules = opts.rules || activeRules();
12426
+ const findings = [];
12427
+ const seenSpans = /* @__PURE__ */ new Set();
12428
+ const lineAt = makeLineResolver(text);
12429
+ for (const r of rules) {
12430
+ const re = globalRegexFor(r.pattern);
12431
+ re.lastIndex = 0;
12432
+ let m;
12433
+ while ((m = re.exec(text)) !== null) {
12434
+ const secret = secretOf(r, m);
12435
+ if (secret == null) {
12436
+ if (re.lastIndex === m.index)
12437
+ re.lastIndex++;
12438
+ continue;
12783
12439
  }
12784
- if (qualifies) {
12785
- const name = basename11(cur);
12786
- return { id: `ws:${name}`, displayName: name, source: "auto-workspace" };
12440
+ const at = m.index + (m[0].length - secret.length);
12441
+ const span = `${at}:${secret.length}`;
12442
+ if (!seenSpans.has(span)) {
12443
+ seenSpans.add(span);
12444
+ findings.push({ rule: r.label, line: lineAt(at), preview: maskSecret(secret) });
12787
12445
  }
12446
+ if (re.lastIndex === m.index)
12447
+ re.lastIndex++;
12788
12448
  }
12789
- last = cur;
12790
- cur = dirname13(cur);
12791
- depth++;
12792
12449
  }
12793
- return null;
12450
+ for (const { start, end } of findContextualSecrets(text)) {
12451
+ const line = lineAt(start);
12452
+ findings.push({ rule: "secret-context", line, preview: maskSecret(text.slice(start, end)) });
12453
+ }
12454
+ return findings;
12794
12455
  }
12795
- function countGitChildren(dir) {
12796
- let count = 0;
12797
- let entries;
12456
+ function privacyView() {
12457
+ let mtimeMs = -1;
12798
12458
  try {
12799
- entries = readdirSync12(dir);
12459
+ mtimeMs = statSync14(settingsFilePath()).mtimeMs;
12800
12460
  } catch {
12801
- return 0;
12802
- }
12803
- for (const name of entries) {
12804
- if (name.startsWith("."))
12805
- continue;
12806
- const child = join17(dir, name);
12807
- try {
12808
- const st = statSync14(child);
12809
- if (!st.isDirectory())
12810
- continue;
12811
- if (existsSync16(join17(child, ".git")))
12812
- count++;
12813
- } catch {
12814
- }
12815
12461
  }
12816
- return count;
12817
- }
12818
- function safeRealpath(p) {
12462
+ if (privacyCache && privacyCache.mtimeMs === mtimeMs)
12463
+ return privacyCache;
12464
+ let redactToolOutputs = false, redactFilePaths2 = false;
12819
12465
  try {
12820
- return realpathSync2(p);
12466
+ const s = loadSettings();
12467
+ redactToolOutputs = !!s.privacy?.redactToolOutputs;
12468
+ redactFilePaths2 = !!s.privacy?.redactFilePaths;
12821
12469
  } catch {
12822
- return resolve(p);
12823
12470
  }
12471
+ privacyCache = { mtimeMs, redactToolOutputs, redactFilePaths: redactFilePaths2 };
12472
+ return privacyCache;
12824
12473
  }
12825
- function isInside(child, parent) {
12826
- if (!parent)
12827
- return false;
12828
- if (child === parent)
12829
- return true;
12830
- const c = child.endsWith(sep) ? child : child + sep;
12831
- const p = parent.endsWith(sep) ? parent : parent + sep;
12832
- return c.startsWith(p);
12474
+ function _resetPrivacyCache() {
12475
+ privacyCache = null;
12833
12476
  }
12834
- function globMatch(pattern, path) {
12835
- const re = new RegExp("^" + globToRegex(toPosix(pattern)) + "$");
12836
- return re.test(toPosix(path));
12477
+ function isToolOutputRedactionEnabled() {
12478
+ return privacyView().redactToolOutputs;
12837
12479
  }
12838
- function toPosix(p) {
12839
- return p.replace(/\\/g, "/");
12480
+ function isFilePathRedactionEnabled() {
12481
+ return privacyView().redactFilePaths;
12840
12482
  }
12841
- function globToRegex(glob) {
12842
- let out = "";
12843
- let i = 0;
12844
- while (i < glob.length) {
12845
- const c = glob[i];
12846
- if (c === "*" && glob[i + 1] === "*") {
12847
- out += ".*";
12848
- i += 2;
12849
- if (glob[i] === "/")
12850
- i++;
12851
- } else if (c === "*") {
12852
- out += "[^/]*";
12853
- i++;
12854
- } else if (c === "?") {
12855
- out += "[^/]";
12856
- i++;
12857
- } else if (c === "{") {
12858
- const end = glob.indexOf("}", i);
12859
- if (end === -1) {
12860
- out += "\\{";
12861
- i++;
12862
- continue;
12863
- }
12864
- const opts = glob.slice(i + 1, end).split(",").map(escapeRe).join("|");
12865
- out += "(?:" + opts + ")";
12866
- i = end + 1;
12867
- } else {
12868
- out += escapeRe(c);
12869
- i++;
12870
- }
12871
- }
12872
- return out;
12483
+ function redactFilePaths(text) {
12484
+ if (!text)
12485
+ return text;
12486
+ return text.replace(PATH_REGEX, (m) => {
12487
+ if (m.startsWith("[REDACTED:"))
12488
+ return m;
12489
+ const h = createHash4("sha256").update(m).digest("hex").slice(0, 12);
12490
+ return `[path:${h}]`;
12491
+ });
12873
12492
  }
12874
- function escapeRe(s) {
12875
- return s.replace(/[.+^${}()|[\]\\]/g, "\\$&");
12493
+ function applyChunkPrivacy(text) {
12494
+ let out = redactSecrets(text);
12495
+ if (privacyView().redactFilePaths)
12496
+ out = redactFilePaths(out);
12497
+ return out;
12876
12498
  }
12877
- var DEFAULT_AUTO_WORKSPACE_MIN_REPOS, BOT_WORKTREE_MARKERS, resolveCache, gitCache, workspaceCache, wsIdCache, cachedConfig, GIT_TIMEOUT_MS, AUTO_WORKSPACE_MAX_DEPTH;
12878
- var init_project_resolver = __esm({
12879
- "../engine/dist/core/project-resolver.js"() {
12880
- "use strict";
12881
- init_paths();
12882
- DEFAULT_AUTO_WORKSPACE_MIN_REPOS = 3;
12883
- BOT_WORKTREE_MARKERS = [".claude-pr-bot"];
12884
- resolveCache = /* @__PURE__ */ new Map();
12885
- gitCache = /* @__PURE__ */ new Map();
12886
- workspaceCache = /* @__PURE__ */ new Map();
12887
- wsIdCache = /* @__PURE__ */ new Map();
12888
- cachedConfig = null;
12889
- GIT_TIMEOUT_MS = 1e4;
12890
- AUTO_WORKSPACE_MAX_DEPTH = 2;
12891
- }
12892
- });
12893
-
12894
- // ../engine/dist/core/credentials-env.js
12895
- function envCredentials(env = process.env) {
12896
- const token = (env.CHAT_RECALL_TOKEN || "").trim();
12897
- const server = (env.CHAT_RECALL_SERVER || "").trim();
12898
- if (!token || !server)
12899
- return null;
12900
- if (!/^https?:\/\//i.test(server))
12901
- return null;
12902
- return { serverUrl: normalise(server), token };
12499
+ function redactToolResultBody(toolName, body) {
12500
+ if (!body)
12501
+ return body;
12502
+ return `[redacted: ${toolName} output, ${body.length} chars]`;
12903
12503
  }
12904
- var normalise;
12905
- var init_credentials_env = __esm({
12906
- "../engine/dist/core/credentials-env.js"() {
12504
+ var DEFAULT_REDACTION_RULES, SECRET_CONTEXT_RE, HIGH_ENTROPY_TOKEN_RE, cache, CANARY, MAX_PACK_RULES, MAX_PATTERN_LEN, serverPack, globalRegexCache, privacyCache, PATH_REGEX;
12505
+ var init_secret_redactor = __esm({
12506
+ "../engine/dist/core/secret-redactor.js"() {
12907
12507
  "use strict";
12908
- normalise = (u) => u.trim().replace(/\/+$/, "");
12508
+ init_settings();
12509
+ DEFAULT_REDACTION_RULES = [
12510
+ // AWS access tokens. Prefix-anchored: AKIA, ASIA, ABIA (long-term,
12511
+ // STS, EC2). Always exactly 16 hex/alphanumeric after the prefix.
12512
+ // NOTE: this matches only the access-key-ID, NOT the secret key value —
12513
+ // the `env-secret` rule below covers AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN.
12514
+ { label: "aws-access-token", pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
12515
+ // AWS STS session tokens. These are long base64 blobs with NO `NAME=` context
12516
+ // when pasted bare into prose/code, so `env-secret` misses them — but they
12517
+ // reliably start with one of a handful of base64 prefixes (the encoded
12518
+ // `{"...` JSON header). Prefix-anchored → low false-positive. This is the
12519
+ // bare-value form that leaked (`search('IQoJEXAMPLEfakeSESSIONtokenNOTreal01234567890abcDEF==...')`).
12520
+ { label: "aws-session-token", pattern: /\b(?:IQoJ|FwoG|FwoH|FQoG|FQoH|FwoD)[A-Za-z0-9/+]{40,}={0,2}/g },
12521
+ // Env/shell-assignment secrets — a VAR whose name ends in KEY/SECRET/TOKEN/
12522
+ // PASSWORD/CREDENTIAL, then `=`/`:`, then the value. Zero-width lookbehind so
12523
+ // only the VALUE is redacted (the var name stays for context). This is the
12524
+ // dominant real-world leak vector (`export` dumps, .env files, `curl -H`,
12525
+ // tool output) and is exactly what leaked AWS_SECRET_ACCESS_KEY and
12526
+ // AWS_SESSION_TOKEN in cleartext while the access-key-ID rule above caught
12527
+ // only the (least-sensitive) ID.
12528
+ // Suffix list deliberately broad: DB_PASS (our own helm chart's name!),
12529
+ // NPM_AUTH, SENTRY_DSN, SESSION/COOKIE secrets and BEARER vars all leaked
12530
+ // past the original KEY/SECRET/TOKEN/PASSWORD-only list. Values under 12
12531
+ // chars don't redact, so flag-ish vars (BYPASS=true) stay readable.
12532
+ // Context CONSUMED, secret in group 1 — see RedactionRule.secret. As a
12533
+ // lookbehind this was the second most expensive rule in the collector.
12534
+ { 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 },
12535
+ // Credentials embedded in a connection URL: scheme://user:PASSWORD@host.
12536
+ // Redacts only the password segment (keeps scheme/user/host for context).
12537
+ //
12538
+ // Scheme-agnostic on purpose. The old rule allowlisted eight DB schemes, so
12539
+ // `https://user:pass@host`, `ssh://`, `ftp://`, `smtp://` and every other
12540
+ // userinfo URL walked through in cleartext — and a `jdbc:postgresql://…`
12541
+ // prefix broke the \b anchor for the one family it did cover. Any scheme
12542
+ // followed by userinfo is a credential, so match the shape, not a list.
12543
+ { label: "url-password", secret: 1, pattern: /\b[a-zA-Z][a-zA-Z0-9+.-]{1,31}:\/\/[^:@\s/]{1,64}:([^@\s/]{1,256})(?=@)/g },
12544
+ // Bare AWS secret-access-key VALUE: EXACTLY 40 base64 chars, word-bounded,
12545
+ // containing upper+lower+digit. This is the gitleaks heuristic — the mixed-case
12546
+ // requirement excludes 40-char lowercase-hex git SHAs, and the exact-40 bound
12547
+ // excludes longer blobs/tokens. It catches the value when it appears bare in
12548
+ // prose with NO `NAME=` and NO nearby context word (e.g. inside a markdown
12549
+ // table or quoted in analysis) — the residual leak path the env-secret and
12550
+ // contextual rules both miss. Placed AFTER env-secret/url-password so the
12551
+ // keyed/URL forms keep their more-specific labels.
12552
+ // The three stacked lookaheads each re-scanned the same 40 characters, which
12553
+ // made this the MOST expensive rule of all 106 the collector runs (13.1% of a
12554
+ // 29.5 MB scan). The mixed-case-plus-digit test is now one JS pass.
12555
+ {
12556
+ label: "aws-secret-key",
12557
+ secret: 1,
12558
+ pattern: /(?<![A-Za-z0-9/+])([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+])/g,
12559
+ where: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) && /[0-9]/.test(v)
12560
+ },
12561
+ // GitHub personal access tokens.
12562
+ { label: "github-pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
12563
+ // GitHub fine-grained PATs.
12564
+ { label: "github-fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
12565
+ // GitHub OAuth.
12566
+ { label: "github-oauth", pattern: /\bgho_[A-Za-z0-9]{36}\b/g },
12567
+ // GitHub app + refresh.
12568
+ { label: "github-app", pattern: /\b(?:ghs|ghu|ghr)_[A-Za-z0-9]{36}\b/g },
12569
+ // Slack bot/user/app tokens.
12570
+ { label: "slack-token", pattern: /\bxox[abopr]-[A-Za-z0-9-]{10,}\b/g },
12571
+ // Slack incoming webhook URL.
12572
+ { label: "slack-webhook", pattern: /\bhttps:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+\b/g },
12573
+ // Stripe live + test keys.
12574
+ { label: "stripe-key", pattern: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
12575
+ // Anthropic.
12576
+ { label: "anthropic-key", pattern: /\bsk-ant-[a-zA-Z0-9_-]{40,}\b/g },
12577
+ // OpenAI. Two shapes, deliberately kept apart. The classic key is
12578
+ // `sk-` + base62 with NO hyphens; the modern scoped keys carry an explicit
12579
+ // proj/svcacct/admin segment first. Allowing hyphens after a bare `sk-`
12580
+ // (which is what admitted sk-proj- in the first place) also matches ordinary
12581
+ // kebab-case text — `sk-fix-the-login-redirect-bug`, `.sk-button-primary`,
12582
+ // `sk-2024-01-01-backup` — and this corpus is full of branch names and CSS
12583
+ // classes. Requiring the segment keeps sk-proj- coverage without that.
12584
+ { label: "openai-key", pattern: /\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}\b|\bsk-[A-Za-z0-9]{20,}\b/g },
12585
+ // Google API key (AIza…).
12586
+ { label: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
12587
+ // Generic JWT — three base64url segments.
12588
+ { label: "jwt", pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
12589
+ // Private key blocks (PEM). The label is a free word run rather than a fixed
12590
+ // list, so ENCRYPTED PRIVATE KEY and PGP PRIVATE KEY BLOCK are covered
12591
+ // without another enumeration, and \1 forces END to name the same kind as
12592
+ // BEGIN.
12593
+ //
12594
+ // The body is [^-], NOT [\s\S]*?. That is a performance boundary, not a
12595
+ // style choice. An unanchored prefix with an unbounded lazy gap re-scans to
12596
+ // end-of-input from every candidate start, so text carrying many BEGIN
12597
+ // markers and no matching END costs O(n^2): 500KB of that measured 810ms
12598
+ // here, and this runs synchronously on the server's event loop over
12599
+ // tenant-supplied text (services/secret-rescan.ts). A PEM body is base64
12600
+ // plus headers and contains no '-', so excluding it makes the scan stop at
12601
+ // the next delimiter instead of at EOF — same matches, linear cost.
12602
+ { label: "private-key", pattern: /-----BEGIN ([A-Z0-9 ]{0,32})PRIVATE KEY(?: BLOCK)?-----[^-]{0,10000}?-----END \1PRIVATE KEY(?: BLOCK)?-----/g },
12603
+ // PuTTY keys are not PEM. Match the Private-Lines payload — the actual key
12604
+ // material — rather than spanning the whole file, which would need the same
12605
+ // unbounded gap the rule above just removed. Line-anchored and base64-only,
12606
+ // so it cannot run away.
12607
+ { label: "putty-key", pattern: /^Private-Lines:[ \t]*\d{1,4}[\r\n]+(?:[A-Za-z0-9+/=]{8,120}[\r\n]+){1,400}/gm },
12608
+ // npm tokens (uuid-shaped — overlap with our UUID filter; intentional).
12609
+ { label: "npm-token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/g },
12610
+ // .npmrc auth lines: //registry.npmjs.org/:_authToken=VALUE (the key is not
12611
+ // ALL-CAPS, so the env-secret rule above never matched it). The leading `:`
12612
+ // is required — it is what makes this npmrc syntax rather than any shell var
12613
+ // ending in _AUTH, which env-secret already owns and labels better.
12614
+ // [^\s"'`]+ so the closing quote is not swallowed, and a ${VAR} placeholder
12615
+ // is left alone — redacting it destroys a config example and hides nothing.
12616
+ // The rule the perf profile caught the wedged daemon inside: 98.94% of samples.
12617
+ { label: "npmrc-auth", secret: 1, pattern: /:_(?:authToken|password|auth)[ \t]*=[ \t]*(?!\$\{)([^\s"'`]{12,})/gi },
12618
+ // HTTP Authorization headers. env-secret requires an ALL-CAPS name, and
12619
+ // `Authorization` is mixed case, so both Bearer and Basic walked straight
12620
+ // through — including short opaque tokens the contextual pass also misses
12621
+ // (it needs 32+ chars AND mixed-case-plus-digit).
12622
+ // Same two guards as npmrc-auth: stop at a quote, and skip the obvious
12623
+ // placeholders that fill documentation (<YOUR_TOKEN>, ${TOKEN}, xxx…).
12624
+ { label: "auth-header", secret: 1, pattern: /\bauthorization[ \t]*:[ \t]*(?:bearer|basic|token)[ \t]+(?![<$])([^\s"'`<]{8,})/gi },
12625
+ // Vendor tokens with unambiguous prefixes. Each is a fixed literal, so a
12626
+ // false positive is essentially impossible and the cost of listing is low.
12627
+ { label: "gitlab-pat", pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
12628
+ { label: "slack-app-token", pattern: /\bxapp-\d-[A-Z0-9]{8,}-\d{10,}-[a-f0-9]{32,}\b/g },
12629
+ { label: "sendgrid-key", pattern: /\bSG\.[A-Za-z0-9_-]{20,24}\.[A-Za-z0-9_-]{40,50}\b/g },
12630
+ // (?<![-\w]) so `cache-key-<md5>` is not read as a mailgun key.
12631
+ { label: "mailgun-key", pattern: /(?<![-\w])key-[a-f0-9]{32}\b/g },
12632
+ { label: "digitalocean-pat", pattern: /\bdop_v1_[a-f0-9]{64}\b/g },
12633
+ { label: "huggingface-token", pattern: /\bhf_[A-Za-z0-9]{30,}\b/g },
12634
+ { label: "shopify-token", pattern: /\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}\b/g },
12635
+ { label: "pypi-token", pattern: /\bpypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{50,}\b/g },
12636
+ // No twilio-sid rule: AC…/SK… are Twilio SIDs, which are public account and
12637
+ // key identifiers, not the auth token. Redacting them hid nothing and
12638
+ // collided with 32-hex etags and MD5 sums.
12639
+ // No discord-bot-token rule either: the shape (word.word.word) matched
12640
+ // ordinary dotted symbols such as a.b.ApplicationContextProviderImpl.
12641
+ { label: "telegram-bot-token", pattern: /\b\d{8,10}:AA[A-Za-z0-9_-]{32,}\b/g },
12642
+ { label: "doppler-token", pattern: /\bdp\.(?:pt|st|sa|ct)\.[A-Za-z0-9_-]{20,}\b/g },
12643
+ // Azure Storage SAS signature — the `sig=` parameter is the actual secret.
12644
+ { label: "azure-sas", secret: 1, pattern: /[?&]sig=([A-Za-z0-9%/+=]{20,})/g }
12645
+ ];
12646
+ SECRET_CONTEXT_RE = /\b(?:secret|password|passwd|credential|access[ _-]?key|private[ _-]?key|api[ _-]?key|auth(?:oriz\w*)?|bearer|token)\b/gi;
12647
+ HIGH_ENTROPY_TOKEN_RE = /[A-Za-z0-9/+_-]{32,}/g;
12648
+ cache = null;
12649
+ CANARY = "the quick brown fox jumps over the lazy dog 0123456789 commit 8ba1f109551bd432803012645ac136ddd475d0fb src/index.ts:42";
12650
+ MAX_PACK_RULES = 200;
12651
+ MAX_PATTERN_LEN = 512;
12652
+ serverPack = null;
12653
+ globalRegexCache = /* @__PURE__ */ new Map();
12654
+ privacyCache = null;
12655
+ PATH_REGEX = /(?<![A-Za-z0-9_])(?:\/[^\s'"`<>]+|[A-Za-z]:\\[^\s'"`<>]+)/g;
12909
12656
  }
12910
12657
  });
12911
12658
 
@@ -14888,111 +14635,6 @@ var init_source_registry = __esm({
14888
14635
  }
14889
14636
  });
14890
14637
 
14891
- // ../engine/dist/parsers/session-source.js
14892
- var SessionSource;
14893
- var init_session_source = __esm({
14894
- "../engine/dist/parsers/session-source.js"() {
14895
- "use strict";
14896
- init_session();
14897
- init_chunker();
14898
- init_settings();
14899
- SessionSource = class {
14900
- sourceType = "session";
14901
- claudeDir;
14902
- constructor(claudeDir) {
14903
- this.claudeDir = claudeDir;
14904
- }
14905
- async *discover() {
14906
- if (!isSourceEnabled("claude", "sessions"))
14907
- return;
14908
- for (const [entry, sessionPath] of getAllSessions(this.claudeDir)) {
14909
- yield this.entryToItem(entry, sessionPath);
14910
- }
14911
- }
14912
- async parse(item) {
14913
- const content = await parseSessionFile(item.filePath);
14914
- const entry = this.itemToEntry(item);
14915
- const sessionChunks = chunkSession(entry, content);
14916
- item.extra = {
14917
- ...item.extra || {},
14918
- ...content.metadata
14919
- };
14920
- return sessionChunks.map((sc) => ({
14921
- chunkId: sc.chunkId,
14922
- itemId: sc.sessionId,
14923
- sourceType: "session",
14924
- // Empty title is preferable to "Session abc12345" placeholder text:
14925
- // the latter pollutes search results and gets surfaced in the UI as
14926
- // a misleading heading. Downstream renderers fall back to
14927
- // project path / time when title is empty.
14928
- title: sc.firstPrompt.slice(0, 100),
14929
- text: sc.text,
14930
- chunkType: sc.chunkType,
14931
- projectPath: sc.projectPath,
14932
- filePath: sc.sourceFile,
14933
- mtime: sc.mtime
14934
- }));
14935
- }
14936
- async extractLinks(item) {
14937
- if (item.filePath) {
14938
- try {
14939
- const { detectForkPredecessor: detectForkPredecessor2 } = await Promise.resolve().then(() => (init_raw(), raw_exports));
14940
- const predecessor = detectForkPredecessor2(item.filePath);
14941
- if (predecessor) {
14942
- return [{
14943
- sourceType: "session",
14944
- sourceId: item.id,
14945
- targetType: "session",
14946
- targetId: predecessor,
14947
- linkType: "forked_from",
14948
- confidence: 1
14949
- }];
14950
- }
14951
- } catch {
14952
- }
14953
- }
14954
- return [];
14955
- }
14956
- entryToItem(entry, sessionPath) {
14957
- return {
14958
- id: entry.sessionId,
14959
- sourceType: "session",
14960
- // Empty title when there's no first prompt — the UI then knows to
14961
- // fall back to summary or project path instead of rendering the
14962
- // useless "Session abc12345" placeholder as a heading.
14963
- title: entry.firstPrompt ? entry.firstPrompt.replace(/\n/g, " ").trim().slice(0, 100) : "",
14964
- projectPath: entry.projectPath,
14965
- filePath: sessionPath,
14966
- mtime: entry.fileMtime,
14967
- contentPreview: entry.firstPrompt?.slice(0, 200),
14968
- extra: {
14969
- messageCount: entry.messageCount,
14970
- gitBranch: entry.gitBranch,
14971
- created: entry.created,
14972
- modified: entry.modified,
14973
- isSidechain: entry.isSidechain
14974
- }
14975
- };
14976
- }
14977
- itemToEntry(item) {
14978
- const extra = item.extra || {};
14979
- return {
14980
- sessionId: item.id,
14981
- fullPath: item.filePath,
14982
- fileMtime: item.mtime,
14983
- firstPrompt: item.contentPreview || "",
14984
- messageCount: extra.messageCount || 0,
14985
- created: extra.created || "",
14986
- modified: extra.modified || "",
14987
- gitBranch: extra.gitBranch || "",
14988
- projectPath: item.projectPath,
14989
- isSidechain: extra.isSidechain || false
14990
- };
14991
- }
14992
- };
14993
- }
14994
- });
14995
-
14996
14638
  // ../engine/dist/core/utils.js
14997
14639
  import { existsSync as existsSync21, readdirSync as readdirSync15 } from "fs";
14998
14640
  import { homedir as homedir6 } from "os";
@@ -18349,7 +17991,6 @@ var init_plugins_source = __esm({
18349
17991
 
18350
17992
  // ../engine/dist/parsers/all-sources.js
18351
17993
  function buildSourceRegistry(registry = new SourceRegistry()) {
18352
- registry.register(new SessionSource());
18353
17994
  registry.register(new PlanSource());
18354
17995
  registry.register(new TaskSource());
18355
17996
  registry.register(new ClaudeMdSource());
@@ -18373,7 +18014,6 @@ var init_all_sources = __esm({
18373
18014
  "../engine/dist/parsers/all-sources.js"() {
18374
18015
  "use strict";
18375
18016
  init_source_registry();
18376
- init_session_source();
18377
18017
  init_plan_source();
18378
18018
  init_task_source();
18379
18019
  init_claude_md_source();
@@ -18817,7 +18457,7 @@ var init_http = __esm({
18817
18457
  CLI_VERSION = JSON.parse(readFileSync31(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
18818
18458
  } catch {
18819
18459
  try {
18820
- if (true) CLI_VERSION = "0.6.8";
18460
+ if (true) CLI_VERSION = "0.7.0";
18821
18461
  } catch {
18822
18462
  }
18823
18463
  }
@@ -18842,27 +18482,6 @@ var init_redact_deep = __esm({
18842
18482
  }
18843
18483
  });
18844
18484
 
18845
- // ../engine/dist/core/session-multi-tool.js
18846
- function extractTurnsAny(sessionId, opts = {}) {
18847
- const backend = getBackendForId(sessionId);
18848
- if (!backend)
18849
- return { sessionId, found: false, turns: [], startMs: 0, endMs: 0 };
18850
- return backend.extractTurns(sessionId, opts);
18851
- }
18852
- function replaySessionAny(sessionId) {
18853
- const backend = getBackendForId(sessionId);
18854
- if (!backend)
18855
- return { sessionId, found: false, projectPath: "", files: [], totalLinesAdded: 0, totalLinesRemoved: 0 };
18856
- return backend.replay(sessionId);
18857
- }
18858
- var init_session_multi_tool = __esm({
18859
- "../engine/dist/core/session-multi-tool.js"() {
18860
- "use strict";
18861
- init_tool_backend();
18862
- init_backends();
18863
- }
18864
- });
18865
-
18866
18485
  // src/derived.ts
18867
18486
  function collectDerivedRows(ref, mtime, count, maxRowBytes) {
18868
18487
  const id = ref.prefixedId;
@@ -19337,7 +18956,7 @@ var init_telemetry = __esm({
19337
18956
  init_sync_client();
19338
18957
  cliVersion2 = () => {
19339
18958
  try {
19340
- return true ? "0.6.8" : "dev";
18959
+ return true ? "0.7.0" : "dev";
19341
18960
  } catch {
19342
18961
  return "dev";
19343
18962
  }
@@ -19352,6 +18971,7 @@ var collector_health_exports = {};
19352
18971
  __export(collector_health_exports, {
19353
18972
  CRASHLOOP_RESTARTS: () => CRASHLOOP_RESTARTS,
19354
18973
  STALE_AFTER_MS: () => STALE_AFTER_MS,
18974
+ applySyncOutcome: () => applySyncOutcome,
19355
18975
  collectorHealthPath: () => collectorHealthPath,
19356
18976
  judgeHealth: () => judgeHealth,
19357
18977
  progressLine: () => progressLine,
@@ -19362,6 +18982,22 @@ __export(collector_health_exports, {
19362
18982
  });
19363
18983
  import { readFileSync as readFileSync32, writeFileSync as writeFileSync7, mkdirSync as mkdirSync8 } from "node:fs";
19364
18984
  import { join as join40, dirname as dirname17 } from "node:path";
18985
+ function applySyncOutcome(prior, outcome, now = Date.now()) {
18986
+ const t = { lastOkAt: prior?.lastOkAt ?? null, failures: prior?.failures ?? 0 };
18987
+ if (prior?.lastError !== void 0)
18988
+ t.lastError = prior.lastError;
18989
+ if (outcome.ok) {
18990
+ t.lastOkAt = now;
18991
+ t.failures = 0;
18992
+ delete t.lastError;
18993
+ return t;
18994
+ }
18995
+ if ((outcome.accepted ?? 0) > 0)
18996
+ t.lastOkAt = now;
18997
+ t.failures += 1;
18998
+ t.lastError = (outcome.error ?? "unknown error").slice(0, 160);
18999
+ return t;
19000
+ }
19365
19001
  function progressLine(h) {
19366
19002
  const p = h?.progress;
19367
19003
  if (!p || p.complete || p.total <= 0)
@@ -19920,6 +19556,7 @@ var sync_client_exports = {};
19920
19556
  __export(sync_client_exports, {
19921
19557
  BATCH_SCAN_MAX_BYTES: () => BATCH_SCAN_MAX_BYTES,
19922
19558
  FULL_BUILD_MAX_BYTES: () => FULL_BUILD_MAX_BYTES,
19559
+ _acceptedUploads: () => _acceptedUploads,
19923
19560
  _fetchTenantSyncConfigForTests: () => _fetchTenantSyncConfigForTests,
19924
19561
  _resetCapsCacheForTests: () => _resetCapsCacheForTests,
19925
19562
  _resetTenantSecurityConfigCache: () => _resetTenantSecurityConfigCache,
@@ -20183,6 +19820,9 @@ function removeCredentials(serverUrl) {
20183
19820
  writeFileSync9(credPath(), JSON.stringify({ targets: kept }, null, 2));
20184
19821
  return true;
20185
19822
  }
19823
+ function _acceptedUploads(url) {
19824
+ return acceptedUploads.get(url) ?? 0;
19825
+ }
20186
19826
  async function syncSessions(opts = {}) {
20187
19827
  const targets = loadAllCredentials();
20188
19828
  if (targets.length === 0) throw new Error("Not logged in \u2014 run `chat-recall login <server-url> --token <token>`");
@@ -20206,10 +19846,11 @@ async function syncSessions(opts = {}) {
20206
19846
  };
20207
19847
  continue;
20208
19848
  }
19849
+ acceptedUploads.set(cred.serverUrl, 0);
20209
19850
  try {
20210
19851
  const r = await syncToTarget(cred, opts);
20211
19852
  noteTargetSuccess(cred.serverUrl);
20212
- perTarget[cred.serverUrl] = { ok: true };
19853
+ perTarget[cred.serverUrl] = { ok: true, accepted: acceptedUploads.get(cred.serverUrl) ?? 0 };
20213
19854
  agg = agg ? {
20214
19855
  uploaded: agg.uploaded + r.uploaded,
20215
19856
  skipped: agg.skipped + r.skipped,
@@ -20226,7 +19867,7 @@ async function syncSessions(opts = {}) {
20226
19867
  } catch (e) {
20227
19868
  const msg = e instanceof Error ? e.message : String(e);
20228
19869
  errors.push(`${cred.serverUrl}: ${msg}`);
20229
- perTarget[cred.serverUrl] = { ok: false, error: msg };
19870
+ perTarget[cred.serverUrl] = { ok: false, error: msg, accepted: acceptedUploads.get(cred.serverUrl) ?? 0 };
20230
19871
  const next = noteTargetFailure(cred.serverUrl, msg);
20231
19872
  const notice = breakerNotice(cred.serverUrl, next);
20232
19873
  if (notice) console.error(`[sync] ${notice}`);
@@ -20492,7 +20133,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20492
20133
  return [];
20493
20134
  }
20494
20135
  });
20495
- const MAX_BATCH_BYTES = 4 * 1024 * 1024;
20136
+ const MAX_BATCH_BYTES = Number(process.env.CHAT_RECALL_MAX_BATCH_BYTES) || (isLocalHost(cred.serverUrl) ? 4 * 1024 * 1024 : 1024 * 1024);
20496
20137
  const sleep2 = (ms) => new Promise((r) => setTimeout(r, ms));
20497
20138
  const explicitThrottle = typeof opts.throttleMs === "number" && opts.throttleMs > 0;
20498
20139
  const throttleMs = explicitThrottle ? opts.throttleMs : 0;
@@ -20527,6 +20168,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20527
20168
  signal: ac.signal
20528
20169
  });
20529
20170
  if (res.ok) {
20171
+ acceptedUploads.set(cred.serverUrl, (acceptedUploads.get(cred.serverUrl) ?? 0) + 1);
20530
20172
  const body2 = await res.json().catch(() => ({}));
20531
20173
  if (typeof body2.telemetry === "boolean") {
20532
20174
  setTelemetryEligible(base3, body2.telemetry);
@@ -20577,7 +20219,8 @@ async function syncToTargetInScope(cred, opts = {}) {
20577
20219
  if (attempt >= RETRY_DELAYS_MS.length) throw new Error(`sync failed after ${attempt + 1} attempts: HTTP ${res.status} ${text}`);
20578
20220
  const retryAfterS = Number(res.headers.get("retry-after"));
20579
20221
  const waitMs = res.status === 429 && retryAfterS > 0 ? retryAfterS * 1e3 : RETRY_DELAYS_MS[attempt];
20580
- console.error(`[sync] HTTP ${res.status} from ${base3} \u2014 backing off ${Math.round(waitMs / 1e3)}s (attempt ${attempt + 1})`);
20222
+ const why = res.status === 524 ? ` \u2014 the proxy cut the request at its time limit; the batch is too big for one request` : "";
20223
+ console.error(`[sync] HTTP ${res.status} from ${base3}${why} \u2014 backing off ${Math.round(waitMs / 1e3)}s (attempt ${attempt + 1})`);
20581
20224
  await sleep2(waitMs);
20582
20225
  continue;
20583
20226
  } catch (err) {
@@ -21436,7 +21079,7 @@ async function syncIncremental(opts = {}) {
21436
21079
  lock.release();
21437
21080
  }
21438
21081
  }
21439
- var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, _fetchTenantSyncConfigForTests, SYNC_RULES_REFUSAL, hashPath, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
21082
+ var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, _fetchTenantSyncConfigForTests, SYNC_RULES_REFUSAL, hashPath, acceptedUploads, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, FULL_BUILD_TAIL_BYTES, BATCH_SCAN_MAX_BYTES;
21440
21083
  var init_sync_client = __esm({
21441
21084
  "src/sync-client.ts"() {
21442
21085
  "use strict";
@@ -21481,7 +21124,7 @@ var init_sync_client = __esm({
21481
21124
  ownVersion = JSON.parse(readFileSync35(join42(dir, "../package.json"), "utf-8")).version || ownVersion;
21482
21125
  } catch {
21483
21126
  try {
21484
- if (true) ownVersion = "0.6.8";
21127
+ if (true) ownVersion = "0.7.0";
21485
21128
  } catch {
21486
21129
  }
21487
21130
  }
@@ -21501,6 +21144,7 @@ var init_sync_client = __esm({
21501
21144
  _fetchTenantSyncConfigForTests = fetchTenantSyncConfig;
21502
21145
  SYNC_RULES_REFUSAL = /* @__PURE__ */ Symbol.for("chat-recall.syncRulesRefusal");
21503
21146
  hashPath = (p) => p ? "p_" + createHash9("sha256").update(p).digest("hex").slice(0, 12) : "";
21147
+ acceptedUploads = /* @__PURE__ */ new Map();
21504
21148
  MAX_DERIVED_ROW_BYTES = 2 * 1024 * 1024;
21505
21149
  isLocalHost = isPrivateHost;
21506
21150
  CAPS_TTL_MS = 60 * 6e4;
@@ -25596,7 +25240,7 @@ var init_intent_drain = __esm({
25596
25240
  ownVersion2 = JSON.parse(readFileSync45(join56(dir, "../package.json"), "utf-8")).version || ownVersion2;
25597
25241
  } catch {
25598
25242
  try {
25599
- if (true) ownVersion2 = "0.6.8";
25243
+ if (true) ownVersion2 = "0.7.0";
25600
25244
  } catch {
25601
25245
  }
25602
25246
  }
@@ -27968,6 +27612,7 @@ var DEFAULT_ALLOW = [
27968
27612
  "recall_user_prompts",
27969
27613
  "recall_decision_record",
27970
27614
  "recall_decisions",
27615
+ "recall_decision_scan",
27971
27616
  "recall_analytics_summary",
27972
27617
  "recall_outcome_summary",
27973
27618
  "recall_regenerate_summary",
@@ -29945,7 +29590,19 @@ program.command("guard").description("Check a pending tool call against the deci
29945
29590
  if (target.token) headers.authorization = `Bearer ${target.token}`;
29946
29591
  const res = await fetchWithTimeout2(
29947
29592
  `${target.base}/api/decisions/check`,
29948
- { method: "POST", headers, body: JSON.stringify({ names, via, project: opts.project }) },
29593
+ {
29594
+ method: "POST",
29595
+ headers,
29596
+ // The folder group is resolved HERE, from the directory the hook runs
29597
+ // in. The server has no checkout to read it from, and a lookup per
29598
+ // tool call is exactly the cost this guard cannot afford.
29599
+ body: JSON.stringify({
29600
+ names,
29601
+ via,
29602
+ project: opts.project,
29603
+ workspace: resolveWorkspaceId(process.cwd())?.slice(3)
29604
+ })
29605
+ },
29949
29606
  4e3
29950
29607
  );
29951
29608
  if (!res.ok) return;