token-goat 2.6.33 → 2.6.35

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 (25) hide show
  1. package/README.md +4 -0
  2. package/SECURITY.md +11 -10
  3. package/dist/{token-goat-chunk-FN6WLFIX.mjs → token-goat-chunk-D5IZEGN2.mjs} +817 -53
  4. package/dist/{token-goat-hook-chunk-HXHFLKEL.mjs → token-goat-chunk-DQIT5JIF.mjs} +7 -2
  5. package/dist/{token-goat-chunk-72R7M5H7.mjs → token-goat-chunk-DRAIYVUI.mjs} +16 -14
  6. package/dist/{token-goat-chunk-RPOXYJWK.mjs → token-goat-chunk-HW4VUKJ5.mjs} +77 -26
  7. package/dist/{token-goat-chunk-TDYTBCT4.mjs → token-goat-chunk-I2KCEGIP.mjs} +704 -138
  8. package/dist/{token-goat-chunk-WFRR2DBG.mjs → token-goat-chunk-K7W3P2TJ.mjs} +583 -343
  9. package/dist/{token-goat-chunk-OHQ7RBLL.mjs → token-goat-chunk-SIHYQCTM.mjs} +2 -2
  10. package/dist/{token-goat-chunk-77CX3MCN.mjs → token-goat-chunk-UIZVX3HN.mjs} +2 -2
  11. package/dist/{token-goat-chunk-L6FVTDPD.mjs → token-goat-chunk-Y2NH6DGZ.mjs} +564 -31
  12. package/dist/{token-goat-chunk-HPWPUBAK.mjs → token-goat-chunk-YF67EWRH.mjs} +5 -5
  13. package/dist/{token-goat-hook-chunk-E6AIPKNF.mjs → token-goat-hook-chunk-2O7P4Z6Q.mjs} +579 -339
  14. package/dist/{token-goat-hook-chunk-WBBHV6TZ.mjs → token-goat-hook-chunk-3AAH72SO.mjs} +77 -26
  15. package/dist/{token-goat-hook-chunk-QD2DX2U7.mjs → token-goat-hook-chunk-4POU6OHK.mjs} +564 -31
  16. package/dist/{token-goat-hook-chunk-ZT47M3IA.mjs → token-goat-hook-chunk-CRBMPT74.mjs} +2 -2
  17. package/dist/{token-goat-hook-chunk-26DLRODR.mjs → token-goat-hook-chunk-D6UJEFHX.mjs} +2 -2
  18. package/dist/{token-goat-hook-chunk-XBOWDNJB.mjs → token-goat-hook-chunk-J4HKWUWZ.mjs} +5 -5
  19. package/dist/{token-goat-chunk-RQQ4SY2P.mjs → token-goat-hook-chunk-NIPXIAPA.mjs} +7 -2
  20. package/dist/{token-goat-hook-chunk-VDXQXYJK.mjs → token-goat-hook-chunk-NXVT4F3S.mjs} +817 -53
  21. package/dist/{token-goat-hook-chunk-EKGDLI4D.mjs → token-goat-hook-chunk-VU6VGZBM.mjs} +16 -14
  22. package/dist/{token-goat-hook-chunk-UWV4MTYI.mjs → token-goat-hook-chunk-XM4W3WCO.mjs} +704 -138
  23. package/dist/token-goat-hook.mjs +5 -5
  24. package/dist/token-goat.core.mjs +5 -5
  25. package/package.json +11 -10
@@ -4,7 +4,7 @@ import {
4
4
  loadConfig,
5
5
  redactSecrets,
6
6
  stripAnsiCodes
7
- } from "./token-goat-hook-chunk-E6AIPKNF.mjs";
7
+ } from "./token-goat-chunk-K7W3P2TJ.mjs";
8
8
 
9
9
  // src/tool_filters/helpers.ts
10
10
  import * as fs from "node:fs";
@@ -3416,7 +3416,12 @@ var GhRunLogFilter = class extends ToolFilter {
3416
3416
  const name = pathName(first).toLowerCase();
3417
3417
  if (stem !== "gh" && name !== "gh") return false;
3418
3418
  const positionals = positionalArgs(argv.slice(1));
3419
- return positionals.length >= 2 && positionals[0] === "run" && positionals[1] === "view" && argv.includes("--log");
3419
+ return positionals.length >= 2 && positionals[0] === "run" && positionals[1] === "view" && // `--log-failed` is the CI-triage spelling and emits the identical
3420
+ // job/step/timestamp column format, so it needs this filter too. A plain
3421
+ // `argv.includes('--log')` is a whole-token test and never matches it,
3422
+ // which sent it to GhFilter -- whose pass-step regex anchors at line
3423
+ // start and so matches nothing while the column prefix is still present.
3424
+ (argv.includes("--log") || argv.includes("--log-failed"));
3420
3425
  }
3421
3426
  compress(stdout, stderr, _exitCode, _argv) {
3422
3427
  const merged = this.combineOutput(stdout, stderr);
@@ -25,7 +25,7 @@ import {
25
25
  runSkeleton,
26
26
  runSymbol,
27
27
  withPinnedReads
28
- } from "./token-goat-chunk-FN6WLFIX.mjs";
28
+ } from "./token-goat-chunk-D5IZEGN2.mjs";
29
29
  import {
30
30
  buildProjectMap,
31
31
  embeddingsDepsAvailable,
@@ -34,7 +34,7 @@ import {
34
34
  getProjectIndexCounts,
35
35
  isWorkerRunning,
36
36
  mapLookupBytesSaved
37
- } from "./token-goat-chunk-TDYTBCT4.mjs";
37
+ } from "./token-goat-chunk-I2KCEGIP.mjs";
38
38
  import {
39
39
  VERSION,
40
40
  dataDir,
@@ -45,7 +45,7 @@ import {
45
45
  normalizePath,
46
46
  recordStat,
47
47
  resolveProjectRoot
48
- } from "./token-goat-chunk-WFRR2DBG.mjs";
48
+ } from "./token-goat-chunk-K7W3P2TJ.mjs";
49
49
  import "./token-goat-chunk-AO2QD2AG.mjs";
50
50
  import "./token-goat-chunk-AEX54RUZ.mjs";
51
51
 
@@ -126,7 +126,19 @@ function forCompare(p) {
126
126
  return process.platform === "win32" ? p.toLowerCase() : p;
127
127
  }
128
128
  function resolveToolRoot(projectRoot) {
129
- return resolveProjectRoot(projectRoot !== void 0 ? { project: projectRoot } : {});
129
+ const resolved = resolveProjectRoot(projectRoot !== void 0 ? { project: projectRoot } : {});
130
+ assertRootAllowed(resolved);
131
+ return resolved;
132
+ }
133
+ var RootNotAllowedError = class extends Error {
134
+ };
135
+ function assertRootAllowed(resolvedRoot) {
136
+ const allowedRoots = loadConfig().mcp.allowed_roots;
137
+ if (allowedRoots.length === 0) return;
138
+ if (allowedRoots.some((allowed) => checkWithinProjectRoot(resolvedRoot, allowed).inside)) return;
139
+ throw new RootNotAllowedError(
140
+ `refused: "${resolvedRoot}" is not inside any root listed in mcp.allowed_roots. A caller-supplied projectRoot is untrusted input, so this deployment pins which roots may be named; add the root to mcp.allowed_roots (or TOKEN_GOAT_MCP_ALLOWED_ROOTS) to permit it.`
141
+ );
130
142
  }
131
143
  function checkWithinProjectRoot(target, resolvedRoot) {
132
144
  const root = forCompare(normalizePath(realPathOrSelf(resolvedRoot)));
@@ -150,16 +162,6 @@ function specFilePart(spec) {
150
162
  var NO_PINS = /* @__PURE__ */ new Map();
151
163
  function confineTargets(targets, resolvedRoot, splitCommas = true) {
152
164
  if (!loadConfig(resolvedRoot).mcp.confine_reads_to_project_root) return { ok: true, targets, pins: NO_PINS };
153
- const allowedRoots = loadConfig().mcp.allowed_roots;
154
- if (allowedRoots.length > 0 && !allowedRoots.some((allowed) => checkWithinProjectRoot(resolvedRoot, allowed).inside)) {
155
- return {
156
- ok: false,
157
- refusal: toCallToolResult({
158
- text: `refused: "${resolvedRoot}" is not inside any root listed in mcp.allowed_roots. A caller-supplied projectRoot is untrusted input, so this deployment pins which roots may be named; add the root to mcp.allowed_roots (or TOKEN_GOAT_MCP_ALLOWED_ROOTS) to permit it.`,
159
- code: 1
160
- })
161
- };
162
- }
163
165
  const checked = [];
164
166
  const pins = /* @__PURE__ */ new Map();
165
167
  for (const raw of targets) {
@@ -76,13 +76,15 @@ import {
76
76
  runSymbol,
77
77
  runTestFor,
78
78
  runTypes,
79
+ runXmlOutline,
80
+ runXmlQuery,
79
81
  runYamlOutline,
80
82
  runYamlQuery,
81
83
  runZipList,
82
84
  runZipRead,
83
85
  symbolNamesInFile,
84
86
  upsertNote
85
- } from "./token-goat-chunk-FN6WLFIX.mjs";
87
+ } from "./token-goat-chunk-D5IZEGN2.mjs";
86
88
  import {
87
89
  BASH_OUTPUT_SUBDIR,
88
90
  GEMINI_TOOL_NAME_MAP,
@@ -103,7 +105,7 @@ import {
103
105
  readStdinJson,
104
106
  searchRecall,
105
107
  storeWebOutput
106
- } from "./token-goat-chunk-OHQ7RBLL.mjs";
108
+ } from "./token-goat-chunk-SIHYQCTM.mjs";
107
109
  import {
108
110
  AGENT_SALT_MARKER,
109
111
  CONTEXT_AUTOCOMPACT_TOKENS,
@@ -149,6 +151,7 @@ import {
149
151
  findLatestSessionId,
150
152
  findMemSuggestionCandidates,
151
153
  findMemoryMd,
154
+ findOrphanedChunkPaths,
152
155
  findStrayClaudeMdBlocks,
153
156
  findSystemTempFiles,
154
157
  formatAge,
@@ -170,6 +173,7 @@ import {
170
173
  incrementSkillHit,
171
174
  indexFileEmbeddings,
172
175
  indexFileSync,
176
+ indexedPathSpellingIsStale,
173
177
  installClaudeMd,
174
178
  installCodex,
175
179
  installCopilotCli,
@@ -207,6 +211,7 @@ import {
207
211
  pruneBlobs,
208
212
  pruneBlockedRoot,
209
213
  pruneDeletedFiles,
214
+ pruneOrphanedChunks,
210
215
  pruneSkillOutputs,
211
216
  pruneSystemTempFiles,
212
217
  querySheet,
@@ -238,7 +243,7 @@ import {
238
243
  vscodeDecoderConfigured,
239
244
  walkProject,
240
245
  writeCompact
241
- } from "./token-goat-chunk-TDYTBCT4.mjs";
246
+ } from "./token-goat-chunk-I2KCEGIP.mjs";
242
247
  import {
243
248
  C,
244
249
  CONFIG_KEY_ENV_OVERRIDES,
@@ -255,13 +260,17 @@ import {
255
260
  configPath,
256
261
  countNoun,
257
262
  dataDir,
263
+ decodeSource,
258
264
  defaultConfig,
259
265
  detectLanguage,
266
+ detectSourceEncoding,
267
+ encodeSource,
260
268
  ensureDirSync,
261
269
  ensureNewline,
262
270
  escapeRegExp,
263
271
  extractErrorMessage,
264
272
  fg,
273
+ fileIsAbsent,
265
274
  findProject,
266
275
  fingerprintContent,
267
276
  fingerprintFile,
@@ -313,7 +322,7 @@ import {
313
322
  withFileLock,
314
323
  withRetryOnLock,
315
324
  writeJsonSettings
316
- } from "./token-goat-chunk-WFRR2DBG.mjs";
325
+ } from "./token-goat-chunk-K7W3P2TJ.mjs";
317
326
  import {
318
327
  __export
319
328
  } from "./token-goat-chunk-AEX54RUZ.mjs";
@@ -5459,7 +5468,7 @@ function purgeRoots() {
5459
5468
  function purgeDataDirectories() {
5460
5469
  const result = { removed: [], absent: [], failed: [] };
5461
5470
  for (const root of purgeRoots()) {
5462
- if (!fs10.existsSync(root)) {
5471
+ if (fileIsAbsent(root)) {
5463
5472
  result.absent.push(root);
5464
5473
  continue;
5465
5474
  }
@@ -6139,7 +6148,7 @@ async function runDoctorAndExit(opts) {
6139
6148
  }
6140
6149
 
6141
6150
  // src/webfetch.ts
6142
- import { existsSync as existsSync8, readdirSync as readdirSync5, statSync as statSync4, unlinkSync as unlinkSync2 } from "fs";
6151
+ import { existsSync as existsSync7, readdirSync as readdirSync5, statSync as statSync4, unlinkSync as unlinkSync2 } from "fs";
6143
6152
  import * as http from "http";
6144
6153
  import * as https from "https";
6145
6154
  import { isIPv4, isIPv6 } from "net";
@@ -6326,7 +6335,7 @@ function isPrivateIPv6(ip) {
6326
6335
  var STALE_DOWNLOAD_AGE_MS = 10 * 60 * 1e3;
6327
6336
  function cleanupStaleDownloads() {
6328
6337
  const cacheDir = webCacheDir();
6329
- if (!existsSync8(cacheDir)) return 0;
6338
+ if (!existsSync7(cacheDir)) return 0;
6330
6339
  let removed = 0;
6331
6340
  try {
6332
6341
  const files = readdirSync5(cacheDir);
@@ -8718,7 +8727,7 @@ function collectFiles(projectRoot, patterns, opts = {}) {
8718
8727
  }
8719
8728
  let content;
8720
8729
  try {
8721
- content = redactIfDotenv(p, fs13.readFileSync(fd, "utf8"));
8730
+ content = redactIfDotenv(p, decodeSource(fs13.readFileSync(fd)));
8722
8731
  } catch {
8723
8732
  result.skipped.push(`${rel} (unreadable)`);
8724
8733
  continue;
@@ -9525,7 +9534,7 @@ function isMarkerOccurrence(matched, hasColon) {
9525
9534
  function scanFileForTodos(filePath, kindSet) {
9526
9535
  let text;
9527
9536
  try {
9528
- text = fs16.readFileSync(filePath, "utf8");
9537
+ text = decodeSource(fs16.readFileSync(filePath));
9529
9538
  } catch {
9530
9539
  return [];
9531
9540
  }
@@ -11833,9 +11842,10 @@ function cmdProject(opts) {
11833
11842
  const removed = before.length - after.length;
11834
11843
  const stale = before.filter((r) => !after.includes(r));
11835
11844
  const staleTempFiles = findSystemTempFiles();
11845
+ const orphanChunkPaths = findOrphanedChunkPaths();
11836
11846
  if (opts.dryRun === true) {
11837
11847
  if (opts.json === true) {
11838
- emit(JSON.stringify({ dryRun: true, wouldPrune: removed, stale, wouldPruneTempFiles: staleTempFiles.length, staleTempFiles, blocked_roots: before }, null, 2));
11848
+ emit(JSON.stringify({ dryRun: true, wouldPrune: removed, stale, wouldPruneTempFiles: staleTempFiles.length, staleTempFiles, wouldPruneOrphanChunkFiles: orphanChunkPaths.length, orphanChunkPaths, blocked_roots: before }, null, 2));
11839
11849
  return;
11840
11850
  }
11841
11851
  if (removed === 0) {
@@ -11850,18 +11860,26 @@ function cmdProject(opts) {
11850
11860
  emit(`Would prune ${staleTempFiles.length} stale indexed temp-dir file(s):`);
11851
11861
  for (const p of staleTempFiles) emit(` ${p}`);
11852
11862
  }
11863
+ if (orphanChunkPaths.length === 0) {
11864
+ emit("Would prune 0 orphaned embedding chunk file(s). Nothing to do.");
11865
+ } else {
11866
+ emit(`Would prune orphaned embedding chunks for ${orphanChunkPaths.length} file(s):`);
11867
+ for (const p of orphanChunkPaths) emit(` ${p}`);
11868
+ }
11853
11869
  return;
11854
11870
  }
11855
11871
  cfg.worker.blocked_roots = after;
11856
11872
  saveConfigSafe(cfg);
11857
11873
  invalidateConfigCache();
11858
11874
  const prunedTempFiles = pruneSystemTempFiles();
11875
+ const prunedOrphanChunks = pruneOrphanedChunks();
11859
11876
  if (opts.json === true) {
11860
- emit(JSON.stringify({ pruned: removed, blocked_roots: after, prunedTempFiles: prunedTempFiles.length }, null, 2));
11877
+ emit(JSON.stringify({ pruned: removed, blocked_roots: after, prunedTempFiles: prunedTempFiles.length, prunedOrphanChunkFiles: prunedOrphanChunks.length }, null, 2));
11861
11878
  return;
11862
11879
  }
11863
11880
  emit(`Pruned ${removed} stale root(s). Remaining: ${after.length}`);
11864
11881
  emit(`Pruned ${prunedTempFiles.length} stale indexed temp-dir file(s).`);
11882
+ emit(`Pruned orphaned embedding chunks for ${prunedOrphanChunks.length} file(s).`);
11865
11883
  return;
11866
11884
  }
11867
11885
  emitErr2(`project: unknown action '${action}'. Use list, exclude, or prune.`);
@@ -13046,15 +13064,19 @@ function formatSpentCell(row) {
13046
13064
  if (row.bytesEmitted === null) return "n/a (legacy)";
13047
13065
  return row.legacyEmissions > 0 ? `${row.bytesEmitted} (${row.legacyEmissions} legacy)` : String(row.bytesEmitted);
13048
13066
  }
13067
+ function suppressedCell(row) {
13068
+ if (!row.suppressed) return "no";
13069
+ return row.suppressionPermanent ? "yes (permanent)" : "yes";
13070
+ }
13049
13071
  function printSummary(rows) {
13050
13072
  const w = (text) => {
13051
13073
  process.stdout.write(text);
13052
13074
  };
13053
- w(pad("category", 22) + pad("emitted", 9) + pad("acted-on", 10) + pad("efficacy", 10) + pad("suppressed", 12) + pad("manual+", 9) + pad("manual-", 9) + "spent\n");
13075
+ w(pad("category", 22) + pad("emitted", 9) + pad("acted-on", 10) + pad("efficacy", 10) + pad("suppressed", 17) + pad("manual+", 9) + pad("manual-", 9) + "spent\n");
13054
13076
  for (const row of rows) {
13055
13077
  const pct = row.efficacyPct === null ? "n/a" : `${row.efficacyPct}%`;
13056
13078
  w(
13057
- pad(row.category, 22) + pad(String(row.emitted), 9) + pad(String(row.actedOn), 10) + pad(pct, 10) + pad(row.suppressed ? "yes" : "no", 12) + pad(String(row.manualEffective), 9) + pad(String(row.manualIneffective), 9) + formatSpentCell(row) + "\n"
13079
+ pad(row.category, 22) + pad(String(row.emitted), 9) + pad(String(row.actedOn), 10) + pad(pct, 10) + pad(suppressedCell(row), 17) + pad(String(row.manualEffective), 9) + pad(String(row.manualIneffective), 9) + formatSpentCell(row) + "\n"
13058
13080
  );
13059
13081
  }
13060
13082
  }
@@ -13094,6 +13116,13 @@ function runHintStatsCommand(opts = {}) {
13094
13116
  process.stdout.write("No hint emissions recorded yet \u2014 the zeros below are absence of data, not measured ineffectiveness.\n");
13095
13117
  }
13096
13118
  printSummary(rows);
13119
+ const permanent = rows.filter((r) => r.suppressionPermanent);
13120
+ if (permanent.length > 0) {
13121
+ const names = permanent.map((r) => r.category).join(", ");
13122
+ process.stdout.write(`
13123
+ hints.backoff_thresholds is empty, so suppression here is permanent rather than a self-healing throttle: ${names} will not emit again on any occasion. Set backoff_thresholds to re-enable probe emissions, or run \`token-goat hint-stats --reset\` to clear the ledger.
13124
+ `);
13125
+ }
13097
13126
  printTotals(getHintStatsTotals());
13098
13127
  }
13099
13128
 
@@ -13333,7 +13362,8 @@ async function cmdIndex(pathArg, opts = {}) {
13333
13362
  const sha = fingerprintFile(key);
13334
13363
  if (sha === null && !fs26.existsSync(key)) continue;
13335
13364
  const entry = sha !== null ? getFileEntry(key, dbPath) : null;
13336
- const parseUnchanged = !force && sha !== null && entry?.sha === sha;
13365
+ const spellingStale = entry !== null && indexedPathSpellingIsStale(entry.filePath, key);
13366
+ const parseUnchanged = !force && !spellingStale && sha !== null && entry?.sha === sha;
13337
13367
  const embeddingsEnabled = loadConfig().indexing?.embeddings_enabled ?? true;
13338
13368
  const depsAvailable = embeddingsEnabled && embeddingsDepsAvailable(getDb(dbPath));
13339
13369
  const embedUnchanged = !force && parseUnchanged && sha !== null && isEmbedFresh(entry?.embedSha, sha, embeddingsEnabled, depsAvailable);
@@ -13416,7 +13446,7 @@ async function cmdMcpServe() {
13416
13446
  let StdioServerTransport;
13417
13447
  try {
13418
13448
  ;
13419
- ({ createMcpServer } = await import("./token-goat-chunk-72R7M5H7.mjs"));
13449
+ ({ createMcpServer } = await import("./token-goat-chunk-DRAIYVUI.mjs"));
13420
13450
  ({ StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js"));
13421
13451
  } catch (err2) {
13422
13452
  process.stderr.write(
@@ -13437,7 +13467,7 @@ async function cmdHook(event, opts) {
13437
13467
  if (typeof opts.harness === "string" && opts.harness.length > 0) {
13438
13468
  process.env[ENV_KEYS.HARNESS_OVERRIDE] = opts.harness;
13439
13469
  }
13440
- const { relay } = await import("./token-goat-chunk-L6FVTDPD.mjs");
13470
+ const { relay } = await import("./token-goat-chunk-Y2NH6DGZ.mjs");
13441
13471
  await relay(event);
13442
13472
  }
13443
13473
  async function cmdInstall(opts) {
@@ -13901,7 +13931,7 @@ function cmdBashOutput(id, opts) {
13901
13931
  if (st.isFIFO() || st.isSocket()) {
13902
13932
  throw new CliError(`--file '${opts.file}' is a special file (FIFO or socket) \u2014 only regular files are supported`);
13903
13933
  }
13904
- content = redactIfDotenv(opts.file, fs26.readFileSync(opts.file, "utf-8"));
13934
+ content = redactIfDotenv(opts.file, decodeSource(fs26.readFileSync(opts.file)));
13905
13935
  } catch (e) {
13906
13936
  if (e instanceof CliError) throw e;
13907
13937
  throw new CliError(`cannot read file: ${opts.file}`);
@@ -14216,6 +14246,16 @@ function cmdYamlOutline(file, opts) {
14216
14246
  function cmdYamlQuery(file, yamlPath, opts) {
14217
14247
  process.exitCode = runYamlQuery({ file, path: yamlPath, ...opts });
14218
14248
  }
14249
+ function cmdXmlOutline(file, opts) {
14250
+ process.exitCode = runXmlOutline({
14251
+ file,
14252
+ ...opts.json === true ? { json: true } : {},
14253
+ ...opts.maxDepth !== void 0 ? { maxDepth: requireNonNegativeInt("--max-depth", opts.maxDepth) } : {}
14254
+ });
14255
+ }
14256
+ function cmdXmlQuery(file, xmlPath, opts) {
14257
+ process.exitCode = runXmlQuery({ file, path: xmlPath, ...opts });
14258
+ }
14219
14259
  function cmdOpenApiOutline(file, opts) {
14220
14260
  process.exitCode = runOpenApiOutline({ file, ...opts });
14221
14261
  }
@@ -14284,7 +14324,7 @@ function emitExtraFileArgsNote(command, first, extras, opts = {}) {
14284
14324
  }
14285
14325
  async function cmdCompress(opts) {
14286
14326
  try {
14287
- const bashRunner = await import("./token-goat-chunk-77CX3MCN.mjs");
14327
+ const bashRunner = await import("./token-goat-chunk-UIZVX3HN.mjs");
14288
14328
  if (opts.compress === false) {
14289
14329
  process.exitCode = bashRunner.runRaw(opts.cmd, parseTimeout(opts.timeout, bashRunner.DEFAULT_TIMEOUT_SECONDS));
14290
14330
  return;
@@ -14310,7 +14350,7 @@ async function cmdSkillBody(name, opts) {
14310
14350
  if (filePath === null) {
14311
14351
  throw new CliError(`skill '${name}' not found`);
14312
14352
  }
14313
- const body = fs26.readFileSync(filePath, "utf-8");
14353
+ const body = decodeSource(fs26.readFileSync(filePath));
14314
14354
  if (opts.compact === true) {
14315
14355
  out(extractCompactFromMarker(body) ?? body);
14316
14356
  } else {
@@ -14327,7 +14367,7 @@ async function cmdSkillCompact(name, opts) {
14327
14367
  for (const skill of skills) {
14328
14368
  const filePath = await getSkillFilePath(skill.name);
14329
14369
  if (!filePath) continue;
14330
- const body2 = fs26.readFileSync(filePath, "utf-8");
14370
+ const body2 = decodeSource(fs26.readFileSync(filePath));
14331
14371
  const compact2 = extractCompactFromMarker(body2);
14332
14372
  if (compact2 === null) continue;
14333
14373
  const sourceSha2 = contentHash(body2);
@@ -14531,7 +14571,7 @@ async function cmdSkillSection(nameHeading, headingArg) {
14531
14571
  if (!filePath) {
14532
14572
  throw new CliError(`skill '${skillName}' not found`);
14533
14573
  }
14534
- const body = fs26.readFileSync(filePath, "utf-8");
14574
+ const body = decodeSource(fs26.readFileSync(filePath));
14535
14575
  const extracted = extractNamedSection(body, heading);
14536
14576
  if (!extracted) {
14537
14577
  process.exitCode = 1;
@@ -14962,7 +15002,13 @@ function writeReplacedBuffer(file, replacedBuf, preWriteStat) {
14962
15002
  const testDelayMs = Number(process.env["TOKEN_GOAT_TEST_REPLACE_DELAY_MS"] ?? "");
14963
15003
  if (Number.isFinite(testDelayMs) && testDelayMs > 0) {
14964
15004
  process.stderr.write("TOKEN_GOAT_TEST_REPLACE_DELAY_READY\n");
14965
- sleepSync(testDelayMs);
15005
+ const until = process.env["TOKEN_GOAT_TEST_REPLACE_DELAY_UNTIL"];
15006
+ const deadline = Date.now() + testDelayMs;
15007
+ if (until !== void 0 && until !== "") {
15008
+ while (Date.now() < deadline && !fs26.existsSync(until)) sleepSync(5);
15009
+ } else {
15010
+ sleepSync(testDelayMs);
15011
+ }
14966
15012
  }
14967
15013
  let preRenameStat;
14968
15014
  try {
@@ -15113,13 +15159,14 @@ function cmdInsertSection(file, opts) {
15113
15159
  else if (allHeadings.length > 0) messages.push(`Try: token-goat outline ${file}`);
15114
15160
  throw new CliError(messages.join("\n"));
15115
15161
  }
15116
- let rawText;
15162
+ let rawBytes;
15117
15163
  try {
15118
- rawText = fs26.readFileSync(file, "utf-8");
15164
+ rawBytes = fs26.readFileSync(file);
15119
15165
  } catch (e) {
15120
15166
  mapFsError(e, void 0, file);
15121
15167
  }
15122
- if (rawText.charCodeAt(0) === 65279) rawText = rawText.slice(1);
15168
+ const sourceEncoding = detectSourceEncoding(rawBytes);
15169
+ const rawText = decodeSource(rawBytes);
15123
15170
  const eol = detectDominantEol(Buffer.from(rawText, "utf8"));
15124
15171
  const lfLines = rawText.replace(/\r\n/g, "\n").split("\n");
15125
15172
  const insertAt = result.lineEnd;
@@ -15138,7 +15185,7 @@ function cmdInsertSection(file, opts) {
15138
15185
  }
15139
15186
  }
15140
15187
  try {
15141
- atomicWriteBuffer(file, Buffer.from(mergedText, "utf8"));
15188
+ atomicWriteBuffer(file, encodeSource(mergedText, sourceEncoding));
15142
15189
  } catch (e) {
15143
15190
  mapFsError(e, void 0, file);
15144
15191
  }
@@ -15809,6 +15856,10 @@ function buildProgram() {
15809
15856
  program2.command("yaml-query <file> <path>").description(
15810
15857
  "extract one value or a projected/filtered subset from a YAML document by dot-path instead of a raw Read (same grammar as json-query)\n\npath grammar: dot-separated keys with optional bracket segments -- [n] index, [*] wildcard (projects every element/value), [field=value] filter (keeps array elements whose field stringifies to value). Examples: spec.containers[0].image, items[*].name, items[kind=Service]"
15811
15858
  ).option("--head <n>", "limit a projected/filtered result to the first N items").option("--json", "emit the result as JSON instead of text").action(guard(cmdYamlQuery));
15859
+ program2.command("xml-outline <file>").description("structural summary of an XML document (element hierarchy / attribute names / child counts) instead of a raw Read").option("--json", "emit the outline as JSON instead of text").option("--max-depth <n>", "max depth of element hierarchy to show").action(guard(cmdXmlOutline));
15860
+ program2.command("xml-query <file> <path>").description(
15861
+ "extract elements or attributes from an XML document by tag path instead of a raw Read\n\npath grammar: slash- or dot-separated tag names with optional bracket segments and attribute selectors -- [n] index, [*] wildcard, [@attr] or [@attr=value] filter, and trailing @attr to extract attribute value. Examples: root.child, catalog/book[@id=101]/title, /feed/entry[*]/@href"
15862
+ ).option("--head <n>", "limit a matching result list to the first N items").option("--json", "emit the result as JSON instead of text").action(guard(cmdXmlQuery));
15812
15863
  program2.command("openapi-outline <file>").description("per-operation listing (method, path, operationId, summary, tags) of an OpenAPI 3.x / Swagger 2.0 spec (JSON or YAML) instead of a raw Read").option("--json", "emit the operation list as JSON instead of text").action(guard(cmdOpenApiOutline));
15813
15864
  program2.command("openapi-op <file> <operation>").description(
15814
15865
  'full detail (parameters, request body schema, response schemas, description) for exactly one OpenAPI operation instead of a raw Read\n\noperation may be an operationId (exact match) or a "METHOD path" spec, e.g. "GET /users/{id}"'