groove-dev 0.27.122 → 0.27.124
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.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/index.js +12 -4
- package/node_modules/@groove-dev/daemon/src/introducer.js +3 -4
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +35 -3
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/index.js +12 -4
- package/packages/daemon/src/introducer.js +3 -4
- package/packages/daemon/src/tunnel-manager.js +35 -3
- package/packages/gui/package.json +1 -1
|
@@ -295,8 +295,12 @@ export class Daemon {
|
|
|
295
295
|
if (this._registryIoTimer) return;
|
|
296
296
|
this._registryIoTimer = setTimeout(() => {
|
|
297
297
|
this._registryIoTimer = null;
|
|
298
|
-
|
|
299
|
-
|
|
298
|
+
try {
|
|
299
|
+
this.introducer.writeRegistryFile(this.projectDir);
|
|
300
|
+
this.introducer.injectGrooveSection(this.projectDir);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error('[registry-io] Failed to update registry files:', err.message);
|
|
303
|
+
}
|
|
300
304
|
}, 2000);
|
|
301
305
|
};
|
|
302
306
|
|
|
@@ -765,8 +769,12 @@ export class Daemon {
|
|
|
765
769
|
// 5. Refresh journalist context — regenerate project map and registry
|
|
766
770
|
// so stale agent references don't persist in GROOVE_PROJECT_MAP.md
|
|
767
771
|
if (cleaned > 0) {
|
|
768
|
-
|
|
769
|
-
|
|
772
|
+
try {
|
|
773
|
+
this.introducer.writeRegistryFile(this.projectDir);
|
|
774
|
+
this.introducer.injectGrooveSection(this.projectDir);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
console.error('[registry-io] Failed to refresh registry during GC:', err.message);
|
|
777
|
+
}
|
|
770
778
|
// Clear journalist's stale in-memory state for removed agents
|
|
771
779
|
this.journalist.lastLogSizes = Object.fromEntries(
|
|
772
780
|
Object.entries(this.journalist.lastLogSizes).filter(([id]) => {
|
|
@@ -477,7 +477,7 @@ export class Introducer {
|
|
|
477
477
|
if (agents.length === 0) {
|
|
478
478
|
const regPath = resolve(projectDir, 'AGENTS_REGISTRY.md');
|
|
479
479
|
if (existsSync(regPath)) {
|
|
480
|
-
writeFileSync(regPath, '');
|
|
480
|
+
try { writeFileSync(regPath, ''); } catch { /* dir may be gone */ }
|
|
481
481
|
}
|
|
482
482
|
return;
|
|
483
483
|
}
|
|
@@ -494,6 +494,7 @@ export class Introducer {
|
|
|
494
494
|
for (const [teamId, teamAgents] of teamGroups) {
|
|
495
495
|
const team = teamId !== '_default' ? this.daemon.teams?.get(teamId) : null;
|
|
496
496
|
const dir = team?.workingDir || projectDir;
|
|
497
|
+
if (!existsSync(dir)) continue;
|
|
497
498
|
|
|
498
499
|
const lines = [
|
|
499
500
|
`# AGENTS REGISTRY`,
|
|
@@ -518,9 +519,7 @@ export class Introducer {
|
|
|
518
519
|
}
|
|
519
520
|
|
|
520
521
|
injectGrooveSection(projectDir) {
|
|
521
|
-
|
|
522
|
-
// This section is delimited by markers so we can update it without
|
|
523
|
-
// clobbering the user's content.
|
|
522
|
+
if (!existsSync(projectDir)) return;
|
|
524
523
|
const claudeMdPath = resolve(projectDir, 'CLAUDE.md');
|
|
525
524
|
const agents = this.daemon.registry.getAll();
|
|
526
525
|
|
|
@@ -347,8 +347,36 @@ export class TunnelManager {
|
|
|
347
347
|
failCount: 0,
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
// Verify the remote daemon is actually reachable through the tunnel.
|
|
351
|
+
// The cached test result (line 270) assumes daemonRunning=true based on
|
|
352
|
+
// lastConnected, but the daemon may have stopped since then.
|
|
353
|
+
let remoteAlive = false;
|
|
354
|
+
try {
|
|
355
|
+
const probe = await fetch(`http://localhost:${localPort}/api/health`, {
|
|
356
|
+
signal: AbortSignal.timeout(5000),
|
|
357
|
+
});
|
|
358
|
+
remoteAlive = probe.ok;
|
|
359
|
+
} catch { /* not reachable */ }
|
|
360
|
+
|
|
361
|
+
if (!remoteAlive && config.autoStart) {
|
|
362
|
+
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
|
|
363
|
+
await this.autoStart(id);
|
|
364
|
+
// Give the daemon a moment to accept connections through the tunnel
|
|
365
|
+
for (let i = 0; i < 5; i++) {
|
|
366
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
367
|
+
try {
|
|
368
|
+
const retry = await fetch(`http://localhost:${localPort}/api/health`, {
|
|
369
|
+
signal: AbortSignal.timeout(3000),
|
|
370
|
+
});
|
|
371
|
+
if (retry.ok) { remoteAlive = true; break; }
|
|
372
|
+
} catch { /* retry */ }
|
|
373
|
+
}
|
|
374
|
+
} else if (!remoteAlive && !config.autoStart) {
|
|
375
|
+
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'waiting', message: 'Remote daemon not running. Start it manually or enable auto-start.' } });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const skipUpgrade = remoteAlive && testResult.remoteVersion && testResult.remoteVersion === getLocalVersion();
|
|
379
|
+
if (remoteAlive && !preConnectHandled && !skipUpgrade) {
|
|
352
380
|
await this._checkAndUpgradeRunning(id, config, localPort);
|
|
353
381
|
}
|
|
354
382
|
|
|
@@ -449,7 +477,11 @@ export class TunnelManager {
|
|
|
449
477
|
this.daemon.broadcast({ type: 'tunnel.version-mismatch', data: { id, localVersion: localVer, remoteVersion: installedVer, message: 'Pinned version not available on npm, installed latest' } });
|
|
450
478
|
}
|
|
451
479
|
|
|
452
|
-
const
|
|
480
|
+
const cdPrefix = config.projectDir ? `cd "${config.projectDir}" && ` : '';
|
|
481
|
+
const setProjectDir = config.projectDir
|
|
482
|
+
? `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; `
|
|
483
|
+
: '';
|
|
484
|
+
const restartCmd = `kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 2; ${cdPrefix}GROOVE_BIN=$(which groove) && nohup "$GROOVE_BIN" start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 4; curl -sf http://localhost:${REMOTE_PORT}/api/status && (${setProjectDir}true) || true`;
|
|
453
485
|
const restartResult = execFileSync('ssh', [...sshBase, sshCmd(restartCmd)], {
|
|
454
486
|
encoding: 'utf8',
|
|
455
487
|
timeout: 60000,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "groove-dev",
|
|
3
|
-
"version": "0.27.
|
|
3
|
+
"version": "0.27.124",
|
|
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)",
|
|
@@ -295,8 +295,12 @@ export class Daemon {
|
|
|
295
295
|
if (this._registryIoTimer) return;
|
|
296
296
|
this._registryIoTimer = setTimeout(() => {
|
|
297
297
|
this._registryIoTimer = null;
|
|
298
|
-
|
|
299
|
-
|
|
298
|
+
try {
|
|
299
|
+
this.introducer.writeRegistryFile(this.projectDir);
|
|
300
|
+
this.introducer.injectGrooveSection(this.projectDir);
|
|
301
|
+
} catch (err) {
|
|
302
|
+
console.error('[registry-io] Failed to update registry files:', err.message);
|
|
303
|
+
}
|
|
300
304
|
}, 2000);
|
|
301
305
|
};
|
|
302
306
|
|
|
@@ -765,8 +769,12 @@ export class Daemon {
|
|
|
765
769
|
// 5. Refresh journalist context — regenerate project map and registry
|
|
766
770
|
// so stale agent references don't persist in GROOVE_PROJECT_MAP.md
|
|
767
771
|
if (cleaned > 0) {
|
|
768
|
-
|
|
769
|
-
|
|
772
|
+
try {
|
|
773
|
+
this.introducer.writeRegistryFile(this.projectDir);
|
|
774
|
+
this.introducer.injectGrooveSection(this.projectDir);
|
|
775
|
+
} catch (err) {
|
|
776
|
+
console.error('[registry-io] Failed to refresh registry during GC:', err.message);
|
|
777
|
+
}
|
|
770
778
|
// Clear journalist's stale in-memory state for removed agents
|
|
771
779
|
this.journalist.lastLogSizes = Object.fromEntries(
|
|
772
780
|
Object.entries(this.journalist.lastLogSizes).filter(([id]) => {
|
|
@@ -477,7 +477,7 @@ export class Introducer {
|
|
|
477
477
|
if (agents.length === 0) {
|
|
478
478
|
const regPath = resolve(projectDir, 'AGENTS_REGISTRY.md');
|
|
479
479
|
if (existsSync(regPath)) {
|
|
480
|
-
writeFileSync(regPath, '');
|
|
480
|
+
try { writeFileSync(regPath, ''); } catch { /* dir may be gone */ }
|
|
481
481
|
}
|
|
482
482
|
return;
|
|
483
483
|
}
|
|
@@ -494,6 +494,7 @@ export class Introducer {
|
|
|
494
494
|
for (const [teamId, teamAgents] of teamGroups) {
|
|
495
495
|
const team = teamId !== '_default' ? this.daemon.teams?.get(teamId) : null;
|
|
496
496
|
const dir = team?.workingDir || projectDir;
|
|
497
|
+
if (!existsSync(dir)) continue;
|
|
497
498
|
|
|
498
499
|
const lines = [
|
|
499
500
|
`# AGENTS REGISTRY`,
|
|
@@ -518,9 +519,7 @@ export class Introducer {
|
|
|
518
519
|
}
|
|
519
520
|
|
|
520
521
|
injectGrooveSection(projectDir) {
|
|
521
|
-
|
|
522
|
-
// This section is delimited by markers so we can update it without
|
|
523
|
-
// clobbering the user's content.
|
|
522
|
+
if (!existsSync(projectDir)) return;
|
|
524
523
|
const claudeMdPath = resolve(projectDir, 'CLAUDE.md');
|
|
525
524
|
const agents = this.daemon.registry.getAll();
|
|
526
525
|
|
|
@@ -347,8 +347,36 @@ export class TunnelManager {
|
|
|
347
347
|
failCount: 0,
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
-
|
|
351
|
-
|
|
350
|
+
// Verify the remote daemon is actually reachable through the tunnel.
|
|
351
|
+
// The cached test result (line 270) assumes daemonRunning=true based on
|
|
352
|
+
// lastConnected, but the daemon may have stopped since then.
|
|
353
|
+
let remoteAlive = false;
|
|
354
|
+
try {
|
|
355
|
+
const probe = await fetch(`http://localhost:${localPort}/api/health`, {
|
|
356
|
+
signal: AbortSignal.timeout(5000),
|
|
357
|
+
});
|
|
358
|
+
remoteAlive = probe.ok;
|
|
359
|
+
} catch { /* not reachable */ }
|
|
360
|
+
|
|
361
|
+
if (!remoteAlive && config.autoStart) {
|
|
362
|
+
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'starting' } });
|
|
363
|
+
await this.autoStart(id);
|
|
364
|
+
// Give the daemon a moment to accept connections through the tunnel
|
|
365
|
+
for (let i = 0; i < 5; i++) {
|
|
366
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
367
|
+
try {
|
|
368
|
+
const retry = await fetch(`http://localhost:${localPort}/api/health`, {
|
|
369
|
+
signal: AbortSignal.timeout(3000),
|
|
370
|
+
});
|
|
371
|
+
if (retry.ok) { remoteAlive = true; break; }
|
|
372
|
+
} catch { /* retry */ }
|
|
373
|
+
}
|
|
374
|
+
} else if (!remoteAlive && !config.autoStart) {
|
|
375
|
+
this.daemon.broadcast({ type: 'tunnel.status', data: { id, step: 'waiting', message: 'Remote daemon not running. Start it manually or enable auto-start.' } });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const skipUpgrade = remoteAlive && testResult.remoteVersion && testResult.remoteVersion === getLocalVersion();
|
|
379
|
+
if (remoteAlive && !preConnectHandled && !skipUpgrade) {
|
|
352
380
|
await this._checkAndUpgradeRunning(id, config, localPort);
|
|
353
381
|
}
|
|
354
382
|
|
|
@@ -449,7 +477,11 @@ export class TunnelManager {
|
|
|
449
477
|
this.daemon.broadcast({ type: 'tunnel.version-mismatch', data: { id, localVersion: localVer, remoteVersion: installedVer, message: 'Pinned version not available on npm, installed latest' } });
|
|
450
478
|
}
|
|
451
479
|
|
|
452
|
-
const
|
|
480
|
+
const cdPrefix = config.projectDir ? `cd "${config.projectDir}" && ` : '';
|
|
481
|
+
const setProjectDir = config.projectDir
|
|
482
|
+
? `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; `
|
|
483
|
+
: '';
|
|
484
|
+
const restartCmd = `kill $(lsof -t -i:${REMOTE_PORT}) 2>/dev/null || true; sleep 2; ${cdPrefix}GROOVE_BIN=$(which groove) && nohup "$GROOVE_BIN" start > /tmp/groove-daemon.log 2>&1 < /dev/null & disown; sleep 4; curl -sf http://localhost:${REMOTE_PORT}/api/status && (${setProjectDir}true) || true`;
|
|
453
485
|
const restartResult = execFileSync('ssh', [...sshBase, sshCmd(restartCmd)], {
|
|
454
486
|
encoding: 'utf8',
|
|
455
487
|
timeout: 60000,
|