@drewpayment/mink 0.12.0 → 0.13.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dashboard/out/404.html +1 -1
  2. package/dashboard/out/action-log.html +1 -1
  3. package/dashboard/out/action-log.txt +1 -1
  4. package/dashboard/out/activity.html +1 -1
  5. package/dashboard/out/activity.txt +1 -1
  6. package/dashboard/out/bugs.html +1 -1
  7. package/dashboard/out/bugs.txt +1 -1
  8. package/dashboard/out/capture.html +1 -1
  9. package/dashboard/out/capture.txt +1 -1
  10. package/dashboard/out/config.html +1 -1
  11. package/dashboard/out/config.txt +1 -1
  12. package/dashboard/out/daemon.html +1 -1
  13. package/dashboard/out/daemon.txt +1 -1
  14. package/dashboard/out/design.html +1 -1
  15. package/dashboard/out/design.txt +1 -1
  16. package/dashboard/out/discord.html +1 -1
  17. package/dashboard/out/discord.txt +1 -1
  18. package/dashboard/out/file-index.html +1 -1
  19. package/dashboard/out/file-index.txt +1 -1
  20. package/dashboard/out/index.html +1 -1
  21. package/dashboard/out/index.txt +1 -1
  22. package/dashboard/out/insights.html +1 -1
  23. package/dashboard/out/insights.txt +1 -1
  24. package/dashboard/out/learning.html +1 -1
  25. package/dashboard/out/learning.txt +1 -1
  26. package/dashboard/out/overview.html +1 -1
  27. package/dashboard/out/overview.txt +1 -1
  28. package/dashboard/out/scheduler.html +1 -1
  29. package/dashboard/out/scheduler.txt +1 -1
  30. package/dashboard/out/sync.html +1 -1
  31. package/dashboard/out/sync.txt +1 -1
  32. package/dashboard/out/tokens.html +1 -1
  33. package/dashboard/out/tokens.txt +1 -1
  34. package/dashboard/out/waste.html +1 -1
  35. package/dashboard/out/waste.txt +1 -1
  36. package/dashboard/out/wiki.html +1 -1
  37. package/dashboard/out/wiki.txt +1 -1
  38. package/dist/cli.bun.js +748 -10
  39. package/dist/cli.node.js +752 -12
  40. package/package.json +1 -1
  41. package/src/cli.ts +14 -0
  42. package/src/commands/init.ts +5 -1
  43. package/src/commands/post-read.ts +18 -0
  44. package/src/commands/post-tool.ts +48 -0
  45. package/src/commands/retrieve.ts +32 -0
  46. package/src/core/code-skeleton.ts +108 -0
  47. package/src/core/compress-tool-output.ts +127 -0
  48. package/src/core/compression.ts +81 -0
  49. package/src/core/hook-output.ts +42 -0
  50. package/src/core/output-compression.ts +252 -0
  51. package/src/core/token-estimate.ts +40 -0
  52. package/src/repositories/compression-cache-repo.ts +97 -0
  53. package/src/repositories/token-ledger-repo.ts +87 -0
  54. package/src/storage/schema.ts +50 -1
  55. package/src/types/compression.ts +29 -0
  56. package/src/types/config.ts +40 -0
  57. package/src/types/hook-input.ts +4 -0
  58. package/src/types/token-ledger.ts +33 -0
  59. /package/dashboard/out/_next/static/{Cr7-P-E43jbsBjy4hA6wH → Yl3F-J4CwvYf6yWG-SSmG}/_buildManifest.js +0 -0
  60. /package/dashboard/out/_next/static/{Cr7-P-E43jbsBjy4hA6wH → Yl3F-J4CwvYf6yWG-SSmG}/_ssgManifest.js +0 -0
package/dist/cli.bun.js CHANGED
@@ -421,6 +421,41 @@ var init_config = __esm(() => {
421
421
  envVar: "MINK_PROJECTS_IDENTITY",
422
422
  description: "Project identity strategy: path-derived (legacy) or git-remote (stable across machines)",
423
423
  scope: "shared"
424
+ },
425
+ {
426
+ key: "compression.enabled",
427
+ default: "false",
428
+ envVar: "MINK_COMPRESSION_ENABLED",
429
+ description: "Enable tool-output compression (spec 21). Off until inline compression ships.",
430
+ scope: "shared"
431
+ },
432
+ {
433
+ key: "compression.threshold-tokens",
434
+ default: "800",
435
+ envVar: "MINK_COMPRESSION_THRESHOLD_TOKENS",
436
+ description: "Minimum estimated token size before a tool output is eligible for compression",
437
+ scope: "shared"
438
+ },
439
+ {
440
+ key: "compression.min-savings-ratio",
441
+ default: "0.25",
442
+ envVar: "MINK_COMPRESSION_MIN_SAVINGS_RATIO",
443
+ description: "Discard a compression attempt unless it saves at least this fraction of tokens",
444
+ scope: "shared"
445
+ },
446
+ {
447
+ key: "compression.holdout-fraction",
448
+ default: "0.1",
449
+ envVar: "MINK_COMPRESSION_HOLDOUT_FRACTION",
450
+ description: "Fraction of eligible outputs left uncompressed as a measured control group",
451
+ scope: "shared"
452
+ },
453
+ {
454
+ key: "compression.retention-hours",
455
+ default: "168",
456
+ envVar: "MINK_COMPRESSION_RETENTION_HOURS",
457
+ description: "How long compressed originals stay retrievable before eviction",
458
+ scope: "shared"
424
459
  }
425
460
  ];
426
461
  VALID_KEYS = new Set(CONFIG_KEYS.map((k) => k.key));
@@ -3095,7 +3130,7 @@ function readMeta(db, key) {
3095
3130
  function writeMeta(db, key, value) {
3096
3131
  db.prepare("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
3097
3132
  }
3098
- var SCHEMA_VERSION = 1, INITIAL_SCHEMA = `
3133
+ var SCHEMA_VERSION = 3, INITIAL_SCHEMA = `
3099
3134
  CREATE TABLE IF NOT EXISTS meta (
3100
3135
  key TEXT PRIMARY KEY,
3101
3136
  value TEXT NOT NULL
@@ -3257,6 +3292,55 @@ CREATE TABLE IF NOT EXISTS counters (
3257
3292
  file_index_hits INTEGER NOT NULL DEFAULT 0,
3258
3293
  file_index_misses INTEGER NOT NULL DEFAULT 0
3259
3294
  );
3295
+
3296
+ -- Tool-output compression measurement (spec 21). One row per compression
3297
+ -- decision: either a compressed arm (compressed_tokens < original_tokens) or a
3298
+ -- holdout arm (left uncompressed for control, compressed_tokens = original_tokens).
3299
+ -- These are append-only telemetry, independent of session lifecycle, written at
3300
+ -- the moment a tool output is processed. New table \u2192 applied to existing DBs via
3301
+ -- IF NOT EXISTS on the next open.
3302
+ CREATE TABLE IF NOT EXISTS ledger_compressions (
3303
+ id TEXT PRIMARY KEY,
3304
+ created_at TEXT NOT NULL,
3305
+ tool_name TEXT NOT NULL,
3306
+ content_kind TEXT NOT NULL,
3307
+ original_tokens INTEGER NOT NULL DEFAULT 0,
3308
+ compressed_tokens INTEGER NOT NULL DEFAULT 0,
3309
+ holdout INTEGER NOT NULL DEFAULT 0,
3310
+ device_id TEXT NOT NULL
3311
+ );
3312
+ CREATE INDEX IF NOT EXISTS idx_ledger_compressions_created ON ledger_compressions(created_at);
3313
+ CREATE INDEX IF NOT EXISTS idx_ledger_compressions_device ON ledger_compressions(device_id);
3314
+
3315
+ -- Per-device compression aggregates, summed across devices like ledger_lifetime.
3316
+ -- measured_savings only credits compressed arms (holdout arms save nothing by
3317
+ -- construction), so the reported figure is a true measured delta, not an estimate.
3318
+ CREATE TABLE IF NOT EXISTS ledger_compression_lifetime (
3319
+ device_id TEXT PRIMARY KEY,
3320
+ total_events INTEGER NOT NULL DEFAULT 0,
3321
+ total_holdout_events INTEGER NOT NULL DEFAULT 0,
3322
+ total_original_tokens INTEGER NOT NULL DEFAULT 0,
3323
+ total_compressed_tokens INTEGER NOT NULL DEFAULT 0,
3324
+ total_measured_savings INTEGER NOT NULL DEFAULT 0
3325
+ );
3326
+
3327
+ -- Reversible-compression cache (spec 21 \xA7Reversibility). When a tool output is
3328
+ -- compressed, the original is stored here keyed by a short retrieval token and
3329
+ -- embedded in the compressed result; "mink retrieve <token>" returns it
3330
+ -- byte-exact. Rows expire after the configured retention window; an expired or
3331
+ -- unknown token is a graceful miss. This is a local cache, not synced state, so
3332
+ -- (unlike other tables) it carries no merge semantics beyond device_id for audit.
3333
+ CREATE TABLE IF NOT EXISTS compression_cache (
3334
+ token TEXT PRIMARY KEY,
3335
+ created_at TEXT NOT NULL,
3336
+ expires_at TEXT NOT NULL,
3337
+ tool_name TEXT NOT NULL,
3338
+ content_kind TEXT NOT NULL,
3339
+ content TEXT NOT NULL,
3340
+ size_bytes INTEGER NOT NULL,
3341
+ device_id TEXT NOT NULL
3342
+ );
3343
+ CREATE INDEX IF NOT EXISTS idx_compression_cache_expires ON compression_cache(expires_at);
3260
3344
  `;
3261
3345
 
3262
3346
  // src/storage/migrate-json.ts
@@ -3674,6 +3758,68 @@ class TokenLedgerRepo {
3674
3758
  }
3675
3759
  });
3676
3760
  }
3761
+ recordCompression(event, deviceId = getOrCreateDeviceId()) {
3762
+ const id = event.id ?? crypto.randomUUID();
3763
+ const createdAt = event.createdAt ?? new Date().toISOString();
3764
+ const holdout = event.holdout ? 1 : 0;
3765
+ const savings = event.holdout ? 0 : Math.max(0, event.originalTokens - event.compressedTokens);
3766
+ this.db.transaction(() => {
3767
+ this.db.prepare(`
3768
+ INSERT OR REPLACE INTO ledger_compressions
3769
+ (id, created_at, tool_name, content_kind,
3770
+ original_tokens, compressed_tokens, holdout, device_id)
3771
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3772
+ `).run(id, createdAt, event.toolName, event.contentKind, event.originalTokens, event.compressedTokens, holdout, deviceId);
3773
+ this.db.prepare(`
3774
+ INSERT INTO ledger_compression_lifetime
3775
+ (device_id, total_events, total_holdout_events,
3776
+ total_original_tokens, total_compressed_tokens, total_measured_savings)
3777
+ VALUES (?, ?, ?, ?, ?, ?)
3778
+ ON CONFLICT(device_id) DO UPDATE SET
3779
+ total_events = ledger_compression_lifetime.total_events + excluded.total_events,
3780
+ total_holdout_events = ledger_compression_lifetime.total_holdout_events + excluded.total_holdout_events,
3781
+ total_original_tokens = ledger_compression_lifetime.total_original_tokens + excluded.total_original_tokens,
3782
+ total_compressed_tokens = ledger_compression_lifetime.total_compressed_tokens + excluded.total_compressed_tokens,
3783
+ total_measured_savings = ledger_compression_lifetime.total_measured_savings + excluded.total_measured_savings
3784
+ `).run(deviceId, 1, holdout, event.originalTokens, event.compressedTokens, savings);
3785
+ });
3786
+ }
3787
+ compressionLifetime() {
3788
+ const row = this.db.prepare(`
3789
+ SELECT
3790
+ COALESCE(SUM(total_events), 0) AS totalEvents,
3791
+ COALESCE(SUM(total_holdout_events), 0) AS totalHoldoutEvents,
3792
+ COALESCE(SUM(total_original_tokens), 0) AS totalOriginalTokens,
3793
+ COALESCE(SUM(total_compressed_tokens), 0) AS totalCompressedTokens,
3794
+ COALESCE(SUM(total_measured_savings), 0) AS totalMeasuredSavings
3795
+ FROM ledger_compression_lifetime
3796
+ `).get();
3797
+ return {
3798
+ totalEvents: Number(row?.totalEvents ?? 0),
3799
+ totalHoldoutEvents: Number(row?.totalHoldoutEvents ?? 0),
3800
+ totalOriginalTokens: Number(row?.totalOriginalTokens ?? 0),
3801
+ totalCompressedTokens: Number(row?.totalCompressedTokens ?? 0),
3802
+ totalMeasuredSavings: Number(row?.totalMeasuredSavings ?? 0)
3803
+ };
3804
+ }
3805
+ compressionEvents(limit = 100) {
3806
+ const rows = this.db.prepare(`
3807
+ SELECT id, created_at, tool_name, content_kind,
3808
+ original_tokens, compressed_tokens, holdout
3809
+ FROM ledger_compressions
3810
+ ORDER BY created_at DESC
3811
+ LIMIT ?
3812
+ `).all(limit);
3813
+ return rows.map((r) => ({
3814
+ id: String(r.id),
3815
+ createdAt: String(r.created_at),
3816
+ toolName: String(r.tool_name),
3817
+ contentKind: String(r.content_kind),
3818
+ originalTokens: Number(r.original_tokens),
3819
+ compressedTokens: Number(r.compressed_tokens),
3820
+ holdout: Number(r.holdout) === 1
3821
+ }));
3822
+ }
3677
3823
  insertSessionRow(summary, deviceId, archived) {
3678
3824
  this.db.prepare(`
3679
3825
  INSERT OR REPLACE INTO ledger_sessions
@@ -4448,6 +4594,28 @@ function estimateTokens2(content, filePath) {
4448
4594
  }
4449
4595
  return Math.ceil(content.length / ratio);
4450
4596
  }
4597
+ function countTokens(text) {
4598
+ if (!text)
4599
+ return 0;
4600
+ const segments = text.match(/[A-Za-z]+|[0-9]+|[^A-Za-z0-9]/g);
4601
+ if (!segments)
4602
+ return 0;
4603
+ let tokens = 0;
4604
+ for (const seg of segments) {
4605
+ const first = seg.charCodeAt(0);
4606
+ if (first >= 65 && first <= 90 || first >= 97 && first <= 122) {
4607
+ tokens += Math.ceil(seg.length / 4);
4608
+ } else if (first >= 48 && first <= 57) {
4609
+ tokens += Math.ceil(seg.length / 3);
4610
+ } else if (seg === `
4611
+ `) {
4612
+ tokens += 1;
4613
+ } else if (seg === " " || seg === "\t" || seg === "\r") {} else {
4614
+ tokens += 1;
4615
+ }
4616
+ }
4617
+ return tokens;
4618
+ }
4451
4619
  var CODE_EXTENSIONS, PROSE_EXTENSIONS, BINARY_EXTENSIONS;
4452
4620
  var init_token_estimate = __esm(() => {
4453
4621
  CODE_EXTENSIONS = new Set([
@@ -5643,12 +5811,14 @@ function buildHooksConfig(cliPath) {
5643
5811
  PostToolUse: [
5644
5812
  { matcher: "Read", hooks: hook(`${prefix} post-read`) },
5645
5813
  { matcher: "Edit", hooks: hook(`${prefix} post-write`) },
5646
- { matcher: "Write", hooks: hook(`${prefix} post-write`) }
5814
+ { matcher: "Write", hooks: hook(`${prefix} post-write`) },
5815
+ { matcher: "Bash", hooks: hook(`${prefix} post-tool`) },
5816
+ { matcher: "Grep", hooks: hook(`${prefix} post-tool`) }
5647
5817
  ]
5648
5818
  };
5649
5819
  }
5650
5820
  function isMinkCommand(cmd) {
5651
- const hasMinkSubcommand = cmd.includes("session-start") || cmd.includes("session-stop") || cmd.includes("pre-read") || cmd.includes("post-read") || cmd.includes("pre-write") || cmd.includes("post-write");
5821
+ const hasMinkSubcommand = cmd.includes("session-start") || cmd.includes("session-stop") || cmd.includes("pre-read") || cmd.includes("post-read") || cmd.includes("pre-write") || cmd.includes("post-write") || cmd.includes("post-tool");
5652
5822
  if (!hasMinkSubcommand)
5653
5823
  return false;
5654
5824
  if (/(^|\/|\s)mink\s/.test(cmd))
@@ -6782,6 +6952,488 @@ var init_pre_read = __esm(() => {
6782
6952
  init_counters_repo();
6783
6953
  });
6784
6954
 
6955
+ // src/core/compression.ts
6956
+ function numberValue(key, fallback, min, max) {
6957
+ const raw = resolveConfigValue(key).value;
6958
+ const n = Number(raw);
6959
+ if (!Number.isFinite(n))
6960
+ return fallback;
6961
+ return Math.min(max, Math.max(min, n));
6962
+ }
6963
+ function loadCompressionConfig() {
6964
+ return {
6965
+ enabled: resolveConfigValue("compression.enabled").value === "true",
6966
+ thresholdTokens: numberValue("compression.threshold-tokens", 800, 0, Number.MAX_SAFE_INTEGER),
6967
+ minSavingsRatio: numberValue("compression.min-savings-ratio", 0.25, 0, 1),
6968
+ holdoutFraction: numberValue("compression.holdout-fraction", 0.1, 0, 1),
6969
+ retentionHours: numberValue("compression.retention-hours", 168, 0, Number.MAX_SAFE_INTEGER)
6970
+ };
6971
+ }
6972
+ function isEligible(originalTokens, config) {
6973
+ return config.enabled && originalTokens >= config.thresholdTokens;
6974
+ }
6975
+ function meetsMinSavings(originalTokens, compressedTokens, config) {
6976
+ if (originalTokens <= 0)
6977
+ return false;
6978
+ const ratio = (originalTokens - compressedTokens) / originalTokens;
6979
+ return ratio >= config.minSavingsRatio;
6980
+ }
6981
+ function hashUnitInterval(key) {
6982
+ let h = 2166136261;
6983
+ for (let i = 0;i < key.length; i++) {
6984
+ h ^= key.charCodeAt(i);
6985
+ h = Math.imul(h, 16777619);
6986
+ }
6987
+ return (h >>> 0) / 4294967296;
6988
+ }
6989
+ function selectHoldout(eventKey, fraction) {
6990
+ if (fraction <= 0)
6991
+ return false;
6992
+ if (fraction >= 1)
6993
+ return true;
6994
+ return hashUnitInterval(eventKey) < fraction;
6995
+ }
6996
+ var init_compression = __esm(() => {
6997
+ init_global_config();
6998
+ });
6999
+
7000
+ // src/core/code-skeleton.ts
7001
+ function countChar(s, c) {
7002
+ let n = 0;
7003
+ for (let i = 0;i < s.length; i++)
7004
+ if (s[i] === c)
7005
+ n++;
7006
+ return n;
7007
+ }
7008
+ function netBraces(line) {
7009
+ let s = line.replace(/\/\/.*$/, "");
7010
+ s = s.replace(/\/\*.*?\*\//g, "");
7011
+ s = s.replace(/"(?:\\.|[^"\\])*"/g, '""');
7012
+ s = s.replace(/'(?:\\.|[^'\\])*'/g, "''");
7013
+ s = s.replace(/`(?:\\.|[^`\\])*`/g, "``");
7014
+ return countChar(s, "{") - countChar(s, "}");
7015
+ }
7016
+ function stripOpenBrace(sig) {
7017
+ return sig.replace(/\{\s*$/, "").trimEnd();
7018
+ }
7019
+ function extractCodeSkeleton(content, opts = {}) {
7020
+ const rawLines = content.split(`
7021
+ `);
7022
+ const totalLines = rawLines.length > 0 && rawLines[rawLines.length - 1] === "" ? rawLines.length - 1 : rawLines.length;
7023
+ const out = [];
7024
+ let depth = 0;
7025
+ let suppress = Infinity;
7026
+ for (const line of rawLines) {
7027
+ if (out.length >= MAX_SIGNATURES)
7028
+ break;
7029
+ const start = depth;
7030
+ const net = netBraces(line);
7031
+ if (start < suppress) {
7032
+ const isHeading = opts.markdown === true && HEADING.test(line);
7033
+ const captured = isHeading || DECL_ALWAYS.test(line) || DECL_EXPORTED_VAR.test(line) || start >= 1 && MEMBER.test(line);
7034
+ if (captured) {
7035
+ const sig = line.trim();
7036
+ if (net > 0) {
7037
+ if (DESCEND.test(line) && !isHeading) {
7038
+ out.push(INDENT.repeat(start) + stripOpenBrace(sig) + " {");
7039
+ } else {
7040
+ out.push(INDENT.repeat(start) + stripOpenBrace(sig) + " { \u2026 }");
7041
+ suppress = start + 1;
7042
+ }
7043
+ } else {
7044
+ out.push(INDENT.repeat(start) + sig);
7045
+ }
7046
+ }
7047
+ }
7048
+ depth = Math.max(0, depth + net);
7049
+ if (depth < suppress)
7050
+ suppress = Infinity;
7051
+ }
7052
+ if (out.length === 0)
7053
+ return null;
7054
+ return { lines: out, totalLines };
7055
+ }
7056
+ var MAX_SIGNATURES = 80, INDENT = " ", DECL_ALWAYS, DECL_EXPORTED_VAR, MEMBER, HEADING, DESCEND;
7057
+ var init_code_skeleton = __esm(() => {
7058
+ DECL_ALWAYS = /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|interface|type|enum|namespace|module|def|fn|func|impl|struct|trait)\b/;
7059
+ DECL_EXPORTED_VAR = /^\s*export\s+(?:default\s+)?(?:const|let|var)\b/;
7060
+ MEMBER = /^\s*(?:public\s+|private\s+|protected\s+|readonly\s+|static\s+|async\s+|get\s+|set\s+|#)*[\w$]+\??\s*(?:\(|:|=)/;
7061
+ HEADING = /^#{1,6}\s+\S/;
7062
+ DESCEND = /\b(?:class|interface|enum|namespace|module|struct|trait|impl)\b/;
7063
+ });
7064
+
7065
+ // src/core/output-compression.ts
7066
+ function stripAnsi(s) {
7067
+ return s.replace(ANSI, "");
7068
+ }
7069
+ function omittedMarker(n) {
7070
+ return ` \u2026 ${n} line${n === 1 ? "" : "s"} omitted \u2014 mink retrieve \u2026`;
7071
+ }
7072
+ function toLines(content) {
7073
+ const lines = content.split(`
7074
+ `);
7075
+ if (lines.length > 0 && lines[lines.length - 1] === "")
7076
+ lines.pop();
7077
+ return lines;
7078
+ }
7079
+ function compressLog(content) {
7080
+ const lines = toLines(stripAnsi(content));
7081
+ const collapsed = [];
7082
+ let i = 0;
7083
+ while (i < lines.length) {
7084
+ let run = 1;
7085
+ while (i + run < lines.length && lines[i + run] === lines[i])
7086
+ run++;
7087
+ collapsed.push(run > 1 ? `${lines[i]} (\xD7${run})` : lines[i]);
7088
+ i += run;
7089
+ }
7090
+ if (collapsed.length <= LOG_HEAD + LOG_TAIL) {
7091
+ if (collapsed.length === lines.length)
7092
+ return null;
7093
+ return {
7094
+ compressed: collapsed.join(`
7095
+ `),
7096
+ omittedNote: `collapsed ${lines.length - collapsed.length} repeated line(s)`
7097
+ };
7098
+ }
7099
+ const omitted = collapsed.length - LOG_HEAD - LOG_TAIL;
7100
+ const head = collapsed.slice(0, LOG_HEAD);
7101
+ const tail = collapsed.slice(collapsed.length - LOG_TAIL);
7102
+ return {
7103
+ compressed: [...head, omittedMarker(omitted), ...tail].join(`
7104
+ `),
7105
+ omittedNote: `${omitted} of ${collapsed.length} log line(s) omitted (middle)`
7106
+ };
7107
+ }
7108
+ function compressSearch(content) {
7109
+ const lines = toLines(content);
7110
+ const seen = new Set;
7111
+ const perFile = new Map;
7112
+ const omittedByFile = new Map;
7113
+ const out = [];
7114
+ for (const line of lines) {
7115
+ if (seen.has(line))
7116
+ continue;
7117
+ seen.add(line);
7118
+ const colon = line.indexOf(":");
7119
+ const file = colon > 0 ? line.slice(0, colon) : line;
7120
+ const count = perFile.get(file) ?? 0;
7121
+ if (count < SEARCH_MAX_PER_FILE) {
7122
+ perFile.set(file, count + 1);
7123
+ out.push(line);
7124
+ } else {
7125
+ omittedByFile.set(file, (omittedByFile.get(file) ?? 0) + 1);
7126
+ }
7127
+ }
7128
+ let totalOmitted = 0;
7129
+ for (const [file, n] of omittedByFile) {
7130
+ totalOmitted += n;
7131
+ out.push(` \u2026 +${n} more match(es) in ${file} \u2014 mink retrieve \u2026`);
7132
+ }
7133
+ const dedupRemoved = lines.length - seen.size;
7134
+ if (totalOmitted === 0 && dedupRemoved === 0)
7135
+ return null;
7136
+ const notes = [];
7137
+ if (totalOmitted > 0)
7138
+ notes.push(`${totalOmitted} match(es) capped`);
7139
+ if (dedupRemoved > 0)
7140
+ notes.push(`${dedupRemoved} duplicate(s) removed`);
7141
+ return { compressed: out.join(`
7142
+ `), omittedNote: notes.join("; ") };
7143
+ }
7144
+ function compressFile(filePath, content) {
7145
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
7146
+ const markdown = ext === ".md" || ext === ".mdx" || ext === ".markdown";
7147
+ const skeleton = extractCodeSkeleton(content, { markdown });
7148
+ if (!skeleton) {
7149
+ return compressText(content);
7150
+ }
7151
+ const header = `${filePath} \u2014 structural summary ` + `(${skeleton.lines.length} signature(s) of ${skeleton.totalLines} lines)`;
7152
+ return {
7153
+ compressed: [header, ...skeleton.lines].join(`
7154
+ `),
7155
+ omittedNote: `bodies elided; ${skeleton.totalLines} lines available via mink retrieve`
7156
+ };
7157
+ }
7158
+ function crush(value) {
7159
+ if (Array.isArray(value)) {
7160
+ let omitted = 0;
7161
+ const mapEl = (el) => {
7162
+ const r = crush(el);
7163
+ omitted += r.omitted;
7164
+ return r.value;
7165
+ };
7166
+ if (value.length <= JSON_ARRAY_HEAD + JSON_ARRAY_TAIL) {
7167
+ return { value: value.map(mapEl), omitted };
7168
+ }
7169
+ const dropped = value.length - JSON_ARRAY_HEAD - JSON_ARRAY_TAIL;
7170
+ omitted += dropped;
7171
+ const out = [
7172
+ ...value.slice(0, JSON_ARRAY_HEAD).map(mapEl),
7173
+ `\u2026 ${dropped} element(s) omitted \u2014 mink retrieve \u2026`,
7174
+ ...value.slice(value.length - JSON_ARRAY_TAIL).map(mapEl)
7175
+ ];
7176
+ return { value: out, omitted };
7177
+ }
7178
+ if (value && typeof value === "object") {
7179
+ let omitted = 0;
7180
+ const out = {};
7181
+ for (const [k, v] of Object.entries(value)) {
7182
+ const r = crush(v);
7183
+ omitted += r.omitted;
7184
+ out[k] = r.value;
7185
+ }
7186
+ return { value: out, omitted };
7187
+ }
7188
+ return { value, omitted: 0 };
7189
+ }
7190
+ function compressJson(content) {
7191
+ let parsed;
7192
+ try {
7193
+ parsed = JSON.parse(content);
7194
+ } catch {
7195
+ return null;
7196
+ }
7197
+ const { value, omitted } = crush(parsed);
7198
+ if (omitted === 0)
7199
+ return null;
7200
+ return {
7201
+ compressed: JSON.stringify(value, null, 2),
7202
+ omittedNote: `${omitted} array element(s) sampled out`
7203
+ };
7204
+ }
7205
+ function compressText(content) {
7206
+ const lines = toLines(content);
7207
+ if (lines.length <= TEXT_HEAD + TEXT_TAIL)
7208
+ return null;
7209
+ const omitted = lines.length - TEXT_HEAD - TEXT_TAIL;
7210
+ const head = lines.slice(0, TEXT_HEAD);
7211
+ const tail = lines.slice(lines.length - TEXT_TAIL);
7212
+ return {
7213
+ compressed: [...head, omittedMarker(omitted), ...tail].join(`
7214
+ `),
7215
+ omittedNote: `${omitted} of ${lines.length} line(s) omitted (middle)`
7216
+ };
7217
+ }
7218
+ function detectContentKind(toolName, content, filePath) {
7219
+ const t = toolName.toLowerCase();
7220
+ if (t === "read")
7221
+ return "file";
7222
+ if (t === "grep" || t === "glob")
7223
+ return "search";
7224
+ if (t === "bash")
7225
+ return "log";
7226
+ const head = content.trimStart()[0];
7227
+ if (head === "{" || head === "[") {
7228
+ try {
7229
+ JSON.parse(content);
7230
+ return "json";
7231
+ } catch {}
7232
+ }
7233
+ if (filePath)
7234
+ return "file";
7235
+ return "text";
7236
+ }
7237
+ function compressOutput(toolName, content, filePath) {
7238
+ const kind = detectContentKind(toolName, content, filePath);
7239
+ let result;
7240
+ switch (kind) {
7241
+ case "search":
7242
+ result = compressSearch(content);
7243
+ break;
7244
+ case "log":
7245
+ result = compressLog(content);
7246
+ break;
7247
+ case "file":
7248
+ result = compressFile(filePath ?? "file", content);
7249
+ break;
7250
+ case "json":
7251
+ result = compressJson(content);
7252
+ break;
7253
+ case "text":
7254
+ result = compressText(content);
7255
+ break;
7256
+ }
7257
+ if (!result)
7258
+ return null;
7259
+ return { kind, compressed: result.compressed, omittedNote: result.omittedNote };
7260
+ }
7261
+ var SEARCH_MAX_PER_FILE = 5, LOG_HEAD = 40, LOG_TAIL = 40, TEXT_HEAD = 30, TEXT_TAIL = 20, JSON_ARRAY_HEAD = 20, JSON_ARRAY_TAIL = 5, ANSI;
7262
+ var init_output_compression = __esm(() => {
7263
+ init_code_skeleton();
7264
+ ANSI = /\u001B\[[0-9;?]*[ -/]*[@-~]/g;
7265
+ });
7266
+
7267
+ // src/repositories/compression-cache-repo.ts
7268
+ import { randomUUID as randomUUID3 } from "crypto";
7269
+
7270
+ class CompressionCacheRepo {
7271
+ db;
7272
+ constructor(db) {
7273
+ this.db = db;
7274
+ }
7275
+ static for(cwd) {
7276
+ return new CompressionCacheRepo(openProjectDb(cwd));
7277
+ }
7278
+ static newToken() {
7279
+ return `mc-${randomUUID3().slice(0, 8)}`;
7280
+ }
7281
+ store(input, deviceId = getOrCreateDeviceId()) {
7282
+ const token = input.token ?? CompressionCacheRepo.newToken();
7283
+ const now = input.now ?? new Date;
7284
+ const createdAt = now.toISOString();
7285
+ const expiresAt = new Date(now.getTime() + Math.max(0, input.retentionHours) * 3600000).toISOString();
7286
+ this.db.prepare(`
7287
+ INSERT OR REPLACE INTO compression_cache
7288
+ (token, created_at, expires_at, tool_name, content_kind,
7289
+ content, size_bytes, device_id)
7290
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
7291
+ `).run(token, createdAt, expiresAt, input.toolName, input.contentKind, input.content, Buffer.byteLength(input.content, "utf-8"), deviceId);
7292
+ return token;
7293
+ }
7294
+ get(token, now = new Date) {
7295
+ const row = this.db.prepare("SELECT * FROM compression_cache WHERE token = ?").get(token);
7296
+ if (!row)
7297
+ return null;
7298
+ const expiresAt = String(row.expires_at);
7299
+ if (expiresAt <= now.toISOString()) {
7300
+ try {
7301
+ this.db.prepare("DELETE FROM compression_cache WHERE token = ?").run(token);
7302
+ } catch {}
7303
+ return null;
7304
+ }
7305
+ return {
7306
+ token: String(row.token),
7307
+ createdAt: String(row.created_at),
7308
+ expiresAt,
7309
+ toolName: String(row.tool_name),
7310
+ contentKind: String(row.content_kind),
7311
+ content: String(row.content),
7312
+ sizeBytes: Number(row.size_bytes)
7313
+ };
7314
+ }
7315
+ evictExpired(now = new Date) {
7316
+ const r = this.db.prepare("DELETE FROM compression_cache WHERE expires_at <= ?").run(now.toISOString());
7317
+ return Number(r.changes);
7318
+ }
7319
+ count() {
7320
+ const row = this.db.prepare("SELECT COUNT(*) AS n FROM compression_cache").get();
7321
+ return Number(row.n);
7322
+ }
7323
+ }
7324
+ var init_compression_cache_repo = __esm(() => {
7325
+ init_db();
7326
+ init_device();
7327
+ });
7328
+
7329
+ // src/core/compress-tool-output.ts
7330
+ function contentKey(s) {
7331
+ let h = 2166136261;
7332
+ for (let i = 0;i < s.length; i++) {
7333
+ h ^= s.charCodeAt(i);
7334
+ h = Math.imul(h, 16777619);
7335
+ }
7336
+ return (h >>> 0).toString(16);
7337
+ }
7338
+ function render(result, token) {
7339
+ return result.compressed + `
7340
+
7341
+ ` + `\u2014 mink: compressed ${result.kind} output (${result.omittedNote}). ` + `Full original: mink retrieve ${token}`;
7342
+ }
7343
+ function safeRecord(cwd, toolName, contentKind, originalTokens, compressedTokens, holdout) {
7344
+ try {
7345
+ TokenLedgerRepo.for(cwd).recordCompression({
7346
+ toolName,
7347
+ contentKind,
7348
+ originalTokens,
7349
+ compressedTokens,
7350
+ holdout
7351
+ });
7352
+ } catch {}
7353
+ }
7354
+ function compressToolOutput(cwd, toolName, output, filePath) {
7355
+ let cfg;
7356
+ try {
7357
+ cfg = loadCompressionConfig();
7358
+ } catch {
7359
+ return null;
7360
+ }
7361
+ if (!cfg.enabled)
7362
+ return null;
7363
+ if (typeof output !== "string" || output.length === 0)
7364
+ return null;
7365
+ const originalTokens = countTokens(output);
7366
+ if (!isEligible(originalTokens, cfg))
7367
+ return null;
7368
+ const eventKey = contentKey(output);
7369
+ if (selectHoldout(eventKey, cfg.holdoutFraction)) {
7370
+ const kind = detectContentKind(toolName, output, filePath);
7371
+ safeRecord(cwd, toolName, kind, originalTokens, originalTokens, true);
7372
+ return null;
7373
+ }
7374
+ const result = compressOutput(toolName, output, filePath);
7375
+ if (!result)
7376
+ return null;
7377
+ const token = CompressionCacheRepo.newToken();
7378
+ const replacement = render(result, token);
7379
+ const compressedTokens = countTokens(replacement);
7380
+ if (!meetsMinSavings(originalTokens, compressedTokens, cfg))
7381
+ return null;
7382
+ try {
7383
+ CompressionCacheRepo.for(cwd).store({
7384
+ toolName,
7385
+ contentKind: result.kind,
7386
+ content: output,
7387
+ retentionHours: cfg.retentionHours,
7388
+ token
7389
+ });
7390
+ } catch {
7391
+ return null;
7392
+ }
7393
+ safeRecord(cwd, toolName, result.kind, originalTokens, compressedTokens, false);
7394
+ return { updatedToolOutput: replacement, token };
7395
+ }
7396
+ var init_compress_tool_output = __esm(() => {
7397
+ init_compression();
7398
+ init_token_estimate();
7399
+ init_output_compression();
7400
+ init_compression_cache_repo();
7401
+ init_token_ledger_repo();
7402
+ });
7403
+
7404
+ // src/core/hook-output.ts
7405
+ function extractToolOutputText(input) {
7406
+ const tr = input.tool_response;
7407
+ if (tr) {
7408
+ if (typeof tr.content === "string")
7409
+ return tr.content;
7410
+ if (Array.isArray(tr.content)) {
7411
+ const parts = tr.content.map((p) => p && typeof p.text === "string" ? p.text : "").filter((s) => s.length > 0);
7412
+ if (parts.length > 0)
7413
+ return parts.join("");
7414
+ }
7415
+ if (typeof tr.stdout === "string" && tr.stdout.length > 0)
7416
+ return tr.stdout;
7417
+ if (typeof tr.text === "string")
7418
+ return tr.text;
7419
+ const file = tr.file;
7420
+ if (file && typeof file.content === "string")
7421
+ return file.content;
7422
+ }
7423
+ const to = input.tool_output;
7424
+ if (to && typeof to.content === "string")
7425
+ return to.content;
7426
+ return null;
7427
+ }
7428
+ function emitUpdatedToolOutput(text) {
7429
+ process.stdout.write(JSON.stringify({
7430
+ hookSpecificOutput: {
7431
+ hookEventName: "PostToolUse",
7432
+ updatedToolOutput: text
7433
+ }
7434
+ }));
7435
+ }
7436
+
6785
7437
  // src/commands/post-read.ts
6786
7438
  var exports_post_read = {};
6787
7439
  __export(exports_post_read, {
@@ -6911,6 +7563,14 @@ async function postRead(cwd) {
6911
7563
  logWriter.appendReadEntry(new Date().toISOString(), filePath, result.indexHit, result.estimatedTokens);
6912
7564
  } catch {}
6913
7565
  atomicWriteJson(sessionPath(cwd), state);
7566
+ const isRanged = input.tool_input.offset != null || input.tool_input.limit != null;
7567
+ if (!isRanged && content && content.length > 0) {
7568
+ try {
7569
+ const outcome = compressToolOutput(cwd, "Read", content, filePath);
7570
+ if (outcome)
7571
+ emitUpdatedToolOutput(outcome.updatedToolOutput);
7572
+ } catch {}
7573
+ }
6914
7574
  } catch {} finally {
6915
7575
  clearTimeout(timer);
6916
7576
  }
@@ -6924,6 +7584,43 @@ var init_post_read = __esm(() => {
6924
7584
  init_description();
6925
7585
  init_action_log();
6926
7586
  init_device();
7587
+ init_compress_tool_output();
7588
+ });
7589
+
7590
+ // src/commands/post-tool.ts
7591
+ var exports_post_tool = {};
7592
+ __export(exports_post_tool, {
7593
+ postTool: () => postTool
7594
+ });
7595
+ function isPostToolUseInput2(value) {
7596
+ if (value === null || typeof value !== "object")
7597
+ return false;
7598
+ const obj = value;
7599
+ return typeof obj.tool_name === "string";
7600
+ }
7601
+ function isCompressibleTool(toolName) {
7602
+ return toolName === "Bash" || toolName === "Grep" || toolName === "Glob" || toolName.startsWith("mcp__");
7603
+ }
7604
+ async function postTool(cwd) {
7605
+ const timer = setTimeout(() => process.exit(0), 5000);
7606
+ try {
7607
+ const input = await readStdinJson();
7608
+ if (!isPostToolUseInput2(input))
7609
+ return;
7610
+ if (!isCompressibleTool(input.tool_name))
7611
+ return;
7612
+ const output = extractToolOutputText(input);
7613
+ if (!output)
7614
+ return;
7615
+ const outcome = compressToolOutput(cwd, input.tool_name, output);
7616
+ if (outcome)
7617
+ emitUpdatedToolOutput(outcome.updatedToolOutput);
7618
+ } catch {} finally {
7619
+ clearTimeout(timer);
7620
+ }
7621
+ }
7622
+ var init_post_tool = __esm(() => {
7623
+ init_compress_tool_output();
6927
7624
  });
6928
7625
 
6929
7626
  // src/core/pattern-engine.ts
@@ -7162,7 +7859,7 @@ function analyzePostWrite(filePath, fileContent, index) {
7162
7859
  indexEntry
7163
7860
  };
7164
7861
  }
7165
- function isPostToolUseInput2(value) {
7862
+ function isPostToolUseInput3(value) {
7166
7863
  if (value === null || typeof value !== "object")
7167
7864
  return false;
7168
7865
  const obj = value;
@@ -7176,7 +7873,7 @@ async function postWrite(cwd) {
7176
7873
  const timer = setTimeout(() => process.exit(0), 1e4);
7177
7874
  try {
7178
7875
  const input = await readStdinJson();
7179
- if (!isPostToolUseInput2(input))
7876
+ if (!isPostToolUseInput3(input))
7180
7877
  return;
7181
7878
  if (input.tool_name !== "Write" && input.tool_name !== "Edit")
7182
7879
  return;
@@ -7405,6 +8102,35 @@ var init_detect_waste = __esm(() => {
7405
8102
  init_device();
7406
8103
  });
7407
8104
 
8105
+ // src/commands/retrieve.ts
8106
+ var exports_retrieve = {};
8107
+ __export(exports_retrieve, {
8108
+ retrieve: () => retrieve
8109
+ });
8110
+ function retrieve(cwd, args) {
8111
+ const token = args[0];
8112
+ if (!token) {
8113
+ process.stderr.write(`[mink] usage: mink retrieve <token>
8114
+ `);
8115
+ return;
8116
+ }
8117
+ let entry = null;
8118
+ try {
8119
+ entry = CompressionCacheRepo.for(cwd).get(token);
8120
+ } catch {
8121
+ entry = null;
8122
+ }
8123
+ if (!entry) {
8124
+ process.stderr.write(`[mink] no retrievable output for token "${token}" (unknown or expired)
8125
+ `);
8126
+ return;
8127
+ }
8128
+ process.stdout.write(entry.content);
8129
+ }
8130
+ var init_retrieve = __esm(() => {
8131
+ init_compression_cache_repo();
8132
+ });
8133
+
7408
8134
  // src/core/cron-parser.ts
7409
8135
  function parseField(field, min, max) {
7410
8136
  const values = new Set;
@@ -73756,7 +74482,7 @@ var require_dist10 = __commonJS((exports) => {
73756
74482
  exports.PacProxyAgent = undefined;
73757
74483
  var net = __importStar(__require("net"));
73758
74484
  var tls = __importStar(__require("tls"));
73759
- var crypto = __importStar(__require("crypto"));
74485
+ var crypto2 = __importStar(__require("crypto"));
73760
74486
  var events_1 = __require("events");
73761
74487
  var debug_1 = __importDefault(require_src());
73762
74488
  var url_1 = __require("url");
@@ -73806,7 +74532,7 @@ var require_dist10 = __commonJS((exports) => {
73806
74532
  (0, quickjs_emscripten_1.getQuickJS)(),
73807
74533
  this.loadPacFile()
73808
74534
  ]);
73809
- const hash = crypto.createHash("sha1").update(code).digest("hex");
74535
+ const hash = crypto2.createHash("sha1").update(code).digest("hex");
73810
74536
  if (this.resolver && this.resolverHash === hash) {
73811
74537
  debug2("Same sha1 hash for code - contents have not changed, reusing previous proxy resolver");
73812
74538
  return this.resolver;
@@ -81077,12 +81803,12 @@ var init_lib = __esm(() => {
81077
81803
  });
81078
81804
 
81079
81805
  // node_modules/cliui/build/lib/string-utils.js
81080
- function stripAnsi(str) {
81806
+ function stripAnsi2(str) {
81081
81807
  return str.replace(ansi, "");
81082
81808
  }
81083
81809
  function wrap(str, width) {
81084
81810
  const [start, end] = str.match(ansi) || ["", ""];
81085
- str = stripAnsi(str);
81811
+ str = stripAnsi2(str);
81086
81812
  let wrapped = "";
81087
81813
  for (let i = 0;i < str.length; i++) {
81088
81814
  if (i !== 0 && i % width === 0) {
@@ -81107,7 +81833,7 @@ function ui(opts) {
81107
81833
  stringWidth: (str) => {
81108
81834
  return [...str].length;
81109
81835
  },
81110
- stripAnsi,
81836
+ stripAnsi: stripAnsi2,
81111
81837
  wrap
81112
81838
  });
81113
81839
  }
@@ -90528,6 +91254,11 @@ switch (command2) {
90528
91254
  await postRead2(cwd);
90529
91255
  break;
90530
91256
  }
91257
+ case "post-tool": {
91258
+ const { postTool: postTool2 } = await Promise.resolve().then(() => (init_post_tool(), exports_post_tool));
91259
+ await postTool2(cwd);
91260
+ break;
91261
+ }
90531
91262
  case "pre-write": {
90532
91263
  const { preWrite: preWrite2 } = await Promise.resolve().then(() => (init_pre_write(), exports_pre_write));
90533
91264
  await preWrite2(cwd);
@@ -90543,6 +91274,11 @@ switch (command2) {
90543
91274
  detectWaste2(cwd);
90544
91275
  break;
90545
91276
  }
91277
+ case "retrieve": {
91278
+ const { retrieve: retrieve2 } = await Promise.resolve().then(() => (init_retrieve(), exports_retrieve));
91279
+ retrieve2(cwd, process.argv.slice(3));
91280
+ break;
91281
+ }
90546
91282
  case "cron": {
90547
91283
  const { cron: cron2 } = await Promise.resolve().then(() => (init_cron(), exports_cron));
90548
91284
  await cron2(cwd, process.argv.slice(3));
@@ -90712,6 +91448,7 @@ switch (command2) {
90712
91448
  console.log(" restore [backup] Restore state from a backup");
90713
91449
  console.log(" bug search <term> Search the bug log");
90714
91450
  console.log(" detect-waste Detect and flag wasteful patterns");
91451
+ console.log(" retrieve <token> Return a compressed tool output's original (spec 21)");
90715
91452
  console.log(" reflect Generate learning memory reflections");
90716
91453
  console.log(" designqc [target] Capture design screenshots (spec 13)");
90717
91454
  console.log(" framework-advisor Generate framework advisor knowledge file (spec 14)");
@@ -90721,6 +91458,7 @@ switch (command2) {
90721
91458
  console.log(" session-stop Finalize session and log data");
90722
91459
  console.log(" pre-read / post-read File read hooks");
90723
91460
  console.log(" pre-write / post-write File write hooks");
91461
+ console.log(" post-tool Tool-output compression hook (Bash/Grep/MCP, spec 21)");
90724
91462
  break;
90725
91463
  default:
90726
91464
  console.error(`[mink] unknown command: ${command2 ?? "(none)"}`);