@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.node.js CHANGED
@@ -422,6 +422,41 @@ var init_config = __esm(() => {
422
422
  envVar: "MINK_PROJECTS_IDENTITY",
423
423
  description: "Project identity strategy: path-derived (legacy) or git-remote (stable across machines)",
424
424
  scope: "shared"
425
+ },
426
+ {
427
+ key: "compression.enabled",
428
+ default: "false",
429
+ envVar: "MINK_COMPRESSION_ENABLED",
430
+ description: "Enable tool-output compression (spec 21). Off until inline compression ships.",
431
+ scope: "shared"
432
+ },
433
+ {
434
+ key: "compression.threshold-tokens",
435
+ default: "800",
436
+ envVar: "MINK_COMPRESSION_THRESHOLD_TOKENS",
437
+ description: "Minimum estimated token size before a tool output is eligible for compression",
438
+ scope: "shared"
439
+ },
440
+ {
441
+ key: "compression.min-savings-ratio",
442
+ default: "0.25",
443
+ envVar: "MINK_COMPRESSION_MIN_SAVINGS_RATIO",
444
+ description: "Discard a compression attempt unless it saves at least this fraction of tokens",
445
+ scope: "shared"
446
+ },
447
+ {
448
+ key: "compression.holdout-fraction",
449
+ default: "0.1",
450
+ envVar: "MINK_COMPRESSION_HOLDOUT_FRACTION",
451
+ description: "Fraction of eligible outputs left uncompressed as a measured control group",
452
+ scope: "shared"
453
+ },
454
+ {
455
+ key: "compression.retention-hours",
456
+ default: "168",
457
+ envVar: "MINK_COMPRESSION_RETENTION_HOURS",
458
+ description: "How long compressed originals stay retrievable before eviction",
459
+ scope: "shared"
425
460
  }
426
461
  ];
427
462
  VALID_KEYS = new Set(CONFIG_KEYS.map((k) => k.key));
@@ -3096,7 +3131,7 @@ function readMeta(db, key) {
3096
3131
  function writeMeta(db, key, value) {
3097
3132
  db.prepare("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, value);
3098
3133
  }
3099
- var SCHEMA_VERSION = 1, INITIAL_SCHEMA = `
3134
+ var SCHEMA_VERSION = 3, INITIAL_SCHEMA = `
3100
3135
  CREATE TABLE IF NOT EXISTS meta (
3101
3136
  key TEXT PRIMARY KEY,
3102
3137
  value TEXT NOT NULL
@@ -3258,6 +3293,55 @@ CREATE TABLE IF NOT EXISTS counters (
3258
3293
  file_index_hits INTEGER NOT NULL DEFAULT 0,
3259
3294
  file_index_misses INTEGER NOT NULL DEFAULT 0
3260
3295
  );
3296
+
3297
+ -- Tool-output compression measurement (spec 21). One row per compression
3298
+ -- decision: either a compressed arm (compressed_tokens < original_tokens) or a
3299
+ -- holdout arm (left uncompressed for control, compressed_tokens = original_tokens).
3300
+ -- These are append-only telemetry, independent of session lifecycle, written at
3301
+ -- the moment a tool output is processed. New table → applied to existing DBs via
3302
+ -- IF NOT EXISTS on the next open.
3303
+ CREATE TABLE IF NOT EXISTS ledger_compressions (
3304
+ id TEXT PRIMARY KEY,
3305
+ created_at TEXT NOT NULL,
3306
+ tool_name TEXT NOT NULL,
3307
+ content_kind TEXT NOT NULL,
3308
+ original_tokens INTEGER NOT NULL DEFAULT 0,
3309
+ compressed_tokens INTEGER NOT NULL DEFAULT 0,
3310
+ holdout INTEGER NOT NULL DEFAULT 0,
3311
+ device_id TEXT NOT NULL
3312
+ );
3313
+ CREATE INDEX IF NOT EXISTS idx_ledger_compressions_created ON ledger_compressions(created_at);
3314
+ CREATE INDEX IF NOT EXISTS idx_ledger_compressions_device ON ledger_compressions(device_id);
3315
+
3316
+ -- Per-device compression aggregates, summed across devices like ledger_lifetime.
3317
+ -- measured_savings only credits compressed arms (holdout arms save nothing by
3318
+ -- construction), so the reported figure is a true measured delta, not an estimate.
3319
+ CREATE TABLE IF NOT EXISTS ledger_compression_lifetime (
3320
+ device_id TEXT PRIMARY KEY,
3321
+ total_events INTEGER NOT NULL DEFAULT 0,
3322
+ total_holdout_events INTEGER NOT NULL DEFAULT 0,
3323
+ total_original_tokens INTEGER NOT NULL DEFAULT 0,
3324
+ total_compressed_tokens INTEGER NOT NULL DEFAULT 0,
3325
+ total_measured_savings INTEGER NOT NULL DEFAULT 0
3326
+ );
3327
+
3328
+ -- Reversible-compression cache (spec 21 §Reversibility). When a tool output is
3329
+ -- compressed, the original is stored here keyed by a short retrieval token and
3330
+ -- embedded in the compressed result; "mink retrieve <token>" returns it
3331
+ -- byte-exact. Rows expire after the configured retention window; an expired or
3332
+ -- unknown token is a graceful miss. This is a local cache, not synced state, so
3333
+ -- (unlike other tables) it carries no merge semantics beyond device_id for audit.
3334
+ CREATE TABLE IF NOT EXISTS compression_cache (
3335
+ token TEXT PRIMARY KEY,
3336
+ created_at TEXT NOT NULL,
3337
+ expires_at TEXT NOT NULL,
3338
+ tool_name TEXT NOT NULL,
3339
+ content_kind TEXT NOT NULL,
3340
+ content TEXT NOT NULL,
3341
+ size_bytes INTEGER NOT NULL,
3342
+ device_id TEXT NOT NULL
3343
+ );
3344
+ CREATE INDEX IF NOT EXISTS idx_compression_cache_expires ON compression_cache(expires_at);
3261
3345
  `;
3262
3346
 
3263
3347
  // src/storage/migrate-json.ts
@@ -3675,6 +3759,68 @@ class TokenLedgerRepo {
3675
3759
  }
3676
3760
  });
3677
3761
  }
3762
+ recordCompression(event, deviceId = getOrCreateDeviceId()) {
3763
+ const id = event.id ?? crypto.randomUUID();
3764
+ const createdAt = event.createdAt ?? new Date().toISOString();
3765
+ const holdout = event.holdout ? 1 : 0;
3766
+ const savings = event.holdout ? 0 : Math.max(0, event.originalTokens - event.compressedTokens);
3767
+ this.db.transaction(() => {
3768
+ this.db.prepare(`
3769
+ INSERT OR REPLACE INTO ledger_compressions
3770
+ (id, created_at, tool_name, content_kind,
3771
+ original_tokens, compressed_tokens, holdout, device_id)
3772
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
3773
+ `).run(id, createdAt, event.toolName, event.contentKind, event.originalTokens, event.compressedTokens, holdout, deviceId);
3774
+ this.db.prepare(`
3775
+ INSERT INTO ledger_compression_lifetime
3776
+ (device_id, total_events, total_holdout_events,
3777
+ total_original_tokens, total_compressed_tokens, total_measured_savings)
3778
+ VALUES (?, ?, ?, ?, ?, ?)
3779
+ ON CONFLICT(device_id) DO UPDATE SET
3780
+ total_events = ledger_compression_lifetime.total_events + excluded.total_events,
3781
+ total_holdout_events = ledger_compression_lifetime.total_holdout_events + excluded.total_holdout_events,
3782
+ total_original_tokens = ledger_compression_lifetime.total_original_tokens + excluded.total_original_tokens,
3783
+ total_compressed_tokens = ledger_compression_lifetime.total_compressed_tokens + excluded.total_compressed_tokens,
3784
+ total_measured_savings = ledger_compression_lifetime.total_measured_savings + excluded.total_measured_savings
3785
+ `).run(deviceId, 1, holdout, event.originalTokens, event.compressedTokens, savings);
3786
+ });
3787
+ }
3788
+ compressionLifetime() {
3789
+ const row = this.db.prepare(`
3790
+ SELECT
3791
+ COALESCE(SUM(total_events), 0) AS totalEvents,
3792
+ COALESCE(SUM(total_holdout_events), 0) AS totalHoldoutEvents,
3793
+ COALESCE(SUM(total_original_tokens), 0) AS totalOriginalTokens,
3794
+ COALESCE(SUM(total_compressed_tokens), 0) AS totalCompressedTokens,
3795
+ COALESCE(SUM(total_measured_savings), 0) AS totalMeasuredSavings
3796
+ FROM ledger_compression_lifetime
3797
+ `).get();
3798
+ return {
3799
+ totalEvents: Number(row?.totalEvents ?? 0),
3800
+ totalHoldoutEvents: Number(row?.totalHoldoutEvents ?? 0),
3801
+ totalOriginalTokens: Number(row?.totalOriginalTokens ?? 0),
3802
+ totalCompressedTokens: Number(row?.totalCompressedTokens ?? 0),
3803
+ totalMeasuredSavings: Number(row?.totalMeasuredSavings ?? 0)
3804
+ };
3805
+ }
3806
+ compressionEvents(limit = 100) {
3807
+ const rows = this.db.prepare(`
3808
+ SELECT id, created_at, tool_name, content_kind,
3809
+ original_tokens, compressed_tokens, holdout
3810
+ FROM ledger_compressions
3811
+ ORDER BY created_at DESC
3812
+ LIMIT ?
3813
+ `).all(limit);
3814
+ return rows.map((r) => ({
3815
+ id: String(r.id),
3816
+ createdAt: String(r.created_at),
3817
+ toolName: String(r.tool_name),
3818
+ contentKind: String(r.content_kind),
3819
+ originalTokens: Number(r.original_tokens),
3820
+ compressedTokens: Number(r.compressed_tokens),
3821
+ holdout: Number(r.holdout) === 1
3822
+ }));
3823
+ }
3678
3824
  insertSessionRow(summary, deviceId, archived) {
3679
3825
  this.db.prepare(`
3680
3826
  INSERT OR REPLACE INTO ledger_sessions
@@ -4449,6 +4595,28 @@ function estimateTokens2(content, filePath) {
4449
4595
  }
4450
4596
  return Math.ceil(content.length / ratio);
4451
4597
  }
4598
+ function countTokens(text) {
4599
+ if (!text)
4600
+ return 0;
4601
+ const segments = text.match(/[A-Za-z]+|[0-9]+|[^A-Za-z0-9]/g);
4602
+ if (!segments)
4603
+ return 0;
4604
+ let tokens = 0;
4605
+ for (const seg of segments) {
4606
+ const first = seg.charCodeAt(0);
4607
+ if (first >= 65 && first <= 90 || first >= 97 && first <= 122) {
4608
+ tokens += Math.ceil(seg.length / 4);
4609
+ } else if (first >= 48 && first <= 57) {
4610
+ tokens += Math.ceil(seg.length / 3);
4611
+ } else if (seg === `
4612
+ `) {
4613
+ tokens += 1;
4614
+ } else if (seg === " " || seg === "\t" || seg === "\r") {} else {
4615
+ tokens += 1;
4616
+ }
4617
+ }
4618
+ return tokens;
4619
+ }
4452
4620
  var CODE_EXTENSIONS, PROSE_EXTENSIONS, BINARY_EXTENSIONS;
4453
4621
  var init_token_estimate = __esm(() => {
4454
4622
  CODE_EXTENSIONS = new Set([
@@ -5813,12 +5981,14 @@ function buildHooksConfig(cliPath) {
5813
5981
  PostToolUse: [
5814
5982
  { matcher: "Read", hooks: hook(`${prefix} post-read`) },
5815
5983
  { matcher: "Edit", hooks: hook(`${prefix} post-write`) },
5816
- { matcher: "Write", hooks: hook(`${prefix} post-write`) }
5984
+ { matcher: "Write", hooks: hook(`${prefix} post-write`) },
5985
+ { matcher: "Bash", hooks: hook(`${prefix} post-tool`) },
5986
+ { matcher: "Grep", hooks: hook(`${prefix} post-tool`) }
5817
5987
  ]
5818
5988
  };
5819
5989
  }
5820
5990
  function isMinkCommand(cmd) {
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");
5991
+ 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");
5822
5992
  if (!hasMinkSubcommand)
5823
5993
  return false;
5824
5994
  if (/(^|\/|\s)mink\s/.test(cmd))
@@ -7121,6 +7291,488 @@ var init_pre_read = __esm(() => {
7121
7291
  init_counters_repo();
7122
7292
  });
7123
7293
 
7294
+ // src/core/compression.ts
7295
+ function numberValue(key, fallback, min, max) {
7296
+ const raw = resolveConfigValue(key).value;
7297
+ const n = Number(raw);
7298
+ if (!Number.isFinite(n))
7299
+ return fallback;
7300
+ return Math.min(max, Math.max(min, n));
7301
+ }
7302
+ function loadCompressionConfig() {
7303
+ return {
7304
+ enabled: resolveConfigValue("compression.enabled").value === "true",
7305
+ thresholdTokens: numberValue("compression.threshold-tokens", 800, 0, Number.MAX_SAFE_INTEGER),
7306
+ minSavingsRatio: numberValue("compression.min-savings-ratio", 0.25, 0, 1),
7307
+ holdoutFraction: numberValue("compression.holdout-fraction", 0.1, 0, 1),
7308
+ retentionHours: numberValue("compression.retention-hours", 168, 0, Number.MAX_SAFE_INTEGER)
7309
+ };
7310
+ }
7311
+ function isEligible(originalTokens, config) {
7312
+ return config.enabled && originalTokens >= config.thresholdTokens;
7313
+ }
7314
+ function meetsMinSavings(originalTokens, compressedTokens, config) {
7315
+ if (originalTokens <= 0)
7316
+ return false;
7317
+ const ratio = (originalTokens - compressedTokens) / originalTokens;
7318
+ return ratio >= config.minSavingsRatio;
7319
+ }
7320
+ function hashUnitInterval(key) {
7321
+ let h = 2166136261;
7322
+ for (let i = 0;i < key.length; i++) {
7323
+ h ^= key.charCodeAt(i);
7324
+ h = Math.imul(h, 16777619);
7325
+ }
7326
+ return (h >>> 0) / 4294967296;
7327
+ }
7328
+ function selectHoldout(eventKey, fraction) {
7329
+ if (fraction <= 0)
7330
+ return false;
7331
+ if (fraction >= 1)
7332
+ return true;
7333
+ return hashUnitInterval(eventKey) < fraction;
7334
+ }
7335
+ var init_compression = __esm(() => {
7336
+ init_global_config();
7337
+ });
7338
+
7339
+ // src/core/code-skeleton.ts
7340
+ function countChar(s, c) {
7341
+ let n = 0;
7342
+ for (let i = 0;i < s.length; i++)
7343
+ if (s[i] === c)
7344
+ n++;
7345
+ return n;
7346
+ }
7347
+ function netBraces(line) {
7348
+ let s = line.replace(/\/\/.*$/, "");
7349
+ s = s.replace(/\/\*.*?\*\//g, "");
7350
+ s = s.replace(/"(?:\\.|[^"\\])*"/g, '""');
7351
+ s = s.replace(/'(?:\\.|[^'\\])*'/g, "''");
7352
+ s = s.replace(/`(?:\\.|[^`\\])*`/g, "``");
7353
+ return countChar(s, "{") - countChar(s, "}");
7354
+ }
7355
+ function stripOpenBrace(sig) {
7356
+ return sig.replace(/\{\s*$/, "").trimEnd();
7357
+ }
7358
+ function extractCodeSkeleton(content, opts = {}) {
7359
+ const rawLines = content.split(`
7360
+ `);
7361
+ const totalLines = rawLines.length > 0 && rawLines[rawLines.length - 1] === "" ? rawLines.length - 1 : rawLines.length;
7362
+ const out = [];
7363
+ let depth = 0;
7364
+ let suppress = Infinity;
7365
+ for (const line of rawLines) {
7366
+ if (out.length >= MAX_SIGNATURES)
7367
+ break;
7368
+ const start = depth;
7369
+ const net = netBraces(line);
7370
+ if (start < suppress) {
7371
+ const isHeading = opts.markdown === true && HEADING.test(line);
7372
+ const captured = isHeading || DECL_ALWAYS.test(line) || DECL_EXPORTED_VAR.test(line) || start >= 1 && MEMBER.test(line);
7373
+ if (captured) {
7374
+ const sig = line.trim();
7375
+ if (net > 0) {
7376
+ if (DESCEND.test(line) && !isHeading) {
7377
+ out.push(INDENT.repeat(start) + stripOpenBrace(sig) + " {");
7378
+ } else {
7379
+ out.push(INDENT.repeat(start) + stripOpenBrace(sig) + " { … }");
7380
+ suppress = start + 1;
7381
+ }
7382
+ } else {
7383
+ out.push(INDENT.repeat(start) + sig);
7384
+ }
7385
+ }
7386
+ }
7387
+ depth = Math.max(0, depth + net);
7388
+ if (depth < suppress)
7389
+ suppress = Infinity;
7390
+ }
7391
+ if (out.length === 0)
7392
+ return null;
7393
+ return { lines: out, totalLines };
7394
+ }
7395
+ var MAX_SIGNATURES = 80, INDENT = " ", DECL_ALWAYS, DECL_EXPORTED_VAR, MEMBER, HEADING, DESCEND;
7396
+ var init_code_skeleton = __esm(() => {
7397
+ 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/;
7398
+ DECL_EXPORTED_VAR = /^\s*export\s+(?:default\s+)?(?:const|let|var)\b/;
7399
+ MEMBER = /^\s*(?:public\s+|private\s+|protected\s+|readonly\s+|static\s+|async\s+|get\s+|set\s+|#)*[\w$]+\??\s*(?:\(|:|=)/;
7400
+ HEADING = /^#{1,6}\s+\S/;
7401
+ DESCEND = /\b(?:class|interface|enum|namespace|module|struct|trait|impl)\b/;
7402
+ });
7403
+
7404
+ // src/core/output-compression.ts
7405
+ function stripAnsi(s) {
7406
+ return s.replace(ANSI, "");
7407
+ }
7408
+ function omittedMarker(n) {
7409
+ return ` … ${n} line${n === 1 ? "" : "s"} omitted — mink retrieve …`;
7410
+ }
7411
+ function toLines(content) {
7412
+ const lines = content.split(`
7413
+ `);
7414
+ if (lines.length > 0 && lines[lines.length - 1] === "")
7415
+ lines.pop();
7416
+ return lines;
7417
+ }
7418
+ function compressLog(content) {
7419
+ const lines = toLines(stripAnsi(content));
7420
+ const collapsed = [];
7421
+ let i = 0;
7422
+ while (i < lines.length) {
7423
+ let run = 1;
7424
+ while (i + run < lines.length && lines[i + run] === lines[i])
7425
+ run++;
7426
+ collapsed.push(run > 1 ? `${lines[i]} (×${run})` : lines[i]);
7427
+ i += run;
7428
+ }
7429
+ if (collapsed.length <= LOG_HEAD + LOG_TAIL) {
7430
+ if (collapsed.length === lines.length)
7431
+ return null;
7432
+ return {
7433
+ compressed: collapsed.join(`
7434
+ `),
7435
+ omittedNote: `collapsed ${lines.length - collapsed.length} repeated line(s)`
7436
+ };
7437
+ }
7438
+ const omitted = collapsed.length - LOG_HEAD - LOG_TAIL;
7439
+ const head = collapsed.slice(0, LOG_HEAD);
7440
+ const tail = collapsed.slice(collapsed.length - LOG_TAIL);
7441
+ return {
7442
+ compressed: [...head, omittedMarker(omitted), ...tail].join(`
7443
+ `),
7444
+ omittedNote: `${omitted} of ${collapsed.length} log line(s) omitted (middle)`
7445
+ };
7446
+ }
7447
+ function compressSearch(content) {
7448
+ const lines = toLines(content);
7449
+ const seen = new Set;
7450
+ const perFile = new Map;
7451
+ const omittedByFile = new Map;
7452
+ const out = [];
7453
+ for (const line of lines) {
7454
+ if (seen.has(line))
7455
+ continue;
7456
+ seen.add(line);
7457
+ const colon = line.indexOf(":");
7458
+ const file = colon > 0 ? line.slice(0, colon) : line;
7459
+ const count = perFile.get(file) ?? 0;
7460
+ if (count < SEARCH_MAX_PER_FILE) {
7461
+ perFile.set(file, count + 1);
7462
+ out.push(line);
7463
+ } else {
7464
+ omittedByFile.set(file, (omittedByFile.get(file) ?? 0) + 1);
7465
+ }
7466
+ }
7467
+ let totalOmitted = 0;
7468
+ for (const [file, n] of omittedByFile) {
7469
+ totalOmitted += n;
7470
+ out.push(` … +${n} more match(es) in ${file} — mink retrieve …`);
7471
+ }
7472
+ const dedupRemoved = lines.length - seen.size;
7473
+ if (totalOmitted === 0 && dedupRemoved === 0)
7474
+ return null;
7475
+ const notes = [];
7476
+ if (totalOmitted > 0)
7477
+ notes.push(`${totalOmitted} match(es) capped`);
7478
+ if (dedupRemoved > 0)
7479
+ notes.push(`${dedupRemoved} duplicate(s) removed`);
7480
+ return { compressed: out.join(`
7481
+ `), omittedNote: notes.join("; ") };
7482
+ }
7483
+ function compressFile(filePath, content) {
7484
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
7485
+ const markdown = ext === ".md" || ext === ".mdx" || ext === ".markdown";
7486
+ const skeleton = extractCodeSkeleton(content, { markdown });
7487
+ if (!skeleton) {
7488
+ return compressText(content);
7489
+ }
7490
+ const header = `${filePath} — structural summary ` + `(${skeleton.lines.length} signature(s) of ${skeleton.totalLines} lines)`;
7491
+ return {
7492
+ compressed: [header, ...skeleton.lines].join(`
7493
+ `),
7494
+ omittedNote: `bodies elided; ${skeleton.totalLines} lines available via mink retrieve`
7495
+ };
7496
+ }
7497
+ function crush(value) {
7498
+ if (Array.isArray(value)) {
7499
+ let omitted = 0;
7500
+ const mapEl = (el) => {
7501
+ const r = crush(el);
7502
+ omitted += r.omitted;
7503
+ return r.value;
7504
+ };
7505
+ if (value.length <= JSON_ARRAY_HEAD + JSON_ARRAY_TAIL) {
7506
+ return { value: value.map(mapEl), omitted };
7507
+ }
7508
+ const dropped = value.length - JSON_ARRAY_HEAD - JSON_ARRAY_TAIL;
7509
+ omitted += dropped;
7510
+ const out = [
7511
+ ...value.slice(0, JSON_ARRAY_HEAD).map(mapEl),
7512
+ `… ${dropped} element(s) omitted — mink retrieve …`,
7513
+ ...value.slice(value.length - JSON_ARRAY_TAIL).map(mapEl)
7514
+ ];
7515
+ return { value: out, omitted };
7516
+ }
7517
+ if (value && typeof value === "object") {
7518
+ let omitted = 0;
7519
+ const out = {};
7520
+ for (const [k, v] of Object.entries(value)) {
7521
+ const r = crush(v);
7522
+ omitted += r.omitted;
7523
+ out[k] = r.value;
7524
+ }
7525
+ return { value: out, omitted };
7526
+ }
7527
+ return { value, omitted: 0 };
7528
+ }
7529
+ function compressJson(content) {
7530
+ let parsed;
7531
+ try {
7532
+ parsed = JSON.parse(content);
7533
+ } catch {
7534
+ return null;
7535
+ }
7536
+ const { value, omitted } = crush(parsed);
7537
+ if (omitted === 0)
7538
+ return null;
7539
+ return {
7540
+ compressed: JSON.stringify(value, null, 2),
7541
+ omittedNote: `${omitted} array element(s) sampled out`
7542
+ };
7543
+ }
7544
+ function compressText(content) {
7545
+ const lines = toLines(content);
7546
+ if (lines.length <= TEXT_HEAD + TEXT_TAIL)
7547
+ return null;
7548
+ const omitted = lines.length - TEXT_HEAD - TEXT_TAIL;
7549
+ const head = lines.slice(0, TEXT_HEAD);
7550
+ const tail = lines.slice(lines.length - TEXT_TAIL);
7551
+ return {
7552
+ compressed: [...head, omittedMarker(omitted), ...tail].join(`
7553
+ `),
7554
+ omittedNote: `${omitted} of ${lines.length} line(s) omitted (middle)`
7555
+ };
7556
+ }
7557
+ function detectContentKind(toolName, content, filePath) {
7558
+ const t = toolName.toLowerCase();
7559
+ if (t === "read")
7560
+ return "file";
7561
+ if (t === "grep" || t === "glob")
7562
+ return "search";
7563
+ if (t === "bash")
7564
+ return "log";
7565
+ const head = content.trimStart()[0];
7566
+ if (head === "{" || head === "[") {
7567
+ try {
7568
+ JSON.parse(content);
7569
+ return "json";
7570
+ } catch {}
7571
+ }
7572
+ if (filePath)
7573
+ return "file";
7574
+ return "text";
7575
+ }
7576
+ function compressOutput(toolName, content, filePath) {
7577
+ const kind = detectContentKind(toolName, content, filePath);
7578
+ let result;
7579
+ switch (kind) {
7580
+ case "search":
7581
+ result = compressSearch(content);
7582
+ break;
7583
+ case "log":
7584
+ result = compressLog(content);
7585
+ break;
7586
+ case "file":
7587
+ result = compressFile(filePath ?? "file", content);
7588
+ break;
7589
+ case "json":
7590
+ result = compressJson(content);
7591
+ break;
7592
+ case "text":
7593
+ result = compressText(content);
7594
+ break;
7595
+ }
7596
+ if (!result)
7597
+ return null;
7598
+ return { kind, compressed: result.compressed, omittedNote: result.omittedNote };
7599
+ }
7600
+ 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;
7601
+ var init_output_compression = __esm(() => {
7602
+ init_code_skeleton();
7603
+ ANSI = /\[[0-9;?]*[ -/]*[@-~]/g;
7604
+ });
7605
+
7606
+ // src/repositories/compression-cache-repo.ts
7607
+ import { randomUUID as randomUUID3 } from "crypto";
7608
+
7609
+ class CompressionCacheRepo {
7610
+ db;
7611
+ constructor(db) {
7612
+ this.db = db;
7613
+ }
7614
+ static for(cwd) {
7615
+ return new CompressionCacheRepo(openProjectDb(cwd));
7616
+ }
7617
+ static newToken() {
7618
+ return `mc-${randomUUID3().slice(0, 8)}`;
7619
+ }
7620
+ store(input, deviceId = getOrCreateDeviceId()) {
7621
+ const token = input.token ?? CompressionCacheRepo.newToken();
7622
+ const now = input.now ?? new Date;
7623
+ const createdAt = now.toISOString();
7624
+ const expiresAt = new Date(now.getTime() + Math.max(0, input.retentionHours) * 3600000).toISOString();
7625
+ this.db.prepare(`
7626
+ INSERT OR REPLACE INTO compression_cache
7627
+ (token, created_at, expires_at, tool_name, content_kind,
7628
+ content, size_bytes, device_id)
7629
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
7630
+ `).run(token, createdAt, expiresAt, input.toolName, input.contentKind, input.content, Buffer.byteLength(input.content, "utf-8"), deviceId);
7631
+ return token;
7632
+ }
7633
+ get(token, now = new Date) {
7634
+ const row = this.db.prepare("SELECT * FROM compression_cache WHERE token = ?").get(token);
7635
+ if (!row)
7636
+ return null;
7637
+ const expiresAt = String(row.expires_at);
7638
+ if (expiresAt <= now.toISOString()) {
7639
+ try {
7640
+ this.db.prepare("DELETE FROM compression_cache WHERE token = ?").run(token);
7641
+ } catch {}
7642
+ return null;
7643
+ }
7644
+ return {
7645
+ token: String(row.token),
7646
+ createdAt: String(row.created_at),
7647
+ expiresAt,
7648
+ toolName: String(row.tool_name),
7649
+ contentKind: String(row.content_kind),
7650
+ content: String(row.content),
7651
+ sizeBytes: Number(row.size_bytes)
7652
+ };
7653
+ }
7654
+ evictExpired(now = new Date) {
7655
+ const r = this.db.prepare("DELETE FROM compression_cache WHERE expires_at <= ?").run(now.toISOString());
7656
+ return Number(r.changes);
7657
+ }
7658
+ count() {
7659
+ const row = this.db.prepare("SELECT COUNT(*) AS n FROM compression_cache").get();
7660
+ return Number(row.n);
7661
+ }
7662
+ }
7663
+ var init_compression_cache_repo = __esm(() => {
7664
+ init_db();
7665
+ init_device();
7666
+ });
7667
+
7668
+ // src/core/compress-tool-output.ts
7669
+ function contentKey(s) {
7670
+ let h = 2166136261;
7671
+ for (let i = 0;i < s.length; i++) {
7672
+ h ^= s.charCodeAt(i);
7673
+ h = Math.imul(h, 16777619);
7674
+ }
7675
+ return (h >>> 0).toString(16);
7676
+ }
7677
+ function render(result, token) {
7678
+ return result.compressed + `
7679
+
7680
+ ` + `— mink: compressed ${result.kind} output (${result.omittedNote}). ` + `Full original: mink retrieve ${token}`;
7681
+ }
7682
+ function safeRecord(cwd, toolName, contentKind, originalTokens, compressedTokens, holdout) {
7683
+ try {
7684
+ TokenLedgerRepo.for(cwd).recordCompression({
7685
+ toolName,
7686
+ contentKind,
7687
+ originalTokens,
7688
+ compressedTokens,
7689
+ holdout
7690
+ });
7691
+ } catch {}
7692
+ }
7693
+ function compressToolOutput(cwd, toolName, output, filePath) {
7694
+ let cfg;
7695
+ try {
7696
+ cfg = loadCompressionConfig();
7697
+ } catch {
7698
+ return null;
7699
+ }
7700
+ if (!cfg.enabled)
7701
+ return null;
7702
+ if (typeof output !== "string" || output.length === 0)
7703
+ return null;
7704
+ const originalTokens = countTokens(output);
7705
+ if (!isEligible(originalTokens, cfg))
7706
+ return null;
7707
+ const eventKey = contentKey(output);
7708
+ if (selectHoldout(eventKey, cfg.holdoutFraction)) {
7709
+ const kind = detectContentKind(toolName, output, filePath);
7710
+ safeRecord(cwd, toolName, kind, originalTokens, originalTokens, true);
7711
+ return null;
7712
+ }
7713
+ const result = compressOutput(toolName, output, filePath);
7714
+ if (!result)
7715
+ return null;
7716
+ const token = CompressionCacheRepo.newToken();
7717
+ const replacement = render(result, token);
7718
+ const compressedTokens = countTokens(replacement);
7719
+ if (!meetsMinSavings(originalTokens, compressedTokens, cfg))
7720
+ return null;
7721
+ try {
7722
+ CompressionCacheRepo.for(cwd).store({
7723
+ toolName,
7724
+ contentKind: result.kind,
7725
+ content: output,
7726
+ retentionHours: cfg.retentionHours,
7727
+ token
7728
+ });
7729
+ } catch {
7730
+ return null;
7731
+ }
7732
+ safeRecord(cwd, toolName, result.kind, originalTokens, compressedTokens, false);
7733
+ return { updatedToolOutput: replacement, token };
7734
+ }
7735
+ var init_compress_tool_output = __esm(() => {
7736
+ init_compression();
7737
+ init_token_estimate();
7738
+ init_output_compression();
7739
+ init_compression_cache_repo();
7740
+ init_token_ledger_repo();
7741
+ });
7742
+
7743
+ // src/core/hook-output.ts
7744
+ function extractToolOutputText(input) {
7745
+ const tr = input.tool_response;
7746
+ if (tr) {
7747
+ if (typeof tr.content === "string")
7748
+ return tr.content;
7749
+ if (Array.isArray(tr.content)) {
7750
+ const parts = tr.content.map((p) => p && typeof p.text === "string" ? p.text : "").filter((s) => s.length > 0);
7751
+ if (parts.length > 0)
7752
+ return parts.join("");
7753
+ }
7754
+ if (typeof tr.stdout === "string" && tr.stdout.length > 0)
7755
+ return tr.stdout;
7756
+ if (typeof tr.text === "string")
7757
+ return tr.text;
7758
+ const file = tr.file;
7759
+ if (file && typeof file.content === "string")
7760
+ return file.content;
7761
+ }
7762
+ const to = input.tool_output;
7763
+ if (to && typeof to.content === "string")
7764
+ return to.content;
7765
+ return null;
7766
+ }
7767
+ function emitUpdatedToolOutput(text) {
7768
+ process.stdout.write(JSON.stringify({
7769
+ hookSpecificOutput: {
7770
+ hookEventName: "PostToolUse",
7771
+ updatedToolOutput: text
7772
+ }
7773
+ }));
7774
+ }
7775
+
7124
7776
  // src/commands/post-read.ts
7125
7777
  var exports_post_read = {};
7126
7778
  __export(exports_post_read, {
@@ -7250,6 +7902,14 @@ async function postRead(cwd) {
7250
7902
  logWriter.appendReadEntry(new Date().toISOString(), filePath, result.indexHit, result.estimatedTokens);
7251
7903
  } catch {}
7252
7904
  atomicWriteJson(sessionPath(cwd), state);
7905
+ const isRanged = input.tool_input.offset != null || input.tool_input.limit != null;
7906
+ if (!isRanged && content && content.length > 0) {
7907
+ try {
7908
+ const outcome = compressToolOutput(cwd, "Read", content, filePath);
7909
+ if (outcome)
7910
+ emitUpdatedToolOutput(outcome.updatedToolOutput);
7911
+ } catch {}
7912
+ }
7253
7913
  } catch {} finally {
7254
7914
  clearTimeout(timer);
7255
7915
  }
@@ -7263,6 +7923,43 @@ var init_post_read = __esm(() => {
7263
7923
  init_description();
7264
7924
  init_action_log();
7265
7925
  init_device();
7926
+ init_compress_tool_output();
7927
+ });
7928
+
7929
+ // src/commands/post-tool.ts
7930
+ var exports_post_tool = {};
7931
+ __export(exports_post_tool, {
7932
+ postTool: () => postTool
7933
+ });
7934
+ function isPostToolUseInput2(value) {
7935
+ if (value === null || typeof value !== "object")
7936
+ return false;
7937
+ const obj = value;
7938
+ return typeof obj.tool_name === "string";
7939
+ }
7940
+ function isCompressibleTool(toolName) {
7941
+ return toolName === "Bash" || toolName === "Grep" || toolName === "Glob" || toolName.startsWith("mcp__");
7942
+ }
7943
+ async function postTool(cwd) {
7944
+ const timer = setTimeout(() => process.exit(0), 5000);
7945
+ try {
7946
+ const input = await readStdinJson();
7947
+ if (!isPostToolUseInput2(input))
7948
+ return;
7949
+ if (!isCompressibleTool(input.tool_name))
7950
+ return;
7951
+ const output = extractToolOutputText(input);
7952
+ if (!output)
7953
+ return;
7954
+ const outcome = compressToolOutput(cwd, input.tool_name, output);
7955
+ if (outcome)
7956
+ emitUpdatedToolOutput(outcome.updatedToolOutput);
7957
+ } catch {} finally {
7958
+ clearTimeout(timer);
7959
+ }
7960
+ }
7961
+ var init_post_tool = __esm(() => {
7962
+ init_compress_tool_output();
7266
7963
  });
7267
7964
 
7268
7965
  // src/core/pattern-engine.ts
@@ -7501,7 +8198,7 @@ function analyzePostWrite(filePath, fileContent, index) {
7501
8198
  indexEntry
7502
8199
  };
7503
8200
  }
7504
- function isPostToolUseInput2(value) {
8201
+ function isPostToolUseInput3(value) {
7505
8202
  if (value === null || typeof value !== "object")
7506
8203
  return false;
7507
8204
  const obj = value;
@@ -7515,7 +8212,7 @@ async function postWrite(cwd) {
7515
8212
  const timer = setTimeout(() => process.exit(0), 1e4);
7516
8213
  try {
7517
8214
  const input = await readStdinJson();
7518
- if (!isPostToolUseInput2(input))
8215
+ if (!isPostToolUseInput3(input))
7519
8216
  return;
7520
8217
  if (input.tool_name !== "Write" && input.tool_name !== "Edit")
7521
8218
  return;
@@ -7744,6 +8441,35 @@ var init_detect_waste = __esm(() => {
7744
8441
  init_device();
7745
8442
  });
7746
8443
 
8444
+ // src/commands/retrieve.ts
8445
+ var exports_retrieve = {};
8446
+ __export(exports_retrieve, {
8447
+ retrieve: () => retrieve
8448
+ });
8449
+ function retrieve(cwd, args) {
8450
+ const token = args[0];
8451
+ if (!token) {
8452
+ process.stderr.write(`[mink] usage: mink retrieve <token>
8453
+ `);
8454
+ return;
8455
+ }
8456
+ let entry = null;
8457
+ try {
8458
+ entry = CompressionCacheRepo.for(cwd).get(token);
8459
+ } catch {
8460
+ entry = null;
8461
+ }
8462
+ if (!entry) {
8463
+ process.stderr.write(`[mink] no retrievable output for token "${token}" (unknown or expired)
8464
+ `);
8465
+ return;
8466
+ }
8467
+ process.stdout.write(entry.content);
8468
+ }
8469
+ var init_retrieve = __esm(() => {
8470
+ init_compression_cache_repo();
8471
+ });
8472
+
7747
8473
  // src/core/cron-parser.ts
7748
8474
  function parseField(field, min, max) {
7749
8475
  const values = new Set;
@@ -10654,12 +11380,14 @@ function buildHooksConfig2(cliPath) {
10654
11380
  PostToolUse: [
10655
11381
  { matcher: "Read", hooks: hook(`${prefix} post-read`) },
10656
11382
  { matcher: "Edit", hooks: hook(`${prefix} post-write`) },
10657
- { matcher: "Write", hooks: hook(`${prefix} post-write`) }
11383
+ { matcher: "Write", hooks: hook(`${prefix} post-write`) },
11384
+ { matcher: "Bash", hooks: hook(`${prefix} post-tool`) },
11385
+ { matcher: "Grep", hooks: hook(`${prefix} post-tool`) }
10658
11386
  ]
10659
11387
  };
10660
11388
  }
10661
11389
  function isMinkCommand2(cmd) {
10662
- 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");
11390
+ 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");
10663
11391
  if (!hasMinkSubcommand)
10664
11392
  return false;
10665
11393
  if (/(^|\/|\s)mink\s/.test(cmd))
@@ -77088,7 +77816,7 @@ var require_dist10 = __commonJS((exports) => {
77088
77816
  exports.PacProxyAgent = undefined;
77089
77817
  var net = __importStar(__require("net"));
77090
77818
  var tls = __importStar(__require("tls"));
77091
- var crypto = __importStar(__require("crypto"));
77819
+ var crypto2 = __importStar(__require("crypto"));
77092
77820
  var events_1 = __require("events");
77093
77821
  var debug_1 = __importDefault(require_src());
77094
77822
  var url_1 = __require("url");
@@ -77138,7 +77866,7 @@ var require_dist10 = __commonJS((exports) => {
77138
77866
  (0, quickjs_emscripten_1.getQuickJS)(),
77139
77867
  this.loadPacFile()
77140
77868
  ]);
77141
- const hash = crypto.createHash("sha1").update(code).digest("hex");
77869
+ const hash = crypto2.createHash("sha1").update(code).digest("hex");
77142
77870
  if (this.resolver && this.resolverHash === hash) {
77143
77871
  debug2("Same sha1 hash for code - contents have not changed, reusing previous proxy resolver");
77144
77872
  return this.resolver;
@@ -84409,12 +85137,12 @@ var init_lib = __esm(() => {
84409
85137
  });
84410
85138
 
84411
85139
  // node_modules/cliui/build/lib/string-utils.js
84412
- function stripAnsi(str) {
85140
+ function stripAnsi2(str) {
84413
85141
  return str.replace(ansi, "");
84414
85142
  }
84415
85143
  function wrap(str, width) {
84416
85144
  const [start, end] = str.match(ansi) || ["", ""];
84417
- str = stripAnsi(str);
85145
+ str = stripAnsi2(str);
84418
85146
  let wrapped = "";
84419
85147
  for (let i = 0;i < str.length; i++) {
84420
85148
  if (i !== 0 && i % width === 0) {
@@ -84439,7 +85167,7 @@ function ui(opts) {
84439
85167
  stringWidth: (str) => {
84440
85168
  return [...str].length;
84441
85169
  },
84442
- stripAnsi,
85170
+ stripAnsi: stripAnsi2,
84443
85171
  wrap
84444
85172
  });
84445
85173
  }
@@ -93860,6 +94588,11 @@ switch (command2) {
93860
94588
  await postRead2(cwd);
93861
94589
  break;
93862
94590
  }
94591
+ case "post-tool": {
94592
+ const { postTool: postTool2 } = await Promise.resolve().then(() => (init_post_tool(), exports_post_tool));
94593
+ await postTool2(cwd);
94594
+ break;
94595
+ }
93863
94596
  case "pre-write": {
93864
94597
  const { preWrite: preWrite2 } = await Promise.resolve().then(() => (init_pre_write(), exports_pre_write));
93865
94598
  await preWrite2(cwd);
@@ -93875,6 +94608,11 @@ switch (command2) {
93875
94608
  detectWaste3(cwd);
93876
94609
  break;
93877
94610
  }
94611
+ case "retrieve": {
94612
+ const { retrieve: retrieve2 } = await Promise.resolve().then(() => (init_retrieve(), exports_retrieve));
94613
+ retrieve2(cwd, process.argv.slice(3));
94614
+ break;
94615
+ }
93878
94616
  case "cron": {
93879
94617
  const { cron: cron2 } = await Promise.resolve().then(() => (init_cron(), exports_cron));
93880
94618
  await cron2(cwd, process.argv.slice(3));
@@ -94044,6 +94782,7 @@ switch (command2) {
94044
94782
  console.log(" restore [backup] Restore state from a backup");
94045
94783
  console.log(" bug search <term> Search the bug log");
94046
94784
  console.log(" detect-waste Detect and flag wasteful patterns");
94785
+ console.log(" retrieve <token> Return a compressed tool output's original (spec 21)");
94047
94786
  console.log(" reflect Generate learning memory reflections");
94048
94787
  console.log(" designqc [target] Capture design screenshots (spec 13)");
94049
94788
  console.log(" framework-advisor Generate framework advisor knowledge file (spec 14)");
@@ -94053,6 +94792,7 @@ switch (command2) {
94053
94792
  console.log(" session-stop Finalize session and log data");
94054
94793
  console.log(" pre-read / post-read File read hooks");
94055
94794
  console.log(" pre-write / post-write File write hooks");
94795
+ console.log(" post-tool Tool-output compression hook (Bash/Grep/MCP, spec 21)");
94056
94796
  break;
94057
94797
  default:
94058
94798
  console.error(`[mink] unknown command: ${command2 ?? "(none)"}`);