token-goat 2.9.10 → 2.9.12

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 (27) hide show
  1. package/README.md +54 -11
  2. package/SECURITY.md +17 -19
  3. package/THIRD_PARTY_NOTICES.md +667 -0
  4. package/dist/{token-goat-chunk-YFXFUG3L.mjs → token-goat-chunk-4CK445AW.mjs} +1275 -655
  5. package/dist/token-goat-chunk-6F5TLJC7.mjs +3728 -0
  6. package/dist/{token-goat-chunk-ATVQ6IIT.mjs → token-goat-chunk-6NIGPPN6.mjs} +10 -2
  7. package/dist/{token-goat-chunk-SLGOIXKF.mjs → token-goat-chunk-A6QTLAWO.mjs} +85 -328
  8. package/dist/{token-goat-chunk-7ALFFPJW.mjs → token-goat-chunk-GCXX67HM.mjs} +2699 -2175
  9. package/dist/{token-goat-chunk-PA5UXNOT.mjs → token-goat-chunk-GM6QZWCF.mjs} +5088 -7746
  10. package/dist/{token-goat-chunk-DSHYDU53.mjs → token-goat-chunk-HKFOH6JH.mjs} +11 -7
  11. package/dist/{token-goat-chunk-LYWIRYCF.mjs → token-goat-chunk-LHLQFGWQ.mjs} +1 -1
  12. package/dist/token-goat-chunk-LOCOX2ML.mjs +3564 -0
  13. package/dist/token-goat-chunk-OTW7LC4Y.mjs +5942 -0
  14. package/dist/{token-goat-chunk-FQD3OB5W.mjs → token-goat-chunk-QCQUIPIP.mjs} +6068 -1687
  15. package/dist/{token-goat-chunk-DCYHAUW3.mjs → token-goat-chunk-SBYRP3X4.mjs} +69 -37
  16. package/dist/{token-goat-chunk-JS44SRAX.mjs → token-goat-chunk-SF2CKCFQ.mjs} +3 -2
  17. package/dist/{token-goat-chunk-PXWBHSFB.mjs → token-goat-chunk-T6M7DAW3.mjs} +31 -4
  18. package/dist/{token-goat-chunk-ZQODJQTP.mjs → token-goat-chunk-THLHC6QJ.mjs} +354 -180
  19. package/dist/{token-goat-chunk-VCEK77ED.mjs → token-goat-chunk-TOGYS5A7.mjs} +5307 -6350
  20. package/dist/{token-goat-chunk-2TJBY5RD.mjs → token-goat-chunk-VRWX6QYW.mjs} +6 -5
  21. package/dist/token-goat-hook.mjs +6 -5
  22. package/dist/token-goat.core.mjs +8 -6
  23. package/docs/C4_RUNTIME_ARCHITECTURE.md +1 -1
  24. package/docs/cli.md +4 -1
  25. package/docs/install.md +131 -11
  26. package/docs/security.md +2 -0
  27. package/package.json +8 -3
@@ -2,7 +2,7 @@ import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
4
  detectHarness
5
- } from "./token-goat-chunk-VCEK77ED.mjs";
5
+ } from "./token-goat-chunk-TOGYS5A7.mjs";
6
6
  import {
7
7
  init_define_import_meta_env
8
8
  } from "./token-goat-chunk-A37V4PBF.mjs";
@@ -13,6 +13,14 @@ var CLAUDE_CODE_BASH_OUTPUT_CAP_BYTES = 2e4;
13
13
  function bashOutputCapBytes(harness = detectHarness()) {
14
14
  return harness === "claudecode" ? CLAUDE_CODE_BASH_OUTPUT_CAP_BYTES : null;
15
15
  }
16
+ function clipToDeliveryCap(text, reserveBytes) {
17
+ const cap = bashOutputCapBytes();
18
+ const room = cap === null ? null : cap - reserveBytes;
19
+ if (room === null || room <= 0 || Buffer.byteLength(text, "utf-8") <= room) return { text, clipped: false };
20
+ const sliced = Buffer.from(text, "utf-8").subarray(0, room).toString("utf-8");
21
+ const lastNewline = sliced.lastIndexOf("\n");
22
+ return { text: lastNewline > 0 ? sliced.slice(0, lastNewline) : sliced, clipped: true };
23
+ }
16
24
  function deliveredOutputBytes(originalBytes, harness) {
17
25
  const cap = bashOutputCapBytes(harness);
18
26
  if (cap === null) return Math.max(0, originalBytes);
@@ -78,7 +86,7 @@ function canRunWrappedShell() {
78
86
  }
79
87
 
80
88
  export {
81
- bashOutputCapBytes,
89
+ clipToDeliveryCap,
82
90
  deliveredOutputBytes,
83
91
  wrappedShell,
84
92
  canRunWrappedShell
@@ -1,329 +1,43 @@
1
1
  import { createRequire as __cjsRequire } from 'node:module';
2
2
  const require = __cjsRequire(import.meta.url);
3
3
  import {
4
+ PARSER_FINGERPRINT,
4
5
  enqueueDirtyPathSafe,
5
- estimateTokensFromLength,
6
6
  getProjectFileEntries,
7
7
  getTrackedFiles,
8
8
  indexRecallEntry,
9
9
  isBlobStale,
10
10
  loadBlob,
11
11
  storeBlob
12
- } from "./token-goat-chunk-PA5UXNOT.mjs";
12
+ } from "./token-goat-chunk-GM6QZWCF.mjs";
13
+ import {
14
+ estimateTokensFromLength,
15
+ fingerprintFile,
16
+ getDb,
17
+ getDisplayRoot,
18
+ redactSecrets,
19
+ shortFingerprint
20
+ } from "./token-goat-chunk-TOGYS5A7.mjs";
21
+ import {
22
+ registerReset
23
+ } from "./token-goat-chunk-EEIDFMEM.mjs";
13
24
  import {
14
25
  SYMBOL_BODY_CHAR_CAP,
15
- copilotCliMcpToolsDir,
26
+ compileGuardedRegex,
16
27
  countNoun,
28
+ displaySafeJson,
17
29
  displaySafeText,
18
30
  extractErrorMessage,
19
- fingerprintFile,
20
31
  foldPath,
21
- getDb,
22
- getDisplayRoot,
32
+ globalDbPath,
23
33
  normalizePath,
24
- redactSecrets,
25
34
  resolveIndexPath,
26
- shortFingerprint,
27
35
  toDisplayPath
28
- } from "./token-goat-chunk-VCEK77ED.mjs";
29
- import {
30
- registerReset
31
- } from "./token-goat-chunk-EEIDFMEM.mjs";
36
+ } from "./token-goat-chunk-LOCOX2ML.mjs";
32
37
  import {
33
38
  init_define_import_meta_env
34
39
  } from "./token-goat-chunk-A37V4PBF.mjs";
35
40
 
36
- // src/hooks_cli.ts
37
- init_define_import_meta_env();
38
-
39
- // src/copilot_mcp_names.ts
40
- init_define_import_meta_env();
41
-
42
- // src/copilot_mcp_tools.ts
43
- init_define_import_meta_env();
44
- import fs from "node:fs";
45
- import path from "node:path";
46
- function wireShapeLength(tools) {
47
- const wire = tools.map((tool) => {
48
- const out = {};
49
- if (typeof tool.name === "string") out["name"] = tool.name;
50
- if (typeof tool.description === "string") out["description"] = tool.description;
51
- if (tool.inputSchema !== void 0) out["inputSchema"] = tool.inputSchema;
52
- return out;
53
- });
54
- return JSON.stringify(wire).length;
55
- }
56
- function readCopilotMcpTools(dir = copilotCliMcpToolsDir()) {
57
- let entries;
58
- try {
59
- entries = fs.readdirSync(dir);
60
- } catch {
61
- return { cacheFound: false, servers: [], unreadable: 0 };
62
- }
63
- const newest = /* @__PURE__ */ new Map();
64
- let unreadable = 0;
65
- for (const entry of entries) {
66
- if (!entry.toLowerCase().endsWith(".json")) continue;
67
- let parsed;
68
- try {
69
- parsed = JSON.parse(fs.readFileSync(path.join(dir, entry), "utf8"));
70
- } catch {
71
- unreadable += 1;
72
- continue;
73
- }
74
- if (typeof parsed !== "object" || parsed === null) {
75
- unreadable += 1;
76
- continue;
77
- }
78
- const record = parsed;
79
- const serverName = typeof record.serverName === "string" ? record.serverName.trim() : "";
80
- if (serverName === "") {
81
- unreadable += 1;
82
- continue;
83
- }
84
- const tools = Array.isArray(record.tools) ? record.tools : [];
85
- const updatedAt = typeof record.updatedAt === "string" ? record.updatedAt : "";
86
- const definitionBytes = wireShapeLength(tools);
87
- const candidate = {
88
- serverName,
89
- toolCount: tools.length,
90
- definitionBytes,
91
- estimatedTokens: estimateTokensFromLength(definitionBytes),
92
- updatedAt,
93
- // `tools` is an unchecked cast off a JSON array, so every element is
94
- // treated as untrusted here rather than trusted to match CachedTool.
95
- toolNames: tools.map((t) => t?.name).filter((n) => typeof n === "string" && n !== "")
96
- };
97
- const existing = newest.get(serverName);
98
- if (existing === void 0 || candidate.updatedAt > existing.updatedAt) newest.set(serverName, candidate);
99
- }
100
- const servers = [...newest.values()].sort(
101
- (a, b) => b.definitionBytes - a.definitionBytes || a.serverName.localeCompare(b.serverName)
102
- );
103
- return { cacheFound: true, servers, unreadable };
104
- }
105
-
106
- // src/copilot_mcp_names.ts
107
- var _cached = null;
108
- function copilotMcpToolNameMap() {
109
- if (_cached) return _cached;
110
- const map = /* @__PURE__ */ new Map();
111
- try {
112
- for (const server of readCopilotMcpTools().servers) {
113
- for (const toolName of server.toolNames) {
114
- map.set(`${server.serverName}-${toolName}`, `mcp__${server.serverName}__${toolName}`);
115
- }
116
- }
117
- } catch {
118
- }
119
- _cached = map;
120
- return map;
121
- }
122
- function canonicalizeCopilotMcpToolName(toolName) {
123
- return copilotMcpToolNameMap().get(toolName) ?? toolName;
124
- }
125
-
126
- // src/hooks_cli.ts
127
- var _LOG = {
128
- warn: (msg, ...args) => console.warn(`[hooks_cli] ${msg}`, ...args),
129
- debug: (msg, ...args) => console.error(`[hooks_cli] ${msg}`, ...args),
130
- error: (msg, err, ...args) => console.error(`[hooks_cli] ${msg}`, ...err ? [err] : [], ...args)
131
- };
132
- var CODEX_TOOL_NAME_MAP = {
133
- bash: "Bash",
134
- edit_file: "Edit",
135
- edit: "Edit",
136
- apply_patch: "Edit",
137
- write_file: "Write",
138
- search_files: "Grep",
139
- grep: "Grep",
140
- list_files: "Glob",
141
- glob: "Glob",
142
- web_search: "WebFetch"
143
- };
144
- var GEMINI_TOOL_NAME_MAP = {
145
- run_shell_command: "Bash",
146
- read_file: "Read",
147
- read_many_files: "Read",
148
- list_directory: "Read",
149
- write_file: "Write",
150
- replace: "Edit",
151
- glob: "Glob",
152
- grep_search: "Grep",
153
- // 'search_file_content' was GREP_TOOL_NAME's value before gemini-cli renamed it to 'grep_search' (confirmed against gemini-cli's current base-declarations.ts, which now defines GREP_TOOL_NAME = 'grep_search'; some of gemini-cli's own docs pages still lag the rename). Kept as a backward-compat entry for older installed gemini-cli versions that still emit the pre-rename name -- not dead/hallucinated, just legacy.
154
- search_file_content: "Grep",
155
- // Gemini's real web-search tool is registered as 'google_web_search' (WEB_SEARCH_TOOL_NAME in gemini-cli's tool-names.ts) -- 'web_search' is not a tool name Gemini CLI ever emits, so the old entry here silently never matched a real invocation.
156
- google_web_search: "WebFetch",
157
- web_fetch: "WebFetch"
158
- };
159
- var GEMINI_INPUT_KEY_MAP = {
160
- // Gemini's real write_file/replace tool schemas already use 'file_path' (and replace's 'old_string'/'new_string') verbatim -- identical to token-goat's own canonical keys (see getFilePath() in hooks_common.ts, which reads event.toolInput['file_path']) -- confirmed against gemini-cli's own EditToolParams/WriteFileToolParams interfaces. No remap is needed for Write/Edit; the previous entries here actively renamed a key that was already correct (file_path -> path), which would have silently corrupted the path argument on every real Gemini Write/Edit call.
161
- // 'Read' covers THREE distinct raw Gemini tools (see GEMINI_TOOL_NAME_MAP above), and they do NOT share one schema: read_file already uses 'file_path' verbatim, no remap needed; list_directory uses 'dir_path' (confirmed against gemini-cli's LSToolParams) -- remapped to 'file_path' below, exactly mirroring Grep's existing dir_path->path fix; read_many_files uses 'include' (confirmed against gemini-cli's ReadManyFilesParams), an ARRAY of glob patterns, not a single file path -- there is no single string to remap it to, so it is deliberately left unmapped. getFilePath() (hooks_common.ts) will return undefined for this call, and preReadHandler/postReadHandler already fall back to passOutput() on an undefined path (see preReadHandler's `if (filePath === undefined) return passOutput()`), so a real read_many_files call still succeeds normally; it just doesn't get session-dedup tracking or read-count hints. Accepted limitation, not a bug to force-fit.
162
- Read: { dir_path: "file_path" },
163
- // Grep's real tool (grep_search) calls its target-directory argument 'dir_path', which preReadHandler's Grep fallback (event.toolInput['path']) doesn't recognize, so that one remains remapped.
164
- Grep: { dir_path: "path" }
165
- };
166
- var GROK_TOOL_NAME_MAP = {
167
- read_file: "Read",
168
- write: "Write",
169
- search_replace: "Edit",
170
- run_terminal_command: "Bash",
171
- // The grok 0.2.93 binary registers its shell tool as `tool.run_terminal_cmd` (tracing-id table in ~/.grok/bin/agent.exe) and its newer embedded hooks doc's PreToolUse example sends `"toolName": "run_terminal_cmd"`, while the older embedded doc revision and a 2026-07-09 live capture on the same version both show `run_terminal_command`. Which spelling arrives is profile/version dependent, so BOTH are mapped: an unmatched extra entry is a harmless no-op, a missing one silently kills every Bash hook (compression, wrap, hints) on that profile.
172
- run_terminal_cmd: "Bash",
173
- grep: "Grep",
174
- list_dir: "Glob",
175
- // `glob` is a distinct registered grok tool (tool.glob in the 0.2.93 binary's tracing-id table), separate from list_dir; unmapped it arrived as lowercase 'glob' and matched no handler.
176
- glob: "Glob",
177
- // web_fetch/web_search were entirely absent from this map, so grok's URL fetches bypassed token-goat's WebFetch pipeline (URL-policy deny included) and its searches bypassed the WebSearch dedup/compression. Verified against the 0.2.93 binary: tool.web_fetch / tool.web_search registered ids, grok_build WebFetchInput's field described as "The URL to fetch content from" (i.e. `url`), grok_build WebSearchInput's fields query/citations/allowed_domains -- all matching the keys hooks_fetch.ts / hooks_websearch.ts already read, so a name map alone revives both.
178
- web_fetch: "WebFetch",
179
- web_search: "WebSearch",
180
- // The Hashline prompt profile registers hashline_read/hashline_edit/hashline_grep (tool.hashline_* in the binary; grok's own hook alias table pairs them with Read/Edit/Grep). Their input key shapes were not individually verified: if one differs, the mapped handler degrades to the same no-op as an unmapped name, never a wrong rewrite. The *_concise twins are the GrokBuildConcise profile's registrations of the same three core tools.
181
- hashline_read: "Read",
182
- hashline_edit: "Edit",
183
- hashline_grep: "Grep",
184
- read_file_concise: "Read",
185
- search_replace_concise: "Edit",
186
- run_terminal_cmd_concise: "Bash"
187
- };
188
- var GROK_INPUT_KEY_MAP = {
189
- Read: { target_file: "file_path" }
190
- };
191
- var KIMI_TOOL_NAME_MAP = {
192
- FetchURL: "WebFetch",
193
- ReadMediaFile: "Read"
194
- };
195
- var KIMI_INPUT_KEY_MAP = {
196
- // line_offset/n_lines are Kimi's Read paging arguments (ReadInputSchema in MoonshotAI/kimi-code packages/agent-core-v2/src/agent/tools/os/read/read.ts: `path`, `line_offset` "the line number to start reading from", `n_lines` "the number of lines to read"). Unmapped, every ranged Kimi read looked unbounded to hooks_read.ts's estimateRequestedSlice and was gated on the whole file's size -- a small slice of a big file could draw the large-file deny meant for full reads. line_offset's 1-indexed positive form matches Read's own offset semantics exactly; its negative tail-read form has no token-goat equivalent and is clamped to 1 by estimateRequestedSlice's `offset >= 1` guard, degrading to a window of the right SIZE (which is all the gate consumes). ReadMediaFile shares this map via its Read rename and carries only `path`, so the extra entries never touch it.
197
- Read: { path: "file_path", line_offset: "offset", n_lines: "limit" },
198
- Write: { path: "file_path" },
199
- Edit: { path: "file_path" }
200
- };
201
- var QWEN_TOOL_NAME_MAP = {
202
- read_file: "Read",
203
- write_file: "Write",
204
- edit: "Edit",
205
- replace: "Edit",
206
- notebook_edit: "NotebookEdit",
207
- run_shell_command: "Bash",
208
- grep_search: "Grep",
209
- search_file_content: "Grep",
210
- glob: "Glob",
211
- web_fetch: "WebFetch",
212
- web_search: "WebSearch",
213
- list_directory: "Read"
214
- };
215
- var QWEN_INPUT_KEY_MAP = {
216
- Read: { path: "file_path" }
217
- };
218
- function remapInputKeys(input, keyMap) {
219
- const newInput = {};
220
- for (const [k, v] of Object.entries(input)) {
221
- newInput[keyMap[k] || k] = v;
222
- }
223
- return newInput;
224
- }
225
- function remapToolName(obj, toolName, nameMap, inputKeyMap) {
226
- const mapped = nameMap[toolName] ?? toolName;
227
- const result = { ...obj };
228
- result["tool_name"] = mapped;
229
- const rawInput = obj["tool_input"];
230
- const keyMap = inputKeyMap[mapped];
231
- if (keyMap && typeof rawInput === "object" && rawInput !== null && !Array.isArray(rawInput)) {
232
- result["tool_input"] = remapInputKeys(rawInput, keyMap);
233
- }
234
- return result;
235
- }
236
- function grokToCanonicalWire(obj) {
237
- const wire = { ...obj };
238
- if (typeof wire["toolName"] === "string" && wire["tool_name"] === void 0) {
239
- wire["tool_name"] = wire["toolName"];
240
- }
241
- if (typeof wire["toolInput"] === "object" && wire["toolInput"] !== null && !Array.isArray(wire["toolInput"]) && wire["tool_input"] === void 0) {
242
- wire["tool_input"] = wire["toolInput"];
243
- }
244
- if (typeof wire["sessionId"] === "string" && wire["session_id"] === void 0) {
245
- wire["session_id"] = wire["sessionId"];
246
- }
247
- const toolResult = wire["toolResult"];
248
- if (toolResult !== null && typeof toolResult === "object" && !Array.isArray(toolResult)) {
249
- const tr = toolResult;
250
- if (typeof tr["output_for_prompt"] === "string") {
251
- wire["tool_response"] = { content: tr["output_for_prompt"], exit_code: tr["exit_code"] };
252
- } else {
253
- const firstString = Object.entries(tr).find(([k, v]) => k !== "type" && typeof v === "string");
254
- if (firstString) {
255
- wire["tool_response"] = { content: firstString[1] };
256
- }
257
- }
258
- }
259
- return wire;
260
- }
261
- function aliasLlmContentToOutput(result) {
262
- const toolResponse = result["tool_response"];
263
- if (toolResponse === null || typeof toolResponse !== "object" || Array.isArray(toolResponse)) return;
264
- const responseRecord = toolResponse;
265
- if (typeof responseRecord["llmContent"] === "string" && responseRecord["output"] === void 0) {
266
- result["tool_response"] = { ...responseRecord, output: responseRecord["llmContent"] };
267
- }
268
- }
269
- function normalizePayload(payload, harness = "claude") {
270
- if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
271
- _LOG.warn("normalizePayload: payload is not a dict; received %s", typeof payload);
272
- return {};
273
- }
274
- const rawObj = payload;
275
- const obj = harness === "grok" ? grokToCanonicalWire(rawObj) : rawObj;
276
- const toolName = obj["tool_name"];
277
- if (typeof toolName !== "string" || !toolName.trim()) {
278
- _LOG.debug("normalizePayload: tool_name missing or invalid; received %s", toolName);
279
- return {};
280
- }
281
- if (harness === "grok") {
282
- const result2 = remapToolName(obj, toolName, GROK_TOOL_NAME_MAP, GROK_INPUT_KEY_MAP);
283
- result2["_tg_harness"] = harness;
284
- return result2;
285
- }
286
- if (harness === "codex") {
287
- const mapped = CODEX_TOOL_NAME_MAP[toolName];
288
- const result2 = { ...obj };
289
- if (mapped) {
290
- result2["tool_name"] = mapped;
291
- }
292
- result2["_tg_harness"] = harness;
293
- return result2;
294
- }
295
- if (harness === "copilot_cli") {
296
- const result2 = { ...obj };
297
- result2["tool_name"] = canonicalizeCopilotMcpToolName(toolName);
298
- result2["_tg_harness"] = harness;
299
- return result2;
300
- }
301
- if (harness === "kimi") {
302
- const result2 = remapToolName(obj, toolName, KIMI_TOOL_NAME_MAP, KIMI_INPUT_KEY_MAP);
303
- result2["_tg_harness"] = harness;
304
- return result2;
305
- }
306
- if (harness === "qwen") {
307
- const result2 = remapToolName(obj, toolName, QWEN_TOOL_NAME_MAP, QWEN_INPUT_KEY_MAP);
308
- aliasLlmContentToOutput(result2);
309
- result2["_tg_harness"] = harness;
310
- return result2;
311
- }
312
- if (harness === "gemini") {
313
- const result2 = remapToolName(obj, toolName, GEMINI_TOOL_NAME_MAP, GEMINI_INPUT_KEY_MAP);
314
- if ("functionCallId" in result2 && !("toolUseId" in result2)) {
315
- result2["toolUseId"] = result2["functionCallId"];
316
- delete result2["functionCallId"];
317
- }
318
- aliasLlmContentToOutput(result2);
319
- result2["_tg_harness"] = harness;
320
- return result2;
321
- }
322
- const result = { ...obj };
323
- result["_tg_harness"] = harness;
324
- return result;
325
- }
326
-
327
41
  // src/web_cache.ts
328
42
  init_define_import_meta_env();
329
43
  var WEB_OUTPUT_SUBDIR = "web_outputs";
@@ -426,12 +140,8 @@ function entryMatches(entry, re, pattern) {
426
140
  return haystacks.some((h) => re !== null ? re.test(h) : h.includes(pattern));
427
141
  }
428
142
  function filterCommandManifest(manifest, pattern) {
429
- let re;
430
- try {
431
- re = new RegExp(pattern);
432
- } catch {
433
- re = null;
434
- }
143
+ const guarded = compileGuardedRegex(pattern);
144
+ const re = guarded.ok ? guarded.re : null;
435
145
  const result = [];
436
146
  for (const entry of manifest) {
437
147
  if (entryMatches(entry, re, pattern)) {
@@ -467,8 +177,30 @@ function formatCommandManifest(manifest) {
467
177
 
468
178
  // src/reconcile.ts
469
179
  init_define_import_meta_env();
470
- import * as fs2 from "node:fs";
180
+ import * as fs from "node:fs";
471
181
  var DEFAULT_RECONCILE_BUDGET_MS = 1500;
182
+ function readReconcileCursor(dbPath, root) {
183
+ try {
184
+ const row = getDb(dbPath).prepare("SELECT last_scanned_path FROM reconcile_cursor WHERE root = ?").get(root);
185
+ return row?.last_scanned_path ?? null;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+ function writeReconcileCursor(dbPath, root, lastScannedPath) {
191
+ try {
192
+ getDb(dbPath).prepare(
193
+ "INSERT INTO reconcile_cursor (root, last_scanned_path, updated_at) VALUES (?, ?, ?) ON CONFLICT(root) DO UPDATE SET last_scanned_path = excluded.last_scanned_path, updated_at = excluded.updated_at"
194
+ ).run(root, lastScannedPath, Date.now());
195
+ } catch {
196
+ }
197
+ }
198
+ function clearReconcileCursor(dbPath, root) {
199
+ try {
200
+ getDb(dbPath).prepare("DELETE FROM reconcile_cursor WHERE root = ?").run(root);
201
+ } catch {
202
+ }
203
+ }
472
204
  function runReconcile(opts = {}) {
473
205
  const raw = reconcileProject(opts);
474
206
  const root = getDisplayRoot(opts.cwd ?? process.cwd());
@@ -480,7 +212,7 @@ function runReconcile(opts = {}) {
480
212
  removed: display(raw.removed)
481
213
  };
482
214
  if (opts.json === true) {
483
- process.stdout.write(`${JSON.stringify(result, null, 2)}
215
+ process.stdout.write(`${displaySafeJson(result)}
484
216
  `);
485
217
  return 0;
486
218
  }
@@ -503,6 +235,10 @@ function runReconcile(opts = {}) {
503
235
  if (result.mtimeOnly > 0) {
504
236
  lines.push(`${countNoun(result.mtimeOnly, "file")} had a newer timestamp but identical content, so ${result.mtimeOnly === 1 ? "it was" : "they were"} left alone.`);
505
237
  }
238
+ if (result.parserStale > 0) {
239
+ const reparseVerb = opts.dryRun === true ? "would reparse" : "queued for reparse";
240
+ lines.push(`${countNoun(result.parserStale, "file")} of the above ${result.parserStale === 1 ? "was" : "were"} unchanged on disk but indexed by an older parser (${reparseVerb}).`);
241
+ }
506
242
  if (result.trackedUnavailable) {
507
243
  lines.push("This project has an index but git listed no files in it, so nothing could be compared and no deletions were computed. Run token-goat inside the repository, or reindex with --walk if this directory is deliberately not under git.");
508
244
  }
@@ -520,21 +256,34 @@ function reconcileProject(opts = {}) {
520
256
  const cwd = opts.cwd ?? process.cwd();
521
257
  const budgetMs = opts.budgetMs ?? DEFAULT_RECONCILE_BUDGET_MS;
522
258
  const startedAt = Date.now();
259
+ const dbPath = opts.dbPath ?? globalDbPath();
523
260
  const tracked = getTrackedFiles(cwd);
524
261
  const projectRoot = resolveIndexPath(".", cwd);
525
- const indexed = getProjectFileEntries(projectRoot);
262
+ const indexed = getProjectFileEntries(projectRoot, dbPath);
263
+ let scanOrder = tracked;
264
+ const cursorPath = tracked.length > 0 ? readReconcileCursor(dbPath, projectRoot) : null;
265
+ if (cursorPath !== null) {
266
+ const cursorFolded = foldPath(normalizePath(cursorPath));
267
+ const cursorIdx = tracked.findIndex((f) => foldPath(normalizePath(f)) === cursorFolded);
268
+ if (cursorIdx !== -1) {
269
+ scanOrder = [...tracked.slice(cursorIdx + 1), ...tracked.slice(0, cursorIdx + 1)];
270
+ }
271
+ }
526
272
  const changed = [];
527
273
  const added = [];
528
274
  const seenOnDisk = /* @__PURE__ */ new Set();
529
275
  let mtimeOnly = 0;
276
+ let parserStale = 0;
530
277
  let scanned = 0;
531
278
  let budgetExhausted = false;
532
- for (const file of tracked) {
279
+ let lastScanned = null;
280
+ for (const file of scanOrder) {
533
281
  if (Date.now() - startedAt > budgetMs) {
534
282
  budgetExhausted = true;
535
283
  break;
536
284
  }
537
285
  scanned++;
286
+ lastScanned = file;
538
287
  const folded = foldPath(normalizePath(file));
539
288
  seenOnDisk.add(folded);
540
289
  const entry = indexed.get(folded);
@@ -542,9 +291,14 @@ function reconcileProject(opts = {}) {
542
291
  added.push(file);
543
292
  continue;
544
293
  }
294
+ if (entry.parserSha !== PARSER_FINGERPRINT) {
295
+ changed.push(file);
296
+ parserStale++;
297
+ continue;
298
+ }
545
299
  let mtimeMs;
546
300
  try {
547
- mtimeMs = fs2.statSync(file).mtimeMs;
301
+ mtimeMs = fs.statSync(file).mtimeMs;
548
302
  } catch {
549
303
  changed.push(file);
550
304
  continue;
@@ -574,6 +328,11 @@ function reconcileProject(opts = {}) {
574
328
  enqueueDirtyPathSafe(resolveIndexPath(p, cwd), { alreadyResolved: true });
575
329
  enqueued++;
576
330
  }
331
+ if (budgetExhausted) {
332
+ if (lastScanned !== null) writeReconcileCursor(dbPath, projectRoot, lastScanned);
333
+ } else if (tracked.length > 0) {
334
+ clearReconcileCursor(dbPath, projectRoot);
335
+ }
577
336
  }
578
337
  return {
579
338
  scanned,
@@ -581,6 +340,7 @@ function reconcileProject(opts = {}) {
581
340
  added,
582
341
  removed,
583
342
  mtimeOnly,
343
+ parserStale,
584
344
  budgetExhausted,
585
345
  trackedUnavailable,
586
346
  unscanned: Math.max(0, tracked.length - scanned),
@@ -658,10 +418,10 @@ function readStdinJson(timeoutMs = DEFAULT_STDIN_TIMEOUT_MS, maxBytes = MAX_STDI
658
418
 
659
419
  // src/symbol_body_probe.ts
660
420
  init_define_import_meta_env();
661
- import * as fs3 from "fs";
421
+ import * as fs2 from "fs";
662
422
  var OVERSIZED_BODY_PROBE_SQL = `SELECT 1 FROM symbols WHERE LENGTH(body) > ${SYMBOL_BODY_CHAR_CAP} LIMIT 1`;
663
423
  function checkSymbolBodySize(dbPath) {
664
- if (!fs3.existsSync(dbPath)) {
424
+ if (!fs2.existsSync(dbPath)) {
665
425
  return { name: "Symbol body size", status: "ok", message: "no database yet" };
666
426
  }
667
427
  try {
@@ -686,7 +446,7 @@ function checkSymbolBodySize(dbPath) {
686
446
 
687
447
  // src/resident_context.ts
688
448
  init_define_import_meta_env();
689
- import * as fs4 from "node:fs";
449
+ import * as fs3 from "node:fs";
690
450
  var LARGE_TASK_LIST_BYTES = 2e4;
691
451
  var LARGE_SKILL_BODY_BYTES = 2e4;
692
452
  var SKILL_BODY_REPEAT_THRESHOLD = 2;
@@ -775,9 +535,9 @@ function collectInvokedSkills(acc, record) {
775
535
  function skillName(skill) {
776
536
  const name = skill["name"];
777
537
  if (typeof name === "string" && name.trim() !== "") return name.trim();
778
- const path2 = skill["path"];
779
- if (typeof path2 !== "string") return null;
780
- const segments = path2.split(/[\\/]/).filter((part) => part !== "");
538
+ const path = skill["path"];
539
+ if (typeof path !== "string") return null;
540
+ const segments = path.split(/[\\/]/).filter((part) => part !== "");
781
541
  const last = segments[segments.length - 1];
782
542
  return last === void 0 || last === "" ? null : last;
783
543
  }
@@ -848,13 +608,13 @@ function repeatedSkillBodyHint(injections) {
848
608
  function readTranscriptTail(transcriptPath, maxBytes = RESIDENT_TAIL_MAX_BYTES) {
849
609
  let fd = null;
850
610
  try {
851
- const stat = fs4.statSync(transcriptPath);
611
+ const stat = fs3.statSync(transcriptPath);
852
612
  if (!stat.isFile() || stat.size === 0) return [];
853
613
  const start = Math.max(0, stat.size - maxBytes);
854
614
  const length = stat.size - start;
855
- fd = fs4.openSync(transcriptPath, "r");
615
+ fd = fs3.openSync(transcriptPath, "r");
856
616
  const buf = Buffer.allocUnsafe(length);
857
- const read = fs4.readSync(fd, buf, 0, length, start);
617
+ const read = fs3.readSync(fd, buf, 0, length, start);
858
618
  const lines = buf.subarray(0, read).toString("utf8").split("\n");
859
619
  if (start > 0) lines.shift();
860
620
  return lines;
@@ -863,7 +623,7 @@ function readTranscriptTail(transcriptPath, maxBytes = RESIDENT_TAIL_MAX_BYTES)
863
623
  } finally {
864
624
  if (fd !== null) {
865
625
  try {
866
- fs4.closeSync(fd);
626
+ fs3.closeSync(fd);
867
627
  } catch {
868
628
  }
869
629
  }
@@ -889,9 +649,7 @@ function accumulateResidentLines(lines) {
889
649
  }
890
650
 
891
651
  export {
892
- readCopilotMcpTools,
893
- GEMINI_TOOL_NAME_MAP,
894
- normalizePayload,
652
+ checkSymbolBodySize,
895
653
  WEB_OUTPUT_SUBDIR,
896
654
  storeWebOutput,
897
655
  getWebOutput,
@@ -906,7 +664,6 @@ export {
906
664
  runReconcile,
907
665
  isReconcileClean,
908
666
  reconcileProject,
909
- checkSymbolBodySize,
910
667
  createResidentContextStats,
911
668
  accumulateResidentLine,
912
669
  summarizeResidentContext,