@drewpayment/mink 0.12.0-beta.3 → 0.12.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) 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 +139 -57
  39. package/dist/cli.node.js +139 -57
  40. package/package.json +1 -1
  41. package/src/commands/dashboard.ts +49 -3
  42. package/src/commands/post-read.ts +94 -9
  43. package/src/core/framework-advisor/generate.ts +11 -1
  44. package/src/core/note-linker.ts +12 -7
  45. package/src/types/hook-input.ts +10 -0
  46. /package/dashboard/out/_next/static/{qQncyEK2SSmDpJw1uhqt9 → eZlC6TEe7TWUABN2-Ho0J}/_buildManifest.js +0 -0
  47. /package/dashboard/out/_next/static/{qQncyEK2SSmDpJw1uhqt9 → eZlC6TEe7TWUABN2-Ho0J}/_ssgManifest.js +0 -0
@@ -1,20 +1,26 @@
1
1
  import { relative } from "path";
2
+ import { readFileSync } from "fs";
2
3
  import { readStdinJson } from "../core/stdin";
3
4
  import { sessionPath, actionLogShardPath } from "../core/paths";
4
5
  import { safeReadJson, atomicWriteJson } from "../core/fs-utils";
5
6
  import { createSessionState, isSessionState, recordRead } from "../core/session";
6
7
  import { FileIndexRepo } from "../repositories/file-index-repo";
7
8
  import { estimateTokens, isBinaryFile } from "../core/token-estimate";
9
+ import { extractDescription } from "../core/description";
8
10
  import { createActionLogWriter } from "../core/action-log";
9
11
  import { getOrCreateDeviceId } from "../core/device";
10
12
  import type { SessionState } from "../types/session";
11
- import type { IndexLookup } from "../types/file-index";
13
+ import type { FileIndexEntry, IndexLookup } from "../types/file-index";
12
14
  import type { PostToolUseInput } from "../types/hook-input";
13
15
 
14
16
  export interface PostReadResult {
15
17
  estimatedTokens: number;
16
18
  indexHit: boolean;
17
19
  source: "content" | "index-fallback" | "none";
20
+ // Populated when content was available and the file was not already in
21
+ // the index — lets the caller seed the index lazily so that read-only
22
+ // browsing sessions don't accumulate zero index hits.
23
+ indexEntry: FileIndexEntry | null;
18
24
  }
19
25
 
20
26
  export function analyzePostRead(
@@ -25,16 +31,42 @@ export function analyzePostRead(
25
31
  // Binary file — skip token estimation
26
32
  if (isBinaryFile(filePath, content ?? undefined)) {
27
33
  const entry = index ? index.lookupEntry(filePath) : null;
28
- return { estimatedTokens: 0, indexHit: !!entry, source: "none" };
34
+ return {
35
+ estimatedTokens: 0,
36
+ indexHit: !!entry,
37
+ source: "none",
38
+ indexEntry: null,
39
+ };
29
40
  }
30
41
 
31
42
  // Content available — estimate from actual content
32
43
  if (content !== null && content.length > 0) {
33
44
  const entry = index ? index.lookupEntry(filePath) : null;
45
+ const tokens = estimateTokens(content, filePath);
46
+ // On miss, build a seed entry so the index grows from reads, not just
47
+ // writes and scans. Description failures must never throw out the read.
48
+ let indexEntry: FileIndexEntry | null = null;
49
+ if (!entry) {
50
+ let description = "";
51
+ try {
52
+ description = extractDescription(filePath, content);
53
+ } catch {
54
+ description = "";
55
+ }
56
+ const now = new Date().toISOString();
57
+ indexEntry = {
58
+ filePath,
59
+ description,
60
+ estimatedTokens: tokens,
61
+ lastModified: now,
62
+ lastIndexed: now,
63
+ };
64
+ }
34
65
  return {
35
- estimatedTokens: estimateTokens(content, filePath),
66
+ estimatedTokens: tokens,
36
67
  indexHit: !!entry,
37
68
  source: "content",
69
+ indexEntry,
38
70
  };
39
71
  }
40
72
 
@@ -46,11 +78,17 @@ export function analyzePostRead(
46
78
  estimatedTokens: entry.estimatedTokens,
47
79
  indexHit: true,
48
80
  source: "index-fallback",
81
+ indexEntry: null,
49
82
  };
50
83
  }
51
84
  }
52
85
 
53
- return { estimatedTokens: 0, indexHit: false, source: "none" };
86
+ return {
87
+ estimatedTokens: 0,
88
+ indexHit: false,
89
+ source: "none",
90
+ indexEntry: null,
91
+ };
54
92
  }
55
93
 
56
94
  function isPostToolUseInput(value: unknown): value is PostToolUseInput {
@@ -61,9 +99,32 @@ function isPostToolUseInput(value: unknown): value is PostToolUseInput {
61
99
  return true;
62
100
  }
63
101
 
64
- function extractContent(input: PostToolUseInput): string | null {
65
- if (!input.tool_output) return null;
66
- if (typeof input.tool_output.content === "string") {
102
+ // Pull file content out of the PostToolUse payload. Claude Code has shipped
103
+ // at least two payload shapes for the Read tool:
104
+ // • legacy: `tool_output.content` is a plain string
105
+ // • current: `tool_response` carries the content — either as a string, as
106
+ // a Content[]-style array (`{ type: "text", text: "..." }`), or nested
107
+ // under `tool_response.file.content`
108
+ // We accept any of them so a hook contract drift can't silently zero out
109
+ // token estimation again.
110
+ export function extractContent(input: PostToolUseInput): string | null {
111
+ // Current shape — tool_response
112
+ const tr = input.tool_response;
113
+ if (tr) {
114
+ if (typeof tr.content === "string") return tr.content;
115
+ if (Array.isArray(tr.content)) {
116
+ const parts = tr.content
117
+ .map((p) => (p && typeof p.text === "string" ? p.text : ""))
118
+ .filter((s) => s.length > 0);
119
+ if (parts.length > 0) return parts.join("");
120
+ }
121
+ if (tr.file && typeof tr.file.content === "string") {
122
+ return tr.file.content;
123
+ }
124
+ if (typeof tr.text === "string") return tr.text;
125
+ }
126
+ // Legacy shape — tool_output
127
+ if (input.tool_output && typeof input.tool_output.content === "string") {
67
128
  return input.tool_output.content;
68
129
  }
69
130
  return null;
@@ -92,11 +153,35 @@ export async function postRead(cwd: string): Promise<void> {
92
153
  // File index repository — one-key lookup, no whole-index load.
93
154
  const repo = FileIndexRepo.for(cwd);
94
155
 
95
- // Extract content from tool output
96
- const content = extractContent(input);
156
+ // Primary path: read content from disk by file path. This is the
157
+ // cleanest source because it doesn't depend on Claude Code's evolving
158
+ // hook payload schema (which has silently dropped `tool_output.content`
159
+ // in favor of nested `tool_response` shapes, breaking token
160
+ // measurement). Mirrors post-write.ts's approach.
161
+ let content: string | null = null;
162
+ try {
163
+ content = readFileSync(absolutePath, "utf-8");
164
+ } catch {
165
+ // File unreadable (permissions, deleted between read and hook) —
166
+ // fall back to whatever the payload carries.
167
+ }
168
+ if (content === null) {
169
+ content = extractContent(input);
170
+ }
97
171
 
98
172
  const result = analyzePostRead(filePath, content, repo);
99
173
 
174
+ // Seed the file index on a miss. Read-only browsing sessions otherwise
175
+ // accumulate zero index hits because the index only grows via
176
+ // `mink scan` (capped) or post-write.
177
+ if (result.indexEntry) {
178
+ try {
179
+ repo.upsert(result.indexEntry);
180
+ } catch {
181
+ // Never crash the hook over an index upsert failure.
182
+ }
183
+ }
184
+
100
185
  // Record the read in session state
101
186
  recordRead(state, filePath, result.estimatedTokens, result.indexHit);
102
187
 
@@ -24,10 +24,13 @@ export function generateKnowledgeMarkdown(
24
24
  ): string {
25
25
  const parts: string[] = [];
26
26
 
27
+ // Prompt-cache stability: keep the title and stable summary line (counts
28
+ // only — no timestamps) at the top. The volatile `generatedAt` timestamp
29
+ // lives in the footer so regeneration doesn't bust LLM prefix caches.
27
30
  parts.push(`# Framework Advisor Knowledge Base`);
28
31
  parts.push("");
29
32
  parts.push(
30
- `> Generated: ${k.generatedAt} | Version: ${k.version} | Frameworks: ${k.frameworks.length}`
33
+ `> Version: ${k.version} | Frameworks: ${k.frameworks.length}`
31
34
  );
32
35
  parts.push("");
33
36
 
@@ -111,6 +114,13 @@ export function generateKnowledgeMarkdown(
111
114
  }
112
115
  }
113
116
 
117
+ // ── Footer (volatile — must stay at end for prompt-cache stability) ──
118
+ parts.push(`---`);
119
+ parts.push(``);
120
+ parts.push(`<!-- mink:footer (volatile — keep at end of file) -->`);
121
+ parts.push(`> Generated: ${k.generatedAt}`);
122
+ parts.push(``);
123
+
114
124
  return parts.join("\n");
115
125
  }
116
126
 
@@ -67,16 +67,12 @@ export function addBacklink(
67
67
  }
68
68
 
69
69
  export function updateMasterIndex(vaultRootPath: string): void {
70
- const now = new Date().toISOString().split("T")[0];
70
+ // Prompt-cache stability: keep the prefix (title + stable category sections)
71
+ // at the top. Volatile fields (updated timestamps) live in the footer so a
72
+ // regenerated index never busts an LLM provider's prefix prompt cache.
71
73
  const sections: string[] = [
72
- `---`,
73
- `updated: "${new Date().toISOString()}"`,
74
- `---`,
75
- ``,
76
74
  `# Knowledge Base`,
77
75
  ``,
78
- `> Last updated: ${now}`,
79
- ``,
80
76
  ];
81
77
 
82
78
  const categories = [
@@ -115,6 +111,15 @@ export function updateMasterIndex(vaultRootPath: string): void {
115
111
  sections.push("");
116
112
  }
117
113
 
114
+ // ── Footer (volatile, must stay at the END for prompt-cache stability) ──
115
+ const nowIso = new Date().toISOString();
116
+ const nowDate = nowIso.split("T")[0];
117
+ sections.push(`---`);
118
+ sections.push(``);
119
+ sections.push(`<!-- mink:footer (volatile — keep at end of file) -->`);
120
+ sections.push(`> Last updated: ${nowDate} (${nowIso})`);
121
+ sections.push(``);
122
+
118
123
  const indexPath = vaultMasterIndexPath();
119
124
  atomicWriteText(indexPath, sections.join("\n"));
120
125
  }
@@ -20,8 +20,18 @@ export interface PostToolUseInput {
20
20
  old_string?: string;
21
21
  new_string?: string;
22
22
  };
23
+ // Legacy / older hook payload shape — kept for backward compatibility.
23
24
  tool_output?: {
24
25
  content?: string;
25
26
  [key: string]: unknown;
26
27
  };
28
+ // Current Claude Code PostToolUse shape (>= 0.x). The Read tool delivers
29
+ // file content nested under `tool_response`; the exact field depends on
30
+ // the tool. We accept several common shapes opportunistically.
31
+ tool_response?: {
32
+ content?: string | Array<{ type?: string; text?: string }>;
33
+ file?: { content?: string };
34
+ text?: string;
35
+ [key: string]: unknown;
36
+ };
27
37
  }