gnosys 5.11.4 → 5.12.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 (240) hide show
  1. package/dist/cli.js +324 -5150
  2. package/dist/index.js +364 -235
  3. package/dist/lib/addCommand.d.ts +9 -0
  4. package/dist/lib/addCommand.js +103 -0
  5. package/dist/lib/addStructuredCommand.d.ts +16 -0
  6. package/dist/lib/addStructuredCommand.js +103 -0
  7. package/dist/lib/ambiguityCommand.d.ts +4 -0
  8. package/dist/lib/ambiguityCommand.js +36 -0
  9. package/dist/lib/apiKeyVault.d.ts +78 -0
  10. package/dist/lib/apiKeyVault.js +447 -0
  11. package/dist/lib/askCommand.d.ts +13 -0
  12. package/dist/lib/askCommand.js +145 -0
  13. package/dist/lib/audioExtract.js +4 -1
  14. package/dist/lib/auditCommand.d.ts +7 -0
  15. package/dist/lib/auditCommand.js +27 -0
  16. package/dist/lib/backupCommand.d.ts +6 -0
  17. package/dist/lib/backupCommand.js +54 -0
  18. package/dist/lib/bootstrapCommand.d.ts +15 -0
  19. package/dist/lib/bootstrapCommand.js +51 -0
  20. package/dist/lib/briefingCommand.d.ts +7 -0
  21. package/dist/lib/briefingCommand.js +92 -0
  22. package/dist/lib/centralizeCommand.d.ts +5 -0
  23. package/dist/lib/centralizeCommand.js +16 -0
  24. package/dist/lib/chatCommand.d.ts +12 -0
  25. package/dist/lib/chatCommand.js +46 -0
  26. package/dist/lib/checkCommand.d.ts +4 -0
  27. package/dist/lib/checkCommand.js +133 -0
  28. package/dist/lib/clientReadOverlay.d.ts +27 -0
  29. package/dist/lib/clientReadOverlay.js +73 -0
  30. package/dist/lib/clientReadResolve.d.ts +32 -0
  31. package/dist/lib/clientReadResolve.js +84 -0
  32. package/dist/lib/commitContextCommand.d.ts +9 -0
  33. package/dist/lib/commitContextCommand.js +142 -0
  34. package/dist/lib/config.d.ts +43 -3
  35. package/dist/lib/config.js +58 -57
  36. package/dist/lib/configCommand.d.ts +10 -0
  37. package/dist/lib/configCommand.js +321 -0
  38. package/dist/lib/connectCommand.d.ts +8 -0
  39. package/dist/lib/connectCommand.js +19 -0
  40. package/dist/lib/db.d.ts +52 -0
  41. package/dist/lib/db.js +169 -1
  42. package/dist/lib/dearchiveCommand.d.ts +7 -0
  43. package/dist/lib/dearchiveCommand.js +41 -0
  44. package/dist/lib/discoverCommand.d.ts +9 -0
  45. package/dist/lib/discoverCommand.js +87 -0
  46. package/dist/lib/doctorCommand.d.ts +6 -0
  47. package/dist/lib/doctorCommand.js +256 -0
  48. package/dist/lib/dream.d.ts +42 -2
  49. package/dist/lib/dream.js +290 -30
  50. package/dist/lib/dreamCommand.d.ts +10 -0
  51. package/dist/lib/dreamCommand.js +195 -0
  52. package/dist/lib/dreamLaunchd.d.ts +2 -0
  53. package/dist/lib/dreamLaunchd.js +72 -0
  54. package/dist/lib/dreamLogCommand.d.ts +10 -0
  55. package/dist/lib/dreamLogCommand.js +58 -0
  56. package/dist/lib/dreamReport.d.ts +7 -0
  57. package/dist/lib/dreamReport.js +114 -0
  58. package/dist/lib/dreamRunLog.d.ts +121 -0
  59. package/dist/lib/dreamRunLog.js +212 -0
  60. package/dist/lib/embeddings.js +3 -0
  61. package/dist/lib/exportCommand.d.ts +18 -0
  62. package/dist/lib/exportCommand.js +101 -0
  63. package/dist/lib/fsearchCommand.d.ts +8 -0
  64. package/dist/lib/fsearchCommand.js +44 -0
  65. package/dist/lib/graphCommand.d.ts +4 -0
  66. package/dist/lib/graphCommand.js +68 -0
  67. package/dist/lib/helperGenerateCommand.d.ts +5 -0
  68. package/dist/lib/helperGenerateCommand.js +27 -0
  69. package/dist/lib/historyCommand.d.ts +5 -0
  70. package/dist/lib/historyCommand.js +51 -0
  71. package/dist/lib/hybridSearchCommand.d.ts +12 -0
  72. package/dist/lib/hybridSearchCommand.js +95 -0
  73. package/dist/lib/importCommand.d.ts +16 -0
  74. package/dist/lib/importCommand.js +89 -0
  75. package/dist/lib/importProjectCommand.d.ts +6 -0
  76. package/dist/lib/importProjectCommand.js +43 -0
  77. package/dist/lib/ingestCommand.d.ts +13 -0
  78. package/dist/lib/ingestCommand.js +95 -0
  79. package/dist/lib/installOutput.d.ts +36 -0
  80. package/dist/lib/installOutput.js +55 -0
  81. package/dist/lib/lensCommand.d.ts +20 -0
  82. package/dist/lib/lensCommand.js +61 -0
  83. package/dist/lib/lensing.d.ts +1 -0
  84. package/dist/lib/lensing.js +50 -9
  85. package/dist/lib/linksCommand.d.ts +7 -0
  86. package/dist/lib/linksCommand.js +48 -0
  87. package/dist/lib/listCommand.d.ts +8 -0
  88. package/dist/lib/listCommand.js +74 -0
  89. package/dist/lib/llm.d.ts +1 -1
  90. package/dist/lib/llm.js +26 -8
  91. package/dist/lib/localDiskCheck.d.ts +17 -0
  92. package/dist/lib/localDiskCheck.js +54 -0
  93. package/dist/lib/machineConfig.d.ts +11 -1
  94. package/dist/lib/machineConfig.js +16 -0
  95. package/dist/lib/machineRegistry.d.ts +61 -0
  96. package/dist/lib/machineRegistry.js +80 -0
  97. package/dist/lib/maintainCommand.d.ts +8 -0
  98. package/dist/lib/maintainCommand.js +34 -0
  99. package/dist/lib/masterLease.d.ts +20 -0
  100. package/dist/lib/masterLease.js +68 -0
  101. package/dist/lib/migrateCommand.d.ts +7 -0
  102. package/dist/lib/migrateCommand.js +158 -0
  103. package/dist/lib/migrateDbCommand.d.ts +9 -0
  104. package/dist/lib/migrateDbCommand.js +94 -0
  105. package/dist/lib/modelValidation.d.ts +5 -0
  106. package/dist/lib/modelValidation.js +27 -0
  107. package/dist/lib/openrouterTiers.d.ts +29 -0
  108. package/dist/lib/openrouterTiers.js +113 -0
  109. package/dist/lib/prefCommand.d.ts +10 -0
  110. package/dist/lib/prefCommand.js +118 -0
  111. package/dist/lib/projectsCommand.d.ts +8 -0
  112. package/dist/lib/projectsCommand.js +131 -0
  113. package/dist/lib/readCommand.d.ts +7 -0
  114. package/dist/lib/readCommand.js +62 -0
  115. package/dist/lib/recall.d.ts +3 -0
  116. package/dist/lib/recall.js +19 -4
  117. package/dist/lib/recallCommand.d.ts +11 -0
  118. package/dist/lib/recallCommand.js +112 -0
  119. package/dist/lib/reflectCommand.d.ts +8 -0
  120. package/dist/lib/reflectCommand.js +61 -0
  121. package/dist/lib/reindexCommand.d.ts +4 -0
  122. package/dist/lib/reindexCommand.js +34 -0
  123. package/dist/lib/reindexGraphCommand.d.ts +4 -0
  124. package/dist/lib/reindexGraphCommand.js +12 -0
  125. package/dist/lib/reinforceCommand.d.ts +8 -0
  126. package/dist/lib/reinforceCommand.js +40 -0
  127. package/dist/lib/remote.d.ts +5 -1
  128. package/dist/lib/remote.js +5 -1
  129. package/dist/lib/remoteWizard.d.ts +24 -5
  130. package/dist/lib/remoteWizard.js +308 -319
  131. package/dist/lib/restoreCommand.d.ts +5 -0
  132. package/dist/lib/restoreCommand.js +35 -0
  133. package/dist/lib/sandboxStartCommand.d.ts +6 -0
  134. package/dist/lib/sandboxStartCommand.js +25 -0
  135. package/dist/lib/sandboxStatusCommand.d.ts +4 -0
  136. package/dist/lib/sandboxStatusCommand.js +24 -0
  137. package/dist/lib/sandboxStopCommand.d.ts +4 -0
  138. package/dist/lib/sandboxStopCommand.js +21 -0
  139. package/dist/lib/searchCommand.d.ts +9 -0
  140. package/dist/lib/searchCommand.js +90 -0
  141. package/dist/lib/semanticSearchCommand.d.ts +8 -0
  142. package/dist/lib/semanticSearchCommand.js +52 -0
  143. package/dist/lib/setup/configSetRender.js +2 -0
  144. package/dist/lib/setup/providerGlyphs.d.ts +19 -0
  145. package/dist/lib/setup/providerGlyphs.js +42 -0
  146. package/dist/lib/setup/remoteRender.d.ts +31 -1
  147. package/dist/lib/setup/remoteRender.js +95 -4
  148. package/dist/lib/setup/sections/providers.d.ts +17 -0
  149. package/dist/lib/setup/sections/providers.js +255 -0
  150. package/dist/lib/setup/sections/routing.d.ts +2 -6
  151. package/dist/lib/setup/sections/routing.js +33 -85
  152. package/dist/lib/setup/sections/taskRoutingEditor.d.ts +17 -0
  153. package/dist/lib/setup/sections/taskRoutingEditor.js +149 -0
  154. package/dist/lib/setup/summary.d.ts +9 -0
  155. package/dist/lib/setup/summary.js +51 -37
  156. package/dist/lib/setup/ui/status.d.ts +1 -0
  157. package/dist/lib/setup/ui/status.js +2 -0
  158. package/dist/lib/setup.d.ts +108 -3
  159. package/dist/lib/setup.js +762 -157
  160. package/dist/lib/setupKeys.d.ts +42 -0
  161. package/dist/lib/setupKeys.js +564 -0
  162. package/dist/lib/setupRemoteCommand.d.ts +4 -0
  163. package/dist/lib/setupRemoteCommand.js +28 -0
  164. package/dist/lib/setupRemotePullCommand.d.ts +5 -0
  165. package/dist/lib/setupRemotePullCommand.js +52 -0
  166. package/dist/lib/setupRemotePushCommand.d.ts +5 -0
  167. package/dist/lib/setupRemotePushCommand.js +57 -0
  168. package/dist/lib/setupRemoteResolveCommand.d.ts +4 -0
  169. package/dist/lib/setupRemoteResolveCommand.js +48 -0
  170. package/dist/lib/setupRemoteStatusCommand.d.ts +4 -0
  171. package/dist/lib/setupRemoteStatusCommand.js +73 -0
  172. package/dist/lib/setupRemoteSyncCommand.d.ts +6 -0
  173. package/dist/lib/setupRemoteSyncCommand.js +65 -0
  174. package/dist/lib/setupSyncProjectsCommand.d.ts +4 -0
  175. package/dist/lib/setupSyncProjectsCommand.js +292 -0
  176. package/dist/lib/staleCommand.d.ts +8 -0
  177. package/dist/lib/staleCommand.js +34 -0
  178. package/dist/lib/statsCommand.d.ts +6 -0
  179. package/dist/lib/statsCommand.js +142 -0
  180. package/dist/lib/statusCommand.d.ts +18 -0
  181. package/dist/lib/statusCommand.js +250 -0
  182. package/dist/lib/storesCommand.d.ts +2 -0
  183. package/dist/lib/storesCommand.js +4 -0
  184. package/dist/lib/syncClient.d.ts +47 -0
  185. package/dist/lib/syncClient.js +212 -0
  186. package/dist/lib/syncCommand.d.ts +6 -0
  187. package/dist/lib/syncCommand.js +57 -0
  188. package/dist/lib/syncDoctorCommand.d.ts +5 -0
  189. package/dist/lib/syncDoctorCommand.js +100 -0
  190. package/dist/lib/syncIngest.d.ts +19 -0
  191. package/dist/lib/syncIngest.js +152 -0
  192. package/dist/lib/syncIngestLaunchd.d.ts +8 -0
  193. package/dist/lib/syncIngestLaunchd.js +93 -0
  194. package/dist/lib/syncIngestStartup.d.ts +5 -0
  195. package/dist/lib/syncIngestStartup.js +29 -0
  196. package/dist/lib/syncIngestSystemd.d.ts +10 -0
  197. package/dist/lib/syncIngestSystemd.js +97 -0
  198. package/dist/lib/syncIngestTimer.d.ts +8 -0
  199. package/dist/lib/syncIngestTimer.js +27 -0
  200. package/dist/lib/syncIngestTimerCommand.d.ts +7 -0
  201. package/dist/lib/syncIngestTimerCommand.js +83 -0
  202. package/dist/lib/syncLock.d.ts +6 -0
  203. package/dist/lib/syncLock.js +74 -0
  204. package/dist/lib/syncSnapshot.d.ts +30 -0
  205. package/dist/lib/syncSnapshot.js +184 -0
  206. package/dist/lib/syncStaging.d.ts +81 -0
  207. package/dist/lib/syncStaging.js +239 -0
  208. package/dist/lib/tagsAddCommand.d.ts +8 -0
  209. package/dist/lib/tagsAddCommand.js +18 -0
  210. package/dist/lib/tagsCommand.d.ts +4 -0
  211. package/dist/lib/tagsCommand.js +16 -0
  212. package/dist/lib/timelineCommand.d.ts +7 -0
  213. package/dist/lib/timelineCommand.js +49 -0
  214. package/dist/lib/traceCommand.d.ts +6 -0
  215. package/dist/lib/traceCommand.js +39 -0
  216. package/dist/lib/traverseCommand.d.ts +6 -0
  217. package/dist/lib/traverseCommand.js +58 -0
  218. package/dist/lib/updateCommand.d.ts +13 -0
  219. package/dist/lib/updateCommand.js +67 -0
  220. package/dist/lib/updateStatusCommand.d.ts +5 -0
  221. package/dist/lib/updateStatusCommand.js +38 -0
  222. package/dist/lib/webAddCommand.d.ts +8 -0
  223. package/dist/lib/webAddCommand.js +55 -0
  224. package/dist/lib/webBuildCommand.d.ts +10 -0
  225. package/dist/lib/webBuildCommand.js +65 -0
  226. package/dist/lib/webBuildIndexCommand.d.ts +8 -0
  227. package/dist/lib/webBuildIndexCommand.js +37 -0
  228. package/dist/lib/webIngestCommand.d.ts +11 -0
  229. package/dist/lib/webIngestCommand.js +51 -0
  230. package/dist/lib/webInitCommand.d.ts +9 -0
  231. package/dist/lib/webInitCommand.js +167 -0
  232. package/dist/lib/webRemoveCommand.d.ts +5 -0
  233. package/dist/lib/webRemoveCommand.js +41 -0
  234. package/dist/lib/webStatusCommand.d.ts +5 -0
  235. package/dist/lib/webStatusCommand.js +94 -0
  236. package/dist/lib/webUpdateCommand.d.ts +7 -0
  237. package/dist/lib/webUpdateCommand.js +72 -0
  238. package/dist/lib/workingSetCommand.d.ts +6 -0
  239. package/dist/lib/workingSetCommand.js +37 -0
  240. package/package.json +2 -1
@@ -1,53 +1,76 @@
1
1
  /**
2
- * Interactive wizard for `gnosys remote configure`.
2
+ * Interactive wizard for multi-machine sync (v13 design).
3
3
  *
4
- * Handles three primary scenarios:
5
- * 1. Fresh setup local DB only, configuring a remote for the first time
6
- * 2. Reconfigure already have a remote, want to change it or disconnect
7
- * 3. Join existing — second machine joining a remote that already has data
4
+ * Flows:
5
+ * - Fresh setup: explanation master vs client role-specific prompts
6
+ * - Reconfigure: change path, re-validate, or disconnect (when already configured)
8
7
  */
9
- import { readdirSync, statSync } from "fs";
8
+ import { randomUUID } from "crypto";
9
+ import { existsSync, mkdirSync, renameSync } from "fs";
10
10
  import * as path from "path";
11
11
  import { createInterface } from "readline/promises";
12
12
  import { GnosysDB } from "./db.js";
13
13
  import { RemoteSync, validateLocation, getConfiguredRemotePath, clearRemoteSyncConfig, } from "./remote.js";
14
+ import { ensureMachineConfig, writeMachineConfig, readMachineConfig, } from "./machineConfig.js";
15
+ import { getGnosysHome } from "./paths.js";
16
+ import { atomicWriteFileSync } from "./atomicWrite.js";
17
+ import { readMasterMarker, writeMasterMarker } from "./masterLease.js";
18
+ import { checkMasterPathLocalDisk, LOCAL_DISK_ACK_PHRASE, } from "./localDiskCheck.js";
19
+ import { stagingDirForMachine, clientPresencePath, machineStagingDir } from "./syncStaging.js";
14
20
  import { safeQuestion } from "./setup/ui/safePrompt.js";
15
21
  import { Spinner } from "./setup/ui/spinner.js";
16
22
  import { printStatus } from "./setup/ui/status.js";
17
23
  import { Footer } from "./setup/ui/footer.js";
18
- import { renderRemoteIntro, renderValidationSummary, renderRemoteDiff, SYNC_MODE_LABELS, } from "./setup/remoteRender.js";
24
+ import { renderRemoteIntro, renderValidationSummary, renderRemoteDiff, renderV13ExplanationScreen, renderMasterBackupWarning, renderBackupDeclineAckPrompt, BACKUP_RISK_PHRASE, TAILSCALE_GUIDE_URL, } from "./setup/remoteRender.js";
19
25
  const REMOTE_PATH_KEY = "remote_path";
20
26
  const REMOTE_MODE_KEY = "remote_mode";
21
- // ─── Helpers ────────────────────────────────────────────────────────────
22
- /** List likely remote candidates from /Volumes/. Filters out system volumes. */
23
- function detectVolumeCandidates() {
24
- try {
25
- const entries = readdirSync("/Volumes");
26
- const skip = new Set([
27
- "Macintosh HD",
28
- "Macintosh HD - Data",
29
- "Recovery",
30
- "Update",
31
- "Preboot",
32
- "VM",
33
- ]);
34
- return entries
35
- .filter((name) => !name.startsWith(".") && !skip.has(name))
36
- .filter((name) => !/Backups of /i.test(name))
37
- .map((name) => `/Volumes/${name}`)
38
- .filter((p) => {
39
- try {
40
- return statSync(p).isDirectory();
27
+ const PRE_MASTER_BACKUP_NAME = ".pre-master-backup";
28
+ export { BACKUP_RISK_PHRASE, TAILSCALE_GUIDE_URL };
29
+ const TAILSCALE_INLINE_FALLBACK = `Tailscale lets other machines reach this one as if it were on the same LAN.
30
+ Install Tailscale on each machine, sign in with the same account, then use the master folder path
31
+ that Tailscale exposes (often under /Volumes/ or a synced folder path).`;
32
+ export async function showTailscaleClientGuide(rl) {
33
+ console.log("");
34
+ console.log("You will need a way for this machine to reach the master folder (usually Tailscale).");
35
+ console.log("A basic explanation is shown below. For a full walkthrough, visit:");
36
+ console.log(` ${TAILSCALE_GUIDE_URL}`);
37
+ console.log("(If that page is unavailable, this inline text is the primary source.)");
38
+ console.log("");
39
+ const open = await askConfirm(rl, "Open the guide in your browser now?", false);
40
+ if (open) {
41
+ try {
42
+ const { execSync } = await import("child_process");
43
+ if (process.platform === "darwin") {
44
+ execSync(`open "${TAILSCALE_GUIDE_URL}"`, { stdio: "ignore" });
41
45
  }
42
- catch {
43
- return false;
46
+ else if (process.platform === "win32") {
47
+ execSync(`start "" "${TAILSCALE_GUIDE_URL}"`, { stdio: "ignore", shell: "cmd.exe" });
48
+ }
49
+ else {
50
+ execSync(`xdg-open "${TAILSCALE_GUIDE_URL}"`, { stdio: "ignore" });
44
51
  }
45
- });
52
+ }
53
+ catch {
54
+ console.log(TAILSCALE_INLINE_FALLBACK);
55
+ }
46
56
  }
47
- catch {
48
- return [];
57
+ else {
58
+ console.log(TAILSCALE_INLINE_FALLBACK);
49
59
  }
60
+ console.log("");
61
+ }
62
+ /** Returns true when the user typed the exact expected phrase (trimmed). */
63
+ export function matchesTypedPhrase(input, expected) {
64
+ return input.trim() === expected;
65
+ }
66
+ /**
67
+ * Cloned-install detection: a presence file already exists for this machineId
68
+ * (typical after VM clone / backup restore).
69
+ */
70
+ export function detectClonedStagingPresence(masterPath, machineId) {
71
+ return existsSync(clientPresencePath(masterPath, machineId));
50
72
  }
73
+ // ─── Helpers ────────────────────────────────────────────────────────────
51
74
  async function ask(rl, prompt) {
52
75
  return (await safeQuestion(rl, prompt)).trim();
53
76
  }
@@ -76,9 +99,11 @@ async function askConfirm(rl, prompt, defaultYes = true) {
76
99
  return defaultYes;
77
100
  return answer === "y" || answer === "yes";
78
101
  }
102
+ async function askTypedPhrase(rl, prompt, expected) {
103
+ const typed = await ask(rl, prompt);
104
+ return matchesTypedPhrase(typed, expected);
105
+ }
79
106
  function showValidationSummary(validation) {
80
- // v5.9.3 Screen 6 — route through the renderer so each check renders as
81
- // a `✓` / `✗` status line. Identical content, atom-styled output.
82
107
  console.log(renderValidationSummary({
83
108
  pathExists: validation.checks.pathExists,
84
109
  writable: validation.checks.writable,
@@ -93,63 +118,52 @@ function showValidationSummary(validation) {
93
118
  errors: validation.errors,
94
119
  }));
95
120
  }
96
- /**
97
- * Hierarchical sync-mode picker. Per design §4 Screen 6 the default
98
- * `read & write` is one keystroke (enter), and the other modes hide
99
- * behind a `more options` affordance.
100
- *
101
- * Returns the chosen mode, or null when the user explicitly cancels.
102
- */
103
- async function pickSyncMode(rl) {
104
- console.log("");
105
- console.log(" Sync mode");
106
- console.log("");
107
- console.log(` 1 read & write ${SYNC_MODE_LABELS["read-write"]} ◂ recommended`);
108
- console.log(` 2 more options pull-only, push-only`);
109
- console.log("");
110
- console.log(Footer("1–2 · pick enter · use recommended"));
111
- const answer = (await safeQuestion(rl, " > ")).trim();
112
- if (!answer || answer === "1")
113
- return "read-write";
114
- if (answer !== "2") {
115
- printStatus("warn", "invalid choice — using `read & write`");
116
- return "read-write";
117
- }
118
- // Nested submenu — all three modes + back.
119
- console.log("");
120
- console.log(` 1 read & write ${SYNC_MODE_LABELS["read-write"]} ◂ recommended`);
121
- console.log(` 2 pull-only ${SYNC_MODE_LABELS["pull-only"]}`);
122
- console.log(` 3 push-only ${SYNC_MODE_LABELS["push-only"]}`);
123
- console.log(` 4 back`);
124
- console.log("");
125
- console.log(Footer("1–4 · pick"));
126
- const sub = (await safeQuestion(rl, " > ")).trim();
127
- switch (sub) {
128
- case "1": return "read-write";
129
- case "2": return "pull-only";
130
- case "3": return "push-only";
131
- case "4": return null;
132
- default:
133
- printStatus("warn", "invalid choice — using `read & write`");
134
- return "read-write";
121
+ function persistMultiMachineConfig(localDb, masterPath, role) {
122
+ localDb.setMeta(REMOTE_PATH_KEY, masterPath);
123
+ localDb.setMeta(REMOTE_MODE_KEY, role);
124
+ const mc = ensureMachineConfig().config;
125
+ mc.remote = { enabled: true, path: masterPath, role };
126
+ writeMachineConfig(mc);
127
+ }
128
+ function archiveLocalDbBeforeMaster() {
129
+ const home = getGnosysHome();
130
+ const dbPath = path.join(home, "gnosys.db");
131
+ const backupPath = path.join(home, PRE_MASTER_BACKUP_NAME);
132
+ if (existsSync(dbPath) && !existsSync(backupPath)) {
133
+ renameSync(dbPath, backupPath);
135
134
  }
136
135
  }
136
+ function writeClientPresenceFile(masterPath, machineId) {
137
+ const dir = machineStagingDir(masterPath, machineId);
138
+ mkdirSync(dir, { recursive: true });
139
+ const presencePath = clientPresencePath(masterPath, machineId);
140
+ if (!existsSync(presencePath)) {
141
+ atomicWriteFileSync(presencePath, JSON.stringify({ machineId, firstSeenAt: new Date().toISOString() }, null, 2) + "\n");
142
+ }
143
+ }
144
+ async function pickFolderPath(rl, prompt, defaultPath) {
145
+ const hint = defaultPath ? ` [${defaultPath}] ` : " ";
146
+ const raw = await ask(rl, prompt + hint);
147
+ const chosen = raw || defaultPath || "";
148
+ if (!chosen)
149
+ return null;
150
+ return path.resolve(chosen);
151
+ }
137
152
  // ─── Main wizard ────────────────────────────────────────────────────────
138
153
  export async function runConfigureWizard(centralDb, externalRl) {
139
154
  const ownsRl = !externalRl;
140
155
  const rl = externalRl ?? createInterface({ input: process.stdin, output: process.stdout });
141
156
  try {
142
157
  const localCount = centralDb.getMemoryCount();
143
- const currentRemote = centralDb.getMeta(REMOTE_PATH_KEY);
144
- console.log("");
145
- console.log(renderRemoteIntro(localCount.active, localCount.archived, currentRemote || null));
146
- console.log("");
158
+ const currentRemote = getConfiguredRemotePath(centralDb);
147
159
  if (currentRemote) {
148
- // Reconfigure flow
160
+ console.log("");
161
+ console.log(renderRemoteIntro(localCount.active, localCount.archived, currentRemote));
162
+ console.log("");
149
163
  const choice = await askChoice(rl, "What would you like to do?", [
150
- { key: "1", label: "Change remote location" },
151
- { key: "2", label: "Re-validate current remote" },
152
- { key: "3", label: "Disconnect remote (local-only — warns if sync is needed)" },
164
+ { key: "1", label: "Change master folder path" },
165
+ { key: "2", label: "Re-validate current master folder" },
166
+ { key: "3", label: "Disconnect multi-machine sync (single-machine only)" },
153
167
  { key: "4", label: "Cancel" },
154
168
  ], "4");
155
169
  if (choice === "4")
@@ -158,151 +172,118 @@ export async function runConfigureWizard(centralDb, externalRl) {
158
172
  return await disconnectRemote(rl, centralDb);
159
173
  if (choice === "2")
160
174
  return await revalidateRemote(rl, centralDb, currentRemote);
161
- // choice === "1": fall through to setup flow
175
+ // choice 1: fall through to fresh v13 flow with explanation skipped? Use change-path only
176
+ return await changeMasterPathFlow(rl, centralDb, currentRemote);
162
177
  }
163
- // Setup flow (new or change)
164
- return await setupRemoteFlow(rl, centralDb, localCount.active);
178
+ return await runFreshV13SetupFlow(rl, centralDb, localCount.active);
165
179
  }
166
180
  finally {
167
- if (ownsRl) {
181
+ if (ownsRl)
168
182
  rl.close();
169
- }
170
183
  }
171
184
  }
172
- // ─── Setup flow ─────────────────────────────────────────────────────────
173
- async function setupRemoteFlow(rl, centralDb, localActiveCount) {
185
+ async function runFreshV13SetupFlow(rl, centralDb, localActiveCount) {
186
+ console.log(renderV13ExplanationScreen());
174
187
  console.log("");
175
- console.log("Step 1: Choose remote location");
188
+ const proceed = await askConfirm(rl, "Would you like to set this up?", true);
189
+ if (!proceed) {
190
+ printStatus("progress", "staying single-machine", "multi-machine sync not configured");
191
+ return false;
192
+ }
176
193
  console.log("");
177
- const candidates = detectVolumeCandidates();
178
- let remotePath;
179
- if (candidates.length > 0) {
180
- // askChoice() prints the option list — don't double-print it here.
181
- const choices = [
182
- ...candidates.map((_, i) => ({ key: String(i + 1), label: candidates[i] })),
183
- { key: String(candidates.length + 1), label: "Custom path" },
184
- { key: String(candidates.length + 2), label: "Skip" },
185
- ];
186
- const choice = await askChoice(rl, "Detected mounted volumes — select one:", choices);
187
- if (choice === String(candidates.length + 2))
194
+ const roleChoice = await askChoice(rl, "Is this machine going to be the master, or a client that joins an existing master?", [
195
+ { key: "1", label: "This machine is the master (it will hold the main folder)" },
196
+ { key: "2", label: "This machine is a client (it will connect to a master on another machine)" },
197
+ { key: "3", label: "Cancel" },
198
+ ], "3");
199
+ if (roleChoice === "3")
200
+ return false;
201
+ if (roleChoice === "1") {
202
+ return await runMasterSetupFlow(rl, centralDb, localActiveCount);
203
+ }
204
+ return await runClientSetupFlow(rl, centralDb, localActiveCount);
205
+ }
206
+ async function runMasterSetupFlow(rl, centralDb, localActiveCount) {
207
+ console.log(renderMasterBackupWarning());
208
+ const keepBackups = await askConfirm(rl, "Do you want to keep automatic backups enabled?", true);
209
+ if (!keepBackups) {
210
+ console.log(renderBackupDeclineAckPrompt());
211
+ if (!(await askTypedPhrase(rl, "Phrase: ", BACKUP_RISK_PHRASE))) {
212
+ printStatus("warn", "backup acknowledgement required", "setup cancelled");
188
213
  return false;
189
- if (choice === String(candidates.length + 1)) {
190
- remotePath = await ask(rl, "Custom path (e.g. /Volumes/nas/gnosys): ");
191
- }
192
- else {
193
- const idx = parseInt(choice, 10) - 1;
194
- const volume = candidates[idx];
195
- // Suggest a gnosys subdirectory inside the volume
196
- const suggested = path.join(volume, "gnosys");
197
- const useSubdir = await askConfirm(rl, `Use ${suggested} (recommended subdirectory)?`);
198
- remotePath = useSubdir ? suggested : volume;
199
214
  }
200
215
  }
201
- else {
202
- console.log("No mounted volumes detected at /Volumes/.");
203
- console.log("Common options: NAS via SMB/AFP, external drive, or Tailscale-mounted share.\n");
204
- remotePath = await ask(rl, "Enter remote path (e.g. /Volumes/nas/gnosys): ");
216
+ let moveExisting = false;
217
+ if (localActiveCount > 0) {
218
+ console.log("");
219
+ console.log(`This machine already has a local brain with ${localActiveCount} memories.`);
220
+ console.log("");
221
+ const brainChoice = await askChoice(rl, "Do you want to:", [
222
+ { key: "1", label: "Move the existing brain into the master folder (recommended)" },
223
+ { key: "2", label: "Start a fresh master folder (existing local memories will be ignored)" },
224
+ ], "1");
225
+ moveExisting = brainChoice === "1";
205
226
  }
206
- if (!remotePath) {
207
- console.log("No path provided. Cancelling.");
227
+ console.log("");
228
+ console.log("The master database must live on this machine's local disk (not NAS, iCloud, Dropbox, or a network mount).");
229
+ console.log("");
230
+ const defaultMaster = path.join(getGnosysHome(), "master-brain");
231
+ const masterPath = await pickFolderPath(rl, "Master folder path on this machine's local disk:", defaultMaster);
232
+ if (!masterPath) {
233
+ printStatus("warn", "no path provided", "setup cancelled");
234
+ return false;
235
+ }
236
+ const diskCheck = checkMasterPathLocalDisk(masterPath);
237
+ console.log(`\n${diskCheck.message}`);
238
+ if (diskCheck.verdict === "network") {
239
+ printStatus("fail", "master folder must be on local disk, not a network mount");
208
240
  return false;
209
241
  }
210
- // Step 2: Validate — v5.9.3 Screen 6: animate the validation under a
211
- // Spinner so the path-check feedback lands before the mode picker.
242
+ if (diskCheck.verdict === "unknown") {
243
+ console.log(`\nType this phrase exactly to continue:\n ${LOCAL_DISK_ACK_PHRASE}\n`);
244
+ if (!(await askTypedPhrase(rl, "Phrase: ", LOCAL_DISK_ACK_PHRASE))) {
245
+ printStatus("warn", "local disk acknowledgement required", "setup cancelled");
246
+ return false;
247
+ }
248
+ }
212
249
  console.log("");
213
- const validateSpinner = Spinner(`checking ${remotePath}…`);
214
- const validation = await validateLocation(remotePath);
250
+ const validateSpinner = Spinner(`checking ${masterPath}…`);
251
+ const validation = await validateLocation(masterPath);
215
252
  if (validation.ok) {
216
253
  const latency = validation.checks.latencyMs;
217
- validateSpinner.ok("path exists, writable", latency !== null ? `${latency} ms` : undefined);
254
+ validateSpinner.ok("folder ready", latency !== null ? `${latency} ms` : undefined);
218
255
  }
219
256
  else {
220
257
  validateSpinner.fail("validation failed");
221
258
  }
222
259
  showValidationSummary(validation);
223
260
  if (!validation.ok) {
224
- printStatus("fail", "remote not configured");
261
+ printStatus("fail", "master folder not configured");
225
262
  return false;
226
263
  }
227
- if (validation.warnings.length > 0) {
228
- const proceed = await askConfirm(rl, "Continue despite warnings?", true);
229
- if (!proceed)
264
+ const { config: mc } = ensureMachineConfig();
265
+ const existingMarker = readMasterMarker(masterPath);
266
+ if (existingMarker?.holderMachineId && existingMarker.holderMachineId !== mc.machineId) {
267
+ printStatus("fail", "another machine already owns this master folder", `holder: ${existingMarker.holderMachineId}`);
268
+ const takeover = await askConfirm(rl, "Attempt stale-takeover (advanced — only if the previous master is gone)?", false);
269
+ if (!takeover)
230
270
  return false;
231
- }
232
- // v5.9.3 Screen 6 — hierarchical sync-mode picker before data strategy.
233
- // Default is read-write (one keystroke). Persisted to remote_mode meta.
234
- const syncMode = await pickSyncMode(rl);
235
- if (syncMode === null) {
236
- printStatus("warn", "cancelled at mode picker — no changes written");
237
- return false;
238
- }
239
- // Step 3: Decide what to do based on existing DB state
240
- console.log("\nStep 3: Data strategy");
241
- console.log("");
242
- const remoteHasData = validation.checks.existingDb.found && (validation.checks.existingDb.memoryCount ?? 0) > 0;
243
- const localHasData = localActiveCount > 0;
244
- let strategy = "configure-only";
245
- if (!remoteHasData && !localHasData) {
246
- // Both empty — just point at remote
247
- console.log(" Both local and remote are empty. Configuring remote without data transfer.");
248
- strategy = "configure-only";
249
- }
250
- else if (!remoteHasData && localHasData) {
251
- // Local has data, remote is empty — initial migration
252
- console.log(` Your local DB has ${localActiveCount} memories.`);
253
- console.log(" The remote is empty.");
254
- const migrate = await askConfirm(rl, "Copy your local memories to the remote now?", true);
255
- strategy = migrate ? "migrate" : "configure-only";
256
- }
257
- else if (remoteHasData && !localHasData) {
258
- // Remote has data, local empty — pull from remote (this is the "second machine" scenario)
259
- console.log(` The remote has ${validation.checks.existingDb.memoryCount} memories.`);
260
- console.log(" Your local DB is empty.");
261
- const pull = await askConfirm(rl, "Pull all memories from remote to local now?", true);
262
- strategy = pull ? "pull" : "configure-only";
263
- }
264
- else {
265
- // BOTH have data — the tricky case
266
- // Reword to match deci-037: remote is the canonical source of truth,
267
- // local is an offline-resilience cache. The two counts shown here are
268
- // pre-merge snapshots, not "two co-equal copies".
269
- console.log(` Remote DB (source of truth): ${validation.checks.existingDb.memoryCount} memories`);
270
- console.log(` Local cache (offline backup): ${localActiveCount} memories`);
271
- console.log("");
272
- const choice = await askChoice(rl, "How do you want to combine them?", [
273
- { key: "1", label: "Merge — push local-only memories up, pull remote-only down, flag conflicts (recommended)" },
274
- { key: "2", label: "Replace remote with local (overwrites remote — destructive)" },
275
- { key: "3", label: "Replace local with remote (overwrites local cache)" },
276
- { key: "4", label: "Skip — configure remote without touching either DB" },
277
- ], "1");
278
- if (choice === "1")
279
- strategy = "merge";
280
- else if (choice === "2")
281
- strategy = "migrate"; // overwrites
282
- else if (choice === "3")
283
- strategy = "pull";
284
- else
285
- strategy = "configure-only"; // "Skip" is configure-only, not cancel
286
- if (strategy === "migrate" || strategy === "pull") {
287
- const confirm = await askConfirm(rl, `\nThis will overwrite the ${strategy === "migrate" ? "remote" : "local"} DB. Are you sure?`, false);
288
- if (!confirm) {
289
- console.log("Cancelled.");
290
- return false;
291
- }
292
- }
293
- }
294
- // Step 4: Save config and execute strategy. v5.9.3 Screen 6 wraps the
295
- // long-running sync calls in Spinners and prints a final Diff() block.
296
- const previousRemote = centralDb.getMeta(REMOTE_PATH_KEY) || null;
297
- centralDb.setMeta(REMOTE_PATH_KEY, remotePath);
298
- centralDb.setMeta(REMOTE_MODE_KEY, syncMode);
299
- const sync = new RemoteSync(centralDb, remotePath);
300
- try {
301
- if (strategy === "migrate") {
302
- const spin = Spinner(`doing first sync to ${remotePath}…`);
271
+ writeMasterMarker(masterPath, mc.machineId, { previousEpoch: existingMarker.epoch });
272
+ }
273
+ const previousRemote = getConfiguredRemotePath(centralDb);
274
+ persistMultiMachineConfig(centralDb, masterPath, "master");
275
+ if (!existingMarker?.holderMachineId || existingMarker.holderMachineId === mc.machineId) {
276
+ writeMasterMarker(masterPath, mc.machineId);
277
+ }
278
+ mkdirSync(path.join(masterPath, "backups"), { recursive: true });
279
+ mkdirSync(path.join(masterPath, ".gnosys-staging"), { recursive: true });
280
+ if (moveExisting) {
281
+ const spin = Spinner(`moving local brain into ${masterPath}…`);
282
+ const sync = new RemoteSync(centralDb, masterPath);
283
+ try {
303
284
  const result = await sync.migrate();
304
285
  if (result.ok) {
305
- spin.ok("first sync complete", `${result.copied} memories pushed`);
286
+ spin.ok("brain moved", `${result.copied} memories`);
306
287
  }
307
288
  else {
308
289
  spin.fail("migration had errors");
@@ -311,42 +292,116 @@ async function setupRemoteFlow(rl, centralDb, localActiveCount) {
311
292
  return false;
312
293
  }
313
294
  }
314
- else if (strategy === "pull") {
315
- const spin = Spinner(`doing first sync from ${remotePath}…`);
316
- const result = await sync.pull({ strategy: "newer-wins" });
317
- spin.ok("first sync complete", `${result.pulled} memories pulled`);
318
- for (const e of result.errors)
319
- printStatus("fail", e);
295
+ finally {
296
+ sync.closeRemote();
320
297
  }
321
- else if (strategy === "merge") {
322
- const spin = Spinner(`merging local and remote at ${remotePath}…`);
323
- const result = await sync.sync();
324
- spin.ok("merge complete", `pushed ${result.pushed} · pulled ${result.pulled} · conflicts ${result.conflicts.length}`);
325
- if (result.conflicts.length > 0) {
326
- printStatus("warn", "conflicts need resolution");
327
- for (const c of result.conflicts)
328
- console.log(` ${c.memoryId}: ${c.title}`);
329
- printStatus("progress", "resolve with", "gnosys remote resolve <memory-id> --keep <local|remote>");
330
- }
331
- for (const e of result.errors)
332
- printStatus("fail", e);
298
+ }
299
+ archiveLocalDbBeforeMaster();
300
+ console.log("");
301
+ console.log(renderRemoteDiff({ previousRemote, newRemote: masterPath, roleOrMode: "master" }));
302
+ printStatus("ok", "saved", "machine.json + remote_path");
303
+ console.log(Footer("run `gnosys remote status` to check sync state"));
304
+ return true;
305
+ }
306
+ async function runClientSetupFlow(rl, centralDb, localActiveCount) {
307
+ await showTailscaleClientGuide(rl);
308
+ const masterPath = await pickFolderPath(rl, "Enter the master folder path as it appears on this machine:");
309
+ if (!masterPath) {
310
+ printStatus("warn", "no path provided", "setup cancelled");
311
+ return false;
312
+ }
313
+ console.log("");
314
+ const validateSpinner = Spinner(`checking ${masterPath}…`);
315
+ const validation = await validateLocation(masterPath);
316
+ if (validation.ok) {
317
+ validateSpinner.ok("master folder reachable");
318
+ }
319
+ else {
320
+ validateSpinner.fail("validation failed");
321
+ }
322
+ showValidationSummary(validation);
323
+ if (!validation.ok) {
324
+ printStatus("fail", "master folder not configured");
325
+ return false;
326
+ }
327
+ const marker = readMasterMarker(masterPath);
328
+ if (!marker?.holderMachineId) {
329
+ printStatus("warn", "no master.json found", "the folder may not be set up as a master yet — continue only if you trust this path");
330
+ const trust = await askConfirm(rl, "Continue anyway?", false);
331
+ if (!trust)
332
+ return false;
333
+ }
334
+ let { config: mc } = ensureMachineConfig();
335
+ if (detectClonedStagingPresence(masterPath, mc.machineId)) {
336
+ console.log("");
337
+ printStatus("warn", "presence file already exists for this machineId on the master", "common after VM clone or backup restore");
338
+ const remint = await askConfirm(rl, "Re-mint this machine's ID (recommended)?", true);
339
+ if (remint) {
340
+ mc = { ...mc, machineId: randomUUID() };
341
+ writeMachineConfig(mc);
342
+ printStatus("ok", "new machineId", mc.machineId);
333
343
  }
334
344
  }
335
- finally {
336
- sync.closeRemote();
345
+ if (localActiveCount > 0) {
346
+ console.log("");
347
+ console.log(`This machine already has ${localActiveCount} memories locally.`);
348
+ console.log("");
349
+ console.log("If you choose to keep them, they will be treated as NEW memories and sent to the master.");
350
+ console.log("This may create duplicates if the same memories already exist on the master.");
351
+ console.log("");
352
+ const dupChoice = await askChoice(rl, "Do you want to:", [
353
+ {
354
+ key: "1",
355
+ label: "Keep the local memories and push them later (acknowledged risk of duplicates)",
356
+ },
357
+ { key: "2", label: "Start fresh (local memories will be ignored)" },
358
+ ], "2");
359
+ if (dupChoice === "1") {
360
+ const ack = await askConfirm(rl, "Acknowledge duplicate risk and continue?", false);
361
+ if (!ack)
362
+ return false;
363
+ }
337
364
  }
338
- // Final Diff + save confirmation per the design.
365
+ const previousRemote = getConfiguredRemotePath(centralDb);
366
+ persistMultiMachineConfig(centralDb, masterPath, "client");
367
+ writeClientPresenceFile(masterPath, mc.machineId);
339
368
  console.log("");
340
- console.log(renderRemoteDiff({ previousRemote, newRemote: remotePath, mode: syncMode }));
341
- printStatus("ok", "saved", "~/.gnosys/gnosys.json");
342
- console.log(Footer("run `gnosys remote status` anytime to check sync state"));
369
+ console.log(renderRemoteDiff({ previousRemote, newRemote: masterPath, roleOrMode: "client" }));
370
+ printStatus("ok", "saved", "machine.json + remote_path");
371
+ console.log(Footer("client machines stage new memories; ingest runs on the master"));
372
+ return true;
373
+ }
374
+ async function changeMasterPathFlow(rl, centralDb, currentRemote) {
375
+ const role = readMachineConfig()?.remote.role ??
376
+ centralDb.getMeta(REMOTE_MODE_KEY) ??
377
+ "client";
378
+ const newPath = await pickFolderPath(rl, `New master folder path (current: ${currentRemote}):`);
379
+ if (!newPath || newPath === currentRemote) {
380
+ printStatus("warn", "no change", "path unchanged");
381
+ return false;
382
+ }
383
+ const validation = await validateLocation(newPath);
384
+ showValidationSummary(validation);
385
+ if (!validation.ok)
386
+ return false;
387
+ persistMultiMachineConfig(centralDb, newPath, role);
388
+ if (role === "master") {
389
+ const { config: mc } = ensureMachineConfig();
390
+ const existing = readMasterMarker(newPath);
391
+ if (existing?.holderMachineId && existing.holderMachineId !== mc.machineId) {
392
+ printStatus("fail", "another machine owns the target master folder");
393
+ return false;
394
+ }
395
+ writeMasterMarker(newPath, mc.machineId, { previousEpoch: existing?.epoch });
396
+ }
397
+ printStatus("ok", "master folder updated", newPath);
343
398
  return true;
344
399
  }
345
400
  // ─── Reconfigure helpers ────────────────────────────────────────────────
346
401
  async function disconnectRemote(rl, localDb) {
347
402
  const remotePath = getConfiguredRemotePath(localDb);
348
403
  if (!remotePath) {
349
- printStatus("warn", "remote is not configured");
404
+ printStatus("warn", "multi-machine sync is not configured");
350
405
  return false;
351
406
  }
352
407
  const localCounts = localDb.getMemoryCount();
@@ -371,103 +426,24 @@ async function disconnectRemote(rl, localDb) {
371
426
  const remoteReachable = Boolean(syncStatus?.reachable && validation.ok);
372
427
  const remoteActive = remoteCounts?.active ?? validation.checks.existingDb.memoryCount ?? null;
373
428
  console.log("");
374
- printStatus("warn", "disconnecting makes this machine local-only");
429
+ printStatus("warn", "disconnecting returns this machine to single-machine mode");
375
430
  console.log(` local ~/.gnosys/gnosys.db — ${localCounts.active} active memories`);
376
431
  if (remoteReachable && remoteActive !== null) {
377
- console.log(` remote ${remotePath} — ${remoteActive} active memories`);
432
+ console.log(` master ${remotePath} — ${remoteActive} active memories`);
378
433
  }
379
434
  else {
380
- printStatus("warn", "remote is not reachable", remotePath);
435
+ printStatus("warn", "master folder is not reachable", remotePath);
381
436
  }
382
437
  console.log("");
383
- console.log(" The remote folder and gnosys.db are not deleted.");
384
- console.log(" After disconnect, this Mac will not read or write that remote,");
385
- console.log(" even when the volume is mounted.");
386
- const pendingPull = syncStatus?.pendingPull ?? 0;
387
- const pendingPush = syncStatus?.pendingPush ?? 0;
388
- const conflicts = syncStatus?.conflicts.length ?? 0;
389
- const countGap = remoteActive !== null && remoteActive > localCounts.active
390
- ? remoteActive - localCounts.active
391
- : 0;
392
- const shouldRecommendSync = remoteReachable &&
393
- (pendingPull > 0 || pendingPush > 0 || conflicts > 0 || countGap > 0);
394
- if (shouldRecommendSync) {
395
- console.log("");
396
- printStatus("warn", "your local cache may be behind the shared remote brain");
397
- if (countGap > 0) {
398
- console.log(` Remote has about ${countGap} more active memor${countGap === 1 ? "y" : "ies"} than local.`);
399
- }
400
- if (pendingPull > 0 || pendingPush > 0) {
401
- const parts = [];
402
- if (pendingPull > 0)
403
- parts.push(`${pendingPull} to pull into local`);
404
- if (pendingPush > 0)
405
- parts.push(`${pendingPush} to push to remote`);
406
- console.log(` Pending sync: ${parts.join(", ")}.`);
407
- }
408
- if (conflicts > 0) {
409
- console.log(` ${conflicts} unresolved conflict${conflicts === 1 ? "" : "s"} — resolve before or during sync.`);
410
- }
411
- printStatus("progress", "recommended", "gnosys setup remote sync");
412
- }
413
- const choice = await askChoice(rl, shouldRecommendSync
414
- ? "Sync first so you do not lose access to remote-only memories on this Mac:"
415
- : "How do you want to proceed?", remoteReachable
416
- ? [
417
- { key: "1", label: "Run gnosys setup remote sync, then disconnect (recommended)" },
418
- { key: "2", label: "Disconnect now without syncing (keep current local DB only)" },
419
- { key: "3", label: "Cancel" },
420
- ]
421
- : [
422
- { key: "2", label: "Disconnect anyway (local DB only; remote not reachable to sync)" },
423
- { key: "3", label: "Cancel" },
424
- ], "3");
425
- if (choice === "3") {
426
- console.log("Cancelled.");
427
- return false;
428
- }
429
- if (choice === "1" && remoteReachable) {
430
- const spin = Spinner("syncing local and remote…");
431
- const syncRun = new RemoteSync(localDb, remotePath);
432
- try {
433
- const result = await syncRun.sync();
434
- if (result.errors.length > 0) {
435
- spin.fail("sync had errors");
436
- for (const e of result.errors)
437
- printStatus("fail", e);
438
- const proceed = await askConfirm(rl, "Disconnect anyway without a clean sync?", false);
439
- if (!proceed)
440
- return false;
441
- }
442
- else {
443
- spin.ok("sync complete", `pushed ${result.pushed} · pulled ${result.pulled} · conflicts ${result.conflicts.length}`);
444
- if (result.conflicts.length > 0) {
445
- printStatus("warn", "conflicts still open", "gnosys setup remote resolve <id> --keep <local|remote>");
446
- }
447
- }
448
- }
449
- finally {
450
- syncRun.closeRemote();
451
- }
452
- }
453
- else if (choice === "1" && !remoteReachable) {
454
- printStatus("fail", "cannot sync — mount the remote or cancel");
455
- return false;
456
- }
457
- if (choice === "2" && shouldRecommendSync) {
458
- const risky = await askConfirm(rl, "Disconnect without syncing? You may only see local memories on this Mac until you reconnect.", false);
459
- if (!risky) {
460
- console.log("Cancelled.");
461
- return false;
462
- }
463
- }
464
- const confirm = await askConfirm(rl, "Disconnect now? Remote files stay on disk; this machine uses ~/.gnosys/gnosys.db only.", false);
438
+ console.log(" The master folder on disk is not deleted.");
439
+ console.log(" After disconnect, this machine stops using multi-machine sync.");
440
+ const confirm = await askConfirm(rl, "Disconnect now? (You can re-run setup later.)", false);
465
441
  if (!confirm) {
466
442
  console.log("Cancelled.");
467
443
  return false;
468
444
  }
469
445
  clearRemoteSyncConfig(localDb);
470
- console.log(" Remote disconnected. Gnosys is now local-only on this machine.");
446
+ printStatus("ok", "disconnected", "multi-machine sync is off on this machine");
471
447
  return true;
472
448
  }
473
449
  async function revalidateRemote(_rl, _centralDb, currentRemote) {
@@ -475,10 +451,10 @@ async function revalidateRemote(_rl, _centralDb, currentRemote) {
475
451
  const spin = Spinner(`checking ${currentRemote}…`);
476
452
  const validation = await validateLocation(currentRemote);
477
453
  if (validation.ok) {
478
- spin.ok("remote is healthy");
454
+ spin.ok("master folder is reachable");
479
455
  }
480
456
  else {
481
- spin.fail("validation failed", "the remote may be unreachable or the path is wrong");
457
+ spin.fail("validation failed", "the master may be unreachable or the path is wrong");
482
458
  }
483
459
  showValidationSummary(validation);
484
460
  return validation.ok;
@@ -489,21 +465,27 @@ export async function configureFromPath(centralDb, remotePath, opts = {}) {
489
465
  const validation = await validateLocation(remotePath);
490
466
  showValidationSummary(validation);
491
467
  if (!validation.ok) {
492
- console.log("\nValidation failed. Remote not configured.");
468
+ console.log("\nValidation failed. Master folder not configured.");
493
469
  return false;
494
470
  }
495
- centralDb.setMeta(REMOTE_PATH_KEY, remotePath);
496
- console.log(`\n✓ Remote configured: ${remotePath}`);
497
- if (opts.migrate && !validation.checks.existingDb.found) {
498
- console.log("\nMigrating local DB to remote...");
471
+ const role = opts.role ?? "client";
472
+ persistMultiMachineConfig(centralDb, remotePath, role);
473
+ console.log(`\n✓ Multi-machine sync configured: ${remotePath} (${role})`);
474
+ if (role === "master") {
475
+ const { config: mc } = ensureMachineConfig();
476
+ writeMasterMarker(remotePath, mc.machineId);
477
+ }
478
+ if (opts.migrate && role === "master" && !validation.checks.existingDb.found) {
479
+ console.log("\nMigrating local DB to master folder...");
499
480
  const sync = new RemoteSync(centralDb, remotePath);
500
481
  try {
501
482
  const result = await sync.migrate();
502
483
  if (result.ok) {
503
- console.log(` ✓ Copied ${result.copied} memories to remote.`);
484
+ console.log(` ✓ Copied ${result.copied} memories to master folder.`);
485
+ archiveLocalDbBeforeMaster();
504
486
  }
505
487
  else {
506
- console.log(` ✗ Migration had errors:`);
488
+ console.log(" ✗ Migration had errors:");
507
489
  for (const e of result.errors)
508
490
  console.log(` ${e}`);
509
491
  return false;
@@ -514,7 +496,14 @@ export async function configureFromPath(centralDb, remotePath, opts = {}) {
514
496
  }
515
497
  }
516
498
  else if (validation.checks.existingDb.found) {
517
- console.log("\nExisting DB found at remote. Run 'gnosys remote sync' to merge.");
499
+ console.log("\nExisting DB found at master folder.");
518
500
  }
519
501
  return true;
520
502
  }
503
+ export { stagingDirForMachine, clientPresencePath } from "./syncStaging.js";
504
+ export const __test = {
505
+ matchesTypedPhrase,
506
+ detectClonedStagingPresence,
507
+ stagingDirForMachine,
508
+ clientPresencePath,
509
+ };