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
@@ -4,23 +4,34 @@
4
4
  import { resolve } from 'path';
5
5
  import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
6
6
  import { randomUUID } from 'crypto';
7
- const RUNTIME_TYPES = ['ollama', 'vllm', 'llama-cpp', 'tgi', 'openai-compatible'];
7
+ import { homedir } from 'os';
8
+ import { spawn } from 'child_process';
9
+ import { LlamaServerManager } from './llama-server.js';
10
+ import { MLXServerManager } from './mlx-server.js';
11
+ const RUNTIME_TYPES = ['ollama', 'vllm', 'llama-cpp', 'mlx', 'tgi', 'openai-compatible'];
8
12
  const DEFAULT_OLLAMA_ENDPOINT = 'http://localhost:11434';
13
+ const GLOBAL_GROOVE_DIR = resolve(homedir(), '.groove');
14
+
15
+ function localURL(endpoint) { return endpoint.replace('localhost', '127.0.0.1'); }
9
16
 
10
17
  export class ModelLab {
11
18
  constructor(daemon) {
12
19
  this.daemon = daemon;
13
- this.runtimesPath = resolve(daemon.grooveDir, 'lab-runtimes.json');
20
+ this.runtimesPath = resolve(GLOBAL_GROOVE_DIR, 'lab-runtimes.json');
14
21
  this.presetsPath = resolve(daemon.grooveDir, 'lab-presets.json');
15
22
  this.sessionsDir = resolve(daemon.grooveDir, 'lab-sessions');
16
23
  this.runtimes = new Map();
17
24
  this.presets = new Map();
18
25
  this.sessions = new Map();
26
+ this._processes = new Map();
27
+ this._installedTools = null;
19
28
  this._ensureDirs();
20
29
  this._load();
30
+ this._detectInstalledTools();
21
31
  }
22
32
 
23
33
  _ensureDirs() {
34
+ try { mkdirSync(GLOBAL_GROOVE_DIR, { recursive: true }); } catch { /* best-effort */ }
24
35
  try { mkdirSync(this.sessionsDir, { recursive: true }); } catch { /* best-effort */ }
25
36
  }
26
37
 
@@ -57,6 +68,36 @@ export class ModelLab {
57
68
  } catch { /* dir may not exist yet */ }
58
69
  }
59
70
 
71
+ _detectInstalledTools() {
72
+ // Detect installed inference tools (not running servers) on startup.
73
+ // Results are cached and broadcast so the GUI can show "Start" buttons.
74
+ try {
75
+ const llamaInstalled = LlamaServerManager.isInstalled();
76
+ const mlxInstalled = MLXServerManager.isInstalled();
77
+ const mlxModels = MLXServerManager.scanModels();
78
+ const mlxVersion = mlxInstalled ? MLXServerManager.getVersion() : null;
79
+
80
+ this._installedTools = {
81
+ llama: { installed: llamaInstalled },
82
+ mlx: { installed: mlxInstalled, version: mlxVersion, models: mlxModels },
83
+ };
84
+
85
+ this.daemon?.broadcast({ type: 'lab:tools:detected', data: this._installedTools });
86
+ } catch {
87
+ this._installedTools = { llama: { installed: false }, mlx: { installed: false, version: null, models: [] } };
88
+ }
89
+ }
90
+
91
+ getInstalledTools() {
92
+ if (!this._installedTools) this._detectInstalledTools();
93
+ return this._installedTools;
94
+ }
95
+
96
+ refreshInstalledTools() {
97
+ this._detectInstalledTools();
98
+ return this._installedTools;
99
+ }
100
+
60
101
  _saveRuntimes() {
61
102
  writeFileSync(this.runtimesPath, JSON.stringify([...this.runtimes.values()], null, 2));
62
103
  }
@@ -74,7 +115,7 @@ export class ModelLab {
74
115
 
75
116
  // ─── Runtimes ───────────────────────────────────────────────
76
117
 
77
- async addRuntime({ name, type, endpoint, apiKey, models }) {
118
+ async addRuntime({ name, type, endpoint, apiKey, models, launchConfig }) {
78
119
  const id = randomUUID().slice(0, 8);
79
120
  const runtime = {
80
121
  id,
@@ -83,6 +124,7 @@ export class ModelLab {
83
124
  endpoint: endpoint.replace(/\/+$/, ''),
84
125
  apiKey: apiKey || null,
85
126
  models: models || [],
127
+ launchConfig: launchConfig || null,
86
128
  createdAt: new Date().toISOString(),
87
129
  };
88
130
  this.runtimes.set(id, runtime);
@@ -96,7 +138,7 @@ export class ModelLab {
96
138
  const rt = this.runtimes.get(id);
97
139
  if (!rt) return null;
98
140
 
99
- // Stop the llama-server process if this is a local model runtime
141
+ // Stop the llama-server process if this is a local GGUF runtime
100
142
  if (rt._localModelId) {
101
143
  const mm = this.daemon.modelManager;
102
144
  const ls = this.daemon.llamaServer;
@@ -106,6 +148,22 @@ export class ModelLab {
106
148
  }
107
149
  }
108
150
 
151
+ // Stop the MLX server if this is an MLX runtime
152
+ if (rt._mlxModelId) {
153
+ const ms = this.daemon.mlxServer;
154
+ if (ms) {
155
+ const hfId = rt._mlxModelId.startsWith('mlx:') ? rt._mlxModelId.slice(4) : rt._mlxModelId;
156
+ ms.stopServer(hfId).catch(() => {});
157
+ }
158
+ }
159
+
160
+ // Stop any managed process
161
+ const proc = this._processes.get(id);
162
+ if (proc) {
163
+ try { proc.kill('SIGTERM'); } catch {}
164
+ this._processes.delete(id);
165
+ }
166
+
109
167
  this.runtimes.delete(id);
110
168
  this._saveRuntimes();
111
169
  this.daemon.broadcast({ type: 'lab:runtime:removed', data: { id } });
@@ -113,6 +171,135 @@ export class ModelLab {
113
171
  return rt;
114
172
  }
115
173
 
174
+ async startRuntime(id) {
175
+ const rt = this.runtimes.get(id);
176
+ if (!rt) throw new Error('Runtime not found');
177
+
178
+ // MLX runtimes — use built-in MLXServerManager
179
+ if (rt.type === 'mlx' && !rt.launchConfig) {
180
+ const modelId = rt._mlxModelId || this._deriveModelId(rt, 'MLX - ');
181
+ if (modelId) {
182
+ const hfId = modelId.startsWith('mlx:') ? modelId.slice(4) : modelId;
183
+ const ms = this.daemon.mlxServer;
184
+ if (!ms) throw new Error('MLX server manager not available');
185
+ const endpoint = await ms.ensureServer(hfId);
186
+ rt.endpoint = endpoint;
187
+ if (!rt._mlxModelId) { rt._mlxModelId = `mlx:${hfId}`; }
188
+ this._saveRuntimes();
189
+ this.daemon.broadcast({ type: 'lab:runtime:started', data: { id } });
190
+ this.daemon.audit.log('lab.runtime.start', { id, name: rt.name });
191
+ return rt;
192
+ }
193
+ }
194
+
195
+ // llama-cpp runtimes — use built-in LlamaServerManager
196
+ if (rt.type === 'llama-cpp' && rt._localModelId && !rt.launchConfig) {
197
+ const mm = this.daemon.modelManager;
198
+ const ls = this.daemon.llamaServer;
199
+ if (!mm || !ls) throw new Error('llama-server not available');
200
+ const modelPath = mm.getModelPath(rt._localModelId);
201
+ if (!modelPath) throw new Error('Model file not found');
202
+ const endpoint = await ls.ensureServer(modelPath);
203
+ rt.endpoint = endpoint;
204
+ this._saveRuntimes();
205
+ this.daemon.broadcast({ type: 'lab:runtime:started', data: { id } });
206
+ this.daemon.audit.log('lab.runtime.start', { id, name: rt.name });
207
+ return rt;
208
+ }
209
+
210
+ // Generic launchConfig runtimes (vLLM, TGI, etc.)
211
+ if (!rt.launchConfig) throw new Error('No launch config — use the assistant to set up this runtime first');
212
+ if (this._processes.has(id)) throw new Error('Server already running');
213
+
214
+ const lc = rt.launchConfig;
215
+ const proc = spawn(lc.command, lc.args, {
216
+ stdio: ['ignore', 'pipe', 'pipe'],
217
+ env: { ...process.env, ...lc.env },
218
+ detached: false,
219
+ });
220
+
221
+ if (!proc.pid) throw new Error('Failed to start server process');
222
+
223
+ this._processes.set(id, proc);
224
+
225
+ proc.on('exit', (code, signal) => {
226
+ this._processes.delete(id);
227
+ this.daemon?.broadcast({ type: 'lab:runtime:stopped', data: { id, code, signal } });
228
+ });
229
+
230
+ const endpoint = rt.endpoint.replace('localhost', '127.0.0.1');
231
+ const healthUrl = rt.type === 'ollama' ? `${endpoint}/api/tags` : `${endpoint}/v1/models`;
232
+
233
+ try {
234
+ await this._waitForServer(healthUrl, 60000);
235
+ } catch (err) {
236
+ await this.stopRuntime(id);
237
+ throw new Error(`Server failed to become healthy: ${err.message}`);
238
+ }
239
+
240
+ this.daemon.broadcast({ type: 'lab:runtime:started', data: { id } });
241
+ this.daemon.audit.log('lab.runtime.start', { id, name: rt.name });
242
+ return rt;
243
+ }
244
+
245
+ _deriveModelId(rt, prefix) {
246
+ if (rt.name && rt.name.startsWith(prefix)) {
247
+ return rt.name.slice(prefix.length).trim();
248
+ }
249
+ if (rt.models?.length > 0) {
250
+ return rt.models[0].id || rt.models[0].name;
251
+ }
252
+ return null;
253
+ }
254
+
255
+ async _waitForServer(url, timeout) {
256
+ const start = Date.now();
257
+ while (Date.now() - start < timeout) {
258
+ try {
259
+ const res = await fetch(url, { signal: AbortSignal.timeout(2000) });
260
+ if (res.ok) return;
261
+ } catch { /* server still starting */ }
262
+ await new Promise((r) => setTimeout(r, 1000));
263
+ }
264
+ throw new Error(`Health check timed out after ${timeout / 1000}s`);
265
+ }
266
+
267
+ async stopRuntime(id) {
268
+ const rt = this.runtimes.get(id);
269
+ if (!rt) throw new Error('Runtime not found');
270
+
271
+ if (rt._localModelId) {
272
+ const mm = this.daemon.modelManager;
273
+ const ls = this.daemon.llamaServer;
274
+ if (mm && ls) {
275
+ const modelPath = mm.getModelPath(rt._localModelId);
276
+ if (modelPath) await ls.stopServer(modelPath);
277
+ }
278
+ }
279
+
280
+ if (rt._mlxModelId) {
281
+ const ms = this.daemon.mlxServer;
282
+ if (ms) {
283
+ const hfId = rt._mlxModelId.startsWith('mlx:') ? rt._mlxModelId.slice(4) : rt._mlxModelId;
284
+ await ms.stopServer(hfId);
285
+ }
286
+ }
287
+
288
+ const proc = this._processes.get(id);
289
+ if (proc) {
290
+ await new Promise((resolve) => {
291
+ const timeout = setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} }, 5000);
292
+ proc.on('exit', () => { clearTimeout(timeout); resolve(); });
293
+ try { proc.kill('SIGTERM'); } catch { clearTimeout(timeout); resolve(); }
294
+ });
295
+ this._processes.delete(id);
296
+ }
297
+
298
+ this.daemon.broadcast({ type: 'lab:runtime:stopped', data: { id } });
299
+ this.daemon.audit.log('lab.runtime.stop', { id, name: rt.name });
300
+ return rt;
301
+ }
302
+
116
303
  getRuntime(id) {
117
304
  return this.runtimes.get(id) || null;
118
305
  }
@@ -147,9 +334,9 @@ export class ModelLab {
147
334
 
148
335
  async _discoverModels(rt) {
149
336
  if (rt.type === 'ollama') {
150
- return this._discoverOllamaModels(rt.endpoint);
337
+ return this._discoverOllamaModels(localURL(rt.endpoint));
151
338
  }
152
- return this._discoverOpenAIModels(rt.endpoint, rt.apiKey);
339
+ return this._discoverOpenAIModels(localURL(rt.endpoint), rt.apiKey);
153
340
  }
154
341
 
155
342
  async _discoverOllamaModels(endpoint) {
@@ -185,15 +372,16 @@ export class ModelLab {
185
372
  async getRuntimeStatus(rt) {
186
373
  try {
187
374
  const start = Date.now();
375
+ const ep = localURL(rt.endpoint);
188
376
  if (rt.type === 'ollama') {
189
- const resp = await fetch(`${rt.endpoint}/api/tags`, {
377
+ const resp = await fetch(`${ep}/api/tags`, {
190
378
  signal: AbortSignal.timeout(5000),
191
379
  });
192
380
  return { online: resp.ok, latency: Date.now() - start };
193
381
  }
194
382
  const headers = {};
195
383
  if (rt.apiKey) headers['Authorization'] = `Bearer ${rt.apiKey}`;
196
- const resp = await fetch(`${rt.endpoint}/v1/models`, {
384
+ const resp = await fetch(`${ep}/v1/models`, {
197
385
  headers,
198
386
  signal: AbortSignal.timeout(5000),
199
387
  });
@@ -320,7 +508,7 @@ export class ModelLab {
320
508
 
321
509
  if (rt.type === 'ollama') {
322
510
  try {
323
- const mem = await this.getOllamaMemoryUsage(rt.endpoint);
511
+ const mem = await this.getOllamaMemoryUsage(localURL(rt.endpoint));
324
512
  if (mem) onEvent({ type: 'memory', usage: mem });
325
513
  } catch { /* ignore */ }
326
514
  }
@@ -439,9 +627,13 @@ export class ModelLab {
439
627
  this._saveSession(session);
440
628
  }
441
629
 
442
- // ─── Launch Local GGUF ───────────────────────────────────────
630
+ // ─── Launch Local Model ──────────────────────────────────────
443
631
 
444
632
  async launchLocalModel(modelId) {
633
+ if (modelId.startsWith('mlx:')) {
634
+ return this.launchMLXModel(modelId);
635
+ }
636
+
445
637
  const mm = this.daemon.modelManager;
446
638
  const ls = this.daemon.llamaServer;
447
639
  if (!mm || !ls) throw new Error('Local model serving not available');
@@ -480,9 +672,113 @@ export class ModelLab {
480
672
  }
481
673
 
482
674
  listLocalModels() {
675
+ const models = [];
676
+
677
+ // GGUF models from ModelManager
483
678
  const mm = this.daemon.modelManager;
484
- if (!mm) return [];
485
- return mm.getInstalled().filter((m) => m.exists);
679
+ if (mm) {
680
+ for (const m of mm.getInstalled().filter((m) => m.exists)) {
681
+ models.push({ ...m, type: 'gguf', compatibleBackends: ['llama-cpp'] });
682
+ }
683
+ }
684
+
685
+ // HuggingFace cache models (MLX + standard HF)
686
+ try {
687
+ const hfModels = MLXServerManager.scanModels();
688
+ for (const m of hfModels) {
689
+ models.push(m);
690
+ }
691
+ } catch { /* scan may fail */ }
692
+
693
+ return models;
694
+ }
695
+
696
+ // ─── Model Suggestions ───────────────────────────────────────
697
+
698
+ async suggestAlternativeModel(modelId, targetBackend) {
699
+ const baseName = this._extractBaseName(modelId);
700
+ if (!baseName) return null;
701
+
702
+ const searchQueries = [];
703
+ if (targetBackend === 'mlx') {
704
+ searchQueries.push(`mlx-community/${baseName}`);
705
+ } else if (targetBackend === 'llama-cpp') {
706
+ searchQueries.push(baseName, `${baseName}-GGUF`);
707
+ } else if (targetBackend === 'vllm' || targetBackend === 'tgi') {
708
+ searchQueries.push(baseName);
709
+ }
710
+
711
+ for (const query of searchQueries) {
712
+ try {
713
+ const resp = await fetch(
714
+ `https://huggingface.co/api/models?search=${encodeURIComponent(query)}&limit=5&sort=downloads`,
715
+ { signal: AbortSignal.timeout(8000) },
716
+ );
717
+ if (!resp.ok) continue;
718
+ const results = await resp.json();
719
+
720
+ for (const r of results) {
721
+ if (targetBackend === 'mlx' && !r.modelId?.includes('mlx')) continue;
722
+ if (targetBackend === 'llama-cpp' && !r.modelId?.toLowerCase().includes('gguf')) continue;
723
+ if (targetBackend === 'vllm' || targetBackend === 'tgi') {
724
+ if (r.modelId?.includes('gguf') || r.modelId?.includes('mlx')) continue;
725
+ }
726
+ return {
727
+ repoId: r.modelId,
728
+ name: r.modelId?.split('/').pop() || r.modelId,
729
+ downloads: r.downloads || 0,
730
+ };
731
+ }
732
+ } catch { /* network error, skip */ }
733
+ }
734
+ return null;
735
+ }
736
+
737
+ _extractBaseName(modelId) {
738
+ let name = modelId;
739
+ if (name.startsWith('mlx:') || name.startsWith('hf:')) name = name.slice(name.indexOf(':') + 1);
740
+ name = name.split('/').pop() || name;
741
+ name = name
742
+ .replace(/\.gguf$/i, '')
743
+ .replace(/[-_](4bit|8bit|3bit|bf16|fp16|MLX|GGUF|Q\d_\w+)/gi, '')
744
+ .replace(/-+$/, '');
745
+ return name || null;
746
+ }
747
+
748
+ // ─── Launch MLX Model ────────────────────────────────────────
749
+
750
+ async launchMLXModel(modelId) {
751
+ const ms = this.daemon.mlxServer;
752
+ if (!ms) throw new Error('MLX server manager not available');
753
+
754
+ // modelId is "mlx:mlx-community/ModelName"
755
+ const hfModelId = modelId.startsWith('mlx:') ? modelId.slice(4) : modelId;
756
+
757
+ const endpoint = await ms.ensureServer(hfModelId);
758
+ if (!endpoint) throw new Error('Failed to start MLX server');
759
+
760
+ // Check if we already have a runtime for this model
761
+ const existing = [...this.runtimes.values()].find(
762
+ (r) => r._mlxModelId === modelId,
763
+ );
764
+ if (existing) {
765
+ existing.endpoint = endpoint;
766
+ this._saveRuntimes();
767
+ this.daemon.broadcast({ type: 'lab:runtime:updated', data: existing });
768
+ return { runtime: existing, model: hfModelId };
769
+ }
770
+
771
+ const shortName = hfModelId.split('/').pop() || hfModelId;
772
+ const runtime = await this.addRuntime({
773
+ name: `MLX - ${shortName}`,
774
+ type: 'mlx',
775
+ endpoint,
776
+ models: [{ id: 'default', name: shortName }],
777
+ });
778
+ runtime._mlxModelId = modelId;
779
+ this._saveRuntimes();
780
+
781
+ return { runtime, model: hfModelId };
486
782
  }
487
783
 
488
784
  // ─── Auto-detect Ollama ─────────────────────────────────────
@@ -28,7 +28,7 @@ export class ProjectManager {
28
28
 
29
29
  // Get agent registry for scope awareness
30
30
  const agents = this.daemon.registry.getAll();
31
- const agentRecord = agents.find((a) => a.name === agent);
31
+ const agentRecord = agents.find((a) => a.id === agent) || agents.find((a) => a.name === agent);
32
32
  const scope = agentRecord?.scope?.join(', ') || 'unrestricted';
33
33
  const role = agentRecord?.role || 'unknown';
34
34
 
@@ -1058,7 +1058,7 @@ For normal file edits within your scope, proceed without review.
1058
1058
  // ─── Agent Loop path (local models with built-in agentic runtime) ───
1059
1059
  if (provider.constructor.useAgentLoop) {
1060
1060
  provider.normalizeConfig(spawnConfig);
1061
- const loopConfig = provider.getLoopConfig(spawnConfig);
1061
+ const loopConfig = provider.getLoopConfig(spawnConfig, this.daemon);
1062
1062
  logStream.write(`[${new Date().toISOString()}] GROOVE agent-loop: model=${loopConfig.model} api=${loopConfig.apiBase}\n`);
1063
1063
 
1064
1064
  const loop = new AgentLoop({ daemon: this.daemon, agent, loopConfig, logStream });
@@ -2245,7 +2245,7 @@ For normal file edits within your scope, proceed without review.
2245
2245
  if (storedKey) spawnConfig.apiKey = storedKey;
2246
2246
  }
2247
2247
 
2248
- const loopConfig = provider.getLoopConfig(spawnConfig);
2248
+ const loopConfig = provider.getLoopConfig(spawnConfig, this.daemon);
2249
2249
  const loop = new AgentLoop({ daemon: this.daemon, agent: newAgent, loopConfig, logStream });
2250
2250
 
2251
2251
  this.handles.set(newAgent.id, { loop, logStream });
@@ -133,18 +133,46 @@ export class LocalProvider extends Provider {
133
133
  return config;
134
134
  }
135
135
 
136
- getLoopConfig(agent) {
137
- const model = agent.model || 'qwen2.5-coder:7b';
138
- const contextWindow = this.getContextWindow(model);
139
-
140
- // Determine API endpoint
141
- let apiBase = 'http://localhost:11434/v1'; // Ollama's OpenAI-compatible endpoint (default)
136
+ getLoopConfig(agent, daemon) {
137
+ let model = agent.model || 'qwen2.5-coder:7b';
138
+ let apiBase = 'http://localhost:11434/v1';
139
+ let apiKey = agent.apiKey || null;
142
140
 
143
- // Custom endpoint override from agent config or daemon config
144
141
  if (agent.apiBase) {
145
142
  apiBase = agent.apiBase;
146
143
  }
147
144
 
145
+ // Resolve GGUF models (gguf:<id>) — find the lab runtime serving this model
146
+ if (model.startsWith('gguf:') && daemon?.modelLab) {
147
+ const ggufId = model.slice(5);
148
+ const runtimes = daemon.modelLab.listRuntimes();
149
+ const rt = runtimes.find(r =>
150
+ r._localModelId === ggufId ||
151
+ r.models?.some(rm => rm.id === ggufId || rm.name === ggufId)
152
+ );
153
+ if (rt) {
154
+ apiBase = rt.endpoint.includes('/v1') ? rt.endpoint : `${rt.endpoint}/v1`;
155
+ if (rt.apiKey) apiKey = rt.apiKey;
156
+ const rtModel = rt.models?.[0];
157
+ model = rtModel?.id || rtModel?.name || ggufId;
158
+ }
159
+ }
160
+
161
+ // Resolve runtime models (runtime:<runtimeId>:<modelId>)
162
+ if (model.startsWith('runtime:') && daemon?.modelLab) {
163
+ const parts = model.split(':');
164
+ const runtimeId = parts[1];
165
+ const modelId = parts.slice(2).join(':');
166
+ const rt = daemon.modelLab.getRuntime(runtimeId);
167
+ if (rt) {
168
+ apiBase = rt.endpoint.includes('/v1') ? rt.endpoint : `${rt.endpoint}/v1`;
169
+ if (rt.apiKey) apiKey = rt.apiKey;
170
+ model = modelId;
171
+ }
172
+ }
173
+
174
+ const contextWindow = this.getContextWindow(model);
175
+
148
176
  return {
149
177
  apiBase,
150
178
  model,
@@ -153,7 +181,7 @@ export class LocalProvider extends Provider {
153
181
  maxResponseTokens: 4096,
154
182
  stream: true,
155
183
  headers: {},
156
- apiKey: agent.apiKey || null,
184
+ apiKey,
157
185
  introContext: agent.introContext || '',
158
186
  };
159
187
  }
@@ -11,16 +11,31 @@ export class Registry extends EventEmitter {
11
11
  super();
12
12
  this.state = state;
13
13
  this.agents = new Map();
14
+ this._counters = new Map();
15
+ this._initCounters();
16
+ }
17
+
18
+ _initCounters() {
19
+ for (const agent of this.agents.values()) {
20
+ const match = agent.name.match(/^(.+)-(\d+)$/);
21
+ if (!match) continue;
22
+ const role = match[1];
23
+ const num = parseInt(match[2], 10);
24
+ const current = this._counters.get(role) || 0;
25
+ if (num > current) this._counters.set(role, num);
26
+ }
14
27
  }
15
28
 
16
29
  add(config) {
17
- let name = config.name || `${config.role}-${this.agents.size + 1}`;
18
- // Dedup: ensure name is unique within the same team
19
- const teamId = config.teamId || null;
30
+ const role = config.role;
31
+ const count = (this._counters.get(role) || 0) + 1;
32
+ this._counters.set(role, count);
33
+ let name = config.name || `${role}-${count}`;
34
+ // Dedup: ensure name is globally unique (no two agents ever share a name)
20
35
  const existing = this.getAll();
21
- if (existing.some((a) => a.name === name && a.teamId === teamId)) {
36
+ if (existing.some((a) => a.name === name)) {
22
37
  let suffix = 2;
23
- while (existing.some((a) => a.name === `${name}-${suffix}` && a.teamId === teamId)) suffix++;
38
+ while (existing.some((a) => a.name === `${name}-${suffix}`)) suffix++;
24
39
  name = `${name}-${suffix}`;
25
40
  }
26
41
  const agent = {
@@ -130,6 +145,7 @@ export class Registry extends EventEmitter {
130
145
  agent.pid = null;
131
146
  this.agents.set(agent.id, agent);
132
147
  }
148
+ this._initCounters();
133
149
  if (agents.length > 0) {
134
150
  this.emit('change', { changed: agents.map((a) => a.id) });
135
151
  }