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 +5 -3
- package/dist/{token-goat-chunk-2F6TFBZE.mjs → token-goat-chunk-2G6RAB4G.mjs} +119 -13
- package/dist/{token-goat-chunk-FBGBTICM.mjs → token-goat-chunk-73MF6YWW.mjs} +3 -3
- package/dist/{token-goat-chunk-TUPJRK7R.mjs → token-goat-chunk-7JDXDERZ.mjs} +1 -1
- package/dist/{token-goat-chunk-LVCBDJVE.mjs → token-goat-chunk-AOBZUFNJ.mjs} +5 -5
- package/dist/{token-goat-chunk-44Y77VHR.mjs → token-goat-chunk-ELDJRLHZ.mjs} +128 -6
- package/dist/{token-goat-chunk-4KZILRZN.mjs → token-goat-chunk-I6TOUPLP.mjs} +2 -2
- package/dist/{token-goat-chunk-SRAR6DOK.mjs → token-goat-chunk-RMDQFTQD.mjs} +95 -11
- package/dist/{token-goat-chunk-VXSYZGBA.mjs → token-goat-chunk-VBXBLGTO.mjs} +217 -16
- package/dist/{token-goat-chunk-AO6MFFTW.mjs → token-goat-chunk-XEDQH5DA.mjs} +2 -2
- package/dist/{token-goat-chunk-J35AKWEQ.mjs → token-goat-chunk-Y2SYNH3S.mjs} +5 -5
- package/dist/{token-goat-chunk-A62K4XW2.mjs → token-goat-chunk-YUGNM3KL.mjs} +5 -5
- package/dist/token-goat-hook.mjs +5 -5
- package/dist/token-goat.core.mjs +5 -5
- package/package.json +4 -2
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-
|
|
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
|
-
|
|
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
|
-
|
|
2832
|
-
|
|
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
|
|
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
|
|
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:
|
|
5181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
summarizeOutputDelta,
|
|
24
24
|
summarizeResidentContext,
|
|
25
25
|
taskListPruneHint
|
|
26
|
-
} from "./token-goat-chunk-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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.
|
|
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
|
-
|
|
6193
|
-
|
|
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
|
|
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-
|
|
13
|
+
} from "./token-goat-chunk-7JDXDERZ.mjs";
|
|
14
14
|
import {
|
|
15
15
|
loadConfig,
|
|
16
16
|
recordStat
|
|
17
|
-
} from "./token-goat-chunk-
|
|
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
|
|