groove-dev 0.27.51 → 0.27.53

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.51",
3
+ "version": "0.27.53",
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.51",
3
+ "version": "0.27.53",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -3866,7 +3866,14 @@ Keep responses concise. Help them think, don't lecture them about the system the
3866
3866
  // so revoked or expired codes lock the feature automatically. Non-blocking.
3867
3867
  daemon.revalidateBetaCode = async function revalidateBetaCode() {
3868
3868
  const cfg = daemon.config?.networkBeta;
3869
- if (!cfg?.unlocked || !cfg?.code) return;
3869
+ if (!cfg?.unlocked) return;
3870
+ if (!cfg?.code) {
3871
+ daemon.config.networkBeta = { ...cfg, unlocked: false, expiresAt: null, features: [] };
3872
+ await persistConfig();
3873
+ daemon.audit.log('beta.revoked', { reason: 'missing code' });
3874
+ daemon.broadcast({ type: 'config:updated' });
3875
+ return;
3876
+ }
3870
3877
  const remote = await validateCodeWithServer(cfg.code);
3871
3878
  // If we couldn't reach the server, keep the current unlocked state —
3872
3879
  // network failures must not lock out beta users.
@@ -4017,6 +4024,9 @@ Keep responses concise. Help them think, don't lecture them about the system the
4017
4024
  if (!existsSync(deployPath)) {
4018
4025
  return res.status(400).json({ error: `Deploy path not found: ${deployPath}` });
4019
4026
  }
4027
+ if (!isInsideGrooveHome(deployPath) && !deployPath.startsWith(resolve(process.env.HOME || '', 'Desktop'))) {
4028
+ return res.status(400).json({ error: 'Deploy path outside allowed directories' });
4029
+ }
4020
4030
 
4021
4031
  const signalFlag = supportsSignalFlag(cfg.version) ? '--signal' : '--relay';
4022
4032
  const args = [
@@ -4205,11 +4215,57 @@ Keep responses concise. Help them think, don't lecture them about the system the
4205
4215
  ...(total_layers !== undefined ? { totalLayers: total_layers } : {}),
4206
4216
  };
4207
4217
  }) : [];
4218
+ const primaryModel = Array.isArray(data.models) && data.models[0] ? data.models[0] : {};
4219
+
4220
+ // Enrich local node state from signal's authoritative topology.
4221
+ // Signal truncates IDs (e.g. "0xf608fd..."), so match by prefix.
4222
+ if (daemon.networkNode?.active && daemon.networkNode.nodeId) {
4223
+ const selfId = daemon.networkNode.nodeId;
4224
+ const signalNodes = Array.isArray(data.nodes) ? data.nodes : [];
4225
+ const self = signalNodes.find((n) => {
4226
+ const nid = n.node_id || n.nodeId || '';
4227
+ const prefix = nid.replace(/\.{2,}$/, '');
4228
+ return selfId === nid || (prefix.length >= 6 && selfId.startsWith(prefix));
4229
+ });
4230
+ let changed = false;
4231
+ if (self) {
4232
+ if (Array.isArray(self.layers) && self.layers.length === 2) {
4233
+ daemon.networkNode.layers = self.layers;
4234
+ changed = true;
4235
+ }
4236
+ if (self.device) {
4237
+ daemon.networkNode.hardware = {
4238
+ device: self.device,
4239
+ memory: daemon.networkNode.hardware?.memory || null,
4240
+ gpu: daemon.networkNode.hardware?.gpu || null,
4241
+ };
4242
+ changed = true;
4243
+ }
4244
+ }
4245
+ const availModel = Array.isArray(data.models)
4246
+ ? data.models.find((m) => m && m.available !== false)
4247
+ : null;
4248
+ if (availModel && !daemon.networkNode.model) {
4249
+ daemon.networkNode.model = availModel.name || null;
4250
+ changed = true;
4251
+ }
4252
+ if (changed) broadcastNodeStatus();
4253
+ }
4254
+
4255
+ const capStr = (s, max = 200) => (typeof s === 'string' ? s.slice(0, max) : s);
4256
+ const safeNodes = (Array.isArray(data.nodes) ? data.nodes : []).map((n) => ({
4257
+ node_id: capStr(n.node_id || n.nodeId),
4258
+ device: capStr(n.device),
4259
+ layers: Array.isArray(n.layers) ? n.layers.slice(0, 2) : n.layers,
4260
+ status: capStr(n.status, 50),
4261
+ active_sessions: n.active_sessions ?? 0,
4262
+ }));
4263
+
4208
4264
  return res.json({
4209
- nodes: Array.isArray(data.nodes) ? data.nodes : [],
4265
+ nodes: safeNodes,
4210
4266
  models,
4211
- coverage: data.covered_layers ?? data.coverage ?? 0,
4212
- totalLayers: data.total_layers ?? data.totalLayers ?? 24,
4267
+ coverage: data.covered_layers ?? primaryModel.covered_layers ?? data.coverage ?? 0,
4268
+ totalLayers: data.total_layers ?? primaryModel.total_layers ?? data.totalLayers ?? 24,
4213
4269
  activeSessions: data.active_sessions ?? data.activeSessions ?? 0,
4214
4270
  totalNodes: data.total_nodes ?? data.totalNodes ?? (Array.isArray(data.nodes) ? data.nodes.length : 0),
4215
4271
  });
@@ -4312,15 +4368,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
4312
4368
  };
4313
4369
 
4314
4370
  try {
4315
- // Build clone URL with optional PAT
4316
4371
  const pat = daemon.credentials?.getKey?.('github-pat') || null;
4317
- const cloneUrl = pat
4318
- ? NETWORK_REPO_URL.replace('https://', `https://${pat}@`)
4319
- : NETWORK_REPO_URL;
4320
4372
 
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
4373
  let installVersion;
4325
4374
  try {
4326
4375
  installVersion = (await getLatestNetworkTag()) || NETWORK_VERSION;
@@ -4330,10 +4379,16 @@ Keep responses concise. Help them think, don't lecture them about the system the
4330
4379
 
4331
4380
  broadcastInstallProgress('cloning', `Cloning network package ${installVersion}...`, 0);
4332
4381
 
4333
- const cloneArgs = ['clone', '--branch', installVersion, '--depth', '1', cloneUrl, installPath];
4382
+ const cloneArgs = ['clone', '--branch', installVersion, '--depth', '1', NETWORK_REPO_URL, installPath];
4383
+ const cloneEnv = { ...process.env, GIT_TERMINAL_PROMPT: '0' };
4384
+ if (pat) {
4385
+ cloneEnv.GIT_CONFIG_COUNT = '1';
4386
+ cloneEnv.GIT_CONFIG_KEY_0 = 'http.extraHeader';
4387
+ cloneEnv.GIT_CONFIG_VALUE_0 = `Authorization: token ${pat}`;
4388
+ }
4334
4389
  const clone = spawn('git', cloneArgs, {
4335
4390
  stdio: ['ignore', 'pipe', 'pipe'],
4336
- env: { ...process.env, GIT_TERMINAL_PROMPT: '0' },
4391
+ env: cloneEnv,
4337
4392
  });
4338
4393
 
4339
4394
  const stripCredentials = (s) => s.replace(/https:\/\/[^@]+@/g, 'https://***@');
@@ -155,5 +155,5 @@ export function loadConfig(grooveDir) {
155
155
  }
156
156
 
157
157
  export function saveConfig(grooveDir, config) {
158
- writeFileSync(resolve(grooveDir, 'config.json'), JSON.stringify(config, null, 2));
158
+ writeFileSync(resolve(grooveDir, 'config.json'), JSON.stringify(config, null, 2), { mode: 0o600 });
159
159
  }
@@ -740,7 +740,7 @@ For normal file edits within your scope, proceed without review.
740
740
  }
741
741
 
742
742
  const spawnCmd = provider.buildSpawnCommand(spawnConfig);
743
- const { command, args, env, stdin: stdinData } = spawnCmd;
743
+ const { command, args, env, stdin: stdinData, cwd: providerCwd } = spawnCmd;
744
744
 
745
745
  // Log the spawn command (mask anything that looks like an API key)
746
746
  const maskArg = (a) => /^(sk-|AIza|key-|token-)/.test(a) ? '***' : a;
@@ -765,7 +765,7 @@ For normal file edits within your scope, proceed without review.
765
765
 
766
766
  // Spawn the process (use pipe for stdin if provider needs to send prompt via stdin)
767
767
  const proc = cpSpawn(command, args, {
768
- cwd: agent.workingDir || this.daemon.projectDir,
768
+ cwd: providerCwd || agent.workingDir || this.daemon.projectDir,
769
769
  env: { ...process.env, ...env, ...integrationEnv, GROOVE_AGENT_ID: agent.id, GROOVE_AGENT_NAME: agent.name, GROOVE_DAEMON_HOST: this.daemon.host || '127.0.0.1', GROOVE_DAEMON_PORT: String(this.daemon.port || 31415) },
770
770
  stdio: [stdinData ? 'pipe' : 'ignore', 'pipe', 'pipe'],
771
771
  detached: false,
@@ -66,6 +66,11 @@ function stripScheme(url) {
66
66
  return url.replace(/^wss?:\/\//i, '').replace(/\/.*$/, '');
67
67
  }
68
68
 
69
+ function isAllowedSignalHost(host) {
70
+ const h = (host || '').replace(/^(wss?|https?):\/\//i, '').replace(/\/.*$/, '').toLowerCase();
71
+ return h === 'signal.groovedev.ai' || h.endsWith('.groovedev.ai');
72
+ }
73
+
69
74
  export class GrooveNetworkProvider extends Provider {
70
75
  static name = 'groove-network';
71
76
  static displayName = 'Groove Network';
@@ -88,6 +93,7 @@ export class GrooveNetworkProvider extends Provider {
88
93
  buildSpawnCommand(agent) {
89
94
  const cfg = getConfig() || {};
90
95
  const signal = stripScheme(cfg.signalUrl);
96
+ if (!isAllowedSignalHost(signal)) throw new Error('Invalid signal host');
91
97
  const model = agent.model || GrooveNetworkProvider.models[0].id;
92
98
  const maxTokens = agent.maxTokens || 500;
93
99
  const prompt = agent.prompt || '';
@@ -115,6 +121,7 @@ export class GrooveNetworkProvider extends Provider {
115
121
  buildHeadlessCommand(prompt, model) {
116
122
  const cfg = getConfig() || {};
117
123
  const signal = stripScheme(cfg.signalUrl);
124
+ if (!isAllowedSignalHost(signal)) throw new Error('Invalid signal host');
118
125
  const m = model || GrooveNetworkProvider.models[0].id;
119
126
  const deployPath = expandHome(cfg.deployPath) || resolve(homedir(), 'Desktop/groove-deploy');
120
127
  return {
@@ -145,19 +152,31 @@ export class GrooveNetworkProvider extends Provider {
145
152
  }
146
153
  try {
147
154
  const msg = JSON.parse(trimmed);
148
- if (msg && typeof msg === 'object' && typeof msg.type === 'string') {
149
- return {
150
- type: msg.type,
151
- text: msg.text,
152
- sessionId: msg.session_id,
153
- tokensGenerated: msg.tokens_generated,
154
- error: msg.error,
155
- signal: msg.signal,
156
- nodesAvailable: msg.nodes_available,
157
- nodes: msg.nodes,
158
- raw: msg,
159
- };
155
+ if (!msg || typeof msg !== 'object' || typeof msg.type !== 'string') {
156
+ return { type: 'activity', data: trimmed };
157
+ }
158
+
159
+ if (msg.type === 'token' && msg.text) {
160
+ return { type: 'activity', subtype: 'text', data: msg.text, tokensGenerated: msg.tokens_generated };
161
+ }
162
+ if (msg.type === 'complete' || msg.type === 'result') {
163
+ return { type: 'result', text: msg.text || '', tokensGenerated: msg.tokens_generated, sessionId: msg.session_id };
160
164
  }
165
+ if (msg.type === 'error') {
166
+ return { type: 'activity', subtype: 'error', data: msg.error || msg.message || 'Unknown error' };
167
+ }
168
+
169
+ const labels = {
170
+ signal_connected: `Connected to ${msg.signal || 'signal'}`,
171
+ matched: `Matched with ${Array.isArray(msg.nodes) ? msg.nodes.length : '?'} nodes`,
172
+ connected: `Session ${(msg.session_id || '').slice(0, 8) || 'started'}`,
173
+ pipeline: `Pipeline ready — ${Array.isArray(msg.nodes) ? msg.nodes.length : '?'} nodes`,
174
+ };
175
+ if (labels[msg.type]) {
176
+ return { type: 'activity', data: labels[msg.type], sessionId: msg.session_id };
177
+ }
178
+
179
+ return { type: 'activity', data: msg.text || msg.message || msg.type, raw: msg };
161
180
  } catch { /* not JSON, fall through */ }
162
181
  return { type: 'activity', data: trimmed };
163
182
  }