@drewpayment/mink 0.12.0-beta.4 → 0.12.0-beta.6
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/dashboard/out/404.html +1 -1
- package/dashboard/out/action-log.html +1 -1
- package/dashboard/out/action-log.txt +1 -1
- package/dashboard/out/activity.html +1 -1
- package/dashboard/out/activity.txt +1 -1
- package/dashboard/out/bugs.html +1 -1
- package/dashboard/out/bugs.txt +1 -1
- package/dashboard/out/capture.html +1 -1
- package/dashboard/out/capture.txt +1 -1
- package/dashboard/out/config.html +1 -1
- package/dashboard/out/config.txt +1 -1
- package/dashboard/out/daemon.html +1 -1
- package/dashboard/out/daemon.txt +1 -1
- package/dashboard/out/design.html +1 -1
- package/dashboard/out/design.txt +1 -1
- package/dashboard/out/discord.html +1 -1
- package/dashboard/out/discord.txt +1 -1
- package/dashboard/out/file-index.html +1 -1
- package/dashboard/out/file-index.txt +1 -1
- package/dashboard/out/index.html +1 -1
- package/dashboard/out/index.txt +1 -1
- package/dashboard/out/insights.html +1 -1
- package/dashboard/out/insights.txt +1 -1
- package/dashboard/out/learning.html +1 -1
- package/dashboard/out/learning.txt +1 -1
- package/dashboard/out/overview.html +1 -1
- package/dashboard/out/overview.txt +1 -1
- package/dashboard/out/scheduler.html +1 -1
- package/dashboard/out/scheduler.txt +1 -1
- package/dashboard/out/sync.html +1 -1
- package/dashboard/out/sync.txt +1 -1
- package/dashboard/out/tokens.html +1 -1
- package/dashboard/out/tokens.txt +1 -1
- package/dashboard/out/waste.html +1 -1
- package/dashboard/out/waste.txt +1 -1
- package/dashboard/out/wiki.html +1 -1
- package/dashboard/out/wiki.txt +1 -1
- package/dist/cli.bun.js +148 -69
- package/dist/cli.node.js +148 -69
- package/package.json +1 -1
- package/src/commands/post-read.ts +94 -9
- package/src/commands/status.ts +37 -12
- package/src/core/framework-advisor/generate.ts +11 -1
- package/src/core/note-linker.ts +12 -7
- package/src/core/state-aggregator.ts +3 -3
- package/src/types/hook-input.ts +10 -0
- /package/dashboard/out/_next/static/{i9-16JmUxsS4K70sSYdYA → 2qo2HCBP_HsNoQT7-KrRp}/_buildManifest.js +0 -0
- /package/dashboard/out/_next/static/{i9-16JmUxsS4K70sSYdYA → 2qo2HCBP_HsNoQT7-KrRp}/_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 {
|
|
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:
|
|
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 {
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
//
|
|
96
|
-
|
|
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
|
|
package/src/commands/status.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
configPath,
|
|
6
6
|
learningMemoryPath,
|
|
7
7
|
actionLogPath,
|
|
8
|
+
projectDir,
|
|
8
9
|
} from "../core/paths";
|
|
9
10
|
import { safeReadJson } from "../core/fs-utils";
|
|
10
11
|
import { FileIndexRepo } from "../repositories/file-index-repo";
|
|
@@ -13,6 +14,9 @@ import {
|
|
|
13
14
|
aggregateTokenLedger,
|
|
14
15
|
aggregateBugMemory,
|
|
15
16
|
aggregateLearningMemory,
|
|
17
|
+
listDeviceShardsAt,
|
|
18
|
+
listLearningMemorySidecarPathsAt,
|
|
19
|
+
shardPath,
|
|
16
20
|
} from "../core/state-aggregator";
|
|
17
21
|
import { loadCounters } from "../core/state-counters";
|
|
18
22
|
import { getDaemonStatus } from "../core/daemon";
|
|
@@ -32,16 +36,6 @@ function checkJsonFile(name: string, filePath: string, validator?: (v: unknown)
|
|
|
32
36
|
return { name, path: filePath, status: "ok" };
|
|
33
37
|
}
|
|
34
38
|
|
|
35
|
-
function checkTextFile(name: string, filePath: string): FileCheck {
|
|
36
|
-
if (!existsSync(filePath)) return { name, path: filePath, status: "missing" };
|
|
37
|
-
try {
|
|
38
|
-
readFileSync(filePath, "utf-8");
|
|
39
|
-
return { name, path: filePath, status: "ok" };
|
|
40
|
-
} catch {
|
|
41
|
-
return { name, path: filePath, status: "corrupt" };
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
39
|
function checkDbFile(name: string, filePath: string): FileCheck {
|
|
46
40
|
if (!existsSync(filePath)) return { name, path: filePath, status: "missing" };
|
|
47
41
|
try {
|
|
@@ -55,6 +49,37 @@ function checkDbFile(name: string, filePath: string): FileCheck {
|
|
|
55
49
|
}
|
|
56
50
|
}
|
|
57
51
|
|
|
52
|
+
// Reports "ok" when canonical OR any device shard / sidecar exists with content.
|
|
53
|
+
// action-log and learning-memory live in per-device shards; checking only the
|
|
54
|
+
// canonical path made initialized projects look empty.
|
|
55
|
+
function checkShardedText(name: string, candidatePaths: string[]): FileCheck {
|
|
56
|
+
const canonical = candidatePaths[0];
|
|
57
|
+
for (const p of candidatePaths) {
|
|
58
|
+
if (!existsSync(p)) continue;
|
|
59
|
+
try {
|
|
60
|
+
if (statSync(p).size === 0) continue;
|
|
61
|
+
readFileSync(p, "utf-8");
|
|
62
|
+
return { name, path: p, status: "ok" };
|
|
63
|
+
} catch {
|
|
64
|
+
return { name, path: p, status: "corrupt" };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { name, path: canonical, status: "missing" };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function actionLogCandidates(cwd: string): string[] {
|
|
71
|
+
const dir = projectDir(cwd);
|
|
72
|
+
return [
|
|
73
|
+
actionLogPath(cwd),
|
|
74
|
+
...listDeviceShardsAt(dir).map((id) => shardPath(dir, id, "action-log.md")),
|
|
75
|
+
];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function learningMemoryCandidates(cwd: string): string[] {
|
|
79
|
+
const dir = projectDir(cwd);
|
|
80
|
+
return [learningMemoryPath(cwd), ...listLearningMemorySidecarPathsAt(dir)];
|
|
81
|
+
}
|
|
82
|
+
|
|
58
83
|
export function status(cwd: string): void {
|
|
59
84
|
console.log("[mink] project status");
|
|
60
85
|
console.log();
|
|
@@ -77,8 +102,8 @@ export function status(cwd: string): void {
|
|
|
77
102
|
checkJsonFile("session.json", sessionPath(cwd)),
|
|
78
103
|
checkDbFile("mink.db", projectDbPath(cwd)),
|
|
79
104
|
checkJsonFile("config.json", configPath(cwd)),
|
|
80
|
-
|
|
81
|
-
|
|
105
|
+
checkShardedText("learning-memory.md", learningMemoryCandidates(cwd)),
|
|
106
|
+
checkShardedText("action-log.md", actionLogCandidates(cwd)),
|
|
82
107
|
];
|
|
83
108
|
|
|
84
109
|
console.log(" State files:");
|
|
@@ -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
|
-
`>
|
|
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
|
|
package/src/core/note-linker.ts
CHANGED
|
@@ -67,16 +67,12 @@ export function addBacklink(
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
export function updateMasterIndex(vaultRootPath: string): void {
|
|
70
|
-
|
|
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
|
}
|
|
@@ -28,7 +28,7 @@ import type {
|
|
|
28
28
|
// projectDir(cwd)). The cwd-based variants below are thin wrappers for the
|
|
29
29
|
// common case where the caller has cwd in hand.
|
|
30
30
|
|
|
31
|
-
function listDeviceShardsAt(projDir: string): string[] {
|
|
31
|
+
export function listDeviceShardsAt(projDir: string): string[] {
|
|
32
32
|
const stateDir = join(projDir, "state");
|
|
33
33
|
if (!existsSync(stateDir)) return [];
|
|
34
34
|
try {
|
|
@@ -46,7 +46,7 @@ function listDeviceShardsAt(projDir: string): string[] {
|
|
|
46
46
|
|
|
47
47
|
const SIDECAR_RE = /^learning-memory\.([^.]+)\.md$/;
|
|
48
48
|
|
|
49
|
-
function listLearningMemorySidecarPathsAt(projDir: string): string[] {
|
|
49
|
+
export function listLearningMemorySidecarPathsAt(projDir: string): string[] {
|
|
50
50
|
if (!existsSync(projDir)) return [];
|
|
51
51
|
try {
|
|
52
52
|
return readdirSync(projDir)
|
|
@@ -57,7 +57,7 @@ function listLearningMemorySidecarPathsAt(projDir: string): string[] {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
function shardPath(projDir: string, deviceId: string, file: string): string {
|
|
60
|
+
export function shardPath(projDir: string, deviceId: string, file: string): string {
|
|
61
61
|
return join(projDir, "state", deviceId, file);
|
|
62
62
|
}
|
|
63
63
|
|
package/src/types/hook-input.ts
CHANGED
|
@@ -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
|
}
|
|
File without changes
|
/package/dashboard/out/_next/static/{i9-16JmUxsS4K70sSYdYA → 2qo2HCBP_HsNoQT7-KrRp}/_ssgManifest.js
RENAMED
|
File without changes
|