groove-dev 0.27.81 → 0.27.85
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 +38 -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-BbDDgazC.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 +24 -11
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +13 -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 +38 -1
- package/packages/daemon/src/tool-executor.js +22 -11
- package/packages/gui/dist/assets/{index-BJgEJ9lZ.js → index-BbDDgazC.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 +24 -11
- package/packages/gui/src/components/chat/chat-view.jsx +13 -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/spash-page.png +0 -0
- package/ui.png +0 -0
- package/welcome.png +0 -0
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;
|
|
@@ -731,6 +739,22 @@ For normal file edits within your scope, proceed without review.
|
|
|
731
739
|
});
|
|
732
740
|
}
|
|
733
741
|
|
|
742
|
+
if (this.daemon.trajectoryCapture) {
|
|
743
|
+
try {
|
|
744
|
+
if (status === 'completed') {
|
|
745
|
+
this.daemon.trajectoryCapture.onAgentComplete(agent.id, {
|
|
746
|
+
status: 'SUCCESS', exit_code: code || 0, signal,
|
|
747
|
+
});
|
|
748
|
+
} else {
|
|
749
|
+
this.daemon.trajectoryCapture.onAgentCrash(agent.id,
|
|
750
|
+
signal ? 'Killed by signal ' + signal : 'Exit status ' + status
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
|
|
754
|
+
this.daemon.state.set('training_sessions_captured', count);
|
|
755
|
+
} catch (e) { /* fail silent */ }
|
|
756
|
+
}
|
|
757
|
+
|
|
734
758
|
this.daemon.broadcast({ type: 'agent:exit', agentId: agent.id, code: code || 0, signal, status });
|
|
735
759
|
if (this.daemon.integrations) this.daemon.integrations.refreshMcpJson();
|
|
736
760
|
if (status === 'completed' && this.daemon.journalist) {
|
|
@@ -753,12 +777,15 @@ For normal file edits within your scope, proceed without review.
|
|
|
753
777
|
}
|
|
754
778
|
});
|
|
755
779
|
|
|
756
|
-
// Wire errors — broadcast to GUI for display
|
|
780
|
+
// Wire errors — broadcast to GUI for display, crash on fatal API errors
|
|
757
781
|
loop.on('error', ({ message }) => {
|
|
758
782
|
this.daemon.broadcast({
|
|
759
783
|
type: 'agent:output', agentId: agent.id,
|
|
760
784
|
data: { type: 'activity', subtype: 'error', data: message },
|
|
761
785
|
});
|
|
786
|
+
if ((message.includes('API error 4') && !message.includes('API error 429')) || message.includes('Inference API unreachable')) {
|
|
787
|
+
loop.stop();
|
|
788
|
+
}
|
|
762
789
|
});
|
|
763
790
|
|
|
764
791
|
// Start the agent loop with the fully assembled prompt
|
|
@@ -1833,6 +1860,9 @@ For normal file edits within your scope, proceed without review.
|
|
|
1833
1860
|
type: 'agent:output', agentId: newAgent.id,
|
|
1834
1861
|
data: { type: 'activity', subtype: 'error', data: errMsg },
|
|
1835
1862
|
});
|
|
1863
|
+
if ((errMsg.includes('API error 4') && !errMsg.includes('API error 429')) || errMsg.includes('Inference API unreachable')) {
|
|
1864
|
+
loop.stop();
|
|
1865
|
+
}
|
|
1836
1866
|
});
|
|
1837
1867
|
|
|
1838
1868
|
loop.start();
|
|
@@ -1874,6 +1904,13 @@ For normal file edits within your scope, proceed without review.
|
|
|
1874
1904
|
if (loop) {
|
|
1875
1905
|
await loop.stop();
|
|
1876
1906
|
this.handles.delete(agentId);
|
|
1907
|
+
if (this.daemon.trajectoryCapture) {
|
|
1908
|
+
try {
|
|
1909
|
+
this.daemon.trajectoryCapture.onAgentComplete(agentId, { status: 'STOPPED' });
|
|
1910
|
+
const count = (this.daemon.state.get('training_sessions_captured') || 0) + 1;
|
|
1911
|
+
this.daemon.state.set('training_sessions_captured', count);
|
|
1912
|
+
} catch (e) { /* fail silent */ }
|
|
1913
|
+
}
|
|
1877
1914
|
this.daemon.registry.update(agentId, { status: 'stopped', pid: null });
|
|
1878
1915
|
return;
|
|
1879
1916
|
}
|
|
@@ -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 || '.'}` };
|