@drewpayment/mink 0.6.1 → 0.8.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 (48) hide show
  1. package/README.md +19 -0
  2. package/agents/mink-agent.md.tmpl +84 -0
  3. package/dashboard/out/404.html +1 -1
  4. package/dashboard/out/action-log.html +1 -1
  5. package/dashboard/out/action-log.txt +1 -1
  6. package/dashboard/out/activity.html +1 -1
  7. package/dashboard/out/activity.txt +1 -1
  8. package/dashboard/out/bugs.html +1 -1
  9. package/dashboard/out/bugs.txt +1 -1
  10. package/dashboard/out/capture.html +1 -1
  11. package/dashboard/out/capture.txt +1 -1
  12. package/dashboard/out/config.html +1 -1
  13. package/dashboard/out/config.txt +1 -1
  14. package/dashboard/out/daemon.html +1 -1
  15. package/dashboard/out/daemon.txt +1 -1
  16. package/dashboard/out/design.html +1 -1
  17. package/dashboard/out/design.txt +1 -1
  18. package/dashboard/out/discord.html +1 -1
  19. package/dashboard/out/discord.txt +1 -1
  20. package/dashboard/out/file-index.html +1 -1
  21. package/dashboard/out/file-index.txt +1 -1
  22. package/dashboard/out/index.html +1 -1
  23. package/dashboard/out/index.txt +1 -1
  24. package/dashboard/out/insights.html +1 -1
  25. package/dashboard/out/insights.txt +1 -1
  26. package/dashboard/out/learning.html +1 -1
  27. package/dashboard/out/learning.txt +1 -1
  28. package/dashboard/out/overview.html +1 -1
  29. package/dashboard/out/overview.txt +1 -1
  30. package/dashboard/out/scheduler.html +1 -1
  31. package/dashboard/out/scheduler.txt +1 -1
  32. package/dashboard/out/sync.html +1 -1
  33. package/dashboard/out/sync.txt +1 -1
  34. package/dashboard/out/tokens.html +1 -1
  35. package/dashboard/out/tokens.txt +1 -1
  36. package/dashboard/out/waste.html +1 -1
  37. package/dashboard/out/waste.txt +1 -1
  38. package/dashboard/out/wiki.html +1 -1
  39. package/dashboard/out/wiki.txt +1 -1
  40. package/dist/cli.js +804 -376
  41. package/package.json +2 -1
  42. package/src/cli.ts +8 -1
  43. package/src/commands/agent.ts +245 -0
  44. package/src/commands/daemon.ts +12 -1
  45. package/src/commands/init.ts +27 -0
  46. package/src/core/daemon-service.ts +227 -0
  47. /package/dashboard/out/_next/static/{FiL3S_40BA764FL66DRZV → EC-_8nIOf1GnPrIqZ7Mk3}/_buildManifest.js +0 -0
  48. /package/dashboard/out/_next/static/{FiL3S_40BA764FL66DRZV → EC-_8nIOf1GnPrIqZ7Mk3}/_ssgManifest.js +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drewpayment/mink",
3
- "version": "0.6.1",
3
+ "version": "0.8.0",
4
4
  "description": "A hidden presence that moves alongside the developer — token efficiency and cross-project wiki for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -19,6 +19,7 @@
19
19
  "src/**/*.ts",
20
20
  "dist/cli.js",
21
21
  "skills/**/*",
22
+ "agents/**/*",
22
23
  "dashboard/out"
23
24
  ],
24
25
  "publishConfig": {
package/src/cli.ts CHANGED
@@ -143,6 +143,12 @@ switch (command) {
143
143
  break;
144
144
  }
145
145
 
146
+ case "agent": {
147
+ const { agent } = await import("./commands/agent");
148
+ await agent(cwd, process.argv.slice(3));
149
+ break;
150
+ }
151
+
146
152
  case "sync": {
147
153
  const { sync } = await import("./commands/sync");
148
154
  await sync(process.argv.slice(3));
@@ -213,6 +219,7 @@ switch (command) {
213
219
  console.log(" note list [filters] List notes (--category, --tag, --recent)");
214
220
  console.log(" note search <term> Full-text search across the vault");
215
221
  console.log(" skill install Install /mink:note skill for Claude Code");
222
+ console.log(" agent Open a Claude Code session with the mink-agent persona");
216
223
  console.log();
217
224
  console.log("Devices & Sync:");
218
225
  console.log(" device Show current device info");
@@ -235,7 +242,7 @@ switch (command) {
235
242
  console.log();
236
243
  console.log("Automation & Analysis:");
237
244
  console.log(" dashboard [--port=N] Open the real-time web dashboard");
238
- console.log(" daemon <cmd> Manage the background daemon (start|stop|restart|logs)");
245
+ console.log(" daemon <cmd> Manage the background daemon (start|stop|restart|logs|install|uninstall)");
239
246
  console.log(" cron <cmd> [id] Manage scheduled tasks (list|run|retry)");
240
247
  console.log(" update [options] Update Mink across registered projects");
241
248
  console.log(" restore [backup] Restore state from a backup");
@@ -0,0 +1,245 @@
1
+ import { join, resolve, dirname } from "path";
2
+ import { homedir } from "os";
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ readFileSync,
7
+ writeFileSync,
8
+ } from "fs";
9
+ import { createHash } from "crypto";
10
+ import { spawnSync } from "child_process";
11
+ import { minkRoot } from "../core/paths";
12
+ import { resolveVaultPath } from "../core/vault";
13
+
14
+ const AGENT_NAME = "mink-agent";
15
+ const TEMPLATE_FILE = `${AGENT_NAME}.md.tmpl`;
16
+ const INSTALLED_FILE = `${AGENT_NAME}.md`;
17
+
18
+ function getAgentTemplatePath(): string {
19
+ let dir = dirname(new URL(import.meta.url).pathname);
20
+ while (true) {
21
+ if (
22
+ existsSync(join(dir, "package.json")) &&
23
+ existsSync(join(dir, "agents", TEMPLATE_FILE))
24
+ ) {
25
+ return join(dir, "agents", TEMPLATE_FILE);
26
+ }
27
+ const parent = dirname(dir);
28
+ if (parent === dir) break;
29
+ dir = parent;
30
+ }
31
+ return resolve(
32
+ dirname(new URL(import.meta.url).pathname),
33
+ "../../agents",
34
+ TEMPLATE_FILE
35
+ );
36
+ }
37
+
38
+ function getMinkVersion(): string {
39
+ let dir = dirname(new URL(import.meta.url).pathname);
40
+ while (true) {
41
+ const pkgPath = join(dir, "package.json");
42
+ if (existsSync(pkgPath)) {
43
+ try {
44
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
45
+ if (pkg.name && pkg.version) return pkg.version;
46
+ } catch {
47
+ // fall through
48
+ }
49
+ }
50
+ const parent = dirname(dir);
51
+ if (parent === dir) break;
52
+ dir = parent;
53
+ }
54
+ return "unknown";
55
+ }
56
+
57
+ function renderTemplate(template: string, vars: Record<string, string>): string {
58
+ let out = template;
59
+ for (const [key, value] of Object.entries(vars)) {
60
+ out = out.split(`{{${key}}}`).join(value);
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function sha256(text: string): string {
66
+ return createHash("sha256").update(text).digest("hex");
67
+ }
68
+
69
+ function claudeAgentsDir(): string {
70
+ return join(homedir(), ".claude", "agents");
71
+ }
72
+
73
+ function installedAgentPath(): string {
74
+ return join(claudeAgentsDir(), INSTALLED_FILE);
75
+ }
76
+
77
+ interface InstallResult {
78
+ action: "installed" | "updated" | "unchanged" | "skipped";
79
+ path: string;
80
+ }
81
+
82
+ function installAgentDefinition(opts: { force: boolean; skip: boolean }): InstallResult {
83
+ const templatePath = getAgentTemplatePath();
84
+ if (!existsSync(templatePath)) {
85
+ throw new Error(
86
+ `[mink agent] bundled agent template not found at ${templatePath}\n` +
87
+ " This usually means the package was installed without bundled assets."
88
+ );
89
+ }
90
+
91
+ const installed = installedAgentPath();
92
+
93
+ if (opts.skip && existsSync(installed)) {
94
+ return { action: "skipped", path: installed };
95
+ }
96
+
97
+ const template = readFileSync(templatePath, "utf-8");
98
+ const rendered = renderTemplate(template, {
99
+ MINK_ROOT: minkRoot(),
100
+ VAULT_PATH: resolveVaultPath(),
101
+ MINK_VERSION: getMinkVersion(),
102
+ });
103
+
104
+ const exists = existsSync(installed);
105
+ if (!opts.force && exists) {
106
+ const current = readFileSync(installed, "utf-8");
107
+ if (sha256(current) === sha256(rendered)) {
108
+ return { action: "unchanged", path: installed };
109
+ }
110
+ }
111
+
112
+ mkdirSync(claudeAgentsDir(), { recursive: true });
113
+ writeFileSync(installed, rendered);
114
+ return {
115
+ action: exists ? "updated" : "installed",
116
+ path: installed,
117
+ };
118
+ }
119
+
120
+ function isClaudeOnPath(): boolean {
121
+ const result = spawnSync("claude", ["--version"], {
122
+ stdio: "ignore",
123
+ });
124
+ return !result.error && result.status === 0;
125
+ }
126
+
127
+ interface ParsedArgs {
128
+ noUpdate: boolean;
129
+ reinstall: boolean;
130
+ passthrough: string[];
131
+ showHelp: boolean;
132
+ }
133
+
134
+ function parseArgs(args: string[]): ParsedArgs {
135
+ const out: ParsedArgs = {
136
+ noUpdate: false,
137
+ reinstall: false,
138
+ passthrough: [],
139
+ showHelp: false,
140
+ };
141
+ let inPassthrough = false;
142
+ for (const arg of args) {
143
+ if (inPassthrough) {
144
+ out.passthrough.push(arg);
145
+ continue;
146
+ }
147
+ if (arg === "--") {
148
+ inPassthrough = true;
149
+ continue;
150
+ }
151
+ if (arg === "--no-update") {
152
+ out.noUpdate = true;
153
+ continue;
154
+ }
155
+ if (arg === "--reinstall") {
156
+ out.reinstall = true;
157
+ continue;
158
+ }
159
+ if (arg === "--help" || arg === "-h") {
160
+ out.showHelp = true;
161
+ continue;
162
+ }
163
+ out.passthrough.push(arg);
164
+ }
165
+ return out;
166
+ }
167
+
168
+ function printHelp(): void {
169
+ console.log("Usage: mink agent [options] [-- <claude args...>]");
170
+ console.log();
171
+ console.log("Open an interactive Claude Code session in your mink home with");
172
+ console.log("the mink-agent persona — a proactive note/wiki assistant.");
173
+ console.log();
174
+ console.log("Options:");
175
+ console.log(" --no-update Don't refresh ~/.claude/agents/mink-agent.md if it exists");
176
+ console.log(" --reinstall Force overwrite the installed agent definition");
177
+ console.log(" -- <args> Forward remaining arguments to `claude`");
178
+ console.log();
179
+ console.log("Environment:");
180
+ console.log(" MINK_AGENT_NO_UPDATE=1 Equivalent to --no-update");
181
+ console.log();
182
+ console.log("The agent is bound to your mink root and resolved vault path. Changing");
183
+ console.log("`mink config wiki.path` triggers a refresh on the next launch.");
184
+ }
185
+
186
+ export async function agent(_cwd: string, rawArgs: string[]): Promise<void> {
187
+ const args = parseArgs(rawArgs);
188
+
189
+ if (args.showHelp) {
190
+ printHelp();
191
+ return;
192
+ }
193
+
194
+ const skipUpdate = args.noUpdate || process.env.MINK_AGENT_NO_UPDATE === "1";
195
+
196
+ const root = minkRoot();
197
+ if (!existsSync(root)) {
198
+ mkdirSync(root, { recursive: true });
199
+ }
200
+
201
+ let result: InstallResult;
202
+ try {
203
+ result = installAgentDefinition({
204
+ force: args.reinstall,
205
+ skip: skipUpdate && !args.reinstall,
206
+ });
207
+ } catch (err) {
208
+ const msg = err instanceof Error ? err.message : String(err);
209
+ console.error(msg);
210
+ process.exit(1);
211
+ }
212
+
213
+ switch (result.action) {
214
+ case "installed":
215
+ console.log(`[mink] installed mink-agent definition (v${getMinkVersion()}) -> ${result.path}`);
216
+ break;
217
+ case "updated":
218
+ console.log(`[mink] updated mink-agent definition -> ${result.path}`);
219
+ break;
220
+ case "unchanged":
221
+ case "skipped":
222
+ // silent
223
+ break;
224
+ }
225
+
226
+ if (!isClaudeOnPath()) {
227
+ console.error("[mink agent] `claude` (Claude Code CLI) was not found on PATH.");
228
+ console.error(" Install Claude Code: https://claude.com/claude-code");
229
+ process.exit(1);
230
+ }
231
+
232
+ const claudeArgs = ["--agent", AGENT_NAME, ...args.passthrough];
233
+ const child = spawnSync("claude", claudeArgs, {
234
+ cwd: root,
235
+ stdio: "inherit",
236
+ });
237
+
238
+ if (child.error) {
239
+ console.error(`[mink agent] failed to launch claude: ${child.error.message}`);
240
+ process.exit(1);
241
+ }
242
+ if (typeof child.status === "number") {
243
+ process.exit(child.status);
244
+ }
245
+ }
@@ -1,5 +1,6 @@
1
1
  import { readFileSync, existsSync } from "fs";
2
2
  import { startDaemon, stopDaemon } from "../core/daemon";
3
+ import { installService, uninstallService } from "../core/daemon-service";
3
4
  import { schedulerLogPath } from "../core/paths";
4
5
 
5
6
  export async function daemon(cwd: string, args: string[]): Promise<void> {
@@ -36,11 +37,21 @@ export async function daemon(cwd: string, args: string[]): Promise<void> {
36
37
  break;
37
38
  }
38
39
 
40
+ case "install":
41
+ installService({ force: args.includes("--force") });
42
+ break;
43
+
44
+ case "uninstall":
45
+ uninstallService();
46
+ break;
47
+
39
48
  default:
40
49
  console.error(
41
50
  `[mink] unknown daemon subcommand: ${subcommand ?? "(none)"}`
42
51
  );
43
- console.error("Usage: mink daemon <start|stop|restart|logs>");
52
+ console.error(
53
+ "Usage: mink daemon <start|stop|restart|logs|install|uninstall>"
54
+ );
44
55
  process.exit(1);
45
56
  }
46
57
  }
@@ -106,6 +106,30 @@ function isMinkHook(entry: HookEntry | Record<string, unknown>): boolean {
106
106
  return false;
107
107
  }
108
108
 
109
+ const MINK_RULE_CONTENT = `---
110
+ description: Mink context management — automatic via hooks
111
+ ---
112
+
113
+ This project uses **Mink** (\`@drewpayment/mink\`) for cross-session context management.
114
+
115
+ ## How it works
116
+ - Mink runs automatically through Claude Code hooks configured in \`.claude/settings.json\` (SessionStart, PreToolUse, PostToolUse, Stop).
117
+ - 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.).
118
+ - Read intelligence, write enforcement, bug memory, and the token ledger are handled by the hooks. You do not need to manually read or update any state files.
119
+
120
+ ## When to act on Mink
121
+ - If the user asks to "save a note", "remember this", "log this to my wiki", or similar, use the \`mink-note\` skill — it captures into the user's \`~/.mink/\` vault.
122
+ - If a hook surfaces a learning, past bug, or repeat-read warning, treat that as authoritative project memory and follow it.
123
+ - The \`mink dashboard\` and \`mink agent\` commands are user tools — do not invoke them on the user's behalf.
124
+ `;
125
+
126
+ export function writeMinkRule(cwd: string): string {
127
+ const rulePath = resolve(cwd, ".claude", "rules", "mink.md");
128
+ mkdirSync(dirname(rulePath), { recursive: true });
129
+ atomicWriteText(rulePath, MINK_RULE_CONTENT);
130
+ return rulePath;
131
+ }
132
+
109
133
  export function mergeHooksIntoSettings(
110
134
  settingsPath: string,
111
135
  newHooks: HooksConfig
@@ -148,6 +172,7 @@ export async function init(cwd: string): Promise<void> {
148
172
  }
149
173
 
150
174
  mergeHooksIntoSettings(settingsPath, hooks);
175
+ const rulePath = writeMinkRule(cwd);
151
176
 
152
177
  mkdirSync(dir, { recursive: true });
153
178
 
@@ -173,12 +198,14 @@ export async function init(cwd: string): Promise<void> {
173
198
  console.log(`[mink] upgrade complete`);
174
199
  console.log(` project: ${projectId}`);
175
200
  console.log(` hooks: ${settingsPath}`);
201
+ console.log(` rule: ${rulePath}`);
176
202
  } else {
177
203
  console.log(`[mink] initialized`);
178
204
  console.log(` project: ${projectId}`);
179
205
  console.log(` state: ${dir}`);
180
206
  console.log(` runtime: ${runtime}`);
181
207
  console.log(` hooks: ${settingsPath}`);
208
+ console.log(` rule: ${rulePath}`);
182
209
  }
183
210
 
184
211
  // Run initial scan
@@ -0,0 +1,227 @@
1
+ import { execSync } from "child_process";
2
+ import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "fs";
3
+ import { homedir } from "os";
4
+ import { dirname, join, resolve } from "path";
5
+ import { resolveCliPath } from "../commands/init";
6
+
7
+ export type ServicePlatform = "systemd" | "launchd";
8
+
9
+ export interface ServiceInvocation {
10
+ /** Absolute path to the executable used in ExecStart / ProgramArguments[0]. */
11
+ executable: string;
12
+ /** Arguments following the executable (e.g. ["daemon", "start"] or ["<cli.js>", "daemon", "start"]). */
13
+ args: string[];
14
+ /** Directory that should be added to PATH for the service's environment. */
15
+ pathDir: string;
16
+ }
17
+
18
+ export interface ServicePaths {
19
+ unitFile: string;
20
+ unitDir: string;
21
+ }
22
+
23
+ export function detectPlatform(): ServicePlatform | null {
24
+ if (process.platform === "linux") return "systemd";
25
+ if (process.platform === "darwin") return "launchd";
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * Resolve how the service should invoke mink.
31
+ *
32
+ * Prefer argv[1] when it is a bin shim (no .js/.ts extension) — that is the
33
+ * stable, install-method-agnostic entry point (e.g. ~/.bun/bin/mink). Fall
34
+ * back to invoking the compiled bundle via the current interpreter when
35
+ * running from source or from a non-shim entry.
36
+ */
37
+ export function resolveServiceInvocation(): ServiceInvocation {
38
+ const entry = process.argv[1];
39
+ if (entry && !/\.(js|ts|mjs|cjs)$/.test(entry) && existsSync(entry)) {
40
+ return {
41
+ executable: entry,
42
+ args: ["daemon", "start"],
43
+ pathDir: dirname(entry),
44
+ };
45
+ }
46
+
47
+ const cliPath = resolveCliPath();
48
+ const interpreter = process.execPath;
49
+ return {
50
+ executable: interpreter,
51
+ args: [cliPath, "daemon", "start"],
52
+ pathDir: dirname(interpreter),
53
+ };
54
+ }
55
+
56
+ export function servicePaths(platform: ServicePlatform): ServicePaths {
57
+ const home = homedir();
58
+ if (platform === "systemd") {
59
+ const unitDir = join(home, ".config", "systemd", "user");
60
+ return { unitDir, unitFile: join(unitDir, "mink-daemon.service") };
61
+ }
62
+ const unitDir = join(home, "Library", "LaunchAgents");
63
+ return { unitDir, unitFile: join(unitDir, "com.mink.daemon.plist") };
64
+ }
65
+
66
+ /** Build a systemd user unit file for the mink daemon. */
67
+ export function renderSystemdUnit(inv: ServiceInvocation): string {
68
+ const execStart = [inv.executable, ...inv.args].join(" ");
69
+ const stopArgs = inv.args.map((a) => (a === "start" ? "stop" : a));
70
+ const execStop = [inv.executable, ...stopArgs].join(" ");
71
+ const pathEnv = `${inv.pathDir}:/usr/local/bin:/usr/bin:/bin`;
72
+
73
+ return [
74
+ "[Unit]",
75
+ "Description=Mink background daemon",
76
+ "After=network-online.target",
77
+ "Wants=network-online.target",
78
+ "",
79
+ "[Service]",
80
+ "Type=forking",
81
+ `ExecStart=${execStart}`,
82
+ `ExecStop=${execStop}`,
83
+ "Restart=on-failure",
84
+ "RestartSec=10",
85
+ `Environment="PATH=${pathEnv}"`,
86
+ "",
87
+ "[Install]",
88
+ "WantedBy=default.target",
89
+ "",
90
+ ].join("\n");
91
+ }
92
+
93
+ /** Build a launchd user agent plist for the mink daemon. */
94
+ export function renderLaunchdPlist(inv: ServiceInvocation, logPath: string): string {
95
+ const programArgs = [inv.executable, ...inv.args]
96
+ .map((a) => ` <string>${escapeXml(a)}</string>`)
97
+ .join("\n");
98
+ const pathEnv = `${inv.pathDir}:/usr/local/bin:/usr/bin:/bin`;
99
+
100
+ return [
101
+ '<?xml version="1.0" encoding="UTF-8"?>',
102
+ '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
103
+ '<plist version="1.0">',
104
+ "<dict>",
105
+ " <key>Label</key>",
106
+ " <string>com.mink.daemon</string>",
107
+ " <key>ProgramArguments</key>",
108
+ " <array>",
109
+ programArgs,
110
+ " </array>",
111
+ " <key>RunAtLoad</key>",
112
+ " <true/>",
113
+ " <key>KeepAlive</key>",
114
+ " <dict>",
115
+ " <key>SuccessfulExit</key>",
116
+ " <false/>",
117
+ " </dict>",
118
+ " <key>EnvironmentVariables</key>",
119
+ " <dict>",
120
+ " <key>PATH</key>",
121
+ ` <string>${escapeXml(pathEnv)}</string>`,
122
+ " </dict>",
123
+ " <key>StandardOutPath</key>",
124
+ ` <string>${escapeXml(logPath)}</string>`,
125
+ " <key>StandardErrorPath</key>",
126
+ ` <string>${escapeXml(logPath)}</string>`,
127
+ "</dict>",
128
+ "</plist>",
129
+ "",
130
+ ].join("\n");
131
+ }
132
+
133
+ function escapeXml(s: string): string {
134
+ return s
135
+ .replace(/&/g, "&amp;")
136
+ .replace(/</g, "&lt;")
137
+ .replace(/>/g, "&gt;")
138
+ .replace(/"/g, "&quot;");
139
+ }
140
+
141
+ export interface InstallOptions {
142
+ force?: boolean;
143
+ }
144
+
145
+ export function installService(options: InstallOptions = {}): void {
146
+ const platform = detectPlatform();
147
+ if (!platform) {
148
+ console.error(
149
+ `[mink] daemon install is not supported on ${process.platform} (supported: linux, darwin)`
150
+ );
151
+ process.exit(1);
152
+ }
153
+
154
+ const paths = servicePaths(platform);
155
+ if (existsSync(paths.unitFile) && !options.force) {
156
+ console.error(`[mink] unit file already exists: ${paths.unitFile}`);
157
+ console.error(
158
+ " re-run with --force to overwrite, or run `mink daemon uninstall` first"
159
+ );
160
+ process.exit(1);
161
+ }
162
+
163
+ const inv = resolveServiceInvocation();
164
+ mkdirSync(paths.unitDir, { recursive: true });
165
+
166
+ if (platform === "systemd") {
167
+ writeFileSync(paths.unitFile, renderSystemdUnit(inv));
168
+ try {
169
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
170
+ } catch {
171
+ // systemctl may be unavailable (e.g. CI, WSL1) — the file is still written.
172
+ }
173
+ console.log(`[mink] wrote ${paths.unitFile}`);
174
+ console.log("[mink] next steps:");
175
+ console.log(" systemctl --user enable --now mink-daemon.service");
176
+ console.log(" # To survive logout (one-time, requires sudo):");
177
+ console.log(` sudo loginctl enable-linger ${process.env.USER ?? "$USER"}`);
178
+ } else {
179
+ const { schedulerLogPath } = require("./paths") as typeof import("./paths");
180
+ writeFileSync(paths.unitFile, renderLaunchdPlist(inv, schedulerLogPath()));
181
+ console.log(`[mink] wrote ${paths.unitFile}`);
182
+ console.log("[mink] next steps:");
183
+ console.log(` launchctl load -w ${paths.unitFile}`);
184
+ console.log(" # Launch agents run automatically on login; no lingering needed.");
185
+ }
186
+ }
187
+
188
+ export function uninstallService(): void {
189
+ const platform = detectPlatform();
190
+ if (!platform) {
191
+ console.error(
192
+ `[mink] daemon uninstall is not supported on ${process.platform} (supported: linux, darwin)`
193
+ );
194
+ process.exit(1);
195
+ }
196
+
197
+ const paths = servicePaths(platform);
198
+ if (!existsSync(paths.unitFile)) {
199
+ console.log(`[mink] no unit file at ${paths.unitFile} — nothing to uninstall`);
200
+ return;
201
+ }
202
+
203
+ if (platform === "systemd") {
204
+ try {
205
+ execSync("systemctl --user disable --now mink-daemon.service", {
206
+ stdio: "ignore",
207
+ });
208
+ } catch {
209
+ // Service may not be enabled / running — proceed to file removal.
210
+ }
211
+ unlinkSync(paths.unitFile);
212
+ try {
213
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
214
+ } catch {
215
+ // Ignore.
216
+ }
217
+ console.log(`[mink] removed ${paths.unitFile}`);
218
+ } else {
219
+ try {
220
+ execSync(`launchctl unload -w ${paths.unitFile}`, { stdio: "ignore" });
221
+ } catch {
222
+ // Ignore — may not be loaded.
223
+ }
224
+ unlinkSync(paths.unitFile);
225
+ console.log(`[mink] removed ${paths.unitFile}`);
226
+ }
227
+ }