gnosys 5.12.0 → 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 (72) hide show
  1. package/dist/cli.js +48 -7
  2. package/dist/index.js +179 -10
  3. package/dist/lib/addCommand.js +0 -1
  4. package/dist/lib/archive.js +0 -2
  5. package/dist/lib/askCommand.js +1 -1
  6. package/dist/lib/attachCommand.d.ts +17 -0
  7. package/dist/lib/attachCommand.js +66 -0
  8. package/dist/lib/attachments.d.ts +43 -2
  9. package/dist/lib/attachments.js +81 -2
  10. package/dist/lib/chat/choose.js +2 -2
  11. package/dist/lib/clientReadOverlay.js +3 -0
  12. package/dist/lib/config.d.ts +1 -48
  13. package/dist/lib/configCommand.js +2 -2
  14. package/dist/lib/db.d.ts +16 -1
  15. package/dist/lib/db.js +216 -119
  16. package/dist/lib/dbWrite.d.ts +1 -1
  17. package/dist/lib/dearchiveCommand.js +1 -1
  18. package/dist/lib/docxExtract.js +1 -1
  19. package/dist/lib/dream.d.ts +8 -0
  20. package/dist/lib/dream.js +35 -1
  21. package/dist/lib/dreamLogCommand.js +1 -1
  22. package/dist/lib/dreamRunLog.d.ts +1 -1
  23. package/dist/lib/dreamRunLog.js +26 -4
  24. package/dist/lib/embeddings.js +0 -3
  25. package/dist/lib/exportProject.d.ts +3 -2
  26. package/dist/lib/exportProject.js +2 -1
  27. package/dist/lib/federated.js +1 -1
  28. package/dist/lib/hybridSearchCommand.js +1 -1
  29. package/dist/lib/importProject.js +2 -1
  30. package/dist/lib/llm.js +1 -1
  31. package/dist/lib/lock.d.ts +1 -1
  32. package/dist/lib/lock.js +5 -3
  33. package/dist/lib/migrate.js +0 -1
  34. package/dist/lib/multimodalIngest.js +1 -1
  35. package/dist/lib/platform.d.ts +0 -6
  36. package/dist/lib/platform.js +0 -28
  37. package/dist/lib/readCommand.js +11 -10
  38. package/dist/lib/remoteWizard.d.ts +1 -1
  39. package/dist/lib/remoteWizard.js +4 -4
  40. package/dist/lib/rulesGen.d.ts +8 -0
  41. package/dist/lib/rulesGen.js +16 -0
  42. package/dist/lib/search.d.ts +0 -2
  43. package/dist/lib/search.js +0 -7
  44. package/dist/lib/semanticSearchCommand.js +1 -1
  45. package/dist/lib/setup/sections/providers.js +56 -4
  46. package/dist/lib/setup/sections/routing.js +42 -5
  47. package/dist/lib/setup/sections/taskRoutingEditor.d.ts +1 -5
  48. package/dist/lib/setup/sections/taskRoutingEditor.js +0 -10
  49. package/dist/lib/setup/ui/header.js +0 -1
  50. package/dist/lib/setup/ui/status.d.ts +0 -1
  51. package/dist/lib/setup/ui/status.js +0 -2
  52. package/dist/lib/setup.d.ts +0 -15
  53. package/dist/lib/setup.js +13 -158
  54. package/dist/lib/staleCommand.js +2 -2
  55. package/dist/lib/syncClient.d.ts +0 -6
  56. package/dist/lib/syncClient.js +36 -14
  57. package/dist/lib/syncDoctorCommand.js +2 -2
  58. package/dist/lib/syncIngest.d.ts +11 -0
  59. package/dist/lib/syncIngest.js +24 -1
  60. package/dist/lib/syncIngestStartup.js +2 -2
  61. package/dist/lib/syncSnapshot.d.ts +2 -0
  62. package/dist/lib/syncSnapshot.js +4 -0
  63. package/dist/lib/syncStaging.d.ts +0 -2
  64. package/dist/lib/syncStaging.js +0 -2
  65. package/dist/lib/updateCommand.js +1 -1
  66. package/dist/lib/webBuildCommand.js +1 -1
  67. package/dist/lib/webIndex.js +0 -1
  68. package/dist/lib/webIngestCommand.js +1 -1
  69. package/dist/sandbox/client.js +1 -1
  70. package/dist/sandbox/manager.js +1 -14
  71. package/dist/sandbox/server.js +3 -5
  72. package/package.json +5 -2
@@ -78,14 +78,49 @@ export async function runRoutingSetup(opts) {
78
78
  console.log("");
79
79
  const choice = await askChoice(opts.rl, "What would you like to do?", [
80
80
  "Keep current routing (no changes)",
81
- "Edit tasks — pick by number, then provider + model for each",
82
- "Reset all task overrides to use default",
81
+ "Edit tasks — set the same provider + model for all tasks (simple global default)",
82
+ "Edit tasks pick different providers/models for specific tasks (advanced)",
83
+ "Reset all task overrides to the current default (the one shown in the main setup summary)",
83
84
  ], 0);
84
85
  if (choice === 0) {
85
86
  console.log(`${DIM}No changes.${RESET}`);
86
87
  return false;
87
88
  }
88
- if (choice === 2) {
89
+ if (choice === 1) {
90
+ // Edit tasks — set the same provider + model for all tasks (simple global default)
91
+ console.log("");
92
+ printStatus("progress", "setting a single default for all tasks…");
93
+ try {
94
+ const { fetchDynamicModels } = await import("../../setup.js");
95
+ const { pickModel } = await import("../../setup.js");
96
+ const dynamicModels = await fetchDynamicModels();
97
+ const currentModel = getProviderModel(cfg, provider);
98
+ const chosenModel = await pickModel(opts.rl, provider, dynamicModels, `Default model for ${provider} (used for all tasks + dream)`, currentModel);
99
+ if (chosenModel && chosenModel !== currentModel) {
100
+ const after = await loadConfig(storePath);
101
+ await updateConfig(storePath, {
102
+ llm: {
103
+ ...after.llm,
104
+ [provider]: {
105
+ ...(after.llm[provider] || {}),
106
+ model: chosenModel,
107
+ },
108
+ },
109
+ // Clear per-task overrides so everything truly uses the single default
110
+ taskModels: {},
111
+ });
112
+ printStatus("ok", `default set for everything · ${provider} / ${chosenModel}`);
113
+ }
114
+ else {
115
+ printStatus("ok", "no change to the global default");
116
+ }
117
+ }
118
+ catch (err) {
119
+ printStatus("warn", "could not update default model", String(err));
120
+ }
121
+ return true;
122
+ }
123
+ if (choice === 3) {
89
124
  const { Diff } = await import("../ui/diff.js");
90
125
  const overridesBeingCleared = Object.entries(cfg.taskModels ?? {})
91
126
  .filter(([, v]) => v.provider !== provider || v.model !== model)
@@ -102,16 +137,18 @@ export async function runRoutingSetup(opts) {
102
137
  else {
103
138
  console.log(`${DIM}No overrides to clear — already using default everywhere.${RESET}`);
104
139
  }
105
- const confirmReset = await askYesNo(opts.rl, "Reset all task overrides?", true);
140
+ console.log(`${DIM}The current default is ${provider} / ${model} (set via the main setup "Default provider" or the simple global option above).${RESET}`);
141
+ const confirmReset = await askYesNo(opts.rl, "Reset all task overrides to the current default?", true);
106
142
  if (!confirmReset) {
107
143
  console.log(`${DIM}Cancelled.${RESET}`);
108
144
  return false;
109
145
  }
110
146
  await updateConfig(storePath, { taskModels: {} });
111
- printStatus("ok", "routing reset", "all tasks use default provider/model");
147
+ printStatus("ok", "routing reset", "all tasks now use the global default provider/model");
112
148
  console.log(Footer("press enter to return"));
113
149
  return true;
114
150
  }
151
+ // choice === 2 → advanced per-task editor (the powerful comma-list path)
115
152
  const patch = await runCommaListRoutingEditor(opts.rl, storePath, cfg);
116
153
  if (!patch) {
117
154
  return false;
@@ -2,7 +2,7 @@
2
2
  * Comma-list task routing editor (provider + model per selected task).
3
3
  */
4
4
  import type { Interface as ReadlineInterface } from "readline/promises";
5
- import { type GnosysConfig } from "../../config.js";
5
+ import type { GnosysConfig } from "../../config.js";
6
6
  /**
7
7
  * Let the user pick tasks by number, then provider + model for each.
8
8
  * Returns a patch for taskModels + dream, or null if cancelled.
@@ -11,7 +11,3 @@ export declare function runCommaListRoutingEditor(rl: ReadlineInterface, storePa
11
11
  taskModels: NonNullable<GnosysConfig["taskModels"]>;
12
12
  dream: GnosysConfig["dream"];
13
13
  } | null>;
14
- export declare function applyRoutingPatch(projectDir: string, patch: {
15
- taskModels: NonNullable<GnosysConfig["taskModels"]>;
16
- dream: GnosysConfig["dream"];
17
- }): Promise<GnosysConfig>;
@@ -1,9 +1,7 @@
1
1
  /**
2
2
  * Comma-list task routing editor (provider + model per selected task).
3
3
  */
4
- import { loadConfig, updateConfig, } from "../../config.js";
5
4
  import { ASSIGNABLE_TASK_LIST, fetchDynamicModels, getAssignableRouting, modelForTaskAssignment, parseCommaSeparatedTaskSelection, pickModel, pickProvider, TASK_DESCRIPTIONS, } from "../../setup.js";
6
- import { resolveActiveStorePath } from "../storePath.js";
7
5
  import { safeQuestion } from "../ui/safePrompt.js";
8
6
  import { renderProviderMark } from "../providerGlyphs.js";
9
7
  import { c, color, glyph } from "../ui/tokens.js";
@@ -139,11 +137,3 @@ export async function runCommaListRoutingEditor(rl, storePath, cfg) {
139
137
  }
140
138
  return { taskModels, dream };
141
139
  }
142
- export async function applyRoutingPatch(projectDir, patch) {
143
- const storePath = resolveActiveStorePath(projectDir);
144
- await updateConfig(storePath, {
145
- taskModels: patch.taskModels,
146
- dream: patch.dream,
147
- });
148
- return loadConfig(storePath);
149
- }
@@ -39,7 +39,6 @@ export function Header(crumbs, opts = {}) {
39
39
  }
40
40
  /** Strip ANSI escapes so we can measure printable width. Exported for callers/tests that need printable measurement. */
41
41
  export function stripAnsi(s) {
42
- // eslint-disable-next-line no-control-regex
43
42
  return s.replace(/\x1b\[[0-9;]*m/g, "");
44
43
  }
45
44
  /** Convenience: print header + trailing blank line to stdout. */
@@ -18,4 +18,3 @@ export type StatusKind = "ok" | "warn" | "fail" | "progress";
18
18
  export declare function Status(kind: StatusKind, text: string, meta?: string): string;
19
19
  /** Convenience: print a single status line. */
20
20
  export declare function printStatus(kind: StatusKind, text: string, meta?: string): void;
21
- export { MASTER_UNREACHABLE_MESSAGE, formatMemoriesWaitingToSync, formatFailedToSyncCount, formatOfflinePushStarting, renderClientSyncStatusLines, type ClientSyncStatusInput, } from "../remoteRender.js";
@@ -46,5 +46,3 @@ export function Status(kind, text, meta) {
46
46
  export function printStatus(kind, text, meta) {
47
47
  process.stdout.write(`${Status(kind, text, meta)}\n`);
48
48
  }
49
- // ─── v13 multi-machine sync (re-export render helpers for CLI/MCP callers) ─
50
- export { MASTER_UNREACHABLE_MESSAGE, formatMemoriesWaitingToSync, formatFailedToSyncCount, formatOfflinePushStarting, renderClientSyncStatusLines, } from "../remoteRender.js";
@@ -138,20 +138,6 @@ export declare function runSetup(opts: {
138
138
  directory?: string;
139
139
  nonInteractive?: boolean;
140
140
  }): Promise<SetupResult>;
141
- export interface ProviderOnlySetupOpts {
142
- directory?: string;
143
- rl?: ReadlineInterface;
144
- }
145
- /**
146
- * Update ONLY `llm.defaultProvider` in gnosys.json. Used by the summary
147
- * panel row 1 ("provider") so it stops dragging the user into the full
148
- * model picker — that's row 2's job.
149
- *
150
- * v5.9.4 Bug 4 — before this split, both summary rows routed through
151
- * `runModelsSetup`, leaving no way to swap provider without also choosing
152
- * a new model. Now row 1 picks a provider, row 2 picks a model.
153
- */
154
- export declare function runProviderOnlySetup(opts?: ProviderOnlySetupOpts): Promise<void>;
155
141
  /** Where validated keys are persisted (§3.10). */
156
142
  export type KeyPersistDestination = "secure" | "dotenv" | "none";
157
143
  /**
@@ -252,4 +238,3 @@ export declare function getApiKeyForProvider(provider: string, opts?: {
252
238
  task?: LlmTaskName;
253
239
  directory?: string;
254
240
  }): Promise<string>;
255
- export { getApiKeyForProviderFromConfig } from "./apiKeyVault.js";
package/dist/lib/setup.js CHANGED
@@ -271,16 +271,6 @@ export async function fetchDynamicModels() {
271
271
  return {};
272
272
  }
273
273
  }
274
- /**
275
- * Get model tiers for a provider — tries dynamic first, falls back to hardcoded.
276
- */
277
- async function getModelTiers(provider) {
278
- const dynamic = await fetchDynamicModels();
279
- if (dynamic[provider] && dynamic[provider].length > 0) {
280
- return dynamic[provider];
281
- }
282
- return PROVIDER_TIERS[provider] ?? [];
283
- }
284
274
  // ─── Provider display names and env var mapping ─────────────────────────────
285
275
  const PROVIDER_DISPLAY = {
286
276
  anthropic: "Anthropic (Claude)",
@@ -675,7 +665,7 @@ export async function detectIDEs(projectDir) {
675
665
  */
676
666
  export function upsertGrokMcpBlock(existing, name, entry) {
677
667
  // Drop mistaken v5.9.4 `[mcp.<name>]` sections so we don't leave dead config.
678
- let content = removeTomlSection(existing, `[mcp.${name}]`);
668
+ const content = removeTomlSection(existing, `[mcp.${name}]`);
679
669
  const sectionHeader = `[mcp_servers.${name}]`;
680
670
  const lines = content.split("\n");
681
671
  const headerIdx = lines.findIndex((line) => line.trim() === sectionHeader);
@@ -779,7 +769,7 @@ export async function setupIDE(ide, projectDir) {
779
769
  // 1. Strip legacy hand-written sections in ~/.codex/config.toml.
780
770
  const userCodexConfig = path.join(os.homedir(), ".codex", "config.toml");
781
771
  try {
782
- let existing = await fs.readFile(userCodexConfig, "utf-8");
772
+ const existing = await fs.readFile(userCodexConfig, "utf-8");
783
773
  const cleaned = removeTomlSection(removeTomlSection(existing, "[mcp.gnosys]"), "[gnosys]");
784
774
  if (cleaned !== existing) {
785
775
  await fs.writeFile(userCodexConfig, cleaned, "utf-8");
@@ -796,7 +786,7 @@ export async function setupIDE(ide, projectDir) {
796
786
  let alreadyCorrect = false;
797
787
  try {
798
788
  const existing = runCli("codex", ["mcp", "get", "gnosys"], { allowFailure: true });
799
- if (existing && existing.includes(gnosysCmd) && !existing.includes(" serve")) {
789
+ if (existing?.includes(gnosysCmd) && !existing.includes(" serve")) {
800
790
  alreadyCorrect = true;
801
791
  }
802
792
  else if (existing) {
@@ -1439,8 +1429,7 @@ export async function runSetup(opts) {
1439
1429
  const gnomeIdx = hasSecret ? idx++ : -1;
1440
1430
  const envIdx = idx++;
1441
1431
  const dotenvIdx = idx++;
1442
- const skipIdx = idx++;
1443
- // skipIdx is unused as a variable but documents the last index
1432
+ idx++; // skip entry consumes the last index (value itself unused)
1444
1433
  if (keyChoice === keychainIdx) {
1445
1434
  // macOS Keychain — reuse existing key if available, otherwise ask
1446
1435
  console.log();
@@ -1974,60 +1963,6 @@ export async function runSetup(opts) {
1974
1963
  throw err;
1975
1964
  }
1976
1965
  }
1977
- /**
1978
- * Update ONLY `llm.defaultProvider` in gnosys.json. Used by the summary
1979
- * panel row 1 ("provider") so it stops dragging the user into the full
1980
- * model picker — that's row 2's job.
1981
- *
1982
- * v5.9.4 Bug 4 — before this split, both summary rows routed through
1983
- * `runModelsSetup`, leaving no way to swap provider without also choosing
1984
- * a new model. Now row 1 picks a provider, row 2 picks a model.
1985
- */
1986
- export async function runProviderOnlySetup(opts = {}) {
1987
- const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
1988
- const ownsRl = !opts.rl;
1989
- const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
1990
- try {
1991
- const { Header } = await import("./setup/ui/header.js");
1992
- const { Title } = await import("./setup/ui/title.js");
1993
- const { Spinner } = await import("./setup/ui/spinner.js");
1994
- const { printStatus } = await import("./setup/ui/status.js");
1995
- console.log();
1996
- console.log(Header(["gnosys", "setup", "provider"]));
1997
- console.log();
1998
- console.log(Title("Default provider", "pick the LLM provider — model stays as configured"));
1999
- console.log();
2000
- const existingConfig = await loadExistingConfig(projectDir);
2001
- const currentProvider = existingConfig?.llm.defaultProvider;
2002
- const pricingSpin = Spinner("fetching latest pricing from openrouter…");
2003
- const fetchStart = Date.now();
2004
- const dynamicModels = await fetchDynamicModels();
2005
- const fetchMs = Date.now() - fetchStart;
2006
- if (Object.keys(dynamicModels).length > 0) {
2007
- pricingSpin.ok("pricing loaded", `${fetchMs} ms`);
2008
- }
2009
- else {
2010
- pricingSpin.fail("pricing fetch failed", "using bundled tiers");
2011
- }
2012
- console.log();
2013
- const provider = await pickProvider(rl, dynamicModels, "Choose your LLM provider", currentProvider);
2014
- if (!provider || provider === currentProvider) {
2015
- printStatus("warn", "no change · provider unchanged");
2016
- return;
2017
- }
2018
- const storePath = ensureActiveStorePath(projectDir);
2019
- const existingLlm = existingConfig?.llm ?? {};
2020
- await updateConfig(storePath, {
2021
- llm: { ...existingLlm, defaultProvider: provider },
2022
- });
2023
- printStatus("ok", `default provider · ${provider}`, `${storePath}/gnosys.json`);
2024
- printStatus("progress", "model unchanged", "use row 2 to swap the model");
2025
- }
2026
- finally {
2027
- if (ownsRl)
2028
- rl.close();
2029
- }
2030
- }
2031
1966
  /**
2032
1967
  * Build API key requirements from the selected task set only (§3.7).
2033
1968
  * One global key per distinct cloud provider in the selection.
@@ -2142,7 +2077,6 @@ export async function promptKeyDestinationAndPersist(opts) {
2142
2077
  (await askChoice(opts.rl, "Where should I store this key?", options));
2143
2078
  const secureIdx = 0;
2144
2079
  const dotenvIdx = 1;
2145
- const noneIdx = 2;
2146
2080
  if (choice === secureIdx) {
2147
2081
  if (storeApiKeySecret(opts.service, opts.key, opts.provider)) {
2148
2082
  const store = process.platform === "darwin" ? "macOS Keychain" : "GNOME Keyring";
@@ -2571,84 +2505,6 @@ async function runModelsTaskRoutingSetup(ctx) {
2571
2505
  await updateConfig(storePath, updatePayload);
2572
2506
  printStatus("ok", `saved task routing · ${storePath}/gnosys.json`);
2573
2507
  }
2574
- /**
2575
- * Lightweight model-management command. Supports three operations:
2576
- * --list: print available models for the current provider
2577
- * --refresh: clear the OpenRouter cache and re-fetch
2578
- * --set X: update the default model in gnosys.json (no prompts)
2579
- */
2580
- async function runModelsCommand(opts = {}) {
2581
- const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
2582
- const existingConfig = await loadExistingConfig(projectDir);
2583
- const currentProvider = existingConfig?.llm.defaultProvider;
2584
- if (opts.refresh) {
2585
- const cacheFile = path.join(os.homedir(), ".config", "gnosys", "models-cache.json");
2586
- try {
2587
- await fs.unlink(cacheFile);
2588
- console.log(`${CHECK} Cache cleared.`);
2589
- }
2590
- catch {
2591
- console.log(`${DIM}No cache to clear.${RESET}`);
2592
- }
2593
- }
2594
- if (opts.list) {
2595
- if (!currentProvider) {
2596
- console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
2597
- return;
2598
- }
2599
- console.log();
2600
- console.log(`${BOLD}Available models for ${currentProvider}:${RESET}`);
2601
- console.log();
2602
- const dynamicModels = await fetchDynamicModels();
2603
- const tiers = dynamicModels[currentProvider] ?? PROVIDER_TIERS[currentProvider] ?? [];
2604
- if (tiers.length === 0) {
2605
- console.log(` ${DIM}No models in catalog. Try '--refresh' or use a custom model name.${RESET}`);
2606
- return;
2607
- }
2608
- for (const t of tiers) {
2609
- const rec = t.recommended ? ` ${CYAN}<- recommended${RESET}` : "";
2610
- const price = t.input === 0 && t.output === 0
2611
- ? "free"
2612
- : `$${t.input.toFixed(2)}–$${t.output.toFixed(2)}/M`;
2613
- console.log(` ${t.name.padEnd(24)} ${t.model.padEnd(40)} ${DIM}${price}${RESET}${rec}`);
2614
- }
2615
- return;
2616
- }
2617
- if (opts.set) {
2618
- if (!currentProvider) {
2619
- console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
2620
- return;
2621
- }
2622
- // v5.9.4 Bug 10 — unified store resolution.
2623
- const storePath = ensureActiveStorePath(projectDir);
2624
- const existingProviderConfig = existingConfig?.llm?.[currentProvider];
2625
- const providerConfigBase = (typeof existingProviderConfig === "object" && existingProviderConfig !== null)
2626
- ? existingProviderConfig
2627
- : {};
2628
- await updateConfig(storePath, {
2629
- llm: {
2630
- ...(existingConfig?.llm ?? {}),
2631
- defaultProvider: currentProvider,
2632
- [currentProvider]: { ...providerConfigBase, model: opts.set },
2633
- },
2634
- });
2635
- console.log(`${CHECK} Default model set to ${GREEN}${opts.set}${RESET} for ${currentProvider}.`);
2636
- return;
2637
- }
2638
- // No flags: show current config
2639
- if (!currentProvider) {
2640
- console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
2641
- return;
2642
- }
2643
- const currentModel = existingConfig
2644
- ? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
2645
- : "";
2646
- console.log();
2647
- console.log(`Provider: ${GREEN}${currentProvider}${RESET}`);
2648
- console.log(`Model: ${GREEN}${currentModel}${RESET}`);
2649
- console.log();
2650
- console.log(`${DIM}Use '--list' to see options, '--set <model>' to change, '--refresh' to update catalog.${RESET}`);
2651
- }
2652
2508
  /**
2653
2509
  * Walks the user through configuring dream mode. Handles:
2654
2510
  * - enable/disable
@@ -2857,23 +2713,23 @@ export async function runDreamSetup(opts = {}) {
2857
2713
  let discoverRelationships = dDiscover;
2858
2714
  if (editChoice === "e") {
2859
2715
  const idleAns = await askInput(rl, "idle minutes before triggering", { default: String(dIdle) });
2860
- idleMinutes = Math.max(1, parseInt(idleAns) || dIdle);
2716
+ idleMinutes = Math.max(1, parseInt(idleAns, 10) || dIdle);
2861
2717
  const runtimeAns = await askInput(rl, "max runtime minutes", { default: String(dRuntime) });
2862
- maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns) || dRuntime);
2718
+ maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns, 10) || dRuntime);
2863
2719
  const minMemAns = await askInput(rl, "minimum memories before activating", { default: String(dMinMem) });
2864
- minMemories = Math.max(1, parseInt(minMemAns) || dMinMem);
2720
+ minMemories = Math.max(1, parseInt(minMemAns, 10) || dMinMem);
2865
2721
  const scheduleStartAns = await askInput(rl, "night window start hour (0-23)", { default: String(dScheduleStart) });
2866
- scheduleStartHour = Math.min(23, Math.max(0, parseInt(scheduleStartAns) || dScheduleStart));
2722
+ scheduleStartHour = Math.min(23, Math.max(0, parseInt(scheduleStartAns, 10) || dScheduleStart));
2867
2723
  const scheduleEndAns = await askInput(rl, "night window end hour (0-23)", { default: String(dScheduleEnd) });
2868
- scheduleEndHour = Math.min(23, Math.max(0, parseInt(scheduleEndAns) || dScheduleEnd));
2724
+ scheduleEndHour = Math.min(23, Math.max(0, parseInt(scheduleEndAns, 10) || dScheduleEnd));
2869
2725
  const systemIdleAns = await askInput(rl, "real machine idle minutes required", { default: String(dSystemIdle) });
2870
- systemIdleMinutes = Math.max(1, parseInt(systemIdleAns) || dSystemIdle);
2726
+ systemIdleMinutes = Math.max(1, parseInt(systemIdleAns, 10) || dSystemIdle);
2871
2727
  const minNewAns = await askInput(rl, "minimum changed memories before dreaming", { default: String(dMinNewMemories) });
2872
- minNewMemoriesToDream = Math.max(0, parseInt(minNewAns) || dMinNewMemories);
2728
+ minNewMemoriesToDream = Math.max(0, parseInt(minNewAns, 10) || dMinNewMemories);
2873
2729
  const minHoursAns = await askInput(rl, "minimum hours between successful dreams", { default: String(dMinHours) });
2874
- minHoursBetweenRuns = Math.max(0, parseInt(minHoursAns) || dMinHours);
2730
+ minHoursBetweenRuns = Math.max(0, parseInt(minHoursAns, 10) || dMinHours);
2875
2731
  const maxCallsAns = await askInput(rl, "maximum LLM calls per dream run", { default: String(dMaxCalls) });
2876
- maxLLMCallsPerRun = Math.max(0, parseInt(maxCallsAns) || dMaxCalls);
2732
+ maxLLMCallsPerRun = Math.max(0, parseInt(maxCallsAns, 10) || dMaxCalls);
2877
2733
  selfCritique = await askYesNo(rl, "self-critique (rule + LLM-based review flagging)", dSelfCritique);
2878
2734
  generateSummaries = await askYesNo(rl, "generate summaries (LLM)", dGenSummaries);
2879
2735
  discoverRelationships = await askYesNo(rl, "discover relationships (LLM)", dDiscover);
@@ -3009,4 +2865,3 @@ export async function getApiKeyForProvider(provider, opts) {
3009
2865
  }
3010
2866
  return readFirstInChain(provider) ?? "";
3011
2867
  }
3012
- export { getApiKeyForProviderFromConfig } from "./apiKeyVault.js";
@@ -1,6 +1,6 @@
1
1
  export async function runStaleCommand(getResolver, opts) {
2
2
  const resolver = await getResolver();
3
- const threshold = parseInt(opts.days);
3
+ const threshold = parseInt(opts.days, 10);
4
4
  const cutoff = new Date();
5
5
  cutoff.setDate(cutoff.getDate() - threshold);
6
6
  const cutoffStr = cutoff.toISOString().split("T")[0];
@@ -18,7 +18,7 @@ export async function runStaleCommand(getResolver, opts) {
18
18
  b.frontmatter.modified;
19
19
  return (aDate || "").localeCompare(bDate || "");
20
20
  })
21
- .slice(0, parseInt(opts.limit));
21
+ .slice(0, parseInt(opts.limit, 10));
22
22
  if (stale.length === 0) {
23
23
  console.log(`No memories older than ${threshold} days.`);
24
24
  return;
@@ -33,15 +33,9 @@ export interface V13SyncStatus {
33
33
  }
34
34
  export declare function isMasterReachable(masterPath: string): boolean;
35
35
  export declare function countClientWaitingStaging(masterPath: string, machineId: string): number;
36
- /** Client should hide snapshot reads when master is unreachable (v13 offline rule). */
37
- export declare function shouldHideSnapshotReads(masterPath: string): boolean;
38
36
  export declare function listClientReceipts(masterPath: string, machineId: string): IngestReceipt[];
39
37
  export declare function getIngestedUlids(masterPath: string, machineId: string): Set<string>;
40
38
  export declare function openClientReadContext(localDb: GnosysDB, masterPath: string, machineId: string): ClientReadContext;
41
39
  /** Release snapshot/master DB handles opened by openClientReadContext. */
42
40
  export declare function closeClientReadContext(ctx: ClientReadContext): void;
43
- /**
44
- * @deprecated Use openClientReadContext — returns only the read DB without overlay or cleanup.
45
- */
46
- export declare function openClientReadDb(localDb: GnosysDB, masterPath: string): GnosysDB;
47
41
  export declare function getV13SyncStatus(localDb: GnosysDB): V13SyncStatus;
@@ -6,7 +6,7 @@ import path from "path";
6
6
  import { GnosysDB } from "./db.js";
7
7
  import { readMachineConfig, getMachineId } from "./machineConfig.js";
8
8
  import { getConfiguredRemotePath } from "./remote.js";
9
- import { clientSnapshotStore, formatSnapshotAge, getClientAcceptedManifest, } from "./syncSnapshot.js";
9
+ import { acceptClientSnapshot, clientSnapshotStore, compareSnapshotVersion, formatSnapshotAge, getClientAcceptedManifest, getMasterManifest, } from "./syncSnapshot.js";
10
10
  import { countFailedStagingFiles, machineStagingDir, stagingRoot } from "./syncStaging.js";
11
11
  import { renderClientSyncStatusLines, } from "./setup/remoteRender.js";
12
12
  const REACHABILITY_TTL_MS = 30_000;
@@ -53,10 +53,6 @@ export function countClientWaitingStaging(masterPath, machineId) {
53
53
  return 0;
54
54
  }
55
55
  }
56
- /** Client should hide snapshot reads when master is unreachable (v13 offline rule). */
57
- export function shouldHideSnapshotReads(masterPath) {
58
- return !isMasterReachable(masterPath);
59
- }
60
56
  export function listClientReceipts(masterPath, machineId) {
61
57
  const dir = path.join(stagingRoot(masterPath), machineId, "receipts");
62
58
  if (!existsSync(dir))
@@ -92,7 +88,39 @@ export function openClientReadContext(localDb, masterPath, machineId) {
92
88
  const reachable = isMasterReachable(masterPath);
93
89
  const ingestedUlids = getIngestedUlids(masterPath, machineId);
94
90
  const pendingAdds = localDb.listActivePendingAdds().filter((p) => !ingestedUlids.has(p.id));
91
+ const store = clientSnapshotStore(masterPath);
92
+ const snapPath = path.join(store, "gnosys.db");
95
93
  if (reachable) {
94
+ // v13 completion (5.12.x): clients must not open the live gnosys.db over
95
+ // the network (concurrent master writes + network FS = torn-page hazard,
96
+ // DESIGN.md "The Simple Rule"). Refresh the verified local snapshot copy
97
+ // when the master has published a newer one, then read the local copy.
98
+ const manifest = getMasterManifest(masterPath);
99
+ if (manifest) {
100
+ const accepted = getClientAcceptedManifest(masterPath);
101
+ if (compareSnapshotVersion(accepted, manifest)) {
102
+ // Best effort — on checksum/copy failure we fall through to whatever
103
+ // local snapshot (or live-DB fallback) is still available.
104
+ acceptClientSnapshot(masterPath, manifest);
105
+ }
106
+ if (existsSync(snapPath)) {
107
+ const snapDb = new GnosysDB(store);
108
+ if (snapDb.isAvailable()) {
109
+ return {
110
+ db: snapDb,
111
+ localDb,
112
+ pendingOverlay: pendingAdds,
113
+ source: "snapshot",
114
+ masterReachable: true,
115
+ ownsReadDb: true,
116
+ };
117
+ }
118
+ snapDb.close();
119
+ }
120
+ }
121
+ // Compatibility fallback: master has never published a snapshot (older
122
+ // master version, or first sweep still pending) — open the live DB as
123
+ // before. Disappears once the master runs one publish-enabled sweep.
96
124
  const masterDb = new GnosysDB(masterPath);
97
125
  if (masterDb.isAvailable()) {
98
126
  return {
@@ -106,8 +134,9 @@ export function openClientReadContext(localDb, masterPath, machineId) {
106
134
  }
107
135
  masterDb.close();
108
136
  }
109
- const store = clientSnapshotStore(masterPath);
110
- const snapPath = path.join(store, "gnosys.db");
137
+ // Offline (soft rule, signed off 2026-06-11): the last-accepted snapshot
138
+ // stays readable — immutable + checksummed, and offline clients are
139
+ // add-only, so staleness is the only risk. Status surfaces snapshotAge.
111
140
  if (existsSync(snapPath)) {
112
141
  const snapDb = new GnosysDB(store);
113
142
  if (snapDb.isAvailable()) {
@@ -137,13 +166,6 @@ export function closeClientReadContext(ctx) {
137
166
  ctx.db.close();
138
167
  }
139
168
  }
140
- /**
141
- * @deprecated Use openClientReadContext — returns only the read DB without overlay or cleanup.
142
- */
143
- export function openClientReadDb(localDb, masterPath) {
144
- const ctx = openClientReadContext(localDb, masterPath, getMachineId());
145
- return ctx.db;
146
- }
147
169
  export function getV13SyncStatus(localDb) {
148
170
  const mc = readMachineConfig();
149
171
  const masterPath = getConfiguredRemotePath(localDb);
@@ -2,7 +2,7 @@ import { GnosysDB } from "./db.js";
2
2
  import { getConfiguredRemotePath } from "./remote.js";
3
3
  import { readMachineConfig } from "./machineConfig.js";
4
4
  import { getV13SyncStatus } from "./syncClient.js";
5
- import { runMasterIngestSweep } from "./syncIngest.js";
5
+ import { runMasterIngestSweepAndPublish } from "./syncIngest.js";
6
6
  import { getSyncIngestTimerStatus } from "./syncIngestTimer.js";
7
7
  import { quarantineStaleTmpFiles, stagingRoot } from "./syncStaging.js";
8
8
  import { existsSync, readdirSync } from "fs";
@@ -48,7 +48,7 @@ export async function runSyncDoctorCommand(opts) {
48
48
  quarantineStaleTmpFiles(masterPath, id);
49
49
  }
50
50
  }
51
- ingestResult = runMasterIngestSweep(masterPath, {
51
+ ingestResult = await runMasterIngestSweepAndPublish(masterPath, {
52
52
  quiet: opts.quiet || !!opts.json,
53
53
  });
54
54
  if (ingestResult.errors.length > 0) {
@@ -17,3 +17,14 @@ export interface IngestSweepOptions {
17
17
  * Always-on cheap ingest sweep (agent start + timer). Single-writer lock on local disk.
18
18
  */
19
19
  export declare function runMasterIngestSweep(masterPath: string, opts?: IngestSweepOptions): IngestSweepResult;
20
+ /**
21
+ * v13 completion (5.12.x): sweep, then publish a fresh immutable snapshot
22
+ * when the sweep changed the DB (or none has ever been published). Clients
23
+ * read the published snapshot via a verified local copy instead of opening
24
+ * the live gnosys.db over the network — the hazard the design forbids.
25
+ *
26
+ * Kept separate from runMasterIngestSweep: publishMasterSnapshot acquires
27
+ * the same master-ingest lock, so it must run after the sweep releases it,
28
+ * and the sweep itself stays synchronous for existing callers.
29
+ */
30
+ export declare function runMasterIngestSweepAndPublish(masterPath: string, opts?: IngestSweepOptions): Promise<IngestSweepResult>;
@@ -8,7 +8,7 @@ import { getGnosysHome } from "./paths.js";
8
8
  import { ensureMachineConfig } from "./machineConfig.js";
9
9
  import { listPendingStagingQueue, observeStagingFile, parseStagedFile, quarantineStagingFile, quarantineStaleTmpFiles, stagingRoot, verifyMemoryExistsInDb, } from "./syncStaging.js";
10
10
  import { assertMasterLeaseHeld, readMasterMarker, touchMasterMarkerHeartbeat, validateLeaseEpochBeforeWrite, } from "./masterLease.js";
11
- import { acquireWriteLockSync } from "./syncLock.js";
11
+ import { acquireWriteLockSync, } from "./syncLock.js";
12
12
  const INGEST_LOCK_NAME = "master-ingest.lock";
13
13
  function payloadToMemory(p, now) {
14
14
  const contentHash = fnv1a(`${p.title}\n${p.content}`);
@@ -150,3 +150,26 @@ export function runMasterIngestSweep(masterPath, opts) {
150
150
  }
151
151
  return result;
152
152
  }
153
+ /**
154
+ * v13 completion (5.12.x): sweep, then publish a fresh immutable snapshot
155
+ * when the sweep changed the DB (or none has ever been published). Clients
156
+ * read the published snapshot via a verified local copy instead of opening
157
+ * the live gnosys.db over the network — the hazard the design forbids.
158
+ *
159
+ * Kept separate from runMasterIngestSweep: publishMasterSnapshot acquires
160
+ * the same master-ingest lock, so it must run after the sweep releases it,
161
+ * and the sweep itself stays synchronous for existing callers.
162
+ */
163
+ export async function runMasterIngestSweepAndPublish(masterPath, opts) {
164
+ const result = runMasterIngestSweep(masterPath, opts);
165
+ try {
166
+ const { getMasterManifest, publishMasterSnapshot } = await import("./syncSnapshot.js");
167
+ if (result.ingested > 0 || !getMasterManifest(masterPath)) {
168
+ await publishMasterSnapshot(masterPath);
169
+ }
170
+ }
171
+ catch (err) {
172
+ result.errors.push(`snapshot publish: ${err instanceof Error ? err.message : String(err)}`);
173
+ }
174
+ return result;
175
+ }
@@ -1,7 +1,7 @@
1
1
  import { GnosysDB } from "./db.js";
2
2
  import { readMachineConfig } from "./machineConfig.js";
3
3
  import { getConfiguredRemotePath } from "./remote.js";
4
- import { runMasterIngestSweep } from "./syncIngest.js";
4
+ import { runMasterIngestSweepAndPublish } from "./syncIngest.js";
5
5
  /**
6
6
  * Run one ingest sweep on MCP server startup (master role only).
7
7
  * Non-blocking — callers should fire-and-forget.
@@ -22,7 +22,7 @@ export async function maybeRunStartupIngestSweep() {
22
22
  }
23
23
  if (!masterPath)
24
24
  return;
25
- const result = runMasterIngestSweep(masterPath, { quiet: true });
25
+ const result = await runMasterIngestSweepAndPublish(masterPath, { quiet: true });
26
26
  if (result.errors.length > 0) {
27
27
  console.error(`[sync] Startup ingest sweep: ${result.ingested} ingested, ${result.errors.length} error(s)`);
28
28
  }
@@ -11,6 +11,8 @@ export interface SnapshotManifestFile {
11
11
  }
12
12
  export declare function masterSnapshotsDir(masterPath: string): string;
13
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;
14
16
  /** Publish a new immutable snapshot on the master (re-validates lease epoch first). */
15
17
  export declare function publishMasterSnapshot(masterPath: string): Promise<SnapshotManifestFile | null>;
16
18
  export declare function compareSnapshotVersion(accepted: {
@@ -34,6 +34,10 @@ function readManifestFile(masterPath) {
34
34
  return null;
35
35
  }
36
36
  }
37
+ /** Read the master's currently published snapshot manifest (null if none). */
38
+ export function getMasterManifest(masterPath) {
39
+ return readManifestFile(masterPath);
40
+ }
37
41
  function writeManifestFile(masterPath, manifest) {
38
42
  const dir = masterSnapshotsDir(masterPath);
39
43
  mkdirSync(dir, { recursive: true });
@@ -34,8 +34,6 @@ export type StagedFileParseResult = {
34
34
  };
35
35
  export declare function stagingRoot(masterPath: string): string;
36
36
  export declare function machineStagingDir(masterPath: string, machineId: string): string;
37
- /** Alias used by remoteWizard and tests. */
38
- export declare const stagingDirForMachine: typeof machineStagingDir;
39
37
  export declare function clientPresencePath(masterPath: string, machineId: string): string;
40
38
  export declare function failedQuarantineDir(masterPath: string, machineId: string): string;
41
39
  export declare function buildStagingFileName(memoryUlid: string, unixMs?: number): string;