token-goat 2.9.6 → 2.9.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -86,7 +86,7 @@ import {
86
86
  runZipRead,
87
87
  symbolNamesInFile,
88
88
  upsertNote
89
- } from "./token-goat-chunk-46VKOCUH.mjs";
89
+ } from "./token-goat-chunk-L6Q2XM5X.mjs";
90
90
  import {
91
91
  DEFAULT_RECONCILE_BUDGET_MS,
92
92
  GEMINI_TOOL_NAME_MAP,
@@ -108,7 +108,7 @@ import {
108
108
  runReconcile,
109
109
  storeWebOutput,
110
110
  summarizeResidentContext
111
- } from "./token-goat-chunk-7GJBID7S.mjs";
111
+ } from "./token-goat-chunk-QCHD2ENV.mjs";
112
112
  import {
113
113
  AGENT_SALT_MARKER,
114
114
  BASH_OUTPUT_SUBDIR,
@@ -190,6 +190,7 @@ import {
190
190
  isParseSkipEligible,
191
191
  isRecallCacheType,
192
192
  isSuppressionCategory,
193
+ isTreeSitterAvailable,
193
194
  isWorkerRunning,
194
195
  listBlobs,
195
196
  listOutputs,
@@ -230,12 +231,14 @@ import {
230
231
  storeCompact,
231
232
  storeOutput,
232
233
  tokenGoatHome,
234
+ treeSitterCoreAvailable,
235
+ treeSitterCoreLoadError,
233
236
  trimToBudget,
234
237
  urlPolicyDenialReason,
235
238
  visionTokensSavedByText,
236
239
  walkProject,
237
240
  writeCompact
238
- } from "./token-goat-chunk-3HJQR4OO.mjs";
241
+ } from "./token-goat-chunk-BYDDWQ2T.mjs";
239
242
  import {
240
243
  C,
241
244
  CONFIG_KEY_ENV_OVERRIDES,
@@ -346,6 +349,7 @@ import {
346
349
  savedTokensFromBytes,
347
350
  sleepSync,
348
351
  stripAnsi,
352
+ stripLower,
349
353
  stripOwnHooksFromMap,
350
354
  stripStaleGroupHooks,
351
355
  suggestPackageNames,
@@ -368,7 +372,7 @@ import {
368
372
  withFileLock,
369
373
  withRetryOnLock,
370
374
  writeJsonSettings
371
- } from "./token-goat-chunk-T2OE7MYM.mjs";
375
+ } from "./token-goat-chunk-KIEOFWLL.mjs";
372
376
  import {
373
377
  __export,
374
378
  init_define_import_meta_env
@@ -6313,6 +6317,29 @@ function checkTsCompiler() {
6313
6317
  message: err2 !== null ? `unavailable: ${extractErrorMessage(err2)}` : "unavailable (not attempted)"
6314
6318
  };
6315
6319
  }
6320
+ var TREE_SITTER_LANGUAGES = ["typescript", "javascript", "python", "go", "rust", "ruby", "java", "c", "cpp"];
6321
+ function checkTreeSitter() {
6322
+ const name = "Tree-sitter";
6323
+ if (!treeSitterCoreAvailable()) {
6324
+ const err2 = treeSitterCoreLoadError();
6325
+ const cause = err2 !== null ? `: ${extractErrorMessage(err2)}` : " (not attempted)";
6326
+ return {
6327
+ name,
6328
+ status: "warn",
6329
+ message: "unavailable -- the source skeleton fold, the code body fold's disk-parse fallback, and tree-sitter indexing are all disabled; likely cause: the optional native dependency `tree-sitter` is not installed, or is not resolvable from the bundle's location" + cause
6330
+ };
6331
+ }
6332
+ const available = TREE_SITTER_LANGUAGES.filter((lang) => isTreeSitterAvailable(lang));
6333
+ if (available.length === TREE_SITTER_LANGUAGES.length) {
6334
+ return { name, status: "ok", message: `available (${available.length}/${TREE_SITTER_LANGUAGES.length} grammars)` };
6335
+ }
6336
+ const missing = TREE_SITTER_LANGUAGES.filter((lang) => !available.includes(lang));
6337
+ return {
6338
+ name,
6339
+ status: "warn",
6340
+ message: `partially available (${available.length}/${TREE_SITTER_LANGUAGES.length} grammars); missing: ${missing.join(", ")}`
6341
+ };
6342
+ }
6316
6343
  function checkEmbeddings(config) {
6317
6344
  const name = "Embeddings";
6318
6345
  if ((config.indexing?.embeddings_enabled ?? true) === false) {
@@ -6688,6 +6715,7 @@ function runDoctor(dataDir2, configPath2, rootDir, processes) {
6688
6715
  const actualDataDir = dataDir2 || dataDir();
6689
6716
  results.push(checkInstall());
6690
6717
  results.push(checkTsCompiler());
6718
+ results.push(checkTreeSitter());
6691
6719
  results.push(checkStrayClaudeMdBlocks());
6692
6720
  results.push(checkWorkerRunning(actualDataDir) ? { name: "Worker", status: "ok", message: "running" } : { name: "Worker", status: "warn", message: "not running" });
6693
6721
  results.push(checkDbExists(actualDataDir));
@@ -14937,11 +14965,14 @@ function readClaudeJsonConfig(claudeJsonPath, projectRoot) {
14937
14965
  function discoverMcpConfig(projectRoot, home) {
14938
14966
  const mcpJsonPath = path28.join(projectRoot, ".mcp.json");
14939
14967
  const claudeJsonPath = path28.join(home, ".claude.json");
14940
- const sourcesChecked = [mcpJsonPath, claudeJsonPath];
14968
+ const copilotJsonPath = path28.join(home, ".copilot", "mcp-config.json");
14969
+ const sourcesChecked = [mcpJsonPath, claudeJsonPath, copilotJsonPath];
14941
14970
  const fromMcpJson = readMcpJsonFile(mcpJsonPath);
14942
14971
  if (fromMcpJson !== null) return { servers: fromMcpJson, sourcePath: mcpJsonPath, sourcesChecked };
14943
14972
  const fromClaudeJson = readClaudeJsonConfig(claudeJsonPath, projectRoot);
14944
14973
  if (fromClaudeJson !== null) return { servers: fromClaudeJson, sourcePath: claudeJsonPath, sourcesChecked };
14974
+ const fromCopilotJson = readMcpJsonFile(copilotJsonPath);
14975
+ if (fromCopilotJson !== null) return { servers: fromCopilotJson, sourcePath: copilotJsonPath, sourcesChecked };
14945
14976
  return { servers: null, sourcePath: null, sourcesChecked };
14946
14977
  }
14947
14978
  function analyzeMcpCache() {
@@ -15730,7 +15761,7 @@ async function cmdMcpServe() {
15730
15761
  let StdioServerTransport;
15731
15762
  try {
15732
15763
  ;
15733
- ({ createMcpServer } = await import("./token-goat-chunk-C7LLUVUT.mjs"));
15764
+ ({ createMcpServer } = await import("./token-goat-chunk-LQ3SGWIW.mjs"));
15734
15765
  ({ StdioServerTransport } = await import("./token-goat-chunk-EVC4TOLE.mjs"));
15735
15766
  } catch (err2) {
15736
15767
  process.stderr.write(
@@ -15755,7 +15786,7 @@ async function cmdHook(event, opts) {
15755
15786
  if (typeof opts.harness === "string" && opts.harness.length > 0) {
15756
15787
  process.env[ENV_KEYS.HARNESS_OVERRIDE] = opts.harness;
15757
15788
  }
15758
- const { relay } = await import("./token-goat-chunk-TX64YIIV.mjs");
15789
+ const { relay } = await import("./token-goat-chunk-K5GKX6ND.mjs");
15759
15790
  await relay(event);
15760
15791
  }
15761
15792
  async function cmdInstall(opts) {
@@ -16688,7 +16719,7 @@ function emitExtraFileArgsNote(command, first, extras, opts = {}) {
16688
16719
  }
16689
16720
  async function cmdCompress(opts) {
16690
16721
  try {
16691
- const bashRunner = await import("./token-goat-chunk-UJORWTI4.mjs");
16722
+ const bashRunner = await import("./token-goat-chunk-WBPVYAEO.mjs");
16692
16723
  if (opts.compress === false) {
16693
16724
  process.exitCode = bashRunner.runRaw(opts.cmd, parseTimeout(opts.timeout, bashRunner.DEFAULT_TIMEOUT_SECONDS));
16694
16725
  return;
@@ -16961,8 +16992,12 @@ async function cmdSkillSection(nameHeading, headingArg) {
16961
16992
  const body = decodeSource(fs29.readFileSync(filePath));
16962
16993
  const extracted = extractNamedSection(body, heading);
16963
16994
  if (!extracted) {
16964
- const messages = [`Section '${heading}' not found in skill '${skillName}'`];
16965
16995
  const allHeadings = listSections(filePath);
16996
+ const wanted = stripLower(heading);
16997
+ if (allHeadings.some((h) => stripLower(h) === wanted)) {
16998
+ throw new CliError(`Section '${heading}' in skill '${skillName}' is present but empty`);
16999
+ }
17000
+ const messages = [`Section '${heading}' not found in skill '${skillName}'`];
16966
17001
  const available = filterSimilarHeadings(allHeadings, heading);
16967
17002
  if (available.length > 0) messages.push(didYouMean(available));
16968
17003
  else if (allHeadings.length === 0) messages.push(`skill '${skillName}' has no headings`);
@@ -9,11 +9,12 @@ import {
9
9
  isBlobStale,
10
10
  loadBlob,
11
11
  storeBlob
12
- } from "./token-goat-chunk-3HJQR4OO.mjs";
12
+ } from "./token-goat-chunk-BYDDWQ2T.mjs";
13
13
  import {
14
14
  SYMBOL_BODY_CHAR_CAP,
15
15
  copilotCliMcpToolsDir,
16
16
  countNoun,
17
+ displaySafeText,
17
18
  extractErrorMessage,
18
19
  fingerprintFile,
19
20
  foldPath,
@@ -24,7 +25,7 @@ import {
24
25
  resolveIndexPath,
25
26
  shortFingerprint,
26
27
  toDisplayPath
27
- } from "./token-goat-chunk-T2OE7MYM.mjs";
28
+ } from "./token-goat-chunk-KIEOFWLL.mjs";
28
29
  import {
29
30
  registerReset
30
31
  } from "./token-goat-chunk-EEIDFMEM.mjs";
@@ -840,8 +841,9 @@ function repeatedSkillBodyHint(injections) {
840
841
  const worst = injections[0];
841
842
  if (worst === void 0) return null;
842
843
  if (worst.count < SKILL_BODY_REPEAT_THRESHOLD || worst.bytes < LARGE_SKILL_BODY_BYTES) return null;
844
+ const skill = displaySafeText(worst.skill);
843
845
  const tokens = formatTokenEstimate(estimateTokensFromLength(worst.bytes));
844
- return `The \`${worst.skill}\` skill body has been injected ${worst.count} times this session (${formatBytes(worst.bytes)} total, ~${tokens} tok est). Slash expansion and the Skill tool both send the whole body every time, and no hook can intercept either. If it is already loaded, work from it instead of re-invoking; to reread one part, use \`token-goat skill-section ${worst.skill} '<heading>'\`.`;
846
+ return `The \`${skill}\` skill body has been injected ${worst.count} times this session (${formatBytes(worst.bytes)} total, ~${tokens} tok est). Slash expansion and the Skill tool both send the whole body every time, and no hook can intercept either. If it is already loaded, work from it instead of re-invoking; to reread one part, use \`token-goat skill-section ${skill} '<heading>'\`.`;
845
847
  }
846
848
  function readTranscriptTail(transcriptPath, maxBytes = RESIDENT_TAIL_MAX_BYTES) {
847
849
  let fd = null;
@@ -3,7 +3,7 @@ const require = __cjsRequire(import.meta.url);
3
3
  import {
4
4
  deliveredOutputBytes,
5
5
  wrappedShell
6
- } from "./token-goat-chunk-LXIC7MTW.mjs";
6
+ } from "./token-goat-chunk-OBDTBOQA.mjs";
7
7
  import {
8
8
  ToolFilter,
9
9
  capTokens,
@@ -15,7 +15,7 @@ import {
15
15
  recordStat,
16
16
  selectFilter,
17
17
  shlexSplit
18
- } from "./token-goat-chunk-T2OE7MYM.mjs";
18
+ } from "./token-goat-chunk-KIEOFWLL.mjs";
19
19
  import "./token-goat-chunk-EEIDFMEM.mjs";
20
20
  import {
21
21
  init_define_import_meta_env
@@ -2,11 +2,11 @@ import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
4
  relayInProcess
5
- } from "./token-goat-chunk-EIZYCVBM.mjs";
6
- import "./token-goat-chunk-7GJBID7S.mjs";
7
- import "./token-goat-chunk-3HJQR4OO.mjs";
8
- import "./token-goat-chunk-LXIC7MTW.mjs";
9
- import "./token-goat-chunk-T2OE7MYM.mjs";
5
+ } from "./token-goat-chunk-P5SQX2VK.mjs";
6
+ import "./token-goat-chunk-QCHD2ENV.mjs";
7
+ import "./token-goat-chunk-BYDDWQ2T.mjs";
8
+ import "./token-goat-chunk-OBDTBOQA.mjs";
9
+ import "./token-goat-chunk-KIEOFWLL.mjs";
10
10
  import "./token-goat-chunk-EEIDFMEM.mjs";
11
11
  import {
12
12
  init_define_import_meta_env
@@ -2,13 +2,13 @@ import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./token-goat-chunk-J7LGMKF3.mjs";
6
- import "./token-goat-chunk-46VKOCUH.mjs";
7
- import "./token-goat-chunk-7GJBID7S.mjs";
8
- import "./token-goat-chunk-3HJQR4OO.mjs";
5
+ } from "./token-goat-chunk-PNMGVJ4C.mjs";
6
+ import "./token-goat-chunk-L6Q2XM5X.mjs";
7
+ import "./token-goat-chunk-QCHD2ENV.mjs";
8
+ import "./token-goat-chunk-BYDDWQ2T.mjs";
9
9
  import {
10
10
  installEpipeGuard
11
- } from "./token-goat-chunk-T2OE7MYM.mjs";
11
+ } from "./token-goat-chunk-KIEOFWLL.mjs";
12
12
  import "./token-goat-chunk-EEIDFMEM.mjs";
13
13
  import {
14
14
  init_define_import_meta_env
package/docs/install.md CHANGED
@@ -223,6 +223,68 @@ To upgrade cleanly:
223
223
 
224
224
  **AI agents handling an upgrade:** complete all three steps in sequence. After step 3, confirm no `token_goat` entries remain in `settings.json` before reporting the upgrade done.
225
225
 
226
+ ### Command auto-approval and permissions
227
+
228
+ By default, `token-goat install` configures hook triggers only. It never writes permission entries into your environment. If your setup requires confirmation for shell commands, an approval prompt can appear whenever file names, line numbers, or arguments change.
229
+
230
+ You can add wildcard rules to avoid repeated prompts for routine queries:
231
+
232
+ #### Claude Code (`~/.claude/settings.json`)
233
+
234
+ To allow all `token-goat` commands without prompting:
235
+
236
+ ```json
237
+ {
238
+ "permissions": {
239
+ "allow": [
240
+ "Bash(token-goat *)",
241
+ "Bash(token-goat:*)"
242
+ ]
243
+ }
244
+ }
245
+ ```
246
+
247
+ To limit auto-approval strictly to read-only queries:
248
+
249
+ ```json
250
+ {
251
+ "permissions": {
252
+ "allow": [
253
+ "Bash(token-goat read *)",
254
+ "Bash(token-goat symbol *)",
255
+ "Bash(token-goat section *)",
256
+ "Bash(token-goat outline *)",
257
+ "Bash(token-goat skeleton *)",
258
+ "Bash(token-goat brief *)",
259
+ "Bash(token-goat json-query *)",
260
+ "Bash(token-goat yaml-query *)",
261
+ "Bash(token-goat xml-query *)",
262
+ "Bash(token-goat map *)",
263
+ "Bash(token-goat refs *)",
264
+ "Bash(token-goat deps *)"
265
+ ]
266
+ }
267
+ }
268
+ ```
269
+
270
+ #### Codex CLI (`~/.codex/config.toml`)
271
+
272
+ Running `token-goat install --codex` automatically computes and writes `trusted_hash` into `[hooks.state]` so Codex trusts the hooks. To allow automatic command execution, set `approval_policy` in `~/.codex/config.toml`:
273
+
274
+ ```toml
275
+ approval_policy = "never"
276
+ ```
277
+
278
+ #### Copilot and VS Code MCP
279
+
280
+ For VS Code using the token-goat MCP server (`token-goat install --vscode`), enable MCP auto-approval in your VS Code `settings.json`:
281
+
282
+ ```json
283
+ {
284
+ "chat.mcp.autoApprove": true
285
+ }
286
+ ```
287
+
226
288
  ## What gets installed?
227
289
 
228
290
  `token-goat install` writes the following on your machine — nothing else, anywhere. Every entry is reversed by `token-goat uninstall`. Integrations for other harnesses are additive on the way out as well as in, so a plain uninstall does not touch one you installed with `--codex`, `--copilot`, or a sibling flag: rather than undo something you did not ask about, it names each one still present and the flag that removes it. Run `token-goat doctor` at any time to see which of these are currently present.
package/docs/security.md CHANGED
@@ -15,12 +15,12 @@ Outbound network is reserved to these explicit cases:
15
15
  - Google Drive API calls, only if you already authorized Drive in Claude Code. Token-goat never prompts for its own auth.
16
16
  - Image fetches from URLs: either explicit via `token-goat fetch-image <url>`, or when the AI agent issues a WebFetch call that returns image content — the hook intercepts and shrinks the image. The URL always originates from the agent's work, not from token-goat itself.
17
17
  - `token-goat screenshot <url>` navigates a headless browser to the URL you give it, subject to the target restrictions described below.
18
- - The first `token-goat semantic` run on a machine downloads the embedding model from `huggingface.co`, pinned to an immutable commit rather than a mutable branch and checked against a recorded SHA-256 and byte length before it is used, and only once `onnxruntime-node` has been installed (see below — it is not part of a default install). Subsequent runs use the local cache, re-verify it, and make no network call. Skip the download entirely by setting `indexing.embeddings_enabled = false` (it is on by default), in which case `semantic` falls back to full-text search.
18
+ - The first `token-goat semantic` run on a machine downloads the embedding model from `huggingface.co`, pinned to an immutable commit rather than a mutable branch and checked against a recorded SHA-256 and byte length before it is used, and only once `onnxruntime-node` has been installed (see below — it is not part of a default install). Subsequent runs use the local cache, re-verify it, and make no network call. Setting `TOKEN_GOAT_MODEL_CACHE_DIR` adds one more place to look before the network: a directory you name that outlives any single data root, which is how a continuous-integration job stops refetching the weights once per worker. A file taken from there is checked against the same recorded digest as a downloaded one and dropped from the shared directory if it fails, so a tampered copy costs a download and cannot substitute different weights. It is an environment variable only, so no project config file can point it anywhere. Skip the download entirely by setting `indexing.embeddings_enabled = false` (it is on by default), in which case `semantic` falls back to full-text search.
19
19
  - The first optical-character read of an image downloads the English language data (about 4 MB) from `cdn.jsdelivr.net`, at a fixed version path. Subsequent reads use the local cache. This happens for an explicit `token-goat image-text`, and also for the automatic text extraction the image-shrink hook performs when the agent reads a screenshot; turn the automatic one off with `image_shrink.ocr_enabled = false`.
20
20
 
21
21
  **One switch for all of it.** Set `network.offline = true` (env `TOKEN_GOAT_OFFLINE`) and every one of the paths above refuses instead of connecting, saying so rather than failing quietly. Anything already cached keeps working: a machine that has the embedding model still runs `semantic`, and one that has the language data still reads text out of images. This is one of the settings a per-project config file may not touch, so cloning a repository cannot switch it back off.
22
22
 
23
- **A repository cannot reconfigure the security controls.** A project-root `.token-goat.toml` layers on top of your global config, which is what it is for: hint thresholds, indexing settings, compression tuning. But that file arrives with the repository, so whoever wrote the repository wrote it. Seven whole sections are therefore off limits to it, and come from your global config or the environment only: `injection` (prompt-injection fencing), `webfetch` (the fetch allow and deny lists), `gdrive` (the Google Drive integration), `mcp` (root confinement and the allowed-roots list), `network` (offline mode), `redaction` (the secret-redaction rules), and `screenshot` (the headless browser). Twelve individual keys inside otherwise-overridable sections are locked the same way. Five decide what gets indexed at all: `indexing.cross_project_symbols`, `indexing.skip_dirs`, `indexing.skip_files`, `indexing.large_file_skip_kb` and `indexing.large_file_symbol_only_kb`, which matter because an unindexed file answers `symbol`, `read` and `semantic` in the same words a name that never existed does. Five more are the switches that decide whether a large file arrives folded rather than whole: `hints.fold_code_bodies`, `hints.fold_comment_blocks`, `hints.fold_prose_paragraphs`, `hints.outline_large_documents` and `hints.skeleton_large_sources`. The last two are `image_shrink.max_image_pixels`, the decompression-bomb cap, and `worker.blocked_roots`, the folders you have kept out of the index. A project file that sets one of them is ignored, and token-goat prints a line naming what it dropped. Everything else stays project-overridable.
23
+ **A repository cannot reconfigure the security controls.** A project-root `.token-goat.toml` layers on top of your global config, which is what it is for: hint thresholds, indexing settings, compression tuning. But that file arrives with the repository, so whoever wrote the repository wrote it. Seven whole sections are therefore off limits to it, and come from your global config or the environment only: `injection` (prompt-injection fencing), `webfetch` (the fetch allow and deny lists), `gdrive` (the Google Drive integration), `mcp` (root confinement and the allowed-roots list), `network` (offline mode), `redaction` (the secret-redaction rules), and `screenshot` (the headless browser). Thirteen individual keys inside otherwise-overridable sections are locked the same way. Five decide what gets indexed at all: `indexing.cross_project_symbols`, `indexing.skip_dirs`, `indexing.skip_files`, `indexing.large_file_skip_kb` and `indexing.large_file_symbol_only_kb`, which matter because an unindexed file answers `symbol`, `read` and `semantic` in the same words a name that never existed does. Five more are the switches that decide whether a large file arrives folded rather than whole: `hints.fold_code_bodies`, `hints.fold_comment_blocks`, `hints.fold_prose_paragraphs`, `hints.outline_large_documents` and `hints.skeleton_large_sources`. The last three are `image_shrink.max_image_pixels`, the decompression-bomb cap, `worker.blocked_roots`, the folders you have kept out of the index, and `compact_assist.summary_budget_chars`, the length target token-goat hands to whoever writes your compaction summary, which a repository must not be able to set low enough to erase the session. A project file that sets one of them is ignored, and token-goat prints a line naming what it dropped. Everything else stays project-overridable.
24
24
 
25
25
  **The lock covers the config file, not the environment.** These settings still read a `TOKEN_GOAT_*` environment variable, and a repository has ways to set one: a `.envrc` for direnv, a `terminal.integrated.env.*` block in a committed `.vscode/settings.json`, a `containerEnv` entry in a devcontainer. Refusing environment overrides would break the operator who exports a variable in their own shell, which is the legitimate case and the common one, so token-goat reports instead of refusing: `token-goat doctor` prints a `Security config overrides` line naming every locked security setting the environment is currently deciding, and the variable to unset. It covers all twenty locked keys that read an environment variable, and it derives that set from the same two tables that define what a project config may not write, rather than from a list kept alongside them. Settings with a safe side (booleans) are reported when the environment holds them open; settings without one (lists, sizes) are reported whenever the environment supplies a value at all, since there is nothing to compare against. A default install, where nothing is set, prints a single ok line.
26
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-goat",
3
- "version": "2.9.6",
3
+ "version": "2.9.9",
4
4
  "description": "Surgical token-reduction companion for Claude Code and other AI coding agents",
5
5
  "type": "module",
6
6
  "main": "./dist/token-goat.mjs",