@drewpayment/mink 0.2.2 → 0.3.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.3.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,12 @@ 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
+
140
146
  case "bug-search": {
141
147
  const { bugSearch } = await import("./commands/bug-search");
142
148
  bugSearch(cwd, process.argv.slice(3).join(" "));
@@ -189,13 +195,22 @@ switch (command) {
189
195
  console.log(" config [key] [value] Manage global user settings");
190
196
  console.log();
191
197
  console.log("Notes & Wiki:");
192
- console.log(" wiki <cmd> Manage the notes/wiki vault (init|status|rebuild-index|organize)");
198
+ console.log(" wiki <cmd> Manage the notes/wiki vault (init|status|link|unlink|links|rebuild-index|organize)");
193
199
  console.log(" note \"text\" Capture a note to the vault");
194
200
  console.log(" note --daily [text] Create or append to today's daily note");
195
201
  console.log(" note list [filters] List notes (--category, --tag, --recent)");
196
202
  console.log(" note search <term> Full-text search across the vault");
197
203
  console.log(" skill install Install /mink:note skill for Claude Code");
198
204
  console.log();
205
+ console.log("Sync:");
206
+ console.log(" sync Full manual sync (pull then push)");
207
+ console.log(" sync init <remote-url> Connect ~/.mink to a git remote for cross-device sync");
208
+ console.log(" sync status Show sync state (remote, last sync, pending changes)");
209
+ console.log(" sync push Manually push local changes");
210
+ console.log(" sync pull Manually pull remote changes");
211
+ console.log(" sync pause / resume Temporarily disable/enable auto-sync");
212
+ console.log(" sync disconnect Remove git tracking (data preserved)");
213
+ console.log();
199
214
  console.log("Automation & Analysis:");
200
215
  console.log(" dashboard [--port=N] Open the real-time web dashboard");
201
216
  console.log(" daemon <cmd> Manage the background daemon (start|stop|restart|logs)");
@@ -7,6 +7,16 @@ 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
+ // Sync pull before session begins (if enabled)
11
+ try {
12
+ const { isSyncInitialized, syncPull } = require("../core/sync");
13
+ if (isSyncInitialized()) {
14
+ syncPull((msg: string) => console.error(msg));
15
+ }
16
+ } catch {
17
+ // Never crash hooks
18
+ }
19
+
10
20
  const dir = projectDir(cwd);
11
21
  mkdirSync(dir, { recursive: true });
12
22
 
@@ -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));