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.
Files changed (34) hide show
  1. package/CLAUDE.md +0 -11
  2. package/moe-training/package-lock.json +7 -4
  3. package/moe-training/package.json +1 -1
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/src/agent-loop.js +84 -36
  7. package/node_modules/@groove-dev/daemon/src/process.js +15 -1
  8. package/node_modules/@groove-dev/daemon/src/tool-executor.js +22 -11
  9. package/node_modules/@groove-dev/gui/dist/assets/{index-BJgEJ9lZ.js → index-DoEeiBhY.js} +1732 -1732
  10. package/node_modules/@groove-dev/gui/dist/assets/index-fhMxiPGp.css +1 -0
  11. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  12. package/node_modules/@groove-dev/gui/package.json +1 -1
  13. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +83 -2
  14. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +4 -2
  15. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +17 -6
  16. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +12 -1
  17. package/node_modules/@groove-dev/gui/src/stores/groove.js +28 -1
  18. package/package.json +1 -1
  19. package/packages/cli/package.json +1 -1
  20. package/packages/daemon/package.json +1 -1
  21. package/packages/daemon/src/agent-loop.js +84 -36
  22. package/packages/daemon/src/process.js +15 -1
  23. package/packages/daemon/src/tool-executor.js +22 -11
  24. package/packages/gui/dist/assets/{index-BJgEJ9lZ.js → index-DoEeiBhY.js} +1732 -1732
  25. package/packages/gui/dist/assets/index-fhMxiPGp.css +1 -0
  26. package/packages/gui/dist/index.html +2 -2
  27. package/packages/gui/package.json +1 -1
  28. package/packages/gui/src/components/chat/chat-header.jsx +83 -2
  29. package/packages/gui/src/components/chat/chat-input.jsx +4 -2
  30. package/packages/gui/src/components/chat/chat-messages.jsx +17 -6
  31. package/packages/gui/src/components/chat/chat-view.jsx +12 -1
  32. package/packages/gui/src/stores/groove.js +28 -1
  33. package/node_modules/@groove-dev/gui/dist/assets/index-kbR5tOHu.css +0 -1
  34. 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": "^11.0.0",
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": "11.10.0",
58
- "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.10.0.tgz",
59
- "integrity": "sha512-EwhOpyXiOEL/lKzHz9AW1msWFNzGc/z+LzeB3/jnFJpxu+th2yqvzsSWas1v9jgs9+xiXJcD5A8CJxAG2TaghQ==",
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": {
@@ -13,7 +13,7 @@
13
13
  "start:server": "node server/index.js"
14
14
  },
15
15
  "dependencies": {
16
- "better-sqlite3": "^11.0.0",
16
+ "better-sqlite3": "^12.9.0",
17
17
  "uuid": "^9.0.0",
18
18
  "express": "^4.18.0"
19
19
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.81",
3
+ "version": "0.27.84",
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.81",
3
+ "version": "0.27.84",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -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: 'text', data: content });
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: `${toolName}: ${inputSummary}` });
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: result.success ? `${toolName}: done` : `${toolName}: error ${result.error}`,
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
- let response;
235
- try {
236
- response = await fetch(url, {
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
- if (!response.ok) {
254
- const text = await response.text().catch(() => '');
255
- const errMsg = `API error ${response.status}: ${text.slice(0, 500)}`;
256
- this._writeLog({ type: 'error', text: errMsg });
257
- this.emit('error', { message: errMsg });
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
- // Parse streaming or non-streaming response
262
- if (body.stream) {
263
- return this._parseSSE(response);
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
- return this._parseJSON(response);
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, execFileSync } from 'child_process';
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._resolvePath(filePath);
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
- // 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, {
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._resolvePath(cwd) : this.workingDir;
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._resolvePath(searchPath) : this.workingDir;
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._resolvePath(dirPath) : this.workingDir;
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 || '.'}` };