@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drewpayment/mink",
3
- "version": "0.9.1",
3
+ "version": "0.10.1",
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": {
package/src/cli.ts CHANGED
@@ -107,6 +107,12 @@ switch (command) {
107
107
  break;
108
108
  }
109
109
 
110
+ case "upgrade": {
111
+ const { upgrade } = await import("./commands/upgrade");
112
+ await upgrade(cwd, process.argv.slice(3));
113
+ break;
114
+ }
115
+
110
116
  case "restore": {
111
117
  const { restore } = await import("./commands/restore");
112
118
  restore(cwd, process.argv.slice(3));
@@ -213,7 +219,7 @@ switch (command) {
213
219
  console.log(" config [key] [value] Manage global user settings");
214
220
  console.log();
215
221
  console.log("Notes & Wiki:");
216
- console.log(" wiki <cmd> Manage the notes/wiki vault (init|status|link|unlink|links|rebuild-index|organize)");
222
+ console.log(" wiki <cmd> Manage the notes/wiki vault (init|status|link|unlink|links|rebuild-index|scan|organize)");
217
223
  console.log(" note \"text\" Capture a note to the vault");
218
224
  console.log(" note --daily [text] Create or append to today's daily note");
219
225
  console.log(" note list [filters] List notes (--category, --tag, --recent)");
@@ -244,7 +250,8 @@ switch (command) {
244
250
  console.log(" dashboard [--port=N] Open the real-time web dashboard");
245
251
  console.log(" daemon <cmd> Manage the background daemon (start|stop|restart|logs|install|uninstall)");
246
252
  console.log(" cron <cmd> [id] Manage scheduled tasks (list|run|retry)");
247
- console.log(" update [options] Update Mink across registered projects");
253
+ console.log(" update [options] Update Mink hooks across registered projects");
254
+ console.log(" upgrade [options] Self-upgrade the mink CLI from npm (--check|--dry-run|--force)");
248
255
  console.log(" restore [backup] Restore state from a backup");
249
256
  console.log(" bug search <term> Search the bug log");
250
257
  console.log(" detect-waste Detect and flag wasteful patterns");
@@ -1,8 +1,13 @@
1
1
  import { readFileSync } from "fs";
2
- import { join } from "path";
2
+ import { join, relative } from "path";
3
3
  import { fileIndexPath, configPath } from "../core/paths";
4
4
  import { atomicWriteJson, safeReadJson } from "../core/fs-utils";
5
- import { scanProject, loadConfig, getExcludes } from "../core/scanner";
5
+ import {
6
+ scanProject,
7
+ scanProjectWithStats,
8
+ loadConfig,
9
+ getExcludes,
10
+ } from "../core/scanner";
6
11
  import { extractDescription } from "../core/description";
7
12
  import { estimateTokens } from "../core/token-estimate";
8
13
  import {
@@ -13,6 +18,11 @@ import {
13
18
  } from "../core/index-store";
14
19
  import type { FileIndex, FileIndexEntry } from "../types/file-index";
15
20
 
21
+ function configRelativePath(cfgPath: string, cwd: string): string {
22
+ const rel = relative(cwd, cfgPath);
23
+ return rel.startsWith("..") ? cfgPath : rel;
24
+ }
25
+
16
26
  function loadExistingIndex(indexPath: string): FileIndex {
17
27
  const raw = safeReadJson(indexPath);
18
28
  if (isFileIndex(raw)) return raw;
@@ -64,7 +74,8 @@ export function scan(cwd: string, options: { check: boolean }): void {
64
74
  const start = Date.now();
65
75
  const index = loadExistingIndex(idxPath);
66
76
 
67
- const scanned = scanProject(cwd, excludes, maxFiles);
77
+ const stats = scanProjectWithStats(cwd, excludes, maxFiles);
78
+ const scanned = stats.files;
68
79
 
69
80
  // Build new entries, preserving lifetime counters
70
81
  const newIndex = createEmptyIndex();
@@ -95,7 +106,19 @@ export function scan(cwd: string, options: { check: boolean }): void {
95
106
  atomicWriteJson(idxPath, newIndex);
96
107
 
97
108
  const elapsed = Date.now() - start;
98
- console.log(
99
- `[mink] indexed ${newIndex.header.totalFiles} files in ${elapsed}ms`
100
- );
109
+ if (stats.truncated > 0) {
110
+ console.log(
111
+ `[mink] scanned ${stats.totalScanned} files; indexed ${newIndex.header.totalFiles} most recent in ${elapsed}ms`
112
+ );
113
+ console.log(
114
+ ` ${stats.truncated} files past maxFiles=${maxFiles} were not indexed`
115
+ );
116
+ console.log(
117
+ ` raise the cap by setting "maxFiles" in ${configRelativePath(cfgPath, cwd)}`
118
+ );
119
+ } else {
120
+ console.log(
121
+ `[mink] indexed ${newIndex.header.totalFiles} files in ${elapsed}ms`
122
+ );
123
+ }
101
124
  }
@@ -0,0 +1,128 @@
1
+ import { runSelfUpgrade, PACKAGE_NAME, type UpgradeResult } from "../core/self-update";
2
+
3
+ interface UpgradeArgs {
4
+ check: boolean;
5
+ dryRun: boolean;
6
+ force: boolean;
7
+ yes: boolean;
8
+ help: boolean;
9
+ }
10
+
11
+ function parseArgs(args: string[]): UpgradeArgs {
12
+ const out: UpgradeArgs = {
13
+ check: false,
14
+ dryRun: false,
15
+ force: false,
16
+ yes: false,
17
+ help: false,
18
+ };
19
+ for (const arg of args) {
20
+ switch (arg) {
21
+ case "--check":
22
+ out.check = true;
23
+ break;
24
+ case "--dry-run":
25
+ out.dryRun = true;
26
+ break;
27
+ case "--force":
28
+ out.force = true;
29
+ break;
30
+ case "--yes":
31
+ case "-y":
32
+ out.yes = true;
33
+ break;
34
+ case "--help":
35
+ case "-h":
36
+ out.help = true;
37
+ break;
38
+ }
39
+ }
40
+ return out;
41
+ }
42
+
43
+ function printHelp(): void {
44
+ console.log("Usage: mink upgrade [options]");
45
+ console.log("");
46
+ console.log("Check the npm registry for a newer mink release and install it.");
47
+ console.log(`Tracks the 'latest' dist-tag of ${PACKAGE_NAME}.`);
48
+ console.log("");
49
+ console.log("Options:");
50
+ console.log(" --check Report whether an upgrade is available; do not install");
51
+ console.log(" --dry-run Resolve everything but do not run the install command");
52
+ console.log(" --force Install the latest version even if it is not strictly newer");
53
+ console.log(" --yes, -y Skip the interactive confirmation prompt");
54
+ console.log(" --help, -h Show this help");
55
+ console.log("");
56
+ console.log("Auto-update on a schedule:");
57
+ console.log(" mink config set cli.auto-update true");
58
+ console.log(" mink config set cli.auto-update-schedule \"0 4 * * *\"");
59
+ }
60
+
61
+ function describeResult(r: UpgradeResult): string {
62
+ switch (r.status) {
63
+ case "up-to-date":
64
+ return `Already up-to-date — ${r.current} matches latest.`;
65
+ case "update-available":
66
+ return `Update available: ${r.current} → ${r.latest}` +
67
+ (r.packageManager ? ` (would install via ${r.packageManager})` : "");
68
+ case "would-upgrade":
69
+ return `Would upgrade: ${r.current} → ${r.latest}\n command: ${r.command}`;
70
+ case "upgraded":
71
+ return `Upgraded ${r.from} → ${r.to} (via ${r.packageManager}).`;
72
+ case "skipped":
73
+ return `Skipped: ${r.reason}`;
74
+ case "error":
75
+ return `Error: ${r.reason}`;
76
+ }
77
+ }
78
+
79
+ async function confirm(prompt: string): Promise<boolean> {
80
+ if (!process.stdin.isTTY) return false;
81
+ process.stdout.write(prompt);
82
+ return new Promise<boolean>((resolveConfirm) => {
83
+ process.stdin.setEncoding("utf-8");
84
+ process.stdin.once("data", (chunk) => {
85
+ const answer = String(chunk).trim().toLowerCase();
86
+ resolveConfirm(answer === "y" || answer === "yes");
87
+ });
88
+ });
89
+ }
90
+
91
+ export async function upgrade(_cwd: string, args: string[]): Promise<void> {
92
+ const parsed = parseArgs(args);
93
+
94
+ if (parsed.help) {
95
+ printHelp();
96
+ return;
97
+ }
98
+
99
+ // For interactive runs without --yes/--check/--dry-run/--force, do a check
100
+ // first and ask for confirmation before mutating the global install.
101
+ const isCheckLike = parsed.check || parsed.dryRun;
102
+ if (!isCheckLike && !parsed.yes && process.stdin.isTTY) {
103
+ const probe = await runSelfUpgrade({ source: "manual", checkOnly: true, force: parsed.force });
104
+ console.log(describeResult(probe));
105
+ if (probe.status !== "update-available" && !parsed.force) {
106
+ return;
107
+ }
108
+ const ok = await confirm("Proceed with install? [y/N] ");
109
+ if (!ok) {
110
+ console.log("Aborted.");
111
+ return;
112
+ }
113
+ }
114
+
115
+ const result = await runSelfUpgrade({
116
+ source: "manual",
117
+ checkOnly: parsed.check,
118
+ dryRun: parsed.dryRun,
119
+ force: parsed.force,
120
+ interactive: true,
121
+ });
122
+
123
+ console.log(describeResult(result));
124
+
125
+ if (result.status === "error") {
126
+ process.exit(1);
127
+ }
128
+ }
@@ -14,7 +14,11 @@ import {
14
14
  import { atomicWriteJson } from "../core/fs-utils";
15
15
  import { setConfigValue } from "../core/global-config";
16
16
  import { seedTemplates } from "../core/vault-templates";
17
- import { rebuildVaultIndex, loadVaultIndex } from "../core/note-index";
17
+ import {
18
+ rebuildVaultIndex,
19
+ loadVaultIndex,
20
+ vaultIndexStaleness,
21
+ } from "../core/note-index";
18
22
  import { updateMasterIndex } from "../core/note-linker";
19
23
  import type { VaultManifest, NoteCategory } from "../types/note";
20
24
 
@@ -32,6 +36,7 @@ export async function wiki(
32
36
  wikiStatus();
33
37
  break;
34
38
  case "rebuild-index":
39
+ case "scan":
35
40
  wikiRebuildIndex();
36
41
  break;
37
42
  case "organize":
@@ -51,7 +56,8 @@ export async function wiki(
51
56
  console.log();
52
57
  console.log(" init Initialize the notes/wiki vault");
53
58
  console.log(" status Show vault statistics");
54
- console.log(" rebuild-index Full rescan and reindex of vault");
59
+ console.log(" rebuild-index Full rescan and reindex of vault (alias: scan)");
60
+ console.log(" scan Alias for rebuild-index");
55
61
  console.log(" organize List inbox notes needing categorization");
56
62
  console.log(" link <path> [name] Symlink external notes into the vault");
57
63
  console.log(" unlink <name> Remove a symlinked directory from the vault");
@@ -174,6 +180,13 @@ function wikiStatus(): void {
174
180
  }
175
181
 
176
182
  const vaultPath = resolveVaultPath();
183
+ const staleness = vaultIndexStaleness();
184
+ if (staleness.isStale) {
185
+ console.log(
186
+ `[mink] vault index is stale (${staleness.reason}) — rebuilding...`
187
+ );
188
+ rebuildVaultIndex();
189
+ }
177
190
  const index = loadVaultIndex();
178
191
 
179
192
  const categoryCounts: Record<string, number> = {
@@ -200,7 +213,10 @@ function wikiStatus(): void {
200
213
  }
201
214
  console.log();
202
215
  console.log(
203
- ` last indexed: ${index.lastScanTimestamp || "never"}`
216
+ ` last full scan: ${index.lastFullScanTimestamp || "never"}`
217
+ );
218
+ console.log(
219
+ ` last update: ${index.lastScanTimestamp || "never"}`
204
220
  );
205
221
 
206
222
  const links = listLinks();
@@ -7,6 +7,7 @@ import type { VaultIndex, VaultIndexEntry, NoteCategory } from "../types/note";
7
7
  export function createEmptyVaultIndex(): VaultIndex {
8
8
  return {
9
9
  lastScanTimestamp: "",
10
+ lastFullScanTimestamp: "",
10
11
  totalNotes: 0,
11
12
  entries: {},
12
13
  };
@@ -182,7 +183,9 @@ export function rebuildVaultIndex(): VaultIndex {
182
183
  }
183
184
  }
184
185
 
185
- index.lastScanTimestamp = new Date().toISOString();
186
+ const now = new Date().toISOString();
187
+ index.lastScanTimestamp = now;
188
+ index.lastFullScanTimestamp = now;
186
189
  saveVaultIndex(index);
187
190
  return index;
188
191
  }
@@ -219,6 +222,52 @@ export function getRecentNotes(n: number): VaultIndexEntry[] {
219
222
  .slice(0, n);
220
223
  }
221
224
 
225
+ export interface VaultStaleness {
226
+ isStale: boolean;
227
+ reason: string | null;
228
+ diskCount: number;
229
+ indexCount: number;
230
+ lastFullScan: string | null;
231
+ }
232
+
233
+ export function vaultIndexStaleness(): VaultStaleness {
234
+ const index = loadVaultIndex();
235
+ const root = resolveVaultPath();
236
+ const diskCount = collectAllMarkdown(root).length;
237
+ const indexCount = Object.keys(index.entries).length;
238
+ const lastFullScan = index.lastFullScanTimestamp || null;
239
+
240
+ if (!lastFullScan) {
241
+ return {
242
+ isStale: true,
243
+ reason: "no full scan on record",
244
+ diskCount,
245
+ indexCount,
246
+ lastFullScan: null,
247
+ };
248
+ }
249
+
250
+ const delta = Math.abs(diskCount - indexCount);
251
+ const threshold = Math.max(5, Math.floor(diskCount * 0.05));
252
+ if (delta >= threshold) {
253
+ return {
254
+ isStale: true,
255
+ reason: `${diskCount} files on disk but ${indexCount} in index`,
256
+ diskCount,
257
+ indexCount,
258
+ lastFullScan,
259
+ };
260
+ }
261
+
262
+ return {
263
+ isStale: false,
264
+ reason: null,
265
+ diskCount,
266
+ indexCount,
267
+ lastFullScan,
268
+ };
269
+ }
270
+
222
271
  interface ScannedMarkdown {
223
272
  absolutePath: string;
224
273
  relativePath: string;
@@ -87,13 +87,29 @@ export function getExcludes(config: ProjectConfig): string[] {
87
87
  return [...DEFAULT_EXCLUDES, ...(config.excludePatterns ?? [])];
88
88
  }
89
89
 
90
- export function scanProject(
90
+ export interface ScanStats {
91
+ files: ScannedFile[];
92
+ totalScanned: number;
93
+ truncated: number;
94
+ }
95
+
96
+ export function scanProjectWithStats(
91
97
  projectRoot: string,
92
98
  excludes: string[],
93
99
  maxFiles: number = DEFAULT_MAX_FILES
94
- ): ScannedFile[] {
100
+ ): ScanStats {
95
101
  const results: ScannedFile[] = [];
96
102
  walkDirectory(projectRoot, projectRoot, excludes, results);
97
103
  results.sort((a, b) => b.mtimeMs - a.mtimeMs);
98
- return results.slice(0, maxFiles);
104
+ const totalScanned = results.length;
105
+ const files = results.slice(0, maxFiles);
106
+ return { files, totalScanned, truncated: totalScanned - files.length };
107
+ }
108
+
109
+ export function scanProject(
110
+ projectRoot: string,
111
+ excludes: string[],
112
+ maxFiles: number = DEFAULT_MAX_FILES
113
+ ): ScannedFile[] {
114
+ return scanProjectWithStats(projectRoot, excludes, maxFiles).files;
99
115
  }