token-goat 2.9.29 → 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 +53 -42
- package/SECURITY.md +37 -11
- package/THIRD_PARTY_NOTICES.md +41 -4
- 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/ort-wasm-simd-threaded.mjs +59 -0
- package/dist/{token-goat-chunk-SE6E4BKJ.mjs → token-goat-chunk-25TRAWET.mjs} +23 -6
- package/dist/token-goat-chunk-2KXO6V2B.mjs +203 -0
- package/dist/{token-goat-chunk-UXSXQQIY.mjs → token-goat-chunk-2M7E4PF7.mjs} +43 -14
- package/dist/token-goat-chunk-2UPZGKKY.mjs +188 -0
- 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-2WOUYWLM.mjs → token-goat-chunk-4GILLLWY.mjs} +34 -7
- package/dist/token-goat-chunk-4NAPX7BA.mjs +90 -0
- package/dist/{token-goat-chunk-NMBWLRHW.mjs → token-goat-chunk-4SC7UJYV.mjs} +9981 -11563
- package/dist/token-goat-chunk-4SMRO4TO.mjs +366 -0
- package/dist/{token-goat-chunk-O6I2XU4P.mjs → token-goat-chunk-4ZBFWLXF.mjs} +310 -441
- package/dist/token-goat-chunk-57BIAXJL.mjs +286 -0
- package/dist/token-goat-chunk-5AC66HNP.mjs +115 -0
- package/dist/token-goat-chunk-5IMLKSOP.mjs +145 -0
- package/dist/token-goat-chunk-5RG5SBL6.mjs +221 -0
- package/dist/{token-goat-chunk-RRNZMM3A.mjs → token-goat-chunk-5Y6ZYL6U.mjs} +21 -2
- package/dist/token-goat-chunk-646E6U44.mjs +398 -0
- package/dist/token-goat-chunk-65N4RHP4.mjs +33 -0
- package/dist/{token-goat-chunk-6AK46ZT7.mjs → token-goat-chunk-6DG6HWP3.mjs} +13 -3
- package/dist/token-goat-chunk-6ENQSNRZ.mjs +573 -0
- package/dist/token-goat-chunk-6ZY4GNF2.mjs +91 -0
- package/dist/{token-goat-chunk-E5TK5DNO.mjs → token-goat-chunk-76FGDLRO.mjs} +574 -340
- package/dist/{token-goat-chunk-TGOXNAHW.mjs → token-goat-chunk-7EZTZOMC.mjs} +780 -767
- package/dist/token-goat-chunk-7MAOA6X4.mjs +3647 -0
- package/dist/token-goat-chunk-7R2ZVVAX.mjs +218 -0
- package/dist/{token-goat-chunk-TCQANXYC.mjs → token-goat-chunk-AM5V6GSX.mjs} +18 -2
- package/dist/token-goat-chunk-AUNGYYQX.mjs +184 -0
- package/dist/token-goat-chunk-AUQYRMMH.mjs +2985 -0
- package/dist/token-goat-chunk-B3PEKBYG.mjs +128 -0
- package/dist/{token-goat-chunk-KOPIR3KZ.mjs → token-goat-chunk-BA4HI6BW.mjs} +1910 -165
- package/dist/{token-goat-chunk-52NSDWYY.mjs → token-goat-chunk-BFCWY73P.mjs} +58 -20
- package/dist/token-goat-chunk-BPGCUMR7.mjs +30 -0
- package/dist/token-goat-chunk-BWGFUZZE.mjs +25 -0
- package/dist/token-goat-chunk-C75NA7PL.mjs +322 -0
- package/dist/token-goat-chunk-CBIQ7WTD.mjs +87 -0
- package/dist/token-goat-chunk-CETM74KC.mjs +307 -0
- package/dist/token-goat-chunk-CIMQ2BB3.mjs +233 -0
- package/dist/{token-goat-chunk-DNSPXQXQ.mjs → token-goat-chunk-CJLS3CDL.mjs} +70 -18
- package/dist/{token-goat-chunk-MELOKFDJ.mjs → token-goat-chunk-CO3UYX6G.mjs} +7 -1
- package/dist/token-goat-chunk-COPNXRJM.mjs +154 -0
- package/dist/token-goat-chunk-CSJ5L3NL.mjs +479 -0
- package/dist/token-goat-chunk-CVCEVUGK.mjs +315 -0
- package/dist/token-goat-chunk-CXHOFN7R.mjs +371 -0
- package/dist/token-goat-chunk-D735C4QM.mjs +603 -0
- package/dist/{token-goat-chunk-E4ZOJIZK.mjs → token-goat-chunk-DGS2LVNA.mjs} +70 -160
- package/dist/{token-goat-chunk-L7H3U27L.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-EELTBKDM.mjs +613 -0
- package/dist/token-goat-chunk-EVMZJ27Z.mjs +147 -0
- package/dist/token-goat-chunk-EXKGNHEG.mjs +2096 -0
- package/dist/{token-goat-chunk-YOOVFPTP.mjs → token-goat-chunk-F4HYL64R.mjs} +12 -2
- package/dist/token-goat-chunk-F6PAUIEK.mjs +624 -0
- package/dist/token-goat-chunk-FDSREWLX.mjs +692 -0
- package/dist/{token-goat-chunk-NMTKNYGF.mjs → token-goat-chunk-FNFBKLRD.mjs} +74 -9
- package/dist/token-goat-chunk-G5PJ7AGM.mjs +36 -0
- package/dist/token-goat-chunk-GGGG7RWB.mjs +236 -0
- package/dist/{token-goat-chunk-TDUT2CL6.mjs → token-goat-chunk-GL3ETKSC.mjs} +276 -19
- package/dist/{token-goat-chunk-7CPMDFGB.mjs → token-goat-chunk-GVB2MUST.mjs} +265 -1567
- package/dist/token-goat-chunk-GWFLAUMQ.mjs +307 -0
- package/dist/token-goat-chunk-HAVAOB3S.mjs +87 -0
- package/dist/token-goat-chunk-HLPW63A7.mjs +48 -0
- package/dist/token-goat-chunk-HU4TORC5.mjs +1377 -0
- package/dist/token-goat-chunk-I7VKZZAW.mjs +35 -0
- package/dist/token-goat-chunk-IA6XSPE3.mjs +218 -0
- package/dist/token-goat-chunk-IAGXMHIK.mjs +863 -0
- package/dist/{token-goat-chunk-435UQTKD.mjs → token-goat-chunk-IK2DRMFX.mjs} +129 -74
- package/dist/{token-goat-chunk-ZER6EAXZ.mjs → token-goat-chunk-INNPTRUX.mjs} +2954 -2783
- package/dist/token-goat-chunk-JJ5BX4F4.mjs +129 -0
- package/dist/token-goat-chunk-JVFSHVFJ.mjs +129 -0
- package/dist/token-goat-chunk-KGEFFSHD.mjs +50 -0
- package/dist/token-goat-chunk-KKUFXAPD.mjs +25 -0
- package/dist/token-goat-chunk-KQBLRPBN.mjs +433 -0
- package/dist/token-goat-chunk-LCNQ2CNN.mjs +497 -0
- 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-2AWQ2R5X.mjs → token-goat-chunk-MHX7XCC3.mjs} +3 -2
- package/dist/token-goat-chunk-MNWKPRAL.mjs +13 -0
- package/dist/token-goat-chunk-N5LSOVSU.mjs +137 -0
- package/dist/{token-goat-chunk-DPNLUJWB.mjs → token-goat-chunk-N5MMR2IW.mjs} +30 -8
- package/dist/token-goat-chunk-N7B3JQ2C.mjs +160 -0
- package/dist/{token-goat-chunk-VMFWNHGR.mjs → token-goat-chunk-NISNMD7T.mjs} +2545 -2953
- package/dist/token-goat-chunk-NMD42SFQ.mjs +434 -0
- package/dist/token-goat-chunk-NOSS5VWY.mjs +174 -0
- package/dist/token-goat-chunk-NSNSPPBG.mjs +69 -0
- package/dist/token-goat-chunk-OBOFIHKN.mjs +38 -0
- package/dist/token-goat-chunk-OBRFJ5O5.mjs +76 -0
- package/dist/token-goat-chunk-ODBT2USM.mjs +800 -0
- 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-ASLWOQ3I.mjs → token-goat-chunk-OYIYKDDT.mjs} +2 -1
- package/dist/{token-goat-chunk-67BNIKKO.mjs → token-goat-chunk-P6HQSWQ3.mjs} +55 -47
- package/dist/token-goat-chunk-P6VGNTQT.mjs +71 -0
- package/dist/token-goat-chunk-PFWIMH2E.mjs +261 -0
- package/dist/token-goat-chunk-PJZXIS3G.mjs +39 -0
- package/dist/token-goat-chunk-PUO5INXP.mjs +606 -0
- package/dist/token-goat-chunk-PX5PQWTY.mjs +462 -0
- package/dist/token-goat-chunk-Q5QDBC6O.mjs +111 -0
- package/dist/token-goat-chunk-QFNQXSDT.mjs +40 -0
- package/dist/token-goat-chunk-QH7YVYOY.mjs +3957 -0
- package/dist/token-goat-chunk-QLMUHEPB.mjs +43 -0
- package/dist/{token-goat-chunk-PYSUADX4.mjs → token-goat-chunk-QQOJIHL3.mjs} +50 -14
- package/dist/token-goat-chunk-QQTWJY2O.mjs +85 -0
- package/dist/token-goat-chunk-QYHO5CPH.mjs +93 -0
- package/dist/token-goat-chunk-RELAVWAD.mjs +27 -0
- package/dist/token-goat-chunk-RSLPOBKQ.mjs +339 -0
- package/dist/token-goat-chunk-SAMFDIS2.mjs +712 -0
- package/dist/token-goat-chunk-SDELXLDM.mjs +48 -0
- package/dist/{token-goat-chunk-PGTH2U4C.mjs → token-goat-chunk-SOAFZNNZ.mjs} +164 -40
- package/dist/{token-goat-chunk-G23DWHV4.mjs → token-goat-chunk-SRTHT7KV.mjs} +33 -15
- package/dist/token-goat-chunk-T5P5ACNQ.mjs +212 -0
- package/dist/token-goat-chunk-T5YGG4V3.mjs +45 -0
- package/dist/token-goat-chunk-TAUZ4OFS.mjs +110 -0
- package/dist/token-goat-chunk-TM6MC754.mjs +40 -0
- package/dist/token-goat-chunk-TWUKBOMP.mjs +72 -0
- package/dist/token-goat-chunk-U2KSB4SG.mjs +56 -0
- package/dist/token-goat-chunk-U4WHMG6Y.mjs +898 -0
- package/dist/token-goat-chunk-UA5V6P3P.mjs +122 -0
- package/dist/token-goat-chunk-UGJHGR5A.mjs +52 -0
- package/dist/{token-goat-chunk-RQGGMPCR.mjs → token-goat-chunk-UOUDVA5U.mjs} +76 -16
- package/dist/{token-goat-chunk-HNFZZHFW.mjs → token-goat-chunk-URRJ33GR.mjs} +22 -4
- package/dist/token-goat-chunk-UZOROKIS.mjs +165 -0
- package/dist/token-goat-chunk-VIHGGGEI.mjs +4234 -0
- package/dist/token-goat-chunk-VTJLAA7P.mjs +21 -0
- package/dist/token-goat-chunk-VWDXLT4F.mjs +678 -0
- package/dist/token-goat-chunk-VWZA2H4P.mjs +368 -0
- package/dist/{token-goat-chunk-QO3LXVVR.mjs → token-goat-chunk-VXEXMJDL.mjs} +2964 -3153
- package/dist/token-goat-chunk-WBA5SKT4.mjs +547 -0
- package/dist/{token-goat-chunk-ONXH6NZ5.mjs → token-goat-chunk-WDH44VAU.mjs} +19 -22
- package/dist/token-goat-chunk-X2SUP3EI.mjs +107 -0
- package/dist/token-goat-chunk-X3BELZSJ.mjs +125 -0
- package/dist/{token-goat-chunk-RLP6J7WR.mjs → token-goat-chunk-XLRBNZCC.mjs} +40 -21
- package/dist/{token-goat-chunk-S4VSXURL.mjs → token-goat-chunk-XRKUX6CM.mjs} +4 -0
- package/dist/token-goat-chunk-XX6O25RN.mjs +82 -0
- package/dist/token-goat-chunk-XZSUHBVS.mjs +65 -0
- package/dist/token-goat-chunk-Y2ZTQOCL.mjs +35 -0
- package/dist/{token-goat-chunk-UA5BKWWQ.mjs → token-goat-chunk-YB4VMGK2.mjs} +15 -4
- package/dist/token-goat-chunk-YBIIY3HR.mjs +51 -0
- package/dist/{token-goat-chunk-PKRWMHVP.mjs → token-goat-chunk-YHVDKQBR.mjs} +389 -221
- package/dist/token-goat-chunk-YJB5XLWI.mjs +26 -0
- package/dist/{token-goat-chunk-PQ53BSCF.mjs → token-goat-chunk-YKB6AQG3.mjs} +53 -31
- package/dist/token-goat-chunk-YMTG2YA5.mjs +35 -0
- package/dist/token-goat-chunk-YPBZYQRV.mjs +40 -0
- package/dist/{token-goat-chunk-HUYUHODZ.mjs → token-goat-chunk-YY6AVUFL.mjs} +13246 -12247
- package/dist/token-goat-chunk-Z7N5OMMP.mjs +98 -0
- package/dist/{token-goat-chunk-BGHSSJUA.mjs → token-goat-chunk-ZFOBKUWH.mjs} +1 -1
- package/dist/token-goat-chunk-ZMBL6UVV.mjs +110 -0
- package/dist/token-goat-chunk-ZNMNUPAZ.mjs +938 -0
- package/dist/token-goat-hook-client.cjs +87 -23
- package/dist/token-goat-hook-client.mjs +3 -3
- package/dist/token-goat-hook.mjs +76 -31
- package/dist/token-goat.core.mjs +6 -6
- package/docs/C4_RUNTIME_ARCHITECTURE.md +2 -2
- package/docs/cli.md +113 -60
- package/docs/install.md +63 -20
- package/docs/security.md +10 -8
- package/package.json +18 -7
- package/scripts/verify-native-dist.mjs +386 -0
- package/dist/token-goat-chunk-2NNUA2LR.mjs +0 -143
- package/dist/token-goat-chunk-44BM7DDQ.mjs +0 -294
- package/dist/token-goat-chunk-BGYOZ6DO.mjs +0 -24
- package/dist/token-goat-chunk-C5YPNZKQ.mjs +0 -318
- package/dist/token-goat-chunk-CIQ2FTY3.mjs +0 -1990
- package/dist/token-goat-chunk-D4ETHJD6.mjs +0 -246
- package/dist/token-goat-chunk-DNRPXHYH.mjs +0 -1062
- package/dist/token-goat-chunk-EUSDXRXU.mjs +0 -1429
- package/dist/token-goat-chunk-FTP4U4QA.mjs +0 -64
- package/dist/token-goat-chunk-G7IWXCFF.mjs +0 -456
- package/dist/token-goat-chunk-GLXRTTLE.mjs +0 -36
- package/dist/token-goat-chunk-H5J3C56Q.mjs +0 -1511
- package/dist/token-goat-chunk-IVWI2OCK.mjs +0 -368
- package/dist/token-goat-chunk-JTKCQMQA.mjs +0 -418
- package/dist/token-goat-chunk-LZNBQTDK.mjs +0 -26
- package/dist/token-goat-chunk-ORNFMIER.mjs +0 -295
- package/dist/token-goat-chunk-RTMVLHWX.mjs +0 -7113
- package/dist/token-goat-chunk-VQ6F6T7X.mjs +0 -36
- package/dist/token-goat-chunk-VZQEWRVH.mjs +0 -312
- package/dist/token-goat-chunk-XYWPOSCN.mjs +0 -807
- package/dist/token-goat-chunk-YAGLESS3.mjs +0 -23
- package/dist/token-goat-chunk-YKSRAWUU.mjs +0 -33
- package/dist/token-goat-chunk-Z6KBMOBI.mjs +0 -53
|
@@ -0,0 +1,3957 @@
|
|
|
1
|
+
import { createRequire as __cjsRequire } from 'node:module';
|
|
2
|
+
const require = __cjsRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
findVerifiedFileEvidence,
|
|
5
|
+
readEvidenceFileText
|
|
6
|
+
} from "./token-goat-chunk-NOSS5VWY.mjs";
|
|
7
|
+
import {
|
|
8
|
+
getContextPressure,
|
|
9
|
+
loadSessionCache,
|
|
10
|
+
readAllSessionManifests
|
|
11
|
+
} from "./token-goat-chunk-NMD42SFQ.mjs";
|
|
12
|
+
import {
|
|
13
|
+
contentHash,
|
|
14
|
+
getCompactAnySessionSync,
|
|
15
|
+
isCompactStale
|
|
16
|
+
} from "./token-goat-chunk-SAMFDIS2.mjs";
|
|
17
|
+
import {
|
|
18
|
+
applyHintTracking,
|
|
19
|
+
classifyReadHint,
|
|
20
|
+
logSuppressedDetection,
|
|
21
|
+
meetsSavingsFloor
|
|
22
|
+
} from "./token-goat-chunk-4SMRO4TO.mjs";
|
|
23
|
+
import {
|
|
24
|
+
DELIVERS_CONTENT_RE
|
|
25
|
+
} from "./token-goat-chunk-MNWKPRAL.mjs";
|
|
26
|
+
import {
|
|
27
|
+
enqueueDirtyPathSafe
|
|
28
|
+
} from "./token-goat-chunk-CIMQ2BB3.mjs";
|
|
29
|
+
import {
|
|
30
|
+
claudeConfigDir
|
|
31
|
+
} from "./token-goat-chunk-BPGCUMR7.mjs";
|
|
32
|
+
import {
|
|
33
|
+
getBashOutput
|
|
34
|
+
} from "./token-goat-chunk-KQBLRPBN.mjs";
|
|
35
|
+
import {
|
|
36
|
+
DEFAULT_MAX_AGE_MS,
|
|
37
|
+
isGeneratedFile,
|
|
38
|
+
isInBuildDir,
|
|
39
|
+
isLockFile,
|
|
40
|
+
isManifestFile
|
|
41
|
+
} from "./token-goat-chunk-4ZBFWLXF.mjs";
|
|
42
|
+
import {
|
|
43
|
+
materializeShrunkImageFile,
|
|
44
|
+
registerHook,
|
|
45
|
+
sessionStateKey
|
|
46
|
+
} from "./token-goat-chunk-C75NA7PL.mjs";
|
|
47
|
+
import {
|
|
48
|
+
commandPathIsTouchable,
|
|
49
|
+
preToolPathDeclined,
|
|
50
|
+
vscodePathAllowed
|
|
51
|
+
} from "./token-goat-chunk-P6VGNTQT.mjs";
|
|
52
|
+
import {
|
|
53
|
+
OUTPUT_FIRST_TOOL_RESPONSE_KEYS,
|
|
54
|
+
VSCODE_TOOL_NAME_KEY,
|
|
55
|
+
contextOutput,
|
|
56
|
+
denyOutput,
|
|
57
|
+
extractToolResponseField,
|
|
58
|
+
getCwd,
|
|
59
|
+
getFilePath,
|
|
60
|
+
passOutput,
|
|
61
|
+
resolveEventPath,
|
|
62
|
+
resolveToolResponseFieldPath
|
|
63
|
+
} from "./token-goat-chunk-76FGDLRO.mjs";
|
|
64
|
+
import {
|
|
65
|
+
clearReadRangeIfChanged,
|
|
66
|
+
dropFileLineRangesIfChanged,
|
|
67
|
+
epochReadCounts,
|
|
68
|
+
evaluateRangeEvasion,
|
|
69
|
+
getCompactedAt,
|
|
70
|
+
getFileLineRanges,
|
|
71
|
+
getFileServedOutputs,
|
|
72
|
+
getReadRangeDenials,
|
|
73
|
+
getRecentReadRanges,
|
|
74
|
+
getSessionFileEntry,
|
|
75
|
+
getSessionFiles,
|
|
76
|
+
getSessionId,
|
|
77
|
+
getTranscriptPath,
|
|
78
|
+
lockReadRange,
|
|
79
|
+
markHintShown,
|
|
80
|
+
rangeLockMessage,
|
|
81
|
+
recordFileLineRange,
|
|
82
|
+
recordFileRead,
|
|
83
|
+
recordLargeFileHintPending,
|
|
84
|
+
recordRecentReadRange,
|
|
85
|
+
resetFileLineRanges,
|
|
86
|
+
takePendingLargeFileHint,
|
|
87
|
+
unrecordRefusedRead,
|
|
88
|
+
wasFileFullyReadThisSession,
|
|
89
|
+
wasFileReadThisSession,
|
|
90
|
+
wasFileTruncatedThisSession,
|
|
91
|
+
wasHintShown
|
|
92
|
+
} from "./token-goat-chunk-GL3ETKSC.mjs";
|
|
93
|
+
import {
|
|
94
|
+
extractShellBannerHeading
|
|
95
|
+
} from "./token-goat-chunk-OXFMP75N.mjs";
|
|
96
|
+
import {
|
|
97
|
+
MARKDOWN_SIZE_THRESHOLD,
|
|
98
|
+
eachUnfencedLine,
|
|
99
|
+
extractChangelogVersionHint,
|
|
100
|
+
extractMarkdownHeadings,
|
|
101
|
+
formatHeadingTreeParts,
|
|
102
|
+
getWellKnownSections,
|
|
103
|
+
ipynbToVirtualSource,
|
|
104
|
+
isImagePath,
|
|
105
|
+
normalizeSourceText
|
|
106
|
+
} from "./token-goat-chunk-NISNMD7T.mjs";
|
|
107
|
+
import {
|
|
108
|
+
ATX_DOT_BREAK_RE,
|
|
109
|
+
matchAtxHeading
|
|
110
|
+
} from "./token-goat-chunk-EVMZJ27Z.mjs";
|
|
111
|
+
import {
|
|
112
|
+
commonGitDir,
|
|
113
|
+
getFileEntry,
|
|
114
|
+
querySymbols
|
|
115
|
+
} from "./token-goat-chunk-IK2DRMFX.mjs";
|
|
116
|
+
import {
|
|
117
|
+
applyExifOrientation,
|
|
118
|
+
calculateFitInside,
|
|
119
|
+
decodeBmp,
|
|
120
|
+
decodeGif,
|
|
121
|
+
decodeJpeg,
|
|
122
|
+
decodePng,
|
|
123
|
+
encodeJpeg,
|
|
124
|
+
encodePng,
|
|
125
|
+
probeBufferMeta,
|
|
126
|
+
resizeRgba
|
|
127
|
+
} from "./token-goat-chunk-5Y6ZYL6U.mjs";
|
|
128
|
+
import {
|
|
129
|
+
recordStat,
|
|
130
|
+
savedTokensFromBytes
|
|
131
|
+
} from "./token-goat-chunk-U4WHMG6Y.mjs";
|
|
132
|
+
import {
|
|
133
|
+
detectLanguage,
|
|
134
|
+
filePathSpellingsClause,
|
|
135
|
+
fileRowByIndexKey,
|
|
136
|
+
languageHasFlag,
|
|
137
|
+
withProbeIndex
|
|
138
|
+
} from "./token-goat-chunk-GVB2MUST.mjs";
|
|
139
|
+
import {
|
|
140
|
+
findHtmlHeadingMatches
|
|
141
|
+
} from "./token-goat-chunk-OUKDTVNU.mjs";
|
|
142
|
+
import {
|
|
143
|
+
parse
|
|
144
|
+
} from "./token-goat-chunk-3BTK54F3.mjs";
|
|
145
|
+
import {
|
|
146
|
+
windowsSystem32Exe
|
|
147
|
+
} from "./token-goat-chunk-65N4RHP4.mjs";
|
|
148
|
+
import {
|
|
149
|
+
loadConfig
|
|
150
|
+
} from "./token-goat-chunk-7EZTZOMC.mjs";
|
|
151
|
+
import {
|
|
152
|
+
detectHarness
|
|
153
|
+
} from "./token-goat-chunk-QYHO5CPH.mjs";
|
|
154
|
+
import {
|
|
155
|
+
findProject,
|
|
156
|
+
fingerprintContent,
|
|
157
|
+
fingerprintFile,
|
|
158
|
+
makeProjectAt,
|
|
159
|
+
shortFingerprint
|
|
160
|
+
} from "./token-goat-chunk-4GBVPH3D.mjs";
|
|
161
|
+
import {
|
|
162
|
+
IDENTICAL_READ_MIN_BODY_BYTES,
|
|
163
|
+
PER_FILE_COUNTERFACTUAL_CEILING,
|
|
164
|
+
atomicWriteBytes,
|
|
165
|
+
atomicWriteText,
|
|
166
|
+
containsLineRun,
|
|
167
|
+
countNoun,
|
|
168
|
+
decodeSource,
|
|
169
|
+
ensureDirSync,
|
|
170
|
+
isSequentialPagingChain,
|
|
171
|
+
isWithinQuietHours,
|
|
172
|
+
runGit,
|
|
173
|
+
sanitizeIdForFilename,
|
|
174
|
+
statSize,
|
|
175
|
+
toKB
|
|
176
|
+
} from "./token-goat-chunk-HU4TORC5.mjs";
|
|
177
|
+
import {
|
|
178
|
+
configGetCommand,
|
|
179
|
+
docNavigation,
|
|
180
|
+
fencedCommand,
|
|
181
|
+
fileSubject,
|
|
182
|
+
grepLinesHint,
|
|
183
|
+
leadWithCommand,
|
|
184
|
+
nameSubject,
|
|
185
|
+
quotedArg,
|
|
186
|
+
quotedArgs,
|
|
187
|
+
sentenceStart,
|
|
188
|
+
stripUnsafeSuggestions
|
|
189
|
+
} from "./token-goat-chunk-GGGG7RWB.mjs";
|
|
190
|
+
import {
|
|
191
|
+
foldPath,
|
|
192
|
+
foldPathForContainment,
|
|
193
|
+
isInsideRoot,
|
|
194
|
+
isNodeModulesPath
|
|
195
|
+
} from "./token-goat-chunk-AM5V6GSX.mjs";
|
|
196
|
+
import {
|
|
197
|
+
TOOL_RESULTS_ID_CHARS,
|
|
198
|
+
displaySafePath,
|
|
199
|
+
displaySafeText,
|
|
200
|
+
fenceUntrustedFileContent,
|
|
201
|
+
hostPathOfIndexKey,
|
|
202
|
+
isUncOrDevicePath,
|
|
203
|
+
normalizePath,
|
|
204
|
+
resolveIndexPath
|
|
205
|
+
} from "./token-goat-chunk-FNFBKLRD.mjs";
|
|
206
|
+
import {
|
|
207
|
+
dataDir,
|
|
208
|
+
tokenGoatHome
|
|
209
|
+
} from "./token-goat-chunk-CO3UYX6G.mjs";
|
|
210
|
+
import {
|
|
211
|
+
init_define_import_meta_env
|
|
212
|
+
} from "./token-goat-chunk-A37V4PBF.mjs";
|
|
213
|
+
|
|
214
|
+
// src/indexed_source.ts
|
|
215
|
+
init_define_import_meta_env();
|
|
216
|
+
function indexedSourceText(filePath, raw) {
|
|
217
|
+
const text = normalizeSourceText(raw);
|
|
218
|
+
return isVirtualIndexedPath(filePath) ? normalizeSourceText(ipynbToVirtualSource(text).content) : text;
|
|
219
|
+
}
|
|
220
|
+
function isVirtualIndexedPath(filePath) {
|
|
221
|
+
return filePath.toLowerCase().endsWith(".ipynb");
|
|
222
|
+
}
|
|
223
|
+
function virtualIndexedScopeNote(displayPath, consequence) {
|
|
224
|
+
return `[${displayPath} is a notebook: its indexed line numbers address the flattened cell source, not the JSON in the file, so ${consequence}.]`;
|
|
225
|
+
}
|
|
226
|
+
var NOTEBOOK_CELL_LINES_SUFFIX = " (notebook cell lines)";
|
|
227
|
+
function formatSymbolLocation(displayPath, lineStart, lineEnd) {
|
|
228
|
+
const range = lineEnd === void 0 ? `${lineStart}` : `${lineStart}-${lineEnd}`;
|
|
229
|
+
const suffix = isVirtualIndexedPath(displayPath) ? NOTEBOOK_CELL_LINES_SUFFIX : "";
|
|
230
|
+
return `${displayPath}:${range}${suffix}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// src/rewrite_permission.ts
|
|
234
|
+
init_define_import_meta_env();
|
|
235
|
+
import { execFile, spawnSync } from "node:child_process";
|
|
236
|
+
import * as fs from "node:fs";
|
|
237
|
+
import * as os from "node:os";
|
|
238
|
+
import * as path from "node:path";
|
|
239
|
+
function agentTypeOf(raw) {
|
|
240
|
+
const agentType = raw["agent_type"];
|
|
241
|
+
return typeof agentType === "string" ? agentType : void 0;
|
|
242
|
+
}
|
|
243
|
+
var KNOWN_MODES = /* @__PURE__ */ new Set(["default", "plan", "acceptEdits", "auto", "dontAsk", "bypassPermissions"]);
|
|
244
|
+
var READ_ONLY_PLAIN = /* @__PURE__ */ new Set(["ls", "cat", "echo", "pwd", "head", "tail", "grep", "wc", "which", "diff", "stat", "du"]);
|
|
245
|
+
var READ_ONLY_FIRST_WORDS = /* @__PURE__ */ new Set([...READ_ONLY_PLAIN, "find", "git", "cd"]);
|
|
246
|
+
var GIT_READ_SUBCOMMANDS = /* @__PURE__ */ new Set(["status", "diff", "log", "show"]);
|
|
247
|
+
var GIT_READ_FLAG = /^(?:-s|--short|-b|--branch|--porcelain(?:=v[12])?|--stat|--shortstat|--numstat|--name-only|--name-status|--cached|--staged|-p|--patch|--no-patch|--oneline|--graph|--decorate|--all|--no-color|--summary|-n\d*|-\d+|--max-count=\d+|--format=\S*|--pretty=\S*|--)$/;
|
|
248
|
+
var FIND_WRITE_FLAGS = /* @__PURE__ */ new Set(["-exec", "-execdir", "-ok", "-okdir", "-delete", "-fprint", "-fprint0", "-fprintf", "-fls"]);
|
|
249
|
+
var UNPROVABLE_FIRST_WORDS = /* @__PURE__ */ new Set(["timeout", "time", "nice", "nohup", "stdbuf", "command", "builtin", "noglob", "xargs", "watch", "setsid", "ionice", "flock", "env", "sudo", "doas", "exec", "eval", "source", ".", "sh", "bash", "zsh", "dash", "ksh", "fish", "pwsh", "powershell", "cmd", "cmd.exe", "nocorrect"]);
|
|
250
|
+
var PROTECTED_PATH_FRAGMENTS = [".git", ".config/git", ".vscode", ".idea", ".husky", ".cargo", ".devcontainer", ".yarn", ".mvn", ".claude", ".bashrc", ".zshrc", ".profile", ".envrc", ".npmrc", ".pnp.", "bunfig.toml", ".bazelrc", ".pre-commit-config.yaml", "lefthook.yml", ".mcp.json"];
|
|
251
|
+
var RM_WORD = /(?:^|[^a-z0-9_.-])(?:rm|rmdir)(?:[^a-z0-9_-]|$)/;
|
|
252
|
+
function namesRmOrProtectedPath(text) {
|
|
253
|
+
const lowered = text.toLowerCase();
|
|
254
|
+
return RM_WORD.test(lowered) || PROTECTED_PATH_FRAGMENTS.some((p) => lowered.includes(p));
|
|
255
|
+
}
|
|
256
|
+
var INTERPRETER_WORDS = /* @__PURE__ */ new Set(["python", "python3", "py", "node", "deno", "bun", "perl", "ruby", "php", "lua", "awk", "gawk", "busybox", "ssh"]);
|
|
257
|
+
function isSimpleCommand(command) {
|
|
258
|
+
const words = simpleWords(command);
|
|
259
|
+
return words !== null && !words.some((w) => /[{}[\]*?~!#|;&<>()^]/.test(w)) && !words.some((w, i) => {
|
|
260
|
+
const base = (w.split("/").pop() ?? w).toLowerCase().replace(/\.exe$/, "");
|
|
261
|
+
return (base !== "." || i === 0) && (UNPROVABLE_FIRST_WORDS.has(base) || INTERPRETER_WORDS.has(base));
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
var FILE_RULE_TOOLS = ["read", "edit", "write", "multiedit", "notebookedit"];
|
|
265
|
+
var BASH_COMMAND_CEILING = 1e4;
|
|
266
|
+
function normalizedMode(mode) {
|
|
267
|
+
if (mode === void 0 || mode === null || mode === "") return "default";
|
|
268
|
+
return typeof mode === "string" && KNOWN_MODES.has(mode) ? mode : null;
|
|
269
|
+
}
|
|
270
|
+
function parseRule(raw, trustedAllow) {
|
|
271
|
+
if (typeof raw !== "string") return { raw: String(raw), tool: void 0, content: void 0, trustedAllow: false };
|
|
272
|
+
const m = /^([A-Za-z0-9_*.-]+)(?:\(([\s\S]*)\))?$/.exec(raw.trim());
|
|
273
|
+
if (m === null) return { raw, tool: void 0, content: void 0, trustedAllow: false };
|
|
274
|
+
return { raw, tool: m[1], content: m[2], trustedAllow };
|
|
275
|
+
}
|
|
276
|
+
function wildcardMatches(pattern, text) {
|
|
277
|
+
let p = 0;
|
|
278
|
+
let t = 0;
|
|
279
|
+
let star = -1;
|
|
280
|
+
let resume = 0;
|
|
281
|
+
while (t < text.length) {
|
|
282
|
+
if (p < pattern.length && pattern[p] !== "*" && pattern[p] === text[t]) {
|
|
283
|
+
p++;
|
|
284
|
+
t++;
|
|
285
|
+
} else if (p < pattern.length && pattern[p] === "*") {
|
|
286
|
+
star = p++;
|
|
287
|
+
resume = t;
|
|
288
|
+
} else if (star >= 0) {
|
|
289
|
+
p = star + 1;
|
|
290
|
+
t = ++resume;
|
|
291
|
+
} else {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
while (p < pattern.length && pattern[p] === "*") p++;
|
|
296
|
+
return p === pattern.length;
|
|
297
|
+
}
|
|
298
|
+
function ruleToolIs(rule, names) {
|
|
299
|
+
if (rule.tool === void 0) return false;
|
|
300
|
+
const tool = rule.tool.toLowerCase();
|
|
301
|
+
return names.some((n) => wildcardMatches(tool, n));
|
|
302
|
+
}
|
|
303
|
+
function haystacks(parts) {
|
|
304
|
+
const base = parts.join("\n").toLowerCase().replace(/["']/g, "");
|
|
305
|
+
return [base.split("\\").join("/"), base.split("\\").join("")];
|
|
306
|
+
}
|
|
307
|
+
function realSpellings(p, cwd) {
|
|
308
|
+
const abs = path.resolve(cwd, p);
|
|
309
|
+
if (isUncOrDevicePath(p) || isUncOrDevicePath(abs)) return [];
|
|
310
|
+
let existing = abs;
|
|
311
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
312
|
+
try {
|
|
313
|
+
const real = path.join(fs.realpathSync.native(existing), path.relative(existing, abs));
|
|
314
|
+
const same = process.platform === "win32" ? real.toLowerCase() === abs.toLowerCase() : real === abs;
|
|
315
|
+
return same ? [] : [real];
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
const parent = path.dirname(existing);
|
|
319
|
+
if (parent === existing) break;
|
|
320
|
+
existing = parent;
|
|
321
|
+
}
|
|
322
|
+
return [];
|
|
323
|
+
}
|
|
324
|
+
function shellPathWords(command) {
|
|
325
|
+
return command.split(/[\s;&|<>()=,'"`]+/).filter((w) => w !== "" && !w.startsWith("-")).slice(0, 64);
|
|
326
|
+
}
|
|
327
|
+
function isWordChar(ch) {
|
|
328
|
+
return ch !== void 0 && /[a-z0-9]/.test(ch);
|
|
329
|
+
}
|
|
330
|
+
function containsPiece(text, piece) {
|
|
331
|
+
let at = text.indexOf(piece.text);
|
|
332
|
+
while (at >= 0) {
|
|
333
|
+
if ((piece.openLeft || !isWordChar(text[at - 1])) && (piece.openRight || !isWordChar(text[at + piece.text.length]))) return true;
|
|
334
|
+
at = text.indexOf(piece.text, at + 1);
|
|
335
|
+
}
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
function bashRuleMayMatch(rule, hays) {
|
|
339
|
+
if (rule.content === void 0) return true;
|
|
340
|
+
const pieces = [];
|
|
341
|
+
for (const token of rule.content.replace(/:\*\s*$/, " ").split(/\s+/)) {
|
|
342
|
+
const bits = token.split("*");
|
|
343
|
+
bits.forEach((bit, i) => {
|
|
344
|
+
const text = bit.replace(/["'\\]/g, "").toLowerCase();
|
|
345
|
+
if (text !== "") pieces.push({ text, openLeft: i > 0, openRight: i < bits.length - 1 });
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
if (pieces.length === 0) return true;
|
|
349
|
+
return pieces.every((p) => hays.some((h) => containsPiece(h, p)));
|
|
350
|
+
}
|
|
351
|
+
function pathRuleMayMatch(rule, hays) {
|
|
352
|
+
const content = rule.content;
|
|
353
|
+
if (content === void 0) return true;
|
|
354
|
+
if (/[{}[\]\\]/.test(content)) return true;
|
|
355
|
+
const segments = content.replace(/^(?:\/\/|~\/|\.\/|\/)/, "").split("/");
|
|
356
|
+
for (let i = segments.length - 1; i >= 0; i--) {
|
|
357
|
+
const pieces = (segments[i] ?? "").split(/[*?]+/).filter((p) => p !== "").map((p) => p.toLowerCase());
|
|
358
|
+
if (pieces.length > 0) return pieces.every((p) => hays.some((h) => h.includes(p)));
|
|
359
|
+
}
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
function bashGlobMatches(content, command) {
|
|
363
|
+
if (!content.endsWith(":*") && !content.endsWith(" *")) return wildcardMatches(content, command);
|
|
364
|
+
const prefix = content.slice(0, -2);
|
|
365
|
+
return wildcardMatches(prefix, command) || wildcardMatches(`${prefix} *`, command) || wildcardMatches(`${prefix} *`, command);
|
|
366
|
+
}
|
|
367
|
+
function simpleWords(command) {
|
|
368
|
+
const words = [];
|
|
369
|
+
let current = "";
|
|
370
|
+
let inWord = false;
|
|
371
|
+
for (let i = 0; i < command.length; i++) {
|
|
372
|
+
const ch = command[i];
|
|
373
|
+
if (ch === " " || ch === " ") {
|
|
374
|
+
if (inWord) words.push(current);
|
|
375
|
+
current = "";
|
|
376
|
+
inWord = false;
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
inWord = true;
|
|
380
|
+
if (ch === "'" || ch === '"') {
|
|
381
|
+
const close = command.indexOf(ch, i + 1);
|
|
382
|
+
if (close < 0) return null;
|
|
383
|
+
const quoted = command.slice(i + 1, close);
|
|
384
|
+
if (ch === '"' && /[$`\\!]/.test(quoted)) return null;
|
|
385
|
+
current += quoted;
|
|
386
|
+
i = close;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (!/[A-Za-z0-9_./:=@%+,-]/.test(ch)) return null;
|
|
390
|
+
current += ch;
|
|
391
|
+
}
|
|
392
|
+
if (inWord) words.push(current);
|
|
393
|
+
return words.length > 0 ? words : null;
|
|
394
|
+
}
|
|
395
|
+
function provenAllowed(snapshot, command) {
|
|
396
|
+
if (command.length > BASH_COMMAND_CEILING) return false;
|
|
397
|
+
const words = simpleWords(command);
|
|
398
|
+
if (words === null) return false;
|
|
399
|
+
const first = words[0];
|
|
400
|
+
if (first.includes("=") || UNPROVABLE_FIRST_WORDS.has(first)) return false;
|
|
401
|
+
if (first === "find" && words.some((w) => FIND_WRITE_FLAGS.has(w))) return false;
|
|
402
|
+
if (READ_ONLY_PLAIN.has(first)) return true;
|
|
403
|
+
if (first === "git" && GIT_READ_SUBCOMMANDS.has(words[1] ?? "") && words.slice(2).every((w) => !w.startsWith("-") || GIT_READ_FLAG.test(w))) return true;
|
|
404
|
+
return snapshot.allow.some((r) => r.trustedAllow && r.tool === "Bash" && (r.content === void 0 || bashGlobMatches(r.content, command)));
|
|
405
|
+
}
|
|
406
|
+
function allSubcommandsReadOnly(command) {
|
|
407
|
+
const parts = command.split(/&&|\|\||[;|\n]/).map((p) => p.trim()).filter((p) => p !== "");
|
|
408
|
+
if (parts.length === 0) return false;
|
|
409
|
+
return parts.every((p) => {
|
|
410
|
+
const first = /^\S+/.exec(p.replace(/^(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*/, ""))?.[0] ?? "";
|
|
411
|
+
return READ_ONLY_FIRST_WORDS.has(first.replace(/["']/g, ""));
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
function decideShell(snapshot, req, mode) {
|
|
415
|
+
if (snapshot.blockReadsOutside) return "skip";
|
|
416
|
+
const original = req.original.trim();
|
|
417
|
+
const lowered = original.toLowerCase();
|
|
418
|
+
if (RM_WORD.test(lowered)) return "skip";
|
|
419
|
+
if (PROTECTED_PATH_FRAGMENTS.some((p) => lowered.includes(p))) return "skip";
|
|
420
|
+
const linked = shellPathWords(original).flatMap((w) => realSpellings(w, req.cwd).map((real) => ({ real, lexical: path.resolve(req.cwd, w) })));
|
|
421
|
+
if (linked.some(({ real, lexical }) => PROTECTED_PATH_FRAGMENTS.some((p) => haystacks([real])[0]?.includes(p) === true && haystacks([lexical])[0]?.includes(p) !== true))) return "skip";
|
|
422
|
+
const hays = haystacks([req.cwd, original, req.rewritten]);
|
|
423
|
+
const realHays = haystacks(linked.map(({ real }) => real));
|
|
424
|
+
const expands = /[$`\\]/.test(original);
|
|
425
|
+
const pathExpands = /[$`~*?[]/.test(original);
|
|
426
|
+
for (const rule of [...snapshot.deny, ...snapshot.ask]) {
|
|
427
|
+
if (rule.tool === void 0) return "skip";
|
|
428
|
+
if (ruleToolIs(rule, ["bash"])) {
|
|
429
|
+
if (expands || bashRuleMayMatch(rule, hays)) return "skip";
|
|
430
|
+
} else if (ruleToolIs(rule, FILE_RULE_TOOLS)) {
|
|
431
|
+
if (pathExpands || pathRuleMayMatch(rule, hays) || linked.length > 0 && pathRuleMayMatch(rule, realHays)) return "skip";
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
if (mode === "bypassPermissions") return "rewrite";
|
|
435
|
+
if (mode !== "plan" && provenAllowed(snapshot, original)) return "approve";
|
|
436
|
+
if (mode === "dontAsk") return "skip";
|
|
437
|
+
const allowHays = req.kind === "shell-wrap" ? hays : haystacks([original]);
|
|
438
|
+
for (const rule of snapshot.allow) {
|
|
439
|
+
if (!ruleToolIs(rule, ["bash"])) continue;
|
|
440
|
+
if (req.kind === "shell-wrap" && rule.content !== void 0 && !rule.content.includes("*") && rule.content === req.rewritten) continue;
|
|
441
|
+
if (bashRuleMayMatch(rule, allowHays)) return "skip";
|
|
442
|
+
}
|
|
443
|
+
if (allSubcommandsReadOnly(original)) return "skip";
|
|
444
|
+
return "rewrite";
|
|
445
|
+
}
|
|
446
|
+
function readRuleMayCover(snapshot, hays) {
|
|
447
|
+
for (const rule of [...snapshot.deny, ...snapshot.ask]) {
|
|
448
|
+
if (rule.tool === void 0) return true;
|
|
449
|
+
if (ruleToolIs(rule, ["read"]) && pathRuleMayMatch(rule, hays)) return true;
|
|
450
|
+
}
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
function decideRead(snapshot, req) {
|
|
454
|
+
if (snapshot.blockReadsOutside) return "skip";
|
|
455
|
+
if (readRuleMayCover(snapshot, haystacks([req.original, req.rewritten, ...realSpellings(req.original, req.cwd), ...realSpellings(req.rewritten, req.cwd)]))) return "skip";
|
|
456
|
+
if (req.harness !== "claudecode") return "rewrite";
|
|
457
|
+
return req.insideCwd === true ? "approve" : "skip";
|
|
458
|
+
}
|
|
459
|
+
function decideAgent(snapshot) {
|
|
460
|
+
for (const rule of [...snapshot.deny, ...snapshot.ask]) {
|
|
461
|
+
if (rule.tool === void 0) return "skip";
|
|
462
|
+
if (ruleToolIs(rule, ["agent", "task"]) && rule.content !== void 0 && /[:*]/.test(rule.content)) return "skip";
|
|
463
|
+
}
|
|
464
|
+
return "rewrite";
|
|
465
|
+
}
|
|
466
|
+
function isOtherHostEntry(entrypoint) {
|
|
467
|
+
return entrypoint !== void 0 && entrypoint !== "" && !/^(sdk-)?cli$/.test(entrypoint);
|
|
468
|
+
}
|
|
469
|
+
var unseenRules;
|
|
470
|
+
var primeAhead = false;
|
|
471
|
+
function decideRewrite(snapshot, req, hidden = (r, filesOnly) => unseenRules?.hidden(r.cwd, r.agentType, filesOnly) ?? true, lineRules = () => unseenRules?.lineRules() ?? !isOtherHostEntry(process.env["CLAUDE_CODE_ENTRYPOINT"])) {
|
|
472
|
+
if (snapshot === null) return "skip";
|
|
473
|
+
const mode = req.harness === "claudecode" ? normalizedMode(req.mode) : "default";
|
|
474
|
+
if (mode === null || mode === "auto") return "skip";
|
|
475
|
+
let verdict;
|
|
476
|
+
switch (req.kind) {
|
|
477
|
+
case "shell-wrap":
|
|
478
|
+
case "shell-query":
|
|
479
|
+
verdict = decideShell(snapshot, req, mode);
|
|
480
|
+
break;
|
|
481
|
+
case "read":
|
|
482
|
+
verdict = decideRead(snapshot, req);
|
|
483
|
+
break;
|
|
484
|
+
case "agent":
|
|
485
|
+
verdict = decideAgent(snapshot);
|
|
486
|
+
}
|
|
487
|
+
if (verdict === "skip") return verdict;
|
|
488
|
+
if (mode !== "bypassPermissions") return verdict === "approve" && (req.harness === "claudecode" && lineRules(req) || snapshot.permissionHooks || hidden(req, true)) ? "skip" : verdict;
|
|
489
|
+
return snapshot.permissionHooks || hidden(req, false) ? "skip" : "approve";
|
|
490
|
+
}
|
|
491
|
+
function asObject(value) {
|
|
492
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
493
|
+
}
|
|
494
|
+
function snapshotFromDocs(docs) {
|
|
495
|
+
const allow = [];
|
|
496
|
+
const deny = [];
|
|
497
|
+
const ask = [];
|
|
498
|
+
let blockReadsOutside = false;
|
|
499
|
+
const managedGroups = new Set(docs.filter((d) => d.role === "managed").map((d) => d.managedGroup ?? "managed"));
|
|
500
|
+
const managedOnly = docs.some((d) => d.role === "managed" && d.json["allowManagedPermissionRulesOnly"] !== void 0 && d.json["allowManagedPermissionRulesOnly"] !== false);
|
|
501
|
+
let permissionHooks = false;
|
|
502
|
+
for (const doc of docs) {
|
|
503
|
+
if (asObject(doc.json["hooks"])?.["PermissionRequest"] !== void 0) permissionHooks = true;
|
|
504
|
+
const permissions = doc.json["permissions"];
|
|
505
|
+
if (permissions === void 0) continue;
|
|
506
|
+
const perms = asObject(permissions);
|
|
507
|
+
if (perms === null) throw new Error("permissions is not an object");
|
|
508
|
+
const block = perms["blockReadsOutsideWorkingDirectories"];
|
|
509
|
+
if (block !== void 0 && block !== false) blockReadsOutside = true;
|
|
510
|
+
let trusted;
|
|
511
|
+
switch (doc.role) {
|
|
512
|
+
case "managed":
|
|
513
|
+
trusted = managedGroups.size === 1;
|
|
514
|
+
break;
|
|
515
|
+
case "user":
|
|
516
|
+
trusted = !managedOnly;
|
|
517
|
+
break;
|
|
518
|
+
case "local":
|
|
519
|
+
trusted = !managedOnly && doc.localTrusted === true;
|
|
520
|
+
break;
|
|
521
|
+
default:
|
|
522
|
+
trusted = false;
|
|
523
|
+
}
|
|
524
|
+
for (const [key, list] of [["allow", allow], ["deny", deny], ["ask", ask]]) {
|
|
525
|
+
const entries = perms[key];
|
|
526
|
+
if (entries === void 0) continue;
|
|
527
|
+
if (!Array.isArray(entries)) throw new Error(`permissions.${key} is not an array`);
|
|
528
|
+
if (key === "allow" && doc.role === "ancestor") continue;
|
|
529
|
+
for (const entry of entries) list.push(parseRule(entry, key === "allow" && trusted));
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return { allow, deny, ask, blockReadsOutside, permissionHooks };
|
|
533
|
+
}
|
|
534
|
+
var SOURCE_FILTER_SLOT = /* @__PURE__ */ Symbol.for("token-goat.permission-source-filter");
|
|
535
|
+
function sourceAllowed(source) {
|
|
536
|
+
const filter = globalThis[SOURCE_FILTER_SLOT];
|
|
537
|
+
return typeof filter !== "function" || filter(source) === true;
|
|
538
|
+
}
|
|
539
|
+
function readIfExists(file, list) {
|
|
540
|
+
if (!sourceAllowed(file)) return void 0;
|
|
541
|
+
try {
|
|
542
|
+
return list === true ? fs.readdirSync(file) : fs.readFileSync(file, "utf8");
|
|
543
|
+
} catch (err) {
|
|
544
|
+
const code = err.code;
|
|
545
|
+
if (code === "ENOENT" || code === "ENOTDIR") return void 0;
|
|
546
|
+
throw err;
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
function readSettingsFile(file) {
|
|
550
|
+
const text = readIfExists(file);
|
|
551
|
+
if (text === void 0) return void 0;
|
|
552
|
+
const json = asObject(JSON.parse(text));
|
|
553
|
+
if (json === null) throw new Error(`${file} is not a JSON object`);
|
|
554
|
+
return json;
|
|
555
|
+
}
|
|
556
|
+
var REG_ABSENT = "ERROR: The system was unable to find the specified registry key or value.";
|
|
557
|
+
var REG_SENTINEL = "HKLM\\SOFTWARE\\TokenGoatNoSuchKey";
|
|
558
|
+
var REG_OPTS = { encoding: "utf8", windowsHide: true, timeout: 5e3 };
|
|
559
|
+
var regArgs = (key) => ["query", key, "/v", "Settings"];
|
|
560
|
+
var regQuery = (key) => spawnSync(windowsSystem32Exe("reg.exe"), regArgs(key), REG_OPTS);
|
|
561
|
+
function regQueryAsync(key) {
|
|
562
|
+
return new Promise((resolve3) => {
|
|
563
|
+
execFile(windowsSystem32Exe("reg.exe"), regArgs(key), REG_OPTS, (err, stdout, stderr) => {
|
|
564
|
+
const status = err === null ? 0 : typeof err.code === "number" && err.killed !== true ? err.code : null;
|
|
565
|
+
resolve3({ status, stdout, stderr });
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
var sentinelMessage = (res) => res.status === 1 ? res.stderr.trim() : "";
|
|
570
|
+
function registrySettings(key, res, missing) {
|
|
571
|
+
if (res.status === 1) {
|
|
572
|
+
if (res.stderr.trim() === REG_ABSENT) return void 0;
|
|
573
|
+
const none = missing();
|
|
574
|
+
if (none !== "" && res.stderr.trim() === none) return void 0;
|
|
575
|
+
}
|
|
576
|
+
const json = res.status === 0 && !res.stdout.includes("\uFFFD") ? asObject(JSON.parse(/^\s*Settings\s+REG_(?:EXPAND_)?SZ\s(.*)$/m.exec(res.stdout)?.[1]?.trim() ?? "null")) : null;
|
|
577
|
+
if (json === null) throw new Error(`unreadable ${key}`);
|
|
578
|
+
return json;
|
|
579
|
+
}
|
|
580
|
+
function readRegistrySettings(keys) {
|
|
581
|
+
let missing;
|
|
582
|
+
return keys.map((key) => registrySettings(key, regQuery(key), () => missing ??= sentinelMessage(regQuery(REG_SENTINEL))));
|
|
583
|
+
}
|
|
584
|
+
async function readRegistrySettingsAsync(keys) {
|
|
585
|
+
const answers = await Promise.all(keys.map(regQueryAsync));
|
|
586
|
+
const needsSentinel = answers.some((res) => res.status === 1 && res.stderr.trim() !== REG_ABSENT);
|
|
587
|
+
const missing = needsSentinel ? sentinelMessage(await regQueryAsync(REG_SENTINEL)) : "";
|
|
588
|
+
return keys.map((key, i) => registrySettings(key, answers[i], () => missing));
|
|
589
|
+
}
|
|
590
|
+
var policyKeys = () => ["HKLM", "HKCU"].map((h) => `${h}\\SOFTWARE\\Policies\\ClaudeCode`).filter((k) => sourceAllowed(`registry:${k}`));
|
|
591
|
+
function managedFileDocs(dir, group, out) {
|
|
592
|
+
const main = readSettingsFile(path.join(dir, "managed-settings.json"));
|
|
593
|
+
if (main !== void 0) out.push({ role: "managed", json: main, managedGroup: group });
|
|
594
|
+
const names = readIfExists(path.join(dir, "managed-settings.d"), true) ?? [];
|
|
595
|
+
for (const name of names.filter((n) => n.endsWith(".json") && !n.startsWith(".")).sort()) {
|
|
596
|
+
const json = readSettingsFile(path.join(dir, "managed-settings.d", name));
|
|
597
|
+
if (json !== void 0) out.push({ role: "managed", json, managedGroup: group });
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
function managedDirs() {
|
|
601
|
+
if (process.platform === "darwin") return ["/Library/Application Support/ClaudeCode"];
|
|
602
|
+
if (process.platform !== "win32") return ["/etc/claude-code"];
|
|
603
|
+
const dirs = /* @__PURE__ */ new Map();
|
|
604
|
+
for (const base of [process.env["ProgramFiles"], "C:\\Program Files"]) {
|
|
605
|
+
if (base !== void 0 && base !== "") dirs.set(path.join(base, "ClaudeCode").toLowerCase(), path.join(base, "ClaudeCode"));
|
|
606
|
+
}
|
|
607
|
+
return [...dirs.values()];
|
|
608
|
+
}
|
|
609
|
+
function managedDocs(registry) {
|
|
610
|
+
const out = [];
|
|
611
|
+
for (const dir of managedDirs()) managedFileDocs(dir, "file", out);
|
|
612
|
+
if (process.platform === "win32") {
|
|
613
|
+
const keys = registry?.keys ?? policyKeys();
|
|
614
|
+
const values = registry?.values ?? readRegistrySettings(keys);
|
|
615
|
+
values.forEach((json, i) => {
|
|
616
|
+
if (json !== void 0) out.push({ role: "managed", json, managedGroup: keys[i].slice(0, 4).toLowerCase() });
|
|
617
|
+
});
|
|
618
|
+
} else if (process.platform === "darwin") {
|
|
619
|
+
for (const plist of ["/Library/Managed Preferences/com.anthropic.claudecode.plist", path.join("/Library/Managed Preferences", os.userInfo().username, "com.anthropic.claudecode.plist")]) {
|
|
620
|
+
if (!sourceAllowed(plist) || !fs.existsSync(plist)) continue;
|
|
621
|
+
const res = spawnSync("plutil", ["-convert", "json", "-o", "-", plist], { encoding: "utf8", timeout: 5e3 });
|
|
622
|
+
if (res.error !== void 0 || res.status !== 0) throw new Error(`unreadable ${plist}`);
|
|
623
|
+
const json = asObject(JSON.parse(res.stdout));
|
|
624
|
+
if (json === null) throw new Error(`${plist} is not a dictionary`);
|
|
625
|
+
out.push({ role: "managed", json, managedGroup: "plist" });
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const remote = readSettingsFile(path.join(claudeConfigDir(), "remote-settings.json"));
|
|
629
|
+
if (remote !== void 0 && Object.keys(remote).length > 0) out.push({ role: "managed", json: remote, managedGroup: "remote" });
|
|
630
|
+
return out;
|
|
631
|
+
}
|
|
632
|
+
var MANAGED_CACHE_MS = 3e4;
|
|
633
|
+
var MANAGED_REFRESH_MS = 2e4;
|
|
634
|
+
var MANAGED_IDLE_MS = 3e5;
|
|
635
|
+
var managedCache;
|
|
636
|
+
var managedRefresh;
|
|
637
|
+
var managedGeneration = 0;
|
|
638
|
+
var managedUsedAt = 0;
|
|
639
|
+
function primeManagedPolicyAhead() {
|
|
640
|
+
if (process.platform !== "win32" || managedRefresh !== void 0) return;
|
|
641
|
+
managedUsedAt = Date.now();
|
|
642
|
+
const refresh = () => {
|
|
643
|
+
if (Date.now() - managedUsedAt > MANAGED_IDLE_MS) return;
|
|
644
|
+
const generation = managedGeneration;
|
|
645
|
+
const at = Date.now();
|
|
646
|
+
const keys = policyKeys();
|
|
647
|
+
readRegistrySettingsAsync(keys).then((values) => {
|
|
648
|
+
if (generation !== managedGeneration || managedCache !== void 0 && managedCache.at > at) return;
|
|
649
|
+
managedCache = { at, docs: managedDocs({ keys, values }) };
|
|
650
|
+
}).catch(() => void 0);
|
|
651
|
+
};
|
|
652
|
+
managedRefresh = setInterval(refresh, MANAGED_REFRESH_MS);
|
|
653
|
+
managedRefresh.unref();
|
|
654
|
+
refresh();
|
|
655
|
+
}
|
|
656
|
+
function localFileTrusted(file) {
|
|
657
|
+
try {
|
|
658
|
+
if (fs.lstatSync(path.dirname(file)).isSymbolicLink()) return false;
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
const res = runGit(["ls-files", "--error-unmatch", path.basename(file)], { cwd: path.dirname(file), timeoutMs: 5e3 });
|
|
663
|
+
return res.exitCode === 1;
|
|
664
|
+
}
|
|
665
|
+
function samePath(a, b) {
|
|
666
|
+
return foldPathForContainment(a) === foldPathForContainment(b);
|
|
667
|
+
}
|
|
668
|
+
function gitMainRoot(dir) {
|
|
669
|
+
for (const d of selfAndAncestors(dir)) {
|
|
670
|
+
if (!fs.existsSync(path.join(d, ".git"))) continue;
|
|
671
|
+
const common = commonGitDir(d);
|
|
672
|
+
return common !== null && path.basename(common) === ".git" ? path.dirname(common) : d;
|
|
673
|
+
}
|
|
674
|
+
return void 0;
|
|
675
|
+
}
|
|
676
|
+
function selfAndAncestors(dir) {
|
|
677
|
+
const out = [];
|
|
678
|
+
let d = dir;
|
|
679
|
+
for (let depth = 0; depth < 64; depth++) {
|
|
680
|
+
out.push(d);
|
|
681
|
+
const parent = path.dirname(d);
|
|
682
|
+
if (parent === d) break;
|
|
683
|
+
d = parent;
|
|
684
|
+
}
|
|
685
|
+
return out;
|
|
686
|
+
}
|
|
687
|
+
function loadPermissionSnapshot(cwd, projectDir = process.env["CLAUDE_PROJECT_DIR"]) {
|
|
688
|
+
try {
|
|
689
|
+
const here = path.resolve(cwd);
|
|
690
|
+
const project = projectDir !== void 0 && path.isAbsolute(projectDir) ? path.resolve(projectDir) : void 0;
|
|
691
|
+
if (project !== void 0 && !isInsideRoot(here, project)) return null;
|
|
692
|
+
const start = project ?? here;
|
|
693
|
+
const mainRoot = gitMainRoot(start);
|
|
694
|
+
const localDir = samePath(here, start) && (process.platform === "win32" || mainRoot === void 0 || samePath(mainRoot, start)) ? start : void 0;
|
|
695
|
+
const now = Date.now();
|
|
696
|
+
managedUsedAt = now;
|
|
697
|
+
if (managedCache === void 0 || now - managedCache.at > MANAGED_CACHE_MS) managedCache = { at: now, docs: managedDocs() };
|
|
698
|
+
const docs = [...managedCache.docs];
|
|
699
|
+
const user = readSettingsFile(path.join(claudeConfigDir(), "settings.json"));
|
|
700
|
+
if (user !== void 0) docs.push({ role: "user", json: user });
|
|
701
|
+
const userLocal = readSettingsFile(path.join(claudeConfigDir(), "settings.local.json"));
|
|
702
|
+
if (userLocal !== void 0) docs.push({ role: "ancestor", json: userLocal });
|
|
703
|
+
const seen = /* @__PURE__ */ new Set();
|
|
704
|
+
for (const dir of [here, start, ...mainRoot === void 0 ? [] : [mainRoot]].flatMap(selfAndAncestors)) {
|
|
705
|
+
const key = process.platform === "win32" ? dir.toLowerCase() : dir;
|
|
706
|
+
if (seen.has(key)) continue;
|
|
707
|
+
seen.add(key);
|
|
708
|
+
const projectSettings = readSettingsFile(path.join(dir, ".claude", "settings.json"));
|
|
709
|
+
if (projectSettings !== void 0) docs.push({ role: samePath(dir, start) ? "project" : "ancestor", json: projectSettings });
|
|
710
|
+
const localFile = path.join(dir, ".claude", "settings.local.json");
|
|
711
|
+
const local = readSettingsFile(localFile);
|
|
712
|
+
if (local !== void 0) {
|
|
713
|
+
const perms = asObject(local["permissions"]);
|
|
714
|
+
const hasAllow = Array.isArray(perms?.["allow"]) && perms["allow"].length > 0;
|
|
715
|
+
docs.push(localDir !== void 0 && samePath(dir, localDir) ? { role: "local", json: local, localTrusted: hasAllow && localFileTrusted(localFile) } : { role: "ancestor", json: local });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
return snapshotFromDocs(docs);
|
|
719
|
+
} catch {
|
|
720
|
+
return null;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
var codexDecide;
|
|
724
|
+
async function loadCodexRules() {
|
|
725
|
+
if (codexDecide !== void 0) return;
|
|
726
|
+
const { decideCodex } = await import("./token-goat-chunk-B3PEKBYG.mjs");
|
|
727
|
+
const helpers = { containsPiece, haystacks, readIfExists, selfAndAncestors, sourceAllowed };
|
|
728
|
+
codexDecide = (req) => decideCodex(req, helpers);
|
|
729
|
+
}
|
|
730
|
+
async function loadHiddenRuleCheck() {
|
|
731
|
+
unseenRules ??= (await import("./token-goat-chunk-CSJ5L3NL.mjs")).hiddenRules({ configDir: claudeConfigDir, managedDirs, runGit, selfAndAncestors, sourceAllowed });
|
|
732
|
+
if (primeAhead) await unseenRules.prime(process.env);
|
|
733
|
+
}
|
|
734
|
+
function primeHiddenRulesAhead() {
|
|
735
|
+
primeAhead = true;
|
|
736
|
+
}
|
|
737
|
+
function loadingHiddenRuleCheck(handler) {
|
|
738
|
+
return (event) => {
|
|
739
|
+
if (unseenRules !== void 0 && !primeAhead || event.raw["permission_mode"] === "auto") return handler(event);
|
|
740
|
+
const harness = detectHarness();
|
|
741
|
+
if (harness !== "claudecode" && harness !== "vscode") return handler(event);
|
|
742
|
+
return loadHiddenRuleCheck().then(() => handler(event), () => handler(event));
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
var UNSEEN_SHELL_RULES = /* @__PURE__ */ new Set(["copilot_cli", "opencode", "grok"]);
|
|
746
|
+
function permissionNeutralRewrite(updatedInput, req) {
|
|
747
|
+
if (UNSEEN_SHELL_RULES.has(req.harness) && (req.kind === "shell-wrap" || req.kind === "shell-query")) return null;
|
|
748
|
+
const codexShell = req.harness === "codex" && (req.kind === "shell-wrap" || req.kind === "shell-query");
|
|
749
|
+
const verdict = req.harness === "claudecode" || req.harness === "vscode" ? decideRewrite(loadPermissionSnapshot(req.cwd), req) : codexShell ? codexDecide?.(req) ?? "skip" : "rewrite";
|
|
750
|
+
if (verdict === "skip") return null;
|
|
751
|
+
return { hookType: "rewriteInput", updatedInput, approve: verdict === "approve" };
|
|
752
|
+
}
|
|
753
|
+
function readHintCrossesRule(harness, cwd, paths, command = "", declined) {
|
|
754
|
+
if (harness !== "claudecode" && harness !== "vscode") return false;
|
|
755
|
+
const snapshot = loadPermissionSnapshot(cwd);
|
|
756
|
+
if (snapshot === null || snapshot.blockReadsOutside) return true;
|
|
757
|
+
if (/[$`~*?[]/.test(command) && [...snapshot.deny, ...snapshot.ask].some((rule) => rule.tool === void 0 || ruleToolIs(rule, ["read"]))) return true;
|
|
758
|
+
return readRuleMayCover(snapshot, haystacks([command, ...paths.map((p) => path.resolve(cwd, p)), ...paths.filter((p) => declined?.(p) !== true).flatMap((p) => realSpellings(p, cwd))]));
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// src/hints/file_type_handler.ts
|
|
762
|
+
init_define_import_meta_env();
|
|
763
|
+
var LARGE_SYMBOL_LINE_THRESHOLD = 200;
|
|
764
|
+
var FILE_TYPE_THRESHOLDS = {
|
|
765
|
+
pdf: 0,
|
|
766
|
+
// always intercept (any size)
|
|
767
|
+
html: 5e4,
|
|
768
|
+
txt: 2e4,
|
|
769
|
+
log: 1e4,
|
|
770
|
+
svg: 8e3,
|
|
771
|
+
xml: 2e4,
|
|
772
|
+
dtsx: 2e4,
|
|
773
|
+
ampkg: 2e4,
|
|
774
|
+
xaml: 2e4,
|
|
775
|
+
csv: 1e4,
|
|
776
|
+
tsv: 1e4,
|
|
777
|
+
json: 2e4,
|
|
778
|
+
yaml: 2e4,
|
|
779
|
+
jsonl: 2e4,
|
|
780
|
+
transcript: 1e4,
|
|
781
|
+
office: 0,
|
|
782
|
+
// always block (.docx etc)
|
|
783
|
+
sqlite: 0,
|
|
784
|
+
// always block (.db, .sqlite, etc)
|
|
785
|
+
parquet: 0,
|
|
786
|
+
// always block (.parquet)
|
|
787
|
+
generic: 1e5
|
|
788
|
+
// catch-all for unrecognized large files
|
|
789
|
+
};
|
|
790
|
+
function formatBytes(n) {
|
|
791
|
+
if (n < 1024) return `${n} B`;
|
|
792
|
+
if (n < 1048576) return `${(n / 1024).toFixed(1)} KB`;
|
|
793
|
+
if (n < 1073741824) return `${(n / 1048576).toFixed(1)} MB`;
|
|
794
|
+
if (n < 1099511627776) return `${(n / 1073741824).toFixed(1)} GB`;
|
|
795
|
+
return `${(n / 1099511627776).toFixed(1)} TB`;
|
|
796
|
+
}
|
|
797
|
+
function previewUnavailable(content, effectiveLength) {
|
|
798
|
+
return content.length === 0 && effectiveLength > 0;
|
|
799
|
+
}
|
|
800
|
+
function BYTE_RANGE_ADVICE(filePath) {
|
|
801
|
+
return `This file is mostly one long line (e.g. base64 or minified content) \u2014 offset/limit line-windowing won't shrink a read here. Sample raw bytes instead, e.g.: dd if=${quotedArg(filePath)} bs=1 skip=<N> count=<M> status=none`;
|
|
802
|
+
}
|
|
803
|
+
function handlePdf(filePath, contentLength) {
|
|
804
|
+
const [quoted, range] = quotedArgs(filePath, "<range>");
|
|
805
|
+
return {
|
|
806
|
+
shouldBlock: true,
|
|
807
|
+
message: [
|
|
808
|
+
`PDF file (${formatBytes(contentLength)}) \u2014 Read cannot return PDF content; this is not retryable with different Read parameters.`,
|
|
809
|
+
`Inspect first: token-goat pdf-meta ${quoted} and token-goat pdf-outline ${quoted}`,
|
|
810
|
+
`Then extract relevant pages: token-goat pdf-extract ${quoted} --pages ${range}`
|
|
811
|
+
].join("\n")
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
function handleHtml(filePath, content, contentLengthHint) {
|
|
815
|
+
const length = contentLengthHint ?? content.length;
|
|
816
|
+
if (length < FILE_TYPE_THRESHOLDS.html) return { shouldBlock: false, message: "" };
|
|
817
|
+
if (previewUnavailable(content, length)) {
|
|
818
|
+
return {
|
|
819
|
+
shouldBlock: true,
|
|
820
|
+
message: `Large HTML file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap). Use token-goat section to extract a section by heading, or convert to text: pandoc ${quotedArg(displaySafePath(filePath))} -t plain`
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
const title = content.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]?.trim();
|
|
824
|
+
const headings = findHtmlHeadingMatches(content).slice(0, 20).map(({ level, heading }) => {
|
|
825
|
+
if (!heading) return "";
|
|
826
|
+
return `${" ".repeat(level - 1)}h${level}: ${heading}`;
|
|
827
|
+
}).filter(Boolean);
|
|
828
|
+
const isMinified = !content.includes("\n") || content.length > 5e4 && content.split("\n").length < 10;
|
|
829
|
+
if (isMinified) {
|
|
830
|
+
return {
|
|
831
|
+
shouldBlock: true,
|
|
832
|
+
message: `HTML file appears minified (${formatBytes(length)}). Consider fetching the source or converting with: pandoc ${quotedArg(displaySafePath(filePath))} -t plain`
|
|
833
|
+
};
|
|
834
|
+
}
|
|
835
|
+
return {
|
|
836
|
+
shouldBlock: true,
|
|
837
|
+
message: [
|
|
838
|
+
// Title and headings are verbatim file bytes spliced into a message the harness attributes to token-goat, so they are fenced as untrusted data.
|
|
839
|
+
`Large HTML file (${formatBytes(length)}).`,
|
|
840
|
+
title || headings.length > 0 ? fenceUntrustedFileContent(
|
|
841
|
+
[title ? `Title: ${title}` : "", headings.length > 0 ? `Headings:
|
|
842
|
+
${headings.join("\n")}` : ""].filter(Boolean).join("\n")
|
|
843
|
+
) : "",
|
|
844
|
+
`Use token-goat section to extract a section by heading, or convert to text: pandoc ${quotedArg(displaySafePath(filePath))} -t plain`
|
|
845
|
+
].filter(Boolean).join("\n")
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
function handleTxt(filePath, content, contentLengthHint) {
|
|
849
|
+
const length = contentLengthHint ?? content.length;
|
|
850
|
+
const isLog = /\.(log|out|err|trace)$/i.test(filePath) || /[\\/]logs[\\/]/.test(filePath);
|
|
851
|
+
const threshold = isLog ? FILE_TYPE_THRESHOLDS.log : FILE_TYPE_THRESHOLDS.txt;
|
|
852
|
+
if (length < threshold) return { shouldBlock: false, message: "" };
|
|
853
|
+
const [quotedLog, errorPattern] = quotedArgs(filePath, "error|ERROR");
|
|
854
|
+
const recall = isLog ? `Log file \u2014 use Read with offset/limit params, or: token-goat bash-output --file ${quotedLog} --tail 100 --grep ${errorPattern}` : "Use Read with offset and limit params to sample specific line ranges.";
|
|
855
|
+
if (previewUnavailable(content, length)) {
|
|
856
|
+
return {
|
|
857
|
+
shouldBlock: true,
|
|
858
|
+
message: `Large text file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap).
|
|
859
|
+
${recall}`
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
const contentSniff = content.slice(0, 1e3);
|
|
863
|
+
if (/^\s*<!DOCTYPE\s+html|^\s*<html[\s>]/i.test(contentSniff)) {
|
|
864
|
+
const htmlResult = handleHtml(filePath, content, contentLengthHint);
|
|
865
|
+
if (htmlResult.shouldBlock) return htmlResult;
|
|
866
|
+
}
|
|
867
|
+
const lines = content.split("\n");
|
|
868
|
+
if (lines.length > 1 && lines[lines.length - 1] === "") lines.pop();
|
|
869
|
+
const preview = [
|
|
870
|
+
"--- first 5 lines ---",
|
|
871
|
+
...lines.slice(0, 5),
|
|
872
|
+
`... (${lines.length.toLocaleString()} lines total) ...`,
|
|
873
|
+
"--- last 5 lines ---",
|
|
874
|
+
...lines.slice(-5)
|
|
875
|
+
].join("\n");
|
|
876
|
+
return {
|
|
877
|
+
shouldBlock: true,
|
|
878
|
+
// The preview is verbatim file bytes, so it is fenced as untrusted data.
|
|
879
|
+
message: `Large text file (${formatBytes(length)}, ${lines.length.toLocaleString()} lines).
|
|
880
|
+
${fenceUntrustedFileContent(preview)}
|
|
881
|
+
|
|
882
|
+
${recall}`
|
|
883
|
+
};
|
|
884
|
+
}
|
|
885
|
+
function handleSvg(filePath, content, contentLengthHint) {
|
|
886
|
+
const length = contentLengthHint ?? content.length;
|
|
887
|
+
if (length < FILE_TYPE_THRESHOLDS.svg) return { shouldBlock: false, message: "" };
|
|
888
|
+
if (previewUnavailable(content, length)) {
|
|
889
|
+
return {
|
|
890
|
+
shouldBlock: true,
|
|
891
|
+
message: [
|
|
892
|
+
`Large SVG file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap).`,
|
|
893
|
+
`Inspect structure: token-goat xml-outline ${quotedArg(filePath)}`,
|
|
894
|
+
`Query layers/elements: token-goat xml-query ${quotedArgs(filePath, "//g[@id]").join(" ")}`
|
|
895
|
+
].join("\n")
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
const titleMatch = content.match(/<title[^>]*>([^<]*)<\/title>/i);
|
|
899
|
+
const title = titleMatch ? titleMatch[1]?.trim() : "";
|
|
900
|
+
const groupIds = [];
|
|
901
|
+
const idRegex = /<g[^>]*\bid=["']([^"']+)["']/gi;
|
|
902
|
+
let match;
|
|
903
|
+
while ((match = idRegex.exec(content)) !== null && groupIds.length < 10) {
|
|
904
|
+
if (match[1]) groupIds.push(match[1]);
|
|
905
|
+
}
|
|
906
|
+
const preview = [
|
|
907
|
+
title ? `Title: ${title}` : "",
|
|
908
|
+
groupIds.length > 0 ? `Layer/Group IDs: ${groupIds.join(", ")}` : ""
|
|
909
|
+
].filter(Boolean).join("\n");
|
|
910
|
+
const [labelPattern, labelGlob] = quotedArgs("<text>", filePath);
|
|
911
|
+
return {
|
|
912
|
+
shouldBlock: true,
|
|
913
|
+
message: [
|
|
914
|
+
`Large SVG/diagram file (${formatBytes(length)}) \u2014 raw coordinate paths flood context.`,
|
|
915
|
+
preview ? fenceUntrustedFileContent(preview) : "",
|
|
916
|
+
`Inspect structure: token-goat xml-outline ${quotedArg(filePath)}`,
|
|
917
|
+
`Query elements: token-goat xml-query ${quotedArgs(filePath, "//g[@id]").join(" ")}`,
|
|
918
|
+
`Search text labels: token-goat grep ${labelPattern} --glob ${labelGlob}`
|
|
919
|
+
].filter(Boolean).join("\n")
|
|
920
|
+
};
|
|
921
|
+
}
|
|
922
|
+
function handleXml(filePath, content, contentLengthHint) {
|
|
923
|
+
const length = contentLengthHint ?? content.length;
|
|
924
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "xml";
|
|
925
|
+
const threshold = ext === "dtsx" || ext === "ampkg" || ext === "xaml" ? FILE_TYPE_THRESHOLDS[ext] : FILE_TYPE_THRESHOLDS.xml;
|
|
926
|
+
if (length < threshold) return { shouldBlock: false, message: "" };
|
|
927
|
+
const typeLabel = ext === "dtsx" ? "SSIS package XML (.dtsx)" : ext === "ampkg" ? "workflow package XML (.ampkg)" : ext === "xaml" ? "XAML (.xaml)" : "XML";
|
|
928
|
+
if (previewUnavailable(content, length)) {
|
|
929
|
+
return {
|
|
930
|
+
shouldBlock: true,
|
|
931
|
+
message: [
|
|
932
|
+
`Large ${typeLabel} file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap).`,
|
|
933
|
+
`Inspect structure: token-goat xml-outline ${quotedArg(filePath)}`,
|
|
934
|
+
`Query nodes: token-goat xml-query ${quotedArgs(filePath, "<selector>").join(" ")}`
|
|
935
|
+
].join("\n")
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
return {
|
|
939
|
+
shouldBlock: true,
|
|
940
|
+
message: [
|
|
941
|
+
`Large ${typeLabel} file (${formatBytes(length)}).`,
|
|
942
|
+
`Inspect hierarchy: token-goat xml-outline ${quotedArg(filePath)}`,
|
|
943
|
+
`Query specific elements: token-goat xml-query ${quotedArgs(filePath, "<selector>").join(" ")}`
|
|
944
|
+
].join("\n")
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function handleOfficeBinary(filePath) {
|
|
948
|
+
const filename = filePath.split(/[\\/]/).pop() || "";
|
|
949
|
+
const parts = filename.split(".");
|
|
950
|
+
const lastPart = parts[parts.length - 1];
|
|
951
|
+
const ext = parts.length > 1 && lastPart ? lastPart.toLowerCase() : "bin";
|
|
952
|
+
return {
|
|
953
|
+
shouldBlock: true,
|
|
954
|
+
message: [
|
|
955
|
+
`Binary Office file (.${ext}) \u2014 cannot be read as text.`,
|
|
956
|
+
`Extract content first: pandoc ${quotedArg(displaySafePath(filePath))} -t plain -o ${quotedArg(displaySafePath(filePath + ".txt"))}`,
|
|
957
|
+
`Then read the extracted .txt file.`
|
|
958
|
+
].join("\n")
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
function handleXlsx(filePath) {
|
|
962
|
+
const [quoted, sheet] = quotedArgs(filePath, "<name>");
|
|
963
|
+
return {
|
|
964
|
+
shouldBlock: true,
|
|
965
|
+
message: [
|
|
966
|
+
`Excel file \u2014 Read cannot return spreadsheet content; this is not retryable with different Read parameters.`,
|
|
967
|
+
`List sheets: token-goat xlsx-sheets ${quoted}`,
|
|
968
|
+
`Then preview a sheet: ${fencedCommand(`token-goat xlsx-head ${quoted} --sheet ${sheet}`)}, or filter rows: ${fencedCommand(`token-goat xlsx-query ${quoted} --sheet ${sheet} --where col=value`)}`
|
|
969
|
+
].join("\n")
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
function handlePptx(filePath) {
|
|
973
|
+
const [quoted, slide] = quotedArgs(filePath, "<n>");
|
|
974
|
+
return {
|
|
975
|
+
shouldBlock: true,
|
|
976
|
+
message: [
|
|
977
|
+
`PowerPoint file \u2014 Read cannot return slide content; this is not retryable with different Read parameters.`,
|
|
978
|
+
`List slides: token-goat pptx-outline ${quoted}`,
|
|
979
|
+
`Then read one slide: token-goat pptx-slide ${quoted} --slide ${slide}`
|
|
980
|
+
].join("\n")
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function handleDocx(filePath) {
|
|
984
|
+
return {
|
|
985
|
+
shouldBlock: true,
|
|
986
|
+
message: [
|
|
987
|
+
`Word file \u2014 Read cannot return document content; this is not retryable with different Read parameters.`,
|
|
988
|
+
`See headings: token-goat docx-outline ${quotedArg(filePath)}`,
|
|
989
|
+
`Extract tables: token-goat docx-tables ${quotedArg(filePath)}`,
|
|
990
|
+
`Read full text: token-goat docx-text ${quotedArg(filePath)}`
|
|
991
|
+
].join("\n")
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function handleCsv(filePath, content, contentLengthHint) {
|
|
995
|
+
const threshold = filePath.toLowerCase().endsWith(".tsv") ? FILE_TYPE_THRESHOLDS.tsv : FILE_TYPE_THRESHOLDS.csv;
|
|
996
|
+
const length = contentLengthHint ?? content.length;
|
|
997
|
+
if (length < threshold) return { shouldBlock: false, message: "" };
|
|
998
|
+
if (previewUnavailable(content, length)) {
|
|
999
|
+
return {
|
|
1000
|
+
shouldBlock: true,
|
|
1001
|
+
message: [
|
|
1002
|
+
`Large CSV file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap).`,
|
|
1003
|
+
`Use token-goat csv-query ${quotedArg(filePath)} --columns "a,b,c" --where col=value --head N to query narrow slices.`
|
|
1004
|
+
].join("\n")
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
const lines = content.split("\n").filter((l) => l.trim());
|
|
1008
|
+
const headers = lines[0] ?? "";
|
|
1009
|
+
const sampleRows = lines.slice(1, 4);
|
|
1010
|
+
const sep = filePath.toLowerCase().endsWith(".tsv") ? " " : ",";
|
|
1011
|
+
let colCount;
|
|
1012
|
+
try {
|
|
1013
|
+
colCount = parse(headers, { delimiter: sep })[0]?.length ?? headers.split(sep).length;
|
|
1014
|
+
} catch {
|
|
1015
|
+
colCount = headers.split(sep).length;
|
|
1016
|
+
}
|
|
1017
|
+
return {
|
|
1018
|
+
shouldBlock: true,
|
|
1019
|
+
message: [
|
|
1020
|
+
`Large CSV file (${formatBytes(length)}, ~${lines.length.toLocaleString()} rows, ${colCount} columns).`,
|
|
1021
|
+
// Header row and sample rows are verbatim file bytes, so they are fenced as untrusted data.
|
|
1022
|
+
fenceUntrustedFileContent(`Columns: ${headers}
|
|
1023
|
+
Sample rows:
|
|
1024
|
+
${sampleRows.join("\n")}`),
|
|
1025
|
+
`Use token-goat csv-query ${quotedArg(filePath)} --columns "a,b,c" --where col=value --head N to query narrow slices.`
|
|
1026
|
+
].join("\n")
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
function handleTranscript(filePath, content, contentLengthHint) {
|
|
1030
|
+
const length = contentLengthHint ?? content.length;
|
|
1031
|
+
if (length < FILE_TYPE_THRESHOLDS.transcript) return { shouldBlock: false, message: "" };
|
|
1032
|
+
const [quoted, speaker] = quotedArgs(filePath, "Name");
|
|
1033
|
+
if (previewUnavailable(content, length)) {
|
|
1034
|
+
return {
|
|
1035
|
+
shouldBlock: true,
|
|
1036
|
+
message: [
|
|
1037
|
+
`Transcript file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap).`,
|
|
1038
|
+
`See structure: token-goat transcript-outline ${quotedArg(filePath)}`,
|
|
1039
|
+
`Slice by speaker/time/pattern: token-goat transcript ${quoted} --speaker ${speaker} --from 00:05:00 --to 00:10:00 --grep pattern`
|
|
1040
|
+
].join("\n")
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
const speakerMatches = [...content.matchAll(/^<v(?:\.\w+)?\s+([^>]+)>/gm)].map((m) => m[1]?.trim() ?? "");
|
|
1044
|
+
const speakers = [...new Set(speakerMatches)].slice(0, 10);
|
|
1045
|
+
const cueCount = (content.match(/-->/g) ?? []).length;
|
|
1046
|
+
return {
|
|
1047
|
+
shouldBlock: true,
|
|
1048
|
+
message: [
|
|
1049
|
+
// Speaker names are verbatim file bytes, so they are fenced as untrusted data.
|
|
1050
|
+
`Transcript file (${formatBytes(length)}, ~${cueCount} cues).`,
|
|
1051
|
+
speakers.length > 0 ? fenceUntrustedFileContent(`Speakers: ${speakers.join(", ")}`) : "",
|
|
1052
|
+
`See structure: token-goat transcript-outline ${quotedArg(filePath)}`,
|
|
1053
|
+
`Slice by speaker/time/pattern: token-goat transcript ${quoted} --speaker ${speaker} --from 00:05:00 --to 00:10:00 --grep pattern`
|
|
1054
|
+
].filter(Boolean).join("\n")
|
|
1055
|
+
};
|
|
1056
|
+
}
|
|
1057
|
+
function handleSqlite(filePath) {
|
|
1058
|
+
return {
|
|
1059
|
+
shouldBlock: true,
|
|
1060
|
+
message: [
|
|
1061
|
+
`SQLite database \u2014 Read cannot return binary database content; this is not retryable with different Read parameters.`,
|
|
1062
|
+
`See tables: token-goat sqlite-tables ${quotedArg(filePath)}`,
|
|
1063
|
+
`See schema: token-goat sqlite-schema ${quotedArg(filePath)}`,
|
|
1064
|
+
`Query data: token-goat sqlite-query ${quotedArgs(filePath, "SELECT ... FROM ... LIMIT 20").join(" ")}`
|
|
1065
|
+
].join("\n")
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
function sqlLiteralPath(p) {
|
|
1069
|
+
const path7 = /^[A-Za-z]:[\\/]/.test(p) ? p.replaceAll("\\", "/") : p;
|
|
1070
|
+
return displaySafePath(path7) === path7 && !/['"$`\\\u2018-\u201E]/.test(path7) ? path7 : "<file>";
|
|
1071
|
+
}
|
|
1072
|
+
function handleParquet(filePath) {
|
|
1073
|
+
return {
|
|
1074
|
+
shouldBlock: true,
|
|
1075
|
+
message: [
|
|
1076
|
+
`Parquet file \u2014 Read cannot return binary columnar content; this is not retryable with different Read parameters.`,
|
|
1077
|
+
`Query with DuckDB: duckdb -c "SELECT * FROM read_parquet('${sqlLiteralPath(filePath)}') LIMIT 10"`
|
|
1078
|
+
].join("\n")
|
|
1079
|
+
};
|
|
1080
|
+
}
|
|
1081
|
+
function handleJson(filePath, content, contentLengthHint) {
|
|
1082
|
+
const length = contentLengthHint ?? content.length;
|
|
1083
|
+
if (length < FILE_TYPE_THRESHOLDS.json) return { shouldBlock: false, message: "" };
|
|
1084
|
+
const [quoted, query] = quotedArgs(filePath, "<path>");
|
|
1085
|
+
const isSpill = /(?:^|[/\\])content\.json$/i.test(filePath) || /[/\\](?:tmp|temp)[/\\]/i.test(filePath);
|
|
1086
|
+
const spillHeader = isSpill ? `This file appears to be an oversized tool output spill (${formatBytes(length)}). Do not read it whole with Read/read_file.` : `Large JSON file (${formatBytes(length)}).`;
|
|
1087
|
+
if (previewUnavailable(content, length)) {
|
|
1088
|
+
return {
|
|
1089
|
+
shouldBlock: true,
|
|
1090
|
+
message: [
|
|
1091
|
+
isSpill ? `This file appears to be an oversized tool output spill (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap).` : `Large JSON file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap).`,
|
|
1092
|
+
`See structure: token-goat json-outline ${quotedArg(filePath)}`,
|
|
1093
|
+
`Query subtree: token-goat json-query ${quoted} ${query}`,
|
|
1094
|
+
isSpill ? `Slice spill: token-goat mcp-output --file ${quoted} --json-query ${query}` : ""
|
|
1095
|
+
].filter(Boolean).join("\n")
|
|
1096
|
+
};
|
|
1097
|
+
}
|
|
1098
|
+
let summary = "";
|
|
1099
|
+
try {
|
|
1100
|
+
const parsed = JSON.parse(content);
|
|
1101
|
+
if (Array.isArray(parsed)) {
|
|
1102
|
+
summary = `JSON array (${parsed.length} items)`;
|
|
1103
|
+
if (parsed.length > 0 && typeof parsed[0] === "object" && parsed[0] !== null) {
|
|
1104
|
+
const itemKeys = Object.keys(parsed[0]).slice(0, 10);
|
|
1105
|
+
summary += `, item keys: ${itemKeys.join(", ")}${Object.keys(parsed[0]).length > 10 ? "..." : ""}`;
|
|
1106
|
+
}
|
|
1107
|
+
} else if (typeof parsed === "object" && parsed !== null) {
|
|
1108
|
+
const keys = Object.keys(parsed).slice(0, 15);
|
|
1109
|
+
summary = `Top-level keys (${Object.keys(parsed).length}): ${keys.join(", ")}${Object.keys(parsed).length > 15 ? "..." : ""}`;
|
|
1110
|
+
}
|
|
1111
|
+
} catch {
|
|
1112
|
+
}
|
|
1113
|
+
return {
|
|
1114
|
+
shouldBlock: true,
|
|
1115
|
+
message: [
|
|
1116
|
+
spillHeader,
|
|
1117
|
+
summary ? fenceUntrustedFileContent(summary) : "",
|
|
1118
|
+
`See structure: token-goat json-outline ${quotedArg(filePath)}`,
|
|
1119
|
+
`Query subtree: token-goat json-query ${quoted} ${query}`,
|
|
1120
|
+
isSpill ? `Slice spill: token-goat mcp-output --file ${quoted} --json-query ${query}` : ""
|
|
1121
|
+
].filter(Boolean).join("\n")
|
|
1122
|
+
};
|
|
1123
|
+
}
|
|
1124
|
+
function handleYaml(filePath, content, contentLengthHint) {
|
|
1125
|
+
const length = contentLengthHint ?? content.length;
|
|
1126
|
+
if (length < FILE_TYPE_THRESHOLDS.yaml) return { shouldBlock: false, message: "" };
|
|
1127
|
+
return {
|
|
1128
|
+
shouldBlock: true,
|
|
1129
|
+
message: [
|
|
1130
|
+
`Large YAML file (${formatBytes(length)}).`,
|
|
1131
|
+
`See structure: token-goat yaml-outline ${quotedArg(filePath)}`,
|
|
1132
|
+
`Query subtree: token-goat yaml-query ${quotedArgs(filePath, "<path>").join(" ")}`
|
|
1133
|
+
].join("\n")
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
function handleJsonl(filePath, content, contentLengthHint) {
|
|
1137
|
+
const length = contentLengthHint ?? content.length;
|
|
1138
|
+
if (length < FILE_TYPE_THRESHOLDS.jsonl) return { shouldBlock: false, message: "" };
|
|
1139
|
+
if (previewUnavailable(content, length)) {
|
|
1140
|
+
return {
|
|
1141
|
+
shouldBlock: true,
|
|
1142
|
+
message: `Large JSON Lines file (${formatBytes(length)}) \u2014 too large to preview (exceeds the in-hook scan cap). Slice records with Read using offset and limit (each line is a single JSON record).`
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
const lines = content.split(/\r?\n/).filter((l) => l.trim().length > 0);
|
|
1146
|
+
let schema = "";
|
|
1147
|
+
if (lines.length > 0) {
|
|
1148
|
+
try {
|
|
1149
|
+
const firstObj = JSON.parse(lines[0]);
|
|
1150
|
+
if (typeof firstObj === "object" && firstObj !== null && !Array.isArray(firstObj)) {
|
|
1151
|
+
const keys = Object.keys(firstObj).slice(0, 15);
|
|
1152
|
+
schema = `Record keys: ${keys.join(", ")}${Object.keys(firstObj).length > 15 ? "..." : ""}`;
|
|
1153
|
+
}
|
|
1154
|
+
} catch {
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
return {
|
|
1158
|
+
shouldBlock: true,
|
|
1159
|
+
message: [
|
|
1160
|
+
`Large JSON Lines file (${formatBytes(length)}, ~${lines.length} records).`,
|
|
1161
|
+
schema ? fenceUntrustedFileContent(schema) : "",
|
|
1162
|
+
`Slice records with Read using offset and limit (each line is a single JSON record), e.g. offset=1, limit=5.`
|
|
1163
|
+
].filter(Boolean).join("\n")
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
function handleGenericLarge(filePath, contentLength) {
|
|
1167
|
+
if (contentLength < FILE_TYPE_THRESHOLDS.generic) return { shouldBlock: false, message: "" };
|
|
1168
|
+
return {
|
|
1169
|
+
shouldBlock: true,
|
|
1170
|
+
message: `Large file (${formatBytes(contentLength)}). Use Read with offset and limit parameters to read specific line ranges rather than loading the entire file.`
|
|
1171
|
+
};
|
|
1172
|
+
}
|
|
1173
|
+
function dispatchFileTypeHandler(filePath, content, contentLengthHint) {
|
|
1174
|
+
const ext = filePath.split(".").pop()?.toLowerCase() ?? "";
|
|
1175
|
+
if (["md", "mdx", "markdown", "rst"].includes(ext)) return null;
|
|
1176
|
+
const effectiveLength = contentLengthHint ?? content.length;
|
|
1177
|
+
if (ext === "pdf") return handlePdf(filePath, effectiveLength);
|
|
1178
|
+
if (ext === "svg") return handleSvg(filePath, content, effectiveLength);
|
|
1179
|
+
if (["xml", "dtsx", "ampkg", "xaml"].includes(ext)) return handleXml(filePath, content, effectiveLength);
|
|
1180
|
+
if (["html", "htm", "xhtml"].includes(ext)) return handleHtml(filePath, content, effectiveLength);
|
|
1181
|
+
if (["txt", "log", "out", "err", "trace"].includes(ext)) return handleTxt(filePath, content, effectiveLength);
|
|
1182
|
+
if (ext === "xlsx") return handleXlsx(filePath);
|
|
1183
|
+
if (ext === "pptx") return handlePptx(filePath);
|
|
1184
|
+
if (ext === "docx") return handleDocx(filePath);
|
|
1185
|
+
if (["odt", "ods", "ott", "odp"].includes(ext)) return handleOfficeBinary(filePath);
|
|
1186
|
+
if (["sqlite", "db", "sqlite3", "db3"].includes(ext)) return handleSqlite(filePath);
|
|
1187
|
+
if (ext === "parquet") return handleParquet(filePath);
|
|
1188
|
+
if (ext === "csv" || ext === "tsv") return handleCsv(filePath, content, effectiveLength);
|
|
1189
|
+
if (ext === "vtt" || ext === "srt") return handleTranscript(filePath, content, effectiveLength);
|
|
1190
|
+
if (ext === "json") return handleJson(filePath, content, effectiveLength);
|
|
1191
|
+
if (["yaml", "yml"].includes(ext)) return handleYaml(filePath, content, effectiveLength);
|
|
1192
|
+
if (ext === "jsonl") return handleJsonl(filePath, content, effectiveLength);
|
|
1193
|
+
return handleGenericLarge(filePath, effectiveLength);
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// src/image_shrink.ts
|
|
1197
|
+
init_define_import_meta_env();
|
|
1198
|
+
import { createHash } from "node:crypto";
|
|
1199
|
+
import * as fs2 from "node:fs";
|
|
1200
|
+
import * as path2 from "node:path";
|
|
1201
|
+
var DEFAULT_MAX_DIMENSION = 1568;
|
|
1202
|
+
var VISION_PATCH_PX = 28;
|
|
1203
|
+
var VISION_TIER_LIMITS = {
|
|
1204
|
+
standard: { maxEdge: 1568, maxTokens: 1568 },
|
|
1205
|
+
high: { maxEdge: 2576, maxTokens: 4784 }
|
|
1206
|
+
};
|
|
1207
|
+
function countImagePatches(width, height) {
|
|
1208
|
+
return Math.ceil(width / VISION_PATCH_PX) * Math.ceil(height / VISION_PATCH_PX);
|
|
1209
|
+
}
|
|
1210
|
+
function roundTiesToEven(value) {
|
|
1211
|
+
const floor = Math.floor(value);
|
|
1212
|
+
if (value - floor !== 0.5) return Math.round(value);
|
|
1213
|
+
return floor % 2 === 0 ? floor : floor + 1;
|
|
1214
|
+
}
|
|
1215
|
+
function fitsVisionLimits(width, height, maxEdge, maxTokens) {
|
|
1216
|
+
return Math.ceil(width / VISION_PATCH_PX) * VISION_PATCH_PX <= maxEdge && Math.ceil(height / VISION_PATCH_PX) * VISION_PATCH_PX <= maxEdge && countImagePatches(width, height) <= maxTokens;
|
|
1217
|
+
}
|
|
1218
|
+
function resizedForVision(width, height, maxEdge, maxTokens) {
|
|
1219
|
+
if (fitsVisionLimits(width, height, maxEdge, maxTokens)) return [width, height];
|
|
1220
|
+
if (height > width) {
|
|
1221
|
+
const [resizedH, resizedW] = resizedForVision(height, width, maxEdge, maxTokens);
|
|
1222
|
+
return [resizedW, resizedH];
|
|
1223
|
+
}
|
|
1224
|
+
const aspectRatio = width / height;
|
|
1225
|
+
let lo = 1;
|
|
1226
|
+
let hi = width;
|
|
1227
|
+
while (lo + 1 < hi) {
|
|
1228
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
1229
|
+
if (fitsVisionLimits(mid, Math.max(roundTiesToEven(mid / aspectRatio), 1), maxEdge, maxTokens)) lo = mid;
|
|
1230
|
+
else hi = mid;
|
|
1231
|
+
}
|
|
1232
|
+
return [lo, Math.max(roundTiesToEven(lo / aspectRatio), 1)];
|
|
1233
|
+
}
|
|
1234
|
+
function visionTokens(width, height, tier) {
|
|
1235
|
+
if (!Number.isFinite(width) || !Number.isFinite(height) || width < 1 || height < 1) return 0;
|
|
1236
|
+
const limits = VISION_TIER_LIMITS[tier];
|
|
1237
|
+
const [w, h] = resizedForVision(Math.floor(width), Math.floor(height), limits.maxEdge, limits.maxTokens);
|
|
1238
|
+
return countImagePatches(w, h);
|
|
1239
|
+
}
|
|
1240
|
+
function visionTokensSaved(fromWidth, fromHeight, toWidth, toHeight, tier) {
|
|
1241
|
+
return Math.max(0, visionTokens(fromWidth, fromHeight, tier) - visionTokens(toWidth, toHeight, tier));
|
|
1242
|
+
}
|
|
1243
|
+
function visionTierMaxTokens(tier) {
|
|
1244
|
+
return VISION_TIER_LIMITS[tier].maxTokens;
|
|
1245
|
+
}
|
|
1246
|
+
function visionTokensSavedByText(width, height, emittedBytes, tier) {
|
|
1247
|
+
const imageSide = width === null || height === null ? visionTierMaxTokens(tier) : visionTokens(width, height, tier);
|
|
1248
|
+
return Math.max(0, imageSide - savedTokensFromBytes(emittedBytes));
|
|
1249
|
+
}
|
|
1250
|
+
var DEFAULT_SIZE_THRESHOLD_BYTES = 512 * 1024;
|
|
1251
|
+
var SINGLE_FRAME_DECODERS = /* @__PURE__ */ new Map([
|
|
1252
|
+
["png", (input) => decodePng(input)],
|
|
1253
|
+
["jpeg", (input) => decodeJpeg(input)],
|
|
1254
|
+
["bmp", (input) => decodeBmp(input)],
|
|
1255
|
+
// The still-image path wants frame 0 and nothing else. Decoding the whole animation to index into it costs a full canvas per frame for frames that are then dropped, and a file can declare far more of them than it contains pixels.
|
|
1256
|
+
["gif", (input) => {
|
|
1257
|
+
const decoded = decodeGif(input, { maxFrames: 1 });
|
|
1258
|
+
const frame = decoded.frames[0];
|
|
1259
|
+
return frame === void 0 ? null : { data: frame.data, width: frame.width, height: frame.height };
|
|
1260
|
+
}]
|
|
1261
|
+
]);
|
|
1262
|
+
function canShrinkFormat(format) {
|
|
1263
|
+
return format != null && SINGLE_FRAME_DECODERS.has(format);
|
|
1264
|
+
}
|
|
1265
|
+
function decoderRefusal(input, format) {
|
|
1266
|
+
const decode = SINGLE_FRAME_DECODERS.get(format ?? "");
|
|
1267
|
+
if (decode === void 0) return null;
|
|
1268
|
+
try {
|
|
1269
|
+
decode(input);
|
|
1270
|
+
return null;
|
|
1271
|
+
} catch (e) {
|
|
1272
|
+
return e instanceof Error ? e.message : String(e);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
function formatShrinkSummary(result, subject) {
|
|
1276
|
+
const saved = result.originalBytes - result.shrunkBytes;
|
|
1277
|
+
const pct = Math.round(saved / result.originalBytes * 100);
|
|
1278
|
+
const summary = `token-goat shrank ${subject}: ${toKB(result.originalBytes)}kb -> ${toKB(result.shrunkBytes)}kb (${pct}% smaller, ${result.width}x${result.height} ${result.format}).`;
|
|
1279
|
+
const dataUrl = `data:image/${result.format};base64,${result.data.toString("base64")}`;
|
|
1280
|
+
return { summary, dataUrl };
|
|
1281
|
+
}
|
|
1282
|
+
var ImageDecodeError = class extends Error {
|
|
1283
|
+
};
|
|
1284
|
+
async function probeAnyMeta(input) {
|
|
1285
|
+
const meta = probeBufferMeta(input);
|
|
1286
|
+
if (meta !== null || input.length < 16 || input.toString("latin1", 4, 8) !== "ftyp") return meta;
|
|
1287
|
+
const { probeIsoBmffMeta } = await import("./token-goat-chunk-UA5V6P3P.mjs");
|
|
1288
|
+
const isoBmff = probeIsoBmffMeta(input);
|
|
1289
|
+
return isoBmff === null ? null : { ...isoBmff, pages: 1 };
|
|
1290
|
+
}
|
|
1291
|
+
async function probeImageMeta(input) {
|
|
1292
|
+
const meta = await probeAnyMeta(input);
|
|
1293
|
+
if (meta === null) {
|
|
1294
|
+
throw new ImageDecodeError("image could not be decoded");
|
|
1295
|
+
}
|
|
1296
|
+
const cfg = loadConfig().image_shrink;
|
|
1297
|
+
const limitInputPixels = cfg.max_image_pixels > 0 ? cfg.max_image_pixels : false;
|
|
1298
|
+
if (limitInputPixels !== false && meta.width * meta.height > limitInputPixels) {
|
|
1299
|
+
throw new ImageDecodeError(`Input image exceeds pixel limit ${limitInputPixels}`);
|
|
1300
|
+
}
|
|
1301
|
+
return {
|
|
1302
|
+
width: meta.width,
|
|
1303
|
+
height: meta.height,
|
|
1304
|
+
format: meta.format,
|
|
1305
|
+
pages: meta.pages,
|
|
1306
|
+
...meta.orientation === void 0 ? {} : { orientation: meta.orientation }
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
async function exceedsConfiguredPixelLimit(input) {
|
|
1310
|
+
const meta = await probeAnyMeta(input);
|
|
1311
|
+
if (meta === null) return false;
|
|
1312
|
+
const cfg = loadConfig().image_shrink;
|
|
1313
|
+
const limitInputPixels = cfg.max_image_pixels > 0 ? cfg.max_image_pixels : false;
|
|
1314
|
+
return limitInputPixels !== false && meta.width * meta.height > limitInputPixels;
|
|
1315
|
+
}
|
|
1316
|
+
async function imageQualifiesForShrink(input) {
|
|
1317
|
+
try {
|
|
1318
|
+
const meta = await probeImageMeta(input);
|
|
1319
|
+
if (meta === null) return false;
|
|
1320
|
+
if (!canShrinkFormat(meta.format)) return false;
|
|
1321
|
+
if (input.length >= DEFAULT_SIZE_THRESHOLD_BYTES) return true;
|
|
1322
|
+
return Math.max(meta.width, meta.height) > DEFAULT_MAX_DIMENSION;
|
|
1323
|
+
} catch {
|
|
1324
|
+
return false;
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
async function shrinkImage(input, opts) {
|
|
1328
|
+
const cfg = loadConfig().image_shrink;
|
|
1329
|
+
const maxDimension = opts?.maxDimension ?? DEFAULT_MAX_DIMENSION;
|
|
1330
|
+
const quality = opts?.quality ?? cfg.jpeg_quality;
|
|
1331
|
+
const sizeThreshold = opts?.sizeThresholdBytes ?? DEFAULT_SIZE_THRESHOLD_BYTES;
|
|
1332
|
+
const originalBytes = input.length;
|
|
1333
|
+
if (originalBytes < sizeThreshold) return null;
|
|
1334
|
+
let inputMeta;
|
|
1335
|
+
try {
|
|
1336
|
+
inputMeta = await probeImageMeta(input);
|
|
1337
|
+
} catch {
|
|
1338
|
+
return null;
|
|
1339
|
+
}
|
|
1340
|
+
if (!inputMeta || inputMeta.width <= 0 || inputMeta.height <= 0) return null;
|
|
1341
|
+
try {
|
|
1342
|
+
const isAnimated = (inputMeta.pages ?? 1) > 1;
|
|
1343
|
+
const { width: targetW, height: targetH } = calculateFitInside(inputMeta.width, inputMeta.height, maxDimension);
|
|
1344
|
+
if (isAnimated && inputMeta.format === "gif") {
|
|
1345
|
+
const decodedGif = decodeGif(input);
|
|
1346
|
+
const { encodeAnimatedGifDelta } = await import("./token-goat-chunk-ZFOBKUWH.mjs");
|
|
1347
|
+
const data2 = encodeAnimatedGifDelta(decodedGif, targetW, targetH);
|
|
1348
|
+
if (data2.length >= originalBytes) return null;
|
|
1349
|
+
return {
|
|
1350
|
+
data: data2,
|
|
1351
|
+
originalBytes,
|
|
1352
|
+
shrunkBytes: data2.length,
|
|
1353
|
+
originalWidth: inputMeta.width,
|
|
1354
|
+
originalHeight: inputMeta.height,
|
|
1355
|
+
width: targetW,
|
|
1356
|
+
height: targetH,
|
|
1357
|
+
format: "gif"
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
const decode = SINGLE_FRAME_DECODERS.get(inputMeta.format ?? "");
|
|
1361
|
+
if (!decode) return null;
|
|
1362
|
+
const decoded = decode(input);
|
|
1363
|
+
if (decoded === null) return null;
|
|
1364
|
+
const oriented = applyExifOrientation(decoded.data, decoded.width, decoded.height, inputMeta.orientation);
|
|
1365
|
+
const dstRgba = resizeRgba(oriented.data, oriented.width, oriented.height, targetW, targetH);
|
|
1366
|
+
const jpegBuf = encodeJpeg(targetW, targetH, dstRgba, quality);
|
|
1367
|
+
const pngBuf = encodePng(targetW, targetH, dstRgba);
|
|
1368
|
+
let data = jpegBuf;
|
|
1369
|
+
let format = "jpeg";
|
|
1370
|
+
if (pngBuf.length < jpegBuf.length) {
|
|
1371
|
+
data = pngBuf;
|
|
1372
|
+
format = "png";
|
|
1373
|
+
}
|
|
1374
|
+
if (data.length >= originalBytes) return null;
|
|
1375
|
+
return {
|
|
1376
|
+
data,
|
|
1377
|
+
originalBytes,
|
|
1378
|
+
shrunkBytes: data.length,
|
|
1379
|
+
originalWidth: inputMeta.width,
|
|
1380
|
+
originalHeight: inputMeta.height,
|
|
1381
|
+
width: targetW,
|
|
1382
|
+
height: targetH,
|
|
1383
|
+
format
|
|
1384
|
+
};
|
|
1385
|
+
} catch {
|
|
1386
|
+
return null;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function statInfo(absPath) {
|
|
1390
|
+
try {
|
|
1391
|
+
const st = fs2.statSync(absPath);
|
|
1392
|
+
return st.isFile() ? { size: st.size, mtimeMs: st.mtimeMs } : null;
|
|
1393
|
+
} catch {
|
|
1394
|
+
return null;
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
function imageShrinkCacheDir() {
|
|
1398
|
+
return path2.join(tokenGoatHome(), "image_shrink_cache");
|
|
1399
|
+
}
|
|
1400
|
+
var SHRINK_ENGINE_REVISION = 3;
|
|
1401
|
+
function shrinkCacheKeyForRevision(revision, originalPath, size, mtimeMs, quality) {
|
|
1402
|
+
return createHash("sha256").update(`r${revision}:${originalPath}:${size}:${mtimeMs}:${quality}`).digest("hex").slice(0, 16);
|
|
1403
|
+
}
|
|
1404
|
+
function shrinkCacheKey(originalPath, size, mtimeMs, quality) {
|
|
1405
|
+
return shrinkCacheKeyForRevision(SHRINK_ENGINE_REVISION, originalPath, size, mtimeMs, quality);
|
|
1406
|
+
}
|
|
1407
|
+
var SHRINK_CACHE_EXTENSION = /* @__PURE__ */ new Map([
|
|
1408
|
+
["jpeg", ".jpg"],
|
|
1409
|
+
["png", ".png"],
|
|
1410
|
+
["gif", ".gif"]
|
|
1411
|
+
]);
|
|
1412
|
+
var SHRINK_CACHE_FORMAT = /* @__PURE__ */ new Map([
|
|
1413
|
+
[".jpg", "jpeg"],
|
|
1414
|
+
[".png", "png"],
|
|
1415
|
+
[".gif", "gif"],
|
|
1416
|
+
[".webp", "webp"]
|
|
1417
|
+
]);
|
|
1418
|
+
function findCachedShrink(originalPath, size, mtimeMs, quality) {
|
|
1419
|
+
const prefix = `token-goat-shrink-${shrinkCacheKey(originalPath, size, mtimeMs, quality)}-`;
|
|
1420
|
+
const dir = imageShrinkCacheDir();
|
|
1421
|
+
let entries;
|
|
1422
|
+
try {
|
|
1423
|
+
entries = fs2.readdirSync(dir);
|
|
1424
|
+
} catch {
|
|
1425
|
+
return null;
|
|
1426
|
+
}
|
|
1427
|
+
for (const file of entries) {
|
|
1428
|
+
if (!file.startsWith(prefix)) continue;
|
|
1429
|
+
const m = /^(\d+)x(\d+)(\.webp|\.jpg|\.png|\.gif)$/.exec(file.slice(prefix.length));
|
|
1430
|
+
if (m === null) continue;
|
|
1431
|
+
const format = SHRINK_CACHE_FORMAT.get(m[3] ?? "");
|
|
1432
|
+
if (format === void 0) continue;
|
|
1433
|
+
return {
|
|
1434
|
+
filePath: path2.join(dir, file),
|
|
1435
|
+
format,
|
|
1436
|
+
originalWidth: Number(m[1]),
|
|
1437
|
+
originalHeight: Number(m[2])
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
return null;
|
|
1441
|
+
}
|
|
1442
|
+
function writeCachedShrink(originalPath, result, mtimeMs, quality) {
|
|
1443
|
+
try {
|
|
1444
|
+
const dir = imageShrinkCacheDir();
|
|
1445
|
+
ensureDirSync(dir);
|
|
1446
|
+
const key = shrinkCacheKey(originalPath, result.originalBytes, mtimeMs, quality);
|
|
1447
|
+
const ext = SHRINK_CACHE_EXTENSION.get(result.format);
|
|
1448
|
+
if (ext === void 0) return;
|
|
1449
|
+
atomicWriteBytes(path2.join(dir, `token-goat-shrink-${key}-${result.originalWidth}x${result.originalHeight}${ext}`), result.data);
|
|
1450
|
+
} catch {
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
var SHRINK_CACHE_PRUNE_THROTTLE_MS = 6e4;
|
|
1454
|
+
var lastShrinkCachePruneAtMs = 0;
|
|
1455
|
+
function pruneShrinkCache() {
|
|
1456
|
+
const now = Date.now();
|
|
1457
|
+
if (now - lastShrinkCachePruneAtMs < SHRINK_CACHE_PRUNE_THROTTLE_MS) return;
|
|
1458
|
+
lastShrinkCachePruneAtMs = now;
|
|
1459
|
+
try {
|
|
1460
|
+
const dir = imageShrinkCacheDir();
|
|
1461
|
+
if (!fs2.existsSync(dir)) return;
|
|
1462
|
+
const cutoff = now - DEFAULT_MAX_AGE_MS;
|
|
1463
|
+
for (const file of fs2.readdirSync(dir)) {
|
|
1464
|
+
if (!file.startsWith("token-goat-shrink-")) continue;
|
|
1465
|
+
const full = path2.join(dir, file);
|
|
1466
|
+
try {
|
|
1467
|
+
const st = fs2.statSync(full);
|
|
1468
|
+
if (st.mtimeMs < cutoff) fs2.unlinkSync(full);
|
|
1469
|
+
} catch {
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
} catch {
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
var HOST_MATERIALIZED_HARNESSES = /* @__PURE__ */ new Set(["copilot_cli", "pi", "openclaw"]);
|
|
1476
|
+
var PATH_REWRITE_HARNESSES = /* @__PURE__ */ new Set(["vscode", "claudecode"]);
|
|
1477
|
+
async function finalizeShrinkResult(result, filePath, event) {
|
|
1478
|
+
const basename5 = displaySafePath(path2.basename(filePath));
|
|
1479
|
+
const harness = detectHarness();
|
|
1480
|
+
if (PATH_REWRITE_HARNESSES.has(harness)) {
|
|
1481
|
+
const file = materializeShrunkImageFile(formatShrinkSummary(result, basename5).dataUrl);
|
|
1482
|
+
if (file === void 0) return passOutput();
|
|
1483
|
+
const cwd = getCwd(event) ?? process.cwd();
|
|
1484
|
+
const rewrite = permissionNeutralRewrite({ ...event.toolInput, file_path: file }, { kind: "read", harness, mode: event.raw["permission_mode"], cwd, original: filePath, rewritten: file, insideCwd: vscodePathAllowed(filePath, cwd), agentType: agentTypeOf(event.raw) });
|
|
1485
|
+
if (rewrite === null) {
|
|
1486
|
+
try {
|
|
1487
|
+
fs2.unlinkSync(file);
|
|
1488
|
+
} catch {
|
|
1489
|
+
}
|
|
1490
|
+
return passOutput();
|
|
1491
|
+
}
|
|
1492
|
+
if (deliveryReplacesRead(harness, rewrite)) recordSavedShrink(result, basename5);
|
|
1493
|
+
recordFileRead(filePath);
|
|
1494
|
+
return rewrite;
|
|
1495
|
+
}
|
|
1496
|
+
const { summary, dataUrl } = formatShrinkSummary(result, basename5);
|
|
1497
|
+
recordFileRead(filePath);
|
|
1498
|
+
return contextOutput(`${summary}
|
|
1499
|
+
${dataUrl}`);
|
|
1500
|
+
}
|
|
1501
|
+
function deliveryReplacesRead(harness, output) {
|
|
1502
|
+
return output.hookType === "rewriteInput" && PATH_REWRITE_HARNESSES.has(harness);
|
|
1503
|
+
}
|
|
1504
|
+
function shrinkCanReplaceRead(harness) {
|
|
1505
|
+
return PATH_REWRITE_HARNESSES.has(harness) || HOST_MATERIALIZED_HARNESSES.has(harness) || harness === "generic";
|
|
1506
|
+
}
|
|
1507
|
+
function recordSavedShrink(result, basename5) {
|
|
1508
|
+
const tier = loadConfig().image_shrink.vision_tier;
|
|
1509
|
+
recordStat("image_shrink", result.originalBytes - result.shrunkBytes, visionTokensSaved(result.originalWidth, result.originalHeight, result.width, result.height, tier), void 0, basename5);
|
|
1510
|
+
}
|
|
1511
|
+
async function preReadImageHandler(event) {
|
|
1512
|
+
if (loadConfig().image_shrink.enabled === false) return passOutput();
|
|
1513
|
+
const vscodeTool = event.raw[VSCODE_TOOL_NAME_KEY];
|
|
1514
|
+
if (vscodeTool !== void 0 && vscodeTool !== "view_image") return passOutput();
|
|
1515
|
+
const filePath = getFilePath(event);
|
|
1516
|
+
if (filePath === void 0) return passOutput();
|
|
1517
|
+
if (!isImagePath(filePath)) return passOutput();
|
|
1518
|
+
if (preToolPathDeclined(event, filePath)) return passOutput();
|
|
1519
|
+
const harness = detectHarness();
|
|
1520
|
+
if ((harness === "claudecode" || harness === "copilot_cli") && !vscodePathAllowed(filePath, getCwd(event))) return passOutput();
|
|
1521
|
+
if (!shrinkCanReplaceRead(harness)) return passOutput();
|
|
1522
|
+
pruneShrinkCache();
|
|
1523
|
+
const stat = statInfo(filePath);
|
|
1524
|
+
if (stat === null) return passOutput();
|
|
1525
|
+
const quality = loadConfig().image_shrink.jpeg_quality;
|
|
1526
|
+
const cached = findCachedShrink(filePath, stat.size, stat.mtimeMs, quality);
|
|
1527
|
+
if (cached !== null) {
|
|
1528
|
+
let cachedData;
|
|
1529
|
+
try {
|
|
1530
|
+
cachedData = fs2.readFileSync(cached.filePath);
|
|
1531
|
+
} catch {
|
|
1532
|
+
cachedData = null;
|
|
1533
|
+
}
|
|
1534
|
+
let meta = null;
|
|
1535
|
+
if (cachedData !== null) {
|
|
1536
|
+
try {
|
|
1537
|
+
meta = await probeImageMeta(cachedData);
|
|
1538
|
+
} catch {
|
|
1539
|
+
meta = null;
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
if (cachedData !== null && meta !== null) {
|
|
1543
|
+
const result2 = {
|
|
1544
|
+
data: cachedData,
|
|
1545
|
+
originalBytes: stat.size,
|
|
1546
|
+
shrunkBytes: cachedData.length,
|
|
1547
|
+
originalWidth: cached.originalWidth,
|
|
1548
|
+
originalHeight: cached.originalHeight,
|
|
1549
|
+
width: meta.width,
|
|
1550
|
+
height: meta.height,
|
|
1551
|
+
format: cached.format
|
|
1552
|
+
};
|
|
1553
|
+
return finalizeShrinkResult(result2, filePath, event);
|
|
1554
|
+
}
|
|
1555
|
+
try {
|
|
1556
|
+
fs2.unlinkSync(cached.filePath);
|
|
1557
|
+
} catch {
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
let input;
|
|
1561
|
+
try {
|
|
1562
|
+
input = fs2.readFileSync(filePath);
|
|
1563
|
+
} catch {
|
|
1564
|
+
return passOutput();
|
|
1565
|
+
}
|
|
1566
|
+
if (!await imageQualifiesForShrink(input)) {
|
|
1567
|
+
if (await exceedsConfiguredPixelLimit(input)) recordStat("image_shrink_over_pixel_limit");
|
|
1568
|
+
return passOutput();
|
|
1569
|
+
}
|
|
1570
|
+
const result = await shrinkImage(input, { quality, sizeThresholdBytes: 0 });
|
|
1571
|
+
if (result === null) {
|
|
1572
|
+
recordStat("image_shrink_skipped");
|
|
1573
|
+
return passOutput();
|
|
1574
|
+
}
|
|
1575
|
+
writeCachedShrink(filePath, result, stat.mtimeMs, quality);
|
|
1576
|
+
return finalizeShrinkResult(result, filePath, event);
|
|
1577
|
+
}
|
|
1578
|
+
registerHook("pre_tool_use", loadingHiddenRuleCheck(preReadImageHandler), { toolName: "Read" });
|
|
1579
|
+
|
|
1580
|
+
// src/hooks_read.ts
|
|
1581
|
+
init_define_import_meta_env();
|
|
1582
|
+
import * as fs8 from "node:fs";
|
|
1583
|
+
import * as path6 from "node:path";
|
|
1584
|
+
|
|
1585
|
+
// src/hint_target.ts
|
|
1586
|
+
init_define_import_meta_env();
|
|
1587
|
+
import { openSync, readSync, closeSync, statSync as statSync2 } from "node:fs";
|
|
1588
|
+
|
|
1589
|
+
// src/index_freshness.ts
|
|
1590
|
+
init_define_import_meta_env();
|
|
1591
|
+
function indexMatchesDisk(resolved) {
|
|
1592
|
+
const entry = getFileEntry(resolved);
|
|
1593
|
+
if (entry === null) return false;
|
|
1594
|
+
if (entry.sha === "") return false;
|
|
1595
|
+
const diskSha = fingerprintFile(hostPathOfIndexKey(resolved));
|
|
1596
|
+
return diskSha !== null && diskSha === entry.sha;
|
|
1597
|
+
}
|
|
1598
|
+
|
|
1599
|
+
// src/hint_target.ts
|
|
1600
|
+
var HINT_PLACEHOLDERS = { section: "SectionHeading", symbol: "SymbolName", key: "KEY_NAME", table: "table_name" };
|
|
1601
|
+
var HINT_TARGET_SCAN_BYTES = 32 * 1024;
|
|
1602
|
+
var HINT_TARGET_INDEX_ROWS = 500;
|
|
1603
|
+
var HINT_TARGET_CANDIDATES = 20;
|
|
1604
|
+
var MAX_HINT_NAME_CHARS = 120;
|
|
1605
|
+
var DECLARATION_WORDS = /^(?:def|class|function|async|export|default|interface|type|const|enum)$/;
|
|
1606
|
+
var SECTION_PATH_RE = /\.(?:md|mdx|markdown|rst|txt|html?|toml|ini|cfg|conf)$/i;
|
|
1607
|
+
var TABLE_HEADER_PATH_RE = /\.(?:toml|ini|cfg|conf)$/i;
|
|
1608
|
+
var KEY_PATH_RE = /\.(?:jsonc?|ya?ml|properties|env)$/i;
|
|
1609
|
+
var ENV_BASENAME_RE = /(?:^|[\\/])\.env(?:\.[\w.-]+)?$/i;
|
|
1610
|
+
var SLICE_KINDS = {
|
|
1611
|
+
section: /* @__PURE__ */ new Set(["heading", "section"]),
|
|
1612
|
+
key: /* @__PURE__ */ new Set(["property", "key", "env_key"]),
|
|
1613
|
+
table: /* @__PURE__ */ new Set(["sql_table", "sql_type", "sql_view"])
|
|
1614
|
+
};
|
|
1615
|
+
var JSON_FIRST_KEY_RE = /^\s*\{\s*"((?:[^"\\\r\n]|\\.)*)"\s*:/;
|
|
1616
|
+
var YAML_KEY_RE = /^([A-Za-z_][\w.-]*)[ \t]*:/gm;
|
|
1617
|
+
var ENV_KEY_RE = /^(?:export[ \t]+)?([A-Za-z_][\w.-]*)[ \t]*[=:]/gm;
|
|
1618
|
+
var TABLE_HEADER_RE = /^[ \t]*\[([^[\]\r\n]+)\]/gm;
|
|
1619
|
+
var SQL_CREATE_RE = /\bCREATE\s+(?:(?:OR\s+REPLACE|TEMP|TEMPORARY|UNLOGGED)\s+)*(?:TABLE|TYPE|VIEW)\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"\r\n]+)"|`([^`\r\n]+)`|\[([^\]\r\n]+)\]|([\w.]+))/gi;
|
|
1620
|
+
var HEADING_LEVEL_RE = /^\s*(#{1,6})\s/;
|
|
1621
|
+
var SETEXT_DASH_RE = /\n[ \t]*-{3,}[ \t]*$/;
|
|
1622
|
+
var SETEXT_UNDERLINE_RE = /\n[ \t]*([=-])\1*[ \t]*$/;
|
|
1623
|
+
function headingLevel(body) {
|
|
1624
|
+
const atx = HEADING_LEVEL_RE.exec(body)?.[1]?.length;
|
|
1625
|
+
if (atx !== void 0) return atx;
|
|
1626
|
+
const underline = SETEXT_UNDERLINE_RE.exec(body)?.[1];
|
|
1627
|
+
return underline === "=" ? 1 : underline === "-" ? 2 : 0;
|
|
1628
|
+
}
|
|
1629
|
+
function sliceForPath(filePath) {
|
|
1630
|
+
if (/\.sql$/i.test(filePath)) return "table";
|
|
1631
|
+
if (KEY_PATH_RE.test(filePath) || ENV_BASENAME_RE.test(filePath)) return "key";
|
|
1632
|
+
if (SECTION_PATH_RE.test(filePath)) return "section";
|
|
1633
|
+
return "symbol";
|
|
1634
|
+
}
|
|
1635
|
+
function escapeHintName(name) {
|
|
1636
|
+
const trimmed = name.trim();
|
|
1637
|
+
if (trimmed === "" || displaySafeText(trimmed) !== trimmed) return "";
|
|
1638
|
+
const probe = "token-goat read " + quotedArg(trimmed);
|
|
1639
|
+
return stripUnsafeSuggestions(probe) === probe ? trimmed : "";
|
|
1640
|
+
}
|
|
1641
|
+
function quotable(raw, slice) {
|
|
1642
|
+
const name = raw.trim();
|
|
1643
|
+
if (name === "" || name.length > MAX_HINT_NAME_CHARS || name.includes("\\") || name.includes("::")) return null;
|
|
1644
|
+
if (slice === "key" && name.includes('"')) return null;
|
|
1645
|
+
return escapeHintName(name) === "" ? null : name;
|
|
1646
|
+
}
|
|
1647
|
+
function continuesName(text, i, step) {
|
|
1648
|
+
const ch = text[i];
|
|
1649
|
+
if (ch === void 0) return false;
|
|
1650
|
+
if (/\w/.test(ch)) return true;
|
|
1651
|
+
return (ch === "-" || ch === "$") && /\w/.test(text[i + step] ?? "");
|
|
1652
|
+
}
|
|
1653
|
+
function namedIn(text, name) {
|
|
1654
|
+
for (let at = text.indexOf(name); at !== -1; at = text.indexOf(name, at + 1)) {
|
|
1655
|
+
if (!continuesName(text, at - 1, -1) && !continuesName(text, at + name.length, 1)) return true;
|
|
1656
|
+
}
|
|
1657
|
+
return false;
|
|
1658
|
+
}
|
|
1659
|
+
function pickForPattern(candidates, pattern, slice) {
|
|
1660
|
+
const section = slice === "section";
|
|
1661
|
+
const held = Array.from(new Set(candidates.flatMap((c) => quotable(c.name, slice) ?? [])));
|
|
1662
|
+
const text = pattern.replace(/\\[A-Za-z]/g, " ");
|
|
1663
|
+
if (!section) {
|
|
1664
|
+
const heldSet = new Set(held);
|
|
1665
|
+
const pair = Array.from(text.matchAll(/([A-Za-z_$][\w$]*)\.([A-Za-z_$][\w$]*)/g)).find((m) => heldSet.has(m[1]) && heldSet.has(m[2]));
|
|
1666
|
+
if (pair !== void 0) return pair[1] + "." + pair[2];
|
|
1667
|
+
}
|
|
1668
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1669
|
+
for (const c of candidates) counts.set(c.name.trim(), (counts.get(c.name.trim()) ?? 0) + 1);
|
|
1670
|
+
const named = held.filter((name) => (section || !DECLARATION_WORDS.test(name)) && (!section || counts.get(name) === 1) && namedIn(text, name));
|
|
1671
|
+
return named.sort((a, b) => b.length - a.length)[0] ?? null;
|
|
1672
|
+
}
|
|
1673
|
+
function pick(candidates, slice) {
|
|
1674
|
+
const usable = candidates.flatMap((c) => {
|
|
1675
|
+
const name = quotable(c.name, slice);
|
|
1676
|
+
return name === null ? [] : [{ name, level: c.level }];
|
|
1677
|
+
});
|
|
1678
|
+
if (slice !== "section") return usable[0]?.name ?? null;
|
|
1679
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1680
|
+
for (const c of candidates) counts.set(c.name.trim(), (counts.get(c.name.trim()) ?? 0) + 1);
|
|
1681
|
+
const unique = usable.filter((c) => counts.get(c.name) === 1);
|
|
1682
|
+
const first = unique[0];
|
|
1683
|
+
if (first === void 0) return null;
|
|
1684
|
+
const top = Math.min(...unique.map((c) => c.level));
|
|
1685
|
+
const loneTitle = first.level === top && unique.filter((c) => c.level === top).length === 1;
|
|
1686
|
+
return loneTitle && unique.length > 1 ? unique[1].name : first.name;
|
|
1687
|
+
}
|
|
1688
|
+
function scanText(text, filePath, slice) {
|
|
1689
|
+
const head = text.length > HINT_TARGET_SCAN_BYTES ? text.slice(0, text.lastIndexOf("\n", HINT_TARGET_SCAN_BYTES)) : text;
|
|
1690
|
+
const all = (re) => Array.from(head.matchAll(re), (m) => ({ name: m.slice(1).find((g) => g !== void 0) ?? "", level: 0 })).slice(0, HINT_TARGET_CANDIDATES);
|
|
1691
|
+
switch (slice) {
|
|
1692
|
+
case "section": {
|
|
1693
|
+
if (TABLE_HEADER_PATH_RE.test(filePath)) return all(TABLE_HEADER_RE);
|
|
1694
|
+
const metaEnd = frontMatterEnd(head);
|
|
1695
|
+
return extractMarkdownHeadings(head).filter((h) => h.lineNumber > metaEnd).map((h) => ({ name: h.text, level: h.level }));
|
|
1696
|
+
}
|
|
1697
|
+
case "table":
|
|
1698
|
+
return all(SQL_CREATE_RE);
|
|
1699
|
+
case "key": {
|
|
1700
|
+
if (/\.jsonc?$/i.test(filePath)) {
|
|
1701
|
+
const first = JSON_FIRST_KEY_RE.exec(head)?.[1];
|
|
1702
|
+
return first === void 0 ? [] : [{ name: first, level: 0 }];
|
|
1703
|
+
}
|
|
1704
|
+
return all(/\.ya?ml$/i.test(filePath) ? YAML_KEY_RE : ENV_KEY_RE);
|
|
1705
|
+
}
|
|
1706
|
+
case "symbol":
|
|
1707
|
+
return extractQuickSymbolSamples(head, filePath, HINT_TARGET_CANDIDATES).map((name) => ({ name, level: 0 }));
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
function frontMatterEnd(text) {
|
|
1711
|
+
if (!/^---\r?\n/.test(text)) return 0;
|
|
1712
|
+
const close = text.split(/\r?\n/).findIndex((line, i) => i > 0 && /^(?:---|\.\.\.)\s*$/.test(line));
|
|
1713
|
+
return close === -1 ? 0 : close + 1;
|
|
1714
|
+
}
|
|
1715
|
+
function readHead(onDisk) {
|
|
1716
|
+
const fd = openSync(onDisk, "r");
|
|
1717
|
+
try {
|
|
1718
|
+
const buf = Buffer.alloc(HINT_TARGET_SCAN_BYTES);
|
|
1719
|
+
const n = readSync(fd, buf, 0, buf.length, 0);
|
|
1720
|
+
const text = buf.subarray(0, n).toString("utf8");
|
|
1721
|
+
return n < buf.length ? text : text.slice(0, text.lastIndexOf("\n") + 1);
|
|
1722
|
+
} finally {
|
|
1723
|
+
closeSync(fd);
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
function indexedCandidates(resolved, onDisk, slice) {
|
|
1727
|
+
if (!indexMatchesDisk(resolved)) return null;
|
|
1728
|
+
const kinds = slice === "symbol" ? null : SLICE_KINDS[slice];
|
|
1729
|
+
const rows = querySymbols({ filePath: resolved, limit: HINT_TARGET_INDEX_ROWS }).filter((s) => kinds === null || kinds.has(s.kind));
|
|
1730
|
+
const metaEnd = slice === "section" && rows.some((s) => SETEXT_DASH_RE.test(s.body)) ? frontMatterEnd(readHead(onDisk)) : 0;
|
|
1731
|
+
return rows.filter((s) => s.lineStart > metaEnd).map((s) => ({ name: s.name, level: s.kind === "heading" ? headingLevel(s.body) : 0 }));
|
|
1732
|
+
}
|
|
1733
|
+
var MORE_HEADINGS_LINE_RE = /^ {2}\.\.\. \(\d+ more headings\)$/;
|
|
1734
|
+
function headingTreeParts(headings, filePath, content) {
|
|
1735
|
+
const parts = formatHeadingTreeParts(headings, filePath);
|
|
1736
|
+
const total = extractMarkdownHeadings(content, Number.MAX_SAFE_INTEGER).length;
|
|
1737
|
+
if (total <= headings.length) return parts;
|
|
1738
|
+
const guidance = parts.guidance.replace(`Large markdown file (${headings.length} headings)`, () => `Large markdown file (showing the first ${headings.length} of ${total} headings)`);
|
|
1739
|
+
const lines = parts.sectionsList.split("\n");
|
|
1740
|
+
const kept = lines.filter((l) => !MORE_HEADINGS_LINE_RE.test(l));
|
|
1741
|
+
return { guidance, sectionsList: [...kept, ` ... (${countNoun(total - kept.length, "more heading")})`].join("\n") };
|
|
1742
|
+
}
|
|
1743
|
+
function hintTargetFromNames(names, slice) {
|
|
1744
|
+
const found = pick(names.filter((name) => name === name.trim()).map((name) => ({ name, level: 0 })), slice);
|
|
1745
|
+
return found === null ? { name: HINT_PLACEHOLDERS[slice], real: false, slice } : { name: found, real: true, slice };
|
|
1746
|
+
}
|
|
1747
|
+
function hintTarget(filePath, slice, source = {}) {
|
|
1748
|
+
const found = (() => {
|
|
1749
|
+
try {
|
|
1750
|
+
if (source.headings !== void 0 && slice === "section") {
|
|
1751
|
+
const metaEnd = frontMatterEnd(source.content ?? "");
|
|
1752
|
+
const name = pick(source.headings.filter((h) => h.lineNumber > metaEnd).map((h) => ({ name: h.text, level: h.level })), slice);
|
|
1753
|
+
if (name !== null) return name;
|
|
1754
|
+
}
|
|
1755
|
+
if (source.content !== void 0) return pick(scanText(source.content, filePath, slice), slice);
|
|
1756
|
+
if (source.cwd !== void 0 && !commandPathIsTouchable(filePath, source.event)) return null;
|
|
1757
|
+
const resolved = source.cwd !== void 0 ? resolveIndexPath(filePath, source.cwd) : filePath;
|
|
1758
|
+
const onDisk = hostPathOfIndexKey(resolved);
|
|
1759
|
+
if (!statSync2(onDisk).isFile()) return null;
|
|
1760
|
+
const indexed = indexedCandidates(resolved, onDisk, slice);
|
|
1761
|
+
const byPattern = (c) => source.pattern === void 0 || slice !== "symbol" && slice !== "section" ? null : pickForPattern(c, source.pattern, slice);
|
|
1762
|
+
const fromIndex = indexed === null ? null : byPattern(indexed) ?? pick(indexed, slice);
|
|
1763
|
+
if (fromIndex !== null) return fromIndex;
|
|
1764
|
+
const scanned = scanText(readHead(onDisk), resolved, slice);
|
|
1765
|
+
return byPattern(scanned) ?? pick(scanned, slice);
|
|
1766
|
+
} catch {
|
|
1767
|
+
return null;
|
|
1768
|
+
}
|
|
1769
|
+
})();
|
|
1770
|
+
return found === null ? { name: source.placeholder ?? HINT_PLACEHOLDERS[slice], real: false, slice } : { name: found, real: true, slice };
|
|
1771
|
+
}
|
|
1772
|
+
function sliceCommand(shownPath, target) {
|
|
1773
|
+
switch (target.slice) {
|
|
1774
|
+
case "section":
|
|
1775
|
+
return "token-goat section " + quotedArg(shownPath + "::" + target.name);
|
|
1776
|
+
case "key": {
|
|
1777
|
+
const format = structuredFormat(shownPath);
|
|
1778
|
+
if (format === null) return configGetCommand(shownPath, target.name);
|
|
1779
|
+
if (/^[\w-]+$/.test(target.name)) return "token-goat " + format + "-query " + quotedArgs(shownPath, target.name).join(" ");
|
|
1780
|
+
if (!target.name.includes("'")) return "token-goat " + format + "-query " + quotedArgs(shownPath, "['" + target.name + "']").join(" ");
|
|
1781
|
+
return "token-goat " + format + "-outline " + quotedArg(shownPath);
|
|
1782
|
+
}
|
|
1783
|
+
case "symbol":
|
|
1784
|
+
case "table":
|
|
1785
|
+
return "token-goat read " + quotedArg(shownPath + "::" + target.name);
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
var NO_HEADING_RANGE_LINES = 80;
|
|
1789
|
+
function sectionOrRangeCommand(shownPath, heading) {
|
|
1790
|
+
return heading.real ? sliceCommand(shownPath, heading) : "token-goat read " + quotedArg(shownPath + "::1-" + NO_HEADING_RANGE_LINES);
|
|
1791
|
+
}
|
|
1792
|
+
var JSON_LOCK_BASENAMES = /* @__PURE__ */ new Set(["package-lock.json", "pipfile.lock", "composer.lock", "package.resolved"]);
|
|
1793
|
+
var YAML_LOCK_BASENAMES = /* @__PURE__ */ new Set(["pnpm-lock.yaml", "pubspec.lock"]);
|
|
1794
|
+
function structuredFormat(shownPath) {
|
|
1795
|
+
const base = (shownPath.split("/").pop() ?? "").toLowerCase();
|
|
1796
|
+
if (JSON_LOCK_BASENAMES.has(base) || /\.jsonc?$/.test(base)) return "json";
|
|
1797
|
+
if (YAML_LOCK_BASENAMES.has(base) || /\.ya?ml$/.test(base)) return "yaml";
|
|
1798
|
+
return null;
|
|
1799
|
+
}
|
|
1800
|
+
function fileQueryHint(shownPath, reason = "", grepSubject = "pattern") {
|
|
1801
|
+
const format = structuredFormat(shownPath);
|
|
1802
|
+
if (format !== null) {
|
|
1803
|
+
return leadWithCommand("token-goat " + format + "-outline " + quotedArg(shownPath), "to list the top-level keys, then `token-goat " + format + "-query " + quotedArgs(shownPath, "<key>").join(" ") + "` to read one value", reason);
|
|
1804
|
+
}
|
|
1805
|
+
if (/\.(toml|ini|cfg)$/i.test(shownPath)) return leadWithCommand(configGetCommand(shownPath, "<key>"), "to read one value", reason);
|
|
1806
|
+
if (/\.(xml|csproj)$/i.test(shownPath)) return leadWithCommand("token-goat xml-outline " + quotedArg(shownPath), "to see the element structure, then `token-goat xml-query " + quotedArgs(shownPath, "<path>").join(" ") + "` to read one element", reason);
|
|
1807
|
+
return grepLinesHint("<" + grepSubject + ">", shownPath, reason);
|
|
1808
|
+
}
|
|
1809
|
+
var LED_COMMAND_RE = /`(token-goat [^`\r\n]+)`/;
|
|
1810
|
+
function sharpenRepeatedDeny(event, output) {
|
|
1811
|
+
if (output.hookType !== "deny" || !event.sessionId || DELIVERS_CONTENT_RE.test(output.message)) return output;
|
|
1812
|
+
const key = "deny-repeat:" + sessionStateKey(event) + ":" + (event.toolName ?? "") + ":" + getCompactedAt() + ":" + shortFingerprint(output.message) + ":" + shortFingerprint(JSON.stringify(event.toolInput));
|
|
1813
|
+
if (!wasHintShown(key)) {
|
|
1814
|
+
markHintShown(key);
|
|
1815
|
+
return output;
|
|
1816
|
+
}
|
|
1817
|
+
const command = LED_COMMAND_RE.exec(output.message)?.[1];
|
|
1818
|
+
const lead = command === void 0 ? "" : "Run `" + command + "` instead. ";
|
|
1819
|
+
return { hookType: "deny", message: "[tg] " + lead + "Repeat refusal of this exact call; the earlier refusal this session has the full reason." };
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
// src/hooks_read_slice.ts
|
|
1823
|
+
init_define_import_meta_env();
|
|
1824
|
+
import * as fs4 from "node:fs";
|
|
1825
|
+
|
|
1826
|
+
// src/snapshots.ts
|
|
1827
|
+
init_define_import_meta_env();
|
|
1828
|
+
import * as fs3 from "node:fs";
|
|
1829
|
+
import * as path3 from "node:path";
|
|
1830
|
+
var MAX_SNAPSHOTS_PER_SESSION = 150;
|
|
1831
|
+
var MAX_SNAPSHOT_BYTES = 256 * 1024;
|
|
1832
|
+
var SNAPSHOT_TRUNCATE_BYTES = 50 * 1024;
|
|
1833
|
+
var KIND_READ = "read";
|
|
1834
|
+
var KIND_PREDICTIVE = "predictive";
|
|
1835
|
+
var VALID_KINDS = /* @__PURE__ */ new Set([KIND_READ, KIND_PREDICTIVE]);
|
|
1836
|
+
function sessionDir(sessionId) {
|
|
1837
|
+
if (!sessionId) return null;
|
|
1838
|
+
const safe = sanitizeIdForFilename(sessionId, 64, "anon");
|
|
1839
|
+
const base = path3.join(tokenGoatHome(), "session_snapshots");
|
|
1840
|
+
const candidate = path3.join(base, safe);
|
|
1841
|
+
try {
|
|
1842
|
+
const rel = path3.relative(base, candidate);
|
|
1843
|
+
if (rel.startsWith("..")) return null;
|
|
1844
|
+
} catch {
|
|
1845
|
+
return null;
|
|
1846
|
+
}
|
|
1847
|
+
return candidate;
|
|
1848
|
+
}
|
|
1849
|
+
function pathKey(filePath) {
|
|
1850
|
+
return fingerprintContent(foldPath(normalizePath(filePath))).slice(0, 32);
|
|
1851
|
+
}
|
|
1852
|
+
function snapshot_path(sessionId, filePath) {
|
|
1853
|
+
const d = sessionDir(sessionId);
|
|
1854
|
+
if (!d) return null;
|
|
1855
|
+
return path3.join(d, `${pathKey(filePath)}.bin`);
|
|
1856
|
+
}
|
|
1857
|
+
function kindSidecarPath(snapshotPath) {
|
|
1858
|
+
return snapshotPath + ".kind";
|
|
1859
|
+
}
|
|
1860
|
+
function readSnapshotKind(sidecarPath) {
|
|
1861
|
+
try {
|
|
1862
|
+
if (!fs3.existsSync(sidecarPath)) return null;
|
|
1863
|
+
const raw = fs3.readFileSync(sidecarPath, "utf8").trim();
|
|
1864
|
+
return VALID_KINDS.has(raw) ? raw : null;
|
|
1865
|
+
} catch {
|
|
1866
|
+
return null;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
function writeSnapshotKind(sidecarPath, kind) {
|
|
1870
|
+
try {
|
|
1871
|
+
const safeKind = VALID_KINDS.has(kind) ? kind : KIND_READ;
|
|
1872
|
+
const dir = path3.dirname(sidecarPath);
|
|
1873
|
+
if (!fs3.existsSync(dir)) {
|
|
1874
|
+
ensureDirSync(dir);
|
|
1875
|
+
}
|
|
1876
|
+
fs3.writeFileSync(sidecarPath, safeKind, "utf8");
|
|
1877
|
+
return true;
|
|
1878
|
+
} catch {
|
|
1879
|
+
return false;
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
function evictOldest(d, maxCount) {
|
|
1883
|
+
try {
|
|
1884
|
+
const entries = [];
|
|
1885
|
+
const files = fs3.readdirSync(d);
|
|
1886
|
+
for (const file of files) {
|
|
1887
|
+
const fullPath = path3.join(d, file);
|
|
1888
|
+
if (!file.endsWith(".bin")) continue;
|
|
1889
|
+
try {
|
|
1890
|
+
const stat = fs3.statSync(fullPath);
|
|
1891
|
+
entries.push([fullPath, stat.mtimeMs]);
|
|
1892
|
+
} catch {
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
if (entries.length <= maxCount) return 0;
|
|
1897
|
+
entries.sort((a, b) => a[1] - b[1]);
|
|
1898
|
+
let removed = 0;
|
|
1899
|
+
const over = entries.length - maxCount;
|
|
1900
|
+
for (const [p] of entries.slice(0, over)) {
|
|
1901
|
+
try {
|
|
1902
|
+
fs3.unlinkSync(p);
|
|
1903
|
+
removed++;
|
|
1904
|
+
try {
|
|
1905
|
+
fs3.unlinkSync(kindSidecarPath(p));
|
|
1906
|
+
} catch {
|
|
1907
|
+
}
|
|
1908
|
+
} catch {
|
|
1909
|
+
continue;
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
return removed;
|
|
1913
|
+
} catch {
|
|
1914
|
+
return 0;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
function store(sessionId, filePath, content, opts = {}) {
|
|
1918
|
+
const kind = opts.kind ?? KIND_READ;
|
|
1919
|
+
const origLen = content.length;
|
|
1920
|
+
if (origLen > MAX_SNAPSHOT_BYTES) {
|
|
1921
|
+
return null;
|
|
1922
|
+
}
|
|
1923
|
+
let stored = content;
|
|
1924
|
+
if (origLen > SNAPSHOT_TRUNCATE_BYTES) {
|
|
1925
|
+
const marker = Buffer.from(`
|
|
1926
|
+
<snapshot truncated at ${origLen} bytes>
|
|
1927
|
+
`);
|
|
1928
|
+
stored = Buffer.concat([content.slice(0, SNAPSHOT_TRUNCATE_BYTES), marker]);
|
|
1929
|
+
}
|
|
1930
|
+
const p = snapshot_path(sessionId, filePath);
|
|
1931
|
+
if (!p) return null;
|
|
1932
|
+
const sha = fingerprintContent(stored);
|
|
1933
|
+
try {
|
|
1934
|
+
const isNewEntry = !fs3.existsSync(p);
|
|
1935
|
+
if (!isNewEntry) {
|
|
1936
|
+
try {
|
|
1937
|
+
const existing = fs3.readFileSync(p);
|
|
1938
|
+
if (Buffer.from(existing).equals(stored)) {
|
|
1939
|
+
const sidecar2 = kindSidecarPath(p);
|
|
1940
|
+
if (readSnapshotKind(sidecar2) !== kind) writeSnapshotKind(sidecar2, kind);
|
|
1941
|
+
return {
|
|
1942
|
+
path: p,
|
|
1943
|
+
content_sha: sha,
|
|
1944
|
+
size_bytes: stored.length
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
} catch {
|
|
1948
|
+
}
|
|
1949
|
+
}
|
|
1950
|
+
const dir = path3.dirname(p);
|
|
1951
|
+
if (!fs3.existsSync(dir)) {
|
|
1952
|
+
ensureDirSync(dir);
|
|
1953
|
+
}
|
|
1954
|
+
if (isNewEntry) {
|
|
1955
|
+
evictOldest(dir, MAX_SNAPSHOTS_PER_SESSION - 1);
|
|
1956
|
+
}
|
|
1957
|
+
atomicWriteBytes(p, stored);
|
|
1958
|
+
const sidecar = kindSidecarPath(p);
|
|
1959
|
+
writeSnapshotKind(sidecar, kind);
|
|
1960
|
+
return {
|
|
1961
|
+
path: p,
|
|
1962
|
+
content_sha: sha,
|
|
1963
|
+
size_bytes: stored.length
|
|
1964
|
+
};
|
|
1965
|
+
} catch {
|
|
1966
|
+
return null;
|
|
1967
|
+
}
|
|
1968
|
+
}
|
|
1969
|
+
function load(sessionId, filePath, opts = {}) {
|
|
1970
|
+
const p = snapshot_path(sessionId, filePath);
|
|
1971
|
+
if (!p || !fs3.existsSync(p)) return null;
|
|
1972
|
+
try {
|
|
1973
|
+
const stat = fs3.statSync(p);
|
|
1974
|
+
if (stat.size > MAX_SNAPSHOT_BYTES) {
|
|
1975
|
+
return null;
|
|
1976
|
+
}
|
|
1977
|
+
} catch {
|
|
1978
|
+
return null;
|
|
1979
|
+
}
|
|
1980
|
+
try {
|
|
1981
|
+
const data = fs3.readFileSync(p);
|
|
1982
|
+
if (opts.expected_sha) {
|
|
1983
|
+
const actualSha = fingerprintContent(data);
|
|
1984
|
+
if (actualSha.toLowerCase() !== opts.expected_sha.toLowerCase()) {
|
|
1985
|
+
return null;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
return data;
|
|
1989
|
+
} catch {
|
|
1990
|
+
return null;
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
function removeEligibleSnapshotFile(fullPath, file, cutoff) {
|
|
1994
|
+
const stat = fs3.lstatSync(fullPath);
|
|
1995
|
+
if ((stat.mode & 61440) === 40960) return false;
|
|
1996
|
+
if (cutoff !== void 0 && stat.mtimeMs >= cutoff) return false;
|
|
1997
|
+
fs3.unlinkSync(fullPath);
|
|
1998
|
+
return file.endsWith(".bin");
|
|
1999
|
+
}
|
|
2000
|
+
function cleanup_stale(maxAgeHours = 24) {
|
|
2001
|
+
const base = path3.join(tokenGoatHome(), "session_snapshots");
|
|
2002
|
+
if (!fs3.existsSync(base)) return 0;
|
|
2003
|
+
const cutoff = Date.now() - maxAgeHours * 3600 * 1e3;
|
|
2004
|
+
let removed = 0;
|
|
2005
|
+
try {
|
|
2006
|
+
const sessionDirs = fs3.readdirSync(base);
|
|
2007
|
+
for (const sessionDir2 of sessionDirs) {
|
|
2008
|
+
const sessionPath = path3.join(base, sessionDir2);
|
|
2009
|
+
try {
|
|
2010
|
+
const stat = fs3.statSync(sessionPath);
|
|
2011
|
+
if (!stat.isDirectory()) continue;
|
|
2012
|
+
} catch {
|
|
2013
|
+
continue;
|
|
2014
|
+
}
|
|
2015
|
+
try {
|
|
2016
|
+
const files = fs3.readdirSync(sessionPath);
|
|
2017
|
+
for (const file of files) {
|
|
2018
|
+
const fullPath = path3.join(sessionPath, file);
|
|
2019
|
+
try {
|
|
2020
|
+
if (removeEligibleSnapshotFile(fullPath, file, cutoff)) removed++;
|
|
2021
|
+
} catch {
|
|
2022
|
+
continue;
|
|
2023
|
+
}
|
|
2024
|
+
}
|
|
2025
|
+
try {
|
|
2026
|
+
fs3.rmdirSync(sessionPath);
|
|
2027
|
+
} catch {
|
|
2028
|
+
}
|
|
2029
|
+
} catch {
|
|
2030
|
+
continue;
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
} catch {
|
|
2034
|
+
return removed;
|
|
2035
|
+
}
|
|
2036
|
+
return removed;
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
// src/hooks_read_slice.ts
|
|
2040
|
+
function readIntToolInput(event, key) {
|
|
2041
|
+
const val = event.toolInput[key];
|
|
2042
|
+
const n = typeof val === "number" ? val : typeof val === "string" && val.trim() !== "" ? Number(val) : NaN;
|
|
2043
|
+
return Number.isFinite(n) ? n : void 0;
|
|
2044
|
+
}
|
|
2045
|
+
var OFFSET_ONLY_EXTRA_LINES = 100;
|
|
2046
|
+
function requestedSliceSpan(window) {
|
|
2047
|
+
if (window.offset === void 0) return void 0;
|
|
2048
|
+
return { start: window.offset, end: window.limit !== void 0 ? window.offset + window.limit - 1 : window.offset + OFFSET_ONLY_EXTRA_LINES };
|
|
2049
|
+
}
|
|
2050
|
+
function rawTextReadHarness(event) {
|
|
2051
|
+
const harness = event.raw["_tg_harness"];
|
|
2052
|
+
return harness === "gemini" || harness === "qwen" || harness === "grok" ? harness : null;
|
|
2053
|
+
}
|
|
2054
|
+
function readRequestedSliceWindow(event) {
|
|
2055
|
+
const rawOffset = readIntToolInput(event, "offset");
|
|
2056
|
+
const rawLimit = readIntToolInput(event, "limit");
|
|
2057
|
+
if (rawOffset !== void 0 || rawLimit !== void 0) {
|
|
2058
|
+
const harness = rawTextReadHarness(event);
|
|
2059
|
+
const zeroBased = harness === "qwen" || harness === "gemini";
|
|
2060
|
+
const offset = rawOffset === void 0 ? 1 : zeroBased ? rawOffset >= 0 ? Math.floor(rawOffset) + 1 : 1 : rawOffset >= 1 ? Math.floor(rawOffset) : 1;
|
|
2061
|
+
const limit = rawLimit !== void 0 && rawLimit > 0 ? Math.floor(rawLimit) : void 0;
|
|
2062
|
+
const isExplicitSlice = offset > 1 || limit !== void 0 && limit > 0;
|
|
2063
|
+
return {
|
|
2064
|
+
offset,
|
|
2065
|
+
limit,
|
|
2066
|
+
isExplicitSlice
|
|
2067
|
+
};
|
|
2068
|
+
}
|
|
2069
|
+
const rawRange = event.toolInput["view_range"] ?? event.toolInput["lines"] ?? event.toolInput["range"];
|
|
2070
|
+
if (Array.isArray(rawRange) && rawRange.length >= 1) {
|
|
2071
|
+
const rawStart = Number(rawRange[0]);
|
|
2072
|
+
const startVal = Number.isFinite(rawStart) && rawStart >= 1 ? Math.floor(rawStart) : 1;
|
|
2073
|
+
if (rawRange.length >= 2) {
|
|
2074
|
+
const rawEnd = Number(rawRange[1]);
|
|
2075
|
+
if (rawEnd === -1) {
|
|
2076
|
+
return { offset: startVal, isExplicitSlice: startVal > 1 };
|
|
2077
|
+
}
|
|
2078
|
+
if (Number.isFinite(rawEnd) && rawEnd >= startVal) {
|
|
2079
|
+
const lineCount = Math.floor(rawEnd - startVal + 1);
|
|
2080
|
+
return { offset: startVal, limit: lineCount, isExplicitSlice: true };
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
return { offset: startVal, isExplicitSlice: startVal > 1 };
|
|
2084
|
+
}
|
|
2085
|
+
const startLine = readIntToolInput(event, "start_line") ?? readIntToolInput(event, "startLine") ?? readIntToolInput(event, "start");
|
|
2086
|
+
const endLine = readIntToolInput(event, "end_line") ?? readIntToolInput(event, "endLine") ?? readIntToolInput(event, "end");
|
|
2087
|
+
if (startLine !== void 0 || endLine !== void 0) {
|
|
2088
|
+
const effectiveStart = startLine !== void 0 && startLine >= 1 ? Math.floor(startLine) : 1;
|
|
2089
|
+
const effectiveLimit = endLine !== void 0 && endLine >= effectiveStart ? Math.floor(endLine - effectiveStart + 1) : void 0;
|
|
2090
|
+
const isExplicitSlice = effectiveStart > 1 || effectiveLimit !== void 0 && effectiveLimit > 0;
|
|
2091
|
+
return { offset: effectiveStart, limit: effectiveLimit, isExplicitSlice };
|
|
2092
|
+
}
|
|
2093
|
+
return { isExplicitSlice: false };
|
|
2094
|
+
}
|
|
2095
|
+
function readStartLine(event) {
|
|
2096
|
+
if (rawTextReadHarness(event) !== null) {
|
|
2097
|
+
const shown = splitRawReadHeader(readResponseText(event))?.shown;
|
|
2098
|
+
return shown?.start ?? readRequestedSliceWindow(event).offset ?? 1;
|
|
2099
|
+
}
|
|
2100
|
+
const resp = event.raw["tool_response"];
|
|
2101
|
+
const file = resp?.["file"];
|
|
2102
|
+
const start = file?.["startLine"];
|
|
2103
|
+
return typeof start === "number" && Number.isSafeInteger(start) && start >= 1 ? start : 1;
|
|
2104
|
+
}
|
|
2105
|
+
var HARNESS_TRUNCATION_NOTICE_RE = /^[ \t]*\[Truncated: PARTIAL view/m;
|
|
2106
|
+
function harnessNumbersReadContent(event) {
|
|
2107
|
+
const resp = event.raw["tool_response"];
|
|
2108
|
+
if (resp === null || typeof resp !== "object") return false;
|
|
2109
|
+
const fieldPath = resolveToolResponseFieldPath(resp, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
|
|
2110
|
+
return fieldPath !== null && fieldPath.length === 2 && fieldPath[0] === "file" && fieldPath[1] === "content";
|
|
2111
|
+
}
|
|
2112
|
+
function numberedRenderBytes(text, startLine) {
|
|
2113
|
+
let bytes = Buffer.byteLength(text, "utf-8");
|
|
2114
|
+
const lines = text.split("\n").length;
|
|
2115
|
+
for (let n = startLine; n < startLine + lines; n++) bytes += String(n).length + 1;
|
|
2116
|
+
return bytes;
|
|
2117
|
+
}
|
|
2118
|
+
function readResponseText(event) {
|
|
2119
|
+
return extractToolResponseField(event.raw, OUTPUT_FIRST_TOOL_RESPONSE_KEYS);
|
|
2120
|
+
}
|
|
2121
|
+
var GEMINI_TRUNCATION_OPENER = "IMPORTANT: The file content has been truncated.";
|
|
2122
|
+
var GEMINI_STATUS_RE = /^Status: Showing lines (\d+)-(\d+) of (\d+) total lines\.$/;
|
|
2123
|
+
var GEMINI_CONTENT_MARKER = "--- FILE CONTENT (truncated) ---";
|
|
2124
|
+
var QWEN_STATUS_RE = /^Showing lines (\d+)-(\d+) of (at least )?(\d+) total lines\.$/;
|
|
2125
|
+
var LINE_CUT_MARKER_RE = /\.\.\. \[truncated\]$/m;
|
|
2126
|
+
function splitRawReadHeader(respText) {
|
|
2127
|
+
const lines = respText.split("\n");
|
|
2128
|
+
const opener = lines[0] === "" ? 1 : 0;
|
|
2129
|
+
if (lines[opener] === GEMINI_TRUNCATION_OPENER) {
|
|
2130
|
+
const marker = lines.indexOf(GEMINI_CONTENT_MARKER, opener + 1);
|
|
2131
|
+
if (marker === -1 || marker > opener + 6) return null;
|
|
2132
|
+
const status2 = lines.slice(opener + 1, marker).map((l) => GEMINI_STATUS_RE.exec(l)).find((m) => m !== null);
|
|
2133
|
+
if (status2 === void 0 || status2 === null) return null;
|
|
2134
|
+
const shown2 = { start: Number(status2[1]), end: Number(status2[2]), total: Number(status2[3]), atLeast: false };
|
|
2135
|
+
return { header: lines.slice(0, marker + 1), body: lines.slice(marker + 1).join("\n"), shown: shown2 };
|
|
2136
|
+
}
|
|
2137
|
+
const status = QWEN_STATUS_RE.exec(lines[0] ?? "");
|
|
2138
|
+
if (status === null) return null;
|
|
2139
|
+
const shown = { start: Number(status[1]), end: Number(status[2]), total: Number(status[4]), atLeast: status[3] !== void 0 };
|
|
2140
|
+
const width = lines[1] === "" && lines[2] === "---" && lines[3] === "" ? 4 : lines[1] === "---" ? 2 : 0;
|
|
2141
|
+
if (width === 0) return null;
|
|
2142
|
+
return { header: lines.slice(0, width), body: lines.slice(width).join("\n"), shown };
|
|
2143
|
+
}
|
|
2144
|
+
function rawReadCutShort(event, split) {
|
|
2145
|
+
const { shown } = split;
|
|
2146
|
+
if (shown.atLeast) return true;
|
|
2147
|
+
const window = readRequestedSliceWindow(event);
|
|
2148
|
+
const reqStart = window.offset ?? 1;
|
|
2149
|
+
const reqEnd = window.limit !== void 0 ? Math.min(reqStart + window.limit - 1, shown.total) : shown.total;
|
|
2150
|
+
if (shown.start !== reqStart || shown.end < reqEnd) return true;
|
|
2151
|
+
return LINE_CUT_MARKER_RE.test(split.body);
|
|
2152
|
+
}
|
|
2153
|
+
function isTruncatedReadDelivery(event, respText) {
|
|
2154
|
+
const harness = rawTextReadHarness(event);
|
|
2155
|
+
if (harness === "gemini" || harness === "qwen") {
|
|
2156
|
+
const split = splitRawReadHeader(respText);
|
|
2157
|
+
return split !== null && rawReadCutShort(event, split);
|
|
2158
|
+
}
|
|
2159
|
+
const resp = event.raw["tool_response"];
|
|
2160
|
+
const file = resp?.["file"];
|
|
2161
|
+
if (file?.["truncatedByTokenCap"] === true) return true;
|
|
2162
|
+
return HARNESS_TRUNCATION_NOTICE_RE.test(respText);
|
|
2163
|
+
}
|
|
2164
|
+
var SLICE_ESTIMATE_SCAN_CAP_BYTES = 2 * 1024 * 1024;
|
|
2165
|
+
var NEAR_SINGLE_LINE_SCAN_THRESHOLD = 20;
|
|
2166
|
+
function scanRequestedSlice(absPath, offset, limit) {
|
|
2167
|
+
const windowEnd = offset + limit;
|
|
2168
|
+
let fd;
|
|
2169
|
+
try {
|
|
2170
|
+
fd = fs4.openSync(hostPathOfIndexKey(absPath), "r");
|
|
2171
|
+
} catch {
|
|
2172
|
+
return null;
|
|
2173
|
+
}
|
|
2174
|
+
try {
|
|
2175
|
+
const buf = Buffer.alloc(64 * 1024);
|
|
2176
|
+
let lineNumber = 1;
|
|
2177
|
+
let sliceBytes = 0;
|
|
2178
|
+
let totalScanned = 0;
|
|
2179
|
+
let afterCr = false;
|
|
2180
|
+
for (; ; ) {
|
|
2181
|
+
if (totalScanned >= SLICE_ESTIMATE_SCAN_CAP_BYTES) {
|
|
2182
|
+
const nearSingleLine = lineNumber < NEAR_SINGLE_LINE_SCAN_THRESHOLD;
|
|
2183
|
+
return { bytes: sliceBytes, trustworthy: nearSingleLine, nearSingleLine };
|
|
2184
|
+
}
|
|
2185
|
+
const bytesRead = fs4.readSync(fd, buf, 0, buf.length, null);
|
|
2186
|
+
if (bytesRead === 0) {
|
|
2187
|
+
const spansWholeFile = offset <= 1 && lineNumber <= windowEnd;
|
|
2188
|
+
return {
|
|
2189
|
+
bytes: sliceBytes,
|
|
2190
|
+
trustworthy: true,
|
|
2191
|
+
nearSingleLine: lineNumber < NEAR_SINGLE_LINE_SCAN_THRESHOLD && totalScanned > 2e3,
|
|
2192
|
+
spansWholeFile
|
|
2193
|
+
};
|
|
2194
|
+
}
|
|
2195
|
+
totalScanned += bytesRead;
|
|
2196
|
+
for (let i = 0; i < bytesRead; i++) {
|
|
2197
|
+
if (lineNumber >= offset && lineNumber < windowEnd) sliceBytes++;
|
|
2198
|
+
const byte = buf[i];
|
|
2199
|
+
if (byte === 10 && afterCr) {
|
|
2200
|
+
afterCr = false;
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
afterCr = byte === 13;
|
|
2204
|
+
if (byte === 10 || byte === 13) {
|
|
2205
|
+
lineNumber++;
|
|
2206
|
+
if (lineNumber >= windowEnd) return { bytes: sliceBytes, trustworthy: true, nearSingleLine: false };
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
} finally {
|
|
2211
|
+
try {
|
|
2212
|
+
fs4.closeSync(fd);
|
|
2213
|
+
} catch {
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
function estimateRequestedSlice(event, absPath) {
|
|
2218
|
+
const window = readRequestedSliceWindow(event);
|
|
2219
|
+
if (window.limit === void 0 || window.limit <= 0) return { kind: "unbounded" };
|
|
2220
|
+
const effectiveOffset = window.offset !== void 0 && window.offset >= 1 ? window.offset : 1;
|
|
2221
|
+
const scan = scanRequestedSlice(absPath, effectiveOffset, window.limit);
|
|
2222
|
+
if (scan === null) return { kind: "unbounded" };
|
|
2223
|
+
if (scan.nearSingleLine) return { kind: "nearSingleLine" };
|
|
2224
|
+
if (scan.trustworthy) return { kind: "bytes", bytes: scan.bytes, spansWholeFile: scan.spansWholeFile === true };
|
|
2225
|
+
return { kind: "unbounded" };
|
|
2226
|
+
}
|
|
2227
|
+
function isSmallSlice(slice, thresholdBytes, fileSize) {
|
|
2228
|
+
if (slice.kind !== "bytes") return false;
|
|
2229
|
+
if (slice.bytes >= thresholdBytes) return false;
|
|
2230
|
+
if (slice.bytes > SLICE_ESTIMATE_SCAN_CAP_BYTES) return false;
|
|
2231
|
+
if (slice.spansWholeFile === true) return false;
|
|
2232
|
+
if (fileSize !== void 0 && fileSize > 0 && slice.bytes >= fileSize) return false;
|
|
2233
|
+
return true;
|
|
2234
|
+
}
|
|
2235
|
+
function isUnseenWindow(window, servedRanges, fullReads) {
|
|
2236
|
+
if (!window.isExplicitSlice || fullReads !== 0 || window.offset === void 0 || window.limit === void 0) return false;
|
|
2237
|
+
const start = window.offset;
|
|
2238
|
+
const end = window.offset + window.limit - 1;
|
|
2239
|
+
return !servedRanges.some(([s, e]) => s <= end && e >= start);
|
|
2240
|
+
}
|
|
2241
|
+
function describeSliceAdvice(slice, rawAbsPath) {
|
|
2242
|
+
const absPath = displaySafePath(rawAbsPath);
|
|
2243
|
+
if (slice.kind === "nearSingleLine") {
|
|
2244
|
+
return BYTE_RANGE_ADVICE(absPath);
|
|
2245
|
+
}
|
|
2246
|
+
if (slice.kind === "bytes") {
|
|
2247
|
+
return `The requested offset/limit range is still ~${toKB(slice.bytes)}KB \u2014 narrow the range further (a smaller limit) rather than reading the whole file.`;
|
|
2248
|
+
}
|
|
2249
|
+
return "Use Read with offset/limit to sample specific sections.";
|
|
2250
|
+
}
|
|
2251
|
+
function buildLineDiffDetailed(oldContent, newContent, label) {
|
|
2252
|
+
const oldLines = oldContent.split("\n");
|
|
2253
|
+
const newLines = newContent.split("\n");
|
|
2254
|
+
let prefix = 0;
|
|
2255
|
+
while (prefix < oldLines.length && prefix < newLines.length && oldLines[prefix] === newLines[prefix]) {
|
|
2256
|
+
prefix++;
|
|
2257
|
+
}
|
|
2258
|
+
let oldSuffix = oldLines.length;
|
|
2259
|
+
let newSuffix = newLines.length;
|
|
2260
|
+
while (oldSuffix > prefix && newSuffix > prefix && oldLines[oldSuffix - 1] === newLines[newSuffix - 1]) {
|
|
2261
|
+
oldSuffix--;
|
|
2262
|
+
newSuffix--;
|
|
2263
|
+
}
|
|
2264
|
+
if (prefix === oldLines.length && prefix === newLines.length) return { text: "", truncated: false };
|
|
2265
|
+
const changedOld = oldLines.slice(prefix, oldSuffix);
|
|
2266
|
+
const changedNew = newLines.slice(prefix, newSuffix);
|
|
2267
|
+
const MAX_LINES = 50;
|
|
2268
|
+
const removedLines = changedOld.map((l) => `-${l}`);
|
|
2269
|
+
const addedLines = changedNew.map((l) => `+${l}`);
|
|
2270
|
+
const allChanges = [...removedLines, ...addedLines];
|
|
2271
|
+
const shownRemoved = Math.min(removedLines.length, MAX_LINES);
|
|
2272
|
+
const shownAdded = Math.max(0, Math.min(addedLines.length, MAX_LINES - shownRemoved));
|
|
2273
|
+
const out = [
|
|
2274
|
+
`--- ${label} (prev)`,
|
|
2275
|
+
`+++ ${label} (current)`,
|
|
2276
|
+
`@@ -${prefix + 1},${shownRemoved} +${prefix + 1},${shownAdded} @@`
|
|
2277
|
+
];
|
|
2278
|
+
const truncated = allChanges.length > MAX_LINES;
|
|
2279
|
+
if (!truncated) {
|
|
2280
|
+
out.push(...allChanges);
|
|
2281
|
+
} else {
|
|
2282
|
+
out.push(...allChanges.slice(0, MAX_LINES));
|
|
2283
|
+
out.push(`... (${countNoun(allChanges.length - MAX_LINES, "more changed line")})`);
|
|
2284
|
+
}
|
|
2285
|
+
return { text: out.join("\n"), truncated };
|
|
2286
|
+
}
|
|
2287
|
+
function buildLineDiff(oldContent, newContent, label) {
|
|
2288
|
+
return buildLineDiffDetailed(oldContent, newContent, label).text;
|
|
2289
|
+
}
|
|
2290
|
+
function loadSnapshotDiff(sessionId, normalized, basename5) {
|
|
2291
|
+
const oldSnap = load(sessionId, normalized);
|
|
2292
|
+
if (oldSnap === null) return { kind: "none" };
|
|
2293
|
+
try {
|
|
2294
|
+
const onDisk = hostPathOfIndexKey(normalized);
|
|
2295
|
+
const sz = statSize(onDisk);
|
|
2296
|
+
if (sz === null || sz > 256 * 1024) return { kind: "none" };
|
|
2297
|
+
const currentContent = fs4.readFileSync(onDisk, "utf8");
|
|
2298
|
+
const TRUNC_MARKER = "\n<snapshot truncated at ";
|
|
2299
|
+
const oldRaw = oldSnap.toString("utf8");
|
|
2300
|
+
const truncIdx = oldRaw.indexOf(TRUNC_MARKER);
|
|
2301
|
+
if (truncIdx >= 0) return { kind: "none" };
|
|
2302
|
+
const oldContent = oldRaw;
|
|
2303
|
+
if (oldContent === currentContent) return { kind: "unchanged", currentContent };
|
|
2304
|
+
const { text: diff, truncated } = buildLineDiffDetailed(oldContent, currentContent, basename5);
|
|
2305
|
+
if (diff === "") return { kind: "none" };
|
|
2306
|
+
if (truncated) return { kind: "none" };
|
|
2307
|
+
const savedBytes = Math.max(0, currentContent.length - diff.length);
|
|
2308
|
+
return { kind: "diff", diff, savedBytes, currentContent };
|
|
2309
|
+
} catch {
|
|
2310
|
+
return { kind: "none" };
|
|
2311
|
+
}
|
|
2312
|
+
}
|
|
2313
|
+
function countTextLines(content) {
|
|
2314
|
+
if (content.length === 0) return 0;
|
|
2315
|
+
const parts = content.split(/\r\n|\r|\n/);
|
|
2316
|
+
if (parts[parts.length - 1] === "") parts.pop();
|
|
2317
|
+
return parts.length;
|
|
2318
|
+
}
|
|
2319
|
+
function estimateTruncatedLineCount(normalized) {
|
|
2320
|
+
try {
|
|
2321
|
+
const onDisk = hostPathOfIndexKey(normalized);
|
|
2322
|
+
const sz = statSize(onDisk);
|
|
2323
|
+
if (sz !== null && sz <= SLICE_ESTIMATE_SCAN_CAP_BYTES) {
|
|
2324
|
+
return countTextLines(fs4.readFileSync(onDisk, "utf8"));
|
|
2325
|
+
}
|
|
2326
|
+
} catch {
|
|
2327
|
+
}
|
|
2328
|
+
return Infinity;
|
|
2329
|
+
}
|
|
2330
|
+
function editAnywayHint(rawPath) {
|
|
2331
|
+
const [quoted, b64, oldFile, newFile] = quotedArgs(displaySafePath(rawPath), "<base64>", "<oldfile>", "<newfile>");
|
|
2332
|
+
return "To edit it anyway, use `token-goat replace " + quoted + " --old-b64 " + b64 + " --new-b64 " + b64 + "` (preferred \u2014 no temp files needed) or `--old-from " + oldFile + " --new-from " + newFile + "` for a snippet edit, or `token-goat write-file " + quoted + " --b64 " + b64 + "` (or `--from " + newFile + "`) to rewrite the whole file \u2014 Read/Edit's own precondition can't be satisfied after this deny.";
|
|
2333
|
+
}
|
|
2334
|
+
function truncatedReadDenyMessage(rawPath) {
|
|
2335
|
+
const normalized = displaySafePath(rawPath);
|
|
2336
|
+
const reason = "File was truncated on last read (>33K tokens).";
|
|
2337
|
+
const skeleton = "token-goat skeleton " + quotedArg(normalized);
|
|
2338
|
+
const target = hintTarget(rawPath, sliceForPath(rawPath));
|
|
2339
|
+
return target.real ? leadWithCommand(sliceCommand(normalized, target), "for one part, or " + fencedCommand(skeleton) + " for structure", reason) : leadWithCommand(skeleton, "for structure, or " + fencedCommand("token-goat read " + quotedArg(normalized + "::SymbolName")) + " for one function", reason);
|
|
2340
|
+
}
|
|
2341
|
+
function readWindowFromDisk(event, normalized) {
|
|
2342
|
+
const onDisk = hostPathOfIndexKey(normalized);
|
|
2343
|
+
const size = statSize(onDisk);
|
|
2344
|
+
if (size === null || size > SLICE_ESTIMATE_SCAN_CAP_BYTES) return null;
|
|
2345
|
+
const text = decodeSource(fs4.readFileSync(onDisk));
|
|
2346
|
+
const window = readRequestedSliceWindow(event);
|
|
2347
|
+
const start = window.offset !== void 0 && window.offset >= 1 ? window.offset : 1;
|
|
2348
|
+
const limit = window.limit;
|
|
2349
|
+
if (start === 1 && (limit === void 0 || limit <= 0)) return text;
|
|
2350
|
+
const lines = text.split("\n");
|
|
2351
|
+
const from = start - 1;
|
|
2352
|
+
return (limit === void 0 || limit <= 0 ? lines.slice(from) : lines.slice(from, from + limit)).join("\n");
|
|
2353
|
+
}
|
|
2354
|
+
var READ_NUMBERED_ROW_RE = /^\s*(\d+)[\t→](.*)$/;
|
|
2355
|
+
function parseReadDelivery(event, respText) {
|
|
2356
|
+
const firstLine = readStartLine(event);
|
|
2357
|
+
const harness = rawTextReadHarness(event);
|
|
2358
|
+
if (harness === "gemini" || harness === "qwen") {
|
|
2359
|
+
const split = splitRawReadHeader(respText);
|
|
2360
|
+
if (split === null) return plainReadResult(respText, firstLine);
|
|
2361
|
+
const parsed = plainReadResult(split.body, firstLine);
|
|
2362
|
+
return parsed === null ? null : { ...parsed, header: split.header };
|
|
2363
|
+
}
|
|
2364
|
+
if (harness === "grok") return wholeNumberedReadResult(respText, firstLine) ?? plainReadResult(respText, firstLine);
|
|
2365
|
+
return harnessNumbersReadContent(event) ? plainReadResult(respText, firstLine) : parseNumberedReadResult(respText, firstLine);
|
|
2366
|
+
}
|
|
2367
|
+
function wholeNumberedReadResult(respText, firstLine) {
|
|
2368
|
+
const lines = respText.split("\n");
|
|
2369
|
+
const body = lines[lines.length - 1] === "" ? lines.slice(0, -1) : lines;
|
|
2370
|
+
if (body.length === 0) return null;
|
|
2371
|
+
const rows = [];
|
|
2372
|
+
for (const [idx, line] of body.entries()) {
|
|
2373
|
+
const m = READ_NUMBERED_ROW_RE.exec(line);
|
|
2374
|
+
if (m === null || Number(m[1]) !== firstLine + idx) return null;
|
|
2375
|
+
rows.push({ no: firstLine + idx, text: m[2] ?? "", raw: line });
|
|
2376
|
+
}
|
|
2377
|
+
return { header: [], rows, trailer: body.length < lines.length ? [""] : [] };
|
|
2378
|
+
}
|
|
2379
|
+
function plainReadResult(respText, firstLine) {
|
|
2380
|
+
if (respText === "") return null;
|
|
2381
|
+
return { header: [], rows: respText.split("\n").map((text, idx) => ({ no: firstLine + idx, text, raw: text })), trailer: [] };
|
|
2382
|
+
}
|
|
2383
|
+
function parseNumberedReadResult(respText, firstLine) {
|
|
2384
|
+
const lines = respText.split("\n");
|
|
2385
|
+
const header = [];
|
|
2386
|
+
const rows = [];
|
|
2387
|
+
const trailer = [];
|
|
2388
|
+
let i = 0;
|
|
2389
|
+
for (; i < lines.length; i++) {
|
|
2390
|
+
const line = lines[i] ?? "";
|
|
2391
|
+
const m = READ_NUMBERED_ROW_RE.exec(line);
|
|
2392
|
+
if (m === null || !Number.isSafeInteger(Number(m[1]))) {
|
|
2393
|
+
header.push(line);
|
|
2394
|
+
continue;
|
|
2395
|
+
}
|
|
2396
|
+
rows.push({ no: Number(m[1]), text: m[2] ?? "", raw: line });
|
|
2397
|
+
i++;
|
|
2398
|
+
break;
|
|
2399
|
+
}
|
|
2400
|
+
for (; i < lines.length; i++) {
|
|
2401
|
+
const line = lines[i] ?? "";
|
|
2402
|
+
const m = READ_NUMBERED_ROW_RE.exec(line);
|
|
2403
|
+
const prev = rows[rows.length - 1];
|
|
2404
|
+
if (m === null || prev === void 0 || Number(m[1]) !== prev.no + 1) break;
|
|
2405
|
+
rows.push({ no: prev.no + 1, text: m[2] ?? "", raw: line });
|
|
2406
|
+
}
|
|
2407
|
+
for (; i < lines.length; i++) trailer.push(lines[i] ?? "");
|
|
2408
|
+
if (rows.length > 0) return { header, rows, trailer };
|
|
2409
|
+
return plainReadResult(respText, firstLine);
|
|
2410
|
+
}
|
|
2411
|
+
function rereadIdentity(sessionId, normalized, prior) {
|
|
2412
|
+
const onDisk = hostPathOfIndexKey(normalized);
|
|
2413
|
+
let current;
|
|
2414
|
+
try {
|
|
2415
|
+
current = fs4.statSync(onDisk);
|
|
2416
|
+
} catch {
|
|
2417
|
+
return { identity: "unknown", basis: "none" };
|
|
2418
|
+
}
|
|
2419
|
+
const snap = load(sessionId, normalized);
|
|
2420
|
+
if (snap !== null && !snap.includes("\n<snapshot truncated at ") && current.size <= 256 * 1024) {
|
|
2421
|
+
try {
|
|
2422
|
+
return { identity: snap.equals(fs4.readFileSync(onDisk)) ? "identical" : "changed", basis: "snapshot" };
|
|
2423
|
+
} catch {
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
if (prior === void 0 || prior.sizeBytes <= 0) return { identity: "unknown", basis: "none" };
|
|
2427
|
+
const changed = current.size !== prior.sizeBytes || current.mtimeMs > (prior.lastFullReadAt ?? prior.lastReadAt);
|
|
2428
|
+
return { identity: changed ? "changed" : "identical", basis: "stat" };
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// src/hooks_read_policy.ts
|
|
2432
|
+
init_define_import_meta_env();
|
|
2433
|
+
|
|
2434
|
+
// src/read_navigation_evidence.ts
|
|
2435
|
+
init_define_import_meta_env();
|
|
2436
|
+
import * as fs5 from "node:fs";
|
|
2437
|
+
var PROBE_ROWS = 60;
|
|
2438
|
+
var KEPT_PER_KIND = 8;
|
|
2439
|
+
function indexedCopyIsStale(filePath, row) {
|
|
2440
|
+
try {
|
|
2441
|
+
const onDisk = hostPathOfIndexKey(filePath);
|
|
2442
|
+
if (fs5.statSync(onDisk).mtimeMs / 1e3 === row.mtime) return false;
|
|
2443
|
+
if (!row.sha || fingerprintFile(onDisk) !== row.sha) return true;
|
|
2444
|
+
enqueueDirtyPathSafe(onDisk);
|
|
2445
|
+
return false;
|
|
2446
|
+
} catch {
|
|
2447
|
+
return true;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
function getReadNavigationEvidence(filePath, dbPath) {
|
|
2451
|
+
return withProbeIndex((db) => {
|
|
2452
|
+
const fileRow = fileRowByIndexKey(db, filePath, "path, mtime, sha");
|
|
2453
|
+
if (!fileRow) return null;
|
|
2454
|
+
const isStale = indexedCopyIsStale(filePath, fileRow);
|
|
2455
|
+
const spellings = filePathSpellingsClause("file_path", fileRow.path);
|
|
2456
|
+
const rows = db.prepare(
|
|
2457
|
+
`SELECT name, kind, line_start, line_end, COUNT(*) OVER () AS total, SUM(CASE WHEN kind = 'heading' THEN 1 ELSE 0 END) OVER () AS headings FROM symbols WHERE ${spellings.clause} ORDER BY line_start ASC, rowid LIMIT ${PROBE_ROWS}`
|
|
2458
|
+
).all(...spellings.params);
|
|
2459
|
+
const first = rows[0];
|
|
2460
|
+
if (first === void 0) return null;
|
|
2461
|
+
const headingCount = Number(first.headings ?? 0);
|
|
2462
|
+
const topSymbols = [];
|
|
2463
|
+
const topHeadings = [];
|
|
2464
|
+
for (const r of rows) {
|
|
2465
|
+
const item = { name: r.name, kind: r.kind, lineStart: r.line_start, lineEnd: r.line_end };
|
|
2466
|
+
const kept = r.kind === "heading" ? topHeadings : topSymbols;
|
|
2467
|
+
if (kept.length < KEPT_PER_KIND) kept.push(item);
|
|
2468
|
+
}
|
|
2469
|
+
return {
|
|
2470
|
+
filePath: fileRow.path,
|
|
2471
|
+
symbolCount: Math.max(0, first.total - headingCount),
|
|
2472
|
+
headingCount,
|
|
2473
|
+
topSymbols,
|
|
2474
|
+
topHeadings,
|
|
2475
|
+
indexedMtime: fileRow.mtime ?? 0,
|
|
2476
|
+
isStale
|
|
2477
|
+
};
|
|
2478
|
+
}, dbPath);
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2481
|
+
// src/hooks_read_policy.ts
|
|
2482
|
+
function formatKb(bytes) {
|
|
2483
|
+
return (bytes / 1024).toFixed(1);
|
|
2484
|
+
}
|
|
2485
|
+
function meetsFirstReadSymbolThreshold(policy, thresholdBytes, size) {
|
|
2486
|
+
return policy !== "off" && thresholdBytes > 0 && size >= thresholdBytes;
|
|
2487
|
+
}
|
|
2488
|
+
function buildReadPolicyContext(args) {
|
|
2489
|
+
const { event, normalizedPath, hints } = args;
|
|
2490
|
+
const window = args.window ?? readRequestedSliceWindow(event);
|
|
2491
|
+
const slice = args.slice ?? (window.isExplicitSlice ? estimateRequestedSlice(event, normalizedPath) : { kind: "unbounded" });
|
|
2492
|
+
return {
|
|
2493
|
+
event,
|
|
2494
|
+
normalizedPath,
|
|
2495
|
+
shownPath: args.shownPath,
|
|
2496
|
+
fileSize: args.fileSize,
|
|
2497
|
+
window,
|
|
2498
|
+
slice,
|
|
2499
|
+
quiet: isWithinQuietHours(hints.quiet_hours),
|
|
2500
|
+
firstReadSymbolBytes: hints.first_read_symbol_bytes,
|
|
2501
|
+
firstReadSymbolPolicy: hints.first_read_symbol_policy
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
function evaluateFirstReadSymbolPolicy(ctx) {
|
|
2505
|
+
const {
|
|
2506
|
+
event,
|
|
2507
|
+
normalizedPath,
|
|
2508
|
+
shownPath,
|
|
2509
|
+
fileSize,
|
|
2510
|
+
firstReadSymbolBytes,
|
|
2511
|
+
firstReadSymbolPolicy
|
|
2512
|
+
} = ctx;
|
|
2513
|
+
if (!meetsFirstReadSymbolThreshold(firstReadSymbolPolicy, firstReadSymbolBytes, fileSize)) return { action: "allow" };
|
|
2514
|
+
if (event.toolName === "Grep") return { action: "allow" };
|
|
2515
|
+
if (firstReadSymbolPolicy === "warn" && ctx.quiet) return { action: "allow" };
|
|
2516
|
+
if (ctx.window.isExplicitSlice && isSmallSlice(ctx.slice, firstReadSymbolBytes, ctx.fileSize)) return { action: "allow" };
|
|
2517
|
+
const evidence = ctx.navigationEvidence !== void 0 ? ctx.navigationEvidence : getReadNavigationEvidence(normalizedPath);
|
|
2518
|
+
if (!evidence || evidence.isStale) return { action: "allow" };
|
|
2519
|
+
if (evidence.symbolCount + evidence.headingCount === 0) return { action: "allow" };
|
|
2520
|
+
const safeShown = displaySafePath(shownPath);
|
|
2521
|
+
const surgicalCommands = [];
|
|
2522
|
+
if (evidence.symbolCount > 0 && evidence.topSymbols.length > 0) {
|
|
2523
|
+
const target = hintTargetFromNames(evidence.topSymbols.map((s) => s.name), "symbol");
|
|
2524
|
+
if (target.real) {
|
|
2525
|
+
surgicalCommands.push("token-goat read " + quotedArg(`${safeShown}::${target.name}`));
|
|
2526
|
+
}
|
|
2527
|
+
} else if (evidence.headingCount > 0 && evidence.topHeadings.length > 0) {
|
|
2528
|
+
const target = hintTargetFromNames(evidence.topHeadings.map((h) => h.name), "section");
|
|
2529
|
+
if (target.real) {
|
|
2530
|
+
surgicalCommands.push("token-goat section " + quotedArg(`${safeShown}::${target.name}`));
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
const outlineCommand = "token-goat outline " + quotedArg(safeShown);
|
|
2534
|
+
const skeletonCommand = "token-goat skeleton " + quotedArg(safeShown);
|
|
2535
|
+
const primaryCommand = surgicalCommands[0] ?? outlineCommand;
|
|
2536
|
+
const kb = formatKb(fileSize);
|
|
2537
|
+
const symbolDetails = evidence.symbolCount > 0 ? `${countNoun(evidence.symbolCount, "indexed symbol")}` : `${countNoun(evidence.headingCount, "indexed heading")}`;
|
|
2538
|
+
const sliceAdvice = ctx.window.isExplicitSlice && ctx.slice.kind === "nearSingleLine" ? ". " + describeSliceAdvice(ctx.slice, normalizedPath) : ", or re-read with offset/limit for a specific line slice.";
|
|
2539
|
+
if (firstReadSymbolPolicy === "deny") {
|
|
2540
|
+
const message2 = leadWithCommand(
|
|
2541
|
+
primaryCommand,
|
|
2542
|
+
"to read surgically",
|
|
2543
|
+
`This file is large (${kb}KB with ${symbolDetails}). Whole-file first read denied by first_read_symbol_policy. Use ${fencedCommand(outlineCommand)} or ${fencedCommand(skeletonCommand)} to map structure${sliceAdvice}`
|
|
2544
|
+
) + "\n" + editAnywayHint(normalizedPath);
|
|
2545
|
+
return { action: "deny", message: message2 };
|
|
2546
|
+
}
|
|
2547
|
+
const message = leadWithCommand(
|
|
2548
|
+
primaryCommand,
|
|
2549
|
+
"to read surgically",
|
|
2550
|
+
`This file is ${kb}KB with ${symbolDetails}; prefer surgical reads or ${fencedCommand(outlineCommand)} over reading the whole file.`
|
|
2551
|
+
);
|
|
2552
|
+
return { action: "warn", message };
|
|
2553
|
+
}
|
|
2554
|
+
|
|
2555
|
+
// src/hints.ts
|
|
2556
|
+
init_define_import_meta_env();
|
|
2557
|
+
var HINT_PRIORITY_MEDIUM = 3;
|
|
2558
|
+
function buildPackageManifestHint(options) {
|
|
2559
|
+
try {
|
|
2560
|
+
const fname = _sanitizeHintPath(options.file_path.split(/[/\\]/).pop() ?? "");
|
|
2561
|
+
const basenameLower = fname.toLowerCase();
|
|
2562
|
+
if (basenameLower === "package.json") {
|
|
2563
|
+
const text = `\`${fname}\` is a package manifest. Run \`token-goat json-query ${quotedArgs(options.shown, "dependencies").join(" ")}\` or \`token-goat json-query ${quotedArgs(options.shown, "devDependencies").join(" ")}\` for focused reads, or \`token-goat json-outline ${quotedArg(options.shown)}\` for every top-level key.`;
|
|
2564
|
+
return {
|
|
2565
|
+
text,
|
|
2566
|
+
hint_priority: HINT_PRIORITY_MEDIUM
|
|
2567
|
+
};
|
|
2568
|
+
}
|
|
2569
|
+
return null;
|
|
2570
|
+
} catch {
|
|
2571
|
+
return null;
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
function _sanitizeHintPath(path7) {
|
|
2575
|
+
if (typeof path7 !== "string") {
|
|
2576
|
+
return "???";
|
|
2577
|
+
}
|
|
2578
|
+
return path7.replace(/[\x00]/g, "").slice(0, 200);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
// src/doc_compact.ts
|
|
2582
|
+
init_define_import_meta_env();
|
|
2583
|
+
import * as fs6 from "fs";
|
|
2584
|
+
import * as path4 from "path";
|
|
2585
|
+
var defaultSentencesPerSection = 2;
|
|
2586
|
+
var headerPrefix = "<!-- token-goat doc-compact source-hash:";
|
|
2587
|
+
var headerRegex = /^<!-- token-goat doc-compact source-hash:(\S+) source:(.+?) -->\r?$/;
|
|
2588
|
+
var compactSubdir = "doc_compacts";
|
|
2589
|
+
function sourceHash(filePath) {
|
|
2590
|
+
try {
|
|
2591
|
+
return fingerprintContent(fs6.readFileSync(filePath));
|
|
2592
|
+
} catch {
|
|
2593
|
+
return "";
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
function _compactSlug(absPathStr) {
|
|
2597
|
+
const h = fingerprintContent(foldPath(absPathStr)).slice(0, 12);
|
|
2598
|
+
const ext = path4.extname(absPathStr);
|
|
2599
|
+
const stem = path4.basename(absPathStr, ext);
|
|
2600
|
+
const safeStem = sanitizeIdForFilename(stem, 32);
|
|
2601
|
+
return `${h}_${safeStem}`;
|
|
2602
|
+
}
|
|
2603
|
+
function compactPathFor(sourcePath) {
|
|
2604
|
+
const abs = resolveIndexPath(sourcePath);
|
|
2605
|
+
return path4.join(dataDir(), compactSubdir, `${_compactSlug(abs)}.md`);
|
|
2606
|
+
}
|
|
2607
|
+
function readCompactHeader(compactPath) {
|
|
2608
|
+
try {
|
|
2609
|
+
const text = fs6.readFileSync(compactPath, "utf-8");
|
|
2610
|
+
const firstLine = text.split("\n")[0] || "";
|
|
2611
|
+
const m = firstLine.match(headerRegex);
|
|
2612
|
+
if (!m || !m[1] || !m[2]) return null;
|
|
2613
|
+
return [m[1], m[2]];
|
|
2614
|
+
} catch {
|
|
2615
|
+
return null;
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
function isCompactFresh(compactPath, sourcePath) {
|
|
2619
|
+
const header = readCompactHeader(compactPath);
|
|
2620
|
+
if (!header) return false;
|
|
2621
|
+
const [storedHash] = header;
|
|
2622
|
+
if (storedHash === "STALE") return false;
|
|
2623
|
+
const currentHash = sourceHash(sourcePath);
|
|
2624
|
+
return !!currentHash && currentHash === storedHash;
|
|
2625
|
+
}
|
|
2626
|
+
function markCompactStale(compactPath) {
|
|
2627
|
+
try {
|
|
2628
|
+
if (!fs6.existsSync(compactPath)) return false;
|
|
2629
|
+
const text = fs6.readFileSync(compactPath, "utf-8");
|
|
2630
|
+
const lines = text.split("\n");
|
|
2631
|
+
if (!lines[0]) return false;
|
|
2632
|
+
const m = lines[0].match(headerRegex);
|
|
2633
|
+
if (!m || m[1] === "STALE") return false;
|
|
2634
|
+
const oldHash = m[1];
|
|
2635
|
+
lines[0] = lines[0].replace(`source-hash:${oldHash}`, "source-hash:STALE");
|
|
2636
|
+
atomicWriteText(compactPath, lines.join("\n"));
|
|
2637
|
+
return true;
|
|
2638
|
+
} catch {
|
|
2639
|
+
return false;
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
function readCompactBody(compactPath) {
|
|
2643
|
+
try {
|
|
2644
|
+
const text = fs6.readFileSync(compactPath, "utf-8");
|
|
2645
|
+
const lines = text.split("\n");
|
|
2646
|
+
if (lines.length < 2) return null;
|
|
2647
|
+
const body = lines.slice(1).join("\n").trimStart();
|
|
2648
|
+
return body.trim() ? body : null;
|
|
2649
|
+
} catch {
|
|
2650
|
+
return null;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
function writeCompact(compactPath, sourcePath, compactBody, sourceRel) {
|
|
2654
|
+
const srcPath = path4.resolve(sourcePath);
|
|
2655
|
+
const sha = sourceHash(srcPath);
|
|
2656
|
+
const displayRel = sourceRel || path4.basename(srcPath);
|
|
2657
|
+
const header = `${headerPrefix}${sha} source:${displayRel} -->
|
|
2658
|
+
`;
|
|
2659
|
+
const fullText = header + compactBody.trimStart();
|
|
2660
|
+
const dir = path4.dirname(compactPath);
|
|
2661
|
+
if (!fs6.existsSync(dir)) {
|
|
2662
|
+
ensureDirSync(dir);
|
|
2663
|
+
}
|
|
2664
|
+
atomicWriteText(compactPath, fullText);
|
|
2665
|
+
}
|
|
2666
|
+
function buildExtractiveCompact(text, maxSentences) {
|
|
2667
|
+
const max = maxSentences ?? defaultSentencesPerSection;
|
|
2668
|
+
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
|
|
2669
|
+
const out = [];
|
|
2670
|
+
let i = 0;
|
|
2671
|
+
const n = lines.length;
|
|
2672
|
+
if (lines[0]?.trim() === "---") {
|
|
2673
|
+
let j = 1;
|
|
2674
|
+
while (j < n && lines[j]?.trim() !== "---") {
|
|
2675
|
+
j++;
|
|
2676
|
+
}
|
|
2677
|
+
if (j < n) {
|
|
2678
|
+
i = j + 1;
|
|
2679
|
+
}
|
|
2680
|
+
}
|
|
2681
|
+
let inCodeBlock = false;
|
|
2682
|
+
let codeBlockLines = 0;
|
|
2683
|
+
let currentHeading = null;
|
|
2684
|
+
let sentencesEmitted = 0;
|
|
2685
|
+
let fence = null;
|
|
2686
|
+
while (i < n) {
|
|
2687
|
+
const line = lines[i];
|
|
2688
|
+
const stripped = line.trim();
|
|
2689
|
+
const fm = /^\s*(`{3,}|~{3,})(.*)$/.exec(line);
|
|
2690
|
+
if (fm !== null && fm[1] !== void 0) {
|
|
2691
|
+
const run = fm[1];
|
|
2692
|
+
const ch = run[0] ?? "";
|
|
2693
|
+
if (fence === null) {
|
|
2694
|
+
fence = { ch, len: run.length };
|
|
2695
|
+
inCodeBlock = true;
|
|
2696
|
+
codeBlockLines = 0;
|
|
2697
|
+
if (currentHeading && sentencesEmitted < max) {
|
|
2698
|
+
out.push(line);
|
|
2699
|
+
codeBlockLines++;
|
|
2700
|
+
}
|
|
2701
|
+
i++;
|
|
2702
|
+
continue;
|
|
2703
|
+
} else if (ch === fence.ch && run.length >= fence.len && (fm[2] ?? "").trim() === "") {
|
|
2704
|
+
fence = null;
|
|
2705
|
+
inCodeBlock = false;
|
|
2706
|
+
if (currentHeading && codeBlockLines > 0) {
|
|
2707
|
+
out.push(line);
|
|
2708
|
+
codeBlockLines++;
|
|
2709
|
+
}
|
|
2710
|
+
i++;
|
|
2711
|
+
continue;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
if (inCodeBlock) {
|
|
2715
|
+
if (codeBlockLines < 10 && currentHeading && sentencesEmitted < max) {
|
|
2716
|
+
out.push(line);
|
|
2717
|
+
codeBlockLines++;
|
|
2718
|
+
}
|
|
2719
|
+
i++;
|
|
2720
|
+
continue;
|
|
2721
|
+
}
|
|
2722
|
+
const headingMatch = stripped.match(/^(#{1,6})\s+(.*)/);
|
|
2723
|
+
if (headingMatch) {
|
|
2724
|
+
currentHeading = stripped;
|
|
2725
|
+
sentencesEmitted = 0;
|
|
2726
|
+
out.push("");
|
|
2727
|
+
out.push(line);
|
|
2728
|
+
i++;
|
|
2729
|
+
continue;
|
|
2730
|
+
}
|
|
2731
|
+
if (currentHeading && sentencesEmitted < max && stripped) {
|
|
2732
|
+
out.push(line);
|
|
2733
|
+
sentencesEmitted++;
|
|
2734
|
+
}
|
|
2735
|
+
i++;
|
|
2736
|
+
}
|
|
2737
|
+
const resultLines = [];
|
|
2738
|
+
let prevBlank = false;
|
|
2739
|
+
for (const ln of out) {
|
|
2740
|
+
const isBlank = !ln.trim();
|
|
2741
|
+
if (isBlank && prevBlank) {
|
|
2742
|
+
continue;
|
|
2743
|
+
}
|
|
2744
|
+
resultLines.push(ln);
|
|
2745
|
+
prevBlank = isBlank;
|
|
2746
|
+
}
|
|
2747
|
+
return resultLines.join("\n").trim() + "\n";
|
|
2748
|
+
}
|
|
2749
|
+
function extractDocCompact(body, heading) {
|
|
2750
|
+
if (heading) {
|
|
2751
|
+
const lines = body.split("\n");
|
|
2752
|
+
const unfencedIdx = /* @__PURE__ */ new Set();
|
|
2753
|
+
for (const [idx2] of eachUnfencedLine(lines)) unfencedIdx.add(idx2);
|
|
2754
|
+
const compactIdx = lines.findIndex((line, idx2) => {
|
|
2755
|
+
if (!unfencedIdx.has(idx2)) return false;
|
|
2756
|
+
return matchAtxHeading(line.trim(), ATX_DOT_BREAK_RE, true)?.name === heading;
|
|
2757
|
+
});
|
|
2758
|
+
if (compactIdx === -1) return "";
|
|
2759
|
+
const matchedHeadingLine = lines[compactIdx].trim();
|
|
2760
|
+
const levelMatch = /^(#+)/.exec(matchedHeadingLine);
|
|
2761
|
+
const matchedLevel = levelMatch ? levelMatch[1].length : 0;
|
|
2762
|
+
let endIdx = lines.length;
|
|
2763
|
+
for (let i = compactIdx + 1; i < lines.length; i++) {
|
|
2764
|
+
if (!unfencedIdx.has(i)) continue;
|
|
2765
|
+
const trimmed = lines[i].trim();
|
|
2766
|
+
const nextLevelMatch = /^(#+)\s/.exec(trimmed);
|
|
2767
|
+
if (nextLevelMatch) {
|
|
2768
|
+
const nextLevel = nextLevelMatch[1].length;
|
|
2769
|
+
if (nextLevel <= matchedLevel) {
|
|
2770
|
+
endIdx = i;
|
|
2771
|
+
break;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
return lines.slice(compactIdx, endIdx).join("\n");
|
|
2776
|
+
}
|
|
2777
|
+
const compactMarker = "<!-- COMPACT_END -->";
|
|
2778
|
+
const idx = body.indexOf(compactMarker);
|
|
2779
|
+
if (idx === -1) return "";
|
|
2780
|
+
return body.slice(0, idx).trim();
|
|
2781
|
+
}
|
|
2782
|
+
function compactDoc(filePath, heading) {
|
|
2783
|
+
try {
|
|
2784
|
+
const body = fs6.readFileSync(filePath, "utf-8");
|
|
2785
|
+
const compact = extractDocCompact(body, heading);
|
|
2786
|
+
return compact || null;
|
|
2787
|
+
} catch {
|
|
2788
|
+
return null;
|
|
2789
|
+
}
|
|
2790
|
+
}
|
|
2791
|
+
|
|
2792
|
+
// src/notebook_compact.ts
|
|
2793
|
+
init_define_import_meta_env();
|
|
2794
|
+
import * as fs7 from "node:fs";
|
|
2795
|
+
import * as path5 from "node:path";
|
|
2796
|
+
var NB_STRIP_MIN_SAVINGS = 4096;
|
|
2797
|
+
function stripNotebook(nbDict) {
|
|
2798
|
+
const cells = [];
|
|
2799
|
+
for (const cell of nbDict.cells ?? []) {
|
|
2800
|
+
if (cell.cell_type === "code") {
|
|
2801
|
+
cells.push({
|
|
2802
|
+
...cell,
|
|
2803
|
+
outputs: [],
|
|
2804
|
+
execution_count: null
|
|
2805
|
+
});
|
|
2806
|
+
} else {
|
|
2807
|
+
cells.push(cell);
|
|
2808
|
+
}
|
|
2809
|
+
}
|
|
2810
|
+
return { ...nbDict, cells };
|
|
2811
|
+
}
|
|
2812
|
+
var SIDECAR_DEFAULT_MAX_COUNT = 200;
|
|
2813
|
+
var SIDECAR_DEFAULT_MAX_AGE_MS = 24 * 3600 * 1e3;
|
|
2814
|
+
function pruneSidecars(cacheRoot, maxCount = SIDECAR_DEFAULT_MAX_COUNT, maxAgeMs = SIDECAR_DEFAULT_MAX_AGE_MS) {
|
|
2815
|
+
const nbStripDir = path5.join(cacheRoot, "nb_strip");
|
|
2816
|
+
let removed = 0;
|
|
2817
|
+
try {
|
|
2818
|
+
if (!fs7.existsSync(nbStripDir)) return 0;
|
|
2819
|
+
const cutoff = Date.now() - maxAgeMs;
|
|
2820
|
+
const kept = [];
|
|
2821
|
+
for (const entry of fs7.readdirSync(nbStripDir)) {
|
|
2822
|
+
const dir = path5.join(nbStripDir, entry);
|
|
2823
|
+
let mtime;
|
|
2824
|
+
try {
|
|
2825
|
+
mtime = fs7.statSync(dir).mtimeMs;
|
|
2826
|
+
} catch {
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
if (mtime < cutoff) {
|
|
2830
|
+
try {
|
|
2831
|
+
fs7.rmSync(dir, { recursive: true, force: true });
|
|
2832
|
+
removed++;
|
|
2833
|
+
} catch {
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
} else {
|
|
2837
|
+
kept.push([dir, mtime]);
|
|
2838
|
+
}
|
|
2839
|
+
}
|
|
2840
|
+
if (kept.length > maxCount) {
|
|
2841
|
+
kept.sort((a, b) => a[1] - b[1]);
|
|
2842
|
+
for (const [dir] of kept.slice(0, kept.length - maxCount)) {
|
|
2843
|
+
try {
|
|
2844
|
+
fs7.rmSync(dir, { recursive: true, force: true });
|
|
2845
|
+
removed++;
|
|
2846
|
+
} catch {
|
|
2847
|
+
continue;
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
} catch {
|
|
2852
|
+
return removed;
|
|
2853
|
+
}
|
|
2854
|
+
return removed;
|
|
2855
|
+
}
|
|
2856
|
+
function getOrCreateSidecar(rawBytes, cacheRoot, opts = {}) {
|
|
2857
|
+
const sha = fingerprintContent(rawBytes);
|
|
2858
|
+
const sidecarDir = path5.join(cacheRoot, "nb_strip", sha);
|
|
2859
|
+
const sidecarPath = path5.join(sidecarDir, "stripped.ipynb");
|
|
2860
|
+
if (fs7.existsSync(sidecarPath)) {
|
|
2861
|
+
return [sidecarPath, false];
|
|
2862
|
+
}
|
|
2863
|
+
let nb;
|
|
2864
|
+
try {
|
|
2865
|
+
nb = JSON.parse(rawBytes.toString("utf-8"));
|
|
2866
|
+
} catch (err) {
|
|
2867
|
+
throw new Error(`Failed to parse notebook JSON: ${String(err)}`, { cause: err });
|
|
2868
|
+
}
|
|
2869
|
+
if (typeof nb !== "object" || nb === null || !("cells" in nb)) {
|
|
2870
|
+
throw new Error("Not a notebook");
|
|
2871
|
+
}
|
|
2872
|
+
const nbDict = nb;
|
|
2873
|
+
const stripped = stripNotebook(nbDict);
|
|
2874
|
+
try {
|
|
2875
|
+
ensureDirSync(sidecarDir);
|
|
2876
|
+
} catch (err) {
|
|
2877
|
+
if (!fs7.existsSync(sidecarDir)) {
|
|
2878
|
+
throw err;
|
|
2879
|
+
}
|
|
2880
|
+
}
|
|
2881
|
+
const strippedJson = JSON.stringify(stripped, null, 2) + "\n";
|
|
2882
|
+
atomicWriteBytes(sidecarPath, Buffer.from(strippedJson, "utf-8"));
|
|
2883
|
+
pruneSidecars(cacheRoot, opts.maxCount ?? SIDECAR_DEFAULT_MAX_COUNT, opts.maxAgeMs ?? SIDECAR_DEFAULT_MAX_AGE_MS);
|
|
2884
|
+
return [sidecarPath, true];
|
|
2885
|
+
}
|
|
2886
|
+
|
|
2887
|
+
// src/hooks_read.ts
|
|
2888
|
+
function isTsConfigFile(basename5) {
|
|
2889
|
+
const lower = basename5.toLowerCase();
|
|
2890
|
+
return /^tsconfig(\..+)?\.json$/i.test(lower) || lower === "jsconfig.json";
|
|
2891
|
+
}
|
|
2892
|
+
var LARGE_FILE_BYTES = FILE_TYPE_THRESHOLDS.generic;
|
|
2893
|
+
var TASK_OUTPUT_DENY_BYTES = 20 * 1024;
|
|
2894
|
+
var SUBAGENT_MD_FIRST_READ_DENY_BYTES = 30 * 1024;
|
|
2895
|
+
var SUBAGENT_MD_FIRST_READ_DENY_EXT_RE = /\.(md|mdx|markdown)$/i;
|
|
2896
|
+
var DENY_THRESHOLD_TIER_MULTIPLIERS = {
|
|
2897
|
+
cool: 1,
|
|
2898
|
+
warm: 0.67,
|
|
2899
|
+
hot: 0.33,
|
|
2900
|
+
critical: 0.18
|
|
2901
|
+
};
|
|
2902
|
+
function largeFileDenyBytes() {
|
|
2903
|
+
const base = loadConfig().hints.large_read_redirect_bytes;
|
|
2904
|
+
const tier = getContextPressure(loadSessionCache(getSessionId()) ?? void 0, getTranscriptPath()).tier;
|
|
2905
|
+
return Math.round(base * DENY_THRESHOLD_TIER_MULTIPLIERS[tier]);
|
|
2906
|
+
}
|
|
2907
|
+
function counterfactualCredit(counterfactualBytes, emittedBytes = 0) {
|
|
2908
|
+
return Math.max(0, Math.min(counterfactualBytes, PER_FILE_COUNTERFACTUAL_CEILING) - emittedBytes);
|
|
2909
|
+
}
|
|
2910
|
+
function diffHintCredit(counterfactualBytes, body) {
|
|
2911
|
+
const credit = counterfactualCredit(counterfactualBytes, body.length);
|
|
2912
|
+
if (savedTokensFromBytes(credit) < loadConfig().hints.diff_hint_min_tokens_saved) return null;
|
|
2913
|
+
return credit;
|
|
2914
|
+
}
|
|
2915
|
+
var SNAPSHOT_DIFF_LEAD = "Here is what changed:";
|
|
2916
|
+
function snapshotDenyText(normalized, snap, suffix) {
|
|
2917
|
+
const subject = readSubject(normalized, suffix);
|
|
2918
|
+
const lead = snap.kind === "unchanged" ? sentenceStart(subject) + " is unchanged since last read. " : "Content changed since last read of " + subject + ". " + SNAPSHOT_DIFF_LEAD + "\n\n" + fenceUntrustedFileContent("```diff\n" + snap.diff + "\n```") + "\n\n";
|
|
2919
|
+
return (lead + suffix).trimEnd();
|
|
2920
|
+
}
|
|
2921
|
+
function readSubject(normalized, rest) {
|
|
2922
|
+
return fileSubject(rest, displaySafePath(normalized), normalized);
|
|
2923
|
+
}
|
|
2924
|
+
function docSnapshotDenyText(normalized, basename5, snap) {
|
|
2925
|
+
return snapshotDenyText(normalized, snap, surgicalHint(normalized, basename5, countTextLines(snap.currentContent), snap.currentContent));
|
|
2926
|
+
}
|
|
2927
|
+
function relPathWithinRoot(root, target) {
|
|
2928
|
+
const rel = path6.relative(root, target).replace(/\\/g, "/");
|
|
2929
|
+
if (rel.startsWith("..") || path6.isAbsolute(rel)) return null;
|
|
2930
|
+
return rel;
|
|
2931
|
+
}
|
|
2932
|
+
function _isDocFile(filePath) {
|
|
2933
|
+
const lower = filePath.toLowerCase();
|
|
2934
|
+
return lower.endsWith(".md") || lower.endsWith(".mdx") || lower.endsWith(".markdown") || lower.endsWith(".rst");
|
|
2935
|
+
}
|
|
2936
|
+
function isSessionArtifactFile(filePath) {
|
|
2937
|
+
if (/[/\\]tasks[/\\][a-z0-9]+\.output$/i.test(filePath)) return true;
|
|
2938
|
+
if (new RegExp("[/\\\\]tool-results[/\\\\]" + TOOL_RESULTS_ID_CHARS + "\\.txt$", "i").test(filePath)) return true;
|
|
2939
|
+
return false;
|
|
2940
|
+
}
|
|
2941
|
+
function lineWindowBytes(filePath, offset, limit) {
|
|
2942
|
+
try {
|
|
2943
|
+
const lines = fs8.readFileSync(filePath, "utf-8").split("\n").slice(offset - 1, offset - 1 + limit);
|
|
2944
|
+
return lines.reduce((sum, line) => sum + Buffer.byteLength(line, "utf-8") + 1, 0);
|
|
2945
|
+
} catch {
|
|
2946
|
+
return Infinity;
|
|
2947
|
+
}
|
|
2948
|
+
}
|
|
2949
|
+
function sessionArtifactRecall(rawPath) {
|
|
2950
|
+
const filePath = displaySafePath(rawPath);
|
|
2951
|
+
return "Use `token-goat bash-output --file " + quotedArg(filePath) + " --tail 50` (or `--grep PATTERN`) to read a slice instead of the full file.";
|
|
2952
|
+
}
|
|
2953
|
+
function priorOutputRecallHint(normalized) {
|
|
2954
|
+
const ids = getFileServedOutputs(normalized);
|
|
2955
|
+
if (ids.length === 0) return "";
|
|
2956
|
+
const latestId = ids[ids.length - 1];
|
|
2957
|
+
return " Recall earlier content with `token-goat bash-output " + latestId + "`.";
|
|
2958
|
+
}
|
|
2959
|
+
function isDiffableSource(basename5) {
|
|
2960
|
+
return languageHasFlag(detectLanguage(basename5), "diffable");
|
|
2961
|
+
}
|
|
2962
|
+
function isSourceExtension(basename5) {
|
|
2963
|
+
return languageHasFlag(detectLanguage(basename5), "sourceHints");
|
|
2964
|
+
}
|
|
2965
|
+
var BINARY_FILE_TYPE_EXTS = /* @__PURE__ */ new Set(["pdf", "docx", "xlsx", "pptx", "odt", "ods", "ott", "odp", "sqlite", "db", "sqlite3", "db3", "parquet"]);
|
|
2966
|
+
var TEXT_FILE_TYPE_EXTS = /* @__PURE__ */ new Set(["html", "htm", "xhtml", "txt", "log", "out", "err", "trace", "csv", "tsv", "vtt", "srt", "svg", "xml", "dtsx", "ampkg", "xaml", "json", "yaml", "yml", "jsonl"]);
|
|
2967
|
+
var DISPATCHED_FILE_TYPE_EXTS = /* @__PURE__ */ new Set([...BINARY_FILE_TYPE_EXTS, ...TEXT_FILE_TYPE_EXTS]);
|
|
2968
|
+
function isDispatchedFileType(basename5) {
|
|
2969
|
+
return DISPATCHED_FILE_TYPE_EXTS.has(path6.extname(basename5).slice(1).toLowerCase());
|
|
2970
|
+
}
|
|
2971
|
+
function extractQuickSymbolSamples(content, _filePath, limit = 3) {
|
|
2972
|
+
const symbols = [];
|
|
2973
|
+
const isShell = _filePath !== void 0 ? /\.(sh|bash|zsh|ksh)$/i.test(_filePath) : /^#!\s*\/(?:usr\/)?(?:bin\/|local\/bin\/)?(?:bash|sh|zsh)/.test(content);
|
|
2974
|
+
if (isShell) {
|
|
2975
|
+
const lines = content.split(/\r?\n/);
|
|
2976
|
+
for (const line of lines) {
|
|
2977
|
+
const banner = extractShellBannerHeading(line);
|
|
2978
|
+
if (banner !== null && !symbols.includes(banner.heading)) {
|
|
2979
|
+
symbols.push(banner.heading);
|
|
2980
|
+
if (symbols.length >= limit) return symbols;
|
|
2981
|
+
}
|
|
2982
|
+
}
|
|
2983
|
+
}
|
|
2984
|
+
const pesterRegex = /^\s*(?:Describe|Context|It|BeforeAll|BeforeEach|AfterAll|AfterEach)\b(?:\s+-[A-Za-z0-9_-]+(?:\s+(?:'[^']*'|"[^"]*"|[^\s{]+))?)*\s*(?:'([^']*)'|"([^"]*)"|([A-Za-z0-9_.-]+))?/gim;
|
|
2985
|
+
let match;
|
|
2986
|
+
while ((match = pesterRegex.exec(content)) !== null) {
|
|
2987
|
+
const rawMatch = match[0].trimStart();
|
|
2988
|
+
const blockKeyword = rawMatch.split(/\s+/)[0] ?? "";
|
|
2989
|
+
const explicitName = match[1] ?? match[2] ?? match[3];
|
|
2990
|
+
const name = explicitName || blockKeyword;
|
|
2991
|
+
if (name && !symbols.includes(name)) {
|
|
2992
|
+
symbols.push(name);
|
|
2993
|
+
if (symbols.length >= limit) return symbols;
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
const psFnRegex = /^\s*(?:function|filter|workflow|configuration)\s+(?:(?:global|local|script|private):)?([A-Za-z0-9_\u00C0-\uFFFF-]+)/gim;
|
|
2997
|
+
while ((match = psFnRegex.exec(content)) !== null) {
|
|
2998
|
+
const name = match[1];
|
|
2999
|
+
if (name && !symbols.includes(name)) {
|
|
3000
|
+
symbols.push(name);
|
|
3001
|
+
if (symbols.length >= limit) return symbols;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
const fnRegex = /(?:(?:export\s+(?:default\s+)?(?:async\s+)?)?(?:function\s+|class\s+|(?:const|let|var)\s+)|def\s+|func(?:\s*\([^)]*\))?\s+|fn\s+)([A-Za-z0-9_$]+)/g;
|
|
3005
|
+
while ((match = fnRegex.exec(content)) !== null) {
|
|
3006
|
+
const name = match[1];
|
|
3007
|
+
if (name && !symbols.includes(name)) {
|
|
3008
|
+
symbols.push(name);
|
|
3009
|
+
if (symbols.length >= limit) break;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
return symbols;
|
|
3013
|
+
}
|
|
3014
|
+
var HINT_NAME_SCAN_LIMIT = 500;
|
|
3015
|
+
var HINT_NAME_COUNT = 3;
|
|
3016
|
+
function indexedHintNames(opts) {
|
|
3017
|
+
return querySymbols({ ...opts, limit: HINT_NAME_SCAN_LIMIT }).map((symbol) => escapeHintName(symbol.name)).filter((name) => name !== "").slice(0, HINT_NAME_COUNT);
|
|
3018
|
+
}
|
|
3019
|
+
function surgicalHint(filePath, basename5, lineCount, fileContent) {
|
|
3020
|
+
if (lineCount < loadConfig().hints.min_file_lines_for_hint) return "";
|
|
3021
|
+
const isDocFile = /\.(md|mdx|rst|txt)$/i.test(basename5);
|
|
3022
|
+
const isJsonFile = /\.(json|jsonc)$/i.test(basename5);
|
|
3023
|
+
const isYamlFile = /\.(yaml|yml)$/i.test(basename5);
|
|
3024
|
+
const isHtmlFile = /\.(html|htm)$/i.test(basename5);
|
|
3025
|
+
const isSectionFile = /\.(css|scss|sass|less|toml)$/i.test(basename5);
|
|
3026
|
+
const isXmlFile = /\.(xml|dtsx|ampkg|xaml)$/i.test(basename5) && !basename5.toLowerCase().endsWith("-meta.xml");
|
|
3027
|
+
if (isXmlFile) {
|
|
3028
|
+
return leadWithCommand(`token-goat xml-outline ${quotedArg(filePath)}`, `for structure, or \`token-goat xml-query ${quotedArgs(filePath, "<selector>").join(" ")}\` for nodes`);
|
|
3029
|
+
} else if (isDocFile && !docNavigation(filePath).section) {
|
|
3030
|
+
return fileQueryHint(filePath, "", "pattern");
|
|
3031
|
+
} else if (isDocFile) {
|
|
3032
|
+
let top = [];
|
|
3033
|
+
if (fileContent !== void 0 && /\.(md|mdx)$/i.test(basename5)) {
|
|
3034
|
+
top = extractMarkdownHeadings(fileContent).map((h) => escapeHintName(h.text.trim())).filter((name) => name !== "").slice(0, HINT_NAME_COUNT);
|
|
3035
|
+
} else if (fileContent === void 0) {
|
|
3036
|
+
try {
|
|
3037
|
+
top = indexedHintNames({ filePath, kind: "heading" });
|
|
3038
|
+
} catch {
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
const heading = hintTarget(filePath, "section", { content: fileContent });
|
|
3042
|
+
return leadWithCommand(sectionOrRangeCommand(filePath, heading), (top.length > 0 ? `(or sections: ${top.join(", ")}) ` : "") + "to extract a part");
|
|
3043
|
+
} else if (isJsonFile || isYamlFile) {
|
|
3044
|
+
const fmt = isJsonFile ? "json" : "yaml";
|
|
3045
|
+
const key = hintTarget(filePath, "key", { content: fileContent });
|
|
3046
|
+
if (key.real) return leadWithCommand(sliceCommand(filePath, key), `to read that value, or \`token-goat ${fmt}-outline ${quotedArg(filePath)}\` for every top-level key`);
|
|
3047
|
+
return isJsonFile ? leadWithCommand(`token-goat json-query ${quotedArgs(filePath, "<key>").join(" ")}`, `(or \`token-goat json-outline ${quotedArg(filePath)}\`) to slice one value`) : leadWithCommand(`token-goat section ${quotedArg(`${filePath}::name`)}`, `or \`token-goat yaml-query ${quotedArgs(filePath, "<key>").join(" ")}\` to extract a part`);
|
|
3048
|
+
} else if (isHtmlFile) {
|
|
3049
|
+
return leadWithCommand(sectionOrRangeCommand(filePath, hintTarget(filePath, "section", { content: fileContent })), `or \`token-goat outline ${quotedArg(filePath)}\` to navigate HTML structure`);
|
|
3050
|
+
} else if (isSectionFile) {
|
|
3051
|
+
if (!/\.toml$/i.test(basename5)) return leadWithCommand(`token-goat outline ${quotedArg(filePath)}`, "to list every selector");
|
|
3052
|
+
const table = hintTarget(filePath, "section", { content: fileContent, placeholder: "name" }).name;
|
|
3053
|
+
return leadWithCommand(`token-goat section ${quotedArg(`${filePath}::${table}`)}`, "to extract a part");
|
|
3054
|
+
} else {
|
|
3055
|
+
const isShellScript = /\.(sh|bash|zsh|ksh)$/i.test(basename5);
|
|
3056
|
+
let namesAreHeadings = false;
|
|
3057
|
+
const samples = fileContent !== void 0 ? extractQuickSymbolSamples(fileContent, filePath, HINT_NAME_SCAN_LIMIT).map(escapeHintName).filter((name) => name !== "").slice(0, HINT_NAME_COUNT) : (() => {
|
|
3058
|
+
try {
|
|
3059
|
+
if (isShellScript) {
|
|
3060
|
+
const headings = indexedHintNames({ filePath, kind: "heading" });
|
|
3061
|
+
if (headings.length > 0) {
|
|
3062
|
+
namesAreHeadings = true;
|
|
3063
|
+
return headings;
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
return indexedHintNames({ filePath });
|
|
3067
|
+
} catch {
|
|
3068
|
+
return [];
|
|
3069
|
+
}
|
|
3070
|
+
})();
|
|
3071
|
+
const fallback = samples.length > 0 ? null : hintTarget(filePath, "symbol", { content: fileContent });
|
|
3072
|
+
const sym = samples[0] ?? fallback?.name ?? HINT_PLACEHOLDERS.symbol;
|
|
3073
|
+
const avail = samples.length > 0 ? `(available: ${samples.join(", ")}) ` : "";
|
|
3074
|
+
if (namesAreHeadings) {
|
|
3075
|
+
return leadWithCommand(`token-goat section ${quotedArg(`${filePath}::${sym}`)}`, `${avail}for a section, or \`token-goat skeleton ${quotedArg(filePath)}\` / \`token-goat outline ${quotedArg(filePath)}\` for structure`);
|
|
3076
|
+
}
|
|
3077
|
+
if (samples.length > 0) {
|
|
3078
|
+
try {
|
|
3079
|
+
const indexed = querySymbols({ filePath, name: sym, limit: 1 })[0];
|
|
3080
|
+
if (indexed !== void 0 && isLargeSymbolSpan(indexed.lineStart, indexed.lineEnd, filePath)) {
|
|
3081
|
+
return `${sentenceStart(nameSubject("symbol", sym))} spans ${indexed.lineEnd - indexed.lineStart + 1} lines -- use \`token-goat grep ${quotedArgs("<pattern>", filePath).join(" ")} -C 15 --symbol\` for a slice inside it, or \`token-goat scope ${quotedArg(`${filePath}:${indexed.lineStart}`)}\` to confirm the enclosing symbol.`;
|
|
3082
|
+
}
|
|
3083
|
+
} catch {
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
if (fallback?.real === false) return leadWithCommand(`token-goat skeleton ${quotedArg(filePath)}`, `or \`token-goat outline ${quotedArg(filePath)}\` for structure, then \`token-goat read ${quotedArg(`${filePath}::${sym}`)}\` for one function`);
|
|
3087
|
+
return leadWithCommand(`token-goat read ${quotedArg(`${filePath}::${sym}`)}`, `${avail}for one function, or \`token-goat skeleton ${quotedArg(filePath)}\` / \`token-goat outline ${quotedArg(filePath)}\` for structure`);
|
|
3088
|
+
}
|
|
3089
|
+
}
|
|
3090
|
+
function isLargeSymbolSpan(lineStart, lineEnd, filePath) {
|
|
3091
|
+
const span = lineEnd - lineStart + 1;
|
|
3092
|
+
if (span > LARGE_SYMBOL_LINE_THRESHOLD) return true;
|
|
3093
|
+
const totalLines = lineCountForSurgicalHint(filePath);
|
|
3094
|
+
return totalLines > 0 && span > totalLines / 2;
|
|
3095
|
+
}
|
|
3096
|
+
var SYMBOL_HINT_PAGE = 500;
|
|
3097
|
+
function symbolsMeetingRange(filePath, range, firstPage) {
|
|
3098
|
+
const meets = (s) => s.lineStart <= range.end && s.lineEnd >= range.start;
|
|
3099
|
+
const found = firstPage.filter(meets);
|
|
3100
|
+
let page = firstPage;
|
|
3101
|
+
let offset = 0;
|
|
3102
|
+
for (; ; ) {
|
|
3103
|
+
const last = page.length === SYMBOL_HINT_PAGE ? page[page.length - 1] : void 0;
|
|
3104
|
+
if (last === void 0 || last.lineStart > range.end) break;
|
|
3105
|
+
offset += SYMBOL_HINT_PAGE;
|
|
3106
|
+
page = querySymbols({ filePath, limit: SYMBOL_HINT_PAGE, offset }).map((s) => ({ name: escapeHintName(s.name), lineStart: s.lineStart, lineEnd: s.lineEnd }));
|
|
3107
|
+
for (const symbol of page) if (meets(symbol)) found.push(symbol);
|
|
3108
|
+
}
|
|
3109
|
+
return found;
|
|
3110
|
+
}
|
|
3111
|
+
function realSymbolReadHint(filePath, shown, range) {
|
|
3112
|
+
let candidates;
|
|
3113
|
+
try {
|
|
3114
|
+
const all = querySymbols({ filePath, limit: SYMBOL_HINT_PAGE }).map((s) => ({ name: escapeHintName(s.name), lineStart: s.lineStart, lineEnd: s.lineEnd }));
|
|
3115
|
+
const overlapping = range !== void 0 ? symbolsMeetingRange(filePath, range, all) : [];
|
|
3116
|
+
candidates = (overlapping.length > 0 ? overlapping : all).filter((s) => s.name !== "");
|
|
3117
|
+
} catch {
|
|
3118
|
+
candidates = [];
|
|
3119
|
+
}
|
|
3120
|
+
const top = candidates[0];
|
|
3121
|
+
if (top === void 0) {
|
|
3122
|
+
return range !== void 0 ? "`token-goat read " + quotedArg(shown + "@" + range.start + "-" + range.end) + "`" : "`token-goat outline " + quotedArg(shown) + "`";
|
|
3123
|
+
}
|
|
3124
|
+
if (isLargeSymbolSpan(top.lineStart, top.lineEnd, filePath)) {
|
|
3125
|
+
const line = range !== void 0 ? range.start : top.lineStart;
|
|
3126
|
+
const span = top.lineEnd - top.lineStart + 1;
|
|
3127
|
+
return "`token-goat grep " + quotedArgs("<pattern>", shown).join(" ") + " -C 15 --symbol` for a slice inside " + nameSubject("symbol", top.name) + " (" + span + " lines), or `token-goat scope " + quotedArg(shown + ":" + line) + "` to confirm the enclosing symbol";
|
|
3128
|
+
}
|
|
3129
|
+
const names = candidates.map((s) => s.name).slice(0, 3);
|
|
3130
|
+
const rest = names.length > 1 ? " (or: " + names.slice(1).join(", ") + ")" : "";
|
|
3131
|
+
return "`token-goat read " + quotedArg(shown + "::" + names[0]) + "`" + rest;
|
|
3132
|
+
}
|
|
3133
|
+
function lineCountForSurgicalHint(filePath, fileStatSize) {
|
|
3134
|
+
try {
|
|
3135
|
+
const onDisk = hostPathOfIndexKey(filePath);
|
|
3136
|
+
if (fileStatSize !== void 0 && fileStatSize > SLICE_ESTIMATE_SCAN_CAP_BYTES) {
|
|
3137
|
+
return 0;
|
|
3138
|
+
}
|
|
3139
|
+
const sz = fileStatSize ?? statSize(onDisk);
|
|
3140
|
+
if (sz !== null && sz > SLICE_ESTIMATE_SCAN_CAP_BYTES) {
|
|
3141
|
+
return 0;
|
|
3142
|
+
}
|
|
3143
|
+
return countTextLines(fs8.readFileSync(onDisk, "utf8"));
|
|
3144
|
+
} catch {
|
|
3145
|
+
return 0;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
function detectSkillFile(filePath) {
|
|
3149
|
+
const match = filePath.match(/\.claude[\\/]skills[\\/]([^\\/]+)[\\/]SKILL\.md$/i);
|
|
3150
|
+
return match ? displaySafePath(match[1]) : null;
|
|
3151
|
+
}
|
|
3152
|
+
function scanCrossSessionManifests(projectRoot, projectHash, filePath, ttlSecs) {
|
|
3153
|
+
try {
|
|
3154
|
+
const relPath = relPathWithinRoot(projectRoot, filePath);
|
|
3155
|
+
if (relPath === null) return false;
|
|
3156
|
+
const foldedRelPath = foldPath(relPath);
|
|
3157
|
+
const manifests = readAllSessionManifests(projectHash, ttlSecs);
|
|
3158
|
+
for (const data of manifests) {
|
|
3159
|
+
const files = data["files"];
|
|
3160
|
+
if (!Array.isArray(files)) continue;
|
|
3161
|
+
for (const entry of files) {
|
|
3162
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
3163
|
+
const entryRelPath = entry["rel_path"];
|
|
3164
|
+
const entryHitCount = entry["hit_count"];
|
|
3165
|
+
if (typeof entryRelPath === "string" && foldPath(entryRelPath) === foldedRelPath && typeof entryHitCount === "number" && entryHitCount > 0) {
|
|
3166
|
+
return true;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
} catch {
|
|
3171
|
+
}
|
|
3172
|
+
return false;
|
|
3173
|
+
}
|
|
3174
|
+
var recordedReadEvents = /* @__PURE__ */ new WeakSet();
|
|
3175
|
+
function recordActualRead(event, filePath) {
|
|
3176
|
+
if (event.toolName === "Grep") return;
|
|
3177
|
+
if (recordedReadEvents.has(event)) return;
|
|
3178
|
+
recordedReadEvents.add(event);
|
|
3179
|
+
recordFileRead(filePath, !readRequestedSliceWindow(event).isExplicitSlice);
|
|
3180
|
+
}
|
|
3181
|
+
function firstReadSymbolOutput(decision, policyContext, hints, recordRead) {
|
|
3182
|
+
const { event, normalizedPath, fileSize } = policyContext;
|
|
3183
|
+
if (decision.action === "deny") {
|
|
3184
|
+
const denyCredit = counterfactualCredit(fileSize);
|
|
3185
|
+
recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit), void 0, "first-read-symbol-deny");
|
|
3186
|
+
return denyOutput(decision.message);
|
|
3187
|
+
}
|
|
3188
|
+
if (decision.action === "warn" && fileSize < LARGE_FILE_BYTES) {
|
|
3189
|
+
if (recordRead) {
|
|
3190
|
+
recordActualRead(event, normalizedPath);
|
|
3191
|
+
recordActualSlice(event, normalizedPath);
|
|
3192
|
+
}
|
|
3193
|
+
if (hints.log_large_file_hint_outcomes) {
|
|
3194
|
+
recordLargeFileHintPending(normalizedPath, fileSize);
|
|
3195
|
+
}
|
|
3196
|
+
if (!policyContext.quiet) {
|
|
3197
|
+
recordStat("session_hint", 0, 0);
|
|
3198
|
+
}
|
|
3199
|
+
return quietContextOutput(decision.message + contextPressureAdvisorySuffix(), [policyContext.shownPath]);
|
|
3200
|
+
}
|
|
3201
|
+
return null;
|
|
3202
|
+
}
|
|
3203
|
+
function recordActualSlice(event, filePath) {
|
|
3204
|
+
const window = readRequestedSliceWindow(event);
|
|
3205
|
+
const span = requestedSliceSpan(window);
|
|
3206
|
+
if (window.isExplicitSlice && span !== void 0) {
|
|
3207
|
+
recordFileLineRange(filePath, span.start, span.end);
|
|
3208
|
+
recordRecentReadRange(foldPath(normalizePath(filePath)), [span.start, span.end]);
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
function quietContextOutput(context, correlators) {
|
|
3212
|
+
if (isWithinQuietHours(loadConfig().hints.quiet_hours)) {
|
|
3213
|
+
return passOutput();
|
|
3214
|
+
}
|
|
3215
|
+
return contextOutput(context, correlators);
|
|
3216
|
+
}
|
|
3217
|
+
function contextPressureAdvisorySuffix() {
|
|
3218
|
+
if (!loadConfig().hints.context_threshold_advisory) return "";
|
|
3219
|
+
const tier = getContextPressure(loadSessionCache(getSessionId()) ?? void 0, getTranscriptPath()).tier;
|
|
3220
|
+
if (tier === "hot") {
|
|
3221
|
+
return " Context pressure: hot -- consider wrapping up soon or running /compact.";
|
|
3222
|
+
}
|
|
3223
|
+
if (tier === "critical") {
|
|
3224
|
+
return " Context pressure: critical -- strongly consider /compact now.";
|
|
3225
|
+
}
|
|
3226
|
+
return "";
|
|
3227
|
+
}
|
|
3228
|
+
function isProtectedRecentRead(normalized, n) {
|
|
3229
|
+
if (n <= 0) return false;
|
|
3230
|
+
const entry = getSessionFileEntry(normalized);
|
|
3231
|
+
if (entry && epochReadCounts(entry).reads >= 4) return false;
|
|
3232
|
+
const compactedAt = getCompactedAt();
|
|
3233
|
+
const ranked = Array.from(getSessionFiles().entries()).filter(([, e]) => e.lastReadAt >= compactedAt).sort((a, b) => {
|
|
3234
|
+
const byRecency = b[1].lastReadAt - a[1].lastReadAt;
|
|
3235
|
+
return byRecency !== 0 ? byRecency : a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0;
|
|
3236
|
+
});
|
|
3237
|
+
const target = foldPath(normalized);
|
|
3238
|
+
const rank = ranked.findIndex(([filePath]) => foldPath(filePath) === target);
|
|
3239
|
+
return rank !== -1 && rank < n;
|
|
3240
|
+
}
|
|
3241
|
+
function alreadyServedOutputId(event, normalized) {
|
|
3242
|
+
try {
|
|
3243
|
+
const ids = getFileServedOutputs(normalized);
|
|
3244
|
+
if (ids.length === 0) return null;
|
|
3245
|
+
const wouldServe = readWindowFromDisk(event, normalized);
|
|
3246
|
+
if (wouldServe === null) return null;
|
|
3247
|
+
const bytes = Buffer.byteLength(wouldServe, "utf-8");
|
|
3248
|
+
if (bytes < IDENTICAL_READ_MIN_BODY_BYTES) return null;
|
|
3249
|
+
for (let i = ids.length - 1; i >= 0; i--) {
|
|
3250
|
+
const id = ids[i];
|
|
3251
|
+
if (id === void 0) continue;
|
|
3252
|
+
const prior = getBashOutput(id);
|
|
3253
|
+
if (prior !== null && containsLineRun(prior.output, wouldServe)) return { id, bytes };
|
|
3254
|
+
}
|
|
3255
|
+
return null;
|
|
3256
|
+
} catch {
|
|
3257
|
+
return null;
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
function preReadHandlerInner(event) {
|
|
3261
|
+
let filePath = getFilePath(event);
|
|
3262
|
+
if (filePath === void 0 && event.toolName === "Grep") {
|
|
3263
|
+
const rawPath = event.toolInput["path"];
|
|
3264
|
+
if (typeof rawPath === "string" && rawPath !== "") filePath = resolveEventPath(event, rawPath);
|
|
3265
|
+
}
|
|
3266
|
+
if (filePath === void 0) return passOutput();
|
|
3267
|
+
const normalized = normalizePath(filePath);
|
|
3268
|
+
const shown = displaySafePath(normalized);
|
|
3269
|
+
const onDisk = hostPathOfIndexKey(normalized);
|
|
3270
|
+
const rangeWindow = readRequestedSliceWindow(event);
|
|
3271
|
+
const rangeSpan = rangeWindow.isExplicitSlice ? requestedSliceSpan(rangeWindow) : void 0;
|
|
3272
|
+
const rangeKey = foldPath(normalized);
|
|
3273
|
+
let rangeIdentity;
|
|
3274
|
+
if (event.toolName !== "Grep" && rangeSpan !== void 0 && loadConfig().hints.reread_deny) {
|
|
3275
|
+
rangeIdentity = dropFileLineRangesIfChanged(normalized);
|
|
3276
|
+
clearReadRangeIfChanged(rangeKey, rangeIdentity);
|
|
3277
|
+
if (getReadRangeDenials(rangeKey).length > 0) {
|
|
3278
|
+
lockReadRange(rangeKey, [rangeSpan.start, rangeSpan.end], rangeIdentity);
|
|
3279
|
+
return denyOutput(rangeLockMessage(shown, [rangeSpan.start, rangeSpan.end]));
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
if (isNodeModulesPath(normalized)) {
|
|
3283
|
+
return denyOutput(
|
|
3284
|
+
"node_modules is typically noise; use npm ls, npm outdated, or npm audit instead for dependency info. To force access, use: token-goat read node_modules/package/file.js::symbol-name or token-goat section node_modules/package/file.js::heading"
|
|
3285
|
+
);
|
|
3286
|
+
}
|
|
3287
|
+
if (event.toolName === "Read" && loadConfig().hints.cross_session_read_dedup) {
|
|
3288
|
+
try {
|
|
3289
|
+
const cwd = getCwd(event) ?? process.cwd();
|
|
3290
|
+
const project = findProject(cwd) ?? makeProjectAt(cwd);
|
|
3291
|
+
const current = readEvidenceFileText(normalized);
|
|
3292
|
+
const evidence = current === null ? null : findVerifiedFileEvidence(project.root, normalized, current);
|
|
3293
|
+
if (evidence !== null) {
|
|
3294
|
+
recordStat("evidence_cache_hit", 0);
|
|
3295
|
+
return quietContextOutput(
|
|
3296
|
+
`Verified cross-session evidence exists for this unchanged file (${evidence.id}). Use a narrow read instead of loading the full file again.`
|
|
3297
|
+
);
|
|
3298
|
+
}
|
|
3299
|
+
} catch {
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
const basename5 = displaySafePath(path6.basename(normalized));
|
|
3303
|
+
if (isLockFile(basename5)) {
|
|
3304
|
+
return denyOutput(
|
|
3305
|
+
"Lock files are rarely useful to read in full. " + fileQueryHint(shown, "Or read the relevant manifest instead.", "package-name")
|
|
3306
|
+
);
|
|
3307
|
+
}
|
|
3308
|
+
if (normalized.toLowerCase().endsWith(".tsbuildinfo")) {
|
|
3309
|
+
return denyOutput(
|
|
3310
|
+
"This is a TypeScript incremental build cache file. You don't need to read it directly."
|
|
3311
|
+
);
|
|
3312
|
+
}
|
|
3313
|
+
if (event.toolName !== "Grep" && (isInBuildDir(normalized) || isGeneratedFile(normalized))) {
|
|
3314
|
+
return denyOutput(
|
|
3315
|
+
"Generated/build artifact \u2014 read the source file instead."
|
|
3316
|
+
);
|
|
3317
|
+
}
|
|
3318
|
+
if (!wasFileReadThisSession(normalized)) {
|
|
3319
|
+
const manifestHint = buildPackageManifestHint({ file_path: normalized, shown });
|
|
3320
|
+
if (manifestHint) {
|
|
3321
|
+
recordActualRead(event, normalized);
|
|
3322
|
+
markHintShown("manifest-hint:" + normalized);
|
|
3323
|
+
return quietContextOutput(manifestHint.text);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
if (isTsConfigFile(basename5) && wasFileReadThisSession(normalized)) {
|
|
3327
|
+
recordActualRead(event, normalized);
|
|
3328
|
+
return quietContextOutput(
|
|
3329
|
+
leadWithCommand(configGetCommand(shown, "compilerOptions.target"), "for a single compiler option, or `token-goat json-outline " + quotedArg(shown) + "` for every top-level key", "Already read this file.")
|
|
3330
|
+
);
|
|
3331
|
+
}
|
|
3332
|
+
if (isManifestFile(basename5) && wasFileReadThisSession(normalized)) {
|
|
3333
|
+
recordActualRead(event, normalized);
|
|
3334
|
+
const field = hintTarget(normalized, sliceForPath(normalized));
|
|
3335
|
+
return quietContextOutput(
|
|
3336
|
+
field.real ? leadWithCommand(sliceCommand(shown, field), "to extract just the value you need", "You've already read this file.") : fileQueryHint(shown, "You've already read this file.", "field"),
|
|
3337
|
+
[shown]
|
|
3338
|
+
);
|
|
3339
|
+
}
|
|
3340
|
+
const skillName = detectSkillFile(normalized);
|
|
3341
|
+
if (skillName && basename5 === "SKILL.md") {
|
|
3342
|
+
try {
|
|
3343
|
+
const body = fs8.readFileSync(onDisk, "utf-8");
|
|
3344
|
+
const bodySha = contentHash(body);
|
|
3345
|
+
const compact = getCompactAnySessionSync(skillName);
|
|
3346
|
+
const stale = isCompactStale(compact, skillName, bodySha);
|
|
3347
|
+
if (stale === true) {
|
|
3348
|
+
recordActualRead(event, normalized);
|
|
3349
|
+
return quietContextOutput(
|
|
3350
|
+
"This skill's cached compact is stale. Run `token-goat skill-compact " + quotedArg(skillName) + "` to regenerate it."
|
|
3351
|
+
);
|
|
3352
|
+
}
|
|
3353
|
+
} catch {
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
const compactUnrangedRead = !readRequestedSliceWindow(event).isExplicitSlice;
|
|
3357
|
+
if (event.toolName !== "Grep" && compactUnrangedRead && loadConfig().hints.stable_doc_compacts && _isDocFile(normalized)) {
|
|
3358
|
+
const compactPath = compactPathFor(normalized);
|
|
3359
|
+
if (isCompactFresh(compactPath, onDisk)) {
|
|
3360
|
+
const compactBody = readCompactBody(compactPath);
|
|
3361
|
+
if (compactBody !== null) {
|
|
3362
|
+
recordActualRead(event, normalized);
|
|
3363
|
+
const fullSize = statSize(onDisk) ?? 0;
|
|
3364
|
+
const savedBytes = counterfactualCredit(fullSize, compactBody.length);
|
|
3365
|
+
recordStat("session_hint", savedBytes, savedTokensFromBytes(savedBytes), void 0, "stable-doc-compact");
|
|
3366
|
+
return denyOutput(
|
|
3367
|
+
"Serving the extractive compact sidecar in place of the full file (source unchanged since the last `compact-doc` build):\n\n" + fenceUntrustedFileContent(compactBody) + "\n\nThis is a lossy extract, not the whole file: front matter and any prose before the first heading are dropped entirely, each section is cut to its opening sentences, and long code fences are truncated. If you need content it left out, re-read with offset/limit for a line window, or `token-goat section " + quotedArg(shown + "::" + hintTarget(normalized, "section", { placeholder: "Heading" }).name) + "` for one section in full. Use `token-goat compact-doc " + quotedArg(shown) + " --force` to rebuild it, or `token-goat compact-doc " + quotedArg(shown) + " --show` to view it directly. " + editAnywayHint(normalized)
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
}
|
|
3372
|
+
const isNotebook = /\.ipynb$/i.test(basename5);
|
|
3373
|
+
if (event.toolName !== "Grep" && isNotebook) {
|
|
3374
|
+
try {
|
|
3375
|
+
const rawBytes = fs8.readFileSync(onDisk);
|
|
3376
|
+
const [sidecarPath] = getOrCreateSidecar(rawBytes, dataDir());
|
|
3377
|
+
const sidecarContent = fs8.readFileSync(sidecarPath, "utf-8");
|
|
3378
|
+
const savedBytes = rawBytes.length - sidecarContent.length;
|
|
3379
|
+
if (savedBytes >= NB_STRIP_MIN_SAVINGS) {
|
|
3380
|
+
recordActualRead(event, normalized);
|
|
3381
|
+
const nbCredit = counterfactualCredit(rawBytes.length, sidecarContent.length);
|
|
3382
|
+
recordStat("session_hint", nbCredit, savedTokensFromBytes(nbCredit), void 0, "notebook-strip");
|
|
3383
|
+
return denyOutput(
|
|
3384
|
+
"Serving the output-stripped notebook in place of the full file (code-cell outputs and execution counts removed; source and metadata preserved):\n\n" + fenceUntrustedFileContent(sidecarContent) + "\n\n" + editAnywayHint(normalized)
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
} catch {
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
const isMarkdown = /\.(md|mdx|markdown|rst)$/i.test(basename5);
|
|
3391
|
+
if (event.toolName !== "Grep" && isMarkdown) {
|
|
3392
|
+
let fileContent = null;
|
|
3393
|
+
let markdownSize = null;
|
|
3394
|
+
try {
|
|
3395
|
+
const sz = statSize(onDisk);
|
|
3396
|
+
if (sz !== null && sz >= MARKDOWN_SIZE_THRESHOLD) {
|
|
3397
|
+
markdownSize = sz;
|
|
3398
|
+
fileContent = fs8.readFileSync(onDisk, "utf8");
|
|
3399
|
+
}
|
|
3400
|
+
} catch {
|
|
3401
|
+
}
|
|
3402
|
+
if (fileContent !== null) {
|
|
3403
|
+
const headings = extractMarkdownHeadings(fileContent);
|
|
3404
|
+
if (headings.length >= 3) {
|
|
3405
|
+
const alreadyRead = wasFileFullyReadThisSession(normalized);
|
|
3406
|
+
const { guidance, sectionsList } = headingTreeParts(headings, normalized, fileContent);
|
|
3407
|
+
const headingTextsLower = new Set(headings.map((h) => h.text.trim().toLowerCase()));
|
|
3408
|
+
const wellKnown = getWellKnownSections(basename5).filter((s) => headingTextsLower.has(s.trim().toLowerCase()));
|
|
3409
|
+
const wellKnownText = wellKnown.length > 0 ? "\nQuick access: " + wellKnown.map((s) => "token-goat section " + quotedArg(shown + "::" + s)).join(" | ") : "";
|
|
3410
|
+
const changelogExtra = basename5.toLowerCase() === "changelog.md" ? extractChangelogVersionHint(fileContent, normalized) : "";
|
|
3411
|
+
const headingTarget = hintTarget(normalized, "section", { headings, content: fileContent });
|
|
3412
|
+
const treeLead = headingTarget.real ? leadWithCommand("token-goat section " + quotedArg(shown + "::" + headingTarget.name), "to read one section") + "\n" : "";
|
|
3413
|
+
const treeGuidance = headingTarget.real ? guidance.split("\n").filter((line) => !line.includes('::Heading Name"')).join("\n") : guidance;
|
|
3414
|
+
let message = treeLead + treeGuidance + "\n" + fenceUntrustedFileContent(sectionsList + changelogExtra) + wellKnownText;
|
|
3415
|
+
const slice = estimateRequestedSlice(event, normalized);
|
|
3416
|
+
const gateSize = slice.kind === "bytes" && markdownSize !== null ? Math.min(slice.bytes, markdownSize) : markdownSize;
|
|
3417
|
+
const tooLargeForFirstRead = gateSize !== null && gateSize >= largeFileDenyBytes();
|
|
3418
|
+
const config2 = loadConfig();
|
|
3419
|
+
const policyEvidence = !alreadyRead && gateSize !== null && config2.hints.first_read_symbol_policy === "deny" && meetsFirstReadSymbolThreshold(config2.hints.first_read_symbol_policy, config2.hints.first_read_symbol_bytes, gateSize) ? getReadNavigationEvidence(normalized) : null;
|
|
3420
|
+
const policyHeadingCount = policyEvidence !== null && !policyEvidence.isStale ? policyEvidence.headingCount : 0;
|
|
3421
|
+
const firstReadSymbolDeny = policyHeadingCount > 0;
|
|
3422
|
+
if (alreadyRead || tooLargeForFirstRead || firstReadSymbolDeny) {
|
|
3423
|
+
if (firstReadSymbolDeny) {
|
|
3424
|
+
recordStat("session_hint", 0, 0);
|
|
3425
|
+
const safeHeading = headingTarget.real ? headingTarget.name : null;
|
|
3426
|
+
const explanation = `This file is large (${formatKb(markdownSize ?? 0)}KB with ${countNoun(policyHeadingCount, "heading")}). Whole-file first read denied by first_read_symbol_policy. Use ${fencedCommand("token-goat outline " + quotedArg(shown))} to map sections, or re-read with offset/limit for a specific section.`;
|
|
3427
|
+
return denyOutput(
|
|
3428
|
+
(safeHeading ? leadWithCommand("token-goat section " + quotedArg(`${shown}::${safeHeading}`), "to read surgically", explanation) : explanation) + "\n" + editAnywayHint(normalized)
|
|
3429
|
+
);
|
|
3430
|
+
}
|
|
3431
|
+
if (alreadyRead) {
|
|
3432
|
+
const snapDiff = loadSnapshotDiff(sessionStateKey(event), normalized, basename5);
|
|
3433
|
+
if (snapDiff.kind === "unchanged") {
|
|
3434
|
+
recordActualRead(event, normalized);
|
|
3435
|
+
recordStat("session_hint", 0, 0);
|
|
3436
|
+
return denyOutput(docSnapshotDenyText(normalized, basename5, snapDiff));
|
|
3437
|
+
}
|
|
3438
|
+
if (snapDiff.kind === "diff") {
|
|
3439
|
+
const diffBody = docSnapshotDenyText(normalized, basename5, snapDiff);
|
|
3440
|
+
if (diffHintCredit(snapDiff.currentContent.length, diffBody) !== null) {
|
|
3441
|
+
recordActualRead(event, normalized);
|
|
3442
|
+
recordStat("session_hint", 0, 0);
|
|
3443
|
+
return denyOutput(diffBody);
|
|
3444
|
+
}
|
|
3445
|
+
}
|
|
3446
|
+
recordActualRead(event, normalized);
|
|
3447
|
+
recordStat("session_hint", 0, 0);
|
|
3448
|
+
} else {
|
|
3449
|
+
message += " " + editAnywayHint(normalized);
|
|
3450
|
+
}
|
|
3451
|
+
return denyOutput(message);
|
|
3452
|
+
}
|
|
3453
|
+
const unrangedRead = !readRequestedSliceWindow(event).isExplicitSlice;
|
|
3454
|
+
if (loadConfig().hints.subagent_markdown_first_read_deny && event.agentId !== void 0 && unrangedRead && SUBAGENT_MD_FIRST_READ_DENY_EXT_RE.test(basename5) && markdownSize !== null && markdownSize >= SUBAGENT_MD_FIRST_READ_DENY_BYTES) {
|
|
3455
|
+
recordStat("subagent_markdown_first_read_deny", 0, 0);
|
|
3456
|
+
return denyOutput(
|
|
3457
|
+
"Subagent first read of a large markdown file (" + Math.round(markdownSize / 1024) + "KB). Read the section you need, not the whole file.\n\n" + message + " " + editAnywayHint(normalized)
|
|
3458
|
+
);
|
|
3459
|
+
}
|
|
3460
|
+
recordActualRead(event, normalized);
|
|
3461
|
+
return quietContextOutput(message, [normalized]);
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
const isMemoryMd = normalized.toLowerCase().includes("memory/memory.md") || /[/\\]memory[/\\][^/\\]+\.md$/i.test(normalized);
|
|
3466
|
+
if (isMemoryMd && wasFileFullyReadThisSession(normalized)) {
|
|
3467
|
+
const memSnapDiff = loadSnapshotDiff(sessionStateKey(event), normalized, basename5);
|
|
3468
|
+
if (memSnapDiff.kind === "unchanged") {
|
|
3469
|
+
recordActualRead(event, normalized);
|
|
3470
|
+
recordStat("session_hint", 0, 0);
|
|
3471
|
+
return denyOutput(docSnapshotDenyText(normalized, basename5, memSnapDiff));
|
|
3472
|
+
}
|
|
3473
|
+
if (memSnapDiff.kind === "diff") {
|
|
3474
|
+
const diffBody = docSnapshotDenyText(normalized, basename5, memSnapDiff);
|
|
3475
|
+
if (diffHintCredit(memSnapDiff.currentContent.length, diffBody) !== null) {
|
|
3476
|
+
recordActualRead(event, normalized);
|
|
3477
|
+
recordStat("session_hint", 0, 0);
|
|
3478
|
+
return denyOutput(diffBody);
|
|
3479
|
+
}
|
|
3480
|
+
}
|
|
3481
|
+
recordActualRead(event, normalized);
|
|
3482
|
+
recordStat("session_hint", 0, 0);
|
|
3483
|
+
return denyOutput(leadWithCommand(
|
|
3484
|
+
"token-goat section " + quotedArg(shown + "::" + hintTarget(normalized, "section").name),
|
|
3485
|
+
"to extract one section",
|
|
3486
|
+
"This file was already read this session. Memory files rarely change mid-session."
|
|
3487
|
+
));
|
|
3488
|
+
}
|
|
3489
|
+
if (/^\.improve-state-.*\.json$/.test(basename5) && wasFileReadThisSession(normalized)) {
|
|
3490
|
+
recordActualRead(event, normalized);
|
|
3491
|
+
recordStat("session_hint", 0, 0);
|
|
3492
|
+
return denyOutput(
|
|
3493
|
+
"Orchestrator state already read this session. " + sessionArtifactRecall(normalized)
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
if (basename5.toLowerCase() === "content.json" && wasFileReadThisSession(normalized)) {
|
|
3497
|
+
recordActualRead(event, normalized);
|
|
3498
|
+
recordStat("session_hint", 0, 0);
|
|
3499
|
+
const [quotedShown, quotedQuery] = quotedArgs(shown, "<path>");
|
|
3500
|
+
return denyOutput(
|
|
3501
|
+
"This file was already read this session. Tool output spill files should not be re-read whole. Use `token-goat json-query " + quotedShown + " " + quotedQuery + "` or `token-goat mcp-output --file " + quotedShown + " --json-query " + quotedQuery + "` to extract what you need."
|
|
3502
|
+
);
|
|
3503
|
+
}
|
|
3504
|
+
if (/^\.env(\.\w+)?$/.test(basename5) && wasFileReadThisSession(normalized)) {
|
|
3505
|
+
recordActualRead(event, normalized);
|
|
3506
|
+
recordStat("session_hint", 0, 0);
|
|
3507
|
+
return denyOutput(leadWithCommand(
|
|
3508
|
+
configGetCommand(shown, hintTarget(normalized, "key").name),
|
|
3509
|
+
"to extract a specific variable",
|
|
3510
|
+
"This file was already read this session. Environment files rarely change mid-session."
|
|
3511
|
+
));
|
|
3512
|
+
}
|
|
3513
|
+
if (isSessionArtifactFile(normalized)) {
|
|
3514
|
+
if (wasFileFullyReadThisSession(normalized)) {
|
|
3515
|
+
if (wasFileTruncatedThisSession(normalized)) {
|
|
3516
|
+
recordActualRead(event, normalized);
|
|
3517
|
+
recordStat("session_hint", 0, 0);
|
|
3518
|
+
return denyOutput(
|
|
3519
|
+
"File was truncated on last read. " + sessionArtifactRecall(normalized)
|
|
3520
|
+
);
|
|
3521
|
+
}
|
|
3522
|
+
const artifactSessionId = sessionStateKey(event);
|
|
3523
|
+
const snapDiff = loadSnapshotDiff(artifactSessionId, normalized, basename5);
|
|
3524
|
+
if (snapDiff.kind === "unchanged") {
|
|
3525
|
+
recordActualRead(event, normalized);
|
|
3526
|
+
recordStat("session_hint", 0, 0);
|
|
3527
|
+
return denyOutput(snapshotDenyText(normalized, snapDiff, sessionArtifactRecall(normalized)));
|
|
3528
|
+
}
|
|
3529
|
+
if (snapDiff.kind === "diff") {
|
|
3530
|
+
const diffBody = snapshotDenyText(normalized, snapDiff, sessionArtifactRecall(normalized));
|
|
3531
|
+
const artifactDiffCredit = diffHintCredit(snapDiff.currentContent.length, diffBody);
|
|
3532
|
+
if (artifactDiffCredit !== null) {
|
|
3533
|
+
recordActualRead(event, normalized);
|
|
3534
|
+
recordStat("session_hint", artifactDiffCredit, savedTokensFromBytes(artifactDiffCredit), void 0, "artifact-snapshot-diff");
|
|
3535
|
+
return denyOutput(diffBody);
|
|
3536
|
+
}
|
|
3537
|
+
}
|
|
3538
|
+
recordActualRead(event, normalized);
|
|
3539
|
+
recordStat("session_hint", 0, 0);
|
|
3540
|
+
return denyOutput(
|
|
3541
|
+
"This file was already read this session. " + sessionArtifactRecall(normalized)
|
|
3542
|
+
);
|
|
3543
|
+
}
|
|
3544
|
+
{
|
|
3545
|
+
const isTaskOutput = /[/\\]tasks[/\\][a-z0-9]+\.output$/i.test(normalized);
|
|
3546
|
+
const label = isTaskOutput ? "Session transcript" : "Tool-result file";
|
|
3547
|
+
const outputSize = statSize(onDisk);
|
|
3548
|
+
const window = readRequestedSliceWindow(event);
|
|
3549
|
+
const windowBytes = window.limit === void 0 ? void 0 : lineWindowBytes(onDisk, window.offset ?? 1, window.limit);
|
|
3550
|
+
if (outputSize !== null && outputSize >= TASK_OUTPUT_DENY_BYTES && (windowBytes === void 0 || windowBytes >= TASK_OUTPUT_DENY_BYTES)) {
|
|
3551
|
+
const artifactDenyCredit = counterfactualCredit(outputSize);
|
|
3552
|
+
recordStat("session_hint", artifactDenyCredit, savedTokensFromBytes(artifactDenyCredit), void 0, "artifact-large-deny");
|
|
3553
|
+
const windowNote = windowBytes === void 0 || window.limit === void 0 || !Number.isFinite(windowBytes) ? "" : " Lines " + (window.offset ?? 1) + "-" + ((window.offset ?? 1) + window.limit - 1) + " span " + toKB(windowBytes) + "KB.";
|
|
3554
|
+
return denyOutput(
|
|
3555
|
+
label + " is large (" + toKB(outputSize) + "KB)." + windowNote + " " + sessionArtifactRecall(normalized) + " A Read with `offset` and `limit` spanning under " + toKB(TASK_OUTPUT_DENY_BYTES) + "KB goes through."
|
|
3556
|
+
);
|
|
3557
|
+
}
|
|
3558
|
+
recordActualRead(event, normalized);
|
|
3559
|
+
return quietContextOutput(label + ": " + sessionArtifactRecall(normalized));
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
const isDocDiffable = /\.(md|mdx|markdown|rst|txt)$/i.test(basename5);
|
|
3563
|
+
const isSourceDiffable = loadConfig().hints.serve_diff_on_reread && isDiffableSource(basename5);
|
|
3564
|
+
if ((isDocDiffable || isSourceDiffable) && wasFileFullyReadThisSession(normalized) && !isProtectedRecentRead(normalized, loadConfig().hints.protect_recent_reads)) {
|
|
3565
|
+
if (wasFileTruncatedThisSession(normalized)) {
|
|
3566
|
+
if (estimateTruncatedLineCount(normalized) >= loadConfig().hints.truncated_read_min_lines) {
|
|
3567
|
+
recordActualRead(event, normalized);
|
|
3568
|
+
recordStat("session_hint", 0, 0);
|
|
3569
|
+
return denyOutput(truncatedReadDenyMessage(normalized));
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
const sessionId = sessionStateKey(event);
|
|
3573
|
+
const snapDiff = loadSnapshotDiff(sessionId, normalized, basename5);
|
|
3574
|
+
if (snapDiff.kind === "unchanged") {
|
|
3575
|
+
recordActualRead(event, normalized);
|
|
3576
|
+
recordStat("session_hint", 0, 0);
|
|
3577
|
+
return denyOutput(docSnapshotDenyText(normalized, basename5, snapDiff));
|
|
3578
|
+
}
|
|
3579
|
+
if (snapDiff.kind === "diff") {
|
|
3580
|
+
const diffBody = docSnapshotDenyText(normalized, basename5, snapDiff);
|
|
3581
|
+
const diffCredit = diffHintCredit(snapDiff.currentContent.length, diffBody);
|
|
3582
|
+
if (diffCredit !== null) {
|
|
3583
|
+
recordActualRead(event, normalized);
|
|
3584
|
+
recordStat("diff_hint", diffCredit, savedTokensFromBytes(diffCredit));
|
|
3585
|
+
return denyOutput(diffBody);
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
const config = loadConfig();
|
|
3590
|
+
if (config.hints.cross_session_read_dedup && !wasFileReadThisSession(normalized)) {
|
|
3591
|
+
try {
|
|
3592
|
+
const cwd = getCwd(event) ?? process.cwd();
|
|
3593
|
+
let project = findProject(cwd);
|
|
3594
|
+
if (!project) {
|
|
3595
|
+
project = makeProjectAt(cwd);
|
|
3596
|
+
}
|
|
3597
|
+
if (relPathWithinRoot(project.root, normalized) !== null) {
|
|
3598
|
+
const ttlSecs = config.hints.cross_session_read_dedup_ttl_secs;
|
|
3599
|
+
if (scanCrossSessionManifests(project.root, project.hash, normalized, ttlSecs)) {
|
|
3600
|
+
recordActualRead(event, normalized);
|
|
3601
|
+
const slice = hintTarget(normalized, sliceForPath(normalized));
|
|
3602
|
+
return quietContextOutput(
|
|
3603
|
+
"This file may have already been read by another agent/session working in this project recently. If you are a subagent continuing shared work, consider whether you already have this content from context, or use `" + (slice.real ? sliceCommand(shown, slice) : "token-goat read " + quotedArg(shown + "::SymbolName")) + "` for a narrower slice instead of a full re-read.",
|
|
3604
|
+
[shown]
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
}
|
|
3608
|
+
} catch {
|
|
3609
|
+
}
|
|
3610
|
+
}
|
|
3611
|
+
if (event.toolName !== "Grep" && !isImagePath(normalized) && wasFileReadThisSession(normalized)) {
|
|
3612
|
+
const entry = getSessionFileEntry(normalized);
|
|
3613
|
+
const counts = entry !== void 0 ? epochReadCounts(entry) : { reads: 1, fullReads: 0 };
|
|
3614
|
+
const reads = Math.max(1, counts.reads);
|
|
3615
|
+
const plural = reads === 1 ? "read" : "reads";
|
|
3616
|
+
const isSourceExt = isSourceExtension(basename5);
|
|
3617
|
+
const fullReads = counts.fullReads;
|
|
3618
|
+
const protectedRead = isProtectedRecentRead(normalized, loadConfig().hints.protect_recent_reads);
|
|
3619
|
+
recordActualRead(event, normalized);
|
|
3620
|
+
const rereadBytes = statSize(onDisk) ?? 0;
|
|
3621
|
+
const requestedSlice = estimateRequestedSlice(event, normalized);
|
|
3622
|
+
const rereadCreditBasis = requestedSlice.kind === "bytes" ? requestedSlice.bytes : rereadBytes;
|
|
3623
|
+
const rereadCredit = counterfactualCredit(rereadCreditBasis);
|
|
3624
|
+
const config2 = loadConfig();
|
|
3625
|
+
const window = readRequestedSliceWindow(event);
|
|
3626
|
+
const isSmallUnseenSlice = isSmallSlice(requestedSlice, config2.hints.reread_deny_min_bytes, rereadBytes) && isUnseenWindow(window, getFileLineRanges(normalized), fullReads);
|
|
3627
|
+
if (fullReads === 0 && !window.isExplicitSlice && !isDispatchedFileType(normalized) && meetsFirstReadSymbolThreshold(config2.hints.first_read_symbol_policy, config2.hints.first_read_symbol_bytes, rereadBytes)) {
|
|
3628
|
+
const policyContext = buildReadPolicyContext({ event, normalizedPath: normalized, shownPath: shown, fileSize: rereadBytes, hints: config2.hints, window, slice: requestedSlice });
|
|
3629
|
+
const policyOutput = firstReadSymbolOutput(evaluateFirstReadSymbolPolicy(policyContext), policyContext, config2.hints, false);
|
|
3630
|
+
if (policyOutput !== null) return policyOutput;
|
|
3631
|
+
}
|
|
3632
|
+
if (config2.hints.log_large_file_hint_outcomes) {
|
|
3633
|
+
const pendingSize = takePendingLargeFileHint(normalized);
|
|
3634
|
+
if (pendingSize !== null) {
|
|
3635
|
+
recordStat("large_file_hint_ignored", 0, 0, void 0, `${normalized} (${pendingSize} bytes) \u2014 hint fired but file was fully re-read instead of surgically read`);
|
|
3636
|
+
}
|
|
3637
|
+
}
|
|
3638
|
+
const fileRereadDenyKey = "reread-file-deny:" + getCompactedAt() + ":" + foldPath(normalized);
|
|
3639
|
+
const fileAlreadyDenied = wasHintShown(fileRereadDenyKey);
|
|
3640
|
+
if (config2.hints.reread_deny) {
|
|
3641
|
+
const alreadyServed = alreadyServedOutputId(event, normalized);
|
|
3642
|
+
if (alreadyServed !== null && !fileAlreadyDenied) {
|
|
3643
|
+
markHintShown(fileRereadDenyKey);
|
|
3644
|
+
if (rangeSpan !== void 0) lockReadRange(rangeKey, [rangeSpan.start, rangeSpan.end], rangeIdentity);
|
|
3645
|
+
const blocked = counterfactualCredit(alreadyServed.bytes);
|
|
3646
|
+
recordStat("read_served_deny", blocked, savedTokensFromBytes(blocked));
|
|
3647
|
+
recordStat("session_hint", 0, 0);
|
|
3648
|
+
return denyOutput(
|
|
3649
|
+
"Every line of this file this read would return was already served in this session, byte for byte. Recall it with `token-goat bash-output " + alreadyServed.id + "`, or pull just the part you need with " + realSymbolReadHint(normalized, shown) + ". Do not attempt to re-read this file with shifted or different line ranges."
|
|
3650
|
+
);
|
|
3651
|
+
}
|
|
3652
|
+
const prevRanges = getFileLineRanges(normalized);
|
|
3653
|
+
if (window.isExplicitSlice && window.offset !== void 0 && window.limit !== void 0) {
|
|
3654
|
+
const start = window.offset;
|
|
3655
|
+
const end = window.offset + window.limit - 1;
|
|
3656
|
+
const containing = prevRanges.find(([s, e]) => s <= start && e >= end);
|
|
3657
|
+
if (containing !== void 0) {
|
|
3658
|
+
const snapDiff = loadSnapshotDiff(sessionStateKey(event), normalized, basename5);
|
|
3659
|
+
const denyRangeReread = () => {
|
|
3660
|
+
markHintShown(fileRereadDenyKey);
|
|
3661
|
+
lockReadRange(rangeKey, [start, end], rangeIdentity);
|
|
3662
|
+
const blocked = counterfactualCredit(rereadCreditBasis);
|
|
3663
|
+
recordStat("read_served_deny", blocked, savedTokensFromBytes(blocked));
|
|
3664
|
+
recordStat("session_hint", 0, 0);
|
|
3665
|
+
const [cStart, cEnd] = containing;
|
|
3666
|
+
const rangeNote = cStart === start && cEnd === end ? "Lines " + start + ".." + end + " of this file were already read this session. " : "Lines " + start + ".." + end + " of this file were already read this session (served within lines " + cStart + ".." + cEnd + "). ";
|
|
3667
|
+
return denyOutput(
|
|
3668
|
+
rangeNote + "Pull just the part you need with " + realSymbolReadHint(normalized, shown, { start, end }) + "." + priorOutputRecallHint(normalized) + " Do not attempt to re-read this file with shifted or different line ranges."
|
|
3669
|
+
);
|
|
3670
|
+
};
|
|
3671
|
+
if (snapDiff.kind === "unchanged") {
|
|
3672
|
+
return denyRangeReread();
|
|
3673
|
+
}
|
|
3674
|
+
if (snapDiff.kind === "none") {
|
|
3675
|
+
const recordedIdentity = getSessionFileEntry(normalized)?.rangeFileIdentity;
|
|
3676
|
+
let currentIdentity;
|
|
3677
|
+
try {
|
|
3678
|
+
const st = fs8.statSync(onDisk);
|
|
3679
|
+
currentIdentity = { size: st.size, mtimeMs: st.mtimeMs };
|
|
3680
|
+
} catch {
|
|
3681
|
+
currentIdentity = void 0;
|
|
3682
|
+
}
|
|
3683
|
+
if (recordedIdentity === void 0 || currentIdentity !== void 0 && recordedIdentity.size === currentIdentity.size && recordedIdentity.mtimeMs === currentIdentity.mtimeMs) {
|
|
3684
|
+
return denyRangeReread();
|
|
3685
|
+
}
|
|
3686
|
+
resetFileLineRanges(normalized);
|
|
3687
|
+
} else {
|
|
3688
|
+
resetFileLineRanges(normalized);
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
const isPagingTracked = isSourceExt || /\.(md|mdx|markdown|rst|xml|dtsx|ampkg|xaml)$/i.test(basename5);
|
|
3693
|
+
const pagingWindowSpan = requestedSliceSpan(window);
|
|
3694
|
+
const isSequentialChain = pagingWindowSpan !== void 0 && isSequentialPagingChain([...prevRanges, [pagingWindowSpan.start, pagingWindowSpan.end]], 4);
|
|
3695
|
+
if (isPagingTracked && window.isExplicitSlice && prevRanges.length >= 3 && isSequentialChain && pagingWindowSpan !== void 0) {
|
|
3696
|
+
lockReadRange(rangeKey, [pagingWindowSpan.start, pagingWindowSpan.end], rangeIdentity);
|
|
3697
|
+
recordStat("read_count_deny", rereadCredit, savedTokensFromBytes(rereadCredit));
|
|
3698
|
+
recordStat("session_hint", 0, 0);
|
|
3699
|
+
const hint2 = "For lines " + pagingWindowSpan.start + ".." + pagingWindowSpan.end + ", run `token-goat read " + quotedArg(shown + "@" + pagingWindowSpan.start + "-" + pagingWindowSpan.end) + "`; `token-goat skeleton " + quotedArg(shown) + "` maps the rest." + (isSourceExt ? "" : " " + surgicalHint(normalized, basename5, lineCountForSurgicalHint(normalized)));
|
|
3700
|
+
return denyOutput(
|
|
3701
|
+
"Sequential line-range paging detected on " + readSubject(normalized, hint2) + " (" + (prevRanges.length + 1) + " slices read). " + hint2 + " Inspect structure directly without manual chunk paging. Further Read/view line ranges of this file will be denied."
|
|
3702
|
+
);
|
|
3703
|
+
}
|
|
3704
|
+
if (rangeSpan !== void 0 && evaluateRangeEvasion({
|
|
3705
|
+
span: [rangeSpan.start, rangeSpan.end],
|
|
3706
|
+
served: getRecentReadRanges(rangeKey),
|
|
3707
|
+
denied: getReadRangeDenials(rangeKey)
|
|
3708
|
+
}) !== null) {
|
|
3709
|
+
lockReadRange(rangeKey, [rangeSpan.start, rangeSpan.end], rangeIdentity);
|
|
3710
|
+
recordStat("session_hint", 0, 0);
|
|
3711
|
+
return denyOutput(rangeLockMessage(shown, [rangeSpan.start, rangeSpan.end]));
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
const bookDenyIdentity = (branch) => {
|
|
3715
|
+
const { identity, basis } = rereadIdentity(sessionStateKey(event), normalized, entry);
|
|
3716
|
+
recordStat("reread_deny_identity", 0, 0, void 0, `branch=${branch} identity=${identity} basis=${basis} edited=${entry?.wasEdited === true ? 1 : 0}`);
|
|
3717
|
+
};
|
|
3718
|
+
if (config2.hints.reread_deny && !protectedRead) {
|
|
3719
|
+
if (wasFileTruncatedThisSession(normalized)) {
|
|
3720
|
+
if (estimateTruncatedLineCount(normalized) >= config2.hints.truncated_read_min_lines) {
|
|
3721
|
+
recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit), void 0, "reread-truncated-deny");
|
|
3722
|
+
bookDenyIdentity("truncated");
|
|
3723
|
+
return denyOutput(truncatedReadDenyMessage(normalized));
|
|
3724
|
+
}
|
|
3725
|
+
}
|
|
3726
|
+
if (!window.isExplicitSlice && fullReads >= 1 && /\.(md|mdx|markdown|rst)$/i.test(basename5)) {
|
|
3727
|
+
recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit), void 0, "reread-doc-deny");
|
|
3728
|
+
bookDenyIdentity("doc");
|
|
3729
|
+
return denyOutput(leadWithCommand(
|
|
3730
|
+
sectionOrRangeCommand(shown, hintTarget(normalized, "section")),
|
|
3731
|
+
"to read one section",
|
|
3732
|
+
"Markdown file already read this session." + priorOutputRecallHint(normalized)
|
|
3733
|
+
));
|
|
3734
|
+
}
|
|
3735
|
+
if (!window.isExplicitSlice && fullReads >= 1 && /\.(json|jsonc|html|htm)$/i.test(basename5) && rereadBytes >= 8192) {
|
|
3736
|
+
const hint2 = surgicalHint(normalized, basename5, lineCountForSurgicalHint(normalized, rereadBytes));
|
|
3737
|
+
recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit), void 0, "reread-structured-deny");
|
|
3738
|
+
bookDenyIdentity("structured");
|
|
3739
|
+
return denyOutput((hint2 + " " + sentenceStart(readSubject(normalized, hint2)) + " was already read this session (" + reads + " " + plural + ")." + priorOutputRecallHint(normalized)).trimStart());
|
|
3740
|
+
}
|
|
3741
|
+
if (isSourceExt && fullReads >= 2 && !fileAlreadyDenied) {
|
|
3742
|
+
markHintShown(fileRereadDenyKey);
|
|
3743
|
+
recordStat("read_count_deny", rereadCredit, savedTokensFromBytes(rereadCredit));
|
|
3744
|
+
recordStat("session_hint", 0, 0);
|
|
3745
|
+
bookDenyIdentity("source-count");
|
|
3746
|
+
return denyOutput((surgicalHint(normalized, basename5, lineCountForSurgicalHint(normalized)) + " Tried to read this file " + reads + " times already." + priorOutputRecallHint(normalized) + " Do not attempt to re-read this file with shifted or different line ranges.").trimStart());
|
|
3747
|
+
}
|
|
3748
|
+
}
|
|
3749
|
+
const hint = surgicalHint(normalized, basename5, lineCountForSurgicalHint(normalized));
|
|
3750
|
+
if (config2.hints.reread_deny && !protectedRead && !isSmallUnseenSlice && (fullReads >= 1 && rereadBytes >= config2.hints.reread_deny_min_bytes || fullReads >= 2) && !fileAlreadyDenied) {
|
|
3751
|
+
markHintShown(fileRereadDenyKey);
|
|
3752
|
+
recordStat("session_hint", rereadCredit, savedTokensFromBytes(rereadCredit), void 0, "reread-count-deny");
|
|
3753
|
+
bookDenyIdentity("count");
|
|
3754
|
+
return denyOutput((hint + " " + sentenceStart(readSubject(normalized, hint)) + " was already read this session (" + reads + " " + plural + ")." + priorOutputRecallHint(normalized) + " Do not attempt to re-read this file with shifted or different line ranges.").trimStart());
|
|
3755
|
+
}
|
|
3756
|
+
const pagingWindow = readRequestedSliceWindow(event);
|
|
3757
|
+
const activeRanges = getFileLineRanges(normalized);
|
|
3758
|
+
const activeSpan = requestedSliceSpan(pagingWindow);
|
|
3759
|
+
const isSequentialNote = activeSpan !== void 0 && isSequentialPagingChain([...activeRanges, [activeSpan.start, activeSpan.end]], 3);
|
|
3760
|
+
const pagingNote = pagingWindow.isExplicitSlice && activeRanges.length >= 2 && isSequentialNote ? " Sequential line-range paging detected (" + (activeRanges.length + 1) + " slices read). Prefer `token-goat skeleton " + quotedArg(shown) + "` or " + realSymbolReadHint(normalized, shown, activeSpan) + "." : "";
|
|
3761
|
+
const windowIsNew = isUnseenWindow(pagingWindow, activeRanges, fullReads);
|
|
3762
|
+
if (windowIsNew) {
|
|
3763
|
+
recordActualSlice(event, normalized);
|
|
3764
|
+
if (pagingNote === "") return passOutput();
|
|
3765
|
+
if (!isWithinQuietHours(config2.hints.quiet_hours)) {
|
|
3766
|
+
recordStat("session_hint", 0, 0);
|
|
3767
|
+
}
|
|
3768
|
+
return quietContextOutput(pagingNote.trimStart(), [shown]);
|
|
3769
|
+
}
|
|
3770
|
+
if (fullReads !== 0 || pagingWindow.isExplicitSlice) {
|
|
3771
|
+
if (!isWithinQuietHours(config2.hints.quiet_hours)) {
|
|
3772
|
+
recordStat("session_hint", 0, 0);
|
|
3773
|
+
}
|
|
3774
|
+
recordActualSlice(event, normalized);
|
|
3775
|
+
const sectionCommand = _isDocFile(normalized) ? "token-goat section " + quotedArg(shown + "::" + hintTarget(normalized, "section", { placeholder: "SectionName" }).name) : "";
|
|
3776
|
+
const rereadNote = "Note: " + readSubject(normalized, sectionCommand + pagingNote) + " was already read this session (" + reads + " " + plural + ").";
|
|
3777
|
+
return quietContextOutput(
|
|
3778
|
+
(sectionCommand !== "" ? leadWithCommand(sectionCommand, "to read one section", rereadNote) : rereadNote + " Use token-goat read/section/symbol to re-read surgically.") + pagingNote,
|
|
3779
|
+
[shown]
|
|
3780
|
+
);
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
const size = statSize(onDisk);
|
|
3784
|
+
if (event.toolName !== "Grep" && size !== null && !wasFileReadThisSession(normalized) && !isImagePath(normalized) && !isDispatchedFileType(normalized)) {
|
|
3785
|
+
const hints = loadConfig().hints;
|
|
3786
|
+
const policyContext = buildReadPolicyContext({ event, normalizedPath: normalized, shownPath: shown, fileSize: size, hints });
|
|
3787
|
+
const policyOutput = firstReadSymbolOutput(evaluateFirstReadSymbolPolicy(policyContext), policyContext, hints, true);
|
|
3788
|
+
if (policyOutput !== null) return policyOutput;
|
|
3789
|
+
}
|
|
3790
|
+
if (event.toolName !== "Grep" && size !== null && size >= LARGE_FILE_BYTES && !isImagePath(normalized) && !isDispatchedFileType(normalized)) {
|
|
3791
|
+
const slice = estimateRequestedSlice(event, normalized);
|
|
3792
|
+
const gateSize = slice.kind === "bytes" ? Math.min(slice.bytes, size) : size;
|
|
3793
|
+
if (gateSize < LARGE_FILE_BYTES) {
|
|
3794
|
+
recordActualRead(event, normalized);
|
|
3795
|
+
recordActualSlice(event, normalized);
|
|
3796
|
+
return passOutput();
|
|
3797
|
+
}
|
|
3798
|
+
const kb = toKB(size);
|
|
3799
|
+
const config2 = loadConfig();
|
|
3800
|
+
const hint = surgicalHint(normalized, basename5, 1e3);
|
|
3801
|
+
if (gateSize >= largeFileDenyBytes()) {
|
|
3802
|
+
const denyCredit = counterfactualCredit(size);
|
|
3803
|
+
recordStat("session_hint", denyCredit, savedTokensFromBytes(denyCredit), void 0, "large-file-deny");
|
|
3804
|
+
const advice = describeSliceAdvice(slice, normalized) + " " + editAnywayHint(normalized);
|
|
3805
|
+
return denyOutput((hint + " " + sentenceStart(readSubject(normalized, hint + advice)) + " is very large (" + kb + "KB). ").trimStart() + advice);
|
|
3806
|
+
}
|
|
3807
|
+
recordActualRead(event, normalized);
|
|
3808
|
+
recordActualSlice(event, normalized);
|
|
3809
|
+
if (!meetsSavingsFloor(size)) {
|
|
3810
|
+
logSuppressedDetection("read_structural_nav", event.sessionId, normalized);
|
|
3811
|
+
return passOutput();
|
|
3812
|
+
}
|
|
3813
|
+
if (config2.hints.log_large_file_hint_outcomes) {
|
|
3814
|
+
recordLargeFileHintPending(normalized, size);
|
|
3815
|
+
}
|
|
3816
|
+
if (!isWithinQuietHours(config2.hints.quiet_hours)) {
|
|
3817
|
+
recordStat("session_hint", 0, 0);
|
|
3818
|
+
}
|
|
3819
|
+
return quietContextOutput((hint + " " + sentenceStart(readSubject(normalized, hint)) + " is large (" + kb + "KB).").trimStart() + contextPressureAdvisorySuffix(), [shown]);
|
|
3820
|
+
}
|
|
3821
|
+
const fileTypeExt = path6.extname(normalized).slice(1).toLowerCase();
|
|
3822
|
+
const fileStatSize = size ?? statSize(onDisk) ?? 0;
|
|
3823
|
+
const isKnownFileType = DISPATCHED_FILE_TYPE_EXTS.has(fileTypeExt);
|
|
3824
|
+
if (event.toolName !== "Grep" && !isImagePath(normalized) && (isKnownFileType || fileStatSize >= FILE_TYPE_THRESHOLDS.generic)) {
|
|
3825
|
+
const ftSlice = estimateRequestedSlice(event, normalized);
|
|
3826
|
+
const ftEffectiveLength = ftSlice.kind === "bytes" ? Math.min(ftSlice.bytes, fileStatSize) : fileStatSize;
|
|
3827
|
+
let ftContent = "";
|
|
3828
|
+
if (!BINARY_FILE_TYPE_EXTS.has(fileTypeExt) && fileStatSize <= SLICE_ESTIMATE_SCAN_CAP_BYTES) {
|
|
3829
|
+
try {
|
|
3830
|
+
ftContent = fs8.readFileSync(onDisk, "utf8");
|
|
3831
|
+
} catch {
|
|
3832
|
+
}
|
|
3833
|
+
}
|
|
3834
|
+
const ftResult = dispatchFileTypeHandler(normalized, ftContent, ftEffectiveLength);
|
|
3835
|
+
if (ftResult?.shouldBlock) {
|
|
3836
|
+
return denyOutput(ftResult.message);
|
|
3837
|
+
}
|
|
3838
|
+
}
|
|
3839
|
+
const isXmlNudge = /\.(xml|dtsx|ampkg|xaml)$/i.test(basename5);
|
|
3840
|
+
const isDocNudge = /\.(md|mdx|markdown)$/i.test(basename5);
|
|
3841
|
+
const isScriptNudge = /\.(ps1|psm1)$/i.test(basename5);
|
|
3842
|
+
if (event.toolName !== "Grep" && !BINARY_FILE_TYPE_EXTS.has(fileTypeExt) && (!isKnownFileType || isXmlNudge)) {
|
|
3843
|
+
const reqWindow = readRequestedSliceWindow(event);
|
|
3844
|
+
const lineCount = lineCountForSurgicalHint(normalized, fileStatSize);
|
|
3845
|
+
const isSubstantial = fileStatSize >= 5 * 1024 || lineCount >= 50;
|
|
3846
|
+
const isSpanningOver80 = !reqWindow.isExplicitSlice || reqWindow.limit !== void 0 && lineCount > 0 && reqWindow.limit / lineCount >= 0.8;
|
|
3847
|
+
let isIndexedFile = false;
|
|
3848
|
+
if (isSpanningOver80 && isSubstantial) {
|
|
3849
|
+
try {
|
|
3850
|
+
isIndexedFile = getFileEntry(normalized) !== null || filePath !== void 0 && getFileEntry(filePath) !== null;
|
|
3851
|
+
} catch {
|
|
3852
|
+
isIndexedFile = false;
|
|
3853
|
+
}
|
|
3854
|
+
}
|
|
3855
|
+
let isDocEligible = isDocNudge && (reqWindow.isExplicitSlice || lineCount >= 50);
|
|
3856
|
+
if (isDocEligible && !reqWindow.isExplicitSlice) {
|
|
3857
|
+
try {
|
|
3858
|
+
let content = "";
|
|
3859
|
+
if (fileStatSize <= SLICE_ESTIMATE_SCAN_CAP_BYTES) {
|
|
3860
|
+
content = fs8.readFileSync(onDisk, "utf8");
|
|
3861
|
+
}
|
|
3862
|
+
const headings = extractMarkdownHeadings(content);
|
|
3863
|
+
if (headings.length < 3) {
|
|
3864
|
+
isDocEligible = false;
|
|
3865
|
+
}
|
|
3866
|
+
} catch {
|
|
3867
|
+
}
|
|
3868
|
+
}
|
|
3869
|
+
const shouldNudge = (isXmlNudge || isDocEligible || isScriptNudge || isIndexedFile) && !isWithinQuietHours(config.hints.quiet_hours) && (reqWindow.isExplicitSlice || isSubstantial);
|
|
3870
|
+
if (shouldNudge) {
|
|
3871
|
+
recordActualRead(event, normalized);
|
|
3872
|
+
recordActualSlice(event, normalized);
|
|
3873
|
+
recordStat("session_hint", 0, 0);
|
|
3874
|
+
const isTestFile = /\.(tests|test)\.(ps1|[jt]sx?|py)$/i.test(basename5);
|
|
3875
|
+
const nudge = isXmlNudge ? `Note: token-goat available for this file type, consider xml-query/xml-outline first: \`token-goat xml-outline ${quotedArg(shown)}\` or \`token-goat xml-query ${quotedArgs(shown, "<selector>").join(" ")}\`` : isDocNudge ? `Note: token-goat available for this file type, consider section first: \`${sectionOrRangeCommand(shown, hintTarget(normalized, "section"))}\`` : isTestFile ? `Note: token-goat available for this test file, consider surgical read first: \`token-goat read ${quotedArg(`${shown}::${hintTarget(normalized, "symbol", { placeholder: "DescribeBlockName" }).name}`)}\` or \`token-goat skeleton ${quotedArg(shown)}\`` : isScriptNudge ? `Note: token-goat available for this PowerShell file, consider surgical read first: \`token-goat read ${quotedArg(`${shown}::${hintTarget(normalized, "symbol", { placeholder: "FunctionName" }).name}`)}\` or \`token-goat skeleton ${quotedArg(shown)}\`` : `Note: token-goat has this file indexed (>80% read), consider surgical read first: ${surgicalHint(normalized, basename5, lineCount)}`;
|
|
3876
|
+
return quietContextOutput(nudge, isXmlNudge ? void 0 : [shown]);
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
recordActualRead(event, normalized);
|
|
3880
|
+
recordActualSlice(event, normalized);
|
|
3881
|
+
return passOutput();
|
|
3882
|
+
}
|
|
3883
|
+
function preReadHandler(event) {
|
|
3884
|
+
const rawGrepPath = event.toolName === "Grep" ? event.toolInput["path"] : void 0;
|
|
3885
|
+
const target = getFilePath(event) ?? (typeof rawGrepPath === "string" && rawGrepPath !== "" ? resolveEventPath(event, rawGrepPath) : void 0);
|
|
3886
|
+
if (preToolPathDeclined(event, target)) return passOutput();
|
|
3887
|
+
const before = event.toolName === "Read" && target !== void 0 ? getSessionFileEntry(target) : void 0;
|
|
3888
|
+
const output = preReadHandlerInner(event);
|
|
3889
|
+
if (output.hookType !== "pass" && target !== void 0 && readHintCrossesRule(detectHarness(), getCwd(event) ?? process.cwd(), [target])) return passOutput();
|
|
3890
|
+
if (output.hookType === "deny" && event.toolName === "Read" && target !== void 0 && !DELIVERS_CONTENT_RE.test(output.message) && !output.message.includes(SNAPSHOT_DIFF_LEAD)) {
|
|
3891
|
+
unrecordRefusedRead(target, before);
|
|
3892
|
+
}
|
|
3893
|
+
return applyHintTracking(event, output, classifyReadHint);
|
|
3894
|
+
}
|
|
3895
|
+
registerHook("pre_tool_use", preReadHandler, { toolName: "Read" });
|
|
3896
|
+
registerHook("pre_tool_use", preReadHandler, { toolName: "Grep" });
|
|
3897
|
+
|
|
3898
|
+
export {
|
|
3899
|
+
indexedSourceText,
|
|
3900
|
+
isVirtualIndexedPath,
|
|
3901
|
+
virtualIndexedScopeNote,
|
|
3902
|
+
NOTEBOOK_CELL_LINES_SUFFIX,
|
|
3903
|
+
formatSymbolLocation,
|
|
3904
|
+
indexMatchesDisk,
|
|
3905
|
+
agentTypeOf,
|
|
3906
|
+
namesRmOrProtectedPath,
|
|
3907
|
+
isSimpleCommand,
|
|
3908
|
+
shellPathWords,
|
|
3909
|
+
primeManagedPolicyAhead,
|
|
3910
|
+
loadCodexRules,
|
|
3911
|
+
primeHiddenRulesAhead,
|
|
3912
|
+
loadingHiddenRuleCheck,
|
|
3913
|
+
permissionNeutralRewrite,
|
|
3914
|
+
readHintCrossesRule,
|
|
3915
|
+
LARGE_SYMBOL_LINE_THRESHOLD,
|
|
3916
|
+
visionTokens,
|
|
3917
|
+
visionTokensSaved,
|
|
3918
|
+
visionTokensSavedByText,
|
|
3919
|
+
canShrinkFormat,
|
|
3920
|
+
decoderRefusal,
|
|
3921
|
+
formatShrinkSummary,
|
|
3922
|
+
ImageDecodeError,
|
|
3923
|
+
probeImageMeta,
|
|
3924
|
+
imageQualifiesForShrink,
|
|
3925
|
+
shrinkImage,
|
|
3926
|
+
compactPathFor,
|
|
3927
|
+
isCompactFresh,
|
|
3928
|
+
markCompactStale,
|
|
3929
|
+
readCompactBody,
|
|
3930
|
+
writeCompact,
|
|
3931
|
+
buildExtractiveCompact,
|
|
3932
|
+
compactDoc,
|
|
3933
|
+
relPathWithinRoot,
|
|
3934
|
+
isSessionArtifactFile,
|
|
3935
|
+
isDiffableSource,
|
|
3936
|
+
isSourceExtension,
|
|
3937
|
+
recordActualSlice,
|
|
3938
|
+
quietContextOutput,
|
|
3939
|
+
sliceForPath,
|
|
3940
|
+
headingTreeParts,
|
|
3941
|
+
hintTarget,
|
|
3942
|
+
sliceCommand,
|
|
3943
|
+
sectionOrRangeCommand,
|
|
3944
|
+
sharpenRepeatedDeny,
|
|
3945
|
+
store,
|
|
3946
|
+
cleanup_stale,
|
|
3947
|
+
readRequestedSliceWindow,
|
|
3948
|
+
readStartLine,
|
|
3949
|
+
harnessNumbersReadContent,
|
|
3950
|
+
numberedRenderBytes,
|
|
3951
|
+
isTruncatedReadDelivery,
|
|
3952
|
+
SLICE_ESTIMATE_SCAN_CAP_BYTES,
|
|
3953
|
+
buildLineDiff,
|
|
3954
|
+
countTextLines,
|
|
3955
|
+
readWindowFromDisk,
|
|
3956
|
+
parseReadDelivery
|
|
3957
|
+
};
|