groove-dev 0.27.100 → 0.27.102

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 (38) hide show
  1. package/CLAUDE.md +7 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +33 -7
  5. package/node_modules/@groove-dev/daemon/src/gateways/manager.js +2 -2
  6. package/node_modules/@groove-dev/daemon/src/preview.js +148 -2
  7. package/node_modules/@groove-dev/daemon/src/process.js +34 -2
  8. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +10 -4
  9. package/node_modules/@groove-dev/gui/dist/assets/{index-CuFxAnNE.js → index-8gdXdRnq.js} +1743 -1743
  10. package/node_modules/@groove-dev/gui/dist/assets/index-C1ObKizg.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/agents/agent-file-tree.jsx +199 -14
  14. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +19 -6
  15. package/package.json +1 -1
  16. package/packages/cli/package.json +1 -1
  17. package/packages/daemon/package.json +1 -1
  18. package/packages/daemon/src/api.js +33 -7
  19. package/packages/daemon/src/gateways/manager.js +2 -2
  20. package/packages/daemon/src/preview.js +148 -2
  21. package/packages/daemon/src/process.js +34 -2
  22. package/packages/daemon/src/providers/gemini.js +10 -4
  23. package/packages/gui/dist/assets/{index-CuFxAnNE.js → index-8gdXdRnq.js} +1743 -1743
  24. package/packages/gui/dist/assets/index-C1ObKizg.css +1 -0
  25. package/packages/gui/dist/index.html +2 -2
  26. package/packages/gui/package.json +1 -1
  27. package/packages/gui/src/components/agents/agent-file-tree.jsx +199 -14
  28. package/packages/gui/src/components/agents/workspace-mode.jsx +19 -6
  29. package/packages/launch-page/dist/assets/index-Bo186ysq.js +4180 -0
  30. package/packages/launch-page/dist/assets/index-CP4c4yxe.css +1 -0
  31. package/packages/launch-page/dist/index.html +2 -2
  32. package/packages/launch-page/src/App.css +438 -137
  33. package/packages/launch-page/src/App.tsx +171 -123
  34. package/packages/launch-page/src/index.css +9 -2
  35. package/node_modules/@groove-dev/gui/dist/assets/index-BvAGbs8U.css +0 -1
  36. package/packages/gui/dist/assets/index-BvAGbs8U.css +0 -1
  37. package/packages/launch-page/dist/assets/index-BK3nAvHG.js +0 -4180
  38. package/packages/launch-page/dist/assets/index-jrLVZW5U.css +0 -2
package/CLAUDE.md CHANGED
@@ -263,3 +263,10 @@ 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: 0
270
+ See AGENTS_REGISTRY.md for full agent state.
271
+ **Memory policy:** GROOVE manages project memory automatically. Do not read or write MEMORY.md or .groove/memory/ files directly.
272
+ <!-- GROOVE:END -->
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.100",
3
+ "version": "0.27.102",
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.100",
3
+ "version": "0.27.102",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -174,10 +174,10 @@ export function createApi(app, daemon) {
174
174
  const agent = daemon.registry.get(req.params.id);
175
175
  if (!agent) return res.status(404).json({ error: 'Agent not found' });
176
176
 
177
- const isAlive = agent.status === 'running' || agent.status === 'starting';
178
- if (isAlive) {
179
- await daemon.processes.kill(req.params.id);
180
- }
177
+ // Always attempt kill handles race where GUI sees 'running' but daemon
178
+ // already marked the agent completed (common with fast non-interactive
179
+ // providers like Gemini). processes.kill() is a no-op when no handle exists.
180
+ await daemon.processes.kill(req.params.id);
181
181
 
182
182
  // Only purge from registry when explicitly requested.
183
183
  // Killed/completed agents stay visible so the user can review output.
@@ -1519,6 +1519,30 @@ export function createApi(app, daemon) {
1519
1519
  return res.json(newAgent);
1520
1520
  }
1521
1521
 
1522
+ // Non-interactive CLI providers (e.g. Gemini): respawn with the new
1523
+ // message as the prompt, preserving original introContext. These providers
1524
+ // run one prompt per spawn and cannot resume sessions.
1525
+ if (provider?.constructor?.nonInteractive && !daemon.processes.isRunning(req.params.id)) {
1526
+ const oldConfig = { ...agent };
1527
+ daemon.registry.remove(req.params.id);
1528
+ daemon.locks.release(req.params.id);
1529
+
1530
+ const newAgent = await daemon.processes.spawn({
1531
+ role: oldConfig.role,
1532
+ scope: oldConfig.scope,
1533
+ provider: oldConfig.provider,
1534
+ model: oldConfig.model,
1535
+ prompt: message.trim(),
1536
+ introContext: oldConfig.introContext,
1537
+ permission: oldConfig.permission || 'full',
1538
+ workingDir: oldConfig.workingDir,
1539
+ name: oldConfig.name,
1540
+ teamId: oldConfig.teamId,
1541
+ });
1542
+ daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
1543
+ return res.json(newAgent);
1544
+ }
1545
+
1522
1546
  // Running CLI agent (no loop) — queue the message for delivery after
1523
1547
  // the current task completes instead of killing and respawning.
1524
1548
  if (daemon.processes.isRunning(req.params.id)) {
@@ -3364,6 +3388,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3364
3388
  // Resolve base directory from the planner that wrote the file, not the daemon root
3365
3389
  const plannerAgent = found.agentId ? daemon.registry.get(found.agentId) : null;
3366
3390
  const baseDir = plannerAgent?.workingDir || daemon.config?.defaultWorkingDir || daemon.projectDir;
3391
+ const plannerProvider = plannerAgent?.provider || undefined;
3367
3392
 
3368
3393
  // Use the planner's teamId so launched agents join the correct team.
3369
3394
  // Priority: explicit from frontend > agent that wrote the file > most recent planner > default
@@ -3424,6 +3449,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3424
3449
  phase2 = [{
3425
3450
  name: 'qc-agent',
3426
3451
  role: 'fullstack', phase: 2, scope: [],
3452
+ provider: teamProvider || plannerProvider || daemon.config?.defaultProvider || undefined,
3427
3453
  prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, and verify the project builds cleanly (npm run build). Do NOT start long-running dev servers — just verify the build succeeds. Commit all changes. IMPORTANT: Do NOT delete files from other projects or directories outside this project.',
3428
3454
  }];
3429
3455
  }
@@ -3476,7 +3502,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3476
3502
  role: existing.role,
3477
3503
  scope: normalizeScope(config.scope || existing.scope || [], existing.workingDir || projectWorkingDir),
3478
3504
  prompt,
3479
- provider: config.provider || daemon.config?.defaultProvider || existing.provider || undefined,
3505
+ provider: config.provider || plannerProvider || daemon.config?.defaultProvider || existing.provider || undefined,
3480
3506
  model: config.model || existing.model || daemon.config?.defaultModel || 'auto',
3481
3507
  permission: config.permission || existing.permission || 'auto',
3482
3508
  workingDir: existing.workingDir || projectWorkingDir,
@@ -3501,7 +3527,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3501
3527
  role: config.role,
3502
3528
  scope: normalizeScope(config.scope || [], config.workingDir || projectWorkingDir),
3503
3529
  prompt,
3504
- provider: config.provider || daemon.config?.defaultProvider || undefined,
3530
+ provider: config.provider || plannerProvider || daemon.config?.defaultProvider || undefined,
3505
3531
  model: config.model || daemon.config?.defaultModel || 'auto',
3506
3532
  permission: config.permission || 'auto',
3507
3533
  workingDir: config.workingDir || projectWorkingDir,
@@ -3544,7 +3570,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
3544
3570
  waitFor: phase1Ids,
3545
3571
  agents: phase2.map((c) => ({
3546
3572
  role: c.role, scope: c.scope || [], prompt: c.prompt || '',
3547
- provider: c.provider || daemon.config?.defaultProvider || undefined, model: c.model || daemon.config?.defaultModel || 'auto',
3573
+ provider: c.provider || plannerProvider || daemon.config?.defaultProvider || undefined, model: c.model || daemon.config?.defaultModel || 'auto',
3548
3574
  permission: c.permission || 'auto',
3549
3575
  reasoningEffort: c.reasoningEffort, temperature: c.temperature, verbosity: c.verbosity,
3550
3576
  workingDir: c.workingDir || projectWorkingDir,
@@ -799,7 +799,7 @@ export class GatewayManager {
799
799
  role: config.role,
800
800
  scope: config.scope || [],
801
801
  prompt: config.prompt || '',
802
- provider: config.provider || 'claude-code',
802
+ provider: config.provider || this.daemon.config?.defaultProvider || 'claude-code',
803
803
  model: config.model || 'auto',
804
804
  permission: config.permission || 'auto',
805
805
  workingDir: config.workingDir || defaultDir,
@@ -833,7 +833,7 @@ export class GatewayManager {
833
833
  waitFor: phase1Ids,
834
834
  agents: phase2.map((c) => ({
835
835
  role: c.role, scope: c.scope || [], prompt: c.prompt || '',
836
- provider: c.provider || 'claude-code', model: c.model || 'auto',
836
+ provider: c.provider || this.daemon.config?.defaultProvider || 'claude-code', model: c.model || 'auto',
837
837
  permission: c.permission || 'auto',
838
838
  workingDir: c.workingDir || defaultDir,
839
839
  name: c.name || undefined,
@@ -11,7 +11,7 @@
11
11
  // kills the previous one. Previews are also killed on team delete and on
12
12
  // daemon shutdown.
13
13
 
14
- import { spawn as cpSpawn } from 'child_process';
14
+ import { spawn as cpSpawn, execSync } from 'child_process';
15
15
  import { resolve, extname } from 'path';
16
16
  import { existsSync, readFileSync, statSync } from 'fs';
17
17
  import { createServer } from 'http';
@@ -101,11 +101,55 @@ export class PreviewService {
101
101
  return result;
102
102
  }
103
103
 
104
+ const installResult = this._ensureDependencies(teamId, baseDir);
105
+ if (installResult?.failed) {
106
+ this.daemon.audit?.log('preview.failed', { teamId, reason: installResult.reason });
107
+ return { launched: false, reason: installResult.reason };
108
+ }
109
+
104
110
  let result;
105
111
  if (preview.kind === 'static-html') {
106
- result = await this._launchStatic(teamId, baseDir, preview);
112
+ if (this._needsBuild(baseDir, preview)) {
113
+ const buildResult = this._runBuild(teamId, baseDir);
114
+ if (buildResult?.failed) {
115
+ this.daemon.audit?.log('preview.failed', { teamId, reason: buildResult.reason });
116
+ return { launched: false, reason: buildResult.reason };
117
+ }
118
+ const distDir = resolve(baseDir, 'dist');
119
+ if (existsSync(distDir)) {
120
+ result = await this._launchStatic(teamId, distDir, { ...preview, openPath: preview.openPath || 'index.html' });
121
+ } else {
122
+ result = await this._launchStatic(teamId, baseDir, preview);
123
+ }
124
+ } else {
125
+ result = await this._launchStatic(teamId, baseDir, preview);
126
+ }
107
127
  } else if (preview.kind === 'dev-server') {
128
+ if (this._needsPreBuild(baseDir)) {
129
+ const preBuild = this._runBuild(teamId, baseDir);
130
+ if (preBuild?.failed) {
131
+ this.daemon.audit?.log('preview.prebuild-failed', { teamId, reason: preBuild.reason });
132
+ }
133
+ }
108
134
  result = await this._launchDevServer(teamId, baseDir, preview);
135
+ // Fallback: if dev-server failed, try building and serving statically
136
+ if (!result.launched) {
137
+ const pkgPath = resolve(baseDir, 'package.json');
138
+ if (existsSync(pkgPath)) {
139
+ try {
140
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
141
+ if (pkg.scripts?.build) {
142
+ const buildResult = this._runBuild(teamId, baseDir);
143
+ if (!buildResult?.failed) {
144
+ const distDir = resolve(baseDir, 'dist');
145
+ if (existsSync(resolve(distDir, 'index.html'))) {
146
+ result = await this._launchStatic(teamId, distDir, { ...preview, openPath: 'index.html' });
147
+ }
148
+ }
149
+ }
150
+ } catch { /* fallback failed, keep original error */ }
151
+ }
152
+ }
109
153
  } else {
110
154
  result = { launched: false, reason: `unknown_kind: ${preview.kind}` };
111
155
  }
@@ -118,6 +162,99 @@ export class PreviewService {
118
162
  return result;
119
163
  }
120
164
 
165
+ _ensureDependencies(teamId, baseDir) {
166
+ const pkgPath = resolve(baseDir, 'package.json');
167
+ const nodeModules = resolve(baseDir, 'node_modules');
168
+ if (!existsSync(pkgPath) || existsSync(nodeModules)) return null;
169
+ try {
170
+ console.log(`[Groove:Preview] Running npm install in ${baseDir}`);
171
+ this.daemon.audit?.log('preview.npm-install', { teamId, baseDir });
172
+ execSync('npm install', { cwd: baseDir, timeout: 120_000, stdio: 'pipe' });
173
+ return null;
174
+ } catch (err) {
175
+ return { failed: true, reason: `npm install failed: ${err.message?.slice(0, 300)}` };
176
+ }
177
+ }
178
+
179
+ _needsBuild(baseDir, preview) {
180
+ const pkgPath = resolve(baseDir, 'package.json');
181
+ let hasBuildScript = false;
182
+ try {
183
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
184
+ hasBuildScript = !!pkg.scripts?.build;
185
+ } catch { /* no package.json or malformed */ }
186
+
187
+ const distDir = resolve(baseDir, 'dist');
188
+ const distExists = existsSync(distDir);
189
+
190
+ // Primary: build script exists and dist/ doesn't
191
+ if (hasBuildScript && !distExists) return true;
192
+
193
+ // Stale check: dist/ exists but package.json is newer than dist/index.html
194
+ if (hasBuildScript && distExists) {
195
+ const distIndex = resolve(distDir, 'index.html');
196
+ if (existsSync(distIndex) && existsSync(pkgPath)) {
197
+ try {
198
+ const distMtime = statSync(distIndex).mtimeMs;
199
+ const pkgMtime = statSync(pkgPath).mtimeMs;
200
+ if (pkgMtime > distMtime) return true;
201
+ } catch { /* ignore stat errors */ }
202
+ }
203
+ }
204
+
205
+ // Secondary: entry file references .tsx/.jsx sources (needs transpilation)
206
+ const openPath = (preview.openPath || 'index.html').replace(/^\/+/, '');
207
+ const entryFile = resolve(baseDir, openPath);
208
+ if (existsSync(entryFile)) {
209
+ try {
210
+ const html = readFileSync(entryFile, 'utf8');
211
+ if (/src=["'][^"']*\.(tsx?|jsx)["']/i.test(html)) return true;
212
+ } catch { /* ignore */ }
213
+ }
214
+
215
+ // Entry file missing — check if a build might create it
216
+ if (!existsSync(entryFile) && hasBuildScript) {
217
+ const frameworkConfigs = ['vite.config', 'next.config', 'webpack.config'];
218
+ for (const cfg of frameworkConfigs) {
219
+ for (const ext of ['.js', '.ts', '.mjs', '.cjs']) {
220
+ if (existsSync(resolve(baseDir, cfg + ext))) return true;
221
+ }
222
+ }
223
+ }
224
+
225
+ return false;
226
+ }
227
+
228
+ _needsPreBuild(baseDir) {
229
+ const pkgPath = resolve(baseDir, 'package.json');
230
+ if (!existsSync(pkgPath)) return false;
231
+ try {
232
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
233
+ const startScript = pkg.scripts?.start || '';
234
+ if (/\bnext\s+start\b/.test(startScript)) return true;
235
+ if (/\bserve\b/.test(startScript) && !pkg.scripts?.dev) return true;
236
+ if (/\bhttp-server\b/.test(startScript)) return true;
237
+ } catch { /* ignore */ }
238
+ return false;
239
+ }
240
+
241
+ _runBuild(teamId, baseDir) {
242
+ const pkgPath = resolve(baseDir, 'package.json');
243
+ if (!existsSync(pkgPath)) return { failed: true, reason: 'no package.json for build' };
244
+ try {
245
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
246
+ if (!pkg.scripts?.build) return { failed: true, reason: 'no build script' };
247
+ } catch { return { failed: true, reason: 'malformed package.json' }; }
248
+ try {
249
+ console.log(`[Groove:Preview] Running npm run build in ${baseDir}`);
250
+ this.daemon.audit?.log('preview.build', { teamId, baseDir });
251
+ execSync('npm run build', { cwd: baseDir, timeout: 120_000, stdio: 'pipe' });
252
+ return null;
253
+ } catch (err) {
254
+ return { failed: true, reason: `build failed: ${err.message?.slice(0, 300)}` };
255
+ }
256
+ }
257
+
121
258
  _launchStatic(teamId, baseDir, preview) {
122
259
  const openPath = (preview.openPath || 'index.html').replace(/^\/+/, '');
123
260
  const entryFile = resolve(baseDir, openPath);
@@ -130,6 +267,15 @@ export class PreviewService {
130
267
  const filePath = resolve(baseDir, rel);
131
268
  if (!filePath.startsWith(baseDir)) { res.statusCode = 403; return res.end(); }
132
269
  if (!existsSync(filePath) || !statSync(filePath).isFile()) {
270
+ // SPA fallback: serve index.html for HTML requests (client-side routing)
271
+ const acceptsHtml = (req.headers.accept || '').includes('text/html');
272
+ if (acceptsHtml) {
273
+ const fallback = resolve(baseDir, openPath);
274
+ if (existsSync(fallback) && statSync(fallback).isFile()) {
275
+ res.setHeader('Content-Type', 'text/html');
276
+ return res.end(readFileSync(fallback));
277
+ }
278
+ }
133
279
  res.statusCode = 404; return res.end('Not found');
134
280
  }
135
281
  res.setHeader('Content-Type', mimeLookup(extname(filePath)) || 'application/octet-stream');
@@ -741,6 +741,10 @@ For normal file edits within your scope, proceed without review.
741
741
  // Clean up per-agent maps to prevent unbounded growth in long sessions
742
742
  this.peakContextUsage.delete(agent.id);
743
743
  this.pendingMessages.delete(agent.id);
744
+
745
+ // Release file-scope locks so they don't persist after agent death
746
+ if (this.daemon.locks) this.daemon.locks.release(agent.id);
747
+
744
748
  registry.update(agent.id, { status, pid: null });
745
749
 
746
750
  if (this.daemon.timeline) {
@@ -1351,6 +1355,13 @@ For normal file edits within your scope, proceed without review.
1351
1355
  if (block.type === 'text') textParts.push(block.text);
1352
1356
  }
1353
1357
  }
1358
+ if (evt.type === 'message' && evt.content) {
1359
+ const parts = Array.isArray(evt.content) ? evt.content : [evt.content];
1360
+ for (const p of parts) {
1361
+ if (typeof p === 'string') textParts.push(p);
1362
+ else if (p.text) textParts.push(p.text);
1363
+ }
1364
+ }
1354
1365
  } catch { textParts.push(line); }
1355
1366
  }
1356
1367
  const fullText = textParts.join('\n');
@@ -1798,6 +1809,10 @@ For normal file edits within your scope, proceed without review.
1798
1809
  logStream.end();
1799
1810
  this.handles.delete(newAgent.id);
1800
1811
  this._stalledAgents.delete(newAgent.id);
1812
+
1813
+ // Release file-scope locks so they don't persist after agent death
1814
+ if (this.daemon.locks) this.daemon.locks.release(newAgent.id);
1815
+
1801
1816
  const finalStatus = signal === 'SIGTERM' || signal === 'SIGKILL' ? 'killed' : code === 0 ? 'completed' : 'crashed';
1802
1817
  registry.update(newAgent.id, { status: finalStatus, pid: null });
1803
1818
  this.daemon.broadcast({ type: 'agent:exit', agentId: newAgent.id, code, signal, status: finalStatus });
@@ -1929,6 +1944,10 @@ For normal file edits within your scope, proceed without review.
1929
1944
  this._streamThrottle.delete(newAgent.id);
1930
1945
  this.peakContextUsage.delete(newAgent.id);
1931
1946
  this.pendingMessages.delete(newAgent.id);
1947
+
1948
+ // Release file-scope locks so they don't persist after agent death
1949
+ if (this.daemon.locks) this.daemon.locks.release(newAgent.id);
1950
+
1932
1951
  registry.update(newAgent.id, { status, pid: null });
1933
1952
 
1934
1953
  const agentData = registry.get(newAgent.id);
@@ -2077,14 +2096,26 @@ For normal file edits within your scope, proceed without review.
2077
2096
 
2078
2097
  // CLI process path — spawn's exit handler sets status='killed' for SIGTERM
2079
2098
  return new Promise((resolveKill) => {
2099
+ let resolved = false;
2100
+ const resolve = () => { if (!resolved) { resolved = true; resolveKill(); } };
2101
+
2080
2102
  const forceTimer = setTimeout(() => {
2081
2103
  try { proc.kill('SIGKILL'); } catch {}
2082
2104
  }, 5000);
2083
2105
 
2106
+ // Hard timeout: resolve even if exit event never fires (prevents HTTP hang)
2107
+ const hardTimer = setTimeout(() => {
2108
+ clearTimeout(forceTimer);
2109
+ this.handles.delete(agentId);
2110
+ this.daemon.locks.release(agentId);
2111
+ resolve();
2112
+ }, 10000);
2113
+
2084
2114
  proc.on('exit', () => {
2085
2115
  clearTimeout(forceTimer);
2116
+ clearTimeout(hardTimer);
2086
2117
  this.daemon.locks.release(agentId);
2087
- resolveKill();
2118
+ resolve();
2088
2119
  });
2089
2120
 
2090
2121
  try {
@@ -2092,9 +2123,10 @@ For normal file edits within your scope, proceed without review.
2092
2123
  } catch {
2093
2124
  // Already dead
2094
2125
  clearTimeout(forceTimer);
2126
+ clearTimeout(hardTimer);
2095
2127
  this.handles.delete(agentId);
2096
2128
  this.daemon.locks.release(agentId);
2097
- resolveKill();
2129
+ resolve();
2098
2130
  }
2099
2131
  });
2100
2132
  }
@@ -28,6 +28,7 @@ export class GeminiProvider extends Provider {
28
28
  static name = 'gemini';
29
29
  static displayName = 'Gemini CLI';
30
30
  static command = 'gemini';
31
+ static nonInteractive = true;
31
32
  static authType = 'api-key';
32
33
  static envKey = 'GEMINI_API_KEY';
33
34
  static models = [
@@ -59,7 +60,7 @@ export class GeminiProvider extends Provider {
59
60
 
60
61
  args.push('--yolo');
61
62
  args.push('--output-format', 'stream-json');
62
- args.push('-p', '');
63
+ args.push('-p');
63
64
 
64
65
  this._currentModel = agent.model;
65
66
 
@@ -84,8 +85,13 @@ export class GeminiProvider extends Provider {
84
85
  if (agent.role !== 'planner') {
85
86
  parts.push(
86
87
  `## Non-Interactive Commands\n\n` +
87
- `Always use non-interactive flags when running package manager commands to prevent timeout hangs: ` +
88
- `\`npx --yes\`, \`npm create --yes\`, \`npm init --yes\`. Never run these commands without the \`--yes\` flag.`
88
+ `Always use non-interactive flags when running package manager commands to prevent timeout hangs:\n` +
89
+ `- npx: always use \`npx --yes\`\n` +
90
+ `- create-vite: always add \`--no-interactive\` flag (e.g. \`npx --yes create-vite . --template react --no-interactive\`)\n` +
91
+ `- create-next-app: always add \`--yes\` flag\n` +
92
+ `- npm init/create: always add \`--yes\` flag\n` +
93
+ `- Any scaffolding tool: look for --no-interactive, --yes, or -y flags to skip prompts\n` +
94
+ `Never run these commands without non-interactive flags — they will hang and be auto-killed after 5 minutes.`
89
95
  );
90
96
  }
91
97
  return parts.join('\n\n');
@@ -155,7 +161,7 @@ export class GeminiProvider extends Provider {
155
161
 
156
162
  switch (event.type) {
157
163
  case 'init':
158
- return { type: 'activity', subtype: 'assistant', sessionId: event.session_id, model: event.model, data: [{ type: 'text', text: '' }] };
164
+ return { type: 'activity', subtype: 'assistant', model: event.model, data: [{ type: 'text', text: '' }] };
159
165
 
160
166
  case 'message': {
161
167
  if (event.role === 'user') return null;