token-goat 2.9.3 → 2.9.5
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 +8 -4
- package/dist/{token-goat-chunk-XSSQFI4C.mjs → token-goat-chunk-2FVSKFZU.mjs} +5 -5
- package/dist/{token-goat-chunk-7A7SBE6R.mjs → token-goat-chunk-4P6GTCMM.mjs} +220 -23
- package/dist/{token-goat-chunk-6FQMEMWX.mjs → token-goat-chunk-5T2K7DEE.mjs} +108 -46
- package/dist/{token-goat-chunk-3UBORD6Z.mjs → token-goat-chunk-DRL5USXI.mjs} +3 -3
- package/dist/{token-goat-chunk-HC5NMGPD.mjs → token-goat-chunk-GFBXHGFY.mjs} +5 -5
- package/dist/{token-goat-chunk-OKJX2JWW.mjs → token-goat-chunk-HTJP6FHK.mjs} +2 -1
- package/dist/{token-goat-chunk-A4VBNWSL.mjs → token-goat-chunk-JO5JX72D.mjs} +2 -2
- package/dist/{token-goat-chunk-LLNA42NI.mjs → token-goat-chunk-KFFAZ5DJ.mjs} +2 -2
- package/dist/{token-goat-chunk-3XVGPLDS.mjs → token-goat-chunk-U4FTM2SB.mjs} +9765 -9147
- package/dist/{token-goat-chunk-2VVXTAGD.mjs → token-goat-chunk-ZLP6TCGN.mjs} +215 -27
- package/dist/{token-goat-chunk-KKHKXUVS.mjs → token-goat-chunk-ZOKNDG6V.mjs} +229 -97
- package/dist/token-goat-hook.mjs +5 -5
- package/dist/token-goat.core.mjs +5 -5
- package/docs/cli.md +15 -8
- package/docs/security.md +6 -2
- package/package.json +1 -1
|
@@ -12,7 +12,7 @@ init_define_import_meta_env();
|
|
|
12
12
|
import { createRequire } from "node:module";
|
|
13
13
|
function resolveVersion() {
|
|
14
14
|
if (true) {
|
|
15
|
-
return "2.9.
|
|
15
|
+
return "2.9.5";
|
|
16
16
|
}
|
|
17
17
|
const require2 = createRequire(import.meta.url);
|
|
18
18
|
const pkg = require2("../package.json");
|
|
@@ -2273,6 +2273,16 @@ var CONFIG_DEFAULTS = {
|
|
|
2273
2273
|
// existing users -- see the reread_deny/reread_deny_min_bytes fix's commit message.
|
|
2274
2274
|
reread_deny_min_bytes: 51200,
|
|
2275
2275
|
stable_doc_compacts: true,
|
|
2276
|
+
// On. It has now run. The gate that finds the spans answered only from the index, and the index carried the shipping parser stamp on 46 of 17,952 files, so the lever was very nearly dead in practice: a disk parse of the delivered file is now the fallback, worth +3.0 points of withheld bytes on shell reads with no index at all. The cost side is the one this comment used to call unobservable, and it is observable: joining folds to later reads of the folded symbol scores 62.3% recovery, but the same window measured backwards scores 55.8% and a shuffled pairing scores 24.3%, so the excess attributable to the fold is 6.6 points rather than 62. Unlike every re-read mechanism beside it this rewrites a FIRST look, where the reader has no prior copy to notice an omission against, which is why the notice names the symbol and the command that returns it verbatim.
|
|
2277
|
+
fold_code_bodies: true,
|
|
2278
|
+
// On, and separate from the body fold above because the two do not carry the same risk. A body fold needs symbol spans from the index, so it can cut at the wrong line when the index is stale, and it hides the implementation an agent came to read. A comment fold reads the block boundaries off the delivered text itself, so it cannot be stale and works on the first read of a file the indexer has never seen, which is exactly the surface nothing else here reaches. It keeps the opening two lines of a block of 12 or more, so the summary sentence a reader navigates by survives and only the elaboration is replaced, and it alters the text of no line it keeps, its notice naming the absolute range removed so the recall is exact. Measured across this project's own 259 source files it removes 8.07% of the delivered bytes over 420 folds in 170 files. The nearest published measurement is stronger and cruder: removing docstrings outright cost 3 points of resolution rate on SWE-bench Verified for 22% of the tokens (arXiv:2606.01326), and keeping the opening summary is the gentler trade on that curve.
|
|
2279
|
+
fold_comment_blocks: true,
|
|
2280
|
+
// On. It has now run: measured over 2,032 real document reads it removes 43.4% of the pool and 57.4% of the reads it touches, keeping every heading, table, block quote and fenced line, and replacing only the tail of a paragraph whose opening sentence is already a complete one. The recall it needs is a ranged Read of the single line named in the notice, which costs one call and is printed at the point of the cut rather than left for the reader to work out. The project-config lock below stays regardless of this default: a repository still cannot set this key, so the choice to fold is the reader's environment and never the code being read.
|
|
2281
|
+
fold_prose_paragraphs: true,
|
|
2282
|
+
// On. Fires on an untargeted (no offset/limit) Read of a markdown document at least 8,000 bytes with at least 6 headings, replacing the delivered body with a heading tree plus the document preamble when that replacement is meaningfully smaller. Built from the delivered text itself, never the index, so it works on a document the indexer has never seen. Measured over 5,104 real session transcripts (13,870 Read deliveries, 130,249,204 bytes): untargeted markdown reads with >=6 headings at this 8,000-byte floor withhold 41.03% of all Read bytes, within 1.8 points of the best floor tried (2,000 B) while firing far less often on small documents where the interruption is least worth it.
|
|
2283
|
+
outline_large_documents: true,
|
|
2284
|
+
// On. The source-code sibling of outline_large_documents directly above, and gated the same way: an untargeted (no offset/limit) Read of a tree-sitter language at least 12,000 bytes with at least 8 symbols is replaced by its structural skeleton, the preamble plus one declaration line per symbol, with each withheld run named and pointed at the command that returns it. Symbols come from tree-sitter over the delivered text, never the index and never the regex extractors, so a partial symbol list turns the fold off rather than shipping a skeleton missing declarations nothing signals. Measured over 5,104 real session transcripts (130,325,670 delivered Read bytes): 558 reads clear this floor, and the fold withholds 11,241,796 B, 8.63% of all Read bytes.
|
|
2285
|
+
skeleton_large_sources: true,
|
|
2276
2286
|
truncated_read_min_lines: 200,
|
|
2277
2287
|
protect_recent_reads: 4,
|
|
2278
2288
|
warn_unbalanced_shell_quoting: true,
|
|
@@ -2607,6 +2617,16 @@ var PROJECT_LOCKED_SECTIONS = [
|
|
|
2607
2617
|
"screenshot"
|
|
2608
2618
|
];
|
|
2609
2619
|
var PROJECT_LOCKED_KEYS = [
|
|
2620
|
+
// A repository must not be able to decide how much of its own source an agent gets to see. Turning this on folds function bodies out of every Read of this project's files, so a checked-in `.token-goat.toml` setting it true would shrink what a reviewing agent is shown of the very code it came to review -- and the fold is silent about intent, so it reads as normal output. The user's own global config and TOKEN_GOAT_FOLD_CODE_BODIES still set it freely; only the project-supplied layer is refused.
|
|
2621
|
+
"hints.fold_code_bodies",
|
|
2622
|
+
// Same reasoning as the body fold above, on the comments rather than the code: a checked-in project file must not be able to fold a repository's own explanatory comments out of what a reviewing agent is shown, which is precisely where an intent that disagrees with the code would be written down. The user's global config and TOKEN_GOAT_FOLD_COMMENT_BLOCKS still set it freely.
|
|
2623
|
+
"hints.fold_comment_blocks",
|
|
2624
|
+
// Same reasoning one document over: a repository must not be able to fold its own README or changelog out of what a reviewing agent is shown. The user's global config and TOKEN_GOAT_FOLD_PROSE_PARAGRAPHS still set it freely.
|
|
2625
|
+
"hints.fold_prose_paragraphs",
|
|
2626
|
+
// Same reasoning again: a repository must not be able to hide its own documentation's structure from a reviewing agent by disabling the heading-tree replacement, nor -- more to the point here -- by leaving it on to shrink what a reviewing agent sees of a doc the repo itself ships. The user's global config and TOKEN_GOAT_OUTLINE_LARGE_DOCUMENTS still set it freely.
|
|
2627
|
+
"hints.outline_large_documents",
|
|
2628
|
+
// Same reasoning one file type over: a repository must not be able to decide, from its own checked-in config, how much of its source a reviewing agent is shown -- neither by turning the skeleton off to bury a declaration in a wall of bodies, nor by leaving it on to withhold the bodies themselves. The user's global config and TOKEN_GOAT_SKELETON_LARGE_SOURCES still set it freely.
|
|
2629
|
+
"hints.skeleton_large_sources",
|
|
2610
2630
|
"image_shrink.max_image_pixels",
|
|
2611
2631
|
"indexing.cross_project_symbols",
|
|
2612
2632
|
"worker.blocked_roots"
|
|
@@ -3019,6 +3039,11 @@ function _buildConfig(raw, projectRaw = {}) {
|
|
|
3019
3039
|
hi.reread_deny = validatedBool(hi_raw["reread_deny"], hi.reread_deny);
|
|
3020
3040
|
hi.reread_deny_min_bytes = validatedIntWithLegacySentinel(hi_raw["reread_deny_min_bytes"], hi.reread_deny_min_bytes, 2048, ...boundsOf("hints.reread_deny_min_bytes"));
|
|
3021
3041
|
hi.stable_doc_compacts = validatedBool(hi_raw["stable_doc_compacts"], hi.stable_doc_compacts);
|
|
3042
|
+
hi.fold_code_bodies = validatedBool(hi_raw["fold_code_bodies"], hi.fold_code_bodies);
|
|
3043
|
+
hi.fold_comment_blocks = validatedBool(hi_raw["fold_comment_blocks"], hi.fold_comment_blocks);
|
|
3044
|
+
hi.fold_prose_paragraphs = validatedBool(hi_raw["fold_prose_paragraphs"], hi.fold_prose_paragraphs);
|
|
3045
|
+
hi.outline_large_documents = validatedBool(hi_raw["outline_large_documents"], hi.outline_large_documents);
|
|
3046
|
+
hi.skeleton_large_sources = validatedBool(hi_raw["skeleton_large_sources"], hi.skeleton_large_sources);
|
|
3022
3047
|
hi.truncated_read_min_lines = validatedInt(hi_raw["truncated_read_min_lines"], hi.truncated_read_min_lines, ...boundsOf("hints.truncated_read_min_lines"));
|
|
3023
3048
|
hi.protect_recent_reads = validatedInt(hi_raw["protect_recent_reads"], hi.protect_recent_reads, ...boundsOf("hints.protect_recent_reads"));
|
|
3024
3049
|
hi.warn_unbalanced_shell_quoting = validatedBool(hi_raw["warn_unbalanced_shell_quoting"], hi.warn_unbalanced_shell_quoting);
|
|
@@ -3047,6 +3072,11 @@ function _buildConfig(raw, projectRaw = {}) {
|
|
|
3047
3072
|
hi.min_file_lines_for_hint = envInt("TOKEN_GOAT_MIN_FILE_LINES_FOR_HINT", hi.min_file_lines_for_hint, ...boundsOf("hints.min_file_lines_for_hint"));
|
|
3048
3073
|
hi.git_hint_max_ms = envInt("TOKEN_GOAT_GIT_HINT_MAX_MS", hi.git_hint_max_ms, ...boundsOf("hints.git_hint_max_ms"));
|
|
3049
3074
|
hi.stable_doc_compacts = envBool("TOKEN_GOAT_STABLE_DOC_COMPACTS", hi.stable_doc_compacts);
|
|
3075
|
+
hi.fold_code_bodies = envBool("TOKEN_GOAT_FOLD_CODE_BODIES", hi.fold_code_bodies);
|
|
3076
|
+
hi.fold_comment_blocks = envBool("TOKEN_GOAT_FOLD_COMMENT_BLOCKS", hi.fold_comment_blocks);
|
|
3077
|
+
hi.fold_prose_paragraphs = envBool("TOKEN_GOAT_FOLD_PROSE_PARAGRAPHS", hi.fold_prose_paragraphs);
|
|
3078
|
+
hi.outline_large_documents = envBool("TOKEN_GOAT_OUTLINE_LARGE_DOCUMENTS", hi.outline_large_documents);
|
|
3079
|
+
hi.skeleton_large_sources = envBool("TOKEN_GOAT_SKELETON_LARGE_SOURCES", hi.skeleton_large_sources);
|
|
3050
3080
|
hi.context_threshold_advisory = envBool("TOKEN_GOAT_CONTEXT_THRESHOLD_ADVISORY", hi.context_threshold_advisory);
|
|
3051
3081
|
hi.pre_skill_advisory = envBool("TOKEN_GOAT_PRE_SKILL_ADVISORY", hi.pre_skill_advisory);
|
|
3052
3082
|
hi.quiet_hours = envStr("TOKEN_GOAT_QUIET_HOURS", hi.quiet_hours);
|
|
@@ -3214,6 +3244,11 @@ var CONFIG_KEY_ENV_OVERRIDES = {
|
|
|
3214
3244
|
"hints.min_file_lines_for_hint": ["TOKEN_GOAT_MIN_FILE_LINES_FOR_HINT"],
|
|
3215
3245
|
"hints.git_hint_max_ms": ["TOKEN_GOAT_GIT_HINT_MAX_MS"],
|
|
3216
3246
|
"hints.stable_doc_compacts": ["TOKEN_GOAT_STABLE_DOC_COMPACTS"],
|
|
3247
|
+
"hints.fold_code_bodies": ["TOKEN_GOAT_FOLD_CODE_BODIES"],
|
|
3248
|
+
"hints.fold_comment_blocks": ["TOKEN_GOAT_FOLD_COMMENT_BLOCKS"],
|
|
3249
|
+
"hints.fold_prose_paragraphs": ["TOKEN_GOAT_FOLD_PROSE_PARAGRAPHS"],
|
|
3250
|
+
"hints.outline_large_documents": ["TOKEN_GOAT_OUTLINE_LARGE_DOCUMENTS"],
|
|
3251
|
+
"hints.skeleton_large_sources": ["TOKEN_GOAT_SKELETON_LARGE_SOURCES"],
|
|
3217
3252
|
"hints.context_threshold_advisory": ["TOKEN_GOAT_CONTEXT_THRESHOLD_ADVISORY"],
|
|
3218
3253
|
"hints.pre_skill_advisory": ["TOKEN_GOAT_PRE_SKILL_ADVISORY"],
|
|
3219
3254
|
"hints.quiet_hours": ["TOKEN_GOAT_QUIET_HOURS"],
|
|
@@ -3352,6 +3387,11 @@ function saveConfig(config) {
|
|
|
3352
3387
|
reread_deny: config.hints.reread_deny,
|
|
3353
3388
|
reread_deny_min_bytes: config.hints.reread_deny_min_bytes,
|
|
3354
3389
|
stable_doc_compacts: config.hints.stable_doc_compacts,
|
|
3390
|
+
fold_code_bodies: config.hints.fold_code_bodies,
|
|
3391
|
+
fold_comment_blocks: config.hints.fold_comment_blocks,
|
|
3392
|
+
fold_prose_paragraphs: config.hints.fold_prose_paragraphs,
|
|
3393
|
+
outline_large_documents: config.hints.outline_large_documents,
|
|
3394
|
+
skeleton_large_sources: config.hints.skeleton_large_sources,
|
|
3355
3395
|
truncated_read_min_lines: config.hints.truncated_read_min_lines,
|
|
3356
3396
|
protect_recent_reads: config.hints.protect_recent_reads,
|
|
3357
3397
|
prompt_triggers: config.hints.prompt_triggers,
|
|
@@ -4285,7 +4325,7 @@ function makeLineSymbol(filePath, name, kind, line, sig, parent, lines, style) {
|
|
|
4285
4325
|
parent: parent ?? ""
|
|
4286
4326
|
};
|
|
4287
4327
|
}
|
|
4288
|
-
function makeSymbolEmitter(symbols, sections, seen, filePath, maxSymbols =
|
|
4328
|
+
function makeSymbolEmitter(symbols, sections, seen, filePath, maxSymbols = 1e4, maxHeadingLen = 120) {
|
|
4289
4329
|
return function emit(name, kind, line) {
|
|
4290
4330
|
if (!name || name.length > maxHeadingLen) return;
|
|
4291
4331
|
if (symbols.length >= maxSymbols) return;
|
|
@@ -5988,7 +6028,8 @@ var _KIND_GROUPS = [
|
|
|
5988
6028
|
members: /* @__PURE__ */ new Set([
|
|
5989
6029
|
"skill_load",
|
|
5990
6030
|
"skill_oversized_first_load",
|
|
5991
|
-
"skill_compact_inlined"
|
|
6031
|
+
"skill_compact_inlined",
|
|
6032
|
+
"skill_heading_tree_inlined"
|
|
5992
6033
|
])
|
|
5993
6034
|
},
|
|
5994
6035
|
// SOURCE_CONTENT: real rewrites of tool output that remove real bytes (agent report compaction, Grep fold, browser tab dedup, bash/content compression and the handoff pair). The by-source table has shown a 'content' row since the source was added, but the by-kind table had no member set for it, so every one of these kinds printed under 'Other'. The taskoutput: prefix branch in _kindGroupLabel routes here too.
|
|
@@ -6002,6 +6043,9 @@ var _KIND_GROUPS = [
|
|
|
6002
6043
|
"browser_tab_dedup",
|
|
6003
6044
|
"grep:fold",
|
|
6004
6045
|
"read:served_elide",
|
|
6046
|
+
"read:body_fold",
|
|
6047
|
+
"read:markdown_outline",
|
|
6048
|
+
"read:source_skeleton",
|
|
6005
6049
|
"handoff_create",
|
|
6006
6050
|
"handoff_resolve",
|
|
6007
6051
|
"plan_echo_collapse"
|
|
@@ -6317,6 +6361,10 @@ function renderStats(stats, opts) {
|
|
|
6317
6361
|
|
|
6318
6362
|
// src/stats.ts
|
|
6319
6363
|
var HARNESS_UNRECORDED = "unrecorded (pre-2.8.1)";
|
|
6364
|
+
var PRICING_VERSION_UNRECORDED = "unrecorded (pre-tg_version column)";
|
|
6365
|
+
function hasMixedPricingEras(summary) {
|
|
6366
|
+
return Object.keys(summary.by_pricing_version).length > 1;
|
|
6367
|
+
}
|
|
6320
6368
|
var SOURCE_IMAGE = "image";
|
|
6321
6369
|
var SOURCE_HINT = "hint";
|
|
6322
6370
|
var SOURCE_READ = "read";
|
|
@@ -6422,6 +6470,8 @@ var KIND_TO_SOURCE = {
|
|
|
6422
6470
|
skill_oversized_first_load: SOURCE_SKILL,
|
|
6423
6471
|
// Cold first load of an oversized skill where preSkillHandler inlined the compact slice in its reply instead of pointing at `skill-body --compact`. Unlike its skill_oversized_first_load sibling (event-only, 0 bytes -- the pointer deny saves nothing by itself, the follow-up command does) this one records real savings: the full body never landed, the slice did, so bytesSaved is body minus slice.
|
|
6424
6472
|
skill_compact_inlined: SOURCE_SKILL,
|
|
6473
|
+
// Cold first load of an oversized skill with no compact marker at all, where preSkillHandler inlined a heading tree in its reply instead of letting the whole body fall through. Same shape as skill_compact_inlined: real savings, bytesSaved is body minus the rendered tree.
|
|
6474
|
+
skill_heading_tree_inlined: SOURCE_SKILL,
|
|
6425
6475
|
secret_redacted: SOURCE_OTHER,
|
|
6426
6476
|
// Fail-soft diagnostic counters from hooks_edit.ts: they record that a side task threw, never a byte saving, so "other" is the right home. Listed explicitly rather than left to kindToSource()'s fallback so the registration guard can tell a deliberate placement from an unregistered kind.
|
|
6427
6477
|
dirty_queue_append_failed: SOURCE_OTHER,
|
|
@@ -6447,6 +6497,12 @@ var KIND_TO_SOURCE = {
|
|
|
6447
6497
|
// with real bytes removed from it. Filing it under the advisory bucket would add non-hint
|
|
6448
6498
|
// savings to hint_stats.ts's savedBytes, which reads by_source[SOURCE_HINT] wholesale.
|
|
6449
6499
|
"read:served_elide": SOURCE_CONTENT,
|
|
6500
|
+
// Same bucket and same reasoning as read:served_elide directly above: a rewrite of a Read that did happen, with real bytes removed, not an advisory about whether to read at all.
|
|
6501
|
+
"read:body_fold": SOURCE_CONTENT,
|
|
6502
|
+
// Same bucket and same reasoning as read:body_fold directly above: a coarser sibling rewrite of a large untargeted markdown Read (hooks_read.ts foldMarkdownOutline) that replaces the body with a heading tree plus preamble, with real bytes removed, not an advisory about whether to read at all.
|
|
6503
|
+
"read:markdown_outline": SOURCE_CONTENT,
|
|
6504
|
+
// Same bucket and same reasoning as read:markdown_outline directly above, on source instead of prose: the structural-skeleton replacement of a large untargeted source Read (hooks_read.ts foldSourceSkeleton), with real bytes removed, not an advisory about whether to read at all.
|
|
6505
|
+
"read:source_skeleton": SOURCE_CONTENT,
|
|
6450
6506
|
content_retrieve: SOURCE_CONTENT,
|
|
6451
6507
|
handoff_create: SOURCE_CONTENT,
|
|
6452
6508
|
handoff_resolve: SOURCE_CONTENT
|
|
@@ -6730,12 +6786,21 @@ function summarize(windowDays = 30, testDb, homeDir) {
|
|
|
6730
6786
|
const byKind = {};
|
|
6731
6787
|
const byDay = {};
|
|
6732
6788
|
const byHarness = {};
|
|
6789
|
+
const byPricingVersion = {};
|
|
6733
6790
|
let totalEvents = 0;
|
|
6734
6791
|
let totalBytes = 0;
|
|
6735
6792
|
let totalTokens = 0;
|
|
6736
6793
|
const db = testDb ?? getGlobalDb(homeDir);
|
|
6737
6794
|
const hasHarness = statsHasHarnessColumn(db);
|
|
6738
|
-
const
|
|
6795
|
+
const hasVersion = statsHasVersionColumn(db);
|
|
6796
|
+
const cols = [
|
|
6797
|
+
"ts",
|
|
6798
|
+
"kind",
|
|
6799
|
+
"bytes_saved",
|
|
6800
|
+
"tokens_saved",
|
|
6801
|
+
...hasHarness ? ["harness"] : [],
|
|
6802
|
+
...hasVersion ? ["tg_version"] : []
|
|
6803
|
+
].join(", ");
|
|
6739
6804
|
const query = sinceTs !== null ? `SELECT ${cols} FROM stats WHERE ts >= ? ORDER BY ts DESC` : `SELECT ${cols} FROM stats ORDER BY ts DESC`;
|
|
6740
6805
|
const stmt = db.prepare(query);
|
|
6741
6806
|
const rows = sinceTs !== null ? stmt.all(sinceTs) : stmt.all();
|
|
@@ -6769,6 +6834,11 @@ function summarize(windowDays = 30, testDb, homeDir) {
|
|
|
6769
6834
|
byHarness[harness] = zeroBucket();
|
|
6770
6835
|
}
|
|
6771
6836
|
incBucket(byHarness[harness], bytesSaved, tokensSaved);
|
|
6837
|
+
const pricingVersion = row.tg_version || PRICING_VERSION_UNRECORDED;
|
|
6838
|
+
if (!byPricingVersion[pricingVersion]) {
|
|
6839
|
+
byPricingVersion[pricingVersion] = zeroBucket();
|
|
6840
|
+
}
|
|
6841
|
+
incBucket(byPricingVersion[pricingVersion], bytesSaved, tokensSaved);
|
|
6772
6842
|
}
|
|
6773
6843
|
const bySourceDict = {};
|
|
6774
6844
|
for (const [kind, bucket] of Object.entries(byKind)) {
|
|
@@ -6806,6 +6876,7 @@ function summarize(windowDays = 30, testDb, homeDir) {
|
|
|
6806
6876
|
by_project: byProjectList,
|
|
6807
6877
|
by_source: bySourceDict,
|
|
6808
6878
|
by_harness: byHarness,
|
|
6879
|
+
by_pricing_version: byPricingVersion,
|
|
6809
6880
|
counts,
|
|
6810
6881
|
by_command: Object.entries(byCommandDict).map(([command, bucket]) => ({ ...bucket, command })).filter((r) => r.events > 0),
|
|
6811
6882
|
window_days: windowDays
|
|
@@ -6817,6 +6888,15 @@ function _totalsLines(summary) {
|
|
|
6817
6888
|
`Total events: ${summary.total_events}`,
|
|
6818
6889
|
`Bytes saved: ${fmtBytes(summary.total_bytes_saved)}`,
|
|
6819
6890
|
`Tokens saved: ${summary.total_tokens_saved}`,
|
|
6891
|
+
// Disclosure, not a correction: `total_tokens_saved` above sums rows written under whichever
|
|
6892
|
+
// pricing formula was live when each was recorded, and `tg_version` cannot be read back into
|
|
6893
|
+
// "which formula" for a row from before this disclosure existed (PRICING_VERSION_UNRECORDED is
|
|
6894
|
+
// the overwhelming majority of all-time rows). Excluding those rows from the headline would
|
|
6895
|
+
// discard nearly the whole figure rather than fix it, so the honest move is to keep the sum and
|
|
6896
|
+
// say plainly that it spans more than one era, not to quietly present a mixed total as single-formula.
|
|
6897
|
+
...hasMixedPricingEras(summary) ? [
|
|
6898
|
+
`Pricing note: totals mix ${countNoun(Object.keys(summary.by_pricing_version).length, "tg_version era")} (${countNoun(summary.by_pricing_version[PRICING_VERSION_UNRECORDED]?.events ?? 0, "row")} unrecorded); see 'token-goat stats --json' -> by_pricing_version for the breakdown`
|
|
6899
|
+
] : [],
|
|
6820
6900
|
// Printed on its own line, below the token total and never inside it, because it counts
|
|
6821
6901
|
// placeholders rather than tokens. Omitted entirely when nothing was redacted, so the line is
|
|
6822
6902
|
// information rather than a permanent zero. See COUNT_ONLY_KINDS.
|
|
@@ -7257,7 +7337,7 @@ function emitRewrite(updatedOutput, detail, savings, redaction = "count-here") {
|
|
|
7257
7337
|
}
|
|
7258
7338
|
if (savings !== void 0) {
|
|
7259
7339
|
const bytesSaved = savings.originalBytes - Buffer.byteLength(updatedOutput, "utf-8");
|
|
7260
|
-
if (bytesSaved > 0) recordStat(savings.kind, bytesSaved, savedTokensFromBytes(bytesSaved));
|
|
7340
|
+
if (bytesSaved > 0) recordStat(savings.kind, bytesSaved, savedTokensFromBytes(bytesSaved), void 0, savings.detail);
|
|
7261
7341
|
}
|
|
7262
7342
|
return { hookType: "rewriteOutput", updatedOutput };
|
|
7263
7343
|
}
|
|
@@ -9758,11 +9838,46 @@ var DEFAULT_MAX_BYTES = 64 * 1024;
|
|
|
9758
9838
|
var MAX_INSPECT_BYTES = 2 * 1024 * 1024;
|
|
9759
9839
|
var DEFAULT_MAX_INPUT_BYTES = 500 * 1024;
|
|
9760
9840
|
var FALLBACK_MAX_LINE_CHARS = 400;
|
|
9841
|
+
var LONG_LINE_MAX_CHARS = 1e3;
|
|
9842
|
+
var ELIDED_MARKER_RE = /… \[\d+ chars elided\]/;
|
|
9761
9843
|
function getMaxInputBytes() {
|
|
9762
9844
|
const raw = process.env["TOKEN_GOAT_FILTER_MAX_BYTES"];
|
|
9763
9845
|
const v = raw ? Number.parseInt(raw, 10) : 0;
|
|
9764
9846
|
return Number.isFinite(v) && v > 0 ? v : DEFAULT_MAX_INPUT_BYTES;
|
|
9765
9847
|
}
|
|
9848
|
+
function utf8SafeEnd(buf, n) {
|
|
9849
|
+
if (n >= buf.length) return buf.length;
|
|
9850
|
+
let end = n;
|
|
9851
|
+
while (end > 0 && (buf[end] & 192) === 128) end--;
|
|
9852
|
+
return end;
|
|
9853
|
+
}
|
|
9854
|
+
function clampKeepingEnds(text, maxBytes) {
|
|
9855
|
+
const buf = Buffer.from(text, "utf8");
|
|
9856
|
+
if (buf.length <= maxBytes) return null;
|
|
9857
|
+
const lines = text.split("\n");
|
|
9858
|
+
const budget = maxBytes - Buffer.byteLength(`... [${lines.length} more lines elided by token-goat]
|
|
9859
|
+
`, "utf8");
|
|
9860
|
+
const half = Math.floor(budget / 2);
|
|
9861
|
+
let headEnd = 0;
|
|
9862
|
+
for (let used = 0; headEnd < lines.length; headEnd++) {
|
|
9863
|
+
const n = Buffer.byteLength(lines[headEnd], "utf8") + 1;
|
|
9864
|
+
if (used + n > half) break;
|
|
9865
|
+
used += n;
|
|
9866
|
+
}
|
|
9867
|
+
let tailStart = lines.length;
|
|
9868
|
+
for (let used = 0; tailStart > headEnd; tailStart--) {
|
|
9869
|
+
const n = Buffer.byteLength(lines[tailStart - 1], "utf8") + 1;
|
|
9870
|
+
if (used + n > half) break;
|
|
9871
|
+
used += n;
|
|
9872
|
+
}
|
|
9873
|
+
if (headEnd === 0 && tailStart === lines.length) return buf.subarray(0, utf8SafeEnd(buf, maxBytes)).toString("utf8");
|
|
9874
|
+
const elided = tailStart - headEnd;
|
|
9875
|
+
return [
|
|
9876
|
+
...lines.slice(0, headEnd),
|
|
9877
|
+
`... [${elided} more line${elided === 1 ? "" : "s"} elided by token-goat]`,
|
|
9878
|
+
...lines.slice(tailStart)
|
|
9879
|
+
].join("\n");
|
|
9880
|
+
}
|
|
9766
9881
|
function compressionMarker(filter, pct) {
|
|
9767
9882
|
return `
|
|
9768
9883
|
[token-goat: ${filter} filter -${Math.round(pct)}%; disable via TOKEN_GOAT_BASH_COMPRESS]`;
|
|
@@ -9774,6 +9889,7 @@ ${stderr.replace(/\s+$/, "")}`;
|
|
|
9774
9889
|
return stdout.trim() ? stdout.replace(/\s+$/, "") : stderr.replace(/\s+$/, "");
|
|
9775
9890
|
}
|
|
9776
9891
|
var ERROR_SIGNAL_RE = /error:|Error:|ERROR|FAILED|failed|fatal:|Traceback|exception:|Exception:|AssertionError|assert |panic:/i;
|
|
9892
|
+
var TABLE_ROW_ANOMALY_RE = /\b(?:CrashLoopBackOff|ImagePullBackOff|ErrImagePull|CreateContainerError|CreateContainerConfigError|InvalidImageName|RunContainerError|OOMKilled|Evicted|Terminating|ContainerCreating|PodInitializing|NotReady|SchedulingDisabled|Unschedulable|Pending|Failed|Error|Unknown|Unhealthy|DEGRADED|UNAVAILABLE|STOPPED|STOPPING|TERMINATED|TERMINATING|FAILED|ROLLBACK_COMPLETE|ROLLBACK_FAILED|CREATE_FAILED|UPDATE_FAILED|DELETE_FAILED)\b/;
|
|
9777
9893
|
var TIMESTAMP_PREFIX_RE = /^\[?\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?Z?\]?\s*|^\d{2}:\d{2}:\d{2}(?:\.\d+)?\s+/;
|
|
9778
9894
|
var REDIRECT_TOKEN_RE = /^(\d*)(>>?|<<?).*$|^&>$|^>&.*$/;
|
|
9779
9895
|
function maskQuotedSpans(cmd) {
|
|
@@ -9936,18 +10052,28 @@ function truncateMiddleSmart(lines, maxLines, opts = {}) {
|
|
|
9936
10052
|
const total = lines.length;
|
|
9937
10053
|
const effHead = Math.min(headKeep, Math.floor(total / 4));
|
|
9938
10054
|
const effTail = Math.min(tailKeep, Math.floor(total / 4));
|
|
10055
|
+
const chosenErrors = errorIndices.length <= maxErrorLines ? errorIndices : [
|
|
10056
|
+
...errorIndices.slice(0, Math.ceil(maxErrorLines / 2)),
|
|
10057
|
+
...errorIndices.slice(errorIndices.length - Math.floor(maxErrorLines / 2))
|
|
10058
|
+
];
|
|
10059
|
+
const budgetForMiddle = Math.max(0, maxLines - effHead - effTail);
|
|
10060
|
+
const half = Math.ceil(chosenErrors.length / 2);
|
|
10061
|
+
const front = chosenErrors.slice(0, half);
|
|
10062
|
+
const back = chosenErrors.slice(half).reverse();
|
|
10063
|
+
const visitOrder = [];
|
|
10064
|
+
for (let k = 0; k < Math.max(front.length, back.length); k++) {
|
|
10065
|
+
if (k < front.length) visitOrder.push(front[k]);
|
|
10066
|
+
if (k < back.length) visitOrder.push(back[k]);
|
|
10067
|
+
}
|
|
9939
10068
|
const middle = /* @__PURE__ */ new Set();
|
|
9940
|
-
for (
|
|
9941
|
-
const ei = errorIndices[k];
|
|
10069
|
+
outer: for (const ei of visitOrder) {
|
|
9942
10070
|
for (let ci = Math.max(0, ei - errorContext); ci < Math.min(total, ei + errorContext + 1); ci++) {
|
|
10071
|
+
if (ci < effHead || ci >= total - effTail) continue;
|
|
10072
|
+
if (middle.size >= budgetForMiddle) break outer;
|
|
9943
10073
|
middle.add(ci);
|
|
9944
10074
|
}
|
|
9945
10075
|
}
|
|
9946
|
-
|
|
9947
|
-
for (let i = total - effTail; i < total; i++) middle.delete(i);
|
|
9948
|
-
const budgetForMiddle = Math.max(0, maxLines - effHead - effTail);
|
|
9949
|
-
let sortedMiddle = Array.from(middle).sort((a, b) => a - b);
|
|
9950
|
-
if (sortedMiddle.length > budgetForMiddle) sortedMiddle = sortedMiddle.slice(0, budgetForMiddle);
|
|
10076
|
+
const sortedMiddle = Array.from(middle).sort((a, b) => a - b);
|
|
9951
10077
|
const result = [];
|
|
9952
10078
|
const appendSection = (indices) => {
|
|
9953
10079
|
for (let pos = 0; pos < indices.length; pos++) {
|
|
@@ -9978,23 +10104,19 @@ function truncateMiddleSmart(lines, maxLines, opts = {}) {
|
|
|
9978
10104
|
function capBytes(text, maxBytes) {
|
|
9979
10105
|
const encoded = Buffer.from(text, "utf8");
|
|
9980
10106
|
if (encoded.length <= maxBytes) return text;
|
|
9981
|
-
const
|
|
9982
|
-
... [${encoded.length
|
|
9983
|
-
const budget = maxBytes - Buffer.byteLength(
|
|
9984
|
-
if (budget <= 0) return
|
|
9985
|
-
|
|
9986
|
-
|
|
9987
|
-
|
|
9988
|
-
while (slice.length > 0 && (encoded[slice.length] & 192) === 128) {
|
|
9989
|
-
slice = slice.subarray(0, slice.length - 1);
|
|
9990
|
-
}
|
|
9991
|
-
return slice.toString("utf8") + marker;
|
|
10107
|
+
const widestMarker = `
|
|
10108
|
+
... [${encoded.length} bytes elided by token-goat]`;
|
|
10109
|
+
const budget = maxBytes - Buffer.byteLength(widestMarker, "utf8");
|
|
10110
|
+
if (budget <= 0) return widestMarker.trim();
|
|
10111
|
+
const kept = clampKeepingEnds(text, budget) ?? text;
|
|
10112
|
+
return `${kept}
|
|
10113
|
+
... [${encoded.length - Buffer.byteLength(kept, "utf8")} bytes elided by token-goat]`;
|
|
9992
10114
|
}
|
|
9993
10115
|
function capTokens(text, maxTokens) {
|
|
9994
10116
|
const clean = stripAnsiCodes(text);
|
|
9995
10117
|
if (clean.length / 3.5 <= maxTokens) return text;
|
|
9996
10118
|
const maxBytes = Math.floor(maxTokens * 3.5);
|
|
9997
|
-
let truncated =
|
|
10119
|
+
let truncated = clampKeepingEnds(clean, maxBytes) ?? clean;
|
|
9998
10120
|
if (!truncated.includes("[token-goat: output capped at")) {
|
|
9999
10121
|
truncated = truncated.replace(BYTES_ELIDED_MARKER_RE, "");
|
|
10000
10122
|
truncated += `
|
|
@@ -10014,9 +10136,19 @@ function truncateTableRows(text, maxRows, hint) {
|
|
|
10014
10136
|
const lines = text.split("\n");
|
|
10015
10137
|
const nonEmpty = lines.filter((l) => l.trim());
|
|
10016
10138
|
if (nonEmpty.length <= maxRows + 1) return text;
|
|
10017
|
-
const
|
|
10018
|
-
|
|
10019
|
-
|
|
10139
|
+
const header = nonEmpty[0];
|
|
10140
|
+
const rows = nonEmpty.slice(1);
|
|
10141
|
+
const wanted = /* @__PURE__ */ new Set();
|
|
10142
|
+
for (let i = 0; i < rows.length && wanted.size < maxRows; i++) {
|
|
10143
|
+
if (TABLE_ROW_ANOMALY_RE.test(rows[i])) wanted.add(i);
|
|
10144
|
+
}
|
|
10145
|
+
const anomalies = wanted.size;
|
|
10146
|
+
for (let i = 0; i < rows.length && wanted.size < maxRows; i++) wanted.add(i);
|
|
10147
|
+
const kept = [...wanted].sort((a, b) => a - b);
|
|
10148
|
+
const elided = rows.length - kept.length;
|
|
10149
|
+
const note = anomalies ? `[token-goat: ${elided} more rows; ${anomalies} row(s) kept for a not-ready status, the rest from the top; ${hint}]` : `[token-goat: ${elided} more rows; ${hint}]`;
|
|
10150
|
+
return `${[header, ...kept.map((i) => rows[i])].join("\n")}
|
|
10151
|
+
${note}`;
|
|
10020
10152
|
}
|
|
10021
10153
|
function trimRepeatedPrefix(lines, pattern, keep) {
|
|
10022
10154
|
const out = [];
|
|
@@ -10304,6 +10436,7 @@ function shlexSplit(cmd) {
|
|
|
10304
10436
|
function capLongLines(lines, maxChars = FALLBACK_MAX_LINE_CHARS) {
|
|
10305
10437
|
return lines.map((line) => {
|
|
10306
10438
|
if (line.length <= maxChars) return line;
|
|
10439
|
+
if (ELIDED_MARKER_RE.test(line)) return line;
|
|
10307
10440
|
let cut = maxChars;
|
|
10308
10441
|
const high = line.charCodeAt(cut - 1);
|
|
10309
10442
|
const low = line.charCodeAt(cut);
|
|
@@ -10500,7 +10633,7 @@ function isRewriteWorthwhile({
|
|
|
10500
10633
|
return bytesSaved - noticeBytes >= minNetSavingsBytes;
|
|
10501
10634
|
}
|
|
10502
10635
|
function compressedTokensSaved(bytesSaved) {
|
|
10503
|
-
return bytesSaved <= 0 ? 0 : Math.max(1,
|
|
10636
|
+
return bytesSaved <= 0 ? 0 : Math.max(1, savedTokensFromBytes(bytesSaved));
|
|
10504
10637
|
}
|
|
10505
10638
|
var CompressedOutput = class {
|
|
10506
10639
|
constructor(text, originalBytes, compressedBytes, filterName, exitCode = 0, notes = []) {
|
|
@@ -10521,7 +10654,7 @@ var CompressedOutput = class {
|
|
|
10521
10654
|
get bytesSaved() {
|
|
10522
10655
|
return Math.max(0, this.originalBytes - this.compressedBytes);
|
|
10523
10656
|
}
|
|
10524
|
-
/** Estimated token savings
|
|
10657
|
+
/** Estimated token savings, matching `compressedTokensSaved` (bytes/4, the codebase-wide pricing constant). */
|
|
10525
10658
|
get tokensSaved() {
|
|
10526
10659
|
return compressedTokensSaved(this.bytesSaved);
|
|
10527
10660
|
}
|
|
@@ -10632,19 +10765,19 @@ var ToolFilter = class {
|
|
|
10632
10765
|
* Filters that handle errors structurally (pytest, cargo) override this
|
|
10633
10766
|
* directly and leave `errorPassthrough` false.
|
|
10634
10767
|
*/
|
|
10635
|
-
compress(stdout, stderr, exitCode, argv) {
|
|
10768
|
+
compress(stdout, stderr, exitCode, argv, ctx = {}) {
|
|
10636
10769
|
if (this.errorPassthrough) {
|
|
10637
10770
|
const err = preserveStderrOnError(stdout, stderr, exitCode);
|
|
10638
10771
|
if (err !== null) return err;
|
|
10639
10772
|
}
|
|
10640
|
-
return this.compressBody(stdout, stderr, exitCode, argv);
|
|
10773
|
+
return this.compressBody(stdout, stderr, exitCode, argv, ctx);
|
|
10641
10774
|
}
|
|
10642
10775
|
/**
|
|
10643
10776
|
* Inner compression logic, called after the error-passthrough guard.
|
|
10644
10777
|
* Default is a passthrough that joins the two streams — useful when the only
|
|
10645
10778
|
* compression is the ANSI / progress strip `apply` already performed.
|
|
10646
10779
|
*/
|
|
10647
|
-
compressBody(stdout, stderr, _exitCode, _argv) {
|
|
10780
|
+
compressBody(stdout, stderr, _exitCode, _argv, _ctx = {}) {
|
|
10648
10781
|
if (stderr && stdout) return `${stdout.replace(/\s+$/, "")}
|
|
10649
10782
|
---
|
|
10650
10783
|
${stderr.replace(/\s+$/, "")}`;
|
|
@@ -10666,14 +10799,16 @@ ${stderr.replace(/\s+$/, "")}`;
|
|
|
10666
10799
|
const notes = [];
|
|
10667
10800
|
const soBytes = Buffer.from(so, "utf8");
|
|
10668
10801
|
const seBytes = Buffer.from(se, "utf8");
|
|
10669
|
-
|
|
10670
|
-
|
|
10671
|
-
|
|
10802
|
+
const soClamped = clampKeepingEnds(so, maxInput);
|
|
10803
|
+
const seClamped = clampKeepingEnds(se, maxInput);
|
|
10804
|
+
if (soClamped !== null) {
|
|
10805
|
+
so = soClamped;
|
|
10806
|
+
notes.push(`input over ${Math.floor(maxInput / 1024)}KB: kept both ends (TOKEN_GOAT_FILTER_MAX_BYTES)`);
|
|
10672
10807
|
}
|
|
10673
|
-
if (
|
|
10674
|
-
se =
|
|
10675
|
-
if (!notes.some((n) => n.includes("
|
|
10676
|
-
notes.push(`stderr
|
|
10808
|
+
if (seClamped !== null) {
|
|
10809
|
+
se = seClamped;
|
|
10810
|
+
if (!notes.some((n) => n.includes("kept both ends"))) {
|
|
10811
|
+
notes.push(`stderr over ${Math.floor(maxInput / 1024)}KB: kept both ends (TOKEN_GOAT_FILTER_MAX_BYTES)`);
|
|
10677
10812
|
}
|
|
10678
10813
|
}
|
|
10679
10814
|
const originalBytes = soBytes.length + seBytes.length;
|
|
@@ -10695,7 +10830,7 @@ ${stderr.replace(/\s+$/, "")}`;
|
|
|
10695
10830
|
notes.push(`input exceeded inspect budget (${Math.floor(MAX_INSPECT_BYTES / 1024)} KiB); fell back to truncation`);
|
|
10696
10831
|
body = fallbackTruncate(normOut, normErr, maxLines);
|
|
10697
10832
|
} else {
|
|
10698
|
-
body = this.compress(normOut, normErr, exitCode, argv);
|
|
10833
|
+
body = this.compress(normOut, normErr, exitCode, argv, { inputTruncated: soClamped !== null || seClamped !== null });
|
|
10699
10834
|
}
|
|
10700
10835
|
} catch (exc) {
|
|
10701
10836
|
const kind = exc instanceof Error ? exc.constructor.name : "Error";
|
|
@@ -10704,6 +10839,7 @@ ${stderr.replace(/\s+$/, "")}`;
|
|
|
10704
10839
|
const fbErr = this.postNormalise(normalise(se, { skipProgress }));
|
|
10705
10840
|
body = fallbackTruncate(fbOut, fbErr, maxLines);
|
|
10706
10841
|
}
|
|
10842
|
+
body = capLongLines(body.split("\n"), LONG_LINE_MAX_CHARS).join("\n");
|
|
10707
10843
|
const lines = body.split("\n");
|
|
10708
10844
|
if (lines.length > maxLines) body = truncateMiddleSmart(lines, maxLines).join("\n");
|
|
10709
10845
|
body = capBytes(body, maxBytes);
|
|
@@ -15384,7 +15520,7 @@ var GrepFilter = class extends ToolFilter {
|
|
|
15384
15520
|
}
|
|
15385
15521
|
return false;
|
|
15386
15522
|
}
|
|
15387
|
-
compress(stdout, stderr, _exitCode, argv) {
|
|
15523
|
+
compress(stdout, stderr, _exitCode, argv, ctx = {}) {
|
|
15388
15524
|
const text = this.combineOutput(stdout, stderr);
|
|
15389
15525
|
const lines = text.split("\n");
|
|
15390
15526
|
const nonEmpty = lines.filter((l) => l.trim());
|
|
@@ -15411,7 +15547,7 @@ var GrepFilter = class extends ToolFilter {
|
|
|
15411
15547
|
}
|
|
15412
15548
|
const totalMatches = [...fileCounts.values()].reduce((a, b) => a + b, 0) + unattributed;
|
|
15413
15549
|
const numFiles = fileCounts.size;
|
|
15414
|
-
const outLines = [`grep: ${totalMatches} matches across ${numFiles} file(s)`];
|
|
15550
|
+
const outLines = ctx.inputTruncated ? [`grep: at least ${totalMatches} matches across ${numFiles} file(s) (counted over a truncated input; per-file counts below are lower bounds)`] : [`grep: ${totalMatches} matches across ${numFiles} file(s)`];
|
|
15415
15551
|
const sorted = [...fileCounts.entries()].sort((a, b) => b[1] - a[1]);
|
|
15416
15552
|
const shown = sorted.slice(0, _GREP_MAX_FILE_LINES);
|
|
15417
15553
|
for (const [fname, count] of shown) {
|
|
@@ -15494,8 +15630,11 @@ var RgFilter = class _RgFilter extends ToolFilter {
|
|
|
15494
15630
|
const kept = groups.filter((_, i) => topIdx.has(i));
|
|
15495
15631
|
const suppressed = groups.length - kept.length;
|
|
15496
15632
|
const joined = kept.join("\n" + _RgFilter._SEP + "\n");
|
|
15633
|
+
const lastKept = scored[Math.min(_RG_TOP_GROUPS, scored.length) - 1]?.score;
|
|
15634
|
+
const firstDropped = scored[_RG_TOP_GROUPS]?.score;
|
|
15635
|
+
const tied = firstDropped !== void 0 && firstDropped === lastKept;
|
|
15497
15636
|
return joined + `
|
|
15498
|
-
[token-goat: ${suppressed} more match groups suppressed
|
|
15637
|
+
[token-goat: ${suppressed} more match groups suppressed${tied ? ", tied on match count with the ones kept and separated only by filename order" : ", each with fewer matches than those kept"}: rerun with -l for filenames only]`;
|
|
15499
15638
|
}
|
|
15500
15639
|
// Same per-line clip GrepFilter applies: every branch below can return match lines verbatim, so the cap is applied once here rather than at each of the five return sites.
|
|
15501
15640
|
compress(stdout, stderr, exitCode, argv) {
|
|
@@ -16006,6 +16145,12 @@ var RsyncFilter = class extends ToolFilter {
|
|
|
16006
16145
|
var _DIFF_FILE_HEADER_RE = /^(?:diff\s|---\s)/;
|
|
16007
16146
|
var _DIFF_HUNK_RE = /^@@ /;
|
|
16008
16147
|
var _DIFF_MAX_FULL_FILES = 20;
|
|
16148
|
+
var _DIFF_MAX_STAT_EXTRA_LINES = 40;
|
|
16149
|
+
function _isDiffBodyLine(line) {
|
|
16150
|
+
if (line === "") return true;
|
|
16151
|
+
const c = line[0] ?? "";
|
|
16152
|
+
return c === " " || c === "+" || c === "-" || c === "@" || c === "\\";
|
|
16153
|
+
}
|
|
16009
16154
|
function _isDiffAdd(line) {
|
|
16010
16155
|
return line.startsWith("+") && !line.startsWith("+++");
|
|
16011
16156
|
}
|
|
@@ -16115,12 +16260,33 @@ var DiffFilter = class extends ToolFilter {
|
|
|
16115
16260
|
const statLines = [
|
|
16116
16261
|
`[token-goat: large diff (${realFiles.length} files); stat-only view]`
|
|
16117
16262
|
];
|
|
16118
|
-
|
|
16263
|
+
let extrasKept = 0;
|
|
16264
|
+
let extrasDropped = 0;
|
|
16265
|
+
const emitExtras = (candidates) => {
|
|
16266
|
+
const foreign = candidates.filter((l) => l.trim() !== "" && !_isDiffBodyLine(l));
|
|
16267
|
+
for (const line of capLongLines(foreign)) {
|
|
16268
|
+
if (extrasKept >= _DIFF_MAX_STAT_EXTRA_LINES) {
|
|
16269
|
+
extrasDropped++;
|
|
16270
|
+
continue;
|
|
16271
|
+
}
|
|
16272
|
+
extrasKept++;
|
|
16273
|
+
statLines.push(line);
|
|
16274
|
+
}
|
|
16275
|
+
};
|
|
16276
|
+
for (const blockStr of rawBlocks) {
|
|
16119
16277
|
const blockLines = blockStr.split("\n");
|
|
16120
|
-
const header = blockLines[0];
|
|
16278
|
+
const header = blockLines[0] ?? "";
|
|
16279
|
+
if (!_DIFF_FILE_HEADER_RE.test(header)) {
|
|
16280
|
+
emitExtras(blockLines);
|
|
16281
|
+
continue;
|
|
16282
|
+
}
|
|
16121
16283
|
const adds = blockLines.filter(_isDiffAdd).length;
|
|
16122
16284
|
const dels = blockLines.filter(_isDiffRemove).length;
|
|
16123
16285
|
statLines.push(`${header} +${adds} -${dels}`);
|
|
16286
|
+
emitExtras(blockLines.slice(1));
|
|
16287
|
+
}
|
|
16288
|
+
if (extrasDropped > 0) {
|
|
16289
|
+
statLines.push(`[token-goat: ${extrasDropped} more non-diff line${extrasDropped === 1 ? "" : "s"} omitted]`);
|
|
16124
16290
|
}
|
|
16125
16291
|
return statLines.join("\n");
|
|
16126
16292
|
}
|
|
@@ -18137,7 +18303,7 @@ var Sqlite3Filter = class _Sqlite3Filter extends ToolFilter {
|
|
|
18137
18303
|
binaries = /* @__PURE__ */ new Set(["sqlite3"]);
|
|
18138
18304
|
static ROW_THRESHOLD = 20;
|
|
18139
18305
|
static KEEP_ROWS = 5;
|
|
18140
|
-
compress(stdout, stderr, _exitCode, _argv) {
|
|
18306
|
+
compress(stdout, stderr, _exitCode, _argv, ctx = {}) {
|
|
18141
18307
|
const merged = this.combineOutput(stdout, stderr);
|
|
18142
18308
|
const lines = merged.split("\n");
|
|
18143
18309
|
const nonEmpty = lines.filter((ln) => ln.trim());
|
|
@@ -18156,7 +18322,9 @@ var Sqlite3Filter = class _Sqlite3Filter extends ToolFilter {
|
|
|
18156
18322
|
const nonEmptyBody = bodyLines.filter((ln) => ln.trim());
|
|
18157
18323
|
if (nonEmptyBody.length > _Sqlite3Filter.ROW_THRESHOLD) {
|
|
18158
18324
|
kept.push(...nonEmptyBody.slice(0, _Sqlite3Filter.KEEP_ROWS));
|
|
18159
|
-
kept.push(
|
|
18325
|
+
kept.push(
|
|
18326
|
+
ctx.inputTruncated === true ? `[token-goat: at least ${nonEmptyBody.length} rows (counted over a truncated input; showing first ${_Sqlite3Filter.KEEP_ROWS})]` : `[token-goat: ${nonEmptyBody.length} rows (showing first ${_Sqlite3Filter.KEEP_ROWS})]`
|
|
18327
|
+
);
|
|
18160
18328
|
} else {
|
|
18161
18329
|
kept.push(...bodyLines);
|
|
18162
18330
|
}
|
|
@@ -19498,7 +19666,7 @@ var KubectlFilter = class extends ToolFilter {
|
|
|
19498
19666
|
} else if (subcommand === "diff") {
|
|
19499
19667
|
const diffLines = text.split("\n");
|
|
19500
19668
|
if (diffLines.length > 50) {
|
|
19501
|
-
text = headTailCompress(diffLines,
|
|
19669
|
+
text = headTailCompress(diffLines, 35, 15, "diff lines");
|
|
19502
19670
|
}
|
|
19503
19671
|
}
|
|
19504
19672
|
if (stderr.trim()) {
|
|
@@ -19914,8 +20082,14 @@ function _capPatchLinesInBlock(block, maxLines) {
|
|
|
19914
20082
|
const headerLines = lines.slice(0, diffStart);
|
|
19915
20083
|
let diffLines = lines.slice(diffStart);
|
|
19916
20084
|
if (diffLines.length > maxLines) {
|
|
20085
|
+
const tailKeep = Math.min(10, Math.floor(maxLines / 3));
|
|
20086
|
+
const headKeep = maxLines - tailKeep;
|
|
19917
20087
|
const elided = diffLines.length - maxLines;
|
|
19918
|
-
diffLines = [
|
|
20088
|
+
diffLines = [
|
|
20089
|
+
...diffLines.slice(0, headKeep),
|
|
20090
|
+
`--- patch: ${elided} lines omitted by token-goat ---`,
|
|
20091
|
+
...diffLines.slice(diffLines.length - tailKeep)
|
|
20092
|
+
];
|
|
19919
20093
|
}
|
|
19920
20094
|
return [...headerLines, ...diffLines].join("\n");
|
|
19921
20095
|
}
|
|
@@ -19960,7 +20134,7 @@ function _compressGitLogStat(stdout, stderr) {
|
|
|
19960
20134
|
const MAX_STAT_FILES = 20;
|
|
19961
20135
|
return _compressGitLogCapped(stdout, stderr, (block) => _capStatLinesInBlock(block, MAX_STAT_FILES));
|
|
19962
20136
|
}
|
|
19963
|
-
function _compressGitLogEnhanced(stdout, stderr, argv) {
|
|
20137
|
+
function _compressGitLogEnhanced(stdout, stderr, argv, inputTruncated = false) {
|
|
19964
20138
|
const flags = new Set(argv);
|
|
19965
20139
|
let isOneline = flags.has("--oneline") || flags.has("--format=oneline") || flags.has("--pretty=oneline") || argv.some((a) => a.startsWith("--format=%h") || a.startsWith("--pretty=%h"));
|
|
19966
20140
|
if (!isOneline) {
|
|
@@ -19984,7 +20158,8 @@ function _compressGitLogEnhanced(stdout, stderr, argv) {
|
|
|
19984
20158
|
let keptLines;
|
|
19985
20159
|
if (blocks.length > ONELINE_CAP) {
|
|
19986
20160
|
const elided = blocks.length - ONELINE_CAP;
|
|
19987
|
-
|
|
20161
|
+
const elidedNote = inputTruncated ? `[token-goat: at least ${elided} more commits (counted over a truncated input)]` : `[token-goat: +${elided} more commits]`;
|
|
20162
|
+
keptLines = [...blocks.slice(0, ONELINE_CAP), elidedNote];
|
|
19988
20163
|
} else {
|
|
19989
20164
|
keptLines = blocks;
|
|
19990
20165
|
}
|
|
@@ -19999,8 +20174,8 @@ function _compressGitLogEnhanced(stdout, stderr, argv) {
|
|
|
19999
20174
|
var GitLogFilter = class extends GitBaseFilter {
|
|
20000
20175
|
name = "git-log";
|
|
20001
20176
|
subcommands = /* @__PURE__ */ new Set(["log"]);
|
|
20002
|
-
compress(stdout, stderr, _exitCode, argv) {
|
|
20003
|
-
return _compressGitLogEnhanced(stdout, stderr, argv);
|
|
20177
|
+
compress(stdout, stderr, _exitCode, argv, ctx = {}) {
|
|
20178
|
+
return _compressGitLogEnhanced(stdout, stderr, argv, ctx.inputTruncated === true);
|
|
20004
20179
|
}
|
|
20005
20180
|
};
|
|
20006
20181
|
var _GIT_DIFF_BINARY_RE = /^Binary files?(?: .+)? differ$/;
|
|
@@ -20241,43 +20416,6 @@ function _compressGitDiffBody(stdout, stderr, maxHunksPerFile = 10) {
|
|
|
20241
20416
|
if (stderr.trim()) text += "\n---\n" + stderr.replace(/\s+$/, "");
|
|
20242
20417
|
return text;
|
|
20243
20418
|
}
|
|
20244
|
-
function _compressGitDiffSimple(stdout, stderr, maxHunksPerFile = 3) {
|
|
20245
|
-
const fileBlocks = splitBlocks(stdout, _GIT_DIFF_FILE_RE);
|
|
20246
|
-
if (!fileBlocks.length) return stdout;
|
|
20247
|
-
const realFiles = fileBlocks.filter((b) => _GIT_DIFF_FILE_RE.test(b));
|
|
20248
|
-
if (realFiles.length > 200) {
|
|
20249
|
-
const statLines = realFiles.map((b) => {
|
|
20250
|
-
const header = b.split("\n", 1)[0] ?? "";
|
|
20251
|
-
const lines = b.split("\n");
|
|
20252
|
-
const adds = lines.filter(_isDiffAdd2).length;
|
|
20253
|
-
const dels = lines.filter(_isDiffRemove2).length;
|
|
20254
|
-
return `${header} +${adds} -${dels}`;
|
|
20255
|
-
});
|
|
20256
|
-
return `[token-goat: large diff (${realFiles.length} files); showing stat-only view]
|
|
20257
|
-
` + statLines.join("\n");
|
|
20258
|
-
}
|
|
20259
|
-
const outBlocks = [];
|
|
20260
|
-
for (const block of fileBlocks) {
|
|
20261
|
-
if (!_GIT_DIFF_FILE_RE.test(block)) {
|
|
20262
|
-
outBlocks.push(block);
|
|
20263
|
-
continue;
|
|
20264
|
-
}
|
|
20265
|
-
const hunks = splitBlocks(block, _GIT_DIFF_HUNK_RE);
|
|
20266
|
-
if (hunks.length <= maxHunksPerFile + 1) {
|
|
20267
|
-
outBlocks.push(block);
|
|
20268
|
-
continue;
|
|
20269
|
-
}
|
|
20270
|
-
const head = hunks.slice(0, maxHunksPerFile + 1);
|
|
20271
|
-
const elided = hunks.slice(maxHunksPerFile + 1);
|
|
20272
|
-
outBlocks.push(
|
|
20273
|
-
head.join("\n") + `
|
|
20274
|
-
[token-goat: +${elided.length} more hunks in this file elided]`
|
|
20275
|
-
);
|
|
20276
|
-
}
|
|
20277
|
-
let text = outBlocks.join("\n");
|
|
20278
|
-
if (stderr.trim()) text += "\n---\n" + stderr.replace(/\s+$/, "");
|
|
20279
|
-
return text;
|
|
20280
|
-
}
|
|
20281
20419
|
function _compressGitDiffEnhanced(stdout, stderr, argv) {
|
|
20282
20420
|
const flags = new Set(argv);
|
|
20283
20421
|
const isStat = flags.has("--stat") || flags.has("--shortstat") || flags.has("--name-only");
|
|
@@ -20794,13 +20932,7 @@ var GitFilter = class extends GitBaseFilter {
|
|
|
20794
20932
|
const positionals = gitPositionalArgs(argv.slice(1));
|
|
20795
20933
|
const subcommand = positionals[0] ?? "";
|
|
20796
20934
|
if (subcommand === "diff" || subcommand === "show") {
|
|
20797
|
-
|
|
20798
|
-
try {
|
|
20799
|
-
maxHunksPerFile = loadConfig().bash_diff.max_hunks_per_file;
|
|
20800
|
-
} catch {
|
|
20801
|
-
maxHunksPerFile = void 0;
|
|
20802
|
-
}
|
|
20803
|
-
return maxHunksPerFile === void 0 ? _compressGitDiffSimple(stdout, stderr) : _compressGitDiffSimple(stdout, stderr, maxHunksPerFile);
|
|
20935
|
+
return _compressGitDiffEnhanced(stdout, stderr, argv);
|
|
20804
20936
|
}
|
|
20805
20937
|
if (subcommand === "ls-files" || subcommand === "ls-tree")
|
|
20806
20938
|
return _truncateListing(stdout, stderr, 100);
|