@drewpayment/mink 0.13.0-beta.1 → 0.13.0-beta.3
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 +4 -20
- package/dashboard/out/404.html +1 -1
- package/dashboard/out/_next/static/U9AeObddt4LmJkKRZpEfy/_buildManifest.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/activity/page-c285fb9f63d9a82a.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/bugs/page-f3ba7d8f50a96568.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/capture/page-e004bec9af99a244.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/compression/page-21e1af119b3f81ff.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/config/page-d47fb6f588ccfd4b.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/daemon/page-52f913e751416717.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/design/page-53a76719b9af5830.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/discord/page-04502d12c4a96cf7.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/file-index/page-a1bd10e04bb219d9.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/insights/page-7367274963571b6b.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/learning/{page-b766adc79099adb4.js → page-4a03cf7b9a6106fd.js} +1 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/overview/page-38b8430b5c56e807.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/scheduler/page-510b78c9b0a61012.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/sync/page-b7215c2a29a7d7a7.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/tokens/page-1be7ed35a5c9bd39.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/waste/page-24a726e6d63f771a.js +1 -0
- package/dashboard/out/_next/static/chunks/app/(panels)/wiki/page-230d2d1cae6507a8.js +1 -0
- package/dashboard/out/_next/static/chunks/app/layout-70a6d18f8e464960.js +1 -0
- package/dashboard/out/action-log.html +1 -1
- package/dashboard/out/action-log.txt +4 -4
- package/dashboard/out/activity.html +1 -1
- package/dashboard/out/activity.txt +5 -5
- package/dashboard/out/bugs.html +1 -1
- package/dashboard/out/bugs.txt +5 -5
- package/dashboard/out/capture.html +1 -1
- package/dashboard/out/capture.txt +5 -5
- package/dashboard/out/compression.html +1 -0
- package/dashboard/out/compression.txt +24 -0
- package/dashboard/out/config.html +1 -1
- package/dashboard/out/config.txt +5 -5
- package/dashboard/out/daemon.html +1 -1
- package/dashboard/out/daemon.txt +5 -5
- package/dashboard/out/design.html +1 -1
- package/dashboard/out/design.txt +5 -5
- package/dashboard/out/discord.html +1 -1
- package/dashboard/out/discord.txt +5 -5
- package/dashboard/out/file-index.html +1 -1
- package/dashboard/out/file-index.txt +5 -5
- package/dashboard/out/index.html +1 -1
- package/dashboard/out/index.txt +4 -4
- package/dashboard/out/insights.html +1 -1
- package/dashboard/out/insights.txt +5 -5
- package/dashboard/out/learning.html +1 -1
- package/dashboard/out/learning.txt +5 -5
- package/dashboard/out/overview.html +1 -1
- package/dashboard/out/overview.txt +5 -5
- package/dashboard/out/scheduler.html +1 -1
- package/dashboard/out/scheduler.txt +5 -5
- package/dashboard/out/sync.html +1 -1
- package/dashboard/out/sync.txt +5 -5
- package/dashboard/out/tokens.html +1 -1
- package/dashboard/out/tokens.txt +5 -5
- package/dashboard/out/waste.html +1 -1
- package/dashboard/out/waste.txt +5 -5
- package/dashboard/out/wiki.html +1 -1
- package/dashboard/out/wiki.txt +5 -5
- package/dist/cli.bun.js +1300 -908
- package/dist/cli.node.js +1319 -928
- package/package.json +1 -1
- package/src/cli.ts +17 -20
- package/src/commands/init.ts +14 -123
- package/src/commands/post-read.ts +18 -0
- package/src/commands/post-tool.ts +48 -0
- package/src/commands/retrieve.ts +32 -0
- package/src/commands/status.ts +13 -1
- package/src/core/code-skeleton.ts +108 -0
- package/src/core/compress-tool-output.ts +127 -0
- package/src/core/compression.ts +81 -0
- package/src/core/dashboard-api.ts +20 -1
- package/src/core/dashboard-server.ts +3 -0
- package/src/core/hook-output.ts +42 -0
- package/src/core/output-compression.ts +252 -0
- package/src/core/token-estimate.ts +40 -0
- package/src/repositories/compression-cache-repo.ts +97 -0
- package/src/repositories/token-ledger-repo.ts +142 -0
- package/src/storage/schema.ts +50 -1
- package/src/types/compression.ts +29 -0
- package/src/types/config.ts +40 -0
- package/src/types/dashboard.ts +22 -1
- package/src/types/hook-input.ts +4 -0
- package/src/types/token-ledger.ts +55 -0
- package/dashboard/out/_next/static/UWfkbJY4zr9fSt7O-CAge/_buildManifest.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/activity/page-096a97ba539d5323.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/bugs/page-449d31c133432458.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/capture/page-c6617aa0a8a7333e.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/config/page-aa0a0623b3fdd0d8.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/daemon/page-7cd3fac2f5d87a0d.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/design/page-5304675c96b6793b.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/discord/page-9940dde80ba2a69e.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/file-index/page-ecd8a753614e981e.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/insights/page-7909d8beb8d8ef7a.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/overview/page-7a9e86dcde67d6a9.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/scheduler/page-a88f93204c9742a1.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/sync/page-8a9ad4c36aa6cb65.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/tokens/page-8dac7d50d4db2756.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/waste/page-bcf56144faf7d133.js +0 -1
- package/dashboard/out/_next/static/chunks/app/(panels)/wiki/page-a32fdbd0bf58b30b.js +0 -1
- package/dashboard/out/_next/static/chunks/app/layout-782cd26e0ccc4514.js +0 -1
- package/src/core/agent-detect.ts +0 -88
- package/src/core/agent-pi.ts +0 -314
- package/src/core/prompt.ts +0 -27
- /package/dashboard/out/_next/static/{UWfkbJY4zr9fSt7O-CAge → U9AeObddt4LmJkKRZpEfy}/_ssgManifest.js +0 -0
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -16,24 +16,8 @@ switch (command) {
|
|
|
16
16
|
break;
|
|
17
17
|
|
|
18
18
|
case "init": {
|
|
19
|
-
const { init
|
|
20
|
-
|
|
21
|
-
const agentFlagIndex = args.findIndex(
|
|
22
|
-
(a) => a === "--agent" || a.startsWith("--agent=")
|
|
23
|
-
);
|
|
24
|
-
let agentValue: string | undefined;
|
|
25
|
-
if (agentFlagIndex !== -1) {
|
|
26
|
-
const a = args[agentFlagIndex];
|
|
27
|
-
agentValue = a.includes("=") ? a.split("=").slice(1).join("=") : args[agentFlagIndex + 1];
|
|
28
|
-
}
|
|
29
|
-
const yes = args.includes("--yes") || args.includes("-y");
|
|
30
|
-
const targets = agentValue ? resolveTargetsFromFlag(agentValue) : undefined;
|
|
31
|
-
if (agentValue && (!targets || targets.length === 0)) {
|
|
32
|
-
console.error(`[mink] unknown --agent value: ${agentValue}`);
|
|
33
|
-
console.error(" Valid: claude, pi, all (or a comma-separated list)");
|
|
34
|
-
process.exit(1);
|
|
35
|
-
}
|
|
36
|
-
await init(cwd, { targets, interactive: !yes });
|
|
19
|
+
const { init } = await import("./commands/init");
|
|
20
|
+
await init(cwd);
|
|
37
21
|
break;
|
|
38
22
|
}
|
|
39
23
|
|
|
@@ -69,6 +53,12 @@ switch (command) {
|
|
|
69
53
|
break;
|
|
70
54
|
}
|
|
71
55
|
|
|
56
|
+
case "post-tool": {
|
|
57
|
+
const { postTool } = await import("./commands/post-tool");
|
|
58
|
+
await postTool(cwd);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
|
|
72
62
|
case "pre-write": {
|
|
73
63
|
const { preWrite } = await import("./commands/pre-write");
|
|
74
64
|
await preWrite(cwd);
|
|
@@ -87,6 +77,12 @@ switch (command) {
|
|
|
87
77
|
break;
|
|
88
78
|
}
|
|
89
79
|
|
|
80
|
+
case "retrieve": {
|
|
81
|
+
const { retrieve } = await import("./commands/retrieve");
|
|
82
|
+
retrieve(cwd, process.argv.slice(3));
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
|
|
90
86
|
case "cron": {
|
|
91
87
|
const { cron } = await import("./commands/cron");
|
|
92
88
|
await cron(cwd, process.argv.slice(3));
|
|
@@ -237,8 +233,7 @@ switch (command) {
|
|
|
237
233
|
console.log("Usage: mink <command> [options]");
|
|
238
234
|
console.log();
|
|
239
235
|
console.log("Commands:");
|
|
240
|
-
console.log(" init
|
|
241
|
-
console.log(" --agent claude|pi|all (default: detect & prompt)");
|
|
236
|
+
console.log(" init Initialize Mink in the current project");
|
|
242
237
|
console.log(" status Display project health at a glance");
|
|
243
238
|
console.log(" scan [--check] Force a full file index rescan");
|
|
244
239
|
console.log(" config [key] [value] Manage global user settings");
|
|
@@ -280,6 +275,7 @@ switch (command) {
|
|
|
280
275
|
console.log(" restore [backup] Restore state from a backup");
|
|
281
276
|
console.log(" bug search <term> Search the bug log");
|
|
282
277
|
console.log(" detect-waste Detect and flag wasteful patterns");
|
|
278
|
+
console.log(" retrieve <token> Return a compressed tool output's original (spec 21)");
|
|
283
279
|
console.log(" reflect Generate learning memory reflections");
|
|
284
280
|
console.log(" designqc [target] Capture design screenshots (spec 13)");
|
|
285
281
|
console.log(" framework-advisor Generate framework advisor knowledge file (spec 14)");
|
|
@@ -289,6 +285,7 @@ switch (command) {
|
|
|
289
285
|
console.log(" session-stop Finalize session and log data");
|
|
290
286
|
console.log(" pre-read / post-read File read hooks");
|
|
291
287
|
console.log(" pre-write / post-write File write hooks");
|
|
288
|
+
console.log(" post-tool Tool-output compression hook (Bash/Grep/MCP, spec 21)");
|
|
292
289
|
break;
|
|
293
290
|
|
|
294
291
|
default:
|
package/src/commands/init.ts
CHANGED
|
@@ -12,23 +12,6 @@ import {
|
|
|
12
12
|
isInsideVault,
|
|
13
13
|
vaultProjects,
|
|
14
14
|
} from "../core/vault";
|
|
15
|
-
import {
|
|
16
|
-
type AgentId,
|
|
17
|
-
AGENTS,
|
|
18
|
-
detectAgents,
|
|
19
|
-
resolveTargetsFromFlag,
|
|
20
|
-
} from "../core/agent-detect";
|
|
21
|
-
import { installPi } from "../core/agent-pi";
|
|
22
|
-
import { ask, stdinIsInteractive } from "../core/prompt";
|
|
23
|
-
|
|
24
|
-
export { resolveTargetsFromFlag };
|
|
25
|
-
|
|
26
|
-
export interface InitOptions {
|
|
27
|
-
/** Explicit set of hosts to wire. When omitted, Mink detects and/or prompts. */
|
|
28
|
-
targets?: AgentId[];
|
|
29
|
-
/** Allow an interactive agent-selection prompt when stdin is a TTY. */
|
|
30
|
-
interactive?: boolean;
|
|
31
|
-
}
|
|
32
15
|
|
|
33
16
|
interface HookCommand {
|
|
34
17
|
type: "command";
|
|
@@ -108,6 +91,9 @@ export function buildHooksConfig(cliPath: string): HooksConfig {
|
|
|
108
91
|
{ matcher: "Read", hooks: hook(`${prefix} post-read`) },
|
|
109
92
|
{ matcher: "Edit", hooks: hook(`${prefix} post-write`) },
|
|
110
93
|
{ matcher: "Write", hooks: hook(`${prefix} post-write`) },
|
|
94
|
+
// Tool-output compression (spec 21) — a no-op until enabled via config.
|
|
95
|
+
{ matcher: "Bash", hooks: hook(`${prefix} post-tool`) },
|
|
96
|
+
{ matcher: "Grep", hooks: hook(`${prefix} post-tool`) },
|
|
111
97
|
],
|
|
112
98
|
};
|
|
113
99
|
}
|
|
@@ -119,7 +105,8 @@ function isMinkCommand(cmd: string): boolean {
|
|
|
119
105
|
cmd.includes("pre-read") ||
|
|
120
106
|
cmd.includes("post-read") ||
|
|
121
107
|
cmd.includes("pre-write") ||
|
|
122
|
-
cmd.includes("post-write")
|
|
108
|
+
cmd.includes("post-write") ||
|
|
109
|
+
cmd.includes("post-tool");
|
|
123
110
|
if (!hasMinkSubcommand) return false;
|
|
124
111
|
// Match the new bin-shim format (`mink <subcmd>` or `/abs/path/to/mink <subcmd>`)
|
|
125
112
|
// as well as legacy formats (`bun run .../cli.js ...`, `node .../cli.js ...`,
|
|
@@ -185,80 +172,17 @@ export function mergeHooksIntoSettings(
|
|
|
185
172
|
atomicWriteJson(settingsPath, existing);
|
|
186
173
|
}
|
|
187
174
|
|
|
188
|
-
/** Wire Mink into Claude Code: settings.json hooks + the project rule file. */
|
|
189
|
-
export function installClaude(
|
|
190
|
-
cwd: string,
|
|
191
|
-
cliPath: string
|
|
192
|
-
): { settingsPath: string; rulePath: string } {
|
|
193
|
-
const settingsPath = resolve(cwd, ".claude", "settings.json");
|
|
194
|
-
mergeHooksIntoSettings(settingsPath, buildHooksConfig(cliPath));
|
|
195
|
-
const rulePath = writeMinkRule(cwd);
|
|
196
|
-
return { settingsPath, rulePath };
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Decide which hosts to wire. Explicit `targets` win; otherwise Mink detects
|
|
201
|
-
* installed hosts and — only when running interactively at a TTY — asks the
|
|
202
|
-
* user to confirm. Non-interactive runs fall back to the detected set, and to
|
|
203
|
-
* Claude Code when nothing is detected (preserving Mink's original behavior).
|
|
204
|
-
*/
|
|
205
|
-
export async function resolveTargets(
|
|
206
|
-
cwd: string,
|
|
207
|
-
opts: InitOptions
|
|
208
|
-
): Promise<AgentId[]> {
|
|
209
|
-
if (opts.targets && opts.targets.length > 0) return opts.targets;
|
|
210
|
-
|
|
211
|
-
const detected = detectAgents(cwd);
|
|
212
|
-
const detectedIds = detected.filter((a) => a.detected).map((a) => a.id);
|
|
213
|
-
|
|
214
|
-
if (opts.interactive && stdinIsInteractive()) {
|
|
215
|
-
return promptForAgents(detected, detectedIds);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return detectedIds.length > 0 ? detectedIds : ["claude"];
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function promptForAgents(
|
|
222
|
-
detected: ReturnType<typeof detectAgents>,
|
|
223
|
-
defaults: AgentId[]
|
|
224
|
-
): Promise<AgentId[]> {
|
|
225
|
-
const fallback: AgentId[] = defaults.length > 0 ? defaults : ["claude"];
|
|
226
|
-
|
|
227
|
-
console.log("Which assistant(s) should Mink work with?");
|
|
228
|
-
detected.forEach((a, i) => {
|
|
229
|
-
const tag = a.detected ? ` (detected — ${a.signals.join(", ")})` : "";
|
|
230
|
-
console.log(` ${i + 1}) ${a.label}${tag}`);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const answer = (
|
|
234
|
-
await ask(
|
|
235
|
-
`Enter numbers (comma-separated), 'a' for all [default: ${fallback.join(", ")}]: `
|
|
236
|
-
)
|
|
237
|
-
)
|
|
238
|
-
.trim()
|
|
239
|
-
.toLowerCase();
|
|
240
|
-
|
|
241
|
-
if (answer === "") return fallback;
|
|
242
|
-
if (answer === "a" || answer === "all") return AGENTS.map((a) => a.id);
|
|
243
|
-
|
|
244
|
-
const picked = answer
|
|
245
|
-
.split(",")
|
|
246
|
-
.map((s) => parseInt(s.trim(), 10))
|
|
247
|
-
.filter((n) => Number.isInteger(n) && n >= 1 && n <= detected.length)
|
|
248
|
-
.map((n) => detected[n - 1].id);
|
|
249
|
-
|
|
250
|
-
return picked.length > 0 ? picked : fallback;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
175
|
function isExistingInstallation(cwd: string): boolean {
|
|
254
176
|
const dir = projectDir(cwd);
|
|
255
177
|
if (!existsSync(dir)) return false;
|
|
256
178
|
return existsSync(join(dir, "file-index.json"));
|
|
257
179
|
}
|
|
258
180
|
|
|
259
|
-
export async function init(cwd: string
|
|
181
|
+
export async function init(cwd: string): Promise<void> {
|
|
260
182
|
const runtime = detectRuntime();
|
|
261
183
|
const cliPath = resolveCliPath();
|
|
184
|
+
const hooks = buildHooksConfig(cliPath);
|
|
185
|
+
const settingsPath = resolve(cwd, ".claude", "settings.json");
|
|
262
186
|
const dir = projectDir(cwd);
|
|
263
187
|
const upgrading = isExistingInstallation(cwd);
|
|
264
188
|
|
|
@@ -269,25 +193,8 @@ export async function init(cwd: string, opts: InitOptions = {}): Promise<void> {
|
|
|
269
193
|
console.log(` backup: ${backupName}`);
|
|
270
194
|
}
|
|
271
195
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// Wire each selected host. Each installer is idempotent — re-running replaces
|
|
275
|
-
// Mink's prior entries rather than duplicating them — and touches only that
|
|
276
|
-
// host's configuration.
|
|
277
|
-
const wired: Record<string, string[]> = {};
|
|
278
|
-
for (const target of targets) {
|
|
279
|
-
if (target === "claude") {
|
|
280
|
-
const { settingsPath, rulePath } = installClaude(cwd, cliPath);
|
|
281
|
-
wired.claude = [`hooks: ${settingsPath}`, `rule: ${rulePath}`];
|
|
282
|
-
} else if (target === "pi") {
|
|
283
|
-
const r = installPi(cwd, cliPath);
|
|
284
|
-
wired.pi = [
|
|
285
|
-
`extension: ${r.extensionPath}`,
|
|
286
|
-
`guidance: ${r.guidancePath}`,
|
|
287
|
-
...(r.notePath ? [`note skill: ${r.notePath}`] : []),
|
|
288
|
-
];
|
|
289
|
-
}
|
|
290
|
-
}
|
|
196
|
+
mergeHooksIntoSettings(settingsPath, hooks);
|
|
197
|
+
const rulePath = writeMinkRule(cwd);
|
|
291
198
|
|
|
292
199
|
mkdirSync(dir, { recursive: true });
|
|
293
200
|
|
|
@@ -310,13 +217,6 @@ export async function init(cwd: string, opts: InitOptions = {}): Promise<void> {
|
|
|
310
217
|
!Array.isArray(existingMeta.pathsByDevice)
|
|
311
218
|
? (existingMeta.pathsByDevice as Record<string, string>)
|
|
312
219
|
: {};
|
|
313
|
-
// Record the set of wired hosts as the authoritative source of truth, unioned
|
|
314
|
-
// with any previously wired host so a single-target re-init never silently
|
|
315
|
-
// unwires the other.
|
|
316
|
-
const priorAgents = Array.isArray(existingMeta?.agents)
|
|
317
|
-
? (existingMeta!.agents as string[])
|
|
318
|
-
: [];
|
|
319
|
-
const agents = Array.from(new Set([...priorAgents, ...targets]));
|
|
320
220
|
atomicWriteJson(metaPath, {
|
|
321
221
|
...(existingMeta ?? {}),
|
|
322
222
|
cwd,
|
|
@@ -324,30 +224,21 @@ export async function init(cwd: string, opts: InitOptions = {}): Promise<void> {
|
|
|
324
224
|
initTimestamp: existingMeta?.initTimestamp ?? new Date().toISOString(),
|
|
325
225
|
version: "0.1.0",
|
|
326
226
|
pathsByDevice: { ...existingPathsByDevice, [deviceId]: cwd },
|
|
327
|
-
agents,
|
|
328
227
|
...(isNotesProject ? { projectType: "notes" } : {}),
|
|
329
228
|
});
|
|
330
229
|
|
|
331
|
-
const printWiring = () => {
|
|
332
|
-
for (const id of Object.keys(wired)) {
|
|
333
|
-
const label = AGENTS.find((a) => a.id === id)?.label ?? id;
|
|
334
|
-
console.log(` ${label}:`);
|
|
335
|
-
for (const line of wired[id]) console.log(` ${line}`);
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
|
|
339
230
|
if (upgrading) {
|
|
340
231
|
console.log(`[mink] upgrade complete`);
|
|
341
232
|
console.log(` project: ${projectId}`);
|
|
342
|
-
console.log(`
|
|
343
|
-
|
|
233
|
+
console.log(` hooks: ${settingsPath}`);
|
|
234
|
+
console.log(` rule: ${rulePath}`);
|
|
344
235
|
} else {
|
|
345
236
|
console.log(`[mink] initialized`);
|
|
346
237
|
console.log(` project: ${projectId} (${identity.source})`);
|
|
347
238
|
console.log(` state: ${dir}`);
|
|
348
239
|
console.log(` runtime: ${runtime}`);
|
|
349
|
-
console.log(`
|
|
350
|
-
|
|
240
|
+
console.log(` hooks: ${settingsPath}`);
|
|
241
|
+
console.log(` rule: ${rulePath}`);
|
|
351
242
|
}
|
|
352
243
|
|
|
353
244
|
// Surface a one-time hint when the project is in a git repo with no remote
|
|
@@ -9,6 +9,8 @@ import { estimateTokens, isBinaryFile } from "../core/token-estimate";
|
|
|
9
9
|
import { extractDescription } from "../core/description";
|
|
10
10
|
import { createActionLogWriter } from "../core/action-log";
|
|
11
11
|
import { getOrCreateDeviceId } from "../core/device";
|
|
12
|
+
import { compressToolOutput } from "../core/compress-tool-output";
|
|
13
|
+
import { emitUpdatedToolOutput } from "../core/hook-output";
|
|
12
14
|
import type { SessionState } from "../types/session";
|
|
13
15
|
import type { FileIndexEntry, IndexLookup } from "../types/file-index";
|
|
14
16
|
import type { PostToolUseInput } from "../types/hook-input";
|
|
@@ -202,6 +204,22 @@ export async function postRead(cwd: string): Promise<void> {
|
|
|
202
204
|
|
|
203
205
|
// Persist state
|
|
204
206
|
atomicWriteJson(sessionPath(cwd), state);
|
|
207
|
+
|
|
208
|
+
// Tool-output compression (spec 21). Substitute a compact, reversible
|
|
209
|
+
// summary for a large whole-file read. Skipped for ranged reads (their
|
|
210
|
+
// output is only a slice) and a no-op unless compression is enabled. Uses
|
|
211
|
+
// the on-disk content as the canonical original so signature extraction
|
|
212
|
+
// works on raw source and `mink retrieve` returns the file itself.
|
|
213
|
+
const isRanged =
|
|
214
|
+
input.tool_input.offset != null || input.tool_input.limit != null;
|
|
215
|
+
if (!isRanged && content && content.length > 0) {
|
|
216
|
+
try {
|
|
217
|
+
const outcome = compressToolOutput(cwd, "Read", content, filePath);
|
|
218
|
+
if (outcome) emitUpdatedToolOutput(outcome.updatedToolOutput);
|
|
219
|
+
} catch {
|
|
220
|
+
// Compression is advisory — never break the read over it.
|
|
221
|
+
}
|
|
222
|
+
}
|
|
205
223
|
} catch {
|
|
206
224
|
// Never crash — exit silently
|
|
207
225
|
} finally {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Generic PostToolUse compression hook (spec 21) for tools that produce large,
|
|
2
|
+
// non-file output — Bash, Grep/Glob, and MCP tools. The Read tool is handled by
|
|
3
|
+
// post-read (which has the on-disk content and ranged-read awareness); this hook
|
|
4
|
+
// compresses the payload text directly.
|
|
5
|
+
//
|
|
6
|
+
// Like every Mink hook: non-blocking, time-boxed, and silent on failure. It is a
|
|
7
|
+
// no-op unless compression is enabled, so wiring it up costs nothing until the
|
|
8
|
+
// user opts in.
|
|
9
|
+
|
|
10
|
+
import { readStdinJson } from "../core/stdin";
|
|
11
|
+
import { extractToolOutputText, emitUpdatedToolOutput } from "../core/hook-output";
|
|
12
|
+
import { compressToolOutput } from "../core/compress-tool-output";
|
|
13
|
+
import type { PostToolUseInput } from "../types/hook-input";
|
|
14
|
+
|
|
15
|
+
function isPostToolUseInput(value: unknown): value is PostToolUseInput {
|
|
16
|
+
if (value === null || typeof value !== "object") return false;
|
|
17
|
+
const obj = value as Record<string, unknown>;
|
|
18
|
+
return typeof obj.tool_name === "string";
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Tools whose output we compress here. Read is excluded (post-read owns it).
|
|
22
|
+
function isCompressibleTool(toolName: string): boolean {
|
|
23
|
+
return (
|
|
24
|
+
toolName === "Bash" ||
|
|
25
|
+
toolName === "Grep" ||
|
|
26
|
+
toolName === "Glob" ||
|
|
27
|
+
toolName.startsWith("mcp__")
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function postTool(cwd: string): Promise<void> {
|
|
32
|
+
const timer = setTimeout(() => process.exit(0), 5000);
|
|
33
|
+
try {
|
|
34
|
+
const input = await readStdinJson();
|
|
35
|
+
if (!isPostToolUseInput(input)) return;
|
|
36
|
+
if (!isCompressibleTool(input.tool_name)) return;
|
|
37
|
+
|
|
38
|
+
const output = extractToolOutputText(input);
|
|
39
|
+
if (!output) return;
|
|
40
|
+
|
|
41
|
+
const outcome = compressToolOutput(cwd, input.tool_name, output);
|
|
42
|
+
if (outcome) emitUpdatedToolOutput(outcome.updatedToolOutput);
|
|
43
|
+
} catch {
|
|
44
|
+
// Never crash — exit silently.
|
|
45
|
+
} finally {
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// `mink retrieve <token>` — return the byte-exact original of a previously
|
|
2
|
+
// compressed tool output (spec 21 §Reversibility). Prints the original to
|
|
3
|
+
// stdout on a hit; on a miss (unknown or expired token) it prints a short,
|
|
4
|
+
// non-fatal notice to stderr and exits 0 so the assistant is never stranded by
|
|
5
|
+
// an error.
|
|
6
|
+
|
|
7
|
+
import { CompressionCacheRepo } from "../repositories/compression-cache-repo";
|
|
8
|
+
|
|
9
|
+
export function retrieve(cwd: string, args: string[]): void {
|
|
10
|
+
const token = args[0];
|
|
11
|
+
if (!token) {
|
|
12
|
+
process.stderr.write("[mink] usage: mink retrieve <token>\n");
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let entry = null;
|
|
17
|
+
try {
|
|
18
|
+
entry = CompressionCacheRepo.for(cwd).get(token);
|
|
19
|
+
} catch {
|
|
20
|
+
// Treat any storage error as a miss — never throw at the assistant.
|
|
21
|
+
entry = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (!entry) {
|
|
25
|
+
process.stderr.write(
|
|
26
|
+
`[mink] no retrievable output for token "${token}" (unknown or expired)\n`
|
|
27
|
+
);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
process.stdout.write(entry.content);
|
|
32
|
+
}
|
package/src/commands/status.ts
CHANGED
|
@@ -149,7 +149,19 @@ export function status(cwd: string): void {
|
|
|
149
149
|
console.log(` Sessions: ${lt.totalSessions}`);
|
|
150
150
|
console.log(` Total tokens: ${lt.totalTokens.toLocaleString()}`);
|
|
151
151
|
console.log(` Reads: ${lt.totalReads} Writes: ${lt.totalWrites}`);
|
|
152
|
-
console.log(` Estimated savings: ${lt.totalEstimatedSavings.toLocaleString()} tokens`);
|
|
152
|
+
console.log(` Estimated savings (heuristic): ${lt.totalEstimatedSavings.toLocaleString()} tokens`);
|
|
153
|
+
const comp = ledger.compression;
|
|
154
|
+
if (comp && comp.totalEvents > 0) {
|
|
155
|
+
const ratio =
|
|
156
|
+
comp.totalOriginalTokens > 0
|
|
157
|
+
? Math.round((comp.totalMeasuredSavings / comp.totalOriginalTokens) * 100)
|
|
158
|
+
: 0;
|
|
159
|
+
console.log(
|
|
160
|
+
` Measured compression savings: ${comp.totalMeasuredSavings.toLocaleString()} tokens` +
|
|
161
|
+
` (${ratio}% over ${comp.totalEvents} event${comp.totalEvents === 1 ? "" : "s"}` +
|
|
162
|
+
`, ${comp.totalHoldoutEvents} held out)`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
153
165
|
} catch {
|
|
154
166
|
console.log(" Token ledger: error reading");
|
|
155
167
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Deterministic, dependency-free code skeleton extraction (spec 21 phase 3).
|
|
2
|
+
//
|
|
3
|
+
// Produces a structural outline of source: top-level declarations and the direct
|
|
4
|
+
// members of classes/interfaces, with function/method bodies elided to "{ … }".
|
|
5
|
+
// It is brace-depth aware (with strings and comments masked so stray braces don't
|
|
6
|
+
// desync the depth), which lets it descend into a class to capture method
|
|
7
|
+
// signatures while suppressing the statements inside those methods.
|
|
8
|
+
//
|
|
9
|
+
// This is a heuristic skeleton, not a real parser — Mink stays zero-dependency.
|
|
10
|
+
// Because tool-output compression is reversible (the original is cached), a
|
|
11
|
+
// slightly imperfect skeleton is harmless: the model can always `mink retrieve`.
|
|
12
|
+
// The same extractor is intended to enrich the file index later.
|
|
13
|
+
|
|
14
|
+
const MAX_SIGNATURES = 80;
|
|
15
|
+
const INDENT = " ";
|
|
16
|
+
|
|
17
|
+
// Declarations that always anchor the skeleton.
|
|
18
|
+
const DECL_ALWAYS =
|
|
19
|
+
/^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?(?:async\s+)?(?:function|class|interface|type|enum|namespace|module|def|fn|func|impl|struct|trait)\b/;
|
|
20
|
+
// Variable declarations only matter to the public surface when exported.
|
|
21
|
+
const DECL_EXPORTED_VAR = /^\s*export\s+(?:default\s+)?(?:const|let|var)\b/;
|
|
22
|
+
// Inside a class/interface body (depth >= 1): method signatures and fields.
|
|
23
|
+
const MEMBER =
|
|
24
|
+
/^\s*(?:public\s+|private\s+|protected\s+|readonly\s+|static\s+|async\s+|get\s+|set\s+|#)*[\w$]+\??\s*(?:\(|:|=)/;
|
|
25
|
+
// Markdown headings (only honoured for markdown files).
|
|
26
|
+
const HEADING = /^#{1,6}\s+\S/;
|
|
27
|
+
// Keywords whose block we descend into to capture members rather than elide.
|
|
28
|
+
const DESCEND = /\b(?:class|interface|enum|namespace|module|struct|trait|impl)\b/;
|
|
29
|
+
|
|
30
|
+
function countChar(s: string, c: string): number {
|
|
31
|
+
let n = 0;
|
|
32
|
+
for (let i = 0; i < s.length; i++) if (s[i] === c) n++;
|
|
33
|
+
return n;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Net brace delta for a line, with strings and comments masked so braces inside
|
|
37
|
+
// them don't affect depth tracking.
|
|
38
|
+
function netBraces(line: string): number {
|
|
39
|
+
let s = line.replace(/\/\/.*$/, "");
|
|
40
|
+
s = s.replace(/\/\*.*?\*\//g, "");
|
|
41
|
+
s = s.replace(/"(?:\\.|[^"\\])*"/g, '""');
|
|
42
|
+
s = s.replace(/'(?:\\.|[^'\\])*'/g, "''");
|
|
43
|
+
s = s.replace(/`(?:\\.|[^`\\])*`/g, "``");
|
|
44
|
+
return countChar(s, "{") - countChar(s, "}");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function stripOpenBrace(sig: string): string {
|
|
48
|
+
return sig.replace(/\{\s*$/, "").trimEnd();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface CodeSkeleton {
|
|
52
|
+
lines: string[];
|
|
53
|
+
totalLines: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Extract a skeleton, or null when the content has no recognisable structure
|
|
57
|
+
// (the caller then falls back to a generic text window).
|
|
58
|
+
export function extractCodeSkeleton(
|
|
59
|
+
content: string,
|
|
60
|
+
opts: { markdown?: boolean } = {}
|
|
61
|
+
): CodeSkeleton | null {
|
|
62
|
+
const rawLines = content.split("\n");
|
|
63
|
+
const totalLines =
|
|
64
|
+
rawLines.length > 0 && rawLines[rawLines.length - 1] === ""
|
|
65
|
+
? rawLines.length - 1
|
|
66
|
+
: rawLines.length;
|
|
67
|
+
|
|
68
|
+
const out: string[] = [];
|
|
69
|
+
let depth = 0;
|
|
70
|
+
let suppress = Infinity; // suppress lines while inside an elided function body
|
|
71
|
+
|
|
72
|
+
for (const line of rawLines) {
|
|
73
|
+
if (out.length >= MAX_SIGNATURES) break;
|
|
74
|
+
const start = depth;
|
|
75
|
+
const net = netBraces(line);
|
|
76
|
+
|
|
77
|
+
if (start < suppress) {
|
|
78
|
+
const isHeading = opts.markdown === true && HEADING.test(line);
|
|
79
|
+
const captured =
|
|
80
|
+
isHeading ||
|
|
81
|
+
DECL_ALWAYS.test(line) ||
|
|
82
|
+
DECL_EXPORTED_VAR.test(line) ||
|
|
83
|
+
(start >= 1 && MEMBER.test(line));
|
|
84
|
+
|
|
85
|
+
if (captured) {
|
|
86
|
+
// Trim the source indentation; we re-indent by structural depth.
|
|
87
|
+
const sig = line.trim();
|
|
88
|
+
if (net > 0) {
|
|
89
|
+
if (DESCEND.test(line) && !isHeading) {
|
|
90
|
+
out.push(INDENT.repeat(start) + stripOpenBrace(sig) + " {");
|
|
91
|
+
// descend — keep capturing members at the next depth
|
|
92
|
+
} else {
|
|
93
|
+
out.push(INDENT.repeat(start) + stripOpenBrace(sig) + " { … }");
|
|
94
|
+
suppress = start + 1; // skip this body's contents
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
out.push(INDENT.repeat(start) + sig);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
depth = Math.max(0, depth + net);
|
|
103
|
+
if (depth < suppress) suppress = Infinity; // left the elided body
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (out.length === 0) return null;
|
|
107
|
+
return { lines: out, totalLines };
|
|
108
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// Compression pipeline orchestrator (spec 21). Ties together the config/holdout
|
|
2
|
+
// decisions, the pure engine, the reversible cache, and the ledger. Returns the
|
|
3
|
+
// replacement text to emit, or null to pass the original through unchanged.
|
|
4
|
+
//
|
|
5
|
+
// Invariants:
|
|
6
|
+
// - Disabled by default (config gate) → no-op.
|
|
7
|
+
// - Reversible or nothing: the original is stored BEFORE we return a compressed
|
|
8
|
+
// result; if storage fails we pass the original through, so a compressed
|
|
9
|
+
// result is never shown without a retrievable original (spec 21 edge case).
|
|
10
|
+
// - Every failure degrades to "no compression" — a hook must never throw.
|
|
11
|
+
// - Holdout arms pass the original through but are still measured.
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
loadCompressionConfig,
|
|
15
|
+
isEligible,
|
|
16
|
+
meetsMinSavings,
|
|
17
|
+
selectHoldout,
|
|
18
|
+
} from "./compression";
|
|
19
|
+
import { countTokens } from "./token-estimate";
|
|
20
|
+
import { compressOutput, detectContentKind } from "./output-compression";
|
|
21
|
+
import type { CompressionResult } from "../types/compression";
|
|
22
|
+
import { CompressionCacheRepo } from "../repositories/compression-cache-repo";
|
|
23
|
+
import { TokenLedgerRepo } from "../repositories/token-ledger-repo";
|
|
24
|
+
|
|
25
|
+
// Deterministic FNV-1a → hex, used as a stable per-event key so an identical
|
|
26
|
+
// output always lands in the same holdout arm (spec 21 edge case).
|
|
27
|
+
function contentKey(s: string): string {
|
|
28
|
+
let h = 0x811c9dc5;
|
|
29
|
+
for (let i = 0; i < s.length; i++) {
|
|
30
|
+
h ^= s.charCodeAt(i);
|
|
31
|
+
h = Math.imul(h, 0x01000193);
|
|
32
|
+
}
|
|
33
|
+
return (h >>> 0).toString(16);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Render the replacement: stable compressed body first, volatile retrieval
|
|
37
|
+
// footer (random token) last — matching Mink's "volatile at the end" cache
|
|
38
|
+
// discipline so the body forms a stable prefix.
|
|
39
|
+
function render(result: CompressionResult, token: string): string {
|
|
40
|
+
return (
|
|
41
|
+
result.compressed +
|
|
42
|
+
"\n\n" +
|
|
43
|
+
`— mink: compressed ${result.kind} output (${result.omittedNote}). ` +
|
|
44
|
+
`Full original: mink retrieve ${token}`
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function safeRecord(
|
|
49
|
+
cwd: string,
|
|
50
|
+
toolName: string,
|
|
51
|
+
contentKind: string,
|
|
52
|
+
originalTokens: number,
|
|
53
|
+
compressedTokens: number,
|
|
54
|
+
holdout: boolean
|
|
55
|
+
): void {
|
|
56
|
+
try {
|
|
57
|
+
TokenLedgerRepo.for(cwd).recordCompression({
|
|
58
|
+
toolName,
|
|
59
|
+
contentKind,
|
|
60
|
+
originalTokens,
|
|
61
|
+
compressedTokens,
|
|
62
|
+
holdout,
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
// Measurement is best-effort — never block the hook over a ledger write.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface CompressOutcome {
|
|
70
|
+
updatedToolOutput: string;
|
|
71
|
+
token: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function compressToolOutput(
|
|
75
|
+
cwd: string,
|
|
76
|
+
toolName: string,
|
|
77
|
+
output: string,
|
|
78
|
+
filePath?: string
|
|
79
|
+
): CompressOutcome | null {
|
|
80
|
+
let cfg;
|
|
81
|
+
try {
|
|
82
|
+
cfg = loadCompressionConfig();
|
|
83
|
+
} catch {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (!cfg.enabled) return null;
|
|
87
|
+
if (typeof output !== "string" || output.length === 0) return null;
|
|
88
|
+
|
|
89
|
+
const originalTokens = countTokens(output);
|
|
90
|
+
if (!isEligible(originalTokens, cfg)) return null;
|
|
91
|
+
|
|
92
|
+
const eventKey = contentKey(output);
|
|
93
|
+
|
|
94
|
+
// Holdout arm: pass the original through, but record it as a control.
|
|
95
|
+
if (selectHoldout(eventKey, cfg.holdoutFraction)) {
|
|
96
|
+
const kind = detectContentKind(toolName, output, filePath);
|
|
97
|
+
safeRecord(cwd, toolName, kind, originalTokens, originalTokens, true);
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const result = compressOutput(toolName, output, filePath);
|
|
102
|
+
if (!result) return null;
|
|
103
|
+
|
|
104
|
+
const token = CompressionCacheRepo.newToken();
|
|
105
|
+
const replacement = render(result, token);
|
|
106
|
+
const compressedTokens = countTokens(replacement);
|
|
107
|
+
|
|
108
|
+
// Discard a weak compression and pass the original through.
|
|
109
|
+
if (!meetsMinSavings(originalTokens, compressedTokens, cfg)) return null;
|
|
110
|
+
|
|
111
|
+
// Store the original FIRST. If we cannot, do not compress — never show a
|
|
112
|
+
// compressed result without a retrievable original.
|
|
113
|
+
try {
|
|
114
|
+
CompressionCacheRepo.for(cwd).store({
|
|
115
|
+
toolName,
|
|
116
|
+
contentKind: result.kind,
|
|
117
|
+
content: output,
|
|
118
|
+
retentionHours: cfg.retentionHours,
|
|
119
|
+
token,
|
|
120
|
+
});
|
|
121
|
+
} catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
safeRecord(cwd, toolName, result.kind, originalTokens, compressedTokens, false);
|
|
126
|
+
return { updatedToolOutput: replacement, token };
|
|
127
|
+
}
|