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 +1 -0
- package/dist/token-goat.mjs +394 -237
- package/package.json +1 -1
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. |
|
package/dist/token-goat.mjs
CHANGED
|
@@ -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.
|
|
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/
|
|
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
|
|
13439
|
-
import * as
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
15001
|
-
import * as
|
|
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 =
|
|
15016
|
-
const candidate =
|
|
15081
|
+
const base = path13.join(os4.homedir(), ".token-goat", "session_snapshots");
|
|
15082
|
+
const candidate = path13.join(base, safe);
|
|
15017
15083
|
try {
|
|
15018
|
-
const rel =
|
|
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
|
|
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 =
|
|
15040
|
-
if (!
|
|
15041
|
-
|
|
15105
|
+
const dir = path13.dirname(sidecarPath);
|
|
15106
|
+
if (!fs12.existsSync(dir)) {
|
|
15107
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
15042
15108
|
}
|
|
15043
|
-
|
|
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 =
|
|
15118
|
+
const files = fs12.readdirSync(d);
|
|
15053
15119
|
for (const file of files) {
|
|
15054
|
-
const fullPath =
|
|
15120
|
+
const fullPath = path13.join(d, file);
|
|
15055
15121
|
if (!file.endsWith(".bin")) continue;
|
|
15056
15122
|
try {
|
|
15057
|
-
const stat2 =
|
|
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
|
-
|
|
15135
|
+
fs12.unlinkSync(p);
|
|
15070
15136
|
removed++;
|
|
15071
15137
|
try {
|
|
15072
|
-
|
|
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 (
|
|
15167
|
+
if (fs12.existsSync(p)) {
|
|
15102
15168
|
try {
|
|
15103
|
-
const existing =
|
|
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 =
|
|
15115
|
-
if (!
|
|
15116
|
-
|
|
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
|
-
|
|
15121
|
-
|
|
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 || !
|
|
15201
|
+
if (!p || !fs12.existsSync(p)) return null;
|
|
15136
15202
|
try {
|
|
15137
|
-
const stat2 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
16708
|
+
import * as fs13 from "node:fs";
|
|
16643
16709
|
import * as os5 from "node:os";
|
|
16644
|
-
import * as
|
|
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 ?
|
|
16747
|
+
pre = (isWin32 ? path15.win32.resolve : path15.resolve)(pre);
|
|
16682
16748
|
} else {
|
|
16683
|
-
pre = (isWin32 ?
|
|
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 =
|
|
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 =
|
|
16791
|
+
const entries = fs13.readdirSync(pathStr, { withFileTypes: true });
|
|
16726
16792
|
for (const entry of entries) {
|
|
16727
16793
|
if (entry.isDirectory()) {
|
|
16728
|
-
const gitPath =
|
|
16729
|
-
if (
|
|
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 =
|
|
16809
|
+
const markerPath = path15.join(current, marker);
|
|
16744
16810
|
try {
|
|
16745
|
-
if (!
|
|
16811
|
+
if (!fs13.existsSync(markerPath)) {
|
|
16746
16812
|
return false;
|
|
16747
16813
|
}
|
|
16748
|
-
const stat2 =
|
|
16814
|
+
const stat2 = fs13.lstatSync(markerPath);
|
|
16749
16815
|
if (!stat2.isSymbolicLink()) {
|
|
16750
16816
|
return true;
|
|
16751
16817
|
}
|
|
16752
|
-
const resolved =
|
|
16753
|
-
const rel =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
16975
|
-
const gzExists = await
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
17168
|
+
const st = await fs14.stat(lockPath);
|
|
17103
17169
|
if (Date.now() - st.mtimeMs > SKILL_HIT_LOCK_STALE_MS) {
|
|
17104
|
-
await
|
|
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
|
|
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
|
|
17232
|
+
const stat2 = await fs14.stat(resolve6(dir, compactFileId));
|
|
17167
17233
|
compactLen = stat2.size;
|
|
17168
|
-
compactText = await
|
|
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
|
|
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
|
|
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 (!
|
|
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(
|
|
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
|
|
17274
|
-
import * as
|
|
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(
|
|
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 =
|
|
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 =
|
|
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 ${
|
|
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
|
|
17430
|
+
import * as fs16 from "fs";
|
|
17365
17431
|
import * as crypto3 from "crypto";
|
|
17366
|
-
import * as
|
|
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 =
|
|
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 =
|
|
17382
|
-
const stem =
|
|
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 =
|
|
17388
|
-
return
|
|
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 =
|
|
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 (!
|
|
17412
|
-
const text =
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
17503
|
+
const srcPath = path17.resolve(sourcePath);
|
|
17438
17504
|
const sha = sourceHash(srcPath);
|
|
17439
|
-
const displayRel = sourceRel ||
|
|
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 =
|
|
17444
|
-
if (!
|
|
17445
|
-
|
|
17509
|
+
const dir = path17.dirname(compactPath);
|
|
17510
|
+
if (!fs16.existsSync(dir)) {
|
|
17511
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
17446
17512
|
}
|
|
17447
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 +
|
|
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 +
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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+['"](
|
|
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
|
|
32021
|
-
|
|
32022
|
-
const
|
|
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
|
-
|
|
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,
|
|
32063
|
+
function sedRangeHint(filePath, ranges) {
|
|
32048
32064
|
const lang = detectLanguage(filePath);
|
|
32049
|
-
const
|
|
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 ' +
|
|
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 ' +
|
|
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 ' +
|
|
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 " +
|
|
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,
|
|
32561
|
+
const { filePath, ranges } = sedRange;
|
|
32545
32562
|
recordStat("session_hint", 0, 0);
|
|
32546
32563
|
const sedDedupKey = resolveIndexPath(filePath, preHookCwd ?? process.cwd());
|
|
32547
|
-
const
|
|
32548
|
-
|
|
32549
|
-
|
|
32550
|
-
|
|
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
|
-
|
|
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(
|