groove-dev 0.27.59 → 0.27.61

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 (68) 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 +73 -56
  4. package/node_modules/@groove-dev/daemon/src/conversations.js +78 -35
  5. package/node_modules/@groove-dev/daemon/src/journalist.js +1 -0
  6. package/node_modules/@groove-dev/daemon/src/process.js +17 -7
  7. package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
  8. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +63 -0
  9. package/node_modules/@groove-dev/daemon/src/providers/codex.js +55 -0
  10. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +53 -0
  11. package/node_modules/@groove-dev/daemon/src/providers/groove-network.js +1 -1
  12. package/node_modules/@groove-dev/daemon/src/providers/index.js +16 -1
  13. package/node_modules/@groove-dev/daemon/src/providers/local.js +44 -0
  14. package/node_modules/@groove-dev/daemon/src/providers/ollama.js +44 -0
  15. package/node_modules/@groove-dev/daemon/src/rotator.js +4 -0
  16. package/node_modules/@groove-dev/gui/dist/assets/index-B3AqeyS4.css +1 -0
  17. package/node_modules/@groove-dev/gui/dist/assets/index-DWao9glo.js +8614 -0
  18. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  19. package/node_modules/@groove-dev/gui/package.json +1 -1
  20. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +3 -2
  21. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +1 -1
  22. package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +13 -7
  23. package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +245 -0
  24. package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +1 -1
  25. package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +1 -1
  26. package/node_modules/@groove-dev/gui/src/components/network/network-status.jsx +5 -5
  27. package/node_modules/@groove-dev/gui/src/components/network/node-details.jsx +1 -1
  28. package/node_modules/@groove-dev/gui/src/components/ui/update-modal.jsx +70 -0
  29. package/node_modules/@groove-dev/gui/src/stores/groove.js +66 -6
  30. package/node_modules/@groove-dev/gui/src/views/network.jsx +99 -17
  31. package/package.json +1 -1
  32. package/packages/cli/package.json +1 -1
  33. package/packages/daemon/package.json +1 -1
  34. package/packages/daemon/src/api.js +73 -56
  35. package/packages/daemon/src/conversations.js +78 -35
  36. package/packages/daemon/src/journalist.js +1 -0
  37. package/packages/daemon/src/process.js +17 -7
  38. package/packages/daemon/src/providers/base.js +4 -0
  39. package/packages/daemon/src/providers/claude-code.js +63 -0
  40. package/packages/daemon/src/providers/codex.js +55 -0
  41. package/packages/daemon/src/providers/gemini.js +53 -0
  42. package/packages/daemon/src/providers/groove-network.js +1 -1
  43. package/packages/daemon/src/providers/index.js +16 -1
  44. package/packages/daemon/src/providers/local.js +44 -0
  45. package/packages/daemon/src/providers/ollama.js +44 -0
  46. package/packages/daemon/src/rotator.js +4 -0
  47. package/packages/gui/dist/assets/index-B3AqeyS4.css +1 -0
  48. package/packages/gui/dist/assets/index-DWao9glo.js +8614 -0
  49. package/packages/gui/dist/index.html +2 -2
  50. package/packages/gui/package.json +1 -1
  51. package/packages/gui/src/components/chat/chat-view.jsx +3 -2
  52. package/packages/gui/src/components/chat/model-picker.jsx +1 -1
  53. package/packages/gui/src/components/layout/status-bar.jsx +13 -7
  54. package/packages/gui/src/components/network/activity-chart.jsx +245 -0
  55. package/packages/gui/src/components/network/compute-header.jsx +1 -1
  56. package/packages/gui/src/components/network/network-health.jsx +1 -1
  57. package/packages/gui/src/components/network/network-status.jsx +5 -5
  58. package/packages/gui/src/components/network/node-details.jsx +1 -1
  59. package/packages/gui/src/components/ui/update-modal.jsx +70 -0
  60. package/packages/gui/src/stores/groove.js +66 -6
  61. package/packages/gui/src/views/network.jsx +99 -17
  62. package/default/fix-beta-endpoint-deployment.md +0 -68
  63. package/default/groovedev-beta-auth-endpoint.md +0 -166
  64. package/default/security-review-prompt.md +0 -98
  65. package/node_modules/@groove-dev/gui/dist/assets/index-BrfCzrxJ.css +0 -1
  66. package/node_modules/@groove-dev/gui/dist/assets/index-BycOlqLx.js +0 -8614
  67. package/packages/gui/dist/assets/index-BrfCzrxJ.css +0 -1
  68. package/packages/gui/dist/assets/index-BycOlqLx.js +0 -8614
@@ -9,11 +9,11 @@ import { ScrollArea } from '../components/ui/scroll-area';
9
9
  import { cn } from '../lib/cn';
10
10
  import { NodeToggle } from '../components/network/node-toggle';
11
11
  import { ComputeHeader } from '../components/network/compute-header';
12
- import { FleetTable } from '../components/network/fleet-table';
12
+ import { ActivityChart } from '../components/network/activity-chart';
13
13
  import { ActivityStream } from '../components/network/activity-stream';
14
14
  import { NetworkHealth } from '../components/network/network-health';
15
- import { HEX } from '../lib/theme-hex';
16
- import { Globe, Download, Check, AlertCircle, Loader2, Trash2, ArrowUpCircle } from 'lucide-react';
15
+ import { HEX, hexAlpha } from '../lib/theme-hex';
16
+ import { Globe, Download, Check, AlertCircle, Loader2, Trash2, ArrowUpCircle, Zap } from 'lucide-react';
17
17
 
18
18
  const REQUIREMENTS = [
19
19
  'Python 3.10 or higher',
@@ -312,6 +312,99 @@ function NetworkHeader() {
312
312
  );
313
313
  }
314
314
 
315
+ function IdleHero() {
316
+ const node = useGrooveStore((s) => s.networkNode);
317
+ const networkStatus = useGrooveStore((s) => s.networkStatus);
318
+ const startNetworkNode = useGrooveStore((s) => s.startNetworkNode);
319
+ const [pending, setPending] = useState(false);
320
+
321
+ const hardware = node.hardware || {};
322
+ const activeNodes = (networkStatus.nodes || []).filter((n) => n.status === 'active').length;
323
+ const activeSessions = networkStatus.activeSessions || 0;
324
+
325
+ async function handleStart() {
326
+ setPending(true);
327
+ try { await startNetworkNode(); }
328
+ catch { /* toasted in store */ }
329
+ setPending(false);
330
+ }
331
+
332
+ return (
333
+ <div className="flex flex-col h-full">
334
+ <NetworkHeader />
335
+ <div
336
+ className="flex-1 flex items-center justify-center bg-surface-0"
337
+ style={{ background: `radial-gradient(ellipse at 50% 40%, ${hexAlpha(HEX.accent, 0.06)} 0%, transparent 70%) ${HEX.surface0}` }}
338
+ >
339
+ <div className="max-w-lg w-full px-6 flex flex-col items-center text-center">
340
+ <div
341
+ className="w-24 h-24 rounded-full flex items-center justify-center mb-6 mx-auto"
342
+ style={{
343
+ background: hexAlpha(HEX.accent, 0.08),
344
+ border: `1px solid ${hexAlpha(HEX.accent, 0.15)}`,
345
+ boxShadow: `0 0 40px ${hexAlpha(HEX.accent, 0.1)}`,
346
+ }}
347
+ >
348
+ <Globe size={56} className="text-accent" strokeWidth={1.25} />
349
+ </div>
350
+
351
+ <h2 className="text-xl font-semibold text-text-0 font-sans text-center">
352
+ Join the Groove Network
353
+ </h2>
354
+
355
+ <p className="text-sm text-text-2 font-sans text-center leading-relaxed mt-2 max-w-sm mx-auto">
356
+ Contribute your compute to power decentralized AI inference. Your hardware runs model shards alongside other nodes in the network.
357
+ </p>
358
+
359
+ <div className="mt-8 w-full">
360
+ <div className="text-2xs font-mono text-text-3 uppercase tracking-widest mb-2 text-left">Your Hardware</div>
361
+ <div className="grid grid-cols-3 gap-2">
362
+ <div className="bg-surface-1 rounded-sm border border-border-subtle px-4 py-3">
363
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider">Device</div>
364
+ <div className="text-sm font-mono text-text-0 mt-1 truncate">{hardware.device || 'auto'}</div>
365
+ </div>
366
+ <div className="bg-surface-1 rounded-sm border border-border-subtle px-4 py-3">
367
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider">Memory</div>
368
+ <div className="text-sm font-mono text-text-0 mt-1 truncate">{hardware.memory || '—'}</div>
369
+ </div>
370
+ <div className="bg-surface-1 rounded-sm border border-border-subtle px-4 py-3">
371
+ <div className="text-2xs font-mono text-text-4 uppercase tracking-wider">GPU</div>
372
+ <div className="text-sm font-mono text-text-0 mt-1 truncate">{hardware.gpu || 'None'}</div>
373
+ </div>
374
+ </div>
375
+ </div>
376
+
377
+ <div className="mt-6 w-full max-w-sm mx-auto">
378
+ <Button variant="primary" size="lg" onClick={handleStart} disabled={pending} className="w-full">
379
+ {pending ? (
380
+ <><Loader2 size={14} className="animate-spin" /> Connecting…</>
381
+ ) : (
382
+ <><Zap size={14} /> Start Contributing</>
383
+ )}
384
+ </Button>
385
+ </div>
386
+
387
+ <div className="mt-4 text-center text-2xs font-mono text-text-3">
388
+ {activeNodes > 0 || activeSessions > 0 ? (
389
+ <span>
390
+ <span className="text-accent">{activeNodes}</span> node{activeNodes !== 1 ? 's' : ''} online · {activeSessions} active session{activeSessions !== 1 ? 's' : ''}
391
+ </span>
392
+ ) : (
393
+ <span>Checking network status…</span>
394
+ )}
395
+ </div>
396
+
397
+ {node.nodeId && (
398
+ <div className="mt-6 text-center text-2xs font-mono text-text-4">
399
+ Node identity: {node.nodeId.length > 14 ? `${node.nodeId.slice(0, 6)}…${node.nodeId.slice(-4)}` : node.nodeId}
400
+ </div>
401
+ )}
402
+ </div>
403
+ </div>
404
+ </div>
405
+ );
406
+ }
407
+
315
408
  export default function NetworkView() {
316
409
  const fetchNetworkNodeStatus = useGrooveStore((s) => s.fetchNetworkNodeStatus);
317
410
  const fetchNetworkStatus = useGrooveStore((s) => s.fetchNetworkStatus);
@@ -341,18 +434,7 @@ export default function NetworkView() {
341
434
  }
342
435
 
343
436
  if (!nodeActive) {
344
- return (
345
- <div className="flex flex-col h-full">
346
- <NetworkHeader />
347
- <ScrollArea className="flex-1">
348
- <div className="flex items-center justify-center min-h-full px-6 py-12">
349
- <div className="w-full max-w-md">
350
- <NodeToggle />
351
- </div>
352
- </div>
353
- </ScrollArea>
354
- </div>
355
- );
437
+ return <IdleHero />;
356
438
  }
357
439
 
358
440
  return (
@@ -361,10 +443,10 @@ export default function NetworkView() {
361
443
 
362
444
  <ComputeHeader />
363
445
 
364
- <div className="flex-1 min-h-0 flex flex-col" style={{ background: '#282c34', gap: '1px' }}>
446
+ <div className="flex-1 min-h-0 flex flex-col" style={{ background: HEX.surface3, gap: '1px' }}>
365
447
  <div className="min-h-0 flex-1 grid" style={{ gridTemplateColumns: '3fr 1.5fr', gap: '0 1px' }}>
366
448
  <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1">
367
- <FleetTable />
449
+ <ActivityChart />
368
450
  </div>
369
451
  <div className="min-w-0 min-h-0 overflow-hidden bg-surface-1">
370
452
  <NetworkHealth />
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.59",
3
+ "version": "0.27.61",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.59",
3
+ "version": "0.27.61",
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.59",
3
+ "version": "0.27.61",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -2,11 +2,11 @@
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
4
  import express from 'express';
5
- import { resolve, dirname, join } from 'path';
5
+ import { resolve, dirname, join, sep } from 'path';
6
6
  import { fileURLToPath } from 'url';
7
7
  import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, copyFileSync, realpathSync } from 'fs';
8
8
  import { spawn, execFile } from 'child_process';
9
- import { createHash } from 'crypto';
9
+ import { createHash, randomUUID } from 'crypto';
10
10
  import { hostname, networkInterfaces, homedir } from 'os';
11
11
  import { lookup as mimeLookup } from './mimetypes.js';
12
12
  import { listProviders, getProvider } from './providers/index.js';
@@ -14,7 +14,7 @@ import { OllamaProvider } from './providers/ollama.js';
14
14
  import { ClaudeCodeProvider } from './providers/claude-code.js';
15
15
  import { supportsSignalFlag, compareSemver, parseSemver } from './providers/groove-network.js';
16
16
  import { validateAgentConfig } from './validate.js';
17
- import { ROLE_INTEGRATIONS } from './process.js';
17
+ import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from './process.js';
18
18
 
19
19
  const __dirname = dirname(fileURLToPath(import.meta.url));
20
20
  const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
@@ -846,13 +846,18 @@ export function createApi(app, daemon) {
846
846
  if (req.body.title !== undefined) daemon.conversations.rename(req.params.id, req.body.title);
847
847
  if (req.body.pinned !== undefined) daemon.conversations.pin(req.params.id, req.body.pinned);
848
848
  if (req.body.archived !== undefined) daemon.conversations.archive(req.params.id, req.body.archived);
849
+ if (req.body.model !== undefined || req.body.provider !== undefined) {
850
+ const newProvider = req.body.provider || conv.provider;
851
+ const newModel = req.body.model || conv.model;
852
+ daemon.conversations.updateModel(req.params.id, newProvider, newModel);
853
+ }
849
854
  if (req.body.mode !== undefined) {
850
855
  if (req.body.mode !== 'api' && req.body.mode !== 'agent') {
851
856
  return res.status(400).json({ error: 'mode must be "api" or "agent"' });
852
857
  }
853
858
  await daemon.conversations.setMode(req.params.id, req.body.mode);
854
859
  }
855
- daemon.audit.log('conversation.update', { id: req.params.id, mode: req.body.mode });
860
+ daemon.audit.log('conversation.update', { id: req.params.id, provider: req.body.provider, model: req.body.model, mode: req.body.mode });
856
861
  res.json(daemon.conversations.get(req.params.id));
857
862
  } catch (err) {
858
863
  res.status(400).json({ error: err.message });
@@ -1105,8 +1110,9 @@ export function createApi(app, daemon) {
1105
1110
  if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, message.trim());
1106
1111
 
1107
1112
  // Agent loop path — send message directly to the running loop
1113
+ const wrappedMessage = wrapWithRoleReminder(agent.role, message.trim());
1108
1114
  if (daemon.processes.hasAgentLoop(req.params.id)) {
1109
- const sent = await daemon.processes.sendMessage(req.params.id, message.trim());
1115
+ const sent = await daemon.processes.sendMessage(req.params.id, wrappedMessage);
1110
1116
  if (sent) {
1111
1117
  daemon.audit.log('agent.chat', { id: req.params.id });
1112
1118
  return res.json({ id: agent.id, status: 'message_sent' });
@@ -1144,7 +1150,7 @@ export function createApi(app, daemon) {
1144
1150
  // Running CLI agent (no loop) — queue the message for delivery after
1145
1151
  // the current task completes instead of killing and respawning.
1146
1152
  if (daemon.processes.isRunning(req.params.id)) {
1147
- daemon.processes.queueMessage(req.params.id, message.trim());
1153
+ daemon.processes.queueMessage(req.params.id, wrappedMessage);
1148
1154
  daemon.audit.log('agent.chat.queued', { id: req.params.id });
1149
1155
  return res.json({ id: agent.id, status: 'message_queued' });
1150
1156
  }
@@ -1156,8 +1162,8 @@ export function createApi(app, daemon) {
1156
1162
  const SESSION_RESUME_CEILING = 5_000_000;
1157
1163
  const resumed = !!agent.sessionId && (agent.tokensUsed || 0) < SESSION_RESUME_CEILING;
1158
1164
  const newAgent = resumed
1159
- ? await daemon.processes.resume(req.params.id, message.trim())
1160
- : await daemon.rotator.rotate(req.params.id, { additionalPrompt: message.trim() });
1165
+ ? await daemon.processes.resume(req.params.id, wrappedMessage)
1166
+ : await daemon.rotator.rotate(req.params.id, { additionalPrompt: wrappedMessage });
1161
1167
 
1162
1168
  daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed });
1163
1169
  res.json(newAgent);
@@ -2340,7 +2346,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
2340
2346
  // Browse absolute paths (for directory picker in agent config)
2341
2347
  // Dirs only, localhost-only, no file content exposed
2342
2348
  app.get('/api/browse-system', (req, res) => {
2343
- const absPath = req.query.path || process.env.HOME || '/';
2349
+ const absPath = req.query.path || homedir();
2344
2350
  if (absPath.includes('\0')) return res.status(400).json({ error: 'Invalid path' });
2345
2351
  if (!existsSync(absPath)) return res.status(404).json({ error: 'Not found' });
2346
2352
 
@@ -3474,7 +3480,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3474
3480
  // Resolve shell shortcuts — GUI sends ~/... and ./...
3475
3481
  let resolvedPath = targetPath;
3476
3482
  if (resolvedPath.startsWith('~/') || resolvedPath === '~') {
3477
- resolvedPath = resolve(process.env.HOME || '/tmp', resolvedPath.slice(2));
3483
+ resolvedPath = resolve(homedir(), resolvedPath.slice(2));
3478
3484
  } else if (!resolvedPath.startsWith('/')) {
3479
3485
  resolvedPath = resolve(daemon.projectDir, resolvedPath);
3480
3486
  }
@@ -3948,15 +3954,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
3948
3954
  const BETA_RATE_WINDOW_MS = 60_000;
3949
3955
 
3950
3956
  function getMachineId() {
3951
- const nets = networkInterfaces();
3952
- const macs = [];
3953
- for (const name of Object.keys(nets)) {
3954
- for (const iface of nets[name] || []) {
3955
- if (iface.mac && iface.mac !== '00:00:00:00:00:00') macs.push(iface.mac);
3956
- }
3957
- }
3958
- macs.sort();
3959
- return createHash('sha256').update(`${hostname()}|${macs.join(',')}`).digest('hex');
3957
+ const idFile = join(daemon.grooveDir, '.machine-id');
3958
+ try {
3959
+ const existing = readFileSync(idFile, 'utf8').trim();
3960
+ if (existing.length >= 32) return existing;
3961
+ } catch {}
3962
+ const id = createHash('sha256').update(`${hostname()}|${randomUUID()}`).digest('hex');
3963
+ try { writeFileSync(idFile, id, { mode: 0o600 }); } catch {}
3964
+ return id;
3960
3965
  }
3961
3966
 
3962
3967
  async function validateCodeWithServer(code) {
@@ -4103,11 +4108,9 @@ Keep responses concise. Help them think, don't lecture them about the system the
4103
4108
 
4104
4109
  app.post('/api/beta/deactivate', async (req, res) => {
4105
4110
  // Stop the node if it's running before locking the feature away.
4106
- try {
4107
- if (daemon.networkNode?.proc && !daemon.networkNode.proc.killed) {
4108
- daemon.networkNode.proc.kill('SIGINT');
4109
- }
4110
- } catch { /* ignore */ }
4111
+ if (daemon.networkNode?.proc && !daemon.networkNode.proc.killed) {
4112
+ safeKill(daemon.networkNode.proc);
4113
+ }
4111
4114
  daemon.networkNode = {
4112
4115
  active: false, status: 'stopped', pid: null, proc: null,
4113
4116
  nodeId: null, layers: null, model: null, sessions: 0,
@@ -4220,20 +4223,20 @@ Keep responses concise. Help them think, don't lecture them about the system the
4220
4223
  // Resolve deploy path (handles ~ and defaults to ~/Desktop/groove-deploy)
4221
4224
  let deployPath = cfg.deployPath || null;
4222
4225
  if (!deployPath) {
4223
- deployPath = resolve(process.env.HOME || '', 'Desktop/groove-deploy');
4226
+ deployPath = resolve(homedir(), 'Desktop', 'groove-deploy');
4224
4227
  } else if (deployPath.startsWith('~/')) {
4225
- deployPath = resolve(process.env.HOME || '', deployPath.slice(2));
4228
+ deployPath = resolve(homedir(), deployPath.slice(2));
4226
4229
  }
4227
4230
 
4228
4231
  if (!existsSync(deployPath)) {
4229
4232
  return res.status(400).json({ error: `Deploy path not found: ${deployPath}` });
4230
4233
  }
4231
- if (!isInsideGrooveHome(deployPath) && !deployPath.startsWith(resolve(process.env.HOME || '', 'Desktop'))) {
4234
+ if (!isInsideGrooveHome(deployPath) && !deployPath.startsWith(resolve(homedir(), 'Desktop'))) {
4232
4235
  return res.status(400).json({ error: 'Deploy path outside allowed directories' });
4233
4236
  }
4234
4237
 
4235
4238
  const signalFlag = supportsSignalFlag(cfg.version) ? '--signal' : '--relay';
4236
- const model = cfg.model || 'google/gemma-3-4b';
4239
+ const model = cfg.model || 'Qwen/Qwen3-4B';
4237
4240
  const args = [
4238
4241
  '-m', 'src.node.server',
4239
4242
  signalFlag, signal,
@@ -4245,7 +4248,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4245
4248
 
4246
4249
  let proc;
4247
4250
  try {
4248
- proc = spawn(join(deployPath, 'venv', 'bin', 'python3'), args, {
4251
+ proc = spawn(venvPython(deployPath), args, {
4249
4252
  cwd: deployPath,
4250
4253
  env: { ...process.env, PYTHONUNBUFFERED: '1' },
4251
4254
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -4368,11 +4371,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4368
4371
  if (!node?.active || !node.proc) {
4369
4372
  return res.status(409).json({ error: 'Node not running' });
4370
4373
  }
4371
- try {
4372
- node.proc.kill('SIGINT');
4373
- } catch (err) {
4374
- return res.status(500).json({ error: `Failed to stop node: ${err.message}` });
4375
- }
4374
+ safeKill(node.proc);
4376
4375
  daemon.networkNode.status = 'stopping';
4377
4376
  pushNodeEvent('stopping', { pid: node.pid });
4378
4377
  broadcastNodeStatus();
@@ -4484,7 +4483,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4484
4483
  models,
4485
4484
  compute: data.compute || null,
4486
4485
  coverage: data.covered_layers ?? primaryModel.covered_layers ?? data.coverage ?? 0,
4487
- totalLayers: data.total_layers ?? primaryModel.total_layers ?? data.totalLayers ?? 34,
4486
+ totalLayers: data.total_layers ?? primaryModel.total_layers ?? data.totalLayers ?? 36,
4488
4487
  activeSessions: data.active_sessions ?? data.activeSessions ?? 0,
4489
4488
  totalNodes: data.total_nodes ?? data.totalNodes ?? (Array.isArray(data.nodes) ? data.nodes.length : 0),
4490
4489
  });
@@ -4529,10 +4528,10 @@ Keep responses concise. Help them think, don't lecture them about the system the
4529
4528
  } : null;
4530
4529
  res.json({
4531
4530
  nodes: selfNode,
4532
- models: ['google/gemma-3-4b'],
4531
+ models: ['Qwen/Qwen3-4B'],
4533
4532
  compute: localCompute,
4534
4533
  coverage,
4535
- totalLayers: 34,
4534
+ totalLayers: 36,
4536
4535
  activeSessions: node.sessions || 0,
4537
4536
  totalNodes: selfNode.length,
4538
4537
  });
@@ -4610,9 +4609,37 @@ Keep responses concise. Help them think, don't lecture them about the system the
4610
4609
 
4611
4610
  // --- Network package install/uninstall ---
4612
4611
 
4612
+ const IS_WIN = process.platform === 'win32';
4613
4613
  const NETWORK_REPO_URL = 'https://github.com/grooveai-dev/groove-network.git';
4614
4614
  const NETWORK_VERSION = 'v0.2.0';
4615
4615
 
4616
+ function venvPython(base) {
4617
+ return IS_WIN
4618
+ ? join(base, 'venv', 'Scripts', 'python.exe')
4619
+ : join(base, 'venv', 'bin', 'python3');
4620
+ }
4621
+
4622
+ function spawnSetupSh(cwd) {
4623
+ if (IS_WIN) {
4624
+ return spawn('cmd.exe', ['/c', 'bash setup.sh --json'], {
4625
+ cwd,
4626
+ stdio: ['ignore', 'pipe', 'pipe'],
4627
+ env: { ...process.env, PYTHONUNBUFFERED: '1' },
4628
+ });
4629
+ }
4630
+ return spawn('bash', ['setup.sh', '--json'], {
4631
+ cwd,
4632
+ stdio: ['ignore', 'pipe', 'pipe'],
4633
+ env: { ...process.env, PYTHONUNBUFFERED: '1' },
4634
+ });
4635
+ }
4636
+
4637
+ function safeKill(proc, signal = 'SIGINT') {
4638
+ try {
4639
+ if (IS_WIN) { proc.kill(); } else { proc.kill(signal); }
4640
+ } catch { /* ignore */ }
4641
+ }
4642
+
4616
4643
  function networkRoot() {
4617
4644
  return resolve(homedir(), '.groove', 'network');
4618
4645
  }
@@ -4636,12 +4663,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
4636
4663
  // Defensive: only permit fs ops on paths that resolve inside ~/.groove/.
4637
4664
  // Uses realpathSync when the path exists to defeat symlink escapes.
4638
4665
  function isInsideGrooveHome(target) {
4639
- const home = resolve(homedir(), '.groove') + '/';
4666
+ const home = resolve(homedir(), '.groove') + sep;
4640
4667
  const resolved = resolve(target);
4641
4668
  let full;
4642
- try { full = existsSync(resolved) ? realpathSync(resolved) + '/' : resolved + '/'; }
4643
- catch { full = resolved + '/'; }
4644
- const realHome = existsSync(home.slice(0, -1)) ? realpathSync(home.slice(0, -1)) + '/' : home;
4669
+ try { full = existsSync(resolved) ? realpathSync(resolved) + sep : resolved + sep; }
4670
+ catch { full = resolved + sep; }
4671
+ const realHome = existsSync(home.slice(0, -1)) ? realpathSync(home.slice(0, -1)) + sep : home;
4645
4672
  return full.startsWith(realHome);
4646
4673
  }
4647
4674
 
@@ -4748,11 +4775,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4748
4775
  broadcastInstallProgress('cloned', 'Repository cloned', 10);
4749
4776
 
4750
4777
  // Run setup.sh --json from the install directory
4751
- const setup = spawn('bash', ['setup.sh', '--json'], {
4752
- cwd: installPath,
4753
- stdio: ['ignore', 'pipe', 'pipe'],
4754
- env: { ...process.env, PYTHONUNBUFFERED: '1' },
4755
- });
4778
+ const setup = spawnSetupSh(installPath);
4756
4779
 
4757
4780
  daemon.networkInstall.proc = setup;
4758
4781
 
@@ -4816,7 +4839,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4816
4839
  try {
4817
4840
  const node = daemon.networkNode;
4818
4841
  if (node?.active && node.proc && !node.proc.killed) {
4819
- try { node.proc.kill('SIGINT'); } catch { /* ignore */ }
4842
+ safeKill(node.proc);
4820
4843
  daemon.networkNode.status = 'stopping';
4821
4844
  pushNodeEvent('stopping', { pid: node.pid, reason: 'uninstall' });
4822
4845
  broadcastNodeStatus();
@@ -4868,9 +4891,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4868
4891
  let stderr = '';
4869
4892
  proc.stdout.on('data', (c) => { stdout += c.toString(); });
4870
4893
  proc.stderr.on('data', (c) => { stderr += c.toString(); });
4871
- const timeout = setTimeout(() => {
4872
- try { proc.kill('SIGTERM'); } catch { /* ignore */ }
4873
- }, 10_000);
4894
+ const timeout = setTimeout(() => { safeKill(proc, 'SIGTERM'); }, 10_000);
4874
4895
  proc.on('error', () => { clearTimeout(timeout); resolvePromise(null); });
4875
4896
  proc.on('close', (code) => {
4876
4897
  clearTimeout(timeout);
@@ -4957,7 +4978,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
4957
4978
  try {
4958
4979
  const node = daemon.networkNode;
4959
4980
  if (node?.active && node.proc && !node.proc.killed) {
4960
- try { node.proc.kill('SIGINT'); } catch { /* ignore */ }
4981
+ safeKill(node.proc);
4961
4982
  daemon.networkNode.status = 'stopping';
4962
4983
  pushNodeEvent('stopping', { pid: node.pid, reason: 'update' });
4963
4984
  broadcastNodeStatus();
@@ -5002,11 +5023,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
5002
5023
 
5003
5024
  broadcastUpdateProgress('deps', 'Updating dependencies...', 30);
5004
5025
 
5005
- const setup = spawn('bash', ['setup.sh', '--json'], {
5006
- cwd: installPath,
5007
- stdio: ['ignore', 'pipe', 'pipe'],
5008
- env: { ...process.env, PYTHONUNBUFFERED: '1' },
5009
- });
5026
+ const setup = spawnSetupSh(installPath);
5010
5027
 
5011
5028
  daemon.networkInstall.proc = setup;
5012
5029