groove-dev 0.27.67 → 0.27.69

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 (42) 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 +137 -3
  4. package/node_modules/@groove-dev/daemon/src/process.js +11 -5
  5. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +19 -1
  6. package/node_modules/@groove-dev/gui/dist/assets/index-DhnTm_1P.js +8614 -0
  7. package/node_modules/@groove-dev/gui/dist/assets/index-oQ0ejlfH.css +1 -0
  8. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  9. package/node_modules/@groove-dev/gui/package.json +1 -1
  10. package/node_modules/@groove-dev/gui/src/app.jsx +23 -0
  11. package/node_modules/@groove-dev/gui/src/components/agents/folder-browser.jsx +23 -5
  12. package/node_modules/@groove-dev/gui/src/components/network/activity-chart.jsx +208 -124
  13. package/node_modules/@groove-dev/gui/src/components/network/compute-header.jsx +12 -1
  14. package/node_modules/@groove-dev/gui/src/components/network/identity-bar.jsx +4 -40
  15. package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +600 -0
  16. package/node_modules/@groove-dev/gui/src/components/network/token-waterfall.jsx +111 -0
  17. package/node_modules/@groove-dev/gui/src/stores/groove.js +60 -0
  18. package/node_modules/@groove-dev/gui/src/views/network.jsx +6 -0
  19. package/package.json +1 -1
  20. package/packages/cli/package.json +1 -1
  21. package/packages/daemon/package.json +1 -1
  22. package/packages/daemon/src/api.js +137 -3
  23. package/packages/daemon/src/process.js +11 -5
  24. package/packages/daemon/src/tunnel-manager.js +19 -1
  25. package/packages/gui/dist/assets/index-DhnTm_1P.js +8614 -0
  26. package/packages/gui/dist/assets/index-oQ0ejlfH.css +1 -0
  27. package/packages/gui/dist/index.html +2 -2
  28. package/packages/gui/package.json +1 -1
  29. package/packages/gui/src/app.jsx +23 -0
  30. package/packages/gui/src/components/agents/folder-browser.jsx +23 -5
  31. package/packages/gui/src/components/network/activity-chart.jsx +208 -124
  32. package/packages/gui/src/components/network/compute-header.jsx +12 -1
  33. package/packages/gui/src/components/network/identity-bar.jsx +4 -40
  34. package/packages/gui/src/components/network/performance-dashboard.jsx +600 -0
  35. package/packages/gui/src/components/network/token-waterfall.jsx +111 -0
  36. package/packages/gui/src/stores/groove.js +60 -0
  37. package/packages/gui/src/views/network.jsx +6 -0
  38. package/spash-page.png +0 -0
  39. package/node_modules/@groove-dev/gui/dist/assets/index-MPNqazCA.js +0 -8614
  40. package/node_modules/@groove-dev/gui/dist/assets/index-YeunozTU.css +0 -1
  41. package/packages/gui/dist/assets/index-MPNqazCA.js +0 -8614
  42. package/packages/gui/dist/assets/index-YeunozTU.css +0 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.67",
3
+ "version": "0.27.69",
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.67",
3
+ "version": "0.27.69",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -8,6 +8,7 @@ import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSy
8
8
  import { spawn, execFile, execFileSync } from 'child_process';
9
9
  import { createHash, randomUUID } from 'crypto';
10
10
  import { hostname, networkInterfaces, homedir } from 'os';
11
+ import { StringDecoder } from 'string_decoder';
11
12
  import { lookup as mimeLookup } from './mimetypes.js';
12
13
  import { listProviders, getProvider } from './providers/index.js';
13
14
  import { OllamaProvider } from './providers/ollama.js';
@@ -4299,14 +4300,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
4299
4300
  hardware: getLocalHardware(),
4300
4301
  startedAt: Date.now(),
4301
4302
  events: [],
4303
+ lastTokenTiming: null,
4302
4304
  };
4305
+ if (!daemon.networkBenchmarks) daemon.networkBenchmarks = [];
4303
4306
 
4304
4307
  pushNodeEvent('starting', { pid: proc.pid, signal, device });
4305
4308
  broadcastNodeStatus();
4306
4309
 
4307
4310
  let stderrBuf = '';
4311
+ const stderrDecoder = new StringDecoder('utf8');
4308
4312
  proc.stderr.on('data', (chunk) => {
4309
- stderrBuf += chunk.toString();
4313
+ stderrBuf += stderrDecoder.write(chunk);
4310
4314
  let idx;
4311
4315
  while ((idx = stderrBuf.indexOf('\n')) !== -1) {
4312
4316
  const line = stderrBuf.slice(0, idx).trim();
@@ -4367,14 +4371,49 @@ Keep responses concise. Help them think, don't lecture them about the system the
4367
4371
  if (entry.capabilities || entry.hardware) {
4368
4372
  daemon.networkNode.hardware = normalizeHardware(entry.capabilities || entry.hardware); changed = true;
4369
4373
  }
4374
+ if (entry.type === 'token') {
4375
+ const timing = {
4376
+ token_ms: entry.token_ms, pipeline_ms: entry.pipeline_ms,
4377
+ prefill_ms: entry.prefill_ms, logits_deser_ms: entry.logits_deser_ms,
4378
+ sample_ms: entry.sample_ms, decode_ms: entry.decode_ms,
4379
+ tps: entry.tps, ttft_ms: entry.ttft_ms, is_prefill: entry.is_prefill,
4380
+ tokens_generated: entry.tokens_generated,
4381
+ stages: Array.isArray(entry.stages) ? entry.stages : [],
4382
+ };
4383
+ daemon.networkNode.lastTokenTiming = timing;
4384
+ daemon.broadcast({ type: 'network:token:timing', data: timing });
4385
+ }
4386
+ if (entry.type === 'timing') {
4387
+ const summary = {
4388
+ ttft_ms: entry.ttft_ms, tps: entry.tps,
4389
+ tokens_generated: entry.tokens_generated,
4390
+ total_network_ms: entry.total_network_ms,
4391
+ total_compute_ms: entry.total_compute_ms,
4392
+ p2p_sends: entry.p2p_sends, relay_sends: entry.relay_sends,
4393
+ stage_0_avg_ms: entry.stage_0_avg_ms, stage_0_count: entry.stage_0_count,
4394
+ stage_1_avg_ms: entry.stage_1_avg_ms, stage_1_count: entry.stage_1_count,
4395
+ t: Date.now(),
4396
+ };
4397
+ if (!daemon.networkBenchmarks) daemon.networkBenchmarks = [];
4398
+ daemon.networkBenchmarks.push(summary);
4399
+ if (daemon.networkBenchmarks.length > 100) daemon.networkBenchmarks.shift();
4400
+ daemon.broadcast({ type: 'network:timing:summary', data: summary });
4401
+ }
4370
4402
  pushNodeEvent(msg || 'log', entry);
4371
4403
  if (changed) broadcastNodeStatus();
4372
4404
  }
4373
4405
  });
4374
4406
 
4407
+ let stdoutBuf = '';
4408
+ const stdoutDecoder = new StringDecoder('utf8');
4375
4409
  proc.stdout.on('data', (chunk) => {
4376
- const line = chunk.toString().trim();
4377
- if (line) pushNodeEvent('stdout', { line });
4410
+ stdoutBuf += stdoutDecoder.write(chunk);
4411
+ let idx;
4412
+ while ((idx = stdoutBuf.indexOf('\n')) !== -1) {
4413
+ const line = stdoutBuf.slice(0, idx).trim();
4414
+ stdoutBuf = stdoutBuf.slice(idx + 1);
4415
+ if (line) pushNodeEvent('stdout', { line });
4416
+ }
4378
4417
  });
4379
4418
 
4380
4419
  proc.on('error', (err) => {
@@ -4384,6 +4423,11 @@ Keep responses concise. Help them think, don't lecture them about the system the
4384
4423
  });
4385
4424
 
4386
4425
  proc.on('exit', (code, signal) => {
4426
+ const trailing = stdoutDecoder.end();
4427
+ if (trailing) stdoutBuf += trailing;
4428
+ if (stdoutBuf.trim()) pushNodeEvent('stdout', { line: stdoutBuf.trim() });
4429
+ const trailingErr = stderrDecoder.end();
4430
+ if (trailingErr) stderrBuf += trailingErr;
4387
4431
  daemon.networkNode.active = false;
4388
4432
  daemon.networkNode.status = 'stopped';
4389
4433
  daemon.networkNode.pid = null;
@@ -4409,6 +4453,96 @@ Keep responses concise. Help them think, don't lecture them about the system the
4409
4453
  res.json({ stopping: true });
4410
4454
  });
4411
4455
 
4456
+ app.get('/api/network/benchmarks', networkGate, (req, res) => {
4457
+ res.json(daemon.networkBenchmarks || []);
4458
+ });
4459
+
4460
+ app.get('/api/network/timing', networkGate, (req, res) => {
4461
+ res.json({
4462
+ current: daemon.networkNode?.lastTokenTiming || null,
4463
+ benchmarkCount: (daemon.networkBenchmarks || []).length,
4464
+ });
4465
+ });
4466
+
4467
+ app.get('/api/network/traces', networkGate, (req, res) => {
4468
+ const tracesDir = resolve(homedir(), '.groove', 'traces');
4469
+ if (!existsSync(tracesDir)) return res.json([]);
4470
+ try {
4471
+ const files = readdirSync(tracesDir)
4472
+ .filter((f) => f.endsWith('.jsonl'))
4473
+ .map((f) => {
4474
+ const st = statSync(resolve(tracesDir, f));
4475
+ return { filename: f, size: st.size, mtime: st.mtimeMs };
4476
+ })
4477
+ .sort((a, b) => b.mtime - a.mtime);
4478
+ res.json(files);
4479
+ } catch { res.json([]); }
4480
+ });
4481
+
4482
+ app.get('/api/network/traces/live', networkGate, (req, res) => {
4483
+ const tracesDir = resolve(homedir(), '.groove', 'traces');
4484
+ if (!existsSync(tracesDir)) {
4485
+ return res.json({ lines: [], nextOffset: 0, filename: null, active: false });
4486
+ }
4487
+ try {
4488
+ const files = readdirSync(tracesDir)
4489
+ .filter((f) => f.endsWith('.jsonl'))
4490
+ .map((f) => {
4491
+ const st = statSync(resolve(tracesDir, f));
4492
+ return { filename: f, mtime: st.mtimeMs };
4493
+ })
4494
+ .sort((a, b) => b.mtime - a.mtime);
4495
+ if (files.length === 0) {
4496
+ return res.json({ lines: [], nextOffset: 0, filename: null, active: false });
4497
+ }
4498
+ const newest = files[0];
4499
+ const offset = Math.max(0, parseInt(req.query.offset, 10) || 0);
4500
+ const filePath = resolve(tracesDir, newest.filename);
4501
+ const raw = readFileSync(filePath, 'utf8');
4502
+ const allLines = raw.split('\n').filter(Boolean);
4503
+ const sliced = allLines.slice(offset);
4504
+ const parsed = [];
4505
+ for (const line of sliced) {
4506
+ try { parsed.push(JSON.parse(line)); } catch { /* skip malformed */ }
4507
+ }
4508
+ const active = !!(daemon.networkNode?.active && (daemon.networkNode.sessions || 0) > 0);
4509
+ res.json({
4510
+ lines: parsed,
4511
+ nextOffset: offset + sliced.length,
4512
+ filename: newest.filename,
4513
+ active,
4514
+ });
4515
+ } catch {
4516
+ res.json({ lines: [], nextOffset: 0, filename: null, active: false });
4517
+ }
4518
+ });
4519
+
4520
+ app.get('/api/network/traces/:filename', networkGate, (req, res) => {
4521
+ const { filename } = req.params;
4522
+ if (!filename || /[/\\]/.test(filename) || !filename.endsWith('.jsonl')) {
4523
+ return res.status(400).json({ error: 'Invalid filename' });
4524
+ }
4525
+ const tracesDir = resolve(homedir(), '.groove', 'traces');
4526
+ const filePath = resolve(tracesDir, filename);
4527
+ if (!filePath.startsWith(tracesDir + sep)) {
4528
+ return res.status(400).json({ error: 'Invalid filename' });
4529
+ }
4530
+ if (!existsSync(filePath)) {
4531
+ return res.status(404).json({ error: 'Trace file not found' });
4532
+ }
4533
+ try {
4534
+ const raw = readFileSync(filePath, 'utf8');
4535
+ const lines = raw.split('\n').filter(Boolean).slice(0, 5000);
4536
+ const entries = [];
4537
+ for (const line of lines) {
4538
+ try { entries.push(JSON.parse(line)); } catch { /* skip malformed lines */ }
4539
+ }
4540
+ res.json(entries);
4541
+ } catch (err) {
4542
+ res.status(500).json({ error: `Failed to read trace: ${err.message}` });
4543
+ }
4544
+ });
4545
+
4412
4546
  function isAllowedSignalHost(host) {
4413
4547
  const h = (host || '').replace(/^(wss?|https?):\/\//i, '').replace(/\/.*$/, '').toLowerCase();
4414
4548
  return h === 'signal.groovedev.ai' || h.endsWith('.groovedev.ai');
@@ -3,7 +3,7 @@
3
3
 
4
4
  import { spawn as cpSpawn } from 'child_process';
5
5
  import { createWriteStream, mkdirSync, chmodSync, existsSync, readFileSync, writeFileSync, unlinkSync, readdirSync, copyFileSync } from 'fs';
6
- import { resolve, dirname } from 'path';
6
+ import { resolve, dirname, isAbsolute } from 'path';
7
7
  import { fileURLToPath } from 'url';
8
8
  import { getProvider, getInstalledProviders } from './providers/index.js';
9
9
  import { AgentLoop } from './agent-loop.js';
@@ -366,13 +366,19 @@ export class ProcessManager {
366
366
  async spawn(config) {
367
367
  const { registry, locks, introducer } = this.daemon;
368
368
 
369
- // Validate workingDir is within the project directory
369
+ // Validate workingDir. Must be an absolute path to an existing directory.
370
+ // We intentionally do NOT require it to live under daemon.projectDir — remote
371
+ // SSH users legitimately run projects anywhere on the server (e.g. /var/www,
372
+ // /opt/myapp) while daemon.projectDir defaults to their homedir.
370
373
  if (config.workingDir) {
374
+ if (typeof config.workingDir !== 'string' || config.workingDir.includes('\0') || !isAbsolute(config.workingDir)) {
375
+ throw new Error('workingDir must be an absolute path');
376
+ }
371
377
  const resolved = resolve(config.workingDir);
372
- const projResolved = resolve(this.daemon?.projectDir || process.cwd());
373
- if (!resolved.startsWith(projResolved)) {
374
- throw new Error('workingDir must be within project directory');
378
+ if (!existsSync(resolved)) {
379
+ throw new Error(`workingDir does not exist: ${resolved}`);
375
380
  }
381
+ config.workingDir = resolved;
376
382
  }
377
383
 
378
384
  // Ambassador spawn guard: one ambassador per federation peer
@@ -396,6 +396,24 @@ export class TunnelManager {
396
396
  const target = `${config.user}@${config.host}`;
397
397
  const keyArgs = config.sshKeyPath ? ['-i', config.sshKeyPath] : [];
398
398
 
399
+ // Build the remote bash command:
400
+ // 1. cd into the saved projectDir (if set) so the daemon inherits that cwd
401
+ // 2. launch `groove start` detached via nohup
402
+ // 3. poll /api/health until it responds
403
+ // 4. explicitly POST /api/project-dir so the daemon's projectDir matches
404
+ // config.projectDir even if the backgrounded cwd didn't stick (this
405
+ // also updates the editor root used for /api/browse, /api/files/*)
406
+ const cdPrefix = config.projectDir ? `cd "${config.projectDir}" && ` : '';
407
+ const setProjectDir = config.projectDir
408
+ ? `curl -sf -X POST -H 'Content-Type: application/json' --data '{"path":"${config.projectDir}"}' http://localhost:${REMOTE_PORT}/api/project-dir > /dev/null 2>&1 || true; `
409
+ : '';
410
+ const remoteCmd =
411
+ `${cdPrefix}nohup groove start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; ` +
412
+ `sleep 5; ` +
413
+ `curl -sf http://localhost:${REMOTE_PORT}/api/health > /dev/null ` +
414
+ `&& (${setProjectDir}echo __DAEMON_OK__) ` +
415
+ `|| (echo __DAEMON_FAIL__; tail -20 /tmp/groove-daemon.log 2>/dev/null)`;
416
+
399
417
  try {
400
418
  const result = execFileSync('ssh', [
401
419
  ...keyArgs,
@@ -403,7 +421,7 @@ export class TunnelManager {
403
421
  '-o', 'ConnectTimeout=10',
404
422
  '-o', 'BatchMode=yes',
405
423
  target,
406
- `bash -lc '${config.projectDir ? `cd "${config.projectDir}" && ` : ''}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)'`,
424
+ `bash -lc '${remoteCmd}'`,
407
425
  ], {
408
426
  encoding: 'utf8',
409
427
  timeout: 45000,