groove-dev 0.27.33 → 0.27.34

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.33",
3
+ "version": "0.27.34",
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",
@@ -14,15 +14,18 @@ export async function start(options) {
14
14
  // ── First-run interactive wizard ────────────────────────────
15
15
  let setupKeys = {};
16
16
  if (isFirstRun) {
17
- try {
18
- const result = await runSetupWizard();
19
- setupKeys = result.keys || {};
20
- } catch (err) {
21
- // If stdin is not interactive (piped), skip wizard
22
- if (err.code === 'ERR_USE_AFTER_CLOSE' || !process.stdin.isTTY) {
23
- console.log(chalk.dim(' Non-interactive mode skipping setup wizard.'));
24
- } else {
25
- throw err;
17
+ if (!process.stdin.isTTY) {
18
+ console.log(chalk.dim(' Non-interactive mode skipping setup wizard.'));
19
+ } else {
20
+ try {
21
+ const result = await runSetupWizard();
22
+ setupKeys = result.keys || {};
23
+ } catch (err) {
24
+ if (err.code === 'ERR_USE_AFTER_CLOSE') {
25
+ console.log(chalk.dim(' Non-interactive mode — skipping setup wizard.'));
26
+ } else {
27
+ throw err;
28
+ }
26
29
  }
27
30
  }
28
31
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.33",
3
+ "version": "0.27.34",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -3,10 +3,19 @@
3
3
 
4
4
  import { execFileSync, spawn } from 'child_process';
5
5
  import { existsSync, writeFileSync, readFileSync, statSync } from 'fs';
6
- import { resolve } from 'path';
6
+ import { resolve, dirname, join } from 'path';
7
+ import { fileURLToPath } from 'url';
7
8
  import { createConnection } from 'net';
8
9
  import crypto from 'crypto';
9
10
 
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ function getLocalVersion() {
13
+ try {
14
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', '..', 'package.json'), 'utf8'));
15
+ return pkg.version || '0.0.0';
16
+ } catch { return '0.0.0'; }
17
+ }
18
+
10
19
  const REMOTE_PORT = 31415;
11
20
  const DEFAULT_LOCAL_PORT = 31416;
12
21
  const MAX_PORT_ATTEMPTS = 10;
@@ -185,7 +194,7 @@ export class TunnelManager {
185
194
  '-o', 'StrictHostKeyChecking=accept-new',
186
195
  '-o', 'BatchMode=yes',
187
196
  target,
188
- `curl -sf http://localhost:${REMOTE_PORT}/api/health 2>/dev/null || (which groove >/dev/null 2>&1 && echo __GROOVE_STOPPED__ || echo __GROOVE_NOT_INSTALLED__)`,
197
+ `bash -lc 'curl -sf http://localhost:${REMOTE_PORT}/api/health 2>/dev/null || (which groove >/dev/null 2>&1 && echo __GROOVE_VER__$(groove --version 2>/dev/null || echo unknown)__GROOVE_STOPPED__ || echo __GROOVE_NOT_INSTALLED__)'`,
189
198
  ], {
190
199
  encoding: 'utf8',
191
200
  timeout: 20000,
@@ -196,7 +205,9 @@ export class TunnelManager {
196
205
  return { reachable: true, daemonRunning: false, grooveInstalled: false };
197
206
  }
198
207
  if (result.includes('__GROOVE_STOPPED__')) {
199
- return { reachable: true, daemonRunning: false, grooveInstalled: true };
208
+ const verMatch = result.match(/__GROOVE_VER__(.+?)__GROOVE_STOPPED__/);
209
+ const remoteVersion = verMatch ? verMatch[1].trim() : null;
210
+ return { reachable: true, daemonRunning: false, grooveInstalled: true, remoteVersion };
200
211
  }
201
212
  return { reachable: true, daemonRunning: true, grooveInstalled: true };
202
213
  } catch (err) {
@@ -236,6 +247,11 @@ export class TunnelManager {
236
247
  this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'installing' } });
237
248
  await this.remoteInstall(id);
238
249
  } else if (!testResult.daemonRunning && testResult.grooveInstalled) {
250
+ const localVer = getLocalVersion();
251
+ if (testResult.remoteVersion && testResult.remoteVersion !== localVer) {
252
+ this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'upgrading', from: testResult.remoteVersion, to: localVer } });
253
+ await this._remoteUpgrade(id, config);
254
+ }
239
255
  this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
240
256
  await this.autoStart(id);
241
257
  }
@@ -332,6 +348,24 @@ export class TunnelManager {
332
348
  }
333
349
  }
334
350
 
351
+ async _remoteUpgrade(id, config) {
352
+ const target = `${config.user}@${config.host}`;
353
+ const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
354
+ const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
355
+ const installCmd = config.user === 'root' ? 'npm i -g groove-dev' : 'sudo npm i -g groove-dev';
356
+
357
+ try {
358
+ execFileSync('ssh', [...sshBase, `bash -lc '${installCmd}'`], {
359
+ encoding: 'utf8',
360
+ timeout: 120000,
361
+ stdio: ['pipe', 'pipe', 'pipe'],
362
+ });
363
+ } catch (err) {
364
+ const output = err.stdout?.toString() || err.stderr?.toString() || err.message;
365
+ throw new Error(`Remote upgrade failed: ${output.slice(-400)}`);
366
+ }
367
+ }
368
+
335
369
  async autoStart(id) {
336
370
  const config = this.saved.get(id);
337
371
  if (!config) throw new Error(`Remote ${id} not found`);
@@ -346,7 +380,7 @@ export class TunnelManager {
346
380
  '-o', 'ConnectTimeout=10',
347
381
  '-o', 'BatchMode=yes',
348
382
  target,
349
- `bash -lc 'nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 4; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || echo __DAEMON_FAIL__'`,
383
+ `bash -lc 'nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 5; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || (echo __DAEMON_FAIL__; tail -20 /tmp/groove-daemon.log 2>/dev/null)'`,
350
384
  ], {
351
385
  encoding: 'utf8',
352
386
  timeout: 45000,
@@ -354,10 +388,12 @@ export class TunnelManager {
354
388
  });
355
389
 
356
390
  if (result.includes('__DAEMON_FAIL__')) {
357
- throw new Error('Daemon process started but health check failed — check /tmp/groove-daemon.log on remote');
391
+ const logLines = result.split('__DAEMON_FAIL__')[1]?.trim() || '';
392
+ const detail = logLines ? `: ${logLines.slice(-300)}` : '';
393
+ throw new Error(`Remote daemon failed to start${detail}`);
358
394
  }
359
395
  } catch (err) {
360
- if (err.message.includes('Daemon process started')) throw err;
396
+ if (err.message.includes('Remote daemon failed')) throw err;
361
397
  const output = err.stdout?.toString() || err.stderr?.toString() || err.message;
362
398
  throw new Error(`Failed to start remote daemon: ${output.slice(-300)}`);
363
399
  }
@@ -423,7 +459,7 @@ export class TunnelManager {
423
459
  try {
424
460
  const result = execFileSync('ssh', [
425
461
  ...sshBase,
426
- remoteCmd(`nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 4; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || echo __DAEMON_FAIL__`),
462
+ remoteCmd(`nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 5; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || (echo __DAEMON_FAIL__; tail -20 /tmp/groove-daemon.log 2>/dev/null)`),
427
463
  ], {
428
464
  encoding: 'utf8',
429
465
  timeout: 45000,
@@ -431,7 +467,9 @@ export class TunnelManager {
431
467
  });
432
468
 
433
469
  if (result.includes('__DAEMON_FAIL__')) {
434
- throw new Error('Groove installed but daemon failed to start — check /tmp/groove-daemon.log on remote');
470
+ const logLines = result.split('__DAEMON_FAIL__')[1]?.trim() || '';
471
+ const detail = logLines ? `: ${logLines.slice(-300)}` : '';
472
+ throw new Error(`Groove installed but daemon failed to start${detail}`);
435
473
  }
436
474
  } catch (err) {
437
475
  if (err.message.includes('Groove installed')) throw err;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.33",
3
+ "version": "0.27.34",
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.33",
3
+ "version": "0.27.34",
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.33",
3
+ "version": "0.27.34",
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",
@@ -14,15 +14,18 @@ export async function start(options) {
14
14
  // ── First-run interactive wizard ────────────────────────────
15
15
  let setupKeys = {};
16
16
  if (isFirstRun) {
17
- try {
18
- const result = await runSetupWizard();
19
- setupKeys = result.keys || {};
20
- } catch (err) {
21
- // If stdin is not interactive (piped), skip wizard
22
- if (err.code === 'ERR_USE_AFTER_CLOSE' || !process.stdin.isTTY) {
23
- console.log(chalk.dim(' Non-interactive mode skipping setup wizard.'));
24
- } else {
25
- throw err;
17
+ if (!process.stdin.isTTY) {
18
+ console.log(chalk.dim(' Non-interactive mode skipping setup wizard.'));
19
+ } else {
20
+ try {
21
+ const result = await runSetupWizard();
22
+ setupKeys = result.keys || {};
23
+ } catch (err) {
24
+ if (err.code === 'ERR_USE_AFTER_CLOSE') {
25
+ console.log(chalk.dim(' Non-interactive mode — skipping setup wizard.'));
26
+ } else {
27
+ throw err;
28
+ }
26
29
  }
27
30
  }
28
31
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.33",
3
+ "version": "0.27.34",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -3,10 +3,19 @@
3
3
 
4
4
  import { execFileSync, spawn } from 'child_process';
5
5
  import { existsSync, writeFileSync, readFileSync, statSync } from 'fs';
6
- import { resolve } from 'path';
6
+ import { resolve, dirname, join } from 'path';
7
+ import { fileURLToPath } from 'url';
7
8
  import { createConnection } from 'net';
8
9
  import crypto from 'crypto';
9
10
 
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ function getLocalVersion() {
13
+ try {
14
+ const pkg = JSON.parse(readFileSync(join(__dirname, '..', '..', '..', 'package.json'), 'utf8'));
15
+ return pkg.version || '0.0.0';
16
+ } catch { return '0.0.0'; }
17
+ }
18
+
10
19
  const REMOTE_PORT = 31415;
11
20
  const DEFAULT_LOCAL_PORT = 31416;
12
21
  const MAX_PORT_ATTEMPTS = 10;
@@ -185,7 +194,7 @@ export class TunnelManager {
185
194
  '-o', 'StrictHostKeyChecking=accept-new',
186
195
  '-o', 'BatchMode=yes',
187
196
  target,
188
- `curl -sf http://localhost:${REMOTE_PORT}/api/health 2>/dev/null || (which groove >/dev/null 2>&1 && echo __GROOVE_STOPPED__ || echo __GROOVE_NOT_INSTALLED__)`,
197
+ `bash -lc 'curl -sf http://localhost:${REMOTE_PORT}/api/health 2>/dev/null || (which groove >/dev/null 2>&1 && echo __GROOVE_VER__$(groove --version 2>/dev/null || echo unknown)__GROOVE_STOPPED__ || echo __GROOVE_NOT_INSTALLED__)'`,
189
198
  ], {
190
199
  encoding: 'utf8',
191
200
  timeout: 20000,
@@ -196,7 +205,9 @@ export class TunnelManager {
196
205
  return { reachable: true, daemonRunning: false, grooveInstalled: false };
197
206
  }
198
207
  if (result.includes('__GROOVE_STOPPED__')) {
199
- return { reachable: true, daemonRunning: false, grooveInstalled: true };
208
+ const verMatch = result.match(/__GROOVE_VER__(.+?)__GROOVE_STOPPED__/);
209
+ const remoteVersion = verMatch ? verMatch[1].trim() : null;
210
+ return { reachable: true, daemonRunning: false, grooveInstalled: true, remoteVersion };
200
211
  }
201
212
  return { reachable: true, daemonRunning: true, grooveInstalled: true };
202
213
  } catch (err) {
@@ -236,6 +247,11 @@ export class TunnelManager {
236
247
  this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'installing' } });
237
248
  await this.remoteInstall(id);
238
249
  } else if (!testResult.daemonRunning && testResult.grooveInstalled) {
250
+ const localVer = getLocalVersion();
251
+ if (testResult.remoteVersion && testResult.remoteVersion !== localVer) {
252
+ this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'upgrading', from: testResult.remoteVersion, to: localVer } });
253
+ await this._remoteUpgrade(id, config);
254
+ }
239
255
  this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
240
256
  await this.autoStart(id);
241
257
  }
@@ -332,6 +348,24 @@ export class TunnelManager {
332
348
  }
333
349
  }
334
350
 
351
+ async _remoteUpgrade(id, config) {
352
+ const target = `${config.user}@${config.host}`;
353
+ const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
354
+ const sshBase = [...keyArgs, '-p', String(config.port || 22), '-o', 'ConnectTimeout=10', '-o', 'BatchMode=yes', target];
355
+ const installCmd = config.user === 'root' ? 'npm i -g groove-dev' : 'sudo npm i -g groove-dev';
356
+
357
+ try {
358
+ execFileSync('ssh', [...sshBase, `bash -lc '${installCmd}'`], {
359
+ encoding: 'utf8',
360
+ timeout: 120000,
361
+ stdio: ['pipe', 'pipe', 'pipe'],
362
+ });
363
+ } catch (err) {
364
+ const output = err.stdout?.toString() || err.stderr?.toString() || err.message;
365
+ throw new Error(`Remote upgrade failed: ${output.slice(-400)}`);
366
+ }
367
+ }
368
+
335
369
  async autoStart(id) {
336
370
  const config = this.saved.get(id);
337
371
  if (!config) throw new Error(`Remote ${id} not found`);
@@ -346,7 +380,7 @@ export class TunnelManager {
346
380
  '-o', 'ConnectTimeout=10',
347
381
  '-o', 'BatchMode=yes',
348
382
  target,
349
- `bash -lc 'nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 4; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || echo __DAEMON_FAIL__'`,
383
+ `bash -lc 'nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 5; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || (echo __DAEMON_FAIL__; tail -20 /tmp/groove-daemon.log 2>/dev/null)'`,
350
384
  ], {
351
385
  encoding: 'utf8',
352
386
  timeout: 45000,
@@ -354,10 +388,12 @@ export class TunnelManager {
354
388
  });
355
389
 
356
390
  if (result.includes('__DAEMON_FAIL__')) {
357
- throw new Error('Daemon process started but health check failed — check /tmp/groove-daemon.log on remote');
391
+ const logLines = result.split('__DAEMON_FAIL__')[1]?.trim() || '';
392
+ const detail = logLines ? `: ${logLines.slice(-300)}` : '';
393
+ throw new Error(`Remote daemon failed to start${detail}`);
358
394
  }
359
395
  } catch (err) {
360
- if (err.message.includes('Daemon process started')) throw err;
396
+ if (err.message.includes('Remote daemon failed')) throw err;
361
397
  const output = err.stdout?.toString() || err.stderr?.toString() || err.message;
362
398
  throw new Error(`Failed to start remote daemon: ${output.slice(-300)}`);
363
399
  }
@@ -423,7 +459,7 @@ export class TunnelManager {
423
459
  try {
424
460
  const result = execFileSync('ssh', [
425
461
  ...sshBase,
426
- remoteCmd(`nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 4; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || echo __DAEMON_FAIL__`),
462
+ remoteCmd(`nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 5; curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null && echo __DAEMON_OK__ || (echo __DAEMON_FAIL__; tail -20 /tmp/groove-daemon.log 2>/dev/null)`),
427
463
  ], {
428
464
  encoding: 'utf8',
429
465
  timeout: 45000,
@@ -431,7 +467,9 @@ export class TunnelManager {
431
467
  });
432
468
 
433
469
  if (result.includes('__DAEMON_FAIL__')) {
434
- throw new Error('Groove installed but daemon failed to start — check /tmp/groove-daemon.log on remote');
470
+ const logLines = result.split('__DAEMON_FAIL__')[1]?.trim() || '';
471
+ const detail = logLines ? `: ${logLines.slice(-300)}` : '';
472
+ throw new Error(`Groove installed but daemon failed to start${detail}`);
435
473
  }
436
474
  } catch (err) {
437
475
  if (err.message.includes('Groove installed')) throw err;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.33",
3
+ "version": "0.27.34",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",