groove-dev 0.27.81 → 0.27.84
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/CLAUDE.md +0 -11
- package/moe-training/package-lock.json +7 -4
- package/moe-training/package.json +1 -1
- 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/agent-loop.js +84 -36
- package/node_modules/@groove-dev/daemon/src/process.js +15 -1
- package/node_modules/@groove-dev/daemon/src/tool-executor.js +22 -11
- package/node_modules/@groove-dev/gui/dist/assets/{index-BJgEJ9lZ.js → index-DoEeiBhY.js} +1732 -1732
- package/node_modules/@groove-dev/gui/dist/assets/index-fhMxiPGp.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +83 -2
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +4 -2
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +17 -6
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +12 -1
- package/node_modules/@groove-dev/gui/src/stores/groove.js +28 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/agent-loop.js +84 -36
- package/packages/daemon/src/process.js +15 -1
- package/packages/daemon/src/tool-executor.js +22 -11
- package/packages/gui/dist/assets/{index-BJgEJ9lZ.js → index-DoEeiBhY.js} +1732 -1732
- package/packages/gui/dist/assets/index-fhMxiPGp.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/chat/chat-header.jsx +83 -2
- package/packages/gui/src/components/chat/chat-input.jsx +4 -2
- package/packages/gui/src/components/chat/chat-messages.jsx +17 -6
- package/packages/gui/src/components/chat/chat-view.jsx +12 -1
- package/packages/gui/src/stores/groove.js +28 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-kbR5tOHu.css +0 -1
- package/packages/gui/dist/assets/index-kbR5tOHu.css +0 -1
package/CLAUDE.md
CHANGED
|
@@ -263,14 +263,3 @@ Audit-driven release. Multi-agent orchestration system with 7 coordination layer
|
|
|
263
263
|
- Dashboard: routing donut, cache panel, context health gauges
|
|
264
264
|
- Monitor/QC agent mode (stay active, loop)
|
|
265
265
|
- Distribution: demo video, HN launch, Twitter content
|
|
266
|
-
|
|
267
|
-
<!-- GROOVE:START -->
|
|
268
|
-
## GROOVE Orchestration (auto-injected)
|
|
269
|
-
Active agents: 2
|
|
270
|
-
| Name | Role | Scope |
|
|
271
|
-
|------|------|-------|
|
|
272
|
-
| planner-4 | planner | - |
|
|
273
|
-
| planner-7 | planner | - |
|
|
274
|
-
See AGENTS_REGISTRY.md for full agent state.
|
|
275
|
-
**Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
|
|
276
|
-
<!-- GROOVE:END -->
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
"version": "0.1.0",
|
|
10
10
|
"license": "FSL-1.1-Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
|
-
"better-sqlite3": "^
|
|
12
|
+
"better-sqlite3": "^12.9.0",
|
|
13
13
|
"express": "^4.18.0",
|
|
14
14
|
"uuid": "^9.0.0"
|
|
15
15
|
}
|
|
@@ -54,14 +54,17 @@
|
|
|
54
54
|
"license": "MIT"
|
|
55
55
|
},
|
|
56
56
|
"node_modules/better-sqlite3": {
|
|
57
|
-
"version": "
|
|
58
|
-
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-
|
|
59
|
-
"integrity": "sha512-
|
|
57
|
+
"version": "12.9.0",
|
|
58
|
+
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.9.0.tgz",
|
|
59
|
+
"integrity": "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==",
|
|
60
60
|
"hasInstallScript": true,
|
|
61
61
|
"license": "MIT",
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"bindings": "^1.5.0",
|
|
64
64
|
"prebuild-install": "^7.1.1"
|
|
65
|
+
},
|
|
66
|
+
"engines": {
|
|
67
|
+
"node": "20.x || 22.x || 23.x || 24.x || 25.x"
|
|
65
68
|
}
|
|
66
69
|
},
|
|
67
70
|
"node_modules/bindings": {
|
|
@@ -36,6 +36,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
36
36
|
agent.workingDir || daemon.projectDir,
|
|
37
37
|
daemon,
|
|
38
38
|
agent.id,
|
|
39
|
+
daemon.projectDir,
|
|
39
40
|
);
|
|
40
41
|
|
|
41
42
|
// Session persistence
|
|
@@ -57,6 +58,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
57
58
|
|
|
58
59
|
async start(initialPrompt) {
|
|
59
60
|
this.running = true;
|
|
61
|
+
this.isInitialPrompt = true;
|
|
60
62
|
this._writeLog({ type: 'system', event: 'start', model: this.config.model });
|
|
61
63
|
|
|
62
64
|
if (initialPrompt) {
|
|
@@ -78,6 +80,19 @@ export class AgentLoop extends EventEmitter {
|
|
|
78
80
|
this.emit('error', { message: err.message });
|
|
79
81
|
}
|
|
80
82
|
|
|
83
|
+
if (this.running && this.isInitialPrompt && this._shouldAutoComplete()) {
|
|
84
|
+
this.running = false;
|
|
85
|
+
const duration = Date.now() - this.startedAt;
|
|
86
|
+
this.daemon.tokens.recordResult(this.agent.id, { durationMs: duration, turns: this.turns });
|
|
87
|
+
this.emit('exit', { code: 0, signal: null, status: 'completed' });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (this.running && this.isInitialPrompt && this.turns <= 1 && this.totalTokensIn === 0 && this.totalTokensOut === 0) {
|
|
91
|
+
this.running = false;
|
|
92
|
+
this.emit('exit', { code: 1, signal: null, status: 'crashed' });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.isInitialPrompt = false;
|
|
81
96
|
this._saveSession();
|
|
82
97
|
this.idle = true;
|
|
83
98
|
}
|
|
@@ -142,14 +157,14 @@ export class AgentLoop extends EventEmitter {
|
|
|
142
157
|
if (content) {
|
|
143
158
|
this._writeLog({ type: 'assistant', content: content.slice(0, 2000) });
|
|
144
159
|
}
|
|
145
|
-
this.emit('output', { type: 'result', data: content || 'Turn complete', turns: this.turns });
|
|
160
|
+
this.emit('output', { type: 'result', subtype: 'assistant', data: content || 'Turn complete', turns: this.turns });
|
|
146
161
|
break;
|
|
147
162
|
}
|
|
148
163
|
|
|
149
164
|
// Has tool calls — broadcast text before executing tools (if model sent text + tools)
|
|
150
165
|
if (content) {
|
|
151
166
|
this._writeLog({ type: 'assistant', content: content.slice(0, 2000) });
|
|
152
|
-
this.emit('output', { type: 'activity', subtype: '
|
|
167
|
+
this.emit('output', { type: 'activity', subtype: 'assistant', data: content });
|
|
153
168
|
}
|
|
154
169
|
|
|
155
170
|
// Execute each tool call
|
|
@@ -168,7 +183,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
168
183
|
|
|
169
184
|
// Log + broadcast tool invocation
|
|
170
185
|
this._writeLog({ type: 'tool_use', tool: toolName, input: inputSummary });
|
|
171
|
-
this.emit('output', { type: 'activity', subtype: 'tool_use', data:
|
|
186
|
+
this.emit('output', { type: 'activity', subtype: 'tool_use', data: [{ type: 'tool_use', name: toolName, input: args }] });
|
|
172
187
|
|
|
173
188
|
// Feed classifier for adaptive routing
|
|
174
189
|
this.daemon.classifier.addEvent(this.agent.id, {
|
|
@@ -188,7 +203,7 @@ export class AgentLoop extends EventEmitter {
|
|
|
188
203
|
});
|
|
189
204
|
this.emit('output', {
|
|
190
205
|
type: 'activity', subtype: 'tool_result',
|
|
191
|
-
data:
|
|
206
|
+
data: [{ type: 'tool_result', name: toolName, success: result.success, output: resultPreview }],
|
|
192
207
|
});
|
|
193
208
|
|
|
194
209
|
if (!result.success) {
|
|
@@ -209,11 +224,15 @@ export class AgentLoop extends EventEmitter {
|
|
|
209
224
|
}
|
|
210
225
|
}
|
|
211
226
|
|
|
227
|
+
_shouldAutoComplete() {
|
|
228
|
+
const lastAssistant = [...this.messages].reverse().find(m => m.role === 'assistant');
|
|
229
|
+
if (!lastAssistant) return false;
|
|
230
|
+
return lastAssistant.content && (!lastAssistant.tool_calls || lastAssistant.tool_calls.length === 0) && this.turns >= 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
212
233
|
// --- API Communication ---
|
|
213
234
|
|
|
214
235
|
async _callApi() {
|
|
215
|
-
this.abortController = new AbortController();
|
|
216
|
-
|
|
217
236
|
const body = {
|
|
218
237
|
model: this.config.model,
|
|
219
238
|
messages: this.messages,
|
|
@@ -223,46 +242,75 @@ export class AgentLoop extends EventEmitter {
|
|
|
223
242
|
max_tokens: this.config.maxResponseTokens || 4096,
|
|
224
243
|
};
|
|
225
244
|
|
|
226
|
-
// Streaming for real-time output
|
|
227
245
|
if (this.config.stream !== false) {
|
|
228
246
|
body.stream = true;
|
|
229
247
|
body.stream_options = { include_usage: true };
|
|
230
248
|
}
|
|
231
249
|
|
|
232
250
|
const url = `${this.config.apiBase}/chat/completions`;
|
|
251
|
+
const maxRetries = 3;
|
|
252
|
+
let lastError = null;
|
|
253
|
+
|
|
254
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
255
|
+
this.abortController = new AbortController();
|
|
256
|
+
|
|
257
|
+
let response;
|
|
258
|
+
try {
|
|
259
|
+
response = await fetch(url, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: {
|
|
262
|
+
'Content-Type': 'application/json',
|
|
263
|
+
...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {}),
|
|
264
|
+
...this.config.headers,
|
|
265
|
+
},
|
|
266
|
+
body: JSON.stringify(body),
|
|
267
|
+
signal: this.abortController.signal,
|
|
268
|
+
});
|
|
269
|
+
} catch (err) {
|
|
270
|
+
if (err.name === 'AbortError') return null;
|
|
271
|
+
lastError = `Inference API unreachable: ${err.message}`;
|
|
272
|
+
if (attempt < 2) {
|
|
273
|
+
this._writeLog({ type: 'retry', attempt: attempt + 1, reason: lastError, delayMs: 2000 });
|
|
274
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
this._writeLog({ type: 'error', text: `API request failed: ${err.message}` });
|
|
278
|
+
this.emit('error', { message: lastError });
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
233
281
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
method: 'POST',
|
|
238
|
-
headers: {
|
|
239
|
-
'Content-Type': 'application/json',
|
|
240
|
-
...(this.config.apiKey ? { Authorization: `Bearer ${this.config.apiKey}` } : {}),
|
|
241
|
-
...this.config.headers,
|
|
242
|
-
},
|
|
243
|
-
body: JSON.stringify(body),
|
|
244
|
-
signal: this.abortController.signal,
|
|
245
|
-
});
|
|
246
|
-
} catch (err) {
|
|
247
|
-
if (err.name === 'AbortError') return null;
|
|
248
|
-
this._writeLog({ type: 'error', text: `API request failed: ${err.message}` });
|
|
249
|
-
this.emit('error', { message: `Inference API unreachable: ${err.message}` });
|
|
250
|
-
return null;
|
|
251
|
-
}
|
|
282
|
+
if (!response.ok) {
|
|
283
|
+
const text = await response.text().catch(() => '');
|
|
284
|
+
const errMsg = `API error ${response.status}: ${text.slice(0, 500)}`;
|
|
252
285
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
return null;
|
|
259
|
-
}
|
|
286
|
+
if (response.status === 401 || response.status === 403) {
|
|
287
|
+
this._writeLog({ type: 'error', text: errMsg });
|
|
288
|
+
this.emit('error', { message: errMsg });
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
260
291
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
292
|
+
if ((response.status === 429 || response.status === 503) && attempt < maxRetries) {
|
|
293
|
+
const delay = Math.pow(2, attempt + 1) * 1000;
|
|
294
|
+
this._writeLog({ type: 'retry', attempt: attempt + 1, reason: errMsg, delayMs: delay });
|
|
295
|
+
await new Promise(r => setTimeout(r, delay));
|
|
296
|
+
lastError = errMsg;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this._writeLog({ type: 'error', text: errMsg });
|
|
301
|
+
this.emit('error', { message: errMsg });
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (body.stream) {
|
|
306
|
+
return this._parseSSE(response);
|
|
307
|
+
}
|
|
308
|
+
return this._parseJSON(response);
|
|
264
309
|
}
|
|
265
|
-
|
|
310
|
+
|
|
311
|
+
this._writeLog({ type: 'error', text: `API failed after retries: ${lastError}` });
|
|
312
|
+
this.emit('error', { message: lastError });
|
|
313
|
+
return null;
|
|
266
314
|
}
|
|
267
315
|
|
|
268
316
|
async _parseSSE(response) {
|
|
@@ -454,6 +454,14 @@ export class ProcessManager {
|
|
|
454
454
|
);
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
+
// Validate explicit model against provider's supported models
|
|
458
|
+
if (config.model && config.model !== 'auto' && provider.constructor.models) {
|
|
459
|
+
const valid = provider.constructor.models.some(m => m.id === config.model);
|
|
460
|
+
if (!valid) {
|
|
461
|
+
throw new Error(`Model '${config.model}' is not available for ${provider.constructor.displayName}. Available: ${provider.constructor.models.map(m => m.id).join(', ')}`);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
457
465
|
// Resolve auto model routing before registering
|
|
458
466
|
// Treat missing/null/empty model as 'auto' — GUI sends empty string for "Auto" option
|
|
459
467
|
let resolvedModel = config.model;
|
|
@@ -753,12 +761,15 @@ For normal file edits within your scope, proceed without review.
|
|
|
753
761
|
}
|
|
754
762
|
});
|
|
755
763
|
|
|
756
|
-
// Wire errors — broadcast to GUI for display
|
|
764
|
+
// Wire errors — broadcast to GUI for display, crash on fatal API errors
|
|
757
765
|
loop.on('error', ({ message }) => {
|
|
758
766
|
this.daemon.broadcast({
|
|
759
767
|
type: 'agent:output', agentId: agent.id,
|
|
760
768
|
data: { type: 'activity', subtype: 'error', data: message },
|
|
761
769
|
});
|
|
770
|
+
if ((message.includes('API error 4') && !message.includes('API error 429')) || message.includes('Inference API unreachable')) {
|
|
771
|
+
loop.stop();
|
|
772
|
+
}
|
|
762
773
|
});
|
|
763
774
|
|
|
764
775
|
// Start the agent loop with the fully assembled prompt
|
|
@@ -1833,6 +1844,9 @@ For normal file edits within your scope, proceed without review.
|
|
|
1833
1844
|
type: 'agent:output', agentId: newAgent.id,
|
|
1834
1845
|
data: { type: 'activity', subtype: 'error', data: errMsg },
|
|
1835
1846
|
});
|
|
1847
|
+
if ((errMsg.includes('API error 4') && !errMsg.includes('API error 429')) || errMsg.includes('Inference API unreachable')) {
|
|
1848
|
+
loop.stop();
|
|
1849
|
+
}
|
|
1836
1850
|
});
|
|
1837
1851
|
|
|
1838
1852
|
loop.start();
|
|
@@ -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
|
|
5
|
+
import { execSync } from 'child_process';
|
|
6
6
|
import { resolve, relative, dirname, sep } from 'path';
|
|
7
7
|
import { minimatch } from 'minimatch';
|
|
8
8
|
|
|
@@ -120,10 +120,11 @@ export const TOOL_DEFINITIONS = [
|
|
|
120
120
|
];
|
|
121
121
|
|
|
122
122
|
export class ToolExecutor {
|
|
123
|
-
constructor(workingDir, daemon, agentId) {
|
|
123
|
+
constructor(workingDir, daemon, agentId, projectDir) {
|
|
124
124
|
this.workingDir = resolve(workingDir);
|
|
125
125
|
this.daemon = daemon;
|
|
126
126
|
this.agentId = agentId;
|
|
127
|
+
this.projectDir = resolve(projectDir || workingDir);
|
|
127
128
|
}
|
|
128
129
|
|
|
129
130
|
async execute(name, args) {
|
|
@@ -155,6 +156,15 @@ export class ToolExecutor {
|
|
|
155
156
|
return resolved;
|
|
156
157
|
}
|
|
157
158
|
|
|
159
|
+
_resolveReadPath(filePath) {
|
|
160
|
+
if (!filePath) throw new Error('Path is required');
|
|
161
|
+
const resolved = resolve(this.workingDir, filePath);
|
|
162
|
+
if (!resolved.startsWith(this.projectDir + sep) && resolved !== this.projectDir) {
|
|
163
|
+
throw new Error(`Access denied: path outside project directory`);
|
|
164
|
+
}
|
|
165
|
+
return resolved;
|
|
166
|
+
}
|
|
167
|
+
|
|
158
168
|
_checkWriteScope(resolvedPath) {
|
|
159
169
|
if (!this.daemon?.locks) return;
|
|
160
170
|
const rel = relative(this.workingDir, resolvedPath);
|
|
@@ -171,7 +181,7 @@ export class ToolExecutor {
|
|
|
171
181
|
// --- Tool Implementations ---
|
|
172
182
|
|
|
173
183
|
readFile({ path: filePath, offset, limit }) {
|
|
174
|
-
const resolved = this.
|
|
184
|
+
const resolved = this._resolveReadPath(filePath);
|
|
175
185
|
if (!existsSync(resolved)) {
|
|
176
186
|
return { success: false, error: `File not found: ${filePath}` };
|
|
177
187
|
}
|
|
@@ -241,16 +251,17 @@ export class ToolExecutor {
|
|
|
241
251
|
const execCwd = cwd ? this._resolvePath(cwd) : this.workingDir;
|
|
242
252
|
const timeoutMs = Math.min(timeout || 30000, 120000);
|
|
243
253
|
|
|
254
|
+
if (/\brm\s+(-\w+\s+)*-rf\s+(\/|~)/.test(command) || /\bsudo\b/.test(command)) {
|
|
255
|
+
return { success: false, error: 'Command blocked: dangerous shell pattern detected' };
|
|
256
|
+
}
|
|
257
|
+
|
|
244
258
|
try {
|
|
245
|
-
|
|
246
|
-
const parts = command.split(/\s+/);
|
|
247
|
-
const cmd = parts[0];
|
|
248
|
-
const cmdArgs = parts.slice(1);
|
|
249
|
-
const output = execFileSync(cmd, cmdArgs, {
|
|
259
|
+
const output = execSync(command, {
|
|
250
260
|
cwd: execCwd,
|
|
251
261
|
timeout: timeoutMs,
|
|
252
262
|
maxBuffer: 1024 * 1024,
|
|
253
263
|
encoding: 'utf8',
|
|
264
|
+
shell: true,
|
|
254
265
|
env: { ...process.env, GROOVE_AGENT_ID: this.agentId },
|
|
255
266
|
});
|
|
256
267
|
// Cap output to prevent context window blowup
|
|
@@ -265,7 +276,7 @@ export class ToolExecutor {
|
|
|
265
276
|
}
|
|
266
277
|
|
|
267
278
|
searchFiles({ pattern, cwd }) {
|
|
268
|
-
const searchDir = cwd ? this.
|
|
279
|
+
const searchDir = cwd ? this._resolveReadPath(cwd) : this.workingDir;
|
|
269
280
|
const results = [];
|
|
270
281
|
|
|
271
282
|
const walk = (dir, depth) => {
|
|
@@ -298,7 +309,7 @@ export class ToolExecutor {
|
|
|
298
309
|
if (!/^[a-zA-Z0-9_.\-\/\\*?[\]{}()|^$+\s]+$/.test(pattern)) {
|
|
299
310
|
throw new Error('Invalid search pattern');
|
|
300
311
|
}
|
|
301
|
-
const searchDir = searchPath ? this.
|
|
312
|
+
const searchDir = searchPath ? this._resolveReadPath(searchPath) : this.workingDir;
|
|
302
313
|
|
|
303
314
|
// Prefer ripgrep (faster, respects .gitignore), fall back to grep
|
|
304
315
|
const escapedPattern = pattern.replace(/'/g, "'\\''");
|
|
@@ -342,7 +353,7 @@ export class ToolExecutor {
|
|
|
342
353
|
}
|
|
343
354
|
|
|
344
355
|
listDirectory({ path: dirPath } = {}) {
|
|
345
|
-
const resolved = dirPath ? this.
|
|
356
|
+
const resolved = dirPath ? this._resolveReadPath(dirPath) : this.workingDir;
|
|
346
357
|
|
|
347
358
|
if (!existsSync(resolved)) {
|
|
348
359
|
return { success: false, error: `Directory not found: ${dirPath || '.'}` };
|