groove-dev 0.27.14 → 0.27.17
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/README.md +37 -1
- package/developerID_application.cer +0 -0
- package/node_modules/@groove-dev/daemon/src/api.js +587 -68
- package/node_modules/@groove-dev/daemon/src/classifier.js +24 -0
- package/node_modules/@groove-dev/daemon/src/credentials.js +12 -2
- package/node_modules/@groove-dev/daemon/src/federation/ambassador.js +204 -0
- package/node_modules/@groove-dev/daemon/src/federation/connection.js +359 -0
- package/node_modules/@groove-dev/daemon/src/federation/contracts.js +112 -0
- package/node_modules/@groove-dev/daemon/src/federation/whitelist.js +190 -0
- package/node_modules/@groove-dev/daemon/src/federation.js +166 -7
- package/node_modules/@groove-dev/daemon/src/index.js +172 -19
- package/node_modules/@groove-dev/daemon/src/introducer.js +52 -7
- package/node_modules/@groove-dev/daemon/src/journalist.js +46 -1
- package/node_modules/@groove-dev/daemon/src/memory.js +36 -16
- package/node_modules/@groove-dev/daemon/src/process.js +140 -23
- package/node_modules/@groove-dev/daemon/src/providers/base.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +1 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +124 -28
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +104 -17
- package/node_modules/@groove-dev/daemon/src/providers/index.js +17 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +10 -1
- package/node_modules/@groove-dev/daemon/src/rotator.js +93 -30
- package/node_modules/@groove-dev/daemon/src/skills.js +33 -3
- package/node_modules/@groove-dev/daemon/src/terminal-pty.js +9 -1
- package/node_modules/@groove-dev/daemon/src/tool-executor.js +11 -5
- package/node_modules/@groove-dev/daemon/src/toys.js +69 -0
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +24 -5
- package/node_modules/@groove-dev/daemon/templates/toys-catalog.json +242 -0
- package/node_modules/@groove-dev/daemon/test/classifier.test.js +98 -0
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +72 -1
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +117 -0
- package/node_modules/@groove-dev/daemon/test/memory.test.js +37 -1
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +183 -4
- package/node_modules/@groove-dev/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +3 -2
- package/node_modules/@groove-dev/gui/index.html +1 -0
- package/node_modules/@groove-dev/gui/src/app.css +7 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +37 -10
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +21 -31
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +11 -6
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/node_modules/@groove-dev/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +33 -2
- package/node_modules/@groove-dev/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/node_modules/@groove-dev/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/node_modules/@groove-dev/gui/src/components/editor/goto-line.jsx +35 -0
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +12 -6
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +13 -3
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +0 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/node_modules/@groove-dev/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/node_modules/@groove-dev/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +12 -5
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-activity.jsx +98 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-panel.jsx +290 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-peers.jsx +126 -0
- package/node_modules/@groove-dev/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +110 -67
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/settings/server-detail.jsx +310 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +4 -1
- package/node_modules/@groove-dev/gui/src/components/settings/server-list.jsx +59 -0
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-card.jsx +78 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-creator.jsx +144 -0
- package/node_modules/@groove-dev/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/lib/electron.js +15 -0
- package/node_modules/@groove-dev/gui/src/lib/format.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +373 -58
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +148 -42
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +92 -2
- package/node_modules/@groove-dev/gui/src/views/federation.jsx +37 -0
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +2 -42
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +32 -132
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +327 -0
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/toys.jsx +162 -0
- package/package.json +1 -1
- package/packages/daemon/src/api.js +587 -68
- package/packages/daemon/src/classifier.js +24 -0
- package/packages/daemon/src/credentials.js +12 -2
- package/packages/daemon/src/federation/ambassador.js +204 -0
- package/packages/daemon/src/federation/connection.js +359 -0
- package/packages/daemon/src/federation/contracts.js +112 -0
- package/packages/daemon/src/federation/whitelist.js +190 -0
- package/packages/daemon/src/federation.js +166 -7
- package/packages/daemon/src/index.js +172 -19
- package/packages/daemon/src/introducer.js +52 -7
- package/packages/daemon/src/journalist.js +46 -1
- package/packages/daemon/src/memory.js +36 -16
- package/packages/daemon/src/process.js +140 -23
- package/packages/daemon/src/providers/base.js +1 -0
- package/packages/daemon/src/providers/claude-code.js +1 -0
- package/packages/daemon/src/providers/codex.js +124 -28
- package/packages/daemon/src/providers/gemini.js +104 -17
- package/packages/daemon/src/providers/index.js +17 -0
- package/packages/daemon/src/registry.js +10 -1
- package/packages/daemon/src/rotator.js +93 -30
- package/packages/daemon/src/skills.js +33 -3
- package/packages/daemon/src/terminal-pty.js +9 -1
- package/packages/daemon/src/tool-executor.js +11 -5
- package/packages/daemon/src/toys.js +69 -0
- package/packages/daemon/src/tunnel-manager.js +24 -5
- package/packages/daemon/templates/toys-catalog.json +242 -0
- package/packages/gui/dist/assets/index-BglPgjlu.js +8607 -0
- package/packages/gui/dist/assets/index-CGcwmmJv.css +1 -0
- package/packages/gui/dist/index.html +3 -2
- package/packages/gui/index.html +1 -0
- package/packages/gui/src/app.css +7 -0
- package/packages/gui/src/app.jsx +37 -10
- package/packages/gui/src/components/agents/agent-chat.jsx +21 -31
- package/packages/gui/src/components/agents/agent-config.jsx +11 -6
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/spawn-wizard.jsx +42 -1
- package/packages/gui/src/components/editor/breadcrumbs.jsx +30 -0
- package/packages/gui/src/components/editor/code-editor.jsx +33 -2
- package/packages/gui/src/components/editor/editor-status-bar.jsx +26 -0
- package/packages/gui/src/components/editor/editor-tabs.jsx +113 -34
- package/packages/gui/src/components/editor/goto-line.jsx +35 -0
- package/packages/gui/src/components/editor/terminal.jsx +12 -6
- package/packages/gui/src/components/layout/activity-bar.jsx +13 -3
- package/packages/gui/src/components/layout/app-shell.jsx +0 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +165 -47
- package/packages/gui/src/components/layout/command-palette.jsx +6 -2
- package/packages/gui/src/components/layout/terminal-panel.jsx +10 -9
- package/packages/gui/src/components/marketplace/repo-import.jsx +9 -1
- package/packages/gui/src/components/onboarding/provider-card.jsx +134 -0
- package/packages/gui/src/components/onboarding/setup-wizard.jsx +819 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +12 -5
- package/packages/gui/src/components/pro/upgrade-card.jsx +15 -8
- package/packages/gui/src/components/pro/upgrade-modal.jsx +151 -0
- package/packages/gui/src/components/settings/federation-activity.jsx +98 -0
- package/packages/gui/src/components/settings/federation-panel.jsx +290 -0
- package/packages/gui/src/components/settings/federation-peers.jsx +126 -0
- package/packages/gui/src/components/settings/federation-wizard.jsx +293 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +110 -67
- package/packages/gui/src/components/settings/remote-server-card.jsx +3 -3
- package/packages/gui/src/components/settings/server-detail.jsx +310 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +4 -1
- package/packages/gui/src/components/settings/server-list.jsx +59 -0
- package/packages/gui/src/components/settings/ssh-wizard.jsx +549 -0
- package/packages/gui/src/components/toys/toy-card.jsx +78 -0
- package/packages/gui/src/components/toys/toy-creator.jsx +144 -0
- package/packages/gui/src/components/toys/toy-launcher.jsx +187 -0
- package/packages/gui/src/components/ui/toast.jsx +2 -2
- package/packages/gui/src/lib/electron.js +15 -0
- package/packages/gui/src/lib/format.js +1 -0
- package/packages/gui/src/stores/groove.js +373 -58
- package/packages/gui/src/views/agents.jsx +148 -42
- package/packages/gui/src/views/editor.jsx +92 -2
- package/packages/gui/src/views/federation.jsx +37 -0
- package/packages/gui/src/views/marketplace.jsx +2 -42
- package/packages/gui/src/views/settings.jsx +32 -132
- package/packages/gui/src/views/subscription-panel.jsx +327 -0
- package/packages/gui/src/views/teams.jsx +3 -3
- package/packages/gui/src/views/toys.jsx +162 -0
- package/plans/chat-persistence-refactor.md +154 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +0 -677
- package/packages/gui/dist/assets/index-BE6lYcd7.css +0 -1
- package/packages/gui/dist/assets/index-zdzOLAZM.js +0 -677
|
@@ -1,12 +1,29 @@
|
|
|
1
1
|
// GROOVE — Provider Registry
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
|
+
import { execSync } from 'child_process';
|
|
4
5
|
import { ClaudeCodeProvider } from './claude-code.js';
|
|
5
6
|
import { CodexProvider } from './codex.js';
|
|
6
7
|
import { GeminiProvider } from './gemini.js';
|
|
7
8
|
import { OllamaProvider } from './ollama.js';
|
|
8
9
|
import { LocalProvider } from './local.js';
|
|
9
10
|
|
|
11
|
+
// Electron forks may not inherit the full shell PATH, causing `which` to miss
|
|
12
|
+
// globally-installed CLI tools. Augment PATH with common npm global bin dirs.
|
|
13
|
+
(function augmentPath() {
|
|
14
|
+
const extra = ['/usr/local/bin', '/opt/homebrew/bin'];
|
|
15
|
+
try {
|
|
16
|
+
const npmPrefix = execSync('npm config get prefix 2>/dev/null', { encoding: 'utf8', timeout: 5000 }).trim();
|
|
17
|
+
const npmGlobal = npmPrefix ? `${npmPrefix}/bin` : '';
|
|
18
|
+
if (npmGlobal) extra.push(npmGlobal);
|
|
19
|
+
} catch { /* npm itself may not be in PATH yet */ }
|
|
20
|
+
const home = process.env.HOME || '';
|
|
21
|
+
if (home) extra.push(`${home}/.npm-global/bin`);
|
|
22
|
+
const cur = process.env.PATH || '';
|
|
23
|
+
const toAdd = extra.filter(p => p && !cur.split(':').includes(p));
|
|
24
|
+
if (toAdd.length) process.env.PATH = [...toAdd, cur].join(':');
|
|
25
|
+
})();
|
|
26
|
+
|
|
10
27
|
const providers = {
|
|
11
28
|
'claude-code': new ClaudeCodeProvider(),
|
|
12
29
|
'codex': new CodexProvider(),
|
|
@@ -12,9 +12,18 @@ export class Registry extends EventEmitter {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
add(config) {
|
|
15
|
+
let name = config.name || `${config.role}-${this.agents.size + 1}`;
|
|
16
|
+
// Dedup: ensure name is unique within the same team
|
|
17
|
+
const teamId = config.teamId || null;
|
|
18
|
+
const existing = this.getAll();
|
|
19
|
+
if (existing.some((a) => a.name === name && a.teamId === teamId)) {
|
|
20
|
+
let suffix = 2;
|
|
21
|
+
while (existing.some((a) => a.name === `${name}-${suffix}` && a.teamId === teamId)) suffix++;
|
|
22
|
+
name = `${name}-${suffix}`;
|
|
23
|
+
}
|
|
15
24
|
const agent = {
|
|
16
25
|
id: randomUUID().slice(0, 8),
|
|
17
|
-
name
|
|
26
|
+
name,
|
|
18
27
|
role: config.role,
|
|
19
28
|
scope: config.scope || [],
|
|
20
29
|
provider: config.provider || 'claude-code',
|
|
@@ -4,14 +4,23 @@
|
|
|
4
4
|
import { EventEmitter } from 'events';
|
|
5
5
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
6
6
|
import { resolve } from 'path';
|
|
7
|
+
import { getProvider } from './providers/index.js';
|
|
7
8
|
|
|
8
|
-
const DEFAULT_THRESHOLD = 0.75
|
|
9
|
-
const HARD_CEILING = 0.
|
|
9
|
+
const DEFAULT_THRESHOLD = 0.65; // For non-self-managing providers (was 0.75)
|
|
10
|
+
const HARD_CEILING = 0.80; // Force rotate (was 0.85) — only for non-self-managing
|
|
10
11
|
const CHECK_INTERVAL = 15_000;
|
|
11
12
|
const QUALITY_THRESHOLD = 40; // Score below this triggers quality rotation
|
|
12
13
|
const MIN_EVENTS = 10; // Minimum classifier events before scoring
|
|
13
14
|
const MIN_AGE_SEC = 120; // Minimum agent age before quality rotation
|
|
14
15
|
const SCORE_HISTORY_MAX = 40; // ~10 min at 15s intervals
|
|
16
|
+
const COOLDOWN_MS = 5 * 60 * 1000; // 5 minutes between rotations per agent
|
|
17
|
+
const TOKEN_CEILING = 5_000_000; // 5M tokens per agent (non-self-managing only)
|
|
18
|
+
const ROLE_MULTIPLIERS = {
|
|
19
|
+
planner: 10,
|
|
20
|
+
fullstack: 4,
|
|
21
|
+
security: 4,
|
|
22
|
+
analyst: 5,
|
|
23
|
+
};
|
|
15
24
|
|
|
16
25
|
export class Rotator extends EventEmitter {
|
|
17
26
|
constructor(daemon) {
|
|
@@ -20,6 +29,7 @@ export class Rotator extends EventEmitter {
|
|
|
20
29
|
this.interval = null;
|
|
21
30
|
this.rotationHistory = [];
|
|
22
31
|
this.rotating = new Set();
|
|
32
|
+
this.lastRotationTime = new Map(); // agentId -> timestamp of last rotation
|
|
23
33
|
this.enabled = false;
|
|
24
34
|
this.liveScores = {};
|
|
25
35
|
this.scoreHistory = {};
|
|
@@ -91,6 +101,17 @@ export class Rotator extends EventEmitter {
|
|
|
91
101
|
this.enabled = false;
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
_isOnCooldown(agentId) {
|
|
105
|
+
const lastTime = this.lastRotationTime.get(agentId);
|
|
106
|
+
if (!lastTime) return false;
|
|
107
|
+
return (Date.now() - lastTime) < COOLDOWN_MS;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
_getTokenCeiling(agent) {
|
|
111
|
+
const multiplier = ROLE_MULTIPLIERS[agent.role] || 1;
|
|
112
|
+
return TOKEN_CEILING * multiplier;
|
|
113
|
+
}
|
|
114
|
+
|
|
94
115
|
_idleMs(agent) {
|
|
95
116
|
return agent.lastActivity
|
|
96
117
|
? Date.now() - new Date(agent.lastActivity).getTime()
|
|
@@ -131,27 +152,49 @@ export class Rotator extends EventEmitter {
|
|
|
131
152
|
for (const agent of running) {
|
|
132
153
|
if (this.rotating.has(agent.id)) continue;
|
|
133
154
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
await this.rotate(agent.id, { reason: 'hard_ceiling' });
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
155
|
+
// Determine if provider manages its own context (e.g. Claude Code compacts internally)
|
|
156
|
+
const providerInstance = getProvider(agent.provider);
|
|
157
|
+
const selfManagesContext = providerInstance?.constructor?.managesOwnContext ?? false;
|
|
140
158
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
159
|
+
if (!selfManagesContext) {
|
|
160
|
+
// Non-Claude: threshold + ceiling + quality rotation
|
|
161
|
+
// These providers fill up linearly and degrade without external rotation
|
|
144
162
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
await this.rotate(agent.id, { reason: 'context_threshold' });
|
|
163
|
+
// Hard ceiling — force rotate, no idle check, bypasses cooldown (safety override)
|
|
164
|
+
if (agent.contextUsage >= HARD_CEILING) {
|
|
165
|
+
console.log(` Rotator: ${agent.name} at ${Math.round(agent.contextUsage * 100)}% — FORCE rotating (hard ceiling)`);
|
|
166
|
+
await this.rotate(agent.id, { reason: 'hard_ceiling' });
|
|
150
167
|
continue;
|
|
151
168
|
}
|
|
169
|
+
|
|
170
|
+
// Token ceiling — force rotate when total tokens exceed ceiling, bypasses cooldown
|
|
171
|
+
const tokenCeiling = this._getTokenCeiling(agent);
|
|
172
|
+
if (agent.tokensUsed >= tokenCeiling) {
|
|
173
|
+
console.log(` Rotator: ${agent.name} at ${(agent.tokensUsed || 0).toLocaleString()} tokens — FORCE rotating (token ceiling ${tokenCeiling.toLocaleString()})`);
|
|
174
|
+
await this.rotate(agent.id, { reason: 'token_ceiling', tokensUsed: agent.tokensUsed, ceiling: tokenCeiling });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Cooldown — skip threshold/quality rotation if recently rotated
|
|
179
|
+
if (this._isOnCooldown(agent.id)) continue;
|
|
180
|
+
|
|
181
|
+
// Context threshold — rotate when idle
|
|
182
|
+
const threshold = this.daemon.adaptive
|
|
183
|
+
? this.daemon.adaptive.getThreshold(agent.provider, agent.role)
|
|
184
|
+
: DEFAULT_THRESHOLD;
|
|
185
|
+
if (agent.contextUsage >= threshold) {
|
|
186
|
+
if (this._idleMs(agent) > 10_000) {
|
|
187
|
+
console.log(` Rotator: ${agent.name} at ${Math.round(agent.contextUsage * 100)}% — rotating (context)`);
|
|
188
|
+
await this.rotate(agent.id, { reason: 'context_threshold' });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
152
192
|
}
|
|
153
193
|
|
|
154
|
-
//
|
|
194
|
+
// Cooldown — skip quality rotation for self-managing providers if recently rotated
|
|
195
|
+
if (this._isOnCooldown(agent.id)) continue;
|
|
196
|
+
|
|
197
|
+
// All providers: quality-based rotation — detects degradation before tokens are wasted
|
|
155
198
|
const quality = this.scoreLiveSession(agent);
|
|
156
199
|
if (quality.hasEnoughData && quality.score < QUALITY_THRESHOLD) {
|
|
157
200
|
if (this._idleMs(agent) > 10_000) {
|
|
@@ -208,12 +251,12 @@ export class Rotator extends EventEmitter {
|
|
|
208
251
|
this.daemon.memory.appendHandoffBrief(agent.role, {
|
|
209
252
|
timestamp: new Date().toISOString(),
|
|
210
253
|
agentId: agent.id,
|
|
211
|
-
newAgentId: null,
|
|
254
|
+
newAgentId: null,
|
|
212
255
|
reason: options.reason || 'manual',
|
|
213
256
|
oldTokens: agent.tokensUsed,
|
|
214
257
|
contextUsage: agent.contextUsage,
|
|
215
258
|
brief: brief.slice(0, 4000),
|
|
216
|
-
});
|
|
259
|
+
}, agent.workingDir);
|
|
217
260
|
}
|
|
218
261
|
|
|
219
262
|
const record = {
|
|
@@ -233,17 +276,28 @@ export class Rotator extends EventEmitter {
|
|
|
233
276
|
const routingMode = this.daemon.router.getMode(agentId);
|
|
234
277
|
const respawnModel = routingMode.mode === 'auto' ? 'auto' : agent.model;
|
|
235
278
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
279
|
+
let newAgent;
|
|
280
|
+
try {
|
|
281
|
+
newAgent = await processes.spawn({
|
|
282
|
+
role: agent.role,
|
|
283
|
+
scope: agent.scope,
|
|
284
|
+
provider: agent.provider,
|
|
285
|
+
model: respawnModel,
|
|
286
|
+
prompt: brief,
|
|
287
|
+
permission: agent.permission || 'full',
|
|
288
|
+
workingDir: agent.workingDir,
|
|
289
|
+
name: agent.name,
|
|
290
|
+
teamId: agent.teamId,
|
|
291
|
+
});
|
|
292
|
+
} catch (spawnErr) {
|
|
293
|
+
// Spawn failed — old agent still in registry with 'killed' status.
|
|
294
|
+
// Don't lose it — the user can see and retry.
|
|
295
|
+
console.error(`[Groove] Rotation spawn failed for ${agent.name}: ${spawnErr.message}`);
|
|
296
|
+
throw spawnErr;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Spawn succeeded — safe to remove old agent entry
|
|
300
|
+
registry.remove(agentId);
|
|
247
301
|
|
|
248
302
|
if (agent.tokensUsed > 0) {
|
|
249
303
|
registry.update(newAgent.id, { tokensUsed: agent.tokensUsed });
|
|
@@ -254,6 +308,7 @@ export class Rotator extends EventEmitter {
|
|
|
254
308
|
|
|
255
309
|
record.newAgentId = newAgent.id;
|
|
256
310
|
record.newTokens = 0;
|
|
311
|
+
this.lastRotationTime.set(newAgent.id, Date.now());
|
|
257
312
|
this.rotationHistory.push(record);
|
|
258
313
|
|
|
259
314
|
if (this.rotationHistory.length > 100) {
|
|
@@ -358,6 +413,7 @@ export class Rotator extends EventEmitter {
|
|
|
358
413
|
const contextRotations = this.rotationHistory.filter((r) => r.reason === 'context_threshold').length;
|
|
359
414
|
const naturalCompactions = this.rotationHistory.filter((r) => r.reason === 'natural_compaction').length;
|
|
360
415
|
const hardCeilingRotations = this.rotationHistory.filter((r) => r.reason === 'hard_ceiling').length;
|
|
416
|
+
const tokenCeilingRotations = this.rotationHistory.filter((r) => r.reason === 'token_ceiling').length;
|
|
361
417
|
return {
|
|
362
418
|
enabled: this.enabled,
|
|
363
419
|
totalRotations,
|
|
@@ -366,9 +422,16 @@ export class Rotator extends EventEmitter {
|
|
|
366
422
|
contextRotations,
|
|
367
423
|
naturalCompactions,
|
|
368
424
|
hardCeilingRotations,
|
|
425
|
+
tokenCeilingRotations,
|
|
369
426
|
rotating: Array.from(this.rotating),
|
|
370
427
|
liveScores: this.liveScores,
|
|
371
428
|
scoreHistory: this.scoreHistory,
|
|
429
|
+
defaultThreshold: DEFAULT_THRESHOLD,
|
|
430
|
+
hardCeiling: HARD_CEILING,
|
|
431
|
+
qualityThreshold: QUALITY_THRESHOLD,
|
|
432
|
+
cooldownMs: COOLDOWN_MS,
|
|
433
|
+
tokenCeiling: TOKEN_CEILING,
|
|
434
|
+
roleMultipliers: ROLE_MULTIPLIERS,
|
|
372
435
|
};
|
|
373
436
|
}
|
|
374
437
|
}
|
|
@@ -37,6 +37,9 @@ export class SkillStore {
|
|
|
37
37
|
|
|
38
38
|
// Fetch full registry from live API in background
|
|
39
39
|
this._refreshRegistry();
|
|
40
|
+
|
|
41
|
+
// Sync subscription cache from stored user on startup
|
|
42
|
+
this._syncSubscriptionCache(this.getUser());
|
|
40
43
|
}
|
|
41
44
|
|
|
42
45
|
// --- Auth ---
|
|
@@ -69,22 +72,45 @@ export class SkillStore {
|
|
|
69
72
|
return null; // Invalid token
|
|
70
73
|
}
|
|
71
74
|
} catch {
|
|
72
|
-
// Can't validate —
|
|
73
|
-
|
|
75
|
+
// Can't validate — keep existing stored user so subscription data survives
|
|
76
|
+
const existingUser = this.getUser();
|
|
77
|
+
user = existingUser || { id: 'unknown' };
|
|
74
78
|
}
|
|
75
79
|
|
|
76
80
|
this.daemon.config.marketplace = { token, user };
|
|
77
81
|
const { saveConfig } = await import('./firstrun.js');
|
|
78
82
|
saveConfig(this.daemon.grooveDir, this.daemon.config);
|
|
79
83
|
this.daemon.audit.log('marketplace.login', { userId: user?.id });
|
|
84
|
+
this._syncSubscriptionCache(user);
|
|
80
85
|
return user;
|
|
81
86
|
}
|
|
82
87
|
|
|
88
|
+
_syncSubscriptionCache(user) {
|
|
89
|
+
const sub = user?.subscription;
|
|
90
|
+
if (!this.daemon) return;
|
|
91
|
+
if (!sub) {
|
|
92
|
+
console.log(`[Groove:Sub] No subscription data on user — cache unchanged (plan: ${this.daemon.subscriptionCache?.plan || 'unknown'})`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.daemon.subscriptionCache = {
|
|
96
|
+
plan: sub.plan || 'community',
|
|
97
|
+
status: sub.status || (sub.plan !== 'community' ? 'active' : 'none'),
|
|
98
|
+
active: sub.status === 'active' || (sub.plan && sub.plan !== 'community'),
|
|
99
|
+
features: sub.features || [],
|
|
100
|
+
seats: sub.seats || 1,
|
|
101
|
+
periodEnd: sub.periodEnd || null,
|
|
102
|
+
cancelAtPeriodEnd: sub.cancelAtPeriodEnd || false,
|
|
103
|
+
validatedAt: Date.now(),
|
|
104
|
+
};
|
|
105
|
+
this.daemon.broadcast({ type: 'subscription:updated', data: this.daemon.subscriptionCache });
|
|
106
|
+
}
|
|
107
|
+
|
|
83
108
|
/** Clear stored auth */
|
|
84
109
|
async clearAuth() {
|
|
85
110
|
delete this.daemon.config.marketplace;
|
|
86
111
|
const { saveConfig } = await import('./firstrun.js');
|
|
87
112
|
saveConfig(this.daemon.grooveDir, this.daemon.config);
|
|
113
|
+
this.daemon.subscriptionCache = { plan: 'community', status: 'none', features: [], active: false, validatedAt: 0 };
|
|
88
114
|
this.daemon.audit.log('marketplace.logout');
|
|
89
115
|
}
|
|
90
116
|
|
|
@@ -98,7 +124,11 @@ export class SkillStore {
|
|
|
98
124
|
headers: { 'Authorization': `Bearer ${token}` },
|
|
99
125
|
signal: AbortSignal.timeout(8000),
|
|
100
126
|
});
|
|
101
|
-
if (res.ok)
|
|
127
|
+
if (res.ok) {
|
|
128
|
+
const user = await res.json();
|
|
129
|
+
this._syncSubscriptionCache(user);
|
|
130
|
+
return user;
|
|
131
|
+
}
|
|
102
132
|
} catch { /* offline — assume valid */ return this.getUser(); }
|
|
103
133
|
|
|
104
134
|
// 401 — token expired, clear it
|
|
@@ -115,7 +115,7 @@ export class TerminalManager {
|
|
|
115
115
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
116
116
|
});
|
|
117
117
|
|
|
118
|
-
const session = { proc, ws, id };
|
|
118
|
+
const session = { proc, ws, id, label: options.label || '' };
|
|
119
119
|
this.sessions.set(id, session);
|
|
120
120
|
|
|
121
121
|
proc.stdout.on('data', (data) => {
|
|
@@ -160,6 +160,14 @@ export class TerminalManager {
|
|
|
160
160
|
session.proc.stdin.write(`\x1b]7;${rows};${cols}\x07`);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
rename(id, label) {
|
|
164
|
+
const session = this.sessions.get(id);
|
|
165
|
+
if (!session) return false;
|
|
166
|
+
if (typeof label !== 'string' || label.length > 100) return false;
|
|
167
|
+
session.label = label;
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
|
|
163
171
|
kill(id) {
|
|
164
172
|
const session = this.sessions.get(id);
|
|
165
173
|
if (!session) return;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
3
|
|
|
4
4
|
import { readFileSync, writeFileSync, readdirSync, statSync, mkdirSync, existsSync } from 'fs';
|
|
5
|
-
import { execSync } from 'child_process';
|
|
5
|
+
import { execSync, execFileSync } from 'child_process';
|
|
6
6
|
import { resolve, relative, dirname, sep } from 'path';
|
|
7
7
|
import { minimatch } from 'minimatch';
|
|
8
8
|
|
|
@@ -242,12 +242,15 @@ export class ToolExecutor {
|
|
|
242
242
|
const timeoutMs = Math.min(timeout || 30000, 120000);
|
|
243
243
|
|
|
244
244
|
try {
|
|
245
|
-
|
|
245
|
+
// Split command safely for shell: false — avoids shell injection
|
|
246
|
+
const parts = command.split(/\s+/);
|
|
247
|
+
const cmd = parts[0];
|
|
248
|
+
const cmdArgs = parts.slice(1);
|
|
249
|
+
const output = execFileSync(cmd, cmdArgs, {
|
|
246
250
|
cwd: execCwd,
|
|
247
|
-
encoding: 'utf8',
|
|
248
251
|
timeout: timeoutMs,
|
|
249
|
-
maxBuffer:
|
|
250
|
-
|
|
252
|
+
maxBuffer: 1024 * 1024,
|
|
253
|
+
encoding: 'utf8',
|
|
251
254
|
env: { ...process.env, GROOVE_AGENT_ID: this.agentId },
|
|
252
255
|
});
|
|
253
256
|
// Cap output to prevent context window blowup
|
|
@@ -292,6 +295,9 @@ export class ToolExecutor {
|
|
|
292
295
|
}
|
|
293
296
|
|
|
294
297
|
searchContent({ pattern, path: searchPath, glob: globFilter }) {
|
|
298
|
+
if (!/^[a-zA-Z0-9_.\-\/\\*?[\]{}()|^$+\s]+$/.test(pattern)) {
|
|
299
|
+
throw new Error('Invalid search pattern');
|
|
300
|
+
}
|
|
295
301
|
const searchDir = searchPath ? this._resolvePath(searchPath) : this.workingDir;
|
|
296
302
|
|
|
297
303
|
// Prefer ripgrep (faster, respects .gitignore), fall back to grep
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// GROOVE — Toys Registry & Launch Logic
|
|
2
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
3
|
+
|
|
4
|
+
import { readFileSync } from 'fs';
|
|
5
|
+
import { resolve, dirname } from 'path';
|
|
6
|
+
import { fileURLToPath } from 'url';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const CATALOG_PATH = resolve(__dirname, '../templates/toys-catalog.json');
|
|
10
|
+
const TOY_ID_PATTERN = /^[a-z0-9-]{1,64}$/;
|
|
11
|
+
|
|
12
|
+
export class Toys {
|
|
13
|
+
constructor(daemon) {
|
|
14
|
+
this.daemon = daemon;
|
|
15
|
+
this.catalog = JSON.parse(readFileSync(CATALOG_PATH, 'utf8'));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
list(category) {
|
|
19
|
+
if (!category) return this.catalog;
|
|
20
|
+
return this.catalog.filter((t) => t.category === category);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get(id) {
|
|
24
|
+
return this.catalog.find((t) => t.id === id) || null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async launch(id, { apiKey, starterPrompt } = {}) {
|
|
28
|
+
if (!id || typeof id !== 'string' || !TOY_ID_PATTERN.test(id)) {
|
|
29
|
+
throw new Error('Invalid toy id');
|
|
30
|
+
}
|
|
31
|
+
if (apiKey !== undefined && apiKey !== null && typeof apiKey !== 'string') {
|
|
32
|
+
throw new Error('apiKey must be a string');
|
|
33
|
+
}
|
|
34
|
+
if (starterPrompt !== undefined && starterPrompt !== null && typeof starterPrompt !== 'string') {
|
|
35
|
+
throw new Error('starterPrompt must be a string');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const toy = this.get(id);
|
|
39
|
+
if (!toy) throw new Error(`Toy not found: ${id}`);
|
|
40
|
+
|
|
41
|
+
if (apiKey) {
|
|
42
|
+
this.daemon.credentials.setKey('toy:' + id, apiKey);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const team = this.daemon.teams.create('Toy: ' + toy.name);
|
|
46
|
+
|
|
47
|
+
const plannerPrompt =
|
|
48
|
+
'You are exploring the ' + toy.name + ' API.\n'
|
|
49
|
+
+ 'Documentation: ' + toy.docsUrl + '\n'
|
|
50
|
+
+ 'Base URL: ' + toy.baseUrl + '\n'
|
|
51
|
+
+ (apiKey
|
|
52
|
+
? 'API Key: ' + apiKey + ' (auth type: ' + toy.authType + ', key goes in: ' + toy.keyHeader + ')\n'
|
|
53
|
+
: 'This API requires no authentication.\n')
|
|
54
|
+
+ 'Your first task: Use WebFetch to read the API documentation page at ' + toy.docsUrl + '. Study all available endpoints, data structures, parameters, and rate limits.\n'
|
|
55
|
+
+ 'Then present a clear summary of what this API offers and ask the user what they would like to build.\n'
|
|
56
|
+
+ (starterPrompt ? 'The user is interested in: ' + starterPrompt + '\n' : '')
|
|
57
|
+
+ 'Suggest these project ideas: ' + toy.starterPrompts.join(', ');
|
|
58
|
+
|
|
59
|
+
const agent = await this.daemon.processes.spawn({
|
|
60
|
+
role: 'planner',
|
|
61
|
+
name: 'Toy-' + toy.name.replace(/\s+/g, '-'),
|
|
62
|
+
provider: 'claude-code',
|
|
63
|
+
prompt: plannerPrompt,
|
|
64
|
+
teamId: team.id,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
return { team, agent, toyId: id };
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -56,9 +56,21 @@ export class TunnelManager {
|
|
|
56
56
|
);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
async init() {
|
|
60
|
+
for (const [id, config] of this.saved) {
|
|
61
|
+
if (config.autoConnect) {
|
|
62
|
+
try {
|
|
63
|
+
await this.connect(id);
|
|
64
|
+
} catch (err) {
|
|
65
|
+
this.daemon.broadcast({ type: 'tunnel.error', data: { id, error: err.message } });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
59
71
|
getSaved() {
|
|
60
72
|
return Array.from(this.saved.values()).map(s => ({
|
|
61
|
-
...s,
|
|
73
|
+
...this._sanitize(s),
|
|
62
74
|
active: this.active.has(s.id),
|
|
63
75
|
...(this.active.get(s.id) || {}),
|
|
64
76
|
}));
|
|
@@ -149,9 +161,9 @@ export class TunnelManager {
|
|
|
149
161
|
return merged;
|
|
150
162
|
}
|
|
151
163
|
|
|
152
|
-
delete(id) {
|
|
164
|
+
async delete(id) {
|
|
153
165
|
if (!this.saved.has(id)) throw new Error(`Remote ${id} not found`);
|
|
154
|
-
if (this.active.has(id)) this.disconnect(id);
|
|
166
|
+
if (this.active.has(id)) await this.disconnect(id);
|
|
155
167
|
const name = this.saved.get(id).name;
|
|
156
168
|
this.saved.delete(id);
|
|
157
169
|
this._save();
|
|
@@ -419,17 +431,24 @@ export class TunnelManager {
|
|
|
419
431
|
return { installed: verify.grooveInstalled, daemonRunning: verify.daemonRunning };
|
|
420
432
|
}
|
|
421
433
|
|
|
434
|
+
_sanitize(entry) {
|
|
435
|
+
if (!entry) return entry;
|
|
436
|
+
const { sshKeyPath, ...safe } = entry;
|
|
437
|
+
safe.sshKeyDisplay = sshKeyPath ? sshKeyPath.split('/').pop() : null;
|
|
438
|
+
return safe;
|
|
439
|
+
}
|
|
440
|
+
|
|
422
441
|
getStatus(id) {
|
|
423
442
|
const saved = this.saved.get(id);
|
|
424
443
|
if (!saved) return null;
|
|
425
444
|
const active = this.active.get(id);
|
|
426
|
-
return { ...saved, active: !!active, ...(active || {}) };
|
|
445
|
+
return { ...this._sanitize(saved), active: !!active, ...(active || {}) };
|
|
427
446
|
}
|
|
428
447
|
|
|
429
448
|
getActive() {
|
|
430
449
|
return Array.from(this.active.entries()).map(([id, conn]) => ({
|
|
431
450
|
...conn,
|
|
432
|
-
...(this.saved.get(id) || {}),
|
|
451
|
+
...this._sanitize(this.saved.get(id) || {}),
|
|
433
452
|
id,
|
|
434
453
|
}));
|
|
435
454
|
}
|