token-goat 2.6.5 → 2.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -328,9 +328,9 @@ This writes `.pi/extensions/token-goat.ts` in the current project only. Remove i
328
328
  token-goat install --copilot
329
329
  ```
330
330
 
331
- The `--copilot` flag patches Claude Code and registers a Copilot CLI hook config: `~/.copilot/hooks/token-goat.json` (a `{ version, hooks }` file registering `preToolUse`, `postToolUse`, `preCompact`, `agentStop`, and `subagentStop`, per Copilot's own [hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference)) plus the shim script it points at, `~/.copilot/hooks/token-goat-shim.js`. Unlike Codex, Copilot's event names and response schema (`permissionDecision`/`modifiedArgs` for `preToolUse`, `modifiedResult`/`additionalContext` for `postToolUse`) genuinely differ from Claude Code's, so the shim translates rather than passes through.
331
+ The `--copilot` flag patches Claude Code and registers a Copilot CLI hook config: `~/.copilot/hooks/token-goat.json` (a `{ version, hooks }` file registering `preToolUse`, `postToolUse`, `preCompact`, `agentStop`, `subagentStop`, and `userPromptSubmitted`, per Copilot's own [hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference)) plus the shim script it points at, `~/.copilot/hooks/token-goat-shim.js`. Unlike Codex, Copilot's event names and response schema (`permissionDecision`/`modifiedArgs` for `preToolUse`, `modifiedResult`/`additionalContext` for `postToolUse`, `decision`/`reason` for `agentStop`/`subagentStop`) genuinely differ from Claude Code's, so the shim translates rather than passes through.
332
332
 
333
- What works: **bash output compression and re-read denial** (`preToolUse` returns `modifiedArgs` or `permissionDecision: "deny"`), **image shrinking and post-edit indexing** (`postToolUse` returns `additionalContext`), and the **compaction manifest** (`preCompact`). Copilot's built-in tool names (`shell`, `read`, `write`, `url`, `memory`, and MCP-server calls) are remapped onto token-goat's internal names where a clear match exists (`shell`→Bash, `read`→Read, `write`→Write, `url`→WebFetch); `memory` and MCP tool calls pass through unmapped and simply no-op.
333
+ What works: **bash output compression and re-read denial** (`preToolUse` returns `modifiedArgs` or `permissionDecision: "deny"`), **image shrinking and post-edit indexing** (`postToolUse` returns `additionalContext`), and **stop-hallucination logging** (`agentStop`/`subagentStop` map a token-goat `deny` onto `decision: "block"`, everything else onto `decision: "allow"`). `preCompact` and `userPromptSubmitted` are notification-only on real Copilot CLI, per its docs: Copilot never reads a response body for either, so token-goat's compaction manifest and prompt-context hints have no surfacing channel there. The shim still calls through for both so token-goat's internal side effects keep running, but nothing gets injected back into the agent. Copilot's built-in tool names (`shell`, `read`, `write`, `url`, `memory`, and MCP-server calls) are remapped onto token-goat's internal names where a clear match exists (`shell`→Bash, `read`→Read, `write`→Write, `url`→WebFetch); `memory` and MCP tool calls pass through unmapped and simply no-op.
334
334
 
335
335
  No ambient environment variable documents "this process is running under Copilot CLI" the way Codex/opencode set one, so the shim sets `TOKEN_GOAT_HARNESS_OVERRIDE=copilot_cli` itself before calling `token-goat hook` (same workaround `--pi` uses). To install for one project instead of user scope: `token-goat install --copilot --local` (writes `.github/hooks/token-goat.json` in the current project). To remove: `token-goat uninstall --copilot`.
336
336
 
@@ -137045,7 +137045,7 @@ init_define_import_meta_env();
137045
137045
  import { createRequire } from "node:module";
137046
137046
  function resolveVersion() {
137047
137047
  if (true) {
137048
- return "2.6.5";
137048
+ return "2.6.6";
137049
137049
  }
137050
137050
  const require2 = createRequire(import.meta.url);
137051
137051
  const pkg = require2("../package.json");
@@ -140157,29 +140157,85 @@ var COPILOT_CLI_HOOK_SCRIPT = `#!/usr/bin/env node
140157
140157
  const { spawnSync } = require('node:child_process')
140158
140158
 
140159
140159
  // Copilot event name -> token-goat internal HookEventName (src/types.ts's
140160
- // HOOK_EVENTS). Only these five have a token-goat handler; every other real
140161
- // Copilot event (sessionEnd, userPromptSubmitted, postToolUseFailure,
140162
- // subagentStart, errorOccurred, notification, permissionRequest) is left
140163
- // unimplemented rather than guessed at, and falls through to the default
140164
- // no-op below. 'sessionStart' is handled as a permanent no-op even though
140165
- // it's a real Copilot event, because token-goat has no internal session_start
140166
- // handler (mirrors PI_EXTENSION_SCRIPT's documented precedent).
140160
+ // HOOK_EVENTS). Only these six have a token-goat handler; every other real
140161
+ // Copilot event (sessionEnd, postToolUseFailure, subagentStart,
140162
+ // errorOccurred, notification, permissionRequest) is left unimplemented
140163
+ // rather than guessed at, and falls through to the default no-op below.
140164
+ // 'sessionStart' is handled as a permanent no-op even though it's a real
140165
+ // Copilot event, because token-goat has no internal session_start handler
140166
+ // (mirrors PI_EXTENSION_SCRIPT's documented precedent).
140167
140167
  const COPILOT_TO_TG_EVENT = {
140168
140168
  preToolUse: 'pre_tool_use',
140169
140169
  postToolUse: 'post_tool_use',
140170
140170
  preCompact: 'pre_compact',
140171
140171
  agentStop: 'stop',
140172
140172
  subagentStop: 'subagent_stop',
140173
+ userPromptSubmitted: 'user_prompt_submit',
140174
+ }
140175
+
140176
+ // Copilot built-in tool name -> token-goat internal tool name. Confirmed via
140177
+ // @github/copilot-sdk type definitions and multiple real GitHub issue payload
140178
+ // dumps -- supersedes an earlier docs-based guess (shell/read/write/url) that
140179
+ // didn't hold up in practice; 'write' in particular was never a real
140180
+ // toolName value, only a Copilot permission-pattern keyword. 'powershell'
140181
+ // maps to the same 'Bash' handler as 'bash' since both are shell-command
140182
+ // execution from token-goat's perspective (mirrors how hooks_bash.ts's own
140183
+ // filters already treat powershell-wrapped commands as part of the Bash
140184
+ // pipeline, not a separate tool). 'task', 'ask_user', 'memory', and
140185
+ // MCP-server tool invocations (<server-name>-<tool-name>) have no
140186
+ // token-goat equivalent and are passed through unmapped (safe no-op for
140187
+ // handlers that don't recognize the name).
140188
+ const TOOL_TO_TG = {
140189
+ bash: 'Bash',
140190
+ powershell: 'Bash',
140191
+ view: 'Read',
140192
+ create: 'Write',
140193
+ edit: 'Edit',
140194
+ web_fetch: 'WebFetch',
140195
+ grep: 'Grep',
140196
+ glob: 'Glob',
140197
+ }
140198
+
140199
+ // Confirmed via github/copilot-cli#3349 (open, unresolved as of writing): some
140200
+ // real Copilot CLI invocations send toolArgs as a JSON-*encoded string*
140201
+ // rather than a parsed object, contradicting the documented schema. Left
140202
+ // unhandled, canonical.tool_input would become a raw string and every
140203
+ // downstream event.toolInput[key] lookup in token-goat's handlers would
140204
+ // silently return undefined -- the deny/dedup mechanism would no-op with no
140205
+ // error surfaced. Parse it defensively; on a non-string, absent, or
140206
+ // unparsable value, fall back to {} rather than throwing (this shim's
140207
+ // convention throughout is fail-open, never crash on a malformed payload).
140208
+ function parseMaybeJsonObject(value) {
140209
+ if (value && typeof value === 'object') return value
140210
+ if (typeof value === 'string') {
140211
+ try {
140212
+ const parsed = JSON.parse(value)
140213
+ if (parsed && typeof parsed === 'object') return parsed
140214
+ } catch {
140215
+ // fall through to {}
140216
+ }
140217
+ }
140218
+ return {}
140173
140219
  }
140174
140220
 
140175
- // Copilot built-in tool name -> token-goat internal tool name. 'memory' and
140176
- // MCP-server tool invocations have no equivalent and are passed through
140177
- // unmapped (safe no-op for handlers that don't recognize the name).
140178
- const TOOL_TO_TG = {
140179
- shell: 'Bash',
140180
- read: 'Read',
140181
- write: 'Write',
140182
- url: 'WebFetch',
140221
+ // view/edit/create send the file path under 'path'; every token-goat handler these
140222
+ // three tools reach only ever looks for 'file_path' (getFilePath in hooks_common.ts).
140223
+ // Keyed by the ORIGINAL Copilot tool name (before TOOL_TO_TG renames it) since that's
140224
+ // the name toolArgs' shape is keyed to, not token-goat's internal tool name.
140225
+ const FILE_PATH_ARG_KEY = {
140226
+ view: 'path',
140227
+ edit: 'path',
140228
+ create: 'path',
140229
+ }
140230
+
140231
+ function remapToolInput(copilotToolName, input) {
140232
+ const pathKey = FILE_PATH_ARG_KEY[copilotToolName]
140233
+ if (pathKey === undefined || !input || typeof input !== 'object' || !(pathKey in input)) {
140234
+ return input
140235
+ }
140236
+ // Add file_path alongside the original key rather than renaming it, so nothing that
140237
+ // might read the original 'path' key elsewhere (e.g. a future handler) loses it.
140238
+ return Object.assign({}, input, { file_path: input[pathKey] })
140183
140239
  }
140184
140240
 
140185
140241
  function main() {
@@ -140219,7 +140275,25 @@ function main() {
140219
140275
  }
140220
140276
  if (toolName) {
140221
140277
  canonical.tool_name = TOOL_TO_TG[toolName] || toolName
140222
- canonical.tool_input = (payload && payload.toolArgs) || {}
140278
+ canonical.tool_input = remapToolInput(toolName, parseMaybeJsonObject(payload && payload.toolArgs))
140279
+ }
140280
+
140281
+ // postToolUse only: confirmed via https://docs.github.com/en/copilot/reference/hooks-reference
140282
+ // that Copilot's toolResult is an object ({resultType, textResultForLlm}), not a bare string
140283
+ // or array. Without this, token-goat's tool_response consumers (hooks_read.ts's
140284
+ // extractReadOutput, hooks_bash.ts's extractBashOutput, etc. -- all of which check for a
140285
+ // string or an object keyed by output/content/text/body) never see any content, so
140286
+ // post-read/post-bash stats stay empty no matter how many tool calls happen. Extract the
140287
+ // LLM-facing text directly rather than forwarding the raw object, since textResultForLlm
140288
+ // isn't one of those recognized object keys.
140289
+ const rawResult = payload && payload.toolResult
140290
+ if (rawResult && typeof rawResult === 'object') {
140291
+ const tr = rawResult
140292
+ // text_result_for_llm: Copilot's docs also describe a "VS Code compatible" snake_case
140293
+ // wire format (tool_result.text_result_for_llm) alongside the camelCase one above; try
140294
+ // both rather than assuming only the camelCase shape ever reaches this shim.
140295
+ const text = typeof tr.textResultForLlm === 'string' ? tr.textResultForLlm : tr.text_result_for_llm
140296
+ if (typeof text === 'string') canonical.tool_response = text
140223
140297
  }
140224
140298
 
140225
140299
  // A single command string (not an args array) with shell: true, exactly like
@@ -140273,13 +140347,27 @@ function translate(copilotEvent, resp) {
140273
140347
  return {}
140274
140348
  }
140275
140349
 
140276
- // preCompact / agentStop / subagentStop: Copilot's docs (as fetched) do not
140277
- // enumerate an output schema for these three events. This maps the one
140278
- // context channel token-goat produces for them (systemMessage, per
140279
- // src/hook_registry.ts's serializeOutput) onto additionalContext as a
140280
- // best-effort guess -- unconfirmed, verify against Copilot's real behavior.
140281
- const context = extractContext(resp)
140282
- if (context) return { additionalContext: context }
140350
+ if (copilotEvent === 'agentStop' || copilotEvent === 'subagentStop') {
140351
+ // Confirmed against the hooks reference doc: the only accepted response
140352
+ // shape for these two events is {decision, reason} -- additionalContext
140353
+ // is not part of their schema and Copilot silently ignores it there.
140354
+ // token-goat's internal 'stop'/'subagent_stop' handlers never return a
140355
+ // real deny today (subagentStopHandler only ever logs and passes), but a
140356
+ // future deny is mapped through here rather than silently dropped.
140357
+ if (resp && resp.decision === 'block') {
140358
+ const reason = (resp && resp.reason) || 'blocked by token-goat'
140359
+ return { decision: 'block', reason: reason }
140360
+ }
140361
+ return { decision: 'allow' }
140362
+ }
140363
+
140364
+ // preCompact / userPromptSubmitted: confirmed against the hooks reference
140365
+ // doc that both are notification-only -- Copilot never reads a response
140366
+ // body for either, so any additionalContext/systemMessage token-goat
140367
+ // produces has no surfacing channel here. This still routes through the
140368
+ // token-goat hook call above (unlike sessionStart's early no-op) so the
140369
+ // internal handler's own side effects keep running; only the response is
140370
+ // discarded.
140283
140371
  return {}
140284
140372
  }
140285
140373
 
@@ -140290,7 +140378,26 @@ function extractContext(resp) {
140290
140378
  return undefined
140291
140379
  }
140292
140380
 
140293
- main()
140381
+ // Hard outer safety net, on top of main()'s own per-step try/catch fallbacks
140382
+ // (JSON.parse, readFileSync, spawnSync): a hook error must never itself cause
140383
+ // Copilot's fail-closed "(hook errored)" behavior, which denies EVERY tool
140384
+ // call unconditionally (the exact live-production failure mode behind
140385
+ // github/copilot-cli#4001). Any uncaught exception anywhere in this script --
140386
+ // including one a future code path adds that the existing per-step guards
140387
+ // don't anticipate -- still guarantees stdout gets valid JSON and the
140388
+ // process exits 0. process.exitCode is set explicitly and unconditionally at
140389
+ // the very end of every path so nothing upstream (e.g. an unhandled-rejection
140390
+ // warning in some Node versions nudging exit-code inference) can flip it.
140391
+ try {
140392
+ main()
140393
+ } catch {
140394
+ try {
140395
+ process.stdout.write('{}')
140396
+ } catch {
140397
+ // stdout itself is broken; nothing more can be done here.
140398
+ }
140399
+ }
140400
+ process.exitCode = 0
140294
140401
  `;
140295
140402
 
140296
140403
  // src/bridges/copilot_cli_install.ts
@@ -140298,7 +140405,14 @@ init_define_import_meta_env();
140298
140405
  import * as fs6 from "node:fs";
140299
140406
  import * as os4 from "node:os";
140300
140407
  import * as path9 from "node:path";
140301
- var COPILOT_CLI_HOOK_EVENTS = ["preToolUse", "postToolUse", "preCompact", "agentStop", "subagentStop"];
140408
+ var COPILOT_CLI_HOOK_EVENTS = [
140409
+ "preToolUse",
140410
+ "postToolUse",
140411
+ "preCompact",
140412
+ "agentStop",
140413
+ "subagentStop",
140414
+ "userPromptSubmitted"
140415
+ ];
140302
140416
  function copilotCliUserHooksDir() {
140303
140417
  return path9.join(os4.homedir(), ".copilot", "hooks");
140304
140418
  }
@@ -140315,7 +140429,7 @@ function copilotCliScriptPath(opts = {}) {
140315
140429
  return path9.join(copilotCliHooksDir(opts), "token-goat-shim.js");
140316
140430
  }
140317
140431
  function hookCommandFor2(scriptPath, event) {
140318
- return `node "${scriptPath}" ${event}`;
140432
+ return `"${process.execPath}" "${scriptPath}" ${event}`;
140319
140433
  }
140320
140434
  function buildConfig(scriptPath) {
140321
140435
  const hooks = {};
@@ -140324,7 +140438,7 @@ function buildConfig(scriptPath) {
140324
140438
  }
140325
140439
  return { version: 1, hooks };
140326
140440
  }
140327
- function writeIfDifferent(p, content) {
140441
+ function writeIfDifferent(p, content, backup = false) {
140328
140442
  let existing;
140329
140443
  try {
140330
140444
  existing = fs6.readFileSync(p, "utf8");
@@ -140332,6 +140446,7 @@ function writeIfDifferent(p, content) {
140332
140446
  existing = void 0;
140333
140447
  }
140334
140448
  if (existing === content) return false;
140449
+ if (backup) backupFile(p);
140335
140450
  ensureDirSync(path9.dirname(p));
140336
140451
  atomicWriteText(p, content);
140337
140452
  return true;
@@ -140341,7 +140456,7 @@ function installCopilotCli(opts = {}) {
140341
140456
  const scriptPath = copilotCliScriptPath(opts);
140342
140457
  const scriptChanged = writeIfDifferent(scriptPath, COPILOT_CLI_HOOK_SCRIPT);
140343
140458
  const desiredText = JSON.stringify(buildConfig(scriptPath), null, 2) + "\n";
140344
- const configChanged = writeIfDifferent(configPath2, desiredText);
140459
+ const configChanged = writeIfDifferent(configPath2, desiredText, true);
140345
140460
  return { configPath: configPath2, scriptPath, alreadyInstalled: !scriptChanged && !configChanged };
140346
140461
  }
140347
140462
  function uninstallCopilotCli(opts = {}) {
@@ -141122,11 +141237,25 @@ function pressureRawTotal(cache) {
141122
141237
  }
141123
141238
  function getEffectiveAutoTriggerWindow() {
141124
141239
  const ca = loadConfig().compact_assist;
141240
+ const isConfigDefault = !isAutoTriggerMultiplierExplicit();
141125
141241
  const multiplier = getAutoTriggerMultiplier(
141126
- ca.harness === "auto" ? { configExplicitMultiplier: ca.auto_trigger_multiplier } : { configExplicitMultiplier: ca.auto_trigger_multiplier, harness: ca.harness }
141242
+ ca.harness === "auto" ? { configExplicitMultiplier: ca.auto_trigger_multiplier, isConfigDefault } : { configExplicitMultiplier: ca.auto_trigger_multiplier, harness: ca.harness, isConfigDefault }
141127
141243
  );
141128
141244
  return CONTEXT_AUTOCOMPACT_TOKENS * multiplier;
141129
141245
  }
141246
+ function isAutoTriggerMultiplierExplicit() {
141247
+ try {
141248
+ const text = fs10.readFileSync(configPath(), "utf8");
141249
+ const raw = parse(text);
141250
+ const ca_raw = raw["compact_assist"];
141251
+ if (ca_raw === null || typeof ca_raw !== "object" || Array.isArray(ca_raw)) {
141252
+ return false;
141253
+ }
141254
+ return ca_raw["auto_trigger_multiplier"] !== void 0;
141255
+ } catch {
141256
+ return false;
141257
+ }
141258
+ }
141130
141259
  function getContextPressure(cache) {
141131
141260
  try {
141132
141261
  if (!cache) {
@@ -142122,8 +142251,15 @@ function stripCstyleComments(text, lineCommentRe) {
142122
142251
  break;
142123
142252
  }
142124
142253
  result += line.slice(j, open);
142125
- inComment = true;
142126
- j = open;
142254
+ const close = line.indexOf("*/", open + 2);
142255
+ if (close === -1) {
142256
+ result += " ".repeat(line.length - open);
142257
+ inComment = true;
142258
+ break;
142259
+ }
142260
+ result += " ".repeat(close + 2 - open);
142261
+ j = close + 2;
142262
+ inComment = false;
142127
142263
  } else {
142128
142264
  const close = line.indexOf("*/", j);
142129
142265
  if (close === -1) {
@@ -142196,12 +142332,16 @@ function stripBlockCommentSpan(line, inComment) {
142196
142332
  }
142197
142333
  return { code, inComment: comment };
142198
142334
  }
142199
- function stripLineComment(line) {
142200
- let idx = line.indexOf("//");
142201
- while (idx !== -1 && isInsideStringLiteral(line, idx)) {
142202
- idx = line.indexOf("//", idx + 1);
142335
+ function stripLineComment(line, markers = ["//"]) {
142336
+ let cutIdx = -1;
142337
+ for (const marker of markers) {
142338
+ let idx = line.indexOf(marker);
142339
+ while (idx !== -1 && isInsideStringLiteral(line, idx)) {
142340
+ idx = line.indexOf(marker, idx + 1);
142341
+ }
142342
+ if (idx !== -1 && (cutIdx === -1 || idx < cutIdx)) cutIdx = idx;
142203
142343
  }
142204
- return idx === -1 ? line : line.slice(0, idx);
142344
+ return cutIdx === -1 ? line : line.slice(0, cutIdx);
142205
142345
  }
142206
142346
  function stripStringLiterals(line) {
142207
142347
  let out2 = "";
@@ -142367,7 +142507,7 @@ function extractCsharp(content, filePath) {
142367
142507
  }
142368
142508
  }
142369
142509
  }
142370
- const braceLine = stripStringLiterals(line);
142510
+ const braceLine = stripStringLiterals(stripLineComment(line));
142371
142511
  const openBraces = (braceLine.match(/\{/g) ?? []).length;
142372
142512
  const closeBraces = (braceLine.match(/\}/g) ?? []).length;
142373
142513
  braceDepth += openBraces - closeBraces;
@@ -142435,7 +142575,7 @@ function extractPhp(content, filePath) {
142435
142575
  const line = codeLine.trimEnd();
142436
142576
  const stripped = line.trimStart();
142437
142577
  if (!stripped || stripped.startsWith("//") || stripped.startsWith("#")) continue;
142438
- const braceLine = stripStringLiterals(line);
142578
+ const braceLine = stripStringLiterals(stripLineComment(line, ["//", "#"]));
142439
142579
  const openB = (braceLine.match(/\{/g) ?? []).length;
142440
142580
  const closeB = (braceLine.match(/\}/g) ?? []).length;
142441
142581
  braceDepth += openB - closeB;
@@ -143057,16 +143197,14 @@ function extractMakefile(content, filePath) {
143057
143197
  init_define_import_meta_env();
143058
143198
  var MAX_SYMBOLS4 = 500;
143059
143199
  var MAX_HEADING_LEN5 = 120;
143060
- var LINE_COMMENT_RE = /\/\/[^\n]*/g;
143061
143200
  function stripComments2(text) {
143062
- let out2 = stripCstyleComments(text);
143063
- out2 = out2.replace(LINE_COMMENT_RE, (m) => " ".repeat(m.length));
143064
- return out2;
143201
+ const out2 = stripCstyleComments(text);
143202
+ return out2.split("\n").map((line) => stripLineComment(line)).join("\n");
143065
143203
  }
143066
143204
  var TOP_LEVEL_RE = /^[ \t]*(?<keyword>message|enum|service)\s+(?<name>[A-Za-z_][A-Za-z0-9_]*)\s*\{/gm;
143067
143205
  var EXTEND_RE = /^[ \t]*extend\s+(?<name>[A-Za-z_][A-Za-z0-9_.]*)\s*\{/gm;
143068
- var RPC_RE = /^\s+rpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/gm;
143069
- var ONEOF_RE = /^\s+oneof\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/gm;
143206
+ var RPC_RE = /^[ \t]+rpc\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(/gm;
143207
+ var ONEOF_RE = /^[ \t]+oneof\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/gm;
143070
143208
  var IMPORT_RE2 = /^import\s+(?:weak\s+|public\s+)?["']([^"']+)["']/gm;
143071
143209
  var KIND_MAP2 = /* @__PURE__ */ new Map([
143072
143210
  ["message", "proto_message"],
@@ -143206,18 +143344,25 @@ function extractPowershell(content, filePath) {
143206
143344
  const rawLine = lines[i] ?? "";
143207
143345
  const lineNum = i + 1;
143208
143346
  let line = rawLine;
143347
+ let openedBlockCommentThisLine = false;
143209
143348
  if (!inBlockComment) {
143210
143349
  const openIdx = findUnquoted(rawLine, "<#");
143211
143350
  if (openIdx !== -1) {
143212
- const closeIdx = rawLine.indexOf("#>", openIdx + 2);
143213
- if (closeIdx !== -1) {
143214
- line = rawLine.slice(0, openIdx) + " ".repeat(closeIdx + 2 - openIdx) + rawLine.slice(closeIdx + 2);
143215
- } else {
143216
- inBlockComment = true;
143351
+ const hashIdx2 = findUnquoted(rawLine, "#");
143352
+ const isRealOpener = hashIdx2 === -1 || hashIdx2 >= openIdx;
143353
+ if (isRealOpener) {
143354
+ const closeIdx = rawLine.indexOf("#>", openIdx + 2);
143355
+ if (closeIdx !== -1) {
143356
+ line = rawLine.slice(0, openIdx) + " ".repeat(closeIdx + 2 - openIdx) + rawLine.slice(closeIdx + 2);
143357
+ } else {
143358
+ inBlockComment = true;
143359
+ openedBlockCommentThisLine = true;
143360
+ line = rawLine.slice(0, openIdx);
143361
+ }
143217
143362
  }
143218
143363
  }
143219
143364
  }
143220
- if (inBlockComment) {
143365
+ if (inBlockComment && !openedBlockCommentThisLine) {
143221
143366
  const closeMarkerIdx = rawLine.indexOf("#>");
143222
143367
  if (closeMarkerIdx === -1) {
143223
143368
  continue;
@@ -143332,7 +143477,7 @@ function loadGrammar(lang, filePath) {
143332
143477
  } catch {
143333
143478
  grammar = null;
143334
143479
  }
143335
- _grammarCache.set(lang, grammar);
143480
+ _grammarCache.set(cacheKey, grammar);
143336
143481
  return grammar;
143337
143482
  }
143338
143483
  function isTreeSitterAvailable(lang) {
@@ -143907,7 +144052,11 @@ function extractJsonSymbols(content, filePath) {
143907
144052
  } else {
143908
144053
  inString = false;
143909
144054
  let k = i + 1;
143910
- while (k < content.length && /\s/.test(content[k] ?? "")) k++;
144055
+ let keyToColonNewlines = 0;
144056
+ while (k < content.length && /\s/.test(content[k] ?? "")) {
144057
+ if (content[k] === "\n") keyToColonNewlines++;
144058
+ k++;
144059
+ }
143911
144060
  if (content[k] === ":" && depthWhenStringOpened === 1) {
143912
144061
  let v = k + 1;
143913
144062
  let gapNewlines = 0;
@@ -143918,7 +144067,7 @@ function extractJsonSymbols(content, filePath) {
143918
144067
  let lineEnd = strStartLine;
143919
144068
  let body = (lines[strStartLine - 1] ?? "").trim();
143920
144069
  if (content[v] === '"') {
143921
- let valueLine = line + gapNewlines;
144070
+ let valueLine = line + keyToColonNewlines + gapNewlines;
143922
144071
  let valueEscaping = false;
143923
144072
  for (let j = v + 1; j < content.length; j++) {
143924
144073
  const vch = content[j];
@@ -155899,6 +156048,7 @@ var GoTestFilter = class extends ToolFilter {
155899
156048
  continue;
155900
156049
  }
155901
156050
  if (TEST_RPC_RE.test(line)) {
156051
+ if (inFailBlock) inFailBlock = false;
155902
156052
  lastRunLine = line;
155903
156053
  droppedRun += 1;
155904
156054
  continue;
@@ -162348,12 +162498,12 @@ var CdkFilter = class extends ToolFilter {
162348
162498
  }
162349
162499
  };
162350
162500
  var cdkFilter = new CdkFilter();
162351
- var _VAULT_TABLE_DIVIDER_RE = /^\s*-{3,}\s+-{3,}\s*$/;
162501
+ var _VAULT_TABLE_DIVIDER_RE = /^\s*-{3,}(?:\s+-{3,})?\s*$/;
162352
162502
  var _VAULT_LEASE_META_RE = /^\s*(?:lease_(?:id|renewable|duration|accessor)|token_(?:policies|accessor|type|ttl|issue_time|expire_time|explicit_max_ttl|num_uses|renewable)|renewable|request_id)\s/i;
162353
162503
  var _VAULT_SUCCESS_RE = /^\s*Success!\s+/i;
162354
162504
  var _VAULT_HEADER_RE = /^\s*(?:WARNING|==>|Key\s+Value\s*$)/i;
162355
162505
  var _VAULT_AUTH_HEADER_RE = /^\s*(?:Token\s+information:|The\s+token\s+information|Complete\s+the\s+following|vault\s+(?:kv|secrets|auth|policy|lease|token)\s)/i;
162356
- var _VAULT_LIST_ITEM_RE = /^\s{1,6}[a-zA-Z0-9_./-]+\/?$/;
162506
+ var _VAULT_LIST_ITEM_RE = /^\s{0,6}[a-zA-Z0-9_./-]+\/?$/;
162357
162507
  var _VAULT_LIST_HEADER_RE = /^\s*Keys\s*$/i;
162358
162508
  var _VAULT_LIST_COLLAPSE_THRESHOLD = 10;
162359
162509
  var VaultFilter = class extends ToolFilter {
@@ -162366,7 +162516,7 @@ var VaultFilter = class extends ToolFilter {
162366
162516
  const kept = [];
162367
162517
  let metaCount = 0;
162368
162518
  let dividerCount = 0;
162369
- const isListCmd = argv.length >= 2 && argv[0].toLowerCase() === "vault" && (argv[1].toLowerCase() === "list" || argv.length >= 3 && argv[1].toLowerCase() === "kv" && argv[2].toLowerCase() === "list");
162519
+ const isListCmd = argv.length >= 2 && pathStem(argv[0]).toLowerCase() === "vault" && (argv[1].toLowerCase() === "list" || argv.length >= 3 && argv[1].toLowerCase() === "kv" && argv[2].toLowerCase() === "list");
162370
162520
  const listItems = [];
162371
162521
  let inListBody = false;
162372
162522
  for (const line of lines) {
@@ -166492,6 +166642,9 @@ var PSQL_NOTICE_RE = /^(NOTICE|WARNING|HINT|DETAIL):/i;
166492
166642
  var PSQL_ERROR_RE = /^(ERROR|FATAL|PANIC):/i;
166493
166643
  var PSQL_ROWS_RE = /^\((\d+) rows?\)$/;
166494
166644
  var PSQL_CREATE_RE = /^(CREATE TABLE|CREATE INDEX|CREATE UNIQUE INDEX|CREATE SEQUENCE|CREATE TYPE|CREATE FUNCTION|CREATE VIEW|CREATE TRIGGER|ALTER TABLE|ADD CONSTRAINT)\b/i;
166645
+ function pluralize(n, singular, plural2 = `${singular}s`) {
166646
+ return `${n} ${n === 1 ? singular : plural2}`;
166647
+ }
166495
166648
  var PsqlFilter = class _PsqlFilter extends ToolFilter {
166496
166649
  name = "psql";
166497
166650
  binaries = /* @__PURE__ */ new Set(["psql"]);
@@ -166505,14 +166658,26 @@ var PsqlFilter = class _PsqlFilter extends ToolFilter {
166505
166658
  const lines = text.split("\n");
166506
166659
  const createTables = lines.filter((ln) => /^CREATE TABLE\b/i.test(ln)).length;
166507
166660
  const createIndexes = lines.filter((ln) => /^CREATE (UNIQUE )?INDEX\b/i.test(ln)).length;
166661
+ const createFunctions = lines.filter((ln) => /^CREATE FUNCTION\b/i.test(ln)).length;
166662
+ const createViews = lines.filter((ln) => /^CREATE VIEW\b/i.test(ln)).length;
166663
+ const createTypes = lines.filter((ln) => /^CREATE TYPE\b/i.test(ln)).length;
166664
+ const createSequences = lines.filter((ln) => /^CREATE SEQUENCE\b/i.test(ln)).length;
166665
+ const createTriggers = lines.filter((ln) => /^CREATE TRIGGER\b/i.test(ln)).length;
166666
+ const alterations = lines.filter((ln) => /^(ALTER TABLE|ADD CONSTRAINT)\b/i.test(ln)).length;
166508
166667
  if (createTables >= 3) {
166509
166668
  const nonDdl = [];
166510
166669
  for (const ln of lines) {
166511
166670
  if (PSQL_CREATE_RE.test(ln)) continue;
166512
166671
  nonDdl.push(ln);
166513
166672
  }
166514
- const summaryParts = [`${createTables} tables`];
166515
- if (createIndexes) summaryParts.push(`${createIndexes} indexes`);
166673
+ const summaryParts = [pluralize(createTables, "table")];
166674
+ if (createIndexes) summaryParts.push(pluralize(createIndexes, "index", "indexes"));
166675
+ if (createFunctions) summaryParts.push(pluralize(createFunctions, "function"));
166676
+ if (createViews) summaryParts.push(pluralize(createViews, "view"));
166677
+ if (createTypes) summaryParts.push(pluralize(createTypes, "type"));
166678
+ if (createSequences) summaryParts.push(pluralize(createSequences, "sequence"));
166679
+ if (createTriggers) summaryParts.push(pluralize(createTriggers, "trigger"));
166680
+ if (alterations) summaryParts.push(pluralize(alterations, "alteration"));
166516
166681
  nonDdl.unshift(`[token-goat: Created ${summaryParts.join(", ")}]`);
166517
166682
  return this.finalize(nonDdl);
166518
166683
  }
@@ -167446,6 +167611,14 @@ var EnvFilter = class extends ToolFilter {
167446
167611
  };
167447
167612
  var envFilter = new EnvFilter();
167448
167613
  var JSON_ARRAY_MAX_ITEMS = 50;
167614
+ function stableStringify(value) {
167615
+ if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
167616
+ if (value !== null && typeof value === "object") {
167617
+ const keys = Object.keys(value).sort();
167618
+ return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
167619
+ }
167620
+ return JSON.stringify(value);
167621
+ }
167449
167622
  var JsonArrayFilter = class extends ToolFilter {
167450
167623
  name = "json_array";
167451
167624
  binaries = /* @__PURE__ */ new Set(["json"]);
@@ -167473,14 +167646,15 @@ var JsonArrayFilter = class extends ToolFilter {
167473
167646
  const dupCounts = /* @__PURE__ */ new Map();
167474
167647
  for (const item of data) {
167475
167648
  if (item !== null && typeof item === "object" && !Array.isArray(item)) {
167649
+ const valueKey = stableStringify(item);
167476
167650
  const ks = Object.keys(item).sort().join(",");
167477
167651
  const preserve = Object.values(item).some(
167478
167652
  (v) => typeof v === "string" && hasHighEntropyToken(v)
167479
167653
  );
167480
- if (seen.has(ks) && !preserve) {
167654
+ if (seen.has(valueKey) && !preserve) {
167481
167655
  dupCounts.set(ks, (dupCounts.get(ks) ?? 0) + 1);
167482
167656
  } else {
167483
- if (!seen.has(ks)) seen.set(ks, kept.length);
167657
+ if (!seen.has(valueKey)) seen.set(valueKey, kept.length);
167484
167658
  kept.push(item);
167485
167659
  }
167486
167660
  } else {
@@ -169473,11 +169647,24 @@ export default function (pi: ExtensionAPI) {
169473
169647
  pi.on("tool_result", async (event, _ctx) => {
169474
169648
  const tg = TOOL_TO_TG[event.toolName];
169475
169649
  if (!tg) return;
169650
+ // event.content is tool_result's real output field (ToolResultEventBase.content:
169651
+ // (TextContent | ImageContent)[] -- verified against pi's own
169652
+ // core/extensions/types.ts). Join the text blocks into a single string so
169653
+ // extractReadOutput's truncation-marker detection (which only recognizes a
169654
+ // string tool_response.output) can see it, mirroring opencode.ts's
169655
+ // tool_response: { output } shape.
169656
+ const contentBlocks = Array.isArray(event.content) ? event.content : [];
169657
+ const output = contentBlocks
169658
+ .filter((c: { type?: string }) => c && c.type === "text")
169659
+ .map((c: { text?: string }) => c.text ?? "")
169660
+ .join("
169661
+ ");
169476
169662
  callHook("post_tool_use", {
169477
169663
  session_id: sessionId,
169478
169664
  tool_name: tg,
169479
169665
  tool_input: toToolInput(event.toolName, (event.input ?? {}) as Record<string, unknown>),
169480
169666
  cwd,
169667
+ tool_response: { output },
169481
169668
  });
169482
169669
  });
169483
169670
 
@@ -194429,18 +194616,7 @@ function listSections(filePath) {
194429
194616
  return headers.map((h) => h.heading);
194430
194617
  }
194431
194618
  function listAllSections(filePath) {
194432
- let text;
194433
- try {
194434
- text = readFileSync22(filePath, "utf-8");
194435
- } catch {
194436
- return [];
194437
- }
194438
- if (text.charCodeAt(0) === 65279) {
194439
- text = text.slice(1);
194440
- }
194441
- const language = detectLanguage(filePath);
194442
- const { headers } = findHeaders(text, language);
194443
- return headers.map((h) => h.heading);
194619
+ return listSections(filePath);
194444
194620
  }
194445
194621
 
194446
194622
  // src/overflow_guard.ts
@@ -197188,13 +197364,19 @@ function runLineRange(range2, opts) {
197188
197364
  function resolveSymbolSpec(spec, forceRefresh) {
197189
197365
  const { file: file2, symbol: symbol2 } = parseReadSpec(spec);
197190
197366
  if (symbol2 === void 0 || symbol2 === "") return null;
197191
- const dotParts = symbol2.split(".");
197192
- const [symBase, methodName] = dotParts.length > 1 ? [dotParts[0] ?? symbol2, dotParts[dotParts.length - 1]] : [symbol2, void 0];
197193
- const lookupName = methodName ?? symBase;
197194
197367
  const resolved = resolveIndexPath(file2);
197195
197368
  if (forceRefresh === true) {
197196
197369
  indexFileSync(resolved, globalDbPath());
197197
197370
  }
197371
+ if (symbol2.includes(".")) {
197372
+ const exactMatch = querySymbols({ name: symbol2, filePath: resolved, limit: 10 });
197373
+ if (exactMatch.length > 0) {
197374
+ return exactMatch[0] ?? null;
197375
+ }
197376
+ }
197377
+ const dotParts = symbol2.split(".");
197378
+ const [symBase, methodName] = dotParts.length > 1 ? [dotParts[0] ?? symbol2, dotParts[dotParts.length - 1]] : [symbol2, void 0];
197379
+ const lookupName = methodName ?? symBase;
197198
197380
  let candidates = querySymbols({ name: lookupName, filePath: resolved, limit: 10 });
197199
197381
  if (candidates.length === 0) {
197200
197382
  const foldedFile = foldPath(file2);
@@ -203746,7 +203928,7 @@ async function cmdInstall(opts) {
203746
203928
  }
203747
203929
  }
203748
203930
  if (opts.copilot === true) {
203749
- const copilotResult = installCopilotCli();
203931
+ const copilotResult = installCopilotCli({ local: opts.local === true });
203750
203932
  if (copilotResult.alreadyInstalled) {
203751
203933
  out(`Copilot CLI integration already installed \u2192 ${copilotResult.configPath}`);
203752
203934
  } else {
@@ -203827,7 +204009,7 @@ function cmdUninstall(opts) {
203827
204009
  );
203828
204010
  }
203829
204011
  if (opts.copilot === true) {
203830
- const copilotRemoved = uninstallCopilotCli();
204012
+ const copilotRemoved = uninstallCopilotCli({ local: opts.local === true });
203831
204013
  out(
203832
204014
  copilotRemoved ? "Removed token-goat Copilot CLI integration." : "No token-goat Copilot CLI integration to remove."
203833
204015
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-goat",
3
- "version": "2.6.5",
3
+ "version": "2.6.6",
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",