groove-dev 0.17.8 → 0.18.2
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/node_modules/@groove-dev/cli/package.json +4 -3
- package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
- package/node_modules/@groove-dev/daemon/integrations-registry.json +0 -40
- package/node_modules/@groove-dev/daemon/package.json +4 -3
- package/node_modules/@groove-dev/daemon/src/api.js +212 -21
- package/node_modules/@groove-dev/daemon/src/index.js +68 -1
- package/node_modules/@groove-dev/daemon/src/integrations.js +59 -20
- package/node_modules/@groove-dev/daemon/src/process.js +83 -11
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/gui/.groove/audit.log +1 -0
- package/node_modules/@groove-dev/gui/.groove/codebase-index.json +64 -0
- package/node_modules/@groove-dev/gui/.groove/config.json +10 -0
- package/node_modules/@groove-dev/gui/.groove/coordination.md +5 -0
- package/node_modules/@groove-dev/gui/.groove/credentials.json +6 -0
- package/node_modules/@groove-dev/gui/.groove/daemon.port +1 -0
- package/node_modules/@groove-dev/gui/.groove/federation/identity.key +3 -0
- package/node_modules/@groove-dev/gui/.groove/federation/identity.pub +3 -0
- package/node_modules/@groove-dev/gui/.groove/integrations/package.json +6 -0
- package/node_modules/@groove-dev/gui/.groove/state.json +3 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-x5suAiK7.js +182 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +5 -4
- package/node_modules/@groove-dev/gui/src/App.jsx +149 -76
- package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +47 -7
- package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +918 -580
- package/node_modules/@groove-dev/gui/src/stores/groove.js +31 -2
- package/node_modules/@groove-dev/gui/src/views/AgentTree.jsx +133 -67
- package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
- package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +121 -44
- package/package.json +1 -2
- package/packages/cli/package.json +4 -3
- package/packages/daemon/integrations-registry.json +0 -40
- package/packages/daemon/package.json +4 -3
- package/packages/daemon/src/api.js +212 -21
- package/packages/daemon/src/index.js +68 -1
- package/packages/daemon/src/integrations.js +59 -20
- package/packages/daemon/src/process.js +83 -11
- package/packages/daemon/src/providers/claude-code.js +4 -0
- package/packages/daemon/src/registry.js +1 -1
- package/packages/gui/dist/assets/index-x5suAiK7.js +182 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js +68 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js +1420 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js +17 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +22 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +34 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js +101 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +2534 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +789 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_language.js +115 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_language.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_search.js +1136 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_search.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_state.js +63 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_state.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js +179 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_view.js +104 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_view.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js +46 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js +121 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js +9237 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xyflow_react.js +9934 -0
- package/packages/gui/node_modules/.vite/deps/@xyflow_react.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/_metadata.json +184 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js +5169 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js +2000 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js +1115 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js +701 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js +1776 -0
- package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js +280 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js +1004 -0
- package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js +292 -0
- package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js +1062 -0
- package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js +10985 -0
- package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js +3459 -0
- package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/package.json +3 -0
- package/packages/gui/node_modules/.vite/deps/react-dom.js +6 -0
- package/packages/gui/node_modules/.vite/deps/react-dom.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react-dom_client.js +20217 -0
- package/packages/gui/node_modules/.vite/deps/react-dom_client.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react.js +5 -0
- package/packages/gui/node_modules/.vite/deps/react.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/zustand.js +56 -0
- package/packages/gui/node_modules/.vite/deps/zustand.js.map +7 -0
- package/packages/gui/package.json +5 -4
- package/packages/gui/src/App.jsx +149 -76
- package/packages/gui/src/components/AgentActions.jsx +130 -1
- package/packages/gui/src/components/AgentChat.jsx +47 -7
- package/packages/gui/src/components/AgentNode.jsx +13 -83
- package/packages/gui/src/components/SpawnPanel.jsx +918 -580
- package/packages/gui/src/stores/groove.js +31 -2
- package/packages/gui/src/views/AgentTree.jsx +133 -67
- package/packages/gui/src/views/FileEditor.jsx +85 -1
- package/packages/gui/src/views/IntegrationsStore.jsx +121 -44
- package/docs/FILE-EDITOR-PLAN.md +0 -253
- package/docs/GUI_DESIGN_SPEC.md +0 -402
- package/docs/SKILLS-API-SPEC.md +0 -277
- package/node_modules/@groove-dev/gui/dist/assets/index-D5dtDQf0.js +0 -156
- package/packages/gui/dist/assets/index-D5dtDQf0.js +0 -156
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groove-dev/cli",
|
|
3
3
|
"version": "0.11.0",
|
|
4
|
-
"description": "GROOVE CLI
|
|
4
|
+
"description": "GROOVE CLI \u2014 manage AI coding agents from your terminal",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
|
@@ -11,5 +11,6 @@
|
|
|
11
11
|
"@groove-dev/daemon": "*",
|
|
12
12
|
"commander": "^12.1.0",
|
|
13
13
|
"chalk": "^5.3.0"
|
|
14
|
-
}
|
|
15
|
-
|
|
14
|
+
},
|
|
15
|
+
"private": true
|
|
16
|
+
}
|
|
@@ -338,26 +338,6 @@
|
|
|
338
338
|
"ratingCount": 0,
|
|
339
339
|
"verified": "community"
|
|
340
340
|
},
|
|
341
|
-
{
|
|
342
|
-
"id": "filesystem",
|
|
343
|
-
"name": "Filesystem",
|
|
344
|
-
"description": "Read, write, search, and manage files on the local filesystem",
|
|
345
|
-
"category": "developer",
|
|
346
|
-
"icon": "folder",
|
|
347
|
-
"tags": ["files", "filesystem", "local", "storage"],
|
|
348
|
-
"roles": ["backend", "fullstack", "devops"],
|
|
349
|
-
"npmPackage": "@modelcontextprotocol/server-filesystem",
|
|
350
|
-
"transport": "stdio",
|
|
351
|
-
"command": "npx",
|
|
352
|
-
"args": ["-y", "@modelcontextprotocol/server-filesystem"],
|
|
353
|
-
"authType": "none",
|
|
354
|
-
"envKeys": [],
|
|
355
|
-
"featured": false,
|
|
356
|
-
"downloads": 0,
|
|
357
|
-
"rating": 0,
|
|
358
|
-
"ratingCount": 0,
|
|
359
|
-
"verified": "mcp-official"
|
|
360
|
-
},
|
|
361
341
|
{
|
|
362
342
|
"id": "google-maps",
|
|
363
343
|
"name": "Google Maps",
|
|
@@ -385,25 +365,5 @@
|
|
|
385
365
|
"rating": 0,
|
|
386
366
|
"ratingCount": 0,
|
|
387
367
|
"verified": "mcp-official"
|
|
388
|
-
},
|
|
389
|
-
{
|
|
390
|
-
"id": "sqlite",
|
|
391
|
-
"name": "SQLite",
|
|
392
|
-
"description": "Query and manage SQLite databases, inspect schemas, run SQL",
|
|
393
|
-
"category": "database",
|
|
394
|
-
"icon": "database",
|
|
395
|
-
"tags": ["sql", "database", "local", "lightweight"],
|
|
396
|
-
"roles": ["analyst", "backend"],
|
|
397
|
-
"npmPackage": "mcp-sqlite",
|
|
398
|
-
"transport": "stdio",
|
|
399
|
-
"command": "npx",
|
|
400
|
-
"args": ["-y", "mcp-sqlite"],
|
|
401
|
-
"authType": "none",
|
|
402
|
-
"envKeys": [],
|
|
403
|
-
"featured": false,
|
|
404
|
-
"downloads": 0,
|
|
405
|
-
"rating": 0,
|
|
406
|
-
"ratingCount": 0,
|
|
407
|
-
"verified": "mcp-official"
|
|
408
368
|
}
|
|
409
369
|
]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@groove-dev/daemon",
|
|
3
3
|
"version": "0.11.0",
|
|
4
|
-
"description": "GROOVE daemon
|
|
4
|
+
"description": "GROOVE daemon \u2014 agent orchestration engine",
|
|
5
5
|
"license": "FSL-1.1-Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "src/index.js",
|
|
@@ -14,5 +14,6 @@
|
|
|
14
14
|
"ws": "^8.17.0",
|
|
15
15
|
"express": "^4.21.0",
|
|
16
16
|
"minimatch": "^10.0.0"
|
|
17
|
-
}
|
|
18
|
-
|
|
17
|
+
},
|
|
18
|
+
"private": true
|
|
19
|
+
}
|
|
@@ -5,8 +5,9 @@ import express from 'express';
|
|
|
5
5
|
import { resolve, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
7
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream } from 'fs';
|
|
8
|
+
import { spawn as cpSpawn } from 'child_process';
|
|
8
9
|
import { lookup as mimeLookup } from './mimetypes.js';
|
|
9
|
-
import { listProviders } from './providers/index.js';
|
|
10
|
+
import { listProviders, getProvider } from './providers/index.js';
|
|
10
11
|
import { validateAgentConfig } from './validate.js';
|
|
11
12
|
|
|
12
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
@@ -351,6 +352,67 @@ export function createApi(app, daemon) {
|
|
|
351
352
|
});
|
|
352
353
|
});
|
|
353
354
|
|
|
355
|
+
// Plan chat — direct API (fast, sub-second) when API key available, CLI fallback otherwise
|
|
356
|
+
const PLAN_SYSTEM = `You are the planning assistant built into Groove's spawn panel. The user is configuring an AI agent right now — they're looking at a form with role selection, file scope, skills, integrations, effort level, and a task prompt. Your conversation will be synthesized into the agent's task prompt when they click "Generate Prompt."
|
|
357
|
+
|
|
358
|
+
Your job: help them think through what the agent should do, then craft a clear plan. Be direct and practical. Don't ask how they'll feed input to agents or what tools to use — they're already inside Groove doing it. Focus on the TASK itself.
|
|
359
|
+
|
|
360
|
+
What you know about the system:
|
|
361
|
+
- The user is in the spawn panel, configuring an agent before launching it
|
|
362
|
+
- The left panel has: role picker, directory, permissions, effort, integrations, skills, schedule
|
|
363
|
+
- When done planning, "Generate Prompt" synthesizes this chat into the agent's task prompt
|
|
364
|
+
- Agents are Claude Code instances with full terminal/file access in the specified directory
|
|
365
|
+
- Agents can read/write files, run commands, use MCP integrations (Slack, GitHub, etc.)
|
|
366
|
+
- The journalist system prevents cold starts during context rotation — agents don't lose context
|
|
367
|
+
|
|
368
|
+
Keep responses concise. Help them think, don't lecture them about the system they built.`;
|
|
369
|
+
|
|
370
|
+
app.post('/api/journalist/query', async (req, res) => {
|
|
371
|
+
try {
|
|
372
|
+
const { prompt } = req.body || {};
|
|
373
|
+
if (!prompt) return res.status(400).json({ error: 'prompt is required' });
|
|
374
|
+
|
|
375
|
+
// Fast path: direct Anthropic API call (sub-second)
|
|
376
|
+
const apiKey = daemon.credentials.getKey('anthropic-api');
|
|
377
|
+
if (apiKey) {
|
|
378
|
+
const apiRes = await fetch('https://api.anthropic.com/v1/messages', {
|
|
379
|
+
method: 'POST',
|
|
380
|
+
headers: {
|
|
381
|
+
'Content-Type': 'application/json',
|
|
382
|
+
'x-api-key': apiKey,
|
|
383
|
+
'anthropic-version': '2023-06-01',
|
|
384
|
+
},
|
|
385
|
+
body: JSON.stringify({
|
|
386
|
+
model: 'claude-haiku-4-5-20251001',
|
|
387
|
+
max_tokens: 1024,
|
|
388
|
+
system: PLAN_SYSTEM,
|
|
389
|
+
messages: [{ role: 'user', content: prompt }],
|
|
390
|
+
}),
|
|
391
|
+
});
|
|
392
|
+
const data = await apiRes.json();
|
|
393
|
+
if (data.content?.[0]?.text) {
|
|
394
|
+
return res.json({ response: data.content[0].text, mode: 'fast' });
|
|
395
|
+
}
|
|
396
|
+
if (data.error) {
|
|
397
|
+
return res.status(400).json({ error: data.error.message || 'API error' });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Slow path: CLI fallback for subscription auth (~10s)
|
|
402
|
+
const fullPrompt = `${PLAN_SYSTEM}\n\n${prompt}`;
|
|
403
|
+
const response = await daemon.journalist.callHeadless(fullPrompt);
|
|
404
|
+
res.json({ response, mode: 'cli' });
|
|
405
|
+
} catch (err) {
|
|
406
|
+
res.status(500).json({ error: err.message });
|
|
407
|
+
}
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
// Check if Anthropic API key is configured
|
|
411
|
+
app.get('/api/anthropic-key/status', (req, res) => {
|
|
412
|
+
const hasKey = !!daemon.credentials.getKey('anthropic-api');
|
|
413
|
+
res.json({ configured: hasKey });
|
|
414
|
+
});
|
|
415
|
+
|
|
354
416
|
// Trigger journalist cycle manually
|
|
355
417
|
app.post('/api/journalist/cycle', async (req, res) => {
|
|
356
418
|
try {
|
|
@@ -507,11 +569,13 @@ export function createApi(app, daemon) {
|
|
|
507
569
|
// Parameterized :id routes (after specific routes above)
|
|
508
570
|
|
|
509
571
|
app.post('/api/integrations/:id/authenticate', (req, res) => {
|
|
572
|
+
console.log(`[Groove:API] POST /api/integrations/${req.params.id}/authenticate`);
|
|
510
573
|
try {
|
|
511
574
|
const handle = daemon.integrations.authenticate(req.params.id);
|
|
575
|
+
console.log(`[Groove:API] Authenticate started, PID: ${handle.pid}`);
|
|
512
576
|
res.json({ ok: true, pid: handle.pid });
|
|
513
|
-
// Auto-cleanup tracked by the handle timeout
|
|
514
577
|
} catch (err) {
|
|
578
|
+
console.log(`[Groove:API] Authenticate error: ${err.message}`);
|
|
515
579
|
res.status(400).json({ error: err.message });
|
|
516
580
|
}
|
|
517
581
|
});
|
|
@@ -720,6 +784,34 @@ export function createApi(app, daemon) {
|
|
|
720
784
|
}
|
|
721
785
|
});
|
|
722
786
|
|
|
787
|
+
// Browse absolute paths (for directory picker in agent config)
|
|
788
|
+
// Dirs only, localhost-only, no file content exposed
|
|
789
|
+
app.get('/api/browse-system', (req, res) => {
|
|
790
|
+
const absPath = req.query.path || process.env.HOME || '/';
|
|
791
|
+
if (absPath.includes('\0')) return res.status(400).json({ error: 'Invalid path' });
|
|
792
|
+
if (!existsSync(absPath)) return res.status(404).json({ error: 'Not found' });
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
const entries = readdirSync(absPath, { withFileTypes: true })
|
|
796
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') && e.name !== 'node_modules')
|
|
797
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
798
|
+
.map((e) => {
|
|
799
|
+
const full = resolve(absPath, e.name);
|
|
800
|
+
let hasChildren = false;
|
|
801
|
+
try {
|
|
802
|
+
hasChildren = readdirSync(full, { withFileTypes: true })
|
|
803
|
+
.some((c) => c.isDirectory() && !c.name.startsWith('.') && c.name !== 'node_modules');
|
|
804
|
+
} catch { /* unreadable */ }
|
|
805
|
+
return { name: e.name, path: full, hasChildren };
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
const parent = absPath === '/' ? null : resolve(absPath, '..');
|
|
809
|
+
res.json({ current: absPath, parent, dirs: entries });
|
|
810
|
+
} catch (err) {
|
|
811
|
+
res.status(500).json({ error: err.message });
|
|
812
|
+
}
|
|
813
|
+
});
|
|
814
|
+
|
|
723
815
|
// --- File Editor API ---
|
|
724
816
|
|
|
725
817
|
const LANG_MAP = {
|
|
@@ -740,6 +832,11 @@ export function createApi(app, daemon) {
|
|
|
740
832
|
|
|
741
833
|
const IGNORED_NAMES = new Set(['.git', 'node_modules', '.DS_Store', '.groove', '__pycache__', '.next', '.cache', 'dist', 'coverage']);
|
|
742
834
|
|
|
835
|
+
// Editor root directory — defaults to projectDir but can be changed at runtime
|
|
836
|
+
let editorRootDir = daemon.projectDir;
|
|
837
|
+
|
|
838
|
+
function getEditorRoot() { return editorRootDir; }
|
|
839
|
+
|
|
743
840
|
function validateFilePath(relPath, projectDir) {
|
|
744
841
|
if (!relPath || typeof relPath !== 'string') return { error: 'path is required' };
|
|
745
842
|
if (relPath.startsWith('/') || relPath.includes('..') || relPath.includes('\0')) {
|
|
@@ -750,6 +847,27 @@ export function createApi(app, daemon) {
|
|
|
750
847
|
return { fullPath };
|
|
751
848
|
}
|
|
752
849
|
|
|
850
|
+
// Get/set the editor working directory
|
|
851
|
+
app.get('/api/files/root', (req, res) => {
|
|
852
|
+
res.json({ root: editorRootDir });
|
|
853
|
+
});
|
|
854
|
+
|
|
855
|
+
app.post('/api/files/root', (req, res) => {
|
|
856
|
+
const { root } = req.body || {};
|
|
857
|
+
if (!root || typeof root !== 'string') return res.status(400).json({ error: 'root path is required' });
|
|
858
|
+
// Must be absolute and exist
|
|
859
|
+
if (!root.startsWith('/')) return res.status(400).json({ error: 'root must be an absolute path' });
|
|
860
|
+
if (root.includes('\0') || root.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
861
|
+
if (!existsSync(root)) return res.status(404).json({ error: 'Directory not found' });
|
|
862
|
+
try {
|
|
863
|
+
const stat = statSync(root);
|
|
864
|
+
if (!stat.isDirectory()) return res.status(400).json({ error: 'Path is not a directory' });
|
|
865
|
+
} catch { return res.status(400).json({ error: 'Cannot access directory' }); }
|
|
866
|
+
editorRootDir = root;
|
|
867
|
+
daemon.audit.log('editor.root.set', { root });
|
|
868
|
+
res.json({ ok: true, root: editorRootDir });
|
|
869
|
+
});
|
|
870
|
+
|
|
753
871
|
// File tree — returns dirs + files for a given path
|
|
754
872
|
app.get('/api/files/tree', (req, res) => {
|
|
755
873
|
const relPath = req.query.path || '';
|
|
@@ -759,8 +877,9 @@ export function createApi(app, daemon) {
|
|
|
759
877
|
return res.status(400).json({ error: 'Invalid path' });
|
|
760
878
|
}
|
|
761
879
|
|
|
762
|
-
const
|
|
763
|
-
|
|
880
|
+
const rootDir = getEditorRoot();
|
|
881
|
+
const fullPath = relPath ? resolve(rootDir, relPath) : rootDir;
|
|
882
|
+
if (!fullPath.startsWith(rootDir)) {
|
|
764
883
|
return res.status(400).json({ error: 'Path outside project' });
|
|
765
884
|
}
|
|
766
885
|
if (!existsSync(fullPath)) {
|
|
@@ -810,7 +929,7 @@ export function createApi(app, daemon) {
|
|
|
810
929
|
|
|
811
930
|
// Read file contents
|
|
812
931
|
app.get('/api/files/read', (req, res) => {
|
|
813
|
-
const result = validateFilePath(req.query.path,
|
|
932
|
+
const result = validateFilePath(req.query.path, getEditorRoot());
|
|
814
933
|
if (result.error) return res.status(400).json({ error: result.error });
|
|
815
934
|
|
|
816
935
|
if (!existsSync(result.fullPath)) {
|
|
@@ -846,7 +965,7 @@ export function createApi(app, daemon) {
|
|
|
846
965
|
// Write file contents
|
|
847
966
|
app.post('/api/files/write', (req, res) => {
|
|
848
967
|
const { path: relPath, content } = req.body;
|
|
849
|
-
const result = validateFilePath(relPath,
|
|
968
|
+
const result = validateFilePath(relPath, getEditorRoot());
|
|
850
969
|
if (result.error) return res.status(400).json({ error: result.error });
|
|
851
970
|
|
|
852
971
|
if (typeof content !== 'string') {
|
|
@@ -868,7 +987,7 @@ export function createApi(app, daemon) {
|
|
|
868
987
|
// Create a new file
|
|
869
988
|
app.post('/api/files/create', (req, res) => {
|
|
870
989
|
const { path: relPath, content = '' } = req.body;
|
|
871
|
-
const result = validateFilePath(relPath,
|
|
990
|
+
const result = validateFilePath(relPath, getEditorRoot());
|
|
872
991
|
if (result.error) return res.status(400).json({ error: result.error });
|
|
873
992
|
|
|
874
993
|
if (existsSync(result.fullPath)) {
|
|
@@ -893,7 +1012,7 @@ export function createApi(app, daemon) {
|
|
|
893
1012
|
// Create a new directory
|
|
894
1013
|
app.post('/api/files/mkdir', (req, res) => {
|
|
895
1014
|
const { path: relPath } = req.body;
|
|
896
|
-
const result = validateFilePath(relPath,
|
|
1015
|
+
const result = validateFilePath(relPath, getEditorRoot());
|
|
897
1016
|
if (result.error) return res.status(400).json({ error: result.error });
|
|
898
1017
|
|
|
899
1018
|
if (existsSync(result.fullPath)) {
|
|
@@ -912,7 +1031,7 @@ export function createApi(app, daemon) {
|
|
|
912
1031
|
// Delete a file or directory
|
|
913
1032
|
app.delete('/api/files/delete', (req, res) => {
|
|
914
1033
|
const relPath = req.query.path || req.body?.path;
|
|
915
|
-
const result = validateFilePath(relPath,
|
|
1034
|
+
const result = validateFilePath(relPath, getEditorRoot());
|
|
916
1035
|
if (result.error) return res.status(400).json({ error: result.error });
|
|
917
1036
|
|
|
918
1037
|
if (!existsSync(result.fullPath)) {
|
|
@@ -936,9 +1055,9 @@ export function createApi(app, daemon) {
|
|
|
936
1055
|
// Rename / move a file or directory
|
|
937
1056
|
app.post('/api/files/rename', (req, res) => {
|
|
938
1057
|
const { oldPath, newPath } = req.body;
|
|
939
|
-
const oldResult = validateFilePath(oldPath,
|
|
1058
|
+
const oldResult = validateFilePath(oldPath, getEditorRoot());
|
|
940
1059
|
if (oldResult.error) return res.status(400).json({ error: oldResult.error });
|
|
941
|
-
const newResult = validateFilePath(newPath,
|
|
1060
|
+
const newResult = validateFilePath(newPath, getEditorRoot());
|
|
942
1061
|
if (newResult.error) return res.status(400).json({ error: newResult.error });
|
|
943
1062
|
|
|
944
1063
|
if (!existsSync(oldResult.fullPath)) {
|
|
@@ -962,7 +1081,7 @@ export function createApi(app, daemon) {
|
|
|
962
1081
|
|
|
963
1082
|
// Serve raw file (images, video, etc.)
|
|
964
1083
|
app.get('/api/files/raw', (req, res) => {
|
|
965
|
-
const result = validateFilePath(req.query.path,
|
|
1084
|
+
const result = validateFilePath(req.query.path, getEditorRoot());
|
|
966
1085
|
if (result.error) return res.status(400).json({ error: result.error });
|
|
967
1086
|
|
|
968
1087
|
if (!existsSync(result.fullPath)) {
|
|
@@ -1033,9 +1152,25 @@ export function createApi(app, daemon) {
|
|
|
1033
1152
|
|
|
1034
1153
|
// --- Recommended Team (from planner) ---
|
|
1035
1154
|
|
|
1155
|
+
// Find recommended-team.json — check all agent working dirs, then daemon's grooveDir
|
|
1156
|
+
function findRecommendedTeam() {
|
|
1157
|
+
// Check agent working dirs first (planner may have written there)
|
|
1158
|
+
const agents = daemon.registry.getAll();
|
|
1159
|
+
for (const agent of agents) {
|
|
1160
|
+
if (agent.workingDir) {
|
|
1161
|
+
const p = resolve(agent.workingDir, '.groove', 'recommended-team.json');
|
|
1162
|
+
if (existsSync(p)) return p;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
// Fallback to daemon's .groove dir
|
|
1166
|
+
const p = resolve(daemon.grooveDir, 'recommended-team.json');
|
|
1167
|
+
if (existsSync(p)) return p;
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1036
1171
|
app.get('/api/recommended-team', (req, res) => {
|
|
1037
|
-
const teamPath =
|
|
1038
|
-
if (!
|
|
1172
|
+
const teamPath = findRecommendedTeam();
|
|
1173
|
+
if (!teamPath) {
|
|
1039
1174
|
return res.json({ exists: false, agents: [] });
|
|
1040
1175
|
}
|
|
1041
1176
|
try {
|
|
@@ -1047,8 +1182,8 @@ export function createApi(app, daemon) {
|
|
|
1047
1182
|
});
|
|
1048
1183
|
|
|
1049
1184
|
app.post('/api/recommended-team/launch', async (req, res) => {
|
|
1050
|
-
const teamPath =
|
|
1051
|
-
if (!
|
|
1185
|
+
const teamPath = findRecommendedTeam();
|
|
1186
|
+
if (!teamPath) {
|
|
1052
1187
|
return res.status(404).json({ error: 'No recommended team found. Run a planner first.' });
|
|
1053
1188
|
}
|
|
1054
1189
|
try {
|
|
@@ -1057,8 +1192,24 @@ export function createApi(app, daemon) {
|
|
|
1057
1192
|
return res.status(400).json({ error: 'Recommended team is empty' });
|
|
1058
1193
|
}
|
|
1059
1194
|
|
|
1195
|
+
const defaultDir = daemon.config?.defaultWorkingDir || undefined;
|
|
1196
|
+
|
|
1197
|
+
// Separate phase 1 (builders) and phase 2 (QC/finisher)
|
|
1198
|
+
const phase1 = agents.filter((a) => !a.phase || a.phase === 1);
|
|
1199
|
+
let phase2 = agents.filter((a) => a.phase === 2);
|
|
1200
|
+
|
|
1201
|
+
// Safety net: if planner forgot the QC agent, auto-add one
|
|
1202
|
+
if (phase2.length === 0 && phase1.length >= 2) {
|
|
1203
|
+
phase2 = [{
|
|
1204
|
+
role: 'fullstack', phase: 2, scope: [],
|
|
1205
|
+
prompt: 'QC Senior Dev: All builder agents have completed. Audit their changes for correctness, fix any issues, run tests, build the project, commit all changes, and launch. Output the localhost URL where the app can be accessed.',
|
|
1206
|
+
}];
|
|
1207
|
+
}
|
|
1208
|
+
|
|
1209
|
+
// Spawn phase 1 agents immediately
|
|
1060
1210
|
const spawned = [];
|
|
1061
|
-
|
|
1211
|
+
const phase1Ids = [];
|
|
1212
|
+
for (const config of phase1) {
|
|
1062
1213
|
const validated = validateAgentConfig({
|
|
1063
1214
|
role: config.role,
|
|
1064
1215
|
scope: config.scope || [],
|
|
@@ -1066,19 +1217,59 @@ export function createApi(app, daemon) {
|
|
|
1066
1217
|
provider: config.provider || 'claude-code',
|
|
1067
1218
|
model: config.model || 'auto',
|
|
1068
1219
|
permission: config.permission || 'auto',
|
|
1069
|
-
workingDir: config.workingDir ||
|
|
1220
|
+
workingDir: config.workingDir || defaultDir,
|
|
1221
|
+
name: config.name || undefined,
|
|
1070
1222
|
});
|
|
1071
1223
|
const agent = await daemon.processes.spawn(validated);
|
|
1072
1224
|
spawned.push({ id: agent.id, name: agent.name, role: agent.role });
|
|
1225
|
+
phase1Ids.push(agent.id);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
// If there are phase 2 agents, register them for auto-spawn on phase 1 completion
|
|
1229
|
+
if (phase2.length > 0 && phase1Ids.length > 0) {
|
|
1230
|
+
daemon._pendingPhase2 = daemon._pendingPhase2 || [];
|
|
1231
|
+
daemon._pendingPhase2.push({
|
|
1232
|
+
waitFor: phase1Ids,
|
|
1233
|
+
agents: phase2.map((c) => ({
|
|
1234
|
+
role: c.role, scope: c.scope || [], prompt: c.prompt || '',
|
|
1235
|
+
provider: c.provider || 'claude-code', model: c.model || 'auto',
|
|
1236
|
+
permission: c.permission || 'auto',
|
|
1237
|
+
workingDir: c.workingDir || defaultDir,
|
|
1238
|
+
name: c.name || undefined,
|
|
1239
|
+
})),
|
|
1240
|
+
});
|
|
1073
1241
|
}
|
|
1074
1242
|
|
|
1075
|
-
daemon.audit.log('team.launch', {
|
|
1076
|
-
|
|
1243
|
+
daemon.audit.log('team.launch', {
|
|
1244
|
+
phase1: spawned.length, phase2Pending: phase2.length,
|
|
1245
|
+
agents: spawned.map((a) => a.role),
|
|
1246
|
+
});
|
|
1247
|
+
res.json({ launched: spawned.length, phase2Pending: phase2.length, agents: spawned });
|
|
1077
1248
|
} catch (err) {
|
|
1078
1249
|
res.status(500).json({ error: err.message });
|
|
1079
1250
|
}
|
|
1080
1251
|
});
|
|
1081
1252
|
|
|
1253
|
+
// Clean up stale artifacts (old plans, recommended teams, etc.)
|
|
1254
|
+
app.post('/api/cleanup', (req, res) => {
|
|
1255
|
+
let cleaned = 0;
|
|
1256
|
+
// Clean recommended-team.json from all known locations
|
|
1257
|
+
const locations = [resolve(daemon.grooveDir, 'recommended-team.json')];
|
|
1258
|
+
for (const agent of daemon.registry.getAll()) {
|
|
1259
|
+
if (agent.workingDir) {
|
|
1260
|
+
locations.push(resolve(agent.workingDir, '.groove', 'recommended-team.json'));
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
const defaultDir = daemon.config?.defaultWorkingDir;
|
|
1264
|
+
if (defaultDir) locations.push(resolve(defaultDir, '.groove', 'recommended-team.json'));
|
|
1265
|
+
|
|
1266
|
+
for (const p of locations) {
|
|
1267
|
+
if (existsSync(p)) { try { unlinkSync(p); cleaned++; } catch { /* */ } }
|
|
1268
|
+
}
|
|
1269
|
+
daemon.audit.log('cleanup', { cleaned });
|
|
1270
|
+
res.json({ ok: true, cleaned });
|
|
1271
|
+
});
|
|
1272
|
+
|
|
1082
1273
|
// --- Command Center Dashboard ---
|
|
1083
1274
|
|
|
1084
1275
|
app.get('/api/dashboard', (req, res) => {
|
|
@@ -1259,7 +1450,7 @@ export function createApi(app, daemon) {
|
|
|
1259
1450
|
app.patch('/api/config', async (req, res) => {
|
|
1260
1451
|
const ALLOWED_KEYS = [
|
|
1261
1452
|
'port', 'journalistInterval', 'rotationThreshold', 'autoRotation',
|
|
1262
|
-
'qcThreshold', 'maxAgents', 'defaultProvider',
|
|
1453
|
+
'qcThreshold', 'maxAgents', 'defaultProvider', 'defaultWorkingDir',
|
|
1263
1454
|
];
|
|
1264
1455
|
for (const key of Object.keys(req.body)) {
|
|
1265
1456
|
if (!ALLOWED_KEYS.includes(key)) {
|
|
@@ -5,7 +5,7 @@ import { createServer as createHttpServer } from 'http';
|
|
|
5
5
|
import { createServer as createNetServer } from 'net';
|
|
6
6
|
import { execFileSync } from 'child_process';
|
|
7
7
|
import { resolve } from 'path';
|
|
8
|
-
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync } from 'fs';
|
|
8
|
+
import { readFileSync, writeFileSync, unlinkSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs';
|
|
9
9
|
import express from 'express';
|
|
10
10
|
import { WebSocketServer } from 'ws';
|
|
11
11
|
import { Registry } from './registry.js';
|
|
@@ -247,6 +247,15 @@ export class Daemon {
|
|
|
247
247
|
tester.listen(port, bindHost);
|
|
248
248
|
}).catch(() => false);
|
|
249
249
|
|
|
250
|
+
if (!(await checkPort(this.port))) {
|
|
251
|
+
// Wait for port release (e.g., after groove stop)
|
|
252
|
+
let retries = 5;
|
|
253
|
+
while (retries > 0 && !(await checkPort(this.port))) {
|
|
254
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
255
|
+
retries--;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
250
259
|
if (!(await checkPort(this.port))) {
|
|
251
260
|
const originalPort = this.port;
|
|
252
261
|
// Try next 10 ports
|
|
@@ -281,6 +290,7 @@ export class Daemon {
|
|
|
281
290
|
this.journalist.start();
|
|
282
291
|
this.rotator.start();
|
|
283
292
|
this.scheduler.start();
|
|
293
|
+
this._startGarbageCollector();
|
|
284
294
|
|
|
285
295
|
// Scan codebase for workspace/structure awareness
|
|
286
296
|
this.indexer.scan();
|
|
@@ -290,6 +300,62 @@ export class Daemon {
|
|
|
290
300
|
});
|
|
291
301
|
}
|
|
292
302
|
|
|
303
|
+
_startGarbageCollector() {
|
|
304
|
+
// Run once on startup, then every 24 hours
|
|
305
|
+
this._gc();
|
|
306
|
+
this._gcInterval = setInterval(() => this._gc(), 24 * 60 * 60 * 1000);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
_gc() {
|
|
310
|
+
const { grooveDir } = this;
|
|
311
|
+
let cleaned = 0;
|
|
312
|
+
|
|
313
|
+
try {
|
|
314
|
+
// 1. Clean old log files (>7 days, agent no longer exists)
|
|
315
|
+
const logsDir = resolve(grooveDir, 'logs');
|
|
316
|
+
if (existsSync(logsDir)) {
|
|
317
|
+
const now = Date.now();
|
|
318
|
+
const sevenDays = 7 * 24 * 60 * 60 * 1000;
|
|
319
|
+
for (const file of readdirSync(logsDir)) {
|
|
320
|
+
const p = resolve(logsDir, file);
|
|
321
|
+
try {
|
|
322
|
+
const age = now - statSync(p).mtimeMs;
|
|
323
|
+
if (age > sevenDays) { unlinkSync(p); cleaned++; }
|
|
324
|
+
} catch { /* skip */ }
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// 2. Clean stale recommended-team.json from daemon dir (not working dirs — those are user-managed)
|
|
329
|
+
// Only clean if no planner agent is currently running
|
|
330
|
+
const hasPlanner = this.registry.getAll().some((a) => a.role === 'planner' && (a.status === 'running' || a.status === 'starting'));
|
|
331
|
+
if (!hasPlanner) {
|
|
332
|
+
const teamFile = resolve(grooveDir, 'recommended-team.json');
|
|
333
|
+
if (existsSync(teamFile)) {
|
|
334
|
+
try {
|
|
335
|
+
const age = Date.now() - statSync(teamFile).mtimeMs;
|
|
336
|
+
if (age > 24 * 60 * 60 * 1000) { unlinkSync(teamFile); cleaned++; } // >24h old
|
|
337
|
+
} catch { /* skip */ }
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// 3. Prune audit log (keep last 1000 lines)
|
|
342
|
+
const auditFile = resolve(grooveDir, 'audit.log');
|
|
343
|
+
if (existsSync(auditFile)) {
|
|
344
|
+
try {
|
|
345
|
+
const lines = readFileSync(auditFile, 'utf8').split('\n');
|
|
346
|
+
if (lines.length > 1000) {
|
|
347
|
+
writeFileSync(auditFile, lines.slice(-1000).join('\n'));
|
|
348
|
+
cleaned++;
|
|
349
|
+
}
|
|
350
|
+
} catch { /* skip */ }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (cleaned > 0) {
|
|
354
|
+
this.audit.log('gc.run', { cleaned });
|
|
355
|
+
}
|
|
356
|
+
} catch { /* gc should never crash the daemon */ }
|
|
357
|
+
}
|
|
358
|
+
|
|
293
359
|
async stop() {
|
|
294
360
|
// Persist state before shutdown
|
|
295
361
|
this.state.set('agents', this.registry.getAll());
|
|
@@ -299,6 +365,7 @@ export class Daemon {
|
|
|
299
365
|
this.journalist.stop();
|
|
300
366
|
this.rotator.stop();
|
|
301
367
|
this.scheduler.stop();
|
|
368
|
+
if (this._gcInterval) clearInterval(this._gcInterval);
|
|
302
369
|
|
|
303
370
|
// Clean up file watchers and terminal sessions
|
|
304
371
|
this.fileWatcher.unwatchAll();
|