groove-dev 0.27.27 → 0.27.29

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 +7 -0
  2. package/README.md +28 -0
  3. package/node_modules/@groove-dev/cli/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/src/api.js +99 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +103 -45
  7. package/node_modules/@groove-dev/daemon/src/process.js +12 -0
  8. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +26 -1
  9. package/node_modules/@groove-dev/daemon/test/journalist.test.js +1 -1
  10. package/node_modules/@groove-dev/gui/dist/assets/{index-DieCV-v1.js → index-CNsQ3n1t.js} +1730 -1730
  11. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  12. package/node_modules/@groove-dev/gui/package.json +1 -1
  13. package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +149 -23
  14. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +206 -44
  15. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +11 -24
  16. package/node_modules/@groove-dev/gui/src/components/marketplace/marketplace-card.jsx +1 -36
  17. package/node_modules/@groove-dev/gui/src/lib/integration-logos.js +39 -0
  18. package/node_modules/@groove-dev/gui/src/views/settings.jsx +2 -2
  19. package/package.json +1 -1
  20. package/packages/cli/package.json +1 -1
  21. package/packages/daemon/package.json +1 -1
  22. package/packages/daemon/src/api.js +99 -0
  23. package/packages/daemon/src/journalist.js +103 -45
  24. package/packages/daemon/src/process.js +12 -0
  25. package/packages/daemon/src/providers/claude-code.js +26 -1
  26. package/packages/gui/dist/assets/{index-DieCV-v1.js → index-CNsQ3n1t.js} +1730 -1730
  27. package/packages/gui/dist/index.html +1 -1
  28. package/packages/gui/package.json +1 -1
  29. package/packages/gui/src/components/agents/agent-config.jsx +149 -23
  30. package/packages/gui/src/components/agents/spawn-wizard.jsx +206 -44
  31. package/packages/gui/src/components/marketplace/integration-wizard.jsx +11 -24
  32. package/packages/gui/src/components/marketplace/marketplace-card.jsx +1 -36
  33. package/packages/gui/src/lib/integration-logos.js +39 -0
  34. package/packages/gui/src/views/settings.jsx +2 -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 -->
package/README.md CHANGED
@@ -7,6 +7,20 @@ The open-source orchestration layer for AI coding tools. Spawn teams of agents,
7
7
  [![npm](https://img.shields.io/npm/v/groove-dev)](https://www.npmjs.com/package/groove-dev)
8
8
  [![License](https://img.shields.io/badge/license-FSL--1.1--Apache--2.0-blue)](LICENSE)
9
9
 
10
+ ## Get Started
11
+
12
+ ### Download the App
13
+
14
+ No terminal needed. Download, install, open a project folder.
15
+
16
+ - [**macOS** (.dmg)](https://github.com/grooveai-dev/groove/releases/latest) — Intel and Apple Silicon
17
+ - [**Windows** (.exe)](https://github.com/grooveai-dev/groove/releases/latest) — one-click installer
18
+ - [**Linux** (.AppImage)](https://github.com/grooveai-dev/groove/releases/latest) — x64 and ARM
19
+
20
+ Everything is bundled — daemon, GUI, auto-updates. No Node.js required.
21
+
22
+ ### Developer Install
23
+
10
24
  ```bash
11
25
  npm i -g groove-dev
12
26
  groove start
@@ -16,6 +30,20 @@ The GUI opens at `http://localhost:31415`. On a VPS? groove detects it and tells
16
30
 
17
31
  ---
18
32
 
33
+ ## Desktop App
34
+
35
+ The desktop app is the easiest way to use groove — no terminal, no dependencies, no setup.
36
+
37
+ - **Full GUI dashboard** — agent tree, chat, editor, telemetry, marketplace, all in one window
38
+ - **Bundled daemon** — starts automatically when you open a project, stops when you close it
39
+ - **System tray** — quick access to recent projects, daemon status, and controls
40
+ - **Automatic updates** — new versions install silently in the background
41
+ - **Multi-workspace** — open any project folder; manages one daemon per project
42
+ - **Platform support** — macOS (Intel + Apple Silicon), Windows (x64 + ARM64), Linux (x64 + ARM64)
43
+ - **No dependencies** — everything is bundled. No Node.js, no terminal knowledge needed
44
+
45
+ ---
46
+
19
47
  ## The Problem
20
48
 
21
49
  AI coding agents waste your money and lose their way:
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.27",
3
+ "version": "0.27.29",
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.27",
3
+ "version": "0.27.29",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -9,7 +9,9 @@ import { spawn, execFile } from 'child_process';
9
9
  import { lookup as mimeLookup } from './mimetypes.js';
10
10
  import { listProviders, getProvider } from './providers/index.js';
11
11
  import { OllamaProvider } from './providers/ollama.js';
12
+ import { ClaudeCodeProvider } from './providers/claude-code.js';
12
13
  import { validateAgentConfig } from './validate.js';
14
+ import { ROLE_INTEGRATIONS } from './process.js';
13
15
 
14
16
  const __dirname = dirname(fileURLToPath(import.meta.url));
15
17
  const pkgVersion = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version;
@@ -179,6 +181,88 @@ export function createApi(app, daemon) {
179
181
  res.json({ ok: true });
180
182
  });
181
183
 
184
+ // --- Role-to-Integration Mapping ---
185
+
186
+ app.get('/api/roles/integrations', (req, res) => {
187
+ const roleFilter = req.query.role;
188
+ const entries = roleFilter ? { [roleFilter]: ROLE_INTEGRATIONS[roleFilter] || [] } : ROLE_INTEGRATIONS;
189
+ const result = {};
190
+ for (const [role, ids] of Object.entries(entries)) {
191
+ result[role] = (ids || []).map((id) => {
192
+ const status = daemon.integrations.getStatus(id);
193
+ const entry = daemon.integrations.registry.find((r) => r.id === id);
194
+ return {
195
+ id,
196
+ name: entry?.name || id,
197
+ installed: status?.installed || false,
198
+ configured: status?.configured || false,
199
+ authenticated: status?.authenticated || false,
200
+ };
201
+ });
202
+ }
203
+ if (roleFilter) return res.json(result[roleFilter] || []);
204
+ res.json(result);
205
+ });
206
+
207
+ app.post('/api/agents/preflight', (req, res) => {
208
+ const { role, integrations } = req.body || {};
209
+ if (!role || !Array.isArray(integrations)) {
210
+ return res.status(400).json({ error: 'role and integrations[] required' });
211
+ }
212
+ const issues = [];
213
+ for (const id of integrations) {
214
+ const status = daemon.integrations.getStatus(id);
215
+ const entry = daemon.integrations.registry.find((r) => r.id === id);
216
+ const name = entry?.name || id;
217
+ if (!status || !status.installed) {
218
+ issues.push({ integrationId: id, name, problem: 'not_installed' });
219
+ } else if (!status.configured) {
220
+ issues.push({ integrationId: id, name, problem: 'not_configured' });
221
+ } else if (!status.authenticated) {
222
+ issues.push({ integrationId: id, name, problem: 'not_authenticated' });
223
+ }
224
+ }
225
+ res.json({ ready: issues.length === 0, issues });
226
+ });
227
+
228
+ // --- Agent Integration Attach/Detach ---
229
+
230
+ app.post('/api/agents/:id/integrations/:integrationId', (req, res) => {
231
+ const agent = daemon.registry.get(req.params.id);
232
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
233
+
234
+ const integrationId = req.params.integrationId;
235
+ const status = daemon.integrations.getStatus(integrationId);
236
+ if (!status || !status.installed) {
237
+ return res.status(400).json({ error: `Integration not installed: ${integrationId}` });
238
+ }
239
+
240
+ const integrations = new Set(agent.integrations || []);
241
+ integrations.add(integrationId);
242
+ const updated = Array.from(integrations);
243
+
244
+ daemon.registry.update(req.params.id, { integrations: updated });
245
+ daemon.integrations.writeMcpJson(daemon.integrations.getActiveIntegrations());
246
+ daemon.integrations.refreshMcpJson();
247
+ daemon.audit.log('agent.integration.attach', { agentId: req.params.id, integrationId });
248
+ daemon.broadcast({ type: 'agent:integration:attach', agentId: req.params.id, integrationId });
249
+ res.json({ ok: true, integrations: updated });
250
+ });
251
+
252
+ app.delete('/api/agents/:id/integrations/:integrationId', (req, res) => {
253
+ const agent = daemon.registry.get(req.params.id);
254
+ if (!agent) return res.status(404).json({ error: 'Agent not found' });
255
+
256
+ const integrationId = req.params.integrationId;
257
+ const integrations = (agent.integrations || []).filter((id) => id !== integrationId);
258
+
259
+ daemon.registry.update(req.params.id, { integrations });
260
+ daemon.integrations.refreshMcpJson();
261
+ daemon.audit.log('agent.integration.detach', { agentId: req.params.id, integrationId });
262
+ daemon.broadcast({ type: 'agent:integration:detach', agentId: req.params.id, integrationId });
263
+ res.json({ ok: true, integrations });
264
+ });
265
+
182
266
  // Lock management
183
267
  app.get('/api/locks', (req, res) => {
184
268
  res.json(daemon.locks.getAll());
@@ -307,10 +391,25 @@ export function createApi(app, daemon) {
307
391
  // Enrich with credential status
308
392
  for (const p of providers) {
309
393
  p.hasKey = daemon.credentials.hasKey(p.id);
394
+ if (p.id === 'claude-code') {
395
+ p.authStatus = ClaudeCodeProvider.getAuthStatus();
396
+ }
310
397
  }
311
398
  res.json(providers);
312
399
  });
313
400
 
401
+ // --- Claude Code Auth ---
402
+
403
+ app.get('/api/providers/claude-code/auth', (req, res) => {
404
+ res.json(ClaudeCodeProvider.getAuthStatus());
405
+ });
406
+
407
+ app.post('/api/providers/claude-code/login', (req, res) => {
408
+ ClaudeCodeProvider.triggerLogin();
409
+ daemon.audit.log('claude-code.login.started', {});
410
+ res.json({ ok: true });
411
+ });
412
+
314
413
  // --- Ollama ---
315
414
 
316
415
  app.get('/api/providers/ollama/hardware', (req, res) => {
@@ -166,7 +166,7 @@ export class Journalist {
166
166
  return false;
167
167
  }
168
168
 
169
- collectFilteredLogs(agents) {
169
+ collectFilteredLogs(agents, { since } = {}) {
170
170
  const result = {};
171
171
 
172
172
  for (const agent of agents) {
@@ -181,7 +181,7 @@ export class Journalist {
181
181
  const size = Buffer.byteLength(content);
182
182
  this.lastLogSizes[agent.id] = size;
183
183
 
184
- const { entries, explorationEntries } = this.filterLog(content, agent);
184
+ const { entries, explorationEntries } = this.filterLog(content, agent, { since });
185
185
  result[agent.id] = { agent, entries, explorationEntries };
186
186
  } catch {
187
187
  result[agent.id] = { agent, entries: [], explorationEntries: [] };
@@ -191,7 +191,7 @@ export class Journalist {
191
191
  return result;
192
192
  }
193
193
 
194
- filterLog(rawLog, agent) {
194
+ filterLog(rawLog, agent, { since } = {}) {
195
195
  // Parse stream-json lines and extract meaningful events.
196
196
  // Focus on PROGRESS (writes, edits, commands with results) not EXPLORATION (reads, greps).
197
197
  // Exploration tools are tracked separately so handoff briefs can include what was examined.
@@ -200,12 +200,15 @@ export class Journalist {
200
200
  const lines = rawLog.split('\n');
201
201
  const toolResults = new Map(); // tool_use_id -> result text
202
202
 
203
+ const sinceDate = since ? new Date(since) : null;
204
+
203
205
  // First pass: collect tool results (and error flags) so we can attach them to tool calls
204
206
  const toolErrors = new Set(); // tool_use_ids that returned errors
205
207
  for (const line of lines) {
206
208
  if (!line.trim() || line.startsWith('[')) continue;
207
209
  try {
208
210
  const data = JSON.parse(line);
211
+ if (sinceDate && data.timestamp && new Date(data.timestamp) < sinceDate) continue;
209
212
  if (data.type === 'user' && data.message?.content) {
210
213
  const content = Array.isArray(data.message.content) ? data.message.content : [];
211
214
  for (const block of content) {
@@ -227,6 +230,7 @@ export class Journalist {
227
230
 
228
231
  try {
229
232
  const data = JSON.parse(line);
233
+ if (sinceDate && data.timestamp && new Date(data.timestamp) < sinceDate) continue;
230
234
 
231
235
  // Tool use — only keep WRITES, EDITS, and COMMANDS (progress, not exploration)
232
236
  if (data.type === 'assistant' && data.message?.content) {
@@ -415,6 +419,56 @@ export class Journalist {
415
419
  return parts.join('\n');
416
420
  }
417
421
 
422
+ buildRotationSynthesisPrompt(agent, entries, options = {}) {
423
+ const dir = agent.workingDir ? `\nWorking directory: ${agent.workingDir}` : '';
424
+ const reason = options.reason || 'manual rotation';
425
+
426
+ const parts = [
427
+ 'You are briefing the next AI agent taking over this exact role.',
428
+ `The previous agent (${agent.name}, role: ${agent.role}) is being rotated out.`,
429
+ `Scope: ${agent.scope?.join(', ') || 'unrestricted'}${dir}`,
430
+ `Rotation reason: ${reason}`,
431
+ '',
432
+ 'Analyze the session log below and produce a structured handoff brief.',
433
+ '',
434
+ 'Output EXACTLY these sections:',
435
+ '',
436
+ '## Accomplishments',
437
+ '(What was completed. Name files, functions, and line numbers.)',
438
+ '',
439
+ '## In Progress',
440
+ '(What was actively being worked on when rotation happened.)',
441
+ '',
442
+ '## Key Decisions',
443
+ '(Architectural or implementation choices made and why.)',
444
+ '',
445
+ '## Blockers/Errors',
446
+ '(Unresolved errors, failed attempts, things that did not work.)',
447
+ '',
448
+ '## Next Steps',
449
+ '(What should be done next, in priority order.)',
450
+ '',
451
+ 'Be specific. Name files, functions, and line numbers. Do not summarize vaguely.',
452
+ 'Keep your response under 2000 characters.',
453
+ '',
454
+ '---',
455
+ '',
456
+ `### Session Log for ${agent.name} (${agent.role})`,
457
+ '',
458
+ ];
459
+
460
+ let totalChars = 0;
461
+ const cap = 30_000;
462
+ for (const entry of entries.slice(-200)) {
463
+ const line = this.formatEntry(entry);
464
+ if (totalChars + line.length > cap) break;
465
+ parts.push(line);
466
+ totalChars += line.length;
467
+ }
468
+
469
+ return parts.join('\n');
470
+ }
471
+
418
472
  formatEntry(entry) {
419
473
  switch (entry.type) {
420
474
  case 'tool': {
@@ -739,35 +793,10 @@ export class Journalist {
739
793
  // --- Handoff Brief for Context Rotation ---
740
794
 
741
795
  async generateHandoffBrief(agent, options = {}) {
742
- const filteredLogs = this.collectFilteredLogs([agent]);
796
+ const filteredLogs = this.collectFilteredLogs([agent], { since: agent.spawnedAt });
743
797
  const agentLog = filteredLogs[agent.id];
744
798
  const entries = agentLog?.entries || [];
745
799
 
746
- const errorSummary = entries
747
- .filter((e) => e.type === 'error')
748
- .map((e) => `- ${e.text}`)
749
- .slice(-10)
750
- .join('\n');
751
-
752
- const resultSummary = entries
753
- .filter((e) => e.type === 'result')
754
- .map((e) => e.text)
755
- .slice(-3)
756
- .join('\n');
757
-
758
- // Build file changes section — group Edit/Write operations by file path
759
- const fileChanges = {};
760
- for (const e of entries.filter((e) => e.type === 'tool' && (e.tool === 'Edit' || e.tool === 'Write'))) {
761
- const file = e.input || 'unknown';
762
- if (!fileChanges[file]) fileChanges[file] = [];
763
- if (e.diff) fileChanges[file].push(e.diff);
764
- else fileChanges[file].push(e.tool === 'Write' ? 'created' : 'modified');
765
- }
766
- const fileChangesSummary = Object.entries(fileChanges)
767
- .map(([file, changes]) => `- **${file}**: ${changes.slice(0, 3).join('; ')}`)
768
- .slice(0, 20)
769
- .join('\n');
770
-
771
800
  // Layer 7 memory: discoveries, constraints, specializations
772
801
  const discoveries = this.daemon.memory?.getDiscoveriesMarkdown(agent.role, 10, 2000) || '';
773
802
  const constraints = this.daemon.memory?.getConstraintsMarkdown(2000) || '';
@@ -776,26 +805,58 @@ export class Journalist {
776
805
  ? `- Quality profile: ${specialization.avgQualityScore}/100 across ${specialization.sessionCount} sessions`
777
806
  : '';
778
807
 
779
- // Pull recent rotation history from persistent memory (Layer 7).
780
- // Gives the new agent causal continuity: what the last 3 agents struggled
781
- // with, decided, and solved — not just what the current session did.
782
808
  const recentChain = this.daemon.memory?.getRecentHandoffMarkdown(agent.role, 3, 3000, agent.workingDir, agent.teamId) || '';
783
809
 
784
- // Pull the user's recent messages scoped to this agent
785
810
  const agentFeedback = this.getUserFeedback(agent.id).slice(-5);
786
811
  const conversationSummary = agentFeedback.length > 0
787
812
  ? agentFeedback.map((fb) => `- "${fb.message}"`).join('\n')
788
813
  : '';
789
814
 
790
- // Compact last 5 tool calls (not full output, just tool + target)
791
- const recentTools = entries
792
- .filter((e) => e.type === 'tool' || e.type === 'error')
793
- .slice(-5)
794
- .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 80)}`)
795
- .join('\n');
815
+ // Try AI-synthesized session summary
816
+ let sessionSummary = '';
817
+ try {
818
+ const prompt = this.buildRotationSynthesisPrompt(agent, entries, options);
819
+ sessionSummary = await this.callHeadless(prompt, { trackAs: '__rotation__' });
820
+ } catch {
821
+ // Fallback: structural summary from raw logs
822
+ const errorSummary = entries
823
+ .filter((e) => e.type === 'error')
824
+ .map((e) => `- ${e.text}`)
825
+ .slice(-10)
826
+ .join('\n');
827
+
828
+ const resultSummary = entries
829
+ .filter((e) => e.type === 'result')
830
+ .map((e) => e.text)
831
+ .slice(-3)
832
+ .join('\n');
833
+
834
+ const fileChanges = {};
835
+ for (const e of entries.filter((e) => e.type === 'tool' && (e.tool === 'Edit' || e.tool === 'Write'))) {
836
+ const file = e.input || 'unknown';
837
+ if (!fileChanges[file]) fileChanges[file] = [];
838
+ if (e.diff) fileChanges[file].push(e.diff);
839
+ else fileChanges[file].push(e.tool === 'Write' ? 'created' : 'modified');
840
+ }
841
+ const fileChangesSummary = Object.entries(fileChanges)
842
+ .map(([file, changes]) => `- **${file}**: ${changes.slice(0, 3).join('; ')}`)
843
+ .slice(0, 20)
844
+ .join('\n');
845
+
846
+ const recentTools = entries
847
+ .filter((e) => e.type === 'tool' || e.type === 'error')
848
+ .slice(-5)
849
+ .map((e) => `- ${e.type === 'error' ? 'ERROR ' : ''}${e.tool}: ${(e.input || '').slice(0, 80)}`)
850
+ .join('\n');
851
+
852
+ const fallbackParts = [];
853
+ if (errorSummary) fallbackParts.push(`## Unresolved Errors\n\n${errorSummary}`);
854
+ if (recentTools) fallbackParts.push(`## Last 5 Tool Calls\n\n${recentTools}`);
855
+ if (fileChangesSummary) fallbackParts.push(`## Files Modified\n\n${fileChangesSummary}`);
856
+ if (resultSummary) fallbackParts.push(`## Accomplishments\n\n${resultSummary}`);
857
+ sessionSummary = fallbackParts.join('\n\n');
858
+ }
796
859
 
797
- // Brief priority: errors > constraints > recent tools > accomplishments
798
- // The rotator already wraps this with session continuation context
799
860
  return [
800
861
  `# Handoff Brief — ${agent.name} (${agent.role})`,
801
862
  ``,
@@ -804,13 +865,10 @@ export class Journalist {
804
865
  `Rotation: ${options.reason || 'manual'}${options.qualityScore ? ` (quality: ${options.qualityScore}/100)` : ''} | Tokens: ${agent.tokensUsed}`,
805
866
  specLine,
806
867
  ``,
807
- errorSummary ? `## Unresolved Errors (fix these first)\n\n${errorSummary}\n` : '',
868
+ sessionSummary ? `## Session Summary\n\n${sessionSummary}\n` : '',
808
869
  constraints ? `## Project Constraints (must follow)\n\n${constraints}\n` : '',
809
870
  discoveries ? `## Known Issues & Fixes\n\n${discoveries}\n` : '',
810
871
  conversationSummary ? `## Recent User Messages\n\n${conversationSummary}\n` : '',
811
- recentTools ? `## Last 5 Tool Calls\n\n${recentTools}\n` : '',
812
- fileChangesSummary ? `## Files Modified\n\n${fileChangesSummary}\n` : '',
813
- resultSummary ? `## Accomplishments\n\n${resultSummary}\n` : '',
814
872
  recentChain ? `## Rotation History\n\n${recentChain}\n` : '',
815
873
  agent.prompt ? `## Original Task\n\n${agent.prompt}\n` : '',
816
874
  ``,
@@ -266,6 +266,18 @@ IMPORTANT: Do not use markdown formatting like ** or ### in your output. Write i
266
266
  `,
267
267
  };
268
268
 
269
+ // Role-to-integration mapping — recommended integrations per role for onboarding preflight
270
+ export const ROLE_INTEGRATIONS = {
271
+ ea: ['gmail', 'google-calendar'],
272
+ cmo: ['gmail', 'slack', 'hubspot'],
273
+ cfo: ['stripe', 'google-sheets'],
274
+ support: ['gmail', 'slack', 'zendesk'],
275
+ analyst: ['google-sheets', 'postgres', 'mixpanel'],
276
+ home: ['home-assistant'],
277
+ slides: ['google-slides'],
278
+ creative: ['google-docs'],
279
+ };
280
+
269
281
  // Permission-level prompt instructions
270
282
  // "auto" = PM reviews risky ops via API. "full" = no reviews, max speed.
271
283
  const PERMISSION_PROMPTS = {
@@ -1,7 +1,7 @@
1
1
  // GROOVE — Claude Code Provider
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import { execSync } from 'child_process';
4
+ import { execSync, spawn as cpSpawn } from 'child_process';
5
5
  import { writeFileSync, readFileSync, existsSync } from 'fs';
6
6
  import { resolve } from 'path';
7
7
  import { homedir } from 'os';
@@ -223,4 +223,29 @@ export class ClaudeCodeProvider extends Provider {
223
223
 
224
224
  return merged;
225
225
  }
226
+
227
+ static getAuthStatus() {
228
+ try {
229
+ const out = execSync('claude auth status --json', { encoding: 'utf8', timeout: 10_000, stdio: ['pipe', 'pipe', 'pipe'] });
230
+ const data = JSON.parse(out);
231
+ return {
232
+ authenticated: true,
233
+ authMethod: data.authMethod || data.auth_method || 'unknown',
234
+ email: data.email || null,
235
+ subscriptionType: data.subscriptionType || data.subscription_type || null,
236
+ orgName: data.orgName || data.org_name || null,
237
+ };
238
+ } catch (err) {
239
+ return { authenticated: false, error: err.message };
240
+ }
241
+ }
242
+
243
+ static triggerLogin() {
244
+ const child = cpSpawn('claude', ['auth', 'login', '--claudeai'], {
245
+ detached: true,
246
+ stdio: 'ignore',
247
+ });
248
+ child.unref();
249
+ return { pid: child.pid };
250
+ }
226
251
  }
@@ -201,7 +201,7 @@ describe('Journalist', () => {
201
201
  assert.ok(brief.includes('src/api/**'));
202
202
  assert.ok(brief.includes('5000'));
203
203
  assert.ok(brief.includes('Build the auth API'));
204
- assert.ok(brief.includes('Write'));
204
+ assert.ok(brief.includes('Session Summary') || brief.includes('Write'));
205
205
  });
206
206
 
207
207
  it('instructs the agent to deliver the output, not passively wait', async () => {