token-goat 2.9.6 → 2.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -37,7 +37,7 @@ Restart your AI sessions. Run `token-goat stats` a couple of minutes after your
37
37
 
38
38
  > **Built and continually improved, free, by one person. If it saves you tokens, drop a ⭐️ at the top of this page. One click. Makes my day. Also, if you'd like anything added, [drop me a line](mailto:token-goat@dfkhelper.com).**
39
39
 
40
- [Install](docs/install.md) · [CLI](docs/cli.md) · [What gets installed?](docs/install.md#what-gets-installed) · [Stats](#stats-display) · [Security & uninstall](docs/security.md)
40
+ [Install](docs/install.md) · [CLI](docs/cli.md) · [Architecture Q&A](docs/architecture-qa.html) · [What gets installed?](docs/install.md#what-gets-installed) · [Stats](#stats-display) · [Security & uninstall](docs/security.md)
41
41
 
42
42
  ---
43
43
 
@@ -129,7 +129,7 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
129
129
  | `claude-in-chrome`'s `computer`/`browser_batch` return a raw, full-resolution base64 screenshot in-band, with no destination-file option to redirect through image-shrink | Inline screenshot blocks are shrunk via the same image-shrink pipeline in place, and a repeated `Tab Context:` listing (appended to nearly every call, often unchanged) collapses to a placeholder once seen unchanged this session |
130
130
  | Agent tool spawns a subagent with no orientation and no reuse hints | A `PreToolUse` handler appends a compact briefing pack to the prompt: a one-line project-map summary, 2-3 recent cached-output IDs, and a surgical-read reminder (~300 tokens) |
131
131
  | Subagent's own final report runs long and gets discarded once the parent moves on | Agent tool results ≥8000 characters get a recall pointer appended (`token-goat recall`); the original report always reaches the parent untouched |
132
- | Agent tool spawns with no `subagent_type` (or an explicit `general-purpose`), so the lane starts by paying for every tool and MCP schema on the machine | Once per session, a `PostToolUse` advisory names up to three `tools:`-restricted agent definitions found in `~/.claude/agents` and suggests passing one as `subagent_type` on a future spawn that fits. It fires only when at least one restricted definition exists, states outright that the observed spawn already ran and nothing was saved, and records a zero-credit stat |
132
+ | Agent tool spawns with no `subagent_type` (or an explicit `general-purpose`), so the lane starts by paying for every tool and MCP schema on the machine | Once per session, a `PostToolUse` advisory names up to three `tools:`-restricted agent definitions found in `~/.claude/agents` or the current project's `.claude/agents` and suggests passing one as `subagent_type` on a future spawn that fits. It fires only when at least one restricted definition exists, states outright that the observed spawn already ran and nothing was saved, and records a zero-credit stat |
133
133
  | Large MCP tool result (≥2 KB) is a homogeneous array of objects, e.g. a list/search result | Deterministic structural compression: table-ified into one header row + tab-delimited rows, with columns constant across every row hoisted into a single `constant:` line instead of repeated per row; only applied when it saves ≥15%. Full original always recoverable via `token-goat bash-output <id>` (labeled `[token-goat: compressed, full via mcp-output <id>]`). Disable with `TOKEN_GOAT_MCP_COMPRESS=0` |
134
134
  | Large MCP tool result (≥2 KB) doesn't table-ify (an object-rooted config dump, a single-resource response, a nested settings tree) but most of its bytes are `null`/`""`/`[]`/`{}` | Deterministic empty-value pruning: recursively drops those four empty shapes to a fixed point (a container left empty by its own dropped children is dropped too) and renders the remainder as compact JSON with a trailing `dropped N empty value(s) (null, "", [], {})` summary line; `0` and `false` are never dropped; only applied when it saves ≥15%. Same full-recovery-by-id guarantee and `TOKEN_GOAT_MCP_COMPRESS=0` opt-out as the table pass |
135
135
  | GitHub MCP tool result (`list_pull_requests`, `list_issues`, `search_code`, `get_file_contents`, `pull_request_read`, …) carries dozens of boilerplate fields per object | GitHub compression pack strips `_links`, `node_id`, `gravatar_id`, `site_admin`, and every `*_url` field (`avatar_url`, `html_url`, `events_url`, `gists_url`, `followers_url`, …) except `download_url`/`git_url`/`clone_url`/`ssh_url`, before handing the shrunk JSON to the same table-ifying pass — same `TOKEN_GOAT_MCP_COMPRESS=0` opt-out and full-recovery-by-id guarantee |
@@ -169,6 +169,9 @@ The fastest way to reduce AI token costs is fixing these five, not writing short
169
169
  | Fetched web content lands raw in model context | Always wrapped in an untrusted-content fence; scanned for attack patterns, and a matched pattern name is written to the log and into the fence's notice |
170
170
  | Chatty log repeats the same error or event thousands of times | `token-goat logfold` collapses consecutive duplicates to `[Nx]` counts; same event logged with different timestamps or request IDs folds correctly — ~90–95% smaller on repetitive logs |
171
171
  | Reading poetry.lock or package-lock.json to find a pinned version | `token-goat lockdeps` returns a name/version table of direct dependencies; optional packages and transitive entries excluded |
172
+ | Large SVG / diagram file (≥8 KB) read in full | Coordinate path flooding blocked; extracted layer/group IDs and title shown; redirects to `token-goat xml-outline` and `xml-query` |
173
+ | Broad recursive Glob sweep (`*`, `**/*`) on root directory | Pre-Glob hook warns against tree-dumping and points at `token-goat map --compact` for fast, lightweight structure inspection |
174
+ | Compound test/build pipeline (`npm run build && npm run typecheck && npm test`) | Post-Bash hook routes chained build/test/lint commands to `generic-ci` compression, dropping verbose passing steps and compiler noise |
172
175
 
173
176
  On a per-token API plan, 100K wasted tokens per session runs about $0.30. Five sessions a week is ~$450/year. AI coding cost reduction at that scale comes from fixing the waste, not from using the product less. Token-goat is free. And on subscription plans, it can result in limits feeling 10x higher.
174
177
 
@@ -320,7 +323,7 @@ corpus of captured shell output through the real compression path and reports ho
320
323
  result got, alongside a count of the lines each case is not allowed to lose. Deleting output makes
321
324
  the first number look better, so the second one is what keeps the first honest.
322
325
 
323
- Every command, with its flags, its output, and the comparison workflows that chain them: **[CLI reference](docs/cli.md)**.
326
+ Every command, with its flags, its output, and the comparison workflows that chain them: **[CLI reference](docs/cli.md)**. For architectural analysis, empirical evaluations, and technical answers to common engineering questions: **[Architecture & Technical Q&A](docs/architecture-qa.html)**.
324
327
 
325
328
  ## MCP server
326
329
 
@@ -422,7 +425,7 @@ never drift on where `mcp.json` lives or what key name it looks for.
422
425
 
423
426
  **What the index actually holds, in plain terms.** The point of a surgical read is returning a function body without the file around it, which means the database stores those bodies. `symbols.body` holds the source text of every indexed symbol, `symbols.docstring` its doc comment, `refs.context` the line around each reference, and `chunks.text` the passages that semantic search embeds. There is also a full-text index over the bodies and docstrings. So the database is not a list of names and line numbers: it is a substantial copy of your source, sitting in a plain unencrypted SQLite file outside the repository.
424
427
 
425
- The file-by-file table for each harness, and the path that file sits at: **[What gets installed](docs/install.md#what-gets-installed)**.
428
+ The file-by-file table for each harness, and the path that file sits at: **[What gets installed](docs/install.md#what-gets-installed)** (see also **[Permissions & auto-approval](docs/install.md#command-auto-approval-and-permissions)**).
426
429
 
427
430
  ## Zero maintenance
428
431
 
package/SECURITY.md CHANGED
@@ -101,7 +101,7 @@ For a scanner that ingests a bill of materials rather than a lockfile, `npm run
101
101
 
102
102
  ## Verifying what you installed
103
103
 
104
- Every published version is built and pushed by one workflow, [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which runs only when a GitHub release is published (or manually, and then only from `main`). It publishes with npm provenance, so npm holds a signed attestation tying the tarball to the commit and workflow run that produced it. Nothing is ever published from a laptop.
104
+ Every published version is built and pushed by one workflow, [`.github/workflows/publish.yml`](.github/workflows/publish.yml), which runs only when a GitHub release is published (or manually, and then only from `main`). It publishes with npm provenance, so npm holds a signed attestation tying the tarball to the commit and workflow run that produced it. Nothing is ever published from a developer workstation.
105
105
 
106
106
  To check a copy you already have:
107
107
 
@@ -47,6 +47,7 @@ import {
47
47
  getFilePath,
48
48
  getHarnessName,
49
49
  globalDbPath,
50
+ hasUnquotedOperator,
50
51
  isAutoTriggerMultiplierExplicit,
51
52
  isCaseInsensitiveFs,
52
53
  isCodeFenceDelimiter,
@@ -100,7 +101,7 @@ import {
100
101
  toDisplayPath,
101
102
  toKB,
102
103
  withFileLock
103
- } from "./token-goat-chunk-T2OE7MYM.mjs";
104
+ } from "./token-goat-chunk-KIEOFWLL.mjs";
104
105
  import {
105
106
  registerReset
106
107
  } from "./token-goat-chunk-EEIDFMEM.mjs";
@@ -6688,11 +6689,15 @@ function getFileLineRanges(filePath) {
6688
6689
  return _fileLineRanges.get(foldPath(normalizePath(filePath))) ?? [];
6689
6690
  }
6690
6691
  var MAX_SERVED_OUTPUTS_PER_FILE = 8;
6692
+ var GENERIC_SERVED_OUTPUT_KEY = "\0<bash-generic-served>";
6693
+ var GENERIC_SERVED_OUTPUT_FOLDED_KEY = foldPath(normalizePath(GENERIC_SERVED_OUTPUT_KEY));
6694
+ var MAX_GENERIC_SERVED_OUTPUTS = 32;
6691
6695
  function recordFileServedOutput(filePath, outputId) {
6692
6696
  const key = foldPath(normalizePath(filePath));
6697
+ const cap = key === GENERIC_SERVED_OUTPUT_FOLDED_KEY ? MAX_GENERIC_SERVED_OUTPUTS : MAX_SERVED_OUTPUTS_PER_FILE;
6693
6698
  const ids = (_fileServedOutputs.get(key) ?? []).filter((id) => id !== outputId);
6694
6699
  ids.push(outputId);
6695
- if (ids.length > MAX_SERVED_OUTPUTS_PER_FILE) ids.splice(0, ids.length - MAX_SERVED_OUTPUTS_PER_FILE);
6700
+ if (ids.length > cap) ids.splice(0, ids.length - cap);
6696
6701
  _fileServedOutputs.set(key, ids);
6697
6702
  }
6698
6703
  function getFileServedOutputs(filePath) {
@@ -7624,6 +7629,7 @@ function isPsMultilineSystemQuery(cmd) {
7624
7629
  }
7625
7630
  function getMonitoringRecallHint(cmd) {
7626
7631
  const trimmed = cmd.trim();
7632
+ if (hasUnquotedOperator(trimmed, ["&&", "||", ";"])) return null;
7627
7633
  for (const { pattern, recallHint } of MONITORING_COMMAND_PATTERNS) {
7628
7634
  if (pattern.test(trimmed)) return recallHint;
7629
7635
  }
@@ -8191,13 +8197,15 @@ function mergeLineRanges(disk, mem) {
8191
8197
  }
8192
8198
  return Array.from(byPath.entries());
8193
8199
  }
8200
+ var GENERIC_SERVED_OUTPUT_FOLDED_KEY2 = foldPath(normalizePath(GENERIC_SERVED_OUTPUT_KEY));
8194
8201
  function mergeServedOutputs(disk, mem) {
8195
8202
  const byPath = /* @__PURE__ */ new Map();
8196
8203
  for (const [filePath, ids] of disk) byPath.set(filePath, [...ids]);
8197
8204
  for (const [filePath, ids] of mem) {
8198
8205
  const prev = byPath.get(filePath) ?? [];
8199
8206
  const merged = [...prev.filter((id) => !ids.includes(id)), ...ids];
8200
- if (merged.length > MAX_SERVED_OUTPUTS_PER_FILE) merged.splice(0, merged.length - MAX_SERVED_OUTPUTS_PER_FILE);
8207
+ const cap = filePath === GENERIC_SERVED_OUTPUT_FOLDED_KEY2 ? MAX_GENERIC_SERVED_OUTPUTS : MAX_SERVED_OUTPUTS_PER_FILE;
8208
+ if (merged.length > cap) merged.splice(0, merged.length - cap);
8201
8209
  byPath.set(filePath, merged);
8202
8210
  }
8203
8211
  return Array.from(byPath.entries());
@@ -9939,7 +9947,7 @@ function pruneShrinkCache() {
9939
9947
  }
9940
9948
  }
9941
9949
  async function finalizeShrinkResult(result, filePath) {
9942
- const basename12 = path6.basename(filePath);
9950
+ const basename12 = displaySafePath(path6.basename(filePath));
9943
9951
  const shrinkSaved = result.originalBytes - result.shrunkBytes;
9944
9952
  const tier = loadConfig().image_shrink.vision_tier;
9945
9953
  recordStat("image_shrink", shrinkSaved, visionTokensSaved(result.originalWidth, result.originalHeight, result.width, result.height, tier), void 0, basename12);
@@ -10260,6 +10268,47 @@ function modelDir() {
10260
10268
  function downloadUrl(file) {
10261
10269
  return `https://huggingface.co/${DEFAULT_MODEL}/resolve/${PINNED_MODEL_REVISION}/${file.name}`;
10262
10270
  }
10271
+ function sharedModelCacheDir() {
10272
+ const raw = process.env["TOKEN_GOAT_MODEL_CACHE_DIR"]?.trim();
10273
+ if (!raw) return null;
10274
+ return path7.join(raw, ...DEFAULT_MODEL.split("/"), PINNED_MODEL_REVISION);
10275
+ }
10276
+ async function copyFromSharedCache(shared, file, target) {
10277
+ const source = path7.join(shared, file.name);
10278
+ const temp = `${target}.${process.pid}.shared`;
10279
+ try {
10280
+ if (!fs11.existsSync(source)) return false;
10281
+ fs11.copyFileSync(source, temp);
10282
+ if (await sha256Of(temp) !== file.sha256) {
10283
+ fs11.rmSync(temp, { force: true, maxRetries: 20, retryDelay: 25 });
10284
+ fs11.rmSync(source, { force: true, maxRetries: 20, retryDelay: 25 });
10285
+ return false;
10286
+ }
10287
+ fs11.renameSync(temp, target);
10288
+ return true;
10289
+ } catch {
10290
+ try {
10291
+ fs11.rmSync(temp, { force: true, maxRetries: 20, retryDelay: 25 });
10292
+ } catch {
10293
+ }
10294
+ return false;
10295
+ }
10296
+ }
10297
+ function publishToSharedCache(shared, file, target) {
10298
+ const destination = path7.join(shared, file.name);
10299
+ const temp = `${destination}.${process.pid}.partial`;
10300
+ try {
10301
+ if (fs11.existsSync(destination)) return;
10302
+ fs11.mkdirSync(path7.dirname(destination), { recursive: true });
10303
+ fs11.copyFileSync(target, temp);
10304
+ fs11.renameSync(temp, destination);
10305
+ } catch {
10306
+ try {
10307
+ fs11.rmSync(temp, { force: true, maxRetries: 20, retryDelay: 25 });
10308
+ } catch {
10309
+ }
10310
+ }
10311
+ }
10263
10312
  var _ort = null;
10264
10313
  var _ortError = null;
10265
10314
  var _ortLoadAttempted = false;
@@ -10355,6 +10404,7 @@ async function ensureModelFiles(modelName = DEFAULT_MODEL) {
10355
10404
  ensureDataDirPrivate();
10356
10405
  const dir = modelDir();
10357
10406
  const offline = loadConfig().network.offline;
10407
+ const shared = sharedModelCacheDir();
10358
10408
  for (const file of MODEL_FILES) {
10359
10409
  const target = path7.join(dir, file.name);
10360
10410
  if (fs11.existsSync(target)) {
@@ -10362,6 +10412,10 @@ async function ensureModelFiles(modelName = DEFAULT_MODEL) {
10362
10412
  if (digest === file.sha256) continue;
10363
10413
  fs11.rmSync(target, { force: true });
10364
10414
  }
10415
+ if (shared) {
10416
+ fs11.mkdirSync(path7.dirname(target), { recursive: true });
10417
+ if (await copyFromSharedCache(shared, file, target)) continue;
10418
+ }
10365
10419
  if (offline) {
10366
10420
  throw new Error(
10367
10421
  `Offline mode is on (network.offline): refusing to download ${file.name} for the embedding model. Copy the pinned files into ${dir} on a connected machine to use semantic search here.`
@@ -10372,6 +10426,7 @@ async function ensureModelFiles(modelName = DEFAULT_MODEL) {
10372
10426
  `Downloading the embedding model, once (${file.name}, ${Math.round(file.bytes / 1024 / 1024)} MB) into ${dir}`
10373
10427
  );
10374
10428
  await download(file, target);
10429
+ if (shared) publishToSharedCache(shared, file, target);
10375
10430
  }
10376
10431
  return dir;
10377
10432
  }
@@ -11061,7 +11116,7 @@ function _pathPriorityPenalty(filePath) {
11061
11116
 
11062
11117
  // src/parser_fingerprint.ts
11063
11118
  init_define_import_meta_env();
11064
- var PARSER_FINGERPRINT = "567346f98dadd8cb";
11119
+ var PARSER_FINGERPRINT = "d96c01d5957651c0";
11065
11120
 
11066
11121
  // src/parser.ts
11067
11122
  init_define_import_meta_env();
@@ -14798,16 +14853,26 @@ ${commented}`);
14798
14853
  // src/parser.ts
14799
14854
  var _require3 = createRequire3(import.meta.url);
14800
14855
  var _parserCtor;
14856
+ var _parserCtorError = null;
14857
+ var _parserCtorOverride = void 0;
14801
14858
  var _grammarCache = /* @__PURE__ */ new Map();
14802
14859
  function loadParserCtor() {
14860
+ if (_parserCtorOverride !== void 0) return _parserCtorOverride;
14803
14861
  if (_parserCtor !== void 0) return _parserCtor;
14804
14862
  try {
14805
14863
  _parserCtor = _require3("tree-sitter");
14806
- } catch {
14864
+ } catch (e) {
14807
14865
  _parserCtor = null;
14866
+ _parserCtorError = e instanceof Error ? e : new Error(String(e));
14808
14867
  }
14809
14868
  return _parserCtor;
14810
14869
  }
14870
+ function treeSitterCoreAvailable() {
14871
+ return loadParserCtor() !== null;
14872
+ }
14873
+ function treeSitterCoreLoadError() {
14874
+ return _parserCtorError;
14875
+ }
14811
14876
  var CPP_HEADER_SNIFF_RE = /\bclass\s+\w|\bnamespace\s+\w|\btemplate\s*<|::\s*\w|\b(?:public|private|protected)\s*:/;
14812
14877
  var MAX_SYMBOL_BODY_CHARS = SYMBOL_BODY_CHAR_CAP;
14813
14878
  function boundSymbolBody(body) {
@@ -18768,6 +18833,9 @@ var FILE_TYPE_THRESHOLDS = {
18768
18833
  // always intercept (any size)
18769
18834
  html: 5e4,
18770
18835
  txt: 2e4,
18836
+ log: 1e4,
18837
+ svg: 8e3,
18838
+ xml: 2e4,
18771
18839
  csv: 1e4,
18772
18840
  tsv: 1e4,
18773
18841
  transcript: 1e4,
@@ -18835,8 +18903,9 @@ ${headings.join("\n")}` : ""].filter(Boolean).join("\n")
18835
18903
  }
18836
18904
  function handleTxt(filePath, content, contentLengthHint) {
18837
18905
  const length = contentLengthHint ?? content.length;
18838
- if (length < FILE_TYPE_THRESHOLDS.txt) return { shouldBlock: false, message: "" };
18839
18906
  const isLog = /\.(log|out|err|trace)$/i.test(filePath) || /[\\/]logs[\\/]/.test(filePath);
18907
+ const threshold = isLog ? FILE_TYPE_THRESHOLDS.log : FILE_TYPE_THRESHOLDS.txt;
18908
+ if (length < threshold) return { shouldBlock: false, message: "" };
18840
18909
  const recall = isLog ? `Log file \u2014 use Read with offset/limit params, or: token-goat bash-output --file "${filePath}" --tail 100 --grep "error|ERROR"` : "Use Read with offset and limit params to sample specific line ranges.";
18841
18910
  if (previewUnavailable(content, length)) {
18842
18911
  return {
@@ -18868,6 +18937,64 @@ ${fenceUntrustedFileContent(preview)}
18868
18937
  ${recall}`
18869
18938
  };
18870
18939
  }
18940
+ function handleSvg(filePath, content, contentLengthHint) {
18941
+ const length = contentLengthHint ?? content.length;
18942
+ if (length < FILE_TYPE_THRESHOLDS.svg) return { shouldBlock: false, message: "" };
18943
+ if (previewUnavailable(content, length)) {
18944
+ return {
18945
+ shouldBlock: true,
18946
+ message: [
18947
+ `Large SVG file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap).`,
18948
+ `Inspect structure: token-goat xml-outline "${filePath}"`,
18949
+ `Query layers/elements: token-goat xml-query "${filePath}" "//g[@id]"`
18950
+ ].join("\n")
18951
+ };
18952
+ }
18953
+ const titleMatch = content.match(/<title[^>]*>([^<]*)<\/title>/i);
18954
+ const title = titleMatch ? titleMatch[1]?.trim() : "";
18955
+ const groupIds = [];
18956
+ const idRegex = /<g[^>]*\bid=["']([^"']+)["']/gi;
18957
+ let match;
18958
+ while ((match = idRegex.exec(content)) !== null && groupIds.length < 10) {
18959
+ if (match[1]) groupIds.push(match[1]);
18960
+ }
18961
+ const preview = [
18962
+ title ? `Title: ${title}` : "",
18963
+ groupIds.length > 0 ? `Layer/Group IDs: ${groupIds.join(", ")}` : ""
18964
+ ].filter(Boolean).join("\n");
18965
+ return {
18966
+ shouldBlock: true,
18967
+ message: [
18968
+ `Large SVG/diagram file (${formatBytes(length)}) \u2014 raw coordinate paths flood context.`,
18969
+ preview ? fenceUntrustedFileContent(preview) : "",
18970
+ `Inspect structure: token-goat xml-outline "${filePath}"`,
18971
+ `Query elements: token-goat xml-query "${filePath}" "//g[@id]"`,
18972
+ `Search text labels: token-goat grep "<text>" --glob "${filePath}"`
18973
+ ].filter(Boolean).join("\n")
18974
+ };
18975
+ }
18976
+ function handleXml(filePath, content, contentLengthHint) {
18977
+ const length = contentLengthHint ?? content.length;
18978
+ if (length < FILE_TYPE_THRESHOLDS.xml) return { shouldBlock: false, message: "" };
18979
+ if (previewUnavailable(content, length)) {
18980
+ return {
18981
+ shouldBlock: true,
18982
+ message: [
18983
+ `Large XML file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap).`,
18984
+ `Inspect structure: token-goat xml-outline "${filePath}"`,
18985
+ `Query nodes: token-goat xml-query "${filePath}" "<selector>"`
18986
+ ].join("\n")
18987
+ };
18988
+ }
18989
+ return {
18990
+ shouldBlock: true,
18991
+ message: [
18992
+ `Large XML file (${formatBytes(length)}).`,
18993
+ `Inspect hierarchy: token-goat xml-outline "${filePath}"`,
18994
+ `Query specific elements: token-goat xml-query "${filePath}" "<selector>"`
18995
+ ].join("\n")
18996
+ };
18997
+ }
18871
18998
  function handleOfficeBinary(filePath) {
18872
18999
  const filename = filePath.split(/[\\/]/).pop() || "";
18873
19000
  const parts = filename.split(".");
@@ -18986,6 +19113,8 @@ function dispatchFileTypeHandler(filePath, content, contentLengthHint) {
18986
19113
  if (["md", "mdx", "markdown", "rst"].includes(ext)) return null;
18987
19114
  const effectiveLength = contentLengthHint ?? content.length;
18988
19115
  if (ext === "pdf") return handlePdf(filePath, effectiveLength);
19116
+ if (ext === "svg") return handleSvg(filePath, content, effectiveLength);
19117
+ if (ext === "xml") return handleXml(filePath, content, effectiveLength);
18989
19118
  if (["html", "htm", "xhtml"].includes(ext)) return handleHtml(filePath, content, effectiveLength);
18990
19119
  if (["txt", "log", "out", "err", "trace"].includes(ext)) return handleTxt(filePath, content, effectiveLength);
18991
19120
  if (ext === "xlsx") return handleXlsx(filePath);
@@ -19335,7 +19464,7 @@ function buildDeltaCapsule(projectRoot, limit = 8) {
19335
19464
  }).slice(0, limit);
19336
19465
  if (changed.length === 0) return null;
19337
19466
  return `Cross-session evidence changed since it was cached:
19338
- ${changed.map((entry) => `- ${entry.source} (use a fresh surgical read)`).join("\n")}`;
19467
+ ${changed.map((entry) => `- ${displaySafePath(entry.source)} (use a fresh surgical read)`).join("\n")}`;
19339
19468
  }
19340
19469
 
19341
19470
  // src/notebook_compact.ts
@@ -19941,6 +20070,41 @@ function readIntToolInput(event, key) {
19941
20070
  }
19942
20071
  return void 0;
19943
20072
  }
20073
+ function readRequestedSliceWindow(event) {
20074
+ const rawOffset = readIntToolInput(event, "offset");
20075
+ const rawLimit = readIntToolInput(event, "limit");
20076
+ if (rawOffset !== void 0 || rawLimit !== void 0) {
20077
+ return {
20078
+ offset: rawOffset !== void 0 && rawOffset >= 1 ? Math.floor(rawOffset) : 1,
20079
+ limit: rawLimit !== void 0 && rawLimit > 0 ? Math.floor(rawLimit) : void 0,
20080
+ isExplicitSlice: true
20081
+ };
20082
+ }
20083
+ const rawRange = event.toolInput["view_range"] ?? event.toolInput["lines"] ?? event.toolInput["range"];
20084
+ if (Array.isArray(rawRange) && rawRange.length >= 1) {
20085
+ const rawStart = Number(rawRange[0]);
20086
+ const startVal = Number.isFinite(rawStart) && rawStart >= 1 ? Math.floor(rawStart) : 1;
20087
+ if (rawRange.length >= 2) {
20088
+ const rawEnd = Number(rawRange[1]);
20089
+ if (rawEnd === -1) {
20090
+ return { offset: startVal, isExplicitSlice: true };
20091
+ }
20092
+ if (Number.isFinite(rawEnd) && rawEnd >= startVal) {
20093
+ const lineCount = Math.floor(rawEnd - startVal + 1);
20094
+ return { offset: startVal, limit: lineCount, isExplicitSlice: true };
20095
+ }
20096
+ }
20097
+ return { offset: startVal, isExplicitSlice: true };
20098
+ }
20099
+ const startLine = readIntToolInput(event, "start_line") ?? readIntToolInput(event, "startLine") ?? readIntToolInput(event, "start");
20100
+ const endLine = readIntToolInput(event, "end_line") ?? readIntToolInput(event, "endLine") ?? readIntToolInput(event, "end");
20101
+ if (startLine !== void 0 || endLine !== void 0) {
20102
+ const effectiveStart = startLine !== void 0 && startLine >= 1 ? Math.floor(startLine) : 1;
20103
+ const effectiveLimit = endLine !== void 0 && endLine >= effectiveStart ? Math.floor(endLine - effectiveStart + 1) : void 0;
20104
+ return { offset: effectiveStart, limit: effectiveLimit, isExplicitSlice: true };
20105
+ }
20106
+ return { isExplicitSlice: false };
20107
+ }
19944
20108
  function readStartLine(event) {
19945
20109
  const resp = event.raw["tool_response"];
19946
20110
  if (resp === null || typeof resp !== "object") return 1;
@@ -20004,11 +20168,10 @@ function scanRequestedSlice(absPath, offset, limit) {
20004
20168
  }
20005
20169
  }
20006
20170
  function estimateRequestedSlice(event, absPath) {
20007
- const offset = readIntToolInput(event, "offset");
20008
- const limit = readIntToolInput(event, "limit");
20009
- if (limit === void 0 || limit <= 0) return { kind: "unbounded" };
20010
- const effectiveOffset = offset !== void 0 && offset >= 1 ? offset : 1;
20011
- const scan = scanRequestedSlice(absPath, effectiveOffset, limit);
20171
+ const window2 = readRequestedSliceWindow(event);
20172
+ if (window2.limit === void 0 || window2.limit <= 0) return { kind: "unbounded" };
20173
+ const effectiveOffset = window2.offset !== void 0 && window2.offset >= 1 ? window2.offset : 1;
20174
+ const scan = scanRequestedSlice(absPath, effectiveOffset, window2.limit);
20012
20175
  if (scan === null) return { kind: "unbounded" };
20013
20176
  if (scan.nearSingleLine) return { kind: "nearSingleLine" };
20014
20177
  if (scan.trustworthy) return { kind: "bytes", bytes: scan.bytes };
@@ -20032,7 +20195,7 @@ function isSourceExtension(basename12) {
20032
20195
  return language === "apex" || language === "salesforce_metadata" || language === "salesforce_markup";
20033
20196
  }
20034
20197
  var BINARY_FILE_TYPE_EXTS = /* @__PURE__ */ new Set(["pdf", "docx", "xlsx", "pptx", "odt", "ods", "ott", "odp"]);
20035
- var TEXT_FILE_TYPE_EXTS = /* @__PURE__ */ new Set(["html", "htm", "xhtml", "txt", "log", "out", "err", "trace", "csv", "tsv", "vtt", "srt"]);
20198
+ var TEXT_FILE_TYPE_EXTS = /* @__PURE__ */ new Set(["html", "htm", "xhtml", "txt", "log", "out", "err", "trace", "csv", "tsv", "vtt", "srt", "svg", "xml"]);
20036
20199
  var DISPATCHED_FILE_TYPE_EXTS = /* @__PURE__ */ new Set([...BINARY_FILE_TYPE_EXTS, ...TEXT_FILE_TYPE_EXTS]);
20037
20200
  function isDispatchedFileType(basename12) {
20038
20201
  return DISPATCHED_FILE_TYPE_EXTS.has(path21.extname(basename12).slice(1).toLowerCase());
@@ -20051,7 +20214,7 @@ function surgicalHint(filePath, basename12, lineCount) {
20051
20214
  }
20052
20215
  function detectSkillFile(filePath) {
20053
20216
  const match = filePath.match(/\.claude[\\/]skills[\\/]([^\\/]+)[\\/]SKILL\.md$/i);
20054
- return match ? match[1] : null;
20217
+ return match ? displaySafePath(match[1]) : null;
20055
20218
  }
20056
20219
  function buildLineDiffDetailed(oldContent, newContent, label) {
20057
20220
  const oldLines = oldContent.split("\n");
@@ -20196,7 +20359,7 @@ function preReadHandlerInner(event) {
20196
20359
  } catch {
20197
20360
  }
20198
20361
  }
20199
- const basename12 = path21.basename(normalized);
20362
+ const basename12 = displaySafePath(path21.basename(normalized));
20200
20363
  if (isLockFile(basename12)) {
20201
20364
  return denyOutput(
20202
20365
  'Lock files are rarely useful to read in full. Use `token-goat section "' + shown + '::<section>"` to extract a specific dependency, or read the relevant manifest instead.'
@@ -20248,7 +20411,7 @@ function preReadHandlerInner(event) {
20248
20411
  } catch {
20249
20412
  }
20250
20413
  }
20251
- const compactUnrangedRead = readIntToolInput(event, "offset") === void 0 && readIntToolInput(event, "limit") === void 0;
20414
+ const compactUnrangedRead = !readRequestedSliceWindow(event).isExplicitSlice;
20252
20415
  if (event.toolName !== "Grep" && compactUnrangedRead && loadConfig().hints.stable_doc_compacts && _isDocFile(normalized)) {
20253
20416
  const compactPath = compactPathFor(normalized);
20254
20417
  if (isCompactFresh(compactPath, normalized)) {
@@ -20257,7 +20420,7 @@ function preReadHandlerInner(event) {
20257
20420
  recordActualRead(event, normalized);
20258
20421
  const fullSize = statSize(normalized) ?? 0;
20259
20422
  const savedBytes = counterfactualCredit(fullSize, compactBody.length);
20260
- recordStat("session_hint", savedBytes, savedTokensFromBytes(savedBytes));
20423
+ recordStat("session_hint", savedBytes, savedTokensFromBytes(savedBytes), void 0, "stable-doc-compact");
20261
20424
  return denyOutput(
20262
20425
  "Serving the extractive compact sidecar in place of the full file (source unchanged since the last `compact-doc` build):\n\n" + fenceUntrustedFileContent(compactBody) + '\n\nThis is a lossy extract, not the whole file: front matter and any prose before the first heading are dropped entirely, each section is cut to its opening sentences, and long code fences are truncated. If you need content it left out, re-read with offset/limit for a line window, or `token-goat section "' + shown + '::Heading"` for one section in full. Use `token-goat compact-doc "' + shown + '" --force` to rebuild it, or `token-goat compact-doc "' + shown + '" --show` to view it directly. ' + editAnywayHint(normalized)
20263
20426
  );
@@ -20274,7 +20437,7 @@ function preReadHandlerInner(event) {
20274
20437
  if (savedBytes >= NB_STRIP_MIN_SAVINGS) {
20275
20438
  recordActualRead(event, normalized);
20276
20439
  const nbCredit = counterfactualCredit(rawBytes.length, sidecarContent.length);
20277
- recordStat("session_hint", nbCredit, savedTokensFromBytes(nbCredit));
20440
+ recordStat("session_hint", nbCredit, savedTokensFromBytes(nbCredit), void 0, "notebook-strip");
20278
20441
  return denyOutput(
20279
20442
  "Serving the output-stripped notebook in place of the full file (code-cell outputs and execution counts removed; source and metadata preserved):\n\n" + fenceUntrustedFileContent(sidecarContent) + "\n\n" + editAnywayHint(normalized)
20280
20443
  );
@@ -20331,7 +20494,7 @@ function preReadHandlerInner(event) {
20331
20494
  }
20332
20495
  return denyOutput(message);
20333
20496
  }
20334
- const unrangedRead = readIntToolInput(event, "offset") === void 0 && readIntToolInput(event, "limit") === void 0;
20497
+ const unrangedRead = !readRequestedSliceWindow(event).isExplicitSlice;
20335
20498
  if (loadConfig().hints.subagent_markdown_first_read_deny && event.agentId !== void 0 && unrangedRead && SUBAGENT_MD_FIRST_READ_DENY_EXT_RE.test(basename12) && markdownSize !== null && markdownSize >= SUBAGENT_MD_FIRST_READ_DENY_BYTES) {
20336
20499
  recordStat("subagent_markdown_first_read_deny", 0, 0);
20337
20500
  return denyOutput(
@@ -20402,7 +20565,7 @@ function preReadHandlerInner(event) {
20402
20565
  if (snapDiff.kind === "diff") {
20403
20566
  recordActualRead(event, normalized);
20404
20567
  const artifactDiffCredit = counterfactualCredit(snapDiff.currentContent.length, snapDiff.diff.length);
20405
- recordStat("session_hint", artifactDiffCredit, savedTokensFromBytes(artifactDiffCredit));
20568
+ recordStat("session_hint", artifactDiffCredit, savedTokensFromBytes(artifactDiffCredit), void 0, "artifact-snapshot-diff");
20406
20569
  return denyOutput(
20407
20570
  "Content changed since last read of " + basename12 + ". Here is what changed:\n\n" + fenceUntrustedFileContent("```diff\n" + snapDiff.diff + "\n```") + "\n\n" + sessionArtifactRecall(normalized)
20408
20571
  );
@@ -20420,7 +20583,7 @@ function preReadHandlerInner(event) {
20420
20583
  recordActualRead(event, normalized);
20421
20584
  if (outputSize !== null && outputSize >= TASK_OUTPUT_DENY_BYTES) {
20422
20585
  const artifactDenyCredit = counterfactualCredit(outputSize);
20423
- recordStat("session_hint", artifactDenyCredit, savedTokensFromBytes(artifactDenyCredit));
20586
+ recordStat("session_hint", artifactDenyCredit, savedTokensFromBytes(artifactDenyCredit), void 0, "artifact-large-deny");
20424
20587
  return denyOutput(
20425
20588
  label + " is large (" + toKB(outputSize) + "KB). " + sessionArtifactRecall(normalized)
20426
20589
  );
@@ -20509,12 +20672,12 @@ function preReadHandlerInner(event) {
20509
20672
  if (config2.hints.reread_deny && !protectedRead) {
20510
20673
  if (wasFileTruncatedThisSession(normalized)) {
20511
20674
  if (estimateTruncatedLineCount(normalized) >= config2.hints.truncated_read_min_lines) {
20512
- recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit));
20675
+ recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit), void 0, "reread-truncated-deny");
20513
20676
  return denyOutput(truncatedReadDenyMessage(normalized));
20514
20677
  }
20515
20678
  }
20516
20679
  if (/\.(md|mdx|markdown|rst)$/i.test(basename12)) {
20517
- recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit));
20680
+ recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit), void 0, "reread-doc-deny");
20518
20681
  return denyOutput(
20519
20682
  'Markdown file already read this session. Use `token-goat section "' + shown + '::HeadingName"` to read one section.'
20520
20683
  );
@@ -20530,7 +20693,7 @@ function preReadHandlerInner(event) {
20530
20693
  }
20531
20694
  const hint = _isDocFile(normalized) ? 'Use `token-goat section "' + shown + '::SectionName"` to read one section.' : "Use token-goat read/section/symbol to re-read surgically.";
20532
20695
  if (config2.hints.reread_deny && !protectedRead && (rereadBytes >= config2.hints.reread_deny_min_bytes || reads >= 2)) {
20533
- recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit));
20696
+ recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit), void 0, "reread-count-deny");
20534
20697
  return denyOutput(
20535
20698
  shown + " was already read this session (" + reads + " " + plural + "). " + hint
20536
20699
  );
@@ -20555,7 +20718,7 @@ function preReadHandlerInner(event) {
20555
20718
  const hint = _isDocFile(normalized) ? 'Use `token-goat section "' + shown + '::SectionName"` to read one section.' : "Consider token-goat skeleton or token-goat section.";
20556
20719
  if (gateSize >= largeFileDenyBytes()) {
20557
20720
  const denyCredit = counterfactualCredit(size);
20558
- recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit));
20721
+ recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit), void 0, "large-file-deny");
20559
20722
  return denyOutput(
20560
20723
  shown + " is very large (" + kb + "KB). " + hint + " " + describeSliceAdvice(slice, normalized) + " " + editAnywayHint(normalized)
20561
20724
  );
@@ -20701,9 +20864,9 @@ function readWindowFromDisk(event, normalized) {
20701
20864
  const size = statSize(normalized);
20702
20865
  if (size === null || size > SLICE_ESTIMATE_SCAN_CAP_BYTES) return null;
20703
20866
  const text = decodeSource(fs21.readFileSync(normalized));
20704
- const limit = readIntToolInput(event, "limit");
20705
- const offset = readIntToolInput(event, "offset");
20706
- const start = offset !== void 0 && offset >= 1 ? offset : 1;
20867
+ const window2 = readRequestedSliceWindow(event);
20868
+ const start = window2.offset !== void 0 && window2.offset >= 1 ? window2.offset : 1;
20869
+ const limit = window2.limit;
20707
20870
  if (start === 1 && (limit === void 0 || limit <= 0)) return text;
20708
20871
  const lines2 = text.split("\n");
20709
20872
  const from = start - 1;
@@ -20826,7 +20989,7 @@ function elideAlreadyServedLines(event, respText) {
20826
20989
  function structuralFoldInputs(event, respText) {
20827
20990
  const filePath = getFilePath(event);
20828
20991
  if (filePath === void 0) return null;
20829
- if (readIntToolInput(event, "offset") !== void 0 || readIntToolInput(event, "limit") !== void 0) return null;
20992
+ if (readRequestedSliceWindow(event).isExplicitSlice) return null;
20830
20993
  if (isTruncatedReadDelivery(event, respText)) return null;
20831
20994
  if (redactSecrets(respText).count > 0) return null;
20832
20995
  const parsed = parseNumberedReadResult(respText, readStartLine(event));
@@ -20861,14 +21024,15 @@ function foldCodeBodies(event, respText) {
20861
21024
  if (filePath === void 0) return null;
20862
21025
  const normalized = normalizePath(filePath);
20863
21026
  if (isImagePath(normalized)) return null;
20864
- const requestedOffset = readIntToolInput(event, "offset");
21027
+ const sliceWin = readRequestedSliceWindow(event);
21028
+ const requestedOffset = sliceWin.offset;
20865
21029
  if (requestedOffset !== void 0 && readStartLine(event) !== requestedOffset) return null;
20866
21030
  if (isTruncatedReadDelivery(event, respText)) return null;
20867
21031
  if (redactSecrets(respText).count > 0) return null;
20868
21032
  const parsed = parseNumberedReadResult(respText, readStartLine(event));
20869
21033
  if (parsed === null) return null;
20870
21034
  const shown = displaySafePath(toDisplayPath(findProject(getCwd(event) ?? process.cwd())?.root, normalized));
20871
- const folded = foldDelivery(parsed.rows, normalized, shown, requestedOffset !== void 0 || readIntToolInput(event, "limit") !== void 0);
21035
+ const folded = foldDelivery(parsed.rows, normalized, shown, sliceWin.isExplicitSlice);
20872
21036
  if (folded === null) return null;
20873
21037
  const rewritten = [...parsed.header, fenceUntrustedFileContent(folded.numbered.join("\n")), ...parsed.trailer].join("\n");
20874
21038
  const originalBytes = Buffer.byteLength(respText, "utf-8");
@@ -21991,6 +22155,7 @@ export {
21991
22155
  clearCurlDownload,
21992
22156
  recordFileLineRange,
21993
22157
  getFileLineRanges,
22158
+ GENERIC_SERVED_OUTPUT_KEY,
21994
22159
  recordFileServedOutput,
21995
22160
  getFileServedOutputs,
21996
22161
  markFileTruncated,
@@ -22099,6 +22264,9 @@ export {
22099
22264
  PARSER_FINGERPRINT,
22100
22265
  stripLeadingAttributes2 as stripLeadingAttributes,
22101
22266
  IMPORT_RE2 as IMPORT_RE,
22267
+ treeSitterCoreAvailable,
22268
+ treeSitterCoreLoadError,
22269
+ isTreeSitterAvailable,
22102
22270
  yamlOpenQuoteAfter,
22103
22271
  yamlLineClosesQuote,
22104
22272
  lineOpenDelimiterAfter,
@@ -7,11 +7,11 @@ import {
7
7
  expandGlobs,
8
8
  leftoverIntegrations,
9
9
  run
10
- } from "./token-goat-chunk-J7LGMKF3.mjs";
11
- import "./token-goat-chunk-46VKOCUH.mjs";
12
- import "./token-goat-chunk-7GJBID7S.mjs";
13
- import "./token-goat-chunk-3HJQR4OO.mjs";
14
- import "./token-goat-chunk-T2OE7MYM.mjs";
10
+ } from "./token-goat-chunk-PNMGVJ4C.mjs";
11
+ import "./token-goat-chunk-L6Q2XM5X.mjs";
12
+ import "./token-goat-chunk-QCHD2ENV.mjs";
13
+ import "./token-goat-chunk-BYDDWQ2T.mjs";
14
+ import "./token-goat-chunk-KIEOFWLL.mjs";
15
15
  import "./token-goat-chunk-EEIDFMEM.mjs";
16
16
  import "./token-goat-chunk-A37V4PBF.mjs";
17
17
  export {
@@ -4,14 +4,14 @@ import {
4
4
  buildEvent,
5
5
  relay,
6
6
  relayInProcess
7
- } from "./token-goat-chunk-EIZYCVBM.mjs";
7
+ } from "./token-goat-chunk-P5SQX2VK.mjs";
8
8
  import {
9
9
  MAX_STDIN_BYTES,
10
10
  readStdinJson
11
- } from "./token-goat-chunk-7GJBID7S.mjs";
12
- import "./token-goat-chunk-3HJQR4OO.mjs";
13
- import "./token-goat-chunk-LXIC7MTW.mjs";
14
- import "./token-goat-chunk-T2OE7MYM.mjs";
11
+ } from "./token-goat-chunk-QCHD2ENV.mjs";
12
+ import "./token-goat-chunk-BYDDWQ2T.mjs";
13
+ import "./token-goat-chunk-OBDTBOQA.mjs";
14
+ import "./token-goat-chunk-KIEOFWLL.mjs";
15
15
  import "./token-goat-chunk-EEIDFMEM.mjs";
16
16
  import "./token-goat-chunk-A37V4PBF.mjs";
17
17
  export {