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
@@ -89,27 +89,10 @@ export class ConversationManager {
89
89
  const id = randomUUID().slice(0, 12);
90
90
  const now = new Date().toISOString();
91
91
 
92
- let agentId = null;
93
-
94
- if (mode === 'agent') {
95
- const defaultTeam = this.daemon.teams.getDefault();
96
- const workingDir = defaultTeam?.workingDir || this.daemon.projectDir;
97
-
98
- const agent = await this.daemon.processes.spawn({
99
- role: 'chat',
100
- provider,
101
- model: model || null,
102
- workingDir,
103
- teamId: defaultTeam?.id || null,
104
- permission: 'full',
105
- });
106
- agentId = agent.id;
107
- }
108
-
109
92
  const conversation = {
110
93
  id,
111
94
  title: title || 'New Chat',
112
- agentId,
95
+ agentId: null,
113
96
  provider,
114
97
  model: model || null,
115
98
  mode: mode === 'agent' ? 'agent' : 'api',
@@ -265,35 +248,7 @@ export class ConversationManager {
265
248
  this._modeChanging.add(id);
266
249
 
267
250
  try {
268
- if (mode === 'agent') {
269
- const existingAgent = conv.agentId ? this.daemon.registry.get(conv.agentId) : null;
270
- const alive = existingAgent && (existingAgent.status === 'running' || existingAgent.status === 'starting');
271
-
272
- if (!alive) {
273
- const defaultTeam = this.daemon.teams.getDefault();
274
- const workingDir = defaultTeam?.workingDir || this.daemon.projectDir;
275
- const agent = await this.daemon.processes.spawn({
276
- role: 'chat',
277
- provider: conv.provider,
278
- model: conv.model || null,
279
- workingDir,
280
- teamId: defaultTeam?.id || null,
281
- permission: 'full',
282
- });
283
- conv.agentId = agent.id;
284
- }
285
- } else {
286
- // Switching to API mode — kill the agent if running
287
- this._killStreamingProcess(id);
288
- if (conv.agentId) {
289
- const agent = this.daemon.registry.get(conv.agentId);
290
- if (agent && (agent.status === 'running' || agent.status === 'starting')) {
291
- try { await this.daemon.processes.kill(conv.agentId); } catch { /* ignore */ }
292
- }
293
- if (agent) this.daemon.registry.remove(conv.agentId);
294
- conv.agentId = null;
295
- }
296
- }
251
+ this._killStreamingProcess(id);
297
252
 
298
253
  conv.mode = mode;
299
254
  conv.updatedAt = new Date().toISOString();
@@ -354,7 +309,6 @@ export class ConversationManager {
354
309
  async sendMessage(id, message, history, { reasoningEffort, verbosity } = {}) {
355
310
  const conv = this.conversations.get(id);
356
311
  if (!conv) throw new Error('Conversation not found');
357
- if (conv.mode !== 'api') throw new Error('sendMessage only works in API mode');
358
312
 
359
313
  this._killStreamingProcess(id);
360
314
 
@@ -395,6 +349,17 @@ export class ConversationManager {
395
349
  try { tcAgentId = tc.onChatTurnStart(id, providerName, modelId, message); } catch { /* never block chat */ }
396
350
  }
397
351
 
352
+ // Agent mode prefers headless CLI (has tools + web search), falls back to API
353
+ if (conv.mode === 'agent') {
354
+ const headlessCmd = provider.buildHeadlessCommand(
355
+ this._buildHistoryPrompt(history, message), modelId,
356
+ );
357
+ if (headlessCmd) {
358
+ return this._sendViaHeadlessCLI(id, conv, provider, providerName, modelId, message, history, tcAgentId, tc);
359
+ }
360
+ // Provider has no CLI — fall through to API streaming
361
+ }
362
+
398
363
  // Try direct API streaming first (sub-second latency)
399
364
  const controller = provider.streamChat(
400
365
  messages, modelId, apiKey,
@@ -448,6 +413,11 @@ export class ConversationManager {
448
413
  }
449
414
 
450
415
  // Fallback: headless CLI spawn (for providers without streamChat or missing API key)
416
+ return this._sendViaHeadlessCLI(id, conv, provider, providerName, modelId, message, history, tcAgentId, tc);
417
+ }
418
+
419
+ _sendViaHeadlessCLI(id, conv, provider, providerName, modelId, message, history, tcAgentId, tc) {
420
+ let tcResponseText = '';
451
421
  const prompt = this._buildHistoryPrompt(history, message);
452
422
  const headlessCmd = provider.buildHeadlessCommand(prompt, modelId);
453
423
  if (!headlessCmd) {
@@ -5,7 +5,7 @@ import { existsSync, mkdirSync, writeFileSync, readFileSync, readdirSync, unlink
5
5
  import { resolve } from 'path';
6
6
  import { randomUUID } from 'crypto';
7
7
  import { validateAgentConfig } from '../validate.js';
8
- import { eventToSummary, agentListText, statusText, approvalsText, teamsText, schedulesText, briefText, tokensText, logText, planText, truncate, formatTokens } from './formatter.js';
8
+ import { eventToSummary, agentListText, statusText, approvalsText, teamsText, schedulesText, briefText, tokensText, logText, planText, truncate, formatTokens, formatDuration, formatCost } from './formatter.js';
9
9
 
10
10
  const GATEWAY_TYPES = ['telegram', 'discord', 'slack'];
11
11
 
@@ -304,6 +304,40 @@ export class GatewayManager {
304
304
  this.daemon.credentials.deleteKey(`gateway:${id}:${key}`);
305
305
  }
306
306
 
307
+ // -------------------------------------------------------------------
308
+ // Schedule Notifications — direct, targeted notification for automations
309
+ // -------------------------------------------------------------------
310
+
311
+ /**
312
+ * Send a completion notification for a scheduled automation run.
313
+ * Called directly by the scheduler — bypasses global event routing.
314
+ */
315
+ sendScheduleNotification(gatewayIds, summary) {
316
+ if (!gatewayIds || gatewayIds.length === 0) return;
317
+
318
+ const statusIcon = summary.status === 'success' ? '✅' : '❌';
319
+ const lines = [
320
+ `${statusIcon} Automation: ${summary.name} — ${summary.status}`,
321
+ ];
322
+ if (summary.description) {
323
+ lines.push(summary.description);
324
+ }
325
+ lines.push(`Duration: ${formatDuration(summary.duration)} | Cost: ${formatCost(summary.cost)} | Agents: ${summary.agentCount}`);
326
+ if (summary.errors) {
327
+ lines.push(`Error: ${truncate(summary.errors, 500)}`);
328
+ }
329
+
330
+ const message = lines.join('\n');
331
+
332
+ for (const gid of gatewayIds) {
333
+ const gw = this.gateways.get(gid);
334
+ if (!gw || !gw.connected) continue;
335
+ gw.send(message).catch((err) => {
336
+ console.log(`[Groove:Gateway] Schedule notification failed (${gid}): ${err.message}`);
337
+ });
338
+ }
339
+ }
340
+
307
341
  // -------------------------------------------------------------------
308
342
  // Command Routing — chat command → daemon internals
309
343
  // -------------------------------------------------------------------
@@ -42,6 +42,7 @@ import { TunnelManager } from './tunnel-manager.js';
42
42
  import { ModelManager } from './model-manager.js';
43
43
  import { ModelLab } from './model-lab.js';
44
44
  import { LlamaServerManager } from './llama-server.js';
45
+ import { MLXServerManager } from './mlx-server.js';
45
46
  import { RepoImporter } from './repo-import.js';
46
47
  import { ConversationManager } from './conversations.js';
47
48
  import { Toys } from './toys.js';
@@ -151,6 +152,7 @@ export class Daemon {
151
152
  this.preview = new PreviewService(this);
152
153
  this.modelManager = new ModelManager(this);
153
154
  this.llamaServer = new LlamaServerManager(this);
155
+ this.mlxServer = new MLXServerManager(this);
154
156
  this.mcpManager = new McpManager(this);
155
157
  this.tunnelManager = new TunnelManager(this);
156
158
  this.repoImporter = new RepoImporter(this);
@@ -848,6 +850,7 @@ export class Daemon {
848
850
  if (this.preview) await this.preview.killAll();
849
851
  this.mcpManager.stopAll();
850
852
  await this.llamaServer.stopAll();
853
+ await this.mlxServer.stopAll();
851
854
 
852
855
  // Clean up PID and host files
853
856
  if (existsSync(this.pidFile)) {
@@ -168,7 +168,7 @@ export class Journalist {
168
168
 
169
169
  hasNewActivity(agents) {
170
170
  for (const agent of agents) {
171
- const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.name}.log`);
171
+ const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
172
172
  if (!existsSync(logPath)) continue;
173
173
  try {
174
174
  const size = statSync(logPath).size;
@@ -182,7 +182,7 @@ export class Journalist {
182
182
  const result = {};
183
183
 
184
184
  for (const agent of agents) {
185
- const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.name}.log`);
185
+ const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
186
186
  if (!existsSync(logPath)) {
187
187
  result[agent.id] = { agent, entries: [], explorationEntries: [] };
188
188
  continue;
@@ -846,7 +846,7 @@ export class Journalist {
846
846
  for (const [agentId, { agent, entries }] of Object.entries(filteredLogs)) {
847
847
  if (entries.length === 0) continue;
848
848
 
849
- const agentDir = resolve(logsDir, agent.name);
849
+ const agentDir = resolve(logsDir, agent.id);
850
850
  mkdirSync(agentDir, { recursive: true });
851
851
 
852
852
  const logPath = resolve(agentDir, `${dateStr}-session.md`);
@@ -904,13 +904,24 @@ export class Journalist {
904
904
  .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || e.text || '').slice(0, 200)}`)
905
905
  .join('\n');
906
906
 
907
- // Try AI-synthesized session summary
907
+ // Try AI-synthesized session summary — but only if the session log has
908
+ // meaningful entries. With an empty/minimal log the headless Claude sees
909
+ // repo-wide git status (from its own project context) and fills the brief
910
+ // with unrelated changes from other teams, leaking cross-team state.
911
+ const meaningfulEntries = entries.filter((e) => e.type === 'tool' || e.type === 'error' || e.type === 'user' || e.type === 'result');
908
912
  let sessionSummary = '';
909
- try {
910
- const prompt = this.buildRotationSynthesisPrompt(agent, entries, options);
911
- sessionSummary = await this.callHeadless(prompt, { trackAs: '__rotation__' });
912
- } catch {
913
- // Fallback: structural summary from raw logs
913
+ let needsFallback = meaningfulEntries.length < 3;
914
+
915
+ if (!needsFallback) {
916
+ try {
917
+ const prompt = this.buildRotationSynthesisPrompt(agent, entries, options);
918
+ sessionSummary = await this.callHeadless(prompt, { trackAs: '__rotation__' });
919
+ } catch {
920
+ needsFallback = true;
921
+ }
922
+ }
923
+
924
+ if (needsFallback) {
914
925
  const errorSummary = entries
915
926
  .filter((e) => e.type === 'error')
916
927
  .map((e) => `- ${e.text}`)
@@ -941,7 +952,6 @@ export class Journalist {
941
952
  .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 200)}`)
942
953
  .join('\n');
943
954
 
944
- // Build investigation timeline from thinking entries — these capture reasoning and decisions
945
955
  const thinkingEntries = entries
946
956
  .filter((e) => e.type === 'thinking' && e.text && e.text.length > 80)
947
957
  .slice(-10)
@@ -1005,7 +1015,7 @@ export class Journalist {
1005
1015
  * Budget: keeps recent turns verbatim, summarizes oldest if over maxChars.
1006
1016
  */
1007
1017
  extractConversationThread(agent, { maxChars = 60000 } = {}) {
1008
- const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.name}.log`);
1018
+ const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
1009
1019
  if (!existsSync(logPath)) return null;
1010
1020
 
1011
1021
  let content;
@@ -1211,7 +1221,7 @@ export class Journalist {
1211
1221
  * Used by the Introducer to tell new agents what their teammates built.
1212
1222
  */
1213
1223
  getAgentFiles(agent) {
1214
- const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.name}.log`);
1224
+ const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
1215
1225
  if (!existsSync(logPath)) return [];
1216
1226
 
1217
1227
  try {
@@ -1249,7 +1259,7 @@ export class Journalist {
1249
1259
  * Used to capture planner conclusions, build summaries, etc.
1250
1260
  */
1251
1261
  getAgentResult(agent) {
1252
- const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.name}.log`);
1262
+ const logPath = resolve(this.daemon.grooveDir, 'logs', `${agent.id}.log`);
1253
1263
  if (!existsSync(logPath)) return '';
1254
1264
 
1255
1265
  try {
@@ -0,0 +1,365 @@
1
+ // GROOVE — MLX Server Manager
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+ //
4
+ // Manages mlx_lm.server inference server instances on Apple Silicon.
5
+ // Scans ~/.cache/huggingface/hub/ for cached MLX models.
6
+ // Mirrors LlamaServerManager API: ensureServer, stopServer, getStatus.
7
+
8
+ import { spawn, execSync } from 'child_process';
9
+ import { existsSync, readdirSync, readFileSync } from 'fs';
10
+ import { resolve } from 'path';
11
+ import { homedir } from 'os';
12
+
13
+ const BASE_PORT = 8080;
14
+ const MAX_SERVERS = 3;
15
+ const HEALTH_TIMEOUT = 60000; // 60s — MLX may need to load model into memory
16
+ const HEALTH_POLL_INTERVAL = 1000;
17
+ const IDLE_TIMEOUT = 300000; // 5 minutes
18
+
19
+ const HF_CACHE_DIR = resolve(homedir(), '.cache', 'huggingface', 'hub');
20
+ const HF_MODEL_DIR_PREFIX = 'models--';
21
+
22
+ export class MLXServerManager {
23
+ constructor(daemon) {
24
+ this.daemon = daemon;
25
+ this.servers = new Map(); // modelId -> { proc, port, users, startedAt, lastUsed, ready }
26
+ }
27
+
28
+ static isInstalled() {
29
+ try {
30
+ execSync('python3 -c "import mlx_lm; print(mlx_lm.__version__)"', {
31
+ stdio: ['ignore', 'pipe', 'ignore'],
32
+ timeout: 10000,
33
+ });
34
+ return true;
35
+ } catch {
36
+ return false;
37
+ }
38
+ }
39
+
40
+ static getVersion() {
41
+ try {
42
+ const out = execSync('python3 -c "import mlx_lm; print(mlx_lm.__version__)"', {
43
+ stdio: ['ignore', 'pipe', 'ignore'],
44
+ timeout: 10000,
45
+ });
46
+ return out.toString().trim();
47
+ } catch {
48
+ return null;
49
+ }
50
+ }
51
+
52
+ static getPythonPath() {
53
+ // Check venv first, then system python
54
+ const venvPython = resolve(homedir(), '.mlx-env', 'bin', 'python3');
55
+ if (existsSync(venvPython)) {
56
+ try {
57
+ execSync(`${venvPython} -c "import mlx_lm"`, { stdio: 'ignore', timeout: 10000 });
58
+ return venvPython;
59
+ } catch { /* fall through */ }
60
+ }
61
+ try {
62
+ execSync('python3 -c "import mlx_lm"', { stdio: 'ignore', timeout: 10000 });
63
+ return 'python3';
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+
69
+ // --- Model Scanning ---
70
+
71
+ static scanModels() {
72
+ const models = [];
73
+ if (!existsSync(HF_CACHE_DIR)) return models;
74
+
75
+ try {
76
+ const entries = readdirSync(HF_CACHE_DIR);
77
+ for (const entry of entries) {
78
+ if (!entry.startsWith(HF_MODEL_DIR_PREFIX)) continue;
79
+
80
+ const modelName = entry.slice(HF_MODEL_DIR_PREFIX.length).replace(/--/g, '/');
81
+ const snapshotsDir = resolve(HF_CACHE_DIR, entry, 'snapshots');
82
+ if (!existsSync(snapshotsDir)) continue;
83
+
84
+ let snapshotDir = null;
85
+ try {
86
+ const snapshots = readdirSync(snapshotsDir);
87
+ if (snapshots.length === 0) continue;
88
+ snapshotDir = resolve(snapshotsDir, snapshots[snapshots.length - 1]);
89
+ } catch { continue; }
90
+
91
+ let hasWeights = false;
92
+ let hasNpz = false;
93
+ let configData = null;
94
+ try {
95
+ const files = readdirSync(snapshotDir);
96
+ hasWeights = files.some((f) =>
97
+ f.endsWith('.safetensors') || f.endsWith('.npz') || f === 'weights.npz'
98
+ );
99
+ if (!hasWeights) continue;
100
+ hasNpz = files.some((f) => f.endsWith('.npz'));
101
+
102
+ const configPath = resolve(snapshotDir, 'config.json');
103
+ if (existsSync(configPath)) {
104
+ configData = JSON.parse(readFileSync(configPath, 'utf8'));
105
+ }
106
+ } catch { continue; }
107
+
108
+ const isMLX = isMLXModel(modelName, hasNpz, configData);
109
+ const type = isMLX ? 'mlx' : 'hf';
110
+ const prefix = isMLX ? 'mlx:' : 'hf:';
111
+ const shortName = modelName.split('/').pop() || modelName;
112
+ const params = parseMLXParams(shortName, configData);
113
+ const quant = parseMLXQuantization(shortName);
114
+
115
+ models.push({
116
+ id: `${prefix}${modelName}`,
117
+ modelId: modelName,
118
+ filename: shortName,
119
+ type,
120
+ compatibleBackends: isMLX ? ['mlx'] : ['vllm', 'tgi'],
121
+ parameters: params,
122
+ quantization: quant,
123
+ snapshotPath: snapshotDir,
124
+ cachedAt: entry,
125
+ });
126
+ }
127
+ } catch { /* best effort */ }
128
+
129
+ return models;
130
+ }
131
+
132
+ // --- Server Lifecycle ---
133
+
134
+ async ensureServer(modelId, options = {}) {
135
+ if (this.servers.has(modelId)) {
136
+ const server = this.servers.get(modelId);
137
+ server.users++;
138
+ server.lastUsed = Date.now();
139
+ return `http://127.0.0.1:${server.port}`;
140
+ }
141
+
142
+ if (this.servers.size >= MAX_SERVERS) {
143
+ await this._evictLRU();
144
+ }
145
+
146
+ const pythonPath = MLXServerManager.getPythonPath();
147
+ if (!pythonPath) {
148
+ throw new Error('mlx_lm not installed — run: pip3 install "mlx-lm[server]"');
149
+ }
150
+
151
+ const port = this._allocatePort();
152
+
153
+ const args = [
154
+ '-m', 'mlx_lm.server',
155
+ '--model', modelId,
156
+ '--port', String(port),
157
+ ];
158
+
159
+ const proc = spawn(pythonPath, args, {
160
+ stdio: ['ignore', 'pipe', 'pipe'],
161
+ detached: false,
162
+ });
163
+
164
+ if (!proc.pid) {
165
+ throw new Error('Failed to start mlx_lm.server — check installation');
166
+ }
167
+
168
+ const server = {
169
+ proc,
170
+ port,
171
+ modelId,
172
+ users: 1,
173
+ startedAt: Date.now(),
174
+ lastUsed: Date.now(),
175
+ ready: false,
176
+ };
177
+
178
+ this.servers.set(modelId, server);
179
+
180
+ const stderrBuf = [];
181
+ proc.stderr.on('data', (chunk) => {
182
+ stderrBuf.push(chunk.toString());
183
+ if (stderrBuf.join('').length > 4096) stderrBuf.shift();
184
+ });
185
+
186
+ proc.on('exit', (code, signal) => {
187
+ this.servers.delete(modelId);
188
+ this.daemon?.broadcast({
189
+ type: 'mlx:server:stopped',
190
+ data: { modelId, port, code, signal },
191
+ });
192
+ });
193
+
194
+ try {
195
+ await this._waitForHealth(port);
196
+ server.ready = true;
197
+
198
+ this.daemon?.broadcast({
199
+ type: 'mlx:server:ready',
200
+ data: { modelId, port },
201
+ });
202
+
203
+ return `http://127.0.0.1:${port}`;
204
+ } catch (err) {
205
+ await this.stopServer(modelId);
206
+ const stderr = stderrBuf.join('').slice(-500);
207
+ throw new Error(`mlx_lm.server failed to start: ${stderr || err.message}`);
208
+ }
209
+ }
210
+
211
+ releaseServer(modelId) {
212
+ const server = this.servers.get(modelId);
213
+ if (!server) return;
214
+
215
+ server.users = Math.max(0, server.users - 1);
216
+ server.lastUsed = Date.now();
217
+
218
+ if (server.users === 0) {
219
+ setTimeout(() => {
220
+ const s = this.servers.get(modelId);
221
+ if (s && s.users === 0 && Date.now() - s.lastUsed >= IDLE_TIMEOUT) {
222
+ this.stopServer(modelId);
223
+ }
224
+ }, IDLE_TIMEOUT + 1000);
225
+ }
226
+ }
227
+
228
+ async stopServer(modelId) {
229
+ const server = this.servers.get(modelId);
230
+ if (!server) return false;
231
+
232
+ return new Promise((resolve) => {
233
+ const timeout = setTimeout(() => {
234
+ try { server.proc.kill('SIGKILL'); } catch {}
235
+ }, 5000);
236
+
237
+ server.proc.on('exit', () => {
238
+ clearTimeout(timeout);
239
+ this.servers.delete(modelId);
240
+ resolve(true);
241
+ });
242
+
243
+ try {
244
+ server.proc.kill('SIGTERM');
245
+ } catch {
246
+ clearTimeout(timeout);
247
+ this.servers.delete(modelId);
248
+ resolve(true);
249
+ }
250
+ });
251
+ }
252
+
253
+ async stopAll() {
254
+ const ids = Array.from(this.servers.keys());
255
+ await Promise.all(ids.map((id) => this.stopServer(id)));
256
+ }
257
+
258
+ // --- Health Check ---
259
+
260
+ async _waitForHealth(port) {
261
+ const start = Date.now();
262
+ while (Date.now() - start < HEALTH_TIMEOUT) {
263
+ try {
264
+ const res = await fetch(`http://127.0.0.1:${port}/v1/models`, {
265
+ signal: AbortSignal.timeout(2000),
266
+ });
267
+ if (res.ok) return true;
268
+ } catch { /* server still loading */ }
269
+ await new Promise((r) => setTimeout(r, HEALTH_POLL_INTERVAL));
270
+ }
271
+ throw new Error(`mlx_lm.server health check timed out after ${HEALTH_TIMEOUT / 1000}s`);
272
+ }
273
+
274
+ // --- Port Management ---
275
+
276
+ _allocatePort() {
277
+ const usedPorts = new Set(Array.from(this.servers.values()).map((s) => s.port));
278
+ let port = BASE_PORT;
279
+ while (usedPorts.has(port) && port < BASE_PORT + 100) {
280
+ port++;
281
+ }
282
+ return port;
283
+ }
284
+
285
+ async _evictLRU() {
286
+ let lru = null;
287
+ for (const [id, server] of this.servers) {
288
+ if (!lru || server.users < lru.users ||
289
+ (server.users === lru.users && server.lastUsed < lru.lastUsed)) {
290
+ lru = { id, ...server };
291
+ }
292
+ }
293
+ if (lru) {
294
+ await this.stopServer(lru.id);
295
+ }
296
+ }
297
+
298
+ // --- Status ---
299
+
300
+ getRunningServers() {
301
+ return Array.from(this.servers.entries()).map(([modelId, s]) => ({
302
+ modelId,
303
+ port: s.port,
304
+ users: s.users,
305
+ ready: s.ready,
306
+ uptime: Date.now() - s.startedAt,
307
+ lastUsed: s.lastUsed,
308
+ }));
309
+ }
310
+
311
+ getStatus() {
312
+ return {
313
+ installed: MLXServerManager.isInstalled(),
314
+ version: MLXServerManager.getVersion(),
315
+ running: this.servers.size,
316
+ maxServers: MAX_SERVERS,
317
+ servers: this.getRunningServers(),
318
+ cachedModels: MLXServerManager.scanModels().length,
319
+ };
320
+ }
321
+ }
322
+
323
+ // --- Format Detection ---
324
+
325
+ function isMLXModel(modelName, hasNpz, configData) {
326
+ if (modelName.startsWith('mlx-community/')) return true;
327
+ if (hasNpz) return true;
328
+ if (/[-_]mlx[-_]/i.test(modelName) || modelName.toLowerCase().endsWith('-mlx')) return true;
329
+ if (configData?.quantization_config?.quant_method === 'mlx') return true;
330
+ return false;
331
+ }
332
+
333
+ // --- Parsing Utilities ---
334
+
335
+ function parseMLXParams(name, config) {
336
+ // Try config.json first
337
+ if (config) {
338
+ const hidden = config.hidden_size;
339
+ const layers = config.num_hidden_layers;
340
+ const vocab = config.vocab_size;
341
+ if (hidden && layers) {
342
+ const approx = (hidden * layers * vocab * 4) / 1e9;
343
+ if (approx > 0.1) {
344
+ if (approx < 1.5) return '0.5-1B';
345
+ if (approx < 5) return `${Math.round(approx)}B`;
346
+ if (approx < 10) return `${Math.round(approx)}B`;
347
+ return `${Math.round(approx)}B`;
348
+ }
349
+ }
350
+ }
351
+
352
+ // Fallback: parse from name
353
+ const match = name.match(/(\d+\.?\d*)[bB]/);
354
+ if (match) return `${match[1]}B`;
355
+ return null;
356
+ }
357
+
358
+ function parseMLXQuantization(name) {
359
+ const lower = name.toLowerCase();
360
+ if (lower.includes('8bit') || lower.includes('8-bit')) return 'W8';
361
+ if (lower.includes('4bit') || lower.includes('4-bit')) return 'W4';
362
+ if (lower.includes('3bit') || lower.includes('3-bit')) return 'W3';
363
+ if (lower.includes('bf16') || lower.includes('fp16')) return 'FP16';
364
+ return null;
365
+ }