groove-dev 0.27.50 → 0.27.51

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.50",
3
+ "version": "0.27.51",
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.50",
3
+ "version": "0.27.51",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -3999,7 +3999,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3999
3999
  }
4000
4000
 
4001
4001
  const cfg = daemon.config.networkBeta || {};
4002
- const signal = cfg.signalUrl || 'signal.groovedev.ai';
4002
+ const signal = stripScheme(cfg.signalUrl);
4003
4003
  if (!isAllowedSignalHost(signal)) {
4004
4004
  return res.status(400).json({ error: 'Invalid signal host' });
4005
4005
  }
@@ -4022,13 +4022,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
4022
4022
  const args = [
4023
4023
  '-m', 'src.node.server',
4024
4024
  signalFlag, signal,
4025
+ '--tls',
4025
4026
  '--device', device,
4026
4027
  '--max-context', String(maxContext),
4027
4028
  ];
4028
4029
 
4029
4030
  let proc;
4030
4031
  try {
4031
- proc = spawn(join(deployPath, 'venv', 'bin', 'python3.12'), args, {
4032
+ proc = spawn(join(deployPath, 'venv', 'bin', 'python3'), args, {
4032
4033
  cwd: deployPath,
4033
4034
  env: { ...process.env, PYTHONUNBUFFERED: '1' },
4034
4035
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -4063,7 +4064,33 @@ Keep responses concise. Help them think, don't lecture them about the system the
4063
4064
  stderrBuf = stderrBuf.slice(idx + 1);
4064
4065
  if (!line) continue;
4065
4066
  if (line[0] !== '{') {
4067
+ // Python node emits plain-text logs like "Node identity: abc123",
4068
+ // "shard loaded: layers 0-12", "registered with signal". Parse those
4069
+ // here so the GUI reflects reality even without structured logging.
4070
+ let changed = false;
4071
+ const idMatch = line.match(/Node identity:\s*([A-Za-z0-9_\-:.]+)/i);
4072
+ if (idMatch && idMatch[1] !== daemon.networkNode.nodeId) {
4073
+ daemon.networkNode.nodeId = idMatch[1]; changed = true;
4074
+ }
4075
+ const layerMatch = line.match(/layers?\s*(\d+)\s*[-–to]+\s*(\d+)/i);
4076
+ if (layerMatch) {
4077
+ const start = parseInt(layerMatch[1], 10);
4078
+ const end = parseInt(layerMatch[2], 10);
4079
+ if (Number.isFinite(start) && Number.isFinite(end)) {
4080
+ daemon.networkNode.layers = [start, end]; changed = true;
4081
+ }
4082
+ }
4083
+ const modelMatch = line.match(/model[:\s]+([A-Za-z0-9_\-./]+\/[A-Za-z0-9_\-.]+)/i);
4084
+ if (modelMatch && modelMatch[1] !== daemon.networkNode.model) {
4085
+ daemon.networkNode.model = modelMatch[1]; changed = true;
4086
+ }
4087
+ if (/\bregistered\b/i.test(line) || /\bconnected\b/i.test(line)) {
4088
+ if (daemon.networkNode.status !== 'connected') {
4089
+ daemon.networkNode.status = 'connected'; changed = true;
4090
+ }
4091
+ }
4066
4092
  pushNodeEvent('log', { line });
4093
+ if (changed) broadcastNodeStatus();
4067
4094
  continue;
4068
4095
  }
4069
4096
  let entry;
@@ -4138,10 +4165,18 @@ Keep responses concise. Help them think, don't lecture them about the system the
4138
4165
  });
4139
4166
 
4140
4167
  function isAllowedSignalHost(host) {
4141
- const h = (host || '').replace(/^https?:\/\//i, '').replace(/\/.*$/, '').toLowerCase();
4168
+ const h = (host || '').replace(/^(wss?|https?):\/\//i, '').replace(/\/.*$/, '').toLowerCase();
4142
4169
  return h === 'signal.groovedev.ai' || h.endsWith('.groovedev.ai');
4143
4170
  }
4144
4171
 
4172
+ // The Python node/client code prepends the scheme itself from `--tls`.
4173
+ // Daemon must pass a BARE host to --relay/--signal; otherwise the Python
4174
+ // side ends up with a double-scheme URI like wss://wss://host.
4175
+ function stripScheme(url) {
4176
+ if (!url) return 'signal.groovedev.ai';
4177
+ return url.replace(/^wss?:\/\//i, '').replace(/\/.*$/, '');
4178
+ }
4179
+
4145
4180
  app.get('/api/network/status', networkGate, async (req, res) => {
4146
4181
  const cfg = daemon.config.networkBeta || {};
4147
4182
  const signalHost = cfg.signalUrl || 'signal.groovedev.ai';
@@ -4150,9 +4185,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
4150
4185
  return res.status(400).json({ error: 'Invalid signal host' });
4151
4186
  }
4152
4187
 
4153
- const statusUrl = /^https?:\/\//i.test(signalHost)
4154
- ? `${signalHost.replace(/\/$/, '')}/status`
4155
- : `https://${signalHost}/status`;
4188
+ const bareHost = signalHost.replace(/^(wss?|https?):\/\//i, '').replace(/\/.*$/, '');
4189
+ const statusUrl = `https://${bareHost}/status`;
4156
4190
 
4157
4191
  try {
4158
4192
  const controller = new AbortController();
@@ -4161,7 +4195,24 @@ Keep responses concise. Help them think, don't lecture them about the system the
4161
4195
  clearTimeout(timer);
4162
4196
  if (r.ok) {
4163
4197
  const data = await r.json();
4164
- return res.json(data);
4198
+ // Signal service returns snake_case; GUI expects camelCase.
4199
+ const models = Array.isArray(data.models) ? data.models.map((m) => {
4200
+ if (!m || typeof m !== 'object') return m;
4201
+ const { covered_layers, total_layers, ...rest } = m;
4202
+ return {
4203
+ ...rest,
4204
+ ...(covered_layers !== undefined ? { coveredLayers: covered_layers } : {}),
4205
+ ...(total_layers !== undefined ? { totalLayers: total_layers } : {}),
4206
+ };
4207
+ }) : [];
4208
+ return res.json({
4209
+ nodes: Array.isArray(data.nodes) ? data.nodes : [],
4210
+ models,
4211
+ coverage: data.covered_layers ?? data.coverage ?? 0,
4212
+ totalLayers: data.total_layers ?? data.totalLayers ?? 24,
4213
+ activeSessions: data.active_sessions ?? data.activeSessions ?? 0,
4214
+ totalNodes: data.total_nodes ?? data.totalNodes ?? (Array.isArray(data.nodes) ? data.nodes.length : 0),
4215
+ });
4165
4216
  }
4166
4217
  } catch { /* fall through to local snapshot */ }
4167
4218
 
@@ -4180,13 +4231,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
4180
4231
  coverage,
4181
4232
  totalLayers: 24,
4182
4233
  activeSessions: node.sessions || 0,
4234
+ totalNodes: selfNode.length,
4183
4235
  });
4184
4236
  });
4185
4237
 
4186
4238
  // --- Network package install/uninstall ---
4187
4239
 
4188
4240
  const NETWORK_REPO_URL = 'https://github.com/grooveai-dev/groove-network.git';
4189
- const NETWORK_VERSION = 'v0.1.0';
4241
+ const NETWORK_VERSION = 'v0.2.0';
4190
4242
 
4191
4243
  function networkRoot() {
4192
4244
  return resolve(homedir(), '.groove', 'network');
@@ -4266,9 +4318,19 @@ Keep responses concise. Help them think, don't lecture them about the system the
4266
4318
  ? NETWORK_REPO_URL.replace('https://', `https://${pat}@`)
4267
4319
  : NETWORK_REPO_URL;
4268
4320
 
4269
- broadcastInstallProgress('cloning', 'Cloning network package...', 0);
4321
+ // Resolve the latest released tag so fresh installs track new releases
4322
+ // without requiring a code change. Falls back to NETWORK_VERSION if the
4323
+ // remote lookup fails (offline, rate-limited, no tags yet).
4324
+ let installVersion;
4325
+ try {
4326
+ installVersion = (await getLatestNetworkTag()) || NETWORK_VERSION;
4327
+ } catch {
4328
+ installVersion = NETWORK_VERSION;
4329
+ }
4330
+
4331
+ broadcastInstallProgress('cloning', `Cloning network package ${installVersion}...`, 0);
4270
4332
 
4271
- const cloneArgs = ['clone', '--branch', NETWORK_VERSION, '--depth', '1', cloneUrl, installPath];
4333
+ const cloneArgs = ['clone', '--branch', installVersion, '--depth', '1', cloneUrl, installPath];
4272
4334
  const clone = spawn('git', cloneArgs, {
4273
4335
  stdio: ['ignore', 'pipe', 'pipe'],
4274
4336
  env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
@@ -4344,12 +4406,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
4344
4406
  ...(daemon.config.networkBeta || {}),
4345
4407
  installed: true,
4346
4408
  deployPath: installPath,
4347
- version: NETWORK_VERSION,
4409
+ version: installVersion,
4348
4410
  };
4349
4411
  await persistConfig();
4350
4412
  daemon.broadcast({ type: 'config:updated' });
4351
- broadcastInstallProgress('done', 'Network package installed', 100);
4352
- daemon.audit.log('network.install', { path: installPath, version: NETWORK_VERSION });
4413
+ broadcastInstallProgress('done', `Network package ${installVersion} installed`, 100);
4414
+ daemon.audit.log('network.install', { path: installPath, version: installVersion });
4353
4415
  daemon.networkInstall = { running: false };
4354
4416
  } catch (err) {
4355
4417
  fail(err?.message || 'Install failed');
@@ -58,10 +58,18 @@ function signalFlagName() {
58
58
  return supportsSignalFlag(cfg.version) ? '--signal' : '--relay';
59
59
  }
60
60
 
61
+ // The Python client prepends the scheme itself — daemon passes a bare host
62
+ // and adds `--tls` to request wss://. Strip any ws:// or wss:// a user may
63
+ // have left in the stored signalUrl (e.g. from an older daemon default).
64
+ function stripScheme(url) {
65
+ if (!url) return 'signal.groovedev.ai';
66
+ return url.replace(/^wss?:\/\//i, '').replace(/\/.*$/, '');
67
+ }
68
+
61
69
  export class GrooveNetworkProvider extends Provider {
62
70
  static name = 'groove-network';
63
71
  static displayName = 'Groove Network';
64
- static command = 'python3.12';
72
+ static command = 'python3';
65
73
  static authType = 'none';
66
74
 
67
75
  static models = [
@@ -79,7 +87,7 @@ export class GrooveNetworkProvider extends Provider {
79
87
 
80
88
  buildSpawnCommand(agent) {
81
89
  const cfg = getConfig() || {};
82
- const signal = cfg.signalUrl || 'signal.groovedev.ai';
90
+ const signal = stripScheme(cfg.signalUrl);
83
91
  const model = agent.model || GrooveNetworkProvider.models[0].id;
84
92
  const maxTokens = agent.maxTokens || 500;
85
93
  const prompt = agent.prompt || '';
@@ -89,6 +97,7 @@ export class GrooveNetworkProvider extends Provider {
89
97
  const args = [
90
98
  '-m', 'src.consumer.client',
91
99
  signalFlagName(), signal,
100
+ '--tls',
92
101
  '--model', model,
93
102
  '--prompt', prompt,
94
103
  '--max-tokens', String(maxTokens),
@@ -96,7 +105,7 @@ export class GrooveNetworkProvider extends Provider {
96
105
  ];
97
106
 
98
107
  return {
99
- command: join(deployPath, 'venv', 'bin', 'python3.12'),
108
+ command: join(deployPath, 'venv', 'bin', 'python3'),
100
109
  args,
101
110
  env: { PYTHONUNBUFFERED: '1' },
102
111
  cwd: deployPath,
@@ -105,14 +114,15 @@ export class GrooveNetworkProvider extends Provider {
105
114
 
106
115
  buildHeadlessCommand(prompt, model) {
107
116
  const cfg = getConfig() || {};
108
- const signal = cfg.signalUrl || 'signal.groovedev.ai';
117
+ const signal = stripScheme(cfg.signalUrl);
109
118
  const m = model || GrooveNetworkProvider.models[0].id;
110
119
  const deployPath = expandHome(cfg.deployPath) || resolve(homedir(), 'Desktop/groove-deploy');
111
120
  return {
112
- command: join(deployPath, 'venv', 'bin', 'python3.12'),
121
+ command: join(deployPath, 'venv', 'bin', 'python3'),
113
122
  args: [
114
123
  '-m', 'src.consumer.client',
115
124
  signalFlagName(), signal,
125
+ '--tls',
116
126
  '--model', m,
117
127
  '--prompt', prompt,
118
128
  '--max-tokens', '500',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.50",
3
+ "version": "0.27.51",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.50",
3
+ "version": "0.27.51",
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.50",
3
+ "version": "0.27.51",
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.50",
3
+ "version": "0.27.51",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -3999,7 +3999,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3999
3999
  }
4000
4000
 
4001
4001
  const cfg = daemon.config.networkBeta || {};
4002
- const signal = cfg.signalUrl || 'signal.groovedev.ai';
4002
+ const signal = stripScheme(cfg.signalUrl);
4003
4003
  if (!isAllowedSignalHost(signal)) {
4004
4004
  return res.status(400).json({ error: 'Invalid signal host' });
4005
4005
  }
@@ -4022,13 +4022,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
4022
4022
  const args = [
4023
4023
  '-m', 'src.node.server',
4024
4024
  signalFlag, signal,
4025
+ '--tls',
4025
4026
  '--device', device,
4026
4027
  '--max-context', String(maxContext),
4027
4028
  ];
4028
4029
 
4029
4030
  let proc;
4030
4031
  try {
4031
- proc = spawn(join(deployPath, 'venv', 'bin', 'python3.12'), args, {
4032
+ proc = spawn(join(deployPath, 'venv', 'bin', 'python3'), args, {
4032
4033
  cwd: deployPath,
4033
4034
  env: { ...process.env, PYTHONUNBUFFERED: '1' },
4034
4035
  stdio: ['ignore', 'pipe', 'pipe'],
@@ -4063,7 +4064,33 @@ Keep responses concise. Help them think, don't lecture them about the system the
4063
4064
  stderrBuf = stderrBuf.slice(idx + 1);
4064
4065
  if (!line) continue;
4065
4066
  if (line[0] !== '{') {
4067
+ // Python node emits plain-text logs like "Node identity: abc123",
4068
+ // "shard loaded: layers 0-12", "registered with signal". Parse those
4069
+ // here so the GUI reflects reality even without structured logging.
4070
+ let changed = false;
4071
+ const idMatch = line.match(/Node identity:\s*([A-Za-z0-9_\-:.]+)/i);
4072
+ if (idMatch && idMatch[1] !== daemon.networkNode.nodeId) {
4073
+ daemon.networkNode.nodeId = idMatch[1]; changed = true;
4074
+ }
4075
+ const layerMatch = line.match(/layers?\s*(\d+)\s*[-–to]+\s*(\d+)/i);
4076
+ if (layerMatch) {
4077
+ const start = parseInt(layerMatch[1], 10);
4078
+ const end = parseInt(layerMatch[2], 10);
4079
+ if (Number.isFinite(start) && Number.isFinite(end)) {
4080
+ daemon.networkNode.layers = [start, end]; changed = true;
4081
+ }
4082
+ }
4083
+ const modelMatch = line.match(/model[:\s]+([A-Za-z0-9_\-./]+\/[A-Za-z0-9_\-.]+)/i);
4084
+ if (modelMatch && modelMatch[1] !== daemon.networkNode.model) {
4085
+ daemon.networkNode.model = modelMatch[1]; changed = true;
4086
+ }
4087
+ if (/\bregistered\b/i.test(line) || /\bconnected\b/i.test(line)) {
4088
+ if (daemon.networkNode.status !== 'connected') {
4089
+ daemon.networkNode.status = 'connected'; changed = true;
4090
+ }
4091
+ }
4066
4092
  pushNodeEvent('log', { line });
4093
+ if (changed) broadcastNodeStatus();
4067
4094
  continue;
4068
4095
  }
4069
4096
  let entry;
@@ -4138,10 +4165,18 @@ Keep responses concise. Help them think, don't lecture them about the system the
4138
4165
  });
4139
4166
 
4140
4167
  function isAllowedSignalHost(host) {
4141
- const h = (host || '').replace(/^https?:\/\//i, '').replace(/\/.*$/, '').toLowerCase();
4168
+ const h = (host || '').replace(/^(wss?|https?):\/\//i, '').replace(/\/.*$/, '').toLowerCase();
4142
4169
  return h === 'signal.groovedev.ai' || h.endsWith('.groovedev.ai');
4143
4170
  }
4144
4171
 
4172
+ // The Python node/client code prepends the scheme itself from `--tls`.
4173
+ // Daemon must pass a BARE host to --relay/--signal; otherwise the Python
4174
+ // side ends up with a double-scheme URI like wss://wss://host.
4175
+ function stripScheme(url) {
4176
+ if (!url) return 'signal.groovedev.ai';
4177
+ return url.replace(/^wss?:\/\//i, '').replace(/\/.*$/, '');
4178
+ }
4179
+
4145
4180
  app.get('/api/network/status', networkGate, async (req, res) => {
4146
4181
  const cfg = daemon.config.networkBeta || {};
4147
4182
  const signalHost = cfg.signalUrl || 'signal.groovedev.ai';
@@ -4150,9 +4185,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
4150
4185
  return res.status(400).json({ error: 'Invalid signal host' });
4151
4186
  }
4152
4187
 
4153
- const statusUrl = /^https?:\/\//i.test(signalHost)
4154
- ? `${signalHost.replace(/\/$/, '')}/status`
4155
- : `https://${signalHost}/status`;
4188
+ const bareHost = signalHost.replace(/^(wss?|https?):\/\//i, '').replace(/\/.*$/, '');
4189
+ const statusUrl = `https://${bareHost}/status`;
4156
4190
 
4157
4191
  try {
4158
4192
  const controller = new AbortController();
@@ -4161,7 +4195,24 @@ Keep responses concise. Help them think, don't lecture them about the system the
4161
4195
  clearTimeout(timer);
4162
4196
  if (r.ok) {
4163
4197
  const data = await r.json();
4164
- return res.json(data);
4198
+ // Signal service returns snake_case; GUI expects camelCase.
4199
+ const models = Array.isArray(data.models) ? data.models.map((m) => {
4200
+ if (!m || typeof m !== 'object') return m;
4201
+ const { covered_layers, total_layers, ...rest } = m;
4202
+ return {
4203
+ ...rest,
4204
+ ...(covered_layers !== undefined ? { coveredLayers: covered_layers } : {}),
4205
+ ...(total_layers !== undefined ? { totalLayers: total_layers } : {}),
4206
+ };
4207
+ }) : [];
4208
+ return res.json({
4209
+ nodes: Array.isArray(data.nodes) ? data.nodes : [],
4210
+ models,
4211
+ coverage: data.covered_layers ?? data.coverage ?? 0,
4212
+ totalLayers: data.total_layers ?? data.totalLayers ?? 24,
4213
+ activeSessions: data.active_sessions ?? data.activeSessions ?? 0,
4214
+ totalNodes: data.total_nodes ?? data.totalNodes ?? (Array.isArray(data.nodes) ? data.nodes.length : 0),
4215
+ });
4165
4216
  }
4166
4217
  } catch { /* fall through to local snapshot */ }
4167
4218
 
@@ -4180,13 +4231,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
4180
4231
  coverage,
4181
4232
  totalLayers: 24,
4182
4233
  activeSessions: node.sessions || 0,
4234
+ totalNodes: selfNode.length,
4183
4235
  });
4184
4236
  });
4185
4237
 
4186
4238
  // --- Network package install/uninstall ---
4187
4239
 
4188
4240
  const NETWORK_REPO_URL = 'https://github.com/grooveai-dev/groove-network.git';
4189
- const NETWORK_VERSION = 'v0.1.0';
4241
+ const NETWORK_VERSION = 'v0.2.0';
4190
4242
 
4191
4243
  function networkRoot() {
4192
4244
  return resolve(homedir(), '.groove', 'network');
@@ -4266,9 +4318,19 @@ Keep responses concise. Help them think, don't lecture them about the system the
4266
4318
  ? NETWORK_REPO_URL.replace('https://', `https://${pat}@`)
4267
4319
  : NETWORK_REPO_URL;
4268
4320
 
4269
- broadcastInstallProgress('cloning', 'Cloning network package...', 0);
4321
+ // Resolve the latest released tag so fresh installs track new releases
4322
+ // without requiring a code change. Falls back to NETWORK_VERSION if the
4323
+ // remote lookup fails (offline, rate-limited, no tags yet).
4324
+ let installVersion;
4325
+ try {
4326
+ installVersion = (await getLatestNetworkTag()) || NETWORK_VERSION;
4327
+ } catch {
4328
+ installVersion = NETWORK_VERSION;
4329
+ }
4330
+
4331
+ broadcastInstallProgress('cloning', `Cloning network package ${installVersion}...`, 0);
4270
4332
 
4271
- const cloneArgs = ['clone', '--branch', NETWORK_VERSION, '--depth', '1', cloneUrl, installPath];
4333
+ const cloneArgs = ['clone', '--branch', installVersion, '--depth', '1', cloneUrl, installPath];
4272
4334
  const clone = spawn('git', cloneArgs, {
4273
4335
  stdio: ['ignore', 'pipe', 'pipe'],
4274
4336
  env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
@@ -4344,12 +4406,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
4344
4406
  ...(daemon.config.networkBeta || {}),
4345
4407
  installed: true,
4346
4408
  deployPath: installPath,
4347
- version: NETWORK_VERSION,
4409
+ version: installVersion,
4348
4410
  };
4349
4411
  await persistConfig();
4350
4412
  daemon.broadcast({ type: 'config:updated' });
4351
- broadcastInstallProgress('done', 'Network package installed', 100);
4352
- daemon.audit.log('network.install', { path: installPath, version: NETWORK_VERSION });
4413
+ broadcastInstallProgress('done', `Network package ${installVersion} installed`, 100);
4414
+ daemon.audit.log('network.install', { path: installPath, version: installVersion });
4353
4415
  daemon.networkInstall = { running: false };
4354
4416
  } catch (err) {
4355
4417
  fail(err?.message || 'Install failed');
@@ -58,10 +58,18 @@ function signalFlagName() {
58
58
  return supportsSignalFlag(cfg.version) ? '--signal' : '--relay';
59
59
  }
60
60
 
61
+ // The Python client prepends the scheme itself — daemon passes a bare host
62
+ // and adds `--tls` to request wss://. Strip any ws:// or wss:// a user may
63
+ // have left in the stored signalUrl (e.g. from an older daemon default).
64
+ function stripScheme(url) {
65
+ if (!url) return 'signal.groovedev.ai';
66
+ return url.replace(/^wss?:\/\//i, '').replace(/\/.*$/, '');
67
+ }
68
+
61
69
  export class GrooveNetworkProvider extends Provider {
62
70
  static name = 'groove-network';
63
71
  static displayName = 'Groove Network';
64
- static command = 'python3.12';
72
+ static command = 'python3';
65
73
  static authType = 'none';
66
74
 
67
75
  static models = [
@@ -79,7 +87,7 @@ export class GrooveNetworkProvider extends Provider {
79
87
 
80
88
  buildSpawnCommand(agent) {
81
89
  const cfg = getConfig() || {};
82
- const signal = cfg.signalUrl || 'signal.groovedev.ai';
90
+ const signal = stripScheme(cfg.signalUrl);
83
91
  const model = agent.model || GrooveNetworkProvider.models[0].id;
84
92
  const maxTokens = agent.maxTokens || 500;
85
93
  const prompt = agent.prompt || '';
@@ -89,6 +97,7 @@ export class GrooveNetworkProvider extends Provider {
89
97
  const args = [
90
98
  '-m', 'src.consumer.client',
91
99
  signalFlagName(), signal,
100
+ '--tls',
92
101
  '--model', model,
93
102
  '--prompt', prompt,
94
103
  '--max-tokens', String(maxTokens),
@@ -96,7 +105,7 @@ export class GrooveNetworkProvider extends Provider {
96
105
  ];
97
106
 
98
107
  return {
99
- command: join(deployPath, 'venv', 'bin', 'python3.12'),
108
+ command: join(deployPath, 'venv', 'bin', 'python3'),
100
109
  args,
101
110
  env: { PYTHONUNBUFFERED: '1' },
102
111
  cwd: deployPath,
@@ -105,14 +114,15 @@ export class GrooveNetworkProvider extends Provider {
105
114
 
106
115
  buildHeadlessCommand(prompt, model) {
107
116
  const cfg = getConfig() || {};
108
- const signal = cfg.signalUrl || 'signal.groovedev.ai';
117
+ const signal = stripScheme(cfg.signalUrl);
109
118
  const m = model || GrooveNetworkProvider.models[0].id;
110
119
  const deployPath = expandHome(cfg.deployPath) || resolve(homedir(), 'Desktop/groove-deploy');
111
120
  return {
112
- command: join(deployPath, 'venv', 'bin', 'python3.12'),
121
+ command: join(deployPath, 'venv', 'bin', 'python3'),
113
122
  args: [
114
123
  '-m', 'src.consumer.client',
115
124
  signalFlagName(), signal,
125
+ '--tls',
116
126
  '--model', m,
117
127
  '--prompt', prompt,
118
128
  '--max-tokens', '500',
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.50",
3
+ "version": "0.27.51",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",