@drewpayment/mink 0.2.2 → 0.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drewpayment/mink",
3
- "version": "0.2.2",
3
+ "version": "0.4.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": {
package/src/cli.ts CHANGED
@@ -137,6 +137,18 @@ switch (command) {
137
137
  break;
138
138
  }
139
139
 
140
+ case "sync": {
141
+ const { sync } = await import("./commands/sync");
142
+ await sync(process.argv.slice(3));
143
+ break;
144
+ }
145
+
146
+ case "device": {
147
+ const { device } = await import("./commands/device");
148
+ device(process.argv.slice(3));
149
+ break;
150
+ }
151
+
140
152
  case "bug-search": {
141
153
  const { bugSearch } = await import("./commands/bug-search");
142
154
  bugSearch(cwd, process.argv.slice(3).join(" "));
@@ -189,13 +201,25 @@ switch (command) {
189
201
  console.log(" config [key] [value] Manage global user settings");
190
202
  console.log();
191
203
  console.log("Notes & Wiki:");
192
- console.log(" wiki <cmd> Manage the notes/wiki vault (init|status|rebuild-index|organize)");
204
+ console.log(" wiki <cmd> Manage the notes/wiki vault (init|status|link|unlink|links|rebuild-index|organize)");
193
205
  console.log(" note \"text\" Capture a note to the vault");
194
206
  console.log(" note --daily [text] Create or append to today's daily note");
195
207
  console.log(" note list [filters] List notes (--category, --tag, --recent)");
196
208
  console.log(" note search <term> Full-text search across the vault");
197
209
  console.log(" skill install Install /mink:note skill for Claude Code");
198
210
  console.log();
211
+ console.log("Devices & Sync:");
212
+ console.log(" device Show current device info");
213
+ console.log(" device list List all registered devices");
214
+ console.log(" device rename <name> Set a friendly name for this device");
215
+ console.log(" sync Full manual sync (pull then push)");
216
+ console.log(" sync init <remote-url> Connect ~/.mink to a git remote for cross-device sync");
217
+ console.log(" sync status Show sync state (remote, last sync, pending changes)");
218
+ console.log(" sync push Manually push local changes");
219
+ console.log(" sync pull Manually pull remote changes");
220
+ console.log(" sync pause / resume Temporarily disable/enable auto-sync");
221
+ console.log(" sync disconnect Remove git tracking (data preserved)");
222
+ console.log();
199
223
  console.log("Automation & Analysis:");
200
224
  console.log(" dashboard [--port=N] Open the real-time web dashboard");
201
225
  console.log(" daemon <cmd> Manage the background daemon (start|stop|restart|logs)");
@@ -69,7 +69,7 @@ export async function config(args: string[]): Promise<void> {
69
69
  const all = resolveAllConfig();
70
70
  console.log("[mink] configuration:");
71
71
  for (const entry of all) {
72
- let line = ` ${entry.key} = ${entry.value} (source: ${entry.source})`;
72
+ let line = ` ${entry.key} = ${entry.value} (${entry.scope}, source: ${entry.source})`;
73
73
  if (
74
74
  entry.source === "environment variable" &&
75
75
  entry.configFileValue !== undefined
@@ -0,0 +1,65 @@
1
+ import {
2
+ getOrCreateDeviceId,
3
+ listDevices,
4
+ setDeviceName,
5
+ } from "../core/device";
6
+ import { hostname, platform } from "os";
7
+
8
+ export function device(args: string[]): void {
9
+ const sub = args[0] ?? "status";
10
+
11
+ switch (sub) {
12
+ case "status": {
13
+ const id = getOrCreateDeviceId();
14
+ const devices = listDevices();
15
+ const current = devices.find((d) => d.id === id);
16
+ console.log("[mink] device info:");
17
+ console.log(` id: ${id}`);
18
+ console.log(` name: ${current?.name ?? hostname()}`);
19
+ console.log(` hostname: ${hostname()}`);
20
+ console.log(` platform: ${platform()}`);
21
+ if (current?.firstSeen) {
22
+ console.log(` first seen: ${current.firstSeen}`);
23
+ }
24
+ if (current?.lastSeen) {
25
+ console.log(` last seen: ${current.lastSeen}`);
26
+ }
27
+ break;
28
+ }
29
+
30
+ case "list": {
31
+ const devices = listDevices();
32
+ const currentId = getOrCreateDeviceId();
33
+ if (devices.length === 0) {
34
+ console.log("[mink] no devices registered yet");
35
+ return;
36
+ }
37
+ console.log("[mink] registered devices:");
38
+ for (const d of devices) {
39
+ const marker = d.id === currentId ? " (this device)" : "";
40
+ console.log(` ${d.name}${marker}`);
41
+ console.log(` id: ${d.id}`);
42
+ console.log(` hostname: ${d.hostname}`);
43
+ console.log(` platform: ${d.platform}`);
44
+ console.log(` last seen: ${d.lastSeen}`);
45
+ }
46
+ break;
47
+ }
48
+
49
+ case "rename": {
50
+ const name = args.slice(1).join(" ");
51
+ if (!name) {
52
+ console.error("Usage: mink device rename <name>");
53
+ process.exit(1);
54
+ }
55
+ setDeviceName(name);
56
+ console.log(`[mink] device renamed to "${name}"`);
57
+ break;
58
+ }
59
+
60
+ default:
61
+ console.error(`[mink] unknown device subcommand: ${sub}`);
62
+ console.error("Usage: mink device [status|list|rename <name>]");
63
+ process.exit(1);
64
+ }
65
+ }
@@ -7,6 +7,32 @@ import { isWikiEnabled, isVaultInitialized, isInsideVault } from "../core/vault"
7
7
  import { loadVaultIndex } from "../core/note-index";
8
8
 
9
9
  export function sessionStart(cwd: string): void {
10
+ // Migrate config to shared/local split if needed (before sync pull)
11
+ try {
12
+ const { migrateConfigIfNeeded } = require("../core/global-config");
13
+ migrateConfigIfNeeded();
14
+ } catch {
15
+ // Never crash hooks
16
+ }
17
+
18
+ // Register/update this device in the registry
19
+ try {
20
+ const { updateDeviceHeartbeat } = require("../core/device");
21
+ updateDeviceHeartbeat();
22
+ } catch {
23
+ // Never crash hooks
24
+ }
25
+
26
+ // Sync pull before session begins (if enabled)
27
+ try {
28
+ const { isSyncInitialized, syncPull } = require("../core/sync");
29
+ if (isSyncInitialized()) {
30
+ syncPull((msg: string) => console.error(msg));
31
+ }
32
+ } catch {
33
+ // Never crash hooks
34
+ }
35
+
10
36
  const dir = projectDir(cwd);
11
37
  mkdirSync(dir, { recursive: true });
12
38
 
@@ -1,6 +1,5 @@
1
1
  import { statSync, existsSync, readFileSync } from "fs";
2
2
  import { join, dirname } from "path";
3
- import { execSync } from "child_process";
4
3
  import { safeReadJson, atomicWriteJson, atomicWriteText } from "../core/fs-utils";
5
4
  import { isSessionState, buildSummary } from "../core/session";
6
5
  import { reflect } from "./reflect";
@@ -10,10 +9,8 @@ import { createActionLogWriter, consolidateLog } from "../core/action-log";
10
9
  import {
11
10
  isWikiEnabled,
12
11
  isVaultInitialized,
13
- resolveVaultPath,
14
12
  vaultProjects,
15
13
  } from "../core/vault";
16
- import { resolveConfigValue } from "../core/global-config";
17
14
  import type { SessionState, SessionFinalizer } from "../types/session";
18
15
  import type { ProjectConfig } from "../types/file-index";
19
16
 
@@ -128,13 +125,11 @@ export function sessionStop(
128
125
  // Never crash hooks
129
126
  }
130
127
 
131
- // Git backup for wiki vault
128
+ // Full mink sync (subsumes wiki git-backup)
132
129
  try {
133
- if (isWikiEnabled() && isVaultInitialized()) {
134
- const gitBackup = resolveConfigValue("wiki.git-backup");
135
- if (gitBackup.value === "true") {
136
- gitBackupVault(onReminder);
137
- }
130
+ const { isSyncInitialized, syncPush } = require("../core/sync");
131
+ if (isSyncInitialized()) {
132
+ syncPush(onReminder);
138
133
  }
139
134
  } catch {
140
135
  // Never crash hooks
@@ -209,53 +204,3 @@ function writeSessionToWiki(
209
204
  }
210
205
  }
211
206
 
212
- function gitBackupVault(
213
- onReminder: (msg: string) => void
214
- ): void {
215
- const vaultPath = resolveVaultPath();
216
-
217
- // Check if vault is a git repo
218
- const gitDir = join(vaultPath, ".git");
219
- if (!existsSync(gitDir)) {
220
- onReminder(
221
- "[mink] wiki git-backup enabled but vault is not a git repo — run 'git init' in " +
222
- vaultPath
223
- );
224
- return;
225
- }
226
-
227
- try {
228
- // Check for changes
229
- const status = execSync("git status --porcelain", {
230
- cwd: vaultPath,
231
- timeout: 5000,
232
- }).toString();
233
-
234
- if (!status.trim()) return; // Nothing to commit
235
-
236
- // Stage and commit
237
- execSync("git add -A", { cwd: vaultPath, timeout: 5000 });
238
- const msg = `mink: vault update ${new Date().toISOString().split("T")[0]}`;
239
- execSync(`git commit -m "${msg}"`, {
240
- cwd: vaultPath,
241
- timeout: 5000,
242
- });
243
-
244
- // Push (best-effort with timeout)
245
- const remote = resolveConfigValue("wiki.git-remote").value;
246
- try {
247
- execSync(`git push ${remote}`, {
248
- cwd: vaultPath,
249
- timeout: 10000,
250
- });
251
- } catch {
252
- onReminder(
253
- `[mink] wiki git push to '${remote}' failed — local commit preserved, will retry next session`
254
- );
255
- }
256
- } catch (err) {
257
- onReminder(
258
- `[mink] wiki git backup error: ${err instanceof Error ? err.message : String(err)}`
259
- );
260
- }
261
- }
@@ -0,0 +1,111 @@
1
+ import {
2
+ initSync,
3
+ syncPull,
4
+ syncPush,
5
+ getSyncStatus,
6
+ disconnectSync,
7
+ isSyncInitialized,
8
+ } from "../core/sync";
9
+ import { setConfigValue } from "../core/global-config";
10
+
11
+ export async function sync(args: string[]): Promise<void> {
12
+ const subcommand = args[0];
13
+
14
+ switch (subcommand) {
15
+ case undefined:
16
+ // No args: full manual sync (pull then push)
17
+ return handleManualSync();
18
+
19
+ case "init":
20
+ return handleInit(args.slice(1));
21
+
22
+ case "status":
23
+ return handleStatus();
24
+
25
+ case "push":
26
+ syncPush((msg) => console.error(msg));
27
+ return;
28
+
29
+ case "pull":
30
+ syncPull((msg) => console.error(msg));
31
+ return;
32
+
33
+ case "pause":
34
+ return handlePause();
35
+
36
+ case "resume":
37
+ return handleResume();
38
+
39
+ case "disconnect":
40
+ return handleDisconnect();
41
+
42
+ default:
43
+ console.error(`[mink] unknown sync subcommand: ${subcommand}`);
44
+ console.error("Usage: mink sync [init|status|push|pull|pause|resume|disconnect]");
45
+ process.exit(1);
46
+ }
47
+ }
48
+
49
+ function handleManualSync(): void {
50
+ if (!isSyncInitialized()) {
51
+ console.error("[mink] sync is not initialized");
52
+ console.error("Run 'mink sync init <remote-url>' to set up sync");
53
+ return;
54
+ }
55
+
56
+ console.log("[mink] pulling remote changes...");
57
+ syncPull((msg) => console.error(msg));
58
+
59
+ console.log("[mink] pushing local changes...");
60
+ syncPush((msg) => console.error(msg));
61
+
62
+ console.log("[mink] sync complete");
63
+ }
64
+
65
+ function handleInit(args: string[]): void {
66
+ const remoteUrl = args[0];
67
+ if (!remoteUrl) {
68
+ console.error("[mink] missing remote URL");
69
+ console.error("Usage: mink sync init <remote-url>");
70
+ console.error("Example: mink sync init git@github.com:user/mink-data.git");
71
+ process.exit(1);
72
+ }
73
+
74
+ initSync(remoteUrl);
75
+ }
76
+
77
+ function handleStatus(): void {
78
+ const status = getSyncStatus();
79
+
80
+ console.log("Mink Sync Status");
81
+ console.log("─".repeat(40));
82
+ console.log(` Enabled: ${status.enabled ? "yes" : "no"}`);
83
+ console.log(` Git initialized: ${status.gitInitialized ? "yes" : "no"}`);
84
+ console.log(` Remote URL: ${status.remoteUrl || "(not set)"}`);
85
+ console.log(` Branch: ${status.branch || "(none)"}`);
86
+ console.log(` Pending changes: ${status.pendingChanges}`);
87
+ console.log(` Last push: ${status.lastPush || "(never)"}`);
88
+ console.log(` Last pull: ${status.lastPull || "(never)"}`);
89
+ }
90
+
91
+ function handlePause(): void {
92
+ setConfigValue("sync.enabled", "false");
93
+ console.log("[mink] sync paused — auto-sync disabled");
94
+ console.log("[mink] run 'mink sync resume' to re-enable");
95
+ }
96
+
97
+ function handleResume(): void {
98
+ const status = getSyncStatus();
99
+ if (!status.gitInitialized) {
100
+ console.error("[mink] sync has not been initialized yet");
101
+ console.error("Run 'mink sync init <remote-url>' first");
102
+ return;
103
+ }
104
+
105
+ setConfigValue("sync.enabled", "true");
106
+ console.log("[mink] sync resumed — auto-sync re-enabled");
107
+ }
108
+
109
+ function handleDisconnect(): void {
110
+ disconnectSync();
111
+ }
@@ -7,6 +7,9 @@ import {
7
7
  isVaultInitialized,
8
8
  vaultManifestPath,
9
9
  vaultTemplates,
10
+ linkExternal,
11
+ unlinkExternal,
12
+ listLinks,
10
13
  } from "../core/vault";
11
14
  import { atomicWriteJson } from "../core/fs-utils";
12
15
  import { setConfigValue } from "../core/global-config";
@@ -34,13 +37,25 @@ export async function wiki(
34
37
  case "organize":
35
38
  wikiOrganize();
36
39
  break;
40
+ case "link":
41
+ wikiLink(args.slice(1));
42
+ break;
43
+ case "unlink":
44
+ wikiUnlink(args.slice(1));
45
+ break;
46
+ case "links":
47
+ wikiLinks();
48
+ break;
37
49
  default:
38
- console.log("Usage: mink wiki <init|status|rebuild-index|organize>");
50
+ console.log("Usage: mink wiki <command>");
39
51
  console.log();
40
52
  console.log(" init Initialize the notes/wiki vault");
41
53
  console.log(" status Show vault statistics");
42
54
  console.log(" rebuild-index Full rescan and reindex of vault");
43
55
  console.log(" organize List inbox notes needing categorization");
56
+ console.log(" link <path> [name] Symlink external notes into the vault");
57
+ console.log(" unlink <name> Remove a symlinked directory from the vault");
58
+ console.log(" links List all linked directories");
44
59
  break;
45
60
  }
46
61
  }
@@ -188,6 +203,15 @@ function wikiStatus(): void {
188
203
  ` last indexed: ${index.lastScanTimestamp || "never"}`
189
204
  );
190
205
 
206
+ const links = listLinks();
207
+ if (links.length > 0) {
208
+ console.log();
209
+ console.log(" Linked directories:");
210
+ for (const link of links) {
211
+ console.log(` ${link.name} -> ${link.target}`);
212
+ }
213
+ }
214
+
191
215
  if (categoryCounts.inbox > 0) {
192
216
  console.log();
193
217
  console.log(
@@ -242,6 +266,87 @@ function wikiOrganize(): void {
242
266
  );
243
267
  }
244
268
 
269
+ function wikiLink(args: string[]): void {
270
+ if (!isVaultInitialized()) {
271
+ console.log("[mink] no vault initialized");
272
+ console.log(" Run 'mink wiki init' first.");
273
+ return;
274
+ }
275
+
276
+ const targetPath = args[0];
277
+ if (!targetPath) {
278
+ console.log("Usage: mink wiki link <path> [name]");
279
+ console.log();
280
+ console.log(" Symlinks an external directory into the vault so it appears");
281
+ console.log(" alongside Mink's content in Obsidian.");
282
+ console.log();
283
+ console.log(" Examples:");
284
+ console.log(" mink wiki link ~/dev/notes");
285
+ console.log(" mink wiki link ~/dev/notes my-notes");
286
+ return;
287
+ }
288
+
289
+ const name = args[1]; // optional override
290
+ const result = linkExternal(targetPath, name);
291
+
292
+ if (!result.ok) {
293
+ console.error(`[mink] ${result.error}`);
294
+ process.exit(1);
295
+ }
296
+
297
+ console.log(`[mink] linked: ${result.linkName} -> ${targetPath}`);
298
+ console.log(` symlink: ${result.linkPath}`);
299
+ console.log();
300
+ console.log(" Open ~/.mink/wiki/ as your Obsidian vault to see everything together.");
301
+ }
302
+
303
+ function wikiUnlink(args: string[]): void {
304
+ if (!isVaultInitialized()) {
305
+ console.log("[mink] no vault initialized");
306
+ return;
307
+ }
308
+
309
+ const name = args[0];
310
+ if (!name) {
311
+ console.log("Usage: mink wiki unlink <name>");
312
+ console.log();
313
+ console.log(" Run 'mink wiki links' to see linked directories.");
314
+ return;
315
+ }
316
+
317
+ const result = unlinkExternal(name);
318
+
319
+ if (!result.ok) {
320
+ console.error(`[mink] ${result.error}`);
321
+ process.exit(1);
322
+ }
323
+
324
+ console.log(`[mink] unlinked: ${name}`);
325
+ console.log(" (original directory was not modified)");
326
+ }
327
+
328
+ function wikiLinks(): void {
329
+ if (!isVaultInitialized()) {
330
+ console.log("[mink] no vault initialized");
331
+ return;
332
+ }
333
+
334
+ const links = listLinks();
335
+
336
+ if (links.length === 0) {
337
+ console.log("[mink] no linked directories");
338
+ console.log(" Use 'mink wiki link <path>' to symlink external notes into the vault.");
339
+ return;
340
+ }
341
+
342
+ console.log("[mink] linked directories:");
343
+ console.log();
344
+ for (const link of links) {
345
+ console.log(` ${link.name} -> ${link.target}`);
346
+ console.log(` linked: ${link.linkedAt}`);
347
+ }
348
+ }
349
+
245
350
  function expandPath(raw: string): string {
246
351
  if (raw.startsWith("~/")) {
247
352
  return resolve(homedir(), raw.slice(2));
@@ -0,0 +1,72 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
2
+ import { dirname } from "path";
3
+ import { hostname, platform } from "os";
4
+ import { randomUUID } from "crypto";
5
+ import { deviceIdPath, deviceRegistryPath } from "./paths";
6
+ import { safeReadJson, atomicWriteJson } from "./fs-utils";
7
+ import type { DeviceInfo, DeviceRegistry } from "../types/config";
8
+
9
+ export function getOrCreateDeviceId(): string {
10
+ const idPath = deviceIdPath();
11
+ if (existsSync(idPath)) {
12
+ return readFileSync(idPath, "utf-8").trim();
13
+ }
14
+ const id = randomUUID();
15
+ mkdirSync(dirname(idPath), { recursive: true });
16
+ writeFileSync(idPath, id + "\n");
17
+ return id;
18
+ }
19
+
20
+ export function loadDeviceRegistry(): DeviceRegistry {
21
+ const raw = safeReadJson(deviceRegistryPath());
22
+ if (raw !== null && typeof raw === "object" && !Array.isArray(raw) && "devices" in (raw as object)) {
23
+ return raw as DeviceRegistry;
24
+ }
25
+ return { devices: {} };
26
+ }
27
+
28
+ export function saveDeviceRegistry(registry: DeviceRegistry): void {
29
+ atomicWriteJson(deviceRegistryPath(), registry);
30
+ }
31
+
32
+ export function updateDeviceHeartbeat(): void {
33
+ const id = getOrCreateDeviceId();
34
+ const registry = loadDeviceRegistry();
35
+ const now = new Date().toISOString();
36
+ const existing = registry.devices[id];
37
+
38
+ registry.devices[id] = {
39
+ name: existing?.name ?? hostname(),
40
+ hostname: hostname(),
41
+ platform: platform(),
42
+ firstSeen: existing?.firstSeen ?? now,
43
+ lastSeen: now,
44
+ };
45
+
46
+ saveDeviceRegistry(registry);
47
+ }
48
+
49
+ export function listDevices(): Array<DeviceInfo & { id: string }> {
50
+ const registry = loadDeviceRegistry();
51
+ return Object.entries(registry.devices).map(([id, info]) => ({
52
+ id,
53
+ ...info,
54
+ }));
55
+ }
56
+
57
+ export function setDeviceName(name: string): void {
58
+ const id = getOrCreateDeviceId();
59
+ const registry = loadDeviceRegistry();
60
+ const now = new Date().toISOString();
61
+ const existing = registry.devices[id];
62
+
63
+ registry.devices[id] = {
64
+ name,
65
+ hostname: hostname(),
66
+ platform: platform(),
67
+ firstSeen: existing?.firstSeen ?? now,
68
+ lastSeen: now,
69
+ };
70
+
71
+ saveDeviceRegistry(registry);
72
+ }