token-goat 2.9.30 → 2.9.31
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 +24 -7
- package/SECURITY.md +10 -2
- package/THIRD_PARTY_NOTICES.md +1 -1
- package/dist/native/linux-arm64/tg-hook +0 -0
- package/dist/native/linux-x64/tg-hook +0 -0
- package/dist/native/win32-arm64/tg-hook.exe +0 -0
- package/dist/native/win32-x64/tg-hook.exe +0 -0
- package/dist/{token-goat-chunk-QSBRTGM2.mjs → token-goat-chunk-25TRAWET.mjs} +17 -7
- package/dist/token-goat-chunk-2KXO6V2B.mjs +203 -0
- package/dist/{token-goat-chunk-IVKIZWAB.mjs → token-goat-chunk-2M7E4PF7.mjs} +41 -12
- package/dist/{token-goat-chunk-SMVRN2HU.mjs → token-goat-chunk-2UPZGKKY.mjs} +2 -2
- package/dist/token-goat-chunk-2WLPWKHV.mjs +135 -0
- package/dist/token-goat-chunk-2XBMMDBD.mjs +126 -0
- package/dist/token-goat-chunk-32ZOY4N5.mjs +23 -0
- package/dist/token-goat-chunk-3EZRNKTK.mjs +188 -0
- package/dist/token-goat-chunk-4ADZHI7J.mjs +63 -0
- package/dist/token-goat-chunk-4GBVPH3D.mjs +242 -0
- package/dist/{token-goat-chunk-KWPNJBDH.mjs → token-goat-chunk-4GILLLWY.mjs} +7 -4
- package/dist/{token-goat-chunk-67TQJ76B.mjs → token-goat-chunk-4NAPX7BA.mjs} +36 -3
- package/dist/{token-goat-chunk-PU7V3FD6.mjs → token-goat-chunk-4SC7UJYV.mjs} +3354 -2553
- package/dist/token-goat-chunk-4SMRO4TO.mjs +366 -0
- package/dist/{token-goat-chunk-AFLYLHDH.mjs → token-goat-chunk-4ZBFWLXF.mjs} +283 -427
- package/dist/{token-goat-chunk-JDWCRD6M.mjs → token-goat-chunk-57BIAXJL.mjs} +73 -40
- package/dist/token-goat-chunk-5AC66HNP.mjs +115 -0
- package/dist/token-goat-chunk-5IMLKSOP.mjs +145 -0
- package/dist/token-goat-chunk-646E6U44.mjs +398 -0
- package/dist/token-goat-chunk-65N4RHP4.mjs +33 -0
- package/dist/{token-goat-chunk-JN4XMLSD.mjs → token-goat-chunk-6DG6HWP3.mjs} +8 -2
- package/dist/token-goat-chunk-6ENQSNRZ.mjs +573 -0
- package/dist/token-goat-chunk-6ZY4GNF2.mjs +91 -0
- package/dist/{token-goat-chunk-VPBBTSJR.mjs → token-goat-chunk-76FGDLRO.mjs} +262 -255
- package/dist/{token-goat-chunk-7IN2MKDS.mjs → token-goat-chunk-7EZTZOMC.mjs} +121 -289
- package/dist/{token-goat-chunk-OJBNYTJP.mjs → token-goat-chunk-7MAOA6X4.mjs} +640 -80
- package/dist/token-goat-chunk-7R2ZVVAX.mjs +218 -0
- package/dist/{token-goat-chunk-PD5VKH46.mjs → token-goat-chunk-AUNGYYQX.mjs} +2 -2
- package/dist/{token-goat-chunk-HG45AF2B.mjs → token-goat-chunk-AUQYRMMH.mjs} +794 -37
- package/dist/token-goat-chunk-B3PEKBYG.mjs +128 -0
- package/dist/{token-goat-chunk-A3CK6N5Z.mjs → token-goat-chunk-BA4HI6BW.mjs} +1309 -110
- package/dist/{token-goat-chunk-JB6QN6VF.mjs → token-goat-chunk-BFCWY73P.mjs} +59 -23
- package/dist/{token-goat-chunk-B45VMIIH.mjs → token-goat-chunk-BWGFUZZE.mjs} +7 -5
- package/dist/{token-goat-chunk-P4QMM4WK.mjs → token-goat-chunk-C75NA7PL.mjs} +50 -23
- package/dist/token-goat-chunk-CBIQ7WTD.mjs +87 -0
- package/dist/token-goat-chunk-CETM74KC.mjs +307 -0
- package/dist/{token-goat-chunk-H2PHHGM5.mjs → token-goat-chunk-CIMQ2BB3.mjs} +57 -12
- package/dist/{token-goat-chunk-N2NMER56.mjs → token-goat-chunk-CJLS3CDL.mjs} +46 -4
- package/dist/{token-goat-chunk-VES7EL7O.mjs → token-goat-chunk-CO3UYX6G.mjs} +1 -1
- package/dist/token-goat-chunk-COPNXRJM.mjs +154 -0
- package/dist/token-goat-chunk-CSJ5L3NL.mjs +479 -0
- package/dist/{token-goat-chunk-4LCCNRTG.mjs → token-goat-chunk-CVCEVUGK.mjs} +8 -6
- package/dist/{token-goat-chunk-B2RZZCSS.mjs → token-goat-chunk-CXHOFN7R.mjs} +37 -20
- package/dist/token-goat-chunk-D735C4QM.mjs +603 -0
- package/dist/{token-goat-chunk-APGZXSPS.mjs → token-goat-chunk-DGS2LVNA.mjs} +47 -30
- package/dist/{token-goat-chunk-54K3EUV3.mjs → token-goat-chunk-DOWRSMWO.mjs} +1 -1
- package/dist/token-goat-chunk-E45INMY3.mjs +71 -0
- package/dist/token-goat-chunk-EC5F37F4.mjs +244 -0
- package/dist/{token-goat-chunk-XTQAOTSO.mjs → token-goat-chunk-EDBDRVH5.mjs} +4 -1
- package/dist/{token-goat-chunk-5BCIJXMO.mjs → token-goat-chunk-EELTBKDM.mjs} +14 -10
- package/dist/{token-goat-chunk-W3QCJS6G.mjs → token-goat-chunk-F4HYL64R.mjs} +8 -6
- package/dist/token-goat-chunk-F6PAUIEK.mjs +624 -0
- package/dist/token-goat-chunk-FDSREWLX.mjs +692 -0
- package/dist/token-goat-chunk-GGGG7RWB.mjs +236 -0
- package/dist/{token-goat-chunk-C63JQV5K.mjs → token-goat-chunk-GL3ETKSC.mjs} +233 -15
- package/dist/token-goat-chunk-GVB2MUST.mjs +1298 -0
- package/dist/token-goat-chunk-GWFLAUMQ.mjs +307 -0
- package/dist/{token-goat-chunk-ZEJVMUQH.mjs → token-goat-chunk-HLPW63A7.mjs} +2 -2
- package/dist/{token-goat-chunk-GOLEOXZP.mjs → token-goat-chunk-HU4TORC5.mjs} +44 -26
- package/dist/{token-goat-chunk-Z57YODA2.mjs → token-goat-chunk-I7VKZZAW.mjs} +2 -2
- package/dist/{token-goat-chunk-NYKZLXMK.mjs → token-goat-chunk-IA6XSPE3.mjs} +6 -6
- package/dist/{token-goat-chunk-E3OQX2XT.mjs → token-goat-chunk-IAGXMHIK.mjs} +81 -42
- package/dist/{token-goat-chunk-HYBFA5X3.mjs → token-goat-chunk-IK2DRMFX.mjs} +24 -68
- package/dist/{token-goat-chunk-COBQGL5S.mjs → token-goat-chunk-INNPTRUX.mjs} +249 -299
- package/dist/{token-goat-chunk-7NADEDKJ.mjs → token-goat-chunk-JJ5BX4F4.mjs} +6 -1
- package/dist/{token-goat-chunk-PEPTHRFF.mjs → token-goat-chunk-JVFSHVFJ.mjs} +6 -6
- package/dist/token-goat-chunk-KGEFFSHD.mjs +50 -0
- package/dist/token-goat-chunk-KQBLRPBN.mjs +433 -0
- package/dist/{token-goat-chunk-Q5VTGRH7.mjs → token-goat-chunk-LCNQ2CNN.mjs} +55 -319
- package/dist/token-goat-chunk-M3LFJDPU.mjs +58 -0
- package/dist/token-goat-chunk-M52KPT3C.mjs +87 -0
- package/dist/token-goat-chunk-MBEG42H7.mjs +150 -0
- package/dist/{token-goat-chunk-EVC4TOLE.mjs → token-goat-chunk-METECDOH.mjs} +3 -12
- package/dist/{token-goat-chunk-T4FK7VOV.mjs → token-goat-chunk-MHX7XCC3.mjs} +2 -1
- package/dist/token-goat-chunk-N5LSOVSU.mjs +137 -0
- package/dist/{token-goat-chunk-LOBGHXPB.mjs → token-goat-chunk-N5MMR2IW.mjs} +1 -1
- package/dist/token-goat-chunk-N7B3JQ2C.mjs +160 -0
- package/dist/{token-goat-chunk-RHXKCZX3.mjs → token-goat-chunk-NISNMD7T.mjs} +2182 -2959
- package/dist/token-goat-chunk-NMD42SFQ.mjs +434 -0
- package/dist/token-goat-chunk-NOSS5VWY.mjs +174 -0
- package/dist/{token-goat-chunk-KQNZDCBD.mjs → token-goat-chunk-NSNSPPBG.mjs} +25 -3
- package/dist/token-goat-chunk-OBOFIHKN.mjs +38 -0
- package/dist/token-goat-chunk-OBRFJ5O5.mjs +76 -0
- package/dist/{token-goat-chunk-JTQQGXZH.mjs → token-goat-chunk-ODBT2USM.mjs} +124 -57
- package/dist/token-goat-chunk-OS5EM5PP.mjs +107 -0
- package/dist/token-goat-chunk-OUKDTVNU.mjs +1178 -0
- package/dist/token-goat-chunk-OXFMP75N.mjs +566 -0
- package/dist/{token-goat-chunk-DZX2YDPT.mjs → token-goat-chunk-OYIYKDDT.mjs} +2 -2
- package/dist/{token-goat-chunk-QCLDAQTS.mjs → token-goat-chunk-P6HQSWQ3.mjs} +50 -53
- package/dist/token-goat-chunk-P6VGNTQT.mjs +71 -0
- package/dist/{token-goat-chunk-7BXSNBY5.mjs → token-goat-chunk-PFWIMH2E.mjs} +23 -15
- package/dist/token-goat-chunk-PJZXIS3G.mjs +39 -0
- package/dist/token-goat-chunk-PUO5INXP.mjs +606 -0
- package/dist/{token-goat-chunk-GFBSN7BD.mjs → token-goat-chunk-PX5PQWTY.mjs} +22 -250
- package/dist/token-goat-chunk-Q5QDBC6O.mjs +111 -0
- package/dist/token-goat-chunk-QFNQXSDT.mjs +40 -0
- package/dist/{token-goat-chunk-FBNQNSJK.mjs → token-goat-chunk-QH7YVYOY.mjs} +2198 -2782
- package/dist/{token-goat-chunk-64FBPDTK.mjs → token-goat-chunk-QLMUHEPB.mjs} +12 -9
- package/dist/{token-goat-chunk-RZRTDST7.mjs → token-goat-chunk-QQOJIHL3.mjs} +37 -11
- package/dist/{token-goat-chunk-XKI4SEMX.mjs → token-goat-chunk-QQTWJY2O.mjs} +19 -0
- package/dist/{token-goat-chunk-U4N3BN2W.mjs → token-goat-chunk-QYHO5CPH.mjs} +3 -2
- package/dist/token-goat-chunk-RELAVWAD.mjs +27 -0
- package/dist/{token-goat-chunk-ETBHVDB6.mjs → token-goat-chunk-RSLPOBKQ.mjs} +22 -11
- package/dist/token-goat-chunk-SAMFDIS2.mjs +712 -0
- package/dist/token-goat-chunk-SDELXLDM.mjs +48 -0
- package/dist/{token-goat-chunk-4JJS54TR.mjs → token-goat-chunk-SOAFZNNZ.mjs} +161 -38
- package/dist/{token-goat-chunk-664RTTUV.mjs → token-goat-chunk-SRTHT7KV.mjs} +21 -16
- package/dist/{token-goat-chunk-4FVIAHSI.mjs → token-goat-chunk-T5P5ACNQ.mjs} +7 -6
- package/dist/token-goat-chunk-T5YGG4V3.mjs +45 -0
- package/dist/token-goat-chunk-TAUZ4OFS.mjs +110 -0
- package/dist/{token-goat-chunk-COTXZHTS.mjs → token-goat-chunk-TM6MC754.mjs} +4 -3
- package/dist/token-goat-chunk-TWUKBOMP.mjs +72 -0
- package/dist/{token-goat-chunk-3CHUNJ2O.mjs → token-goat-chunk-U2KSB4SG.mjs} +8 -5
- package/dist/{token-goat-chunk-3Z5S4KGM.mjs → token-goat-chunk-U4WHMG6Y.mjs} +29 -49
- package/dist/{token-goat-chunk-PY533T7F.mjs → token-goat-chunk-UGJHGR5A.mjs} +21 -15
- package/dist/{token-goat-chunk-CYCEYAEO.mjs → token-goat-chunk-UOUDVA5U.mjs} +1 -1
- package/dist/{token-goat-chunk-JDHE26RP.mjs → token-goat-chunk-URRJ33GR.mjs} +16 -5
- package/dist/{token-goat-chunk-56EIMS3E.mjs → token-goat-chunk-UZOROKIS.mjs} +23 -17
- package/dist/{token-goat-chunk-CZ3LUXYZ.mjs → token-goat-chunk-VIHGGGEI.mjs} +427 -308
- package/dist/token-goat-chunk-VTJLAA7P.mjs +21 -0
- package/dist/token-goat-chunk-VWDXLT4F.mjs +678 -0
- package/dist/{token-goat-chunk-YARZFCZS.mjs → token-goat-chunk-VWZA2H4P.mjs} +62 -53
- package/dist/{token-goat-chunk-SQSSNQPE.mjs → token-goat-chunk-VXEXMJDL.mjs} +3979 -4745
- package/dist/token-goat-chunk-WBA5SKT4.mjs +547 -0
- package/dist/{token-goat-chunk-SJHWVNDQ.mjs → token-goat-chunk-WDH44VAU.mjs} +7 -4
- package/dist/token-goat-chunk-X2SUP3EI.mjs +107 -0
- package/dist/token-goat-chunk-X3BELZSJ.mjs +125 -0
- package/dist/{token-goat-chunk-FZ3NEW5O.mjs → token-goat-chunk-XLRBNZCC.mjs} +13 -8
- package/dist/{token-goat-chunk-Z44YO3QZ.mjs → token-goat-chunk-XRKUX6CM.mjs} +1 -1
- package/dist/token-goat-chunk-XX6O25RN.mjs +82 -0
- package/dist/token-goat-chunk-XZSUHBVS.mjs +65 -0
- package/dist/{token-goat-chunk-WY3L4DVT.mjs → token-goat-chunk-Y2ZTQOCL.mjs} +13 -8
- package/dist/{token-goat-chunk-UQ5UK2T7.mjs → token-goat-chunk-YB4VMGK2.mjs} +1 -1
- package/dist/token-goat-chunk-YBIIY3HR.mjs +51 -0
- package/dist/{token-goat-chunk-ZQSJMO2R.mjs → token-goat-chunk-YHVDKQBR.mjs} +105 -105
- package/dist/{token-goat-chunk-H2K3QP6H.mjs → token-goat-chunk-YJB5XLWI.mjs} +8 -6
- package/dist/{token-goat-chunk-ZCDTCVIS.mjs → token-goat-chunk-YKB6AQG3.mjs} +5 -2
- package/dist/token-goat-chunk-YMTG2YA5.mjs +35 -0
- package/dist/token-goat-chunk-YPBZYQRV.mjs +40 -0
- package/dist/{token-goat-chunk-HOJLCGDP.mjs → token-goat-chunk-YY6AVUFL.mjs} +2033 -2251
- package/dist/token-goat-chunk-Z7N5OMMP.mjs +98 -0
- package/dist/token-goat-chunk-ZNMNUPAZ.mjs +938 -0
- package/dist/token-goat-hook-client.cjs +1 -1
- package/dist/token-goat-hook-client.mjs +3 -3
- package/dist/token-goat-hook.mjs +67 -38
- package/dist/token-goat.core.mjs +4 -4
- package/docs/C4_RUNTIME_ARCHITECTURE.md +1 -1
- package/docs/cli.md +40 -21
- package/docs/install.md +19 -9
- package/docs/security.md +5 -3
- package/package.json +2 -1
- package/dist/token-goat-chunk-5KDMCMF2.mjs +0 -362
- package/dist/token-goat-chunk-6QMAS7AB.mjs +0 -42
- package/dist/token-goat-chunk-7F2UBPD3.mjs +0 -397
- package/dist/token-goat-chunk-ARDNEC2U.mjs +0 -248
- package/dist/token-goat-chunk-ATCPY7EQ.mjs +0 -134
- package/dist/token-goat-chunk-EFHMMIGP.mjs +0 -68
- package/dist/token-goat-chunk-IVALLT3S.mjs +0 -48
- package/dist/token-goat-chunk-JDU3EXSU.mjs +0 -65
- package/dist/token-goat-chunk-MKPF4CL5.mjs +0 -37
- package/dist/token-goat-chunk-RC35RBUL.mjs +0 -312
- package/dist/token-goat-chunk-USILZ3T3.mjs +0 -53
- package/dist/token-goat-chunk-VL3YIFHZ.mjs +0 -2975
- package/dist/token-goat-chunk-XU4P6Z24.mjs +0 -133
- package/dist/token-goat-chunk-Y776AENC.mjs +0 -1597
package/docs/install.md
CHANGED
|
@@ -148,7 +148,7 @@ The hooks were checked loading and running under agy 1.2.11 on Windows, and the
|
|
|
148
148
|
token-goat install --opencode
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
The `--opencode` flag drops a TypeScript bridge plugin into opencode's plugins directory.
|
|
151
|
+
The `--opencode` flag drops a TypeScript bridge plugin into opencode's plugins directory. Post-edit indexing, compact assist, and rewritten tool results (prompt-injection fencing, secret redaction, and output compression replace the raw result, the same protection Claude Code sessions get) work. So do repeat-search denial for `websearch`, repeat-load denial for `skill`, and the subagent prompt briefing for `task` — all three tool ids and their argument keys were verified against opencode's own source at the installed release's tag. Session hints don't — opencode's plugin API has no way to inject context before a tool read. Neither does image shrinking: the only way to hand opencode a smaller image is to point its `read` at a copy, and opencode checks its `read` and outside-directory permissions against that copy's path instead of the image's, so a rule you set on the image would stop applying. Shell commands are not wrapped for compression either, since opencode's permission rules can come from an organization account or a well-known URL no hook reads; their output is still compressed after they run.
|
|
152
152
|
|
|
153
153
|
### openclaw users
|
|
154
154
|
|
|
@@ -188,7 +188,13 @@ token-goat install --copilot
|
|
|
188
188
|
|
|
189
189
|
The `--copilot` flag patches Claude Code and registers a Copilot CLI hook config: `~/.copilot/hooks/token-goat.json` (a `{ version, hooks }` file registering `sessionStart`, `preToolUse`, `postToolUse`, `postToolUseFailure`, `preCompact`, `agentStop`, `subagentStart`, `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.cjs`. 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.
|
|
190
190
|
|
|
191
|
-
|
|
191
|
+
The shared `sessionStart` hook for Copilot CLI and VS Code reads one complete JSON object without waiting for stdin EOF, then runs the normal handler with a 10-second end-to-end budget and a final 1-second cleanup reserve. On Windows 10 / Windows Server 2016 and newer, the packaged `dist/native/win32-<arch>/tg-hook.exe` creates the canonical process atomically inside a strict Job Object. The owner's sole non-inherited job handle closes on owner death, terminating the owned descendants; neither breakaway flag is enabled on this job. A versioned acknowledgement confirms zero active job processes, root exit and bounded pipe drainage before the parent publishes output. A missing or incompatible owner, a job-association failure, or unverified cleanup fails open with `{}` and a stderr diagnostic, never an unowned Windows fallback. POSIX retains detached process-group cleanup. Input and canonical streams are bounded; the parent performs path arithmetic, not synchronous entry or native-binary filesystem probes. Reminder, notes, drift and receipt work remain enabled, and other events keep their existing behavior. When a start fails open because it missed the deadline, the first tool call of the session delivers it once, and `token-goat doctor` counts these on its "Deferred session starts" line. After upgrading, rerun `token-goat install --copilot` (and `token-goat install --vscode` for a VS Code project install) to refresh the generated shim; an upgrade does not rewrite already-installed hook files.
|
|
192
|
+
|
|
193
|
+
Native-wrapped startup commands also take this bounded path: the rebuilt binary routes startup before its ordinary stdin-to-EOF read or resident-server dispatch, including commands already present in hook manifests. One deadline starts at native entry and is carried through framing, owned relay launch, shim work and cleanup. The generated command shape is unchanged; existing manifests are not rewritten by a rebuild.
|
|
194
|
+
|
|
195
|
+
When startup detects index drift, it still queues the changed files. On Windows, worker activation is deferred until the outermost startup owner has verified tree cleanup, so the existing managed index daemon runs outside every startup job and continues draining after the hook returns. Direct Node startup performs the same activation from its top-level shim. Neither path waits for indexing or database locks before returning context, and a worker already running outside the startup jobs keeps its existing PID slot.
|
|
196
|
+
|
|
197
|
+
What works: **the command-routing reminder** (`sessionStart` returns `additionalContext`, so Copilot is told token-goat exists before it picks its first read tool — this is the one channel that lands ahead of that decision), **re-read denial** (`preToolUse` returns `permissionDecision: "deny"`), **shell output compression** (`postToolUse` returns `modifiedResult` after the command has run; a command is never rewritten before it runs, because the `--allow-tool` and `--deny-tool` rules Copilot was started with are invisible to a hook), **image shrinking** (`preToolUse` on a `view` call returns `modifiedArgs` carrying the full original arguments with `path` swapped to a materialized shrunk copy — Copilot replaces the tool call's arguments wholesale with `modifiedArgs`, so the rewrite must carry them all; only an image inside the working directory is shrunk, since the copy sits in the system temp folder Copilot reads without asking, and a hook cannot see which folders `/add-dir` added or whether Copilot checks the image's path or the copy's), **post-edit indexing** (a `postToolUse` side effect; it needs no response channel), and **stop-hallucination logging** (`agentStop`/`subagentStop` map a token-goat `deny` onto `decision: "block"`, everything else onto `decision: "allow"`). **A subagent's session briefing** goes to the subagent itself: `subagentStart` answers with it, so it is no longer added to the parent's `task` call and left in the parent conversation. A long subagent report is compacted on `subagentStop` (long code blocks trimmed to their ends, repeated blocks folded) with a pointer to the full copy, which `token-goat mcp-output <id> --full` prints. After upgrading, run `token-goat install --copilot` again to add the `subagentStart` hook; until then the briefing keeps arriving the old way. **Prompt hints and the compaction manifest** reach the model through `userPromptSubmitted`: Copilot CLI 1.0.88 appends the returned `additionalContext` to the prompt inside a `<system_reminder>` block. `preCompact` itself ignores a returned `additionalContext`, so token-goat saves the manifest when compaction starts and hands it back with your next prompt. Copilot's built-in tool names are remapped onto token-goat's internal names where a clear match exists (`view`→Read, `edit`→Edit, `create`→Write, `bash`/`powershell`→Bash, `read_bash`/`read_powershell`→BashOutput, `web_fetch`→WebFetch, `grep`→Grep, `glob`→Glob). MCP-server tool calls, which Copilot names `<server>-<tool>` rather than `mcp__<server>__<tool>`, are translated too, but only when the name matches Copilot's own cached tool list exactly — never guessed from the name's shape, because a server name can itself contain a hyphen and a wrong guess would make the read-only MCP dedup path deny an ordinary built-in call. With no cache to match against, nothing is translated. `memory`, `ask_user`, `write_bash`/`write_powershell` (which send keystrokes to a running shell, not commands), and `stop_bash`/`list_bash` pass through unmapped and simply no-op. `task`, Copilot's subagent tool, is not remapped either, but it is handled under its own name: a `task` spawn gets the same prompt briefing, duplicate-spawn advisory, and recall pointer on a long report that a Claude Code `Agent` spawn gets. The once-per-session unrestricted-spawn advisory is the one exception: it is suppressed under Copilot, because its `subagent_type` advice describes Claude Code's Task schema, which Copilot's `task` tool does not use.
|
|
192
198
|
|
|
193
199
|
**Why the background-shell compression matters most on Copilot.** Copilot runs shell commands in the background: a build or a test suite is started once, and the model then checks on it repeatedly while it runs. Each check hands back everything the command has printed since it started, from the first line. So the second check re-sends the whole first check, the third re-sends the first two, and a check ten minutes into a slow build re-sends the same output for the tenth time. The model has already read all of it and pays again for every word, every time. Token-goat sends the first check through untouched, then returns only the new part on each later check, with one line saying that is what it is; a check that found nothing new comes back as a single short line instead of the whole output again. Measured through the installed hook: a second check of 5,200 characters came back as about 1,250, and a third check that added nothing came back as 60 — roughly a quarter of the cost for the second look and about one percent for the third, improving the longer the command runs. Nothing is lost, because what is cut is what was already sent. It only shortens a check when the new output genuinely continues the last one seen; anything else passes straight through, so the worst case is a saving that does not happen rather than a wrong answer.
|
|
194
200
|
|
|
@@ -220,7 +226,9 @@ What works: a repeated read of a large file is denied, with a pointer to what th
|
|
|
220
226
|
|
|
221
227
|
The hooks folder and files are the same ones `token-goat install --copilot` uses, and one shim serves both: it tells a VS Code payload from a Copilot CLI one and answers each in its own format. token-goat records which install owns the files (`token-goat.owners` next to them), so `token-goat uninstall --vscode` leaves the hooks in place while `--copilot` still needs them, and the other way round. To remove: `token-goat uninstall --vscode` (add `--user` for a user-scope install).
|
|
222
228
|
|
|
223
|
-
|
|
229
|
+
The shared `sessionStart` handler has a 10-second end-to-end budget, including reading the complete input object, running the real handler in an isolated child, and cleaning up its owned process tree as described above. It does not wait for stdin EOF or skip reminder, notes, drift reporting, or receipt work. A timeout, malformed input, or failed cleanup returns `{}` with a stderr diagnostic. A start that missed the deadline is not lost: the first tool call of the session delivers it, once, and `token-goat doctor` counts these on its "Deferred session starts" line. This holds whenever the start message was received before the deadline, however late. On a heavily loaded PC the message can reach the hook after the 10-second deadline has passed, so the hook keeps listening for up to one more second past the deadline and still reads a message that lands in that window. A start whose message never arrives has no session identity and is not delivered, and the hook exits about 11 seconds after it started instead of 10. After upgrading, rerun `token-goat install --vscode` (or `token-goat install --vscode --user` for a user-scope install) to refresh the generated shim.
|
|
230
|
+
|
|
231
|
+
If VS Code's `chat.useClaudeHooks` setting is on and token-goat's Claude Code hooks are installed, VS Code also runs them from `~/.claude/settings.json`, so each token-goat hook fires twice. That setting is user-wide and covers every Claude hook in VS Code, not only token-goat's. A user-scope install (`token-goat install --vscode --user`) and `token-goat doctor --repair` turn it off in that case and say so, and `token-goat uninstall --vscode` turns it back on if it is still off. A project-scope install leaves it alone and prints a note instead; set `"chat.useClaudeHooks": false` in your VS Code user settings yourself if you see hooks run twice. `token-goat doctor` warns about the duplicate when a user-scope VS Code install is there for `--repair` to act on; when only a project-scope VS Code install exists it prints a plain note saying why the setting is left alone, which is not counted as a warning. An older or hand-trimmed install that wires only some events still counts as token-goat's hooks, since each wired event still fires. Without token-goat's Claude Code hooks there is no duplicate, and token-goat does not touch the setting.
|
|
224
232
|
|
|
225
233
|
### Visual Studio users
|
|
226
234
|
|
|
@@ -269,7 +277,7 @@ token-goat install --cursor
|
|
|
269
277
|
|
|
270
278
|
### Grok CLI (xAI Grok Build) users
|
|
271
279
|
|
|
272
|
-
Grok Build already reads Claude Code's `~/.claude/settings.json` as a "Harness Compatibility" source out of the box (confirmed against grok 0.2.93 and its own [hooks doc](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/10-hooks.md)), so `token-goat install` alone already gets most of the integration working — image shrinking, session hints, post-edit indexing, and
|
|
280
|
+
Grok Build already reads Claude Code's `~/.claude/settings.json` as a "Harness Compatibility" source out of the box (confirmed against grok 0.2.93 and its own [hooks doc](https://github.com/xai-org/grok-build/blob/main/crates/codegen/xai-grok-pager/docs/user-guide/10-hooks.md)), so `token-goat install` alone already gets most of the integration working — image shrinking, session hints, and post-edit indexing all fire. A shell command is not rewritten for compression, because Grok applies a rewrite before its own permission policy and prompt see the call. The one gap: Grok's own `PreToolUse` hook contract documents only `{"decision":"allow"}` / `{"decision":"deny","reason":"..."}`, never token-goat's harness-independent `{"decision":"block","reason":"..."}` shape (unlike Gemini CLI, whose docs explicitly confirm `"block"` as an accepted alias for `"deny"`), so re-read denial and oversized-first-read redirects don't reliably block on the Claude Code compat path alone.
|
|
273
281
|
|
|
274
282
|
```
|
|
275
283
|
token-goat install --grok
|
|
@@ -326,6 +334,8 @@ You can add wildcard rules to avoid repeated prompts for routine queries:
|
|
|
326
334
|
|
|
327
335
|
To allow all `token-goat` commands without prompting:
|
|
328
336
|
|
|
337
|
+
The rule below also matches the `token-goat compress` wrapper token-goat puts around commands whose output it compresses, so token-goat stops compressing commands it cannot prove you already allow: otherwise the rule would run them unprompted. The read-only list below does not have that effect.
|
|
338
|
+
|
|
329
339
|
```json
|
|
330
340
|
{
|
|
331
341
|
"permissions": {
|
|
@@ -362,7 +372,7 @@ To limit auto-approval strictly to read-only queries:
|
|
|
362
372
|
|
|
363
373
|
#### Codex CLI (`~/.codex/config.toml`)
|
|
364
374
|
|
|
365
|
-
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`:
|
|
375
|
+
Running `token-goat install --codex` automatically computes and writes `trusted_hash` into `[hooks.state]` so Codex trusts the hooks. If you set `CODEX_HOME`, install follows it, as Codex does: every `~/.codex` path below is then under `$CODEX_HOME`. To allow automatic command execution, set `approval_policy` in `~/.codex/config.toml`:
|
|
366
376
|
|
|
367
377
|
```toml
|
|
368
378
|
approval_policy = "never"
|
|
@@ -423,7 +433,7 @@ A bare `token-goat install` (no other flag) installs the Claude Code integration
|
|
|
423
433
|
|
|
424
434
|
**Hook server.** The first hook call also starts up to three hook servers in the background: plain detached `node` processes, not OS services, that answer later hook calls and read-only commands so each call skips starting Node. A read-only command a server has taken waits for its answer however long it runs, rather than starting a second copy beside it. Each listens on a named pipe (Windows) or a Unix socket with mode 0600 (elsewhere), never on a network port, and answers only a caller that proves it holds the random key in `hook-server.key` in the data directory, a file readable only by you. `hook-server.spawn-<slot>`, `hook-server.disabled` and `hook-server.failed` beside it are small marker files: a start rate limit, a record that the config turned the server off, and the reason the last start failed. A server exits after 30 minutes idle, after an upgrade replaces the build it loaded, when `server = false` under `[hooks]` in the global config or `TOKEN_GOAT_HOOK_SERVER=0` turns it off, and within two seconds of its key file being deleted. `uninstall` and `uninstall --purge` stop every running server before touching anything else. `token-goat capabilities` lists it under "Listens for other processes on this machine".
|
|
425
435
|
|
|
426
|
-
**Native hook client.** On Windows and Linux (x64 and arm64), `install` puts a small native program, `tg-hook`, in front of each hook command it writes, for every harness it wires. `tg-hook` hands the call to the hook server and prints its answer, which saves starting Node on every call. When the server cannot answer (it is off, not running yet, or busy), `tg-hook` runs the ordinary Node hook command it wraps, with the same input, so a hook never fails because the server is away. The command after `--` in each entry is exactly the Node command `install` writes without it. On Windows the hook entries name a copy of the binary kept in the data directory under `native\<id>\tg-hook.exe`, not the one inside the npm package, so an upgrade can replace the package while a hook call is still running. `install` refreshes that copy, and moves a running one aside to be removed by a later install. `uninstall` removes the hook entries but leaves that copy in place with the rest of token-goat's data, because a project-scope install in another repository may point at the same copy and `uninstall` cannot see it; delete the `native` folder under the data directory once no install uses it. `install` writes the native form only when the binary for this platform is present and passes its own self-test (`tg-hook --selftest`); otherwise it writes the Node command alone, which is always the case on macOS, where no binary ships. A Windows binary ships only when the release signed it with Authenticode; a release built without the signing certificate carries none, Windows then gets the Node command as macOS does, and `doctor` says this build ships no native hook client for the platform. Linux release binaries carry build provenance attestations; how to check both is under [Verifying what you installed](../SECURITY.md#verifying-what-you-installed). To turn the native client off, set `native = "off"` under `[hooks]` in the global config, or `TOKEN_GOAT_NATIVE_HOOKS=0`, and run `install` again; the variable wins over the config, and a project's `.token-goat.toml` cannot set this key. `token-goat doctor` prints one `Native hooks` line per harness and scope, for example `Native hooks (Codex): native, <path>, self-test passed; last 7 days: 1 served natively, 0 fell back, 0 through Node`: the form wired (native, Node, or mixed), the binary and its self-test result, and over the last seven days how many hook calls the server answered, how many fell back to the Node command and why, and how many ran the Node command directly. An entry written by an older build is flagged as outdated with the `install` command that rewrites it, and an entry whose binary no longer exists is reported as broken, since those events fail rather than fall back. The fallback to Node runs inside `tg-hook`, so it cannot help when `tg-hook` itself is stopped from starting after install, by an antivirus quarantining it or an application-control policy (Smart App Control, App Control for Business, AppLocker) refusing it. Most harnesses then carry on without token-goat's hooks, and Copilot CLI denies every tool call. `doctor` reports either case as a failure; run `install` again, which writes the Node command for a client that still cannot run, and turn the native client off as above if the block comes back.
|
|
436
|
+
**Native hook client.** On Windows and Linux (x64 and arm64), `install` puts a small native program, `tg-hook`, in front of each hook command it writes, for every harness it wires. `tg-hook` hands the call to the hook server and prints its answer, which saves starting Node on every call. When the server cannot answer (it is off, not running yet, or busy), `tg-hook` runs the ordinary Node hook command it wraps, with the same input, so a hook never fails because the server is away. Before running it, `tg-hook` stops every handle it was given from passing to that command, so a hook server the command starts in the background does not keep the hook's output open; PowerShell, which Copilot CLI runs hook commands through on Windows, passes its own output on that way, and with the Node command alone the call that starts a server leaves it holding that output until the server exits. The command after `--` in each entry is exactly the Node command `install` writes without it. On Windows the hook entries name a copy of the binary kept in the data directory under `native\<id>\tg-hook.exe`, not the one inside the npm package, so an upgrade can replace the package while a hook call is still running. `install` refreshes that copy, and moves a running one aside to be removed by a later install. `uninstall` removes the hook entries but leaves that copy in place with the rest of token-goat's data, because a project-scope install in another repository may point at the same copy and `uninstall` cannot see it; delete the `native` folder under the data directory once no install uses it. `install` writes the native form only when the binary for this platform is present and passes its own self-test (`tg-hook --selftest`); otherwise it writes the Node command alone, which is always the case on macOS, where no binary ships. A Windows binary ships only when the release signed it with Authenticode; a release built without the signing certificate carries none, Windows then gets the Node command as macOS does, and `doctor` says this build ships no native hook client for the platform. Linux release binaries carry build provenance attestations; how to check both is under [Verifying what you installed](../SECURITY.md#verifying-what-you-installed). To turn the native client off, set `native = "off"` under `[hooks]` in the global config, or `TOKEN_GOAT_NATIVE_HOOKS=0`, and run `install` again; the variable wins over the config, and a project's `.token-goat.toml` cannot set this key. `token-goat doctor` prints one `Native hooks` line per harness and scope, for example `Native hooks (Codex): native, <path>, self-test passed; last 7 days: 1 served natively, 0 fell back, 0 through Node`: the form wired (native, Node, or mixed), the binary and its self-test result, and over the last seven days how many hook calls the server answered, how many fell back to the Node command and why, and how many ran the Node command directly. An entry written by an older build is flagged as outdated with the `install` command that rewrites it, and an entry whose binary no longer exists is reported as broken, since those events fail rather than fall back. The fallback to Node runs inside `tg-hook`, so it cannot help when `tg-hook` itself is stopped from starting after install, by an antivirus quarantining it or an application-control policy (Smart App Control, App Control for Business, AppLocker) refusing it. Most harnesses then carry on without token-goat's hooks, and Copilot CLI denies every tool call. `doctor` reports either case as a failure; run `install` again, which writes the Node command for a client that still cannot run, and turn the native client off as above if the block comes back.
|
|
427
437
|
|
|
428
438
|
There is no auto-update mechanism. Updating token-goat is always a manual `npm install -g token-goat@latest`.
|
|
429
439
|
|
|
@@ -445,7 +455,7 @@ Three things follow, and they are worth knowing before you decide. It never leav
|
|
|
445
455
|
|
|
446
456
|
| Path | What |
|
|
447
457
|
|------|------|
|
|
448
|
-
| `~/.codex/config.toml` | Hooks block with Codex-specific matchers (`view_image|Bash`, `apply_patch`, `web_search`) plus `PreCompact`/`UserPromptSubmit`/`SubagentStop` global hooks. Existing hooks preserved. |
|
|
458
|
+
| `~/.codex/config.toml` | Hooks block with Codex-specific matchers (`view_image|Bash`, `apply_patch`, `web_search`, `spawn_agent|multi_agent_v1wait_agent`) plus `PreCompact`/`UserPromptSubmit`/`Stop`/`SubagentStop` global hooks. Existing hooks preserved. |
|
|
449
459
|
| `~/.codex/AGENTS.md` | A delimited block (`<!-- token-goat-codex-begin -->` … `<!-- token-goat-codex-end -->`) with the same routing guidance, adapted for Codex tool names. |
|
|
450
460
|
| `~/.codex/hooks/token-goat-shim.cjs` | The hook script `config.toml`'s hook commands invoke (`node "<path>" <event>`). Strips internal `_tg_*` keys and injects `hookSpecificOutput.hookEventName` to satisfy Codex's strict schemas. Regenerated on every `install --codex` run. The `.cjs` extension keeps Node from loading it as an ES module when a package.json above it says `"type": "module"`. A small `token-goat-shim.js` beside it hands off to the `.cjs` file, for sessions started before the rename that still run the old path. |
|
|
451
461
|
|
|
@@ -482,7 +492,7 @@ Three things follow, and they are worth knowing before you decide. It never leav
|
|
|
482
492
|
|
|
483
493
|
| Path | What |
|
|
484
494
|
|------|------|
|
|
485
|
-
| `~/.config/opencode/plugins/token-goat.ts` on every platform, Windows included (`$XDG_CONFIG_HOME/opencode/plugins/token-goat.ts` when that is set) | TypeScript bridge plugin. Fires on `tool.execute.before`, `tool.execute.after`, and `experimental.session.compacting`. Covers
|
|
495
|
+
| `~/.config/opencode/plugins/token-goat.ts` on every platform, Windows included (`$XDG_CONFIG_HOME/opencode/plugins/token-goat.ts` when that is set) | TypeScript bridge plugin. Fires on `tool.execute.before`, `tool.execute.after`, and `experimental.session.compacting`. Covers post-edit indexing, compact assist, and rewritten tool results. |
|
|
486
496
|
|
|
487
497
|
**With `--pi`** (pi extension)
|
|
488
498
|
|
|
@@ -510,7 +520,7 @@ Three things follow, and they are worth knowing before you decide. It never leav
|
|
|
510
520
|
|
|
511
521
|
| Path | What |
|
|
512
522
|
|------|------|
|
|
513
|
-
| `<project>/.vscode/mcp.json` — or `%APPDATA%\Code\User\mcp.json` (Windows) / `~/Library/Application Support/Code/User/mcp.json` (macOS) / `~/.config/Code/User/mcp.json` (Linux) with `--user` | Merges the `token-goat` stdio entry under VS Code's `servers` root key, preserving unrelated servers and settings. `--user` refuses to write when the project scope already has a token-goat-managed entry, to avoid a duplicate registration; the reverse direction is a migration instead, and removes the user-scope install. |
|
|
523
|
+
| `<project>/.vscode/mcp.json` — or `%APPDATA%\Code\User\mcp.json` (Windows) / `~/Library/Application Support/Code/User/mcp.json` (macOS) / `~/.config/Code/User/mcp.json` (Linux, under `$XDG_CONFIG_HOME` when that is an absolute path) with `--user` | Merges the `token-goat` stdio entry under VS Code's `servers` root key, preserving unrelated servers and settings. `--user` refuses to write when the project scope already has a token-goat-managed entry, to avoid a duplicate registration; the reverse direction is a migration instead, and removes the user-scope install. |
|
|
514
524
|
| `<project>/.github/copilot-instructions.md` (`~/.copilot/instructions/token-goat.instructions.md` with `--user`) | A delimited VS Code routing block that documents supported MCP selection and what the agent hooks can and cannot do with built-in file reads. The user-scope file is a personal instructions file with `applyTo: '**'`, so VS Code applies it in every workspace; install creates it with that frontmatter if it is missing and otherwise merges the block in, and uninstall deletes it again when nothing else is left in it. A user-scope install never writes into the folder you run it from. |
|
|
515
525
|
| `<project>/.github/hooks/token-goat.json`, `token-goat-shim.cjs`, `token-goat-shim.js`, `token-goat.owners` (`~/.copilot/hooks/` with `--user`) | VS Code agent hooks, shared with `--copilot`; the owners file records which of the two installs still uses them. |
|
|
516
526
|
|
package/docs/security.md
CHANGED
|
@@ -16,13 +16,15 @@ Outbound network is reserved to these explicit cases:
|
|
|
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
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. Unless `onnxruntime-node` is installed, the same first run also downloads the inference runtime: the `onnxruntime-web` 1.30.0 tarball from `registry.npmjs.org` (33 MB), itself checked against a recorded SHA-256 and byte length, from which only the 14 MB `ort-wasm-simd-threaded.wasm` is kept, checked against a digest of its own. The JavaScript that loads it ships inside token-goat and is checked against its recorded digest before it runs. Nothing is installed and no install script runs. 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, and an entry that is not a plain file of the expected size is passed over without being read at all, so a tampered copy costs a download and cannot substitute different weights, stall the read, or fill the disk. Token-goat also writes there, and treats the directory as somebody else's: it creates its temporary file exclusively rather than through whatever is already sitting at that name, and removes only what it created. Point it at a directory you control all the same, since nothing token-goat does can stop another writer filling it with entries that will simply be rejected. 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
|
-
- The first optical-character read of an image downloads the language data (about 4 MB; English `eng` by default, plus any additional opt-in languages configured via `image_shrink.ocr_lang`, `TOKEN_GOAT_OCR_LANG`, or `--lang` among 16 pinned models) from `cdn.jsdelivr.net`, at a fixed version path with precomputed SHA-256 integrity verification against the decompressed traineddata payload. Subsequent reads use the local cache. This happens for an explicit `token-goat image-text
|
|
19
|
+
- The first optical-character read of an image downloads the language data (about 4 MB; English `eng` by default, plus any additional opt-in languages configured via `image_shrink.ocr_lang`, `TOKEN_GOAT_OCR_LANG`, or `--lang` among 16 pinned models) from `cdn.jsdelivr.net`, at a fixed version path with precomputed SHA-256 integrity verification against the decompressed traineddata payload. Subsequent reads use the local cache. This happens only for an explicit `token-goat image-text`; the image-shrink hook runs no OCR and fetches no language data.
|
|
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). Twenty individual keys inside otherwise-overridable sections are locked the same way. Seven decide what gets indexed at all: `indexing.cross_project_symbols`, `indexing.skip_dirs`, `indexing.skip_files`, `indexing.skip_minified`, `indexing.large_file_skip_kb`, `indexing.large_file_symbol_only_kb` and `indexing.max_chunks_per_file`, 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`. Five more each guard one thing: `image_shrink.max_image_pixels`, the decompression-bomb cap; `worker.blocked_roots`, the folders you have kept out of the index; `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; `semantic.max_distance`, the relevance floor for `semantic`, which a repository must not be able to set low enough that its own code stops matching while the command goes on answering from keyword search as though nothing were missing; and `semantic.weak_distance`, the line past which `semantic` warns that its closest match is weak, which a repository must not be able to raise until every poor hit reads as confident. Two more govern the shared database: `indexing.max_db_size_mb` and `indexing.auto_reclaim_embeddings`, which prevent a repository from manipulating global database disk caps or forcing vector purging across projects. One more is `hooks.native`, which decides whether `install` writes the native hook client into the hook configs of every harness on the machine, so a project's file has no say in it. 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). Twenty-two individual keys inside otherwise-overridable sections are locked the same way. Seven decide what gets indexed at all: `indexing.cross_project_symbols`, `indexing.skip_dirs`, `indexing.skip_files`, `indexing.skip_minified`, `indexing.large_file_skip_kb`, `indexing.large_file_symbol_only_kb` and `indexing.max_chunks_per_file`, 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`. Five more each guard one thing: `image_shrink.max_image_pixels`, the decompression-bomb cap; `worker.blocked_roots`, the folders you have kept out of the index; `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; `semantic.max_distance`, the relevance floor for `semantic`, which a repository must not be able to set low enough that its own code stops matching while the command goes on answering from keyword search as though nothing were missing; and `semantic.weak_distance`, the line past which `semantic` warns that its closest match is weak, which a repository must not be able to raise until every poor hit reads as confident. Two more govern the shared database: `indexing.max_db_size_mb` and `indexing.auto_reclaim_embeddings`, which prevent a repository from manipulating global database disk caps or forcing vector purging across projects. Two more govern symbol-aware first-read thresholds and enforcement: `hints.first_read_symbol_bytes` and `hints.first_read_symbol_policy`. One more is `hooks.native`, which decides whether `install` writes the native hook client into the hook configs of every harness on the machine, so a project's file has no say in it. 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
|
-
**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-
|
|
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-five 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
|
+
|
|
27
|
+
**token-goat's rewrites do not approve your commands.** To compress a command's output, token-goat rewrites the command to run under `token-goat compress`, and it can point a Read of a large image at a smaller copy. Claude Code checks your permission rules against the rewritten call, so token-goat reads your Claude Code settings first (managed policy, `~/.claude/settings.json`, and the `.claude` folders of your project) and leaves the call alone whenever a deny or ask rule could apply to it, or a settings file cannot be read. A path reached through a symlink, a Windows junction or an 8.3 short name is checked under its real spelling too, as Claude Code does. A rewritten call is approved on your behalf only when Claude Code would have run the original with no prompt anyway, for example a read-only command like `git status`, or a command one of your own allow rules covers; an allow in `.claude/settings.local.json` counts only when git reports that file untracked inside a repository, so one shipped in a downloaded folder with no repository never approves anything. Rules are read from the folder the session started in, its main git checkout when that folder is a worktree, and the current directory; once a command's directory leaves the project, token-goat leaves the command alone, since the rules that apply there are not ones it can see. It is never approved in plan mode; otherwise Claude Code asks you as usual. In auto mode and bypassPermissions mode token-goat never adds a prompt: it either compresses a call or leaves it alone. In auto mode it leaves every call alone, since an approval would skip the auto-mode classifier's review. In bypassPermissions mode it rewrites and approves a call only when no rule source a hook cannot read could apply, and otherwise leaves it alone; those sources are a host that answers permission prompts itself (the Agent SDK's `canUseTool`, an editor extension, anything but the terminal `claude` command), a command-line flag that adds or relays rules (`--disallowedTools`, `--settings`, or any flag token-goat does not know to be harmless), a skill, slash command, agent or nested `.claude` folder whose `disallowed-tools` names a tool by pattern, and a `PermissionRequest` hook in a settings file or an installed plugin. A call made inside a subagent is approved only when the subagent is one of Claude Code's own or one whose definition token-goat read, since a definition can remove tools by pattern too. Skill folders reached through a symlink or a Windows junction, and ones inside `node_modules`, are checked like any other; one in a folder git ignores is passed over only when the folder holding its `.claude` is ignored, which is the folder Claude Code asks git about. The folders Claude Code always reads are checked again on every call, so a skill written there counts from the next call. In every other mode where a call can be approved (default, acceptEdits and dontAsk), a call is also left alone while the claude command line carries a flag token-goat does not know to be harmless, or cannot be read, so a `--disallowedTools` rule now holds against a compressed command there too; a session launched with `--remote-control` is one of these, because that flag is not on the list of known-harmless flags, and `--setting-sources` is harmless only when it names user, project and local. Those modes also leave a call alone while a skill, command, agent or plugin could remove its tool by pattern or a PermissionRequest hook could answer for it, for Claude Code and for VS Code alike (the command line is asked only of the terminal command), and until that check has loaded no mode approves a call. Two unseen sources can still let a call be approved. The first is a deliberate cost trade: a `.claude` folder nested deeper in the project is searched again for each new session, working directory or project folder, and otherwise at most once a minute, which keeps the hook fast at the price of a skill written there going unseen for up to a minute, except that a change to the checkout's index or HEAD (a pull, a checkout, a `git add`), or to a folder within three levels of the project or on the path to a listed `.claude` at any depth, ends that minute at once; no clean answer is kept when a stamp is under 2 seconds old, cannot be read, or the folders to stamp reach 2,000. The one hole left is a `.claude` folder first created, untracked, more than three levels down in a folder that holds no file git lists, which keeps the minute. A skill file git tracks that is gone from the disk is read from git's index, so moving a skill away and back between two calls does not hide it; a gone file git never listed is no rule, as Claude Code reads skills from disk. Nothing about the claude process or the registry policy is kept in a file between hooks, because a file the same user can write is a file an agent can write, and a forged "no flags" would approve what `--disallowedTools` refuses; the resident hook server remembers the command line for the session in its own memory, and a one-shot hook reads it again. A command line that says nothing about its flags (a Node process that retitled itself, which leaves only its title in `/proc` on Linux) is read as unreadable, not as having none. The second is an accepted limit: rules a host passes in a way token-goat cannot see. When `CLAUDE_CODE_ENTRYPOINT` is set to something other than the terminal command (an editor extension, the desktop app, a remote session), outside bypassPermissions mode token-goat judges the call by your settings files alone, because it cannot tell how that host starts Claude Code; a missing or empty value takes the strict path instead, both before and after the check is loaded. This applies to Claude Code itself; VS Code runs the same settings walk with the mode treated as `default` and no command line rule, and Codex, Copilot CLI and every other harness never reach it. A project that can set `CLAUDE_PID` or `CLAUDE_CODE_ENTRYPOINT` through the `env` of its settings can already add `permissions.allow` rules, so forging either is no escalation. Put rules you rely on in a settings file. token-goat's own hints follow the same rules: a hint that answers a Read, a Grep or a `cat` by naming a token-goat command that reads the file itself (`token-goat read`, `json-query`, `skeleton`) is not given for a path a Read deny or ask rule could cover, since no Read rule applies to that command, and Claude Code then answers the call as it would without token-goat; a shell command whose `$`, `~` or wildcard names files a hook cannot resolve gets no such hint while any Read deny or ask rule exists. On Codex, a command is rewritten only when no rule in a Codex rules file (`rules/*.rules` in `~/.codex`, `CODEX_HOME`, the system Codex folder, and every `.codex` folder above the current directory) could match it, whatever the rule's decision, since an `allow` rule runs the command outside the sandbox and the wrapper would lose that; a rules file token-goat cannot read, or an administrator's `requirements.toml` or macOS managed preference that adds rules, stops every command rewrite. Requirements Codex fetches from the cloud (Agent Security in the Admin Console) are an accepted limit: no file on the machine holds them, so a hook cannot read them; where those apply, `TOKEN_GOAT_BASH_COMPRESS=0` turns compression off, though a plain `rg` or `grep` listing of one file's definitions is still answered from the index. On GitHub Copilot CLI, opencode and Grok, a command is never rewritten, a deliberate safety choice and not only a limit, since their shell permission rules cannot be seen and token-goat will not rewrite a command it cannot show those rules would allow: Copilot's `--allow-tool` and `--deny-tool` rules arrive on its command line, opencode's permission rules can come from an organization account or a well-known URL and its check for directories outside the project reads the paths a wrapped command would hide, and Grok applies a rewrite before its own policy and prompt see the call; none of that is visible to a hook. On Copilot CLI and opencode, a recognized command's output is still compressed after the command has run. On opencode, a Read of a large image is not pointed at a smaller copy either, because opencode checks its `read` permission against the copy's path instead of the image's. On Copilot CLI, only an image inside the working directory is pointed at a smaller copy: Copilot asks before reading outside the working directory, the system temp folder and any folder added with `/add-dir`, the copy sits in the temp folder, and a hook cannot tell whether Copilot checks the image's path or the copy's, nor which folders were added.
|
|
26
28
|
|
|
27
29
|
**Security reports.** See [SECURITY.md](../SECURITY.md). Email `token-goat@dfkhelper.com`; do not file as a GitHub issue. Reports are acknowledged within 7 days; coordinated disclosure with a 90-day default window.
|
|
28
30
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "token-goat",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.31",
|
|
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",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"model:warm": "tsx scripts/warm-model-cache.ts",
|
|
30
30
|
"eval:hints": "tsx scripts/eval-hints.ts",
|
|
31
31
|
"eval:retrieval": "tsx scripts/eval-retrieval.ts",
|
|
32
|
+
"eval:gate": "tsx scripts/eval-gate.ts",
|
|
32
33
|
"test:guards": "vitest run tests/guards",
|
|
33
34
|
"test:matrix": "vitest run tests/command_matrix_e2e",
|
|
34
35
|
"test:watch": "vitest",
|
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
import { createRequire as __cjsRequire } from 'node:module';
|
|
2
|
-
const require = __cjsRequire(import.meta.url);
|
|
3
|
-
import {
|
|
4
|
-
atomicWriteText,
|
|
5
|
-
ensureDirSync,
|
|
6
|
-
extractErrorMessage,
|
|
7
|
-
withFileLock
|
|
8
|
-
} from "./token-goat-chunk-GOLEOXZP.mjs";
|
|
9
|
-
import {
|
|
10
|
-
displaySafeText
|
|
11
|
-
} from "./token-goat-chunk-FNFBKLRD.mjs";
|
|
12
|
-
import {
|
|
13
|
-
dataDir
|
|
14
|
-
} from "./token-goat-chunk-VES7EL7O.mjs";
|
|
15
|
-
import {
|
|
16
|
-
init_define_import_meta_env
|
|
17
|
-
} from "./token-goat-chunk-A37V4PBF.mjs";
|
|
18
|
-
|
|
19
|
-
// src/worker_lifecycle.ts
|
|
20
|
-
init_define_import_meta_env();
|
|
21
|
-
import { spawn } from "node:child_process";
|
|
22
|
-
import * as fs from "node:fs";
|
|
23
|
-
import * as os from "node:os";
|
|
24
|
-
import * as path from "node:path";
|
|
25
|
-
import { fileURLToPath } from "node:url";
|
|
26
|
-
|
|
27
|
-
// src/env_proxy.ts
|
|
28
|
-
init_define_import_meta_env();
|
|
29
|
-
var PROXY_VARS = ["HTTPS_PROXY", "https_proxy", "HTTP_PROXY", "http_proxy"];
|
|
30
|
-
function proxyConfigured(env = process.env) {
|
|
31
|
-
return PROXY_VARS.some((k) => (env[k] ?? "").trim() !== "");
|
|
32
|
-
}
|
|
33
|
-
function nodeFetchHonoursEnvProxy(version = process.versions.node) {
|
|
34
|
-
const [major = 0, minor = 0] = version.split(".").map((n) => Number.parseInt(n, 10));
|
|
35
|
-
if (major >= 24) return true;
|
|
36
|
-
return major === 22 && minor >= 21;
|
|
37
|
-
}
|
|
38
|
-
function fetchBypassesProxy(env = process.env, nodeVersion = process.versions.node) {
|
|
39
|
-
return env["NODE_USE_ENV_PROXY"] === void 0 && proxyConfigured(env) && nodeFetchHonoursEnvProxy(nodeVersion);
|
|
40
|
-
}
|
|
41
|
-
function rerunWithEnvProxy(spawn2, argv = process.argv, env = process.env, nodeVersion = process.versions.node) {
|
|
42
|
-
if (!fetchBypassesProxy(env, nodeVersion)) return null;
|
|
43
|
-
const result = spawn2(process.execPath, [...process.execArgv, ...argv.slice(1)], { env: withEnvProxyEnabled(env, nodeVersion), stdio: "inherit" });
|
|
44
|
-
return result.status ?? 1;
|
|
45
|
-
}
|
|
46
|
-
function withEnvProxyEnabled(env, nodeVersion = process.versions.node) {
|
|
47
|
-
if (env["NODE_USE_ENV_PROXY"] !== void 0 || !proxyConfigured(env) || !nodeFetchHonoursEnvProxy(nodeVersion)) return env;
|
|
48
|
-
return { ...env, NODE_USE_ENV_PROXY: "1" };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// src/worker_lifecycle.ts
|
|
52
|
-
var DEFAULT_POLL_INTERVAL_MS = 2e3;
|
|
53
|
-
function resolvePollIntervalMs(explicit) {
|
|
54
|
-
if (explicit !== void 0) return explicit;
|
|
55
|
-
const parsed = parseInt(process.env["TG_WORKER_POLL_MS"] ?? "", 10);
|
|
56
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_POLL_INTERVAL_MS;
|
|
57
|
-
}
|
|
58
|
-
var WORKER_HEARTBEAT_STALE_MS = 6e4;
|
|
59
|
-
var WORKER_HEARTBEAT_REFRESH_MS = 5e3;
|
|
60
|
-
var WORKER_STARTUP_GRACE_MS = 1e4;
|
|
61
|
-
var heartbeatWriteTimes = /* @__PURE__ */ new Map();
|
|
62
|
-
function drainHeartbeatPathFor(dir) {
|
|
63
|
-
return path.join(dir, "queue", "drain-heartbeat");
|
|
64
|
-
}
|
|
65
|
-
function writeDrainHeartbeat(dir, force = false) {
|
|
66
|
-
const now = Date.now();
|
|
67
|
-
if (!force && now - (heartbeatWriteTimes.get(dir) ?? 0) < WORKER_HEARTBEAT_REFRESH_MS) return;
|
|
68
|
-
try {
|
|
69
|
-
atomicWriteText(drainHeartbeatPathFor(dir), `${process.pid}
|
|
70
|
-
`);
|
|
71
|
-
heartbeatWriteTimes.set(dir, now);
|
|
72
|
-
} catch {
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
function hasFreshWorkerHeartbeat(dir, pid) {
|
|
76
|
-
try {
|
|
77
|
-
const heartbeatPath = drainHeartbeatPathFor(dir);
|
|
78
|
-
if (Date.now() - fs.statSync(heartbeatPath).mtimeMs > WORKER_HEARTBEAT_STALE_MS) return false;
|
|
79
|
-
return fs.readFileSync(heartbeatPath, "utf8").trim() === String(pid);
|
|
80
|
-
} catch {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
function pidFileIsWithinStartupGrace(dir) {
|
|
85
|
-
try {
|
|
86
|
-
return Date.now() - fs.statSync(workerPidPath(dir)).mtimeMs < WORKER_STARTUP_GRACE_MS;
|
|
87
|
-
} catch {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function workerPidPath(dir = dataDir()) {
|
|
92
|
-
return path.join(dir, "worker.pid");
|
|
93
|
-
}
|
|
94
|
-
function workerStampPath(dir = dataDir()) {
|
|
95
|
-
return path.join(dir, "worker.stamp");
|
|
96
|
-
}
|
|
97
|
-
function workerErrorLogPath(dir) {
|
|
98
|
-
return path.join(dir, "worker-errors.log");
|
|
99
|
-
}
|
|
100
|
-
function oneLogLine(line) {
|
|
101
|
-
return displaySafeText(line.replace(/[\n\r]+$/, "")) + "\n";
|
|
102
|
-
}
|
|
103
|
-
function appendWorkerErrorLog(dir, line) {
|
|
104
|
-
try {
|
|
105
|
-
fs.appendFileSync(workerErrorLogPath(dir), oneLogLine(line));
|
|
106
|
-
} catch {
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
function pidAlive(pid) {
|
|
110
|
-
try {
|
|
111
|
-
process.kill(pid, 0);
|
|
112
|
-
return true;
|
|
113
|
-
} catch (err) {
|
|
114
|
-
return err.code === "EPERM";
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function readPidFile(dir) {
|
|
118
|
-
try {
|
|
119
|
-
const raw = fs.readFileSync(workerPidPath(dir), "utf8").trim();
|
|
120
|
-
if (!/^\d+$/.test(raw)) return null;
|
|
121
|
-
return parseInt(raw, 10);
|
|
122
|
-
} catch {
|
|
123
|
-
return null;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
function isWorkerRunning(dir = dataDir()) {
|
|
127
|
-
const pid = readPidFile(dir);
|
|
128
|
-
if (pid === null) return false;
|
|
129
|
-
return pidAlive(pid) && hasFreshWorkerHeartbeat(dir, pid);
|
|
130
|
-
}
|
|
131
|
-
function workerHealthCheckMarkerPath(dir) {
|
|
132
|
-
return path.join(dir, "worker-healthcheck.marker");
|
|
133
|
-
}
|
|
134
|
-
var WORKER_HEALTHCHECK_MIN_INTERVAL_MS = 5 * 60 * 1e3;
|
|
135
|
-
function ensureWorkerAlive(dir = dataDir()) {
|
|
136
|
-
if (process.env["TOKEN_GOAT_NO_WORKER_SPAWN"] === "1") return;
|
|
137
|
-
const markerPath = workerHealthCheckMarkerPath(dir);
|
|
138
|
-
try {
|
|
139
|
-
const stat = fs.statSync(markerPath);
|
|
140
|
-
if (Date.now() - stat.mtimeMs < WORKER_HEALTHCHECK_MIN_INTERVAL_MS) return;
|
|
141
|
-
} catch {
|
|
142
|
-
}
|
|
143
|
-
try {
|
|
144
|
-
ensureDirSync(dir);
|
|
145
|
-
fs.writeFileSync(markerPath, "");
|
|
146
|
-
} catch {
|
|
147
|
-
}
|
|
148
|
-
const livePid = readPidFile(dir);
|
|
149
|
-
if (livePid !== null && pidAlive(livePid) && hasFreshWorkerHeartbeat(dir, livePid)) {
|
|
150
|
-
if (workerBundleMatches(dir)) return;
|
|
151
|
-
stopWorker(dir, livePid);
|
|
152
|
-
const nowPid = readPidFile(dir);
|
|
153
|
-
if (nowPid !== null && nowPid !== livePid) return;
|
|
154
|
-
}
|
|
155
|
-
try {
|
|
156
|
-
startDetachedWorker({ dataDir: dir });
|
|
157
|
-
} catch (e) {
|
|
158
|
-
if (e instanceof WorkerAlreadyRunningError) return;
|
|
159
|
-
try {
|
|
160
|
-
appendWorkerErrorLog(
|
|
161
|
-
dir,
|
|
162
|
-
`${(/* @__PURE__ */ new Date()).toISOString()} ensureWorkerAlive: auto-restart failed: ${extractErrorMessage(e)}
|
|
163
|
-
`
|
|
164
|
-
);
|
|
165
|
-
} catch {
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
function stopWorker(dir = dataDir(), expectedPid) {
|
|
170
|
-
const pid = readPidFile(dir);
|
|
171
|
-
if (pid === null) return false;
|
|
172
|
-
if (expectedPid !== void 0 && pid !== expectedPid) return false;
|
|
173
|
-
const running = isWorkerRunning(dir);
|
|
174
|
-
if (running) {
|
|
175
|
-
try {
|
|
176
|
-
process.kill(pid);
|
|
177
|
-
} catch {
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (readPidFile(dir) === pid) {
|
|
181
|
-
try {
|
|
182
|
-
fs.rmSync(workerPidPath(dir), { force: true });
|
|
183
|
-
} catch {
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
return running;
|
|
187
|
-
}
|
|
188
|
-
var WorkerAlreadyRunningError = class extends Error {
|
|
189
|
-
constructor(message = "worker already running") {
|
|
190
|
-
super(message);
|
|
191
|
-
this.name = "WorkerAlreadyRunningError";
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
var WorkerDataDirUnwritableError = class extends Error {
|
|
195
|
-
constructor(dir, cause) {
|
|
196
|
-
super(`the data directory ${displaySafeText(dir)} cannot be written (${extractErrorMessage(cause)}), and the worker keeps its pid file, queue and index there; make it writable and run \`token-goat worker start\` again`, { cause });
|
|
197
|
-
this.name = "WorkerDataDirUnwritableError";
|
|
198
|
-
}
|
|
199
|
-
};
|
|
200
|
-
function dataDirWriteRefusal(dir) {
|
|
201
|
-
const probe = path.join(dir, `.write-probe-${process.pid}`);
|
|
202
|
-
try {
|
|
203
|
-
fs.writeFileSync(probe, "");
|
|
204
|
-
} catch (e) {
|
|
205
|
-
return e;
|
|
206
|
-
}
|
|
207
|
-
try {
|
|
208
|
-
fs.rmSync(probe, { force: true });
|
|
209
|
-
} catch {
|
|
210
|
-
}
|
|
211
|
-
return void 0;
|
|
212
|
-
}
|
|
213
|
-
function discardSpawnedWorker(child, pid) {
|
|
214
|
-
try {
|
|
215
|
-
process.kill(pid);
|
|
216
|
-
} catch {
|
|
217
|
-
}
|
|
218
|
-
child.unref();
|
|
219
|
-
}
|
|
220
|
-
function claimWorkerPidFile(dir, pid) {
|
|
221
|
-
const pidPath = workerPidPath(dir);
|
|
222
|
-
try {
|
|
223
|
-
fs.writeFileSync(pidPath, `${pid}
|
|
224
|
-
`, { flag: "wx" });
|
|
225
|
-
return true;
|
|
226
|
-
} catch (e) {
|
|
227
|
-
if (e.code !== "EEXIST") throw e;
|
|
228
|
-
}
|
|
229
|
-
return withFileLock(`${pidPath}.reclaim`, () => reclaimStalePidFile(dir, pid), { waitMs: 0 }) ?? false;
|
|
230
|
-
}
|
|
231
|
-
function reclaimStalePidFile(dir, pid) {
|
|
232
|
-
const pidPath = workerPidPath(dir);
|
|
233
|
-
const existingPid = readPidFile(dir);
|
|
234
|
-
if (existingPid !== null && pidAlive(existingPid) && (hasFreshWorkerHeartbeat(dir, existingPid) || pidFileIsWithinStartupGrace(dir))) {
|
|
235
|
-
return false;
|
|
236
|
-
}
|
|
237
|
-
try {
|
|
238
|
-
fs.rmSync(pidPath, { force: true });
|
|
239
|
-
} catch {
|
|
240
|
-
}
|
|
241
|
-
try {
|
|
242
|
-
fs.writeFileSync(pidPath, `${pid}
|
|
243
|
-
`, { flag: "wx" });
|
|
244
|
-
return true;
|
|
245
|
-
} catch (e2) {
|
|
246
|
-
if (e2.code === "EEXIST") return false;
|
|
247
|
-
throw e2;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
function daemonEntryScript() {
|
|
251
|
-
const self = fileURLToPath(import.meta.url);
|
|
252
|
-
const launcher = path.join(path.dirname(self), "token-goat.mjs");
|
|
253
|
-
try {
|
|
254
|
-
if (fs.existsSync(launcher)) return launcher;
|
|
255
|
-
} catch {
|
|
256
|
-
}
|
|
257
|
-
return self;
|
|
258
|
-
}
|
|
259
|
-
function currentDaemonStamp() {
|
|
260
|
-
const entry = daemonEntryScript();
|
|
261
|
-
try {
|
|
262
|
-
const st = fs.statSync(entry);
|
|
263
|
-
return `${entry}|${st.size}|${Math.trunc(st.mtimeMs)}`;
|
|
264
|
-
} catch {
|
|
265
|
-
return entry;
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
function workerBundleMatches(dir) {
|
|
269
|
-
let raw;
|
|
270
|
-
try {
|
|
271
|
-
raw = fs.readFileSync(workerStampPath(dir), "utf8").trim();
|
|
272
|
-
} catch {
|
|
273
|
-
return false;
|
|
274
|
-
}
|
|
275
|
-
const parts = raw.split("|");
|
|
276
|
-
if (parts.length !== 3) return false;
|
|
277
|
-
const [entry, sizeStr, mtimeStr] = parts;
|
|
278
|
-
if (entry !== daemonEntryScript()) return true;
|
|
279
|
-
const size = Number(sizeStr);
|
|
280
|
-
const mtimeMs = Number(mtimeStr);
|
|
281
|
-
if (!Number.isFinite(size) || !Number.isFinite(mtimeMs)) return false;
|
|
282
|
-
try {
|
|
283
|
-
const st = fs.statSync(entry);
|
|
284
|
-
return st.size === size && Math.trunc(st.mtimeMs) === mtimeMs;
|
|
285
|
-
} catch {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
function startDetachedWorker(opts) {
|
|
290
|
-
const pollIntervalMs = resolvePollIntervalMs(opts?.pollIntervalMs);
|
|
291
|
-
const dir = path.resolve(opts?.dataDir ?? dataDir());
|
|
292
|
-
try {
|
|
293
|
-
ensureDirSync(dir);
|
|
294
|
-
} catch (e) {
|
|
295
|
-
if (e.code !== "EEXIST" || !fs.existsSync(dir)) throw e;
|
|
296
|
-
}
|
|
297
|
-
const refusal = dataDirWriteRefusal(dir);
|
|
298
|
-
if (refusal !== void 0) throw new WorkerDataDirUnwritableError(dir, refusal);
|
|
299
|
-
const cwd = fs.existsSync(os.tmpdir()) ? os.tmpdir() : dir;
|
|
300
|
-
const child = spawn(
|
|
301
|
-
process.execPath,
|
|
302
|
-
[daemonEntryScript(), "--worker-daemon"],
|
|
303
|
-
{
|
|
304
|
-
cwd,
|
|
305
|
-
detached: true,
|
|
306
|
-
stdio: "ignore",
|
|
307
|
-
windowsHide: true,
|
|
308
|
-
env: {
|
|
309
|
-
...withEnvProxyEnabled(process.env),
|
|
310
|
-
TG_WORKER_POLL_MS: String(pollIntervalMs),
|
|
311
|
-
TG_WORKER_DATA_DIR: dir
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
);
|
|
315
|
-
child.on("error", () => void 0);
|
|
316
|
-
const pid = child.pid;
|
|
317
|
-
if (pid === void 0) {
|
|
318
|
-
throw new Error("startDetachedWorker: spawn produced no pid");
|
|
319
|
-
}
|
|
320
|
-
let claimed;
|
|
321
|
-
try {
|
|
322
|
-
claimed = claimWorkerPidFile(dir, pid);
|
|
323
|
-
} catch (e) {
|
|
324
|
-
discardSpawnedWorker(child, pid);
|
|
325
|
-
throw e;
|
|
326
|
-
}
|
|
327
|
-
if (!claimed) {
|
|
328
|
-
discardSpawnedWorker(child, pid);
|
|
329
|
-
throw new WorkerAlreadyRunningError();
|
|
330
|
-
}
|
|
331
|
-
try {
|
|
332
|
-
fs.writeFileSync(workerStampPath(dir), currentDaemonStamp());
|
|
333
|
-
} catch {
|
|
334
|
-
}
|
|
335
|
-
child.unref();
|
|
336
|
-
return pid;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
export {
|
|
340
|
-
proxyConfigured,
|
|
341
|
-
nodeFetchHonoursEnvProxy,
|
|
342
|
-
fetchBypassesProxy,
|
|
343
|
-
rerunWithEnvProxy,
|
|
344
|
-
resolvePollIntervalMs,
|
|
345
|
-
drainHeartbeatPathFor,
|
|
346
|
-
writeDrainHeartbeat,
|
|
347
|
-
workerPidPath,
|
|
348
|
-
workerStampPath,
|
|
349
|
-
workerErrorLogPath,
|
|
350
|
-
oneLogLine,
|
|
351
|
-
appendWorkerErrorLog,
|
|
352
|
-
readPidFile,
|
|
353
|
-
isWorkerRunning,
|
|
354
|
-
ensureWorkerAlive,
|
|
355
|
-
stopWorker,
|
|
356
|
-
WorkerAlreadyRunningError,
|
|
357
|
-
WorkerDataDirUnwritableError,
|
|
358
|
-
dataDirWriteRefusal,
|
|
359
|
-
claimWorkerPidFile,
|
|
360
|
-
currentDaemonStamp,
|
|
361
|
-
startDetachedWorker
|
|
362
|
-
};
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { createRequire as __cjsRequire } from 'node:module';
|
|
2
|
-
const require = __cjsRequire(import.meta.url);
|
|
3
|
-
import {
|
|
4
|
-
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
5
|
-
DEFAULT_TIMEOUT_SECONDS,
|
|
6
|
-
MAX_CAPTURE_BYTES,
|
|
7
|
-
resolveFilter,
|
|
8
|
-
run,
|
|
9
|
-
runRaw,
|
|
10
|
-
spawnTarget,
|
|
11
|
-
unknownFilterNotice
|
|
12
|
-
} from "./token-goat-chunk-7F2UBPD3.mjs";
|
|
13
|
-
import "./token-goat-chunk-54K3EUV3.mjs";
|
|
14
|
-
import "./token-goat-chunk-COBQGL5S.mjs";
|
|
15
|
-
import "./token-goat-chunk-AFLYLHDH.mjs";
|
|
16
|
-
import "./token-goat-chunk-HYBFA5X3.mjs";
|
|
17
|
-
import "./token-goat-chunk-ARDNEC2U.mjs";
|
|
18
|
-
import "./token-goat-chunk-3Z5S4KGM.mjs";
|
|
19
|
-
import "./token-goat-chunk-VL3YIFHZ.mjs";
|
|
20
|
-
import "./token-goat-chunk-XB5KKOCG.mjs";
|
|
21
|
-
import "./token-goat-chunk-4FVIAHSI.mjs";
|
|
22
|
-
import "./token-goat-chunk-7IN2MKDS.mjs";
|
|
23
|
-
import "./token-goat-chunk-U4N3BN2W.mjs";
|
|
24
|
-
import "./token-goat-chunk-YZAAZU2S.mjs";
|
|
25
|
-
import "./token-goat-chunk-IVE6U7KV.mjs";
|
|
26
|
-
import "./token-goat-chunk-GOLEOXZP.mjs";
|
|
27
|
-
import "./token-goat-chunk-AM5V6GSX.mjs";
|
|
28
|
-
import "./token-goat-chunk-FNFBKLRD.mjs";
|
|
29
|
-
import "./token-goat-chunk-VES7EL7O.mjs";
|
|
30
|
-
import "./token-goat-chunk-KWPNJBDH.mjs";
|
|
31
|
-
import "./token-goat-chunk-HAVAOB3S.mjs";
|
|
32
|
-
import "./token-goat-chunk-A37V4PBF.mjs";
|
|
33
|
-
export {
|
|
34
|
-
DEFAULT_HEARTBEAT_INTERVAL_MS,
|
|
35
|
-
DEFAULT_TIMEOUT_SECONDS,
|
|
36
|
-
MAX_CAPTURE_BYTES,
|
|
37
|
-
resolveFilter,
|
|
38
|
-
run,
|
|
39
|
-
runRaw,
|
|
40
|
-
spawnTarget,
|
|
41
|
-
unknownFilterNotice
|
|
42
|
-
};
|