token-goat 2.6.0 → 2.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -356,6 +356,7 @@ To upgrade cleanly:
356
356
  |---------|-------------|
357
357
  | `token-goat symbol <name>` | Jump to a symbol definition. |
358
358
  | `token-goat read "file::symbol"` | Pull one function or class, not the whole file. Supports qualified lookups (`read "file.py::Class.method"`) and line ranges: `read "file.py@10-40"` for lines 10 to 40 inclusive, or `read "file.py@42"` for one line. Line ranges read straight from disk, so they work on any file, including paths outside an indexed project. |
359
+ | `token-goat replace <file>` | Replace one string in a file using `--old-from`/`--new-from` or `--old-b64`/`--new-b64`; `--all` replaces every match. |
359
360
  | `token-goat section "doc.md::Heading"` | Pull one Markdown section by heading. A miss that is an unambiguous prefix of exactly one heading auto-redirects with a `(redirected from: …)` marker (and a `redirectedFrom` field under `--json`). Disambiguate duplicates with `"doc.md::Heading#2"`. |
360
361
  | `token-goat skill-section "<name>::<heading>"` | Extract a named section from an installed skill without reading the full skill file. |
361
362
  | `token-goat skeleton "file"` | Show all signatures in a file without bodies — typically 70–90% fewer tokens than a full read. |
@@ -9498,7 +9498,7 @@ init_define_import_meta_env();
9498
9498
  import { createRequire } from "node:module";
9499
9499
  function resolveVersion() {
9500
9500
  if (true) {
9501
- return "2.6.0";
9501
+ return "2.6.1";
9502
9502
  }
9503
9503
  const require2 = createRequire(import.meta.url);
9504
9504
  const pkg = require2("../package.json");
@@ -13243,34 +13243,10 @@ function pruneDeletedFiles(rootPrefix, dbPath = globalDbPath()) {
13243
13243
  return pruned;
13244
13244
  }
13245
13245
 
13246
- // src/relay.ts
13247
- init_define_import_meta_env();
13248
-
13249
- // src/bridges/index.ts
13250
- init_define_import_meta_env();
13251
-
13252
- // src/bridges/registry.ts
13253
- init_define_import_meta_env();
13254
- function detectHarness() {
13255
- const env = process.env;
13256
- if (env["TERM_PROGRAM"] === "claude-code" || env["CLAUDE_CODE_VERSION"] !== void 0) {
13257
- return "claudecode";
13258
- }
13259
- if (env["CODEX_SESSION_ID"] !== void 0) return "codex";
13260
- if (env["OPENCODE_SESSION_ID"] !== void 0) return "opencode";
13261
- return "generic";
13262
- }
13263
- var _cached = null;
13264
- function resetHarnessCache() {
13265
- _cached = null;
13266
- }
13267
- registerReset(resetHarnessCache);
13268
-
13269
- // src/bridges/claudecode.ts
13270
- init_define_import_meta_env();
13271
-
13272
- // src/bridges/codex.ts
13246
+ // src/hooks_index.ts
13273
13247
  init_define_import_meta_env();
13248
+ import * as fs10 from "node:fs";
13249
+ import * as path12 from "node:path";
13274
13250
 
13275
13251
  // src/hook_registry.ts
13276
13252
  init_define_import_meta_env();
@@ -13307,11 +13283,18 @@ var CLAUDE_CODE_EVENT_NAMES = {
13307
13283
  user_prompt_submit: "UserPromptSubmit",
13308
13284
  subagent_stop: "SubagentStop"
13309
13285
  };
13286
+ var EVENTS_WITHOUT_ADDITIONAL_CONTEXT = /* @__PURE__ */ new Set([
13287
+ "notification",
13288
+ "pre_compact"
13289
+ ]);
13310
13290
  function serializeOutput(output, eventName) {
13311
13291
  switch (output.hookType) {
13312
13292
  case "deny":
13313
13293
  return JSON.stringify({ decision: "block", reason: output.message });
13314
13294
  case "context":
13295
+ if (EVENTS_WITHOUT_ADDITIONAL_CONTEXT.has(eventName)) {
13296
+ return JSON.stringify({ systemMessage: output.context });
13297
+ }
13315
13298
  return JSON.stringify({
13316
13299
  hookSpecificOutput: {
13317
13300
  hookEventName: CLAUDE_CODE_EVENT_NAMES[eventName],
@@ -13333,6 +13316,111 @@ function serializeOutput(output, eventName) {
13333
13316
  }
13334
13317
  }
13335
13318
 
13319
+ // src/hooks_common.ts
13320
+ init_define_import_meta_env();
13321
+ function getToolName(event) {
13322
+ return event.toolName;
13323
+ }
13324
+ function getToolInput(event) {
13325
+ return event.toolInput;
13326
+ }
13327
+ function getFilePath(event) {
13328
+ const value = event.toolInput["file_path"];
13329
+ return typeof value === "string" && value !== "" ? value : void 0;
13330
+ }
13331
+ function passOutput() {
13332
+ return { hookType: "pass" };
13333
+ }
13334
+ function denyOutput(message) {
13335
+ return { hookType: "deny", message };
13336
+ }
13337
+ function contextOutput(context) {
13338
+ return { hookType: "context", context };
13339
+ }
13340
+
13341
+ // src/hooks_index.ts
13342
+ function dirtyQueuePath() {
13343
+ return path12.join(dataDir(), "queue", "dirty.txt");
13344
+ }
13345
+ function appendDirtyPath(normalizedPath) {
13346
+ const queuePath = dirtyQueuePath();
13347
+ const dir = path12.dirname(queuePath);
13348
+ try {
13349
+ fs10.mkdirSync(dir, { recursive: true });
13350
+ } catch (e) {
13351
+ if (e.code !== "EEXIST" || !fs10.existsSync(dir)) throw e;
13352
+ }
13353
+ let leadingNewline = "";
13354
+ try {
13355
+ const existing = fs10.readFileSync(queuePath, "utf8");
13356
+ if (existing.length > 0 && !existing.endsWith("\n")) leadingNewline = "\n";
13357
+ } catch {
13358
+ }
13359
+ fs10.appendFileSync(queuePath, `${leadingNewline}${normalizedPath}
13360
+ `);
13361
+ }
13362
+ function getDirtyPaths() {
13363
+ const queuePath = dirtyQueuePath();
13364
+ let raw;
13365
+ try {
13366
+ raw = fs10.readFileSync(queuePath, "utf8");
13367
+ } catch {
13368
+ return [];
13369
+ }
13370
+ const seen = /* @__PURE__ */ new Set();
13371
+ const out2 = [];
13372
+ for (const line of raw.split("\n")) {
13373
+ const trimmed = line.trim();
13374
+ if (trimmed === "" || seen.has(trimmed)) continue;
13375
+ seen.add(trimmed);
13376
+ out2.push(trimmed);
13377
+ }
13378
+ return out2;
13379
+ }
13380
+ function preCompactIndexHandler(_event) {
13381
+ const paths = getDirtyPaths();
13382
+ if (paths.length > 0) {
13383
+ const sidecar = path12.join(dataDir(), "queue", "pending.txt");
13384
+ try {
13385
+ fs10.mkdirSync(path12.dirname(sidecar), { recursive: true });
13386
+ atomicWriteBytes(sidecar, Buffer.from(`${paths.join("\n")}
13387
+ `, "utf8"));
13388
+ } catch {
13389
+ }
13390
+ }
13391
+ return passOutput();
13392
+ }
13393
+ registerHook("pre_compact", preCompactIndexHandler);
13394
+
13395
+ // src/relay.ts
13396
+ init_define_import_meta_env();
13397
+
13398
+ // src/bridges/index.ts
13399
+ init_define_import_meta_env();
13400
+
13401
+ // src/bridges/registry.ts
13402
+ init_define_import_meta_env();
13403
+ function detectHarness() {
13404
+ const env = process.env;
13405
+ if (env["TERM_PROGRAM"] === "claude-code" || env["CLAUDE_CODE_VERSION"] !== void 0) {
13406
+ return "claudecode";
13407
+ }
13408
+ if (env["CODEX_SESSION_ID"] !== void 0) return "codex";
13409
+ if (env["OPENCODE_SESSION_ID"] !== void 0) return "opencode";
13410
+ return "generic";
13411
+ }
13412
+ var _cached = null;
13413
+ function resetHarnessCache() {
13414
+ _cached = null;
13415
+ }
13416
+ registerReset(resetHarnessCache);
13417
+
13418
+ // src/bridges/claudecode.ts
13419
+ init_define_import_meta_env();
13420
+
13421
+ // src/bridges/codex.ts
13422
+ init_define_import_meta_env();
13423
+
13336
13424
  // src/hooks_cli.ts
13337
13425
  init_define_import_meta_env();
13338
13426
  var _LOG = {
@@ -13435,34 +13523,12 @@ var HOOK_EVENTS = [
13435
13523
 
13436
13524
  // src/hooks_read.ts
13437
13525
  init_define_import_meta_env();
13438
- import * as fs16 from "node:fs";
13439
- import * as path17 from "node:path";
13440
-
13441
- // src/hooks_common.ts
13442
- init_define_import_meta_env();
13443
- function getToolName(event) {
13444
- return event.toolName;
13445
- }
13446
- function getToolInput(event) {
13447
- return event.toolInput;
13448
- }
13449
- function getFilePath(event) {
13450
- const value = event.toolInput["file_path"];
13451
- return typeof value === "string" && value !== "" ? value : void 0;
13452
- }
13453
- function passOutput() {
13454
- return { hookType: "pass" };
13455
- }
13456
- function denyOutput(message) {
13457
- return { hookType: "deny", message };
13458
- }
13459
- function contextOutput(context) {
13460
- return { hookType: "context", context };
13461
- }
13526
+ import * as fs17 from "node:fs";
13527
+ import * as path18 from "node:path";
13462
13528
 
13463
13529
  // src/config.ts
13464
13530
  init_define_import_meta_env();
13465
- import * as fs10 from "node:fs";
13531
+ import * as fs11 from "node:fs";
13466
13532
 
13467
13533
  // node_modules/smol-toml/dist/index.js
13468
13534
  init_define_import_meta_env();
@@ -14582,7 +14648,7 @@ function loadConfig() {
14582
14648
  const p = configPath();
14583
14649
  let currentMtime = 0;
14584
14650
  try {
14585
- currentMtime = fs10.statSync(p).mtimeMs;
14651
+ currentMtime = fs11.statSync(p).mtimeMs;
14586
14652
  } catch {
14587
14653
  }
14588
14654
  const envFp = configEnvFingerprint();
@@ -14592,7 +14658,7 @@ function loadConfig() {
14592
14658
  let raw = {};
14593
14659
  if (currentMtime !== 0) {
14594
14660
  try {
14595
- const text = fs10.readFileSync(p, "utf8");
14661
+ const text = fs11.readFileSync(p, "utf8");
14596
14662
  raw = parse2(text);
14597
14663
  } catch {
14598
14664
  }
@@ -14627,7 +14693,7 @@ function loadPersistedConfig() {
14627
14693
  const p = configPath();
14628
14694
  let raw = {};
14629
14695
  try {
14630
- const text = fs10.readFileSync(p, "utf8");
14696
+ const text = fs11.readFileSync(p, "utf8");
14631
14697
  raw = parse2(text);
14632
14698
  } catch {
14633
14699
  }
@@ -14997,8 +15063,8 @@ function saveConfig(config) {
14997
15063
 
14998
15064
  // src/snapshots.ts
14999
15065
  init_define_import_meta_env();
15000
- import * as fs11 from "node:fs";
15001
- import * as path12 from "node:path";
15066
+ import * as fs12 from "node:fs";
15067
+ import * as path13 from "node:path";
15002
15068
  import * as crypto from "node:crypto";
15003
15069
  import * as os4 from "node:os";
15004
15070
  var MAX_SNAPSHOTS_PER_SESSION = 150;
@@ -15012,10 +15078,10 @@ var SESSION_DIR_RE = /[^a-zA-Z0-9_-]/g;
15012
15078
  function sessionDir(sessionId) {
15013
15079
  if (!sessionId) return null;
15014
15080
  const safe = sessionId.replace(SESSION_DIR_RE, "_").slice(0, 64) || "anon";
15015
- const base = path12.join(os4.homedir(), ".token-goat", "session_snapshots");
15016
- const candidate = path12.join(base, safe);
15081
+ const base = path13.join(os4.homedir(), ".token-goat", "session_snapshots");
15082
+ const candidate = path13.join(base, safe);
15017
15083
  try {
15018
- const rel = path12.relative(base, candidate);
15084
+ const rel = path13.relative(base, candidate);
15019
15085
  if (rel.startsWith("..")) return null;
15020
15086
  } catch {
15021
15087
  return null;
@@ -15028,7 +15094,7 @@ function pathKey(filePath) {
15028
15094
  function snapshot_path(sessionId, filePath) {
15029
15095
  const d = sessionDir(sessionId);
15030
15096
  if (!d) return null;
15031
- return path12.join(d, `${pathKey(filePath)}.bin`);
15097
+ return path13.join(d, `${pathKey(filePath)}.bin`);
15032
15098
  }
15033
15099
  function kindSidecarPath(snapshotPath) {
15034
15100
  return snapshotPath + ".kind";
@@ -15036,11 +15102,11 @@ function kindSidecarPath(snapshotPath) {
15036
15102
  function writeSnapshotKind(sidecarPath, kind) {
15037
15103
  try {
15038
15104
  const safeKind = VALID_KINDS.has(kind) ? kind : KIND_READ;
15039
- const dir = path12.dirname(sidecarPath);
15040
- if (!fs11.existsSync(dir)) {
15041
- fs11.mkdirSync(dir, { recursive: true });
15105
+ const dir = path13.dirname(sidecarPath);
15106
+ if (!fs12.existsSync(dir)) {
15107
+ fs12.mkdirSync(dir, { recursive: true });
15042
15108
  }
15043
- fs11.writeFileSync(sidecarPath, safeKind, "utf8");
15109
+ fs12.writeFileSync(sidecarPath, safeKind, "utf8");
15044
15110
  return true;
15045
15111
  } catch {
15046
15112
  return false;
@@ -15049,12 +15115,12 @@ function writeSnapshotKind(sidecarPath, kind) {
15049
15115
  function evictOldest(d, maxCount) {
15050
15116
  try {
15051
15117
  const entries = [];
15052
- const files = fs11.readdirSync(d);
15118
+ const files = fs12.readdirSync(d);
15053
15119
  for (const file of files) {
15054
- const fullPath = path12.join(d, file);
15120
+ const fullPath = path13.join(d, file);
15055
15121
  if (!file.endsWith(".bin")) continue;
15056
15122
  try {
15057
- const stat2 = fs11.statSync(fullPath);
15123
+ const stat2 = fs12.statSync(fullPath);
15058
15124
  entries.push([fullPath, stat2.mtimeMs]);
15059
15125
  } catch {
15060
15126
  continue;
@@ -15066,10 +15132,10 @@ function evictOldest(d, maxCount) {
15066
15132
  const over = entries.length - maxCount;
15067
15133
  for (const [p] of entries.slice(0, over)) {
15068
15134
  try {
15069
- fs11.unlinkSync(p);
15135
+ fs12.unlinkSync(p);
15070
15136
  removed++;
15071
15137
  try {
15072
- fs11.unlinkSync(kindSidecarPath(p));
15138
+ fs12.unlinkSync(kindSidecarPath(p));
15073
15139
  } catch {
15074
15140
  }
15075
15141
  } catch {
@@ -15098,9 +15164,9 @@ function store(sessionId, filePath, content, opts = {}) {
15098
15164
  if (!p) return null;
15099
15165
  const sha = crypto.createHash("sha256").update(stored).digest("hex");
15100
15166
  try {
15101
- if (fs11.existsSync(p)) {
15167
+ if (fs12.existsSync(p)) {
15102
15168
  try {
15103
- const existing = fs11.readFileSync(p);
15169
+ const existing = fs12.readFileSync(p);
15104
15170
  if (Buffer.from(existing).equals(stored)) {
15105
15171
  return {
15106
15172
  path: p,
@@ -15111,14 +15177,14 @@ function store(sessionId, filePath, content, opts = {}) {
15111
15177
  } catch {
15112
15178
  }
15113
15179
  }
15114
- const dir = path12.dirname(p);
15115
- if (!fs11.existsSync(dir)) {
15116
- fs11.mkdirSync(dir, { recursive: true });
15180
+ const dir = path13.dirname(p);
15181
+ if (!fs12.existsSync(dir)) {
15182
+ fs12.mkdirSync(dir, { recursive: true });
15117
15183
  }
15118
15184
  evictOldest(dir, MAX_SNAPSHOTS_PER_SESSION - 1);
15119
15185
  const tempPath = p + ".tmp";
15120
- fs11.writeFileSync(tempPath, stored);
15121
- fs11.renameSync(tempPath, p);
15186
+ fs12.writeFileSync(tempPath, stored);
15187
+ fs12.renameSync(tempPath, p);
15122
15188
  const sidecar = kindSidecarPath(p);
15123
15189
  writeSnapshotKind(sidecar, kind);
15124
15190
  return {
@@ -15132,9 +15198,9 @@ function store(sessionId, filePath, content, opts = {}) {
15132
15198
  }
15133
15199
  function load(sessionId, filePath, opts = {}) {
15134
15200
  const p = snapshot_path(sessionId, filePath);
15135
- if (!p || !fs11.existsSync(p)) return null;
15201
+ if (!p || !fs12.existsSync(p)) return null;
15136
15202
  try {
15137
- const stat2 = fs11.statSync(p);
15203
+ const stat2 = fs12.statSync(p);
15138
15204
  if (stat2.size > MAX_SNAPSHOT_BYTES) {
15139
15205
  return null;
15140
15206
  }
@@ -15142,7 +15208,7 @@ function load(sessionId, filePath, opts = {}) {
15142
15208
  return null;
15143
15209
  }
15144
15210
  try {
15145
- const data = fs11.readFileSync(p);
15211
+ const data = fs12.readFileSync(p);
15146
15212
  if (opts.expected_sha) {
15147
15213
  const actualSha = crypto.createHash("sha256").update(data).digest("hex");
15148
15214
  if (actualSha.toLowerCase() !== opts.expected_sha.toLowerCase()) {
@@ -15664,7 +15730,7 @@ function dispatchFileTypeHandler(filePath, content, contentLengthHint) {
15664
15730
 
15665
15731
  // src/stats.ts
15666
15732
  init_define_import_meta_env();
15667
- import * as path13 from "node:path";
15733
+ import * as path14 from "node:path";
15668
15734
 
15669
15735
  // src/render/stats_renderer.ts
15670
15736
  init_define_import_meta_env();
@@ -16445,7 +16511,7 @@ CREATE INDEX IF NOT EXISTS idx_stats_kind ON stats(kind);
16445
16511
  var _globalSchemaApplied = /* @__PURE__ */ new Set();
16446
16512
  registerReset(() => _globalSchemaApplied.clear());
16447
16513
  function getGlobalDb() {
16448
- const dbPath = path13.join(dataDir(), "global.db");
16514
+ const dbPath = path14.join(dataDir(), "global.db");
16449
16515
  const db = getDb(dbPath);
16450
16516
  if (!_globalSchemaApplied.has(dbPath)) {
16451
16517
  db.exec(GLOBAL_SCHEMA_SQL);
@@ -16639,9 +16705,9 @@ function renderStats2(opts) {
16639
16705
  // src/project.ts
16640
16706
  init_define_import_meta_env();
16641
16707
  import * as crypto2 from "node:crypto";
16642
- import * as fs12 from "node:fs";
16708
+ import * as fs13 from "node:fs";
16643
16709
  import * as os5 from "node:os";
16644
- import * as path14 from "node:path";
16710
+ import * as path15 from "node:path";
16645
16711
  var WSL_PREFIX_RE = /^\/mnt\/([a-zA-Z])\/(.*)$/;
16646
16712
  var CYGWIN_PREFIX_RE = /^\/cygdrive\/([a-zA-Z])\/(.*)$/;
16647
16713
  var MSYS_PREFIX_RE = /^\/([a-zA-Z])\/(.*)$/;
@@ -16678,9 +16744,9 @@ function canonicalize(inputPath) {
16678
16744
  const slashed = pathStr.replace(/\\/g, "/");
16679
16745
  let pre = isWin32 ? normalizeShellDrivePrefix(slashed) : slashed;
16680
16746
  if (pre !== slashed) {
16681
- pre = (isWin32 ? path14.win32.resolve : path14.resolve)(pre);
16747
+ pre = (isWin32 ? path15.win32.resolve : path15.resolve)(pre);
16682
16748
  } else {
16683
- pre = (isWin32 ? path14.win32.resolve : path14.resolve)(pathStr);
16749
+ pre = (isWin32 ? path15.win32.resolve : path15.resolve)(pathStr);
16684
16750
  }
16685
16751
  let normalized = pre.replace(/\\/g, "/");
16686
16752
  if (isWin32) {
@@ -16706,7 +16772,7 @@ function makeProjectAt(root) {
16706
16772
  );
16707
16773
  }
16708
16774
  try {
16709
- const stat2 = fs12.statSync(canonical);
16775
+ const stat2 = fs13.statSync(canonical);
16710
16776
  if (!stat2.isDirectory()) {
16711
16777
  throw new Error(`makeProjectAt: path is not a directory: ${canonical}`);
16712
16778
  }
@@ -16722,11 +16788,11 @@ function makeProjectAt(root) {
16722
16788
  function isRepoContainer(pathStr) {
16723
16789
  let nestedRepos = 0;
16724
16790
  try {
16725
- const entries = fs12.readdirSync(pathStr, { withFileTypes: true });
16791
+ const entries = fs13.readdirSync(pathStr, { withFileTypes: true });
16726
16792
  for (const entry of entries) {
16727
16793
  if (entry.isDirectory()) {
16728
- const gitPath = path14.join(pathStr, entry.name, ".git");
16729
- if (fs12.existsSync(gitPath)) {
16794
+ const gitPath = path15.join(pathStr, entry.name, ".git");
16795
+ if (fs13.existsSync(gitPath)) {
16730
16796
  nestedRepos++;
16731
16797
  if (nestedRepos >= REPO_CONTAINER_THRESHOLD) {
16732
16798
  return true;
@@ -16740,17 +16806,17 @@ function isRepoContainer(pathStr) {
16740
16806
  return false;
16741
16807
  }
16742
16808
  function markerExists(current, marker) {
16743
- const markerPath = path14.join(current, marker);
16809
+ const markerPath = path15.join(current, marker);
16744
16810
  try {
16745
- if (!fs12.existsSync(markerPath)) {
16811
+ if (!fs13.existsSync(markerPath)) {
16746
16812
  return false;
16747
16813
  }
16748
- const stat2 = fs12.lstatSync(markerPath);
16814
+ const stat2 = fs13.lstatSync(markerPath);
16749
16815
  if (!stat2.isSymbolicLink()) {
16750
16816
  return true;
16751
16817
  }
16752
- const resolved = fs12.realpathSync(markerPath);
16753
- const rel = path14.relative(path14.resolve(current), path14.resolve(resolved));
16818
+ const resolved = fs13.realpathSync(markerPath);
16819
+ const rel = path15.relative(path15.resolve(current), path15.resolve(resolved));
16754
16820
  return !rel.startsWith("..");
16755
16821
  } catch {
16756
16822
  return false;
@@ -16785,7 +16851,7 @@ function findProject(cwd) {
16785
16851
  break;
16786
16852
  }
16787
16853
  }
16788
- const parent = path14.dirname(current);
16854
+ const parent = path15.dirname(current);
16789
16855
  if (parent === current) {
16790
16856
  break;
16791
16857
  }
@@ -16796,11 +16862,11 @@ function findProject(cwd) {
16796
16862
 
16797
16863
  // src/skill_cache.ts
16798
16864
  init_define_import_meta_env();
16799
- import * as fs13 from "fs/promises";
16865
+ import * as fs14 from "fs/promises";
16800
16866
  import { createHash as createHash4 } from "crypto";
16801
16867
  import { resolve as resolve6 } from "path";
16802
16868
  import { homedir as homedir5 } from "os";
16803
- import { readdirSync as readdirSync6, readFileSync as readFileSync8, existsSync as existsSync8, statSync as statSync9, unlinkSync as unlinkSync4 } from "node:fs";
16869
+ import { readdirSync as readdirSync6, readFileSync as readFileSync9, existsSync as existsSync9, statSync as statSync9, unlinkSync as unlinkSync4 } from "node:fs";
16804
16870
  var COMPACT_END_MARKER = "<!-- COMPACT_END -->";
16805
16871
  var SKILLS_OUTPUT_SUBDIR = "skills";
16806
16872
  var _skillOutputsDirOverride = null;
@@ -16819,7 +16885,7 @@ function skillsSourceDir() {
16819
16885
  }
16820
16886
  async function ensureSkillsDir() {
16821
16887
  try {
16822
- await fs13.mkdir(skillOutputsDir(), { recursive: true });
16888
+ await fs14.mkdir(skillOutputsDir(), { recursive: true });
16823
16889
  } catch {
16824
16890
  }
16825
16891
  }
@@ -16927,14 +16993,14 @@ function extractNamedSection(body, heading) {
16927
16993
  async function listOutputs() {
16928
16994
  try {
16929
16995
  const dir = skillOutputsDir();
16930
- const entries = await fs13.readdir(dir, { withFileTypes: true });
16996
+ const entries = await fs14.readdir(dir, { withFileTypes: true });
16931
16997
  const metas = [];
16932
16998
  for (const entry of entries) {
16933
16999
  if (!entry.isFile() || !entry.name.endsWith(".meta")) {
16934
17000
  continue;
16935
17001
  }
16936
17002
  try {
16937
- const content = await fs13.readFile(resolve6(dir, entry.name), "utf-8");
17003
+ const content = await fs14.readFile(resolve6(dir, entry.name), "utf-8");
16938
17004
  const meta = JSON.parse(content);
16939
17005
  metas.push(meta);
16940
17006
  } catch {
@@ -16971,8 +17037,8 @@ async function findCrossSessionEntry(skillName, contentSha) {
16971
17037
  const bodyPath = resolve6(dir, `${meta.outputId}.txt`);
16972
17038
  const gzPath = resolve6(dir, `${meta.outputId}.gz`);
16973
17039
  try {
16974
- const bodyExists = await fs13.access(bodyPath).then(() => true).catch(() => false);
16975
- const gzExists = await fs13.access(gzPath).then(() => true).catch(() => false);
17040
+ const bodyExists = await fs14.access(bodyPath).then(() => true).catch(() => false);
17041
+ const gzExists = await fs14.access(gzPath).then(() => true).catch(() => false);
16976
17042
  if (bodyExists || gzExists) {
16977
17043
  return meta;
16978
17044
  }
@@ -17050,7 +17116,7 @@ function getCompactAnySessionSync(skillName) {
17050
17116
  if (!entry.isFile() || !entry.name.endsWith("@compact")) continue;
17051
17117
  if (!entry.name.endsWith(suffix)) continue;
17052
17118
  try {
17053
- const text = readFileSync8(resolve6(dir, entry.name), "utf-8");
17119
+ const text = readFileSync9(resolve6(dir, entry.name), "utf-8");
17054
17120
  if (text.trim()) return text;
17055
17121
  } catch {
17056
17122
  continue;
@@ -17079,7 +17145,7 @@ async function readSkillHits(skillName) {
17079
17145
  if (!name) return { count: 0, lastTs: 0 };
17080
17146
  const dir = skillOutputsDir();
17081
17147
  const hitsFile = resolve6(dir, `${sanitizeSkillId(name)}.hits`);
17082
- const content = await fs13.readFile(hitsFile, "utf-8").catch(() => null);
17148
+ const content = await fs14.readFile(hitsFile, "utf-8").catch(() => null);
17083
17149
  if (content) {
17084
17150
  const parsed = JSON.parse(content);
17085
17151
  return parsed;
@@ -17094,14 +17160,14 @@ var SKILL_HIT_LOCK_STALE_MS = 5e3;
17094
17160
  async function acquireSkillHitLock(lockPath) {
17095
17161
  for (let attempt = 0; attempt < SKILL_HIT_LOCK_MAX_ATTEMPTS; attempt++) {
17096
17162
  try {
17097
- await fs13.mkdir(lockPath);
17163
+ await fs14.mkdir(lockPath);
17098
17164
  return true;
17099
17165
  } catch (err2) {
17100
17166
  if (err2.code !== "EEXIST") return false;
17101
17167
  try {
17102
- const st = await fs13.stat(lockPath);
17168
+ const st = await fs14.stat(lockPath);
17103
17169
  if (Date.now() - st.mtimeMs > SKILL_HIT_LOCK_STALE_MS) {
17104
- await fs13.rmdir(lockPath).catch(() => {
17170
+ await fs14.rmdir(lockPath).catch(() => {
17105
17171
  });
17106
17172
  continue;
17107
17173
  }
@@ -17128,7 +17194,7 @@ async function incrementSkillHit(skillName) {
17128
17194
  hits.lastTs = Date.now();
17129
17195
  await atomicWriteText(hitsFile, JSON.stringify(hits, null, 2));
17130
17196
  } finally {
17131
- if (locked) await fs13.rmdir(lockPath).catch(() => {
17197
+ if (locked) await fs14.rmdir(lockPath).catch(() => {
17132
17198
  });
17133
17199
  }
17134
17200
  } catch {
@@ -17163,14 +17229,14 @@ async function listSkills(sessionId) {
17163
17229
  let compactLen = 0;
17164
17230
  let compactText = "";
17165
17231
  try {
17166
- const stat2 = await fs13.stat(resolve6(dir, compactFileId));
17232
+ const stat2 = await fs14.stat(resolve6(dir, compactFileId));
17167
17233
  compactLen = stat2.size;
17168
- compactText = await fs13.readFile(resolve6(dir, compactFileId), "utf-8").catch(() => "");
17234
+ compactText = await fs14.readFile(resolve6(dir, compactFileId), "utf-8").catch(() => "");
17169
17235
  } catch {
17170
17236
  compactLen = 0;
17171
17237
  }
17172
17238
  const hasMarker = extractCompactFromMarker(
17173
- await fs13.readFile(resolve6(dir, `${meta.outputId}.txt`), "utf-8").catch(() => "")
17239
+ await fs14.readFile(resolve6(dir, `${meta.outputId}.txt`), "utf-8").catch(() => "")
17174
17240
  ) !== null;
17175
17241
  const compactStale = isCompactStale(compactText, meta.skillName, meta.contentSha);
17176
17242
  const { count: hitCount } = await readSkillHits(meta.skillName);
@@ -17210,7 +17276,7 @@ async function installedSkillPath(skillName) {
17210
17276
  if (!name) return null;
17211
17277
  const diskPath = resolve6(skillsSourceDir(), name, "SKILL.md");
17212
17278
  try {
17213
- await fs13.access(diskPath);
17279
+ await fs14.access(diskPath);
17214
17280
  return diskPath;
17215
17281
  } catch {
17216
17282
  return null;
@@ -17220,7 +17286,7 @@ function pruneSkillOutputs(maxCount = DEFAULT_MAX_COUNT, maxAgeMs = DEFAULT_MAX_
17220
17286
  const dir = skillOutputsDir();
17221
17287
  let removed = 0;
17222
17288
  try {
17223
- if (!existsSync8(dir)) return 0;
17289
+ if (!existsSync9(dir)) return 0;
17224
17290
  const cutoff = Date.now() - maxAgeMs;
17225
17291
  const entries = [];
17226
17292
  for (const file of readdirSync6(dir)) {
@@ -17228,7 +17294,7 @@ function pruneSkillOutputs(maxCount = DEFAULT_MAX_COUNT, maxAgeMs = DEFAULT_MAX_
17228
17294
  const outputId = file.slice(0, -".meta".length);
17229
17295
  let ts;
17230
17296
  try {
17231
- const parsed = JSON.parse(readFileSync8(resolve6(dir, file), "utf-8"));
17297
+ const parsed = JSON.parse(readFileSync9(resolve6(dir, file), "utf-8"));
17232
17298
  ts = parsed.ts;
17233
17299
  } catch {
17234
17300
  try {
@@ -17270,8 +17336,8 @@ function pruneSkillOutputs(maxCount = DEFAULT_MAX_COUNT, maxAgeMs = DEFAULT_MAX_
17270
17336
 
17271
17337
  // src/image_shrink.ts
17272
17338
  init_define_import_meta_env();
17273
- import * as fs14 from "node:fs";
17274
- import * as path15 from "node:path";
17339
+ import * as fs15 from "node:fs";
17340
+ import * as path16 from "node:path";
17275
17341
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
17276
17342
  ".png",
17277
17343
  ".jpg",
@@ -17298,7 +17364,7 @@ async function loadSharp() {
17298
17364
  return _sharpCache;
17299
17365
  }
17300
17366
  function isImagePath(p) {
17301
- return IMAGE_EXTENSIONS.has(path15.extname(p).toLowerCase());
17367
+ return IMAGE_EXTENSIONS.has(path16.extname(p).toLowerCase());
17302
17368
  }
17303
17369
  async function shrinkImage(input, opts) {
17304
17370
  const maxDimension = opts?.maxDimension ?? DEFAULT_MAX_DIMENSION;
@@ -17330,7 +17396,7 @@ async function shrinkImage(input, opts) {
17330
17396
  }
17331
17397
  function statSize(absPath) {
17332
17398
  try {
17333
- const st = fs14.statSync(absPath);
17399
+ const st = fs15.statSync(absPath);
17334
17400
  return st.isFile() ? st.size : null;
17335
17401
  } catch {
17336
17402
  return null;
@@ -17344,7 +17410,7 @@ async function preReadImageHandler(event) {
17344
17410
  if (size === null || size < DEFAULT_SIZE_THRESHOLD_BYTES) return passOutput();
17345
17411
  let input;
17346
17412
  try {
17347
- input = fs14.readFileSync(filePath);
17413
+ input = fs15.readFileSync(filePath);
17348
17414
  } catch {
17349
17415
  return passOutput();
17350
17416
  }
@@ -17353,7 +17419,7 @@ async function preReadImageHandler(event) {
17353
17419
  const saved = result.originalBytes - result.shrunkBytes;
17354
17420
  const pct = Math.round(saved / result.originalBytes * 100);
17355
17421
  const dataUrl = `data:image/${result.format};base64,${result.data.toString("base64")}`;
17356
- const summary = `token-goat shrank ${path15.basename(filePath)}: ${Math.round(result.originalBytes / 1024)}kb -> ${Math.round(result.shrunkBytes / 1024)}kb (${pct}% smaller, ${result.width}x${result.height} ${result.format}).`;
17422
+ const summary = `token-goat shrank ${path16.basename(filePath)}: ${Math.round(result.originalBytes / 1024)}kb -> ${Math.round(result.shrunkBytes / 1024)}kb (${pct}% smaller, ${result.width}x${result.height} ${result.format}).`;
17357
17423
  return contextOutput(`${summary}
17358
17424
  ${dataUrl}`);
17359
17425
  }
@@ -17361,16 +17427,16 @@ registerHook("pre_tool_use", preReadImageHandler, { toolName: "Read" });
17361
17427
 
17362
17428
  // src/doc_compact.ts
17363
17429
  init_define_import_meta_env();
17364
- import * as fs15 from "fs";
17430
+ import * as fs16 from "fs";
17365
17431
  import * as crypto3 from "crypto";
17366
- import * as path16 from "path";
17432
+ import * as path17 from "path";
17367
17433
  var defaultSentencesPerSection = 2;
17368
17434
  var headerPrefix = "<!-- token-goat doc-compact source-hash:";
17369
17435
  var headerRegex = /^<!-- token-goat doc-compact source-hash:(\S+) source:(.+?) -->$/;
17370
17436
  var compactSubdir = "doc_compacts";
17371
17437
  function sourceHash(filePath) {
17372
17438
  try {
17373
- const data = fs15.readFileSync(filePath);
17439
+ const data = fs16.readFileSync(filePath);
17374
17440
  return crypto3.createHash("sha256").update(data).digest("hex");
17375
17441
  } catch {
17376
17442
  return "";
@@ -17378,18 +17444,18 @@ function sourceHash(filePath) {
17378
17444
  }
17379
17445
  function _compactSlug(absPathStr) {
17380
17446
  const h = crypto3.createHash("sha256").update(absPathStr.toLowerCase()).digest("hex").slice(0, 12);
17381
- const ext2 = path16.extname(absPathStr);
17382
- const stem = path16.basename(absPathStr, ext2);
17447
+ const ext2 = path17.extname(absPathStr);
17448
+ const stem = path17.basename(absPathStr, ext2);
17383
17449
  const safeStem = stem.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 32);
17384
17450
  return `${h}_${safeStem}`;
17385
17451
  }
17386
17452
  function compactPathFor(sourcePath) {
17387
- const abs = path16.resolve(sourcePath);
17388
- return path16.join(dataDir(), compactSubdir, `${_compactSlug(abs)}.md`);
17453
+ const abs = path17.resolve(sourcePath);
17454
+ return path17.join(dataDir(), compactSubdir, `${_compactSlug(abs)}.md`);
17389
17455
  }
17390
17456
  function readCompactHeader(compactPath) {
17391
17457
  try {
17392
- const text = fs15.readFileSync(compactPath, "utf-8");
17458
+ const text = fs16.readFileSync(compactPath, "utf-8");
17393
17459
  const firstLine = text.split("\n")[0] || "";
17394
17460
  const m = firstLine.match(headerRegex);
17395
17461
  if (!m || !m[1] || !m[2]) return null;
@@ -17408,15 +17474,15 @@ function isCompactFresh(compactPath, sourcePath) {
17408
17474
  }
17409
17475
  function markCompactStale(compactPath) {
17410
17476
  try {
17411
- if (!fs15.existsSync(compactPath)) return false;
17412
- const text = fs15.readFileSync(compactPath, "utf-8");
17477
+ if (!fs16.existsSync(compactPath)) return false;
17478
+ const text = fs16.readFileSync(compactPath, "utf-8");
17413
17479
  const lines = text.split("\n");
17414
17480
  if (!lines[0]) return false;
17415
17481
  const m = lines[0].match(headerRegex);
17416
17482
  if (!m || m[1] === "STALE") return false;
17417
17483
  const oldHash = m[1];
17418
17484
  lines[0] = lines[0].replace(`source-hash:${oldHash}`, "source-hash:STALE");
17419
- fs15.writeFileSync(compactPath, lines.join("\n"), "utf-8");
17485
+ fs16.writeFileSync(compactPath, lines.join("\n"), "utf-8");
17420
17486
  return true;
17421
17487
  } catch {
17422
17488
  return false;
@@ -17424,7 +17490,7 @@ function markCompactStale(compactPath) {
17424
17490
  }
17425
17491
  function readCompactBody(compactPath) {
17426
17492
  try {
17427
- const text = fs15.readFileSync(compactPath, "utf-8");
17493
+ const text = fs16.readFileSync(compactPath, "utf-8");
17428
17494
  const lines = text.split("\n");
17429
17495
  if (lines.length < 2) return null;
17430
17496
  const body = lines.slice(1).join("\n").trimStart();
@@ -17434,17 +17500,17 @@ function readCompactBody(compactPath) {
17434
17500
  }
17435
17501
  }
17436
17502
  function writeCompact(compactPath, sourcePath, compactBody, sourceRel) {
17437
- const srcPath = path16.resolve(sourcePath);
17503
+ const srcPath = path17.resolve(sourcePath);
17438
17504
  const sha = sourceHash(srcPath);
17439
- const displayRel = sourceRel || path16.basename(srcPath);
17505
+ const displayRel = sourceRel || path17.basename(srcPath);
17440
17506
  const header = `${headerPrefix}${sha} source:${displayRel} -->
17441
17507
  `;
17442
17508
  const fullText = header + compactBody.trimStart();
17443
- const dir = path16.dirname(compactPath);
17444
- if (!fs15.existsSync(dir)) {
17445
- fs15.mkdirSync(dir, { recursive: true });
17509
+ const dir = path17.dirname(compactPath);
17510
+ if (!fs16.existsSync(dir)) {
17511
+ fs16.mkdirSync(dir, { recursive: true });
17446
17512
  }
17447
- fs15.writeFileSync(compactPath, fullText, "utf-8");
17513
+ fs16.writeFileSync(compactPath, fullText, "utf-8");
17448
17514
  }
17449
17515
  function buildExtractiveCompact(text, maxSentences) {
17450
17516
  const max = maxSentences ?? defaultSentencesPerSection;
@@ -17536,7 +17602,7 @@ function extractDocCompact(body, heading) {
17536
17602
  }
17537
17603
  function compactDoc(filePath, heading) {
17538
17604
  try {
17539
- const body = fs15.readFileSync(filePath, "utf-8");
17605
+ const body = fs16.readFileSync(filePath, "utf-8");
17540
17606
  const compact = extractDocCompact(body, heading);
17541
17607
  return compact || null;
17542
17608
  } catch {
@@ -17570,7 +17636,7 @@ function sessionArtifactRecall(filePath) {
17570
17636
  }
17571
17637
  function statSize2(absPath) {
17572
17638
  try {
17573
- return fs16.statSync(absPath).size;
17639
+ return fs17.statSync(absPath).size;
17574
17640
  } catch {
17575
17641
  return null;
17576
17642
  }
@@ -17626,7 +17692,7 @@ function buildLineDiff(oldContent, newContent, label) {
17626
17692
  }
17627
17693
  function scanCrossSessionManifests(projectRoot, projectHash2, filePath, ttlSecs) {
17628
17694
  try {
17629
- const relPath = path17.relative(projectRoot, filePath).replace(/\\/g, "/");
17695
+ const relPath = path18.relative(projectRoot, filePath).replace(/\\/g, "/");
17630
17696
  const manifests = readAllSessionManifests(projectHash2, ttlSecs);
17631
17697
  for (const data of manifests) {
17632
17698
  const files = data["files"];
@@ -17654,7 +17720,7 @@ function preReadHandler(event) {
17654
17720
  "node_modules is typically noise; use npm ls, npm outdated, or npm audit instead for dependency info. To force access, use: token-goat read node_modules/package/file.js::symbol-name or token-goat section node_modules/package/file.js::heading"
17655
17721
  );
17656
17722
  }
17657
- const basename13 = path17.basename(normalized);
17723
+ const basename13 = path18.basename(normalized);
17658
17724
  if (isLockFile(basename13)) {
17659
17725
  return denyOutput(
17660
17726
  'Lock files are rarely useful to read in full. Use `token-goat section "' + normalized + '::<section>"` to extract a specific dependency, or read the relevant manifest instead.'
@@ -17690,7 +17756,7 @@ function preReadHandler(event) {
17690
17756
  const skillName = detectSkillFile(normalized);
17691
17757
  if (skillName && basename13 === "SKILL.md") {
17692
17758
  try {
17693
- const body = fs16.readFileSync(normalized, "utf-8");
17759
+ const body = fs17.readFileSync(normalized, "utf-8");
17694
17760
  const bodySha = contentHash(body);
17695
17761
  const compact = getCompactAnySessionSync(skillName);
17696
17762
  const stale = isCompactStale(compact, skillName, bodySha);
@@ -17726,7 +17792,7 @@ function preReadHandler(event) {
17726
17792
  const sz = statSize2(normalized);
17727
17793
  if (sz !== null && sz >= MARKDOWN_SIZE_THRESHOLD) {
17728
17794
  markdownSize = sz;
17729
- fileContent = fs16.readFileSync(normalized, "utf8");
17795
+ fileContent = fs17.readFileSync(normalized, "utf8");
17730
17796
  }
17731
17797
  } catch {
17732
17798
  }
@@ -17783,7 +17849,7 @@ function preReadHandler(event) {
17783
17849
  try {
17784
17850
  const sz = statSize2(normalized);
17785
17851
  if (sz !== null && sz <= 256 * 1024) {
17786
- const currentContent = fs16.readFileSync(normalized, "utf8");
17852
+ const currentContent = fs17.readFileSync(normalized, "utf8");
17787
17853
  const TRUNC_MARKER = "\n<snapshot truncated at ";
17788
17854
  const oldRaw = oldArtifactSnap.toString("utf8");
17789
17855
  const truncIdx = oldRaw.indexOf(TRUNC_MARKER);
@@ -17826,7 +17892,7 @@ function preReadHandler(event) {
17826
17892
  recordFileRead(normalized);
17827
17893
  recordStat("session_hint", 0, 0);
17828
17894
  return denyOutput(
17829
- 'File was truncated on last read (>33K tokens). Use `token-goat skeleton "' + normalized + '"` for structure or `token-goat read "' + normalized + '::SymbolName"` for one function.'
17895
+ 'File was truncated on last read (>33K tokens). Use `token-goat skeleton "' + normalized + '"` for structure or `token-goat read "' + normalized + '::SymbolName"` for one function. To edit it anyway, use `token-goat replace "' + normalized + "\" --old-from <oldfile> --new-from <newfile>` \u2014 Read/Edit's own precondition can't be satisfied after this deny."
17830
17896
  );
17831
17897
  }
17832
17898
  const sessionId = getSessionId();
@@ -17835,7 +17901,7 @@ function preReadHandler(event) {
17835
17901
  try {
17836
17902
  const sz = statSize2(normalized);
17837
17903
  if (sz !== null && sz <= 256 * 1024) {
17838
- const currentContent = fs16.readFileSync(normalized, "utf8");
17904
+ const currentContent = fs17.readFileSync(normalized, "utf8");
17839
17905
  const TRUNC_MARKER = "\n<snapshot truncated at ";
17840
17906
  const oldRaw = oldSnap.toString("utf8");
17841
17907
  const truncIdx = oldRaw.indexOf(TRUNC_MARKER);
@@ -17872,7 +17938,7 @@ function preReadHandler(event) {
17872
17938
  if (!project) {
17873
17939
  project = makeProjectAt(cwd);
17874
17940
  }
17875
- const relPath = path17.relative(project.root, normalized).replace(/\\/g, "/");
17941
+ const relPath = path18.relative(project.root, normalized).replace(/\\/g, "/");
17876
17942
  if (!relPath.startsWith("..")) {
17877
17943
  const ttlSecs = config.hints.cross_session_read_dedup_ttl_secs;
17878
17944
  if (scanCrossSessionManifests(project.root, project.hash, normalized, ttlSecs)) {
@@ -17901,25 +17967,25 @@ function preReadHandler(event) {
17901
17967
  }
17902
17968
  if (wasFileTruncatedThisSession(normalized)) {
17903
17969
  return denyOutput(
17904
- 'File was truncated on last read (>33K tokens). Use `token-goat skeleton "' + normalized + '"` for structure or `token-goat read "' + normalized + '::SymbolName"` for one function.'
17970
+ 'File was truncated on last read (>33K tokens). Use `token-goat skeleton "' + normalized + '"` for structure or `token-goat read "' + normalized + '::SymbolName"` for one function. To edit it anyway, use `token-goat replace "' + normalized + "\" --old-from <oldfile> --new-from <newfile>` \u2014 Read/Edit's own precondition can't be satisfied after this deny."
17905
17971
  );
17906
17972
  }
17907
17973
  if (/\.(md|mdx|markdown|rst)$/i.test(basename13)) {
17908
17974
  return denyOutput(
17909
- 'Markdown file already read this session. Use `token-goat section "' + normalized + '::HeadingName"` to read one section.'
17975
+ 'Markdown file already read this session. Use `token-goat section "' + normalized + '::HeadingName"` to read one section. To edit it anyway, use `token-goat replace "' + normalized + "\" --old-from <oldfile> --new-from <newfile>` \u2014 Read/Edit's own precondition can't be satisfied after this deny."
17910
17976
  );
17911
17977
  }
17912
17978
  const isSourceExt = /\.(ts|tsx|js|jsx|py|go|rs|java|rb|php|swift|kt|cpp|c|h)$/i.test(basename13);
17913
17979
  if (isSourceExt && reads >= 2) {
17914
17980
  recordStat("read_count_deny", rereadBytes, Math.round(rereadBytes / 4));
17915
17981
  return denyOutput(
17916
- "Read this file " + reads + ' times already \u2014 use `token-goat read "' + normalized + '::Symbol"`, `token-goat skeleton ' + normalized + "`, or `token-goat outline " + normalized + "` to pull just the part you need."
17982
+ "Read this file " + reads + ' times already \u2014 use `token-goat read "' + normalized + '::Symbol"`, `token-goat skeleton ' + normalized + "`, or `token-goat outline " + normalized + '` to pull just the part you need. To edit it anyway, use `token-goat replace "' + normalized + "\" --old-from <oldfile> --new-from <newfile>` \u2014 Read/Edit's own precondition can't be satisfied after this deny."
17917
17983
  );
17918
17984
  }
17919
17985
  const hint = _isDocFile(normalized) ? 'Use `token-goat section "' + normalized + '::SectionName"` to read one section.' : "Use token-goat read/section/symbol to re-read surgically.";
17920
17986
  if (rereadBytes >= REREAD_DENY_BYTES || reads >= 2) {
17921
17987
  return denyOutput(
17922
- normalized + " was already read this session (" + reads + " " + plural2 + "). " + hint
17988
+ normalized + " was already read this session (" + reads + " " + plural2 + "). " + hint + ' To edit it anyway, use `token-goat replace "' + normalized + "\" --old-from <oldfile> --new-from <newfile>` \u2014 Read/Edit's own precondition can't be satisfied after this deny."
17923
17989
  );
17924
17990
  }
17925
17991
  return contextOutput(
@@ -17934,7 +18000,7 @@ function preReadHandler(event) {
17934
18000
  recordStat("session_hint", size, Math.round(size / 4));
17935
18001
  if (size >= LARGE_FILE_DENY_BYTES) {
17936
18002
  return denyOutput(
17937
- normalized + " is very large (" + kb + "KB). " + hint + " Use Read with offset/limit to sample specific sections."
18003
+ normalized + " is very large (" + kb + "KB). " + hint + ' Use Read with offset/limit to sample specific sections. To edit it anyway, use `token-goat replace "' + normalized + "\" --old-from <oldfile> --new-from <newfile>` \u2014 Read/Edit's own precondition can't be satisfied after this deny."
17938
18004
  );
17939
18005
  }
17940
18006
  recordFileRead(normalized);
@@ -17945,7 +18011,7 @@ function preReadHandler(event) {
17945
18011
  "Note: " + normalized + " is large (" + kb + "KB). " + hint
17946
18012
  );
17947
18013
  }
17948
- const fileTypeExt = path17.extname(normalized).slice(1).toLowerCase();
18014
+ const fileTypeExt = path18.extname(normalized).slice(1).toLowerCase();
17949
18015
  const binaryExts = /* @__PURE__ */ new Set(["pdf", "docx", "xlsx", "pptx", "odt", "ods", "ott", "odp"]);
17950
18016
  const textTypeExts = /* @__PURE__ */ new Set(["html", "htm", "xhtml", "txt", "log", "out", "err", "trace", "csv", "tsv"]);
17951
18017
  const fileStatSize = size ?? statSize2(normalized) ?? 0;
@@ -17954,7 +18020,7 @@ function preReadHandler(event) {
17954
18020
  let ftContent = "";
17955
18021
  if (!binaryExts.has(fileTypeExt)) {
17956
18022
  try {
17957
- ftContent = fs16.readFileSync(normalized, "utf8");
18023
+ ftContent = fs17.readFileSync(normalized, "utf8");
17958
18024
  } catch {
17959
18025
  }
17960
18026
  }
@@ -17987,13 +18053,13 @@ function postReadHandler(event) {
17987
18053
  if (respText.includes("[Truncated:") || respText.includes("Truncated: PARTIAL view")) {
17988
18054
  markFileTruncated(normalized);
17989
18055
  }
17990
- const postBasename = path17.basename(normalized);
18056
+ const postBasename = path18.basename(normalized);
17991
18057
  const diffSourcesEnabled = loadConfig().hints.serve_diff_on_reread;
17992
18058
  if (/\.(md|mdx|markdown|rst|txt)$/i.test(postBasename) || isSessionArtifactFile(normalized) || diffSourcesEnabled && DIFFABLE_SOURCE_RE.test(postBasename)) {
17993
18059
  try {
17994
18060
  const sz = statSize2(normalized);
17995
18061
  if (sz !== null && sz <= 256 * 1024) {
17996
- const content = fs16.readFileSync(normalized);
18062
+ const content = fs17.readFileSync(normalized);
17997
18063
  store(getSessionId(), normalized, content);
17998
18064
  }
17999
18065
  } catch {
@@ -18009,7 +18075,7 @@ function postReadHandler(event) {
18009
18075
  const sessionState = exportSessionState();
18010
18076
  const mappedFiles = [];
18011
18077
  for (const fileEntry of sessionState.files) {
18012
- const relPath = path17.relative(project.root, fileEntry.path).replace(/\\/g, "/");
18078
+ const relPath = path18.relative(project.root, fileEntry.path).replace(/\\/g, "/");
18013
18079
  if (!relPath.startsWith("..")) {
18014
18080
  mappedFiles.push({
18015
18081
  rel_path: relPath,
@@ -18028,65 +18094,6 @@ registerHook("post_tool_use", postReadHandler, { toolName: "Read" });
18028
18094
  // src/hooks_edit.ts
18029
18095
  init_define_import_meta_env();
18030
18096
  import * as path19 from "node:path";
18031
-
18032
- // src/hooks_index.ts
18033
- init_define_import_meta_env();
18034
- import * as fs17 from "node:fs";
18035
- import * as path18 from "node:path";
18036
- function dirtyQueuePath() {
18037
- return path18.join(dataDir(), "queue", "dirty.txt");
18038
- }
18039
- function appendDirtyPath(normalizedPath) {
18040
- const queuePath = dirtyQueuePath();
18041
- const dir = path18.dirname(queuePath);
18042
- try {
18043
- fs17.mkdirSync(dir, { recursive: true });
18044
- } catch (e) {
18045
- if (e.code !== "EEXIST" || !fs17.existsSync(dir)) throw e;
18046
- }
18047
- let leadingNewline = "";
18048
- try {
18049
- const existing = fs17.readFileSync(queuePath, "utf8");
18050
- if (existing.length > 0 && !existing.endsWith("\n")) leadingNewline = "\n";
18051
- } catch {
18052
- }
18053
- fs17.appendFileSync(queuePath, `${leadingNewline}${normalizedPath}
18054
- `);
18055
- }
18056
- function getDirtyPaths() {
18057
- const queuePath = dirtyQueuePath();
18058
- let raw;
18059
- try {
18060
- raw = fs17.readFileSync(queuePath, "utf8");
18061
- } catch {
18062
- return [];
18063
- }
18064
- const seen = /* @__PURE__ */ new Set();
18065
- const out2 = [];
18066
- for (const line of raw.split("\n")) {
18067
- const trimmed = line.trim();
18068
- if (trimmed === "" || seen.has(trimmed)) continue;
18069
- seen.add(trimmed);
18070
- out2.push(trimmed);
18071
- }
18072
- return out2;
18073
- }
18074
- function preCompactIndexHandler(_event) {
18075
- const paths = getDirtyPaths();
18076
- if (paths.length > 0) {
18077
- const sidecar = path18.join(dataDir(), "queue", "pending.txt");
18078
- try {
18079
- fs17.mkdirSync(path18.dirname(sidecar), { recursive: true });
18080
- atomicWriteBytes(sidecar, Buffer.from(`${paths.join("\n")}
18081
- `, "utf8"));
18082
- } catch {
18083
- }
18084
- }
18085
- return passOutput();
18086
- }
18087
- registerHook("pre_compact", preCompactIndexHandler);
18088
-
18089
- // src/hooks_edit.ts
18090
18097
  function postEditHandler(event) {
18091
18098
  const filePath = getFilePath(event);
18092
18099
  if (filePath === void 0) return passOutput();
@@ -32015,15 +32022,24 @@ function extractHeadFile(cmd) {
32015
32022
  return { filePath, isDoc, isConfig };
32016
32023
  }
32017
32024
  function extractSedRange(cmd) {
32018
- const m = /^sed\s+-n\s+['"](\d+),(\d+)p['"]\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>\/dev\/null)?\s*$/.exec(cmd);
32025
+ const m = /^sed\s+-n\s+['"](?:\d+,\d+p)(?:;\d+,\d+p)*['"]\s+(?:"([^"]+)"|'([^']+)'|(\S+))(?:\s+2>\/dev\/null)?\s*$/.exec(cmd);
32019
32026
  if (!m) return null;
32020
- const start = parseInt(m[1], 10);
32021
- const end = parseInt(m[2], 10);
32022
- const filePath = m[3] ?? m[4] ?? m[5];
32027
+ const quotedAddress = /['"]([^'"]+)['"]/.exec(cmd);
32028
+ if (!quotedAddress) return null;
32029
+ const ranges = [];
32030
+ for (const clause of quotedAddress[1].split(";")) {
32031
+ const cm = /^(\d+),(\d+)p$/.exec(clause ?? "");
32032
+ if (!cm) return null;
32033
+ const start = parseInt(cm[1], 10);
32034
+ const end = parseInt(cm[2], 10);
32035
+ if (start < 1 || end < start) return null;
32036
+ ranges.push([start, end]);
32037
+ }
32038
+ if (ranges.length === 0) return null;
32039
+ const filePath = m[1] ?? m[2] ?? m[3];
32023
32040
  if (filePath === void 0) return null;
32024
32041
  if (isTempPath(filePath)) return null;
32025
- if (start < 1 || end < start) return null;
32026
- return { filePath, start, end };
32042
+ return { filePath, ranges };
32027
32043
  }
32028
32044
  var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
32029
32045
  "python",
@@ -32044,20 +32060,21 @@ var SYMBOL_BEARING_LANGUAGES = /* @__PURE__ */ new Set([
32044
32060
  "bash",
32045
32061
  "powershell"
32046
32062
  ]);
32047
- function sedRangeHint(filePath, start, end) {
32063
+ function sedRangeHint(filePath, ranges) {
32048
32064
  const lang = detectLanguage(filePath);
32049
- const rangeRead = '`token-goat read "' + filePath + "@" + start + "-" + end + '"`';
32065
+ const rangeReads = ranges.map(([s, e]) => '`token-goat read "' + filePath + "@" + s + "-" + e + '"`');
32066
+ const allReads = rangeReads.length === 2 ? rangeReads.join(" and ") : rangeReads.length >= 3 ? rangeReads.slice(0, -1).join(", ") + ", and " + rangeReads[rangeReads.length - 1] : rangeReads[0];
32050
32067
  const prefix = "`sed -n` line-range reads bypass read hooks. ";
32051
32068
  if (lang === "markdown") {
32052
- return prefix + 'For Markdown, `token-goat section "' + filePath + '::<heading>"` extracts a whole section by name (robust to line shifts); or ' + rangeRead + " for exactly those lines.";
32069
+ return prefix + 'For Markdown, `token-goat section "' + filePath + '::<heading>"` extracts a whole section by name (robust to line shifts); or ' + allReads + " for exactly those lines.";
32053
32070
  }
32054
32071
  if (lang === "toml" || lang === "json" || lang === "yaml" || lang === "ini") {
32055
- return prefix + 'For config, `token-goat config-get "' + filePath + '" <key>` or `token-goat section "' + filePath + '::<block>"` extracts one value; or ' + rangeRead + " for exactly those lines.";
32072
+ return prefix + 'For config, `token-goat config-get "' + filePath + '" <key>` or `token-goat section "' + filePath + '::<block>"` extracts one value; or ' + allReads + " for exactly those lines.";
32056
32073
  }
32057
32074
  if (SYMBOL_BEARING_LANGUAGES.has(lang)) {
32058
- return prefix + 'For a whole function/class, `token-goat symbol <name>` or `token-goat read "' + filePath + '::<Symbol>"` is robust to line shifts; or ' + rangeRead + " for exactly those lines.";
32075
+ return prefix + 'For a whole function/class, `token-goat symbol <name>` or `token-goat read "' + filePath + '::<Symbol>"` is robust to line shifts; or ' + allReads + " for exactly those lines.";
32059
32076
  }
32060
- return prefix + "Use " + rangeRead + " to read exactly those lines.";
32077
+ return prefix + "Use " + allReads + " to read exactly those lines.";
32061
32078
  }
32062
32079
  function findRangeOverlap(prior, start, end) {
32063
32080
  let best = null;
@@ -32541,15 +32558,23 @@ function preBashHandler(event) {
32541
32558
  }
32542
32559
  const sedRange = extractSedRange(cmd);
32543
32560
  if (sedRange !== null) {
32544
- const { filePath, start, end } = sedRange;
32561
+ const { filePath, ranges } = sedRange;
32545
32562
  recordStat("session_hint", 0, 0);
32546
32563
  const sedDedupKey = resolveIndexPath(filePath, preHookCwd ?? process.cwd());
32547
- const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
32548
- recordFileLineRange(sedDedupKey, start, end);
32549
- if (priorOverlap !== null) {
32550
- return contextOutput(sedOverlapHint(filePath, priorOverlap, start, end));
32564
+ const overlapHints = [];
32565
+ const freshRanges = [];
32566
+ for (const [start, end] of ranges) {
32567
+ const priorOverlap = findRangeOverlap(getFileLineRanges(sedDedupKey), start, end);
32568
+ recordFileLineRange(sedDedupKey, start, end);
32569
+ if (priorOverlap !== null) {
32570
+ overlapHints.push(sedOverlapHint(filePath, priorOverlap, start, end));
32571
+ } else {
32572
+ freshRanges.push([start, end]);
32573
+ }
32551
32574
  }
32552
- return contextOutput(sedRangeHint(filePath, start, end));
32575
+ const hints = [...overlapHints];
32576
+ if (freshRanges.length > 0) hints.push(sedRangeHint(filePath, freshRanges));
32577
+ return contextOutput(hints.join(" "));
32553
32578
  }
32554
32579
  const catJsonPipe = extractCatJsonPipe(cmd);
32555
32580
  if (catJsonPipe !== null) {
@@ -40161,6 +40186,12 @@ function atomicWriteBuffer(dest, data) {
40161
40186
  throw e;
40162
40187
  }
40163
40188
  }
40189
+ function enqueueDirtyPathSafe(filePath) {
40190
+ try {
40191
+ appendDirtyPath(resolveIndexPath(filePath));
40192
+ } catch {
40193
+ }
40194
+ }
40164
40195
  function mapFsError(e, src, dest) {
40165
40196
  const fe = e;
40166
40197
  if (fe.code === "ENOENT") {
@@ -40171,6 +40202,9 @@ function mapFsError(e, src, dest) {
40171
40202
  throw new CliError(`destination directory does not exist: ${destDir}`);
40172
40203
  }
40173
40204
  if (fe.code === "ENOTDIR") {
40205
+ if (src !== void 0 && dest === void 0) {
40206
+ throw new CliError(`source path contains a file where a directory was expected: ${src}`);
40207
+ }
40174
40208
  throw new CliError(`destination path contains a file where a directory was expected: ${dest ?? fe.path ?? ""}`);
40175
40209
  }
40176
40210
  if (fe.code === "EISDIR") {
@@ -40180,6 +40214,9 @@ function mapFsError(e, src, dest) {
40180
40214
  throw new CliError(`destination is a directory, not a file: ${dest ?? (errPath || "(unknown)")}`);
40181
40215
  }
40182
40216
  if (fe.code === "EACCES" || fe.code === "EPERM") {
40217
+ if (src !== void 0 && dest === void 0) {
40218
+ throw new CliError(`permission denied reading: ${src}`);
40219
+ }
40183
40220
  throw new CliError(`permission denied writing to: ${dest ?? fe.path ?? ""}`);
40184
40221
  }
40185
40222
  if (fe.code === "EROFS") {
@@ -40233,6 +40270,76 @@ var WIN_RESERVED = /* @__PURE__ */ new Set([
40233
40270
  "CONIN$",
40234
40271
  "CONOUT$"
40235
40272
  ]);
40273
+ function validateWritablePath(dest, label) {
40274
+ if (!dest || !dest.trim()) {
40275
+ throw new CliError(`${label} path cannot be empty`);
40276
+ }
40277
+ if (dest.includes("\0")) {
40278
+ throw new CliError(`${label} path contains a null byte`);
40279
+ }
40280
+ if (isWindows()) {
40281
+ const base = path33.basename(dest);
40282
+ const stem = base.replace(/\.[^.]*$/, "").toUpperCase();
40283
+ if (WIN_RESERVED.has(stem)) {
40284
+ throw new CliError(`${label} '${base}' is a reserved Windows device name`);
40285
+ }
40286
+ if (base.endsWith(".") || base.endsWith(" ")) {
40287
+ throw new CliError(`${label} filename '${base}' ends with '${base.slice(-1)}' \u2014 Windows NTFS silently strips trailing dots and spaces, which would clobber a different file`);
40288
+ }
40289
+ }
40290
+ }
40291
+ function parseMaxStdinMB() {
40292
+ const raw = process.env["TOKEN_GOAT_MAX_STDIN_MB"] ?? "512";
40293
+ const maxMB = parseInt(raw, 10);
40294
+ if (!Number.isFinite(maxMB) || maxMB <= 0) {
40295
+ throw new CliError(`TOKEN_GOAT_MAX_STDIN_MB must be a positive integer; got '${raw}'`);
40296
+ }
40297
+ return maxMB;
40298
+ }
40299
+ function readTextFileBounded(filePath, label, allowStdIn = false) {
40300
+ if (!filePath || !filePath.trim()) {
40301
+ throw new CliError(`${label} path cannot be empty`);
40302
+ }
40303
+ if (filePath.includes("\0")) {
40304
+ throw new CliError(`${label} path contains a null byte`);
40305
+ }
40306
+ if (!allowStdIn && !isWindows() && /^\/dev\/(stdin|fd\/0)$|^\/proc\/self\/fd\/0$/.test(filePath) && process.stdin.isTTY) {
40307
+ const altLabel = label.endsWith("-from") ? label.replace("-from", "-b64") : "a regular file path";
40308
+ throw new CliError(`${label} ${filePath} requires piped input; use ${altLabel} for interactive use`);
40309
+ }
40310
+ try {
40311
+ const st = fs31.statSync(filePath);
40312
+ if (st.isFIFO() || st.isSocket()) {
40313
+ throw new CliError(`${label} '${filePath}' is a special file (FIFO or socket) \u2014 only regular files are supported`);
40314
+ }
40315
+ const maxBytes = parseMaxStdinMB() * 1024 * 1024;
40316
+ if (st.size > maxBytes) {
40317
+ throw new CliError(`${label} '${filePath}' exceeds size limit (${Math.round(st.size / 1024 / 1024)} MB); set TOKEN_GOAT_MAX_STDIN_MB to override`);
40318
+ }
40319
+ return fs31.readFileSync(filePath, "utf8");
40320
+ } catch (e) {
40321
+ if (e instanceof CliError) throw e;
40322
+ mapFsError(e, filePath);
40323
+ }
40324
+ }
40325
+ function decodeBase64Text(payload, label) {
40326
+ const normalized = payload.replace(/\s/g, "").replace(/-/g, "+").replace(/_/g, "/");
40327
+ if (payload !== "" && normalized === "") {
40328
+ throw new CliError(`${label} payload contains only whitespace \u2014 likely a shell expansion error; pass an empty string explicitly for a zero-byte file`);
40329
+ }
40330
+ const maxBytes = parseMaxStdinMB() * 1024 * 1024;
40331
+ const decodedSize = Math.floor(normalized.replace(/=+$/, "").length * 3 / 4);
40332
+ if (decodedSize > maxBytes) {
40333
+ throw new CliError(`${label} payload would decode to ${Math.round(decodedSize / 1024 / 1024)} MB which exceeds size limit; set TOKEN_GOAT_MAX_STDIN_MB to override`);
40334
+ }
40335
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
40336
+ throw new CliError(`${label} payload contains non-base64 characters \u2014 check for shell expansion of $VAR or backticks`);
40337
+ }
40338
+ if (normalized.replace(/=+$/, "").length % 4 === 1) {
40339
+ throw new CliError(`${label} payload length is invalid (trailing single base64 character cannot decode to any bytes \u2014 payload is likely truncated)`);
40340
+ }
40341
+ return Buffer.from(normalized, "base64").toString("utf8");
40342
+ }
40236
40343
  function cmdWriteFile(dest, opts) {
40237
40344
  if (!dest || !dest.trim()) {
40238
40345
  throw new CliError("destination path cannot be empty");
@@ -40281,6 +40388,7 @@ function cmdWriteFile(dest, opts) {
40281
40388
  if (e instanceof CliError) throw e;
40282
40389
  mapFsError(e, opts.from, dest);
40283
40390
  }
40391
+ enqueueDirtyPathSafe(dest);
40284
40392
  return;
40285
40393
  }
40286
40394
  if (opts.b64 !== void 0) {
@@ -40308,6 +40416,7 @@ function cmdWriteFile(dest, opts) {
40308
40416
  } catch (e) {
40309
40417
  mapFsError(e, void 0, dest);
40310
40418
  }
40419
+ enqueueDirtyPathSafe(dest);
40311
40420
  return;
40312
40421
  }
40313
40422
  if (process.stdin.isTTY) {
@@ -40341,6 +40450,7 @@ function cmdWriteFile(dest, opts) {
40341
40450
  cleanup();
40342
40451
  try {
40343
40452
  atomicWriteBuffer(dest, Buffer.concat(chunks));
40453
+ enqueueDirtyPathSafe(dest);
40344
40454
  resolve15();
40345
40455
  } catch (e) {
40346
40456
  try {
@@ -40371,6 +40481,52 @@ function cmdWriteFile(dest, opts) {
40371
40481
  process.stdin.resume();
40372
40482
  });
40373
40483
  }
40484
+ function cmdReplace(file, opts) {
40485
+ validateWritablePath(file, "target file");
40486
+ const targetText = readTextFileBounded(file, "target file", true);
40487
+ const usingFrom = opts.oldFrom !== void 0 || opts.newFrom !== void 0;
40488
+ const usingB64 = opts.oldB64 !== void 0 || opts.newB64 !== void 0;
40489
+ if (usingFrom && usingB64) {
40490
+ throw new CliError("cannot mix --old-from/--new-from with --old-b64/--new-b64");
40491
+ }
40492
+ if (!usingFrom && !usingB64) {
40493
+ throw new CliError("must provide either --old-from/--new-from or --old-b64/--new-b64");
40494
+ }
40495
+ if (usingFrom) {
40496
+ if (opts.oldFrom === void 0 || opts.newFrom === void 0) {
40497
+ throw new CliError("must pass both --old-from and --new-from together");
40498
+ }
40499
+ } else {
40500
+ if (opts.oldB64 === void 0 || opts.newB64 === void 0) {
40501
+ throw new CliError("must pass both --old-b64 and --new-b64 together");
40502
+ }
40503
+ }
40504
+ const oldText = usingFrom ? readTextFileBounded(opts.oldFrom, "--old-from") : decodeBase64Text(opts.oldB64, "--old-b64");
40505
+ const newText = usingFrom ? readTextFileBounded(opts.newFrom, "--new-from") : decodeBase64Text(opts.newB64, "--new-b64");
40506
+ if (oldText === "") {
40507
+ throw new CliError("old string cannot be empty");
40508
+ }
40509
+ let occurrences = 0;
40510
+ let cursor = 0;
40511
+ while ((cursor = targetText.indexOf(oldText, cursor)) !== -1) {
40512
+ occurrences += 1;
40513
+ cursor += oldText.length;
40514
+ }
40515
+ if (occurrences === 0) {
40516
+ throw new CliError(`old string not found in ${file}`);
40517
+ }
40518
+ if (occurrences > 1 && !opts.all) {
40519
+ throw new CliError(`old string appears ${occurrences} times in ${file} \u2014 pass --all to replace every occurrence, or provide a more specific match`);
40520
+ }
40521
+ const replaced = occurrences === 1 && !opts.all ? targetText.replace(oldText, () => newText) : targetText.replaceAll(oldText, () => newText);
40522
+ try {
40523
+ atomicWriteBuffer(file, Buffer.from(replaced, "utf8"));
40524
+ } catch (e) {
40525
+ mapFsError(e, void 0, file);
40526
+ }
40527
+ enqueueDirtyPathSafe(file);
40528
+ out(`replaced ${occurrences} occurrence${occurrences === 1 ? "" : "s"} in ${file}`);
40529
+ }
40374
40530
  async function cmdGdriveSections(fileId, opts) {
40375
40531
  if (opts.heading !== void 0) {
40376
40532
  const content = await getSectionContent(fileId, opts.heading);
@@ -40805,6 +40961,7 @@ function buildProgram() {
40805
40961
  );
40806
40962
  program2.command("config-get <file> <key>").description("read one value from a config file (TOML/JSON/YAML/INI)").action((file, key) => runExit(() => runConfigGet({ file, key })));
40807
40963
  program2.command("write-file <dest>").description("write exact bytes to a file \u2014 handles backticks, quotes, $vars, CRLF without escaping\n\nModes: --b64 PAYLOAD (base64), --from SOURCE (copy file), or piped stdin").option("--from <source>", "copy bytes from this source file instead of stdin/base64").option("--b64 <payload>", "decode base64 payload and write to dest").action(guard(cmdWriteFile));
40964
+ program2.command("replace <file>").description("replace one string in a file; supply old/new text via --old-from/--new-from or --old-b64/--new-b64, and use --all to replace every occurrence").option("--old-from <source>", "read the old text from this source file").option("--new-from <source>", "read the new text from this source file").option("--old-b64 <payload>", "base64 payload for the old text").option("--new-b64 <payload>", "base64 payload for the new text").option("--all", "replace every occurrence instead of requiring a unique match").action(guard(cmdReplace));
40808
40965
  program2.command("gdrive-sections <file-id>").description("fetch and list sections from a public Google Doc").option("--heading <name>", "get content of one named section").action(guard(cmdGdriveSections));
40809
40966
  program2.command("compress").description("run a shell command and emit a compressed view of its output").requiredOption("-c, --cmd <command>", "the shell command to run, as one string").option("-f, --filter <name>", "filter name (auto-detected from the command when omitted)").option("--timeout <seconds>", "wall-clock timeout in seconds (0 = built-in default)").option("--no-compress", "stream output raw without compression (debug the wrapper)").option("--profile <name>", "compression profile: aggressive | balanced | minimal").option("--max-tokens <n>", "post-compress token cap (0 = no cap)").action(cmdCompress);
40810
40967
  program2.command("version").description("print the token-goat version").action(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "token-goat",
3
- "version": "2.6.0",
3
+ "version": "2.6.1",
4
4
  "description": "Surgical token-reduction companion for Claude Code and other AI coding agents",
5
5
  "type": "module",
6
6
  "main": "./dist/token-goat.mjs",