@drewpayment/mink 0.9.1 → 0.10.1

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 (52) hide show
  1. package/README.md +62 -1
  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.js +988 -454
  40. package/package.json +1 -1
  41. package/src/cli.ts +9 -2
  42. package/src/commands/scan.ts +29 -6
  43. package/src/commands/upgrade.ts +128 -0
  44. package/src/commands/wiki.ts +19 -3
  45. package/src/core/note-index.ts +50 -1
  46. package/src/core/scanner.ts +19 -3
  47. package/src/core/self-update.ts +363 -0
  48. package/src/core/task-registry.ts +52 -2
  49. package/src/types/config.ts +24 -0
  50. package/src/types/note.ts +1 -0
  51. /package/dashboard/out/_next/static/{r7Xr9mrUpunsz4QtD3jh1 → e0QWU9rPMeSlJJLTwij89}/_buildManifest.js +0 -0
  52. /package/dashboard/out/_next/static/{r7Xr9mrUpunsz4QtD3jh1 → e0QWU9rPMeSlJJLTwij89}/_ssgManifest.js +0 -0
@@ -0,0 +1,363 @@
1
+ import { spawnSync } from "child_process";
2
+ import { existsSync, readFileSync } from "fs";
3
+ import { dirname, resolve } from "path";
4
+ import { resolveConfigValue } from "./global-config";
5
+ import { minkRoot } from "./paths";
6
+ import { safeAppendText, atomicWriteText } from "./fs-utils";
7
+ import { join } from "path";
8
+
9
+ export const PACKAGE_NAME = "@drewpayment/mink";
10
+ const NPM_REGISTRY_URL = `https://registry.npmjs.org/${PACKAGE_NAME}/latest`;
11
+ const NETWORK_TIMEOUT_MS = 5_000;
12
+ const INSTALL_TIMEOUT_MS = 10 * 60_000;
13
+ const LOG_MAX_LINES = 1000;
14
+
15
+ export type UpgradeSource = "manual" | "scheduler";
16
+ export type PackageManager = "npm" | "bun";
17
+
18
+ export type UpgradeResult =
19
+ | { status: "up-to-date"; current: string; latest: string }
20
+ | { status: "update-available"; current: string; latest: string; packageManager?: PackageManager }
21
+ | { status: "would-upgrade"; current: string; latest: string; packageManager: PackageManager; command: string }
22
+ | { status: "upgraded"; from: string; to: string; packageManager: PackageManager }
23
+ | { status: "skipped"; reason: string }
24
+ | { status: "error"; reason: string; transient: boolean };
25
+
26
+ export interface UpgradeOptions {
27
+ source: UpgradeSource;
28
+ /** Skip the install step; only report whether an upgrade is available. */
29
+ checkOnly?: boolean;
30
+ /** Don't run the install command, but resolve everything else and report what would run. */
31
+ dryRun?: boolean;
32
+ /** Install even if the latest version is not strictly newer. */
33
+ force?: boolean;
34
+ /** Stream the install command's stdio to the parent terminal. False for scheduler runs. */
35
+ interactive?: boolean;
36
+ /** Override the registry URL (for tests). */
37
+ registryUrlOverride?: string;
38
+ }
39
+
40
+ // ── Version compare ─────────────────────────────────────────────────────────
41
+
42
+ interface ParsedVersion {
43
+ numbers: number[];
44
+ prerelease: string | null;
45
+ }
46
+
47
+ export function parseSemver(input: string): ParsedVersion | null {
48
+ const trimmed = input.trim().replace(/^v/, "");
49
+ if (!trimmed) return null;
50
+ const [versionPart, ...prereleaseParts] = trimmed.split("-");
51
+ const numbers = versionPart.split(".").map((s) => Number.parseInt(s, 10));
52
+ if (numbers.some((n) => Number.isNaN(n))) return null;
53
+ return {
54
+ numbers,
55
+ prerelease: prereleaseParts.length ? prereleaseParts.join("-") : null,
56
+ };
57
+ }
58
+
59
+ /**
60
+ * Compare two semver strings.
61
+ * Returns 1 if a > b, -1 if a < b, 0 if equal.
62
+ * A version with a prerelease tag is considered older than the same version without one
63
+ * (e.g. 1.0.0-rc.1 < 1.0.0). Prerelease vs prerelease falls back to lexicographic compare.
64
+ */
65
+ export function compareSemver(a: string, b: string): number {
66
+ const pa = parseSemver(a);
67
+ const pb = parseSemver(b);
68
+ if (!pa && !pb) return 0;
69
+ if (!pa) return -1;
70
+ if (!pb) return 1;
71
+
72
+ const len = Math.max(pa.numbers.length, pb.numbers.length);
73
+ for (let i = 0; i < len; i++) {
74
+ const ai = pa.numbers[i] ?? 0;
75
+ const bi = pb.numbers[i] ?? 0;
76
+ if (ai > bi) return 1;
77
+ if (ai < bi) return -1;
78
+ }
79
+
80
+ if (pa.prerelease === pb.prerelease) return 0;
81
+ if (pa.prerelease === null) return 1;
82
+ if (pb.prerelease === null) return -1;
83
+ if (pa.prerelease > pb.prerelease) return 1;
84
+ if (pa.prerelease < pb.prerelease) return -1;
85
+ return 0;
86
+ }
87
+
88
+ // ── CLI install path resolution ─────────────────────────────────────────────
89
+
90
+ export interface CliInstallInfo {
91
+ /** Absolute path to the running cli.js (or src/cli.ts in dev mode). */
92
+ cliPath: string;
93
+ /** Absolute path to the package.json that owns the running CLI. */
94
+ packageJsonPath: string;
95
+ /** Version reported by that package.json. */
96
+ currentVersion: string;
97
+ /** True when running under src/cli.ts via bun/tsx. */
98
+ isDevMode: boolean;
99
+ }
100
+
101
+ /**
102
+ * Resolve the install location of the *running* CLI by walking up from
103
+ * `import.meta.url` until we find a package.json. We treat anything ending
104
+ * in `.ts` or living inside the working source tree as dev mode.
105
+ */
106
+ export function getInstallInfo(): CliInstallInfo {
107
+ const selfPath = new URL(import.meta.url).pathname;
108
+ const isDevMode = selfPath.endsWith(".ts");
109
+
110
+ // Walk up directories until we find package.json
111
+ let dir = dirname(selfPath);
112
+ let packageJsonPath: string | null = null;
113
+ for (let i = 0; i < 10; i++) {
114
+ const candidate = join(dir, "package.json");
115
+ if (existsSync(candidate)) {
116
+ packageJsonPath = candidate;
117
+ break;
118
+ }
119
+ const parent = dirname(dir);
120
+ if (parent === dir) break;
121
+ dir = parent;
122
+ }
123
+
124
+ if (!packageJsonPath) {
125
+ throw new Error("Unable to locate package.json for the running mink CLI");
126
+ }
127
+
128
+ let currentVersion = "0.0.0";
129
+ try {
130
+ const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
131
+ if (typeof pkg.version === "string") currentVersion = pkg.version;
132
+ } catch {
133
+ // fall through with 0.0.0
134
+ }
135
+
136
+ return {
137
+ cliPath: selfPath,
138
+ packageJsonPath,
139
+ currentVersion,
140
+ isDevMode,
141
+ };
142
+ }
143
+
144
+ // ── Registry fetch ──────────────────────────────────────────────────────────
145
+
146
+ async function fetchLatestVersion(
147
+ url: string,
148
+ currentVersion: string
149
+ ): Promise<string> {
150
+ const controller = new AbortController();
151
+ const timer = setTimeout(() => controller.abort(), NETWORK_TIMEOUT_MS);
152
+ try {
153
+ const res = await fetch(url, {
154
+ signal: controller.signal,
155
+ headers: {
156
+ "User-Agent": `mink-self-update/${currentVersion}`,
157
+ Accept: "application/json",
158
+ },
159
+ });
160
+ if (!res.ok) {
161
+ throw new Error(`registry returned ${res.status}`);
162
+ }
163
+ const body = (await res.json()) as { version?: unknown };
164
+ if (typeof body.version !== "string") {
165
+ throw new Error("registry response missing version field");
166
+ }
167
+ return body.version;
168
+ } finally {
169
+ clearTimeout(timer);
170
+ }
171
+ }
172
+
173
+ // ── Package manager detection ───────────────────────────────────────────────
174
+
175
+ function isOnPath(bin: string): boolean {
176
+ const result = spawnSync(bin, ["--version"], { stdio: "ignore" });
177
+ return !result.error && result.status === 0;
178
+ }
179
+
180
+ export function detectPackageManager(cliPath: string): PackageManager | null {
181
+ // Honor explicit user override first.
182
+ const configured = resolveConfigValue("cli.auto-update-package-manager").value;
183
+ if (configured === "bun" && isOnPath("bun")) return "bun";
184
+ if (configured === "npm" && isOnPath("npm")) return "npm";
185
+
186
+ // Auto-detect: prefer bun if the install path looks bun-ish (e.g. ~/.bun/install).
187
+ const looksLikeBun = /[\\/]\.bun[\\/]/.test(cliPath);
188
+ if (looksLikeBun && isOnPath("bun")) return "bun";
189
+
190
+ if (isOnPath("npm")) return "npm";
191
+ if (isOnPath("bun")) return "bun";
192
+ return null;
193
+ }
194
+
195
+ function buildInstallCommand(pm: PackageManager, version: string): string[] {
196
+ const ref = `${PACKAGE_NAME}@${version}`;
197
+ if (pm === "bun") return ["bun", "add", "-g", ref];
198
+ return ["npm", "install", "-g", ref];
199
+ }
200
+
201
+ // ── Logging ─────────────────────────────────────────────────────────────────
202
+
203
+ export function selfUpdateLogPath(): string {
204
+ return join(minkRoot(), "self-update.log");
205
+ }
206
+
207
+ function appendLogEntry(entry: Record<string, unknown>): void {
208
+ const path = selfUpdateLogPath();
209
+ const line = JSON.stringify({ timestamp: new Date().toISOString(), ...entry }) + "\n";
210
+ try {
211
+ safeAppendText(path, line);
212
+ rotateLogIfNeeded(path);
213
+ } catch {
214
+ // Logging failures must not crash the upgrade flow.
215
+ }
216
+ }
217
+
218
+ function rotateLogIfNeeded(path: string): void {
219
+ try {
220
+ const content = readFileSync(path, "utf-8");
221
+ const lines = content.split("\n");
222
+ if (lines.length <= LOG_MAX_LINES + 1) return;
223
+ const trimmed = lines.slice(lines.length - LOG_MAX_LINES - 1).join("\n");
224
+ atomicWriteText(path, trimmed);
225
+ } catch {
226
+ // ignore — best effort
227
+ }
228
+ }
229
+
230
+ // ── Main entry point ────────────────────────────────────────────────────────
231
+
232
+ export async function runSelfUpgrade(opts: UpgradeOptions): Promise<UpgradeResult> {
233
+ const result = await runSelfUpgradeInner(opts);
234
+ appendLogEntry({ source: opts.source, ...result });
235
+ return result;
236
+ }
237
+
238
+ async function runSelfUpgradeInner(opts: UpgradeOptions): Promise<UpgradeResult> {
239
+ // 1. Hard kill switch.
240
+ if (process.env.MINK_DISABLE_AUTO_UPDATE === "1" && opts.source === "scheduler") {
241
+ return { status: "skipped", reason: "MINK_DISABLE_AUTO_UPDATE=1" };
242
+ }
243
+
244
+ // 2. Scheduler runs respect the cli.auto-update flag; manual runs do not.
245
+ if (opts.source === "scheduler") {
246
+ const enabled = resolveConfigValue("cli.auto-update").value;
247
+ if (enabled !== "true") {
248
+ return { status: "skipped", reason: "cli.auto-update is disabled" };
249
+ }
250
+ }
251
+
252
+ // 3. Resolve install info.
253
+ let info: CliInstallInfo;
254
+ try {
255
+ info = getInstallInfo();
256
+ } catch (err) {
257
+ return {
258
+ status: "error",
259
+ reason: err instanceof Error ? err.message : String(err),
260
+ transient: false,
261
+ };
262
+ }
263
+
264
+ // 4. Dev-mode guard — never auto-mutate the working source tree.
265
+ if (info.isDevMode) {
266
+ return {
267
+ status: "skipped",
268
+ reason: "running from source tree; refuse to self-upgrade in dev mode",
269
+ };
270
+ }
271
+
272
+ // 5. Fetch latest from registry.
273
+ let latest: string;
274
+ try {
275
+ latest = await fetchLatestVersion(
276
+ opts.registryUrlOverride ?? NPM_REGISTRY_URL,
277
+ info.currentVersion
278
+ );
279
+ } catch (err) {
280
+ return {
281
+ status: "error",
282
+ reason:
283
+ "failed to fetch latest version: " +
284
+ (err instanceof Error ? err.message : String(err)),
285
+ transient: true,
286
+ };
287
+ }
288
+
289
+ // 6. Compare versions.
290
+ const cmp = compareSemver(latest, info.currentVersion);
291
+ if (cmp <= 0 && !opts.force) {
292
+ return { status: "up-to-date", current: info.currentVersion, latest };
293
+ }
294
+
295
+ // 7. Resolve package manager (needed even for dry-run output).
296
+ const pm = detectPackageManager(info.cliPath);
297
+ if (!pm) {
298
+ return {
299
+ status: "error",
300
+ reason: "no package manager (npm or bun) available on PATH",
301
+ transient: false,
302
+ };
303
+ }
304
+
305
+ const cmd = buildInstallCommand(pm, latest);
306
+
307
+ if (opts.checkOnly) {
308
+ return {
309
+ status: "update-available",
310
+ current: info.currentVersion,
311
+ latest,
312
+ packageManager: pm,
313
+ };
314
+ }
315
+
316
+ if (opts.dryRun) {
317
+ return {
318
+ status: "would-upgrade",
319
+ current: info.currentVersion,
320
+ latest,
321
+ packageManager: pm,
322
+ command: cmd.join(" "),
323
+ };
324
+ }
325
+
326
+ // 8. Run install.
327
+ const stdio = opts.interactive ? "inherit" : "pipe";
328
+ const spawned = spawnSync(cmd[0], cmd.slice(1), {
329
+ stdio,
330
+ timeout: INSTALL_TIMEOUT_MS,
331
+ });
332
+ if (spawned.error) {
333
+ return {
334
+ status: "error",
335
+ reason: `install command failed to spawn: ${spawned.error.message}`,
336
+ transient: true,
337
+ };
338
+ }
339
+ if (spawned.status !== 0) {
340
+ const stderr = spawned.stderr ? spawned.stderr.toString().trim() : "";
341
+ return {
342
+ status: "error",
343
+ reason: `${cmd.join(" ")} exited with code ${spawned.status}${stderr ? ": " + stderr.slice(0, 500) : ""}`,
344
+ transient: true,
345
+ };
346
+ }
347
+
348
+ // 9. Verify post-install version by re-reading the on-disk package.json.
349
+ let verifiedVersion = latest;
350
+ try {
351
+ const pkg = JSON.parse(readFileSync(info.packageJsonPath, "utf-8"));
352
+ if (typeof pkg.version === "string") verifiedVersion = pkg.version;
353
+ } catch {
354
+ // package may have been replaced and the path may be stale; trust latest
355
+ }
356
+
357
+ return {
358
+ status: "upgraded",
359
+ from: info.currentVersion,
360
+ to: verifiedVersion,
361
+ packageManager: pm,
362
+ };
363
+ }
@@ -1,4 +1,6 @@
1
1
  import type { TaskDefinition } from "../types/scheduler";
2
+ import { resolveConfigValue } from "./global-config";
3
+ import { parseCronExpression } from "./cron-parser";
2
4
 
3
5
  // ── Built-in Task Definitions ───────────────────────────────────────────────
4
6
 
@@ -53,16 +55,43 @@ const BUILT_IN_TASKS: TaskDefinition[] = [
53
55
  retryPolicy: { maxAttempts: 3, baseDelayMs: 60_000 },
54
56
  timeoutMs: 300_000,
55
57
  },
58
+ {
59
+ id: "cli-self-update",
60
+ name: "CLI Self-Update",
61
+ description: "Check npm for a newer mink release and install it (gated by cli.auto-update)",
62
+ schedule: "0 4 * * *",
63
+ actionType: "function",
64
+ enabled: true,
65
+ retryPolicy: { maxAttempts: 3, baseDelayMs: 60_000 },
66
+ timeoutMs: 10 * 60_000,
67
+ },
56
68
  ];
57
69
 
58
70
  // ── Public API ──────────────────────────────────────────────────────────────
59
71
 
72
+ function resolveTaskSchedule(taskId: string, defaultSchedule: string): string {
73
+ if (taskId !== "cli-self-update") return defaultSchedule;
74
+ try {
75
+ const value = resolveConfigValue("cli.auto-update-schedule").value;
76
+ parseCronExpression(value);
77
+ return value;
78
+ } catch {
79
+ return defaultSchedule;
80
+ }
81
+ }
82
+
83
+ function applyDynamicOverrides(task: TaskDefinition): TaskDefinition {
84
+ if (task.id !== "cli-self-update") return task;
85
+ return { ...task, schedule: resolveTaskSchedule(task.id, task.schedule) };
86
+ }
87
+
60
88
  export function getBuiltInTasks(): TaskDefinition[] {
61
- return BUILT_IN_TASKS;
89
+ return BUILT_IN_TASKS.map(applyDynamicOverrides);
62
90
  }
63
91
 
64
92
  export function getTaskById(id: string): TaskDefinition | undefined {
65
- return BUILT_IN_TASKS.find((t) => t.id === id);
93
+ const task = BUILT_IN_TASKS.find((t) => t.id === id);
94
+ return task ? applyDynamicOverrides(task) : undefined;
66
95
  }
67
96
 
68
97
  // ── AI CLI Execution ────────────────────────────────────────────────────────
@@ -196,6 +225,27 @@ export async function executeTask(
196
225
  break;
197
226
  }
198
227
 
228
+ case "cli-self-update": {
229
+ const { runSelfUpgrade } = await import("./self-update");
230
+ const result = await runSelfUpgrade({
231
+ source: "scheduler",
232
+ interactive: false,
233
+ });
234
+ // Surface non-success results so the scheduler retry/dead-letter logic
235
+ // can react. "skipped" and "up-to-date" are normal outcomes.
236
+ if (result.status === "error") {
237
+ const err = new Error(result.reason);
238
+ if (!result.transient) {
239
+ // Non-transient errors (e.g. no package manager) shouldn't keep retrying;
240
+ // tag the message so the dead-letter logs reflect the cause.
241
+ err.message = `[non-transient] ${err.message}`;
242
+ }
243
+ throw err;
244
+ }
245
+ console.log(`[mink] cli-self-update: ${result.status}`);
246
+ break;
247
+ }
248
+
199
249
  default:
200
250
  throw new Error(`No executor defined for task: ${taskId}`);
201
251
  }
@@ -14,6 +14,9 @@ export interface GlobalConfig {
14
14
  "channel.discord.allowlist"?: string;
15
15
  "channel.default-platform"?: string;
16
16
  "channel.skip-permissions"?: string;
17
+ "cli.auto-update"?: string;
18
+ "cli.auto-update-schedule"?: string;
19
+ "cli.auto-update-package-manager"?: string;
17
20
  }
18
21
 
19
22
  export type ConfigKey = keyof GlobalConfig & string;
@@ -146,6 +149,27 @@ export const CONFIG_KEYS: ConfigKeyMeta[] = [
146
149
  description: "Pass --dangerously-skip-permissions so the channel can run without terminal prompts",
147
150
  scope: "shared",
148
151
  },
152
+ {
153
+ key: "cli.auto-update",
154
+ default: "false",
155
+ envVar: "MINK_CLI_AUTO_UPDATE",
156
+ description: "Auto-upgrade the mink CLI on schedule via the background scheduler",
157
+ scope: "shared",
158
+ },
159
+ {
160
+ key: "cli.auto-update-schedule",
161
+ default: "0 4 * * *",
162
+ envVar: "MINK_CLI_AUTO_UPDATE_SCHEDULE",
163
+ description: "Cron expression governing the cli-self-update scheduled task",
164
+ scope: "shared",
165
+ },
166
+ {
167
+ key: "cli.auto-update-package-manager",
168
+ default: "auto",
169
+ envVar: "MINK_CLI_AUTO_UPDATE_PACKAGE_MANAGER",
170
+ description: "Force a package manager (auto|npm|bun) for self-upgrade installs",
171
+ scope: "local",
172
+ },
149
173
  ];
150
174
 
151
175
  const VALID_KEYS = new Set<string>(CONFIG_KEYS.map((k) => k.key));
package/src/types/note.ts CHANGED
@@ -62,6 +62,7 @@ export interface VaultIndexEntry {
62
62
 
63
63
  export interface VaultIndex {
64
64
  lastScanTimestamp: string;
65
+ lastFullScanTimestamp?: string;
65
66
  totalNotes: number;
66
67
  entries: Record<string, VaultIndexEntry>;
67
68
  }