chat-recall 0.6.9 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/watch.js CHANGED
@@ -115,6 +115,25 @@ var init_tool_backend = __esm({
115
115
  }
116
116
  });
117
117
 
118
+ // ../engine/dist/parsers/banners.js
119
+ function stripInjectedBanners(text) {
120
+ let result = text;
121
+ for (const re of INJECTED_BANNERS)
122
+ result = result.replace(re, " ");
123
+ return result;
124
+ }
125
+ var INJECTED_BANNERS;
126
+ var init_banners = __esm({
127
+ "../engine/dist/parsers/banners.js"() {
128
+ "use strict";
129
+ INJECTED_BANNERS = [
130
+ /MCP issues detected\. ?Run \/mcp list for status\.?/g,
131
+ /Context low[^\n]*Run \/compact[^\n]*/g,
132
+ /API Error:[^\n]{0,120}/g
133
+ ];
134
+ }
135
+ });
136
+
118
137
  // ../engine/dist/core/project-path-match.js
119
138
  function canonicalProjectPath(p) {
120
139
  return (p || "").trim().replace(/\\/g, "/").replace(/[-_]/g, "/").replace(/\/{2,}/g, "/").replace(/\/+$/, "").toLowerCase();
@@ -5031,572 +5050,6 @@ var init_settings = __esm({
5031
5050
  }
5032
5051
  });
5033
5052
 
5034
- // ../engine/dist/core/secret-redactor.js
5035
- import { statSync as statSync3 } from "fs";
5036
- function secretOf(rule, m) {
5037
- const raw = rule.secret != null ? m[rule.secret] : m[0];
5038
- if (raw == null)
5039
- return null;
5040
- if (rule.where && !rule.where(raw))
5041
- return null;
5042
- return raw;
5043
- }
5044
- function looksLikeSecretToken(t) {
5045
- if (t.length < 32)
5046
- return false;
5047
- if (/^[a-f0-9]+$/i.test(t))
5048
- return true;
5049
- return /[A-Z]/.test(t) && /[a-z]/.test(t) && /[0-9]/.test(t);
5050
- }
5051
- function findContextualSecrets(text) {
5052
- if (!text)
5053
- return [];
5054
- const ranges = [];
5055
- let m;
5056
- SECRET_CONTEXT_RE.lastIndex = 0;
5057
- while ((m = SECRET_CONTEXT_RE.exec(text)) !== null) {
5058
- const winStart = m.index + m[0].length;
5059
- const window = text.slice(winStart, winStart + 80);
5060
- HIGH_ENTROPY_TOKEN_RE.lastIndex = 0;
5061
- let tm;
5062
- while ((tm = HIGH_ENTROPY_TOKEN_RE.exec(window)) !== null) {
5063
- if (!looksLikeSecretToken(tm[0]))
5064
- continue;
5065
- 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))) {
5066
- continue;
5067
- }
5068
- ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
5069
- break;
5070
- }
5071
- }
5072
- ranges.sort((a, b) => a.start - b.start);
5073
- const out = [];
5074
- for (const r of ranges) {
5075
- const last = out[out.length - 1];
5076
- if (last && r.start < last.end) {
5077
- last.end = Math.max(last.end, r.end);
5078
- continue;
5079
- }
5080
- out.push({ ...r });
5081
- }
5082
- return out;
5083
- }
5084
- function isEnvRedactionRequested() {
5085
- const v = process.env.CHAT_RECALL_REDACT_INDEX;
5086
- if (v === void 0)
5087
- return null;
5088
- return v === "1" || v === "true" || v === "yes";
5089
- }
5090
- function settingsView() {
5091
- let mtimeMs = -1;
5092
- try {
5093
- mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5094
- } catch {
5095
- }
5096
- if (cache && cache.mtimeMs === mtimeMs)
5097
- return cache;
5098
- let enabled = false;
5099
- const extra = [];
5100
- try {
5101
- const s = loadSettings();
5102
- enabled = !!s.privacy?.redactIndex;
5103
- for (const r of s.privacy?.redactionRules ?? []) {
5104
- try {
5105
- extra.push({ label: r.label, pattern: new RegExp(r.pattern, "g") });
5106
- } catch {
5107
- }
5108
- }
5109
- } catch {
5110
- }
5111
- cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
5112
- return cache;
5113
- }
5114
- function compileServerRule(spec) {
5115
- const name = spec.name || "(unnamed)";
5116
- if (!spec.regex)
5117
- return { name, reason: "empty pattern" };
5118
- if (spec.regex.length > MAX_PATTERN_LEN)
5119
- return { name, reason: `pattern longer than ${MAX_PATTERN_LEN} chars` };
5120
- let re;
5121
- try {
5122
- const flags = "g" + (spec.flags || "").replace(/[^ims]/g, "");
5123
- re = new RegExp(spec.regex, flags);
5124
- } catch (e) {
5125
- return { name, reason: `invalid regex: ${e instanceof Error ? e.message : e}` };
5126
- }
5127
- if (re.test(""))
5128
- return { name, reason: "matches the empty string (would redact everything)" };
5129
- re.lastIndex = 0;
5130
- if (re.test(CANARY))
5131
- return { name, reason: "matches benign text \u2014 too broad" };
5132
- re.lastIndex = 0;
5133
- return { label: `${spec.source === "pack" ? "pack" : "tenant"}:${name}`, pattern: re };
5134
- }
5135
- function installServerRulePack(pack) {
5136
- const rejected = [];
5137
- const rules = [];
5138
- for (const spec of pack.rules || []) {
5139
- if (rules.length >= MAX_PACK_RULES) {
5140
- rejected.push({ name: spec.name || "(unnamed)", reason: `pack exceeds ${MAX_PACK_RULES} rules` });
5141
- continue;
5142
- }
5143
- const out = compileServerRule(spec);
5144
- if ("reason" in out)
5145
- rejected.push(out);
5146
- else
5147
- rules.push(out);
5148
- }
5149
- const version = pack.version || "";
5150
- serverPack = { version, rules };
5151
- return { version, accepted: rules.length, rejected };
5152
- }
5153
- function serverRulePackVersion() {
5154
- return serverPack ? serverPack.version || "unversioned" : null;
5155
- }
5156
- function activeRules() {
5157
- const base2 = settingsView().rules;
5158
- return serverPack && serverPack.rules.length > 0 ? [...base2, ...serverPack.rules] : base2;
5159
- }
5160
- function isRedactionEnabled() {
5161
- const env = isEnvRedactionRequested();
5162
- if (env !== null)
5163
- return env;
5164
- return settingsView().enabled;
5165
- }
5166
- function redactSecrets(text, opts = {}) {
5167
- if (!opts.force && !isRedactionEnabled())
5168
- return text;
5169
- if (!text)
5170
- return text;
5171
- const rules = opts.rules || activeRules();
5172
- let out = text;
5173
- for (const r of rules) {
5174
- out = out.replace(r.pattern, (...args) => {
5175
- const whole = args[0];
5176
- const m = args.slice(0, args.length);
5177
- const secret = secretOf(r, m);
5178
- if (secret == null)
5179
- return whole;
5180
- if (opts.count)
5181
- opts.count.redactions++;
5182
- return whole.slice(0, whole.length - secret.length) + `[REDACTED:${r.label}]`;
5183
- });
5184
- }
5185
- const ctx = findContextualSecrets(out);
5186
- for (let i = ctx.length - 1; i >= 0; i--) {
5187
- const { start, end } = ctx[i];
5188
- if (opts.count)
5189
- opts.count.redactions++;
5190
- out = out.slice(0, start) + "[REDACTED:secret-context]" + out.slice(end);
5191
- }
5192
- return out;
5193
- }
5194
- function maskSecret(s) {
5195
- if (!s)
5196
- return "";
5197
- if (s.length <= 4)
5198
- return "*".repeat(s.length);
5199
- return "*".repeat(Math.min(s.length - 4, 60)) + s.slice(-4);
5200
- }
5201
- function globalRegexFor(pattern) {
5202
- const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
5203
- const key2 = pattern.source + "\0" + flags;
5204
- let re = globalRegexCache.get(key2);
5205
- if (!re) {
5206
- re = new RegExp(pattern.source, flags);
5207
- globalRegexCache.set(key2, re);
5208
- }
5209
- return re;
5210
- }
5211
- function makeLineResolver(text) {
5212
- const starts = [];
5213
- for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1))
5214
- starts.push(i);
5215
- return (index) => {
5216
- let lo = 0, hi = starts.length;
5217
- while (lo < hi) {
5218
- const mid = lo + hi >> 1;
5219
- if (starts[mid] < index)
5220
- lo = mid + 1;
5221
- else
5222
- hi = mid;
5223
- }
5224
- return lo + 1;
5225
- };
5226
- }
5227
- function scanTextForFindings(text, opts = {}) {
5228
- if (!text)
5229
- return [];
5230
- const rules = opts.rules || activeRules();
5231
- const findings = [];
5232
- const seenSpans = /* @__PURE__ */ new Set();
5233
- const lineAt = makeLineResolver(text);
5234
- for (const r of rules) {
5235
- const re = globalRegexFor(r.pattern);
5236
- re.lastIndex = 0;
5237
- let m;
5238
- while ((m = re.exec(text)) !== null) {
5239
- const secret = secretOf(r, m);
5240
- if (secret == null) {
5241
- if (re.lastIndex === m.index)
5242
- re.lastIndex++;
5243
- continue;
5244
- }
5245
- const at = m.index + (m[0].length - secret.length);
5246
- const span = `${at}:${secret.length}`;
5247
- if (!seenSpans.has(span)) {
5248
- seenSpans.add(span);
5249
- findings.push({ rule: r.label, line: lineAt(at), preview: maskSecret(secret) });
5250
- }
5251
- if (re.lastIndex === m.index)
5252
- re.lastIndex++;
5253
- }
5254
- }
5255
- for (const { start, end } of findContextualSecrets(text)) {
5256
- const line = lineAt(start);
5257
- findings.push({ rule: "secret-context", line, preview: maskSecret(text.slice(start, end)) });
5258
- }
5259
- return findings;
5260
- }
5261
- function privacyView() {
5262
- let mtimeMs = -1;
5263
- try {
5264
- mtimeMs = statSync3(settingsFilePath()).mtimeMs;
5265
- } catch {
5266
- }
5267
- if (privacyCache && privacyCache.mtimeMs === mtimeMs)
5268
- return privacyCache;
5269
- let redactToolOutputs = false, redactFilePaths = false;
5270
- try {
5271
- const s = loadSettings();
5272
- redactToolOutputs = !!s.privacy?.redactToolOutputs;
5273
- redactFilePaths = !!s.privacy?.redactFilePaths;
5274
- } catch {
5275
- }
5276
- privacyCache = { mtimeMs, redactToolOutputs, redactFilePaths };
5277
- return privacyCache;
5278
- }
5279
- function isToolOutputRedactionEnabled() {
5280
- return privacyView().redactToolOutputs;
5281
- }
5282
- function redactToolResultBody(toolName, body) {
5283
- if (!body)
5284
- return body;
5285
- return `[redacted: ${toolName} output, ${body.length} chars]`;
5286
- }
5287
- var DEFAULT_REDACTION_RULES, SECRET_CONTEXT_RE, HIGH_ENTROPY_TOKEN_RE, cache, CANARY, MAX_PACK_RULES, MAX_PATTERN_LEN, serverPack, globalRegexCache, privacyCache;
5288
- var init_secret_redactor = __esm({
5289
- "../engine/dist/core/secret-redactor.js"() {
5290
- "use strict";
5291
- init_settings();
5292
- DEFAULT_REDACTION_RULES = [
5293
- // AWS access tokens. Prefix-anchored: AKIA, ASIA, ABIA (long-term,
5294
- // STS, EC2). Always exactly 16 hex/alphanumeric after the prefix.
5295
- // NOTE: this matches only the access-key-ID, NOT the secret key value —
5296
- // the `env-secret` rule below covers AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN.
5297
- { label: "aws-access-token", pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
5298
- // AWS STS session tokens. These are long base64 blobs with NO `NAME=` context
5299
- // when pasted bare into prose/code, so `env-secret` misses them — but they
5300
- // reliably start with one of a handful of base64 prefixes (the encoded
5301
- // `{"...` JSON header). Prefix-anchored → low false-positive. This is the
5302
- // bare-value form that leaked (`search('IQoJEXAMPLEfakeSESSIONtokenNOTreal01234567890abcDEF==...')`).
5303
- { label: "aws-session-token", pattern: /\b(?:IQoJ|FwoG|FwoH|FQoG|FQoH|FwoD)[A-Za-z0-9/+]{40,}={0,2}/g },
5304
- // Env/shell-assignment secrets — a VAR whose name ends in KEY/SECRET/TOKEN/
5305
- // PASSWORD/CREDENTIAL, then `=`/`:`, then the value. Zero-width lookbehind so
5306
- // only the VALUE is redacted (the var name stays for context). This is the
5307
- // dominant real-world leak vector (`export` dumps, .env files, `curl -H`,
5308
- // tool output) and is exactly what leaked AWS_SECRET_ACCESS_KEY and
5309
- // AWS_SESSION_TOKEN in cleartext while the access-key-ID rule above caught
5310
- // only the (least-sensitive) ID.
5311
- // Suffix list deliberately broad: DB_PASS (our own helm chart's name!),
5312
- // NPM_AUTH, SENTRY_DSN, SESSION/COOKIE secrets and BEARER vars all leaked
5313
- // past the original KEY/SECRET/TOKEN/PASSWORD-only list. Values under 12
5314
- // chars don't redact, so flag-ish vars (BYPASS=true) stay readable.
5315
- // Context CONSUMED, secret in group 1 — see RedactionRule.secret. As a
5316
- // lookbehind this was the second most expensive rule in the collector.
5317
- { 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 },
5318
- // Credentials embedded in a connection URL: scheme://user:PASSWORD@host.
5319
- // Redacts only the password segment (keeps scheme/user/host for context).
5320
- //
5321
- // Scheme-agnostic on purpose. The old rule allowlisted eight DB schemes, so
5322
- // `https://user:pass@host`, `ssh://`, `ftp://`, `smtp://` and every other
5323
- // userinfo URL walked through in cleartext — and a `jdbc:postgresql://…`
5324
- // prefix broke the \b anchor for the one family it did cover. Any scheme
5325
- // followed by userinfo is a credential, so match the shape, not a list.
5326
- { label: "url-password", secret: 1, pattern: /\b[a-zA-Z][a-zA-Z0-9+.-]{1,31}:\/\/[^:@\s/]{1,64}:([^@\s/]{1,256})(?=@)/g },
5327
- // Bare AWS secret-access-key VALUE: EXACTLY 40 base64 chars, word-bounded,
5328
- // containing upper+lower+digit. This is the gitleaks heuristic — the mixed-case
5329
- // requirement excludes 40-char lowercase-hex git SHAs, and the exact-40 bound
5330
- // excludes longer blobs/tokens. It catches the value when it appears bare in
5331
- // prose with NO `NAME=` and NO nearby context word (e.g. inside a markdown
5332
- // table or quoted in analysis) — the residual leak path the env-secret and
5333
- // contextual rules both miss. Placed AFTER env-secret/url-password so the
5334
- // keyed/URL forms keep their more-specific labels.
5335
- // The three stacked lookaheads each re-scanned the same 40 characters, which
5336
- // made this the MOST expensive rule of all 106 the collector runs (13.1% of a
5337
- // 29.5 MB scan). The mixed-case-plus-digit test is now one JS pass.
5338
- {
5339
- label: "aws-secret-key",
5340
- secret: 1,
5341
- pattern: /(?<![A-Za-z0-9/+])([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+])/g,
5342
- where: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) && /[0-9]/.test(v)
5343
- },
5344
- // GitHub personal access tokens.
5345
- { label: "github-pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
5346
- // GitHub fine-grained PATs.
5347
- { label: "github-fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
5348
- // GitHub OAuth.
5349
- { label: "github-oauth", pattern: /\bgho_[A-Za-z0-9]{36}\b/g },
5350
- // GitHub app + refresh.
5351
- { label: "github-app", pattern: /\b(?:ghs|ghu|ghr)_[A-Za-z0-9]{36}\b/g },
5352
- // Slack bot/user/app tokens.
5353
- { label: "slack-token", pattern: /\bxox[abopr]-[A-Za-z0-9-]{10,}\b/g },
5354
- // Slack incoming webhook URL.
5355
- { label: "slack-webhook", pattern: /\bhttps:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+\b/g },
5356
- // Stripe live + test keys.
5357
- { label: "stripe-key", pattern: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
5358
- // Anthropic.
5359
- { label: "anthropic-key", pattern: /\bsk-ant-[a-zA-Z0-9_-]{40,}\b/g },
5360
- // OpenAI. Two shapes, deliberately kept apart. The classic key is
5361
- // `sk-` + base62 with NO hyphens; the modern scoped keys carry an explicit
5362
- // proj/svcacct/admin segment first. Allowing hyphens after a bare `sk-`
5363
- // (which is what admitted sk-proj- in the first place) also matches ordinary
5364
- // kebab-case text — `sk-fix-the-login-redirect-bug`, `.sk-button-primary`,
5365
- // `sk-2024-01-01-backup` — and this corpus is full of branch names and CSS
5366
- // classes. Requiring the segment keeps sk-proj- coverage without that.
5367
- { label: "openai-key", pattern: /\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}\b|\bsk-[A-Za-z0-9]{20,}\b/g },
5368
- // Google API key (AIza…).
5369
- { label: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
5370
- // Generic JWT — three base64url segments.
5371
- { label: "jwt", pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
5372
- // Private key blocks (PEM). The label is a free word run rather than a fixed
5373
- // list, so ENCRYPTED PRIVATE KEY and PGP PRIVATE KEY BLOCK are covered
5374
- // without another enumeration, and \1 forces END to name the same kind as
5375
- // BEGIN.
5376
- //
5377
- // The body is [^-], NOT [\s\S]*?. That is a performance boundary, not a
5378
- // style choice. An unanchored prefix with an unbounded lazy gap re-scans to
5379
- // end-of-input from every candidate start, so text carrying many BEGIN
5380
- // markers and no matching END costs O(n^2): 500KB of that measured 810ms
5381
- // here, and this runs synchronously on the server's event loop over
5382
- // tenant-supplied text (services/secret-rescan.ts). A PEM body is base64
5383
- // plus headers and contains no '-', so excluding it makes the scan stop at
5384
- // the next delimiter instead of at EOF — same matches, linear cost.
5385
- { label: "private-key", pattern: /-----BEGIN ([A-Z0-9 ]{0,32})PRIVATE KEY(?: BLOCK)?-----[^-]{0,10000}?-----END \1PRIVATE KEY(?: BLOCK)?-----/g },
5386
- // PuTTY keys are not PEM. Match the Private-Lines payload — the actual key
5387
- // material — rather than spanning the whole file, which would need the same
5388
- // unbounded gap the rule above just removed. Line-anchored and base64-only,
5389
- // so it cannot run away.
5390
- { label: "putty-key", pattern: /^Private-Lines:[ \t]*\d{1,4}[\r\n]+(?:[A-Za-z0-9+/=]{8,120}[\r\n]+){1,400}/gm },
5391
- // npm tokens (uuid-shaped — overlap with our UUID filter; intentional).
5392
- { label: "npm-token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/g },
5393
- // .npmrc auth lines: //registry.npmjs.org/:_authToken=VALUE (the key is not
5394
- // ALL-CAPS, so the env-secret rule above never matched it). The leading `:`
5395
- // is required — it is what makes this npmrc syntax rather than any shell var
5396
- // ending in _AUTH, which env-secret already owns and labels better.
5397
- // [^\s"'`]+ so the closing quote is not swallowed, and a ${VAR} placeholder
5398
- // is left alone — redacting it destroys a config example and hides nothing.
5399
- // The rule the perf profile caught the wedged daemon inside: 98.94% of samples.
5400
- { label: "npmrc-auth", secret: 1, pattern: /:_(?:authToken|password|auth)[ \t]*=[ \t]*(?!\$\{)([^\s"'`]{12,})/gi },
5401
- // HTTP Authorization headers. env-secret requires an ALL-CAPS name, and
5402
- // `Authorization` is mixed case, so both Bearer and Basic walked straight
5403
- // through — including short opaque tokens the contextual pass also misses
5404
- // (it needs 32+ chars AND mixed-case-plus-digit).
5405
- // Same two guards as npmrc-auth: stop at a quote, and skip the obvious
5406
- // placeholders that fill documentation (<YOUR_TOKEN>, ${TOKEN}, xxx…).
5407
- { label: "auth-header", secret: 1, pattern: /\bauthorization[ \t]*:[ \t]*(?:bearer|basic|token)[ \t]+(?![<$])([^\s"'`<]{8,})/gi },
5408
- // Vendor tokens with unambiguous prefixes. Each is a fixed literal, so a
5409
- // false positive is essentially impossible and the cost of listing is low.
5410
- { label: "gitlab-pat", pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
5411
- { label: "slack-app-token", pattern: /\bxapp-\d-[A-Z0-9]{8,}-\d{10,}-[a-f0-9]{32,}\b/g },
5412
- { label: "sendgrid-key", pattern: /\bSG\.[A-Za-z0-9_-]{20,24}\.[A-Za-z0-9_-]{40,50}\b/g },
5413
- // (?<![-\w]) so `cache-key-<md5>` is not read as a mailgun key.
5414
- { label: "mailgun-key", pattern: /(?<![-\w])key-[a-f0-9]{32}\b/g },
5415
- { label: "digitalocean-pat", pattern: /\bdop_v1_[a-f0-9]{64}\b/g },
5416
- { label: "huggingface-token", pattern: /\bhf_[A-Za-z0-9]{30,}\b/g },
5417
- { label: "shopify-token", pattern: /\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}\b/g },
5418
- { label: "pypi-token", pattern: /\bpypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{50,}\b/g },
5419
- // No twilio-sid rule: AC…/SK… are Twilio SIDs, which are public account and
5420
- // key identifiers, not the auth token. Redacting them hid nothing and
5421
- // collided with 32-hex etags and MD5 sums.
5422
- // No discord-bot-token rule either: the shape (word.word.word) matched
5423
- // ordinary dotted symbols such as a.b.ApplicationContextProviderImpl.
5424
- { label: "telegram-bot-token", pattern: /\b\d{8,10}:AA[A-Za-z0-9_-]{32,}\b/g },
5425
- { label: "doppler-token", pattern: /\bdp\.(?:pt|st|sa|ct)\.[A-Za-z0-9_-]{20,}\b/g },
5426
- // Azure Storage SAS signature — the `sig=` parameter is the actual secret.
5427
- { label: "azure-sas", secret: 1, pattern: /[?&]sig=([A-Za-z0-9%/+=]{20,})/g }
5428
- ];
5429
- SECRET_CONTEXT_RE = /\b(?:secret|password|passwd|credential|access[ _-]?key|private[ _-]?key|api[ _-]?key|auth(?:oriz\w*)?|bearer|token)\b/gi;
5430
- HIGH_ENTROPY_TOKEN_RE = /[A-Za-z0-9/+_-]{32,}/g;
5431
- cache = null;
5432
- CANARY = "the quick brown fox jumps over the lazy dog 0123456789 commit 8ba1f109551bd432803012645ac136ddd475d0fb src/index.ts:42";
5433
- MAX_PACK_RULES = 200;
5434
- MAX_PATTERN_LEN = 512;
5435
- serverPack = null;
5436
- globalRegexCache = /* @__PURE__ */ new Map();
5437
- privacyCache = null;
5438
- }
5439
- });
5440
-
5441
- // ../engine/dist/parsers/chunker.js
5442
- function stripInjectedBanners(text) {
5443
- let result = text;
5444
- for (const re of INJECTED_BANNERS)
5445
- result = result.replace(re, " ");
5446
- return result;
5447
- }
5448
- function cleanText(text) {
5449
- let result = text.replace(/<system-reminder>.*?<\/system-reminder>/gs, "");
5450
- result = stripInjectedBanners(result);
5451
- result = result.replace(/\x1b\[[0-9;]*m/g, "");
5452
- result = result.replace(/\s+/g, " ");
5453
- return result.trim();
5454
- }
5455
- function windowText(text, maxChars, maxWindows = 12) {
5456
- const clean = cleanText(text);
5457
- if (clean.length <= maxChars)
5458
- return clean ? [clean] : [];
5459
- const out = [];
5460
- const overlap = 80;
5461
- const step = Math.max(1, maxChars - overlap);
5462
- for (let i = 0; i < clean.length && out.length < maxWindows; i += step) {
5463
- out.push(clean.slice(i, i + maxChars));
5464
- }
5465
- return out;
5466
- }
5467
- function chunkSession(entry, content, maxChunks = 80, maxTokensPerChunk = 500) {
5468
- const chunks = [];
5469
- const maxChars = maxTokensPerChunk * 4;
5470
- const baseData = {
5471
- sessionId: entry.sessionId,
5472
- projectPath: entry.projectPath,
5473
- created: entry.created,
5474
- modified: entry.modified,
5475
- mtime: entry.fileMtime,
5476
- firstPrompt: entry.firstPrompt || content.firstPrompt,
5477
- sourceFile: content.sessionPath
5478
- };
5479
- for (let i = 0; i < Math.min(2, content.summaries.length); i++) {
5480
- const summary = content.summaries[i];
5481
- if (!summary?.trim())
5482
- continue;
5483
- const chunkText = cleanText(summary.slice(0, maxChars));
5484
- if (chunkText.length > 8e3) {
5485
- chunks.push({
5486
- chunkId: `${entry.sessionId}_summary_${i}`,
5487
- chunkType: "summary",
5488
- text: chunkText.slice(0, 8e3),
5489
- sourceLine: 0,
5490
- ...baseData
5491
- });
5492
- } else {
5493
- chunks.push({
5494
- chunkId: `${entry.sessionId}_summary_${i}`,
5495
- chunkType: "summary",
5496
- text: chunkText,
5497
- sourceLine: 0,
5498
- ...baseData
5499
- });
5500
- }
5501
- }
5502
- if (chunks.length >= maxChunks) {
5503
- return chunks.slice(0, maxChunks);
5504
- }
5505
- const firstPrompt = entry.firstPrompt || content.firstPrompt;
5506
- if (firstPrompt?.trim()) {
5507
- const firstLine = content.userMessages[0]?.lineNumber || 0;
5508
- chunks.push({
5509
- chunkId: `${entry.sessionId}_first_prompt`,
5510
- chunkType: "first_prompt",
5511
- text: cleanText(firstPrompt.slice(0, maxChars)),
5512
- sourceLine: firstLine,
5513
- ...baseData
5514
- });
5515
- }
5516
- if (chunks.length >= maxChunks) {
5517
- return chunks.slice(0, maxChunks);
5518
- }
5519
- const userStart = firstPrompt ? 1 : 0;
5520
- for (let i = userStart; i < content.userMessages.length; i++) {
5521
- if (chunks.length >= maxChunks)
5522
- break;
5523
- const msg = content.userMessages[i];
5524
- if (!msg.text.trim())
5525
- continue;
5526
- const windows = windowText(msg.text, maxChars);
5527
- for (let w = 0; w < windows.length; w++) {
5528
- if (chunks.length >= maxChunks)
5529
- break;
5530
- chunks.push({
5531
- chunkId: `${entry.sessionId}_user_${i}_${w}`,
5532
- chunkType: "user_context",
5533
- text: windows[w],
5534
- sourceLine: msg.lineNumber,
5535
- ...baseData
5536
- });
5537
- }
5538
- }
5539
- if (chunks.length >= maxChunks) {
5540
- return chunks.slice(0, maxChunks);
5541
- }
5542
- for (let i = 0; i < content.assistantMessages.length; i++) {
5543
- if (chunks.length >= maxChunks)
5544
- break;
5545
- const msg = content.assistantMessages[i];
5546
- if (!msg.text.trim() || msg.text.length <= 50)
5547
- continue;
5548
- const windows = windowText(msg.text, maxChars);
5549
- for (let w = 0; w < windows.length; w++) {
5550
- if (chunks.length >= maxChunks)
5551
- break;
5552
- chunks.push({
5553
- chunkId: `${entry.sessionId}_assistant_${i}_${w}`,
5554
- chunkType: "assistant",
5555
- text: windows[w],
5556
- sourceLine: msg.lineNumber,
5557
- ...baseData
5558
- });
5559
- }
5560
- }
5561
- if (chunks.length >= maxChunks) {
5562
- return chunks.slice(0, maxChunks);
5563
- }
5564
- if (content.toolResults.length > 0) {
5565
- const webSearches = content.toolResults.filter((r) => r.contentType === "web_search");
5566
- const otherResults = content.toolResults.filter((r) => r.contentType !== "web_search");
5567
- const resultsToProcess = [...webSearches.slice(0, 5), ...otherResults.slice(0, 10)];
5568
- const stripBodies = isToolOutputRedactionEnabled();
5569
- for (let i = 0; i < resultsToProcess.length; i++) {
5570
- if (chunks.length >= maxChunks)
5571
- break;
5572
- const result = resultsToProcess[i];
5573
- if (result.text.trim()) {
5574
- const body = stripBodies && result.contentType !== "web_search" ? redactToolResultBody(result.contentType, result.text) : result.text.slice(0, maxChars);
5575
- chunks.push({
5576
- chunkId: `${entry.sessionId}_${result.contentType}_${i}`,
5577
- chunkType: result.contentType,
5578
- text: cleanText(body),
5579
- sourceLine: result.lineNumber,
5580
- ...baseData
5581
- });
5582
- }
5583
- }
5584
- }
5585
- return chunks.slice(0, maxChunks);
5586
- }
5587
- var INJECTED_BANNERS;
5588
- var init_chunker = __esm({
5589
- "../engine/dist/parsers/chunker.js"() {
5590
- "use strict";
5591
- init_secret_redactor();
5592
- INJECTED_BANNERS = [
5593
- /MCP issues detected\. ?Run \/mcp list for status\.?/g,
5594
- /Context low[^\n]*Run \/compact[^\n]*/g,
5595
- /API Error:[^\n]{0,120}/g
5596
- ];
5597
- }
5598
- });
5599
-
5600
5053
  // ../engine/dist/core/home-approval.js
5601
5054
  import { existsSync as existsSync3, readdirSync as readdirSync2, realpathSync } from "fs";
5602
5055
  import { homedir as homedir2 } from "os";
@@ -6141,7 +5594,7 @@ var init_model_pricing = __esm({
6141
5594
  });
6142
5595
 
6143
5596
  // ../engine/dist/parsers/session.js
6144
- import { createReadStream, existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync2, statSync as statSync4, openSync, readSync, closeSync } from "fs";
5597
+ import { createReadStream, existsSync as existsSync5, readdirSync as readdirSync5, readFileSync as readFileSync2, statSync as statSync3, openSync, readSync, closeSync } from "fs";
6145
5598
  import { createInterface } from "readline";
6146
5599
  import { join as join5, basename as basename2 } from "path";
6147
5600
  function removeCodeBlocks(text) {
@@ -6188,44 +5641,6 @@ function isValuableToolResult(content, toolName = "") {
6188
5641
  }
6189
5642
  return true;
6190
5643
  }
6191
- function parseSessionsIndex(indexPath) {
6192
- if (!existsSync5(indexPath)) {
6193
- return [];
6194
- }
6195
- const data = JSON.parse(readFileSync2(indexPath, "utf-8"));
6196
- const entries = data.entries || [];
6197
- return entries.map((e) => ({
6198
- sessionId: e.sessionId || "",
6199
- fullPath: e.fullPath || "",
6200
- fileMtime: e.fileMtime || 0,
6201
- firstPrompt: e.firstPrompt || "",
6202
- messageCount: e.messageCount || 0,
6203
- created: e.created || "",
6204
- modified: e.modified || "",
6205
- gitBranch: e.gitBranch || "",
6206
- projectPath: e.projectPath || "",
6207
- isSidechain: e.isSidechain || false
6208
- }));
6209
- }
6210
- function* iterAllSessionIndices(claudeDir) {
6211
- const dir = claudeDir || claudeBackend.projectsDir();
6212
- if (!existsSync5(dir)) {
6213
- return;
6214
- }
6215
- const entries = readdirSync5(dir, { withFileTypes: true });
6216
- for (const entry of entries) {
6217
- if (!entry.isDirectory())
6218
- continue;
6219
- const projectDir = join5(dir, entry.name);
6220
- const indexPath = join5(projectDir, "sessions-index.json");
6221
- if (existsSync5(indexPath)) {
6222
- const sessions = parseSessionsIndex(indexPath);
6223
- if (sessions.length > 0) {
6224
- yield [projectDir, sessions];
6225
- }
6226
- }
6227
- }
6228
- }
6229
5644
  function hasSubagentsDir(sessionPath) {
6230
5645
  const dir = sessionPath.slice(0, -6);
6231
5646
  return existsSync5(join5(dir, "subagents"));
@@ -6236,40 +5651,6 @@ function getSubagentPaths(sessionPath) {
6236
5651
  return [];
6237
5652
  return readdirSync5(subagentsDir).filter((f) => f.endsWith(".jsonl")).sort().map((f) => join5(subagentsDir, f));
6238
5653
  }
6239
- function getSubagentProjectPath(sessionPath, folderDerivedPath) {
6240
- const subagentPaths = getSubagentPaths(sessionPath);
6241
- if (subagentPaths.length === 0)
6242
- return folderDerivedPath;
6243
- const cwdCounts = /* @__PURE__ */ new Map();
6244
- for (const filePath of subagentPaths) {
6245
- try {
6246
- const lines = readFileSync2(filePath, "utf-8").split("\n");
6247
- for (const line of lines) {
6248
- if (!line.trim())
6249
- continue;
6250
- try {
6251
- const entry = JSON.parse(line);
6252
- if (entry.type === "user" && typeof entry.cwd === "string" && entry.cwd) {
6253
- cwdCounts.set(entry.cwd, (cwdCounts.get(entry.cwd) ?? 0) + 1);
6254
- }
6255
- } catch {
6256
- }
6257
- }
6258
- } catch {
6259
- }
6260
- }
6261
- if (cwdCounts.size === 0)
6262
- return folderDerivedPath;
6263
- let best = folderDerivedPath;
6264
- let bestCount = 0;
6265
- for (const [cwd, count] of cwdCounts) {
6266
- if (count > bestCount) {
6267
- best = cwd;
6268
- bestCount = count;
6269
- }
6270
- }
6271
- return best;
6272
- }
6273
5654
  function readCwdFromJsonl(sessionPath) {
6274
5655
  try {
6275
5656
  if (!existsSync5(sessionPath))
@@ -6298,22 +5679,6 @@ function readCwdFromJsonl(sessionPath) {
6298
5679
  }
6299
5680
  return "";
6300
5681
  }
6301
- function getSessionMtime(sessionPath) {
6302
- const subagentPaths = getSubagentPaths(sessionPath);
6303
- if (subagentPaths.length === 0) {
6304
- return statSync4(sessionPath).mtimeMs;
6305
- }
6306
- let maxMtime = statSync4(sessionPath).mtimeMs;
6307
- for (const p of subagentPaths) {
6308
- try {
6309
- const m = statSync4(p).mtimeMs;
6310
- if (m > maxMtime)
6311
- maxMtime = m;
6312
- } catch {
6313
- }
6314
- }
6315
- return maxMtime;
6316
- }
6317
5682
  async function parseSessionFile(sessionPath, maxMessages = 50) {
6318
5683
  const sessionId = basename2(sessionPath, ".jsonl");
6319
5684
  const content = {
@@ -6549,78 +5914,12 @@ ${resultText}` : resultText;
6549
5914
  cacheReadTokens: content.metadata.cacheReadTokens,
6550
5915
  cacheCreationTokens: content.metadata.cacheCreationTokens
6551
5916
  });
6552
- return content;
6553
- }
6554
- function* getAllSessions(claudeDir) {
6555
- const seenSessionIds = /* @__PURE__ */ new Set();
6556
- const projectDirs = claudeDir ? [claudeDir] : claudeProjectDirs();
6557
- for (const dir of projectDirs) {
6558
- for (const [, entries] of iterAllSessionIndices(dir)) {
6559
- for (const entry of entries) {
6560
- if (!seenSessionIds.has(entry.sessionId) && existsSync5(entry.fullPath)) {
6561
- seenSessionIds.add(entry.sessionId);
6562
- yield [entry, entry.fullPath];
6563
- }
6564
- }
6565
- }
6566
- if (!existsSync5(dir))
6567
- continue;
6568
- const dirEntries = readdirSync5(dir, { withFileTypes: true });
6569
- for (const projDirEntry of dirEntries) {
6570
- if (!projDirEntry.isDirectory())
6571
- continue;
6572
- const projectPath = join5(dir, projDirEntry.name);
6573
- const files = readdirSync5(projectPath);
6574
- for (const file of files) {
6575
- if (!file.endsWith(".jsonl"))
6576
- continue;
6577
- if (file.startsWith("agent-"))
6578
- continue;
6579
- if (file === "sessions-index.json")
6580
- continue;
6581
- const sessionId = basename2(file, ".jsonl");
6582
- if (seenSessionIds.has(sessionId))
6583
- continue;
6584
- const fullPath = join5(projectPath, file);
6585
- try {
6586
- const stat3 = statSync4(fullPath);
6587
- const derivedProjectPath = resolveProjectDirName(projDirEntry.name);
6588
- const effectiveMtime = getSessionMtime(fullPath);
6589
- let projectPath2 = "";
6590
- if (hasSubagentsDir(fullPath)) {
6591
- projectPath2 = getSubagentProjectPath(fullPath, derivedProjectPath);
6592
- } else {
6593
- const fromJsonl = readCwdFromJsonl(fullPath);
6594
- if (fromJsonl)
6595
- projectPath2 = fromJsonl;
6596
- }
6597
- if (!projectPath2)
6598
- projectPath2 = derivedProjectPath;
6599
- const entry = {
6600
- sessionId,
6601
- fullPath,
6602
- fileMtime: effectiveMtime,
6603
- firstPrompt: "",
6604
- messageCount: 0,
6605
- created: stat3.birthtime.toISOString(),
6606
- modified: new Date(effectiveMtime).toISOString(),
6607
- gitBranch: "",
6608
- projectPath: projectPath2,
6609
- isSidechain: false
6610
- };
6611
- seenSessionIds.add(sessionId);
6612
- yield [entry, fullPath];
6613
- } catch {
6614
- continue;
6615
- }
6616
- }
6617
- }
6618
- }
5917
+ return content;
6619
5918
  }
6620
5919
  var init_session = __esm({
6621
5920
  "../engine/dist/parsers/session.js"() {
6622
5921
  "use strict";
6623
- init_chunker();
5922
+ init_banners();
6624
5923
  init_claude2();
6625
5924
  init_tool_paths();
6626
5925
  init_project_dir_name();
@@ -7286,7 +6585,7 @@ var init_raw = __esm({
7286
6585
  // ../engine/dist/transcript/shadow.js
7287
6586
  import { gzipSync as gzipSync2, gunzipSync as gunzipSync2 } from "zlib";
7288
6587
  import { createHash as createHash2 } from "crypto";
7289
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync as renameSync2, statSync as statSync5 } from "fs";
6588
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2, renameSync as renameSync2, statSync as statSync4, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
7290
6589
  import { dirname as dirname7, join as join8 } from "path";
7291
6590
  function shadowRoot() {
7292
6591
  return join8(getDataDir(), "shadow");
@@ -7494,7 +6793,7 @@ function liveFingerprint(backend, rawId) {
7494
6793
  const loc = backend.findSession(rawId);
7495
6794
  if (!loc?.path)
7496
6795
  return "";
7497
- const st = statSync5(loc.path);
6796
+ const st = statSync4(loc.path);
7498
6797
  return `${st.mtimeMs}:${st.size}`;
7499
6798
  } catch {
7500
6799
  return "";
@@ -7510,9 +6809,32 @@ function seedShadow(sessionId, container) {
7510
6809
  }
7511
6810
  return fullest;
7512
6811
  }
6812
+ function shadowUncompressedBytes(tool, rawId) {
6813
+ const path = shadowFileFor(tool, rawId);
6814
+ let fd = null;
6815
+ try {
6816
+ const size = statSync4(path).size;
6817
+ if (size < 18)
6818
+ return 0;
6819
+ fd = openSync2(path, "r");
6820
+ const tail = Buffer.alloc(4);
6821
+ if (readSync2(fd, tail, 0, 4, size - 4) !== 4)
6822
+ return 0;
6823
+ return tail.readUInt32LE(0);
6824
+ } catch {
6825
+ return 0;
6826
+ } finally {
6827
+ if (fd !== null) {
6828
+ try {
6829
+ closeSync2(fd);
6830
+ } catch {
6831
+ }
6832
+ }
6833
+ }
6834
+ }
7513
6835
  function shadowSizeOnDisk(tool, rawId) {
7514
6836
  try {
7515
- return statSync5(shadowFileFor(tool, rawId)).size;
6837
+ return statSync4(shadowFileFor(tool, rawId)).size;
7516
6838
  } catch {
7517
6839
  return 0;
7518
6840
  }
@@ -7530,7 +6852,7 @@ var init_shadow = __esm({
7530
6852
  });
7531
6853
 
7532
6854
  // ../engine/dist/core/live-session-scan.js
7533
- import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync4, statSync as statSync6, openSync as openSync2, readSync as readSync2, closeSync as closeSync2 } from "fs";
6855
+ import { existsSync as existsSync7, readdirSync as readdirSync6, readFileSync as readFileSync4, statSync as statSync5, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
7534
6856
  import { join as join9, basename as basename5 } from "path";
7535
6857
  function detectTool(sessionId) {
7536
6858
  return getBackendForId(sessionId)?.id ?? "claude";
@@ -7557,6 +6879,15 @@ function sessionFileIndex() {
7557
6879
  return indexCache;
7558
6880
  const byId = /* @__PURE__ */ new Map();
7559
6881
  const seenRoots = /* @__PURE__ */ new Set();
6882
+ const projectPaths = /* @__PURE__ */ new Map();
6883
+ const projectPathOf = (dirName) => {
6884
+ let p = projectPaths.get(dirName);
6885
+ if (p === void 0) {
6886
+ p = resolveProjectDirName(dirName);
6887
+ projectPaths.set(dirName, p);
6888
+ }
6889
+ return p;
6890
+ };
7560
6891
  for (const root of claudeProjectDirs()) {
7561
6892
  if (seenRoots.has(root) || !existsSync7(root))
7562
6893
  continue;
@@ -7584,7 +6915,7 @@ function sessionFileIndex() {
7584
6915
  const located = {
7585
6916
  path: join9(dir, name),
7586
6917
  projectDir: entry.name,
7587
- projectPath: resolveProjectDirName(entry.name)
6918
+ projectPath: projectPathOf(entry.name)
7588
6919
  };
7589
6920
  const prior = byId.get(id);
7590
6921
  if (prior)
@@ -7648,7 +6979,7 @@ function readSessionGroupText(group) {
7648
6979
  const parts = [];
7649
6980
  for (const p of group.paths) {
7650
6981
  try {
7651
- const st = statSync6(p);
6982
+ const st = statSync5(p);
7652
6983
  parts.push(`${p}:${st.mtimeMs}:${st.size}`);
7653
6984
  } catch {
7654
6985
  parts.push(`${p}:missing`);
@@ -7668,7 +6999,7 @@ function readSessionGroupText(group) {
7668
6999
  continue;
7669
7000
  }
7670
7001
  try {
7671
- mtime = Math.max(mtime, statSync6(p).mtimeMs);
7002
+ mtime = Math.max(mtime, statSync5(p).mtimeMs);
7672
7003
  } catch {
7673
7004
  }
7674
7005
  text = text ? mergeLineText(text, raw).text : raw;
@@ -8266,7 +7597,7 @@ var init_flat_string = __esm({
8266
7597
  });
8267
7598
 
8268
7599
  // ../engine/dist/core/first-prompt.js
8269
- import { createReadStream as createReadStream2, existsSync as existsSync9, openSync as openSync3, readSync as readSync3, closeSync as closeSync3 } from "fs";
7600
+ import { createReadStream as createReadStream2, existsSync as existsSync9, openSync as openSync4, readSync as readSync4, closeSync as closeSync4 } from "fs";
8270
7601
  import { StringDecoder } from "string_decoder";
8271
7602
  function stripBanners2(s) {
8272
7603
  let out = s;
@@ -8320,7 +7651,7 @@ function extractFirstUserPromptSync(filePath, options = {}) {
8320
7651
  const maxBytes = options.maxBytes ?? 4 * 1024 * 1024;
8321
7652
  let fd;
8322
7653
  try {
8323
- fd = openSync3(filePath, "r");
7654
+ fd = openSync4(filePath, "r");
8324
7655
  } catch {
8325
7656
  return "";
8326
7657
  }
@@ -8332,7 +7663,7 @@ function extractFirstUserPromptSync(filePath, options = {}) {
8332
7663
  let pos = 0;
8333
7664
  let lineNo = 0;
8334
7665
  for (; ; ) {
8335
- const n = readSync3(fd, buf, 0, CHUNK, pos);
7666
+ const n = readSync4(fd, buf, 0, CHUNK, pos);
8336
7667
  if (n <= 0)
8337
7668
  break;
8338
7669
  pos += n;
@@ -8358,7 +7689,7 @@ function extractFirstUserPromptSync(filePath, options = {}) {
8358
7689
  return "";
8359
7690
  } finally {
8360
7691
  try {
8361
- closeSync3(fd);
7692
+ closeSync4(fd);
8362
7693
  } catch {
8363
7694
  }
8364
7695
  }
@@ -8377,24 +7708,24 @@ var init_first_prompt = __esm({
8377
7708
  });
8378
7709
 
8379
7710
  // ../engine/dist/core/backends/tail-read.js
8380
- import { openSync as openSync4, readSync as readSync4, closeSync as closeSync4, statSync as statSync7 } from "node:fs";
8381
- function readTailFromOffset(path, offset) {
7711
+ import { openSync as openSync5, readSync as readSync5, closeSync as closeSync5, statSync as statSync6 } from "node:fs";
7712
+ function readTailFromOffset(path, offset, maxBytes = TAIL_READ_MAX_BYTES) {
8382
7713
  let size = 0;
8383
7714
  try {
8384
- size = statSync7(path).size;
7715
+ size = statSync6(path).size;
8385
7716
  } catch {
8386
7717
  return { text: "", newOffset: offset };
8387
7718
  }
8388
7719
  if (size <= offset)
8389
7720
  return { text: "", newOffset: offset };
8390
- const len = Math.min(size - offset, 16 * 1024 * 1024);
7721
+ const len = Math.min(size - offset, Math.max(1, maxBytes));
8391
7722
  const buf = Buffer.allocUnsafe(len);
8392
7723
  let fd = null;
8393
7724
  try {
8394
- fd = openSync4(path, "r");
7725
+ fd = openSync5(path, "r");
8395
7726
  let got = 0;
8396
7727
  while (got < len) {
8397
- const n = readSync4(fd, buf, got, len - got, offset + got);
7728
+ const n = readSync5(fd, buf, got, len - got, offset + got);
8398
7729
  if (n <= 0)
8399
7730
  break;
8400
7731
  got += n;
@@ -8403,25 +7734,47 @@ function readTailFromOffset(path, offset) {
8403
7734
  return { text: "", newOffset: offset };
8404
7735
  const slice = got < len ? buf.subarray(0, got) : buf;
8405
7736
  const lastNl = slice.lastIndexOf(10);
8406
- if (lastNl < 0) {
8407
- return { text: "", newOffset: offset };
7737
+ if (lastNl >= 0) {
7738
+ const text = slice.subarray(0, lastNl + 1).toString("utf-8");
7739
+ return { text, newOffset: offset + lastNl + 1 };
8408
7740
  }
8409
- const text = slice.subarray(0, lastNl + 1).toString("utf-8");
8410
- return { text, newOffset: offset + lastNl + 1 };
7741
+ if (offset + got >= size)
7742
+ return { text: "", newOffset: offset };
7743
+ const end = findNewline(fd, offset + got, size);
7744
+ if (end < 0)
7745
+ return { text: "", newOffset: offset };
7746
+ return { text: "", newOffset: end + 1, skippedBytes: end + 1 - offset };
8411
7747
  } catch {
8412
7748
  return { text: "", newOffset: offset };
8413
7749
  } finally {
8414
7750
  if (fd !== null) {
8415
7751
  try {
8416
- closeSync4(fd);
7752
+ closeSync5(fd);
8417
7753
  } catch {
8418
7754
  }
8419
7755
  }
8420
7756
  }
8421
7757
  }
7758
+ function findNewline(fd, from, size) {
7759
+ const block = Buffer.allocUnsafe(SKIP_SCAN_BYTES);
7760
+ let pos = from;
7761
+ while (pos < size) {
7762
+ const n = readSync5(fd, block, 0, Math.min(SKIP_SCAN_BYTES, size - pos), pos);
7763
+ if (n <= 0)
7764
+ return -1;
7765
+ const i = block.subarray(0, n).indexOf(10);
7766
+ if (i >= 0)
7767
+ return pos + i;
7768
+ pos += n;
7769
+ }
7770
+ return -1;
7771
+ }
7772
+ var TAIL_READ_MAX_BYTES, SKIP_SCAN_BYTES;
8422
7773
  var init_tail_read = __esm({
8423
7774
  "../engine/dist/core/backends/tail-read.js"() {
8424
7775
  "use strict";
7776
+ TAIL_READ_MAX_BYTES = 16 * 1024 * 1024;
7777
+ SKIP_SCAN_BYTES = 1024 * 1024;
8425
7778
  }
8426
7779
  });
8427
7780
 
@@ -8698,7 +8051,7 @@ var init_generic_engine = __esm({
8698
8051
  });
8699
8052
 
8700
8053
  // ../engine/dist/core/backends/claude.js
8701
- import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync5, statSync as statSync8, openSync as openSync5, readSync as readSync5, closeSync as closeSync5 } from "fs";
8054
+ import { existsSync as existsSync10, readdirSync as readdirSync7, readFileSync as readFileSync5, statSync as statSync7, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
8702
8055
  import { join as join10, basename as basename8 } from "path";
8703
8056
  function stripReminders(text) {
8704
8057
  return text.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
@@ -8795,13 +8148,13 @@ var init_claude2 = __esm({
8795
8148
  return null;
8796
8149
  let head;
8797
8150
  try {
8798
- const fd = openSync5(located.path, "r");
8151
+ const fd = openSync6(located.path, "r");
8799
8152
  try {
8800
8153
  const buf = Buffer.alloc(65536);
8801
- const n = readSync5(fd, buf, 0, buf.length, 0);
8154
+ const n = readSync6(fd, buf, 0, buf.length, 0);
8802
8155
  head = buf.toString("utf-8", 0, n);
8803
8156
  } finally {
8804
- closeSync5(fd);
8157
+ closeSync6(fd);
8805
8158
  }
8806
8159
  } catch {
8807
8160
  return null;
@@ -8828,7 +8181,7 @@ var init_claude2 = __esm({
8828
8181
  let mtime = 0;
8829
8182
  for (const c of copies) {
8830
8183
  try {
8831
- mtime = Math.max(mtime, statSync8(c.path).mtimeMs);
8184
+ mtime = Math.max(mtime, statSync7(c.path).mtimeMs);
8832
8185
  } catch {
8833
8186
  }
8834
8187
  }
@@ -8904,7 +8257,7 @@ var init_claude2 = __esm({
8904
8257
  const fullPath = join10(projPath, f);
8905
8258
  let stat3;
8906
8259
  try {
8907
- stat3 = statSync8(fullPath);
8260
+ stat3 = statSync7(fullPath);
8908
8261
  } catch {
8909
8262
  continue;
8910
8263
  }
@@ -9125,7 +8478,7 @@ var init_claude2 = __esm({
9125
8478
  const events = this.readEvents(rawId);
9126
8479
  let fileMtime = 0;
9127
8480
  try {
9128
- fileMtime = statSync8(located.path).mtimeMs;
8481
+ fileMtime = statSync7(located.path).mtimeMs;
9129
8482
  } catch {
9130
8483
  }
9131
8484
  return liveScanEditsFromEvents(events, this.fileToolMap, {
@@ -9180,13 +8533,13 @@ var init_claude2 = __esm({
9180
8533
  continue;
9181
8534
  const fp = join10(projPath, f);
9182
8535
  try {
9183
- const st = statSync8(fp);
8536
+ const st = statSync7(fp);
9184
8537
  let mtime = st.mtimeMs;
9185
8538
  const subDir = join10(projPath, f.slice(0, -6), "subagents");
9186
8539
  if (existsSync10(subDir)) {
9187
8540
  for (const sub of readdirSync7(subDir)) {
9188
8541
  try {
9189
- const subSt = statSync8(join10(subDir, sub));
8542
+ const subSt = statSync7(join10(subDir, sub));
9190
8543
  if (subSt.mtimeMs > mtime)
9191
8544
  mtime = subSt.mtimeMs;
9192
8545
  } catch {
@@ -9251,7 +8604,7 @@ var init_claude2 = __esm({
9251
8604
  let total = 0;
9252
8605
  for (const located of findSessionFiles(this.toRawId(prefixedId))) {
9253
8606
  try {
9254
- total += statSync8(located.path).size;
8607
+ total += statSync7(located.path).size;
9255
8608
  } catch {
9256
8609
  }
9257
8610
  }
@@ -9273,14 +8626,14 @@ var init_claude2 = __esm({
9273
8626
  spansMultipleSources(prefixedId) {
9274
8627
  return findSessionFiles(this.toRawId(prefixedId)).length > 1;
9275
8628
  }
9276
- async readFromOffset(prefixedId, offset) {
8629
+ async readFromOffset(prefixedId, offset, maxBytes) {
9277
8630
  const rawId = this.toRawId(prefixedId);
9278
8631
  const copies = findSessionFiles(rawId);
9279
8632
  if (copies.length === 0)
9280
8633
  return { text: "", newOffset: offset };
9281
8634
  if (copies.length > 1)
9282
8635
  return { text: "", newOffset: offset };
9283
- return readTailFromOffset(copies[0].path, offset);
8636
+ return readTailFromOffset(copies[0].path, offset, maxBytes);
9284
8637
  }
9285
8638
  };
9286
8639
  claudeBackend = new ClaudeBackend();
@@ -9864,7 +9217,7 @@ var init_opencode = __esm({
9864
9217
  });
9865
9218
 
9866
9219
  // ../engine/dist/core/backends/codex.js
9867
- import { readdirSync as readdirSync8, readFileSync as readFileSync6, statSync as statSync9, openSync as openSync6, readSync as readSync6, closeSync as closeSync6 } from "fs";
9220
+ import { readdirSync as readdirSync8, readFileSync as readFileSync6, statSync as statSync8, openSync as openSync7, readSync as readSync7, closeSync as closeSync7 } from "fs";
9868
9221
  import { basename as basename9, dirname as dirname10, join as join12 } from "path";
9869
9222
  function parseShellWrites(cmd) {
9870
9223
  const out = [];
@@ -10072,7 +9425,7 @@ var init_codex2 = __esm({
10072
9425
  return null;
10073
9426
  let mtime = 0;
10074
9427
  try {
10075
- mtime = statSync9(located.path).mtimeMs;
9428
+ mtime = statSync8(located.path).mtimeMs;
10076
9429
  } catch {
10077
9430
  }
10078
9431
  return {
@@ -10092,7 +9445,7 @@ var init_codex2 = __esm({
10092
9445
  for (const filePath of walkRollouts(root)) {
10093
9446
  let stat3;
10094
9447
  try {
10095
- stat3 = statSync9(filePath);
9448
+ stat3 = statSync8(filePath);
10096
9449
  } catch {
10097
9450
  continue;
10098
9451
  }
@@ -10288,7 +9641,7 @@ var init_codex2 = __esm({
10288
9641
  let mtime = 0;
10289
9642
  try {
10290
9643
  raw = readFileSync6(path, "utf-8");
10291
- mtime = statSync9(path).mtimeMs;
9644
+ mtime = statSync8(path).mtimeMs;
10292
9645
  } catch {
10293
9646
  return [];
10294
9647
  }
@@ -10388,7 +9741,7 @@ var init_codex2 = __esm({
10388
9741
  const events = this.readEvents(this.toRawId(id));
10389
9742
  let fileMtime = 0;
10390
9743
  try {
10391
- fileMtime = statSync9(located.path).mtimeMs;
9744
+ fileMtime = statSync8(located.path).mtimeMs;
10392
9745
  } catch {
10393
9746
  }
10394
9747
  return liveScanEditsFromEvents(events, this.fileToolMap, {
@@ -10452,7 +9805,7 @@ var init_codex2 = __esm({
10452
9805
  const p = join12(dir, entry.name);
10453
9806
  let st;
10454
9807
  try {
10455
- st = statSync9(p);
9808
+ st = statSync8(p);
10456
9809
  } catch {
10457
9810
  continue;
10458
9811
  }
@@ -10519,7 +9872,7 @@ var init_codex2 = __esm({
10519
9872
  let mtime = 0;
10520
9873
  const push = (path, name) => {
10521
9874
  try {
10522
- const st = statSync9(path);
9875
+ const st = statSync8(path);
10523
9876
  files.push({ name, bytes: readFileSync6(path) });
10524
9877
  if (st.mtimeMs > mtime)
10525
9878
  mtime = st.mtimeMs;
@@ -10538,10 +9891,10 @@ var init_codex2 = __esm({
10538
9891
  if (p === loc.path)
10539
9892
  continue;
10540
9893
  try {
10541
- const fd = openSync6(p, "r");
9894
+ const fd = openSync7(p, "r");
10542
9895
  const buf = Buffer.alloc(4096);
10543
- const n = readSync6(fd, buf, 0, 4096, 0);
10544
- closeSync6(fd);
9896
+ const n = readSync7(fd, buf, 0, 4096, 0);
9897
+ closeSync7(fd);
10545
9898
  const head = buf.toString("utf-8", 0, n);
10546
9899
  if (head.includes(parentId) && head.includes("parent_thread_id"))
10547
9900
  push(p, `subagents/${f}`);
@@ -10561,16 +9914,16 @@ var init_codex2 = __esm({
10561
9914
  if (!loc)
10562
9915
  return 0;
10563
9916
  try {
10564
- return statSync9(loc.path).size;
9917
+ return statSync8(loc.path).size;
10565
9918
  } catch {
10566
9919
  return 0;
10567
9920
  }
10568
9921
  }
10569
- async readFromOffset(prefixedId, offset) {
9922
+ async readFromOffset(prefixedId, offset, maxBytes) {
10570
9923
  const loc = this.findSession(prefixedId);
10571
9924
  if (!loc)
10572
9925
  return { text: "", newOffset: offset };
10573
- return readTailFromOffset(loc.path, offset);
9926
+ return readTailFromOffset(loc.path, offset, maxBytes);
10574
9927
  }
10575
9928
  };
10576
9929
  codexBackend = new CodexBackend();
@@ -10578,7 +9931,7 @@ var init_codex2 = __esm({
10578
9931
  });
10579
9932
 
10580
9933
  // ../engine/dist/core/backends/agy.js
10581
- import { existsSync as existsSync12, readdirSync as readdirSync9, readFileSync as readFileSync7, statSync as statSync10 } from "fs";
9934
+ import { existsSync as existsSync12, readdirSync as readdirSync9, readFileSync as readFileSync7, statSync as statSync9 } from "fs";
10582
9935
  import { basename as basename10, dirname as dirname11, join as join13 } from "path";
10583
9936
  function agyTranscriptPath(logsDir) {
10584
9937
  const full = join13(logsDir, "transcript_full.jsonl");
@@ -10771,7 +10124,7 @@ var init_agy = __esm({
10771
10124
  return null;
10772
10125
  let mtime = 0;
10773
10126
  try {
10774
- mtime = statSync10(path).mtimeMs;
10127
+ mtime = statSync9(path).mtimeMs;
10775
10128
  } catch {
10776
10129
  }
10777
10130
  const projectPath = resolveAgyProject(dirname11(path), this.homeDir());
@@ -10799,7 +10152,7 @@ var init_agy = __esm({
10799
10152
  for (const rawId of sessionDirs) {
10800
10153
  const sessionPath = join13(brainDir, rawId);
10801
10154
  try {
10802
- if (!statSync10(sessionPath).isDirectory())
10155
+ if (!statSync9(sessionPath).isDirectory())
10803
10156
  continue;
10804
10157
  } catch {
10805
10158
  continue;
@@ -10809,7 +10162,7 @@ var init_agy = __esm({
10809
10162
  continue;
10810
10163
  let stat3;
10811
10164
  try {
10812
- stat3 = statSync10(filePath);
10165
+ stat3 = statSync9(filePath);
10813
10166
  } catch {
10814
10167
  continue;
10815
10168
  }
@@ -11074,7 +10427,7 @@ var init_agy = __esm({
11074
10427
  for (const rawId of sessionDirs) {
11075
10428
  const sessionPath = join13(brainDir, rawId);
11076
10429
  try {
11077
- if (!statSync10(sessionPath).isDirectory())
10430
+ if (!statSync9(sessionPath).isDirectory())
11078
10431
  continue;
11079
10432
  } catch {
11080
10433
  continue;
@@ -11083,7 +10436,7 @@ var init_agy = __esm({
11083
10436
  if (!filePath)
11084
10437
  continue;
11085
10438
  try {
11086
- const st = statSync10(filePath);
10439
+ const st = statSync9(filePath);
11087
10440
  if (st.mtimeMs < opts.sinceMs)
11088
10441
  continue;
11089
10442
  candidates2.push({ rawId, mtime: st.mtimeMs });
@@ -11111,7 +10464,7 @@ var init_agy = __esm({
11111
10464
  try {
11112
10465
  return {
11113
10466
  tool: "agy",
11114
- mtime: statSync10(loc.path).mtimeMs,
10467
+ mtime: statSync9(loc.path).mtimeMs,
11115
10468
  files: [{ name: basename10(loc.path), bytes: readFileSync7(loc.path) }]
11116
10469
  };
11117
10470
  } catch {
@@ -11126,16 +10479,16 @@ var init_agy = __esm({
11126
10479
  if (!loc)
11127
10480
  return 0;
11128
10481
  try {
11129
- return statSync10(loc.path).size;
10482
+ return statSync9(loc.path).size;
11130
10483
  } catch {
11131
10484
  return 0;
11132
10485
  }
11133
10486
  }
11134
- async readFromOffset(prefixedId, offset) {
10487
+ async readFromOffset(prefixedId, offset, maxBytes) {
11135
10488
  const loc = this.findSession(prefixedId);
11136
10489
  if (!loc)
11137
10490
  return { text: "", newOffset: offset };
11138
- return readTailFromOffset(loc.path, offset);
10491
+ return readTailFromOffset(loc.path, offset, maxBytes);
11139
10492
  }
11140
10493
  };
11141
10494
  agyBackend = new AgyBackend();
@@ -11143,7 +10496,7 @@ var init_agy = __esm({
11143
10496
  });
11144
10497
 
11145
10498
  // ../engine/dist/core/backends/cursor-store.js
11146
- import { existsSync as existsSync13, readdirSync as readdirSync10, readFileSync as readFileSync8, statSync as statSync11 } from "fs";
10499
+ import { existsSync as existsSync13, readdirSync as readdirSync10, readFileSync as readFileSync8, statSync as statSync10 } from "fs";
11147
10500
  import { join as join14 } from "path";
11148
10501
  function cursorProjectSlug(absPath) {
11149
10502
  return absPath.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
@@ -11160,7 +10513,7 @@ function listChatsIn(chatsRoot) {
11160
10513
  const hashPath3 = join14(chatsRoot, workspaceHash);
11161
10514
  let chatIds;
11162
10515
  try {
11163
- if (!statSync11(hashPath3).isDirectory())
10516
+ if (!statSync10(hashPath3).isDirectory())
11164
10517
  continue;
11165
10518
  chatIds = readdirSync10(hashPath3);
11166
10519
  } catch {
@@ -11182,7 +10535,7 @@ function listChatsIn(chatsRoot) {
11182
10535
  let created = meta.createdAtMs ?? 0;
11183
10536
  if (!mtime || !created) {
11184
10537
  try {
11185
- const st = statSync11(dbPath);
10538
+ const st = statSync10(dbPath);
11186
10539
  mtime = mtime || st.mtimeMs;
11187
10540
  created = created || st.birthtimeMs || st.mtimeMs;
11188
10541
  } catch {
@@ -11225,7 +10578,7 @@ function findChat(chatsRoots, chatId) {
11225
10578
  let created = meta.createdAtMs ?? 0;
11226
10579
  if (!mtime || !created) {
11227
10580
  try {
11228
- const st = statSync11(dbPath);
10581
+ const st = statSync10(dbPath);
11229
10582
  mtime = mtime || st.mtimeMs;
11230
10583
  created = created || st.birthtimeMs || st.mtimeMs;
11231
10584
  } catch {
@@ -11528,7 +10881,7 @@ var init_cursor_store = __esm({
11528
10881
  });
11529
10882
 
11530
10883
  // ../engine/dist/core/backends/cursor-ide.js
11531
- import { existsSync as existsSync14, readFileSync as readFileSync9, readdirSync as readdirSync11, statSync as statSync12 } from "fs";
10884
+ import { existsSync as existsSync14, readFileSync as readFileSync9, readdirSync as readdirSync11, statSync as statSync11 } from "fs";
11532
10885
  import { dirname as dirname12, join as join15 } from "path";
11533
10886
  function jsonOf(value) {
11534
10887
  if (value == null)
@@ -11598,7 +10951,7 @@ function workspaceIndex(workspaceRoots) {
11598
10951
  for (const id of entries) {
11599
10952
  const dir = join15(root, id);
11600
10953
  try {
11601
- if (!statSync12(dir).isDirectory())
10954
+ if (!statSync11(dir).isDirectory())
11602
10955
  continue;
11603
10956
  } catch {
11604
10957
  continue;
@@ -11944,7 +11297,7 @@ var init_cursor_ide = __esm({
11944
11297
  });
11945
11298
 
11946
11299
  // ../engine/dist/core/backends/cursor.js
11947
- import { existsSync as existsSync15, readFileSync as readFileSync10, statSync as statSync13 } from "fs";
11300
+ import { existsSync as existsSync15, readFileSync as readFileSync10, statSync as statSync12 } from "fs";
11948
11301
  import { join as join16 } from "path";
11949
11302
  var PREFIX4, CursorBackend, cursorBackend;
11950
11303
  var init_cursor = __esm({
@@ -11997,7 +11350,7 @@ var init_cursor = __esm({
11997
11350
  if (r.surface === "cli") {
11998
11351
  let mtime2 = r.chat.updatedAtMs;
11999
11352
  try {
12000
- mtime2 = statSync13(r.chat.dbPath).mtimeMs || mtime2;
11353
+ mtime2 = statSync12(r.chat.dbPath).mtimeMs || mtime2;
12001
11354
  } catch {
12002
11355
  }
12003
11356
  return {
@@ -12010,7 +11363,7 @@ var init_cursor = __esm({
12010
11363
  }
12011
11364
  let mtime = r.composer.updatedAtMs;
12012
11365
  try {
12013
- mtime = statSync13(r.composer.globalDbPath).mtimeMs || mtime;
11366
+ mtime = statSync12(r.composer.globalDbPath).mtimeMs || mtime;
12014
11367
  } catch {
12015
11368
  }
12016
11369
  return {
@@ -12256,7 +11609,7 @@ var init_cursor = __esm({
12256
11609
  if (existsSync15(metaPath)) {
12257
11610
  files.push({ name: "meta.json", bytes: readFileSync10(metaPath) });
12258
11611
  }
12259
- return { tool: "cursor", mtime: statSync13(r.chat.dbPath).mtimeMs, files };
11612
+ return { tool: "cursor", mtime: statSync12(r.chat.dbPath).mtimeMs, files };
12260
11613
  }
12261
11614
  const events = readComposerEvents(r.composer);
12262
11615
  if (events.length === 0)
@@ -12264,7 +11617,7 @@ var init_cursor = __esm({
12264
11617
  const body = events.map((e) => JSON.stringify(e)).join("\n") + "\n";
12265
11618
  return {
12266
11619
  tool: "cursor",
12267
- mtime: statSync13(r.composer.globalDbPath).mtimeMs,
11620
+ mtime: statSync12(r.composer.globalDbPath).mtimeMs,
12268
11621
  files: [{ name: `${r.composer.composerId}.jsonl`, bytes: Buffer.from(body, "utf8") }]
12269
11622
  };
12270
11623
  } catch {
@@ -12408,11 +11761,391 @@ function envCredentials(env = process.env) {
12408
11761
  return null;
12409
11762
  return { serverUrl: normalise(server), token };
12410
11763
  }
12411
- var normalise;
12412
- var init_credentials_env = __esm({
12413
- "../engine/dist/core/credentials-env.js"() {
11764
+ var normalise;
11765
+ var init_credentials_env = __esm({
11766
+ "../engine/dist/core/credentials-env.js"() {
11767
+ "use strict";
11768
+ normalise = (u) => u.trim().replace(/\/+$/, "");
11769
+ }
11770
+ });
11771
+
11772
+ // ../engine/dist/core/secret-redactor.js
11773
+ import { statSync as statSync13 } from "fs";
11774
+ function secretOf(rule, m) {
11775
+ const raw = rule.secret != null ? m[rule.secret] : m[0];
11776
+ if (raw == null)
11777
+ return null;
11778
+ if (rule.where && !rule.where(raw))
11779
+ return null;
11780
+ return raw;
11781
+ }
11782
+ function looksLikeSecretToken(t) {
11783
+ if (t.length < 32)
11784
+ return false;
11785
+ if (/^[a-f0-9]+$/i.test(t))
11786
+ return true;
11787
+ return /[A-Z]/.test(t) && /[a-z]/.test(t) && /[0-9]/.test(t);
11788
+ }
11789
+ function findContextualSecrets(text) {
11790
+ if (!text)
11791
+ return [];
11792
+ const ranges = [];
11793
+ let m;
11794
+ SECRET_CONTEXT_RE.lastIndex = 0;
11795
+ while ((m = SECRET_CONTEXT_RE.exec(text)) !== null) {
11796
+ const winStart = m.index + m[0].length;
11797
+ const window = text.slice(winStart, winStart + 80);
11798
+ HIGH_ENTROPY_TOKEN_RE.lastIndex = 0;
11799
+ let tm;
11800
+ while ((tm = HIGH_ENTROPY_TOKEN_RE.exec(window)) !== null) {
11801
+ if (!looksLikeSecretToken(tm[0]))
11802
+ continue;
11803
+ 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))) {
11804
+ continue;
11805
+ }
11806
+ ranges.push({ start: winStart + tm.index, end: winStart + tm.index + tm[0].length });
11807
+ break;
11808
+ }
11809
+ }
11810
+ ranges.sort((a, b) => a.start - b.start);
11811
+ const out = [];
11812
+ for (const r of ranges) {
11813
+ const last = out[out.length - 1];
11814
+ if (last && r.start < last.end) {
11815
+ last.end = Math.max(last.end, r.end);
11816
+ continue;
11817
+ }
11818
+ out.push({ ...r });
11819
+ }
11820
+ return out;
11821
+ }
11822
+ function isEnvRedactionRequested() {
11823
+ const v = process.env.CHAT_RECALL_REDACT_INDEX;
11824
+ if (v === void 0)
11825
+ return null;
11826
+ return v === "1" || v === "true" || v === "yes";
11827
+ }
11828
+ function settingsView() {
11829
+ let mtimeMs = -1;
11830
+ try {
11831
+ mtimeMs = statSync13(settingsFilePath()).mtimeMs;
11832
+ } catch {
11833
+ }
11834
+ if (cache && cache.mtimeMs === mtimeMs)
11835
+ return cache;
11836
+ let enabled = false;
11837
+ const extra = [];
11838
+ try {
11839
+ const s = loadSettings();
11840
+ enabled = !!s.privacy?.redactIndex;
11841
+ for (const r of s.privacy?.redactionRules ?? []) {
11842
+ try {
11843
+ extra.push({ label: r.label, pattern: new RegExp(r.pattern, "g") });
11844
+ } catch {
11845
+ }
11846
+ }
11847
+ } catch {
11848
+ }
11849
+ cache = { mtimeMs, enabled, rules: [...DEFAULT_REDACTION_RULES, ...extra] };
11850
+ return cache;
11851
+ }
11852
+ function compileServerRule(spec) {
11853
+ const name = spec.name || "(unnamed)";
11854
+ if (!spec.regex)
11855
+ return { name, reason: "empty pattern" };
11856
+ if (spec.regex.length > MAX_PATTERN_LEN)
11857
+ return { name, reason: `pattern longer than ${MAX_PATTERN_LEN} chars` };
11858
+ let re;
11859
+ try {
11860
+ const flags = "g" + (spec.flags || "").replace(/[^ims]/g, "");
11861
+ re = new RegExp(spec.regex, flags);
11862
+ } catch (e) {
11863
+ return { name, reason: `invalid regex: ${e instanceof Error ? e.message : e}` };
11864
+ }
11865
+ if (re.test(""))
11866
+ return { name, reason: "matches the empty string (would redact everything)" };
11867
+ re.lastIndex = 0;
11868
+ if (re.test(CANARY))
11869
+ return { name, reason: "matches benign text \u2014 too broad" };
11870
+ re.lastIndex = 0;
11871
+ return { label: `${spec.source === "pack" ? "pack" : "tenant"}:${name}`, pattern: re };
11872
+ }
11873
+ function installServerRulePack(pack) {
11874
+ const rejected = [];
11875
+ const rules = [];
11876
+ for (const spec of pack.rules || []) {
11877
+ if (rules.length >= MAX_PACK_RULES) {
11878
+ rejected.push({ name: spec.name || "(unnamed)", reason: `pack exceeds ${MAX_PACK_RULES} rules` });
11879
+ continue;
11880
+ }
11881
+ const out = compileServerRule(spec);
11882
+ if ("reason" in out)
11883
+ rejected.push(out);
11884
+ else
11885
+ rules.push(out);
11886
+ }
11887
+ const version = pack.version || "";
11888
+ serverPack = { version, rules };
11889
+ return { version, accepted: rules.length, rejected };
11890
+ }
11891
+ function serverRulePackVersion() {
11892
+ return serverPack ? serverPack.version || "unversioned" : null;
11893
+ }
11894
+ function activeRules() {
11895
+ const base2 = settingsView().rules;
11896
+ return serverPack && serverPack.rules.length > 0 ? [...base2, ...serverPack.rules] : base2;
11897
+ }
11898
+ function isRedactionEnabled() {
11899
+ const env = isEnvRedactionRequested();
11900
+ if (env !== null)
11901
+ return env;
11902
+ return settingsView().enabled;
11903
+ }
11904
+ function redactSecrets(text, opts = {}) {
11905
+ if (!opts.force && !isRedactionEnabled())
11906
+ return text;
11907
+ if (!text)
11908
+ return text;
11909
+ const rules = opts.rules || activeRules();
11910
+ let out = text;
11911
+ for (const r of rules) {
11912
+ out = out.replace(r.pattern, (...args) => {
11913
+ const whole = args[0];
11914
+ const m = args.slice(0, args.length);
11915
+ const secret = secretOf(r, m);
11916
+ if (secret == null)
11917
+ return whole;
11918
+ if (opts.count)
11919
+ opts.count.redactions++;
11920
+ return whole.slice(0, whole.length - secret.length) + `[REDACTED:${r.label}]`;
11921
+ });
11922
+ }
11923
+ const ctx = findContextualSecrets(out);
11924
+ for (let i = ctx.length - 1; i >= 0; i--) {
11925
+ const { start, end } = ctx[i];
11926
+ if (opts.count)
11927
+ opts.count.redactions++;
11928
+ out = out.slice(0, start) + "[REDACTED:secret-context]" + out.slice(end);
11929
+ }
11930
+ return out;
11931
+ }
11932
+ function maskSecret(s) {
11933
+ if (!s)
11934
+ return "";
11935
+ if (s.length <= 4)
11936
+ return "*".repeat(s.length);
11937
+ return "*".repeat(Math.min(s.length - 4, 60)) + s.slice(-4);
11938
+ }
11939
+ function globalRegexFor(pattern) {
11940
+ const flags = pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g";
11941
+ const key2 = pattern.source + "\0" + flags;
11942
+ let re = globalRegexCache.get(key2);
11943
+ if (!re) {
11944
+ re = new RegExp(pattern.source, flags);
11945
+ globalRegexCache.set(key2, re);
11946
+ }
11947
+ return re;
11948
+ }
11949
+ function makeLineResolver(text) {
11950
+ const starts = [];
11951
+ for (let i = text.indexOf("\n"); i !== -1; i = text.indexOf("\n", i + 1))
11952
+ starts.push(i);
11953
+ return (index) => {
11954
+ let lo = 0, hi = starts.length;
11955
+ while (lo < hi) {
11956
+ const mid = lo + hi >> 1;
11957
+ if (starts[mid] < index)
11958
+ lo = mid + 1;
11959
+ else
11960
+ hi = mid;
11961
+ }
11962
+ return lo + 1;
11963
+ };
11964
+ }
11965
+ function scanTextForFindings(text, opts = {}) {
11966
+ if (!text)
11967
+ return [];
11968
+ const rules = opts.rules || activeRules();
11969
+ const findings = [];
11970
+ const seenSpans = /* @__PURE__ */ new Set();
11971
+ const lineAt = makeLineResolver(text);
11972
+ for (const r of rules) {
11973
+ const re = globalRegexFor(r.pattern);
11974
+ re.lastIndex = 0;
11975
+ let m;
11976
+ while ((m = re.exec(text)) !== null) {
11977
+ const secret = secretOf(r, m);
11978
+ if (secret == null) {
11979
+ if (re.lastIndex === m.index)
11980
+ re.lastIndex++;
11981
+ continue;
11982
+ }
11983
+ const at = m.index + (m[0].length - secret.length);
11984
+ const span = `${at}:${secret.length}`;
11985
+ if (!seenSpans.has(span)) {
11986
+ seenSpans.add(span);
11987
+ findings.push({ rule: r.label, line: lineAt(at), preview: maskSecret(secret) });
11988
+ }
11989
+ if (re.lastIndex === m.index)
11990
+ re.lastIndex++;
11991
+ }
11992
+ }
11993
+ for (const { start, end } of findContextualSecrets(text)) {
11994
+ const line = lineAt(start);
11995
+ findings.push({ rule: "secret-context", line, preview: maskSecret(text.slice(start, end)) });
11996
+ }
11997
+ return findings;
11998
+ }
11999
+ var DEFAULT_REDACTION_RULES, SECRET_CONTEXT_RE, HIGH_ENTROPY_TOKEN_RE, cache, CANARY, MAX_PACK_RULES, MAX_PATTERN_LEN, serverPack, globalRegexCache;
12000
+ var init_secret_redactor = __esm({
12001
+ "../engine/dist/core/secret-redactor.js"() {
12414
12002
  "use strict";
12415
- normalise = (u) => u.trim().replace(/\/+$/, "");
12003
+ init_settings();
12004
+ DEFAULT_REDACTION_RULES = [
12005
+ // AWS access tokens. Prefix-anchored: AKIA, ASIA, ABIA (long-term,
12006
+ // STS, EC2). Always exactly 16 hex/alphanumeric after the prefix.
12007
+ // NOTE: this matches only the access-key-ID, NOT the secret key value —
12008
+ // the `env-secret` rule below covers AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN.
12009
+ { label: "aws-access-token", pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[A-Z0-9]{16}\b/g },
12010
+ // AWS STS session tokens. These are long base64 blobs with NO `NAME=` context
12011
+ // when pasted bare into prose/code, so `env-secret` misses them — but they
12012
+ // reliably start with one of a handful of base64 prefixes (the encoded
12013
+ // `{"...` JSON header). Prefix-anchored → low false-positive. This is the
12014
+ // bare-value form that leaked (`search('IQoJEXAMPLEfakeSESSIONtokenNOTreal01234567890abcDEF==...')`).
12015
+ { label: "aws-session-token", pattern: /\b(?:IQoJ|FwoG|FwoH|FQoG|FQoH|FwoD)[A-Za-z0-9/+]{40,}={0,2}/g },
12016
+ // Env/shell-assignment secrets — a VAR whose name ends in KEY/SECRET/TOKEN/
12017
+ // PASSWORD/CREDENTIAL, then `=`/`:`, then the value. Zero-width lookbehind so
12018
+ // only the VALUE is redacted (the var name stays for context). This is the
12019
+ // dominant real-world leak vector (`export` dumps, .env files, `curl -H`,
12020
+ // tool output) and is exactly what leaked AWS_SECRET_ACCESS_KEY and
12021
+ // AWS_SESSION_TOKEN in cleartext while the access-key-ID rule above caught
12022
+ // only the (least-sensitive) ID.
12023
+ // Suffix list deliberately broad: DB_PASS (our own helm chart's name!),
12024
+ // NPM_AUTH, SENTRY_DSN, SESSION/COOKIE secrets and BEARER vars all leaked
12025
+ // past the original KEY/SECRET/TOKEN/PASSWORD-only list. Values under 12
12026
+ // chars don't redact, so flag-ish vars (BYPASS=true) stay readable.
12027
+ // Context CONSUMED, secret in group 1 — see RedactionRule.secret. As a
12028
+ // lookbehind this was the second most expensive rule in the collector.
12029
+ { 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 },
12030
+ // Credentials embedded in a connection URL: scheme://user:PASSWORD@host.
12031
+ // Redacts only the password segment (keeps scheme/user/host for context).
12032
+ //
12033
+ // Scheme-agnostic on purpose. The old rule allowlisted eight DB schemes, so
12034
+ // `https://user:pass@host`, `ssh://`, `ftp://`, `smtp://` and every other
12035
+ // userinfo URL walked through in cleartext — and a `jdbc:postgresql://…`
12036
+ // prefix broke the \b anchor for the one family it did cover. Any scheme
12037
+ // followed by userinfo is a credential, so match the shape, not a list.
12038
+ { label: "url-password", secret: 1, pattern: /\b[a-zA-Z][a-zA-Z0-9+.-]{1,31}:\/\/[^:@\s/]{1,64}:([^@\s/]{1,256})(?=@)/g },
12039
+ // Bare AWS secret-access-key VALUE: EXACTLY 40 base64 chars, word-bounded,
12040
+ // containing upper+lower+digit. This is the gitleaks heuristic — the mixed-case
12041
+ // requirement excludes 40-char lowercase-hex git SHAs, and the exact-40 bound
12042
+ // excludes longer blobs/tokens. It catches the value when it appears bare in
12043
+ // prose with NO `NAME=` and NO nearby context word (e.g. inside a markdown
12044
+ // table or quoted in analysis) — the residual leak path the env-secret and
12045
+ // contextual rules both miss. Placed AFTER env-secret/url-password so the
12046
+ // keyed/URL forms keep their more-specific labels.
12047
+ // The three stacked lookaheads each re-scanned the same 40 characters, which
12048
+ // made this the MOST expensive rule of all 106 the collector runs (13.1% of a
12049
+ // 29.5 MB scan). The mixed-case-plus-digit test is now one JS pass.
12050
+ {
12051
+ label: "aws-secret-key",
12052
+ secret: 1,
12053
+ pattern: /(?<![A-Za-z0-9/+])([A-Za-z0-9/+]{40})(?![A-Za-z0-9/+])/g,
12054
+ where: (v) => /[a-z]/.test(v) && /[A-Z]/.test(v) && /[0-9]/.test(v)
12055
+ },
12056
+ // GitHub personal access tokens.
12057
+ { label: "github-pat", pattern: /\bghp_[A-Za-z0-9]{36}\b/g },
12058
+ // GitHub fine-grained PATs.
12059
+ { label: "github-fine", pattern: /\bgithub_pat_[A-Za-z0-9_]{82}\b/g },
12060
+ // GitHub OAuth.
12061
+ { label: "github-oauth", pattern: /\bgho_[A-Za-z0-9]{36}\b/g },
12062
+ // GitHub app + refresh.
12063
+ { label: "github-app", pattern: /\b(?:ghs|ghu|ghr)_[A-Za-z0-9]{36}\b/g },
12064
+ // Slack bot/user/app tokens.
12065
+ { label: "slack-token", pattern: /\bxox[abopr]-[A-Za-z0-9-]{10,}\b/g },
12066
+ // Slack incoming webhook URL.
12067
+ { label: "slack-webhook", pattern: /\bhttps:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+\b/g },
12068
+ // Stripe live + test keys.
12069
+ { label: "stripe-key", pattern: /\b(?:sk|pk|rk)_(?:live|test)_[A-Za-z0-9]{24,}\b/g },
12070
+ // Anthropic.
12071
+ { label: "anthropic-key", pattern: /\bsk-ant-[a-zA-Z0-9_-]{40,}\b/g },
12072
+ // OpenAI. Two shapes, deliberately kept apart. The classic key is
12073
+ // `sk-` + base62 with NO hyphens; the modern scoped keys carry an explicit
12074
+ // proj/svcacct/admin segment first. Allowing hyphens after a bare `sk-`
12075
+ // (which is what admitted sk-proj- in the first place) also matches ordinary
12076
+ // kebab-case text — `sk-fix-the-login-redirect-bug`, `.sk-button-primary`,
12077
+ // `sk-2024-01-01-backup` — and this corpus is full of branch names and CSS
12078
+ // classes. Requiring the segment keeps sk-proj- coverage without that.
12079
+ { label: "openai-key", pattern: /\bsk-(?:proj|svcacct|admin)-[A-Za-z0-9_-]{20,}\b|\bsk-[A-Za-z0-9]{20,}\b/g },
12080
+ // Google API key (AIza…).
12081
+ { label: "google-api-key", pattern: /\bAIza[0-9A-Za-z_-]{35}\b/g },
12082
+ // Generic JWT — three base64url segments.
12083
+ { label: "jwt", pattern: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g },
12084
+ // Private key blocks (PEM). The label is a free word run rather than a fixed
12085
+ // list, so ENCRYPTED PRIVATE KEY and PGP PRIVATE KEY BLOCK are covered
12086
+ // without another enumeration, and \1 forces END to name the same kind as
12087
+ // BEGIN.
12088
+ //
12089
+ // The body is [^-], NOT [\s\S]*?. That is a performance boundary, not a
12090
+ // style choice. An unanchored prefix with an unbounded lazy gap re-scans to
12091
+ // end-of-input from every candidate start, so text carrying many BEGIN
12092
+ // markers and no matching END costs O(n^2): 500KB of that measured 810ms
12093
+ // here, and this runs synchronously on the server's event loop over
12094
+ // tenant-supplied text (services/secret-rescan.ts). A PEM body is base64
12095
+ // plus headers and contains no '-', so excluding it makes the scan stop at
12096
+ // the next delimiter instead of at EOF — same matches, linear cost.
12097
+ { label: "private-key", pattern: /-----BEGIN ([A-Z0-9 ]{0,32})PRIVATE KEY(?: BLOCK)?-----[^-]{0,10000}?-----END \1PRIVATE KEY(?: BLOCK)?-----/g },
12098
+ // PuTTY keys are not PEM. Match the Private-Lines payload — the actual key
12099
+ // material — rather than spanning the whole file, which would need the same
12100
+ // unbounded gap the rule above just removed. Line-anchored and base64-only,
12101
+ // so it cannot run away.
12102
+ { label: "putty-key", pattern: /^Private-Lines:[ \t]*\d{1,4}[\r\n]+(?:[A-Za-z0-9+/=]{8,120}[\r\n]+){1,400}/gm },
12103
+ // npm tokens (uuid-shaped — overlap with our UUID filter; intentional).
12104
+ { label: "npm-token", pattern: /\bnpm_[A-Za-z0-9]{36}\b/g },
12105
+ // .npmrc auth lines: //registry.npmjs.org/:_authToken=VALUE (the key is not
12106
+ // ALL-CAPS, so the env-secret rule above never matched it). The leading `:`
12107
+ // is required — it is what makes this npmrc syntax rather than any shell var
12108
+ // ending in _AUTH, which env-secret already owns and labels better.
12109
+ // [^\s"'`]+ so the closing quote is not swallowed, and a ${VAR} placeholder
12110
+ // is left alone — redacting it destroys a config example and hides nothing.
12111
+ // The rule the perf profile caught the wedged daemon inside: 98.94% of samples.
12112
+ { label: "npmrc-auth", secret: 1, pattern: /:_(?:authToken|password|auth)[ \t]*=[ \t]*(?!\$\{)([^\s"'`]{12,})/gi },
12113
+ // HTTP Authorization headers. env-secret requires an ALL-CAPS name, and
12114
+ // `Authorization` is mixed case, so both Bearer and Basic walked straight
12115
+ // through — including short opaque tokens the contextual pass also misses
12116
+ // (it needs 32+ chars AND mixed-case-plus-digit).
12117
+ // Same two guards as npmrc-auth: stop at a quote, and skip the obvious
12118
+ // placeholders that fill documentation (<YOUR_TOKEN>, ${TOKEN}, xxx…).
12119
+ { label: "auth-header", secret: 1, pattern: /\bauthorization[ \t]*:[ \t]*(?:bearer|basic|token)[ \t]+(?![<$])([^\s"'`<]{8,})/gi },
12120
+ // Vendor tokens with unambiguous prefixes. Each is a fixed literal, so a
12121
+ // false positive is essentially impossible and the cost of listing is low.
12122
+ { label: "gitlab-pat", pattern: /\bglpat-[A-Za-z0-9_-]{20,}\b/g },
12123
+ { label: "slack-app-token", pattern: /\bxapp-\d-[A-Z0-9]{8,}-\d{10,}-[a-f0-9]{32,}\b/g },
12124
+ { label: "sendgrid-key", pattern: /\bSG\.[A-Za-z0-9_-]{20,24}\.[A-Za-z0-9_-]{40,50}\b/g },
12125
+ // (?<![-\w]) so `cache-key-<md5>` is not read as a mailgun key.
12126
+ { label: "mailgun-key", pattern: /(?<![-\w])key-[a-f0-9]{32}\b/g },
12127
+ { label: "digitalocean-pat", pattern: /\bdop_v1_[a-f0-9]{64}\b/g },
12128
+ { label: "huggingface-token", pattern: /\bhf_[A-Za-z0-9]{30,}\b/g },
12129
+ { label: "shopify-token", pattern: /\bshp(?:at|ca|pa|ss)_[a-fA-F0-9]{32}\b/g },
12130
+ { label: "pypi-token", pattern: /\bpypi-AgEIcHlwaS5vcmc[A-Za-z0-9_-]{50,}\b/g },
12131
+ // No twilio-sid rule: AC…/SK… are Twilio SIDs, which are public account and
12132
+ // key identifiers, not the auth token. Redacting them hid nothing and
12133
+ // collided with 32-hex etags and MD5 sums.
12134
+ // No discord-bot-token rule either: the shape (word.word.word) matched
12135
+ // ordinary dotted symbols such as a.b.ApplicationContextProviderImpl.
12136
+ { label: "telegram-bot-token", pattern: /\b\d{8,10}:AA[A-Za-z0-9_-]{32,}\b/g },
12137
+ { label: "doppler-token", pattern: /\bdp\.(?:pt|st|sa|ct)\.[A-Za-z0-9_-]{20,}\b/g },
12138
+ // Azure Storage SAS signature — the `sig=` parameter is the actual secret.
12139
+ { label: "azure-sas", secret: 1, pattern: /[?&]sig=([A-Za-z0-9%/+=]{20,})/g }
12140
+ ];
12141
+ SECRET_CONTEXT_RE = /\b(?:secret|password|passwd|credential|access[ _-]?key|private[ _-]?key|api[ _-]?key|auth(?:oriz\w*)?|bearer|token)\b/gi;
12142
+ HIGH_ENTROPY_TOKEN_RE = /[A-Za-z0-9/+_-]{32,}/g;
12143
+ cache = null;
12144
+ CANARY = "the quick brown fox jumps over the lazy dog 0123456789 commit 8ba1f109551bd432803012645ac136ddd475d0fb src/index.ts:42";
12145
+ MAX_PACK_RULES = 200;
12146
+ MAX_PATTERN_LEN = 512;
12147
+ serverPack = null;
12148
+ globalRegexCache = /* @__PURE__ */ new Map();
12416
12149
  }
12417
12150
  });
12418
12151
 
@@ -13216,10 +12949,10 @@ var init_types = __esm({
13216
12949
 
13217
12950
  // ../engine/dist/transcript/opencode.js
13218
12951
  async function parseOpenCodeSubagents(parentSessionId) {
13219
- const { existsSync: existsSync50 } = await import("fs");
12952
+ const { existsSync: existsSync51 } = await import("fs");
13220
12953
  const { opencodeBackend: opencodeBackend2 } = await Promise.resolve().then(() => (init_opencode(), opencode_exports));
13221
12954
  const dbPath = opencodeBackend2.dbPath();
13222
- if (!existsSync50(dbPath))
12955
+ if (!existsSync51(dbPath))
13223
12956
  return [];
13224
12957
  const realParent = parentSessionId.replace("opencode_", "");
13225
12958
  const db = openSqliteReadonlyOrThrow(dbPath);
@@ -13260,10 +12993,10 @@ async function parseOpenCodeSubagents(parentSessionId) {
13260
12993
  }
13261
12994
  }
13262
12995
  async function parseOpenCodeTranscript(sessionId) {
13263
- const { existsSync: existsSync50 } = await import("fs");
12996
+ const { existsSync: existsSync51 } = await import("fs");
13264
12997
  const { opencodeBackend: opencodeBackend2 } = await Promise.resolve().then(() => (init_opencode(), opencode_exports));
13265
12998
  const dbPath = opencodeBackend2.dbPath();
13266
- if (!existsSync50(dbPath))
12999
+ if (!existsSync51(dbPath))
13267
13000
  return [];
13268
13001
  const realId = sessionId.replace("opencode_", "");
13269
13002
  const db = openSqliteReadonlyOrThrow(dbPath);
@@ -13340,6 +13073,7 @@ __export(transcript_exports, {
13340
13073
  shadowFileFor: () => shadowFileFor,
13341
13074
  shadowRoot: () => shadowRoot,
13342
13075
  shadowSizeOnDisk: () => shadowSizeOnDisk,
13076
+ shadowUncompressedBytes: () => shadowUncompressedBytes,
13343
13077
  snapshotShadow: () => snapshotShadow,
13344
13078
  trimTranscriptForSync: () => trimTranscriptForSync,
13345
13079
  updateShadow: () => updateShadow,
@@ -14446,111 +14180,6 @@ var init_source_registry = __esm({
14446
14180
  }
14447
14181
  });
14448
14182
 
14449
- // ../engine/dist/parsers/session-source.js
14450
- var SessionSource;
14451
- var init_session_source = __esm({
14452
- "../engine/dist/parsers/session-source.js"() {
14453
- "use strict";
14454
- init_session();
14455
- init_chunker();
14456
- init_settings();
14457
- SessionSource = class {
14458
- sourceType = "session";
14459
- claudeDir;
14460
- constructor(claudeDir) {
14461
- this.claudeDir = claudeDir;
14462
- }
14463
- async *discover() {
14464
- if (!isSourceEnabled("claude", "sessions"))
14465
- return;
14466
- for (const [entry, sessionPath] of getAllSessions(this.claudeDir)) {
14467
- yield this.entryToItem(entry, sessionPath);
14468
- }
14469
- }
14470
- async parse(item) {
14471
- const content = await parseSessionFile(item.filePath);
14472
- const entry = this.itemToEntry(item);
14473
- const sessionChunks = chunkSession(entry, content);
14474
- item.extra = {
14475
- ...item.extra || {},
14476
- ...content.metadata
14477
- };
14478
- return sessionChunks.map((sc) => ({
14479
- chunkId: sc.chunkId,
14480
- itemId: sc.sessionId,
14481
- sourceType: "session",
14482
- // Empty title is preferable to "Session abc12345" placeholder text:
14483
- // the latter pollutes search results and gets surfaced in the UI as
14484
- // a misleading heading. Downstream renderers fall back to
14485
- // project path / time when title is empty.
14486
- title: sc.firstPrompt.slice(0, 100),
14487
- text: sc.text,
14488
- chunkType: sc.chunkType,
14489
- projectPath: sc.projectPath,
14490
- filePath: sc.sourceFile,
14491
- mtime: sc.mtime
14492
- }));
14493
- }
14494
- async extractLinks(item) {
14495
- if (item.filePath) {
14496
- try {
14497
- const { detectForkPredecessor: detectForkPredecessor2 } = await Promise.resolve().then(() => (init_raw(), raw_exports));
14498
- const predecessor = detectForkPredecessor2(item.filePath);
14499
- if (predecessor) {
14500
- return [{
14501
- sourceType: "session",
14502
- sourceId: item.id,
14503
- targetType: "session",
14504
- targetId: predecessor,
14505
- linkType: "forked_from",
14506
- confidence: 1
14507
- }];
14508
- }
14509
- } catch {
14510
- }
14511
- }
14512
- return [];
14513
- }
14514
- entryToItem(entry, sessionPath) {
14515
- return {
14516
- id: entry.sessionId,
14517
- sourceType: "session",
14518
- // Empty title when there's no first prompt — the UI then knows to
14519
- // fall back to summary or project path instead of rendering the
14520
- // useless "Session abc12345" placeholder as a heading.
14521
- title: entry.firstPrompt ? entry.firstPrompt.replace(/\n/g, " ").trim().slice(0, 100) : "",
14522
- projectPath: entry.projectPath,
14523
- filePath: sessionPath,
14524
- mtime: entry.fileMtime,
14525
- contentPreview: entry.firstPrompt?.slice(0, 200),
14526
- extra: {
14527
- messageCount: entry.messageCount,
14528
- gitBranch: entry.gitBranch,
14529
- created: entry.created,
14530
- modified: entry.modified,
14531
- isSidechain: entry.isSidechain
14532
- }
14533
- };
14534
- }
14535
- itemToEntry(item) {
14536
- const extra = item.extra || {};
14537
- return {
14538
- sessionId: item.id,
14539
- fullPath: item.filePath,
14540
- fileMtime: item.mtime,
14541
- firstPrompt: item.contentPreview || "",
14542
- messageCount: extra.messageCount || 0,
14543
- created: extra.created || "",
14544
- modified: extra.modified || "",
14545
- gitBranch: extra.gitBranch || "",
14546
- projectPath: item.projectPath,
14547
- isSidechain: extra.isSidechain || false
14548
- };
14549
- }
14550
- };
14551
- }
14552
- });
14553
-
14554
14183
  // ../engine/dist/core/utils.js
14555
14184
  import { existsSync as existsSync21, readdirSync as readdirSync15 } from "fs";
14556
14185
  import { homedir as homedir6 } from "os";
@@ -17907,7 +17536,6 @@ var init_plugins_source = __esm({
17907
17536
 
17908
17537
  // ../engine/dist/parsers/all-sources.js
17909
17538
  function buildSourceRegistry(registry = new SourceRegistry()) {
17910
- registry.register(new SessionSource());
17911
17539
  registry.register(new PlanSource());
17912
17540
  registry.register(new TaskSource());
17913
17541
  registry.register(new ClaudeMdSource());
@@ -17931,7 +17559,6 @@ var init_all_sources = __esm({
17931
17559
  "../engine/dist/parsers/all-sources.js"() {
17932
17560
  "use strict";
17933
17561
  init_source_registry();
17934
- init_session_source();
17935
17562
  init_plan_source();
17936
17563
  init_task_source();
17937
17564
  init_claude_md_source();
@@ -18303,7 +17930,7 @@ var init_sync_fields = __esm({
18303
17930
  });
18304
17931
 
18305
17932
  // ../engine/dist/core/index-lock.js
18306
- import { existsSync as existsSync39, mkdirSync as mkdirSync8, openSync as openSync7, writeFileSync as writeFileSync7, readFileSync as readFileSync31, rmSync as rmSync4, statSync as statSync35, closeSync as closeSync7 } from "fs";
17933
+ import { existsSync as existsSync39, mkdirSync as mkdirSync8, openSync as openSync8, writeFileSync as writeFileSync7, readFileSync as readFileSync31, rmSync as rmSync4, statSync as statSync35, closeSync as closeSync8 } from "fs";
18307
17934
  import { dirname as dirname17, join as join40 } from "path";
18308
17935
  function acquireIndexLock(opts) {
18309
17936
  const lockPath = lockFilePath();
@@ -18315,7 +17942,7 @@ function acquireIndexLock(opts) {
18315
17942
  }
18316
17943
  let fd;
18317
17944
  try {
18318
- fd = openSync7(lockPath, "wx", 384);
17945
+ fd = openSync8(lockPath, "wx", 384);
18319
17946
  } catch (err) {
18320
17947
  if (err.code === "EEXIST")
18321
17948
  return null;
@@ -18325,7 +17952,7 @@ function acquireIndexLock(opts) {
18325
17952
  try {
18326
17953
  writeFileSync7(fd, JSON.stringify(holder));
18327
17954
  } finally {
18328
- closeSync7(fd);
17955
+ closeSync8(fd);
18329
17956
  }
18330
17957
  let released = false;
18331
17958
  return {
@@ -18416,7 +18043,7 @@ var init_http = __esm({
18416
18043
  CLI_VERSION = JSON.parse(readFileSync32(new URL("../package.json", import.meta.url), "utf-8")).version || CLI_VERSION;
18417
18044
  } catch {
18418
18045
  try {
18419
- if (true) CLI_VERSION = "0.6.9";
18046
+ if (true) CLI_VERSION = "0.7.1";
18420
18047
  } catch {
18421
18048
  }
18422
18049
  }
@@ -18911,7 +18538,7 @@ var init_telemetry = __esm({
18911
18538
  init_sync_client();
18912
18539
  cliVersion = () => {
18913
18540
  try {
18914
- return true ? "0.6.9" : "dev";
18541
+ return true ? "0.7.1" : "dev";
18915
18542
  } catch {
18916
18543
  return "dev";
18917
18544
  }
@@ -19136,16 +18763,17 @@ __export(auto_update_exports, {
19136
18763
  noteUpdateFailure: () => noteUpdateFailure,
19137
18764
  planAutoUpdate: () => planAutoUpdate,
19138
18765
  readUpdateState: () => readUpdateState,
18766
+ resolveNpm: () => resolveNpm,
19139
18767
  runAutoUpdate: () => runAutoUpdate,
19140
18768
  runningPrefix: () => runningPrefix,
19141
18769
  sweepStaleStaging: () => sweepStaleStaging,
19142
18770
  tarballUrl: () => tarballUrl
19143
18771
  });
19144
- import { execSync as execSync2 } from "node:child_process";
19145
- import { readFileSync as readFileSync34 } from "node:fs";
18772
+ import { execSync as execSync2, execFileSync as execFileSync3 } from "node:child_process";
18773
+ import { readFileSync as readFileSync34, existsSync as existsSync41 } from "node:fs";
19146
18774
  import { fileURLToPath as fileURLToPath2 } from "node:url";
19147
18775
  import { writeFileSync as writeFileSync8, mkdtempSync as mkdtempSync3, rmSync as rmSync5, readdirSync as readdirSync31, statSync as statSync36, mkdirSync as mkdirSync9 } from "node:fs";
19148
- import { join as join41, dirname as dirname18, sep as sep2 } from "node:path";
18776
+ import { join as join41, dirname as dirname18, sep as sep2, posix as pathPosix, win32 as pathWin32 } from "node:path";
19149
18777
  import { tmpdir as tmpdir2, homedir as homedir15 } from "node:os";
19150
18778
  import { createHash as createHash7 } from "node:crypto";
19151
18779
  function compareVersions(a, b) {
@@ -19260,25 +18888,46 @@ function runningPrefix(moduleUrl = import.meta.url) {
19260
18888
  const beforeNodeModules = here.slice(0, at);
19261
18889
  return beforeNodeModules.endsWith(`${sep2}lib`) ? beforeNodeModules.slice(0, -4) : beforeNodeModules;
19262
18890
  }
19263
- function npmInstall(cmd) {
18891
+ function npmInstall(npm, args) {
19264
18892
  try {
19265
- execSync2(cmd, { stdio: ["ignore", "ignore", "pipe"] });
18893
+ const opts = { stdio: ["ignore", "ignore", "pipe"], env: npm.env };
18894
+ if (npm.shell) execSync2([npm.file, ...npm.args, ...args].map((a) => `"${a}"`).join(" "), opts);
18895
+ else execFileSync3(npm.file, [...npm.args, ...args], opts);
19266
18896
  } catch (e) {
19267
18897
  const err = e;
19268
18898
  const detail = String(err.stderr ?? "").trim().split("\n").filter((l) => l.trim() && !/^npm (notice|warn)\b/i.test(l)).slice(-4).join(" | ").slice(0, 400);
19269
18899
  throw new Error(detail ? `${err.message ?? "install failed"} \u2014 ${detail}` : err.message ?? "install failed");
19270
18900
  }
19271
18901
  }
18902
+ function resolveNpm(loc) {
18903
+ const win = loc.platform === "win32";
18904
+ const p = win ? pathWin32 : pathPosix;
18905
+ const nodeDir = p.dirname(loc.execPath);
18906
+ const cliUnder = (root) => win ? p.join(root, "node_modules", "npm", "bin", "npm-cli.js") : p.join(root, "lib", "node_modules", "npm", "bin", "npm-cli.js");
18907
+ const candidates2 = [
18908
+ cliUnder(win ? nodeDir : p.join(nodeDir, "..")),
18909
+ ...loc.prefix ? [cliUnder(loc.prefix)] : []
18910
+ ];
18911
+ const pathKey = Object.keys(loc.env).find((k) => k.toUpperCase() === "PATH") ?? "PATH";
18912
+ const binDirs = [nodeDir, ...loc.prefix ? [win ? loc.prefix : p.join(loc.prefix, "bin")] : []];
18913
+ const delimiter3 = win ? ";" : ":";
18914
+ const current = loc.env[pathKey] ?? "";
18915
+ const env = { ...loc.env, [pathKey]: [...binDirs, ...current ? [current] : []].join(delimiter3) };
18916
+ const cli = candidates2.find((c) => loc.exists(c));
18917
+ if (cli) return { file: loc.execPath, args: [cli], env, shell: false };
18918
+ return { file: "npm", args: [], env, shell: true };
18919
+ }
19272
18920
  function realInstall(tgz) {
19273
18921
  const prefix = runningPrefix();
18922
+ const npm = resolveNpm({ execPath: process.execPath, prefix, platform: process.platform, env: process.env, exists: existsSync41 });
19274
18923
  if (prefix) {
19275
- npmInstall(`npm install -g --prefix "${prefix}" "${tgz}"`);
18924
+ npmInstall(npm, ["install", "-g", "--prefix", prefix, tgz]);
19276
18925
  return;
19277
18926
  }
19278
18927
  try {
19279
- npmInstall(`npm install -g "${tgz}"`);
18928
+ npmInstall(npm, ["install", "-g", tgz]);
19280
18929
  } catch {
19281
- npmInstall(`npm install -g --prefix "${process.env.HOME}/.local" "${tgz}"`);
18930
+ npmInstall(npm, ["install", "-g", "--prefix", join41(homedir15(), ".local"), tgz]);
19282
18931
  }
19283
18932
  }
19284
18933
  function realRestart(platform2) {
@@ -20081,16 +19730,53 @@ async function syncToTargetInScope(cred, opts = {}) {
20081
19730
  clearTimeout(timer);
20082
19731
  }
20083
19732
  };
19733
+ const postChunkedHead = async (conv, fileSize) => {
19734
+ const headers = { "content-type": "application/json" };
19735
+ if (cred.token) headers.authorization = `Bearer ${cred.token}`;
19736
+ const ac = new AbortController();
19737
+ const timer = setTimeout(() => ac.abort(new Error(`upload timed out after ${UPLOAD_TIMEOUT_MS}ms`)), UPLOAD_TIMEOUT_MS);
19738
+ try {
19739
+ const res = await fetch(`${base2}/api/sync`, {
19740
+ method: "POST",
19741
+ headers,
19742
+ body: JSON.stringify({ conversations: [conv] }),
19743
+ signal: ac.signal
19744
+ });
19745
+ if (!res.ok) throw new Error(`chunked head sync failed: HTTP ${res.status} ${await res.text().catch(() => "")}`);
19746
+ const body = await res.json().catch(() => ({}));
19747
+ const id = conv.session_id;
19748
+ const guarded = (body.shrink_guarded ?? []).find((g) => g.session_id === id);
19749
+ const mtime = conv.mtime;
19750
+ try {
19751
+ if (!guarded) {
19752
+ markSynced(base2, [{ id, mtime, offset: conv.from_offset, size: conv.from_offset, acked: true }]);
19753
+ } else if (typeof guarded.o === "number" && guarded.o > 0 && guarded.o <= fileSize) {
19754
+ markSynced(base2, [{ id, mtime, offset: guarded.o, size: guarded.o, acked: true }]);
19755
+ } else {
19756
+ markSynced(base2, [{ id, mtime }]);
19757
+ }
19758
+ } catch {
19759
+ }
19760
+ appendResults.uploaded++;
19761
+ } finally {
19762
+ clearTimeout(timer);
19763
+ }
19764
+ };
20084
19765
  let appendBatch = [];
19766
+ let appendBatchBytes = 0;
20085
19767
  const APPEND_BATCH_SIZE = 50;
19768
+ const APPEND_BATCH_MAX_BYTES = SYNC_CHUNK_BYTES;
20086
19769
  const appendFlush = async () => {
20087
19770
  if (appendBatch.length === 0) return;
20088
19771
  const batch = appendBatch;
20089
19772
  appendBatch = [];
19773
+ appendBatchBytes = 0;
20090
19774
  await postAppend(batch);
20091
19775
  };
20092
- const appendAdd = async (conv) => {
19776
+ const appendAdd = async (conv, bytes = 0) => {
19777
+ if (appendBatch.length > 0 && appendBatchBytes + bytes > APPEND_BATCH_MAX_BYTES) await appendFlush();
20093
19778
  appendBatch.push(conv);
19779
+ appendBatchBytes += bytes;
20094
19780
  if (appendBatch.length >= APPEND_BATCH_SIZE) await appendFlush();
20095
19781
  };
20096
19782
  const itemsBatch = makeBatcher("items", 100);
@@ -20169,7 +19855,7 @@ async function syncToTargetInScope(cred, opts = {}) {
20169
19855
  let externalFindings = null;
20170
19856
  if (scanSecrets) {
20171
19857
  externalFindings = /* @__PURE__ */ new Map();
20172
- const toScan = slice.filter((ref) => modeOf(ref) === "full" && sessionFileBytes(ref) <= FULL_BUILD_MAX_BYTES);
19858
+ const toScan = slice.filter((ref) => modeOf(ref) === "full" && sessionBuildBytes(ref) <= FULL_BUILD_MAX_BYTES);
20173
19859
  trace?.(`batch secret scan start: ${toScan.length} session(s), \u2264${(BATCH_SCAN_MAX_BYTES / 1048576).toFixed(0)}MB per slice`);
20174
19860
  const batch = batchScanExternal(
20175
19861
  toScan.map((ref) => ({
@@ -20213,11 +19899,17 @@ async function syncToTargetInScope(cred, opts = {}) {
20213
19899
  const ack = ledger.get(ref.prefixedId);
20214
19900
  try {
20215
19901
  const tail = await buildConversationTail(ref, ack.o ?? 0, { mapPath, tenantRules });
20216
- if (tail && tail.findings.length > 0) {
19902
+ const chunked = sessionBuildBytes(ref) > FULL_BUILD_MAX_BYTES;
19903
+ if (tail && tail.findings.length > 0 && !chunked) {
20217
19904
  promotedForFindings = true;
20218
19905
  } else if (tail) {
20219
19906
  redactions += tail.redactions;
20220
- await appendAdd(tail.conv);
19907
+ await appendAdd(tail.conv, tail.bytes);
19908
+ if (tail.findings.length > 0) {
19909
+ await appendFlush();
19910
+ const all = await scanTranscriptFindingsChunked(ref, tenantRules);
19911
+ if (all.length > 0) await findingsBatch.add(all);
19912
+ }
20221
19913
  walked++;
20222
19914
  if (walked % 250 === 0) console.error(`[sync] ${walked}/${slice.length} sessions\u2026`);
20223
19915
  continue;
@@ -20265,12 +19957,13 @@ async function syncToTargetInScope(cred, opts = {}) {
20265
19957
  }
20266
19958
  redactions += built.redactions;
20267
19959
  if (built.srcHash) shippedHash.set(ref.prefixedId, built.srcHash);
20268
- await convBatch.add(built.conv);
19960
+ if (built.conv.chunked) await postChunkedHead(built.conv, sessionFileBytes(ref));
19961
+ else await convBatch.add(built.conv);
20269
19962
  if (built.findings.length > 0) await findingsBatch.add(built.findings);
20270
19963
  if (upload.sessionMeta) {
20271
19964
  await accumulateKg(extractEntities(built.kgText, { projectPath: built.projectPath, sourceType: "session", sessionId: ref.prefixedId, validFrom: ref.mtime ? new Date(ref.mtime).toISOString().slice(0, 10) : void 0 }), ref.prefixedId);
20272
19965
  }
20273
- if (upload.sessionMeta && sessionFileBytes(ref) <= FULL_BUILD_MAX_BYTES) {
19966
+ if (upload.sessionMeta && sessionFileBytes(ref) <= DERIVED_MAX_BYTES) {
20274
19967
  try {
20275
19968
  const count = { redactions: 0 };
20276
19969
  const offloaded = await scanPool().runDerived({
@@ -20450,35 +20143,70 @@ async function syncToTargetInScope(cred, opts = {}) {
20450
20143
  scanMs: Math.round(scanMs)
20451
20144
  };
20452
20145
  }
20146
+ async function scanTranscriptFindingsChunked(ref, tenantRules) {
20147
+ const backend = getBackendForId(ref.prefixedId);
20148
+ const findings = [];
20149
+ if (!backend?.readFromOffset) return findings;
20150
+ let offset = 0;
20151
+ let lineBase = 0;
20152
+ for (; ; ) {
20153
+ const chunk = await backend.readFromOffset(ref.prefixedId, offset, SYNC_CHUNK_BYTES);
20154
+ if (chunk.newOffset <= offset) break;
20155
+ offset = chunk.newOffset;
20156
+ if (chunk.skippedBytes) {
20157
+ lineBase += 1;
20158
+ continue;
20159
+ }
20160
+ try {
20161
+ for (const f of scanTextForFindings(chunk.text)) {
20162
+ findings.push({ session_id: ref.prefixedId, detector: "builtin", rule: f.rule, line: lineBase + f.line, preview: f.preview });
20163
+ }
20164
+ if (tenantRules && tenantRules.length > 0) {
20165
+ for (const f of scanTenantRules(chunk.text, tenantRules)) {
20166
+ findings.push({ session_id: ref.prefixedId, detector: f.detector, rule: f.rule, line: lineBase + f.line, preview: f.preview });
20167
+ }
20168
+ }
20169
+ } catch {
20170
+ }
20171
+ for (let i = chunk.text.indexOf("\n"); i >= 0; i = chunk.text.indexOf("\n", i + 1)) lineBase++;
20172
+ }
20173
+ return dropFuzzyFindings(findings, (f) => ({ detector: f.detector, rule: f.rule }));
20174
+ }
20453
20175
  function sessionFileBytes(ref) {
20454
20176
  const backend = getBackendForId(ref.prefixedId);
20455
20177
  if (!backend?.isAppendOnly?.()) return 0;
20456
20178
  return backend.fileSize?.(ref.prefixedId) ?? 0;
20457
20179
  }
20180
+ function sessionBuildBytes(ref) {
20181
+ const main = sessionFileBytes(ref);
20182
+ if (main === 0) return 0;
20183
+ return Math.max(main, shadowUncompressedBytes(ref.toolId, ref.rawId));
20184
+ }
20458
20185
  async function buildConversationSync(ref, mtime, opts = {}) {
20459
20186
  const mapPath = opts.mapPath ?? ((p) => p);
20460
20187
  let includeRaw = opts.includeRaw !== false;
20461
20188
  let includeMeta = opts.includeMeta !== false;
20462
20189
  let srcHash;
20463
- const fileBytes = sessionFileBytes(ref);
20464
- const oversized = fileBytes > FULL_BUILD_MAX_BYTES;
20465
- let tailText = "";
20466
- let tailOffsetEnd = 0;
20190
+ const oversized = sessionBuildBytes(ref) > FULL_BUILD_MAX_BYTES;
20191
+ let headOffsetEnd = 0;
20192
+ let oversizedFindings = null;
20467
20193
  let transcript = null;
20468
20194
  let fullestContainer = null;
20469
20195
  if (oversized) {
20470
20196
  const backend = getBackendForId(ref.prefixedId);
20471
20197
  if (!backend?.readFromOffset) return null;
20472
20198
  try {
20473
- const tail = await backend.readFromOffset(ref.prefixedId, fileBytes - FULL_BUILD_TAIL_BYTES);
20474
- tailText = tail.text;
20475
- tailOffsetEnd = tail.newOffset;
20476
- const messages = parseTailMessages(ref.toolId, tailText);
20199
+ let head = await backend.readFromOffset(ref.prefixedId, 0, SYNC_CHUNK_BYTES);
20200
+ for (let hops = 0; head.skippedBytes && hops < 8; hops++) {
20201
+ head = await backend.readFromOffset(ref.prefixedId, head.newOffset, SYNC_CHUNK_BYTES);
20202
+ }
20203
+ headOffsetEnd = head.newOffset;
20204
+ const messages = parseTailMessages(ref.toolId, head.text);
20477
20205
  if (messages.length > 0) transcript = { messages, subagents: [] };
20478
20206
  } catch {
20479
20207
  }
20480
20208
  includeRaw = false;
20481
- includeMeta = false;
20209
+ oversizedFindings = await scanTranscriptFindingsChunked(ref, opts.tenantRules);
20482
20210
  } else {
20483
20211
  try {
20484
20212
  const backend = getBackendForId(ref.prefixedId) ?? getBackend("claude");
@@ -20539,7 +20267,6 @@ async function buildConversationSync(ref, mtime, opts = {}) {
20539
20267
  );
20540
20268
  let projectPath = ref.projectPath;
20541
20269
  const meta = { messageCount: textMessages.length };
20542
- if (oversized) meta.truncated = true;
20543
20270
  if (textMessages.filter((m) => m.role === "user").length <= 1) meta.oneShot = true;
20544
20271
  try {
20545
20272
  const cwd = ref.fullPath ? readCwdFromJsonl(ref.fullPath) : "";
@@ -20653,21 +20380,7 @@ async function buildConversationSync(ref, mtime, opts = {}) {
20653
20380
  }
20654
20381
  scanMs = performance.now() - t0;
20655
20382
  }
20656
- if (oversized && tailText) {
20657
- const t0 = performance.now();
20658
- try {
20659
- for (const f of scanTextForFindings(tailText)) {
20660
- findings.push({ session_id: ref.prefixedId, detector: "builtin", rule: f.rule, line: f.line, preview: f.preview });
20661
- }
20662
- if (opts.tenantRules && opts.tenantRules.length > 0) {
20663
- for (const f of scanTenantRules(tailText, opts.tenantRules)) {
20664
- findings.push({ session_id: ref.prefixedId, detector: f.detector, rule: f.rule, line: f.line, preview: f.preview });
20665
- }
20666
- }
20667
- } catch {
20668
- }
20669
- scanMs = performance.now() - t0;
20670
- }
20383
+ if (oversizedFindings) findings.push(...oversizedFindings);
20671
20384
  const keptFindings = dropFuzzyFindings(findings, (f) => ({ detector: f.detector, rule: f.rule }));
20672
20385
  findings.length = 0;
20673
20386
  findings.push(...keptFindings);
@@ -20713,7 +20426,10 @@ async function buildConversationSync(ref, mtime, opts = {}) {
20713
20426
  // (convBatch onFlush uses this, not a recomputed size) so the next
20714
20427
  // append's base_offset matches exactly — no spurious continuity misses on
20715
20428
  // an actively-growing session. undefined for non-append-only backends.
20716
- from_offset: oversized ? tailOffsetEnd : getBackendForId(ref.prefixedId)?.fileSize?.(ref.rawId),
20429
+ from_offset: oversized ? headOffsetEnd : getBackendForId(ref.prefixedId)?.fileSize?.(ref.rawId),
20430
+ // The head of a chunked session. The server reports back when its shrink
20431
+ // guard keeps a fuller stored copy, so the client appends from there.
20432
+ ...oversized ? { chunked: true } : {},
20717
20433
  meta,
20718
20434
  mtime
20719
20435
  },
@@ -20735,12 +20451,30 @@ async function buildConversationTail(ref, fromOffset, opts = {}) {
20735
20451
  const backend = getBackendForId(ref.prefixedId);
20736
20452
  if (!backend?.readFromOffset) return null;
20737
20453
  const mapPath = opts.mapPath ?? ((p) => p);
20738
- const { text: tailText, newOffset } = await backend.readFromOffset(ref.prefixedId, fromOffset);
20739
- if (!tailText.trim()) return null;
20740
- const messages = parseTailMessages(ref.toolId, tailText);
20741
- if (messages.length === 0) return null;
20454
+ const { text: tailText, newOffset, skippedBytes } = await backend.readFromOffset(ref.prefixedId, fromOffset, SYNC_CHUNK_BYTES);
20455
+ if (newOffset <= fromOffset) return null;
20456
+ const messages = skippedBytes ? [] : parseTailMessages(ref.toolId, tailText);
20742
20457
  const textMessages = messages.filter((m) => m.role !== "summary" && m.content?.trim());
20743
- if (textMessages.length === 0 && !messages.some((m) => m.toolCalls?.length)) return null;
20458
+ const hasContent = textMessages.length > 0 || messages.some((m) => m.toolCalls?.length);
20459
+ if (!hasContent) {
20460
+ return {
20461
+ conv: {
20462
+ session_id: ref.prefixedId,
20463
+ tool: ref.toolId,
20464
+ project_path: mapPath(ref.projectPath),
20465
+ append: true,
20466
+ base_offset: fromOffset,
20467
+ from_offset: newOffset,
20468
+ redacted_text: "",
20469
+ envelope: { v: TRANSCRIPT_VERSION, messages: [], subagents: [] },
20470
+ mtime: Math.floor(ref.mtime) || 0
20471
+ },
20472
+ redactions: 0,
20473
+ newOffset,
20474
+ findings: [],
20475
+ bytes: 0
20476
+ };
20477
+ }
20744
20478
  const firstUser = textMessages.find((m) => m.role === "user");
20745
20479
  if (isInternalToolPrompt(firstUser?.content)) return null;
20746
20480
  const count = { redactions: 0 };
@@ -20783,7 +20517,8 @@ async function buildConversationTail(ref, fromOffset, opts = {}) {
20783
20517
  },
20784
20518
  redactions: count.redactions,
20785
20519
  newOffset,
20786
- findings
20520
+ findings,
20521
+ bytes: Buffer.byteLength(tailText)
20787
20522
  };
20788
20523
  }
20789
20524
  function isSyncSkip(r) {
@@ -20813,7 +20548,7 @@ async function syncIncremental(opts = {}) {
20813
20548
  lock.release();
20814
20549
  }
20815
20550
  }
20816
- var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, 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;
20551
+ var ownVersion, ERROR_CLASS, STANDING_FAILURE, walkProgressForUpload, lastServerPackSpec, credPath, _securityConfigCache, SECURITY_CONFIG_TTL_MS, _syncConfigCache, EMPTY_SYNC_CONFIG, SYNC_RULES_REFUSAL, hashPath, acceptedUploads, MAX_DERIVED_ROW_BYTES, isLocalHost, CAPS_TTL_MS, capsOk, advertisedIngestConc, advertisedGzipBody, FULL_BUILD_MAX_BYTES, DERIVED_MAX_BYTES, SYNC_CHUNK_BYTES, BATCH_SCAN_MAX_BYTES;
20817
20552
  var init_sync_client = __esm({
20818
20553
  "src/sync-client.ts"() {
20819
20554
  "use strict";
@@ -20858,7 +20593,7 @@ var init_sync_client = __esm({
20858
20593
  ownVersion = JSON.parse(readFileSync35(join42(dir, "../package.json"), "utf-8")).version || ownVersion;
20859
20594
  } catch {
20860
20595
  try {
20861
- if (true) ownVersion = "0.6.9";
20596
+ if (true) ownVersion = "0.7.1";
20862
20597
  } catch {
20863
20598
  }
20864
20599
  }
@@ -20884,8 +20619,9 @@ var init_sync_client = __esm({
20884
20619
  capsOk = /* @__PURE__ */ new Map();
20885
20620
  advertisedIngestConc = /* @__PURE__ */ new Map();
20886
20621
  advertisedGzipBody = /* @__PURE__ */ new Set();
20887
- FULL_BUILD_MAX_BYTES = Math.max(8, parseInt(process.env.CHAT_RECALL_FULL_BUILD_MAX_MB || "64", 10)) * 1024 * 1024;
20888
- FULL_BUILD_TAIL_BYTES = 16 * 1024 * 1024;
20622
+ FULL_BUILD_MAX_BYTES = Math.max(8, parseInt(process.env.CHAT_RECALL_FULL_BUILD_MAX_MB || "24", 10)) * 1024 * 1024;
20623
+ DERIVED_MAX_BYTES = Math.max(8, parseInt(process.env.CHAT_RECALL_DERIVED_MAX_MB || "64", 10)) * 1024 * 1024;
20624
+ SYNC_CHUNK_BYTES = 8 * 1024 * 1024;
20889
20625
  BATCH_SCAN_MAX_BYTES = Math.max(1, parseInt(process.env.CHAT_RECALL_BATCHSCAN_MAX_MB || "64", 10)) * 1024 * 1024;
20890
20626
  }
20891
20627
  });
@@ -21034,7 +20770,7 @@ var init_artifact_codec = __esm({
21034
20770
  });
21035
20771
 
21036
20772
  // ../engine/dist/core/toolkit-sync.js
21037
- import { existsSync as existsSync41, mkdirSync as mkdirSync11, readFileSync as readFileSync37, writeFileSync as writeFileSync10, cpSync } from "fs";
20773
+ import { existsSync as existsSync42, mkdirSync as mkdirSync11, readFileSync as readFileSync37, writeFileSync as writeFileSync10, cpSync } from "fs";
21038
20774
  import { dirname as dirname20, join as join44, basename as basename22 } from "path";
21039
20775
  import { homedir as homedir17 } from "os";
21040
20776
  function rowFromMemoryItem(type, item) {
@@ -21068,11 +20804,11 @@ function skillsDirFor(tool) {
21068
20804
  }
21069
20805
  function copySkill(row, toTool) {
21070
20806
  const skillDir = row.extra.skillDir || dirname20(row.filePath);
21071
- if (!existsSync41(skillDir))
20807
+ if (!existsSync42(skillDir))
21072
20808
  return { ok: false, status: 404, error: `Source dir missing: ${skillDir}` };
21073
20809
  const name = row.extra.skillName || basename22(skillDir);
21074
20810
  const targetDir = join44(skillsDirFor(toTool), name);
21075
- if (existsSync41(targetDir))
20811
+ if (existsSync42(targetDir))
21076
20812
  return { ok: false, status: 409, error: `Already exists: ${targetDir}` };
21077
20813
  try {
21078
20814
  mkdirSync11(dirname20(targetDir), { recursive: true });
@@ -21104,7 +20840,7 @@ function readMcpEntry(tool, name) {
21104
20840
  return entry;
21105
20841
  }
21106
20842
  for (const t of tries) {
21107
- if (!existsSync41(t.path))
20843
+ if (!existsSync42(t.path))
21108
20844
  continue;
21109
20845
  try {
21110
20846
  const cfg = JSON.parse(readFileSync37(t.path, "utf-8"));
@@ -21117,7 +20853,7 @@ function readMcpEntry(tool, name) {
21117
20853
  return null;
21118
20854
  }
21119
20855
  function readCodexMcpEntry(path, target) {
21120
- if (!existsSync41(path))
20856
+ if (!existsSync42(path))
21121
20857
  return null;
21122
20858
  let content;
21123
20859
  try {
@@ -21171,7 +20907,7 @@ function readCodexMcpEntry(path, target) {
21171
20907
  }
21172
20908
  function writeCodexMcpEntry(path, name, entry) {
21173
20909
  let content = "";
21174
- if (existsSync41(path))
20910
+ if (existsSync42(path))
21175
20911
  content = readFileSync37(path, "utf-8");
21176
20912
  const header = `[mcp_servers.${name}]`;
21177
20913
  if (content.includes(header))
@@ -21229,7 +20965,7 @@ function writeMcpEntry(toTool, name, entry) {
21229
20965
  key2 = "mcp";
21230
20966
  }
21231
20967
  let cfg = {};
21232
- if (existsSync41(path)) {
20968
+ if (existsSync42(path)) {
21233
20969
  try {
21234
20970
  cfg = JSON.parse(readFileSync37(path, "utf-8"));
21235
20971
  } catch {
@@ -21293,7 +21029,7 @@ function copyCommandOrAgent(type, row, toTool) {
21293
21029
  return { ok: false, status: 404, error: `Source unreadable: ${e instanceof Error ? e.message : "failed"}` };
21294
21030
  }
21295
21031
  const out = emit(type, art, toTool);
21296
- if (existsSync41(out.path))
21032
+ if (existsSync42(out.path))
21297
21033
  return { ok: false, status: 409, error: `Already exists: ${out.path}` };
21298
21034
  try {
21299
21035
  mkdirSync11(dirname20(out.path), { recursive: true });
@@ -21313,7 +21049,7 @@ function copyInstructions(row, toTool) {
21313
21049
  return { ok: false, status: 404, error: `Source unreadable: ${e instanceof Error ? e.message : "failed"}` };
21314
21050
  }
21315
21051
  const out = emit("instructions", art, toTool, row.projectPath || void 0);
21316
- if (existsSync41(out.path))
21052
+ if (existsSync42(out.path))
21317
21053
  return { ok: false, status: 409, error: `Already exists: ${out.path}` };
21318
21054
  try {
21319
21055
  mkdirSync11(dirname20(out.path), { recursive: true });
@@ -21456,7 +21192,7 @@ var init_toolkit_sync = __esm({
21456
21192
  });
21457
21193
 
21458
21194
  // ../engine/dist/core/which.js
21459
- import { existsSync as existsSync43, statSync as statSync39, accessSync, constants } from "node:fs";
21195
+ import { existsSync as existsSync44, statSync as statSync39, accessSync, constants } from "node:fs";
21460
21196
  import { join as join46, isAbsolute as isAbsolute2, delimiter as delimiter2, sep as sep3 } from "node:path";
21461
21197
  function executable(p) {
21462
21198
  try {
@@ -21497,7 +21233,7 @@ function resolveOnPath(name) {
21497
21233
  continue;
21498
21234
  for (const c of candidates(name)) {
21499
21235
  const full = join46(dir, c);
21500
- if (existsSync43(full) && executable(full))
21236
+ if (existsSync44(full) && executable(full))
21501
21237
  return full;
21502
21238
  }
21503
21239
  }
@@ -21522,7 +21258,7 @@ __export(toolkit_pull_exports, {
21522
21258
  planPull: () => planPull,
21523
21259
  portableCommand: () => portableCommand
21524
21260
  });
21525
- import { existsSync as existsSync44, mkdirSync as mkdirSync13, statSync as statSync40, writeFileSync as writeFileSync12 } from "node:fs";
21261
+ import { existsSync as existsSync45, mkdirSync as mkdirSync13, statSync as statSync40, writeFileSync as writeFileSync12 } from "node:fs";
21526
21262
  import { dirname as dirname22, join as join47, basename as basename23, isAbsolute as isAbsolute3 } from "node:path";
21527
21263
  function isExecutableFile(p) {
21528
21264
  try {
@@ -21675,7 +21411,7 @@ function executePull(rows, opts = {}) {
21675
21411
  for (const tool of tools) {
21676
21412
  const dir = join47(skillsDirFor(tool), name);
21677
21413
  const file = join47(dir, "SKILL.md");
21678
- if (existsSync44(file)) {
21414
+ if (existsSync45(file)) {
21679
21415
  outcomes.push({ type: "skill", name, tool, status: "present" });
21680
21416
  continue;
21681
21417
  }
@@ -21720,7 +21456,7 @@ function executePull(rows, opts = {}) {
21720
21456
  outcomes.push({ type, name, tool, status: "failed", reason: e instanceof Error ? e.message : "emit failed" });
21721
21457
  continue;
21722
21458
  }
21723
- if (existsSync44(out.path)) {
21459
+ if (existsSync45(out.path)) {
21724
21460
  outcomes.push({ type, name, tool, status: "present" });
21725
21461
  continue;
21726
21462
  }
@@ -21896,6 +21632,9 @@ async function repairSession(id, opts = {}) {
21896
21632
  note: "archive count is a lower bound; the rebuild also unions the on-disk tail"
21897
21633
  };
21898
21634
  }
21635
+ if (!opts.force && endpoints.every((ep) => (currentOnServer.get(ep.url) ?? 0) >= fullest.messages)) {
21636
+ return { sessionId: id, status: "already-full", fullestMessages: fullest.messages, fullestSource: fullest.source, pushed: [] };
21637
+ }
21899
21638
  seedShadow(rawId, fullest.container);
21900
21639
  const located = backend.findSession(rawId);
21901
21640
  const projectPath = located?.projectPath || cwdFromContainer(fullest.container);
@@ -21938,7 +21677,7 @@ async function repairSession(id, opts = {}) {
21938
21677
  try {
21939
21678
  const res = await fetchWithTimeout(`${ep.url}/api/sync`, { method: "POST", headers, body: JSON.stringify({ conversations: [built.conv] }) }, 6e4);
21940
21679
  if (!res.ok) {
21941
- pushed.push({ server: ep.url, before, after: before });
21680
+ pushed.push({ server: ep.url, before, after: before, error: `HTTP ${res.status}` });
21942
21681
  log6(`push to ${ep.url} FAILED: HTTP ${res.status}`);
21943
21682
  continue;
21944
21683
  }
@@ -21947,10 +21686,21 @@ async function repairSession(id, opts = {}) {
21947
21686
  pushed.push({ server: ep.url, before, after: afterN });
21948
21687
  log6(`pushed to ${ep.url}: ${before} \u2192 ${afterN} msg(s)`);
21949
21688
  } catch (e) {
21950
- pushed.push({ server: ep.url, before, after: before });
21951
- log6(`push to ${ep.url} errored: ${e instanceof Error ? e.message : e}`);
21689
+ const error = e instanceof Error ? e.message : String(e);
21690
+ pushed.push({ server: ep.url, before, after: before, error });
21691
+ log6(`push to ${ep.url} errored: ${error}`);
21952
21692
  }
21953
21693
  }
21694
+ if (pushed.every((p) => p.error)) {
21695
+ return {
21696
+ sessionId: id,
21697
+ status: "error",
21698
+ fullestMessages: builtCount,
21699
+ fullestSource: fullest.source,
21700
+ pushed,
21701
+ note: `every push failed: ${pushed.map((p) => `${p.server}: ${p.error}`).join("; ")}`
21702
+ };
21703
+ }
21954
21704
  return { sessionId: id, status: "repaired", fullestMessages: builtCount, fullestSource: fullest.source, pushed };
21955
21705
  }
21956
21706
  async function repairSessions(ids, opts = {}) {
@@ -22160,7 +21910,7 @@ var init_code_intel = __esm({
22160
21910
  });
22161
21911
 
22162
21912
  // ../engine/dist/core/companions.js
22163
- import { existsSync as existsSync47, mkdirSync as mkdirSync16, statSync as statSync43, chmodSync as chmodSync4, unlinkSync as unlinkSync4, renameSync as renameSync5, createWriteStream, readFileSync as readFileSync42, writeFileSync as writeFileSync15 } from "node:fs";
21913
+ import { existsSync as existsSync48, mkdirSync as mkdirSync16, statSync as statSync43, chmodSync as chmodSync4, unlinkSync as unlinkSync4, renameSync as renameSync5, createWriteStream, readFileSync as readFileSync42, writeFileSync as writeFileSync15 } from "node:fs";
22164
21914
  import { homedir as homedir19 } from "node:os";
22165
21915
  import { join as join51 } from "node:path";
22166
21916
  import { execSync as execSync3 } from "node:child_process";
@@ -22283,7 +22033,7 @@ Refusing to install. The release may have been re-uploaded, or something is serv
22283
22033
  renameSync5(tmpPath, CODEINDEX_BIN_PATH);
22284
22034
  } catch (err) {
22285
22035
  try {
22286
- if (existsSync47(tmpPath))
22036
+ if (existsSync48(tmpPath))
22287
22037
  unlinkSync4(tmpPath);
22288
22038
  } catch {
22289
22039
  }
@@ -23367,7 +23117,7 @@ __export(service_installer_exports, {
23367
23117
  uninstallService: () => uninstallService
23368
23118
  });
23369
23119
  import { execSync as execSync5 } from "child_process";
23370
- import { existsSync as existsSync48, mkdirSync as mkdirSync17, readFileSync as readFileSync44, rmSync as rmSync7, writeFileSync as writeFileSync16 } from "fs";
23120
+ import { existsSync as existsSync49, mkdirSync as mkdirSync17, readFileSync as readFileSync44, rmSync as rmSync7, writeFileSync as writeFileSync16 } from "fs";
23371
23121
  import { homedir as homedir20 } from "os";
23372
23122
  import { join as join53 } from "path";
23373
23123
  import { fileURLToPath as fileURLToPath5 } from "url";
@@ -23394,7 +23144,7 @@ function isServiceRunning() {
23394
23144
  return /com\.chat-recall\.watch/.test(tryCmd("launchctl list 2>/dev/null"));
23395
23145
  }
23396
23146
  if (process.platform === "linux") {
23397
- return /active/.test(tryCmd("systemctl --user is-active chat-recall-watch.service 2>/dev/null"));
23147
+ return /^active$/m.test(tryCmd("systemctl --user is-active chat-recall-watch.service 2>/dev/null"));
23398
23148
  }
23399
23149
  return false;
23400
23150
  }
@@ -23403,7 +23153,7 @@ function refreshServiceDefinition() {
23403
23153
  try {
23404
23154
  const { watchJs, node, logFile } = resolveDaemonPaths();
23405
23155
  const unitPath = join53(homedir20(), ".config", "systemd", "user", "chat-recall-watch.service");
23406
- if (!existsSync48(unitPath)) return false;
23156
+ if (!existsSync49(unitPath)) return false;
23407
23157
  const current = readFileSync44(unitPath, "utf8");
23408
23158
  if (!current.includes(watchJs)) return false;
23409
23159
  const wanted = renderSystemdUnit(watchJs, node, logFile);
@@ -23470,7 +23220,12 @@ function renderSystemdUnit(watchJs, node, logFile) {
23470
23220
  // sawtooth. 1536MB keeps GC honest (768 OOMed on the largest subagent-fanout session) with plenty of headroom for the
23471
23221
  // largest transcripts.
23472
23222
  `ExecStart=${node} --max-old-space-size=1536 ${watchJs}`,
23473
- "Restart=on-failure",
23223
+ // ALWAYS: the collector never ends its own work, so any exit systemd did
23224
+ // not ask for is a fault. Under on-failure a clean exit left it dead: on
23225
+ // 2026-09-16 the unit went "inactive (dead) … status=0/SUCCESS" and stayed
23226
+ // down for 8 days, so nothing synced and the auto-update tick, which runs
23227
+ // inside the daemon, never ran. `systemctl stop` still stops it.
23228
+ "Restart=always",
23474
23229
  "RestartSec=10",
23475
23230
  "Nice=10",
23476
23231
  // NEVER STOP RETRYING. systemd's default start limiter gives up after a
@@ -23566,7 +23321,7 @@ function uninstallService() {
23566
23321
  } catch {
23567
23322
  }
23568
23323
  const unitPath = join53(homedir20(), ".config", "systemd", "user", "chat-recall-watch.service");
23569
- if (existsSync48(unitPath)) {
23324
+ if (existsSync49(unitPath)) {
23570
23325
  rmSync7(unitPath, { force: true });
23571
23326
  removed = true;
23572
23327
  }
@@ -23583,7 +23338,7 @@ function uninstallService() {
23583
23338
  } catch {
23584
23339
  }
23585
23340
  let removed = false;
23586
- if (existsSync48(plistPath)) {
23341
+ if (existsSync49(plistPath)) {
23587
23342
  rmSync7(plistPath, { force: true });
23588
23343
  removed = true;
23589
23344
  }
@@ -23640,12 +23395,12 @@ init_codex2();
23640
23395
  init_cursor();
23641
23396
  init_opencode();
23642
23397
  init_agy();
23643
- import { existsSync as existsSync45, readFileSync as readFileSync39, writeFileSync as writeFileSync13, appendFileSync, mkdirSync as mkdirSync14 } from "node:fs";
23398
+ import { existsSync as existsSync46, readFileSync as readFileSync39, writeFileSync as writeFileSync13, appendFileSync, mkdirSync as mkdirSync14 } from "node:fs";
23644
23399
  import { join as join48 } from "node:path";
23645
23400
 
23646
23401
  // src/project-tasks.ts
23647
23402
  init_http();
23648
- import { existsSync as existsSync42, readFileSync as readFileSync38, writeFileSync as writeFileSync11, mkdirSync as mkdirSync12, statSync as statSync38 } from "node:fs";
23403
+ import { existsSync as existsSync43, readFileSync as readFileSync38, writeFileSync as writeFileSync11, mkdirSync as mkdirSync12, statSync as statSync38 } from "node:fs";
23649
23404
  import { join as join45, dirname as dirname21 } from "node:path";
23650
23405
  import { homedir as homedir18 } from "node:os";
23651
23406
 
@@ -23753,7 +23508,7 @@ async function pushProjectTaskStatuses(base2, authHeaders, opts = {}) {
23753
23508
  }
23754
23509
  for (const project of projects) {
23755
23510
  const file = join45(project, fk.filename);
23756
- if (!existsSync42(file)) continue;
23511
+ if (!existsSync43(file)) continue;
23757
23512
  let mtime;
23758
23513
  try {
23759
23514
  mtime = statSync38(file).mtimeMs;
@@ -23814,7 +23569,7 @@ function instructionTargets(rootPath, isGlobal) {
23814
23569
  const tools = installed.length ? installed : [INSTRUCTION_TOOLS[0]];
23815
23570
  if (isGlobal) return [...new Set(tools.map((t) => instructionsPath(t.id)))];
23816
23571
  if (!rootPath) return [];
23817
- const present = INSTRUCTION_FILENAMES.map((f) => join48(rootPath, f)).filter((p) => existsSync45(p));
23572
+ const present = INSTRUCTION_FILENAMES.map((f) => join48(rootPath, f)).filter((p) => existsSync46(p));
23818
23573
  if (present.length) return present;
23819
23574
  return [...new Set(tools.map((t) => join48(rootPath, instructionsFilename(t.id))))];
23820
23575
  }
@@ -23842,7 +23597,7 @@ function applyCodeRecommendation(intent) {
23842
23597
  mkdirSync14(dir, { recursive: true });
23843
23598
  } catch {
23844
23599
  }
23845
- const existing = existsSync45(file) ? readFileSync39(file, "utf8") : "";
23600
+ const existing = existsSync46(file) ? readFileSync39(file, "utf8") : "";
23846
23601
  if (existing.includes(text)) {
23847
23602
  skipped.push(file);
23848
23603
  continue;
@@ -23944,7 +23699,7 @@ try {
23944
23699
  ownVersion2 = JSON.parse(readFileSync39(join48(dir, "../package.json"), "utf-8")).version || ownVersion2;
23945
23700
  } catch {
23946
23701
  try {
23947
- if (true) ownVersion2 = "0.6.9";
23702
+ if (true) ownVersion2 = "0.7.1";
23948
23703
  } catch {
23949
23704
  }
23950
23705
  }
@@ -24248,8 +24003,8 @@ function rotateLogIfLarge(logPath, maxBytes = LOG_MAX_BYTES) {
24248
24003
 
24249
24004
  // src/collector-migrate.ts
24250
24005
  init_http();
24251
- import { existsSync as existsSync46 } from "node:fs";
24252
- function selectProjectsToReindex(projects, current, existsFn = existsSync46) {
24006
+ import { existsSync as existsSync47 } from "node:fs";
24007
+ function selectProjectsToReindex(projects, current, existsFn = existsSync47) {
24253
24008
  const reindex = [];
24254
24009
  const missing = [];
24255
24010
  for (const p of projects) {
@@ -24291,8 +24046,8 @@ async function runCollectorMigration(opts) {
24291
24046
  // auto-indexer/indexer.ts
24292
24047
  init_sync_ledger();
24293
24048
  init_collector_health();
24294
- import { existsSync as existsSync49, readFileSync as readFileSync45, statSync as statSync44 } from "fs";
24295
- var CLI_VERSION3 = true ? "0.6.9" : "0.0.0";
24049
+ import { existsSync as existsSync50, readFileSync as readFileSync45, statSync as statSync44 } from "fs";
24050
+ var CLI_VERSION3 = true ? "0.7.1" : "0.0.0";
24296
24051
  var DEBOUNCE_MS = 5e3;
24297
24052
  var CLAUDE_DIR = claudeBackend.projectsDir();
24298
24053
  var PLANS_DIR = claudeBackend.plansDir();
@@ -24697,7 +24452,7 @@ function discoverWorkspaces() {
24697
24452
  const isExcluded = (p) => excluded.some((x) => p.includes(x));
24698
24453
  const candidates2 = [...newest.entries()].filter(([p]) => !isExcluded(p)).filter(([p]) => !isPersonalPath(p, includeProjects)).filter(([p]) => {
24699
24454
  try {
24700
- return existsSync49(p);
24455
+ return existsSync50(p);
24701
24456
  } catch {
24702
24457
  return false;
24703
24458
  }
@@ -24729,7 +24484,7 @@ function isCodeWorkspace(p) {
24729
24484
  }
24730
24485
  return WORKSPACE_MARKERS.some((m) => {
24731
24486
  try {
24732
- return existsSync49(join54(p, m));
24487
+ return existsSync50(join54(p, m));
24733
24488
  } catch {
24734
24489
  return false;
24735
24490
  }