@drewpayment/mink 0.13.0-beta.1 → 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 (64) hide show
  1. package/README.md +4 -20
  2. package/dashboard/out/404.html +1 -1
  3. package/dashboard/out/action-log.html +1 -1
  4. package/dashboard/out/action-log.txt +1 -1
  5. package/dashboard/out/activity.html +1 -1
  6. package/dashboard/out/activity.txt +1 -1
  7. package/dashboard/out/bugs.html +1 -1
  8. package/dashboard/out/bugs.txt +1 -1
  9. package/dashboard/out/capture.html +1 -1
  10. package/dashboard/out/capture.txt +1 -1
  11. package/dashboard/out/config.html +1 -1
  12. package/dashboard/out/config.txt +1 -1
  13. package/dashboard/out/daemon.html +1 -1
  14. package/dashboard/out/daemon.txt +1 -1
  15. package/dashboard/out/design.html +1 -1
  16. package/dashboard/out/design.txt +1 -1
  17. package/dashboard/out/discord.html +1 -1
  18. package/dashboard/out/discord.txt +1 -1
  19. package/dashboard/out/file-index.html +1 -1
  20. package/dashboard/out/file-index.txt +1 -1
  21. package/dashboard/out/index.html +1 -1
  22. package/dashboard/out/index.txt +1 -1
  23. package/dashboard/out/insights.html +1 -1
  24. package/dashboard/out/insights.txt +1 -1
  25. package/dashboard/out/learning.html +1 -1
  26. package/dashboard/out/learning.txt +1 -1
  27. package/dashboard/out/overview.html +1 -1
  28. package/dashboard/out/overview.txt +1 -1
  29. package/dashboard/out/scheduler.html +1 -1
  30. package/dashboard/out/scheduler.txt +1 -1
  31. package/dashboard/out/sync.html +1 -1
  32. package/dashboard/out/sync.txt +1 -1
  33. package/dashboard/out/tokens.html +1 -1
  34. package/dashboard/out/tokens.txt +1 -1
  35. package/dashboard/out/waste.html +1 -1
  36. package/dashboard/out/waste.txt +1 -1
  37. package/dashboard/out/wiki.html +1 -1
  38. package/dashboard/out/wiki.txt +1 -1
  39. package/dist/cli.bun.js +1232 -904
  40. package/dist/cli.node.js +1251 -924
  41. package/package.json +1 -1
  42. package/src/cli.ts +17 -20
  43. package/src/commands/init.ts +14 -123
  44. package/src/commands/post-read.ts +18 -0
  45. package/src/commands/post-tool.ts +48 -0
  46. package/src/commands/retrieve.ts +32 -0
  47. package/src/core/code-skeleton.ts +108 -0
  48. package/src/core/compress-tool-output.ts +127 -0
  49. package/src/core/compression.ts +81 -0
  50. package/src/core/hook-output.ts +42 -0
  51. package/src/core/output-compression.ts +252 -0
  52. package/src/core/token-estimate.ts +40 -0
  53. package/src/repositories/compression-cache-repo.ts +97 -0
  54. package/src/repositories/token-ledger-repo.ts +87 -0
  55. package/src/storage/schema.ts +50 -1
  56. package/src/types/compression.ts +29 -0
  57. package/src/types/config.ts +40 -0
  58. package/src/types/hook-input.ts +4 -0
  59. package/src/types/token-ledger.ts +33 -0
  60. package/src/core/agent-detect.ts +0 -88
  61. package/src/core/agent-pi.ts +0 -314
  62. package/src/core/prompt.ts +0 -27
  63. /package/dashboard/out/_next/static/{UWfkbJY4zr9fSt7O-CAge → Yl3F-J4CwvYf6yWG-SSmG}/_buildManifest.js +0 -0
  64. /package/dashboard/out/_next/static/{UWfkbJY4zr9fSt7O-CAge → Yl3F-J4CwvYf6yWG-SSmG}/_ssgManifest.js +0 -0
@@ -0,0 +1,29 @@
1
+ // Tool-output compression types (spec 21). The decision/config types live in
2
+ // src/core/compression.ts; these describe the reversible cache and the engine's
3
+ // content-aware output.
4
+
5
+ // What kind of tool output we detected, which selects the compressor and is
6
+ // recorded on the ledger event for later analysis.
7
+ export type ContentKind = "search" | "log" | "file" | "json" | "text";
8
+
9
+ // One stored original, retrievable byte-exact via `mink retrieve <token>` until
10
+ // it expires.
11
+ export interface CompressionCacheEntry {
12
+ token: string;
13
+ createdAt: string;
14
+ expiresAt: string;
15
+ toolName: string;
16
+ contentKind: ContentKind;
17
+ content: string;
18
+ sizeBytes: number;
19
+ }
20
+
21
+ // The result of compressing one output. `compressed` is the body the model will
22
+ // see (sans retrieval affordance, which the pipeline appends); `omittedNote`
23
+ // summarises what was dropped. A compressor returns null when it has nothing
24
+ // worth substituting.
25
+ export interface CompressionResult {
26
+ kind: ContentKind;
27
+ compressed: string;
28
+ omittedNote: string;
29
+ }
@@ -18,6 +18,11 @@ export interface GlobalConfig {
18
18
  "cli.auto-update-schedule"?: string;
19
19
  "cli.auto-update-package-manager"?: string;
20
20
  "projects.identity"?: string;
21
+ "compression.enabled"?: string;
22
+ "compression.threshold-tokens"?: string;
23
+ "compression.min-savings-ratio"?: string;
24
+ "compression.holdout-fraction"?: string;
25
+ "compression.retention-hours"?: string;
21
26
  }
22
27
 
23
28
  export type ConfigKey = keyof GlobalConfig & string;
@@ -179,6 +184,41 @@ export const CONFIG_KEYS: ConfigKeyMeta[] = [
179
184
  "Project identity strategy: path-derived (legacy) or git-remote (stable across machines)",
180
185
  scope: "shared",
181
186
  },
187
+ {
188
+ key: "compression.enabled",
189
+ default: "false",
190
+ envVar: "MINK_COMPRESSION_ENABLED",
191
+ description: "Enable tool-output compression (spec 21). Off until inline compression ships.",
192
+ scope: "shared",
193
+ },
194
+ {
195
+ key: "compression.threshold-tokens",
196
+ default: "800",
197
+ envVar: "MINK_COMPRESSION_THRESHOLD_TOKENS",
198
+ description: "Minimum estimated token size before a tool output is eligible for compression",
199
+ scope: "shared",
200
+ },
201
+ {
202
+ key: "compression.min-savings-ratio",
203
+ default: "0.25",
204
+ envVar: "MINK_COMPRESSION_MIN_SAVINGS_RATIO",
205
+ description: "Discard a compression attempt unless it saves at least this fraction of tokens",
206
+ scope: "shared",
207
+ },
208
+ {
209
+ key: "compression.holdout-fraction",
210
+ default: "0.1",
211
+ envVar: "MINK_COMPRESSION_HOLDOUT_FRACTION",
212
+ description: "Fraction of eligible outputs left uncompressed as a measured control group",
213
+ scope: "shared",
214
+ },
215
+ {
216
+ key: "compression.retention-hours",
217
+ default: "168",
218
+ envVar: "MINK_COMPRESSION_RETENTION_HOURS",
219
+ description: "How long compressed originals stay retrievable before eviction",
220
+ scope: "shared",
221
+ },
182
222
  ];
183
223
 
184
224
  const VALID_KEYS = new Set<string>(CONFIG_KEYS.map((k) => k.key));
@@ -19,6 +19,10 @@ export interface PostToolUseInput {
19
19
  // Edit tool
20
20
  old_string?: string;
21
21
  new_string?: string;
22
+ // Read tool — present for ranged reads; their output is a slice, so we
23
+ // don't substitute a whole-file summary for them (spec 21 edge case).
24
+ offset?: number;
25
+ limit?: number;
22
26
  };
23
27
  // Legacy / older hook payload shape — kept for backward compatibility.
24
28
  tool_output?: {
@@ -41,3 +41,36 @@ export interface TokenLedger {
41
41
  sessions: LedgerSession[];
42
42
  wasteFlags?: WasteFlag[];
43
43
  }
44
+
45
+ // Tool-output compression measurement (spec 21).
46
+
47
+ // What the caller supplies when recording a compression decision. `id` and
48
+ // `createdAt` are generated when omitted. For a holdout arm, pass the original
49
+ // output unchanged so `compressedTokens === originalTokens` and `holdout: true`.
50
+ export interface CompressionEventInput {
51
+ toolName: string;
52
+ contentKind: string;
53
+ originalTokens: number;
54
+ compressedTokens: number;
55
+ holdout: boolean;
56
+ id?: string;
57
+ createdAt?: string;
58
+ }
59
+
60
+ export interface CompressionEvent {
61
+ id: string;
62
+ createdAt: string;
63
+ toolName: string;
64
+ contentKind: string;
65
+ originalTokens: number;
66
+ compressedTokens: number;
67
+ holdout: boolean;
68
+ }
69
+
70
+ export interface CompressionLifetime {
71
+ totalEvents: number;
72
+ totalHoldoutEvents: number;
73
+ totalOriginalTokens: number;
74
+ totalCompressedTokens: number;
75
+ totalMeasuredSavings: number;
76
+ }
@@ -1,88 +0,0 @@
1
- import { execSync } from "child_process";
2
- import { existsSync } from "fs";
3
- import { join } from "path";
4
- import { homedir } from "os";
5
-
6
- // Supported host coding assistants Mink can attach to. Adding a new host is a
7
- // matter of appending an entry here plus an installer in init.ts.
8
- export type AgentId = "claude" | "pi";
9
-
10
- export interface AgentMeta {
11
- id: AgentId;
12
- label: string;
13
- /** Project-local config directory that signals the host is used here. */
14
- projectDir: string;
15
- /** Per-user global config directory that signals the host is installed. */
16
- globalDir: string;
17
- /** Executable name to probe on PATH. */
18
- bin: string;
19
- }
20
-
21
- export const AGENTS: AgentMeta[] = [
22
- {
23
- id: "claude",
24
- label: "Claude Code",
25
- projectDir: ".claude",
26
- globalDir: join(homedir(), ".claude"),
27
- bin: "claude",
28
- },
29
- {
30
- id: "pi",
31
- label: "Pi",
32
- projectDir: ".pi",
33
- globalDir: join(homedir(), ".pi"),
34
- bin: "pi",
35
- },
36
- ];
37
-
38
- export interface AgentInfo extends AgentMeta {
39
- detected: boolean;
40
- /** Human-readable reasons the host was (or was not) detected. */
41
- signals: string[];
42
- }
43
-
44
- function commandExists(bin: string): boolean {
45
- try {
46
- const probe = process.platform === "win32" ? `where ${bin}` : `command -v ${bin}`;
47
- execSync(probe, { stdio: "ignore" });
48
- return true;
49
- } catch {
50
- return false;
51
- }
52
- }
53
-
54
- /**
55
- * Inspect a single host's footprint relative to `cwd`. Detection is best-effort
56
- * and layered, strongest signal first: a project-local config directory is the
57
- * clearest sign the host is actually used here; a global config directory or a
58
- * binary on PATH only prove the host is installed somewhere.
59
- */
60
- export function detectAgent(meta: AgentMeta, cwd: string): AgentInfo {
61
- const signals: string[] = [];
62
- if (existsSync(join(cwd, meta.projectDir))) {
63
- signals.push(`project config (${meta.projectDir}/)`);
64
- }
65
- if (existsSync(meta.globalDir)) {
66
- signals.push("global config");
67
- }
68
- if (commandExists(meta.bin)) {
69
- signals.push("on PATH");
70
- }
71
- return { ...meta, detected: signals.length > 0, signals };
72
- }
73
-
74
- export function detectAgents(cwd: string): AgentInfo[] {
75
- return AGENTS.map((m) => detectAgent(m, cwd));
76
- }
77
-
78
- export function resolveTargetsFromFlag(flag: string): AgentId[] {
79
- const normalized = flag.trim().toLowerCase();
80
- if (normalized === "all") return AGENTS.map((a) => a.id);
81
- const ids = normalized
82
- .split(",")
83
- .map((s) => s.trim())
84
- .filter(Boolean);
85
- const valid = AGENTS.map((a) => a.id) as string[];
86
- const resolved = ids.filter((id): id is AgentId => valid.includes(id));
87
- return resolved;
88
- }
@@ -1,314 +0,0 @@
1
- import { join, resolve, dirname } from "path";
2
- import { existsSync, mkdirSync, copyFileSync, rmSync } from "fs";
3
- import { atomicWriteText } from "./fs-utils";
4
-
5
- // ── Paths ───────────────────────────────────────────────────────────────────
6
- // Pi auto-discovers extensions from `.pi/extensions/*.ts` and skills from
7
- // `.pi/skills/*/SKILL.md`, so simply writing these files wires Mink in — no
8
- // `.pi/settings.json` edit required, which keeps us from clobbering the user's
9
- // own Pi configuration.
10
-
11
- export function piExtensionPath(cwd: string): string {
12
- return join(cwd, ".pi", "extensions", "mink.ts");
13
- }
14
-
15
- export function piGuidanceSkillPath(cwd: string): string {
16
- return join(cwd, ".pi", "skills", "mink", "SKILL.md");
17
- }
18
-
19
- export function piNoteSkillPath(cwd: string): string {
20
- return join(cwd, ".pi", "skills", "mink-note", "SKILL.md");
21
- }
22
-
23
- // ── Extension source ─────────────────────────────────────────────────────────
24
-
25
- /**
26
- * Generate the Pi adapter extension. Like the Claude hook wiring, the `mink`
27
- * invocation is templated: an installed package resolves the portable `mink`
28
- * bin shim (so a committed `.pi/` works across machines), while source-dev mode
29
- * falls back to `bun run <abs cli.ts>`.
30
- *
31
- * The adapter shells out to the same `mink` lifecycle commands every other host
32
- * uses, translating Pi's event/tool shapes into Mink's canonical payload. It is
33
- * deliberately defensive: it never throws into Pi, never blocks a tool, and
34
- * times out fast — matching the safety contract of the Claude hooks.
35
- */
36
- export function buildPiExtension(cliPath: string): string {
37
- const isTsSource = cliPath.endsWith(".ts");
38
- const cmd = isTsSource ? "bun" : "mink";
39
- const baseArgs = isTsSource ? JSON.stringify(["run", cliPath]) : "[]";
40
-
41
- return `// AUTO-GENERATED by \`mink init\`. Do not edit — re-run \`mink init\` to refresh.
42
- //
43
- // Mink adapter for the Pi coding agent. Routes Pi lifecycle and tool events
44
- // into the \`mink\` CLI so Pi shares the same ~/.mink state, file index, ledger,
45
- // and wiki as every other assistant wired to this project.
46
- //
47
- // Field shapes confirmed against pi source (earendil-works/pi,
48
- // packages/coding-agent): read params {path, offset, limit}; write {path,
49
- // content}; edit {path, edits:[{oldText,newText}]} (with legacy file_path /
50
- // top-level oldText,newText). tool_call/tool_result events expose toolName,
51
- // toolCallId, input; tool_result also exposes content (a content-block array),
52
- // details, isError. Advisories are surfaced by returning a modified result from
53
- // the tool_result handler — the documented mechanism. pi.exec has no stdin, so
54
- // the canonical payload is piped to \`mink\` via a spawned child process. Every
55
- // host-API access is defensive: the adapter never throws into Pi or blocks a
56
- // tool — a failure degrades to "no advisory".
57
-
58
- import { spawn } from "node:child_process";
59
-
60
- const MINK_CMD = ${JSON.stringify(cmd)};
61
- const MINK_BASE_ARGS = ${baseArgs};
62
- const TIMEOUT_MS = 5000;
63
-
64
- function runMink(sub, payload, cwd) {
65
- return new Promise((res) => {
66
- let done = false;
67
- const finish = (out) => {
68
- if (done) return;
69
- done = true;
70
- res(out);
71
- };
72
- try {
73
- const child = spawn(MINK_CMD, [...MINK_BASE_ARGS, sub], {
74
- cwd,
75
- stdio: ["pipe", "ignore", "pipe"],
76
- });
77
- let stderr = "";
78
- child.stderr?.on("data", (d) => {
79
- stderr += d.toString();
80
- });
81
- child.on("error", () => finish(""));
82
- child.on("close", () => finish(stderr.trim()));
83
- const timer = setTimeout(() => {
84
- try {
85
- child.kill();
86
- } catch {}
87
- finish("");
88
- }, TIMEOUT_MS);
89
- timer.unref?.();
90
- try {
91
- child.stdin?.end(payload ? JSON.stringify(payload) : "");
92
- } catch {}
93
- } catch {
94
- finish("");
95
- }
96
- });
97
- }
98
-
99
- // Pi's edit tool takes an array of { oldText, newText } replacements (legacy
100
- // inputs may put oldText/newText/new_string at the top level). Concatenate the
101
- // replacement text so Mink's write-enforcement sees everything being written.
102
- function editNewText(input) {
103
- if (Array.isArray(input.edits)) {
104
- return input.edits
105
- .map((e) => e?.newText ?? e?.new_string ?? "")
106
- .filter(Boolean)
107
- .join("\\n");
108
- }
109
- return input.newText ?? input.new_string ?? input.replacement ?? "";
110
- }
111
-
112
- // Resolve Pi's tool name + arguments to Mink's canonical operation. Only the
113
- // three file operations matter; anything else returns null and is ignored.
114
- function toolInfo(event) {
115
- const name = String(event?.toolName ?? event?.tool ?? event?.name ?? "").toLowerCase();
116
- const input = event?.input ?? event?.arguments ?? {};
117
- const filePath = input.path ?? input.file_path ?? input.filePath;
118
- if (!filePath) return null;
119
- if (name === "read") return { op: "read", filePath };
120
- if (name === "write") return { op: "write", filePath, content: input.content ?? input.text ?? "" };
121
- if (name === "edit") return { op: "edit", filePath, newString: editNewText(input) };
122
- return null;
123
- }
124
-
125
- // A tool_result's content is an array of content blocks ({ type, text }); pull
126
- // the text out so Mink's post-read can estimate tokens from real content.
127
- function resultContent(event) {
128
- const c = event?.content;
129
- if (typeof c === "string") return c;
130
- if (Array.isArray(c)) {
131
- const text = c
132
- .map((b) => (typeof b === "string" ? b : b?.text ?? ""))
133
- .filter(Boolean)
134
- .join("\\n");
135
- return text || null;
136
- }
137
- return null;
138
- }
139
-
140
- function prePayload(info) {
141
- if (info.op === "read")
142
- return { sub: "pre-read", payload: { tool_name: "Read", tool_input: { file_path: info.filePath } } };
143
- if (info.op === "write")
144
- return {
145
- sub: "pre-write",
146
- payload: { tool_name: "Write", tool_input: { file_path: info.filePath, content: info.content } },
147
- };
148
- return {
149
- sub: "pre-write",
150
- payload: { tool_name: "Edit", tool_input: { file_path: info.filePath, new_string: info.newString } },
151
- };
152
- }
153
-
154
- function postPayload(info, content) {
155
- if (info.op === "read")
156
- return {
157
- sub: "post-read",
158
- payload: {
159
- tool_name: "Read",
160
- tool_input: { file_path: info.filePath },
161
- tool_output: content == null ? undefined : { content },
162
- },
163
- };
164
- if (info.op === "write")
165
- return {
166
- sub: "post-write",
167
- payload: { tool_name: "Write", tool_input: { file_path: info.filePath, content: info.content } },
168
- };
169
- return {
170
- sub: "post-write",
171
- payload: { tool_name: "Edit", tool_input: { file_path: info.filePath, new_string: info.newString } },
172
- };
173
- }
174
-
175
- export default function (pi) {
176
- const cwd = pi?.ctx?.cwd || process.cwd();
177
- const pending = new Map();
178
- const keyOf = (event) => event?.toolCallId ?? event?.id ?? event?.callId ?? "";
179
-
180
- pi.on?.("session_start", (event) => {
181
- // Skip hot-reloads so an extension reload mid-task doesn't reset the
182
- // ephemeral session state; every genuinely new/resumed session starts fresh.
183
- if (event?.reason === "reload") return;
184
- void runMink("session-start", null, cwd);
185
- });
186
- pi.on?.("agent_end", () => {
187
- void runMink("session-stop", null, cwd);
188
- });
189
- pi.on?.("session_shutdown", () => {
190
- void runMink("session-stop", null, cwd);
191
- });
192
-
193
- pi.on?.("tool_call", async (event) => {
194
- const info = toolInfo(event);
195
- if (!info) return;
196
- const { sub, payload } = prePayload(info);
197
- const advisory = await runMink(sub, payload, cwd);
198
- if (advisory) pending.set(keyOf(event), advisory);
199
- });
200
-
201
- pi.on?.("tool_result", async (event) => {
202
- const info = toolInfo(event);
203
- if (!info) return;
204
- const { sub, payload } = postPayload(info, resultContent(event));
205
- const post = await runMink(sub, payload, cwd);
206
- const pre = pending.get(keyOf(event)) ?? "";
207
- pending.delete(keyOf(event));
208
- const advisory = [pre, post].filter(Boolean).join("\\n");
209
- if (!advisory) return;
210
-
211
- // Surface Mink's advisory into the model's context by appending a text
212
- // block to the tool result — the parity of Claude feeding hook stderr back.
213
- const base = Array.isArray(event.content)
214
- ? event.content
215
- : typeof event.content === "string"
216
- ? [{ type: "text", text: event.content }]
217
- : [];
218
- return {
219
- content: [...base, { type: "text", text: advisory }],
220
- details: event.details,
221
- isError: event.isError,
222
- };
223
- });
224
- }
225
- `;
226
- }
227
-
228
- // ── Guidance skill ───────────────────────────────────────────────────────────
229
- // Pi has no automatic project-rules file (CLAUDE.md / AGENTS.md equivalent), so
230
- // the guidance Mink gives the assistant is delivered as a Pi skill instead.
231
-
232
- const PI_GUIDANCE_SKILL = `---
233
- name: mink
234
- description: Mink context management is active in this project. Read this to understand how Mink memory, write enforcement, and note capture work under Pi.
235
- ---
236
-
237
- # Mink — context management for this project
238
-
239
- This project uses **Mink** (\`@drewpayment/mink\`) for cross-session context management.
240
-
241
- ## How it works
242
- - Mink runs automatically through a Pi extension at \`.pi/extensions/mink.ts\` that hooks session start/stop and every read/edit/write tool call.
243
- - All state lives in \`~/.mink/\` on the user's machine — **not** in this repository. Do not create or write to any in-repo state directory (no \`.wolf/\`, \`.mink/\`, etc.).
244
- - Read intelligence, write enforcement, bug memory, and the token ledger are handled by the extension. You do not need to manually read or update any state files.
245
- - Mink shares one \`~/.mink/\` state across every assistant wired to this project, so history is unified whether the user runs Pi or another assistant.
246
-
247
- ## When to act on Mink
248
- - If the user asks to "save a note", "remember this", "log this to my wiki", or similar, use the \`mink-note\` skill (\`/skill:mink-note\`) — it captures into the user's \`~/.mink/\` vault.
249
- - If the extension surfaces a learning, past bug, or repeat-read warning in context, treat that as authoritative project memory and follow it.
250
- - The \`mink dashboard\` and \`mink agent\` commands are user tools — do not invoke them on the user's behalf.
251
- `;
252
-
253
- function resolveSkillsSourceDir(): string {
254
- // Walk up until we find a package root that contains skills/ — works from
255
- // src/core/agent-pi.ts (dev) and dist/cli.js (installed), which sit at
256
- // different depths relative to the package root.
257
- let dir = dirname(new URL(import.meta.url).pathname);
258
- while (true) {
259
- if (existsSync(join(dir, "package.json")) && existsSync(join(dir, "skills"))) {
260
- return join(dir, "skills");
261
- }
262
- const parent = dirname(dir);
263
- if (parent === dir) break;
264
- dir = parent;
265
- }
266
- return resolve(dirname(new URL(import.meta.url).pathname), "../../skills");
267
- }
268
-
269
- export interface PiInstallResult {
270
- extensionPath: string;
271
- guidancePath: string;
272
- notePath: string | null;
273
- }
274
-
275
- export function installPi(cwd: string, cliPath: string): PiInstallResult {
276
- const extensionPath = piExtensionPath(cwd);
277
- mkdirSync(dirname(extensionPath), { recursive: true });
278
- atomicWriteText(extensionPath, buildPiExtension(cliPath));
279
-
280
- const guidancePath = piGuidanceSkillPath(cwd);
281
- mkdirSync(dirname(guidancePath), { recursive: true });
282
- atomicWriteText(guidancePath, PI_GUIDANCE_SKILL);
283
-
284
- // Mirror the note-capture skill into Pi's skill directory so the single
285
- // source of truth (skills/mink-note) is reused rather than duplicated.
286
- let notePath: string | null = null;
287
- try {
288
- const src = join(resolveSkillsSourceDir(), "mink-note", "SKILL.md");
289
- if (existsSync(src)) {
290
- notePath = piNoteSkillPath(cwd);
291
- mkdirSync(dirname(notePath), { recursive: true });
292
- copyFileSync(src, notePath);
293
- }
294
- } catch {
295
- // Note skill is non-critical — the extension and guidance still work.
296
- notePath = null;
297
- }
298
-
299
- return { extensionPath, guidancePath, notePath };
300
- }
301
-
302
- export function removePi(cwd: string): void {
303
- for (const p of [
304
- piExtensionPath(cwd),
305
- join(cwd, ".pi", "skills", "mink"),
306
- join(cwd, ".pi", "skills", "mink-note"),
307
- ]) {
308
- try {
309
- rmSync(p, { recursive: true, force: true });
310
- } catch {
311
- // best-effort
312
- }
313
- }
314
- }
@@ -1,27 +0,0 @@
1
- import { createInterface } from "readline";
2
-
3
- /**
4
- * Whether the current process can safely prompt the user. We require a real
5
- * interactive TTY on both stdin and stdout and honor the usual escape hatches
6
- * (CI, an explicit opt-out) so `mink init` never blocks a script or pipeline.
7
- */
8
- export function stdinIsInteractive(): boolean {
9
- const stdin = process.stdin as NodeJS.ReadStream & { isTTY?: boolean };
10
- const stdout = process.stdout as NodeJS.WriteStream & { isTTY?: boolean };
11
- return (
12
- Boolean(stdin.isTTY) &&
13
- Boolean(stdout.isTTY) &&
14
- process.env.MINK_NO_PROMPT !== "1" &&
15
- !process.env.CI
16
- );
17
- }
18
-
19
- export function ask(question: string): Promise<string> {
20
- return new Promise((resolve) => {
21
- const rl = createInterface({ input: process.stdin, output: process.stdout });
22
- rl.question(question, (answer) => {
23
- rl.close();
24
- resolve(answer);
25
- });
26
- });
27
- }