token-goat 2.8.0 → 2.8.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/README.md CHANGED
@@ -305,7 +305,7 @@ Installation adds a short routing guide to the agent's instructions. When the ag
305
305
 
306
306
  | Task | Agent flow |
307
307
  | --- | --- |
308
- | Review a PDF | `pdf-meta` and `pdf-outline`, then `pdf-extract --pages` |
308
+ | Review a PDF | `pdf-meta` and `pdf-outline`, then `pdf-locate` to find the pages that mention a term and `pdf-extract --pages` only those |
309
309
  | Review a Word document | `docx-outline`, then `docx-text` |
310
310
  | Review a slide deck | `pptx-outline`, then `pptx-slide` or `pptx-notes` |
311
311
  | Review a workbook | `xlsx-sheets`, then `xlsx-head`, `xlsx-range`, or `xlsx-query` |
@@ -515,6 +515,7 @@ token-goat xlsx-sheets link-map.xlsx
515
515
  token-goat xlsx-query link-map.xlsx --sheet Links --columns publication,source,target --head 50
516
516
  token-goat pdf-meta manual.pdf
517
517
  token-goat pdf-outline manual.pdf
518
+ token-goat pdf-locate manual.pdf "torque spec" --ignore-case
518
519
  token-goat pdf-extract manual.pdf --pages 12-15 --layout --head 120
519
520
  ```
520
521
 
@@ -606,6 +607,7 @@ token-goat pdf-extract manual.pdf --pages 12-15 --layout --head 120
606
607
  | `token-goat config list / get / set / validate` | Inspect or edit `config.toml` from the CLI. `validate` reports unknown keys with did-you-mean suggestions, plus any project-file or environment value that validation rejected or clamped. A project-root `.token-goat.toml` layers on top of the global config, overriding hint thresholds, indexing settings, etc. for that project only. It may not set the security sections `injection`, `webfetch`, `gdrive`, or `mcp`, nor `indexing.cross_project_symbols`: that file arrives with the repository, so a cloned project could otherwise switch off prompt-injection fencing or empty the fetch allow list for anyone who opened it. Those settings come from the global config or the environment only, and a project file that tries to set one is ignored with a message naming what was dropped. `config get`/`list`/`set` report which layer a value actually resolved from — the project file, an environment variable, or the global config — and where that layer's value was clamped or rejected they say so, naming what was asked for and what is in effect instead. |
607
608
  | `token-goat config-get <file> <key>` | Look up one key from a config-shaped file (TOML/INI `key = value`, or YAML) without reading the whole thing. On a `.md` file, a leading `---`-fenced YAML frontmatter block (Jekyll/Hugo/SKILL.md style) is checked first and takes precedence over the TOML/INI fallback; a `.md` file with no frontmatter, or an unclosed fence, falls through to the normal lookup unchanged. |
608
609
  | `token-goat pdf-extract <file>` | Extract plain text from a PDF instead of a raw Read. `--pages <spec>` narrows to a page range (e.g. `1-5` or `3`); `--head`/`--tail`/`--grep`/`--max-matches`/`--section` slice the extracted text the same way `bash-output`/`web-output` do. `--layout` heuristically reconstructs column-aware reading order from text-item coordinates instead of raw content-stream order (imperfect on rotated/overlapping text). |
610
+ | `token-goat pdf-locate <file> <pattern>` | Find which pages of a PDF match a regex, with a snippet per match, so you can `pdf-extract --pages` only those pages instead of pulling the whole document. `-i`/`--ignore-case` for case-insensitive matching; `--max-matches <n>` caps how many page matches to collect (default 50); `--context <n>` sets the snippet length around each match (default 80); `--pages <spec>` narrows the scan to a page range; `-j`/`--json` emits `{ file, pattern, matchCount, pages, matches }`. |
609
611
  | `token-goat pdf-outline <file>` | List a PDF's bookmark/outline tree with page numbers instead of a raw Read. |
610
612
  | `token-goat pdf-meta <file> [--json]` | Page count, title/author, and whether a PDF has an extractable text layer (so you know before extracting whether it's scanned/image-only). `--json` emits `{ pageCount, title, author, hasTextLayer }` — `hasTextLayer` as a real boolean rather than a prose sentence, and an absent title/author as `null` rather than the literal `(none)`. |
611
613
  | `token-goat image-meta <file> [--json]` | Dimensions, format, byte size, and what a `shrinkImage` pass would cost — a cheap "should I even look at this" probe that reads `sharp` metadata only and never runs OCR. Requires `sharp`; degrades with a clear message when it's missing. |
@@ -643,7 +645,7 @@ token-goat pdf-extract manual.pdf --pages 12-15 --layout --head 120
643
645
  | `token-goat zip-list <archive>` | Entry paths and sizes inside a zip-format archive (`.zip`/`.jar`/`.whl`/`.vsix`/`.nupkg` are all zip containers under the hood) instead of a raw Read or an `unzip -l` shell-out. Reads the central directory only — no member is decompressed just to list it. `--json` for structured output. |
644
646
  | `token-goat zip-read <archive> <entry>` | Extract and print exactly one entry's text content from a zip-format archive by its in-archive path, instead of extracting the whole archive to disk. A binary member prints a `[binary content elided by token-goat]` marker instead of raw bytes. |
645
647
  | `token-goat pr-slice <pr>` | Surgical GitHub PR reads via `gh` — one file's diff, a single review-comment thread, the description, or CI check statuses, instead of pulling the whole PR payload into context. |
646
- | `token-goat bridges-status` | Parity matrix of which hooks/commands are wired for each supported harness (Claude Code, Codex, opencode, openclaw, Grok, etc.), side by side. |
648
+ | `token-goat bridges-status` | Parity matrix of which hooks/commands are wired for each supported harness (Claude Code, Codex, opencode, openclaw, Grok, etc.), side by side. A `verified` column says how each row was established — `dogfooded` (driven against the real harness binary), `sourced` (read out of the harness's own source or declarations), or `documented` (from its docs only) — so a claim built from reading alone is never presented as one that was tested. |
647
649
  | `token-goat commands` | Machine-readable manifest of every registered command, its description, options, and arguments (including subcommands like `worker start`). `--json` emits it as structured JSON for external tooling (shell completion, doc generators, scripts) instead of the default text listing. `--grep PATTERN` narrows the manifest to commands whose name, description, or aliases match; a parent command that matches keeps all its subcommands, a parent that only has a matching child keeps just that child; no matches prints `no matches` and exits 0. |
648
650
  | `token-goat mcp-serve` | Run token-goat as an MCP stdio server exposing all 18 tools: read/symbol/section/outline/skeleton/semantic/index_status/refs/brief/map/changed/grep/imports/exports/compress_text/retrieve_text/handoff_create/handoff_resolve. |
649
651
  | `token-goat version` | Print the token-goat version. |
@@ -656,7 +658,7 @@ token-goat pdf-extract manual.pdf --pages 12-15 --layout --head 120
656
658
  | `token-goat project exclude <path>` | Add a project root to the blocklist so the worker never indexes it. Writes the resolved absolute path to `[worker] blocked_roots` in `config.toml`; idempotent. It also removes anything already indexed under that path and says how many files went, so excluding a directory means its contents stop being readable through `symbol` rather than merely stopping future indexing. Remove the entry from the config to re-enable indexing, then run `token-goat index` to bring the contents back. |
657
659
  | `token-goat project prune [--dry-run]` | Remove blocked/excluded roots that no longer exist on disk. `--dry-run` previews removals without touching the config file. Useful after deleting or moving projects. |
658
660
  | `token-goat install` | Wire up hooks (and, with the harness flags below, other AI tool integrations). No `--dry-run` or `--verify` flag — run `token-goat doctor` after install to audit the result. |
659
- | `token-goat doctor` | Confirm everything is wired correctly. Surfaces install state, cold-import timing, cache hit rates, compaction-budget telemetry, opt-in flag status, and canonical-root sanity. A **Security** section reports the posture in one place: whether offline mode is on, whether injection scanning is on, whether the Google Drive integration is enabled, whether fetching runs against an allow list or a deny list, whether MCP reads are confined to the project root, and whether the data directory is readable by other local users. It only warns when a protection that ships on has been switched off, so a default install stays quiet. It read-only audits `~/.copilot/mcp-config.json` for globally configured Chrome DevTools or Playwright `npx` launchers, recommending project scope or removal when inactive; it never prints server configuration or secrets. On Windows, it also reports duplicate Chrome DevTools/Playwright MCP launchers and orphaned Node processes without terminating anything. Pass `--context` to show the **Context footprint** section: a fill bar with severity (ok / warn / high / URGENT), per-component breakdown (skills catalog, loaded skill bodies, CLAUDE.md+MEMORY.md, conversation estimate), session-to-session growth trend with sessions-to-URGENT projection, and tiered compaction recommendations (Tier 0–4) naming the exact commands to run. Auto-shown when fill > 40 % or any loaded skill > 2 K tokens lacks a compact. `--json` emits the check results (one entry per check, with `ok`/`warn`/`fail` status) as JSON instead of text. |
661
+ | `token-goat doctor` | Confirm everything is wired correctly. Surfaces install state, cold-import timing, cache hit rates, compaction-budget telemetry, opt-in flag status, and canonical-root sanity. A **Tool names** check reports any tool name a harness sent that reached no handler wanting it, and calls out the ones that differ from a handled name only by capitalisation or punctuation — the signature of a bridge that forgot to rename something, which is otherwise invisible. A **Security** section reports the posture in one place: whether offline mode is on, whether injection scanning is on, whether the Google Drive integration is enabled, whether fetching runs against an allow list or a deny list, whether MCP reads are confined to the project root, and whether the data directory is readable by other local users. It only warns when a protection that ships on has been switched off, so a default install stays quiet. It read-only audits `~/.copilot/mcp-config.json` for globally configured Chrome DevTools or Playwright `npx` launchers, recommending project scope or removal when inactive; it never prints server configuration or secrets. On Windows, it also reports duplicate Chrome DevTools/Playwright MCP launchers and orphaned Node processes without terminating anything. Pass `--context` to show the **Context footprint** section: a fill bar with severity (ok / warn / high / URGENT), per-component breakdown (skills catalog, loaded skill bodies, CLAUDE.md+MEMORY.md, conversation estimate), session-to-session growth trend with sessions-to-URGENT projection, and tiered compaction recommendations (Tier 0–4) naming the exact commands to run. Auto-shown when fill > 40 % or any loaded skill > 2 K tokens lacks a compact. `--json` emits the check results (one entry per check, with `ok`/`warn`/`fail` status) as JSON instead of text. |
660
662
  | `token-goat baseline` | Emit a project map: file count, per-language file counts, the top indexed symbols (by name/kind/location), and the most recently modified files. `--subagent` emits a terser variant (fewer symbols, fewer recent files) for context handed to a freshly spawned subagent; `--json` for the machine-readable form. |
661
663
  | `token-goat compact-doc <path>` | Build an extractive compact sidecar for a large reference doc (`.md`/`.markdown`). The compact is stored in the token-goat data dir as a SHA-keyed sidecar; `pre_read` serves it in place of the full file when it exists and is fresh, saving 80–95% of context tokens. Use `--force` to rebuild, `--sentences N` to control lines per section (default 2), `--show` to print the result. The sidecar is automatically marked stale when you edit the source file. Config: `[hints] stable_doc_compacts = true` (default on). |
662
664
 
@@ -59,6 +59,7 @@ import {
59
59
  propagateEndLinesToSymbols,
60
60
  pushAll,
61
61
  recordStat,
62
+ recordUnmappedTool,
62
63
  redactIfDotenv,
63
64
  redactSecrets,
64
65
  resolveIndexPath,
@@ -90,7 +91,7 @@ import {
90
91
  withFileLock,
91
92
  writeIfDifferent,
92
93
  writeJsonSettings
93
- } from "./token-goat-chunk-44Y77VHR.mjs";
94
+ } from "./token-goat-chunk-ELDJRLHZ.mjs";
94
95
  import {
95
96
  registerReset
96
97
  } from "./token-goat-chunk-AO2QD2AG.mjs";
@@ -2331,10 +2332,18 @@ var parse2 = function(data, opts = {}) {
2331
2332
  function parseRecords(content, opts) {
2332
2333
  const delimiter = opts.delimiter ?? ",";
2333
2334
  if (opts.noHeader === true) {
2334
- const rows = parse2(content, { columns: false, skip_empty_lines: true, trim: true, delimiter, bom: true });
2335
+ const rows = parse2(content, { columns: false, skip_empty_lines: true, trim: true, delimiter, bom: true, relax_column_count: true });
2335
2336
  return rows.map((row) => Object.fromEntries(row.map((cell, i) => [`col${i + 1}`, cell])));
2336
2337
  }
2337
- return parse2(content, { columns: true, skip_empty_lines: true, trim: true, delimiter, bom: true });
2338
+ const header = csvHeader(content, opts);
2339
+ const dupes = header.filter((name, i) => name !== "" && header.indexOf(name) !== i);
2340
+ if (dupes.length > 0) {
2341
+ const unique = [...new Set(dupes)];
2342
+ throw new Error(
2343
+ `duplicate column ${unique.length === 1 ? "name" : "names"} in header: ${unique.join(", ")} \u2014 rename the duplicates or pass --no-header to address columns positionally as col1, col2, \u2026`
2344
+ );
2345
+ }
2346
+ return parse2(content, { columns: true, skip_empty_lines: true, trim: true, delimiter, bom: true, relax_column_count: true });
2338
2347
  }
2339
2348
  function csvHeader(content, opts) {
2340
2349
  if (opts.noHeader === true) return [];
@@ -2828,8 +2837,10 @@ function usedRange(ws) {
2828
2837
  }
2829
2838
  const rows = rowCount;
2830
2839
  const cols = maxCol;
2831
- const ref2 = rows > 0 && cols > 0 ? `A1:${indexToColLetters(cols)}${rows}` : "A1:A1";
2832
- return { ref: ref2, rows: Math.max(rows, 1), cols: Math.max(cols, 1) };
2840
+ if (rows === 0 || cols === 0) {
2841
+ return { ref: "(empty)", rows: 0, cols: 0 };
2842
+ }
2843
+ return { ref: `A1:${indexToColLetters(cols)}${rows}`, rows, cols };
2833
2844
  }
2834
2845
  async function listSheets(filePath) {
2835
2846
  const wb = await loadWorkbook(filePath);
@@ -2993,6 +3004,40 @@ async function extractPdfText(data, pagesSpec, layout = false) {
2993
3004
  return { text: pages.join("\n\n"), pageCount: doc.numPages, pagesExtracted: end - start + 1 };
2994
3005
  });
2995
3006
  }
3007
+ async function locatePdfPages(data, pattern, opts) {
3008
+ let re;
3009
+ try {
3010
+ re = new RegExp(pattern, opts.ignoreCase === true ? "i" : "");
3011
+ } catch (e) {
3012
+ throw new Error(`invalid regex pattern: ${pattern} (${e instanceof Error ? e.message : String(e)})`, { cause: e });
3013
+ }
3014
+ const pdfjs = await loadPdfjs();
3015
+ if (!pdfjs) throw new Error("pdfjs-dist is not installed; run `npm install pdfjs-dist` to enable pdf-extract");
3016
+ const maxMatches = opts.maxMatches ?? 50;
3017
+ const context = opts.context ?? 80;
3018
+ return withPdfDocument(pdfjs, data, async (doc) => {
3019
+ const range = parsePageRange(opts.pages, doc.numPages);
3020
+ const start = range ? range.start : 1;
3021
+ const end = range ? range.end : doc.numPages;
3022
+ const matches = [];
3023
+ for (let i = start; i <= end && matches.length < maxMatches; i++) {
3024
+ const page = await doc.getPage(i);
3025
+ const content = await page.getTextContent();
3026
+ const textItems = content.items.filter((item) => "str" in item);
3027
+ const pageText = textItems.map((item) => item.str).join(" ");
3028
+ const m = re.exec(pageText);
3029
+ if (m === null) continue;
3030
+ matches.push({ page: i, snippet: locateSnippet(pageText, m.index, m[0].length, context) });
3031
+ }
3032
+ return matches;
3033
+ });
3034
+ }
3035
+ function locateSnippet(text, index, matchLen, context) {
3036
+ const pad = Math.max(0, context - matchLen);
3037
+ const from = Math.max(0, index - Math.floor(pad / 2));
3038
+ const to = Math.min(text.length, index + matchLen + Math.ceil(pad / 2));
3039
+ return text.slice(from, to).replace(/\s+/g, " ").trim();
3040
+ }
2996
3041
  async function resolveDestPage(doc, dest) {
2997
3042
  let explicitDest = dest;
2998
3043
  if (typeof explicitDest === "string") {
@@ -3190,9 +3235,27 @@ function toolMatcherFor(eventName) {
3190
3235
  if (parts.length === 0) return null;
3191
3236
  return parts.join("|");
3192
3237
  }
3238
+ function foldToolName(name) {
3239
+ return name.toLowerCase().replace(/[_-]/g, "");
3240
+ }
3241
+ function noteUnrecognizedTool(event, list) {
3242
+ const toolName = event.toolName;
3243
+ if (typeof toolName !== "string" || toolName === "") return;
3244
+ const named = [];
3245
+ for (const { toolName: want } of list) {
3246
+ if (want === void 0) continue;
3247
+ if (want === toolName) return;
3248
+ named.push(want);
3249
+ }
3250
+ if (named.length === 0) return;
3251
+ const folded = foldToolName(toolName);
3252
+ const nearMiss = named.find((n) => foldToolName(n) === folded) ?? null;
3253
+ recordUnmappedTool(toolName, event.eventName, nearMiss);
3254
+ }
3193
3255
  async function runHook(event) {
3194
3256
  const list = _handlers.get(event.eventName);
3195
3257
  if (list === void 0) return { hookType: "pass" };
3258
+ noteUnrecognizedTool(event, list);
3196
3259
  let advisoryResult;
3197
3260
  for (const { handler, toolName, advisory } of list) {
3198
3261
  if (toolName !== void 0 && toolName !== event.toolName) continue;
@@ -4237,9 +4300,9 @@ function buildGuidanceBody(fallbackToolClause, opts = {}) {
4237
4300
  "- pulling one value or subtree out of a JSON/YAML/XML file (manifest, lockfile, spec, config) \u2192 `json-query file 'a.b.c'` / `yaml-query file 'a.b.c'` / `xml-query file 'a.b.c'`",
4238
4301
  "- opening an image to check its dimensions, format, or size \u2192 `image-meta file`",
4239
4302
  "- opening a screenshot, diagram, or scan to read the text in it \u2192 `image-text file`",
4240
- "- opening a PDF or Office document \u2192 inspect its format first, then read a narrow slice: PDF `pdf-meta`/`pdf-outline` then `pdf-extract`; Word `docx-outline` then `docx-text`; PowerPoint `pptx-outline` then `pptx-slide`/`pptx-notes`; Excel `xlsx-sheets` then `xlsx-head`/`xlsx-range`/`xlsx-query`",
4303
+ "- opening a PDF or Office document \u2192 inspect its format first, then read a narrow slice: PDF `pdf-meta`/`pdf-outline` then `pdf-locate` to find the pages and `pdf-extract` only those; Word `docx-outline` then `docx-text`; PowerPoint `pptx-outline` then `pptx-slide`/`pptx-notes`; Excel `xlsx-sheets` then `xlsx-head`/`xlsx-range`/`xlsx-query`",
4241
4304
  "",
4242
- 'Commands: `symbol NAME`, `read "file::symbol"`, `brief "file::symbol"`, `section "file::Heading"`, `semantic "description"`, `outline file`/`skeleton file`, `map --compact`, `refs file::symbol --callers`, `changed --symbol`, `config-get file KEY`, `json-query file \'a.b.c\'`/`yaml-query`/`xml-query`, `json-outline file`/`yaml-outline`/`xml-outline`, `bash-output`/`web-output`/`mcp-output`, ' + (gdrive ? "`gdrive-sections <file-id>`, " : "") + "`image-meta file`/`image-text file`, `pdf-meta`/`pdf-outline`/`pdf-extract`, `docx-outline`/`docx-text`, `pptx-outline`/`pptx-slide`/`pptx-notes`/`pptx-text`, `xlsx-sheets`/`xlsx-head`/`xlsx-range`/`xlsx-query`.",
4305
+ 'Commands: `symbol NAME`, `read "file::symbol"`, `brief "file::symbol"`, `section "file::Heading"`, `semantic "description"`, `outline file`/`skeleton file`, `map --compact`, `refs file::symbol --callers`, `changed --symbol`, `config-get file KEY`, `json-query file \'a.b.c\'`/`yaml-query`/`xml-query`, `json-outline file`/`yaml-outline`/`xml-outline`, `bash-output`/`web-output`/`mcp-output`, ' + (gdrive ? "`gdrive-sections <file-id>`, " : "") + "`image-meta file`/`image-text file`, `pdf-meta`/`pdf-outline`/`pdf-locate`/`pdf-extract`, `docx-outline`/`docx-text`, `pptx-outline`/`pptx-slide`/`pptx-notes`/`pptx-text`, `xlsx-sheets`/`xlsx-head`/`xlsx-range`/`xlsx-query`.",
4243
4306
  "",
4244
4307
  "Sub-agent briefs must carry this gate verbatim: a sub-agent inherits none of this context and its reads spend the same token budget.",
4245
4308
  "",
@@ -5107,7 +5170,14 @@ const POLL_ID_ARG_KEY = {
5107
5170
  // the same session, since process.pid varies per invocation -- breaking token-goat's
5108
5171
  // session-based dedup/state ledger, which never accumulates across calls as a result. Derive a
5109
5172
  // stable id instead from the one thing that's actually constant across calls for the same
5110
- // session: the working directory Copilot reports in \`payload.cwd\`.
5173
+ // session: the working directory Copilot reports. That field is \`workingDirectory\`, declared
5174
+ // required on BaseHookInput in copilot-sdk/types.d.ts since 1.0.76, so it is present on EVERY
5175
+ // hook event. This previously read \`payload.cwd\`, which Copilot has never sent under any name in
5176
+ // any version -- the key simply did not exist, so this derived every fallback id from
5177
+ // process.cwd() instead and \`canonical.cwd\` below was undefined on every single call. It went
5178
+ // unnoticed because process.cwd() happens to be the project directory Copilot spawns the hook in,
5179
+ // so the fallback was accidentally right; nothing about that was by design. \`cwd\` is still read
5180
+ // as a secondary in case a future version adds it under the shorter name.
5111
5181
  function stableFallbackSessionId(cwd) {
5112
5182
  const key = typeof cwd === 'string' && cwd ? cwd : process.cwd()
5113
5183
  const hash = require('node:crypto').createHash('sha256').update(key).digest('hex').slice(0, 16)
@@ -5177,8 +5247,19 @@ async function main() {
5177
5247
 
5178
5248
  const toolName = payload && payload.toolName
5179
5249
  const canonical = {
5180
- session_id: (payload && payload.sessionId) || stableFallbackSessionId(payload && payload.cwd),
5181
- cwd: payload && payload.cwd,
5250
+ session_id:
5251
+ (payload && payload.sessionId) ||
5252
+ stableFallbackSessionId(payload && (payload.workingDirectory || payload.cwd)),
5253
+ cwd: payload && (payload.workingDirectory || payload.cwd),
5254
+ }
5255
+
5256
+ // userPromptSubmitted only: Copilot declares \`prompt\` required on UserPromptSubmittedHookInput.
5257
+ // hooks_session.ts's userPromptSubmitHandler reads it as \`event.raw['prompt']\` and gates every
5258
+ // branch it has on the text, so without this it saw '' on every Copilot prompt and the
5259
+ // embedded-skill dedup hint could never fire. Same shape as the postToolUseFailure \`error\`
5260
+ // drop: a required field the canonical builder simply did not list.
5261
+ if (typeof (payload && payload.prompt) === 'string' && payload.prompt !== '') {
5262
+ canonical.prompt = payload.prompt
5182
5263
  }
5183
5264
  if (toolName) {
5184
5265
  canonical.tool_name = TOOL_TO_TG[toolName] || toolName
@@ -5193,6 +5274,16 @@ async function main() {
5193
5274
  // post-read/post-bash stats stay empty no matter how many tool calls happen. Extract the
5194
5275
  // LLM-facing text directly rather than forwarding the raw object, since textResultForLlm
5195
5276
  // isn't one of those recognized object keys.
5277
+ // postToolUseFailure only: Copilot's PostToolUseFailureHookInput (copilot-sdk/types.d.ts:1042)
5278
+ // carries {toolName, toolArgs, error} and no toolResult at all -- the failure text lives in
5279
+ // \`error\`, a plain string. Without forwarding it, hooks_tool_failure.ts's extractFailureText
5280
+ // finds nothing to key on and the repeat-failure brake returns pass on every single call: wired,
5281
+ // green, and doing nothing. Found by driving the installed shim rather than by a test, because
5282
+ // the handler's own tests hand it a raw payload that already has the field.
5283
+ if (typeof (payload && payload.error) === 'string' && payload.error !== '') {
5284
+ canonical.error = payload.error
5285
+ }
5286
+
5196
5287
  const rawResult = payload && payload.toolResult
5197
5288
  if (rawResult && typeof rawResult === 'object') {
5198
5289
  const tr = rawResult
@@ -7373,6 +7464,9 @@ function resolveTesseractEntry() {
7373
7464
  }
7374
7465
  return _tesseractEntryPath;
7375
7466
  }
7467
+ function isOcrEngineAvailable() {
7468
+ return !_ocrUnavailableThisProcess && resolveTesseractEntry() !== null;
7469
+ }
7376
7470
  var _ocrUnavailableThisProcess = false;
7377
7471
  function buildChildScript(entryPath, cacheDir) {
7378
7472
  return [
@@ -7487,14 +7581,16 @@ var loadSharp = createLazyModuleLoader(async () => {
7487
7581
  function isImagePath(p) {
7488
7582
  return IMAGE_EXTENSIONS.has(path12.extname(p).toLowerCase());
7489
7583
  }
7584
+ var ImageDecodeError = class extends Error {
7585
+ };
7490
7586
  async function probeImageMeta(input) {
7491
7587
  const sharp = await loadSharp();
7492
7588
  if (sharp === null) return null;
7493
7589
  try {
7494
7590
  const meta = await sharp(input, { limitInputPixels: false }).metadata();
7495
7591
  return { width: meta.width ?? 0, height: meta.height ?? 0, format: meta.format ?? null, pages: meta.pages ?? 1 };
7496
- } catch {
7497
- return null;
7592
+ } catch (e) {
7593
+ throw new ImageDecodeError(e?.message ?? "image could not be decoded");
7498
7594
  }
7499
7595
  }
7500
7596
  async function imageQualifiesForShrink(input) {
@@ -7640,7 +7736,14 @@ async function preReadImageHandler(event) {
7640
7736
  } catch {
7641
7737
  cachedData = null;
7642
7738
  }
7643
- const meta = cachedData !== null ? await probeImageMeta(cachedData) : null;
7739
+ let meta = null;
7740
+ if (cachedData !== null) {
7741
+ try {
7742
+ meta = await probeImageMeta(cachedData);
7743
+ } catch {
7744
+ meta = null;
7745
+ }
7746
+ }
7644
7747
  if (cachedData !== null && meta !== null) {
7645
7748
  const result2 = {
7646
7749
  data: cachedData,
@@ -18839,6 +18942,7 @@ function urlPolicyDenialReason(url, policy) {
18839
18942
  export {
18840
18943
  createLazyModuleLoader,
18841
18944
  extractPdfText,
18945
+ locatePdfPages,
18842
18946
  extractPdfOutline,
18843
18947
  extractPdfMeta,
18844
18948
  docxOutline,
@@ -19005,10 +19109,12 @@ export {
19005
19109
  getSkillFilePath,
19006
19110
  installedSkillPath,
19007
19111
  pruneSkillOutputs,
19112
+ isOcrEngineAvailable,
19008
19113
  ocrImage,
19009
19114
  isTextHeavy,
19010
19115
  formatShrinkSummary,
19011
19116
  isImagePath,
19117
+ ImageDecodeError,
19012
19118
  probeImageMeta,
19013
19119
  imageQualifiesForShrink,
19014
19120
  shrinkImage,
@@ -25,7 +25,7 @@ import {
25
25
  runSkeleton,
26
26
  runSymbol,
27
27
  withPinnedReads
28
- } from "./token-goat-chunk-SRAR6DOK.mjs";
28
+ } from "./token-goat-chunk-RMDQFTQD.mjs";
29
29
  import {
30
30
  buildProjectMap,
31
31
  embeddingsDepsAvailable,
@@ -34,7 +34,7 @@ import {
34
34
  getProjectIndexCounts,
35
35
  isWorkerRunning,
36
36
  mapLookupBytesSaved
37
- } from "./token-goat-chunk-2F6TFBZE.mjs";
37
+ } from "./token-goat-chunk-2G6RAB4G.mjs";
38
38
  import {
39
39
  VERSION,
40
40
  dataDir,
@@ -45,7 +45,7 @@ import {
45
45
  normalizePath,
46
46
  recordStat,
47
47
  resolveProjectRoot
48
- } from "./token-goat-chunk-44Y77VHR.mjs";
48
+ } from "./token-goat-chunk-ELDJRLHZ.mjs";
49
49
  import "./token-goat-chunk-AO2QD2AG.mjs";
50
50
  import "./token-goat-chunk-AEX54RUZ.mjs";
51
51
 
@@ -4,7 +4,7 @@ import {
4
4
  loadConfig,
5
5
  redactSecrets,
6
6
  stripAnsiCodes
7
- } from "./token-goat-chunk-44Y77VHR.mjs";
7
+ } from "./token-goat-chunk-ELDJRLHZ.mjs";
8
8
 
9
9
  // src/tool_filters/helpers.ts
10
10
  import * as fs from "node:fs";
@@ -23,7 +23,7 @@ import {
23
23
  summarizeOutputDelta,
24
24
  summarizeResidentContext,
25
25
  taskListPruneHint
26
- } from "./token-goat-chunk-AO6MFFTW.mjs";
26
+ } from "./token-goat-chunk-XEDQH5DA.mjs";
27
27
  import {
28
28
  BODY_FIRST_TOOL_RESPONSE_KEYS,
29
29
  OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
@@ -118,7 +118,7 @@ import {
118
118
  wasCliReadThisSession,
119
119
  wasFileReadThisSession,
120
120
  wasHintShown
121
- } from "./token-goat-chunk-2F6TFBZE.mjs";
121
+ } from "./token-goat-chunk-2G6RAB4G.mjs";
122
122
  import {
123
123
  canRunWrappedShell,
124
124
  compressOutput,
@@ -129,7 +129,7 @@ import {
129
129
  isRewriteWorthwhile,
130
130
  resolveMinNetSavingsBytes,
131
131
  shlexSplit
132
- } from "./token-goat-chunk-TUPJRK7R.mjs";
132
+ } from "./token-goat-chunk-7JDXDERZ.mjs";
133
133
  import {
134
134
  VERSION,
135
135
  detectHarness,
@@ -149,7 +149,7 @@ import {
149
149
  runGit,
150
150
  shortFingerprint,
151
151
  toKB
152
- } from "./token-goat-chunk-44Y77VHR.mjs";
152
+ } from "./token-goat-chunk-ELDJRLHZ.mjs";
153
153
 
154
154
  // src/hooks_grep.ts
155
155
  function grepIntInput(toolInput, key) {
@@ -620,7 +620,7 @@ import crypto from "node:crypto";
620
620
  var TRACKED_SKILL = "token-goat";
621
621
  var MAX_COMMANDS_SHOWN = 8;
622
622
  async function currentCommandNames() {
623
- const { buildProgram } = await import("./token-goat-chunk-J35AKWEQ.mjs");
623
+ const { buildProgram } = await import("./token-goat-chunk-Y2SYNH3S.mjs");
624
624
  return flattenCommandNames(buildCommandManifest(buildProgram()));
625
625
  }
626
626
  async function recordSkillVersionSnapshot(sessionId, skillName) {
@@ -8,7 +8,7 @@ import {
8
8
  import { createRequire } from "node:module";
9
9
  function resolveVersion() {
10
10
  if (true) {
11
- return "2.8.0";
11
+ return "2.8.1";
12
12
  }
13
13
  const require2 = createRequire(import.meta.url);
14
14
  const pkg = require2("../package.json");
@@ -5801,6 +5801,30 @@ function _renderByCommandSection(stats) {
5801
5801
  }
5802
5802
  return lines;
5803
5803
  }
5804
+ function _renderByHarnessSection(stats) {
5805
+ if (!stats.by_harness || stats.by_harness.length < 2) {
5806
+ return [];
5807
+ }
5808
+ const lines = [..._sectionHeader("By harness"), _tableHeader("harness")];
5809
+ const { grossBytes, shareBytesDenom, shareTokensDenom } = _computeShareDenominators(stats.by_harness);
5810
+ function share(h) {
5811
+ return _absShare(h.bytes, h.tokens, shareBytesDenom, shareTokensDenom);
5812
+ }
5813
+ for (const h of [...stats.by_harness].sort((a, b) => share(b) - share(a))) {
5814
+ lines.push(
5815
+ _tableRow({
5816
+ name: h.harness,
5817
+ fraction: _barFraction(h.bytes, grossBytes),
5818
+ bytes: h.bytes,
5819
+ tokens: h.tokens,
5820
+ events: h.events,
5821
+ share: share(h),
5822
+ nameColor: C.TEXT_PRIMARY
5823
+ })
5824
+ );
5825
+ }
5826
+ return lines;
5827
+ }
5804
5828
  function _renderByDaySection(stats) {
5805
5829
  if (stats.by_day.length === 0) {
5806
5830
  return [];
@@ -5923,6 +5947,7 @@ function renderStats(stats, opts) {
5923
5947
  _renderByKindSection(stats),
5924
5948
  _renderBySourceSection(stats),
5925
5949
  _renderByCommandSection(stats),
5950
+ _renderByHarnessSection(stats),
5926
5951
  _renderByDaySection(stats),
5927
5952
  _renderByProjectSection(stats),
5928
5953
  _renderInsightsSection(stats),
@@ -5932,6 +5957,7 @@ function renderStats(stats, opts) {
5932
5957
  }
5933
5958
 
5934
5959
  // src/stats.ts
5960
+ var HARNESS_UNRECORDED = "unrecorded (pre-2.8.1)";
5935
5961
  var SOURCE_IMAGE = "image";
5936
5962
  var SOURCE_HINT = "hint";
5937
5963
  var SOURCE_READ = "read";
@@ -5978,6 +6004,7 @@ var KIND_TO_SOURCE = {
5978
6004
  csv_query: SOURCE_READ,
5979
6005
  csv_profile: SOURCE_READ,
5980
6006
  pdf_extract: SOURCE_READ,
6007
+ pdf_locate: SOURCE_READ,
5981
6008
  pdf_outline: SOURCE_READ,
5982
6009
  pdf_meta: SOURCE_READ,
5983
6010
  xlsx_sheets: SOURCE_READ,
@@ -6067,6 +6094,7 @@ var COMMAND_KINDS = {
6067
6094
  "csv-query": /* @__PURE__ */ new Set(["csv_query"]),
6068
6095
  "csv-profile": /* @__PURE__ */ new Set(["csv_profile"]),
6069
6096
  "pdf-extract": /* @__PURE__ */ new Set(["pdf_extract"]),
6097
+ "pdf-locate": /* @__PURE__ */ new Set(["pdf_locate"]),
6070
6098
  "pdf-outline": /* @__PURE__ */ new Set(["pdf_outline"]),
6071
6099
  "pdf-meta": /* @__PURE__ */ new Set(["pdf_meta"]),
6072
6100
  "xlsx-sheets": /* @__PURE__ */ new Set(["xlsx_sheets"]),
@@ -6161,19 +6189,53 @@ CREATE TABLE IF NOT EXISTS stats (
6161
6189
  kind TEXT NOT NULL,
6162
6190
  tokens_saved INTEGER NOT NULL DEFAULT 0,
6163
6191
  bytes_saved INTEGER NOT NULL DEFAULT 0,
6164
- detail TEXT
6192
+ detail TEXT,
6193
+ harness TEXT
6165
6194
  );
6166
6195
  CREATE INDEX IF NOT EXISTS idx_stats_ts ON stats(ts);
6167
6196
  CREATE INDEX IF NOT EXISTS idx_stats_kind ON stats(kind);
6197
+ CREATE TABLE IF NOT EXISTS unmapped_tools (
6198
+ harness TEXT NOT NULL,
6199
+ tool_name TEXT NOT NULL,
6200
+ event_name TEXT NOT NULL,
6201
+ near_miss TEXT,
6202
+ first_seen INTEGER NOT NULL,
6203
+ last_seen INTEGER NOT NULL,
6204
+ hits INTEGER NOT NULL DEFAULT 0,
6205
+ PRIMARY KEY (harness, tool_name, event_name)
6206
+ );
6168
6207
  `;
6169
6208
  var _globalSchemaApplied = /* @__PURE__ */ new Set();
6170
6209
  registerReset(() => _globalSchemaApplied.clear());
6210
+ function migrateGlobalSchema(db) {
6211
+ try {
6212
+ db.exec("ALTER TABLE stats ADD COLUMN harness TEXT");
6213
+ } catch (err) {
6214
+ if (!(err instanceof Error) || !/duplicate column/i.test(err.message)) throw err;
6215
+ }
6216
+ }
6217
+ var _harnessColumnByDb = /* @__PURE__ */ new WeakMap();
6218
+ function statsHasHarnessColumn(db) {
6219
+ const cached = _harnessColumnByDb.get(db);
6220
+ if (cached !== void 0) return cached;
6221
+ let present;
6222
+ try {
6223
+ present = db.prepare("PRAGMA table_info(stats)").all().some(
6224
+ (c) => c.name === "harness"
6225
+ );
6226
+ } catch {
6227
+ present = false;
6228
+ }
6229
+ _harnessColumnByDb.set(db, present);
6230
+ return present;
6231
+ }
6171
6232
  function getGlobalDb(homeDir) {
6172
6233
  const basePath = homeDir ? dataDirForHome(homeDir) : dataDir();
6173
6234
  const dbPath = path8.join(basePath, "global.db");
6174
6235
  const db = getDb(dbPath);
6175
6236
  if (!_globalSchemaApplied.has(dbPath)) {
6176
6237
  db.exec(GLOBAL_SCHEMA_SQL);
6238
+ migrateGlobalSchema(db);
6177
6239
  _globalSchemaApplied.add(dbPath);
6178
6240
  }
6179
6241
  return db;
@@ -6188,10 +6250,44 @@ function noStatsMessage(windowDays, homeDir) {
6188
6250
  function recordStat(kind, bytesSaved = 0, tokensSaved = 0, _testDb, detail) {
6189
6251
  try {
6190
6252
  const db = _testDb ?? getGlobalDb();
6253
+ const ts = Math.floor(Date.now() / 1e3);
6254
+ if (statsHasHarnessColumn(db)) {
6255
+ db.prepare(
6256
+ "INSERT INTO stats (ts, kind, bytes_saved, tokens_saved, detail, harness) VALUES (?, ?, ?, ?, ?, ?)"
6257
+ ).run(ts, kind, bytesSaved, tokensSaved, detail ?? null, getHarnessName());
6258
+ } else {
6259
+ db.prepare(
6260
+ "INSERT INTO stats (ts, kind, bytes_saved, tokens_saved, detail) VALUES (?, ?, ?, ?, ?)"
6261
+ ).run(ts, kind, bytesSaved, tokensSaved, detail ?? null);
6262
+ }
6263
+ } catch {
6264
+ }
6265
+ }
6266
+ var MAX_TOOL_NAME_CHARS = 200;
6267
+ function recordUnmappedTool(toolName, eventName, nearMiss, _testDb) {
6268
+ try {
6269
+ if (!toolName) return;
6270
+ const db = _testDb ?? getGlobalDb();
6271
+ const now = Math.floor(Date.now() / 1e3);
6191
6272
  db.prepare(
6192
- "INSERT INTO stats (ts, kind, bytes_saved, tokens_saved, detail) VALUES (?, ?, ?, ?, ?)"
6193
- ).run(Math.floor(Date.now() / 1e3), kind, bytesSaved, tokensSaved, detail ?? null);
6273
+ `INSERT INTO unmapped_tools (harness, tool_name, event_name, near_miss, first_seen, last_seen, hits)
6274
+ VALUES (?, ?, ?, ?, ?, ?, 1)
6275
+ ON CONFLICT(harness, tool_name, event_name) DO UPDATE SET
6276
+ hits = hits + 1,
6277
+ last_seen = excluded.last_seen,
6278
+ near_miss = excluded.near_miss`
6279
+ ).run(getHarnessName(), toolName.slice(0, MAX_TOOL_NAME_CHARS), eventName, nearMiss, now, now);
6280
+ } catch {
6281
+ }
6282
+ }
6283
+ function readUnmappedTools(dbPath, homeDir) {
6284
+ try {
6285
+ const db = dbPath ? getDb(dbPath) : getGlobalDb(homeDir);
6286
+ return db.prepare(
6287
+ "SELECT harness, tool_name, event_name, near_miss, hits, last_seen FROM unmapped_tools ORDER BY hits DESC, tool_name ASC"
6288
+ ).all();
6194
6289
  } catch {
6290
+ return [];
6195
6291
  }
6196
6292
  }
6197
6293
  function summarize(windowDays = 30, testDb, homeDir) {
@@ -6199,11 +6295,14 @@ function summarize(windowDays = 30, testDb, homeDir) {
6199
6295
  const sinceTs = windowDays > 0 ? Math.floor((Date.now() - windowDays * 24 * 60 * 60 * 1e3) / 1e3) : null;
6200
6296
  const byKind = {};
6201
6297
  const byDay = {};
6298
+ const byHarness = {};
6202
6299
  let totalEvents = 0;
6203
6300
  let totalBytes = 0;
6204
6301
  let totalTokens = 0;
6205
6302
  const db = testDb ?? getGlobalDb(homeDir);
6206
- const query = sinceTs !== null ? "SELECT ts, kind, bytes_saved, tokens_saved FROM stats WHERE ts >= ? ORDER BY ts DESC" : "SELECT ts, kind, bytes_saved, tokens_saved FROM stats ORDER BY ts DESC";
6303
+ const hasHarness = statsHasHarnessColumn(db);
6304
+ const cols = hasHarness ? "ts, kind, bytes_saved, tokens_saved, harness" : "ts, kind, bytes_saved, tokens_saved";
6305
+ const query = sinceTs !== null ? `SELECT ${cols} FROM stats WHERE ts >= ? ORDER BY ts DESC` : `SELECT ${cols} FROM stats ORDER BY ts DESC`;
6207
6306
  const stmt = db.prepare(query);
6208
6307
  const rows = sinceTs !== null ? stmt.all(sinceTs) : stmt.all();
6209
6308
  const tsToDateCache = {};
@@ -6227,6 +6326,11 @@ function summarize(windowDays = 30, testDb, homeDir) {
6227
6326
  byDay[dateKey] = zeroBucket();
6228
6327
  }
6229
6328
  incBucket(byDay[dateKey], bytesSaved, tokensSaved);
6329
+ const harness = row.harness || HARNESS_UNRECORDED;
6330
+ if (!byHarness[harness]) {
6331
+ byHarness[harness] = zeroBucket();
6332
+ }
6333
+ incBucket(byHarness[harness], bytesSaved, tokensSaved);
6230
6334
  }
6231
6335
  const bySourceDict = {};
6232
6336
  for (const [kind, bucket] of Object.entries(byKind)) {
@@ -6263,6 +6367,7 @@ function summarize(windowDays = 30, testDb, homeDir) {
6263
6367
  by_day: byDayList,
6264
6368
  by_project: byProjectList,
6265
6369
  by_source: bySourceDict,
6370
+ by_harness: byHarness,
6266
6371
  by_command: Object.entries(byCommandDict).map(([command, bucket]) => ({ ...bucket, command })).filter((r) => r.events > 0),
6267
6372
  window_days: windowDays
6268
6373
  };
@@ -6300,6 +6405,15 @@ function _plainTextStats(summary) {
6300
6405
  );
6301
6406
  }
6302
6407
  }
6408
+ const harnesses = Object.entries(summary.by_harness).filter(([, b]) => b.events > 0).sort((a, b) => b[1].tokens_saved - a[1].tokens_saved);
6409
+ if (harnesses.length > 1) {
6410
+ lines.push("", "## By Harness");
6411
+ for (const [harness, bucket] of harnesses) {
6412
+ lines.push(
6413
+ ` ${harness.padEnd(22)} ${bucket.events.toString().padStart(6)} events ${fmtBytes(bucket.bytes_saved).padStart(8)} ${bucket.tokens_saved.toString().padStart(8)} tokens`
6414
+ );
6415
+ }
6416
+ }
6303
6417
  if (summary.by_command.length > 0) {
6304
6418
  lines.push("", "## By Command");
6305
6419
  for (const row of summary.by_command) {
@@ -6373,7 +6487,13 @@ function _buildStatsData(summary, windowDays) {
6373
6487
  bytes: c.bytes_saved,
6374
6488
  tokens: c.tokens_saved,
6375
6489
  events: c.events
6376
- }))
6490
+ })),
6491
+ by_harness: Object.entries(summary.by_harness).filter(([, b]) => b.events > 0).map(([harness, bucket]) => ({
6492
+ harness,
6493
+ bytes: bucket.bytes_saved,
6494
+ tokens: bucket.tokens_saved,
6495
+ events: bucket.events
6496
+ })).sort((a, b) => b.bytes - a.bytes)
6377
6497
  };
6378
6498
  }
6379
6499
  function renderShortStats(opts) {
@@ -6770,6 +6890,8 @@ export {
6770
6890
  SOURCE_HINT,
6771
6891
  formatLocalTimestamp,
6772
6892
  recordStat,
6893
+ recordUnmappedTool,
6894
+ readUnmappedTools,
6773
6895
  summarize,
6774
6896
  _useRichStats,
6775
6897
  renderShortStats,
@@ -10,11 +10,11 @@ import {
10
10
  selectFilter,
11
11
  shlexSplit,
12
12
  wrappedShell
13
- } from "./token-goat-chunk-TUPJRK7R.mjs";
13
+ } from "./token-goat-chunk-7JDXDERZ.mjs";
14
14
  import {
15
15
  loadConfig,
16
16
  recordStat
17
- } from "./token-goat-chunk-44Y77VHR.mjs";
17
+ } from "./token-goat-chunk-ELDJRLHZ.mjs";
18
18
  import "./token-goat-chunk-AO2QD2AG.mjs";
19
19
  import "./token-goat-chunk-AEX54RUZ.mjs";
20
20