gnosys 5.9.2 → 5.9.4

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 (136) hide show
  1. package/dist/cli.js +311 -147
  2. package/dist/cli.js.map +1 -1
  3. package/dist/lib/chat/index.d.ts.map +1 -1
  4. package/dist/lib/chat/index.js +5 -0
  5. package/dist/lib/chat/index.js.map +1 -1
  6. package/dist/lib/cleanup.d.ts +52 -0
  7. package/dist/lib/cleanup.d.ts.map +1 -0
  8. package/dist/lib/cleanup.js +168 -0
  9. package/dist/lib/cleanup.js.map +1 -0
  10. package/dist/lib/config.d.ts +7 -0
  11. package/dist/lib/config.d.ts.map +1 -1
  12. package/dist/lib/config.js +41 -2
  13. package/dist/lib/config.js.map +1 -1
  14. package/dist/lib/dream.d.ts.map +1 -1
  15. package/dist/lib/dream.js +13 -2
  16. package/dist/lib/dream.js.map +1 -1
  17. package/dist/lib/paths.d.ts +8 -0
  18. package/dist/lib/paths.d.ts.map +1 -1
  19. package/dist/lib/paths.js +15 -0
  20. package/dist/lib/paths.js.map +1 -1
  21. package/dist/lib/remote.d.ts +11 -0
  22. package/dist/lib/remote.d.ts.map +1 -1
  23. package/dist/lib/remote.js +26 -2
  24. package/dist/lib/remote.js.map +1 -1
  25. package/dist/lib/remoteWizard.d.ts.map +1 -1
  26. package/dist/lib/remoteWizard.js +112 -52
  27. package/dist/lib/remoteWizard.js.map +1 -1
  28. package/dist/lib/setup/coldStart.d.ts +71 -0
  29. package/dist/lib/setup/coldStart.d.ts.map +1 -0
  30. package/dist/lib/setup/coldStart.js +122 -0
  31. package/dist/lib/setup/coldStart.js.map +1 -0
  32. package/dist/lib/setup/configSetRender.d.ts +26 -0
  33. package/dist/lib/setup/configSetRender.d.ts.map +1 -0
  34. package/dist/lib/setup/configSetRender.js +103 -0
  35. package/dist/lib/setup/configSetRender.js.map +1 -0
  36. package/dist/lib/setup/dreamRender.d.ts +44 -0
  37. package/dist/lib/setup/dreamRender.d.ts.map +1 -0
  38. package/dist/lib/setup/dreamRender.js +55 -0
  39. package/dist/lib/setup/dreamRender.js.map +1 -0
  40. package/dist/lib/setup/dreamState.d.ts +50 -0
  41. package/dist/lib/setup/dreamState.d.ts.map +1 -0
  42. package/dist/lib/setup/dreamState.js +68 -0
  43. package/dist/lib/setup/dreamState.js.map +1 -0
  44. package/dist/lib/setup/modelsRender.d.ts +25 -0
  45. package/dist/lib/setup/modelsRender.d.ts.map +1 -0
  46. package/dist/lib/setup/modelsRender.js +33 -0
  47. package/dist/lib/setup/modelsRender.js.map +1 -0
  48. package/dist/lib/setup/remoteRender.d.ts +43 -0
  49. package/dist/lib/setup/remoteRender.d.ts.map +1 -0
  50. package/dist/lib/setup/remoteRender.js +65 -0
  51. package/dist/lib/setup/remoteRender.js.map +1 -0
  52. package/dist/lib/setup/routingRender.d.ts +48 -0
  53. package/dist/lib/setup/routingRender.d.ts.map +1 -0
  54. package/dist/lib/setup/routingRender.js +114 -0
  55. package/dist/lib/setup/routingRender.js.map +1 -0
  56. package/dist/lib/setup/sections/ides.d.ts +8 -0
  57. package/dist/lib/setup/sections/ides.d.ts.map +1 -1
  58. package/dist/lib/setup/sections/ides.js +88 -33
  59. package/dist/lib/setup/sections/ides.js.map +1 -1
  60. package/dist/lib/setup/sections/preferences.d.ts +8 -0
  61. package/dist/lib/setup/sections/preferences.d.ts.map +1 -1
  62. package/dist/lib/setup/sections/preferences.js +54 -20
  63. package/dist/lib/setup/sections/preferences.js.map +1 -1
  64. package/dist/lib/setup/sections/routing.d.ts +0 -1
  65. package/dist/lib/setup/sections/routing.d.ts.map +1 -1
  66. package/dist/lib/setup/sections/routing.js +80 -38
  67. package/dist/lib/setup/sections/routing.js.map +1 -1
  68. package/dist/lib/setup/storePath.d.ts +30 -0
  69. package/dist/lib/setup/storePath.d.ts.map +1 -0
  70. package/dist/lib/setup/storePath.js +47 -0
  71. package/dist/lib/setup/storePath.js.map +1 -0
  72. package/dist/lib/setup/summary.d.ts +33 -19
  73. package/dist/lib/setup/summary.d.ts.map +1 -1
  74. package/dist/lib/setup/summary.js +148 -113
  75. package/dist/lib/setup/summary.js.map +1 -1
  76. package/dist/lib/setup/syncProjectsRender.d.ts +57 -0
  77. package/dist/lib/setup/syncProjectsRender.d.ts.map +1 -0
  78. package/dist/lib/setup/syncProjectsRender.js +184 -0
  79. package/dist/lib/setup/syncProjectsRender.js.map +1 -0
  80. package/dist/lib/setup/ui/diff.d.ts +19 -0
  81. package/dist/lib/setup/ui/diff.d.ts.map +1 -0
  82. package/dist/lib/setup/ui/diff.js +34 -0
  83. package/dist/lib/setup/ui/diff.js.map +1 -0
  84. package/dist/lib/setup/ui/footer.d.ts +11 -0
  85. package/dist/lib/setup/ui/footer.d.ts.map +1 -0
  86. package/dist/lib/setup/ui/footer.js +21 -0
  87. package/dist/lib/setup/ui/footer.js.map +1 -0
  88. package/dist/lib/setup/ui/header.d.ts +24 -0
  89. package/dist/lib/setup/ui/header.d.ts.map +1 -0
  90. package/dist/lib/setup/ui/header.js +49 -0
  91. package/dist/lib/setup/ui/header.js.map +1 -0
  92. package/dist/lib/setup/ui/index.d.ts +28 -0
  93. package/dist/lib/setup/ui/index.d.ts.map +1 -0
  94. package/dist/lib/setup/ui/index.js +19 -0
  95. package/dist/lib/setup/ui/index.js.map +1 -0
  96. package/dist/lib/setup/ui/menu.d.ts +27 -0
  97. package/dist/lib/setup/ui/menu.d.ts.map +1 -0
  98. package/dist/lib/setup/ui/menu.js +75 -0
  99. package/dist/lib/setup/ui/menu.js.map +1 -0
  100. package/dist/lib/setup/ui/panel.d.ts +23 -0
  101. package/dist/lib/setup/ui/panel.d.ts.map +1 -0
  102. package/dist/lib/setup/ui/panel.js +60 -0
  103. package/dist/lib/setup/ui/panel.js.map +1 -0
  104. package/dist/lib/setup/ui/prompt.d.ts +27 -0
  105. package/dist/lib/setup/ui/prompt.d.ts.map +1 -0
  106. package/dist/lib/setup/ui/prompt.js +38 -0
  107. package/dist/lib/setup/ui/prompt.js.map +1 -0
  108. package/dist/lib/setup/ui/safePrompt.d.ts +24 -0
  109. package/dist/lib/setup/ui/safePrompt.d.ts.map +1 -0
  110. package/dist/lib/setup/ui/safePrompt.js +71 -0
  111. package/dist/lib/setup/ui/safePrompt.js.map +1 -0
  112. package/dist/lib/setup/ui/spinner.d.ts +28 -0
  113. package/dist/lib/setup/ui/spinner.d.ts.map +1 -0
  114. package/dist/lib/setup/ui/spinner.js +87 -0
  115. package/dist/lib/setup/ui/spinner.js.map +1 -0
  116. package/dist/lib/setup/ui/status.d.ts +21 -0
  117. package/dist/lib/setup/ui/status.d.ts.map +1 -0
  118. package/dist/lib/setup/ui/status.js +49 -0
  119. package/dist/lib/setup/ui/status.js.map +1 -0
  120. package/dist/lib/setup/ui/table.d.ts +37 -0
  121. package/dist/lib/setup/ui/table.d.ts.map +1 -0
  122. package/dist/lib/setup/ui/table.js +82 -0
  123. package/dist/lib/setup/ui/table.js.map +1 -0
  124. package/dist/lib/setup/ui/title.d.ts +14 -0
  125. package/dist/lib/setup/ui/title.d.ts.map +1 -0
  126. package/dist/lib/setup/ui/title.js +24 -0
  127. package/dist/lib/setup/ui/title.js.map +1 -0
  128. package/dist/lib/setup/ui/tokens.d.ts +66 -0
  129. package/dist/lib/setup/ui/tokens.d.ts.map +1 -0
  130. package/dist/lib/setup/ui/tokens.js +87 -0
  131. package/dist/lib/setup/ui/tokens.js.map +1 -0
  132. package/dist/lib/setup.d.ts +38 -0
  133. package/dist/lib/setup.d.ts.map +1 -1
  134. package/dist/lib/setup.js +440 -308
  135. package/dist/lib/setup.js.map +1 -1
  136. package/package.json +1 -1
package/dist/lib/setup.js CHANGED
@@ -16,7 +16,8 @@ import os from "os";
16
16
  import { execSync } from "child_process";
17
17
  import { loadConfig, updateConfig, getProviderModel, } from "./config.js";
18
18
  import { validateModel } from "./modelValidation.js";
19
- import { getGnosysHome } from "./paths.js";
19
+ import { resolveActiveStorePath, ensureActiveStorePath } from "./setup/storePath.js";
20
+ import { safeQuestion } from "./setup/ui/safePrompt.js";
20
21
  // ─── ANSI Colors ────────────────────────────────────────────────────────────
21
22
  const BOLD = "\x1b[1m";
22
23
  const DIM = "\x1b[2m";
@@ -138,6 +139,12 @@ export async function fetchDynamicModels() {
138
139
  })
139
140
  .filter((m) => m.input > 0 && !m.isFree && !m.isVariant && !m.isGuard)
140
141
  .filter((m) => !/audio|search|embed|tts|vision|image|code-/i.test(m.modelId)) // skip specialized models
142
+ // v5.9.4 Bug 3 — xAI: OpenRouter returns weird names like
143
+ // `grok-build-0.1` and `grok-4.20-multi-agent`. Keep only canonical
144
+ // numbered Grok flagship/preview models (e.g. `grok-3`, `grok-4.0`,
145
+ // `grok-4.3`). When nothing matches we fall through to the static
146
+ // tiers a few lines down.
147
+ .filter((m) => ourProvider !== "xai" || /^grok-[0-9]/.test(m.modelId))
141
148
  .sort((a, b) => b.created - a.created); // newest first
142
149
  if (models.length === 0)
143
150
  continue;
@@ -226,6 +233,19 @@ export async function fetchDynamicModels() {
226
233
  if (!tiers.some((t) => t.recommended) && tiers.length > 0) {
227
234
  tiers[0].recommended = true;
228
235
  }
236
+ // v5.9.4 Bug 3 — UNION OpenRouter tiers with static PROVIDER_TIERS.xai
237
+ // so the static catalog (e.g. grok-4.3) always shows up even when
238
+ // OpenRouter omits it. Dedup by model id; static entries win the tie
239
+ // (their pricing matches the launch price more reliably).
240
+ if (ourProvider === "xai") {
241
+ const seen = new Set(tiers.map((t) => t.model));
242
+ for (const staticTier of PROVIDER_TIERS.xai ?? []) {
243
+ if (!seen.has(staticTier.model)) {
244
+ tiers.push(staticTier);
245
+ seen.add(staticTier.model);
246
+ }
247
+ }
248
+ }
229
249
  result[ourProvider] = tiers;
230
250
  }
231
251
  // Cache the result
@@ -532,6 +552,16 @@ export async function detectIDEs(projectDir) {
532
552
  // Not installed
533
553
  }
534
554
  }
555
+ // v5.9.4 Bug 12 — Grok Build (xAI's coding agent) stores MCP config at
556
+ // ~/.grok/config.toml. The directory's presence is the detection signal.
557
+ try {
558
+ const stat = await fs.stat(path.join(home, ".grok"));
559
+ if (stat.isDirectory())
560
+ detected.push("grok-build");
561
+ }
562
+ catch {
563
+ // Not installed
564
+ }
535
565
  return detected;
536
566
  }
537
567
  /**
@@ -551,6 +581,68 @@ function claudeDesktopConfigPath() {
551
581
  }
552
582
  return path.join(home, ".config", "Claude", "claude_desktop_config.json");
553
583
  }
584
+ /**
585
+ * Replace (or append) a `[mcp.<name>]` block inside the TOML text for
586
+ * Grok Build's config file. Preserves every line outside that block —
587
+ * deci-046 read-then-merge rule. We can't pull in a TOML dependency
588
+ * without adding to package.json, so we ship a minimal hand-rolled
589
+ * updater scoped exactly to the `[mcp.gnosys]` use case.
590
+ *
591
+ * Spec assumption: TOML headers we touch are simple `[a.b]` lines with
592
+ * no inline tables or nested arrays. Any other content is left alone.
593
+ *
594
+ * Exported for tests.
595
+ */
596
+ export function upsertGrokMcpBlock(existing, name, entry) {
597
+ const sectionHeader = `[mcp.${name}]`;
598
+ const lines = existing.split("\n");
599
+ const headerIdx = lines.findIndex((line) => line.trim() === sectionHeader);
600
+ const blockBody = renderGrokMcpBlock(entry);
601
+ if (headerIdx === -1) {
602
+ // Append a fresh block, separated by a blank line if the file has content.
603
+ const prefix = existing.length === 0 || existing.endsWith("\n\n")
604
+ ? existing
605
+ : existing.endsWith("\n") ? `${existing}\n` : `${existing}\n\n`;
606
+ return `${prefix}${sectionHeader}\n${blockBody}`;
607
+ }
608
+ // Replace the existing block — everything from sectionHeader up to the
609
+ // next `[` header (or EOF). Count blank lines immediately after the block
610
+ // so we can preserve the original spacing before the next section.
611
+ let endIdx = lines.length;
612
+ for (let i = headerIdx + 1; i < lines.length; i++) {
613
+ if (/^\s*\[/.test(lines[i])) {
614
+ endIdx = i;
615
+ break;
616
+ }
617
+ }
618
+ let trailingBlankBeforeNext = 0;
619
+ while (endIdx > headerIdx + 1 && lines[endIdx - 1].trim() === "") {
620
+ trailingBlankBeforeNext++;
621
+ endIdx--;
622
+ }
623
+ const afterLines = lines.slice(endIdx);
624
+ const hasFollowingSection = afterLines.some((l) => /^\s*\[/.test(l));
625
+ const beforeBlock = lines.slice(0, headerIdx).join("\n");
626
+ const head = beforeBlock.length > 0 ? `${beforeBlock}\n` : "";
627
+ if (!hasFollowingSection) {
628
+ // No following section — drop trailing blank lines and end with a single \n.
629
+ return `${head}${sectionHeader}\n${blockBody}`;
630
+ }
631
+ const gap = "\n".repeat(Math.max(1, trailingBlankBeforeNext));
632
+ const afterBlock = afterLines.join("\n");
633
+ return `${head}${sectionHeader}\n${blockBody}${gap}${afterBlock}`;
634
+ }
635
+ function renderGrokMcpBlock(entry) {
636
+ const argsStr = `[${entry.args.map((a) => JSON.stringify(a)).join(", ")}]`;
637
+ const lines = [
638
+ `command = ${JSON.stringify(entry.command)}`,
639
+ `args = ${argsStr}`,
640
+ ];
641
+ if (typeof entry.startup_timeout_sec === "number") {
642
+ lines.push(`startup_timeout_sec = ${entry.startup_timeout_sec}`);
643
+ }
644
+ return `${lines.join("\n")}\n`;
645
+ }
554
646
  /**
555
647
  * Set up Gnosys MCP integration for a specific IDE.
556
648
  */
@@ -716,6 +808,29 @@ export async function setupIDE(ide, projectDir) {
716
808
  await fs.writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
717
809
  return { success: true, message: "Antigravity MCP config updated (~/.gemini/antigravity/mcp_config.json)" };
718
810
  }
811
+ case "grok-build": {
812
+ // v5.9.4 Bug 12 — Grok Build reads its MCP servers from a
813
+ // `[mcp.<name>]` block in ~/.grok/config.toml. We never clobber
814
+ // unrelated TOML content (per deci-046 read-then-merge rule); the
815
+ // helper preserves every line outside the `[mcp.gnosys]` block.
816
+ const grokDir = path.join(os.homedir(), ".grok");
817
+ const configPath = path.join(grokDir, "config.toml");
818
+ await fs.mkdir(grokDir, { recursive: true });
819
+ let existing = "";
820
+ try {
821
+ existing = await fs.readFile(configPath, "utf-8");
822
+ }
823
+ catch {
824
+ // File doesn't exist yet — start fresh
825
+ }
826
+ const updated = upsertGrokMcpBlock(existing, "gnosys", {
827
+ command: "gnosys",
828
+ args: ["serve"],
829
+ startup_timeout_sec: 90,
830
+ });
831
+ await fs.writeFile(configPath, updated, "utf-8");
832
+ return { success: true, message: "Grok Build MCP config updated (~/.grok/config.toml)" };
833
+ }
719
834
  case "claude-desktop": {
720
835
  // Claude Desktop reads MCP servers from claude_desktop_config.json
721
836
  // in a platform-specific app data directory. Distinct from Claude
@@ -769,7 +884,7 @@ async function askChoice(rl, question, options) {
769
884
  }
770
885
  console.log();
771
886
  while (true) {
772
- const answer = await rl.question(`${DIM}>${RESET} `);
887
+ const answer = await safeQuestion(rl, `${DIM}>${RESET} `);
773
888
  const num = parseInt(answer.trim(), 10);
774
889
  if (num >= 1 && num <= options.length) {
775
890
  return num - 1;
@@ -782,7 +897,7 @@ async function askChoice(rl, question, options) {
782
897
  */
783
898
  async function askInput(rl, prompt, opts) {
784
899
  const suffix = opts?.default ? ` ${DIM}(${opts.default})${RESET}` : "";
785
- const answer = await rl.question(`${prompt}${suffix}: `);
900
+ const answer = await safeQuestion(rl, `${prompt}${suffix}: `);
786
901
  const trimmed = answer.trim();
787
902
  return trimmed || opts?.default || "";
788
903
  }
@@ -791,7 +906,7 @@ async function askInput(rl, prompt, opts) {
791
906
  */
792
907
  async function askYesNo(rl, question, defaultYes = true) {
793
908
  const hint = defaultYes ? "Y/n" : "y/N";
794
- const answer = await rl.question(`${question} [${hint}] `);
909
+ const answer = await safeQuestion(rl, `${question} [${hint}] `);
795
910
  const trimmed = answer.trim().toLowerCase();
796
911
  if (trimmed === "")
797
912
  return defaultYes;
@@ -866,31 +981,21 @@ async function getRegisteredProjects() {
866
981
  }
867
982
  /**
868
983
  * Try to load existing gnosys.json config for displaying current values.
869
- * Checks the project .gnosys dir first, then the global ~/.gnosys dir.
870
- * Returns null if no config found.
984
+ * Resolves the active store via the shared `resolveActiveStorePath` helper
985
+ * (v5.9.4 Bug 10 was reading from a different store than the summary
986
+ * panel, producing stale-display bugs in `gnosys setup models`).
987
+ * Returns null if no config exists in either project or global stores.
871
988
  */
872
989
  async function loadExistingConfig(projectDir) {
873
- // Try project-level config first
990
+ const storePath = resolveActiveStorePath(projectDir);
874
991
  try {
875
- const projectStore = path.join(projectDir, ".gnosys");
876
- const stat = await fs.stat(path.join(projectStore, "gnosys.json"));
992
+ const stat = await fs.stat(path.join(storePath, "gnosys.json"));
877
993
  if (stat.isFile()) {
878
- return await loadConfig(projectStore);
994
+ return await loadConfig(storePath);
879
995
  }
880
996
  }
881
997
  catch {
882
- // No project config
883
- }
884
- // Try global config at ~/.gnosys
885
- try {
886
- const globalStore = getGnosysHome();
887
- const stat = await fs.stat(path.join(globalStore, "gnosys.json"));
888
- if (stat.isFile()) {
889
- return await loadConfig(globalStore);
890
- }
891
- }
892
- catch {
893
- // No global config
998
+ // No config in the resolved store
894
999
  }
895
1000
  return null;
896
1001
  }
@@ -983,16 +1088,10 @@ export async function runSetup(opts) {
983
1088
  let upgraded = false;
984
1089
  try {
985
1090
  // ─── Banner ───────────────────────────────────────────────────────
986
- const tagline = "Persistent Memory for AI Agents";
987
- const versionStr = `Gnosys v${version}`;
988
- const bannerContentWidth = Math.max(versionStr.length, tagline.length);
989
- const bannerInner = bannerContentWidth + 4;
990
- const bannerBorder = "\u2500".repeat(bannerInner);
1091
+ // v5.9.3 redesign: atom-based splash replaces the old ASCII box banner.
1092
+ const { renderColdStartSplash } = await import("./setup/coldStart.js");
991
1093
  console.log();
992
- console.log(`\u250C${bannerBorder}\u2510`);
993
- console.log(`\u2502 ${BOLD}${CYAN}Gnosys${RESET} v${version}${" ".repeat(bannerInner - versionStr.length - 2)}\u2502`);
994
- console.log(`\u2502 ${DIM}${tagline}${RESET}${" ".repeat(bannerInner - tagline.length - 2)}\u2502`);
995
- console.log(`\u2514${bannerBorder}\u2518`);
1094
+ console.log(renderColdStartSplash(version));
996
1095
  console.log();
997
1096
  // ─── Load existing config for defaults ───────────────────────────
998
1097
  const existingConfig = await loadExistingConfig(projectDir);
@@ -1076,7 +1175,12 @@ export async function runSetup(opts) {
1076
1175
  const currentProviderHint = currentProvider
1077
1176
  ? ` ${DIM}(current: ${currentProvider})${RESET}`
1078
1177
  : "";
1079
- const providerIndex = await askChoice(rl, `${BOLD}Step 1/5${RESET} ${DIM}\u2014${RESET} Choose your LLM provider${currentProviderHint}`, providerOptions);
1178
+ // v5.9.3 Screen 1.1 chrome \u2014 Header + Title + step counter wrap.
1179
+ const { renderProviderStepHeader, renderModelStepHeader, renderKeyStepHeader } = await import("./setup/coldStart.js");
1180
+ console.log();
1181
+ console.log(renderProviderStepHeader(version));
1182
+ console.log();
1183
+ const providerIndex = await askChoice(rl, `Choose your LLM provider${currentProviderHint}`, providerOptions);
1080
1184
  const isSkip = providerIndex === PROVIDER_ORDER.length; // last option
1081
1185
  const provider = isSkip ? "skip" : PROVIDER_ORDER[providerIndex];
1082
1186
  // ─── Step 2/5 — Model tier ────────────────────────────────────────
@@ -1096,7 +1200,11 @@ export async function runSetup(opts) {
1096
1200
  return `${t.name} (${t.model}) ${DIM}${formatPrice(t.input, t.output)}${RESET}${rec}`;
1097
1201
  });
1098
1202
  tierOptions.push(`Custom ${DIM}(enter model name)${RESET}`);
1099
- const tierIndex = await askChoice(rl, `${BOLD}Step 2/5${RESET} ${DIM}\u2014${RESET} Choose model tier${currentModelHint}`, tierOptions);
1203
+ // v5.9.3 Screen 1.2 chrome \u2014 Header + Title + step counter wrap.
1204
+ console.log();
1205
+ console.log(renderModelStepHeader(provider, version));
1206
+ console.log();
1207
+ const tierIndex = await askChoice(rl, `Choose model tier${currentModelHint}`, tierOptions);
1100
1208
  if (tierIndex === tiers.length) {
1101
1209
  model = await askInput(rl, "Enter model name");
1102
1210
  }
@@ -1169,8 +1277,9 @@ export async function runSetup(opts) {
1169
1277
  };
1170
1278
  const legacyEnvVar = legacyEnvVars[provider] ?? "";
1171
1279
  if (needsKey) {
1280
+ // v5.9.3 Screen 1.3 chrome \u2014 Header + Title + step counter wrap.
1172
1281
  console.log();
1173
- console.log(`${BOLD}Step 3/5${RESET} ${DIM}\u2014${RESET} API Key`);
1282
+ console.log(renderKeyStepHeader(provider, version));
1174
1283
  console.log();
1175
1284
  // Check where the key currently lives
1176
1285
  const existingKeySource = detectKeySource(envVarName, legacyEnvVar);
@@ -1503,12 +1612,14 @@ export async function runSetup(opts) {
1503
1612
  codex: "Codex",
1504
1613
  "gemini-cli": "Gemini CLI",
1505
1614
  antigravity: "Antigravity",
1615
+ // v5.9.4 Bug 12 — Grok Build (~/.grok/config.toml).
1616
+ "grok-build": "Grok Build",
1506
1617
  };
1507
1618
  // IDEs whose MCP config lives at the user level (~/...) rather than per-project.
1508
1619
  // We don't try to create a project-level directory for these.
1509
- const userLevelIdes = new Set(["claude", "claude-desktop", "gemini-cli", "antigravity"]);
1620
+ const userLevelIdes = new Set(["claude", "claude-desktop", "gemini-cli", "antigravity", "grok-build"]);
1510
1621
  // Build IDE options: show detected ones and offer to create missing ones
1511
- const allIdeKeys = ["claude", "claude-desktop", "cursor", "codex", "gemini-cli", "antigravity"];
1622
+ const allIdeKeys = ["claude", "claude-desktop", "cursor", "codex", "gemini-cli", "antigravity", "grok-build"];
1512
1623
  const ideOptions = [];
1513
1624
  const ideKeyForOption = []; // parallel array mapping option index to IDE key
1514
1625
  for (const ide of allIdeKeys) {
@@ -1590,21 +1701,8 @@ export async function runSetup(opts) {
1590
1701
  const structuringModel = isSkip ? "" : (taskOverrides.structuring?.model ?? getStructuringModel(provider, model));
1591
1702
  // ─── Write config to gnosys.json ─────────────────────────────────
1592
1703
  if (!isSkip) {
1593
- // Determine which store path to write to prefer project, fall back to global
1594
- let storePath;
1595
- const projectStore = path.join(projectDir, ".gnosys");
1596
- const globalStore = getGnosysHome();
1597
- if (fsSync.existsSync(path.join(projectStore, "gnosys.json"))) {
1598
- storePath = projectStore;
1599
- }
1600
- else if (fsSync.existsSync(path.join(globalStore, "gnosys.json"))) {
1601
- storePath = globalStore;
1602
- }
1603
- else {
1604
- // Default to global store — create directory if needed
1605
- await fs.mkdir(globalStore, { recursive: true });
1606
- storePath = globalStore;
1607
- }
1704
+ // v5.9.4 Bug 10unified store resolution.
1705
+ const storePath = ensureActiveStorePath(projectDir);
1608
1706
  // Build the config updates
1609
1707
  // Build LLM config update, preserving existing provider-specific settings
1610
1708
  const existingLlm = existingConfig?.llm;
@@ -1716,7 +1814,7 @@ export async function runSetup(opts) {
1716
1814
  console.log("Share your gnosys.db across machines via NAS or shared drive.");
1717
1815
  console.log(`Your local DB stays fast, the remote is the source of truth.`);
1718
1816
  console.log();
1719
- const setUpRemote = (await rl.question(`Configure remote sync now? [y/N] `)).trim().toLowerCase();
1817
+ const setUpRemote = (await safeQuestion(rl, `Configure remote sync now? [y/N] `)).trim().toLowerCase();
1720
1818
  if (setUpRemote === "y" || setUpRemote === "yes") {
1721
1819
  console.log();
1722
1820
  try {
@@ -1767,6 +1865,60 @@ export async function runSetup(opts) {
1767
1865
  throw err;
1768
1866
  }
1769
1867
  }
1868
+ /**
1869
+ * Update ONLY `llm.defaultProvider` in gnosys.json. Used by the summary
1870
+ * panel row 1 ("provider") so it stops dragging the user into the full
1871
+ * model picker — that's row 2's job.
1872
+ *
1873
+ * v5.9.4 Bug 4 — before this split, both summary rows routed through
1874
+ * `runModelsSetup`, leaving no way to swap provider without also choosing
1875
+ * a new model. Now row 1 picks a provider, row 2 picks a model.
1876
+ */
1877
+ export async function runProviderOnlySetup(opts = {}) {
1878
+ const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
1879
+ const ownsRl = !opts.rl;
1880
+ const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
1881
+ try {
1882
+ const { Header } = await import("./setup/ui/header.js");
1883
+ const { Title } = await import("./setup/ui/title.js");
1884
+ const { Spinner } = await import("./setup/ui/spinner.js");
1885
+ const { printStatus } = await import("./setup/ui/status.js");
1886
+ console.log();
1887
+ console.log(Header(["gnosys", "setup", "provider"]));
1888
+ console.log();
1889
+ console.log(Title("Default provider", "pick the LLM provider — model stays as configured"));
1890
+ console.log();
1891
+ const existingConfig = await loadExistingConfig(projectDir);
1892
+ const currentProvider = existingConfig?.llm.defaultProvider;
1893
+ const pricingSpin = Spinner("fetching latest pricing from openrouter…");
1894
+ const fetchStart = Date.now();
1895
+ const dynamicModels = await fetchDynamicModels();
1896
+ const fetchMs = Date.now() - fetchStart;
1897
+ if (Object.keys(dynamicModels).length > 0) {
1898
+ pricingSpin.ok("pricing loaded", `${fetchMs} ms`);
1899
+ }
1900
+ else {
1901
+ pricingSpin.fail("pricing fetch failed", "using bundled tiers");
1902
+ }
1903
+ console.log();
1904
+ const provider = await pickProvider(rl, dynamicModels, "Choose your LLM provider", currentProvider);
1905
+ if (!provider || provider === currentProvider) {
1906
+ printStatus("warn", "no change · provider unchanged");
1907
+ return;
1908
+ }
1909
+ const storePath = ensureActiveStorePath(projectDir);
1910
+ const existingLlm = existingConfig?.llm ?? {};
1911
+ await updateConfig(storePath, {
1912
+ llm: { ...existingLlm, defaultProvider: provider },
1913
+ });
1914
+ printStatus("ok", `default provider · ${provider}`, `${storePath}/gnosys.json`);
1915
+ printStatus("progress", "model unchanged", "use row 2 to swap the model");
1916
+ }
1917
+ finally {
1918
+ if (ownsRl)
1919
+ rl.close();
1920
+ }
1921
+ }
1770
1922
  /**
1771
1923
  * Models-only configuration — prompts for provider, model, and key (or accepts
1772
1924
  * them via options for non-interactive use). Validates the model against the
@@ -1777,30 +1929,47 @@ export async function runModelsSetup(opts = {}) {
1777
1929
  const ownsRl = !opts.rl;
1778
1930
  const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
1779
1931
  try {
1932
+ // v5.9.3 Screen 3 — Header + Title at the top.
1933
+ const { Header } = await import("./setup/ui/header.js");
1934
+ const { Title } = await import("./setup/ui/title.js");
1935
+ const { Spinner } = await import("./setup/ui/spinner.js");
1936
+ const { printDiff } = await import("./setup/ui/diff.js");
1937
+ const { printStatus } = await import("./setup/ui/status.js");
1780
1938
  console.log();
1781
- console.log(`${BOLD}${CYAN}Gnosys${RESET} ${DIM}— Model Configuration${RESET}`);
1939
+ console.log(Header(["gnosys", "setup", "models"]));
1940
+ console.log();
1941
+ console.log(Title("Model configuration", "pick a provider and model — we'll validate it before saving"));
1782
1942
  console.log();
1783
1943
  const existingConfig = await loadExistingConfig(projectDir);
1784
1944
  const currentProvider = existingConfig?.llm.defaultProvider;
1785
1945
  const currentModel = existingConfig
1786
1946
  ? getProviderModel(existingConfig, existingConfig.llm.defaultProvider)
1787
1947
  : undefined;
1788
- // Step 1: provider (or use --provider flag)
1789
- console.log(`${DIM}Fetching latest model pricing...${RESET}`);
1948
+ // Step 1: provider (or use --provider flag). v5.9.3: animate the
1949
+ // OpenRouter pricing fetch under a Spinner so the user gets feedback
1950
+ // on what would otherwise feel like a hang.
1951
+ const pricingSpin = Spinner("fetching latest pricing from openrouter…");
1952
+ const fetchStart = Date.now();
1790
1953
  const dynamicModels = await fetchDynamicModels();
1954
+ const fetchMs = Date.now() - fetchStart;
1955
+ const modelCount = Object.values(dynamicModels).reduce((n, tiers) => n + tiers.length, 0);
1791
1956
  if (Object.keys(dynamicModels).length > 0) {
1792
- console.log(`${DIM}${CHECK} Live pricing loaded from OpenRouter${RESET}`);
1957
+ pricingSpin.ok(`pricing loaded · ${modelCount} models cached`, `${fetchMs} ms`);
1958
+ }
1959
+ else {
1960
+ // No-op fallback (cache miss + network fail) — keep the hardcoded
1961
+ // tiers but signal that we're running offline.
1962
+ pricingSpin.fail("pricing fetch failed", "using bundled tiers");
1793
1963
  }
1794
1964
  console.log();
1795
1965
  let provider;
1796
1966
  if (opts.provider) {
1797
1967
  if (!PROVIDER_ORDER.includes(opts.provider)) {
1798
- console.log(`${CROSS} Unknown provider: ${opts.provider}`);
1799
- console.log(` Valid: ${PROVIDER_ORDER.join(", ")}`);
1968
+ printStatus("fail", `unknown provider \`${opts.provider}\``, `valid: ${PROVIDER_ORDER.join(", ")}`);
1800
1969
  return;
1801
1970
  }
1802
1971
  provider = opts.provider;
1803
- console.log(`Provider: ${GREEN}${provider}${RESET}`);
1972
+ printStatus("ok", "provider", provider);
1804
1973
  }
1805
1974
  else {
1806
1975
  provider = await pickProvider(rl, dynamicModels, "Choose your LLM provider", currentProvider);
@@ -1809,7 +1978,7 @@ export async function runModelsSetup(opts = {}) {
1809
1978
  let model;
1810
1979
  if (opts.model) {
1811
1980
  model = opts.model;
1812
- console.log(`Model: ${GREEN}${model}${RESET}`);
1981
+ printStatus("ok", "model", model);
1813
1982
  }
1814
1983
  else {
1815
1984
  const tiers = dynamicModels[provider] ?? PROVIDER_TIERS[provider];
@@ -1822,7 +1991,7 @@ export async function runModelsSetup(opts = {}) {
1822
1991
  }
1823
1992
  }
1824
1993
  if (!model) {
1825
- console.log(`${CROSS} No model selected. Aborting.`);
1994
+ printStatus("fail", "no model selected · aborting");
1826
1995
  return;
1827
1996
  }
1828
1997
  // Step 3: load API key from existing storage (if available)
@@ -1849,42 +2018,31 @@ export async function runModelsSetup(opts = {}) {
1849
2018
  console.log(`${WARN} No API key found for ${provider}. Run 'gnosys setup' to configure one.`);
1850
2019
  // Continue anyway — user might just want to update the model in config
1851
2020
  }
1852
- // Step 4: validate (default: true)
2021
+ // Step 4: validate (default: true) — v5.9.3 Screen 3: animated
2022
+ // Spinner with latency reported on success.
1853
2023
  const shouldValidate = opts.validate !== false;
1854
2024
  const isLocalProvider = provider === "ollama" || provider === "lmstudio";
1855
2025
  if (shouldValidate && (apiKey || isLocalProvider)) {
1856
2026
  console.log();
1857
- console.log(`${DIM}Testing ${provider}/${model}...${RESET}`);
2027
+ const validateSpin = Spinner(`validating ${provider} / ${model}…`);
1858
2028
  const customBaseUrl = provider === "custom"
1859
2029
  ? process.env.GNOSYS_LLM_BASE_URL
1860
2030
  : undefined;
1861
2031
  const result = await validateModel(provider, model, apiKey, { customBaseUrl });
1862
2032
  if (result.ok) {
1863
- console.log(` ${CHECK} Model validated (${result.latencyMs}ms)`);
2033
+ validateSpin.ok("model validated", `${result.latencyMs} ms · ${provider} / ${model}`);
1864
2034
  }
1865
2035
  else {
1866
- console.log(` ${WARN} Model test failed: ${result.error}`);
1867
- const proceed = await askYesNo(rl, " Save config anyway?", false);
2036
+ validateSpin.fail("model test failed", result.error);
2037
+ const proceed = await askYesNo(rl, "Save config anyway?", false);
1868
2038
  if (!proceed) {
1869
- console.log(` ${DIM}Cancelled.${RESET}`);
2039
+ printStatus("warn", "cancelled · no changes written");
1870
2040
  return;
1871
2041
  }
1872
2042
  }
1873
2043
  }
1874
- // Step 5: write config
1875
- const projectStore = path.join(projectDir, ".gnosys");
1876
- const globalStore = getGnosysHome();
1877
- let storePath;
1878
- if (fsSync.existsSync(path.join(projectStore, "gnosys.json"))) {
1879
- storePath = projectStore;
1880
- }
1881
- else if (fsSync.existsSync(path.join(globalStore, "gnosys.json"))) {
1882
- storePath = globalStore;
1883
- }
1884
- else {
1885
- await fs.mkdir(globalStore, { recursive: true });
1886
- storePath = globalStore;
1887
- }
2044
+ // Step 5: write config (v5.9.4 Bug 10 — unified store resolution).
2045
+ const storePath = ensureActiveStorePath(projectDir);
1888
2046
  const existingLlm = existingConfig?.llm;
1889
2047
  const existingProviderConfig = existingLlm
1890
2048
  ? existingLlm[provider]
@@ -1902,10 +2060,12 @@ export async function runModelsSetup(opts = {}) {
1902
2060
  },
1903
2061
  },
1904
2062
  });
2063
+ // v5.9.3 Screen 3 — Diff() before the saved confirmation. Shows what
2064
+ // landed in gnosys.json.
2065
+ const { buildModelsDiffRows } = await import("./setup/modelsRender.js");
1905
2066
  console.log();
1906
- console.log(` ${CHECK} Config saved: ${storePath}/gnosys.json`);
1907
- console.log(` ${DIM}Provider: ${provider}${RESET}`);
1908
- console.log(` ${DIM}Model: ${model}${RESET}`);
2067
+ printDiff(buildModelsDiffRows(currentProvider, currentModel, provider, model));
2068
+ printStatus("ok", `saved · ${storePath}/gnosys.json`);
1909
2069
  }
1910
2070
  finally {
1911
2071
  if (ownsRl)
@@ -1960,11 +2120,8 @@ export async function runModelsCommand(opts = {}) {
1960
2120
  console.log(`${WARN} No provider configured. Run 'gnosys setup' first.`);
1961
2121
  return;
1962
2122
  }
1963
- const projectStore = path.join(projectDir, ".gnosys");
1964
- const globalStore = getGnosysHome();
1965
- const storePath = fsSync.existsSync(path.join(projectStore, "gnosys.json"))
1966
- ? projectStore
1967
- : globalStore;
2123
+ // v5.9.4 Bug 10 — unified store resolution.
2124
+ const storePath = ensureActiveStorePath(projectDir);
1968
2125
  const existingProviderConfig = existingConfig?.llm?.[currentProvider];
1969
2126
  const providerConfigBase = (typeof existingProviderConfig === "object" && existingProviderConfig !== null)
1970
2127
  ? existingProviderConfig
@@ -2009,81 +2166,105 @@ export async function runDreamSetup(opts = {}) {
2009
2166
  const ownsRl = !opts.rl;
2010
2167
  const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
2011
2168
  try {
2012
- console.log();
2013
- console.log(`${BOLD}${CYAN}Gnosys${RESET} ${DIM}— Dream Mode Setup${RESET}`);
2014
- console.log();
2169
+ // v5.9.3 Screen 7 — three grouped sub-screens (7.0 enable, 7.1
2170
+ // machine+model, 7.2 thresholds+sub-tasks). Each sub-screen renders
2171
+ // its own Header with `step N of 3` so the progress is always visible.
2172
+ const { Header } = await import("./setup/ui/header.js");
2173
+ const { Title } = await import("./setup/ui/title.js");
2174
+ const { Spinner } = await import("./setup/ui/spinner.js");
2175
+ const { printDiff } = await import("./setup/ui/diff.js");
2176
+ const { printStatus } = await import("./setup/ui/status.js");
2015
2177
  const existingConfig = await loadExistingConfig(projectDir);
2016
2178
  const existingDream = existingConfig?.dream;
2017
2179
  // Show current state via central DB
2018
2180
  const { GnosysDB } = await import("./db.js");
2181
+ const { getMachineId } = await import("./remote.js");
2019
2182
  const localDb = GnosysDB.openLocal();
2020
- const designatedMachine = localDb.getDreamMachineId();
2021
- const localMachine = (() => {
2022
- let id = localDb.getMeta("machine_id");
2023
- if (!id) {
2024
- const hostname = process.env.HOSTNAME || process.env.COMPUTERNAME || "unknown";
2025
- id = `${hostname}-${Date.now().toString(36)}`;
2026
- localDb.setMeta("machine_id", id);
2183
+ // v5.9.4 Bugs 7+8 — also peek at the remote DB (if configured) so re-entry
2184
+ // sees a designation made on a different machine. Open remote read-only;
2185
+ // we'll mirror writes below.
2186
+ const remoteDb = await openRemoteDbIfConfigured(localDb);
2187
+ const designatedMachine = localDb.getDreamMachineId() ?? remoteDb?.getDreamMachineId() ?? null;
2188
+ // v5.9.4 Bug 9 — share the canonical machine-id resolver (os.hostname()
2189
+ // fallback included) instead of re-rolling HOSTNAME/COMPUTERNAME logic.
2190
+ const localMachine = getMachineId(localDb);
2191
+ // Mirror dream_machine_id writes to both DBs (Bug 8).
2192
+ const setDreamMachineEverywhere = (id) => {
2193
+ localDb.setDreamMachineId(id);
2194
+ try {
2195
+ remoteDb?.setDreamMachineId(id);
2027
2196
  }
2028
- return id;
2029
- })();
2030
- const recentRuns = localDb.getRecentDreamRuns(1);
2031
- const lastRun = recentRuns[0];
2032
- console.log(`${DIM}Current state${RESET}`);
2033
- console.log(` Enabled: ${existingDream?.enabled ? GREEN + "yes" + RESET : DIM + "no" + RESET}`);
2034
- console.log(` Designated: ${designatedMachine ? designatedMachine + (designatedMachine === localMachine ? ` ${GREEN}(this machine)${RESET}` : ` ${DIM}(other machine)${RESET}`) : DIM + "none" + RESET}`);
2035
- console.log(` This machine ID: ${DIM}${localMachine}${RESET}`);
2036
- if (existingDream) {
2037
- console.log(` Provider: ${existingDream.provider}${existingDream.model ? "/" + existingDream.model : ""}`);
2038
- console.log(` Idle threshold: ${existingDream.idleMinutes} min`);
2039
- console.log(` Max runtime: ${existingDream.maxRuntimeMinutes} min`);
2040
- }
2041
- if (lastRun) {
2042
- console.log(` Last run: ${DIM}${lastRun.completed}${RESET}`);
2043
- }
2044
- else {
2045
- console.log(` Last run: ${DIM}never${RESET}`);
2046
- }
2197
+ catch { /* remote may be transiently unavailable */ }
2198
+ };
2199
+ const clearDreamMachineEverywhere = () => {
2200
+ localDb.clearDreamMachineId();
2201
+ try {
2202
+ remoteDb?.clearDreamMachineId();
2203
+ }
2204
+ catch { /* remote may be transiently unavailable */ }
2205
+ };
2206
+ // ─── 7.0 Overview & enable ────────────────────────────────────────
2207
+ console.log();
2208
+ console.log(Header(["gnosys", "setup", "dream"], { version: "step 1 of 3" }));
2209
+ console.log();
2210
+ console.log(Title("Dream Mode", "gnosys runs background consolidation while you're idle — merging related memories, generating summaries, surfacing relationships."));
2211
+ console.log();
2212
+ const currentStateLine = existingDream?.enabled
2213
+ ? `enabled · ${designatedMachine ? designatedMachine === localMachine ? `${localMachine} (this machine)` : designatedMachine : "no designated machine"}`
2214
+ : "disabled · no designated machine";
2215
+ printStatus("progress", "current", currentStateLine);
2047
2216
  console.log();
2048
- // Step 1: Enable
2049
- const enabled = await askYesNo(rl, "Enable dream mode?", existingDream?.enabled ?? true);
2217
+ const enabled = await askYesNo(rl, "enable Dream Mode?", existingDream?.enabled ?? true);
2050
2218
  if (!enabled) {
2051
2219
  // Persist disabled state and clear designation
2052
- const storePath = pickStorePath(projectDir);
2220
+ const storePath = ensureActiveStorePath(projectDir);
2053
2221
  await updateConfig(storePath, {
2054
2222
  dream: { ...(existingDream ?? {}), enabled: false },
2055
2223
  });
2056
- localDb.clearDreamMachineId();
2057
- console.log(`${CHECK} Dream mode disabled. Designation cleared.`);
2224
+ clearDreamMachineEverywhere();
2225
+ console.log();
2226
+ printStatus("ok", "dream mode disabled · designation cleared");
2058
2227
  localDb.close();
2228
+ remoteDb?.close();
2059
2229
  return;
2060
2230
  }
2061
- // Step 2: Designate this machine
2231
+ // ─── 7.1 Designated machine + model ───────────────────────────────
2232
+ console.log();
2233
+ console.log(Header(["gnosys", "setup", "dream", "machine"], { version: "step 2 of 3" }));
2234
+ console.log();
2235
+ console.log(Title("Only one machine dreams at a time — we'll designate one now."));
2236
+ console.log();
2237
+ printStatus("progress", "this machine", localMachine);
2238
+ console.log();
2062
2239
  const designate = await askYesNo(rl, designatedMachine === localMachine
2063
- ? "This machine is currently the dream node. Keep it that way?"
2064
- : `Designate THIS machine (${localMachine}) to run dream cycles?`, true);
2240
+ ? "this machine is currently the dreamer keep it?"
2241
+ : `designate THIS machine (${localMachine}) as the dreamer?`, true);
2065
2242
  if (designate) {
2066
- localDb.setDreamMachineId(localMachine);
2067
- console.log(` ${CHECK} ${localMachine} is now the dream node.`);
2243
+ setDreamMachineEverywhere(localMachine);
2244
+ printStatus("ok", `${localMachine} is the dreamer`);
2068
2245
  }
2069
2246
  else if (designatedMachine === localMachine) {
2070
- localDb.clearDreamMachineId();
2071
- console.log(` ${WARN} Designation cleared. No machine will dream until you re-run this on another machine.`);
2247
+ clearDreamMachineEverywhere();
2248
+ printStatus("warn", "designation cleared", "no machine will dream until you re-run on another");
2072
2249
  }
2073
2250
  else {
2074
- console.log(` ${DIM}Keeping current designation: ${designatedMachine || "none"}${RESET}`);
2251
+ printStatus("progress", "keeping current designation", designatedMachine || "none");
2075
2252
  }
2076
- // Step 3: Provider
2253
+ // Pricing fetch — animated Spinner.
2077
2254
  console.log();
2078
- console.log(`${DIM}Fetching latest model pricing...${RESET}`);
2255
+ const pricingSpin = Spinner("fetching latest pricing from openrouter…");
2256
+ const fetchStart = Date.now();
2079
2257
  const dynamicModels = await fetchDynamicModels();
2258
+ const fetchMs = Date.now() - fetchStart;
2080
2259
  if (Object.keys(dynamicModels).length > 0) {
2081
- console.log(`${DIM}${CHECK} Live pricing loaded from OpenRouter${RESET}`);
2260
+ pricingSpin.ok("pricing loaded", `${fetchMs} ms`);
2261
+ }
2262
+ else {
2263
+ pricingSpin.fail("pricing fetch failed", "using bundled tiers");
2082
2264
  }
2083
2265
  console.log();
2084
2266
  const defaultProvider = existingDream?.provider || existingConfig?.llm.defaultProvider || "ollama";
2085
- const dreamProvider = await pickProvider(rl, dynamicModels, `${BOLD}Step 3${RESET} ${DIM}—${RESET} Choose dream LLM provider`, defaultProvider);
2086
- // Step 4: Model
2267
+ const dreamProvider = await pickProvider(rl, dynamicModels, "Choose dream LLM provider — local is recommended (dream runs a lot)", defaultProvider);
2087
2268
  let dreamModel = "";
2088
2269
  if (dreamProvider === "custom" || dreamProvider === "skip") {
2089
2270
  dreamModel = await askInput(rl, "Enter model name");
@@ -2094,63 +2275,83 @@ export async function runDreamSetup(opts = {}) {
2094
2275
  dreamModel = await askInput(rl, "Enter model name");
2095
2276
  }
2096
2277
  else {
2097
- dreamModel = await pickModel(rl, dreamProvider, dynamicModels, `${BOLD}Step 4${RESET} ${DIM}—${RESET} Choose dream model`, existingDream?.model);
2278
+ dreamModel = await pickModel(rl, dreamProvider, dynamicModels, "Choose dream model", existingDream?.model);
2098
2279
  }
2099
2280
  }
2100
- // Step 5: Validate (Layer 1 alert)
2281
+ // Validate animated Spinner with the model latency reported.
2101
2282
  if (dreamProvider !== "skip") {
2102
2283
  const apiKey = await getApiKeyForProvider(dreamProvider);
2103
- if (apiKey || dreamProvider === "ollama" || dreamProvider === "lmstudio") {
2284
+ const isLocalProvider = dreamProvider === "ollama" || dreamProvider === "lmstudio";
2285
+ if (apiKey || isLocalProvider) {
2104
2286
  console.log();
2105
- console.log(`${DIM}Testing ${dreamProvider}/${dreamModel}...${RESET}`);
2287
+ const validateSpin = Spinner(`validating ${dreamProvider} / ${dreamModel}…`);
2106
2288
  try {
2107
2289
  const customBaseUrl = dreamProvider === "custom" ? process.env.GNOSYS_LLM_BASE_URL : undefined;
2108
2290
  const result = await validateModel(dreamProvider, dreamModel, apiKey, { customBaseUrl });
2109
2291
  if (result.ok) {
2110
- console.log(` ${CHECK} Validated (${result.latencyMs}ms)`);
2292
+ validateSpin.ok("model validated", `${result.latencyMs} ms · ${dreamProvider} / ${dreamModel}`);
2111
2293
  }
2112
2294
  else {
2113
- console.log(` ${WARN} Could not reach ${dreamProvider}/${dreamModel}: ${result.error}`);
2114
- console.log(` ${DIM}Dream is configured but won't run useful work until this is fixed.${RESET}`);
2115
- const proceed = await askYesNo(rl, " Continue saving config anyway?", true);
2295
+ validateSpin.fail("could not reach model", result.error);
2296
+ const proceed = await askYesNo(rl, "save config anyway?", true);
2116
2297
  if (!proceed) {
2117
- console.log(` ${DIM}Setup cancelled. No changes saved.${RESET}`);
2298
+ printStatus("warn", "setup cancelled · no changes written");
2118
2299
  localDb.close();
2300
+ remoteDb?.close();
2119
2301
  return;
2120
2302
  }
2121
2303
  }
2122
2304
  }
2123
2305
  catch (err) {
2124
- console.log(` ${DIM}Validation skipped: ${err instanceof Error ? err.message : err}${RESET}`);
2306
+ printStatus("warn", "validation skipped", err instanceof Error ? err.message : String(err));
2125
2307
  }
2126
2308
  }
2127
2309
  else {
2128
- console.log(` ${WARN} No API key configured for ${dreamProvider}. Set one via 'gnosys setup models'.`);
2310
+ printStatus("warn", `no API key for ${dreamProvider}`, "set one via `gnosys setup models`");
2129
2311
  }
2130
2312
  }
2131
- // Step 6-8: schedule
2313
+ // ─── 7.2 Thresholds + sub-tasks ───────────────────────────────────
2132
2314
  console.log();
2133
- console.log(`${BOLD}Step 6${RESET} ${DIM}—${RESET} Schedule`);
2134
- const idleAns = await askInput(rl, `Idle minutes before triggering`, {
2135
- default: String(existingDream?.idleMinutes ?? 10),
2136
- });
2137
- const idleMinutes = Math.max(1, parseInt(idleAns) || 10);
2138
- const runtimeAns = await askInput(rl, `Max runtime minutes`, {
2139
- default: String(existingDream?.maxRuntimeMinutes ?? 30),
2140
- });
2141
- const maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns) || 30);
2142
- const minMemAns = await askInput(rl, `Minimum memories before activating`, {
2143
- default: String(existingDream?.minMemories ?? 10),
2144
- });
2145
- const minMemories = Math.max(1, parseInt(minMemAns) || 10);
2146
- // Step 9: sub-tasks
2315
+ console.log(Header(["gnosys", "setup", "dream", "when & what"], { version: "step 3 of 3" }));
2147
2316
  console.log();
2148
- console.log(`${BOLD}Step 9${RESET} ${DIM}—${RESET} Dream sub-tasks`);
2149
- const selfCritique = await askYesNo(rl, "Self-critique (rule + LLM-based review flagging)", existingDream?.selfCritique ?? true);
2150
- const generateSummaries = await askYesNo(rl, "Generate summaries (LLM)", existingDream?.generateSummaries ?? true);
2151
- const discoverRelationships = await askYesNo(rl, "Discover relationships (LLM)", existingDream?.discoverRelationships ?? true);
2317
+ console.log(Title("Thresholds & sub-tasks", "press enter to accept defaults, or `e` to edit"));
2318
+ console.log();
2319
+ // Defaults pulled from the existing config (or the global defaults).
2320
+ const dIdle = existingDream?.idleMinutes ?? 10;
2321
+ const dRuntime = existingDream?.maxRuntimeMinutes ?? 30;
2322
+ const dMinMem = existingDream?.minMemories ?? 10;
2323
+ const dSelfCritique = existingDream?.selfCritique ?? true;
2324
+ const dGenSummaries = existingDream?.generateSummaries ?? true;
2325
+ const dDiscover = existingDream?.discoverRelationships ?? true;
2326
+ const { renderThresholdsBlock } = await import("./setup/dreamRender.js");
2327
+ for (const line of renderThresholdsBlock(dIdle, dRuntime, dMinMem, {
2328
+ selfCritique: dSelfCritique,
2329
+ generateSummaries: dGenSummaries,
2330
+ discoverRelationships: dDiscover,
2331
+ })) {
2332
+ console.log(line);
2333
+ }
2334
+ console.log();
2335
+ const editChoice = (await askInput(rl, "press enter to accept defaults, or e to edit")).trim().toLowerCase();
2336
+ let idleMinutes = dIdle;
2337
+ let maxRuntimeMinutes = dRuntime;
2338
+ let minMemories = dMinMem;
2339
+ let selfCritique = dSelfCritique;
2340
+ let generateSummaries = dGenSummaries;
2341
+ let discoverRelationships = dDiscover;
2342
+ if (editChoice === "e") {
2343
+ const idleAns = await askInput(rl, "idle minutes before triggering", { default: String(dIdle) });
2344
+ idleMinutes = Math.max(1, parseInt(idleAns) || dIdle);
2345
+ const runtimeAns = await askInput(rl, "max runtime minutes", { default: String(dRuntime) });
2346
+ maxRuntimeMinutes = Math.max(1, parseInt(runtimeAns) || dRuntime);
2347
+ const minMemAns = await askInput(rl, "minimum memories before activating", { default: String(dMinMem) });
2348
+ minMemories = Math.max(1, parseInt(minMemAns) || dMinMem);
2349
+ selfCritique = await askYesNo(rl, "self-critique (rule + LLM-based review flagging)", dSelfCritique);
2350
+ generateSummaries = await askYesNo(rl, "generate summaries (LLM)", dGenSummaries);
2351
+ discoverRelationships = await askYesNo(rl, "discover relationships (LLM)", dDiscover);
2352
+ }
2152
2353
  // Save
2153
- const storePath = pickStorePath(projectDir);
2354
+ const storePath = ensureActiveStorePath(projectDir);
2154
2355
  await updateConfig(storePath, {
2155
2356
  dream: {
2156
2357
  enabled: true,
@@ -2168,159 +2369,90 @@ export async function runDreamSetup(opts = {}) {
2168
2369
  // doesn't fire immediately based on stale history.
2169
2370
  localDb.resetDreamConsecutiveFailures();
2170
2371
  localDb.close();
2372
+ remoteDb?.close();
2373
+ // Final Diff block per the design — provider/machine + the two
2374
+ // threshold fields most users actually care about.
2171
2375
  console.log();
2172
- console.log(` ${CHECK} Dream config saved: ${storePath}/gnosys.json`);
2173
- console.log(` ${DIM}Provider: ${dreamProvider}${dreamModel ? "/" + dreamModel : ""}${RESET}`);
2174
- console.log(` ${DIM}Schedule: idle ${idleMinutes}m / max ${maxRuntimeMinutes}m / min ${minMemories} memories${RESET}`);
2175
- console.log(` ${DIM}Designated: ${designate ? localMachine + " (this machine)" : (designatedMachine || "none")}${RESET}`);
2376
+ printStatus("ok", "dream mode enabled");
2176
2377
  console.log();
2177
- console.log(`${DIM}Tip: 'gnosys dream log' shows recent runs once dream starts cycling.${RESET}`);
2378
+ const { buildDreamDiffRows } = await import("./setup/dreamRender.js");
2379
+ printDiff(buildDreamDiffRows(existingDream
2380
+ ? {
2381
+ provider: existingDream.provider,
2382
+ model: existingDream.model,
2383
+ machine: designatedMachine ?? "—",
2384
+ idleMinutes: existingDream.idleMinutes,
2385
+ maxRuntimeMinutes: existingDream.maxRuntimeMinutes,
2386
+ }
2387
+ : null, {
2388
+ provider: dreamProvider,
2389
+ model: dreamModel || undefined,
2390
+ machine: designate ? localMachine : (designatedMachine ?? "none"),
2391
+ idleMinutes,
2392
+ maxRuntimeMinutes,
2393
+ selfCritique,
2394
+ generateSummaries,
2395
+ discoverRelationships,
2396
+ }));
2397
+ const dreamerName = designate ? localMachine : (designatedMachine ?? "the designated machine");
2398
+ printStatus("progress", `first cycle runs after ${dreamerName} is idle for ${idleMinutes} min`);
2399
+ printStatus("progress", "check status anytime with", "gnosys dashboard");
2178
2400
  }
2179
2401
  finally {
2180
2402
  if (ownsRl)
2181
2403
  rl.close();
2182
2404
  }
2183
2405
  }
2406
+ /**
2407
+ * v5.9.3 (deci-050): `gnosys setup chat` is deprecated. Chat config
2408
+ * moves into the chat TUI's own settings panel in v6.0 (road-014). This
2409
+ * stub renders a deprecation notice and exits. The function signature
2410
+ * is preserved so existing callers (cli.ts subcommand registration)
2411
+ * continue to compile.
2412
+ */
2184
2413
  export async function runChatSetup(opts = {}) {
2185
- const projectDir = opts.directory ? path.resolve(opts.directory) : process.cwd();
2186
- const ownsRl = !opts.rl;
2187
- const rl = opts.rl ?? createInterface({ input: stdin, output: stdout });
2188
- try {
2189
- console.log();
2190
- console.log(`${BOLD}${CYAN}Gnosys${RESET} ${DIM} Chat Setup${RESET}`);
2191
- console.log();
2192
- const existingConfig = await loadExistingConfig(projectDir);
2193
- const existingChat = existingConfig?.chat;
2194
- const existingRecall = existingConfig?.recall;
2195
- const existingChatTask = existingConfig?.taskModels?.chat;
2196
- const defaultLLMProvider = existingConfig?.llm.defaultProvider || "anthropic";
2197
- // ── Current state ────────────────────────────────────────────────
2198
- console.log(`${DIM}Current state${RESET}`);
2199
- const currentProvider = existingChatTask?.provider || defaultLLMProvider;
2200
- const currentModel = existingChatTask?.model || "(default for provider)";
2201
- console.log(` Provider: ${currentProvider}${existingChatTask ? "" : ` ${DIM}(inherited from default)${RESET}`}`);
2202
- console.log(` Model: ${currentModel}`);
2203
- console.log(` Recall mode: ${existingRecall?.aggressive ? "aggressive" : "filtered"} (max ${existingRecall?.maxMemories ?? 8}, threshold ${existingRecall?.minRelevance ?? 0.4})`);
2204
- console.log(` Tools fence: ${existingChat?.toolsEnabled !== false ? GREEN + "on" + RESET : DIM + "off" + RESET}`);
2205
- console.log(` Auto-summarize: ${existingChat?.autoSummarizeAfterTurns && existingChat.autoSummarizeAfterTurns > 0 ? `after ${existingChat.autoSummarizeAfterTurns} turns` : DIM + "off" + RESET}`);
2206
- console.log(` System prefix: ${existingChat?.systemPromptPrefix ? `${DIM}"${existingChat.systemPromptPrefix.slice(0, 50)}${existingChat.systemPromptPrefix.length > 50 ? "…" : ""}"${RESET}` : DIM + "none" + RESET}`);
2207
- console.log();
2208
- // ── Step 1: provider/model override ──────────────────────────────
2209
- const overrideProvider = await askYesNo(rl, existingChatTask
2210
- ? `Currently routing chat to ${currentProvider}. Change it?`
2211
- : `Chat currently uses the default provider (${defaultLLMProvider}). Route chat to a different LLM?`, false);
2212
- let chatProvider;
2213
- let chatModel;
2214
- if (overrideProvider) {
2215
- console.log();
2216
- console.log(`${DIM}Fetching latest model pricing...${RESET}`);
2217
- const dynamicModels = await fetchDynamicModels();
2218
- console.log();
2219
- chatProvider = await pickProvider(rl, dynamicModels, `${BOLD}Step 1${RESET} ${DIM}—${RESET} Choose chat LLM provider`, currentProvider);
2220
- if (chatProvider === "custom" || chatProvider === "skip") {
2221
- chatModel = await askInput(rl, "Enter model name");
2222
- }
2223
- else {
2224
- const tiers = dynamicModels[chatProvider] ?? PROVIDER_TIERS[chatProvider] ?? [];
2225
- if (tiers.length === 0) {
2226
- chatModel = await askInput(rl, "Enter model name");
2227
- }
2228
- else {
2229
- chatModel = await pickModel(rl, chatProvider, dynamicModels, `${BOLD}Step 2${RESET} ${DIM}—${RESET} Choose chat model`, existingChatTask?.model);
2230
- }
2231
- }
2232
- // Validate
2233
- const apiKey = await getApiKeyForProvider(chatProvider);
2234
- if (apiKey || chatProvider === "ollama" || chatProvider === "lmstudio") {
2235
- console.log();
2236
- console.log(`${DIM}Testing ${chatProvider}/${chatModel}...${RESET}`);
2237
- try {
2238
- const customBaseUrl = chatProvider === "custom" ? process.env.GNOSYS_LLM_BASE_URL : undefined;
2239
- const result = await validateModel(chatProvider, chatModel, apiKey, { customBaseUrl });
2240
- if (result.ok) {
2241
- console.log(` ${CHECK} Validated (${result.latencyMs}ms)`);
2242
- }
2243
- else {
2244
- console.log(` ${WARN} Could not reach ${chatProvider}/${chatModel}: ${result.error}`);
2245
- const proceed = await askYesNo(rl, " Continue saving config anyway?", true);
2246
- if (!proceed) {
2247
- console.log(` ${DIM}Setup cancelled. No changes saved.${RESET}`);
2248
- return;
2249
- }
2250
- }
2251
- }
2252
- catch (err) {
2253
- console.log(` ${DIM}Validation skipped: ${err instanceof Error ? err.message : err}${RESET}`);
2254
- }
2255
- }
2256
- else {
2257
- console.log(` ${WARN} No API key configured for ${chatProvider}. Set one via 'gnosys setup models'.`);
2258
- }
2259
- }
2260
- // ── Step 2: recall tuning ─────────────────────────────────────────
2261
- console.log();
2262
- console.log(`${BOLD}Recall${RESET} ${DIM}—${RESET} how aggressively to inject memories into chat`);
2263
- const recallAggressive = await askYesNo(rl, "Aggressive (more memories, lower threshold) vs filtered (fewer, higher quality)?", existingRecall?.aggressive ?? true);
2264
- const maxMemAns = await askInput(rl, "Max memories per chat turn", {
2265
- default: String(existingRecall?.maxMemories ?? 8),
2266
- });
2267
- const maxMemories = Math.max(1, Math.min(20, parseInt(maxMemAns) || 8));
2268
- const thresholdAns = await askInput(rl, "Min relevance score (0.0-1.0, lower returns more)", {
2269
- default: String(existingRecall?.minRelevance ?? 0.4),
2270
- });
2271
- const minRelevance = Math.max(0, Math.min(1, parseFloat(thresholdAns) || 0.4));
2272
- // ── Step 3: chat-only knobs ───────────────────────────────────────
2273
- console.log();
2274
- console.log(`${BOLD}Chat options${RESET}`);
2275
- const toolsEnabled = await askYesNo(rl, "Allow LLM to call gnosys tools via gnosys-tool fences?", existingChat?.toolsEnabled ?? true);
2276
- const autoSumAns = await askInput(rl, "Auto-summarize nudge after N turns (0 to disable)", { default: String(existingChat?.autoSummarizeAfterTurns ?? 0) });
2277
- const autoSummarizeAfterTurns = Math.max(0, parseInt(autoSumAns) || 0);
2278
- const prefixDefault = existingChat?.systemPromptPrefix ?? "";
2279
- const systemPromptPrefix = await askInput(rl, `System prompt prefix (persona/style/domain — Enter for ${prefixDefault ? "current" : "none"})`, { default: prefixDefault });
2280
- // ── Save ──────────────────────────────────────────────────────────
2281
- const storePath = pickStorePath(projectDir);
2282
- const updates = {
2283
- recall: { aggressive: recallAggressive, maxMemories, minRelevance },
2284
- chat: { toolsEnabled, autoSummarizeAfterTurns, systemPromptPrefix },
2285
- };
2286
- if (overrideProvider && chatProvider && chatProvider !== "skip" && chatModel) {
2287
- const existingTaskModels = existingConfig?.taskModels || {};
2288
- updates.taskModels = {
2289
- ...existingTaskModels,
2290
- chat: { provider: chatProvider, model: chatModel },
2291
- };
2292
- }
2293
- await updateConfig(storePath, updates);
2294
- console.log();
2295
- console.log(` ${CHECK} Chat config saved: ${storePath}/gnosys.json`);
2296
- if (overrideProvider && chatProvider && chatModel) {
2297
- console.log(` ${DIM}Provider: ${chatProvider}/${chatModel}${RESET}`);
2298
- }
2299
- else {
2300
- console.log(` ${DIM}Provider: (inherits default → ${defaultLLMProvider})${RESET}`);
2301
- }
2302
- console.log(` ${DIM}Recall: ${recallAggressive ? "aggressive" : "filtered"} (max ${maxMemories}, threshold ${minRelevance})${RESET}`);
2303
- console.log(` ${DIM}Tools: ${toolsEnabled ? "on" : "off"}${RESET}`);
2304
- console.log(` ${DIM}Auto-summarize: ${autoSummarizeAfterTurns > 0 ? `after ${autoSummarizeAfterTurns} turns` : "off"}${RESET}`);
2305
- console.log();
2306
- console.log(`${DIM}Tip: run 'gnosys chat' to start a session.${RESET}`);
2307
- }
2308
- finally {
2309
- if (ownsRl)
2310
- rl.close();
2311
- }
2414
+ void opts; // signature preserved; opts unused in deprecation notice
2415
+ // Use atom-based render so the notice matches the rest of v5.9.3.
2416
+ const { Header } = await import("./setup/ui/header.js");
2417
+ const { Status } = await import("./setup/ui/status.js");
2418
+ const { Footer } = await import("./setup/ui/footer.js");
2419
+ const { c, color } = await import("./setup/ui/tokens.js");
2420
+ const v = `v${getVersion()}`;
2421
+ console.log();
2422
+ console.log(Header(["gnosys", "setup", "chat"], { version: v }));
2423
+ console.log();
2424
+ console.log(Status("warn", "chat settings have moved"));
2425
+ console.log();
2426
+ console.log(` ${color(c.text, "chat is now configured from inside the TUI itself.")}`);
2427
+ console.log(` ${color(c.textDim, "open it with")} ${color(c.text, "gnosys chat")}`);
2428
+ console.log(` ${color(c.textDim, "then press")} ${color(c.text, "⌃, (settings)")}`);
2429
+ console.log();
2430
+ console.log(Footer("v6.0 will retire this command entirely"));
2312
2431
  }
2313
- /** Resolve where to write gnosys.json project store if exists, else global store. */
2314
- function pickStorePath(projectDir) {
2315
- const projectStore = path.join(projectDir, ".gnosys");
2316
- const globalStore = getGnosysHome();
2317
- if (fsSync.existsSync(path.join(projectStore, "gnosys.json"))) {
2318
- return projectStore;
2432
+ // ─── runChatSetup body removed in v5.9.3 (deci-050) ─────────────────
2433
+ // Provider/model override + recall tuning + tools fence + auto-summarize
2434
+ // + system prompt prefix all move to the v6.0 chat TUI's settings panel
2435
+ // (road-014). The exported stub above renders a deprecation notice.
2436
+ /**
2437
+ * Open the remote central DB ONLY when sync is configured AND the share
2438
+ * is reachable. Returns null otherwise. Used by the dream wizard so it
2439
+ * can mirror `dream_machine_id` writes to both DB meta tables (Bug 8).
2440
+ */
2441
+ async function openRemoteDbIfConfigured(localDb) {
2442
+ try {
2443
+ if (!localDb.isAvailable())
2444
+ return null;
2445
+ const remotePath = localDb.getMeta("remote_path");
2446
+ if (!remotePath)
2447
+ return null;
2448
+ if (!fsSync.existsSync(path.join(remotePath, "gnosys.db")))
2449
+ return null;
2450
+ const { GnosysDB } = await import("./db.js");
2451
+ return new GnosysDB(remotePath);
2319
2452
  }
2320
- if (!fsSync.existsSync(globalStore)) {
2321
- fsSync.mkdirSync(globalStore, { recursive: true });
2453
+ catch {
2454
+ return null;
2322
2455
  }
2323
- return globalStore;
2324
2456
  }
2325
2457
  /**
2326
2458
  * Best-effort lookup of the API key for a provider. Used by the dream setup