agent-nuvira 3.3.8 → 3.3.9

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 (257) hide show
  1. package/README.md +57 -50
  2. package/dist/agents/agents/planner.d.ts.map +1 -1
  3. package/dist/agents/agents/planner.js +41 -0
  4. package/dist/agents/agents/planner.js.map +1 -1
  5. package/dist/agents/agents/reasoner.d.ts.map +1 -1
  6. package/dist/agents/agents/reasoner.js +6 -0
  7. package/dist/agents/agents/reasoner.js.map +1 -1
  8. package/dist/agents/agents/runner.d.ts +6 -0
  9. package/dist/agents/agents/runner.d.ts.map +1 -1
  10. package/dist/agents/agents/runner.js +82 -5
  11. package/dist/agents/agents/runner.js.map +1 -1
  12. package/dist/agents/agents/writer-tool-calling.d.ts.map +1 -1
  13. package/dist/agents/agents/writer-tool-calling.js +5 -2
  14. package/dist/agents/agents/writer-tool-calling.js.map +1 -1
  15. package/dist/agents/agents/writer.d.ts.map +1 -1
  16. package/dist/agents/agents/writer.js +8 -4
  17. package/dist/agents/agents/writer.js.map +1 -1
  18. package/dist/agents/fanout-scheduler.d.ts +72 -0
  19. package/dist/agents/fanout-scheduler.d.ts.map +1 -0
  20. package/dist/agents/fanout-scheduler.js +100 -0
  21. package/dist/agents/fanout-scheduler.js.map +1 -0
  22. package/dist/agents/orchestrator.d.ts.map +1 -1
  23. package/dist/agents/orchestrator.js +30 -1
  24. package/dist/agents/orchestrator.js.map +1 -1
  25. package/dist/agents/plan-quality.d.ts +58 -0
  26. package/dist/agents/plan-quality.d.ts.map +1 -0
  27. package/dist/agents/plan-quality.js +152 -0
  28. package/dist/agents/plan-quality.js.map +1 -0
  29. package/dist/agents/prompt-assembly.d.ts +8 -0
  30. package/dist/agents/prompt-assembly.d.ts.map +1 -1
  31. package/dist/agents/prompt-assembly.js +18 -0
  32. package/dist/agents/prompt-assembly.js.map +1 -1
  33. package/dist/agents/step-handoff.d.ts +12 -2
  34. package/dist/agents/step-handoff.d.ts.map +1 -1
  35. package/dist/agents/step-handoff.js +65 -8
  36. package/dist/agents/step-handoff.js.map +1 -1
  37. package/dist/agents/tool-calling-agent.d.ts.map +1 -1
  38. package/dist/agents/tool-calling-agent.js +18 -0
  39. package/dist/agents/tool-calling-agent.js.map +1 -1
  40. package/dist/cli/cache.d.ts.map +1 -1
  41. package/dist/cli/cache.js +34 -4
  42. package/dist/cli/cache.js.map +1 -1
  43. package/dist/cli/chat.d.ts +6 -0
  44. package/dist/cli/chat.d.ts.map +1 -1
  45. package/dist/cli/chat.js +79 -18
  46. package/dist/cli/chat.js.map +1 -1
  47. package/dist/cli/cli-program.d.ts.map +1 -1
  48. package/dist/cli/cli-program.js +6 -0
  49. package/dist/cli/cli-program.js.map +1 -1
  50. package/dist/cli/config.d.ts +10 -0
  51. package/dist/cli/config.d.ts.map +1 -1
  52. package/dist/cli/config.js +179 -1
  53. package/dist/cli/config.js.map +1 -1
  54. package/dist/cli/continuity.d.ts +61 -0
  55. package/dist/cli/continuity.d.ts.map +1 -0
  56. package/dist/cli/continuity.js +112 -0
  57. package/dist/cli/continuity.js.map +1 -0
  58. package/dist/cli/dashboard.d.ts +3 -1
  59. package/dist/cli/dashboard.d.ts.map +1 -1
  60. package/dist/cli/dashboard.js +5 -16
  61. package/dist/cli/dashboard.js.map +1 -1
  62. package/dist/cli/doctor.d.ts.map +1 -1
  63. package/dist/cli/doctor.js +48 -1
  64. package/dist/cli/doctor.js.map +1 -1
  65. package/dist/cli/execute.d.ts +8 -0
  66. package/dist/cli/execute.d.ts.map +1 -1
  67. package/dist/cli/execute.js +34 -1
  68. package/dist/cli/execute.js.map +1 -1
  69. package/dist/cli/loop-executor.d.ts +24 -0
  70. package/dist/cli/loop-executor.d.ts.map +1 -1
  71. package/dist/cli/loop-executor.js +134 -13
  72. package/dist/cli/loop-executor.js.map +1 -1
  73. package/dist/cli/website.d.ts +25 -0
  74. package/dist/cli/website.d.ts.map +1 -0
  75. package/dist/cli/website.js +58 -0
  76. package/dist/cli/website.js.map +1 -0
  77. package/dist/config/manager.d.ts.map +1 -1
  78. package/dist/config/manager.js +35 -0
  79. package/dist/config/manager.js.map +1 -1
  80. package/dist/config/process-env.d.ts.map +1 -1
  81. package/dist/config/process-env.js +20 -0
  82. package/dist/config/process-env.js.map +1 -1
  83. package/dist/config/provider-env.d.ts +2 -1
  84. package/dist/config/provider-env.d.ts.map +1 -1
  85. package/dist/config/provider-env.js +19 -2
  86. package/dist/config/provider-env.js.map +1 -1
  87. package/dist/config/service-catalog.d.ts +81 -0
  88. package/dist/config/service-catalog.d.ts.map +1 -0
  89. package/dist/config/service-catalog.js +286 -0
  90. package/dist/config/service-catalog.js.map +1 -0
  91. package/dist/config/service-probe.d.ts +27 -0
  92. package/dist/config/service-probe.d.ts.map +1 -0
  93. package/dist/config/service-probe.js +104 -0
  94. package/dist/config/service-probe.js.map +1 -0
  95. package/dist/config/types.d.ts +63 -0
  96. package/dist/config/types.d.ts.map +1 -1
  97. package/dist/context/cache.d.ts +46 -2
  98. package/dist/context/cache.d.ts.map +1 -1
  99. package/dist/context/cache.js +85 -7
  100. package/dist/context/cache.js.map +1 -1
  101. package/dist/findings/verdicts.d.ts +59 -0
  102. package/dist/findings/verdicts.d.ts.map +1 -1
  103. package/dist/findings/verdicts.js +117 -0
  104. package/dist/findings/verdicts.js.map +1 -1
  105. package/dist/forwarded.d.ts +2 -0
  106. package/dist/forwarded.d.ts.map +1 -0
  107. package/dist/forwarded.js +2 -0
  108. package/dist/forwarded.js.map +1 -0
  109. package/dist/gateway/registry.d.ts.map +1 -1
  110. package/dist/gateway/registry.js +9 -0
  111. package/dist/gateway/registry.js.map +1 -1
  112. package/dist/learning/auto-router.d.ts +30 -0
  113. package/dist/learning/auto-router.d.ts.map +1 -1
  114. package/dist/learning/auto-router.js +69 -12
  115. package/dist/learning/auto-router.js.map +1 -1
  116. package/dist/learning/context-assembly.d.ts +37 -0
  117. package/dist/learning/context-assembly.d.ts.map +1 -0
  118. package/dist/learning/context-assembly.js +63 -0
  119. package/dist/learning/context-assembly.js.map +1 -0
  120. package/dist/learning/reasoning-trace.d.ts +26 -5
  121. package/dist/learning/reasoning-trace.d.ts.map +1 -1
  122. package/dist/learning/reasoning-trace.js +21 -1
  123. package/dist/learning/reasoning-trace.js.map +1 -1
  124. package/dist/learning/session-digest.d.ts +78 -0
  125. package/dist/learning/session-digest.d.ts.map +1 -0
  126. package/dist/learning/session-digest.js +185 -0
  127. package/dist/learning/session-digest.js.map +1 -0
  128. package/dist/learning/session-recall.d.ts +135 -0
  129. package/dist/learning/session-recall.d.ts.map +1 -0
  130. package/dist/learning/session-recall.js +305 -0
  131. package/dist/learning/session-recall.js.map +1 -0
  132. package/dist/learning/session-store.d.ts +172 -0
  133. package/dist/learning/session-store.d.ts.map +1 -0
  134. package/dist/learning/session-store.js +424 -0
  135. package/dist/learning/session-store.js.map +1 -0
  136. package/dist/learning/step-checkpoint.d.ts +28 -0
  137. package/dist/learning/step-checkpoint.d.ts.map +1 -1
  138. package/dist/learning/step-checkpoint.js +90 -6
  139. package/dist/learning/step-checkpoint.js.map +1 -1
  140. package/dist/learning/working-state.d.ts +19 -0
  141. package/dist/learning/working-state.d.ts.map +1 -1
  142. package/dist/learning/working-state.js +34 -1
  143. package/dist/learning/working-state.js.map +1 -1
  144. package/dist/parity/drivers.d.ts.map +1 -1
  145. package/dist/parity/drivers.js +29 -0
  146. package/dist/parity/drivers.js.map +1 -1
  147. package/dist/parity/wire-fixtures.d.ts +88 -0
  148. package/dist/parity/wire-fixtures.d.ts.map +1 -0
  149. package/dist/parity/wire-fixtures.js +405 -0
  150. package/dist/parity/wire-fixtures.js.map +1 -0
  151. package/dist/tools/artifact-append.d.ts +23 -0
  152. package/dist/tools/artifact-append.d.ts.map +1 -1
  153. package/dist/tools/artifact-append.js +28 -0
  154. package/dist/tools/artifact-append.js.map +1 -1
  155. package/dist/tools/ask-user.d.ts +5 -0
  156. package/dist/tools/ask-user.d.ts.map +1 -1
  157. package/dist/tools/ask-user.js +51 -5
  158. package/dist/tools/ask-user.js.map +1 -1
  159. package/dist/tools/child-agent-entry.js +1 -0
  160. package/dist/tools/child-agent-entry.js.map +1 -1
  161. package/dist/tools/child-agent-runtime.d.ts +7 -0
  162. package/dist/tools/child-agent-runtime.d.ts.map +1 -1
  163. package/dist/tools/child-agent-runtime.js +116 -6
  164. package/dist/tools/child-agent-runtime.js.map +1 -1
  165. package/dist/tools/child-agent-worker.d.ts +15 -0
  166. package/dist/tools/child-agent-worker.d.ts.map +1 -0
  167. package/dist/tools/child-agent-worker.js.map +1 -0
  168. package/dist/tools/coding-tools.d.ts.map +1 -1
  169. package/dist/tools/coding-tools.js +11 -0
  170. package/dist/tools/coding-tools.js.map +1 -1
  171. package/dist/tools/finding-tool.d.ts +6 -1
  172. package/dist/tools/finding-tool.d.ts.map +1 -1
  173. package/dist/tools/finding-tool.js +15 -2
  174. package/dist/tools/finding-tool.js.map +1 -1
  175. package/dist/tools/followup-utils.d.ts +35 -0
  176. package/dist/tools/followup-utils.d.ts.map +1 -1
  177. package/dist/tools/followup-utils.js +36 -0
  178. package/dist/tools/followup-utils.js.map +1 -1
  179. package/dist/tools/loop-project-context.d.ts +4 -1
  180. package/dist/tools/loop-project-context.d.ts.map +1 -1
  181. package/dist/tools/loop-project-context.js +48 -1
  182. package/dist/tools/loop-project-context.js.map +1 -1
  183. package/dist/tools/loop-skill-hint.d.ts.map +1 -1
  184. package/dist/tools/loop-skill-hint.js +7 -0
  185. package/dist/tools/loop-skill-hint.js.map +1 -1
  186. package/dist/tools/modality/image-gen.d.ts +25 -6
  187. package/dist/tools/modality/image-gen.d.ts.map +1 -1
  188. package/dist/tools/modality/image-gen.js +46 -27
  189. package/dist/tools/modality/image-gen.js.map +1 -1
  190. package/dist/tools/modality/image-providers.d.ts +71 -0
  191. package/dist/tools/modality/image-providers.d.ts.map +1 -0
  192. package/dist/tools/modality/image-providers.js +277 -0
  193. package/dist/tools/modality/image-providers.js.map +1 -0
  194. package/dist/tools/registry.d.ts +10 -1
  195. package/dist/tools/registry.d.ts.map +1 -1
  196. package/dist/tools/registry.js +35 -8
  197. package/dist/tools/registry.js.map +1 -1
  198. package/dist/tools/run-cli.d.ts.map +1 -1
  199. package/dist/tools/run-cli.js +39 -1
  200. package/dist/tools/run-cli.js.map +1 -1
  201. package/dist/tools/run-terminal.d.ts.map +1 -1
  202. package/dist/tools/run-terminal.js +50 -6
  203. package/dist/tools/run-terminal.js.map +1 -1
  204. package/dist/tools/subagent-spawner.d.ts +8 -0
  205. package/dist/tools/subagent-spawner.d.ts.map +1 -1
  206. package/dist/tools/subagent-spawner.js +3 -0
  207. package/dist/tools/subagent-spawner.js.map +1 -1
  208. package/dist/tools/tool-loop.d.ts +55 -0
  209. package/dist/tools/tool-loop.d.ts.map +1 -1
  210. package/dist/tools/tool-loop.js +136 -0
  211. package/dist/tools/tool-loop.js.map +1 -1
  212. package/dist/tools/untrusted-content.d.ts +29 -0
  213. package/dist/tools/untrusted-content.d.ts.map +1 -0
  214. package/dist/tools/untrusted-content.js +53 -0
  215. package/dist/tools/untrusted-content.js.map +1 -0
  216. package/dist/tools/web-research.d.ts +76 -14
  217. package/dist/tools/web-research.d.ts.map +1 -1
  218. package/dist/tools/web-research.js +280 -35
  219. package/dist/tools/web-research.js.map +1 -1
  220. package/dist/utils/effect-verification.d.ts +115 -0
  221. package/dist/utils/effect-verification.d.ts.map +1 -0
  222. package/dist/utils/effect-verification.js +337 -0
  223. package/dist/utils/effect-verification.js.map +1 -0
  224. package/dist/utils/open-url.d.ts +16 -0
  225. package/dist/utils/open-url.d.ts.map +1 -0
  226. package/dist/utils/open-url.js +32 -0
  227. package/dist/utils/open-url.js.map +1 -0
  228. package/dist/utils/project-env.d.ts +146 -0
  229. package/dist/utils/project-env.d.ts.map +1 -0
  230. package/dist/utils/project-env.js +584 -0
  231. package/dist/utils/project-env.js.map +1 -0
  232. package/dist/utils/shell.d.ts +7 -0
  233. package/dist/utils/shell.d.ts.map +1 -1
  234. package/dist/utils/shell.js +2 -0
  235. package/dist/utils/shell.js.map +1 -1
  236. package/dist/web-dashboard/chat-console.d.ts +2 -0
  237. package/dist/web-dashboard/chat-console.d.ts.map +1 -1
  238. package/dist/web-dashboard/chat-console.js +7 -0
  239. package/dist/web-dashboard/chat-console.js.map +1 -1
  240. package/dist/web-dashboard/server.d.ts +3 -1
  241. package/dist/web-dashboard/server.d.ts.map +1 -1
  242. package/dist/web-dashboard/server.js +534 -8
  243. package/dist/web-dashboard/server.js.map +1 -1
  244. package/dist/web-dashboard/src/types.d.ts +150 -1
  245. package/dist/web-dashboard/src/types.d.ts.map +1 -1
  246. package/dist/web-dashboard/workspace-guard.d.ts +40 -0
  247. package/dist/web-dashboard/workspace-guard.d.ts.map +1 -0
  248. package/dist/web-dashboard/workspace-guard.js +92 -0
  249. package/dist/web-dashboard/workspace-guard.js.map +1 -0
  250. package/package.json +3 -1
  251. package/src/web-dashboard/public/assets/index-Beportyl.js +207 -0
  252. package/src/web-dashboard/public/assets/index-Beportyl.js.map +1 -0
  253. package/src/web-dashboard/public/assets/index-gtQyg9nm.css +1 -0
  254. package/src/web-dashboard/public/index.html +2 -2
  255. package/src/web-dashboard/public/assets/index-J3sGSCLr.js +0 -207
  256. package/src/web-dashboard/public/assets/index-J3sGSCLr.js.map +0 -1
  257. package/src/web-dashboard/public/assets/index-u7mu-fxK.css +0 -1
@@ -20,6 +20,7 @@ import { registerDagHandlers } from '../observability/dag-bridge.js';
20
20
  import { debugLogDir, debugLoggingEnabled, debugLogsForSession } from '../observability/debug-log.js';
21
21
  import { scrubSecrets } from '../gateway/gateway-log.js';
22
22
  import { resolveBuffConfigDir, resolveBuffConfigPath, resolveNuviraHome } from '../config/paths.js';
23
+ import { createRequire } from 'node:module';
23
24
  import { loadEnv } from '../utils/env.js';
24
25
  import { ConfigManager } from '../config/manager.js';
25
26
  import { runAllChecks } from '../cli/doctor.js';
@@ -57,7 +58,9 @@ import { setSkillEnabled } from '../learning/hub-skill-catalog.js';
57
58
  // hit the mocked readFileSync).
58
59
  import { createConfiguredAdapters } from '../gateway/adapters.js';
59
60
  import { ChannelDirectory, PLATFORM_ENV_VARS, platformConfigVars } from '../gateway/channel-directory.js';
60
- import { applyEnvToProcess, configurablePlatforms, platformConfigStatus, platformEnvVarMeta, redactValue, writeEnvFile, } from '../gateway/platform-config.js';
61
+ import { applyEnvToProcess, configurablePlatforms, envVarState, platformConfigStatus, platformEnvVarMeta, redactValue, writeEnvFile, } from '../gateway/platform-config.js';
62
+ import { SERVICE_CATALOG, getServiceDefinition, } from '../config/service-catalog.js';
63
+ import { probeService } from '../config/service-probe.js';
61
64
  // ─── Constants ──────────────────────────────────────────────────────────────
62
65
  const PORT = parseInt(envBuff('DASHBOARD_PORT') || '3030', 10);
63
66
  const HOST = envBuff('DASHBOARD_HOST') || '127.0.0.1';
@@ -1581,6 +1584,78 @@ function readBenchmarkData() {
1581
1584
  })),
1582
1585
  };
1583
1586
  }
1587
+ /**
1588
+ * The ACTIVE vector-search backend ('json' | 'faiss-ivf' | 'faiss-native').
1589
+ *
1590
+ * `memory.backend` in this payload is the memory PROVIDER tier ('local'; Mem0
1591
+ * is an optional provider) — a different axis from how semantic search is
1592
+ * indexed, which is what the user actually asks about. Resolved the same way
1593
+ * the runtime resolves it: env VECTOR_BACKEND → config memory.vectorBackend →
1594
+ * default 'faiss'. The read MUST be synchronous (readMemoryData builds the
1595
+ * /api payload synchronously), so native detection is a module-presence check;
1596
+ * the CLI's `nuvira memory backend --check` runs the full smoke test.
1597
+ */
1598
+ let vectorBackendNameCache = null;
1599
+ function resolveVectorBackendName() {
1600
+ if (vectorBackendNameCache)
1601
+ return vectorBackendNameCache;
1602
+ let preferred = 'faiss';
1603
+ try {
1604
+ const env = envBuff('VECTOR_BACKEND');
1605
+ if (env === 'json')
1606
+ preferred = 'json';
1607
+ else if (env === 'faiss' || env === 'auto')
1608
+ preferred = 'faiss';
1609
+ else {
1610
+ const cfg = readJSON(resolveBuffConfigPath());
1611
+ if (cfg?.memory?.vectorBackend === 'json')
1612
+ preferred = 'json';
1613
+ }
1614
+ }
1615
+ catch {
1616
+ // fall through to the 'faiss' default
1617
+ }
1618
+ if (preferred === 'json')
1619
+ return (vectorBackendNameCache = 'json');
1620
+ try {
1621
+ const req = createRequire(import.meta.url);
1622
+ const native = req('@faiss-node/native');
1623
+ const mod = native?.default ?? native;
1624
+ if (mod && typeof mod.FaissIndex === 'function')
1625
+ return (vectorBackendNameCache = 'faiss-native');
1626
+ }
1627
+ catch {
1628
+ // native not installed/built — the pure-JS IVF tier handles it
1629
+ }
1630
+ return (vectorBackendNameCache = 'faiss-ivf');
1631
+ }
1632
+ /**
1633
+ * Per-namespace vector counts, read straight from `vectors*.json`. The default
1634
+ * namespace (`vectors.json`) only holds trajectory/session vectors — facts,
1635
+ * repo chunks and the model registry each live in their own file — so a small
1636
+ * "default" number is corpus size, not a broken index. Sorted largest-first.
1637
+ */
1638
+ function readVectorNamespaces() {
1639
+ try {
1640
+ const out = [];
1641
+ for (const file of readdirSync(MEMORY_DIR)) {
1642
+ const m = /^vectors(?:-(.+))?\.json$/.exec(file);
1643
+ if (!m)
1644
+ continue;
1645
+ try {
1646
+ const d = readJSON(join(MEMORY_DIR, file));
1647
+ out.push({ name: m[1] || 'default', entries: d?.entries ? Object.keys(d.entries).length : 0 });
1648
+ }
1649
+ catch {
1650
+ // Unreadable index → skip; one bad file must not blank the panel.
1651
+ }
1652
+ }
1653
+ return out.sort((a, b) => b.entries - a.entries || a.name.localeCompare(b.name));
1654
+ }
1655
+ catch {
1656
+ return [];
1657
+ }
1658
+ }
1584
1659
  function readMemoryData() {
1585
1660
  const data = readJSON(join(MEMORY_DIR, 'trajectories.json'));
1586
1661
  const trajectories = data?.trajectories
@@ -1623,6 +1698,13 @@ function readMemoryData() {
1623
1698
  facts,
1624
1699
  recall,
1625
1700
  backend: 'local', // memory.backend tier (F1 Mem0 is an optional provider — local is the default)
1701
+ // The vector SEARCH backend — how semantic search is indexed (distinct from
1702
+ // the provider tier above). Shown on the Memory panel so the FAISS question
1703
+ // has an answer in the UI, not just in `nuvira memory backend`.
1704
+ vectorBackend: resolveVectorBackendName(),
1705
+ // Per-namespace entry counts so the panel can show where the vectors
1706
+ // actually are (default = trajectories/sessions, facts, repo, …).
1707
+ vectorNamespaces: readVectorNamespaces(),
1626
1708
  };
1627
1709
  }
1628
1710
  function readHealthData() {
@@ -2438,6 +2520,29 @@ const PROVIDER_ENV_VARS = {
2438
2520
  * detected the same way the CLI sees it: env override > vault ref > config
2439
2521
  * plaintext > none (local providers are keyless by design).
2440
2522
  */
2523
+ /**
2524
+ * The directory a dashboard chat turn runs in when the user attached nothing.
2525
+ *
2526
+ * `dashboard.cwd` (set on the Admin page or with `nuvira config set
2527
+ * dashboard.cwd`) makes the fallback EXPLICIT. Returning `undefined` means
2528
+ * "unconfigured", which is the case the chat guard refuses to guess in — the two
2529
+ * outcomes are deliberately different, so an operator who wants the legacy
2530
+ * behaviour has to say so by setting a directory rather than by doing nothing.
2531
+ * A configured path that no longer exists is treated as unconfigured, never as a
2532
+ * silent pass-through to `process.cwd()`.
2533
+ */
2534
+ function resolveDashboardCwd(configManager) {
2535
+ try {
2536
+ const configured = (configManager ?? new ConfigManager()).getAll().dashboard?.cwd;
2537
+ if (typeof configured !== 'string' || !configured.trim())
2538
+ return undefined;
2539
+ const dir = resolve(configured.trim());
2540
+ return existsSync(dir) && statSync(dir).isDirectory() ? dir : undefined;
2541
+ }
2542
+ catch {
2543
+ return undefined;
2544
+ }
2545
+ }
2441
2546
  function summarizeProvider(type, configManager) {
2442
2547
  const c = (configManager.getAll().providers || {})[type];
2443
2548
  const envVar = PROVIDER_ENV_VARS[type];
@@ -2475,6 +2580,37 @@ function buildAdminProviderSummary(configManager) {
2475
2580
  const providers = configManager.getAll().providers || {};
2476
2581
  return Object.keys(providers).map((type) => summarizeProvider(type, configManager));
2477
2582
  }
2583
+ /**
2584
+ * Build one service row from the catalog. `envVarState` reads the credential
2585
+ * env file first (so a value written by the dashboard shows configured before
2586
+ * any restart), then the process env. Values are only returned in full to a
2587
+ * caller allowed to write credentials; everyone else gets a redaction.
2588
+ */
2589
+ function buildServiceRow(def, canReadValues) {
2590
+ const envVars = def.envVars.map((v) => {
2591
+ const st = envVarState(v.varName);
2592
+ return {
2593
+ varName: v.varName,
2594
+ prompt: v.prompt,
2595
+ secret: v.secret,
2596
+ set: st.set,
2597
+ value: canReadValues ? st.value : st.set ? redactValue(st.value) : '',
2598
+ };
2599
+ });
2600
+ return {
2601
+ id: def.id,
2602
+ label: def.label,
2603
+ capability: def.capability,
2604
+ icon: def.icon,
2605
+ description: def.description,
2606
+ keyless: def.keyless,
2607
+ free: def.free,
2608
+ configured: def.keyless || envVars.every((v) => v.set),
2609
+ docsUrl: def.docsUrl,
2610
+ sharedNote: def.sharedNote,
2611
+ envVars,
2612
+ };
2613
+ }
2478
2614
  /** Run ALL state checks — the dashboard command-runner payload. */
2479
2615
  async function runAdminChecks() {
2480
2616
  const configManager = new ConfigManager();
@@ -4380,6 +4516,125 @@ function handleRequest(req, res) {
4380
4516
  })();
4381
4517
  return;
4382
4518
  }
4519
+ // ── Response cache, by workspace ────────────────────────────────────────
4520
+ // An answer is a statement about a DIRECTORY, so the cache is grouped and
4521
+ // cleared per workspace. Reads need a SESSION (the listing carries prompt
4522
+ // previews — conversation content, unlike the config the open admin reads
4523
+ // expose); clearing needs routing.operate.
4524
+ if (pathname === '/api/admin/cache' && req.method === 'GET') {
4525
+ void (async () => {
4526
+ const session = adminSessions.validate(bearerToken(req));
4527
+ if (!session) {
4528
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
4529
+ return;
4530
+ }
4531
+ const { getCache } = await import('../context/cache.js');
4532
+ const workspaces = await getCache().listByWorkspace();
4533
+ writeJson(res, 200, {
4534
+ ok: true,
4535
+ workspaces,
4536
+ total: workspaces.reduce((sum, w) => sum + w.count, 0),
4537
+ });
4538
+ })();
4539
+ return;
4540
+ }
4541
+ if (pathname === '/api/admin/cache/clear' && req.method === 'POST') {
4542
+ void (async () => {
4543
+ const session = adminSessions.validate(bearerToken(req));
4544
+ if (!session) {
4545
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
4546
+ return;
4547
+ }
4548
+ if (!roleCan(session.role, 'routing.operate')) {
4549
+ writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot clear the response cache (requires admin or operator).` });
4550
+ return;
4551
+ }
4552
+ const body = await readJsonBody(req);
4553
+ if (!body) {
4554
+ writeJson(res, 400, { ok: false, error: 'Invalid JSON body.' });
4555
+ return;
4556
+ }
4557
+ // `null` = the answers produced with no workspace. An absent key is a
4558
+ // mistake, not "the null bucket" — say so rather than clearing the wrong set.
4559
+ if (!('scope' in body)) {
4560
+ writeJson(res, 400, { ok: false, error: 'Expected { scope: string | null }.' });
4561
+ return;
4562
+ }
4563
+ const raw = body.scope;
4564
+ if (raw !== null && (typeof raw !== 'string' || !raw.trim())) {
4565
+ writeJson(res, 400, { ok: false, error: 'scope must be a directory path or null.' });
4566
+ return;
4567
+ }
4568
+ const scope = raw === null ? null : resolve(raw);
4569
+ const { getCache } = await import('../context/cache.js');
4570
+ const removed = await getCache().clearWorkspace(scope);
4571
+ writeJson(res, 200, { ok: true, removed, scope });
4572
+ })();
4573
+ return;
4574
+ }
4575
+ // ── Dashboard workspace (`dashboard.cwd`) ───────────────────────────────
4576
+ // The directory an UNATTACHED chat turn runs in. Reads are open (the chat
4577
+ // picker shows it); writes need routing.operate, the same capability that
4578
+ // gates attaching a project. A configured path is validated to exist — a
4579
+ // stale value would silently re-open the accidental-cwd hole this closes.
4580
+ if (pathname === '/api/admin/workspace' && req.method === 'GET') {
4581
+ const configManager = new ConfigManager();
4582
+ const configured = configManager.getAll().dashboard?.cwd ?? null;
4583
+ const effective = resolveDashboardCwd(configManager) ?? null;
4584
+ writeJson(res, 200, {
4585
+ ok: true,
4586
+ configured,
4587
+ effective,
4588
+ // The process cwd is what an unconfigured dashboard falls back to — shown
4589
+ // so an operator can see WHY the guard fires.
4590
+ processCwd: process.cwd(),
4591
+ configuredValid: configured !== null && effective !== null,
4592
+ });
4593
+ return;
4594
+ }
4595
+ if (pathname === '/api/admin/workspace' && req.method === 'PUT') {
4596
+ void (async () => {
4597
+ const session = adminSessions.validate(bearerToken(req));
4598
+ if (!session) {
4599
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
4600
+ return;
4601
+ }
4602
+ if (!roleCan(session.role, 'routing.operate')) {
4603
+ writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot change the dashboard workspace (requires admin or operator).` });
4604
+ return;
4605
+ }
4606
+ const body = await readJsonBody(req);
4607
+ if (!body) {
4608
+ writeJson(res, 400, { ok: false, error: 'Invalid JSON body.' });
4609
+ return;
4610
+ }
4611
+ const configManager = new ConfigManager();
4612
+ // An explicit empty value CLEARS the setting (back to "ask for a folder").
4613
+ if (typeof body.cwd === 'string' && !body.cwd.trim()) {
4614
+ configManager.save({ dashboard: { cwd: undefined } });
4615
+ writeJson(res, 200, { ok: true, configured: null, effective: null, processCwd: process.cwd(), configuredValid: false });
4616
+ return;
4617
+ }
4618
+ if (typeof body.cwd !== 'string') {
4619
+ writeJson(res, 400, { ok: false, error: 'Expected { cwd: string } (empty string clears it).' });
4620
+ return;
4621
+ }
4622
+ const target = resolve(body.cwd.trim());
4623
+ if (!existsSync(target) || !statSync(target).isDirectory()) {
4624
+ writeJson(res, 400, { ok: false, error: `Not a readable directory: ${body.cwd.trim()}` });
4625
+ return;
4626
+ }
4627
+ configManager.save({ dashboard: { cwd: target } });
4628
+ writeJson(res, 200, {
4629
+ ok: true,
4630
+ configured: target,
4631
+ effective: resolveDashboardCwd(configManager) ?? null,
4632
+ processCwd: process.cwd(),
4633
+ configuredValid: true,
4634
+ });
4635
+ })();
4636
+ return;
4637
+ }
4383
4638
  // ── Session 36: user-declared daily budget (routing.quota + cost cap) ──
4384
4639
  // Same config the CLI writes (`nuvira model quota set`) — dashboard is a
4385
4640
  // parallel GUI, never a fork. Reads are open (like checks/catalog); writes
@@ -5144,6 +5399,138 @@ function handleRequest(req, res) {
5144
5399
  writeJson(res, 200, { ok: true, removed: keys, status: platformConfigStatus(platform) });
5145
5400
  return;
5146
5401
  }
5402
+ // ── Service-provider API keys (image / video / search / vision / speech) ──
5403
+ // These are the THIRD-PARTY services the agent calls directly, as opposed to
5404
+ // the LLM providers configured above. Values live in the same 0600 credential
5405
+ // env file (`~/.nuvira/.env`) the platform config writes, read at startup by
5406
+ // loadEnv() and hot-applied to this process — so a save here is honored by
5407
+ // the tool path on the next call, never a cosmetic setting.
5408
+ // GET /api/admin/services — list every service with per-var status. Any
5409
+ // authenticated session may read; values are full only for credential.write.
5410
+ if (pathname === '/api/admin/services' && req.method === 'GET') {
5411
+ const session = adminSessions.validate(bearerToken(req));
5412
+ if (!session) {
5413
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
5414
+ return;
5415
+ }
5416
+ const canReadValues = roleCan(session.role, 'credential.write');
5417
+ writeJson(res, 200, {
5418
+ ok: true,
5419
+ services: SERVICE_CATALOG.map((d) => buildServiceRow(d, canReadValues)),
5420
+ });
5421
+ return;
5422
+ }
5423
+ const serviceMatch = /^\/api\/admin\/services\/([a-z0-9-]+)$/.exec(pathname);
5424
+ // PUT /api/admin/services/:id — write the service's env values (blank = remove).
5425
+ if (serviceMatch && req.method === 'PUT') {
5426
+ void (async () => {
5427
+ const session = adminSessions.validate(bearerToken(req));
5428
+ if (!session) {
5429
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
5430
+ return;
5431
+ }
5432
+ if (!roleCan(session.role, 'credential.write')) {
5433
+ writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot configure service keys (requires admin).` });
5434
+ return;
5435
+ }
5436
+ const def = getServiceDefinition(serviceMatch[1]);
5437
+ if (!def) {
5438
+ writeJson(res, 404, { ok: false, error: `Unknown service '${serviceMatch[1]}'.` });
5439
+ return;
5440
+ }
5441
+ if (def.envVars.length === 0) {
5442
+ writeJson(res, 400, { ok: false, error: `${def.label} is keyless — there is nothing to configure.` });
5443
+ return;
5444
+ }
5445
+ const body = await readJsonBody(req);
5446
+ const values = (body?.values ?? {});
5447
+ const allowed = new Set(def.envVars.map((v) => v.varName));
5448
+ const updates = {};
5449
+ const removes = [];
5450
+ for (const [key, value] of Object.entries(values)) {
5451
+ if (!allowed.has(key)) {
5452
+ writeJson(res, 400, { ok: false, error: `Unknown env var '${key}' for ${def.label}.` });
5453
+ return;
5454
+ }
5455
+ if (typeof value !== 'string')
5456
+ continue;
5457
+ const trimmed = value.trim();
5458
+ if (trimmed === '')
5459
+ removes.push(key);
5460
+ else
5461
+ updates[key] = trimmed;
5462
+ }
5463
+ if (Object.keys(updates).length === 0 && removes.length === 0) {
5464
+ writeJson(res, 400, { ok: false, error: 'Provide at least one value.' });
5465
+ return;
5466
+ }
5467
+ writeEnvFile(updates, removes);
5468
+ applyEnvToProcess(updates, removes);
5469
+ writeJson(res, 200, {
5470
+ ok: true,
5471
+ wrote: Object.keys(updates),
5472
+ removed: removes,
5473
+ service: buildServiceRow(def, true),
5474
+ });
5475
+ })();
5476
+ return;
5477
+ }
5478
+ // DELETE /api/admin/services/:id — remove every env var of the service.
5479
+ if (serviceMatch && req.method === 'DELETE') {
5480
+ const session = adminSessions.validate(bearerToken(req));
5481
+ if (!session) {
5482
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
5483
+ return;
5484
+ }
5485
+ if (!roleCan(session.role, 'credential.write')) {
5486
+ writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot configure service keys (requires admin).` });
5487
+ return;
5488
+ }
5489
+ const def = getServiceDefinition(serviceMatch[1]);
5490
+ if (!def) {
5491
+ writeJson(res, 404, { ok: false, error: `Unknown service '${serviceMatch[1]}'.` });
5492
+ return;
5493
+ }
5494
+ const keys = def.envVars.map((v) => v.varName);
5495
+ if (keys.length > 0) {
5496
+ writeEnvFile({}, keys);
5497
+ applyEnvToProcess({}, keys);
5498
+ }
5499
+ writeJson(res, 200, { ok: true, removed: keys, service: buildServiceRow(def, true) });
5500
+ return;
5501
+ }
5502
+ // POST /api/admin/services/:id/test — probe the configured backend.
5503
+ const serviceTestMatch = /^\/api\/admin\/services\/([a-z0-9-]+)\/test$/.exec(pathname);
5504
+ if (serviceTestMatch && req.method === 'POST') {
5505
+ void (async () => {
5506
+ const session = adminSessions.validate(bearerToken(req));
5507
+ if (!session) {
5508
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
5509
+ return;
5510
+ }
5511
+ if (!roleCan(session.role, 'credential.write')) {
5512
+ writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot test service keys (requires admin).` });
5513
+ return;
5514
+ }
5515
+ const def = getServiceDefinition(serviceTestMatch[1]);
5516
+ if (!def) {
5517
+ writeJson(res, 404, { ok: false, error: `Unknown service '${serviceTestMatch[1]}'.` });
5518
+ return;
5519
+ }
5520
+ // A probe is only meaningful once the required vars are present.
5521
+ const missing = def.envVars.filter((v) => def.requiresAllVars && !envVarState(v.varName).set);
5522
+ if (missing.length > 0) {
5523
+ writeJson(res, 200, { ok: false, error: `Not configured yet — set ${missing.map((m) => m.varName).join(', ')} first.` });
5524
+ return;
5525
+ }
5526
+ const values = {};
5527
+ for (const v of def.envVars)
5528
+ values[v.varName] = envVarState(v.varName).value;
5529
+ const result = await probeService(def, values);
5530
+ writeJson(res, 200, { ok: result.ok, detail: result.detail });
5531
+ })();
5532
+ return;
5533
+ }
5147
5534
  // ── Bedrock onboarding endpoints ────────────────────────────────────────
5148
5535
  // GET /api/bedrock/status — current Bedrock config status
5149
5536
  if (pathname === '/api/bedrock/status' && req.method === 'GET') {
@@ -5273,10 +5660,19 @@ function handleRequest(req, res) {
5273
5660
  writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot view projects.` });
5274
5661
  return;
5275
5662
  }
5276
- const projects = [...recentProjects]
5277
- .map((p) => ({ path: p, name: basename(p) || p, kind: 'recent' }))
5278
- .filter((p) => existsSync(p.path) && statSync(p.path).isDirectory());
5279
- writeJson(res, 200, { ok: true, projects });
5663
+ // The configured default workspace is surfaced as `kind: 'cwd'` so the
5664
+ // picker can show where an unattached turn would land — the directory that
5665
+ // used to be invisible until the agent reported on the wrong folder.
5666
+ const configuredCwd = resolveDashboardCwd();
5667
+ const projects = [
5668
+ ...(configuredCwd
5669
+ ? [{ path: configuredCwd, name: basename(configuredCwd) || configuredCwd, kind: 'cwd' }]
5670
+ : []),
5671
+ ...[...recentProjects]
5672
+ .map((p) => ({ path: p, name: basename(p) || p, kind: 'recent' }))
5673
+ .filter((p) => existsSync(p.path) && statSync(p.path).isDirectory()),
5674
+ ];
5675
+ writeJson(res, 200, { ok: true, projects, configuredCwd: configuredCwd ?? null });
5280
5676
  return;
5281
5677
  }
5282
5678
  // POST /api/projects/attach — validate a directory and build (or reuse) its
@@ -5713,6 +6109,100 @@ function handleRequest(req, res) {
5713
6109
  writeJson(res, 200, { ok: true, session: { id, ...rec } });
5714
6110
  return;
5715
6111
  }
6112
+ // GET /api/continuity — the stored session snapshots + the semantic recall
6113
+ // index, and the EFFECTIVE on/off state of both continuity switches. Same auth
6114
+ // as the chat session sidebar: this is local memory the chat surface owns.
6115
+ if (pathname === '/api/continuity' && req.method === 'GET') {
6116
+ const session = adminSessions.validate(bearerToken(req));
6117
+ if (!session) {
6118
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
6119
+ return;
6120
+ }
6121
+ if (!roleCan(session.role, 'routing.operate')) {
6122
+ writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot view continuity data.` });
6123
+ return;
6124
+ }
6125
+ void (async () => {
6126
+ const { listSessionSnapshots, resolveSessionStore } = await import('../learning/session-store.js');
6127
+ const { listRecallEntries, resolveSessionRecall } = await import('../learning/session-recall.js');
6128
+ const cm = new ConfigManager();
6129
+ const sessions = listSessionSnapshots().map((s) => ({
6130
+ id: s.id,
6131
+ goal: s.goal,
6132
+ cwd: s.cwd,
6133
+ savedAt: s.savedAt,
6134
+ open: s.open,
6135
+ steps: s.accumulators.steps,
6136
+ successfulTools: s.accumulators.successfulTools.length,
6137
+ mutatedPaths: s.accumulators.mutatedPaths,
6138
+ messages: s.messages.length,
6139
+ }));
6140
+ const recall = listRecallEntries().map((e) => ({
6141
+ id: e.id,
6142
+ projectPath: e.projectPath,
6143
+ goal: e.goal,
6144
+ outcome: e.outcome,
6145
+ savedAt: e.savedAt,
6146
+ }));
6147
+ writeJson(res, 200, {
6148
+ ok: true,
6149
+ sessions,
6150
+ recall,
6151
+ toggles: {
6152
+ sessionStore: resolveSessionStore({ configManager: cm }),
6153
+ sessionRecall: resolveSessionRecall({ configManager: cm }),
6154
+ },
6155
+ });
6156
+ })();
6157
+ return;
6158
+ }
6159
+ // POST /api/continuity/clear — forget stored continuity data. Body:
6160
+ // { target: 'sessions' | 'recall' } — clear all of one kind
6161
+ // { target: 'session' | 'recall-entry', id } — clear one entry
6162
+ if (pathname === '/api/continuity/clear' && req.method === 'POST') {
6163
+ const session = adminSessions.validate(bearerToken(req));
6164
+ if (!session) {
6165
+ writeJson(res, 401, { ok: false, error: 'Not authenticated — log in first.' });
6166
+ return;
6167
+ }
6168
+ if (!roleCan(session.role, 'routing.operate')) {
6169
+ writeJson(res, 403, { ok: false, error: `Access denied — role '${session.role}' cannot change continuity data.` });
6170
+ return;
6171
+ }
6172
+ void (async () => {
6173
+ const body = await readJsonBody(req);
6174
+ const target = typeof body?.target === 'string' ? body.target : '';
6175
+ const id = typeof body?.id === 'string' ? body.id : undefined;
6176
+ let removed = 0;
6177
+ if (target === 'sessions') {
6178
+ const { listSessionSnapshots, clearSession } = await import('../learning/session-store.js');
6179
+ for (const s of listSessionSnapshots())
6180
+ if (clearSession(s.id))
6181
+ removed += 1;
6182
+ }
6183
+ else if (target === 'recall') {
6184
+ const { clearSessionRecallIndex } = await import('../learning/session-recall.js');
6185
+ if (clearSessionRecallIndex())
6186
+ removed = 1;
6187
+ }
6188
+ else if (target === 'session' && id) {
6189
+ const { clearSession } = await import('../learning/session-store.js');
6190
+ if (clearSession(id))
6191
+ removed = 1;
6192
+ }
6193
+ else if (target === 'recall-entry' && id) {
6194
+ const { removeRecallEntry } = await import('../learning/session-recall.js');
6195
+ if (removeRecallEntry(id))
6196
+ removed = 1;
6197
+ }
6198
+ else {
6199
+ writeJson(res, 400, { ok: false, error: "Expected { target: 'sessions'|'recall'|'session'|'recall-entry', id? }." });
6200
+ return;
6201
+ }
6202
+ writeJson(res, 200, { ok: true, removed });
6203
+ })();
6204
+ return;
6205
+ }
5716
6206
  // POST /api/chat/resolve — resolve a plain-English ask into the CLI
5717
6207
  // command(s) the intent router would run (the dashboard twin of
5718
6208
  // `nuvira intent resolve`). The Chat UI calls this BEFORE sending to the
@@ -5800,12 +6290,48 @@ function handleRequest(req, res) {
5800
6290
  // describing the codebase. Cache misses rebuild automatically.
5801
6291
  let projectContext;
5802
6292
  let projectPath;
5803
- if (typeof body?.projectPath === 'string' && body.projectPath.trim()) {
5804
- projectPath = body.projectPath.trim();
5805
- const bundle = getProjectBundle(projectPath);
6293
+ const attachedPath = typeof body?.projectPath === 'string' && body.projectPath.trim()
6294
+ ? body.projectPath.trim()
6295
+ : undefined;
6296
+ /**
6297
+ * The workspace for this turn, in priority order: the folder the user
6298
+ * ATTACHED, then the operator-configured `dashboard.cwd`, and otherwise
6299
+ * NOTHING.
6300
+ *
6301
+ * "Nothing" is the important case. The turn used to fall back to the
6302
+ * dashboard server's own working directory, so a service started from the
6303
+ * home folder answered "assess this project" by listing `~` and describing
6304
+ * whatever unrelated checkout it found there — a confident answer about a
6305
+ * project nobody mentioned (reported as "it went to kuttaaddon"). The
6306
+ * guard below turns that into a request for a folder.
6307
+ */
6308
+ const configuredCwd = resolveDashboardCwd();
6309
+ const effectiveProjectPath = attachedPath ?? configuredCwd;
6310
+ if (effectiveProjectPath) {
6311
+ projectPath = effectiveProjectPath;
6312
+ const bundle = getProjectBundle(effectiveProjectPath);
5806
6313
  if (bundle)
5807
6314
  projectContext = formatProjectText(bundle);
5808
6315
  }
6316
+ // No workspace and no configured default: an ask that names a project, or
6317
+ // that produces/consumes files, gets a folder request instead of an answer
6318
+ // about the wrong tree. General chat is unaffected.
6319
+ if (!effectiveProjectPath) {
6320
+ const { needsProjectAttachment, projectAttachmentPrompt, projectAttachmentFollowups } = await import('./workspace-guard.js');
6321
+ if (needsProjectAttachment(message)) {
6322
+ writeJson(res, 200, {
6323
+ ok: true,
6324
+ sessionId,
6325
+ content: projectAttachmentPrompt(),
6326
+ followups: projectAttachmentFollowups(),
6327
+ provider: null,
6328
+ model: null,
6329
+ generationFailed: false,
6330
+ needsProject: true,
6331
+ });
6332
+ return;
6333
+ }
6334
+ }
5809
6335
  // P4 — the dashboard's Cancel button aborts the POST fetch: the server
5810
6336
  // sees the request close and cancels the in-flight turn (the engine
5811
6337
  // aborts the provider request — quota is not spent on a cancelled turn).