token-goat 2.8.0 → 2.8.2

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.
@@ -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-LILS6TIU.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-AM23GDIS.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-IVCTQPZD.mjs";
49
49
  import "./token-goat-chunk-AO2QD2AG.mjs";
50
50
  import "./token-goat-chunk-AEX54RUZ.mjs";
51
51
 
@@ -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-IVCTQPZD.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,
@@ -9819,7 +9922,7 @@ function extractMarkdownHeadings(content, limit = MAX_HEADINGS) {
9819
9922
  const lines2 = content.split("\n");
9820
9923
  for (const [i, line] of eachUnfencedLine(lines2)) {
9821
9924
  if (!line) continue;
9822
- const match = /^(#+)\s+(.+?)(?:\s+#+\s*)?$/.exec(line);
9925
+ const match = /^(#+)\s+([^\r\n]+?)(?:\s+#+)?\s*$/.exec(line);
9823
9926
  if (!match || match.length < 3) continue;
9824
9927
  const hashes = match[1];
9825
9928
  const headingText = match[2];
@@ -9949,7 +10052,7 @@ function handleHtml(filePath, content, contentLengthHint) {
9949
10052
  message: `Large HTML file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap). Use token-goat section to extract a section by heading, or convert to text: pandoc "${filePath}" -t plain`
9950
10053
  };
9951
10054
  }
9952
- const title = content.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.trim();
10055
+ const title = content.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]?.trim();
9953
10056
  const headings = findHtmlHeadingMatches(content).slice(0, 20).map(({ level, heading }) => {
9954
10057
  if (!heading) return "";
9955
10058
  return `${" ".repeat(level - 1)}h${level}: ${heading}`;
@@ -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,
@@ -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-TF5NT3H5.mjs";
14
14
  import {
15
15
  loadConfig,
16
16
  recordStat
17
- } from "./token-goat-chunk-44Y77VHR.mjs";
17
+ } from "./token-goat-chunk-IVCTQPZD.mjs";
18
18
  import "./token-goat-chunk-AO2QD2AG.mjs";
19
19
  import "./token-goat-chunk-AEX54RUZ.mjs";
20
20
 
@@ -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.2";
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) {
@@ -6537,7 +6657,7 @@ var SECRET_PATTERNS = [
6537
6657
  // identifier characters only, so prose that merely mentions a keyword still never reaches
6538
6658
  // a separator and stays unredacted. api[_-]?key covers the apikey and api-key spellings too.
6539
6659
  // `& ; # , :` play two incompatible roles. They separate one field from the next (a query
6540
- // string, a cookie header, an inline env list), and they are also perfectly ordinary password
6660
+ // string, a cookie header, an inline env list), and they are also perfectly ordinary credential
6541
6661
  // characters. Rejecting them outright got the first role right and the second badly wrong: the
6542
6662
  // match stopped at the first one and left everything after it in plain text, so
6543
6663
  // `password=corr&horse&battery` redacted four characters and printed the rest, and
@@ -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,
@@ -4,14 +4,14 @@ import {
4
4
  buildEvent,
5
5
  relay,
6
6
  relayInProcess
7
- } from "./token-goat-chunk-LVCBDJVE.mjs";
7
+ } from "./token-goat-chunk-KBVN4ELV.mjs";
8
8
  import {
9
9
  MAX_STDIN_BYTES,
10
10
  readStdinJson
11
- } from "./token-goat-chunk-AO6MFFTW.mjs";
12
- import "./token-goat-chunk-2F6TFBZE.mjs";
13
- import "./token-goat-chunk-TUPJRK7R.mjs";
14
- import "./token-goat-chunk-44Y77VHR.mjs";
11
+ } from "./token-goat-chunk-5MLXFSRI.mjs";
12
+ import "./token-goat-chunk-AM23GDIS.mjs";
13
+ import "./token-goat-chunk-TF5NT3H5.mjs";
14
+ import "./token-goat-chunk-IVCTQPZD.mjs";
15
15
  import "./token-goat-chunk-AO2QD2AG.mjs";
16
16
  import "./token-goat-chunk-AEX54RUZ.mjs";
17
17
  export {
@@ -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-5MLXFSRI.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-AM23GDIS.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-TF5NT3H5.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-IVCTQPZD.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-SE6V7LUZ.mjs");
624
624
  return flattenCommandNames(buildCommandManifest(buildProgram()));
625
625
  }
626
626
  async function recordSkillVersionSnapshot(sessionId, skillName) {