groove-dev 0.27.143 → 0.27.145

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 (251) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +18 -48
  6. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
  7. package/node_modules/@groove-dev/daemon/src/index.js +3 -0
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
  9. package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
  10. package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
  11. package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
  12. package/node_modules/@groove-dev/daemon/src/process.js +2 -2
  13. package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
  14. package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
  15. package/node_modules/@groove-dev/daemon/src/routes/agents.js +812 -0
  16. package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
  17. package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
  18. package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
  19. package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
  20. package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
  21. package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
  22. package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
  23. package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
  24. package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
  25. package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
  26. package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
  27. package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
  28. package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
  29. package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
  30. package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
  31. package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
  32. package/node_modules/@groove-dev/gui/dist/assets/index-Bxc0gU06.js +1006 -0
  33. package/node_modules/@groove-dev/gui/dist/assets/index-C0pztKBn.css +1 -0
  34. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  35. package/node_modules/@groove-dev/gui/package.json +1 -1
  36. package/node_modules/@groove-dev/gui/src/{app.jsx → App.jsx} +0 -2
  37. package/node_modules/@groove-dev/gui/src/app.css +35 -0
  38. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
  39. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +210 -112
  40. package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
  41. package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -70
  42. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
  43. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
  44. package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
  45. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
  46. package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
  47. package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
  48. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +2 -0
  49. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +68 -66
  50. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +4 -8
  51. package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
  52. package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
  53. package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
  54. package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  55. package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
  56. package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
  57. package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
  58. package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  59. package/node_modules/@groove-dev/gui/src/components/dashboard/token-chart.jsx +4 -4
  60. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +39 -31
  61. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
  62. package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
  63. package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +200 -18
  64. package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +17 -14
  65. package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +335 -152
  66. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  67. package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
  68. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
  69. package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
  70. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
  71. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
  72. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
  73. package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
  74. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +8 -8
  75. package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
  76. package/node_modules/@groove-dev/gui/src/lib/status.js +25 -24
  77. package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
  78. package/node_modules/@groove-dev/gui/src/stores/groove.js +51 -3144
  79. package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
  80. package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +459 -0
  81. package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
  82. package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +226 -0
  83. package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
  84. package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
  85. package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
  86. package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
  87. package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
  88. package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
  89. package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
  90. package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
  91. package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
  92. package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
  93. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +54 -12
  94. package/node_modules/@groove-dev/gui/src/views/models.jsx +419 -496
  95. package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
  96. package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
  97. package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
  98. package/node_modules/axios/CHANGELOG.md +260 -0
  99. package/node_modules/axios/README.md +595 -223
  100. package/node_modules/axios/dist/axios.js +1460 -1090
  101. package/node_modules/axios/dist/axios.js.map +1 -1
  102. package/node_modules/axios/dist/axios.min.js +3 -3
  103. package/node_modules/axios/dist/axios.min.js.map +1 -1
  104. package/node_modules/axios/dist/browser/axios.cjs +1560 -1132
  105. package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
  106. package/node_modules/axios/dist/esm/axios.js +1557 -1128
  107. package/node_modules/axios/dist/esm/axios.js.map +1 -1
  108. package/node_modules/axios/dist/esm/axios.min.js +2 -2
  109. package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
  110. package/node_modules/axios/dist/node/axios.cjs +1594 -1057
  111. package/node_modules/axios/dist/node/axios.cjs.map +1 -1
  112. package/node_modules/axios/index.d.cts +40 -41
  113. package/node_modules/axios/index.d.ts +151 -227
  114. package/node_modules/axios/index.js +2 -0
  115. package/node_modules/axios/lib/adapters/adapters.js +4 -2
  116. package/node_modules/axios/lib/adapters/fetch.js +147 -16
  117. package/node_modules/axios/lib/adapters/http.js +306 -58
  118. package/node_modules/axios/lib/adapters/xhr.js +6 -2
  119. package/node_modules/axios/lib/core/Axios.js +7 -3
  120. package/node_modules/axios/lib/core/AxiosError.js +120 -34
  121. package/node_modules/axios/lib/core/AxiosHeaders.js +27 -25
  122. package/node_modules/axios/lib/core/buildFullPath.js +1 -1
  123. package/node_modules/axios/lib/core/dispatchRequest.js +19 -7
  124. package/node_modules/axios/lib/core/mergeConfig.js +21 -4
  125. package/node_modules/axios/lib/core/settle.js +7 -11
  126. package/node_modules/axios/lib/defaults/index.js +14 -9
  127. package/node_modules/axios/lib/env/data.js +1 -1
  128. package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -2
  129. package/node_modules/axios/lib/helpers/buildURL.js +1 -1
  130. package/node_modules/axios/lib/helpers/cookies.js +14 -2
  131. package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
  132. package/node_modules/axios/lib/helpers/formDataToJSON.js +3 -1
  133. package/node_modules/axios/lib/helpers/formDataToStream.js +3 -2
  134. package/node_modules/axios/lib/helpers/parseProtocol.js +1 -1
  135. package/node_modules/axios/lib/helpers/progressEventReducer.js +5 -5
  136. package/node_modules/axios/lib/helpers/resolveConfig.js +54 -18
  137. package/node_modules/axios/lib/helpers/shouldBypassProxy.js +74 -2
  138. package/node_modules/axios/lib/helpers/toFormData.js +10 -2
  139. package/node_modules/axios/lib/helpers/validator.js +3 -1
  140. package/node_modules/axios/lib/utils.js +33 -21
  141. package/node_modules/axios/package.json +17 -24
  142. package/node_modules/follow-redirects/README.md +7 -5
  143. package/node_modules/follow-redirects/index.js +24 -1
  144. package/node_modules/follow-redirects/package.json +1 -1
  145. package/package.json +1 -1
  146. package/packages/cli/package.json +1 -1
  147. package/packages/daemon/package.json +1 -1
  148. package/packages/daemon/src/api.js +1086 -6532
  149. package/packages/daemon/src/conversations.js +18 -48
  150. package/packages/daemon/src/gateways/manager.js +35 -1
  151. package/packages/daemon/src/index.js +3 -0
  152. package/packages/daemon/src/journalist.js +23 -13
  153. package/packages/daemon/src/mlx-server.js +365 -0
  154. package/packages/daemon/src/model-lab.js +308 -12
  155. package/packages/daemon/src/pm.js +1 -1
  156. package/packages/daemon/src/process.js +2 -2
  157. package/packages/daemon/src/providers/local.js +36 -8
  158. package/packages/daemon/src/registry.js +21 -5
  159. package/packages/daemon/src/routes/agents.js +812 -0
  160. package/packages/daemon/src/routes/coordination.js +318 -0
  161. package/packages/daemon/src/routes/files.js +751 -0
  162. package/packages/daemon/src/routes/integrations.js +485 -0
  163. package/packages/daemon/src/routes/network.js +1784 -0
  164. package/packages/daemon/src/routes/providers.js +755 -0
  165. package/packages/daemon/src/routes/schedules.js +110 -0
  166. package/packages/daemon/src/routes/teams.js +650 -0
  167. package/packages/daemon/src/scheduler.js +456 -24
  168. package/packages/daemon/src/teams.js +1 -1
  169. package/packages/daemon/src/validate.js +38 -1
  170. package/packages/daemon/templates/mlx-setup.json +12 -0
  171. package/packages/daemon/templates/tgi-setup.json +1 -1
  172. package/packages/daemon/templates/vllm-setup.json +1 -1
  173. package/packages/gui/dist/assets/index-Bxc0gU06.js +1006 -0
  174. package/packages/gui/dist/assets/index-C0pztKBn.css +1 -0
  175. package/packages/gui/dist/index.html +2 -2
  176. package/packages/gui/package.json +1 -1
  177. package/packages/gui/src/{app.jsx → App.jsx} +0 -2
  178. package/packages/gui/src/app.css +35 -0
  179. package/packages/gui/src/components/agents/agent-config.jsx +1 -128
  180. package/packages/gui/src/components/agents/agent-feed.jsx +210 -112
  181. package/packages/gui/src/components/agents/agent-node.jsx +8 -13
  182. package/packages/gui/src/components/agents/agent-panel.jsx +2 -70
  183. package/packages/gui/src/components/agents/code-review.jsx +159 -122
  184. package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
  185. package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
  186. package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
  187. package/packages/gui/src/components/automations/automation-card.jsx +274 -0
  188. package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
  189. package/packages/gui/src/components/chat/chat-header.jsx +2 -0
  190. package/packages/gui/src/components/chat/chat-input.jsx +68 -66
  191. package/packages/gui/src/components/chat/chat-view.jsx +4 -8
  192. package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
  193. package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
  194. package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
  195. package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
  196. package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
  197. package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
  198. package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
  199. package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
  200. package/packages/gui/src/components/dashboard/token-chart.jsx +4 -4
  201. package/packages/gui/src/components/lab/chat-playground.jsx +39 -31
  202. package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
  203. package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
  204. package/packages/gui/src/components/lab/parameter-panel.jsx +200 -18
  205. package/packages/gui/src/components/lab/preset-manager.jsx +17 -14
  206. package/packages/gui/src/components/lab/runtime-config.jsx +335 -152
  207. package/packages/gui/src/components/lab/system-prompt-editor.jsx +10 -8
  208. package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
  209. package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
  210. package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
  211. package/packages/gui/src/components/network/network-health.jsx +2 -2
  212. package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
  213. package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
  214. package/packages/gui/src/components/ui/sheet.jsx +5 -2
  215. package/packages/gui/src/components/ui/slider.jsx +8 -8
  216. package/packages/gui/src/lib/cron.js +64 -0
  217. package/packages/gui/src/lib/status.js +25 -24
  218. package/packages/gui/src/lib/theme-hex.js +1 -0
  219. package/packages/gui/src/stores/groove.js +51 -3144
  220. package/packages/gui/src/stores/helpers.js +10 -0
  221. package/packages/gui/src/stores/slices/agents-slice.js +459 -0
  222. package/packages/gui/src/stores/slices/automations-slice.js +96 -0
  223. package/packages/gui/src/stores/slices/chat-slice.js +226 -0
  224. package/packages/gui/src/stores/slices/editor-slice.js +285 -0
  225. package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
  226. package/packages/gui/src/stores/slices/network-slice.js +361 -0
  227. package/packages/gui/src/stores/slices/preview-slice.js +109 -0
  228. package/packages/gui/src/stores/slices/providers-slice.js +897 -0
  229. package/packages/gui/src/stores/slices/teams-slice.js +413 -0
  230. package/packages/gui/src/stores/slices/ui-slice.js +98 -0
  231. package/packages/gui/src/views/agents.jsx +5 -5
  232. package/packages/gui/src/views/dashboard.jsx +12 -13
  233. package/packages/gui/src/views/marketplace.jsx +191 -3
  234. package/packages/gui/src/views/model-lab.jsx +54 -12
  235. package/packages/gui/src/views/models.jsx +419 -496
  236. package/packages/gui/src/views/network.jsx +3 -3
  237. package/packages/gui/src/views/settings.jsx +81 -94
  238. package/packages/gui/src/views/teams.jsx +40 -483
  239. package/SECURITY_SWEEP.md +0 -228
  240. package/TRAINING_DATA_v4.md +0 -6
  241. package/node_modules/@groove-dev/gui/dist/assets/index-CCVvAoQn.css +0 -1
  242. package/node_modules/@groove-dev/gui/dist/assets/index-DGIv_TRm.js +0 -984
  243. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -379
  244. package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
  245. package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
  246. package/packages/gui/dist/assets/index-CCVvAoQn.css +0 -1
  247. package/packages/gui/dist/assets/index-DGIv_TRm.js +0 -984
  248. package/packages/gui/src/components/agents/agent-chat.jsx +0 -379
  249. package/packages/gui/src/views/preview.jsx +0 -6
  250. package/packages/gui/src/views/subscription-panel.jsx +0 -327
  251. package/test.py +0 -571
@@ -0,0 +1,812 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+
3
+ import { resolve } from 'path';
4
+ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'fs';
5
+ import { validateAgentConfig, validateReasoningEffort, validateVerbosity } from '../validate.js';
6
+ import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from '../process.js';
7
+ import { getProvider } from '../providers/index.js';
8
+
9
+ export function registerAgentRoutes(app, daemon) {
10
+ // List all agents
11
+ app.get('/api/agents', (req, res) => {
12
+ res.json(daemon.registry.getAll());
13
+ });
14
+
15
+ // Get single agent
16
+ app.get('/api/agents/:id', (req, res) => {
17
+ const agent = daemon.registry.get(req.params.id);
18
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
19
+ res.json(agent);
20
+ });
21
+
22
+ // Spawn a new agent
23
+ app.post('/api/agents', async (req, res) => {
24
+ try {
25
+ const config = validateAgentConfig(req.body);
26
+ config.teamId = req.body.teamId || daemon.teams.getDefault()?.id || null;
27
+ // Inherit team working directory if agent doesn't specify one
28
+ if (!config.workingDir) {
29
+ const team = daemon.teams.get(config.teamId);
30
+ if (team?.workingDir) config.workingDir = team.workingDir;
31
+ }
32
+ // Inherit configured defaults if the request didn't pick them
33
+ if (!config.provider && daemon.config?.defaultProvider) {
34
+ config.provider = daemon.config.defaultProvider;
35
+ }
36
+ if (!config.model && daemon.config?.defaultModel) {
37
+ config.model = daemon.config.defaultModel;
38
+ }
39
+ const agent = await daemon.processes.spawn(config);
40
+ daemon.audit.log('agent.spawn', { id: agent.id, role: agent.role, provider: agent.provider });
41
+ res.status(201).json(agent);
42
+ } catch (err) {
43
+ res.status(400).json({ error: err.message });
44
+ }
45
+ });
46
+
47
+ // Update agent
48
+ app.patch('/api/agents/:id', (req, res) => {
49
+ const agent = daemon.registry.update(req.params.id, req.body);
50
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
51
+ res.json(agent);
52
+ });
53
+
54
+ // Kill an agent (add ?purge=true to also remove from registry)
55
+ app.delete('/api/agents/:id', async (req, res) => {
56
+ try {
57
+ const agent = daemon.registry.get(req.params.id);
58
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
59
+
60
+ // Always attempt kill — handles race where GUI sees 'running' but daemon
61
+ // already marked the agent completed (common with fast non-interactive
62
+ // providers like Gemini). processes.kill() is a no-op when no handle exists.
63
+ await daemon.processes.kill(req.params.id);
64
+
65
+ // Only purge from registry when explicitly requested.
66
+ // Killed/completed agents stay visible so the user can review output.
67
+ const purge = req.query.purge === 'true';
68
+ if (purge) {
69
+ daemon.registry.remove(req.params.id);
70
+ }
71
+
72
+ daemon.audit.log('agent.kill', { id: agent.id, role: agent.role, purged: purge });
73
+ res.json({ ok: true, purged: purge });
74
+ } catch (err) {
75
+ res.status(400).json({ error: err.message });
76
+ }
77
+ });
78
+
79
+ // Kill all agents and purge registry (used by groove nuke)
80
+ app.delete('/api/agents', async (req, res) => {
81
+ const count = daemon.processes.getRunningCount();
82
+ await daemon.processes.killAll();
83
+ // Purge all agents from registry — kill() no longer does this automatically
84
+ for (const agent of daemon.registry.getAll()) {
85
+ daemon.registry.remove(agent.id);
86
+ }
87
+ daemon.audit.log('agent.kill_all', { count });
88
+ res.json({ ok: true });
89
+ });
90
+
91
+ // --- Role-to-Integration Mapping ---
92
+
93
+ app.get('/api/roles/integrations', (req, res) => {
94
+ const roleFilter = req.query.role;
95
+ const entries = roleFilter ? { [roleFilter]: ROLE_INTEGRATIONS[roleFilter] || [] } : ROLE_INTEGRATIONS;
96
+ const result = {};
97
+ for (const [role, ids] of Object.entries(entries)) {
98
+ result[role] = (ids || []).map((id) => {
99
+ const status = daemon.integrations.getStatus(id);
100
+ const entry = daemon.integrations.registry.find((r) => r.id === id);
101
+ return {
102
+ id,
103
+ name: entry?.name || id,
104
+ installed: status?.installed || false,
105
+ configured: status?.configured || false,
106
+ authenticated: status?.authenticated || false,
107
+ };
108
+ });
109
+ }
110
+ if (roleFilter) return res.json(result[roleFilter] || []);
111
+ res.json(result);
112
+ });
113
+
114
+ app.post('/api/agents/preflight', (req, res) => {
115
+ const { role, integrations } = req.body || {};
116
+ if (!role || !Array.isArray(integrations)) {
117
+ return res.status(400).json({ error: 'role and integrations[] required' });
118
+ }
119
+ const issues = [];
120
+ for (const id of integrations) {
121
+ const status = daemon.integrations.getStatus(id);
122
+ const entry = daemon.integrations.registry.find((r) => r.id === id);
123
+ const name = entry?.name || id;
124
+ if (!status || !status.installed) {
125
+ issues.push({ integrationId: id, name, problem: 'not_installed' });
126
+ } else if (!status.configured) {
127
+ issues.push({ integrationId: id, name, problem: 'not_configured' });
128
+ } else if (!status.authenticated) {
129
+ issues.push({ integrationId: id, name, problem: 'not_authenticated' });
130
+ }
131
+ }
132
+ res.json({ ready: issues.length === 0, issues });
133
+ });
134
+
135
+ // --- Agent Integration Attach/Detach ---
136
+
137
+ app.post('/api/agents/:id/integrations/:integrationId', (req, res) => {
138
+ const agent = daemon.registry.get(req.params.id);
139
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
140
+
141
+ const integrationId = req.params.integrationId;
142
+ const status = daemon.integrations.getStatus(integrationId);
143
+ if (!status || !status.installed) {
144
+ return res.status(400).json({ error: `Integration not installed: ${integrationId}` });
145
+ }
146
+
147
+ const integrations = new Set(agent.integrations || []);
148
+ integrations.add(integrationId);
149
+ const updated = Array.from(integrations);
150
+
151
+ daemon.registry.update(req.params.id, { integrations: updated });
152
+ daemon.integrations.writeMcpJson(daemon.integrations.getActiveIntegrations());
153
+ daemon.integrations.refreshMcpJson();
154
+ daemon.audit.log('agent.integration.attach', { agentId: req.params.id, integrationId });
155
+ daemon.broadcast({ type: 'agent:integration:attach', agentId: req.params.id, integrationId });
156
+ res.json({ ok: true, integrations: updated });
157
+ });
158
+
159
+ app.delete('/api/agents/:id/integrations/:integrationId', (req, res) => {
160
+ const agent = daemon.registry.get(req.params.id);
161
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
162
+
163
+ const integrationId = req.params.integrationId;
164
+ const integrations = (agent.integrations || []).filter((id) => id !== integrationId);
165
+
166
+ daemon.registry.update(req.params.id, { integrations });
167
+ daemon.integrations.refreshMcpJson();
168
+ daemon.audit.log('agent.integration.detach', { agentId: req.params.id, integrationId });
169
+ daemon.broadcast({ type: 'agent:integration:detach', agentId: req.params.id, integrationId });
170
+ res.json({ ok: true, integrations });
171
+ });
172
+
173
+ // --- Agent Routing ---
174
+
175
+ app.post('/api/agents/:id/routing', (req, res) => {
176
+ daemon.router.setMode(req.params.id, req.body.mode, {
177
+ fixedModel: req.body.fixedModel,
178
+ floorModel: req.body.floorModel,
179
+ });
180
+ res.json(daemon.router.getMode(req.params.id));
181
+ });
182
+
183
+ app.get('/api/agents/:id/routing/recommend', (req, res) => {
184
+ const rec = daemon.router.recommend(req.params.id);
185
+ if (!rec) return res.status(404).json({ error: 'Agent not found' });
186
+ res.json(rec);
187
+ });
188
+
189
+ // Downshift suggestion — NEVER auto-applied. User must accept via UI.
190
+ // Returns null (204) when classifier has no strong suggestion.
191
+ app.get('/api/agents/:id/routing/suggestion', (req, res) => {
192
+ const suggestion = daemon.router.getSuggestion(req.params.id);
193
+ if (!suggestion) return res.status(204).send();
194
+ res.json(suggestion);
195
+ });
196
+
197
+ // --- Conversations ---
198
+
199
+ app.get('/api/conversations', (req, res) => {
200
+ res.json({ conversations: daemon.conversations.list() });
201
+ });
202
+
203
+ app.post('/api/conversations', async (req, res) => {
204
+ try {
205
+ const { provider, model, title, mode, reasoning_effort, verbosity } = req.body;
206
+ if (provider && typeof provider !== 'string') {
207
+ return res.status(400).json({ error: 'provider must be a string' });
208
+ }
209
+ if (mode && mode !== 'api' && mode !== 'agent') {
210
+ return res.status(400).json({ error: 'mode must be "api" or "agent"' });
211
+ }
212
+ const validatedEffort = validateReasoningEffort(reasoning_effort);
213
+ const validatedVerbosity = validateVerbosity(verbosity);
214
+ const conversation = await daemon.conversations.create(provider, model, title, mode || 'api', {
215
+ reasoningEffort: validatedEffort,
216
+ verbosity: validatedVerbosity,
217
+ });
218
+ daemon.audit.log('conversation.create', { id: conversation.id, provider, model, mode: conversation.mode });
219
+ res.status(201).json(conversation);
220
+ } catch (err) {
221
+ res.status(400).json({ error: err.message });
222
+ }
223
+ });
224
+
225
+ app.get('/api/conversations/:id', (req, res) => {
226
+ const conversation = daemon.conversations.get(req.params.id);
227
+ if (!conversation) return res.status(404).json({ error: 'Conversation not found' });
228
+ res.json(conversation);
229
+ });
230
+
231
+ app.patch('/api/conversations/:id', async (req, res) => {
232
+ try {
233
+ const conv = daemon.conversations.get(req.params.id);
234
+ if (!conv) return res.status(404).json({ error: 'Conversation not found' });
235
+ if (req.body.title !== undefined) daemon.conversations.rename(req.params.id, req.body.title);
236
+ if (req.body.pinned !== undefined) daemon.conversations.pin(req.params.id, req.body.pinned);
237
+ if (req.body.archived !== undefined) daemon.conversations.archive(req.params.id, req.body.archived);
238
+ if (req.body.model !== undefined || req.body.provider !== undefined) {
239
+ const newProvider = req.body.provider || conv.provider;
240
+ const newModel = req.body.model || conv.model;
241
+ daemon.conversations.updateModel(req.params.id, newProvider, newModel);
242
+ }
243
+ if (req.body.mode !== undefined) {
244
+ if (req.body.mode !== 'api' && req.body.mode !== 'agent') {
245
+ return res.status(400).json({ error: 'mode must be "api" or "agent"' });
246
+ }
247
+ await daemon.conversations.setMode(req.params.id, req.body.mode);
248
+ }
249
+ if (req.body.reasoning_effort !== undefined || req.body.verbosity !== undefined) {
250
+ const validatedEffort = req.body.reasoning_effort !== undefined ? validateReasoningEffort(req.body.reasoning_effort) : undefined;
251
+ const validatedVerbosity = req.body.verbosity !== undefined ? validateVerbosity(req.body.verbosity) : undefined;
252
+ daemon.conversations.updateReasoningSettings(req.params.id, validatedEffort, validatedVerbosity);
253
+ }
254
+ daemon.audit.log('conversation.update', { id: req.params.id, provider: req.body.provider, model: req.body.model, mode: req.body.mode });
255
+ res.json(daemon.conversations.get(req.params.id));
256
+ } catch (err) {
257
+ res.status(400).json({ error: err.message });
258
+ }
259
+ });
260
+
261
+ app.delete('/api/conversations/:id', async (req, res) => {
262
+ try {
263
+ const conv = daemon.conversations.get(req.params.id);
264
+ if (!conv) return res.status(404).json({ error: 'Conversation not found' });
265
+ await daemon.conversations.delete(req.params.id);
266
+ daemon.audit.log('conversation.delete', { id: req.params.id });
267
+ res.json({ ok: true });
268
+ } catch (err) {
269
+ res.status(400).json({ error: err.message });
270
+ }
271
+ });
272
+
273
+ app.post('/api/conversations/:id/message', async (req, res) => {
274
+ try {
275
+ const { message, history, reasoning_effort, verbosity } = req.body;
276
+ if (!message || typeof message !== 'string' || !message.trim()) {
277
+ return res.status(400).json({ error: 'message is required' });
278
+ }
279
+ const validatedEffort = validateReasoningEffort(reasoning_effort);
280
+ const validatedVerbosity = validateVerbosity(verbosity);
281
+
282
+ const conv = daemon.conversations.get(req.params.id);
283
+ if (!conv) return res.status(404).json({ error: 'Conversation not found' });
284
+
285
+ daemon.conversations.autoTitle(req.params.id, message.trim());
286
+ daemon.conversations.touchUpdatedAt(req.params.id);
287
+
288
+ await daemon.conversations.sendMessage(req.params.id, message.trim(), history || [], {
289
+ reasoningEffort: validatedEffort,
290
+ verbosity: validatedVerbosity,
291
+ });
292
+ daemon.audit.log('conversation.message', { id: req.params.id, mode: conv.mode || 'api' });
293
+ res.json({ status: 'streaming', mode: conv.mode || 'api' });
294
+ } catch (err) {
295
+ res.status(400).json({ error: err.message });
296
+ }
297
+ });
298
+
299
+ app.post('/api/conversations/:id/stop', (req, res) => {
300
+ try {
301
+ const conv = daemon.conversations.get(req.params.id);
302
+ if (!conv) return res.status(404).json({ error: 'Conversation not found' });
303
+ daemon.conversations.stopStreaming(req.params.id);
304
+ res.json({ ok: true });
305
+ } catch (err) {
306
+ res.status(400).json({ error: err.message });
307
+ }
308
+ });
309
+
310
+ // --- Image Generation ---
311
+
312
+ app.post('/api/conversations/:id/generate-image', async (req, res) => {
313
+ try {
314
+ const { prompt, model, size, quality } = req.body;
315
+ if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
316
+ return res.status(400).json({ error: 'prompt is required' });
317
+ }
318
+ const conv = daemon.conversations.get(req.params.id);
319
+ if (!conv) return res.status(404).json({ error: 'Conversation not found' });
320
+
321
+ let providerName = conv.provider;
322
+ let provider = getProvider(providerName);
323
+
324
+ // If a specific image model was requested, find the right provider
325
+ if (model) {
326
+ const imageProviders = ['codex', 'grok', 'nano-banana'];
327
+ for (const pid of imageProviders) {
328
+ const p = getProvider(pid);
329
+ if (p?.constructor.models.some((m) => m.id === model)) {
330
+ provider = p;
331
+ providerName = pid;
332
+ break;
333
+ }
334
+ }
335
+ }
336
+
337
+ if (!provider?.generateImage) {
338
+ return res.status(400).json({ error: 'Provider does not support image generation' });
339
+ }
340
+
341
+ const apiKey = daemon.conversations._getApiKey(providerName);
342
+ if (!apiKey) {
343
+ return res.status(400).json({ error: `No API key configured for ${providerName}` });
344
+ }
345
+
346
+ daemon.broadcast({
347
+ type: 'conversation:image-progress',
348
+ data: { conversationId: req.params.id, status: 'generating', prompt: prompt.trim() },
349
+ });
350
+
351
+ const result = await provider.generateImage(prompt.trim(), { model, size, quality, apiKey });
352
+
353
+ daemon.broadcast({
354
+ type: 'conversation:image',
355
+ data: { conversationId: req.params.id, ...result, prompt: prompt.trim() },
356
+ });
357
+
358
+ daemon.conversations.touchUpdatedAt(req.params.id);
359
+ daemon.audit.log('conversation.image', { id: req.params.id, model: result.model, provider: result.provider });
360
+ res.json(result);
361
+ } catch (err) {
362
+ daemon.broadcast({
363
+ type: 'conversation:image-progress',
364
+ data: { conversationId: req.params.id, status: 'error', error: err.message },
365
+ });
366
+ res.status(500).json({ error: err.message });
367
+ }
368
+ });
369
+
370
+ // Stop an agent's current work without killing the agent
371
+ app.post('/api/agents/:id/stop', async (req, res) => {
372
+ try {
373
+ const agent = daemon.registry.get(req.params.id);
374
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
375
+ await daemon.processes.stop(req.params.id);
376
+ daemon.audit.log('agent.stop', { id: req.params.id, name: agent.name });
377
+ res.json({ id: req.params.id, status: 'stopped' });
378
+ } catch (err) {
379
+ res.status(500).json({ error: err.message });
380
+ }
381
+ });
382
+
383
+ // Rotate an agent
384
+ app.post('/api/agents/:id/rotate', async (req, res) => {
385
+ try {
386
+ const oldAgent = daemon.registry.get(req.params.id);
387
+ const newAgent = await daemon.rotator.rotate(req.params.id);
388
+ daemon.audit.log('agent.rotate', { oldId: req.params.id, newId: newAgent.id, role: oldAgent?.role });
389
+ res.json(newAgent);
390
+ } catch (err) {
391
+ res.status(400).json({ error: err.message });
392
+ }
393
+ });
394
+
395
+ // Instruct an agent — send message to agent loop, resume session, or rotate
396
+ // Agent loop = direct message to running loop (local models)
397
+ // Resume = zero cold-start (uses --resume SESSION_ID)
398
+ // Rotation = full handoff brief (only for degradation or no session)
399
+ app.post('/api/agents/:id/instruct', async (req, res) => {
400
+ try {
401
+ const { message, codeContext } = req.body;
402
+ if (!message || typeof message !== 'string' || !message.trim()) {
403
+ return res.status(400).json({ error: 'message is required' });
404
+ }
405
+ const agent = daemon.registry.get(req.params.id);
406
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
407
+
408
+ // Build the final instruction, optionally enriched with code context
409
+ let finalMessage = message.trim();
410
+ if (codeContext && typeof codeContext === 'object') {
411
+ const { filePath, lineStart, lineEnd, selectedCode } = codeContext;
412
+ if (filePath && typeof filePath === 'string' && selectedCode && typeof selectedCode === 'string') {
413
+ const start = Number.isFinite(lineStart) ? lineStart : '?';
414
+ const end = Number.isFinite(lineEnd) ? lineEnd : '?';
415
+ finalMessage = `${finalMessage}\n\nCode context from ${filePath} (lines ${start}-${end}):\n\`\`\`\n${selectedCode}\n\`\`\``;
416
+ }
417
+ }
418
+
419
+ // Record user feedback so the journalist can include it in future agent context
420
+ if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, finalMessage);
421
+
422
+ // Agent loop path — send message directly to the running loop
423
+ const wrappedMessage = wrapWithRoleReminder(agent.role, finalMessage);
424
+ if (daemon.processes.hasAgentLoop(req.params.id)) {
425
+ const sent = await daemon.processes.sendMessage(req.params.id, wrappedMessage);
426
+ if (sent) {
427
+ daemon.audit.log('agent.chat', { id: req.params.id });
428
+ return res.json({ id: agent.id, status: 'message_sent' });
429
+ }
430
+ // Loop exists but not running — fall through to resume/rotate
431
+ }
432
+
433
+ // One-shot providers (groove-network): kill any running instance and
434
+ // respawn with the user's message as --prompt. No handoff brief, no
435
+ // session resume, no message queue — each chat message is a fresh spawn.
436
+ const provider = getProvider(agent.provider);
437
+ if (provider?.constructor?.isOneShot) {
438
+ const oldConfig = { ...agent };
439
+ if (daemon.processes.isRunning(req.params.id)) {
440
+ await daemon.processes.kill(req.params.id);
441
+ }
442
+ daemon.registry.remove(req.params.id);
443
+ daemon.locks.release(req.params.id);
444
+
445
+ const newAgent = await daemon.processes.spawn({
446
+ role: oldConfig.role,
447
+ scope: oldConfig.scope,
448
+ provider: oldConfig.provider,
449
+ model: oldConfig.model,
450
+ prompt: finalMessage,
451
+ permission: oldConfig.permission || 'full',
452
+ workingDir: oldConfig.workingDir,
453
+ name: oldConfig.name,
454
+ teamId: oldConfig.teamId,
455
+ });
456
+ daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
457
+ return res.json(newAgent);
458
+ }
459
+
460
+ // Non-interactive CLI providers (e.g. Gemini): respawn with the new
461
+ // message as the prompt, preserving original introContext. These providers
462
+ // run one prompt per spawn and cannot resume sessions.
463
+ if (provider?.constructor?.nonInteractive && !daemon.processes.isRunning(req.params.id)) {
464
+ const oldConfig = { ...agent };
465
+ daemon.registry.remove(req.params.id);
466
+ daemon.locks.release(req.params.id);
467
+
468
+ const newAgent = await daemon.processes.spawn({
469
+ role: oldConfig.role,
470
+ scope: oldConfig.scope,
471
+ provider: oldConfig.provider,
472
+ model: oldConfig.model,
473
+ prompt: finalMessage,
474
+ introContext: oldConfig.introContext,
475
+ permission: oldConfig.permission || 'full',
476
+ workingDir: oldConfig.workingDir,
477
+ name: oldConfig.name,
478
+ teamId: oldConfig.teamId,
479
+ });
480
+ daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
481
+ return res.json(newAgent);
482
+ }
483
+
484
+ // Running CLI agent (no loop) — queue the message for delivery after
485
+ // the current task completes instead of killing and respawning.
486
+ if (daemon.processes.isRunning(req.params.id)) {
487
+ daemon.processes.queueMessage(req.params.id, wrappedMessage);
488
+ daemon.audit.log('agent.chat.queued', { id: req.params.id });
489
+ return res.json({ id: agent.id, status: 'message_queued' });
490
+ }
491
+
492
+ // CLI agent path — session resume or rotation.
493
+ // Force rotation (fresh session + handoff brief) past the resume ceiling:
494
+ // reviving a >5M-token claude session has crashed the CLI mid-HTTP-parse
495
+ // (V8 fatal in JsonStringifier) — the rotator's handoff brief sidesteps that.
496
+ const SESSION_RESUME_CEILING = 5_000_000;
497
+ const resumed = !!agent.sessionId && (agent.tokensUsed || 0) < SESSION_RESUME_CEILING;
498
+ const newAgent = resumed
499
+ ? await daemon.processes.resume(req.params.id, wrappedMessage)
500
+ : await daemon.rotator.rotate(req.params.id, { additionalPrompt: wrappedMessage });
501
+
502
+ daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed });
503
+ res.json(newAgent);
504
+ } catch (err) {
505
+ res.status(400).json({ error: err.message });
506
+ }
507
+ });
508
+
509
+ // Query an agent (headless one-shot, agent keeps running)
510
+ // For agent loop agents: sends message directly to the loop
511
+ app.post('/api/agents/:id/query', async (req, res) => {
512
+ try {
513
+ const { message } = req.body;
514
+ if (!message || typeof message !== 'string' || !message.trim()) {
515
+ return res.status(400).json({ error: 'message is required' });
516
+ }
517
+ const agent = daemon.registry.get(req.params.id);
518
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
519
+
520
+ // Agent loop agents: send message directly (they're interactive)
521
+ if (daemon.processes.hasAgentLoop(req.params.id)) {
522
+ const sent = await daemon.processes.sendMessage(req.params.id, message.trim());
523
+ return res.json({ response: sent ? 'Message sent to agent' : 'Agent not running', agentId: agent.id, agentName: agent.name });
524
+ }
525
+
526
+ // Build context about the agent's work
527
+ const activity = daemon.classifier?.agentWindows?.[agent.id] || [];
528
+ const recentActivity = activity.slice(-20).map((e) => e.data || e.text || '').join('\n');
529
+
530
+ // Truncate the agent's original prompt to avoid massive payloads
531
+ const taskSummary = agent.prompt ? agent.prompt.slice(0, 500) : '';
532
+ const prompt = [
533
+ `You are answering a question about agent "${agent.name}" (role: ${agent.role}).`,
534
+ `Provider: ${agent.provider}, Tokens used: ${agent.tokensUsed || 0}`,
535
+ taskSummary ? `Task summary: ${taskSummary}` : '',
536
+ recentActivity ? `\nRecent activity:\n${recentActivity}` : '',
537
+ `\nUser question: ${message.trim()}`,
538
+ '\nAnswer concisely based on the agent context above.',
539
+ ].filter(Boolean).join('\n');
540
+
541
+ const response = await daemon.journalist.callHeadless(prompt, { trackAs: '__agent_qa__' });
542
+ res.json({ response, agentId: agent.id, agentName: agent.name });
543
+ } catch (err) {
544
+ res.status(400).json({ error: err.message });
545
+ }
546
+ });
547
+
548
+ // Upload file to agent's working directory
549
+ app.post('/api/agents/:id/upload', (req, res) => {
550
+ const agent = daemon.registry.get(req.params.id);
551
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
552
+
553
+ const { filename, content } = req.body;
554
+ if (!filename || !content) return res.status(400).json({ error: 'filename and content required' });
555
+
556
+ // Sanitize filename — strict allowlist, no path traversal
557
+ const safeName = String(filename).replace(/[^a-zA-Z0-9._-]/g, '_').replace(/^\.+/, '');
558
+ if (!safeName) return res.status(400).json({ error: 'Invalid filename' });
559
+
560
+ const dir = agent.workingDir || daemon.projectDir;
561
+ const filePath = resolve(dir, safeName);
562
+
563
+ // Ensure file stays within working directory
564
+ if (!filePath.startsWith(dir)) {
565
+ return res.status(400).json({ error: 'Path traversal detected' });
566
+ }
567
+
568
+ try {
569
+ mkdirSync(dir, { recursive: true });
570
+ const buffer = Buffer.from(content, 'base64');
571
+ writeFileSync(filePath, buffer);
572
+ daemon.audit.log('file.upload', { agentId: agent.id, filename: safeName, size: buffer.length });
573
+ res.json({ ok: true, path: safeName, size: buffer.length });
574
+ } catch (err) {
575
+ res.status(500).json({ error: `Upload failed: ${err.message}` });
576
+ }
577
+ });
578
+
579
+ // List MD files for an agent (from its working directory + .groove)
580
+ app.get('/api/agents/:id/mdfiles', (req, res) => {
581
+ const agent = daemon.registry.get(req.params.id);
582
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
583
+
584
+ const dir = agent.workingDir || daemon.projectDir;
585
+ const files = [];
586
+
587
+ // Scan working directory for .md files (top level + .groove/)
588
+ try {
589
+ for (const entry of readdirSync(dir)) {
590
+ if (entry.endsWith('.md') && !entry.startsWith('.')) {
591
+ const fullPath = resolve(dir, entry);
592
+ if (statSync(fullPath).isFile()) {
593
+ files.push({ name: entry, path: entry, size: statSync(fullPath).size, source: 'project' });
594
+ }
595
+ }
596
+ }
597
+ const grooveDir = resolve(dir, '.groove');
598
+ if (existsSync(grooveDir)) {
599
+ for (const entry of readdirSync(grooveDir)) {
600
+ if (entry.endsWith('.md')) {
601
+ const fullPath = resolve(grooveDir, entry);
602
+ if (statSync(fullPath).isFile()) {
603
+ files.push({ name: entry, path: `.groove/${entry}`, size: statSync(fullPath).size, source: 'project' });
604
+ }
605
+ }
606
+ }
607
+ }
608
+ } catch { /* dir might not exist */ }
609
+
610
+ // Include personality file from .groove/personalities/
611
+ try {
612
+ const personalityFile = resolve(daemon.grooveDir, 'personalities', `${agent.name}.md`);
613
+ if (existsSync(personalityFile)) {
614
+ const size = statSync(personalityFile).size;
615
+ files.unshift({ name: 'personality.md', path: '__personality__', size, source: 'personality' });
616
+ }
617
+ } catch { /* ignore */ }
618
+
619
+ // Include user-created agent files from .groove/agent-files/<name>/
620
+ try {
621
+ const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
622
+ if (existsSync(agentFilesDir)) {
623
+ for (const entry of readdirSync(agentFilesDir)) {
624
+ if (entry.endsWith('.md')) {
625
+ const fullPath = resolve(agentFilesDir, entry);
626
+ if (statSync(fullPath).isFile()) {
627
+ files.push({ name: entry, path: `__user__/${entry}`, size: statSync(fullPath).size, source: 'user' });
628
+ }
629
+ }
630
+ }
631
+ }
632
+ } catch { /* ignore */ }
633
+
634
+ res.json({ files, workingDir: dir });
635
+ });
636
+
637
+ // Read a specific MD file for an agent
638
+ app.get('/api/agents/:id/mdfiles/read', (req, res) => {
639
+ const agent = daemon.registry.get(req.params.id);
640
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
641
+
642
+ const dir = agent.workingDir || daemon.projectDir;
643
+ const relPath = req.query.path;
644
+ if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
645
+
646
+ if (relPath === '__personality__') {
647
+ const personalityFile = resolve(daemon.grooveDir, 'personalities', `${agent.name}.md`);
648
+ if (existsSync(personalityFile)) {
649
+ return res.json({ content: readFileSync(personalityFile, 'utf8') });
650
+ }
651
+ return res.json({ content: '' });
652
+ }
653
+
654
+ if (relPath.startsWith('__user__/')) {
655
+ const fileName = relPath.slice('__user__/'.length);
656
+ if (!fileName || fileName.includes('/') || fileName.includes('..')) return res.status(400).json({ error: 'Invalid path' });
657
+ const filePath = resolve(daemon.grooveDir, 'agent-files', agent.name, fileName);
658
+ if (existsSync(filePath)) return res.json({ content: readFileSync(filePath, 'utf8') });
659
+ return res.json({ content: '' });
660
+ }
661
+
662
+ const fullPath = resolve(dir, relPath);
663
+ if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
664
+
665
+ try {
666
+ const content = readFileSync(fullPath, 'utf8');
667
+ res.json({ path: relPath, content });
668
+ } catch {
669
+ res.status(404).json({ error: 'File not found' });
670
+ }
671
+ });
672
+
673
+ // Save a MD file for an agent
674
+ app.put('/api/agents/:id/mdfiles/write', (req, res) => {
675
+ const agent = daemon.registry.get(req.params.id);
676
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
677
+
678
+ const dir = agent.workingDir || daemon.projectDir;
679
+ const { path: relPath, content } = req.body;
680
+ if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
681
+ if (typeof content !== 'string') return res.status(400).json({ error: 'Content required' });
682
+
683
+ if (relPath === '__personality__') {
684
+ const personalityDir = resolve(daemon.grooveDir, 'personalities');
685
+ mkdirSync(personalityDir, { recursive: true });
686
+ writeFileSync(resolve(personalityDir, `${agent.name}.md`), content || '', { mode: 0o600 });
687
+ daemon.audit.log('personality.update', { name: agent.name, agentId: agent.id });
688
+ return res.json({ saved: true });
689
+ }
690
+
691
+ if (relPath.startsWith('__user__/')) {
692
+ const fileName = relPath.slice('__user__/'.length);
693
+ if (!fileName || fileName.includes('/') || fileName.includes('..')) return res.status(400).json({ error: 'Invalid path' });
694
+ const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
695
+ mkdirSync(agentFilesDir, { recursive: true });
696
+ writeFileSync(resolve(agentFilesDir, fileName), content || '', { mode: 0o600 });
697
+ daemon.audit.log('mdfile.write.user', { agentId: agent.id, name: fileName });
698
+ return res.json({ saved: true });
699
+ }
700
+
701
+ const fullPath = resolve(dir, relPath);
702
+ if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
703
+
704
+ try {
705
+ writeFileSync(fullPath, content, 'utf8');
706
+ daemon.audit.log('mdfile.write', { agentId: agent.id, path: relPath });
707
+ res.json({ ok: true });
708
+ } catch (err) {
709
+ res.status(500).json({ error: err.message });
710
+ }
711
+ });
712
+
713
+ // Create a new MD file for an agent
714
+ app.post('/api/agents/:id/mdfiles/create', (req, res) => {
715
+ const agent = daemon.registry.get(req.params.id);
716
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
717
+ let name = req.body?.name;
718
+ if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name required' });
719
+ name = name.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
720
+ if (!name) return res.status(400).json({ error: 'Invalid name' });
721
+ if (!name.endsWith('.md')) name += '.md';
722
+ const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
723
+ mkdirSync(agentFilesDir, { recursive: true });
724
+ const filePath = resolve(agentFilesDir, name);
725
+ if (existsSync(filePath)) return res.status(409).json({ error: 'File already exists' });
726
+ writeFileSync(filePath, '', { mode: 0o600 });
727
+ daemon.audit.log('mdfile.create', { agentId: agent.id, name });
728
+ res.json({ name, path: `__user__/${name}` });
729
+ });
730
+
731
+ // --- Agent Skills (attach/detach) ---
732
+
733
+ app.post('/api/agents/:agentId/skills/:skillId', (req, res) => {
734
+ const agent = daemon.registry.get(req.params.agentId);
735
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
736
+ const skillId = req.params.skillId;
737
+ if (!daemon.skills.getContent(skillId)) {
738
+ return res.status(400).json({ error: 'Skill not installed. Install it first.' });
739
+ }
740
+ const skills = agent.skills || [];
741
+ if (skills.includes(skillId)) {
742
+ return res.json({ id: agent.id, skills });
743
+ }
744
+ daemon.registry.update(agent.id, { skills: [...skills, skillId] });
745
+ daemon.audit.log('skill.attach', { agentId: agent.id, skillId });
746
+ res.json({ id: agent.id, skills: [...skills, skillId] });
747
+ });
748
+
749
+ app.delete('/api/agents/:agentId/skills/:skillId', (req, res) => {
750
+ const agent = daemon.registry.get(req.params.agentId);
751
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
752
+ const skills = (agent.skills || []).filter((s) => s !== req.params.skillId);
753
+ daemon.registry.update(agent.id, { skills });
754
+ daemon.audit.log('skill.detach', { agentId: agent.id, skillId: req.params.skillId });
755
+ res.json({ id: agent.id, skills });
756
+ });
757
+
758
+ // --- Agent Repos (attach/detach) ---
759
+
760
+ app.post('/api/agents/:agentId/repos/:importId', (req, res) => {
761
+ const agent = daemon.registry.get(req.params.agentId);
762
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
763
+ const importId = req.params.importId;
764
+ const manifest = daemon.repoImporter.getImport(importId);
765
+ if (!manifest || manifest.status !== 'active') {
766
+ return res.status(400).json({ error: 'Repo not found or not active' });
767
+ }
768
+ const repos = agent.repos || [];
769
+ if (repos.includes(importId)) {
770
+ return res.json({ id: agent.id, repos });
771
+ }
772
+ daemon.registry.update(agent.id, { repos: [...repos, importId] });
773
+ daemon.audit.log('repo.attach', { agentId: agent.id, importId });
774
+ res.json({ id: agent.id, repos: [...repos, importId] });
775
+ });
776
+
777
+ app.delete('/api/agents/:agentId/repos/:importId', (req, res) => {
778
+ const agent = daemon.registry.get(req.params.agentId);
779
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
780
+ const repos = (agent.repos || []).filter((r) => r !== req.params.importId);
781
+ daemon.registry.update(agent.id, { repos });
782
+ daemon.audit.log('repo.detach', { agentId: agent.id, importId: req.params.importId });
783
+ res.json({ id: agent.id, repos });
784
+ });
785
+
786
+ // --- Agent Integrations (attach/detach) ---
787
+
788
+ app.post('/api/agents/:agentId/integrations/:integrationId', (req, res) => {
789
+ const agent = daemon.registry.get(req.params.agentId);
790
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
791
+ const integrationId = req.params.integrationId;
792
+ if (!daemon.integrations._isInstalled(integrationId)) {
793
+ return res.status(400).json({ error: 'Integration not installed. Install it first.' });
794
+ }
795
+ const integrations = agent.integrations || [];
796
+ if (integrations.includes(integrationId)) {
797
+ return res.json({ id: agent.id, integrations });
798
+ }
799
+ daemon.registry.update(agent.id, { integrations: [...integrations, integrationId] });
800
+ daemon.audit.log('integration.attach', { agentId: agent.id, integrationId });
801
+ res.json({ id: agent.id, integrations: [...integrations, integrationId] });
802
+ });
803
+
804
+ app.delete('/api/agents/:agentId/integrations/:integrationId', (req, res) => {
805
+ const agent = daemon.registry.get(req.params.agentId);
806
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
807
+ const integrations = (agent.integrations || []).filter((s) => s !== req.params.integrationId);
808
+ daemon.registry.update(agent.id, { integrations });
809
+ daemon.audit.log('integration.detach', { agentId: agent.id, integrationId: req.params.integrationId });
810
+ res.json({ id: agent.id, integrations });
811
+ });
812
+ }