total-recall-brain 3.45.0 → 3.46.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (363) hide show
  1. package/bin/total-recall.mjs +7 -2
  2. package/frontend/dist/assets/index-BRYE1jmv.css +1 -0
  3. package/frontend/dist/assets/index-D1goIPjC.js +5598 -0
  4. package/frontend/dist/index.html +2 -2
  5. package/frontend/dist/sw.js +5 -4
  6. package/frontend/src/App.tsx +183 -236
  7. package/frontend/src/api/auth-inventory.ts +62 -0
  8. package/frontend/src/api/memory.ts +13 -8
  9. package/frontend/src/api/plugin-credentials.ts +23 -0
  10. package/frontend/src/api/plugins.ts +37 -53
  11. package/frontend/src/api/workspace-drawers.ts +97 -0
  12. package/frontend/src/components/BrainSelector.tsx +9 -3
  13. package/frontend/src/components/plugins/PluginPanel.tsx +83 -42
  14. package/frontend/src/components/plugins/PluginSettings.tsx +10 -20
  15. package/frontend/src/components/plugins/PluginWorkspace.tsx +367 -0
  16. package/frontend/src/components/plugins/ScopedPluginCommands.tsx +142 -0
  17. package/frontend/src/components/plugins/plugin-frame.ts +44 -33
  18. package/frontend/src/components/plugins/scoped-plugin-operations.ts +28 -0
  19. package/frontend/src/components/workspace/DrawerChat.tsx +110 -0
  20. package/frontend/src/components/workspace/DrawerLayout.tsx +196 -0
  21. package/frontend/src/components/workspace/DrawerMarkup.tsx +49 -0
  22. package/frontend/src/components/workspace/PluginChunkLibrary.tsx +92 -0
  23. package/frontend/src/components/workspace/PluginHome.tsx +76 -0
  24. package/frontend/src/components/workspace/WorkspaceDrawers.tsx +96 -0
  25. package/frontend/src/components/workspace/WorkspacePage.tsx +95 -0
  26. package/frontend/src/components/workspace/usePluginDesign.ts +38 -0
  27. package/frontend/src/index.css +409 -136
  28. package/frontend/src/pages/HelpPage.tsx +15 -46
  29. package/frontend/src/pages/MemoryPage.tsx +19 -36
  30. package/frontend/src/pages/SettingsPage.tsx +1 -1
  31. package/metadata.plugin.schema.json +53 -227
  32. package/package.json +1 -1
  33. package/scaffold/.agent/skills/plugin/SKILL.md +13 -11
  34. package/scaffold/.agent/skills/plugin/evals/README.md +14 -21
  35. package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +15 -18
  36. package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +19 -24
  37. package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +12 -20
  38. package/scaffold/.agent/skills/plugin/evals/cases/04-home-collects-credential.mjs +35 -0
  39. package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +8 -28
  40. package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +85 -47
  41. package/scaffold/.agent/skills/plugin/evals/run.mjs +17 -18
  42. package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +4 -4
  43. package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +2 -2
  44. package/scaffold/.agent/skills/plugin/prompts/08-verify.md +4 -4
  45. package/scaffold/.agent/skills/plugin/references/anti-patterns.md +2 -2
  46. package/scaffold/.agent/skills/plugin/references/generative-home.md +38 -0
  47. package/scaffold/.agent/skills/plugin/references/scaffold.md +5 -5
  48. package/scaffold/.agent/skills/plugin/references/surfaces.md +13 -4
  49. package/scaffold/.agent/skills/plugin/scripts/record.mjs +75 -153
  50. package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +4 -4
  51. package/scaffold/.agent/skills/total-recall/plugins/auth/DESIGN.md +76 -0
  52. package/scaffold/.agent/skills/total-recall/plugins/auth/README.md +3 -1
  53. package/scaffold/.agent/skills/total-recall/plugins/auth/cli.mjs +20 -15
  54. package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +6 -6
  55. package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +2 -1
  56. package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +13 -51
  57. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/access-overview.md +17 -0
  58. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/agent-login.md +20 -0
  59. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/baseline-audit.md +22 -0
  60. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/credential-repository-binding.md +17 -0
  61. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/credential-store-maintenance.md +17 -0
  62. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/dashboard-password.md +18 -0
  63. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/dashboard-sessions.md +18 -0
  64. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/endpoint-probe.md +23 -0
  65. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/google-sign-in.md +21 -0
  66. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/mesh-machines.md +16 -0
  67. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/passkey-enrollment.md +21 -0
  68. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/registered-brains.md +16 -0
  69. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/scoped-access-keys.md +17 -0
  70. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/secret-inventory.md +25 -0
  71. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/chunks/signing-key-rotation.md +22 -0
  72. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/home.js +375 -0
  73. package/scaffold/.agent/skills/total-recall/plugins/automations/{ui/DESIGN.md → DESIGN.md} +32 -21
  74. package/scaffold/.agent/skills/total-recall/plugins/automations/adapters.mjs +17 -1
  75. package/scaffold/.agent/skills/total-recall/plugins/automations/cli.mjs +10 -1
  76. package/scaffold/.agent/skills/total-recall/plugins/automations/package-lock.json +6 -6
  77. package/scaffold/.agent/skills/total-recall/plugins/automations/package.json +4 -3
  78. package/scaffold/.agent/skills/total-recall/plugins/automations/plugin.json +11 -27
  79. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/automations.js +2 -2
  80. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/control-ledger.md +42 -0
  81. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/local-inventory-triage.md +17 -0
  82. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/pending-control.md +39 -0
  83. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/run-evidence.md +41 -0
  84. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/schedule-vs-run-evidence.md +17 -0
  85. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/chunks/source-coverage.md +41 -0
  86. package/scaffold/.agent/skills/total-recall/plugins/automations/ui/{card.js → home.js} +32 -27
  87. package/scaffold/.agent/skills/total-recall/plugins/code-quality/DESIGN.md +53 -0
  88. package/scaffold/.agent/skills/total-recall/plugins/code-quality/cli.mjs +23 -0
  89. package/scaffold/.agent/skills/total-recall/plugins/code-quality/commands/run-core.mjs +4 -2
  90. package/scaffold/.agent/skills/total-recall/plugins/code-quality/core/check.mjs +4 -2
  91. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +6 -6
  92. package/scaffold/.agent/skills/total-recall/plugins/code-quality/package.json +2 -1
  93. package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +40 -40
  94. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/config.md +15 -0
  95. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/detect.md +15 -0
  96. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/gate.md +15 -0
  97. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/init.md +15 -0
  98. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/report.md +17 -0
  99. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/run.md +17 -0
  100. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/chunks/snapshot.md +15 -0
  101. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/code-quality-report.mjs +55 -23
  102. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/home.mjs +214 -0
  103. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/DESIGN.md +146 -0
  104. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +6 -6
  105. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package.json +2 -1
  106. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +51 -34
  107. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/src/command.mjs +1 -1
  108. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/create.md +35 -0
  109. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/history.md +35 -0
  110. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/library.md +49 -0
  111. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/list.md +42 -0
  112. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/promote.md +40 -0
  113. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/read.md +25 -0
  114. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/remove.md +26 -0
  115. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/rollback.md +26 -0
  116. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/show.md +53 -0
  117. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/status.md +40 -0
  118. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/test-path.md +38 -0
  119. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/test.md +46 -0
  120. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/chunks/update.md +32 -0
  121. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/home.js +533 -0
  122. package/scaffold/.agent/skills/total-recall/plugins/embeddings/DESIGN.md +110 -0
  123. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +6 -6
  124. package/scaffold/.agent/skills/total-recall/plugins/embeddings/package.json +2 -1
  125. package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +13 -10
  126. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/benchmark.md +49 -0
  127. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/coverage.md +53 -0
  128. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/models.md +43 -0
  129. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/pull.md +41 -0
  130. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/chunks/status.md +59 -0
  131. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/home.js +353 -0
  132. package/scaffold/.agent/skills/total-recall/plugins/git/DESIGN.md +31 -0
  133. package/scaffold/.agent/skills/total-recall/plugins/git/README.md +25 -1
  134. package/scaffold/.agent/skills/total-recall/plugins/git/cli.mjs +5 -1
  135. package/scaffold/.agent/skills/total-recall/plugins/git/package-lock.json +6 -6
  136. package/scaffold/.agent/skills/total-recall/plugins/git/package.json +2 -1
  137. package/scaffold/.agent/skills/total-recall/plugins/git/plugin.json +17 -27
  138. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/commit-memory.mjs +30 -6
  139. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/fleet.mjs +24 -6
  140. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/secret-firewall.mjs +33 -7
  141. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/undo.mjs +29 -5
  142. package/scaffold/.agent/skills/total-recall/plugins/git/src/ops/unmerged.mjs +182 -0
  143. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/change-comparison.md +27 -0
  144. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/commit-identity.md +22 -0
  145. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/create-worktree.md +25 -0
  146. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/registered-worktrees.md +22 -0
  147. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/remove-worktree.md +23 -0
  148. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/repository-ledger.md +25 -0
  149. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/unmerged-work.md +22 -0
  150. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/upstream-divergence.md +17 -0
  151. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/worktree-conflict-triage.md +17 -0
  152. package/scaffold/.agent/skills/total-recall/plugins/git/ui/chunks/worktree-terminal.md +22 -0
  153. package/scaffold/.agent/skills/total-recall/plugins/git/ui/home.js +273 -0
  154. package/scaffold/.agent/skills/total-recall/plugins/mesh/DESIGN.md +35 -0
  155. package/scaffold/.agent/skills/total-recall/plugins/mesh/README.md +4 -5
  156. package/scaffold/.agent/skills/total-recall/plugins/mesh/cli.mjs +9 -3
  157. package/scaffold/.agent/skills/total-recall/plugins/mesh/package-lock.json +6 -6
  158. package/scaffold/.agent/skills/total-recall/plugins/mesh/package.json +2 -1
  159. package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +37 -39
  160. package/scaffold/.agent/skills/total-recall/plugins/mesh/src/cli/mesh.mjs +5 -3
  161. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/active-device-dispatch.md +22 -0
  162. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/control-probe.md +25 -0
  163. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/control-server.md +22 -0
  164. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/credential-readiness.md +23 -0
  165. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/elected-leader.md +22 -0
  166. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/enrollment-key.md +22 -0
  167. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/file-receipt.md +25 -0
  168. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/fleet-execution.md +23 -0
  169. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/fleet-presence.md +23 -0
  170. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/interactive-ssh.md +22 -0
  171. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/local-enrollment.md +23 -0
  172. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/mesh-policy.md +23 -0
  173. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/network-atlas.md +25 -0
  174. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/node-roster.md +26 -0
  175. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/node-selection-proof.md +17 -0
  176. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/owner-observations.md +23 -0
  177. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/peer-path.md +23 -0
  178. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/pinned-leader.md +24 -0
  179. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/reachability-evidence-ladder.md +17 -0
  180. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/recorded-login.md +23 -0
  181. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/remote-execution.md +22 -0
  182. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/chunks/secret-grants.md +26 -0
  183. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/{card.js → home.js} +93 -22
  184. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/DESIGN.md +86 -0
  185. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/README.md +19 -0
  186. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/cli.mjs +5 -1
  187. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package-lock.json +6 -6
  188. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/package.json +2 -1
  189. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/plugin.json +43 -22
  190. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/src/chat.mjs +80 -0
  191. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/adapter-docs-freshness.md +16 -0
  192. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/chat-handoff.md +26 -0
  193. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/fleet-maintenance.md +21 -0
  194. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/harness-inventory.md +21 -0
  195. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/included-access.md +18 -0
  196. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/included-usage-verification.md +15 -0
  197. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/independent-review.md +17 -0
  198. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/managed-execution.md +20 -0
  199. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/official-documentation.md +18 -0
  200. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/quota-recovery.md +21 -0
  201. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/review-routing-decision.md +16 -0
  202. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/route-work.md +18 -0
  203. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/routing-evidence.md +18 -0
  204. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/chunks/run-ledger.md +18 -0
  205. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/home.js +198 -0
  206. package/scaffold/.agent/skills/total-recall/plugins/openwiki/DESIGN.md +75 -0
  207. package/scaffold/.agent/skills/total-recall/plugins/openwiki/README.md +2 -0
  208. package/scaffold/.agent/skills/total-recall/plugins/openwiki/cli.mjs +17 -14
  209. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +6 -6
  210. package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +2 -1
  211. package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +10 -44
  212. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/generate.mjs +5 -4
  213. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/graph.mjs +3 -4
  214. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/ingest.mjs +23 -18
  215. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/mesh-vaults.mjs +8 -3
  216. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/refresh.mjs +6 -3
  217. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/search.mjs +3 -5
  218. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/token.mjs +21 -47
  219. package/scaffold/.agent/skills/total-recall/plugins/openwiki/src/upstream-watcher.mjs +17 -6
  220. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/auto-refresh.md +18 -0
  221. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/bootstrap-pages.md +18 -0
  222. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/generate-wiki.md +21 -0
  223. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/generator-settings.md +18 -0
  224. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/host-probe.md +18 -0
  225. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/ingest-preview.md +18 -0
  226. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/link-hygiene.md +21 -0
  227. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/link-topology.md +19 -0
  228. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/passage-search.md +19 -0
  229. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/propagation-plan.md +18 -0
  230. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/provider-credentials.md +18 -0
  231. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/refresh-report.md +18 -0
  232. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/task-evidence.md +22 -0
  233. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/upstream-record.md +18 -0
  234. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/watch-upstream.md +18 -0
  235. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/chunks/wiki-atlas.md +19 -0
  236. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/panel.js +261 -513
  237. package/scaffold/.agent/skills/total-recall/plugins/security/DESIGN.md +84 -0
  238. package/scaffold/.agent/skills/total-recall/plugins/security/README.md +4 -2
  239. package/scaffold/.agent/skills/total-recall/plugins/security/cli.mjs +7 -19
  240. package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +6 -6
  241. package/scaffold/.agent/skills/total-recall/plugins/security/package.json +2 -3
  242. package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +19 -52
  243. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/device-evidence.md +21 -0
  244. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/evidence-desk.md +18 -0
  245. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/guided-device-review.md +17 -0
  246. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/host-and-network.md +21 -0
  247. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/https-headers.md +17 -0
  248. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/mesh-coverage.md +18 -0
  249. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/private-security-tools.md +16 -0
  250. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/single-origin-header-check.md +17 -0
  251. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/source-review.md +21 -0
  252. package/scaffold/.agent/skills/total-recall/plugins/security/ui/chunks/ssh-permission-plan.md +17 -0
  253. package/scaffold/.agent/skills/total-recall/plugins/security/ui/home.js +283 -0
  254. package/scaffold/.agent/skills/total-recall/plugins/ssss/DESIGN.md +93 -0
  255. package/scaffold/.agent/skills/total-recall/plugins/ssss/package-lock.json +2 -2
  256. package/scaffold/.agent/skills/total-recall/plugins/ssss/package.json +2 -1
  257. package/scaffold/.agent/skills/total-recall/plugins/ssss/plugin.json +5 -3
  258. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/conformance.md +26 -0
  259. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/registry.md +37 -0
  260. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/repair.md +42 -0
  261. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/scaffold.md +33 -0
  262. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/status.md +43 -0
  263. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/validate.md +34 -0
  264. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/validation-diagnostics.md +17 -0
  265. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/chunks/workspace-registry-preflight.md +17 -0
  266. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/home.js +348 -0
  267. package/scaffold/.agent/skills/total-recall/references/operations.md +2 -0
  268. package/src/cli/api.mjs +3 -3
  269. package/src/cli/brief.mjs +3 -0
  270. package/src/cli/config.mjs +16 -0
  271. package/src/cli/connect.mjs +2 -7
  272. package/src/cli/plugin/chunks.mjs +145 -0
  273. package/src/cli/plugin/create.mjs +24 -52
  274. package/src/cli/plugin/index.mjs +20 -9
  275. package/src/cli/setup.mjs +2 -9
  276. package/src/cli/start.mjs +1 -0
  277. package/src/cli/startup.mjs +18 -0
  278. package/src/core/app-deploy/design-tokens.mjs +14 -9
  279. package/src/core/app-deploy/ui-elements.mjs +1 -1
  280. package/src/core/brain-registry.mjs +5 -0
  281. package/src/core/brain-updates.mjs +5 -1
  282. package/src/core/daemon-control.mjs +5 -1
  283. package/src/core/dashboard-bundle.mjs +261 -0
  284. package/src/core/default-plugin-refresh.mjs +39 -0
  285. package/src/core/ensure-dependencies.mjs +19 -15
  286. package/src/core/memory-brain.mjs +30 -8
  287. package/src/core/plugin-activation.mjs +63 -20
  288. package/src/core/plugin-contracts/{card.mjs → manifest.mjs} +23 -57
  289. package/src/core/plugin-contracts/startup-check.mjs +49 -0
  290. package/src/core/plugin-contracts/ui.mjs +9 -3
  291. package/src/core/plugin-distribution.mjs +6 -6
  292. package/src/core/plugin-lifecycle.mjs +58 -0
  293. package/src/core/plugin-loader.mjs +9 -5
  294. package/src/core/plugin-runner.mjs +21 -10
  295. package/src/core/plugin-startup-checks.mjs +44 -0
  296. package/src/core/plugin-state.mjs +19 -5
  297. package/src/core/plugin-store.mjs +30 -11
  298. package/src/core/project-id.mjs +15 -5
  299. package/src/core/required-plugins.mjs +31 -6
  300. package/src/core/schema.mjs +2 -0
  301. package/src/core/ssss-operation-service.mjs +1 -0
  302. package/src/core/startup-health.mjs +28 -3
  303. package/src/core/testing/plugin-fixture.mjs +22 -21
  304. package/src/core/throttled-fetch.mjs +25 -18
  305. package/src/core/ui-chunk-choice.mjs +77 -0
  306. package/src/core/ui-chunk.registry.json +23 -0
  307. package/src/core/ui-chunks.mjs +434 -0
  308. package/src/core/vault-cache.mjs +58 -59
  309. package/src/core/workspace-compositions.mjs +151 -0
  310. package/src/core/workspace-drawers.mjs +79 -0
  311. package/src/server/api.mjs +4 -27
  312. package/src/server/index.mjs +28 -21
  313. package/src/server/memory-events.mjs +31 -0
  314. package/src/server/plugin-brain-targets.mjs +89 -0
  315. package/src/server/rest.mjs +2 -0
  316. package/src/server/route-inventory.mjs +3 -2
  317. package/src/server/route-manifest.json +52 -4
  318. package/src/server/routes/_shared.mjs +7 -14
  319. package/src/server/routes/brains.mjs +29 -44
  320. package/src/server/routes/dashboard.mjs +26 -0
  321. package/src/server/routes/plugins.mjs +154 -45
  322. package/src/server/routes/ui-chunks.mjs +192 -0
  323. package/frontend/dist/assets/index-DACh1dhx.js +0 -5613
  324. package/frontend/dist/assets/index-DtwsDyFM.css +0 -1
  325. package/frontend/src/components/plugins/card/CardEngine.tsx +0 -108
  326. package/frontend/src/components/plugins/card/CardScenes.tsx +0 -103
  327. package/frontend/src/components/plugins/card/ExecutionPicker.tsx +0 -64
  328. package/frontend/src/components/plugins/card/args.ts +0 -28
  329. package/frontend/src/components/plugins/card/card.css +0 -32
  330. package/frontend/src/components/plugins/card/cli-line.ts +0 -13
  331. package/frontend/src/components/plugins/card/contract.ts +0 -91
  332. package/frontend/src/components/plugins/card/format.ts +0 -49
  333. package/frontend/src/components/plugins/card/runner-context.ts +0 -11
  334. package/frontend/src/components/plugins/card/runner.tsx +0 -43
  335. package/frontend/src/components/plugins/card/theme.ts +0 -35
  336. package/frontend/src/components/plugins/card/use-source.ts +0 -22
  337. package/frontend/src/components/plugins/card/widgets.tsx +0 -94
  338. package/frontend/src/components/plugins/workspace/WorkspaceHost.tsx +0 -23
  339. package/frontend/src/pages/PluginsPage.tsx +0 -1719
  340. package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +0 -47
  341. package/scaffold/.agent/skills/plugin/prompts/06-card.md +0 -46
  342. package/scaffold/.agent/skills/plugin/references/card.md +0 -90
  343. package/scaffold/.agent/skills/plugin/subagents/card-designer.md +0 -60
  344. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/DESIGN.md +0 -67
  345. package/scaffold/.agent/skills/total-recall/plugins/auth/ui/card.js +0 -1179
  346. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/DESIGN.md +0 -32
  347. package/scaffold/.agent/skills/total-recall/plugins/code-quality/ui/card.js +0 -588
  348. package/scaffold/.agent/skills/total-recall/plugins/composable-cli/ui/card.js +0 -575
  349. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/DESIGN.md +0 -32
  350. package/scaffold/.agent/skills/total-recall/plugins/embeddings/ui/card.js +0 -482
  351. package/scaffold/.agent/skills/total-recall/plugins/git/ui/DESIGN.md +0 -100
  352. package/scaffold/.agent/skills/total-recall/plugins/git/ui/card.js +0 -637
  353. package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/DESIGN.md +0 -217
  354. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/DESIGN.md +0 -32
  355. package/scaffold/.agent/skills/total-recall/plugins/meta-harness/ui/card.js +0 -1276
  356. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/DESIGN.md +0 -45
  357. package/scaffold/.agent/skills/total-recall/plugins/openwiki/ui/card.js +0 -1401
  358. package/scaffold/.agent/skills/total-recall/plugins/security/scripts/build-ui.mjs +0 -4
  359. package/scaffold/.agent/skills/total-recall/plugins/security/ui/DESIGN.md +0 -41
  360. package/scaffold/.agent/skills/total-recall/plugins/security/ui/card.js +0 -396
  361. package/scaffold/.agent/skills/total-recall/plugins/security/ui/panel.js +0 -98
  362. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/DESIGN.md +0 -15
  363. package/scaffold/.agent/skills/total-recall/plugins/ssss/ui/card.js +0 -182
@@ -12,14 +12,10 @@ import fs from 'node:fs';
12
12
  import path from 'node:path';
13
13
  import os from 'node:os';
14
14
  import { requireAuth, requireScope } from '../auth.mjs';
15
+ import { dashboardBrainDir, readRegisteredProjects } from '../../core/brain-registry.mjs';
15
16
  import { getNodes } from '../../core/vault-cache.mjs';
16
17
  import { logger } from '../../core/logger.mjs';
17
- import {
18
- BRAIN_DIR,
19
- VAULT_DIR,
20
- DERIVED_DIR,
21
- serverError,
22
- } from './_shared.mjs';
18
+ import { serverError } from './_shared.mjs';
23
19
 
24
20
  const router = Router();
25
21
 
@@ -49,8 +45,9 @@ function countMd(dir) {
49
45
  * List all known brains (global + registered projects + optional tenant) with state metadata.
50
46
  */
51
47
  router.get('/api/brains', requireAuth, requireScope('ssss:read'), async (req, res) => {
48
+ res.set('Cache-Control', 'private, no-store');
52
49
  try {
53
- const globalBrainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
50
+ const globalBrainDir = dashboardBrainDir();
54
51
  const brains = [];
55
52
 
56
53
  // Global brain
@@ -75,35 +72,27 @@ router.get('/api/brains', requireAuth, requireScope('ssss:read'), async (req, re
75
72
  });
76
73
 
77
74
  // Project brains from registry
78
- const registryPath = path.join(globalBrainDir, 'config', 'project-registry.json');
79
- if (fs.existsSync(registryPath)) {
80
- try {
81
- const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
82
- for (const project of registry) {
83
- const projectVaultDir = path.join(project.brainDir, 'memory-vault');
84
- let nodeCount = 0;
85
- const exists = fs.existsSync(project.path);
86
- if (!exists) continue; // Hide non-existent projects automatically
87
-
88
- if (fs.existsSync(projectVaultDir)) {
89
- nodeCount = countMd(projectVaultDir);
90
- }
91
-
92
- brains.push({
93
- id: `project:${project.name}`,
94
- name: project.name,
95
- layer: 'project',
96
- path: project.brainDir,
97
- project_root: project.path,
98
- exists: true,
99
- node_count: nodeCount,
100
- registered_at: project.registered_at,
101
- last_compiled: project.last_compiled || getLastModified(path.join(project.brainDir, 'memory-derived', 'graph-index.jsonl')),
102
- });
103
- }
104
- } catch (err) {
105
- logger.error('server', 'Failed to read project registry', { error: err.message });
75
+ try {
76
+ const registry = readRegisteredProjects(globalBrainDir);
77
+ for (const project of registry) {
78
+ if (!project.name || !project.path || !project.brainDir || !fs.existsSync(project.path)) continue;
79
+ const duplicateName = registry.some(other => other !== project && other.name === project.name);
80
+ if (duplicateName && !project.project_id) continue;
81
+ brains.push({
82
+ id: `project:${duplicateName ? project.project_id : project.name}`,
83
+ name: project.name,
84
+ layer: 'project',
85
+ path: project.brainDir,
86
+ project_root: project.path,
87
+ exists: true,
88
+ node_count: countMd(path.join(project.brainDir, 'memory-vault')),
89
+ registered_at: project.registered_at,
90
+ last_compiled: project.last_compiled || getLastModified(path.join(project.brainDir, 'memory-derived', 'graph-index.jsonl')),
91
+ });
106
92
  }
93
+ } catch (err) {
94
+ logger.error('server', 'Failed to read project registry', { error: err.message });
95
+ return res.status(503).json({ error: 'Brain registry metadata is unavailable' });
107
96
  }
108
97
 
109
98
  // Optional remote/tenant vault from config
@@ -145,22 +134,18 @@ router.get('/api/brains', requireAuth, requireScope('ssss:read'), async (req, re
145
134
  * List all memory nodes for a specific brain.
146
135
  */
147
136
  router.get('/api/brains/:id/nodes', requireAuth, requireScope('ssss:read', 'memory:read'), async (req, res) => {
137
+ res.set('Cache-Control', 'private, no-store');
148
138
  try {
149
139
  const brainId = req.params.id;
150
140
  let vaultDir;
151
141
 
152
142
  if (brainId === 'global') {
153
- const brainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
154
- vaultDir = path.join(brainDir, 'memory-vault');
143
+ vaultDir = path.join(dashboardBrainDir(), 'memory-vault');
155
144
  } else if (brainId.startsWith('project:')) {
156
145
  const projectName = brainId.slice('project:'.length);
157
- const globalBrainDir = path.join(os.homedir(), '.agent', 'skills', 'total-recall');
158
- const registryPath = path.join(globalBrainDir, 'config', 'project-registry.json');
159
- if (!fs.existsSync(registryPath)) {
160
- return res.status(404).json({ error: 'Project registry not found' });
161
- }
162
- const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8'));
163
- const project = registry.find(p => p.name === projectName);
146
+ const registry = readRegisteredProjects(dashboardBrainDir());
147
+ const matching = registry.filter(p => p.name === projectName || p.project_id === projectName);
148
+ const project = matching.length === 1 ? matching[0] : null;
164
149
  if (!project) {
165
150
  return res.status(404).json({ error: `Project "${projectName}" not found in registry` });
166
151
  }
@@ -4,8 +4,10 @@ import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { requireAuth, requireScope } from '../auth.mjs';
6
6
  import { getNodes } from '../../core/vault-cache.mjs';
7
+ import { DrawerInputError, readDrawerConfig, writeDrawerConfig } from '../../core/workspace-drawers.mjs';
7
8
  import {
8
9
  VAULT_DIR,
10
+ BRAIN_DIR,
9
11
  INSTRUCTIONS,
10
12
  ROOT as SHARED_ROOT,
11
13
  resolveVaultFromQuery,
@@ -17,6 +19,30 @@ const router = Router();
17
19
  // (breaks on Windows and on paths with encoded characters).
18
20
  const ROOT = process.env.TR_ROOT || SHARED_ROOT || path.join(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
19
21
 
22
+ router.get('/api/dashboard/drawers/:edge', requireAuth, requireScope('config:read'), (req, res) => {
23
+ res.set('Cache-Control', 'no-store');
24
+ try {
25
+ return res.json(readDrawerConfig(BRAIN_DIR, req.params.edge));
26
+ } catch (error) {
27
+ if (error instanceof DrawerInputError) return res.status(400).json({ error: error.message });
28
+ return res.status(500).json({ error: 'Could not read the saved drawer.' });
29
+ }
30
+ });
31
+
32
+ router.put('/api/dashboard/drawers/:edge', requireAuth, requireScope('config:write'), async (req, res) => {
33
+ res.set('Cache-Control', 'no-store');
34
+ if (!req.is('application/json')) return res.status(415).json({ error: 'Use JSON to save the drawer.' });
35
+ try {
36
+ return res.json(await writeDrawerConfig(BRAIN_DIR, req.params.edge, req.body?.config, req.body?.version));
37
+ } catch (error) {
38
+ if (error instanceof DrawerInputError) return res.status(400).json({ error: error.message });
39
+ if (error.message.startsWith('Plugin state version conflict')) {
40
+ return res.status(409).json({ error: 'The drawer changed elsewhere. Reload it before saving.' });
41
+ }
42
+ return res.status(500).json({ error: 'Could not save the drawer.' });
43
+ }
44
+ });
45
+
20
46
  router.get('/api/dashboard/instructions', requireAuth, requireScope('instructions:read'), (req, res) => {
21
47
  const vaultDir = resolveVaultFromQuery(req);
22
48
  const paths = pathsForVault(vaultDir);
@@ -3,7 +3,8 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import matter from "../../core/frontmatter.mjs";
5
5
  import { requireAuth, requireScope } from "../auth.mjs";
6
- import { getPluginById } from "../../core/plugin-loader.mjs";
6
+ import { resolvePluginBrainTargets } from '../plugin-brain-targets.mjs';
7
+ import { discoverPlugins, getPluginById } from "../../core/plugin-loader.mjs";
7
8
  import {
8
9
  listInstalledPlugins,
9
10
  listAvailableBundled,
@@ -18,20 +19,25 @@ import {
18
19
  import { listPeerPlugins } from "../../core/plugin-peers.mjs";
19
20
  import { runPluginCommand, validatePluginSecretInput } from "../../core/plugin-runner.mjs";
20
21
  import { readPluginRecord, patchPluginRecord, emit, vaultFor } from '../../core/plugin-store.mjs';
21
- import { setPluginActive } from '../../core/plugin-activation.mjs';
22
+ import { activatePluginOnUse, setPluginActive } from '../../core/plugin-activation.mjs';
22
23
  import { pluginActivation } from '../../core/plugin-state.mjs';
24
+ import { REQUIRED_INSTALLATION_PLUGIN_IDS } from '../../core/default-plugins.mjs';
23
25
  import { configurationValues } from '../../core/plugin-contracts/configuration.mjs';
24
26
  import { writeNodeValidatedAsync } from "../../core/validated-write.mjs";
25
27
  import { walkMd } from "../../core/vault.mjs";
26
28
  import { serverError, badRequest, VAULT_DIR, BRAIN_DIR, globalBrainDir, sanitizeNode } from "./_shared.mjs";
27
29
 
28
30
  const router = Router();
31
+ let activationTail = Promise.resolve();
29
32
 
30
- // Plugins always resolve against the server's own project. Routes used to take
31
- // a caller-supplied `root`, which let a request point discovery — and the
32
- // runner — at any directory on disk.
33
+ // Omitted selectors use the serving project. Explicit selectors resolve only
34
+ // through registered brains, without accepting host paths from a request.
33
35
  const projectRoot = () => process.cwd();
34
36
 
37
+ function surfaceTarget(req, id) {
38
+ return id === undefined ? { brainDir: BRAIN_DIR, projectRoot: projectRoot() } : resolvePluginBrainTargets([id], req.auth)[0];
39
+ }
40
+
35
41
  router.get('/api/plugins/execution-targets', requireAuth, requireScope('config:read'), async (req, res) => {
36
42
  try {
37
43
  const { loadPluginModule } = await import('../../core/plugin-runtime.mjs');
@@ -43,19 +49,81 @@ router.get('/api/plugins/execution-targets', requireAuth, requireScope('config:r
43
49
 
44
50
  router.put('/api/plugins/:id/activation', requireAuth, requireScope('config:write'), async (req, res) => {
45
51
  try {
46
- const result = await setPluginActive(req.params.id, req.body?.enabled, { projectRoot: projectRoot() });
47
- res.status(result.compilation.ok ? 200 : 500).json({ success: result.compilation.ok, ...result });
48
- } catch (err) { badRequest(res, err.message); }
52
+ const { enabled, brainIds, mode } = req.body || {};
53
+ if (req.query.brain !== undefined || req.body?.brainId !== undefined) {
54
+ return badRequest(res, 'Use the brainIds array to target plugin activation.');
55
+ }
56
+ if (mode === undefined && typeof enabled !== 'boolean') return badRequest(res, 'Plugin activation requires a boolean');
57
+ if (mode !== undefined && (enabled !== undefined || !['on-use', 'use'].includes(mode) ||
58
+ !Array.isArray(brainIds) || (mode === 'use' && brainIds.length !== 1))) {
59
+ return badRequest(res, 'On use requires project brain IDs. Opening a plugin requires one project brain ID.');
60
+ }
61
+ // Global changes rebuild project surfaces too, so serialize writes and rebuilds together.
62
+ const change = activationTail.then(async () => {
63
+ if (brainIds === undefined) {
64
+ const result = await setPluginActive(req.params.id, enabled, { projectRoot: projectRoot() });
65
+ const cleanup = reportedCleanup(result);
66
+ const success = result.compilation.ok && cleanup?.ok !== false;
67
+ return { status: success ? 200 : 500, body: { ...result, success, cleanup, ...(cleanup?.error ? { error: cleanup.error } : {}) } };
68
+ }
69
+ const targets = resolvePluginBrainTargets(brainIds, req.auth);
70
+ if (mode && targets.some(target => target.projectRoot === null)) {
71
+ return { status: 400, body: { error: 'On use is available for individual project brains.' } };
72
+ }
73
+ const results = [];
74
+ for (const target of targets) {
75
+ results.push(await updateSelectedPluginActivation(req.params.id, enabled, mode, target));
76
+ }
77
+ return { status: 200, body: { success: results.every(result => result.success), results } };
78
+ });
79
+ activationTail = change.catch(() => {});
80
+ const { status, body } = await change;
81
+ res.status(status).json(body);
82
+ } catch (err) { res.status(err.status || 400).json({ error: err.message }); }
49
83
  });
50
84
 
85
+ function reportedCleanup(result) {
86
+ if (!result.cleanup) return undefined;
87
+ const { ok, state } = result.cleanup;
88
+ return { ok, state, ...(ok ? {} : { error: 'The plugin is off, but cleanup did not finish. Retry cleanup.' }) };
89
+ }
90
+
91
+ async function updateSelectedPluginActivation(id, enabled, mode, { brainId, name, projectRoot }) {
92
+ const identity = { brainId, name };
93
+ const failure = error => ({ ...identity, success: false, error });
94
+ try {
95
+ const plugin = getPluginById(id, projectRoot);
96
+ if (!plugin?.valid) return failure('Plugin is unavailable in this brain.');
97
+ if ((enabled === false || mode === 'on-use') && REQUIRED_INSTALLATION_PLUGIN_IDS.includes(id)) {
98
+ return failure('Required plugins cannot be disabled.');
99
+ }
100
+ const result = mode === 'use'
101
+ ? await activatePluginOnUse(id, { projectRoot })
102
+ : await setPluginActive(id, mode ?? enabled, { projectRoot, scope: projectRoot === null ? 'shared' : 'project' });
103
+ if (!result) {
104
+ const activation = pluginActivation(plugin, { projectRoot });
105
+ return activation.enabled ? { ...identity, success: true, activation } : failure('This plugin is not set to open on use.');
106
+ }
107
+ if (mode === 'use' && !result.activation.enabled) return failure('The plugin was switched off before it could open.');
108
+ const cleanup = reportedCleanup(result);
109
+ const error = cleanup?.error || (result.compilation.ok ? undefined : 'Activation was saved, but the workspace could not be rebuilt.');
110
+ return { ...identity, success: !error, activation: result.activation, cleanup, error };
111
+ } catch {
112
+ return failure('Could not complete activation in this brain.');
113
+ }
114
+ }
115
+
51
116
  router.post('/api/plugins/:id/credentials', requireAuth, requireScope('config:write'), async (req, res) => {
117
+ let target;
118
+ try { target = surfaceTarget(req, req.query.brain); }
119
+ catch (error) { return res.status(error.status || 400).json({ error: 'Could not select the credential workspace.' }); }
52
120
  try {
53
- const plugin = getPluginById(req.params.id, projectRoot());
121
+ const plugin = getPluginById(req.params.id, target.projectRoot);
54
122
  if (!plugin) return res.status(404).json({ error: 'Plugin not installed' });
55
123
  const { key, value } = req.body || {};
56
124
  if (typeof key !== 'string' || typeof value !== 'string' || value.length > 16384 || !plugin.manifest.secrets?.some(secret => secret.key === key)) return badRequest(res, 'Invalid declared credential');
57
125
  const { loadPluginModule } = await import('../../core/plugin-runtime.mjs');
58
- const owner = await loadPluginModule(plugin.id, 'credentials');
126
+ const owner = await loadPluginModule(plugin.id, 'credentials', { projectRoot: target.projectRoot });
59
127
  const result = await owner.saveCredential({ key, value, configuration: readPluginRecord(plugin)?.configuration || {}, brainDir: process.env.TR_SECRETS_BRAIN || globalBrainDir || BRAIN_DIR });
60
128
  res.json(result);
61
129
  } catch (error) {
@@ -86,15 +154,21 @@ router.put('/api/plugins/:id/configuration', requireAuth, requireScope('config:w
86
154
  /** Serve only modules declared by an installed plugin's UI manifest. */
87
155
  router.get('/api/plugins/:id/ui/:element', requireAuth, requireScope('config:read'), (req, res) => {
88
156
  try {
89
- const plugin = getPluginById(req.params.id, projectRoot());
90
- if (!plugin?.valid) return res.status(404).json({ error: 'Plugin unavailable' });
157
+ const target = surfaceTarget(req, req.query.brain);
158
+ const plugin = getPluginById(req.params.id, target.projectRoot);
159
+ if (!plugin?.valid || (req.query.brain !== undefined && !pluginActivation(plugin, target).enabled)) {
160
+ return res.status(404).json({ error: 'Plugin unavailable' });
161
+ }
91
162
  const element = plugin.manifest?.ui?.elements?.find((item) => item.id === req.params.element);
92
163
  if (!element) return res.status(404).json({ error: 'UI element unavailable' });
93
164
  const root = fs.realpathSync(plugin.dir);
94
165
  const file = fs.realpathSync(path.resolve(root, element.module));
95
166
  if (!file.startsWith(root + path.sep)) return res.status(400).json({ error: 'Invalid UI module path' });
96
167
  res.type('text/javascript').set('X-Content-Type-Options', 'nosniff').send(fs.readFileSync(file, 'utf8'));
97
- } catch (err) { serverError(res, err); }
168
+ } catch (err) {
169
+ if ([400, 403, 404, 409, 503].includes(err.status)) return res.status(err.status).json({ error: err.message });
170
+ serverError(res, err);
171
+ }
98
172
  });
99
173
 
100
174
  /** Serve only the logo a plugin declares in branding.logo, never an arbitrary file. */
@@ -121,15 +195,45 @@ router.get('/api/plugins/:id/logo', requireAuth, requireScope('config:read'), (r
121
195
  * Installed plugins (project + global): manifest facts, provenance, content
122
196
  * hash, sharing state and scheduled-task history. Nothing else.
123
197
  */
124
- router.get("/api/plugins", requireAuth, requireScope("config:read"), (_req, res) => {
198
+ router.get("/api/plugins", requireAuth, requireScope("config:read"), (req, res) => {
125
199
  try {
126
- const plugins = listInstalledPlugins(projectRoot());
200
+ const selection = req.query.brain;
201
+ if (selection !== undefined && typeof selection !== 'string') return badRequest(res, 'Use one comma-separated brain selector.');
202
+ const plugins = selection === undefined
203
+ ? listInstalledPlugins(projectRoot())
204
+ : selectedPluginInventory(resolvePluginBrainTargets(selection.split(','), req.auth));
127
205
  res.json({ success: true, count: plugins.length, plugins });
128
206
  } catch (err) {
207
+ if (err.status) return res.status(err.status).json({ error: err.message });
129
208
  serverError(res, err);
130
209
  }
131
210
  });
132
211
 
212
+ function selectedPluginInventory(targets) {
213
+ const descriptions = new Map();
214
+ const serving = new Map(discoverPlugins(projectRoot()).map(plugin => [plugin.id, plugin]));
215
+ const catalogs = targets.map(target => new Map(
216
+ listInstalledPlugins(target.projectRoot, { descriptions }).map(plugin => [plugin.id, plugin]),
217
+ ));
218
+ const ids = new Set(catalogs.flatMap(catalog => [...catalog.keys()]));
219
+ return [...ids].map(id => {
220
+ const entries = catalogs.map(catalog => catalog.get(id));
221
+ const representative = entries.find(plugin => plugin?.valid) || entries.find(Boolean);
222
+ const activationByBrain = entries.map((plugin, index) => {
223
+ const { brainId, name } = targets[index];
224
+ const state = !plugin ? 'unavailable' : !plugin.valid ? 'error' : (plugin.activation?.state || 'error');
225
+ const mode = state === 'inactive' && plugin.activation?.mode;
226
+ return { brainId, name, state, enabled: state === 'active', required: !!plugin?.activation?.required, ...(mode ? { mode } : {}) };
227
+ });
228
+ const first = activationByBrain[0];
229
+ const state = activationByBrain.every(item => item.state === first.state && item.mode === first.mode) ? first.state : 'mixed';
230
+ const activation = { state, enabled: state === 'active', required: activationByBrain.some(item => item.required),
231
+ ...(state !== 'mixed' && first.mode ? { mode: first.mode } : {}) };
232
+ return { ...representative, activation, activationByBrain,
233
+ servingAvailable: Boolean(serving.get(id) && serving.get(id).dir === representative.dir) };
234
+ });
235
+ }
236
+
133
237
  /**
134
238
  * GET /api/plugins/available
135
239
  * Plugins bundled with this Total Recall package, installable on any node.
@@ -277,28 +381,7 @@ router.get("/api/plugins/:id", requireAuth, requireScope("config:read"), (req, r
277
381
  try {
278
382
  const plugin = getPluginById(req.params.id, projectRoot());
279
383
  if (!plugin) return res.status(404).json({ success: false, error: `Plugin ${req.params.id} not found` });
280
- res.json({ success: true, plugin: { ...describePlugin(plugin), manifest: plugin.manifest } });
281
- } catch (err) {
282
- serverError(res, err);
283
- }
284
- });
285
-
286
- /**
287
- * PATCH /api/plugins/:id/card-face
288
- * Customizes card face skin, theme, colors, headline, quick actions, and metrics.
289
- */
290
- router.patch("/api/plugins/:id/card-face", requireAuth, requireScope("config:write"), (req, res) => {
291
- try {
292
- const plugin = getPluginById(req.params.id, projectRoot());
293
- if (!plugin) return res.status(404).json({ success: false, error: `Plugin ${req.params.id} not found` });
294
- const manifestPath = path.join(plugin.dir, "plugin.json");
295
- if (!fs.existsSync(manifestPath)) return res.status(404).json({ success: false, error: "Manifest not found" });
296
- const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
297
- const { card_face = {}, branding = {} } = req.body || {};
298
- manifest.card_face = { ...(manifest.card_face || {}), ...card_face };
299
- manifest.branding = { ...(manifest.branding || {}), ...branding };
300
- fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
301
- res.json({ success: true, card_face: manifest.card_face, branding: manifest.branding });
384
+ res.json({ success: true, plugin: { ...describePlugin(plugin, projectRoot()), manifest: plugin.manifest } });
302
385
  } catch (err) {
303
386
  serverError(res, err);
304
387
  }
@@ -312,16 +395,37 @@ router.patch("/api/plugins/:id/card-face", requireAuth, requireScope("config:wri
312
395
  * write-level regardless of that declaration.
313
396
  */
314
397
  router.post("/api/plugins/:id/run", requireAuth, (req, res, next) => {
315
- const { subcommand, args = [], stdin, execution } = req.body || {};
398
+ const { subcommand, args = [], stdin, execution, brainId } = req.body || {};
316
399
  try {
317
- const plugin = getPluginById(req.params.id, projectRoot());
318
- const operation = plugin?.valid && plugin.manifest?.cli?.subcommands?.find(item => item.name === subcommand);
400
+ const target = surfaceTarget(req, brainId);
401
+ const plugin = getPluginById(req.params.id, target.projectRoot);
402
+ if (brainId !== undefined && execution) {
403
+ return res.status(400).json({ success: false, error: 'Use the selected brain with local plugin execution.' });
404
+ }
405
+ if (brainId !== undefined && ['on', 'off'].includes(subcommand || args?.[0])) {
406
+ return res.status(400).json({ success: false, error: 'Use the selected brain activation control.' });
407
+ }
408
+ const declared = plugin?.valid ? plugin.manifest?.cli?.subcommands || [] : [];
409
+ const operation = declared.find(item => item.name === subcommand);
410
+ const activation = ['on', 'off'].includes(subcommand) && Array.isArray(args) && !args.length && stdin === undefined && !execution;
411
+ const browserBlocked = item => item.sensitive_output === true || item.terminal_only === true;
412
+ if (operation ? browserBlocked(operation) : !activation && declared.some(browserBlocked)) {
413
+ return res.status(403).json({ success: false, error: 'Use the owning CLI or account controls for this operation.' });
414
+ }
415
+ const terminalFlags = new Set((operation ? [operation] : declared).flatMap(item => item.terminal_only_args ?? []));
416
+ if (Array.isArray(args) && args.some(arg => typeof arg === 'string' && terminalFlags.has(arg.split('=', 1)[0]))) {
417
+ return res.status(403).json({ success: false, error: 'Use the owning CLI for terminal-only arguments.' });
418
+ }
319
419
  res.locals.executionPlugin = plugin;
420
+ res.locals.executionTarget = target;
320
421
  const read = typeof subcommand === 'string' && Array.isArray(args) && args.every(arg => typeof arg === 'string') &&
321
422
  operation?.effect === 'read' && stdin === undefined && !execution &&
322
423
  !operation.args?.some(arg => arg.secret);
323
424
  return requireScope(read ? 'config:read' : 'config:write')(req, res, next);
324
- } catch (error) { badRequest(res, error.message); }
425
+ } catch (error) {
426
+ if ([400, 403, 404, 409, 503].includes(error.status)) return res.status(error.status).json({ error: error.message });
427
+ badRequest(res, error.message);
428
+ }
325
429
  }, async (req, res) => {
326
430
  try {
327
431
  const { subcommand = "", args = [], stdin, execution } = req.body || {};
@@ -329,21 +433,26 @@ router.post("/api/plugins/:id/run", requireAuth, (req, res, next) => {
329
433
  return badRequest(res, "subcommand must be a string and args an array of strings");
330
434
  }
331
435
  const plugin = res.locals.executionPlugin;
436
+ const target = res.locals.executionTarget;
437
+ const cwd = target.projectRoot ?? target.brainDir;
332
438
  if (!plugin) return res.status(404).json({ success: false, error: `Plugin ${req.params.id} not found` });
333
439
  if (!plugin.valid) return badRequest(res, `Plugin ${plugin.id} has an invalid manifest: ${plugin.errors.join("; ")}`);
334
440
  if (!plugin.manifest?.cli?.handler) return badRequest(res, `Plugin ${plugin.id} does not declare a CLI handler`);
441
+ if (req.body?.brainId !== undefined && !pluginActivation(plugin, target).enabled) {
442
+ return res.status(403).json({ success: false, error: 'Plugin is off in the selected brain.' });
443
+ }
335
444
  validatePluginSecretInput(plugin, subcommand, stdin);
336
445
 
337
446
  if (execution) {
338
447
  if (stdin !== undefined) return badRequest(res, 'Credential operations require direct local execution');
339
- if (!pluginActivation(plugin, { projectRoot: projectRoot() }).enabled && !['on', 'off', 'status', 'health', 'jobs', '--help', 'help'].includes(subcommand)) return badRequest(res, `Plugin ${plugin.id} is inactive`);
448
+ if (!pluginActivation(plugin, target).enabled && !['on', 'off', 'status', 'health', 'jobs', '--help', 'help'].includes(subcommand)) return badRequest(res, `Plugin ${plugin.id} is inactive`);
340
449
  const { loadPluginModule } = await import('../../core/plugin-runtime.mjs');
341
- const owner = await loadPluginModule('meta-harness', 'command-executor', { activeRequired: true });
342
- const result = await owner.executeCommand({ plugin, subcommand, args, execution, projectRoot: projectRoot() });
450
+ const owner = await loadPluginModule('meta-harness', 'command-executor', { activeRequired: true, projectRoot: target.projectRoot });
451
+ const result = await owner.executeCommand({ plugin, subcommand, args, execution, projectRoot: cwd });
343
452
  return res.json({ success: result.ok, pluginId: plugin.id, ...result });
344
453
  }
345
454
 
346
- const result = await runPluginCommand(plugin, { subcommand, args, ...(stdin !== undefined ? { stdinText: stdin } : {}), cwd: projectRoot(), secretsBrainDir: process.env.TR_SECRETS_BRAIN || globalBrainDir || BRAIN_DIR, maxOutputBytes: 4 * 1024 * 1024 });
455
+ const result = await runPluginCommand(plugin, { subcommand, args, ...(stdin !== undefined ? { stdinText: stdin } : {}), cwd, projectRoot: target.projectRoot, secretsBrainDir: process.env.TR_SECRETS_BRAIN || globalBrainDir || BRAIN_DIR, maxOutputBytes: 4 * 1024 * 1024 });
347
456
  res.json({ success: result.ok, pluginId: plugin.id, ...result });
348
457
  } catch (err) {
349
458
  badRequest(res, err.message);
@@ -0,0 +1,192 @@
1
+ import { Router } from 'express';
2
+ import { addWorkspaceUiChunk, getUiChunk, readPluginDesign, searchUiChunks, UiChunkInputError } from '../../core/ui-chunks.mjs';
3
+ import { chooseUiComposition } from '../../core/ui-chunk-choice.mjs';
4
+ import {
5
+ createWorkspacePage, listWorkspacePages, prepareWorkspacePage, publishWorkspaceComposition,
6
+ readWorkspaceComposition, readWorkspacePage, saveWorkspaceCompositionPage,
7
+ updateWorkspacePage,
8
+ } from '../../core/workspace-compositions.mjs';
9
+ import { requireAuth, requireScope } from '../auth.mjs';
10
+ import { resolvePluginBrainTargets } from '../plugin-brain-targets.mjs';
11
+ import { BRAIN_DIR } from './_shared.mjs';
12
+
13
+ const router = Router();
14
+
15
+ function workspaceTarget(req, brainId) {
16
+ return brainId === undefined
17
+ ? { brainDir: BRAIN_DIR, projectRoot: process.cwd() }
18
+ : resolvePluginBrainTargets([brainId], req.auth)[0];
19
+ }
20
+
21
+ function sendScopeError(res, error) {
22
+ if ([400, 403, 404, 409, 503].includes(error.status)) {
23
+ res.status(error.status).json({ error: error.message });
24
+ return true;
25
+ }
26
+ return false;
27
+ }
28
+
29
+ function sendWorkspaceData(res, read, resource = 'UI chunk') {
30
+ res.set('Cache-Control', 'private, no-store').set('X-Content-Type-Options', 'nosniff');
31
+ try {
32
+ const result = read();
33
+ if (!result) return res.status(404).json({ error: `${resource} unavailable` });
34
+ return res.json(result);
35
+ } catch (error) {
36
+ if (error instanceof UiChunkInputError) return res.status(400).json({ error: error.message });
37
+ if (sendScopeError(res, error)) return;
38
+ return res.status(500).json({ error: resource === 'UI chunk' ? 'Could not read the UI chunk catalog' : 'Could not read saved pages' });
39
+ }
40
+ }
41
+
42
+ router.get('/api/ui-chunks', requireAuth, requireScope('config:read'), (req, res) => {
43
+ const { plugin, surface, q, limit, offset } = req.query;
44
+ return sendWorkspaceData(res, () => {
45
+ if (limit !== undefined && (typeof limit !== 'string' || !/^\d{1,3}$/.test(limit))) {
46
+ throw new UiChunkInputError('Limit must be between 1 and 200.');
47
+ }
48
+ if (offset !== undefined && (typeof offset !== 'string' || !/^\d{1,16}$/.test(offset))) {
49
+ throw new UiChunkInputError('Offset must be a nonnegative integer.');
50
+ }
51
+ return searchUiChunks({
52
+ ...workspaceTarget(req, req.query.brain), plugin, surface, query: q,
53
+ limit: limit === undefined ? undefined : Number(limit),
54
+ offset: offset === undefined ? undefined : Number(offset),
55
+ });
56
+ });
57
+ });
58
+
59
+ router.get('/api/ui-chunks/:plugin/:id', requireAuth, requireScope('config:read'), (req, res) =>
60
+ sendWorkspaceData(res, () => {
61
+ const chunk = getUiChunk(req.params.plugin, req.params.id, { ...workspaceTarget(req, req.query.brain), surface: req.query.surface });
62
+ return chunk ? { chunk } : null;
63
+ }));
64
+
65
+ router.get('/api/ui-chunks/:plugin/design/revision', requireAuth, requireScope('config:read'), (req, res) =>
66
+ sendWorkspaceData(res, () => {
67
+ const { revision } = req.query;
68
+ if (revision !== undefined && (typeof revision !== 'string' || !/^[a-f0-9]{64}$/.test(revision))) {
69
+ throw new UiChunkInputError('Invalid design revision.');
70
+ }
71
+ return readPluginDesign(req.params.plugin, { ...workspaceTarget(req, req.query.brain), revision });
72
+ }));
73
+
74
+ router.get('/api/ui-chunks/:plugin/composition/current', requireAuth, requireScope('config:read'), (req, res) =>
75
+ sendWorkspaceData(res, () => {
76
+ const { brainDir, projectRoot } = workspaceTarget(req, req.query.brain);
77
+ return readWorkspaceComposition(brainDir, req.params.plugin, projectRoot);
78
+ }));
79
+
80
+ function workspacePagePath(id, brain) {
81
+ return '/memory?' + new URLSearchParams({ page: id, brain });
82
+ }
83
+
84
+ router.get('/api/workspace/pages', requireAuth, requireScope('config:read'), (req, res) =>
85
+ sendWorkspaceData(res, () => {
86
+ const { brainDir } = resolvePluginBrainTargets([req.query.brain], req.auth)[0];
87
+ return { pages: listWorkspacePages(brainDir) };
88
+ }, 'Saved page'));
89
+
90
+ router.get('/api/workspace/pages/:id', requireAuth, requireScope('config:read'), (req, res) =>
91
+ sendWorkspaceData(res, () => {
92
+ const { brainDir, projectRoot } = resolvePluginBrainTargets([req.query.brain], req.auth)[0];
93
+ return readWorkspacePage(brainDir, req.params.id, projectRoot);
94
+ }, 'Saved page'));
95
+
96
+ router.post('/api/workspace/pages', requireAuth, requireScope('config:write'), async (req, res) => {
97
+ res.set('Cache-Control', 'private, no-store');
98
+ if (!req.is('application/json')) return res.status(415).json({ error: 'Use JSON to save a page.' });
99
+ try {
100
+ const { id, title, brainId, sourcePlugin, sourceVersion, keys } = req.body ?? {};
101
+ const { brainDir, projectRoot } = resolvePluginBrainTargets([brainId], req.auth)[0];
102
+ if (keys !== undefined && (sourcePlugin !== undefined || sourceVersion !== undefined)) {
103
+ throw new UiChunkInputError('Supply either ordered block keys or a source Home revision.');
104
+ }
105
+ const page = keys === undefined
106
+ ? await saveWorkspaceCompositionPage(brainDir, { id, title, sourcePlugin, sourceVersion }, projectRoot)
107
+ : await createWorkspacePage(brainDir, { id, title, keys }, projectRoot);
108
+ return res.status(201).json({ ...page, path: workspacePagePath(id, brainId) });
109
+ } catch (error) {
110
+ if (error instanceof UiChunkInputError) return res.status(400).json({ error: error.message });
111
+ if (sendScopeError(res, error)) return;
112
+ return res.status(500).json({ error: 'Could not save the page.' });
113
+ }
114
+ });
115
+
116
+ router.put('/api/workspace/pages/:id', requireAuth, requireScope('config:write'), async (req, res) => {
117
+ res.set('Cache-Control', 'private, no-store');
118
+ if (!req.is('application/json')) return res.status(415).json({ error: 'Use JSON to edit a page.' });
119
+ try {
120
+ const { brainId, revision, keys } = req.body ?? {};
121
+ const { id } = req.params;
122
+ const { brainDir, projectRoot } = resolvePluginBrainTargets([brainId], req.auth)[0];
123
+ const page = await updateWorkspacePage(brainDir, { id, revision, keys }, projectRoot);
124
+ return res.json({ ...page, path: workspacePagePath(id, brainId) });
125
+ } catch (error) {
126
+ if (error instanceof UiChunkInputError) return res.status(400).json({ error: error.message });
127
+ if (sendScopeError(res, error)) return;
128
+ if (error.message.startsWith('Plugin state version conflict')) {
129
+ return res.status(409).json({ error: 'The saved page changed. Read its current revision before editing.' });
130
+ }
131
+ return res.status(500).json({ error: 'Could not edit the saved page.' });
132
+ }
133
+ });
134
+
135
+ router.post('/api/ui-chunks/pool', requireAuth, requireScope('config:write'), async (req, res) => {
136
+ res.set('Cache-Control', 'private, no-store');
137
+ if (!req.is('application/json')) return res.status(415).json({ error: 'Use JSON to add a block.' });
138
+ try {
139
+ const { brainDir, projectRoot } = workspaceTarget(req, req.body?.brainId);
140
+ return res.status(201).json(await addWorkspaceUiChunk(brainDir, req.body ?? {}, projectRoot));
141
+ } catch (error) {
142
+ if (error instanceof UiChunkInputError) return res.status(400).json({ error: error.message });
143
+ if (sendScopeError(res, error)) return;
144
+ if (error.message.startsWith('Plugin state version conflict')) return res.status(409).json({ error: 'That workspace block ID already exists.' });
145
+ return res.status(500).json({ error: 'Could not add the block to the pool.' });
146
+ }
147
+ });
148
+
149
+ router.post(['/api/ui-chunks/suggest', '/api/ui-chunks/compose'], requireAuth, requireScope('config:write'), async (req, res) => {
150
+ res.set('Cache-Control', 'private, no-store');
151
+ if (!req.is('application/json')) return res.status(415).json({ error: 'Use JSON to request a block suggestion.' });
152
+ const { plugin, query, brainId, pageId, pageTitle } = req.body ?? {};
153
+ if (typeof plugin !== 'string' || typeof query !== 'string' || !query.trim()) {
154
+ return res.status(400).json({ error: 'Choose a plugin and describe what you want to do.' });
155
+ }
156
+ try {
157
+ const publish = req.path === '/api/ui-chunks/compose';
158
+ const named = pageId !== undefined || pageTitle !== undefined;
159
+ if (named && !publish) throw new UiChunkInputError('Use compose to publish a saved page.');
160
+ const resolveDestination = () => named
161
+ ? resolvePluginBrainTargets([brainId], req.auth)[0]
162
+ : workspaceTarget(req, brainId);
163
+ const target = resolveDestination();
164
+ if (named) prepareWorkspacePage(target.brainDir, pageId, pageTitle);
165
+ const current = publish && !named ? readWorkspaceComposition(target.brainDir, plugin, target.projectRoot) : null;
166
+ const { chunks } = searchUiChunks({ ...target, plugin: publish ? undefined : plugin, surface: 'page', query, limit: 20 });
167
+ const selection = await chooseUiComposition(chunks, query.trim(), undefined, target);
168
+ if (!publish) return res.json({ chunks, selection });
169
+ if (selection.source !== 'decision' || !selection.keys.length) {
170
+ return res.status(422).json({ error: 'Jev did not select a page to publish.', selection });
171
+ }
172
+ const destination = resolveDestination();
173
+ if (target.brainDir !== destination.brainDir || target.projectRoot !== destination.projectRoot) {
174
+ return res.status(409).json({ error: 'The destination brain changed while Jev was composing.' });
175
+ }
176
+ if (named) {
177
+ const page = await createWorkspacePage(destination.brainDir, { id: pageId, title: pageTitle, keys: selection.keys }, destination.projectRoot);
178
+ return res.json({ selection, version: page.version, path: workspacePagePath(pageId, brainId) });
179
+ }
180
+ const state = await publishWorkspaceComposition(target.brainDir, plugin, selection.keys, current.version, target.projectRoot);
181
+ const params = new URLSearchParams({ plugin });
182
+ if (destination.brainId) params.set('brain', destination.brainId);
183
+ return res.json({ selection, version: state.version, path: '/memory?' + params });
184
+ } catch (error) {
185
+ if (error instanceof UiChunkInputError) return res.status(400).json({ error: error.message });
186
+ if (sendScopeError(res, error)) return;
187
+ if (error.message.startsWith('Plugin state version conflict')) return res.status(409).json({ error: 'The page changed while Jev was composing it.' });
188
+ return res.status(500).json({ error: 'Could not suggest a page block.' });
189
+ }
190
+ });
191
+
192
+ export default router;