gnosys 5.2.24 → 5.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  <p align="center">
6
6
  <a href="https://www.npmjs.com/package/gnosys"><img src="https://img.shields.io/npm/v/gnosys.svg" alt="npm version"></a>
7
7
  <a href="https://github.com/proticom/gnosys/actions"><img src="https://github.com/proticom/gnosys/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
8
- <img src="https://img.shields.io/badge/tests-718%20passing-brightgreen" alt="tests">
8
+ <img src="https://img.shields.io/badge/tests-738%20passing-brightgreen" alt="tests">
9
9
  <img src="https://img.shields.io/badge/coverage-lib%2040%25%20|%20sandbox%2045%25-yellow" alt="coverage">
10
10
  <a href="https://gnosys.ai"><img src="https://img.shields.io/badge/docs-gnosys.ai-C04C4C" alt="docs"></a>
11
11
  <a href="https://gnosys.ai/guide.html"><img src="https://img.shields.io/badge/user%20guide-gnosys.ai%2Fguide-555560" alt="user guide"></a>
@@ -48,6 +48,7 @@ Gnosys takes a different approach: the central brain is a single SQLite database
48
48
  - **Reflection API** — `gnosys.reflect(outcome)` updates confidence and consolidates memories based on real-world outcomes.
49
49
  - **Bulk import** — CSV, JSON, JSONL. Import entire datasets in seconds.
50
50
  - **Obsidian-native** — `gnosys export` generates a full vault with YAML frontmatter, `[[wikilinks]]`, summaries, and graph data.
51
+ - **Multi-machine sync (v5.3.0)** — share your `gnosys.db` across machines via NAS or shared drive. Local cache for speed, remote source of truth for consistency. Built-in conflict detection with skip-and-flag resolution. Run `gnosys remote configure` to set up.
51
52
  - **MCP-compatible** — also runs as a full MCP server that drops into Cursor, Claude Desktop, Claude Code, Cowork, Codex, or any MCP client.
52
53
  - **Zero infrastructure** — no external databases, no Docker (unless you want it), no cloud services. Just `npm install`.
53
54
 
@@ -334,6 +335,11 @@ command = ["gnosys", "serve"]
334
335
  | `gnosys_detect_ambiguity` | Check if a query matches multiple projects |
335
336
  | `gnosys_briefing` | Generate project briefing (categories, activity, tags, summary) |
336
337
  | `gnosys_working_set` | Get recently modified memories for the current project |
338
+ | **Multi-Machine Sync** | |
339
+ | `gnosys_remote_status` | Check sync state (pending changes, conflicts, reachability) |
340
+ | `gnosys_remote_push` | Push local changes to remote |
341
+ | `gnosys_remote_pull` | Pull remote changes to local |
342
+ | `gnosys_remote_resolve` | Resolve a conflict by choosing local or remote |
337
343
 
338
344
  ---
339
345
 
@@ -343,6 +349,35 @@ command = ["gnosys", "serve"]
343
349
 
344
350
  All memories live in a single `~/.gnosys/gnosys.db` with `project_id` and `scope` columns. SQLite is the sole source of truth — no dual-write, no markdown files on disk. Sub-10ms reads, WAL mode for concurrent access. Use `gnosys export` to generate an Obsidian vault on demand. See the [User Guide](https://gnosys.ai/guide.html) for the full schema and memory format.
345
351
 
352
+ ### Multi-Machine Sync
353
+
354
+ Gnosys v5.3.0 supports running across multiple machines with a shared database on a NAS or network share.
355
+
356
+ **How it works:**
357
+ - Local DB at `~/.gnosys/gnosys.db` is your fast working cache
358
+ - Remote DB on shared storage (e.g. `/Volumes/synology/gnosys/`) is the canonical source of truth
359
+ - Reads always hit local for speed
360
+ - Writes go to local first, then sync to remote
361
+ - Per-memory `modified` timestamps detect conflicts
362
+ - Skip-and-flag is the safe default; `--newer-wins` for unattended sync
363
+
364
+ **Setup:**
365
+ ```bash
366
+ gnosys remote configure
367
+ # interactive: validates path, tests SQLite locking, checks latency
368
+ ```
369
+
370
+ **Daily commands:**
371
+ ```bash
372
+ gnosys remote status # pending changes, conflicts, last sync
373
+ gnosys remote sync # two-way sync (push then pull)
374
+ gnosys remote push # local → remote only
375
+ gnosys remote pull # remote → local only
376
+ gnosys remote resolve <id> --keep <local|remote>
377
+ ```
378
+
379
+ **AI-mediated conflict resolution:** Agents using gnosys via MCP can detect sync state and prompt the user when conflicts arise, rather than silently picking a winner. The agent presents both versions and asks which to keep.
380
+
346
381
  ### LLM Providers
347
382
 
348
383
  Eight providers behind a single interface — switch between cloud and local with one command:
@@ -471,6 +506,8 @@ All commands support `--json` for programmatic output. See the [User Guide](http
471
506
 
472
507
  **Web knowledge base:** `web init`, `web ingest`, `web build-index`, `web build`, `web add`, `web remove`, `web status`
473
508
 
509
+ **Multi-machine sync:** `remote configure`, `remote status`, `remote sync`, `remote push`, `remote pull`, `remote resolve`
510
+
474
511
  **Server:** `serve`, `serve --with-maintenance`
475
512
 
476
513
  ---
@@ -480,13 +517,13 @@ All commands support `--json` for programmatic output. See the [User Guide](http
480
517
  ```bash
481
518
  npm install # Install dependencies
482
519
  npm run build # Compile TypeScript
483
- npm test # Run test suite (718 tests)
520
+ npm test # Run test suite (738 tests)
484
521
  npm run test:watch # Run tests in watch mode
485
522
  npm run test:coverage # Run tests with v8 coverage report (HTML in coverage/)
486
523
  npm run dev # Run MCP server in dev mode (tsx)
487
524
  ```
488
525
 
489
- 718 tests across 35+ files. CI runs on Node 20 + 22 with multi-project scenario testing, network-share simulation, and TypeScript strict checking. Publishing uses OIDC trusted publishing via GitHub Actions — no npm tokens needed.
526
+ 738 tests across 35+ files. CI runs on Node 20 + 22 with multi-project scenario testing, network-share simulation, and TypeScript strict checking. Publishing uses OIDC trusted publishing via GitHub Actions — no npm tokens needed.
490
527
 
491
528
  ---
492
529
 
@@ -558,7 +595,7 @@ Real numbers from a 120-memory test vault:
558
595
  | Graph reindex (120 memories) | <1s |
559
596
  | Storage per memory | ~1 KB (SQLite row) |
560
597
  | Embedding storage (120 memories) | ~0.3 MB |
561
- | Test suite | 718 tests, 0 errors |
598
+ | Test suite | 738 tests, 0 errors |
562
599
 
563
600
  All benchmarks on Apple M-series hardware, Node.js 20+. Structured imports bypass LLM entirely.
564
601
 
@@ -578,7 +615,8 @@ Gnosys is open source (MIT) and actively developed. Here's how to get involved:
578
615
  - PRs welcome — especially for new import connectors, LLM providers, and Obsidian plugins
579
616
 
580
617
  **What's next:**
581
- - Real-time multi-machine sync (automatic conflict resolution beyond current iCloud/Dropbox support)
618
+ - Improved network share latency (write-ahead batching for high-latency NAS)
619
+ - Automated background sync via LaunchAgent (macOS) / systemd (Linux)
582
620
  - Temporal memory versioning (valid_from / valid_until)
583
621
  - Cross-session "deep dream" overnight consolidation
584
622
  - Graph visualization in the dashboard
package/dist/cli.js CHANGED
@@ -2469,6 +2469,260 @@ program
2469
2469
  }
2470
2470
  });
2471
2471
  // NOTE: gnosys migrate is defined below (near the end) with --to-central support
2472
+ // ─── gnosys remote (multi-machine sync) ────────────────────────────────
2473
+ const remoteCmd = program
2474
+ .command("remote")
2475
+ .description("Multi-machine sync — share gnosys.db across machines via NAS or shared drive");
2476
+ remoteCmd
2477
+ .command("status")
2478
+ .description("Show remote sync status: pending changes, conflicts, last sync")
2479
+ .option("--json", "Output as JSON")
2480
+ .action(async (opts) => {
2481
+ let centralDb = null;
2482
+ try {
2483
+ centralDb = GnosysDB.openCentral();
2484
+ if (!centralDb.isAvailable()) {
2485
+ console.error("Central DB not available.");
2486
+ process.exit(1);
2487
+ }
2488
+ const remotePath = centralDb.getMeta("remote_path");
2489
+ if (!remotePath) {
2490
+ if (opts.json) {
2491
+ console.log(JSON.stringify({ configured: false, message: "Remote not configured. Run 'gnosys remote configure'." }, null, 2));
2492
+ }
2493
+ else {
2494
+ console.log("Remote sync: not configured.");
2495
+ console.log("Run 'gnosys remote configure' to set up multi-machine sync.");
2496
+ }
2497
+ return;
2498
+ }
2499
+ const { RemoteSync, formatStatus } = await import("./lib/remote.js");
2500
+ const sync = new RemoteSync(centralDb, remotePath);
2501
+ const status = await sync.getStatus();
2502
+ sync.closeRemote();
2503
+ if (opts.json) {
2504
+ console.log(JSON.stringify(status, null, 2));
2505
+ }
2506
+ else {
2507
+ console.log(formatStatus(status));
2508
+ if (status.conflicts.length > 0) {
2509
+ console.log("\nConflicts:");
2510
+ for (const c of status.conflicts) {
2511
+ console.log(` ${c.memoryId}: ${c.title}`);
2512
+ console.log(` local: ${c.localModified}`);
2513
+ console.log(` remote: ${c.remoteModified}`);
2514
+ }
2515
+ console.log("\nResolve with: gnosys remote resolve <memory-id> --keep <local|remote>");
2516
+ }
2517
+ }
2518
+ }
2519
+ catch (err) {
2520
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2521
+ process.exit(1);
2522
+ }
2523
+ finally {
2524
+ centralDb?.close();
2525
+ }
2526
+ });
2527
+ remoteCmd
2528
+ .command("push")
2529
+ .description("Push local changes to remote")
2530
+ .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
2531
+ .action(async (opts) => {
2532
+ let centralDb = null;
2533
+ try {
2534
+ centralDb = GnosysDB.openCentral();
2535
+ if (!centralDb.isAvailable()) {
2536
+ console.error("Central DB not available.");
2537
+ process.exit(1);
2538
+ }
2539
+ const remotePath = centralDb.getMeta("remote_path");
2540
+ if (!remotePath) {
2541
+ console.error("Remote not configured.");
2542
+ process.exit(1);
2543
+ }
2544
+ const { RemoteSync } = await import("./lib/remote.js");
2545
+ const sync = new RemoteSync(centralDb, remotePath);
2546
+ const result = await sync.push({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
2547
+ sync.closeRemote();
2548
+ console.log(`Pushed: ${result.pushed} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}`);
2549
+ if (result.errors.length > 0) {
2550
+ console.log("\nErrors:");
2551
+ for (const e of result.errors)
2552
+ console.log(` ${e}`);
2553
+ }
2554
+ if (result.conflicts.length > 0) {
2555
+ console.log("\nConflicts flagged (run 'gnosys remote status' for details):");
2556
+ for (const c of result.conflicts)
2557
+ console.log(` ${c.memoryId} — ${c.title}`);
2558
+ }
2559
+ }
2560
+ catch (err) {
2561
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2562
+ process.exit(1);
2563
+ }
2564
+ finally {
2565
+ centralDb?.close();
2566
+ }
2567
+ });
2568
+ remoteCmd
2569
+ .command("pull")
2570
+ .description("Pull remote changes to local")
2571
+ .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
2572
+ .action(async (opts) => {
2573
+ let centralDb = null;
2574
+ try {
2575
+ centralDb = GnosysDB.openCentral();
2576
+ if (!centralDb.isAvailable()) {
2577
+ console.error("Central DB not available.");
2578
+ process.exit(1);
2579
+ }
2580
+ const remotePath = centralDb.getMeta("remote_path");
2581
+ if (!remotePath) {
2582
+ console.error("Remote not configured.");
2583
+ process.exit(1);
2584
+ }
2585
+ const { RemoteSync } = await import("./lib/remote.js");
2586
+ const sync = new RemoteSync(centralDb, remotePath);
2587
+ const result = await sync.pull({ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag" });
2588
+ sync.closeRemote();
2589
+ console.log(`Pulled: ${result.pulled} | Skipped: ${result.skipped} | Conflicts: ${result.conflicts.length}`);
2590
+ if (result.errors.length > 0) {
2591
+ console.log("\nErrors:");
2592
+ for (const e of result.errors)
2593
+ console.log(` ${e}`);
2594
+ }
2595
+ }
2596
+ catch (err) {
2597
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2598
+ process.exit(1);
2599
+ }
2600
+ finally {
2601
+ centralDb?.close();
2602
+ }
2603
+ });
2604
+ remoteCmd
2605
+ .command("sync")
2606
+ .description("Two-way sync: push local changes then pull remote changes")
2607
+ .option("--auto", "Run silently for cron/LaunchAgent (skip-and-flag for conflicts)")
2608
+ .option("--newer-wins", "Auto-resolve conflicts by taking the newer version")
2609
+ .action(async (opts) => {
2610
+ let centralDb = null;
2611
+ try {
2612
+ centralDb = GnosysDB.openCentral();
2613
+ if (!centralDb.isAvailable()) {
2614
+ if (!opts.auto)
2615
+ console.error("Central DB not available.");
2616
+ process.exit(1);
2617
+ }
2618
+ const remotePath = centralDb.getMeta("remote_path");
2619
+ if (!remotePath) {
2620
+ if (!opts.auto)
2621
+ console.error("Remote not configured.");
2622
+ process.exit(opts.auto ? 0 : 1);
2623
+ }
2624
+ const { RemoteSync } = await import("./lib/remote.js");
2625
+ const sync = new RemoteSync(centralDb, remotePath);
2626
+ const result = await sync.sync({
2627
+ auto: opts.auto,
2628
+ strategy: opts.newerWins ? "newer-wins" : "skip-and-flag",
2629
+ });
2630
+ sync.closeRemote();
2631
+ if (!opts.auto || result.conflicts.length > 0 || result.errors.length > 0) {
2632
+ console.log(`Pushed: ${result.pushed} | Pulled: ${result.pulled} | Conflicts: ${result.conflicts.length}`);
2633
+ if (result.errors.length > 0) {
2634
+ console.log("\nErrors:");
2635
+ for (const e of result.errors)
2636
+ console.log(` ${e}`);
2637
+ }
2638
+ if (result.conflicts.length > 0) {
2639
+ console.log("\nConflicts need resolution (run 'gnosys remote status' for details).");
2640
+ }
2641
+ }
2642
+ }
2643
+ catch (err) {
2644
+ if (!opts.auto)
2645
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2646
+ process.exit(1);
2647
+ }
2648
+ finally {
2649
+ centralDb?.close();
2650
+ }
2651
+ });
2652
+ remoteCmd
2653
+ .command("resolve <memoryId>")
2654
+ .description("Resolve a sync conflict by choosing local, remote, or merged content")
2655
+ .option("--keep <choice>", "Choice: local | remote", "local")
2656
+ .action(async (memoryId, opts) => {
2657
+ let centralDb = null;
2658
+ try {
2659
+ centralDb = GnosysDB.openCentral();
2660
+ if (!centralDb.isAvailable()) {
2661
+ console.error("Central DB not available.");
2662
+ process.exit(1);
2663
+ }
2664
+ const remotePath = centralDb.getMeta("remote_path");
2665
+ if (!remotePath) {
2666
+ console.error("Remote not configured.");
2667
+ process.exit(1);
2668
+ }
2669
+ if (opts.keep !== "local" && opts.keep !== "remote") {
2670
+ console.error(`--keep must be 'local' or 'remote' (got: ${opts.keep})`);
2671
+ process.exit(1);
2672
+ }
2673
+ const { RemoteSync } = await import("./lib/remote.js");
2674
+ const sync = new RemoteSync(centralDb, remotePath);
2675
+ const result = await sync.resolve(memoryId, opts.keep);
2676
+ sync.closeRemote();
2677
+ if (result.ok) {
2678
+ console.log(`Resolved ${memoryId}: kept ${opts.keep} version.`);
2679
+ }
2680
+ else {
2681
+ console.error(`Failed to resolve: ${result.error}`);
2682
+ process.exit(1);
2683
+ }
2684
+ }
2685
+ catch (err) {
2686
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2687
+ process.exit(1);
2688
+ }
2689
+ finally {
2690
+ centralDb?.close();
2691
+ }
2692
+ });
2693
+ remoteCmd
2694
+ .command("configure")
2695
+ .description("Configure or change the remote sync location (interactive wizard)")
2696
+ .option("--path <path>", "Set remote path non-interactively (skips wizard)")
2697
+ .option("--migrate", "Copy current local DB to remote on first setup (with --path)")
2698
+ .action(async (opts) => {
2699
+ let centralDb = null;
2700
+ try {
2701
+ centralDb = GnosysDB.openCentral();
2702
+ if (!centralDb.isAvailable()) {
2703
+ console.error("Central DB not available.");
2704
+ process.exit(1);
2705
+ }
2706
+ const { runConfigureWizard, configureFromPath } = await import("./lib/remoteWizard.js");
2707
+ if (opts.path) {
2708
+ // Non-interactive mode
2709
+ const ok = await configureFromPath(centralDb, opts.path, { migrate: opts.migrate });
2710
+ process.exit(ok ? 0 : 1);
2711
+ }
2712
+ else {
2713
+ // Interactive wizard
2714
+ const ok = await runConfigureWizard(centralDb);
2715
+ process.exit(ok ? 0 : 1);
2716
+ }
2717
+ }
2718
+ catch (err) {
2719
+ console.error(`Error: ${err instanceof Error ? err.message : err}`);
2720
+ process.exit(1);
2721
+ }
2722
+ finally {
2723
+ centralDb?.close();
2724
+ }
2725
+ });
2472
2726
  // ─── gnosys upgrade ─────────────────────────────────────────────────────
2473
2727
  program
2474
2728
  .command("upgrade")