gnosys 5.11.4 → 5.12.2

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 (265) hide show
  1. package/dist/cli.js +377 -5162
  2. package/dist/index.js +542 -244
  3. package/dist/lib/addCommand.d.ts +9 -0
  4. package/dist/lib/addCommand.js +102 -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/archive.js +0 -2
  12. package/dist/lib/askCommand.d.ts +13 -0
  13. package/dist/lib/askCommand.js +145 -0
  14. package/dist/lib/attachCommand.d.ts +17 -0
  15. package/dist/lib/attachCommand.js +66 -0
  16. package/dist/lib/attachments.d.ts +43 -2
  17. package/dist/lib/attachments.js +81 -2
  18. package/dist/lib/audioExtract.js +4 -1
  19. package/dist/lib/auditCommand.d.ts +7 -0
  20. package/dist/lib/auditCommand.js +27 -0
  21. package/dist/lib/backupCommand.d.ts +6 -0
  22. package/dist/lib/backupCommand.js +54 -0
  23. package/dist/lib/bootstrapCommand.d.ts +15 -0
  24. package/dist/lib/bootstrapCommand.js +51 -0
  25. package/dist/lib/briefingCommand.d.ts +7 -0
  26. package/dist/lib/briefingCommand.js +92 -0
  27. package/dist/lib/centralizeCommand.d.ts +5 -0
  28. package/dist/lib/centralizeCommand.js +16 -0
  29. package/dist/lib/chat/choose.js +2 -2
  30. package/dist/lib/chatCommand.d.ts +12 -0
  31. package/dist/lib/chatCommand.js +46 -0
  32. package/dist/lib/checkCommand.d.ts +4 -0
  33. package/dist/lib/checkCommand.js +133 -0
  34. package/dist/lib/clientReadOverlay.d.ts +27 -0
  35. package/dist/lib/clientReadOverlay.js +76 -0
  36. package/dist/lib/clientReadResolve.d.ts +32 -0
  37. package/dist/lib/clientReadResolve.js +84 -0
  38. package/dist/lib/commitContextCommand.d.ts +9 -0
  39. package/dist/lib/commitContextCommand.js +142 -0
  40. package/dist/lib/config.d.ts +41 -48
  41. package/dist/lib/config.js +58 -57
  42. package/dist/lib/configCommand.d.ts +10 -0
  43. package/dist/lib/configCommand.js +321 -0
  44. package/dist/lib/connectCommand.d.ts +8 -0
  45. package/dist/lib/connectCommand.js +19 -0
  46. package/dist/lib/db.d.ts +68 -1
  47. package/dist/lib/db.js +385 -120
  48. package/dist/lib/dbWrite.d.ts +1 -1
  49. package/dist/lib/dearchiveCommand.d.ts +7 -0
  50. package/dist/lib/dearchiveCommand.js +41 -0
  51. package/dist/lib/discoverCommand.d.ts +9 -0
  52. package/dist/lib/discoverCommand.js +87 -0
  53. package/dist/lib/doctorCommand.d.ts +6 -0
  54. package/dist/lib/doctorCommand.js +256 -0
  55. package/dist/lib/docxExtract.js +1 -1
  56. package/dist/lib/dream.d.ts +50 -2
  57. package/dist/lib/dream.js +324 -30
  58. package/dist/lib/dreamCommand.d.ts +10 -0
  59. package/dist/lib/dreamCommand.js +195 -0
  60. package/dist/lib/dreamLaunchd.d.ts +2 -0
  61. package/dist/lib/dreamLaunchd.js +72 -0
  62. package/dist/lib/dreamLogCommand.d.ts +10 -0
  63. package/dist/lib/dreamLogCommand.js +58 -0
  64. package/dist/lib/dreamReport.d.ts +7 -0
  65. package/dist/lib/dreamReport.js +114 -0
  66. package/dist/lib/dreamRunLog.d.ts +121 -0
  67. package/dist/lib/dreamRunLog.js +234 -0
  68. package/dist/lib/embeddings.js +3 -3
  69. package/dist/lib/exportCommand.d.ts +18 -0
  70. package/dist/lib/exportCommand.js +101 -0
  71. package/dist/lib/exportProject.d.ts +3 -2
  72. package/dist/lib/exportProject.js +2 -1
  73. package/dist/lib/federated.js +1 -1
  74. package/dist/lib/fsearchCommand.d.ts +8 -0
  75. package/dist/lib/fsearchCommand.js +44 -0
  76. package/dist/lib/graphCommand.d.ts +4 -0
  77. package/dist/lib/graphCommand.js +68 -0
  78. package/dist/lib/helperGenerateCommand.d.ts +5 -0
  79. package/dist/lib/helperGenerateCommand.js +27 -0
  80. package/dist/lib/historyCommand.d.ts +5 -0
  81. package/dist/lib/historyCommand.js +51 -0
  82. package/dist/lib/hybridSearchCommand.d.ts +12 -0
  83. package/dist/lib/hybridSearchCommand.js +95 -0
  84. package/dist/lib/importCommand.d.ts +16 -0
  85. package/dist/lib/importCommand.js +89 -0
  86. package/dist/lib/importProject.js +2 -1
  87. package/dist/lib/importProjectCommand.d.ts +6 -0
  88. package/dist/lib/importProjectCommand.js +43 -0
  89. package/dist/lib/ingestCommand.d.ts +13 -0
  90. package/dist/lib/ingestCommand.js +95 -0
  91. package/dist/lib/installOutput.d.ts +36 -0
  92. package/dist/lib/installOutput.js +55 -0
  93. package/dist/lib/lensCommand.d.ts +20 -0
  94. package/dist/lib/lensCommand.js +61 -0
  95. package/dist/lib/lensing.d.ts +1 -0
  96. package/dist/lib/lensing.js +50 -9
  97. package/dist/lib/linksCommand.d.ts +7 -0
  98. package/dist/lib/linksCommand.js +48 -0
  99. package/dist/lib/listCommand.d.ts +8 -0
  100. package/dist/lib/listCommand.js +74 -0
  101. package/dist/lib/llm.d.ts +1 -1
  102. package/dist/lib/llm.js +27 -9
  103. package/dist/lib/localDiskCheck.d.ts +17 -0
  104. package/dist/lib/localDiskCheck.js +54 -0
  105. package/dist/lib/lock.d.ts +1 -1
  106. package/dist/lib/lock.js +5 -3
  107. package/dist/lib/machineConfig.d.ts +11 -1
  108. package/dist/lib/machineConfig.js +16 -0
  109. package/dist/lib/machineRegistry.d.ts +61 -0
  110. package/dist/lib/machineRegistry.js +80 -0
  111. package/dist/lib/maintainCommand.d.ts +8 -0
  112. package/dist/lib/maintainCommand.js +34 -0
  113. package/dist/lib/masterLease.d.ts +20 -0
  114. package/dist/lib/masterLease.js +68 -0
  115. package/dist/lib/migrate.js +0 -1
  116. package/dist/lib/migrateCommand.d.ts +7 -0
  117. package/dist/lib/migrateCommand.js +158 -0
  118. package/dist/lib/migrateDbCommand.d.ts +9 -0
  119. package/dist/lib/migrateDbCommand.js +94 -0
  120. package/dist/lib/modelValidation.d.ts +5 -0
  121. package/dist/lib/modelValidation.js +27 -0
  122. package/dist/lib/multimodalIngest.js +1 -1
  123. package/dist/lib/openrouterTiers.d.ts +29 -0
  124. package/dist/lib/openrouterTiers.js +113 -0
  125. package/dist/lib/platform.d.ts +0 -6
  126. package/dist/lib/platform.js +0 -28
  127. package/dist/lib/prefCommand.d.ts +10 -0
  128. package/dist/lib/prefCommand.js +118 -0
  129. package/dist/lib/projectsCommand.d.ts +8 -0
  130. package/dist/lib/projectsCommand.js +131 -0
  131. package/dist/lib/readCommand.d.ts +7 -0
  132. package/dist/lib/readCommand.js +63 -0
  133. package/dist/lib/recall.d.ts +3 -0
  134. package/dist/lib/recall.js +19 -4
  135. package/dist/lib/recallCommand.d.ts +11 -0
  136. package/dist/lib/recallCommand.js +112 -0
  137. package/dist/lib/reflectCommand.d.ts +8 -0
  138. package/dist/lib/reflectCommand.js +61 -0
  139. package/dist/lib/reindexCommand.d.ts +4 -0
  140. package/dist/lib/reindexCommand.js +34 -0
  141. package/dist/lib/reindexGraphCommand.d.ts +4 -0
  142. package/dist/lib/reindexGraphCommand.js +12 -0
  143. package/dist/lib/reinforceCommand.d.ts +8 -0
  144. package/dist/lib/reinforceCommand.js +40 -0
  145. package/dist/lib/remote.d.ts +5 -1
  146. package/dist/lib/remote.js +5 -1
  147. package/dist/lib/remoteWizard.d.ts +24 -5
  148. package/dist/lib/remoteWizard.js +308 -319
  149. package/dist/lib/restoreCommand.d.ts +5 -0
  150. package/dist/lib/restoreCommand.js +35 -0
  151. package/dist/lib/rulesGen.d.ts +8 -0
  152. package/dist/lib/rulesGen.js +16 -0
  153. package/dist/lib/sandboxStartCommand.d.ts +6 -0
  154. package/dist/lib/sandboxStartCommand.js +25 -0
  155. package/dist/lib/sandboxStatusCommand.d.ts +4 -0
  156. package/dist/lib/sandboxStatusCommand.js +24 -0
  157. package/dist/lib/sandboxStopCommand.d.ts +4 -0
  158. package/dist/lib/sandboxStopCommand.js +21 -0
  159. package/dist/lib/search.d.ts +0 -2
  160. package/dist/lib/search.js +0 -7
  161. package/dist/lib/searchCommand.d.ts +9 -0
  162. package/dist/lib/searchCommand.js +90 -0
  163. package/dist/lib/semanticSearchCommand.d.ts +8 -0
  164. package/dist/lib/semanticSearchCommand.js +52 -0
  165. package/dist/lib/setup/configSetRender.js +2 -0
  166. package/dist/lib/setup/providerGlyphs.d.ts +19 -0
  167. package/dist/lib/setup/providerGlyphs.js +42 -0
  168. package/dist/lib/setup/remoteRender.d.ts +31 -1
  169. package/dist/lib/setup/remoteRender.js +95 -4
  170. package/dist/lib/setup/sections/providers.d.ts +17 -0
  171. package/dist/lib/setup/sections/providers.js +307 -0
  172. package/dist/lib/setup/sections/routing.d.ts +2 -6
  173. package/dist/lib/setup/sections/routing.js +67 -82
  174. package/dist/lib/setup/sections/taskRoutingEditor.d.ts +13 -0
  175. package/dist/lib/setup/sections/taskRoutingEditor.js +139 -0
  176. package/dist/lib/setup/summary.d.ts +9 -0
  177. package/dist/lib/setup/summary.js +51 -37
  178. package/dist/lib/setup/ui/header.js +0 -1
  179. package/dist/lib/setup.d.ts +105 -15
  180. package/dist/lib/setup.js +747 -287
  181. package/dist/lib/setupKeys.d.ts +42 -0
  182. package/dist/lib/setupKeys.js +564 -0
  183. package/dist/lib/setupRemoteCommand.d.ts +4 -0
  184. package/dist/lib/setupRemoteCommand.js +28 -0
  185. package/dist/lib/setupRemotePullCommand.d.ts +5 -0
  186. package/dist/lib/setupRemotePullCommand.js +52 -0
  187. package/dist/lib/setupRemotePushCommand.d.ts +5 -0
  188. package/dist/lib/setupRemotePushCommand.js +57 -0
  189. package/dist/lib/setupRemoteResolveCommand.d.ts +4 -0
  190. package/dist/lib/setupRemoteResolveCommand.js +48 -0
  191. package/dist/lib/setupRemoteStatusCommand.d.ts +4 -0
  192. package/dist/lib/setupRemoteStatusCommand.js +73 -0
  193. package/dist/lib/setupRemoteSyncCommand.d.ts +6 -0
  194. package/dist/lib/setupRemoteSyncCommand.js +65 -0
  195. package/dist/lib/setupSyncProjectsCommand.d.ts +4 -0
  196. package/dist/lib/setupSyncProjectsCommand.js +292 -0
  197. package/dist/lib/staleCommand.d.ts +8 -0
  198. package/dist/lib/staleCommand.js +34 -0
  199. package/dist/lib/statsCommand.d.ts +6 -0
  200. package/dist/lib/statsCommand.js +142 -0
  201. package/dist/lib/statusCommand.d.ts +18 -0
  202. package/dist/lib/statusCommand.js +250 -0
  203. package/dist/lib/storesCommand.d.ts +2 -0
  204. package/dist/lib/storesCommand.js +4 -0
  205. package/dist/lib/syncClient.d.ts +41 -0
  206. package/dist/lib/syncClient.js +234 -0
  207. package/dist/lib/syncCommand.d.ts +6 -0
  208. package/dist/lib/syncCommand.js +57 -0
  209. package/dist/lib/syncDoctorCommand.d.ts +5 -0
  210. package/dist/lib/syncDoctorCommand.js +100 -0
  211. package/dist/lib/syncIngest.d.ts +30 -0
  212. package/dist/lib/syncIngest.js +175 -0
  213. package/dist/lib/syncIngestLaunchd.d.ts +8 -0
  214. package/dist/lib/syncIngestLaunchd.js +93 -0
  215. package/dist/lib/syncIngestStartup.d.ts +5 -0
  216. package/dist/lib/syncIngestStartup.js +29 -0
  217. package/dist/lib/syncIngestSystemd.d.ts +10 -0
  218. package/dist/lib/syncIngestSystemd.js +97 -0
  219. package/dist/lib/syncIngestTimer.d.ts +8 -0
  220. package/dist/lib/syncIngestTimer.js +27 -0
  221. package/dist/lib/syncIngestTimerCommand.d.ts +7 -0
  222. package/dist/lib/syncIngestTimerCommand.js +83 -0
  223. package/dist/lib/syncLock.d.ts +6 -0
  224. package/dist/lib/syncLock.js +74 -0
  225. package/dist/lib/syncSnapshot.d.ts +32 -0
  226. package/dist/lib/syncSnapshot.js +188 -0
  227. package/dist/lib/syncStaging.d.ts +79 -0
  228. package/dist/lib/syncStaging.js +237 -0
  229. package/dist/lib/tagsAddCommand.d.ts +8 -0
  230. package/dist/lib/tagsAddCommand.js +18 -0
  231. package/dist/lib/tagsCommand.d.ts +4 -0
  232. package/dist/lib/tagsCommand.js +16 -0
  233. package/dist/lib/timelineCommand.d.ts +7 -0
  234. package/dist/lib/timelineCommand.js +49 -0
  235. package/dist/lib/traceCommand.d.ts +6 -0
  236. package/dist/lib/traceCommand.js +39 -0
  237. package/dist/lib/traverseCommand.d.ts +6 -0
  238. package/dist/lib/traverseCommand.js +58 -0
  239. package/dist/lib/updateCommand.d.ts +13 -0
  240. package/dist/lib/updateCommand.js +67 -0
  241. package/dist/lib/updateStatusCommand.d.ts +5 -0
  242. package/dist/lib/updateStatusCommand.js +38 -0
  243. package/dist/lib/webAddCommand.d.ts +8 -0
  244. package/dist/lib/webAddCommand.js +55 -0
  245. package/dist/lib/webBuildCommand.d.ts +10 -0
  246. package/dist/lib/webBuildCommand.js +65 -0
  247. package/dist/lib/webBuildIndexCommand.d.ts +8 -0
  248. package/dist/lib/webBuildIndexCommand.js +37 -0
  249. package/dist/lib/webIndex.js +0 -1
  250. package/dist/lib/webIngestCommand.d.ts +11 -0
  251. package/dist/lib/webIngestCommand.js +51 -0
  252. package/dist/lib/webInitCommand.d.ts +9 -0
  253. package/dist/lib/webInitCommand.js +167 -0
  254. package/dist/lib/webRemoveCommand.d.ts +5 -0
  255. package/dist/lib/webRemoveCommand.js +41 -0
  256. package/dist/lib/webStatusCommand.d.ts +5 -0
  257. package/dist/lib/webStatusCommand.js +94 -0
  258. package/dist/lib/webUpdateCommand.d.ts +7 -0
  259. package/dist/lib/webUpdateCommand.js +72 -0
  260. package/dist/lib/workingSetCommand.d.ts +6 -0
  261. package/dist/lib/workingSetCommand.js +37 -0
  262. package/dist/sandbox/client.js +1 -1
  263. package/dist/sandbox/manager.js +1 -14
  264. package/dist/sandbox/server.js +3 -5
  265. package/package.json +6 -2
@@ -0,0 +1,93 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { execSync } from "child_process";
5
+ const LABEL = "com.gnosys.sync-ingest";
6
+ function plistPath() {
7
+ return path.join(os.homedir(), "Library", "LaunchAgents", `${LABEL}.plist`);
8
+ }
9
+ function logPath() {
10
+ return path.join(os.tmpdir(), "gnosys-sync-ingest.log");
11
+ }
12
+ function xmlEscape(value) {
13
+ return value
14
+ .replace(/&/g, "&")
15
+ .replace(/</g, "&lt;")
16
+ .replace(/>/g, "&gt;")
17
+ .replace(/"/g, "&quot;");
18
+ }
19
+ /** Build launchd plist XML (testable without writing to disk). */
20
+ export function buildSyncIngestLaunchAgentPlist(intervalMinutes) {
21
+ const nodePath = process.execPath;
22
+ const cliPath = process.argv[1] || "gnosys";
23
+ const intervalSec = Math.max(1, intervalMinutes) * 60;
24
+ const pathEnv = `${path.dirname(nodePath)}:/usr/local/bin:/usr/bin:/bin`;
25
+ return `<?xml version="1.0" encoding="UTF-8"?>
26
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
27
+ <plist version="1.0">
28
+ <dict>
29
+ <key>Label</key>
30
+ <string>${LABEL}</string>
31
+ <key>ProgramArguments</key>
32
+ <array>
33
+ <string>${xmlEscape(nodePath)}</string>
34
+ <string>${xmlEscape(cliPath)}</string>
35
+ <string>setup</string>
36
+ <string>remote</string>
37
+ <string>doctor</string>
38
+ <string>--ingest</string>
39
+ <string>--quiet</string>
40
+ </array>
41
+ <key>StartInterval</key>
42
+ <integer>${intervalSec}</integer>
43
+ <key>StandardOutPath</key>
44
+ <string>${xmlEscape(logPath())}</string>
45
+ <key>StandardErrorPath</key>
46
+ <string>${xmlEscape(logPath())}</string>
47
+ <key>EnvironmentVariables</key>
48
+ <dict>
49
+ <key>HOME</key>
50
+ <string>${xmlEscape(os.homedir())}</string>
51
+ <key>PATH</key>
52
+ <string>${xmlEscape(pathEnv)}</string>
53
+ </dict>
54
+ </dict>
55
+ </plist>
56
+ `;
57
+ }
58
+ export function installSyncIngestLaunchAgent(intervalMinutes = 15) {
59
+ if (process.platform !== "darwin")
60
+ return null;
61
+ const file = plistPath();
62
+ fs.mkdirSync(path.dirname(file), { recursive: true });
63
+ fs.writeFileSync(file, buildSyncIngestLaunchAgentPlist(intervalMinutes), "utf8");
64
+ try {
65
+ execSync(`launchctl load -w "${file}"`, { stdio: "ignore" });
66
+ }
67
+ catch (err) {
68
+ throw new Error(`Failed to load launch agent: ${err instanceof Error ? err.message : String(err)}`);
69
+ }
70
+ return file;
71
+ }
72
+ export function uninstallSyncIngestLaunchAgent() {
73
+ if (process.platform !== "darwin")
74
+ return null;
75
+ const file = plistPath();
76
+ try {
77
+ execSync(`launchctl unload "${file}"`, { stdio: "ignore" });
78
+ }
79
+ catch {
80
+ // Already unloaded or missing.
81
+ }
82
+ try {
83
+ fs.unlinkSync(file);
84
+ }
85
+ catch {
86
+ // Missing is already uninstalled.
87
+ }
88
+ return file;
89
+ }
90
+ export function getSyncIngestLaunchAgentStatus() {
91
+ const file = plistPath();
92
+ return { installed: fs.existsSync(file), path: file };
93
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Run one ingest sweep on MCP server startup (master role only).
3
+ * Non-blocking — callers should fire-and-forget.
4
+ */
5
+ export declare function maybeRunStartupIngestSweep(): Promise<void>;
@@ -0,0 +1,29 @@
1
+ import { GnosysDB } from "./db.js";
2
+ import { readMachineConfig } from "./machineConfig.js";
3
+ import { getConfiguredRemotePath } from "./remote.js";
4
+ import { runMasterIngestSweepAndPublish } from "./syncIngest.js";
5
+ /**
6
+ * Run one ingest sweep on MCP server startup (master role only).
7
+ * Non-blocking — callers should fire-and-forget.
8
+ */
9
+ export async function maybeRunStartupIngestSweep() {
10
+ const mc = readMachineConfig();
11
+ if (!mc?.remote.enabled || mc.remote.role !== "master")
12
+ return;
13
+ const db = GnosysDB.openLocal();
14
+ let masterPath = null;
15
+ try {
16
+ if (!db.isAvailable())
17
+ return;
18
+ masterPath = getConfiguredRemotePath(db);
19
+ }
20
+ finally {
21
+ db.close();
22
+ }
23
+ if (!masterPath)
24
+ return;
25
+ const result = await runMasterIngestSweepAndPublish(masterPath, { quiet: true });
26
+ if (result.errors.length > 0) {
27
+ console.error(`[sync] Startup ingest sweep: ${result.ingested} ingested, ${result.errors.length} error(s)`);
28
+ }
29
+ }
@@ -0,0 +1,10 @@
1
+ /** Build systemd service unit (testable without writing to disk). */
2
+ export declare function buildSyncIngestSystemdService(): string;
3
+ /** Build systemd timer unit (testable without writing to disk). */
4
+ export declare function buildSyncIngestSystemdTimer(intervalMinutes: number): string;
5
+ export declare function installSyncIngestSystemdTimer(intervalMinutes?: number): string | null;
6
+ export declare function uninstallSyncIngestSystemdTimer(): string | null;
7
+ export declare function getSyncIngestSystemdTimerStatus(): {
8
+ installed: boolean;
9
+ path: string;
10
+ };
@@ -0,0 +1,97 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import { execSync } from "child_process";
5
+ const SERVICE_NAME = "gnosys-sync-ingest";
6
+ function systemdUserDir() {
7
+ return path.join(os.homedir(), ".config", "systemd", "user");
8
+ }
9
+ function serviceFilePath() {
10
+ return path.join(systemdUserDir(), `${SERVICE_NAME}.service`);
11
+ }
12
+ function timerFilePath() {
13
+ return path.join(systemdUserDir(), `${SERVICE_NAME}.timer`);
14
+ }
15
+ function execLine() {
16
+ const nodePath = process.execPath;
17
+ const cliPath = process.argv[1] || "gnosys";
18
+ return `${nodePath} ${cliPath} setup remote doctor --ingest --quiet`;
19
+ }
20
+ /** Build systemd service unit (testable without writing to disk). */
21
+ export function buildSyncIngestSystemdService() {
22
+ return `[Unit]
23
+ Description=Gnosys sync ingest sweep
24
+
25
+ [Service]
26
+ Type=oneshot
27
+ ExecStart=${execLine()}
28
+ Environment=HOME=${os.homedir()}
29
+ `;
30
+ }
31
+ /** Build systemd timer unit (testable without writing to disk). */
32
+ export function buildSyncIngestSystemdTimer(intervalMinutes) {
33
+ const intervalSec = Math.max(1, intervalMinutes) * 60;
34
+ return `[Unit]
35
+ Description=Gnosys sync ingest timer
36
+
37
+ [Timer]
38
+ OnBootSec=5min
39
+ OnUnitActiveSec=${intervalSec}s
40
+
41
+ [Install]
42
+ WantedBy=timers.target
43
+ `;
44
+ }
45
+ export function installSyncIngestSystemdTimer(intervalMinutes = 15) {
46
+ if (process.platform !== "linux")
47
+ return null;
48
+ const dir = systemdUserDir();
49
+ fs.mkdirSync(dir, { recursive: true });
50
+ const servicePath = serviceFilePath();
51
+ const timerPath = timerFilePath();
52
+ fs.writeFileSync(servicePath, buildSyncIngestSystemdService(), "utf8");
53
+ fs.writeFileSync(timerPath, buildSyncIngestSystemdTimer(intervalMinutes), "utf8");
54
+ try {
55
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
56
+ execSync(`systemctl --user enable --now ${SERVICE_NAME}.timer`, { stdio: "ignore" });
57
+ }
58
+ catch (err) {
59
+ throw new Error(`Failed to enable systemd timer: ${err instanceof Error ? err.message : String(err)}`);
60
+ }
61
+ return timerPath;
62
+ }
63
+ export function uninstallSyncIngestSystemdTimer() {
64
+ if (process.platform !== "linux")
65
+ return null;
66
+ const servicePath = serviceFilePath();
67
+ const timerPath = timerFilePath();
68
+ try {
69
+ execSync(`systemctl --user disable --now ${SERVICE_NAME}.timer`, { stdio: "ignore" });
70
+ }
71
+ catch {
72
+ // Timer may already be removed.
73
+ }
74
+ for (const file of [timerPath, servicePath]) {
75
+ try {
76
+ fs.unlinkSync(file);
77
+ }
78
+ catch {
79
+ // Missing is already uninstalled.
80
+ }
81
+ }
82
+ try {
83
+ execSync("systemctl --user daemon-reload", { stdio: "ignore" });
84
+ }
85
+ catch {
86
+ // Best effort.
87
+ }
88
+ return timerPath;
89
+ }
90
+ export function getSyncIngestSystemdTimerStatus() {
91
+ const timerPath = timerFilePath();
92
+ const servicePath = serviceFilePath();
93
+ return {
94
+ installed: fs.existsSync(timerPath) && fs.existsSync(servicePath),
95
+ path: timerPath,
96
+ };
97
+ }
@@ -0,0 +1,8 @@
1
+ export interface SyncIngestTimerStatus {
2
+ installed: boolean;
3
+ path: string;
4
+ platform: "darwin" | "linux" | "unsupported";
5
+ }
6
+ export declare function getSyncIngestTimerStatus(): SyncIngestTimerStatus;
7
+ export declare function installSyncIngestTimer(intervalMinutes?: number): string | null;
8
+ export declare function uninstallSyncIngestTimer(): string | null;
@@ -0,0 +1,27 @@
1
+ import { getSyncIngestLaunchAgentStatus, installSyncIngestLaunchAgent, uninstallSyncIngestLaunchAgent, } from "./syncIngestLaunchd.js";
2
+ import { getSyncIngestSystemdTimerStatus, installSyncIngestSystemdTimer, uninstallSyncIngestSystemdTimer, } from "./syncIngestSystemd.js";
3
+ export function getSyncIngestTimerStatus() {
4
+ if (process.platform === "darwin") {
5
+ const status = getSyncIngestLaunchAgentStatus();
6
+ return { ...status, platform: "darwin" };
7
+ }
8
+ if (process.platform === "linux") {
9
+ const status = getSyncIngestSystemdTimerStatus();
10
+ return { ...status, platform: "linux" };
11
+ }
12
+ return { installed: false, path: "", platform: "unsupported" };
13
+ }
14
+ export function installSyncIngestTimer(intervalMinutes = 15) {
15
+ if (process.platform === "darwin")
16
+ return installSyncIngestLaunchAgent(intervalMinutes);
17
+ if (process.platform === "linux")
18
+ return installSyncIngestSystemdTimer(intervalMinutes);
19
+ return null;
20
+ }
21
+ export function uninstallSyncIngestTimer() {
22
+ if (process.platform === "darwin")
23
+ return uninstallSyncIngestLaunchAgent();
24
+ if (process.platform === "linux")
25
+ return uninstallSyncIngestSystemdTimer();
26
+ return null;
27
+ }
@@ -0,0 +1,7 @@
1
+ export declare function runSyncIngestTimerCommand(opts: {
2
+ install?: boolean;
3
+ uninstall?: boolean;
4
+ status?: boolean;
5
+ interval?: string;
6
+ json?: boolean;
7
+ }): Promise<void>;
@@ -0,0 +1,83 @@
1
+ import { printStatus } from "./setup/ui/status.js";
2
+ import { getSyncIngestTimerStatus, installSyncIngestTimer, uninstallSyncIngestTimer, } from "./syncIngestTimer.js";
3
+ export async function runSyncIngestTimerCommand(opts) {
4
+ const intervalMinutes = Math.max(1, parseInt(opts.interval ?? "15", 10) || 15);
5
+ const timerStatus = getSyncIngestTimerStatus();
6
+ if (opts.json) {
7
+ if (opts.install) {
8
+ try {
9
+ const path = installSyncIngestTimer(intervalMinutes);
10
+ console.log(JSON.stringify({
11
+ action: "install",
12
+ installed: true,
13
+ path,
14
+ intervalMinutes,
15
+ platform: timerStatus.platform,
16
+ }));
17
+ }
18
+ catch (err) {
19
+ process.exitCode = 1;
20
+ console.log(JSON.stringify({
21
+ action: "install",
22
+ installed: false,
23
+ error: err instanceof Error ? err.message : String(err),
24
+ platform: timerStatus.platform,
25
+ }));
26
+ }
27
+ return;
28
+ }
29
+ if (opts.uninstall) {
30
+ try {
31
+ const path = uninstallSyncIngestTimer();
32
+ console.log(JSON.stringify({ action: "uninstall", installed: false, path }));
33
+ }
34
+ catch (err) {
35
+ process.exitCode = 1;
36
+ console.log(JSON.stringify({
37
+ action: "uninstall",
38
+ error: err instanceof Error ? err.message : String(err),
39
+ }));
40
+ }
41
+ return;
42
+ }
43
+ console.log(JSON.stringify({ ...getSyncIngestTimerStatus(), intervalMinutes }));
44
+ return;
45
+ }
46
+ if (timerStatus.platform === "unsupported") {
47
+ console.log("OS-level ingest timer is not supported on this platform.");
48
+ console.log("Windows: use Task Scheduler manually or run `gnosys setup remote doctor --ingest`.");
49
+ if (opts.install || opts.uninstall)
50
+ process.exitCode = 1;
51
+ return;
52
+ }
53
+ if (opts.install) {
54
+ try {
55
+ const installedPath = installSyncIngestTimer(intervalMinutes);
56
+ printStatus("ok", `Ingest timer installed (${installedPath}, every ${intervalMinutes} min)`);
57
+ }
58
+ catch (err) {
59
+ printStatus("fail", err instanceof Error ? err.message : String(err));
60
+ process.exitCode = 1;
61
+ }
62
+ return;
63
+ }
64
+ if (opts.uninstall) {
65
+ try {
66
+ const removedPath = uninstallSyncIngestTimer();
67
+ printStatus("ok", `Ingest timer uninstalled (${removedPath})`);
68
+ }
69
+ catch (err) {
70
+ printStatus("fail", err instanceof Error ? err.message : String(err));
71
+ process.exitCode = 1;
72
+ }
73
+ return;
74
+ }
75
+ const current = getSyncIngestTimerStatus();
76
+ if (current.installed) {
77
+ console.log(`Ingest timer: installed (${current.path})`);
78
+ }
79
+ else {
80
+ console.log("Ingest timer: not installed");
81
+ console.log("Install with: gnosys setup remote timer --install");
82
+ }
83
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Sync advisory lock on the master's **local** ~/.gnosys (never on network share).
3
+ */
4
+ /** Synchronous ingest lock for master sweep (same directory as lock file). */
5
+ export declare function acquireWriteLockSync(lockPath: string, operation: string): () => void;
6
+ export declare function releaseWriteLockSync(lockPath: string): void;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Sync advisory lock on the master's **local** ~/.gnosys (never on network share).
3
+ */
4
+ import fs from "fs";
5
+ import path from "path";
6
+ const LOCK_STALE_MS = 120_000;
7
+ function readLock(lockPath) {
8
+ try {
9
+ const raw = fs.readFileSync(lockPath, "utf-8");
10
+ const o = JSON.parse(raw);
11
+ if (typeof o.pid === "number" && typeof o.timestamp === "number")
12
+ return o;
13
+ }
14
+ catch {
15
+ // no lock
16
+ }
17
+ return null;
18
+ }
19
+ function isStale(lock) {
20
+ if (Date.now() - lock.timestamp > LOCK_STALE_MS)
21
+ return true;
22
+ try {
23
+ process.kill(lock.pid, 0);
24
+ return false;
25
+ }
26
+ catch {
27
+ return true;
28
+ }
29
+ }
30
+ /** Synchronous ingest lock for master sweep (same directory as lock file). */
31
+ export function acquireWriteLockSync(lockPath, operation) {
32
+ fs.mkdirSync(path.dirname(lockPath), { recursive: true });
33
+ const start = Date.now();
34
+ while (true) {
35
+ const existing = readLock(lockPath);
36
+ if (existing && !isStale(existing)) {
37
+ if (Date.now() - start > 30_000) {
38
+ throw new Error(`ingest lock timeout (held by pid ${existing.pid})`);
39
+ }
40
+ // busy-wait briefly
41
+ const waitUntil = Date.now() + 50;
42
+ while (Date.now() < waitUntil) {
43
+ /* spin */
44
+ }
45
+ continue;
46
+ }
47
+ if (existing) {
48
+ try {
49
+ fs.unlinkSync(lockPath);
50
+ }
51
+ catch {
52
+ // race
53
+ }
54
+ }
55
+ try {
56
+ fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, timestamp: Date.now(), operation }));
57
+ return () => releaseWriteLockSync(lockPath);
58
+ }
59
+ catch {
60
+ if (Date.now() - start > 30_000)
61
+ throw new Error("failed to acquire ingest lock");
62
+ }
63
+ }
64
+ }
65
+ export function releaseWriteLockSync(lockPath) {
66
+ try {
67
+ const existing = readLock(lockPath);
68
+ if (existing?.pid === process.pid)
69
+ fs.unlinkSync(lockPath);
70
+ }
71
+ catch {
72
+ // ignore
73
+ }
74
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * v13 snapshot publication — immutable DB copies + manifest with epoch fencing.
3
+ */
4
+ export interface SnapshotManifestFile {
5
+ epoch: number;
6
+ seq: number;
7
+ snapshotFile: string;
8
+ publishedAt: string;
9
+ checksum: string;
10
+ sizeBytes: number;
11
+ }
12
+ export declare function masterSnapshotsDir(masterPath: string): string;
13
+ export declare function clientSnapshotStore(masterPath: string): string;
14
+ /** Read the master's currently published snapshot manifest (null if none). */
15
+ export declare function getMasterManifest(masterPath: string): SnapshotManifestFile | null;
16
+ /** Publish a new immutable snapshot on the master (re-validates lease epoch first). */
17
+ export declare function publishMasterSnapshot(masterPath: string): Promise<SnapshotManifestFile | null>;
18
+ export declare function compareSnapshotVersion(accepted: {
19
+ epoch: number;
20
+ seq: number;
21
+ } | null, incoming: {
22
+ epoch: number;
23
+ seq: number;
24
+ }): boolean;
25
+ /** Copy master snapshot to local client store atomically. */
26
+ export declare function acceptClientSnapshot(masterPath: string, manifest: SnapshotManifestFile): {
27
+ ok: boolean;
28
+ localPath?: string;
29
+ reason?: string;
30
+ };
31
+ export declare function getClientAcceptedManifest(masterPath: string): SnapshotManifestFile | null;
32
+ export declare function formatSnapshotAge(publishedAt: string): string;
@@ -0,0 +1,188 @@
1
+ /**
2
+ * v13 snapshot publication — immutable DB copies + manifest with epoch fencing.
3
+ */
4
+ import { createHash } from "crypto";
5
+ import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, } from "fs";
6
+ import path from "path";
7
+ import { GnosysDB } from "./db.js";
8
+ import { getGnosysHome } from "./paths.js";
9
+ import { atomicWriteFileSync } from "./atomicWrite.js";
10
+ import { assertMasterLeaseHeld, readMasterMarker, touchMasterMarkerHeartbeat, validateLeaseEpochBeforeWrite, } from "./masterLease.js";
11
+ import { ensureMachineConfig } from "./machineConfig.js";
12
+ import { acquireWriteLockSync } from "./syncLock.js";
13
+ const SNAPSHOT_RETENTION = 2;
14
+ const MANIFEST_FILENAME = "snapshot-manifest.json";
15
+ export function masterSnapshotsDir(masterPath) {
16
+ return path.join(masterPath, "snapshots");
17
+ }
18
+ export function clientSnapshotStore(masterPath) {
19
+ const key = createHash("sha256").update(path.resolve(masterPath)).digest("hex").slice(0, 16);
20
+ return path.join(getGnosysHome(), "client-snapshots", key);
21
+ }
22
+ function sha256File(filePath) {
23
+ const buf = readFileSync(filePath);
24
+ return createHash("sha256").update(buf).digest("hex");
25
+ }
26
+ function readManifestFile(masterPath) {
27
+ const p = path.join(masterSnapshotsDir(masterPath), MANIFEST_FILENAME);
28
+ if (!existsSync(p))
29
+ return null;
30
+ try {
31
+ return JSON.parse(readFileSync(p, "utf-8"));
32
+ }
33
+ catch {
34
+ return null;
35
+ }
36
+ }
37
+ /** Read the master's currently published snapshot manifest (null if none). */
38
+ export function getMasterManifest(masterPath) {
39
+ return readManifestFile(masterPath);
40
+ }
41
+ function writeManifestFile(masterPath, manifest) {
42
+ const dir = masterSnapshotsDir(masterPath);
43
+ mkdirSync(dir, { recursive: true });
44
+ atomicWriteFileSync(path.join(dir, MANIFEST_FILENAME), JSON.stringify(manifest, null, 2) + "\n");
45
+ }
46
+ function gcOldSnapshots(masterPath, keepFiles) {
47
+ const dir = masterSnapshotsDir(masterPath);
48
+ if (!existsSync(dir))
49
+ return;
50
+ for (const name of readdirSync(dir)) {
51
+ if (!name.endsWith(".db"))
52
+ continue;
53
+ if (keepFiles.has(name))
54
+ continue;
55
+ try {
56
+ unlinkSync(path.join(dir, name));
57
+ }
58
+ catch {
59
+ // ignore
60
+ }
61
+ }
62
+ }
63
+ /** Publish a new immutable snapshot on the master (re-validates lease epoch first). */
64
+ export async function publishMasterSnapshot(masterPath) {
65
+ const { config: mc } = ensureMachineConfig();
66
+ const lockPath = path.join(getGnosysHome(), "master-ingest.lock");
67
+ const release = acquireWriteLockSync(lockPath, "snapshot-publish");
68
+ try {
69
+ touchMasterMarkerHeartbeat(masterPath);
70
+ assertMasterLeaseHeld(masterPath, mc.machineId);
71
+ const marker = readMasterMarker(masterPath);
72
+ if (!marker)
73
+ return null;
74
+ validateLeaseEpochBeforeWrite(masterPath, marker.epoch, mc.machineId);
75
+ const masterDb = new GnosysDB(masterPath);
76
+ if (!masterDb.isAvailable()) {
77
+ masterDb.close();
78
+ return null;
79
+ }
80
+ const prevDb = masterDb.getSnapshotManifest();
81
+ const prevFile = readManifestFile(masterPath);
82
+ const prevSeq = Math.max(prevDb?.seq ?? 0, prevFile?.seq ?? 0);
83
+ const nextSeq = prevSeq + 1;
84
+ const snapDir = masterSnapshotsDir(masterPath);
85
+ mkdirSync(snapDir, { recursive: true });
86
+ const snapName = `snap-${marker.epoch}-${nextSeq}.db`;
87
+ const finalPath = path.join(snapDir, snapName);
88
+ const tmpPath = path.join(snapDir, `.${snapName}.tmp`);
89
+ await masterDb.backup(path.dirname(tmpPath));
90
+ const created = readdirSync(snapDir).find((n) => n.startsWith("gnosys-backup-") && n.endsWith(".db"));
91
+ if (created) {
92
+ renameSync(path.join(snapDir, created), tmpPath);
93
+ }
94
+ if (existsSync(tmpPath)) {
95
+ renameSync(tmpPath, finalPath);
96
+ }
97
+ const checksum = sha256File(finalPath);
98
+ const sizeBytes = statSync(finalPath).size;
99
+ const publishedAt = new Date().toISOString();
100
+ const fileManifest = {
101
+ epoch: marker.epoch,
102
+ seq: nextSeq,
103
+ snapshotFile: snapName,
104
+ publishedAt,
105
+ checksum,
106
+ sizeBytes,
107
+ };
108
+ writeManifestFile(masterPath, fileManifest);
109
+ masterDb.publishSnapshotManifest({
110
+ epoch: marker.epoch,
111
+ seq: nextSeq,
112
+ snapshotPath: finalPath,
113
+ publishedAt,
114
+ checksum,
115
+ sizeBytes,
116
+ heartbeatAt: publishedAt,
117
+ });
118
+ const keep = new Set(readdirSync(snapDir)
119
+ .filter((n) => n.endsWith(".db"))
120
+ .sort()
121
+ .slice(-SNAPSHOT_RETENTION));
122
+ gcOldSnapshots(masterPath, keep);
123
+ masterDb.close();
124
+ return fileManifest;
125
+ }
126
+ finally {
127
+ release();
128
+ }
129
+ }
130
+ export function compareSnapshotVersion(accepted, incoming) {
131
+ if (!accepted)
132
+ return true;
133
+ if (incoming.epoch > accepted.epoch)
134
+ return true;
135
+ if (incoming.epoch === accepted.epoch && incoming.seq > accepted.seq)
136
+ return true;
137
+ return false;
138
+ }
139
+ /** Copy master snapshot to local client store atomically. */
140
+ export function acceptClientSnapshot(masterPath, manifest) {
141
+ const src = path.join(masterSnapshotsDir(masterPath), manifest.snapshotFile);
142
+ if (!existsSync(src)) {
143
+ return { ok: false, reason: "snapshot file missing on master" };
144
+ }
145
+ const localChecksum = sha256File(src);
146
+ if (localChecksum !== manifest.checksum) {
147
+ return { ok: false, reason: "snapshot checksum mismatch" };
148
+ }
149
+ const store = clientSnapshotStore(masterPath);
150
+ mkdirSync(store, { recursive: true });
151
+ const tmp = path.join(store, ".gnosys.db.tmp");
152
+ const current = path.join(store, "gnosys.db");
153
+ copyFileSync(src, tmp);
154
+ const roundTrip = sha256File(tmp);
155
+ if (roundTrip !== manifest.checksum) {
156
+ try {
157
+ unlinkSync(tmp);
158
+ }
159
+ catch {
160
+ // ignore
161
+ }
162
+ return { ok: false, reason: "local copy verification failed" };
163
+ }
164
+ renameSync(tmp, current);
165
+ const metaPath = path.join(store, "accepted-manifest.json");
166
+ atomicWriteFileSync(metaPath, JSON.stringify(manifest, null, 2) + "\n");
167
+ return { ok: true, localPath: current };
168
+ }
169
+ export function getClientAcceptedManifest(masterPath) {
170
+ const metaPath = path.join(clientSnapshotStore(masterPath), "accepted-manifest.json");
171
+ if (!existsSync(metaPath))
172
+ return null;
173
+ try {
174
+ return JSON.parse(readFileSync(metaPath, "utf-8"));
175
+ }
176
+ catch {
177
+ return null;
178
+ }
179
+ }
180
+ export function formatSnapshotAge(publishedAt) {
181
+ const ms = Date.now() - new Date(publishedAt).getTime();
182
+ const minutes = Math.max(0, Math.floor(ms / 60_000));
183
+ if (minutes < 1)
184
+ return "just now";
185
+ if (minutes === 1)
186
+ return "1 minute ago";
187
+ return `${minutes} minutes ago`;
188
+ }