token-goat 2.9.30 → 2.9.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/README.md +24 -7
  2. package/SECURITY.md +10 -2
  3. package/THIRD_PARTY_NOTICES.md +1 -1
  4. package/dist/native/linux-arm64/tg-hook +0 -0
  5. package/dist/native/linux-x64/tg-hook +0 -0
  6. package/dist/native/win32-arm64/tg-hook.exe +0 -0
  7. package/dist/native/win32-x64/tg-hook.exe +0 -0
  8. package/dist/{token-goat-chunk-QSBRTGM2.mjs → token-goat-chunk-25TRAWET.mjs} +17 -7
  9. package/dist/token-goat-chunk-2KXO6V2B.mjs +203 -0
  10. package/dist/{token-goat-chunk-IVKIZWAB.mjs → token-goat-chunk-2M7E4PF7.mjs} +41 -12
  11. package/dist/{token-goat-chunk-SMVRN2HU.mjs → token-goat-chunk-2UPZGKKY.mjs} +2 -2
  12. package/dist/token-goat-chunk-2WLPWKHV.mjs +135 -0
  13. package/dist/token-goat-chunk-2XBMMDBD.mjs +126 -0
  14. package/dist/token-goat-chunk-32ZOY4N5.mjs +23 -0
  15. package/dist/token-goat-chunk-3EZRNKTK.mjs +188 -0
  16. package/dist/token-goat-chunk-4ADZHI7J.mjs +63 -0
  17. package/dist/token-goat-chunk-4GBVPH3D.mjs +242 -0
  18. package/dist/{token-goat-chunk-KWPNJBDH.mjs → token-goat-chunk-4GILLLWY.mjs} +7 -4
  19. package/dist/{token-goat-chunk-67TQJ76B.mjs → token-goat-chunk-4NAPX7BA.mjs} +36 -3
  20. package/dist/{token-goat-chunk-PU7V3FD6.mjs → token-goat-chunk-4SC7UJYV.mjs} +3354 -2553
  21. package/dist/token-goat-chunk-4SMRO4TO.mjs +366 -0
  22. package/dist/{token-goat-chunk-AFLYLHDH.mjs → token-goat-chunk-4ZBFWLXF.mjs} +283 -427
  23. package/dist/{token-goat-chunk-JDWCRD6M.mjs → token-goat-chunk-57BIAXJL.mjs} +73 -40
  24. package/dist/token-goat-chunk-5AC66HNP.mjs +115 -0
  25. package/dist/token-goat-chunk-5IMLKSOP.mjs +145 -0
  26. package/dist/token-goat-chunk-646E6U44.mjs +398 -0
  27. package/dist/token-goat-chunk-65N4RHP4.mjs +33 -0
  28. package/dist/{token-goat-chunk-JN4XMLSD.mjs → token-goat-chunk-6DG6HWP3.mjs} +8 -2
  29. package/dist/token-goat-chunk-6ENQSNRZ.mjs +573 -0
  30. package/dist/token-goat-chunk-6ZY4GNF2.mjs +91 -0
  31. package/dist/{token-goat-chunk-VPBBTSJR.mjs → token-goat-chunk-76FGDLRO.mjs} +262 -255
  32. package/dist/{token-goat-chunk-7IN2MKDS.mjs → token-goat-chunk-7EZTZOMC.mjs} +121 -289
  33. package/dist/{token-goat-chunk-OJBNYTJP.mjs → token-goat-chunk-7MAOA6X4.mjs} +640 -80
  34. package/dist/token-goat-chunk-7R2ZVVAX.mjs +218 -0
  35. package/dist/{token-goat-chunk-PD5VKH46.mjs → token-goat-chunk-AUNGYYQX.mjs} +2 -2
  36. package/dist/{token-goat-chunk-HG45AF2B.mjs → token-goat-chunk-AUQYRMMH.mjs} +794 -37
  37. package/dist/token-goat-chunk-B3PEKBYG.mjs +128 -0
  38. package/dist/{token-goat-chunk-A3CK6N5Z.mjs → token-goat-chunk-BA4HI6BW.mjs} +1309 -110
  39. package/dist/{token-goat-chunk-JB6QN6VF.mjs → token-goat-chunk-BFCWY73P.mjs} +59 -23
  40. package/dist/{token-goat-chunk-B45VMIIH.mjs → token-goat-chunk-BWGFUZZE.mjs} +7 -5
  41. package/dist/{token-goat-chunk-P4QMM4WK.mjs → token-goat-chunk-C75NA7PL.mjs} +50 -23
  42. package/dist/token-goat-chunk-CBIQ7WTD.mjs +87 -0
  43. package/dist/token-goat-chunk-CETM74KC.mjs +307 -0
  44. package/dist/{token-goat-chunk-H2PHHGM5.mjs → token-goat-chunk-CIMQ2BB3.mjs} +57 -12
  45. package/dist/{token-goat-chunk-N2NMER56.mjs → token-goat-chunk-CJLS3CDL.mjs} +46 -4
  46. package/dist/{token-goat-chunk-VES7EL7O.mjs → token-goat-chunk-CO3UYX6G.mjs} +1 -1
  47. package/dist/token-goat-chunk-COPNXRJM.mjs +154 -0
  48. package/dist/token-goat-chunk-CSJ5L3NL.mjs +479 -0
  49. package/dist/{token-goat-chunk-4LCCNRTG.mjs → token-goat-chunk-CVCEVUGK.mjs} +8 -6
  50. package/dist/{token-goat-chunk-B2RZZCSS.mjs → token-goat-chunk-CXHOFN7R.mjs} +37 -20
  51. package/dist/token-goat-chunk-D735C4QM.mjs +603 -0
  52. package/dist/{token-goat-chunk-APGZXSPS.mjs → token-goat-chunk-DGS2LVNA.mjs} +47 -30
  53. package/dist/{token-goat-chunk-54K3EUV3.mjs → token-goat-chunk-DOWRSMWO.mjs} +1 -1
  54. package/dist/token-goat-chunk-E45INMY3.mjs +71 -0
  55. package/dist/token-goat-chunk-EC5F37F4.mjs +244 -0
  56. package/dist/{token-goat-chunk-XTQAOTSO.mjs → token-goat-chunk-EDBDRVH5.mjs} +4 -1
  57. package/dist/{token-goat-chunk-5BCIJXMO.mjs → token-goat-chunk-EELTBKDM.mjs} +14 -10
  58. package/dist/{token-goat-chunk-W3QCJS6G.mjs → token-goat-chunk-F4HYL64R.mjs} +8 -6
  59. package/dist/token-goat-chunk-F6PAUIEK.mjs +624 -0
  60. package/dist/token-goat-chunk-FDSREWLX.mjs +692 -0
  61. package/dist/token-goat-chunk-GGGG7RWB.mjs +236 -0
  62. package/dist/{token-goat-chunk-C63JQV5K.mjs → token-goat-chunk-GL3ETKSC.mjs} +233 -15
  63. package/dist/token-goat-chunk-GVB2MUST.mjs +1298 -0
  64. package/dist/token-goat-chunk-GWFLAUMQ.mjs +307 -0
  65. package/dist/{token-goat-chunk-ZEJVMUQH.mjs → token-goat-chunk-HLPW63A7.mjs} +2 -2
  66. package/dist/{token-goat-chunk-GOLEOXZP.mjs → token-goat-chunk-HU4TORC5.mjs} +44 -26
  67. package/dist/{token-goat-chunk-Z57YODA2.mjs → token-goat-chunk-I7VKZZAW.mjs} +2 -2
  68. package/dist/{token-goat-chunk-NYKZLXMK.mjs → token-goat-chunk-IA6XSPE3.mjs} +6 -6
  69. package/dist/{token-goat-chunk-E3OQX2XT.mjs → token-goat-chunk-IAGXMHIK.mjs} +81 -42
  70. package/dist/{token-goat-chunk-HYBFA5X3.mjs → token-goat-chunk-IK2DRMFX.mjs} +24 -68
  71. package/dist/{token-goat-chunk-COBQGL5S.mjs → token-goat-chunk-INNPTRUX.mjs} +249 -299
  72. package/dist/{token-goat-chunk-7NADEDKJ.mjs → token-goat-chunk-JJ5BX4F4.mjs} +6 -1
  73. package/dist/{token-goat-chunk-PEPTHRFF.mjs → token-goat-chunk-JVFSHVFJ.mjs} +6 -6
  74. package/dist/token-goat-chunk-KGEFFSHD.mjs +50 -0
  75. package/dist/token-goat-chunk-KQBLRPBN.mjs +433 -0
  76. package/dist/{token-goat-chunk-Q5VTGRH7.mjs → token-goat-chunk-LCNQ2CNN.mjs} +55 -319
  77. package/dist/token-goat-chunk-M3LFJDPU.mjs +58 -0
  78. package/dist/token-goat-chunk-M52KPT3C.mjs +87 -0
  79. package/dist/token-goat-chunk-MBEG42H7.mjs +150 -0
  80. package/dist/{token-goat-chunk-EVC4TOLE.mjs → token-goat-chunk-METECDOH.mjs} +3 -12
  81. package/dist/{token-goat-chunk-T4FK7VOV.mjs → token-goat-chunk-MHX7XCC3.mjs} +2 -1
  82. package/dist/token-goat-chunk-N5LSOVSU.mjs +137 -0
  83. package/dist/{token-goat-chunk-LOBGHXPB.mjs → token-goat-chunk-N5MMR2IW.mjs} +1 -1
  84. package/dist/token-goat-chunk-N7B3JQ2C.mjs +160 -0
  85. package/dist/{token-goat-chunk-RHXKCZX3.mjs → token-goat-chunk-NISNMD7T.mjs} +2182 -2959
  86. package/dist/token-goat-chunk-NMD42SFQ.mjs +434 -0
  87. package/dist/token-goat-chunk-NOSS5VWY.mjs +174 -0
  88. package/dist/{token-goat-chunk-KQNZDCBD.mjs → token-goat-chunk-NSNSPPBG.mjs} +25 -3
  89. package/dist/token-goat-chunk-OBOFIHKN.mjs +38 -0
  90. package/dist/token-goat-chunk-OBRFJ5O5.mjs +76 -0
  91. package/dist/{token-goat-chunk-JTQQGXZH.mjs → token-goat-chunk-ODBT2USM.mjs} +124 -57
  92. package/dist/token-goat-chunk-OS5EM5PP.mjs +107 -0
  93. package/dist/token-goat-chunk-OUKDTVNU.mjs +1178 -0
  94. package/dist/token-goat-chunk-OXFMP75N.mjs +566 -0
  95. package/dist/{token-goat-chunk-DZX2YDPT.mjs → token-goat-chunk-OYIYKDDT.mjs} +2 -2
  96. package/dist/{token-goat-chunk-QCLDAQTS.mjs → token-goat-chunk-P6HQSWQ3.mjs} +50 -53
  97. package/dist/token-goat-chunk-P6VGNTQT.mjs +71 -0
  98. package/dist/{token-goat-chunk-7BXSNBY5.mjs → token-goat-chunk-PFWIMH2E.mjs} +23 -15
  99. package/dist/token-goat-chunk-PJZXIS3G.mjs +39 -0
  100. package/dist/token-goat-chunk-PUO5INXP.mjs +606 -0
  101. package/dist/{token-goat-chunk-GFBSN7BD.mjs → token-goat-chunk-PX5PQWTY.mjs} +22 -250
  102. package/dist/token-goat-chunk-Q5QDBC6O.mjs +111 -0
  103. package/dist/token-goat-chunk-QFNQXSDT.mjs +40 -0
  104. package/dist/{token-goat-chunk-FBNQNSJK.mjs → token-goat-chunk-QH7YVYOY.mjs} +2198 -2782
  105. package/dist/{token-goat-chunk-64FBPDTK.mjs → token-goat-chunk-QLMUHEPB.mjs} +12 -9
  106. package/dist/{token-goat-chunk-RZRTDST7.mjs → token-goat-chunk-QQOJIHL3.mjs} +37 -11
  107. package/dist/{token-goat-chunk-XKI4SEMX.mjs → token-goat-chunk-QQTWJY2O.mjs} +19 -0
  108. package/dist/{token-goat-chunk-U4N3BN2W.mjs → token-goat-chunk-QYHO5CPH.mjs} +3 -2
  109. package/dist/token-goat-chunk-RELAVWAD.mjs +27 -0
  110. package/dist/{token-goat-chunk-ETBHVDB6.mjs → token-goat-chunk-RSLPOBKQ.mjs} +22 -11
  111. package/dist/token-goat-chunk-SAMFDIS2.mjs +712 -0
  112. package/dist/token-goat-chunk-SDELXLDM.mjs +48 -0
  113. package/dist/{token-goat-chunk-4JJS54TR.mjs → token-goat-chunk-SOAFZNNZ.mjs} +161 -38
  114. package/dist/{token-goat-chunk-664RTTUV.mjs → token-goat-chunk-SRTHT7KV.mjs} +21 -16
  115. package/dist/{token-goat-chunk-4FVIAHSI.mjs → token-goat-chunk-T5P5ACNQ.mjs} +7 -6
  116. package/dist/token-goat-chunk-T5YGG4V3.mjs +45 -0
  117. package/dist/token-goat-chunk-TAUZ4OFS.mjs +110 -0
  118. package/dist/{token-goat-chunk-COTXZHTS.mjs → token-goat-chunk-TM6MC754.mjs} +4 -3
  119. package/dist/token-goat-chunk-TWUKBOMP.mjs +72 -0
  120. package/dist/{token-goat-chunk-3CHUNJ2O.mjs → token-goat-chunk-U2KSB4SG.mjs} +8 -5
  121. package/dist/{token-goat-chunk-3Z5S4KGM.mjs → token-goat-chunk-U4WHMG6Y.mjs} +29 -49
  122. package/dist/{token-goat-chunk-PY533T7F.mjs → token-goat-chunk-UGJHGR5A.mjs} +21 -15
  123. package/dist/{token-goat-chunk-CYCEYAEO.mjs → token-goat-chunk-UOUDVA5U.mjs} +1 -1
  124. package/dist/{token-goat-chunk-JDHE26RP.mjs → token-goat-chunk-URRJ33GR.mjs} +16 -5
  125. package/dist/{token-goat-chunk-56EIMS3E.mjs → token-goat-chunk-UZOROKIS.mjs} +23 -17
  126. package/dist/{token-goat-chunk-CZ3LUXYZ.mjs → token-goat-chunk-VIHGGGEI.mjs} +427 -308
  127. package/dist/token-goat-chunk-VTJLAA7P.mjs +21 -0
  128. package/dist/token-goat-chunk-VWDXLT4F.mjs +678 -0
  129. package/dist/{token-goat-chunk-YARZFCZS.mjs → token-goat-chunk-VWZA2H4P.mjs} +62 -53
  130. package/dist/{token-goat-chunk-SQSSNQPE.mjs → token-goat-chunk-VXEXMJDL.mjs} +3979 -4745
  131. package/dist/token-goat-chunk-WBA5SKT4.mjs +547 -0
  132. package/dist/{token-goat-chunk-SJHWVNDQ.mjs → token-goat-chunk-WDH44VAU.mjs} +7 -4
  133. package/dist/token-goat-chunk-X2SUP3EI.mjs +107 -0
  134. package/dist/token-goat-chunk-X3BELZSJ.mjs +125 -0
  135. package/dist/{token-goat-chunk-FZ3NEW5O.mjs → token-goat-chunk-XLRBNZCC.mjs} +13 -8
  136. package/dist/{token-goat-chunk-Z44YO3QZ.mjs → token-goat-chunk-XRKUX6CM.mjs} +1 -1
  137. package/dist/token-goat-chunk-XX6O25RN.mjs +82 -0
  138. package/dist/token-goat-chunk-XZSUHBVS.mjs +65 -0
  139. package/dist/{token-goat-chunk-WY3L4DVT.mjs → token-goat-chunk-Y2ZTQOCL.mjs} +13 -8
  140. package/dist/{token-goat-chunk-UQ5UK2T7.mjs → token-goat-chunk-YB4VMGK2.mjs} +1 -1
  141. package/dist/token-goat-chunk-YBIIY3HR.mjs +51 -0
  142. package/dist/{token-goat-chunk-ZQSJMO2R.mjs → token-goat-chunk-YHVDKQBR.mjs} +105 -105
  143. package/dist/{token-goat-chunk-H2K3QP6H.mjs → token-goat-chunk-YJB5XLWI.mjs} +8 -6
  144. package/dist/{token-goat-chunk-ZCDTCVIS.mjs → token-goat-chunk-YKB6AQG3.mjs} +5 -2
  145. package/dist/token-goat-chunk-YMTG2YA5.mjs +35 -0
  146. package/dist/token-goat-chunk-YPBZYQRV.mjs +40 -0
  147. package/dist/{token-goat-chunk-HOJLCGDP.mjs → token-goat-chunk-YY6AVUFL.mjs} +2033 -2251
  148. package/dist/token-goat-chunk-Z7N5OMMP.mjs +98 -0
  149. package/dist/token-goat-chunk-ZNMNUPAZ.mjs +938 -0
  150. package/dist/token-goat-hook-client.cjs +1 -1
  151. package/dist/token-goat-hook-client.mjs +3 -3
  152. package/dist/token-goat-hook.mjs +67 -38
  153. package/dist/token-goat.core.mjs +4 -4
  154. package/docs/C4_RUNTIME_ARCHITECTURE.md +1 -1
  155. package/docs/cli.md +40 -21
  156. package/docs/install.md +19 -9
  157. package/docs/security.md +5 -3
  158. package/package.json +2 -1
  159. package/dist/token-goat-chunk-5KDMCMF2.mjs +0 -362
  160. package/dist/token-goat-chunk-6QMAS7AB.mjs +0 -42
  161. package/dist/token-goat-chunk-7F2UBPD3.mjs +0 -397
  162. package/dist/token-goat-chunk-ARDNEC2U.mjs +0 -248
  163. package/dist/token-goat-chunk-ATCPY7EQ.mjs +0 -134
  164. package/dist/token-goat-chunk-EFHMMIGP.mjs +0 -68
  165. package/dist/token-goat-chunk-IVALLT3S.mjs +0 -48
  166. package/dist/token-goat-chunk-JDU3EXSU.mjs +0 -65
  167. package/dist/token-goat-chunk-MKPF4CL5.mjs +0 -37
  168. package/dist/token-goat-chunk-RC35RBUL.mjs +0 -312
  169. package/dist/token-goat-chunk-USILZ3T3.mjs +0 -53
  170. package/dist/token-goat-chunk-VL3YIFHZ.mjs +0 -2975
  171. package/dist/token-goat-chunk-XU4P6Z24.mjs +0 -133
  172. package/dist/token-goat-chunk-Y776AENC.mjs +0 -1597
@@ -0,0 +1,1298 @@
1
+ import { createRequire as __cjsRequire } from 'node:module';
2
+ const require = __cjsRequire(import.meta.url);
3
+ import {
4
+ Database
5
+ } from "./token-goat-chunk-XB5KKOCG.mjs";
6
+ import {
7
+ assignFlatEndLines,
8
+ makeLineSymbol,
9
+ propagateEndLinesToSymbols
10
+ } from "./token-goat-chunk-OUKDTVNU.mjs";
11
+ import {
12
+ registerReset
13
+ } from "./token-goat-chunk-YZAAZU2S.mjs";
14
+ import {
15
+ countContentLines,
16
+ ensureDirSync
17
+ } from "./token-goat-chunk-HU4TORC5.mjs";
18
+ import {
19
+ foldCase,
20
+ foldPath,
21
+ isCaseInsensitiveFs,
22
+ sleepSync
23
+ } from "./token-goat-chunk-AM5V6GSX.mjs";
24
+ import {
25
+ normalizePath,
26
+ safeJoin
27
+ } from "./token-goat-chunk-FNFBKLRD.mjs";
28
+ import {
29
+ SYMBOL_BODY_CHAR_CAP,
30
+ dataDir,
31
+ globalDbPath
32
+ } from "./token-goat-chunk-CO3UYX6G.mjs";
33
+ import {
34
+ init_define_import_meta_env
35
+ } from "./token-goat-chunk-A37V4PBF.mjs";
36
+
37
+ // src/languages/ini_idx.ts
38
+ init_define_import_meta_env();
39
+ var MAX_SECTIONS = 200;
40
+ var MAX_HEADING_LEN = 200;
41
+ var MAX_ENV_KEYS = 200;
42
+ var HEADER_RE = /^\[([^\]\r\n]+)\]\s*(?:[;#].*)?$/;
43
+ var ENV_KEY_RE = /^(?:export\s+)?([A-Za-z_][\w.-]*)\s*(?:=|:(?!\/\/))/;
44
+ function extractIni(content, filePath) {
45
+ const symbols = [];
46
+ const sections = [];
47
+ const seen = /* @__PURE__ */ new Set();
48
+ const lines = content.split(/\r?\n/);
49
+ const totalLines = countContentLines(content);
50
+ for (let i = 0; i < lines.length; i++) {
51
+ if (symbols.length >= MAX_SECTIONS) break;
52
+ const line = lines[i] ?? "";
53
+ if (!line || line[0] !== "[") continue;
54
+ const m = HEADER_RE.exec(line);
55
+ if (m === null) continue;
56
+ const name = m[1]?.trim() ?? "";
57
+ if (!name || name.length > MAX_HEADING_LEN) continue;
58
+ const key = `${name}\0${i + 1}`;
59
+ if (seen.has(key)) continue;
60
+ seen.add(key);
61
+ symbols.push(makeLineSymbol(filePath, name, "ini_section", i + 1));
62
+ sections.push({ heading: name, level: 1, line: i + 1, endLine: i + 1 });
63
+ }
64
+ sections.sort((a, b) => a.line - b.line);
65
+ assignFlatEndLines(sections, totalLines);
66
+ return propagateEndLinesToSymbols(symbols, sections);
67
+ }
68
+ function _isEscapedQuote(line, i, q) {
69
+ if (q === "'") return false;
70
+ let backslashes = 0;
71
+ let j = i - 1;
72
+ while (j >= 0 && line[j] === "\\") {
73
+ backslashes++;
74
+ j--;
75
+ }
76
+ return backslashes % 2 === 1;
77
+ }
78
+ function _lineClosesQuote(line, q) {
79
+ for (let i = 0; i < line.length; i++) {
80
+ if (line[i] === q && !_isEscapedQuote(line, i, q)) return true;
81
+ }
82
+ return false;
83
+ }
84
+ function _detectOpenQuote(value) {
85
+ const trimmed = value.replace(/^\s+/, "");
86
+ const q = trimmed[0];
87
+ if (q !== '"' && q !== "'") return null;
88
+ for (let i = 1; i < trimmed.length; i++) {
89
+ if (trimmed[i] === q && !_isEscapedQuote(trimmed, i, q)) return null;
90
+ }
91
+ return q;
92
+ }
93
+ function extractEnv(content, filePath) {
94
+ const symbols = [];
95
+ const seen = /* @__PURE__ */ new Set();
96
+ const lines = content.split(/\r?\n/);
97
+ let openQuote = null;
98
+ for (let i = 0; i < lines.length; i++) {
99
+ if (symbols.length >= MAX_ENV_KEYS) break;
100
+ const line = lines[i] ?? "";
101
+ if (openQuote !== null) {
102
+ if (_lineClosesQuote(line, openQuote)) openQuote = null;
103
+ continue;
104
+ }
105
+ const trimmed = line.replace(/^[ \t]+/, "");
106
+ if (!trimmed || trimmed[0] === "#" || trimmed[0] === ";") continue;
107
+ const m = ENV_KEY_RE.exec(trimmed);
108
+ if (m === null) continue;
109
+ const name = m[1]?.trim() ?? "";
110
+ if (!name || name.length > MAX_HEADING_LEN) continue;
111
+ const key = `${name}\0${i + 1}`;
112
+ if (seen.has(key)) continue;
113
+ seen.add(key);
114
+ symbols.push(makeLineSymbol(filePath, name, "env_key", i + 1));
115
+ openQuote = _detectOpenQuote(trimmed.slice(m[0].length));
116
+ }
117
+ return symbols;
118
+ }
119
+
120
+ // src/language_specs.ts
121
+ init_define_import_meta_env();
122
+ var CODE = { symbolBearing: true, sourceHints: true, grepSource: true, diffable: true };
123
+ var DATA = { symbolBearing: false, sourceHints: false, grepSource: false, diffable: false };
124
+ var LANGUAGE_SPECS = [
125
+ { id: "typescript", extraction: "tree-sitter", extensions: [".ts", ".tsx", ".mts", ".cts"], ...CODE, fence: "typescript", fenceByExtension: { ".tsx": "tsx" } },
126
+ { id: "javascript", extraction: "tree-sitter", extensions: [".js", ".jsx", ".mjs", ".cjs"], ...CODE, fence: "javascript", fenceByExtension: { ".jsx": "jsx" } },
127
+ // Bazel and Starlark (`.bzl`, `.star`, BUILD/WORKSPACE/MODULE.bazel) are a Python dialect: `def` and top-level calls such as `load(...)` parse with the Python grammar (https://github.com/bazelbuild/starlark/blob/master/spec.md).
128
+ { id: "python", extraction: "tree-sitter", extensions: [".py", ".pyi", ".bzl", ".star"], basenames: ["build.bazel", "workspace.bazel", "module.bazel"], exactBasenames: ["BUILD", "WORKSPACE"], ...CODE, fence: "python" },
129
+ { id: "go", extraction: "tree-sitter", extensions: [".go"], ...CODE, fence: "go" },
130
+ { id: "rust", extraction: "tree-sitter", extensions: [".rs"], ...CODE, fence: "rust" },
131
+ // Rake task files, Gemfile, Rakefile and the other extensionless Ruby DSL files are plain Ruby syntax.
132
+ { id: "ruby", extraction: "tree-sitter", extensions: [".rb", ".ruby", ".rake"], basenames: ["gemfile", "rakefile", "vagrantfile", "guardfile", "podfile", "capfile", "fastfile", "brewfile"], ...CODE, fence: "ruby" },
133
+ { id: "java", extraction: "tree-sitter", extensions: [".java"], ...CODE, fence: "java" },
134
+ // `.h` is C unless it declares an Objective-C `@interface` or `@protocol` (refineLanguageByContent); a C++ header parses with the C grammar.
135
+ { id: "c", extraction: "tree-sitter", extensions: [".c", ".h"], label: "C", ...CODE, fence: "c" },
136
+ { id: "cpp", extraction: "tree-sitter", extensions: [".cpp", ".cc", ".cxx", ".hpp", ".hxx"], label: "C++", ...CODE, fence: "cpp" },
137
+ // zsh, ksh and bats scripts share the POSIX `name() {` and `function name {` function forms the bash adapter reads.
138
+ { id: "bash", extraction: "regex", extensions: [".sh", ".bash", ".zsh", ".ksh", ".bats"], ...DATA, symbolBearing: true, fence: "bash" },
139
+ // MDX headings are plain ATX; `.rst` needs an underline heading parser this adapter lacks, so it stays unmapped.
140
+ { id: "markdown", extraction: "regex", extensions: [".md", ".markdown", ".mdx"], ...DATA, fence: "markdown" },
141
+ { id: "toml", extraction: "regex", extensions: [".toml"], basenames: ["cargo.toml", "pyproject.toml"], label: "TOML", ...DATA, diffable: true, fence: "toml" },
142
+ // JSON with comments (`.jsonc`) and Avro schemas (`.avsc`) read with the JSON adapter, which skips `//` and `/* */` comments.
143
+ { id: "json", extraction: "regex", extensions: [".json", ".jsonc", ".avsc"], basenames: ["package.json", "tsconfig.json"], label: "JSON", ...DATA, diffable: true, fence: "json" },
144
+ { id: "yaml", extraction: "regex", extensions: [".yaml", ".yml"], label: "YAML", ...DATA, diffable: true, fence: "yaml" },
145
+ { id: "css", extraction: "regex", extensions: [".css", ".scss", ".sass", ".less"], label: "CSS", ...DATA, grepSource: true, diffable: true, fence: "css", fenceByExtension: { ".scss": "scss", ".sass": "sass", ".less": "less" } },
146
+ { id: "dockerfile", extraction: "regex", extensions: [], basenames: ["dockerfile"], ...DATA, fence: "dockerfile" },
147
+ { id: "csharp", extraction: "regex", extensions: [".cs"], label: "C#", ...CODE, fence: "csharp" },
148
+ { id: "php", extraction: "regex", extensions: [".php"], label: "PHP", ...CODE, fence: "php" },
149
+ { id: "html", extraction: "regex", extensions: [".html", ".htm"], label: "HTML", ...DATA, fence: "html" },
150
+ { id: "liquid", extraction: "regex", extensions: [".liquid"], ...DATA, fence: "liquid" },
151
+ // Six template dialects, each masking its own delimiters out then handing off to the HTML extractor (src/languages/templates_idx.ts) -- markup formats, so DATA defaults like html/liquid.
152
+ { id: "jinja2", extraction: "regex", extensions: [".j2", ".jinja", ".jinja2"], label: "Jinja2", ...DATA, fence: "jinja" },
153
+ { id: "handlebars", extraction: "regex", extensions: [".hbs", ".handlebars"], label: "Handlebars", ...DATA, fence: "handlebars" },
154
+ { id: "erb", extraction: "regex", extensions: [".erb"], label: "ERB", ...DATA, fence: "erb" },
155
+ { id: "ejs", extraction: "regex", extensions: [".ejs"], label: "EJS", ...DATA, fence: "ejs" },
156
+ { id: "nunjucks", extraction: "regex", extensions: [".njk"], label: "Nunjucks", ...DATA, fence: "html" },
157
+ { id: "twig", extraction: "regex", extensions: [".twig"], label: "Twig", ...DATA, fence: "twig" },
158
+ { id: "kotlin", extraction: "regex", extensions: [".kt", ".kts"], ...CODE, fence: "kotlin" },
159
+ { id: "swift", extraction: "regex", extensions: [".swift"], ...CODE, fence: "swift" },
160
+ { id: "scala", extraction: "regex", extensions: [".scala", ".sc"], ...CODE, fence: "scala" },
161
+ { id: "lua", extraction: "regex", extensions: [".lua"], ...CODE, fence: "lua" },
162
+ { id: "elixir", extraction: "regex", extensions: [".ex", ".exs"], ...CODE, fence: "elixir" },
163
+ { id: "dart", extraction: "regex", extensions: [".dart"], ...CODE, fence: "dart" },
164
+ { id: "zig", extraction: "regex", extensions: [".zig"], ...CODE, fence: "zig" },
165
+ { id: "r", extraction: "regex", extensions: [".r"], ...CODE, fence: "r" },
166
+ { id: "graphql", extraction: "regex", extensions: [".graphql", ".gql"], label: "GraphQL", ...DATA, symbolBearing: true, fence: "graphql" },
167
+ // Oracle PL/SQL sources: package spec and body, standalone procedure/function, trigger, and object type spec and body.
168
+ { id: "sql", extraction: "regex", extensions: [".sql", ".pks", ".pkb", ".pls", ".plsql", ".pck", ".prc", ".fnc", ".trg", ".tps", ".tpb"], label: "SQL", ...DATA, symbolBearing: true, diffable: true, fence: "sql" },
169
+ { id: "ini", extraction: "regex", extensions: [".ini", ".cfg", ".conf"], label: "INI", ...DATA, fence: "ini" },
170
+ { id: "nginx", extraction: "regex", extensions: [".nginx"], basenames: ["nginx.conf"], label: "Nginx", ...DATA, symbolBearing: true, diffable: true, fence: "nginx" },
171
+ // `basenames` is matched against a lower-cased basename, so `Caddyfile` is spelled in lower case here; the display spelling is the label's job.
172
+ { id: "caddy", extraction: "regex", extensions: [".caddy"], basenames: ["caddyfile", "caddy.conf"], label: "Caddyfile", ...DATA, symbolBearing: true, diffable: true, fence: "caddy" },
173
+ { id: "apache", extraction: "regex", extensions: [".apache", ".apache2"], basenames: ["httpd.conf", "apache2.conf", ".htaccess"], label: "Apache Config", ...DATA, symbolBearing: true, diffable: true, fence: "apache" },
174
+ // `.mk` fragments (config.mk, rules.mk) share a bare Makefile's syntax.
175
+ { id: "makefile", extraction: "regex", extensions: [".mk"], basenames: ["makefile", "gnumakefile", "bsdmakefile"], ...DATA, fence: "makefile", basenameImportsExtension: ".mk" },
176
+ { id: "proto", extraction: "regex", extensions: [".proto"], label: "Protocol Buffers", ...DATA, symbolBearing: true, fence: "protobuf" },
177
+ { id: "terraform", extraction: "regex", extensions: [".tf", ".tfvars", ".hcl"], ...DATA, symbolBearing: true, fence: "hcl" },
178
+ // `.env` and `.env.<suffix>` are matched by parser_types.ts's DOTENV_VARIANT_RE before this table.
179
+ { id: "env_file", extraction: "regex", extensions: [".env"], basenames: [".envrc"], label: "env file", ...DATA },
180
+ { id: "powershell", extraction: "regex", extensions: [".ps1", ".psm1"], label: "PowerShell", ...CODE, fence: "powershell" },
181
+ // VB.NET, VB6/VBA standard modules, VBScript and VB6 forms. A VB6 class module shares `.cls` with Apex (and LaTeX) and is told apart by content in refineLanguageByContent.
182
+ { id: "vb", extraction: "regex", extensions: [".vb", ".bas", ".vbs", ".frm"], label: "Visual Basic", ...CODE, fence: "vb", fenceByExtension: { ".vb": "vbnet", ".vbs": "vbscript" } },
183
+ {
184
+ id: "cobol",
185
+ extraction: "own-result",
186
+ extensions: [".cbl", ".cob", ".cobol", ".cpy"],
187
+ label: "COBOL",
188
+ ...CODE,
189
+ fence: "cobol",
190
+ partialRefsReason: "PERFORM, GO TO and CALL 'literal' are recorded as references, but a paragraph also runs by falling through from the one above it and a program can be called through a data item holding its name, so `dead` skips COBOL"
191
+ },
192
+ // Natural object sources as NaturalONE and SYSOBJH export them. Maps (.nsm) and DDMs (.nsd) are layouts, not code, and stay unmapped.
193
+ {
194
+ id: "natural",
195
+ extraction: "own-result",
196
+ extensions: [".nsp", ".nsn", ".nss", ".nsa", ".nsl", ".nsg", ".nsc", ".nsh"],
197
+ label: "Natural",
198
+ ...CODE,
199
+ fence: "natural",
200
+ partialRefsReason: "PERFORM, CALLNAT 'literal' and FETCH 'literal' are recorded as references, but an object can also be called through a variable holding its name, so `dead` skips Natural"
201
+ },
202
+ { id: "abap", extraction: "regex", extensions: [".abap"], label: "ABAP", ...CODE, fence: "abap" },
203
+ { id: "sas", extraction: "regex", extensions: [".sas"], label: "SAS", ...CODE, fence: "sas" },
204
+ { id: "pli", extraction: "regex", extensions: [".pli", ".pl1"], label: "PL/I", ...CODE, fence: "pli" },
205
+ // `.rpg` stays unmapped: RPG II and RPG III sources use it, and their fixed layout predates the ILE RPG forms this adapter reads.
206
+ { id: "rpg", extraction: "regex", extensions: [".rpgle", ".sqlrpgle"], label: "RPG", ...CODE, fence: "rpgle" },
207
+ { id: "jcl", extraction: "regex", extensions: [".jcl"], label: "JCL", ...CODE, fence: "jcl" },
208
+ // `.mm` is always Objective-C++. A `.m` (MATLAB uses it too) is Objective-C only on an `#import`, `@interface`, `@implementation` or `@protocol` line, and a `.h` only on `@interface` or `@protocol`: refineLanguageByContent in parser_types.ts decides.
209
+ { id: "objc", extraction: "regex", extensions: [".mm"], label: "Objective-C", ...CODE, fence: "objectivec" },
210
+ // Gradle build scripts and Jenkinsfiles are Groovy.
211
+ { id: "groovy", extraction: "regex", extensions: [".groovy", ".gvy", ".gradle"], basenames: ["jenkinsfile"], label: "Groovy", ...CODE, fence: "groovy", basenameImportsExtension: ".groovy" },
212
+ // A Prolog `.pl` stays unknown (refineLanguageByContent), and a `.t` is Perl only on a Perl marker line.
213
+ { id: "perl", extraction: "regex", extensions: [".pl", ".pm"], label: "Perl", ...CODE, fence: "perl" },
214
+ { id: "solidity", extraction: "regex", extensions: [".sol"], label: "Solidity", ...CODE, fence: "solidity" },
215
+ { id: "thrift", extraction: "regex", extensions: [".thrift"], label: "Thrift", ...DATA, symbolBearing: true, fence: "thrift" },
216
+ { id: "glsl", extraction: "regex", extensions: [".glsl", ".vert", ".frag", ".comp", ".geom", ".tesc", ".tese"], label: "GLSL", ...CODE, fence: "glsl" },
217
+ // `.fx` stays unmapped: other languages use it too.
218
+ { id: "hlsl", extraction: "regex", extensions: [".hlsl", ".hlsli"], label: "HLSL", ...CODE, fence: "hlsl" },
219
+ { id: "wgsl", extraction: "regex", extensions: [".wgsl"], label: "WGSL", ...CODE, fence: "wgsl" },
220
+ { id: "metal", extraction: "regex", extensions: [".metal"], label: "Metal", ...CODE, fence: "metal" },
221
+ // `.f`, `.for` and `.f77` are read as fixed form unless code starts in column 1. `.fpp` stays unmapped: it is used for both forms.
222
+ { id: "fortran", extraction: "regex", extensions: [".f", ".for", ".f77", ".f90", ".f95", ".f03", ".f08"], label: "Fortran", ...CODE, fence: "fortran" },
223
+ // `.inc` stays unmapped (many languages use it), and a `.pp` (Puppet uses it too) is Pascal only on a unit, program or library header: refineLanguageByContent in parser_types.ts decides. Text-form `.dfm` forms list their components.
224
+ { id: "pascal", extraction: "regex", extensions: [".pas", ".dpr", ".dpk", ".lpr", ".dfm"], label: "Pascal", ...CODE, fence: "pascal" },
225
+ // MATLAB has no extension of its own: a `.m` that is not Objective-C is MATLAB only on a `function` or `classdef` header line, which refineLanguageByContent in parser_types.ts checks, so a Mathematica or Mercury `.m` stays unknown.
226
+ { id: "matlab", extraction: "regex", extensions: [], label: "MATLAB", ...CODE, fence: "matlab" },
227
+ { id: "cmake", extraction: "regex", extensions: [".cmake"], basenames: ["cmakelists.txt"], label: "CMake", ...CODE, fence: "cmake", basenameImportsExtension: ".cmake" },
228
+ // One adapter for the three assembly dialects that share these extensions: GNU as (`.s`, and `.S` through the lowercase lookup), NASM (`.asm`, `.nasm`) and IBM High Level Assembler (`.asm`), which the adapter tells apart by content. `.inc` stays unmapped: many languages use it.
229
+ { id: "asm", extraction: "regex", extensions: [".s", ".asm", ".nasm"], label: "Assembly", ...CODE, fence: "asm" },
230
+ { id: "batch", extraction: "regex", extensions: [".bat", ".cmd"], label: "Windows batch", ...CODE, fence: "batch" },
231
+ { id: "erlang", extraction: "regex", extensions: [".erl", ".hrl"], label: "Erlang", ...CODE, fence: "erlang" },
232
+ { id: "vhdl", extraction: "regex", extensions: [".vhd", ".vhdl"], label: "VHDL", ...CODE, fence: "vhdl" },
233
+ // Five Lisp-family dialects, each its own row: none shares an extractor (see common_lisp.ts's module doc for why their lexical rules stay separate rather than a single Lisp masker).
234
+ { id: "common_lisp", extraction: "regex", extensions: [".lisp", ".lsp", ".cl"], label: "Common Lisp", ...CODE, fence: "lisp" },
235
+ { id: "scheme", extraction: "regex", extensions: [".scm", ".ss"], label: "Scheme", ...CODE, fence: "scheme" },
236
+ { id: "racket", extraction: "regex", extensions: [".rkt", ".rktl"], label: "Racket", ...CODE, fence: "racket" },
237
+ { id: "clojure", extraction: "regex", extensions: [".clj", ".cljs", ".cljc"], label: "Clojure", ...CODE, fence: "clojure" },
238
+ { id: "emacs_lisp", extraction: "regex", extensions: [".el"], label: "Emacs Lisp", ...CODE, fence: "lisp" },
239
+ // `.lhs` (literate Haskell) is deliberately not claimed here -- see haskell.ts's module doc.
240
+ { id: "haskell", extraction: "regex", extensions: [".hs"], label: "Haskell", ...CODE, fence: "haskell" },
241
+ // `.mli` interface files share `.ml`'s lexical rules (comments, strings, quoted strings) and are read with the same extractor -- see ocaml.ts's module doc.
242
+ { id: "ocaml", extraction: "regex", extensions: [".ml", ".mli"], label: "OCaml", ...CODE, fence: "ocaml" },
243
+ // `.fsi` signature files and `.fsx` scripts share `.fs`'s lexical rules and are read with the same extractor -- see fsharp.ts's module doc.
244
+ { id: "fsharp", extraction: "regex", extensions: [".fs", ".fsi", ".fsx"], label: "F#", ...CODE, fence: "fsharp" },
245
+ { id: "nix", extraction: "regex", extensions: [".nix"], label: "Nix", ...CODE, fence: "nix" },
246
+ // OpenEdge ABL has no extension of its own: a `.p` or `.w` (Pascal and CWEB use them too) or a `.cls` (Apex, VB6, LaTeX) is ABL only when its head carries an ABL marker, which refineLanguageByContent in parser_types.ts checks. The path-only hooks see a `.p` or `.w` as unknown and a `.cls` as Apex.
247
+ { id: "abl", extraction: "regex", extensions: [], label: "OpenEdge ABL", ...CODE, fence: "abl" },
248
+ { id: "apex", extraction: "regex", extensions: [".cls", ".trigger"], label: "Apex", ...CODE, fence: "apex" },
249
+ // Matched by the `-meta.xml` suffix in detectLanguage, not by an extension.
250
+ { id: "salesforce_metadata", extraction: "regex", extensions: [], label: "Salesforce metadata", ...DATA, symbolBearing: true, sourceHints: true, grepSource: true, fence: "xml" },
251
+ { id: "salesforce_markup", extraction: "own-result", extensions: [".cmp", ".app", ".evt", ".intf", ".design", ".auradoc", ".tokens", ".page", ".component", ".email"], label: "Salesforce markup", ...DATA, symbolBearing: true, sourceHints: true, grepSource: true, fence: "xml" },
252
+ { id: "vue", extraction: "own-result", extensions: [".vue"], ...DATA, fence: "vue" },
253
+ { id: "svelte", extraction: "own-result", extensions: [".svelte"], ...DATA, fence: "svelte" },
254
+ { id: "astro", extraction: "own-result", extensions: [".astro"], ...DATA, fence: "astro" },
255
+ // Notebooks index through their code cells as Python, in parser.ts's ipynb branch.
256
+ { id: "ipynb", extraction: "own-result", extensions: [".ipynb"], label: "Jupyter notebook", ...DATA, fence: "json" }
257
+ ];
258
+ var SPEC_BY_ID = new Map(LANGUAGE_SPECS.map((s) => [s.id, s]));
259
+ function rows() {
260
+ return LANGUAGE_SPECS;
261
+ }
262
+ var EXTENSION_LANGUAGE = new Map(
263
+ LANGUAGE_SPECS.flatMap((s) => s.extensions.map((e) => [e, s.id]))
264
+ );
265
+ var FILENAME_LANGUAGE = new Map(
266
+ rows().flatMap((s) => (s.basenames ?? []).map((b) => [b, s.id]))
267
+ );
268
+ var EXACT_FILENAME_LANGUAGE = new Map(
269
+ rows().flatMap((s) => (s.exactBasenames ?? []).map((b) => [b, s.id]))
270
+ );
271
+ var TREE_SITTER_LANGUAGES = LANGUAGE_SPECS.filter((s) => s.extraction === "tree-sitter").map((s) => s.id);
272
+ function languageHasFlag(language, flag) {
273
+ return SPEC_BY_ID.get(language)?.[flag] === true;
274
+ }
275
+ function languageLabel(language) {
276
+ if (language === "unknown") return "this file type";
277
+ return SPEC_BY_ID.get(language)?.label ?? language;
278
+ }
279
+ function fenceFor(language, ext) {
280
+ const spec = SPEC_BY_ID.get(language);
281
+ if (spec === void 0) return "";
282
+ return spec.fenceByExtension?.[ext] ?? spec.fence ?? "";
283
+ }
284
+ function basenameImportsExtension(language) {
285
+ return SPEC_BY_ID.get(language)?.basenameImportsExtension;
286
+ }
287
+ function partialRefsReason(language) {
288
+ return SPEC_BY_ID.get(language)?.partialRefsReason;
289
+ }
290
+
291
+ // src/parser_types.ts
292
+ init_define_import_meta_env();
293
+ import * as fs from "node:fs";
294
+ import * as path from "node:path";
295
+
296
+ // src/languages/sniff.ts
297
+ init_define_import_meta_env();
298
+ var SNIFF_CHARS = 8192;
299
+ var ABL_MARKER_RES = [
300
+ /^&ANALYZE-SUSPEND\b/i,
301
+ /^&(?:SCOPED|GLOBAL)-DEFINE\s/i,
302
+ /^(?:ROUTINE|BLOCK)-LEVEL\s+ON\s+ERROR\s+UNDO\b/i,
303
+ /^DEF(?:INE)?\s+(?:NEW\s+)?(?:GLOBAL\s+)?(?:SHARED\s+)?(?:VAR|VARIABLE|TEMP-TABLE|BUFFER|QUERY|STREAM|DATASET|FRAME|(?:INPUT|OUTPUT|INPUT-OUTPUT)\s+PARAM(?:ETER)?)\s/i,
304
+ /^FUNCTION\s+[\w-]+\s+RETURNS\s/i,
305
+ /^FOR\s+EACH\s/i,
306
+ /^END\s+(?:PROCEDURE|FUNCTION|CLASS|INTERFACE|METHOD|CONSTRUCTOR)\s*\.$/i,
307
+ /^USING\s+[\w.*-]+(?:\s+FROM\s+(?:ASSEMBLY|PROPATH))?\s*\.$/i
308
+ ];
309
+ var ABL_HEADER_RE = /^(?:PROCEDURE\s+[\w-]+|(?:CLASS|INTERFACE)\s+[\w.-]+)(?:\s|:)/i;
310
+ var ABL_SNIFF_CHARS = SNIFF_CHARS;
311
+ function isAblSource(content) {
312
+ const head = content.slice(0, ABL_SNIFF_CHARS);
313
+ if (head.includes("\0")) return false;
314
+ for (const raw of head.split(/\r?\n/)) {
315
+ const line = raw.trim();
316
+ if (line === "") continue;
317
+ if (ABL_MARKER_RES.some((re) => re.test(line))) return true;
318
+ if (ABL_HEADER_RE.test(line) && line.endsWith(":")) return true;
319
+ }
320
+ return false;
321
+ }
322
+ var SNIFF_FUNCTION_RE = /^function\b\s*(?:(?:\[[\w\s,~]*\]|[A-Za-z]\w*)\s*=\s*)?[A-Za-z]\w*(?:\.[A-Za-z]\w*)?\s*(?:\(|[%#;,]|$)/;
323
+ var SNIFF_CLASSDEF_RE = /^classdef\b\s*(?:\([^)]*\)\s*)?[A-Za-z]\w*\s*(?:<|[%#]|$)/;
324
+ function isMatlabSource(content) {
325
+ for (const raw of content.slice(0, SNIFF_CHARS).split("\n")) {
326
+ const line = raw.trim();
327
+ if (SNIFF_FUNCTION_RE.test(line) || SNIFF_CLASSDEF_RE.test(line)) return true;
328
+ }
329
+ return false;
330
+ }
331
+ function isObjcSource(content) {
332
+ return /^[ \t]*(?:#[ \t]*import[ \t]*[<"]|@(?:interface|implementation|protocol)\b)/m.test(content.slice(0, SNIFF_CHARS));
333
+ }
334
+ function isObjcHeader(content) {
335
+ return /^[ \t]*@(?:interface|protocol)\b/m.test(content.slice(0, SNIFF_CHARS));
336
+ }
337
+ function isPascalSource(content) {
338
+ const head = content.slice(0, SNIFF_CHARS);
339
+ let i = 0;
340
+ for (; ; ) {
341
+ while (i < head.length && /\s/.test(head[i])) i++;
342
+ if (head[i] === "{") {
343
+ const end = head.indexOf("}", i);
344
+ if (end < 0) return false;
345
+ i = end + 1;
346
+ } else if (head.startsWith("(*", i)) {
347
+ const end = head.indexOf("*)", i);
348
+ if (end < 0) return false;
349
+ i = end + 2;
350
+ } else if (head.startsWith("//", i)) {
351
+ const end = head.indexOf("\n", i);
352
+ if (end < 0) return false;
353
+ i = end + 1;
354
+ } else {
355
+ break;
356
+ }
357
+ }
358
+ return /^(?:unit|program|library)[ \t\r\n]+[A-Za-z_][\w.]*[ \t\r\n]*[;(]/i.test(head.slice(i, i + 300));
359
+ }
360
+ var PERL_MARKER_RE = /^(?:#!.*\bperl\b|[ \t]*use[ \t]+(?:strict|warnings|Test::More|Test2::V0|Test::Simple)\b|[ \t]*package[ \t]+[\w:]+[ \t]*;|[ \t]*my[ \t]+[$@%]|[ \t]*sub[ \t]+\w+[ \t]*\{)/m;
361
+ function isPerlSource(content) {
362
+ const head = content.slice(0, SNIFF_CHARS);
363
+ return PERL_MARKER_RE.test(head) && !/^[ \t]*use[ \t]+v6\b/m.test(head);
364
+ }
365
+ function isPrologSource(content) {
366
+ const head = content.slice(0, SNIFF_CHARS);
367
+ if (PERL_MARKER_RE.test(head)) return false;
368
+ return /^:-/m.test(head) || /^[a-z]\w*(?:\([^()\n]*\))?[ \t]*:-/m.test(head);
369
+ }
370
+ var LATEX_CLASS_MARKER_RE = /\\(?:ProvidesClass|documentclass)\b/;
371
+ function isLatexClassFile(content) {
372
+ return LATEX_CLASS_MARKER_RE.test(content.slice(0, SNIFF_CHARS));
373
+ }
374
+
375
+ // src/parser_types.ts
376
+ var DOTENV_VARIANT_RE = /^\.env(\..+)?$/;
377
+ var VB6_HEADER_SCAN_LINES = 40;
378
+ function isVb6ClassModule(content) {
379
+ if (content.includes("\0")) return false;
380
+ const lines = (content.charCodeAt(0) === 65279 ? content.slice(1) : content).split(/\r?\n/, VB6_HEADER_SCAN_LINES);
381
+ const first = lines.find((l) => l.trim() !== "");
382
+ if (first !== void 0 && /^VERSION\s+1\.0\s+CLASS\b/i.test(first.trim())) return true;
383
+ return lines.some((l) => /^Attribute\s+VB_Name\s*=\s*"/i.test(l.trim()));
384
+ }
385
+ function refineLanguageByContent(filePath, language, content) {
386
+ const sniff = CONTENT_SNIFFS.get(path.extname(filePath).toLowerCase());
387
+ if (sniff === void 0 || sniff.from !== language) return language;
388
+ return sniff.refine(content) ?? language;
389
+ }
390
+ var LANGUAGE_SNIFF_BYTES = 8192;
391
+ function sniffHead(content) {
392
+ const head = content.slice(0, LANGUAGE_SNIFF_BYTES);
393
+ return Buffer.byteLength(head, "utf8") <= LANGUAGE_SNIFF_BYTES ? head : Buffer.from(head, "utf8").subarray(0, LANGUAGE_SNIFF_BYTES).toString("utf8");
394
+ }
395
+ var ablOrUnknown = (c) => isAblSource(c) ? "abl" : void 0;
396
+ var clsRefine = (c) => {
397
+ if (isVb6ClassModule(c)) return "vb";
398
+ const abl = ablOrUnknown(c);
399
+ if (abl !== void 0) return abl;
400
+ if (isLatexClassFile(c)) return "unknown";
401
+ return void 0;
402
+ };
403
+ var CONTENT_SNIFFS = /* @__PURE__ */ new Map([
404
+ [".cls", { from: "apex", refine: clsRefine }],
405
+ [".p", { from: "unknown", refine: ablOrUnknown }],
406
+ [".w", { from: "unknown", refine: ablOrUnknown }],
407
+ [".m", { from: "unknown", refine: (c) => isObjcSource(sniffHead(c)) ? "objc" : isMatlabSource(sniffHead(c)) ? "matlab" : void 0 }],
408
+ [".pp", { from: "unknown", refine: (c) => isPascalSource(sniffHead(c)) ? "pascal" : void 0 }],
409
+ [".h", { from: "c", refine: (c) => isObjcHeader(sniffHead(c)) ? "objc" : void 0 }],
410
+ [".pl", { from: "perl", refine: (c) => isPrologSource(sniffHead(c)) ? "unknown" : void 0 }],
411
+ [".t", { from: "unknown", refine: (c) => isPerlSource(sniffHead(c)) ? "perl" : void 0 }]
412
+ ]);
413
+ function needsContentSniff(filePath, language) {
414
+ return CONTENT_SNIFFS.get(path.extname(filePath).toLowerCase())?.from === language;
415
+ }
416
+ function detectLanguageOfFile(filePath) {
417
+ const language = detectLanguage(filePath);
418
+ if (!needsContentSniff(filePath, language)) return language;
419
+ try {
420
+ const fd = fs.openSync(filePath, "r");
421
+ try {
422
+ const buf = Buffer.alloc(LANGUAGE_SNIFF_BYTES);
423
+ const n = fs.readSync(fd, buf, 0, buf.length, 0);
424
+ return refineLanguageByContent(filePath, language, buf.subarray(0, n).toString("utf8"));
425
+ } finally {
426
+ fs.closeSync(fd);
427
+ }
428
+ } catch {
429
+ return language;
430
+ }
431
+ }
432
+ function detectLanguage(filePath) {
433
+ const exactBase = path.basename(filePath);
434
+ const base = exactBase.toLowerCase();
435
+ if (DOTENV_VARIANT_RE.test(base)) return "env_file";
436
+ const byName = EXACT_FILENAME_LANGUAGE.get(exactBase) ?? FILENAME_LANGUAGE.get(base);
437
+ if (byName !== void 0) return byName;
438
+ if (base.endsWith("-meta.xml")) {
439
+ return "salesforce_metadata";
440
+ }
441
+ const ext = path.extname(base).toLowerCase();
442
+ return EXTENSION_LANGUAGE.get(ext) ?? "unknown";
443
+ }
444
+ var UNSUPPORTED_LANGUAGE_EXTENSIONS = /* @__PURE__ */ new Map([
445
+ [".rpg", "RPG II or RPG III"],
446
+ [".nsm", "Natural map"],
447
+ [".nsd", "Natural DDM"]
448
+ ]);
449
+ function nonTreeSitterLanguageCount() {
450
+ return LANGUAGE_SPECS.filter((s) => s.extraction !== "tree-sitter" && s.id !== "ipynb").length;
451
+ }
452
+ function unsupportedLanguageName(filePath) {
453
+ const ext = path.extname(filePath).toLowerCase();
454
+ return UNSUPPORTED_LANGUAGE_EXTENSIONS.get(ext);
455
+ }
456
+
457
+ // src/dotenv_redact.ts
458
+ init_define_import_meta_env();
459
+ var DOTENV_VALUE_PLACEHOLDER = "[REDACTED:dotenv_value]";
460
+ var ASSIGNMENT_RE = /^(\s*(?:export\s+)?[A-Za-z_][\w.-]*\s*(?:\+?=|:(?!\/\/)))/;
461
+ var SAFE_LINE_RE = /^\s*(?:[#;].*)?$/;
462
+ function isDotenvPath(filePath) {
463
+ return detectLanguage(filePath) === "env_file";
464
+ }
465
+ function redactDotenvValues(text) {
466
+ const lines = text.split("\n");
467
+ const out = [];
468
+ let openQuote = null;
469
+ for (const raw of lines) {
470
+ const hasCr = raw.endsWith("\r");
471
+ const line = hasCr ? raw.slice(0, -1) : raw;
472
+ const eol = hasCr ? "\r" : "";
473
+ const emit = (s) => {
474
+ out.push(`${s}${eol}`);
475
+ };
476
+ if (openQuote !== null) {
477
+ if (_lineClosesQuote(line, openQuote)) openQuote = null;
478
+ emit(DOTENV_VALUE_PLACEHOLDER);
479
+ continue;
480
+ }
481
+ if (SAFE_LINE_RE.test(line)) {
482
+ emit(line);
483
+ continue;
484
+ }
485
+ const m = ASSIGNMENT_RE.exec(line);
486
+ if (m === null || m[1] === void 0) {
487
+ emit(DOTENV_VALUE_PLACEHOLDER);
488
+ continue;
489
+ }
490
+ const prefix = m[1];
491
+ openQuote = _detectOpenQuote(line.slice(prefix.length));
492
+ emit(`${prefix}${DOTENV_VALUE_PLACEHOLDER}`);
493
+ }
494
+ return out.join("\n");
495
+ }
496
+ function redactIfDotenv(filePath, text) {
497
+ return isDotenvPath(filePath) ? redactDotenvValues(text) : text;
498
+ }
499
+
500
+ // src/sql_path.ts
501
+ init_define_import_meta_env();
502
+ function pathEqClause(column) {
503
+ return isCaseInsensitiveFs() ? `TG_LOWER(${column}) = ?` : `${column} = ?`;
504
+ }
505
+ function pathSuffixClause(column) {
506
+ const col = isCaseInsensitiveFs() ? `TG_LOWER(${column})` : column;
507
+ return {
508
+ // Both separators are accepted because not every writer into `symbols` stores a normalizePath'd (forward-slash) file_path, and the JS-side path-boundary test this narrowing feeds treats `/` and `\` alike.
509
+ clause: `(${col} = ? OR substr(${col}, -length(?)) = ? OR substr(${col}, -length(?)) = ?)`,
510
+ params: (baseName) => {
511
+ const folded = foldPath(baseName);
512
+ return [folded, `/${folded}`, `/${folded}`, `\\${folded}`, `\\${folded}`];
513
+ }
514
+ };
515
+ }
516
+ function projectScopeClause(column) {
517
+ const caseInsensitive = isCaseInsensitiveFs();
518
+ const col = caseInsensitive ? `TG_LOWER(${column})` : column;
519
+ return {
520
+ // Parenthesised so an OR anywhere in the surrounding WHERE cannot bind tighter than the pair.
521
+ clause: `(${col} >= ? AND ${col} < ?)`,
522
+ params: (root) => {
523
+ const folded = caseInsensitive ? foldPath(normalizePath(root)) : normalizePath(root);
524
+ const lower = folded.endsWith("/") ? folded : `${folded}/`;
525
+ const upper = `${lower.slice(0, -1)}0`;
526
+ return [lower, upper];
527
+ }
528
+ };
529
+ }
530
+ function pathSpellings(filePath) {
531
+ const key = normalizePath(filePath);
532
+ if (!key.includes("/") && !key.includes("\\")) return [key];
533
+ const alt = key.includes("/") ? key.replace(/\//g, "\\") : key.replace(/\\/g, "/");
534
+ return [key, alt];
535
+ }
536
+ function filePathSpellingsClause(column, filePath) {
537
+ const params = pathSpellings(filePath).map(foldPath);
538
+ const clause = params.map(() => pathEqClause(column)).join(" OR ");
539
+ return { clause: params.length > 1 ? `(${clause})` : clause, params };
540
+ }
541
+ function fileRowByIndexKey(db, filePath, columns) {
542
+ const statement = db.prepare(`SELECT ${columns} FROM files WHERE ${pathEqClause("path")}`);
543
+ for (const spelling of pathSpellings(filePath)) {
544
+ const row = statement.get(foldPath(spelling));
545
+ if (row !== void 0) return row;
546
+ }
547
+ return void 0;
548
+ }
549
+
550
+ // src/db.ts
551
+ init_define_import_meta_env();
552
+ import * as fs2 from "node:fs";
553
+ import { createRequire } from "node:module";
554
+ import * as path2 from "node:path";
555
+ var _require = createRequire(import.meta.url);
556
+ var _connections = /* @__PURE__ */ new Map();
557
+ var SCHEMA_SQL = `
558
+ CREATE TABLE IF NOT EXISTS files (
559
+ path TEXT PRIMARY KEY,
560
+ sha TEXT,
561
+ mtime REAL,
562
+ language TEXT,
563
+ indexed_at REAL,
564
+ embed_sha TEXT,
565
+ retry_count INTEGER NOT NULL DEFAULT 0,
566
+ parser_sha TEXT
567
+ );
568
+ -- Expression index on TG_LOWER(path) -- see pathEqClause (sql_path.ts) and TG_LOWER's
569
+ -- registration above. TG_LOWER is registered { deterministic: true }, which is required for
570
+ -- SQLite to index an expression at all; without it CREATE INDEX on a function call throws
571
+ -- "non-deterministic functions prohibited in index expressions". Because pathEqClause emits
572
+ -- this exact 'TG_LOWER(path) = ?' text for every case-insensitive-filesystem query, the planner
573
+ -- matches it against this index and uses SEARCH instead of a full table SCAN, without requiring
574
+ -- any writer to populate a separate folded column (verified via EXPLAIN QUERY PLAN in
575
+ -- db.test.ts / sql_path.test.ts). CREATE INDEX IF NOT EXISTS is purely additive and safe to run
576
+ -- against an already-populated table on every connection open, unlike an ALTER TABLE column add
577
+ -- -- no MIGRATIONS entry or SCHEMA_VERSION bump is needed for this index.
578
+ CREATE INDEX IF NOT EXISTS idx_files_path_folded ON files(TG_LOWER(path));
579
+
580
+ -- Transient-read-failure counters, keyed by path. See the note above SCHEMA_SQL for why they are not in files.
581
+ CREATE TABLE IF NOT EXISTS index_retries (
582
+ path TEXT PRIMARY KEY,
583
+ retry_count INTEGER NOT NULL
584
+ );
585
+ CREATE INDEX IF NOT EXISTS idx_index_retries_path_folded ON index_retries(TG_LOWER(path));
586
+
587
+ CREATE TABLE IF NOT EXISTS symbols (
588
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
589
+ file_path TEXT,
590
+ name TEXT,
591
+ kind TEXT,
592
+ line_start INTEGER,
593
+ line_end INTEGER,
594
+ body TEXT,
595
+ docstring TEXT,
596
+ parent TEXT NOT NULL DEFAULT ''
597
+ );
598
+ CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
599
+ CREATE INDEX IF NOT EXISTS idx_symbols_file ON symbols(file_path);
600
+ CREATE INDEX IF NOT EXISTS idx_symbols_name_kind ON symbols(name, kind);
601
+ CREATE INDEX IF NOT EXISTS idx_symbols_file_folded ON symbols(TG_LOWER(file_path));
602
+ CREATE INDEX IF NOT EXISTS idx_symbols_file_name_folded ON symbols(TG_LOWER(file_path), name);
603
+ -- Partial index backing checkSymbolBodySize (symbol_body_probe.ts), which every SessionStart hook runs.
604
+ -- Its predicate cannot be served by any index above, so the check had to read the whole symbols
605
+ -- table -- 226 MB / 231324 rows here, 229 ms per session start, and the early-exit LIMIT 1 never
606
+ -- fires on a healthy index because there is nothing to find. Indexing the *violating* rows only
607
+ -- makes the check a lookup into a b-tree that is empty on a healthy index: measured 229 ms -> 0.0
608
+ -- ms, 4 KB on disk, and no measurable insert cost (-0.2%, within noise, over 40000 real rows),
609
+ -- because SQLite evaluates the predicate and skips the b-tree write for every row under the cap.
610
+ -- SQLite uses a partial index only where the query's WHERE implies the index's, so the probe in
611
+ -- symbol_body_probe.ts spells its comparison the same way and against the same constant. That makes the
612
+ -- threshold part of the stored schema -- see SYMBOL_BODY_CHAR_CAP in constants.ts for what
613
+ -- changing it requires. A query with a lower threshold correctly gets a full scan instead, so no
614
+ -- other reader can be served stale rows by this index.
615
+ CREATE INDEX IF NOT EXISTS idx_symbols_oversized_body ON symbols(id) WHERE LENGTH(body) > ${SYMBOL_BODY_CHAR_CAP};
616
+
617
+ CREATE TABLE IF NOT EXISTS refs (
618
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
619
+ file_path TEXT,
620
+ name TEXT,
621
+ line INTEGER,
622
+ col INTEGER,
623
+ context TEXT
624
+ );
625
+ CREATE INDEX IF NOT EXISTS idx_refs_name ON refs(name);
626
+ CREATE INDEX IF NOT EXISTS idx_refs_file ON refs(file_path);
627
+ CREATE INDEX IF NOT EXISTS idx_refs_file_folded ON refs(TG_LOWER(file_path));
628
+ CREATE INDEX IF NOT EXISTS idx_refs_file_name_folded ON refs(TG_LOWER(file_path), name);
629
+
630
+ CREATE TABLE IF NOT EXISTS chunks (
631
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
632
+ file_path TEXT,
633
+ start_line INTEGER,
634
+ end_line INTEGER,
635
+ text TEXT,
636
+ kind TEXT
637
+ );
638
+ CREATE INDEX IF NOT EXISTS idx_chunks_file ON chunks(file_path);
639
+ CREATE INDEX IF NOT EXISTS idx_chunks_file_folded ON chunks(TG_LOWER(file_path));
640
+ CREATE INDEX IF NOT EXISTS idx_chunks_file_kind_folded ON chunks(TG_LOWER(file_path), kind);
641
+
642
+ -- Tracks every project root a hook has ever seen an edit for, so the worker's periodic sweep
643
+ -- (sweepKnownRoots in index_prune.ts) knows which roots to auto-prune without scanning the
644
+ -- entire shared files table for distinct top-level directories on every cycle. Purely additive
645
+ -- (no SCHEMA_VERSION bump needed): last_seen_ms is refreshed on every observed edit,
646
+ -- first_missing_ms is set the first sweep that finds the root unreachable and cleared the
647
+ -- moment it's seen reachable again -- see sweepKnownRoots' grace-period logic.
648
+ CREATE TABLE IF NOT EXISTS known_roots (
649
+ root TEXT PRIMARY KEY,
650
+ last_seen_ms REAL NOT NULL,
651
+ first_missing_ms REAL
652
+ );
653
+
654
+ -- Resume point for a budget-truncated reconcile sweep (reconcile.ts), one row per project root. A
655
+ -- project too large to finish a sweep inside DEFAULT_RECONCILE_BUDGET_MS would otherwise scan the
656
+ -- same deterministic (git ls-files) prefix every session forever, leaving every file after the
657
+ -- budget cutoff permanently unchecked. last_scanned_path is the last tracked file the sweep
658
+ -- finished examining before its budget ran out; the next sweep rotates its scan order to resume
659
+ -- right after that file, wrapping back to the start, so repeated truncated sweeps eventually cover
660
+ -- the whole project. Cleared (row deleted) the moment a sweep completes a full lap without running
661
+ -- out of budget. Purely additive (no SCHEMA_VERSION bump needed): a missing row just means "start
662
+ -- from the beginning", the same as a fresh database.
663
+ CREATE TABLE IF NOT EXISTS reconcile_cursor (
664
+ root TEXT PRIMARY KEY,
665
+ last_scanned_path TEXT NOT NULL,
666
+ updated_at REAL NOT NULL
667
+ );
668
+
669
+ -- Projects indexed on purpose, see indexed_roots.ts.
670
+ CREATE TABLE IF NOT EXISTS indexed_roots (
671
+ root TEXT PRIMARY KEY,
672
+ indexed_ms REAL NOT NULL
673
+ );
674
+
675
+ -- Cross-cache full-text search index for 'token-goat recall' (recall_index.ts). One row
676
+ -- per bash-output/web-output/mcp-output blob-store entry (see disk_cache.ts), refreshed
677
+ -- in place (ON CONFLICT DO UPDATE) whenever storeBashOutput/storeWebOutput/storeMcpOutput
678
+ -- write that entry, so recall never needs a separate rebuild step. row_id is a plain
679
+ -- surrogate integer key -- entry_id is the real blob-store id (bash/mcp ids are hex,
680
+ -- web ids are the cache's own scheme) and is not unique on its own since bash-output and
681
+ -- mcp-output ids share one namespace (BASH_OUTPUT_SUBDIR) while web-output ids are a
682
+ -- separate namespace; cache_type disambiguates.
683
+ CREATE TABLE IF NOT EXISTS cache_recall (
684
+ row_id INTEGER PRIMARY KEY AUTOINCREMENT,
685
+ cache_type TEXT NOT NULL,
686
+ entry_id TEXT NOT NULL,
687
+ label TEXT,
688
+ content TEXT,
689
+ stored_at REAL,
690
+ UNIQUE(cache_type, entry_id)
691
+ );
692
+ CREATE INDEX IF NOT EXISTS idx_cache_recall_type ON cache_recall(cache_type);
693
+
694
+ -- Per-emission ledger for 'token-goat hint-stats' (hint_stats.ts). One row per hint
695
+ -- emission event (a hook returning a 'context' HookOutput classified as a discretionary
696
+ -- efficiency nudge, as opposed to a mandatory informational injection -- see hint_stats.ts's
697
+ -- doc comment for the exact category list and what is deliberately excluded). correlator is a
698
+ -- file-path/output-id pointer the hint carries, used to check
699
+ -- whether a later Bash tool call in the same session actually followed the hint's specific
700
+ -- pointer (see resolvePendingHintsForEvent). Supplied by the hint builder itself via
701
+ -- HookOutput's correlators field wherever one exists, and only fallen back to a regex scrape
702
+ -- of the rendered hint text (hint_stats.ts's extractPathCorrelator, whose doc comment records
703
+ -- what that scrape actually measured) where it does not. A row can name SEVERAL files -- the
704
+ -- sed/awk line-range hint joins one per file into one emission -- stored newline-separated and
705
+ -- matched on any member; see joinCorrelators. NULL when no pointer could be supplied or extracted,
706
+ -- in which case the row is inserted already resolved and observable=0: there is nothing a later
707
+ -- command could do that this row would recognize, so no verdict about it was ever observed.
708
+ -- observable=1 is the normal case. Efficacy counts only observable rows, numerator and denominator
709
+ -- both, because an unobservable row's acted_on is a fabrication in whichever direction the code
710
+ -- picked and averaging fabrications into a measured rate is how 174 of bash_redirect's 698 rows
711
+ -- came to hold its efficacy down to 0.86% when the observed figure was 1.15%. calls_remaining is
712
+ -- the countdown of subsequent tool-use events still eligible to resolve this row before it is
713
+ -- considered timed out. A displayed, observable row with resolved=0 is pending: nothing has
714
+ -- scored it yet. Nothing sweeps these on a clock, so a session that ends mid-window leaves its
715
+ -- rows pending for good, and efficacy and suppression count only resolved rows -- counting a
716
+ -- pending one as not acted on is what muted bash_redirect at 1 in 11 when its scored rows stood
717
+ -- at 1 in 6. displayed=0 marks a detection that never reached the agent -- auto-
718
+ -- suppressed, or declined by a hint's own net-benefit gate -- recorded so the ledger can answer
719
+ -- how often a hint fired at all, which it previously could not: a suppressed detection left no
720
+ -- trace, so a fully-muted category was indistinguishable from one that had stopped triggering.
721
+ -- Such a row is always observable=0 and bytes_emitted=0: nothing was shown, so nothing was spent
722
+ -- and no verdict was available. It shares hint_emissions' 180-day retention (pruneHintEmissions
723
+ -- in stats.ts deletes on emitted_at alone), deliberately unlike hint_suppression_probes, which is
724
+ -- one upserted row per key rather than a growing log and is therefore never pruned.
725
+ CREATE TABLE IF NOT EXISTS hint_emissions (
726
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
727
+ category TEXT NOT NULL,
728
+ session_id TEXT NOT NULL,
729
+ harness TEXT NOT NULL,
730
+ correlator TEXT,
731
+ emitted_at REAL NOT NULL,
732
+ resolved INTEGER NOT NULL DEFAULT 0,
733
+ acted_on INTEGER NOT NULL DEFAULT 0,
734
+ calls_remaining INTEGER NOT NULL DEFAULT 0,
735
+ bytes_emitted INTEGER,
736
+ observable INTEGER NOT NULL DEFAULT 1,
737
+ displayed INTEGER NOT NULL DEFAULT 1
738
+ );
739
+ CREATE INDEX IF NOT EXISTS idx_hint_emissions_category ON hint_emissions(category);
740
+ CREATE INDEX IF NOT EXISTS idx_hint_emissions_session_pending ON hint_emissions(session_id, resolved);
741
+
742
+ -- Manual efficacy votes for a hint category (token-goat hint-stats --mark-effective/--mark-ineffective),
743
+ -- kept separate from hint_emissions' automatic acted_on signal so the two are never silently
744
+ -- blended -- see hint_stats.ts's doc comment on why some categories only support this manual signal.
745
+ CREATE TABLE IF NOT EXISTS hint_manual_marks (
746
+ category TEXT PRIMARY KEY,
747
+ effective_count INTEGER NOT NULL DEFAULT 0,
748
+ ineffective_count INTEGER NOT NULL DEFAULT 0
749
+ );
750
+
751
+ -- Durable counter backing hint_stats.ts's backoff-threshold probe-recovery schedule: how many
752
+ -- CONSECUTIVE suppressed occasions have elapsed for (category, harness) since a hint in this
753
+ -- category was last actually shown (either organically, because shouldSuppress no longer holds,
754
+ -- or via a prior probe). shouldSuppress itself stays a pure function of hint_emissions -- this
755
+ -- table exists only because a suppressed occasion is deliberately never written to
756
+ -- hint_emissions (see that table's own comment), so without a separate durable counter here
757
+ -- there would be no way to know "how many suppressed occasions have we seen" across the
758
+ -- short-lived hook CLI processes that call applyHintTracking. Keyed by (category, harness), not
759
+ -- category alone, to match shouldSuppress/categoryStats' own per-harness scoping -- unlike
760
+ -- hint_manual_marks (a human-entered vote, deliberately not harness-split).
761
+ CREATE TABLE IF NOT EXISTS hint_suppression_probes (
762
+ category TEXT NOT NULL,
763
+ harness TEXT NOT NULL,
764
+ streak INTEGER NOT NULL DEFAULT 0,
765
+ PRIMARY KEY (category, harness)
766
+ );
767
+
768
+ -- Free-text architecture/rationale notes (the "why" layer -- see notes.ts), attached either to
769
+ -- a whole file (symbol = '') or to one specific indexed symbol within it (symbol = that
770
+ -- symbol's name). '' rather than NULL for the whole-file case because SQLite's UNIQUE treats
771
+ -- NULLs as pairwise-distinct (never conflicting with each other), which would let note-add
772
+ -- accumulate unlimited duplicate whole-file notes for the same file instead of upserting one;
773
+ -- '' is a real, comparable value so UNIQUE(file_path, symbol) enforces "at most one note per
774
+ -- attachment point" for both cases identically. 'fingerprint' is a SHA-256 digest (see
775
+ -- fingerprintContent in fingerprint.ts) captured at write time of exactly what the note
776
+ -- describes -- the resolved symbol's current body text for a symbol-scoped note, or a stable
777
+ -- digest of the file's current top-level symbol manifest (name:kind:line-range per symbol,
778
+ -- sorted) for a file-scoped note -- so 'token-goat note-list --stale-only' can recompute the
779
+ -- same fingerprint against the live index later and flag a mismatch (see notes.ts's
780
+ -- isNoteStale). Staleness detection is purely advisory: nothing here ever auto-rewrites or
781
+ -- deletes a note's content, only flags that the code it describes has moved since it was
782
+ -- written -- a human/agent re-review decides what to do with a stale note.
783
+ CREATE TABLE IF NOT EXISTS notes (
784
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
785
+ file_path TEXT NOT NULL,
786
+ symbol TEXT NOT NULL DEFAULT '',
787
+ content TEXT NOT NULL,
788
+ fingerprint TEXT NOT NULL,
789
+ created_at REAL NOT NULL,
790
+ updated_at REAL NOT NULL,
791
+ UNIQUE(file_path, symbol)
792
+ );
793
+ CREATE INDEX IF NOT EXISTS idx_notes_file_folded ON notes(TG_LOWER(file_path));
794
+
795
+ -- Baseline for skill_version_drift.ts's one-shot nudge: the token-goat CLI version (and its
796
+ -- flat command-name set, JSON-encoded) active the moment the token-goat skill's body was
797
+ -- last (re)loaded into this session -- see hooks_skill.ts's postSkillHandler. A session that
798
+ -- keeps running after the CLI is upgraded has no other way to learn that new surgical-read
799
+ -- commands now exist (the skill only re-announces itself on an explicit reload), so
800
+ -- checkSkillVersionDrift compares this snapshot against the live command set on each user turn
801
+ -- and fires the nudge exactly once (notified_at) per (re)load. session_id is the primary key
802
+ -- because only one skill (token-goat) is ever tracked here.
803
+ CREATE TABLE IF NOT EXISTS skill_version_snapshots (
804
+ session_id TEXT PRIMARY KEY,
805
+ skill_name TEXT NOT NULL,
806
+ loaded_version TEXT NOT NULL,
807
+ loaded_commands_json TEXT NOT NULL,
808
+ notified_at REAL
809
+ );
810
+
811
+ -- Which embedding stack produced the vectors currently in chunk_vectors -- the model, its
812
+ -- pinned revision, and the inference runtime (see embeddingProvenance in embeddings.ts). The
813
+ -- vector table itself is vec0(rowid, embedding) and has nowhere to record this, so without
814
+ -- this row a database that was embedded by one stack and then added to by another holds two
815
+ -- incomparable sets of vectors under one index, with nothing able to tell them apart. That is
816
+ -- not hypothetical: global.db is machine-wide across every project on the machine (see
817
+ -- constants.ts), so upgrading the runtime, or changing the model or its pinned revision, mixes
818
+ -- old and new vectors for as long as the old files go untouched. Measured drift between two
819
+ -- runtime versions of the same quantized model is 0.9925-0.9978 cosine on the final vector --
820
+ -- small, but enough to reorder near-ties, and invisible to every existing check.
821
+ --
822
+ -- Single-row by construction (the CHECK pins the key), because there is exactly one vector
823
+ -- table per database. An EMPTY table on a database that already holds chunks means the vectors
824
+ -- predate this stamp and their provenance is unknowable -- see ensureEmbeddingProvenance, which
825
+ -- treats that exactly like a mismatch. That is what makes this work without a migration step.
826
+ CREATE TABLE IF NOT EXISTS embedding_provenance (
827
+ id INTEGER PRIMARY KEY CHECK (id = 1),
828
+ provenance TEXT NOT NULL
829
+ );
830
+ `;
831
+ var FTS_TOKENIZER = "unicode61 remove_diacritics 2";
832
+ var FTS_SQL = `
833
+ CREATE VIRTUAL TABLE IF NOT EXISTS symbols_fts USING fts5(
834
+ name,
835
+ body,
836
+ docstring,
837
+ content='symbols',
838
+ content_rowid='id',
839
+ tokenize='${FTS_TOKENIZER}'
840
+ );
841
+ CREATE TRIGGER IF NOT EXISTS symbols_ai AFTER INSERT ON symbols BEGIN
842
+ INSERT INTO symbols_fts(rowid, name, body, docstring)
843
+ VALUES (new.id, new.name, new.body, new.docstring);
844
+ END;
845
+ CREATE TRIGGER IF NOT EXISTS symbols_ad AFTER DELETE ON symbols BEGIN
846
+ INSERT INTO symbols_fts(symbols_fts, rowid, name, body, docstring)
847
+ VALUES ('delete', old.id, old.name, old.body, old.docstring);
848
+ END;
849
+ CREATE TRIGGER IF NOT EXISTS symbols_au AFTER UPDATE ON symbols BEGIN
850
+ INSERT INTO symbols_fts(symbols_fts, rowid, name, body, docstring)
851
+ VALUES ('delete', old.id, old.name, old.body, old.docstring);
852
+ INSERT INTO symbols_fts(rowid, name, body, docstring)
853
+ VALUES (new.id, new.name, new.body, new.docstring);
854
+ END;
855
+
856
+ -- Content-linked FTS5 mirror of cache_recall (recall_index.ts), same shape as symbols_fts
857
+ -- above. An INSERT ... ON CONFLICT DO UPDATE against cache_recall fires the AFTER UPDATE
858
+ -- trigger (not AFTER INSERT) on the conflicting row, same as any other SQLite upsert, so the
859
+ -- delete+reinsert pattern below keeps the fts index correct on a re-indexed (overwritten)
860
+ -- entry, not just a brand-new one.
861
+ CREATE VIRTUAL TABLE IF NOT EXISTS cache_recall_fts USING fts5(
862
+ label,
863
+ content,
864
+ content='cache_recall',
865
+ content_rowid='row_id',
866
+ tokenize='${FTS_TOKENIZER}'
867
+ );
868
+ CREATE TRIGGER IF NOT EXISTS cache_recall_ai AFTER INSERT ON cache_recall BEGIN
869
+ INSERT INTO cache_recall_fts(rowid, label, content)
870
+ VALUES (new.row_id, new.label, new.content);
871
+ END;
872
+ CREATE TRIGGER IF NOT EXISTS cache_recall_ad AFTER DELETE ON cache_recall BEGIN
873
+ INSERT INTO cache_recall_fts(cache_recall_fts, rowid, label, content)
874
+ VALUES ('delete', old.row_id, old.label, old.content);
875
+ END;
876
+ CREATE TRIGGER IF NOT EXISTS cache_recall_au AFTER UPDATE ON cache_recall BEGIN
877
+ INSERT INTO cache_recall_fts(cache_recall_fts, rowid, label, content)
878
+ VALUES ('delete', old.row_id, old.label, old.content);
879
+ INSERT INTO cache_recall_fts(rowid, label, content)
880
+ VALUES (new.row_id, new.label, new.content);
881
+ END;
882
+ `;
883
+ var SCHEMA_VERSION = 17;
884
+ function alterTableIdempotent(conn, sql) {
885
+ try {
886
+ conn.exec(sql);
887
+ } catch (err) {
888
+ if (!(err instanceof Error) || !/duplicate column/i.test(err.message)) throw err;
889
+ }
890
+ }
891
+ function purgeDotenvEmbeddings(conn) {
892
+ let paths;
893
+ try {
894
+ paths = conn.prepare("SELECT DISTINCT file_path FROM chunks").all().map((r) => r.file_path).filter(isDotenvPath);
895
+ } catch {
896
+ return;
897
+ }
898
+ if (paths.length === 0) return;
899
+ for (const p of paths) {
900
+ try {
901
+ conn.prepare("DELETE FROM chunk_vectors WHERE rowid IN (SELECT id FROM chunks WHERE file_path = ?)").run(p);
902
+ } catch {
903
+ }
904
+ conn.prepare("DELETE FROM chunks WHERE file_path = ?").run(p);
905
+ try {
906
+ conn.prepare("UPDATE files SET embed_sha = NULL WHERE path = ?").run(p);
907
+ } catch {
908
+ }
909
+ }
910
+ }
911
+ function rebuildFtsAtCurrentTokenizer(conn) {
912
+ try {
913
+ const declarations = conn.prepare("SELECT sql FROM sqlite_master WHERE name IN ('symbols_fts','cache_recall_fts')").all();
914
+ const current = declarations.map((d) => /tokenize\s*=\s*'([^']*)'/.exec(d.sql ?? "")?.[1] ?? "");
915
+ if (current.length === 2 && current.every((t) => t === FTS_TOKENIZER)) return;
916
+ conn.transaction(() => {
917
+ conn.exec("DROP TABLE IF EXISTS symbols_fts; DROP TABLE IF EXISTS cache_recall_fts;");
918
+ conn.exec(FTS_SQL);
919
+ conn.exec("INSERT INTO symbols_fts(symbols_fts) VALUES('rebuild');");
920
+ conn.exec("INSERT INTO cache_recall_fts(cache_recall_fts) VALUES('rebuild');");
921
+ }).immediate();
922
+ } catch {
923
+ }
924
+ }
925
+ var MIGRATIONS = {
926
+ // v1 -> v2: adds files.embed_sha, tracked separately from files.sha so embedding freshness can be gated independently of parse freshness (see makeIndexer in worker.ts). A pre-existing v1 database's `files` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has the column from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, so a genuine ALTER TABLE failure is never silently lost.
927
+ 1: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN embed_sha TEXT"),
928
+ // v2 -> v3: adds files.retry_count, a durable per-path counter for consecutive transient-read-failure requeues (see MAX_TRANSIENT_RETRIES / requeueDirtyPath / clearRetryCount in worker.ts). Previously this counter lived only in an in-memory Map inside worker.ts, which meant the retry-count reset -- run at the time in the short-lived hook CLI process -- could never actually reach the long-lived detached daemon process's own copy of that Map: they are different Node processes with no shared memory, so the reset was a silent no-op in the real deployed topology. Persisting the counter in `files` makes it visible to both processes via the one thing they do share: the index DB. Same swallow-duplicate-column pattern as v1 -> v2 above. Superseded since: the counter shares the DB but no longer the table, because holding it in `files` meant a never-indexed path got a `files` row the moment one read failed, and three readers take a `files` row to mean the file is indexed -- see the `index_retries` comment in SCHEMA_SQL above. This column is still created for databases that predate that move; nothing reads or writes it.
929
+ 2: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN retry_count INTEGER NOT NULL DEFAULT 0"),
930
+ // v8 -> v9: adds symbols.parent (see SCHEMA_VERSION comment above for why). A pre-existing v8 database's `symbols` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has the column from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v1 -> v2 / v2 -> v3 above.
931
+ 8: (conn) => alterTableIdempotent(conn, "ALTER TABLE symbols ADD COLUMN parent TEXT NOT NULL DEFAULT ''"),
932
+ // v9 -> v10: adds hint_emissions.bytes_emitted (see SCHEMA_VERSION comment above for why). A pre-existing v9 database's `hint_emissions` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has the column from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v1 -> v2 / v2 -> v3 / v8 -> v9 above.
933
+ 9: (conn) => alterTableIdempotent(conn, "ALTER TABLE hint_emissions ADD COLUMN bytes_emitted INTEGER"),
934
+ // v10 -> v11: purge chunks (and their vectors) for dotenv files. Until this version, a tracked `.env` was chunked and embedded verbatim on the git path, so `semantic` returned its values -- see dotenv_redact.ts. Redacting from now on is not enough on its own: the embed-freshness gate (isEmbedFresh in parser.ts) skips a file whose bytes have not changed, so an already-indexed .env would have kept serving its pre-fix chunks indefinitely. Deleting the rows here both removes the stored secrets and, by clearing embed_sha, makes the next drain re-embed the file through the redacting path.
935
+ 10: purgeDotenvEmbeddings,
936
+ // v12 -> v13: adds files.parser_sha, the digest of the extraction logic that produced this file's rows, tracked separately from files.sha for the same reason embed_sha is -- content freshness and parse freshness are different questions, and the content sha alone could only ever answer the first. A pre-existing v12 database's `files` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has it from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v1 -> v2 / v2 -> v3 / v8 -> v9 / v9 -> v10 above. Deliberately left NULL for every existing row rather than backfilled with the current fingerprint: NULL is the truthful answer (nobody recorded which parser wrote those rows), and it is also the answer that makes the freshness gates reparse them once, which is exactly what a database indexed by an older parser needs. v13 -> v14: changes both FTS5 tables' tokenizer to `unicode61 remove_diacritics 2`, so a search for `Noi` or `Viet` finds `Hà Nội` and `Việt Nam` -- combining marks that `remove_diacritics 1`, FTS5's default, leaves in place. This is the first schema change that `CREATE VIRTUAL TABLE IF NOT EXISTS` cannot express at all rather than merely cannot express on a populated table: against an existing virtual table that statement is a silent no-op, so without MIGRATIONS[13] the new tokenizer would reach only databases created after this release. The step drops both tables, re-runs FTS_SQL to re-create them at the current declaration, and rebuilds each from its content table.
937
+ 12: (conn) => alterTableIdempotent(conn, "ALTER TABLE files ADD COLUMN parser_sha TEXT"),
938
+ // v13 -> v14: re-creates both FTS5 tables at the tokenizer FTS_SQL currently declares (see the SCHEMA_VERSION comment above for why no `IF NOT EXISTS` form can do this).
939
+ 13: rebuildFtsAtCurrentTokenizer,
940
+ // v14 -> v15: adds hint_emissions.observable and backfills it from the column that always implied it. A pre-existing v14 database's `hint_emissions` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has it from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v8 -> v9 / v9 -> v10 / v12 -> v13 above. The backfill is exact rather than a guess: until this version a NULL correlator was the only way a row could be born with nothing to wait for, so `correlator IS NULL` identifies precisely the population the new column names. It runs unconditionally after the ALTER because alterTableIdempotent swallows the duplicate-column case, and re-running the UPDATE on an already-migrated database sets the same rows to the same values.
941
+ 14: (conn) => {
942
+ alterTableIdempotent(conn, "ALTER TABLE hint_emissions ADD COLUMN observable INTEGER NOT NULL DEFAULT 1");
943
+ conn.prepare("UPDATE hint_emissions SET observable = 0 WHERE correlator IS NULL").run();
944
+ },
945
+ // v15 -> v16: adds hint_emissions.displayed (see the SCHEMA_VERSION comment above for why). A pre-existing v15 database's `hint_emissions` table predates the column, so it needs an explicit ALTER TABLE here; a brand-new database already has it from SCHEMA_SQL's CREATE TABLE above, so the ALTER TABLE would fail with "duplicate column name" there -- swallow exactly that error and rethrow anything else, same pattern as v8 -> v9 / v9 -> v10 / v12 -> v13 / v14 -> v15 above. No backfill, unlike v14 -> v15: until this version a row was only ever written when a hint was actually shown, so the column's own DEFAULT 1 is already the truth for every existing row, and an UPDATE would be a no-op dressed up as a migration step.
946
+ 15: (conn) => alterTableIdempotent(conn, "ALTER TABLE hint_emissions ADD COLUMN displayed INTEGER NOT NULL DEFAULT 1"),
947
+ // v16 -> v17: backfills indexed_roots.
948
+ 16: backfillIndexedRoots
949
+ };
950
+ function backfillIndexedRoots(conn) {
951
+ const scope = projectScopeClause("file_path");
952
+ const hasSymbols = conn.prepare(`SELECT 1 FROM symbols WHERE ${scope.clause} LIMIT 1`);
953
+ const insert = conn.prepare("INSERT OR IGNORE INTO indexed_roots (root, indexed_ms) VALUES (?, ?)");
954
+ const roots = conn.prepare("SELECT root, last_seen_ms FROM known_roots").all();
955
+ for (const { root, last_seen_ms } of roots) {
956
+ if (hasSymbols.get(...scope.params(root)) !== void 0) insert.run(normalizePath(root), last_seen_ms);
957
+ }
958
+ }
959
+ function runMigrations(conn, fromVersion, toVersion) {
960
+ for (let v = fromVersion; v < toVersion; v++) {
961
+ MIGRATIONS[v]?.(conn);
962
+ }
963
+ }
964
+ var WAL_SWITCH_DEADLINE_MS = 15e3;
965
+ function enableWalWithRetry(conn, budgetMs = WAL_SWITCH_DEADLINE_MS, stopOn) {
966
+ const deadline = Date.now() + budgetMs;
967
+ let lastError;
968
+ for (; ; ) {
969
+ try {
970
+ const mode = conn.pragma("journal_mode = WAL", { simple: true });
971
+ if (String(mode).toLowerCase() === "wal") return;
972
+ lastError = new Error(`got: ${String(mode)}`);
973
+ } catch (e) {
974
+ lastError = e;
975
+ if (isDamagedDatabaseError(e) || stopOn?.(e) === true) throw walSwitchError(e);
976
+ }
977
+ try {
978
+ if (String(conn.pragma("journal_mode", { simple: true })).toLowerCase() === "wal") return;
979
+ } catch {
980
+ }
981
+ if (Date.now() >= deadline) throw walSwitchError(lastError);
982
+ sleepSync(25);
983
+ }
984
+ }
985
+ function isDamagedDatabaseError(e) {
986
+ const code = e?.code;
987
+ return typeof code === "string" && /^SQLITE_(?:NOTADB|CORRUPT)/.test(code);
988
+ }
989
+ function walSwitchError(lastError) {
990
+ return new Error(`db: failed to enable WAL mode (${lastError instanceof Error ? lastError.message : String(lastError)})`, { cause: lastError });
991
+ }
992
+ var HOOK_DB_WAIT_MS = 250;
993
+ var _hookWaits = false;
994
+ function limitDbWaitsForHook(on = true) {
995
+ _hookWaits = on;
996
+ }
997
+ function connectionWaitBudgets() {
998
+ return _hookWaits ? { busyMs: HOOK_DB_WAIT_MS, walSwitchMs: HOOK_DB_WAIT_MS } : { busyMs: 15e3, walSwitchMs: WAL_SWITCH_DEADLINE_MS };
999
+ }
1000
+ function initConnection(conn, walStopOn) {
1001
+ const waits = connectionWaitBudgets();
1002
+ conn.pragma(`busy_timeout = ${waits.busyMs}`);
1003
+ enableWalWithRetry(conn, waits.walSwitchMs, walStopOn);
1004
+ conn.pragma("synchronous = NORMAL");
1005
+ conn.pragma("cache_size = -32000");
1006
+ conn.pragma("temp_store = MEMORY");
1007
+ conn.pragma("mmap_size = 134217728");
1008
+ registerTgLower(conn);
1009
+ const storedVersion = Number(conn.pragma("user_version", { simple: true }));
1010
+ if (storedVersion > SCHEMA_VERSION) throw newerSchemaError(storedVersion);
1011
+ conn.exec(SCHEMA_SQL);
1012
+ try {
1013
+ conn.exec(FTS_SQL);
1014
+ } catch {
1015
+ }
1016
+ if (loadSqliteVec(conn)) {
1017
+ try {
1018
+ conn.exec(
1019
+ `CREATE VIRTUAL TABLE IF NOT EXISTS chunk_vectors USING vec0(
1020
+ embedding float[384]
1021
+ );`
1022
+ );
1023
+ } catch {
1024
+ }
1025
+ }
1026
+ if (storedVersion < SCHEMA_VERSION) {
1027
+ runMigrations(conn, storedVersion, SCHEMA_VERSION);
1028
+ conn.pragma(`user_version = ${SCHEMA_VERSION}`);
1029
+ }
1030
+ }
1031
+ function newerSchemaError(storedVersion) {
1032
+ return new Error(
1033
+ `db: index schema version ${storedVersion} is newer than this token-goat build supports (expected ${SCHEMA_VERSION}). Update token-goat, or delete the stale index database and let it rebuild.`
1034
+ );
1035
+ }
1036
+ function registerTgLower(conn) {
1037
+ conn.function(
1038
+ "TG_LOWER",
1039
+ { deterministic: true },
1040
+ (value) => value === null ? null : foldCase(String(value))
1041
+ );
1042
+ }
1043
+ function loadSqliteVec(conn) {
1044
+ try {
1045
+ const sqliteVec = _require("sqlite-vec");
1046
+ sqliteVec.load(conn);
1047
+ return true;
1048
+ } catch {
1049
+ return false;
1050
+ }
1051
+ }
1052
+ var _readOnlyFallback;
1053
+ var _readOnlyConnections = /* @__PURE__ */ new Map();
1054
+ function allowReadOnlyIndex(onFirstUse) {
1055
+ _readOnlyFallback = { onFirstUse, used: false };
1056
+ }
1057
+ function isReadOnlyDb(dbPath) {
1058
+ if (_readOnlyFallback === void 0) return false;
1059
+ try {
1060
+ getDb(dbPath, { create: false });
1061
+ } catch {
1062
+ return false;
1063
+ }
1064
+ return _readOnlyConnections.has(connectionKey(dbPath).key);
1065
+ }
1066
+ var WRITE_ACCESS_SQLITE_CODE_RE = /^SQLITE_(?:CANTOPEN|READONLY|PERM|IOERR_ACCESS|IOERR_SHMOPEN)/;
1067
+ var WRITE_ACCESS_FS_CODES = /* @__PURE__ */ new Set(["EACCES", "EPERM", "EROFS"]);
1068
+ function isWriteAccessError(e) {
1069
+ for (let cur = e, depth = 0; cur instanceof Error && depth < 4; cur = cur.cause, depth++) {
1070
+ const code = cur.code;
1071
+ if (typeof code === "string" && (WRITE_ACCESS_SQLITE_CODE_RE.test(code) || WRITE_ACCESS_FS_CODES.has(code))) return true;
1072
+ }
1073
+ return false;
1074
+ }
1075
+ function writeRefusal(resolved) {
1076
+ let fd;
1077
+ try {
1078
+ fd = fs2.openSync(resolved, "r+");
1079
+ } catch (e) {
1080
+ return isWriteAccessError(e) ? e : void 0;
1081
+ }
1082
+ fs2.closeSync(fd);
1083
+ return void 0;
1084
+ }
1085
+ function openReadOnlyAt(resolved, immutable, busyTimeoutMs = 15e3) {
1086
+ const conn = new Database(resolved, { readonly: true, fileMustExist: true, immutable });
1087
+ try {
1088
+ conn.pragma(`busy_timeout = ${busyTimeoutMs}`);
1089
+ return { conn, storedVersion: Number(conn.pragma("user_version", { simple: true })) };
1090
+ } catch (e) {
1091
+ try {
1092
+ conn.close();
1093
+ } catch {
1094
+ }
1095
+ throw e;
1096
+ }
1097
+ }
1098
+ function openIndexReadOnly(resolved) {
1099
+ let opened;
1100
+ try {
1101
+ opened = openReadOnlyAt(resolved, false);
1102
+ } catch (e) {
1103
+ if (!isWriteAccessError(e)) throw e;
1104
+ opened = openReadOnlyAt(resolved, true);
1105
+ }
1106
+ const { conn, storedVersion } = opened;
1107
+ try {
1108
+ if (storedVersion > SCHEMA_VERSION) throw newerSchemaError(storedVersion);
1109
+ if (storedVersion < SCHEMA_VERSION) {
1110
+ throw new Error(
1111
+ `db: index schema version ${storedVersion} predates this token-goat build (expected ${SCHEMA_VERSION}) and cannot be upgraded without write access. Run token-goat once where its data directory is writable.`
1112
+ );
1113
+ }
1114
+ registerTgLower(conn);
1115
+ loadSqliteVec(conn);
1116
+ } catch (e) {
1117
+ try {
1118
+ conn.close();
1119
+ } catch {
1120
+ }
1121
+ throw e;
1122
+ }
1123
+ return conn;
1124
+ }
1125
+ function resolveDbPath(dbPath) {
1126
+ if (path2.isAbsolute(dbPath)) return dbPath;
1127
+ if (dbPath.includes("/") || dbPath.includes("\\")) return path2.resolve(dbPath);
1128
+ return safeJoin(dataDir(), dbPath);
1129
+ }
1130
+ function connectionKey(dbPath) {
1131
+ const resolved = resolveDbPath(dbPath);
1132
+ return { resolved, key: foldPath(resolved) };
1133
+ }
1134
+ var PROBE_BUSY_TIMEOUT_MS = 250;
1135
+ function withProbeIndex(fn, dbPath) {
1136
+ const resolved = resolveDbPath(dbPath ?? globalDbPath());
1137
+ if (!fs2.existsSync(resolved)) return null;
1138
+ let conn = null;
1139
+ try {
1140
+ let opened;
1141
+ try {
1142
+ opened = openReadOnlyAt(resolved, false, PROBE_BUSY_TIMEOUT_MS);
1143
+ } catch (e) {
1144
+ if (!isWriteAccessError(e)) throw e;
1145
+ opened = openReadOnlyAt(resolved, true, PROBE_BUSY_TIMEOUT_MS);
1146
+ }
1147
+ conn = opened.conn;
1148
+ if (opened.storedVersion !== SCHEMA_VERSION) return null;
1149
+ registerTgLower(conn);
1150
+ return fn(conn);
1151
+ } catch {
1152
+ return null;
1153
+ } finally {
1154
+ if (conn) {
1155
+ try {
1156
+ conn.close();
1157
+ } catch {
1158
+ }
1159
+ }
1160
+ }
1161
+ }
1162
+ var _emptyIndex;
1163
+ function getReadDb(dbPath) {
1164
+ return getDb(dbPath, { create: false });
1165
+ }
1166
+ function getDb(dbPath, opts = {}) {
1167
+ const { resolved, key } = connectionKey(dbPath);
1168
+ const existing = _connections.get(key);
1169
+ if (existing !== void 0) return existing;
1170
+ if (opts.create === false && !fs2.existsSync(resolved)) {
1171
+ if (_emptyIndex === void 0) {
1172
+ _emptyIndex = new Database(":memory:");
1173
+ registerTgLower(_emptyIndex);
1174
+ _emptyIndex.exec(SCHEMA_SQL);
1175
+ try {
1176
+ _emptyIndex.exec(FTS_SQL);
1177
+ } catch {
1178
+ }
1179
+ }
1180
+ return _emptyIndex;
1181
+ }
1182
+ const fallback = _readOnlyFallback;
1183
+ if (fallback !== void 0) {
1184
+ const readOnly = _readOnlyConnections.get(key);
1185
+ if (readOnly !== void 0) return readOnly;
1186
+ const refused = writeRefusal(resolved);
1187
+ if (refused !== void 0) return openReadOnlyFallback(resolved, key, fallback, refused);
1188
+ }
1189
+ const dir = path2.dirname(resolved);
1190
+ try {
1191
+ ensureDirSync(dir);
1192
+ } catch (e) {
1193
+ if (e.code !== "EEXIST" || !fs2.existsSync(dir)) throw e;
1194
+ }
1195
+ let conn;
1196
+ try {
1197
+ conn = new Database(resolved);
1198
+ initConnection(conn, fallback !== void 0 ? isWriteAccessError : void 0);
1199
+ } catch (e) {
1200
+ try {
1201
+ conn?.close();
1202
+ } catch {
1203
+ }
1204
+ if (fallback !== void 0 && isWriteAccessError(e)) return openReadOnlyFallback(resolved, key, fallback, e);
1205
+ throw e;
1206
+ }
1207
+ _connections.set(key, conn);
1208
+ return conn;
1209
+ }
1210
+ function openReadOnlyFallback(resolved, key, fallback, writeError) {
1211
+ let conn;
1212
+ try {
1213
+ conn = openIndexReadOnly(resolved);
1214
+ } catch (e) {
1215
+ throw isWriteAccessError(e) || !fs2.existsSync(resolved) ? writeError : e;
1216
+ }
1217
+ _readOnlyConnections.set(key, conn);
1218
+ if (!fallback.used) {
1219
+ fallback.used = true;
1220
+ fallback.onFirstUse();
1221
+ }
1222
+ return conn;
1223
+ }
1224
+ function closeDb(dbPath) {
1225
+ const { key } = connectionKey(dbPath);
1226
+ for (const cache of [_connections, _readOnlyConnections]) {
1227
+ const conn = cache.get(key);
1228
+ if (conn === void 0) continue;
1229
+ try {
1230
+ conn.close();
1231
+ } catch {
1232
+ }
1233
+ cache.delete(key);
1234
+ }
1235
+ }
1236
+ function closeAllDbs() {
1237
+ try {
1238
+ _emptyIndex?.close();
1239
+ } catch {
1240
+ }
1241
+ _emptyIndex = void 0;
1242
+ for (const cache of [_connections, _readOnlyConnections]) {
1243
+ for (const conn of cache.values()) {
1244
+ try {
1245
+ conn.close();
1246
+ } catch {
1247
+ }
1248
+ }
1249
+ cache.clear();
1250
+ }
1251
+ }
1252
+ registerReset(closeAllDbs, { perRequest: true });
1253
+ registerReset(() => {
1254
+ _readOnlyFallback = void 0;
1255
+ }, { perRequest: true });
1256
+ registerReset(() => {
1257
+ _hookWaits = false;
1258
+ }, { perRequest: true });
1259
+
1260
+ export {
1261
+ extractIni,
1262
+ _lineClosesQuote,
1263
+ _detectOpenQuote,
1264
+ extractEnv,
1265
+ isAblSource,
1266
+ isMatlabSource,
1267
+ isObjcSource,
1268
+ isObjcHeader,
1269
+ isPascalSource,
1270
+ isPerlSource,
1271
+ isPrologSource,
1272
+ FILENAME_LANGUAGE,
1273
+ TREE_SITTER_LANGUAGES,
1274
+ languageHasFlag,
1275
+ languageLabel,
1276
+ fenceFor,
1277
+ basenameImportsExtension,
1278
+ partialRefsReason,
1279
+ refineLanguageByContent,
1280
+ detectLanguageOfFile,
1281
+ detectLanguage,
1282
+ nonTreeSitterLanguageCount,
1283
+ unsupportedLanguageName,
1284
+ isDotenvPath,
1285
+ redactIfDotenv,
1286
+ pathEqClause,
1287
+ pathSuffixClause,
1288
+ projectScopeClause,
1289
+ filePathSpellingsClause,
1290
+ fileRowByIndexKey,
1291
+ limitDbWaitsForHook,
1292
+ allowReadOnlyIndex,
1293
+ isReadOnlyDb,
1294
+ withProbeIndex,
1295
+ getReadDb,
1296
+ getDb,
1297
+ closeDb
1298
+ };