@drewpayment/mink 0.1.0

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 (72) hide show
  1. package/README.md +347 -0
  2. package/package.json +32 -0
  3. package/src/cli.ts +176 -0
  4. package/src/commands/bug-search.ts +32 -0
  5. package/src/commands/config.ts +109 -0
  6. package/src/commands/cron.ts +295 -0
  7. package/src/commands/daemon.ts +46 -0
  8. package/src/commands/dashboard.ts +21 -0
  9. package/src/commands/designqc.ts +160 -0
  10. package/src/commands/detect-waste.ts +81 -0
  11. package/src/commands/framework-advisor.ts +52 -0
  12. package/src/commands/init.ts +159 -0
  13. package/src/commands/post-read.ts +123 -0
  14. package/src/commands/post-write.ts +157 -0
  15. package/src/commands/pre-read.ts +109 -0
  16. package/src/commands/pre-write.ts +136 -0
  17. package/src/commands/reflect.ts +39 -0
  18. package/src/commands/restore.ts +31 -0
  19. package/src/commands/scan.ts +101 -0
  20. package/src/commands/session-start.ts +21 -0
  21. package/src/commands/session-stop.ts +115 -0
  22. package/src/commands/status.ts +152 -0
  23. package/src/commands/update.ts +121 -0
  24. package/src/core/action-log.ts +341 -0
  25. package/src/core/backup.ts +122 -0
  26. package/src/core/bug-memory.ts +223 -0
  27. package/src/core/cron-parser.ts +94 -0
  28. package/src/core/daemon.ts +152 -0
  29. package/src/core/dashboard-api.ts +280 -0
  30. package/src/core/dashboard-server.ts +580 -0
  31. package/src/core/description.ts +232 -0
  32. package/src/core/design-eval/capture.ts +269 -0
  33. package/src/core/design-eval/route-detect.ts +165 -0
  34. package/src/core/design-eval/server-detect.ts +91 -0
  35. package/src/core/framework-advisor/catalog.ts +360 -0
  36. package/src/core/framework-advisor/decision-tree.ts +287 -0
  37. package/src/core/framework-advisor/generate.ts +132 -0
  38. package/src/core/framework-advisor/migration-prompts.ts +502 -0
  39. package/src/core/framework-advisor/validate.ts +137 -0
  40. package/src/core/fs-utils.ts +30 -0
  41. package/src/core/global-config.ts +74 -0
  42. package/src/core/index-store.ts +72 -0
  43. package/src/core/learning-memory.ts +120 -0
  44. package/src/core/paths.ts +86 -0
  45. package/src/core/pattern-engine.ts +108 -0
  46. package/src/core/project-id.ts +19 -0
  47. package/src/core/project-registry.ts +64 -0
  48. package/src/core/reflection.ts +256 -0
  49. package/src/core/scanner.ts +99 -0
  50. package/src/core/scheduler.ts +352 -0
  51. package/src/core/seed.ts +239 -0
  52. package/src/core/session.ts +128 -0
  53. package/src/core/stdin.ts +13 -0
  54. package/src/core/task-registry.ts +202 -0
  55. package/src/core/token-estimate.ts +36 -0
  56. package/src/core/token-ledger.ts +185 -0
  57. package/src/core/waste-detection.ts +214 -0
  58. package/src/core/write-exclusions.ts +24 -0
  59. package/src/types/action-log.ts +20 -0
  60. package/src/types/backup.ts +6 -0
  61. package/src/types/bug-memory.ts +24 -0
  62. package/src/types/config.ts +59 -0
  63. package/src/types/dashboard.ts +104 -0
  64. package/src/types/design-eval.ts +64 -0
  65. package/src/types/file-index.ts +38 -0
  66. package/src/types/framework-advisor.ts +97 -0
  67. package/src/types/hook-input.ts +27 -0
  68. package/src/types/learning-memory.ts +36 -0
  69. package/src/types/scheduler.ts +82 -0
  70. package/src/types/session.ts +50 -0
  71. package/src/types/token-ledger.ts +43 -0
  72. package/src/types/waste-detection.ts +21 -0
@@ -0,0 +1,239 @@
1
+ import { basename, join } from "path";
2
+ import { readFileSync, existsSync } from "fs";
3
+ import type { LearningMemory, SeedInfo } from "../types/learning-memory";
4
+ import { createEmptyLearningMemory, addEntry } from "./learning-memory";
5
+
6
+ // ─── Framework detection maps ─────────────────────────────────────────────────
7
+
8
+ const NPM_FRAMEWORKS: Record<string, string> = {
9
+ react: "React",
10
+ "react-dom": "React",
11
+ next: "Next.js",
12
+ vue: "Vue",
13
+ nuxt: "Nuxt",
14
+ svelte: "Svelte",
15
+ "@sveltejs/kit": "SvelteKit",
16
+ angular: "Angular",
17
+ "@angular/core": "Angular",
18
+ express: "Express",
19
+ fastify: "Fastify",
20
+ hono: "Hono",
21
+ koa: "Koa",
22
+ "@nestjs/core": "NestJS",
23
+ typescript: "TypeScript",
24
+ jest: "Jest",
25
+ vitest: "Vitest",
26
+ mocha: "Mocha",
27
+ tailwindcss: "Tailwind CSS",
28
+ prisma: "Prisma",
29
+ "@prisma/client": "Prisma",
30
+ "drizzle-orm": "Drizzle",
31
+ };
32
+
33
+ const PYTHON_FRAMEWORKS: Record<string, string> = {
34
+ fastapi: "FastAPI",
35
+ flask: "Flask",
36
+ django: "Django",
37
+ sqlalchemy: "SQLAlchemy",
38
+ pytest: "pytest",
39
+ pydantic: "Pydantic",
40
+ celery: "Celery",
41
+ httpx: "HTTPX",
42
+ uvicorn: "Uvicorn",
43
+ };
44
+
45
+ const CARGO_FRAMEWORKS: Record<string, string> = {
46
+ "actix-web": "Actix Web",
47
+ axum: "Axum",
48
+ tokio: "Tokio",
49
+ serde: "Serde",
50
+ diesel: "Diesel",
51
+ sqlx: "SQLx",
52
+ warp: "Warp",
53
+ rocket: "Rocket",
54
+ };
55
+
56
+ const GO_FRAMEWORKS: Record<string, string> = {
57
+ "github.com/gin-gonic/gin": "Gin",
58
+ "github.com/gofiber/fiber": "Fiber",
59
+ "github.com/labstack/echo": "Echo",
60
+ "gorm.io/gorm": "GORM",
61
+ "github.com/gorilla/mux": "Gorilla Mux",
62
+ };
63
+
64
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
65
+
66
+ function readFile(filePath: string): string | null {
67
+ if (!existsSync(filePath)) return null;
68
+ try {
69
+ return readFileSync(filePath, "utf-8");
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function dedupeOrdered(arr: string[]): string[] {
76
+ return [...new Set(arr)];
77
+ }
78
+
79
+ function detectFromKeys(
80
+ keys: string[],
81
+ map: Record<string, string>
82
+ ): string[] {
83
+ const found: string[] = [];
84
+ for (const key of keys) {
85
+ if (map[key]) found.push(map[key]);
86
+ }
87
+ return dedupeOrdered(found);
88
+ }
89
+
90
+ // ─── Parsers ──────────────────────────────────────────────────────────────────
91
+
92
+ export function parsePackageJson(filePath: string): SeedInfo | null {
93
+ const raw = readFile(filePath);
94
+ if (raw === null) return null;
95
+
96
+ let parsed: unknown;
97
+ try {
98
+ parsed = JSON.parse(raw);
99
+ } catch {
100
+ return null;
101
+ }
102
+
103
+ if (typeof parsed !== "object" || parsed === null) return null;
104
+
105
+ const obj = parsed as Record<string, unknown>;
106
+ const projectName = typeof obj.name === "string" ? obj.name : "";
107
+ const description = typeof obj.description === "string" ? obj.description : "";
108
+
109
+ const deps: Record<string, unknown> =
110
+ typeof obj.dependencies === "object" && obj.dependencies !== null
111
+ ? (obj.dependencies as Record<string, unknown>)
112
+ : {};
113
+ const devDeps: Record<string, unknown> =
114
+ typeof obj.devDependencies === "object" && obj.devDependencies !== null
115
+ ? (obj.devDependencies as Record<string, unknown>)
116
+ : {};
117
+
118
+ const allKeys = [...Object.keys(deps), ...Object.keys(devDeps)];
119
+ const frameworks = detectFromKeys(allKeys, NPM_FRAMEWORKS);
120
+
121
+ return { projectName, description, frameworks };
122
+ }
123
+
124
+ export function parsePyprojectToml(filePath: string): SeedInfo | null {
125
+ const raw = readFile(filePath);
126
+ if (raw === null) return null;
127
+
128
+ // Extract [project] section name
129
+ const nameMatch = raw.match(/\[project\][^\[]*\bname\s*=\s*"([^"]+)"/s);
130
+ const descMatch = raw.match(
131
+ /\[project\][^\[]*\bdescription\s*=\s*"([^"]+)"/s
132
+ );
133
+
134
+ const projectName = nameMatch ? nameMatch[1] : "";
135
+ const description = descMatch ? descMatch[1] : "";
136
+
137
+ // Simple string-match for framework detection across the whole file
138
+ const frameworks: string[] = [];
139
+ for (const [key, label] of Object.entries(PYTHON_FRAMEWORKS)) {
140
+ if (raw.includes(key)) {
141
+ frameworks.push(label);
142
+ }
143
+ }
144
+
145
+ return { projectName, description, frameworks: dedupeOrdered(frameworks) };
146
+ }
147
+
148
+ export function parseCargoToml(filePath: string): SeedInfo | null {
149
+ const raw = readFile(filePath);
150
+ if (raw === null) return null;
151
+
152
+ // Extract [package] section name and description
153
+ const nameMatch = raw.match(/\[package\][^\[]*\bname\s*=\s*"([^"]+)"/s);
154
+ const descMatch = raw.match(
155
+ /\[package\][^\[]*\bdescription\s*=\s*"([^"]+)"/s
156
+ );
157
+
158
+ const projectName = nameMatch ? nameMatch[1] : "";
159
+ const description = descMatch ? descMatch[1] : "";
160
+
161
+ // Simple string-match across file
162
+ const frameworks: string[] = [];
163
+ for (const [key, label] of Object.entries(CARGO_FRAMEWORKS)) {
164
+ if (raw.includes(key)) {
165
+ frameworks.push(label);
166
+ }
167
+ }
168
+
169
+ return { projectName, description, frameworks: dedupeOrdered(frameworks) };
170
+ }
171
+
172
+ export function parseGoMod(filePath: string): SeedInfo | null {
173
+ const raw = readFile(filePath);
174
+ if (raw === null) return null;
175
+
176
+ // Extract module path — last path segment is the project name
177
+ const moduleMatch = raw.match(/^module\s+(\S+)/m);
178
+ const modulePath = moduleMatch ? moduleMatch[1] : "";
179
+ const projectName = modulePath ? basename(modulePath) : "";
180
+
181
+ // String-match require block for framework detection
182
+ const frameworks: string[] = [];
183
+ for (const [key, label] of Object.entries(GO_FRAMEWORKS)) {
184
+ if (raw.includes(key)) {
185
+ frameworks.push(label);
186
+ }
187
+ }
188
+
189
+ return {
190
+ projectName,
191
+ description: "",
192
+ frameworks: dedupeOrdered(frameworks),
193
+ };
194
+ }
195
+
196
+ // ─── Seed ─────────────────────────────────────────────────────────────────────
197
+
198
+ export function seedLearningMemory(projectRoot: string): LearningMemory {
199
+ const parsers: Array<() => SeedInfo | null> = [
200
+ () => parsePackageJson(join(projectRoot, "package.json")),
201
+ () => parsePyprojectToml(join(projectRoot, "pyproject.toml")),
202
+ () => parseCargoToml(join(projectRoot, "Cargo.toml")),
203
+ () => parseGoMod(join(projectRoot, "go.mod")),
204
+ ];
205
+
206
+ const infos: SeedInfo[] = parsers
207
+ .map((fn) => fn())
208
+ .filter((info): info is SeedInfo => info !== null);
209
+
210
+ // Pick first non-empty project name; fallback to directory basename
211
+ const projectName =
212
+ infos.find((i) => i.projectName)?.projectName ?? basename(projectRoot);
213
+
214
+ const mem = createEmptyLearningMemory(projectName);
215
+
216
+ // Add project description if available
217
+ const infoWithDesc = infos.find((i) => i.description);
218
+ if (infoWithDesc?.description) {
219
+ addEntry(
220
+ mem,
221
+ "Key Learnings",
222
+ `Project: ${projectName} — ${infoWithDesc.description}`
223
+ );
224
+ } else {
225
+ addEntry(mem, "Key Learnings", `Project: ${projectName}`);
226
+ }
227
+
228
+ // Collect all detected frameworks across all parsers
229
+ const allFrameworks = dedupeOrdered(infos.flatMap((i) => i.frameworks));
230
+ if (allFrameworks.length > 0) {
231
+ addEntry(
232
+ mem,
233
+ "Key Learnings",
234
+ `Detected frameworks: ${allFrameworks.join(", ")}`
235
+ );
236
+ }
237
+
238
+ return mem;
239
+ }
@@ -0,0 +1,128 @@
1
+ import { randomBytes } from "crypto";
2
+ import type {
3
+ SessionState,
4
+ SessionSummary,
5
+ } from "../types/session";
6
+
7
+ export function createSessionState(): SessionState {
8
+ const now = new Date().toISOString();
9
+ const suffix = randomBytes(2).toString("hex");
10
+ return {
11
+ sessionId: `${now}-${suffix}`,
12
+ startTimestamp: now,
13
+ stopCount: 0,
14
+ reads: {},
15
+ writes: [],
16
+ counters: {
17
+ fileIndexHits: 0,
18
+ fileIndexMisses: 0,
19
+ repeatedReadWarnings: 0,
20
+ learnedRuleWarnings: 0,
21
+ },
22
+ };
23
+ }
24
+
25
+ export function recordRead(
26
+ state: SessionState,
27
+ filePath: string,
28
+ estimatedTokens: number,
29
+ indexHit: boolean
30
+ ): void {
31
+ const existing = state.reads[filePath];
32
+ if (existing) {
33
+ existing.readCount++;
34
+ } else {
35
+ state.reads[filePath] = {
36
+ readCount: 1,
37
+ estimatedTokens,
38
+ firstReadAt: new Date().toISOString(),
39
+ };
40
+ }
41
+
42
+ if (indexHit) {
43
+ state.counters.fileIndexHits++;
44
+ } else {
45
+ state.counters.fileIndexMisses++;
46
+ }
47
+ }
48
+
49
+ export function recordWrite(
50
+ state: SessionState,
51
+ filePath: string,
52
+ action: "create" | "edit",
53
+ estimatedTokens: number
54
+ ): void {
55
+ state.writes.push({
56
+ filePath,
57
+ action,
58
+ estimatedTokens,
59
+ timestamp: new Date().toISOString(),
60
+ });
61
+ }
62
+
63
+ export function calculateSavings(state: SessionState): number {
64
+ const indexSavings = state.counters.fileIndexHits * 200;
65
+
66
+ let repeatedReadSavings = 0;
67
+ for (const read of Object.values(state.reads)) {
68
+ if (read.readCount > 1) {
69
+ repeatedReadSavings += (read.readCount - 1) * read.estimatedTokens;
70
+ }
71
+ }
72
+
73
+ return indexSavings + repeatedReadSavings;
74
+ }
75
+
76
+ export function buildSummary(state: SessionState): SessionSummary {
77
+ const reads = Object.entries(state.reads).map(([filePath, read]) => ({
78
+ filePath,
79
+ ...read,
80
+ }));
81
+
82
+ let totalTokens = 0;
83
+ for (const read of Object.values(state.reads)) {
84
+ totalTokens += read.estimatedTokens;
85
+ }
86
+ for (const write of state.writes) {
87
+ totalTokens += write.estimatedTokens;
88
+ }
89
+
90
+ let repeatedReads = 0;
91
+ for (const read of Object.values(state.reads)) {
92
+ if (read.readCount > 1) {
93
+ repeatedReads += read.readCount - 1;
94
+ }
95
+ }
96
+
97
+ return {
98
+ sessionId: state.sessionId,
99
+ startTimestamp: state.startTimestamp,
100
+ endTimestamp: new Date().toISOString(),
101
+ reads,
102
+ writes: state.writes,
103
+ totals: {
104
+ readCount: Object.keys(state.reads).length,
105
+ writeCount: state.writes.length,
106
+ estimatedTokens: totalTokens,
107
+ repeatedReads,
108
+ fileIndexHits: state.counters.fileIndexHits,
109
+ fileIndexMisses: state.counters.fileIndexMisses,
110
+ },
111
+ estimatedSavings: calculateSavings(state),
112
+ };
113
+ }
114
+
115
+ export function isSessionState(value: unknown): value is SessionState {
116
+ if (value === null || typeof value !== "object") return false;
117
+ const obj = value as Record<string, unknown>;
118
+ return (
119
+ typeof obj.sessionId === "string" &&
120
+ typeof obj.startTimestamp === "string" &&
121
+ typeof obj.stopCount === "number" &&
122
+ typeof obj.reads === "object" &&
123
+ obj.reads !== null &&
124
+ Array.isArray(obj.writes) &&
125
+ typeof obj.counters === "object" &&
126
+ obj.counters !== null
127
+ );
128
+ }
@@ -0,0 +1,13 @@
1
+ export async function readStdinJson(): Promise<unknown | null> {
2
+ try {
3
+ const chunks: Buffer[] = [];
4
+ for await (const chunk of process.stdin) {
5
+ chunks.push(chunk);
6
+ }
7
+ const text = Buffer.concat(chunks).toString("utf-8");
8
+ if (!text.trim()) return null;
9
+ return JSON.parse(text);
10
+ } catch {
11
+ return null;
12
+ }
13
+ }
@@ -0,0 +1,202 @@
1
+ import type { TaskDefinition } from "../types/scheduler";
2
+
3
+ // ── Built-in Task Definitions ───────────────────────────────────────────────
4
+
5
+ const BUILT_IN_TASKS: TaskDefinition[] = [
6
+ {
7
+ id: "file-index-rescan",
8
+ name: "File Index Rescan",
9
+ description: "Full project scan to update the file index",
10
+ schedule: "0 */6 * * *",
11
+ actionType: "function",
12
+ enabled: true,
13
+ retryPolicy: { maxAttempts: 3, baseDelayMs: 60_000 },
14
+ timeoutMs: 120_000,
15
+ },
16
+ {
17
+ id: "action-log-consolidation",
18
+ name: "Action Log Consolidation",
19
+ description: "Compress old sessions in the action log",
20
+ schedule: "0 2 * * *",
21
+ actionType: "function",
22
+ enabled: true,
23
+ retryPolicy: { maxAttempts: 3, baseDelayMs: 60_000 },
24
+ timeoutMs: 60_000,
25
+ },
26
+ {
27
+ id: "waste-detection",
28
+ name: "Waste Detection",
29
+ description: "Analyze token usage for waste patterns",
30
+ schedule: "0 0 * * 1",
31
+ actionType: "function",
32
+ enabled: true,
33
+ retryPolicy: { maxAttempts: 3, baseDelayMs: 60_000 },
34
+ timeoutMs: 120_000,
35
+ },
36
+ {
37
+ id: "learning-memory-reflection",
38
+ name: "Learning Memory Reflection",
39
+ description: "AI-assisted review and pruning of the learning memory",
40
+ schedule: "0 3 * * 0",
41
+ actionType: "ai-cli",
42
+ enabled: true,
43
+ retryPolicy: { maxAttempts: 3, baseDelayMs: 60_000 },
44
+ timeoutMs: 300_000,
45
+ },
46
+ {
47
+ id: "project-suggestions",
48
+ name: "Project Suggestions",
49
+ description: "AI-assisted analysis generating improvement suggestions",
50
+ schedule: "0 4 * * 1",
51
+ actionType: "ai-cli",
52
+ enabled: true,
53
+ retryPolicy: { maxAttempts: 3, baseDelayMs: 60_000 },
54
+ timeoutMs: 300_000,
55
+ },
56
+ ];
57
+
58
+ // ── Public API ──────────────────────────────────────────────────────────────
59
+
60
+ export function getBuiltInTasks(): TaskDefinition[] {
61
+ return BUILT_IN_TASKS;
62
+ }
63
+
64
+ export function getTaskById(id: string): TaskDefinition | undefined {
65
+ return BUILT_IN_TASKS.find((t) => t.id === id);
66
+ }
67
+
68
+ // ── AI CLI Execution ────────────────────────────────────────────────────────
69
+
70
+ const API_KEY_ENV_VARS = [
71
+ "ANTHROPIC_API_KEY",
72
+ "CLAUDE_API_KEY",
73
+ "OPENAI_API_KEY",
74
+ "OPENAI_KEY",
75
+ "AI_API_KEY",
76
+ ];
77
+
78
+ async function executeAiCli(
79
+ prompt: string,
80
+ timeoutMs: number
81
+ ): Promise<string> {
82
+ const env: Record<string, string> = {};
83
+ for (const [key, value] of Object.entries(process.env)) {
84
+ if (value !== undefined && !API_KEY_ENV_VARS.includes(key)) {
85
+ env[key] = value;
86
+ }
87
+ }
88
+
89
+ const proc = Bun.spawn(["claude", "--print", prompt], {
90
+ env,
91
+ stdout: "pipe",
92
+ stderr: "pipe",
93
+ });
94
+
95
+ const timer = setTimeout(() => {
96
+ proc.kill();
97
+ }, timeoutMs);
98
+
99
+ try {
100
+ const exitCode = await proc.exited;
101
+ clearTimeout(timer);
102
+
103
+ if (exitCode !== 0) {
104
+ const stderr = await new Response(proc.stderr).text();
105
+ throw new Error(`AI CLI exited with code ${exitCode}: ${stderr}`);
106
+ }
107
+
108
+ return await new Response(proc.stdout).text();
109
+ } catch (err) {
110
+ clearTimeout(timer);
111
+ if (err instanceof Error && err.message.includes("ENOENT")) {
112
+ throw new Error(
113
+ "AI CLI (claude) is not available. Install it or ensure it is on PATH."
114
+ );
115
+ }
116
+ throw err;
117
+ }
118
+ }
119
+
120
+ // ── Task Execution ──────────────────────────────────────────────────────────
121
+
122
+ export async function executeTask(
123
+ taskId: string,
124
+ projectCwd: string
125
+ ): Promise<void> {
126
+ const task = getTaskById(taskId);
127
+ if (!task) {
128
+ throw new Error(`Unknown task: ${taskId}`);
129
+ }
130
+
131
+ switch (taskId) {
132
+ case "file-index-rescan": {
133
+ const { scan } = await import("../commands/scan");
134
+ scan(projectCwd, { check: false });
135
+ break;
136
+ }
137
+
138
+ case "action-log-consolidation": {
139
+ const { actionLogPath, configPath } = await import("./paths");
140
+ const { consolidateLog } = await import("./action-log");
141
+ const { safeReadJson } = await import("./fs-utils");
142
+ const config = safeReadJson(configPath(projectCwd)) as {
143
+ actionLogMaxEntries?: number;
144
+ actionLogRetentionDays?: number;
145
+ } | null;
146
+ consolidateLog(actionLogPath(projectCwd), {
147
+ maxEntries: config?.actionLogMaxEntries ?? 200,
148
+ retentionDays: config?.actionLogRetentionDays ?? 7,
149
+ });
150
+ break;
151
+ }
152
+
153
+ case "waste-detection": {
154
+ const { detectWaste } = await import("../commands/detect-waste");
155
+ detectWaste(projectCwd);
156
+ break;
157
+ }
158
+
159
+ case "learning-memory-reflection": {
160
+ if (task.actionType === "ai-cli") {
161
+ try {
162
+ const { learningMemoryPath } = await import("./paths");
163
+ const { readFileSync } = await import("fs");
164
+ let memoryContent: string;
165
+ try {
166
+ memoryContent = readFileSync(
167
+ learningMemoryPath(projectCwd),
168
+ "utf-8"
169
+ );
170
+ } catch {
171
+ console.log("[mink] no learning memory found, skipping reflection");
172
+ return;
173
+ }
174
+ const prompt = `Review and suggest pruning for this learning memory. Remove duplicates and outdated entries. Return the cleaned markdown:\n\n${memoryContent}`;
175
+ await executeAiCli(prompt, task.timeoutMs);
176
+ } catch {
177
+ // Fall back to local reflection
178
+ console.log(
179
+ "[mink] AI CLI unavailable, falling back to local reflection"
180
+ );
181
+ }
182
+ }
183
+ // Always run local reflection (either as primary or fallback)
184
+ const { reflect } = await import("../commands/reflect");
185
+ const { learningMemoryPath, configPath, projectDir } = await import(
186
+ "./paths"
187
+ );
188
+ reflect(projectDir(projectCwd), learningMemoryPath(projectCwd), configPath(projectCwd));
189
+ break;
190
+ }
191
+
192
+ case "project-suggestions": {
193
+ console.log(
194
+ "[mink] project-suggestions: not yet implemented — skipping"
195
+ );
196
+ break;
197
+ }
198
+
199
+ default:
200
+ throw new Error(`No executor defined for task: ${taskId}`);
201
+ }
202
+ }
@@ -0,0 +1,36 @@
1
+ const CODE_EXTENSIONS = new Set([
2
+ ".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java",
3
+ ".c", ".cpp", ".h", ".hpp", ".cs", ".rb", ".php", ".swift",
4
+ ".kt", ".scala", ".sh", ".bash", ".zsh", ".sql", ".graphql",
5
+ ]);
6
+
7
+ const PROSE_EXTENSIONS = new Set([
8
+ ".md", ".mdx", ".txt", ".rst", ".adoc", ".tex",
9
+ ]);
10
+
11
+ const BINARY_EXTENSIONS = new Set([
12
+ ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico",
13
+ ".woff", ".woff2", ".ttf", ".eot",
14
+ ".mp3", ".mp4", ".webm", ".zip", ".tar", ".gz",
15
+ ".pdf", ".exe", ".dll", ".so", ".dylib",
16
+ ]);
17
+
18
+ export function isBinaryFile(filePath: string, content?: string): boolean {
19
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
20
+ if (BINARY_EXTENSIONS.has(ext)) return true;
21
+ if (content && content.includes("\0")) return true;
22
+ return false;
23
+ }
24
+
25
+ export function estimateTokens(content: string, filePath: string): number {
26
+ const ext = filePath.slice(filePath.lastIndexOf(".")).toLowerCase();
27
+ let ratio: number;
28
+ if (CODE_EXTENSIONS.has(ext)) {
29
+ ratio = 3.5;
30
+ } else if (PROSE_EXTENSIONS.has(ext)) {
31
+ ratio = 4.0;
32
+ } else {
33
+ ratio = 3.75;
34
+ }
35
+ return Math.ceil(content.length / ratio);
36
+ }