groove-dev 0.27.124 → 0.27.125

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 (31) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/src/api.js +122 -0
  4. package/node_modules/@groove-dev/daemon/src/preview.js +28 -5
  5. package/node_modules/@groove-dev/daemon/src/process.js +21 -0
  6. package/node_modules/@groove-dev/daemon/src/providers/local.js +19 -20
  7. package/node_modules/@groove-dev/daemon/src/providers/ollama.js +66 -3
  8. package/node_modules/@groove-dev/gui/dist/assets/{index-BcmoHTm0.js → index-BU0bL6BB.js} +1748 -1748
  9. package/node_modules/@groove-dev/gui/dist/assets/{index-DWI-g_Sm.css → index-D3RyFPc0.css} +1 -1
  10. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  11. package/node_modules/@groove-dev/gui/package.json +1 -1
  12. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +66 -6
  13. package/node_modules/@groove-dev/gui/src/stores/groove.js +169 -0
  14. package/node_modules/@groove-dev/gui/src/views/agents.jsx +8 -10
  15. package/node_modules/@groove-dev/gui/src/views/models.jsx +507 -236
  16. package/package.json +1 -1
  17. package/packages/cli/package.json +1 -1
  18. package/packages/daemon/package.json +1 -1
  19. package/packages/daemon/src/api.js +122 -0
  20. package/packages/daemon/src/preview.js +28 -5
  21. package/packages/daemon/src/process.js +21 -0
  22. package/packages/daemon/src/providers/local.js +19 -20
  23. package/packages/daemon/src/providers/ollama.js +66 -3
  24. package/packages/gui/dist/assets/{index-BcmoHTm0.js → index-BU0bL6BB.js} +1748 -1748
  25. package/packages/gui/dist/assets/{index-DWI-g_Sm.css → index-D3RyFPc0.css} +1 -1
  26. package/packages/gui/dist/index.html +2 -2
  27. package/packages/gui/package.json +1 -1
  28. package/packages/gui/src/components/agents/spawn-wizard.jsx +66 -6
  29. package/packages/gui/src/stores/groove.js +169 -0
  30. package/packages/gui/src/views/agents.jsx +8 -10
  31. package/packages/gui/src/views/models.jsx +507 -236
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.124",
3
+ "version": "0.27.125",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.124",
3
+ "version": "0.27.125",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -493,6 +493,8 @@ export function createApi(app, daemon) {
493
493
 
494
494
  // --- Ollama ---
495
495
 
496
+ const isValidModelId = (id) => typeof id === 'string' && id.length > 0 && id.length < 200 && /^[a-zA-Z0-9._:/-]+$/.test(id);
497
+
496
498
  app.get('/api/providers/ollama/hardware', (req, res) => {
497
499
  res.json(OllamaProvider.getSystemHardware());
498
500
  });
@@ -507,6 +509,7 @@ export function createApi(app, daemon) {
507
509
  app.post('/api/providers/ollama/pull', async (req, res) => {
508
510
  const { model } = req.body;
509
511
  if (!model) return res.status(400).json({ error: 'model is required' });
512
+ if (!isValidModelId(model)) return res.status(400).json({ error: 'Invalid model ID' });
510
513
  if (!OllamaProvider.isInstalled()) {
511
514
  const install = OllamaProvider.installCommand();
512
515
  return res.status(400).json({ error: `Ollama is not installed. Install with: ${install.command}` });
@@ -541,6 +544,7 @@ export function createApi(app, daemon) {
541
544
  });
542
545
 
543
546
  app.delete('/api/providers/ollama/models/:model', (req, res) => {
547
+ if (!isValidModelId(req.params.model)) return res.status(400).json({ error: 'Invalid model ID' });
544
548
  if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
545
549
  const success = OllamaProvider.deleteModel(req.params.model);
546
550
  if (success) {
@@ -604,6 +608,65 @@ export function createApi(app, daemon) {
604
608
  }
605
609
  });
606
610
 
611
+ app.get('/api/providers/ollama/running', async (req, res) => {
612
+ if (!OllamaProvider.isInstalled()) return res.json({ models: [] });
613
+ const serverRunning = await OllamaProvider.isServerRunning();
614
+ if (!serverRunning) return res.json({ models: [] });
615
+ try {
616
+ const controller = new AbortController();
617
+ const timeout = setTimeout(() => controller.abort(), 5000);
618
+ const apiRes = await fetch('http://localhost:11434/api/ps', { signal: controller.signal });
619
+ clearTimeout(timeout);
620
+ if (!apiRes.ok) return res.json({ models: [] });
621
+ const data = await apiRes.json();
622
+ const models = (data.models || []).map((m) => ({
623
+ name: m.name || m.model || '',
624
+ size: m.size || 0,
625
+ vram: m.size_vram ?? m.size ?? 0,
626
+ expires: m.expires_at || null,
627
+ }));
628
+ res.json({ models });
629
+ } catch {
630
+ res.json({ models: OllamaProvider.getRunningModels() });
631
+ }
632
+ });
633
+
634
+ app.post('/api/providers/ollama/load', async (req, res) => {
635
+ const { model } = req.body;
636
+ if (!model) return res.status(400).json({ error: 'model is required' });
637
+ if (!isValidModelId(model)) return res.status(400).json({ error: 'Invalid model ID' });
638
+ if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
639
+ const serverRunning = await OllamaProvider.isServerRunning();
640
+ if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
641
+ try {
642
+ await OllamaProvider.loadModel(model);
643
+ daemon.broadcast({ type: 'ollama:model:loaded', model });
644
+ daemon.audit.log('ollama.model.load', { model });
645
+ res.json({ ok: true, model });
646
+ } catch (err) {
647
+ daemon.broadcast({ type: 'model:error', model, error: err.message });
648
+ res.status(500).json({ error: `Failed to load model: ${err.message}` });
649
+ }
650
+ });
651
+
652
+ app.post('/api/providers/ollama/unload', async (req, res) => {
653
+ const { model } = req.body;
654
+ if (!model) return res.status(400).json({ error: 'model is required' });
655
+ if (!isValidModelId(model)) return res.status(400).json({ error: 'Invalid model ID' });
656
+ if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
657
+ const serverRunning = await OllamaProvider.isServerRunning();
658
+ if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
659
+ try {
660
+ await OllamaProvider.unloadModel(model);
661
+ daemon.broadcast({ type: 'ollama:model:unloaded', model });
662
+ daemon.audit.log('ollama.model.unload', { model });
663
+ res.json({ ok: true });
664
+ } catch (err) {
665
+ daemon.broadcast({ type: 'model:error', model, error: err.message });
666
+ res.status(500).json({ error: `Failed to unload model: ${err.message}` });
667
+ }
668
+ });
669
+
607
670
  // --- Provider Management (install, login, set-path, verify) ---
608
671
 
609
672
  const MANAGEABLE_PROVIDERS = new Set(['claude-code', 'codex', 'gemini']);
@@ -967,6 +1030,59 @@ export function createApi(app, daemon) {
967
1030
  res.json({ models: recommended, hardware });
968
1031
  });
969
1032
 
1033
+ // --- Ollama Running Models ---
1034
+
1035
+ app.get('/api/models/status', async (req, res) => {
1036
+ const installed = OllamaProvider.isInstalled();
1037
+ if (!installed) return res.json({ serverRunning: false, runningModels: [], installedModels: [], hardware: OllamaProvider.getSystemHardware() });
1038
+ const serverRunning = await OllamaProvider.isServerRunning();
1039
+ const runningModels = serverRunning ? OllamaProvider.getRunningModels() : [];
1040
+ const installedModels = OllamaProvider.getInstalledModels();
1041
+ const hardware = OllamaProvider.getSystemHardware();
1042
+ res.json({ serverRunning, runningModels, installedModels, hardware });
1043
+ });
1044
+
1045
+ app.get('/api/models/running', async (req, res) => {
1046
+ if (!OllamaProvider.isInstalled()) return res.json([]);
1047
+ const serverRunning = await OllamaProvider.isServerRunning();
1048
+ if (!serverRunning) return res.json([]);
1049
+ res.json(OllamaProvider.getRunningModels());
1050
+ });
1051
+
1052
+ app.post('/api/models/:id/load', async (req, res) => {
1053
+ const modelId = req.params.id;
1054
+ if (!modelId) return res.status(400).json({ error: 'model id is required' });
1055
+ if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
1056
+ const serverRunning = await OllamaProvider.isServerRunning();
1057
+ if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
1058
+ try {
1059
+ const result = await OllamaProvider.loadModel(modelId);
1060
+ daemon.broadcast({ type: 'model:loaded', model: modelId });
1061
+ daemon.audit.log('model.load', { model: modelId });
1062
+ res.json(result);
1063
+ } catch (err) {
1064
+ daemon.broadcast({ type: 'model:error', model: modelId, error: err.message });
1065
+ res.status(500).json({ error: `Failed to load model: ${err.message}` });
1066
+ }
1067
+ });
1068
+
1069
+ app.post('/api/models/:id/unload', async (req, res) => {
1070
+ const modelId = req.params.id;
1071
+ if (!modelId) return res.status(400).json({ error: 'model id is required' });
1072
+ if (!OllamaProvider.isInstalled()) return res.status(400).json({ error: 'Ollama is not installed' });
1073
+ const serverRunning = await OllamaProvider.isServerRunning();
1074
+ if (!serverRunning) return res.status(400).json({ error: 'Ollama server is not running' });
1075
+ try {
1076
+ const result = await OllamaProvider.unloadModel(modelId);
1077
+ daemon.broadcast({ type: 'model:unloaded', model: modelId });
1078
+ daemon.audit.log('model.unload', { model: modelId });
1079
+ res.json(result);
1080
+ } catch (err) {
1081
+ daemon.broadcast({ type: 'model:error', model: modelId, error: err.message });
1082
+ res.status(500).json({ error: `Failed to unload model: ${err.message}` });
1083
+ }
1084
+ });
1085
+
970
1086
  app.get('/api/llama/status', (req, res) => {
971
1087
  res.json(daemon.llamaServer.getStatus());
972
1088
  });
@@ -3916,8 +4032,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
3916
4032
  // JS imports: from '/' and import('/')
3917
4033
  out = out.replace(/(from\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
3918
4034
  out = out.replace(/(import\s*\(\s*(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
4035
+ // JS bare import: import '/path'
4036
+ out = out.replace(/(import\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
3919
4037
  // CSS url()
3920
4038
  out = out.replace(/(url\s*\(\s*(["']?))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
4039
+ // CSS @import '/path'
4040
+ out = out.replace(/(@import\s+(["']))\/(?!\/|api\/preview\/)/g, `$1${proxyBase}/`);
4041
+ // Vite base assignments: globalThis.__vite_base = "/" or window.__vite_base = "/"
4042
+ out = out.replace(/((?:globalThis|window)\.__vite_base\s*=\s*(["']))\/(?=["'])/g, `$1${proxyBase}/`);
3921
4043
  return out;
3922
4044
  }
3923
4045
 
@@ -131,7 +131,13 @@ export class PreviewService {
131
131
  result = await this._launchStatic(teamId, baseDir, preview);
132
132
  }
133
133
  } else {
134
- result = await this._launchStatic(teamId, baseDir, preview);
134
+ const distDir = resolve(baseDir, 'dist');
135
+ const openFile = (preview.openPath || 'index.html').replace(/^\/+/, '');
136
+ if (existsSync(resolve(distDir, openFile))) {
137
+ result = await this._launchStatic(teamId, distDir, { ...preview, openPath: openFile });
138
+ } else {
139
+ result = await this._launchStatic(teamId, baseDir, preview);
140
+ }
135
141
  }
136
142
  } else if (preview.kind === 'dev-server') {
137
143
  if (this._needsPreBuild(baseDir)) {
@@ -250,14 +256,31 @@ export class PreviewService {
250
256
  _runBuild(teamId, baseDir) {
251
257
  const pkgPath = resolve(baseDir, 'package.json');
252
258
  if (!existsSync(pkgPath)) return { failed: true, reason: 'no package.json for build' };
259
+ let pkg;
253
260
  try {
254
- const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
261
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
255
262
  if (!pkg.scripts?.build) return { failed: true, reason: 'no build script' };
256
263
  } catch { return { failed: true, reason: 'malformed package.json' }; }
264
+
265
+ const isVite = ['vite.config.js', 'vite.config.ts', 'vite.config.mjs']
266
+ .some((f) => existsSync(resolve(baseDir, f)));
267
+
268
+ let command = 'npm run build';
269
+ const buildScript = (pkg.scripts.build || '').trim();
270
+
271
+ if (isVite && /^(tsc\s*&&\s*)?vite\s+build\s*$/.test(buildScript)) {
272
+ command = `npm run build -- --base=./`;
273
+ }
274
+
275
+ const env = { ...process.env };
276
+ if (isVite && command === 'npm run build') {
277
+ env.VITE_BASE = './';
278
+ }
279
+
257
280
  try {
258
- console.log(`[Groove:Preview] Running npm run build in ${baseDir}`);
259
- this.daemon.audit?.log('preview.build', { teamId, baseDir });
260
- execSync('npm run build', { cwd: baseDir, timeout: 120_000, stdio: 'pipe' });
281
+ console.log(`[Groove:Preview] Running ${command} in ${baseDir}`);
282
+ this.daemon.audit?.log('preview.build', { teamId, baseDir, command });
283
+ execSync(command, { cwd: baseDir, timeout: 120_000, stdio: 'pipe', env });
261
284
  return null;
262
285
  } catch (err) {
263
286
  return { failed: true, reason: `build failed: ${err.message?.slice(0, 300)}` };
@@ -6,6 +6,8 @@ import { createWriteStream, mkdirSync, chmodSync, existsSync, readFileSync, writ
6
6
  import { resolve, dirname, isAbsolute } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { getProvider, getInstalledProviders, resolveProviderCommand } from './providers/index.js';
9
+ import { LocalProvider } from './providers/local.js';
10
+ import { OllamaProvider } from './providers/ollama.js';
9
11
  import { AgentLoop } from './agent-loop.js';
10
12
  import { validateAgentConfig } from './validate.js';
11
13
 
@@ -735,6 +737,25 @@ export class ProcessManager {
735
737
  );
736
738
  }
737
739
 
740
+ // Pre-flight for local model providers: ensure Ollama server is running and model is installed
741
+ if (providerName === 'local' || providerName === 'ollama') {
742
+ try {
743
+ await LocalProvider.ensureServerRunning();
744
+ } catch (err) {
745
+ const agent = registry.add({ ...config, provider: providerName, status: 'error' });
746
+ registry.update(agent.id, { status: 'error', error: 'Ollama server failed to start' });
747
+ this.daemon.broadcast({ type: 'model:error', agentId: agent.id, error: err.message });
748
+ throw new Error('Ollama server failed to start: ' + err.message);
749
+ }
750
+ if (config.model && config.model !== 'auto') {
751
+ const installed = OllamaProvider.getInstalledModels();
752
+ const modelInstalled = installed.some((m) => m.id === config.model || config.model.startsWith(m.id.split(':')[0]));
753
+ if (!modelInstalled) {
754
+ throw new Error(`Model '${config.model}' is not installed. Pull it first with: ollama pull ${config.model}`);
755
+ }
756
+ }
757
+ }
758
+
738
759
  // Validate explicit model against provider's supported models
739
760
  if (config.model && config.model !== 'auto' && provider.constructor.models) {
740
761
  const valid = provider.constructor.models.some(m => m.id === config.model);
@@ -59,27 +59,13 @@ export class LocalProvider extends Provider {
59
59
 
60
60
  // Only return models that are actually installed and ready to use
61
61
  static get models() {
62
- const installed = [];
62
+ if (!LocalProvider._hasOllama()) return [];
63
63
 
64
- // Ollama installed models
65
- if (LocalProvider._hasOllama()) {
66
- try {
67
- const ollamaModels = OllamaProvider.getInstalledModels();
68
- for (const m of ollamaModels) {
69
- installed.push({
70
- id: m.id, name: m.name || m.id,
71
- tier: m.tier || 'medium', category: m.category || 'general',
72
- });
73
- }
74
- } catch { /* Ollama not running */ }
75
- }
76
-
77
- // If nothing installed, show a hint instead of a blank list
78
- if (installed.length === 0) {
79
- return [{ id: '_none', name: 'No models installed — pull one with: ollama pull qwen2.5-coder:7b', tier: 'medium', disabled: true }];
80
- }
81
-
82
- return installed;
64
+ const ollamaModels = OllamaProvider.getInstalledModels();
65
+ return ollamaModels.map((m) => ({
66
+ id: m.id, name: m.name || m.id,
67
+ tier: m.tier || 'medium', category: m.category || 'general',
68
+ }));
83
69
  }
84
70
 
85
71
  // Full catalog for the Models browser (includes uninstalled)
@@ -123,6 +109,19 @@ export class LocalProvider extends Provider {
123
109
  return OllamaProvider.getInstalledModels();
124
110
  }
125
111
 
112
+ static async ensureServerRunning() {
113
+ if (!LocalProvider._hasOllama()) {
114
+ throw new Error('Ollama binary not found. Install with: ' + OllamaProvider.installCommand().command);
115
+ }
116
+ if (await OllamaProvider.isServerRunning()) return true;
117
+ OllamaProvider.startServer();
118
+ for (let i = 0; i < 20; i++) {
119
+ await new Promise((r) => setTimeout(r, 500));
120
+ if (await OllamaProvider.isServerRunning()) return true;
121
+ }
122
+ throw new Error('Ollama server failed to start within 10s');
123
+ }
124
+
126
125
  /**
127
126
  * Get configuration for the agent loop runtime.
128
127
  * Called by ProcessManager when useAgentLoop is true.
@@ -1,8 +1,7 @@
1
1
  // GROOVE — Ollama Provider (Local Models)
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import { execSync } from 'child_process';
5
- import { execFile } from 'child_process';
4
+ import { execSync, execFile, execFileSync } from 'child_process';
6
5
  import os from 'os';
7
6
  import { Provider } from './base.js';
8
7
 
@@ -246,9 +245,73 @@ export class OllamaProvider extends Provider {
246
245
  });
247
246
  }
248
247
 
248
+ static getRunningModels() {
249
+ try {
250
+ const output = execSync('ollama ps', { encoding: 'utf8', timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] });
251
+ const lines = output.split('\n').slice(1).filter(Boolean);
252
+ return lines.map((line) => {
253
+ const parts = line.split(/\s+/);
254
+ return {
255
+ id: parts[0] || '',
256
+ name: parts[0] || '',
257
+ size: parts[1] || '',
258
+ vram: parts[2] || '',
259
+ processor: parts[3] || '',
260
+ until: parts.slice(4).join(' ') || '',
261
+ };
262
+ });
263
+ } catch {
264
+ return [];
265
+ }
266
+ }
267
+
268
+ static async loadModel(modelId) {
269
+ const controller = new AbortController();
270
+ const timeout = setTimeout(() => controller.abort(), 60000);
271
+ try {
272
+ const res = await fetch('http://localhost:11434/api/generate', {
273
+ method: 'POST',
274
+ headers: { 'Content-Type': 'application/json' },
275
+ body: JSON.stringify({ model: modelId, prompt: '', keep_alive: '10m' }),
276
+ signal: controller.signal,
277
+ });
278
+ clearTimeout(timeout);
279
+ if (!res.ok) {
280
+ const text = await res.text();
281
+ throw new Error(`Ollama API ${res.status}: ${text.slice(0, 200)}`);
282
+ }
283
+ return { loaded: true, model: modelId };
284
+ } catch (err) {
285
+ clearTimeout(timeout);
286
+ throw err;
287
+ }
288
+ }
289
+
290
+ static async unloadModel(modelId) {
291
+ const controller = new AbortController();
292
+ const timeout = setTimeout(() => controller.abort(), 10000);
293
+ try {
294
+ const res = await fetch('http://localhost:11434/api/generate', {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json' },
297
+ body: JSON.stringify({ model: modelId, prompt: '', keep_alive: 0 }),
298
+ signal: controller.signal,
299
+ });
300
+ clearTimeout(timeout);
301
+ if (!res.ok) {
302
+ const text = await res.text();
303
+ throw new Error(`Ollama API ${res.status}: ${text.slice(0, 200)}`);
304
+ }
305
+ return { unloaded: true, model: modelId };
306
+ } catch (err) {
307
+ clearTimeout(timeout);
308
+ throw err;
309
+ }
310
+ }
311
+
249
312
  static deleteModel(modelId) {
250
313
  try {
251
- execSync(`ollama rm ${modelId}`, { encoding: 'utf8', timeout: 30000 });
314
+ execFileSync('ollama', ['rm', modelId], { encoding: 'utf8', timeout: 30000 });
252
315
  return true;
253
316
  } catch {
254
317
  return false;