groove-dev 0.27.7 → 0.27.11
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 -7
- package/node_modules/@groove-dev/daemon/src/api.js +496 -44
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +25 -12
- package/node_modules/@groove-dev/daemon/src/index.js +7 -0
- package/node_modules/@groove-dev/daemon/src/introducer.js +72 -4
- package/node_modules/@groove-dev/daemon/src/journalist.js +66 -11
- package/node_modules/@groove-dev/daemon/src/process.js +128 -104
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/daemon/src/repo-import.js +541 -0
- package/node_modules/@groove-dev/daemon/src/rotator.js +28 -1
- package/node_modules/@groove-dev/daemon/src/supervisor.js +2 -1
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +504 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +13 -0
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +5 -4
- package/node_modules/@groove-dev/daemon/test/rotator.test.js +4 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/src/app.css +14 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +13 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +16 -17
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +8 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/layout/app-shell.jsx +7 -1
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/node_modules/@groove-dev/gui/src/components/layout/command-palette.jsx +14 -4
- package/node_modules/@groove-dev/gui/src/components/layout/status-bar.jsx +46 -11
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/node_modules/@groove-dev/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/node_modules/@groove-dev/gui/src/components/pro/pro-gate.jsx +22 -0
- package/node_modules/@groove-dev/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +129 -0
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/node_modules/@groove-dev/gui/src/components/settings/server-dialog.jsx +192 -0
- package/node_modules/@groove-dev/gui/src/components/ui/approval-modal.jsx +63 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/lib/edition.js +4 -0
- package/node_modules/@groove-dev/gui/src/lib/electron.js +17 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +150 -6
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +39 -40
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +82 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +66 -0
- package/node_modules/@groove-dev/gui/vite.config.js +3 -0
- package/package.json +7 -2
- package/packages/daemon/src/api.js +496 -44
- package/packages/daemon/src/gateways/manager.js +25 -12
- package/packages/daemon/src/index.js +7 -0
- package/packages/daemon/src/introducer.js +72 -4
- package/packages/daemon/src/journalist.js +66 -11
- package/packages/daemon/src/process.js +128 -104
- package/packages/daemon/src/registry.js +1 -1
- package/packages/daemon/src/repo-import.js +541 -0
- package/packages/daemon/src/rotator.js +28 -1
- package/packages/daemon/src/supervisor.js +2 -1
- package/packages/daemon/src/tunnel-manager.js +504 -0
- package/packages/daemon/src/validate.js +13 -0
- package/packages/gui/dist/assets/index-BE6lYcd7.css +1 -0
- package/packages/gui/dist/assets/index-zdzOLAZM.js +677 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +2 -2
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-dialog.js +3 -3
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-scroll-area.js +1 -1
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tabs.js +5 -5
- package/packages/gui/node_modules/.vite/deps/@radix-ui_react-tooltip.js +3 -3
- package/packages/gui/node_modules/.vite/deps/_metadata.json +53 -53
- package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js → chunk-DH7AESXW.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-KXLIKZFX.js → chunk-GFE3G4IN.js} +133 -133
- package/packages/gui/node_modules/.vite/deps/chunk-GFE3G4IN.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js → chunk-LKZVMLRH.js} +6 -6
- package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js → chunk-MCVDVNE5.js} +2 -2
- package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js → chunk-SPKVQGZX.js} +6 -6
- package/packages/gui/src/app.css +14 -0
- package/packages/gui/src/app.jsx +13 -0
- package/packages/gui/src/components/agents/agent-config.jsx +130 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +2 -2
- package/packages/gui/src/components/agents/agent-mdfiles.jsx +43 -1
- package/packages/gui/src/components/agents/agent-node.jsx +16 -17
- package/packages/gui/src/components/agents/spawn-wizard.jsx +141 -1
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +3 -3
- package/packages/gui/src/components/dashboard/intel-panel.jsx +8 -8
- package/packages/gui/src/components/dashboard/routing-chart.jsx +3 -3
- package/packages/gui/src/components/dashboard/team-burn-panel.jsx +1 -1
- package/packages/gui/src/components/layout/activity-bar.jsx +4 -4
- package/packages/gui/src/components/layout/app-shell.jsx +7 -1
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +26 -8
- package/packages/gui/src/components/layout/command-palette.jsx +14 -4
- package/packages/gui/src/components/layout/status-bar.jsx +46 -11
- package/packages/gui/src/components/marketplace/repo-card.jsx +64 -0
- package/packages/gui/src/components/marketplace/repo-import.jsx +363 -0
- package/packages/gui/src/components/marketplace/repo-nuke-dialog.jsx +68 -0
- package/packages/gui/src/components/pro/pro-gate.jsx +22 -0
- package/packages/gui/src/components/pro/upgrade-card.jsx +48 -0
- package/packages/gui/src/components/settings/quick-connect.jsx +129 -0
- package/packages/gui/src/components/settings/remote-server-card.jsx +243 -0
- package/packages/gui/src/components/settings/server-dialog.jsx +192 -0
- package/packages/gui/src/components/ui/approval-modal.jsx +63 -0
- package/packages/gui/src/components/ui/toast.jsx +1 -1
- package/packages/gui/src/lib/edition.js +4 -0
- package/packages/gui/src/lib/electron.js +17 -0
- package/packages/gui/src/lib/status.js +1 -0
- package/packages/gui/src/stores/groove.js +150 -6
- package/packages/gui/src/views/dashboard.jsx +39 -40
- package/packages/gui/src/views/marketplace.jsx +82 -0
- package/packages/gui/src/views/settings.jsx +66 -0
- package/packages/gui/vite.config.js +3 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Bl1_J0sN.js +0 -652
- package/node_modules/@groove-dev/gui/dist/assets/index-DjORRpF0.css +0 -1
- package/packages/gui/dist/assets/index-Bl1_J0sN.js +0 -652
- package/packages/gui/dist/assets/index-DjORRpF0.css +0 -1
- package/packages/gui/node_modules/.vite/deps/chunk-KXLIKZFX.js.map +0 -7
- package/test-slack.mjs +0 -28
- /package/packages/gui/node_modules/.vite/deps/{chunk-WYSQD5ZG.js.map → chunk-DH7AESXW.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-3LBP22MX.js.map → chunk-LKZVMLRH.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-J6DMOQWP.js.map → chunk-MCVDVNE5.js.map} +0 -0
- /package/packages/gui/node_modules/.vite/deps/{chunk-3Q7HT7ZF.js.map → chunk-SPKVQGZX.js.map} +0 -0
|
@@ -4,13 +4,64 @@
|
|
|
4
4
|
import express from 'express';
|
|
5
5
|
import { resolve, dirname } from 'path';
|
|
6
6
|
import { fileURLToPath } from 'url';
|
|
7
|
-
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream } from 'fs';
|
|
7
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync, unlinkSync, renameSync, rmSync, createReadStream, copyFileSync } from 'fs';
|
|
8
8
|
import { lookup as mimeLookup } from './mimetypes.js';
|
|
9
9
|
import { listProviders, getProvider } from './providers/index.js';
|
|
10
10
|
import { OllamaProvider } from './providers/ollama.js';
|
|
11
11
|
import { validateAgentConfig } from './validate.js';
|
|
12
12
|
|
|
13
13
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const isPro = process.env.GROOVE_EDITION === 'pro';
|
|
15
|
+
|
|
16
|
+
let _subscriptionCache = { active: true, checkedAt: 0 };
|
|
17
|
+
|
|
18
|
+
function proOnly(req, res, next) {
|
|
19
|
+
if (!isPro) {
|
|
20
|
+
return res.status(403).json({
|
|
21
|
+
error: 'Pro feature',
|
|
22
|
+
edition: 'community',
|
|
23
|
+
upgrade: 'https://groovedev.ai/pro',
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
if (!_subscriptionCache.active) {
|
|
27
|
+
return res.status(403).json({
|
|
28
|
+
error: 'Pro subscription required',
|
|
29
|
+
edition: 'pro',
|
|
30
|
+
subscriptionActive: false,
|
|
31
|
+
upgrade: 'https://groovedev.ai/pro',
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
next();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function _executeApprovalRetry(daemon, approval) {
|
|
38
|
+
const rp = approval.retryPayload;
|
|
39
|
+
if (!rp) return;
|
|
40
|
+
try {
|
|
41
|
+
let resultText;
|
|
42
|
+
if (rp.type === 'integration_exec') {
|
|
43
|
+
const result = await daemon.mcpManager.execTool(rp.integrationId, rp.tool, rp.params);
|
|
44
|
+
resultText = JSON.stringify(result).slice(0, 2000);
|
|
45
|
+
daemon.audit.log('approval.autoRetry', { type: rp.type, integrationId: rp.integrationId, tool: rp.tool, agentId: rp.agentId, approvalId: approval.id });
|
|
46
|
+
} else if (rp.type === 'google_drive_upload') {
|
|
47
|
+
const result = await daemon.integrations.uploadToGoogleDrive(rp.filePath, {
|
|
48
|
+
name: rp.name, folderId: rp.folderId, convert: rp.convert !== false,
|
|
49
|
+
});
|
|
50
|
+
resultText = JSON.stringify(result).slice(0, 2000);
|
|
51
|
+
daemon.audit.log('approval.autoRetry', { type: rp.type, filePath: rp.filePath, agentId: rp.agentId, approvalId: approval.id });
|
|
52
|
+
} else {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (rp.agentId) {
|
|
56
|
+
await daemon.processes.sendMessage(rp.agentId, `Your ${rp.type === 'integration_exec' ? 'integration action' : 'upload'} was approved and executed successfully. Result: ${resultText}`);
|
|
57
|
+
}
|
|
58
|
+
} catch (err) {
|
|
59
|
+
console.log(`[Groove] Auto-retry for approval ${approval.id} failed: ${err.message}`);
|
|
60
|
+
if (rp.agentId) {
|
|
61
|
+
daemon.processes.sendMessage(rp.agentId, `Your ${rp.type === 'integration_exec' ? 'integration action' : 'upload'} was approved but execution failed: ${err.message}`).catch(() => {});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
14
65
|
|
|
15
66
|
export function createApi(app, daemon) {
|
|
16
67
|
// CORS — restrict to localhost + bound interface origins
|
|
@@ -493,6 +544,11 @@ export function createApi(app, daemon) {
|
|
|
493
544
|
res.json(suggestion);
|
|
494
545
|
});
|
|
495
546
|
|
|
547
|
+
// Edition
|
|
548
|
+
app.get('/api/edition', (req, res) => {
|
|
549
|
+
res.json({ edition: isPro ? 'pro' : 'community' });
|
|
550
|
+
});
|
|
551
|
+
|
|
496
552
|
// Daemon status
|
|
497
553
|
app.get('/api/status', (req, res) => {
|
|
498
554
|
res.json({
|
|
@@ -503,6 +559,7 @@ export function createApi(app, daemon) {
|
|
|
503
559
|
host: daemon.host,
|
|
504
560
|
port: daemon.port,
|
|
505
561
|
projectDir: daemon.projectDir,
|
|
562
|
+
edition: isPro ? 'pro' : 'community',
|
|
506
563
|
});
|
|
507
564
|
});
|
|
508
565
|
|
|
@@ -557,10 +614,15 @@ export function createApi(app, daemon) {
|
|
|
557
614
|
});
|
|
558
615
|
});
|
|
559
616
|
|
|
560
|
-
app.post('/api/approvals/:id/approve', (req, res) => {
|
|
617
|
+
app.post('/api/approvals/:id/approve', async (req, res) => {
|
|
561
618
|
const result = daemon.supervisor.approve(req.params.id);
|
|
562
619
|
if (!result) return res.status(404).json({ error: 'Approval not found' });
|
|
563
620
|
daemon.audit.log('approval.approve', { id: req.params.id });
|
|
621
|
+
if (result.retryPayload) {
|
|
622
|
+
_executeApprovalRetry(daemon, result).catch((err) => {
|
|
623
|
+
console.log(`[Groove] Approval auto-retry failed: ${err.message}`);
|
|
624
|
+
});
|
|
625
|
+
}
|
|
564
626
|
res.json(result);
|
|
565
627
|
});
|
|
566
628
|
|
|
@@ -774,7 +836,7 @@ export function createApi(app, daemon) {
|
|
|
774
836
|
if (entry.endsWith('.md') && !entry.startsWith('.')) {
|
|
775
837
|
const fullPath = resolve(dir, entry);
|
|
776
838
|
if (statSync(fullPath).isFile()) {
|
|
777
|
-
files.push({ name: entry, path: entry, size: statSync(fullPath).size });
|
|
839
|
+
files.push({ name: entry, path: entry, size: statSync(fullPath).size, source: 'project' });
|
|
778
840
|
}
|
|
779
841
|
}
|
|
780
842
|
}
|
|
@@ -784,13 +846,37 @@ export function createApi(app, daemon) {
|
|
|
784
846
|
if (entry.endsWith('.md')) {
|
|
785
847
|
const fullPath = resolve(grooveDir, entry);
|
|
786
848
|
if (statSync(fullPath).isFile()) {
|
|
787
|
-
files.push({ name: entry, path: `.groove/${entry}`, size: statSync(fullPath).size });
|
|
849
|
+
files.push({ name: entry, path: `.groove/${entry}`, size: statSync(fullPath).size, source: 'project' });
|
|
788
850
|
}
|
|
789
851
|
}
|
|
790
852
|
}
|
|
791
853
|
}
|
|
792
854
|
} catch { /* dir might not exist */ }
|
|
793
855
|
|
|
856
|
+
// Include personality file from .groove/personalities/
|
|
857
|
+
try {
|
|
858
|
+
const personalityFile = resolve(daemon.grooveDir, 'personalities', `${agent.name}.md`);
|
|
859
|
+
if (existsSync(personalityFile)) {
|
|
860
|
+
const size = statSync(personalityFile).size;
|
|
861
|
+
files.unshift({ name: 'personality.md', path: '__personality__', size, source: 'personality' });
|
|
862
|
+
}
|
|
863
|
+
} catch { /* ignore */ }
|
|
864
|
+
|
|
865
|
+
// Include user-created agent files from .groove/agent-files/<name>/
|
|
866
|
+
try {
|
|
867
|
+
const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
|
|
868
|
+
if (existsSync(agentFilesDir)) {
|
|
869
|
+
for (const entry of readdirSync(agentFilesDir)) {
|
|
870
|
+
if (entry.endsWith('.md')) {
|
|
871
|
+
const fullPath = resolve(agentFilesDir, entry);
|
|
872
|
+
if (statSync(fullPath).isFile()) {
|
|
873
|
+
files.push({ name: entry, path: `__user__/${entry}`, size: statSync(fullPath).size, source: 'user' });
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
} catch { /* ignore */ }
|
|
879
|
+
|
|
794
880
|
res.json({ files, workingDir: dir });
|
|
795
881
|
});
|
|
796
882
|
|
|
@@ -803,6 +889,22 @@ export function createApi(app, daemon) {
|
|
|
803
889
|
const relPath = req.query.path;
|
|
804
890
|
if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
805
891
|
|
|
892
|
+
if (relPath === '__personality__') {
|
|
893
|
+
const personalityFile = resolve(daemon.grooveDir, 'personalities', `${agent.name}.md`);
|
|
894
|
+
if (existsSync(personalityFile)) {
|
|
895
|
+
return res.json({ content: readFileSync(personalityFile, 'utf8') });
|
|
896
|
+
}
|
|
897
|
+
return res.json({ content: '' });
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
if (relPath.startsWith('__user__/')) {
|
|
901
|
+
const fileName = relPath.slice('__user__/'.length);
|
|
902
|
+
if (!fileName || fileName.includes('/') || fileName.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
903
|
+
const filePath = resolve(daemon.grooveDir, 'agent-files', agent.name, fileName);
|
|
904
|
+
if (existsSync(filePath)) return res.json({ content: readFileSync(filePath, 'utf8') });
|
|
905
|
+
return res.json({ content: '' });
|
|
906
|
+
}
|
|
907
|
+
|
|
806
908
|
const fullPath = resolve(dir, relPath);
|
|
807
909
|
if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
|
|
808
910
|
|
|
@@ -824,6 +926,24 @@ export function createApi(app, daemon) {
|
|
|
824
926
|
if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
825
927
|
if (typeof content !== 'string') return res.status(400).json({ error: 'Content required' });
|
|
826
928
|
|
|
929
|
+
if (relPath === '__personality__') {
|
|
930
|
+
const personalityDir = resolve(daemon.grooveDir, 'personalities');
|
|
931
|
+
mkdirSync(personalityDir, { recursive: true });
|
|
932
|
+
writeFileSync(resolve(personalityDir, `${agent.name}.md`), content || '', { mode: 0o600 });
|
|
933
|
+
daemon.audit.log('personality.update', { name: agent.name, agentId: agent.id });
|
|
934
|
+
return res.json({ saved: true });
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
if (relPath.startsWith('__user__/')) {
|
|
938
|
+
const fileName = relPath.slice('__user__/'.length);
|
|
939
|
+
if (!fileName || fileName.includes('/') || fileName.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
940
|
+
const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
|
|
941
|
+
mkdirSync(agentFilesDir, { recursive: true });
|
|
942
|
+
writeFileSync(resolve(agentFilesDir, fileName), content || '', { mode: 0o600 });
|
|
943
|
+
daemon.audit.log('mdfile.write.user', { agentId: agent.id, name: fileName });
|
|
944
|
+
return res.json({ saved: true });
|
|
945
|
+
}
|
|
946
|
+
|
|
827
947
|
const fullPath = resolve(dir, relPath);
|
|
828
948
|
if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
|
|
829
949
|
|
|
@@ -836,6 +956,24 @@ export function createApi(app, daemon) {
|
|
|
836
956
|
}
|
|
837
957
|
});
|
|
838
958
|
|
|
959
|
+
// Create a new MD file for an agent
|
|
960
|
+
app.post('/api/agents/:id/mdfiles/create', (req, res) => {
|
|
961
|
+
const agent = daemon.registry.get(req.params.id);
|
|
962
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
963
|
+
let name = req.body?.name;
|
|
964
|
+
if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name required' });
|
|
965
|
+
name = name.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
|
|
966
|
+
if (!name) return res.status(400).json({ error: 'Invalid name' });
|
|
967
|
+
if (!name.endsWith('.md')) name += '.md';
|
|
968
|
+
const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
|
|
969
|
+
mkdirSync(agentFilesDir, { recursive: true });
|
|
970
|
+
const filePath = resolve(agentFilesDir, name);
|
|
971
|
+
if (existsSync(filePath)) return res.status(409).json({ error: 'File already exists' });
|
|
972
|
+
writeFileSync(filePath, '', { mode: 0o600 });
|
|
973
|
+
daemon.audit.log('mdfile.create', { agentId: agent.id, name });
|
|
974
|
+
res.json({ name, path: `__user__/${name}` });
|
|
975
|
+
});
|
|
976
|
+
|
|
839
977
|
// Rotation stats
|
|
840
978
|
app.get('/api/rotation', (req, res) => {
|
|
841
979
|
res.json({
|
|
@@ -1145,6 +1283,34 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1145
1283
|
res.json({ id: agent.id, skills });
|
|
1146
1284
|
});
|
|
1147
1285
|
|
|
1286
|
+
// --- Agent Repos (attach/detach) ---
|
|
1287
|
+
|
|
1288
|
+
app.post('/api/agents/:agentId/repos/:importId', (req, res) => {
|
|
1289
|
+
const agent = daemon.registry.get(req.params.agentId);
|
|
1290
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
1291
|
+
const importId = req.params.importId;
|
|
1292
|
+
const manifest = daemon.repoImporter.getImport(importId);
|
|
1293
|
+
if (!manifest || manifest.status !== 'active') {
|
|
1294
|
+
return res.status(400).json({ error: 'Repo not found or not active' });
|
|
1295
|
+
}
|
|
1296
|
+
const repos = agent.repos || [];
|
|
1297
|
+
if (repos.includes(importId)) {
|
|
1298
|
+
return res.json({ id: agent.id, repos });
|
|
1299
|
+
}
|
|
1300
|
+
daemon.registry.update(agent.id, { repos: [...repos, importId] });
|
|
1301
|
+
daemon.audit.log('repo.attach', { agentId: agent.id, importId });
|
|
1302
|
+
res.json({ id: agent.id, repos: [...repos, importId] });
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
app.delete('/api/agents/:agentId/repos/:importId', (req, res) => {
|
|
1306
|
+
const agent = daemon.registry.get(req.params.agentId);
|
|
1307
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
1308
|
+
const repos = (agent.repos || []).filter((r) => r !== req.params.importId);
|
|
1309
|
+
daemon.registry.update(agent.id, { repos });
|
|
1310
|
+
daemon.audit.log('repo.detach', { agentId: agent.id, importId: req.params.importId });
|
|
1311
|
+
res.json({ id: agent.id, repos });
|
|
1312
|
+
});
|
|
1313
|
+
|
|
1148
1314
|
// --- Integrations ---
|
|
1149
1315
|
|
|
1150
1316
|
// Google OAuth routes MUST come before parameterized :id routes
|
|
@@ -1336,12 +1502,18 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1336
1502
|
tool,
|
|
1337
1503
|
params: paramsSummary,
|
|
1338
1504
|
description: `${entry.name}: ${tool}`,
|
|
1505
|
+
}, {
|
|
1506
|
+
type: 'integration_exec',
|
|
1507
|
+
integrationId,
|
|
1508
|
+
tool,
|
|
1509
|
+
params: params || {},
|
|
1510
|
+
agentId: agentId || null,
|
|
1339
1511
|
});
|
|
1340
1512
|
daemon.audit.log('integration.exec.blocked', { integrationId, tool, approvalId: approval.id, agentId });
|
|
1341
1513
|
return res.status(202).json({
|
|
1342
1514
|
requiresApproval: true,
|
|
1343
1515
|
approvalId: approval.id,
|
|
1344
|
-
message: `Tool "${tool}" requires approval.
|
|
1516
|
+
message: `Tool "${tool}" requires approval. The user will be prompted automatically. You will receive the result once approved — do not retry.`,
|
|
1345
1517
|
});
|
|
1346
1518
|
}
|
|
1347
1519
|
}
|
|
@@ -1390,12 +1562,19 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
1390
1562
|
filePath,
|
|
1391
1563
|
name: name || filePath.split('/').pop(),
|
|
1392
1564
|
description: `Upload to Google Drive: ${name || filePath.split('/').pop()}`,
|
|
1565
|
+
}, {
|
|
1566
|
+
type: 'google_drive_upload',
|
|
1567
|
+
filePath,
|
|
1568
|
+
name: name || filePath.split('/').pop(),
|
|
1569
|
+
folderId: folderId || null,
|
|
1570
|
+
convert: convert !== false,
|
|
1571
|
+
agentId: agentId || null,
|
|
1393
1572
|
});
|
|
1394
1573
|
daemon.audit.log('integration.upload.blocked', { filePath, approvalId: approval.id, agentId });
|
|
1395
1574
|
return res.status(202).json({
|
|
1396
1575
|
requiresApproval: true,
|
|
1397
1576
|
approvalId: approval.id,
|
|
1398
|
-
message: `Upload requires approval.
|
|
1577
|
+
message: `Upload requires approval. The user will be prompted automatically. You will receive the result once approved — do not retry.`,
|
|
1399
1578
|
});
|
|
1400
1579
|
}
|
|
1401
1580
|
}
|
|
@@ -2036,27 +2215,27 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2036
2215
|
for (const agent of agents) {
|
|
2037
2216
|
if (agent.workingDir) {
|
|
2038
2217
|
const p = resolve(agent.workingDir, '.groove', 'recommended-team.json');
|
|
2039
|
-
if (existsSync(p)) return p;
|
|
2218
|
+
if (existsSync(p)) return { path: p, teamId: agent.teamId || null, agentId: agent.id || null };
|
|
2040
2219
|
}
|
|
2041
2220
|
}
|
|
2042
2221
|
// Fallback to daemon's .groove dir
|
|
2043
2222
|
const p = resolve(daemon.grooveDir, 'recommended-team.json');
|
|
2044
|
-
if (existsSync(p)) return p;
|
|
2223
|
+
if (existsSync(p)) return { path: p, teamId: null, agentId: null };
|
|
2045
2224
|
return null;
|
|
2046
2225
|
}
|
|
2047
2226
|
|
|
2048
2227
|
app.get('/api/recommended-team', (req, res) => {
|
|
2049
|
-
const
|
|
2050
|
-
if (!
|
|
2228
|
+
const found = findRecommendedTeam();
|
|
2229
|
+
if (!found) {
|
|
2051
2230
|
return res.json({ exists: false, agents: [] });
|
|
2052
2231
|
}
|
|
2053
2232
|
try {
|
|
2054
|
-
const raw = JSON.parse(readFileSync(
|
|
2233
|
+
const raw = JSON.parse(readFileSync(found.path, 'utf8'));
|
|
2055
2234
|
// Support both old format (bare array) and new format ({ projectDir, agents })
|
|
2056
2235
|
if (Array.isArray(raw)) {
|
|
2057
|
-
res.json({ exists: true, agents: raw });
|
|
2236
|
+
res.json({ exists: true, agents: raw, teamId: found.teamId });
|
|
2058
2237
|
} else if (raw && Array.isArray(raw.agents)) {
|
|
2059
|
-
res.json({ exists: true, agents: raw.agents, projectDir: raw.projectDir || null });
|
|
2238
|
+
res.json({ exists: true, agents: raw.agents, projectDir: raw.projectDir || null, teamId: found.teamId });
|
|
2060
2239
|
} else {
|
|
2061
2240
|
res.json({ exists: false, agents: [] });
|
|
2062
2241
|
}
|
|
@@ -2066,12 +2245,12 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2066
2245
|
});
|
|
2067
2246
|
|
|
2068
2247
|
app.post('/api/recommended-team/launch', async (req, res) => {
|
|
2069
|
-
const
|
|
2070
|
-
if (!
|
|
2248
|
+
const found = findRecommendedTeam();
|
|
2249
|
+
if (!found) {
|
|
2071
2250
|
return res.status(404).json({ error: 'No recommended team found. Run a planner first.' });
|
|
2072
2251
|
}
|
|
2073
2252
|
try {
|
|
2074
|
-
const raw = JSON.parse(readFileSync(
|
|
2253
|
+
const raw = JSON.parse(readFileSync(found.path, 'utf8'));
|
|
2075
2254
|
|
|
2076
2255
|
// Support both old format (bare array) and new format ({ projectDir, agents })
|
|
2077
2256
|
let agentConfigs;
|
|
@@ -2092,8 +2271,8 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2092
2271
|
const baseDir = daemon.config?.defaultWorkingDir || daemon.projectDir;
|
|
2093
2272
|
|
|
2094
2273
|
// Use the planner's teamId so launched agents join the correct team.
|
|
2095
|
-
//
|
|
2096
|
-
let launchTeamId = req.body?.teamId || null;
|
|
2274
|
+
// Priority: explicit from frontend > agent that wrote the file > most recent planner > default
|
|
2275
|
+
let launchTeamId = req.body?.teamId || found.teamId || null;
|
|
2097
2276
|
if (!launchTeamId) {
|
|
2098
2277
|
const planners = daemon.registry.getAll()
|
|
2099
2278
|
.filter((a) => a.role === 'planner')
|
|
@@ -2143,16 +2322,21 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2143
2322
|
!reused.some((r) => r.id === a.id)
|
|
2144
2323
|
);
|
|
2145
2324
|
|
|
2146
|
-
if (existing
|
|
2147
|
-
//
|
|
2148
|
-
//
|
|
2149
|
-
//
|
|
2325
|
+
if (existing) {
|
|
2326
|
+
// Role already exists in this team — never spawn a duplicate.
|
|
2327
|
+
// With a prompt: kill+respawn with fresh context and the new task.
|
|
2328
|
+
// Without a prompt: keep the existing agent as-is (the planner often
|
|
2329
|
+
// emits Mode-1 shaped JSON with empty prompts on follow-up; if we
|
|
2330
|
+
// let that fall through to "spawn new", we get 2 backends, 2 fronts).
|
|
2331
|
+
if (!prompt) {
|
|
2332
|
+
reused.push({ id: existing.id, name: existing.name, role: existing.role, reusedFrom: existing.name });
|
|
2333
|
+
phase1Ids.push(existing.id);
|
|
2334
|
+
continue;
|
|
2335
|
+
}
|
|
2150
2336
|
try {
|
|
2151
|
-
// Kill old process if running
|
|
2152
2337
|
if (existing.status === 'running' || existing.status === 'starting') {
|
|
2153
2338
|
try { await daemon.processes.kill(existing.id); } catch { /* already dead */ }
|
|
2154
2339
|
}
|
|
2155
|
-
// Remove old entry
|
|
2156
2340
|
daemon.registry.remove(existing.id);
|
|
2157
2341
|
daemon.locks.release(existing.id);
|
|
2158
2342
|
|
|
@@ -2166,6 +2350,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2166
2350
|
permission: config.permission || existing.permission || 'auto',
|
|
2167
2351
|
workingDir: existing.workingDir || projectWorkingDir,
|
|
2168
2352
|
name: existing.name,
|
|
2353
|
+
integrationApproval: config.integrationApproval || existing.integrationApproval || undefined,
|
|
2169
2354
|
});
|
|
2170
2355
|
validated.teamId = defaultTeamId;
|
|
2171
2356
|
const newAgent = await daemon.processes.spawn(validated);
|
|
@@ -2176,7 +2361,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2176
2361
|
failed.push({ role: config.role, error: `reuse failed: ${err.message}` });
|
|
2177
2362
|
}
|
|
2178
2363
|
} else {
|
|
2179
|
-
// No matching
|
|
2364
|
+
// No matching agent — spawn a new one
|
|
2180
2365
|
try {
|
|
2181
2366
|
const validated = validateAgentConfig({
|
|
2182
2367
|
role: config.role,
|
|
@@ -2187,6 +2372,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2187
2372
|
permission: config.permission || 'auto',
|
|
2188
2373
|
workingDir: config.workingDir || projectWorkingDir,
|
|
2189
2374
|
name: config.name || undefined,
|
|
2375
|
+
integrationApproval: config.integrationApproval || undefined,
|
|
2190
2376
|
});
|
|
2191
2377
|
validated.teamId = defaultTeamId;
|
|
2192
2378
|
const agent = await daemon.processes.spawn(validated);
|
|
@@ -2201,18 +2387,30 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2201
2387
|
|
|
2202
2388
|
// Phase 2 agents also scoped to projectWorkingDir
|
|
2203
2389
|
if (phase2.length > 0 && phase1Ids.length > 0) {
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2390
|
+
// Dedup: if a running idle fullstack already exists in this team,
|
|
2391
|
+
// skip the phase2 queue — _triggerIdleQC will notify it when phase 1 completes
|
|
2392
|
+
const existingQC = teamAgents.find((a) =>
|
|
2393
|
+
a.role === 'fullstack' &&
|
|
2394
|
+
(a.status === 'running' || a.status === 'starting')
|
|
2395
|
+
);
|
|
2396
|
+
const qcIsIdle = existingQC && (daemon.journalist?.getAgentFiles(existingQC) || []).length === 0;
|
|
2397
|
+
|
|
2398
|
+
if (existingQC && qcIsIdle) {
|
|
2399
|
+
daemon.audit.log('phase2.skipQueue', { existingQC: existingQC.id, name: existingQC.name, reason: 'idle fullstack exists' });
|
|
2400
|
+
} else {
|
|
2401
|
+
daemon._pendingPhase2 = daemon._pendingPhase2 || [];
|
|
2402
|
+
daemon._pendingPhase2.push({
|
|
2403
|
+
waitFor: phase1Ids,
|
|
2404
|
+
agents: phase2.map((c) => ({
|
|
2405
|
+
role: c.role, scope: c.scope || [], prompt: c.prompt || '',
|
|
2406
|
+
provider: c.provider || undefined, model: c.model || 'auto',
|
|
2407
|
+
permission: c.permission || 'auto',
|
|
2408
|
+
workingDir: c.workingDir || projectWorkingDir,
|
|
2409
|
+
name: c.name || undefined,
|
|
2410
|
+
teamId: defaultTeamId,
|
|
2411
|
+
})),
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2216
2414
|
}
|
|
2217
2415
|
|
|
2218
2416
|
daemon.audit.log('team.launch', {
|
|
@@ -2395,17 +2593,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2395
2593
|
// --- Federation ---
|
|
2396
2594
|
|
|
2397
2595
|
// Federation status
|
|
2398
|
-
app.get('/api/federation', (req, res) => {
|
|
2596
|
+
app.get('/api/federation', proOnly, (req, res) => {
|
|
2399
2597
|
res.json(daemon.federation.getStatus());
|
|
2400
2598
|
});
|
|
2401
2599
|
|
|
2402
2600
|
// List peers
|
|
2403
|
-
app.get('/api/federation/peers', (req, res) => {
|
|
2601
|
+
app.get('/api/federation/peers', proOnly, (req, res) => {
|
|
2404
2602
|
res.json(daemon.federation.getPeers());
|
|
2405
2603
|
});
|
|
2406
2604
|
|
|
2407
2605
|
// Initiate pairing (local CLI calls this with the remote URL)
|
|
2408
|
-
app.post('/api/federation/initiate', async (req, res) => {
|
|
2606
|
+
app.post('/api/federation/initiate', proOnly, async (req, res) => {
|
|
2409
2607
|
try {
|
|
2410
2608
|
const { remoteUrl } = req.body;
|
|
2411
2609
|
if (!remoteUrl || typeof remoteUrl !== 'string') {
|
|
@@ -2419,7 +2617,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2419
2617
|
});
|
|
2420
2618
|
|
|
2421
2619
|
// Accept pairing (remote daemon calls this during key exchange)
|
|
2422
|
-
app.post('/api/federation/pair', (req, res) => {
|
|
2620
|
+
app.post('/api/federation/pair', proOnly, (req, res) => {
|
|
2423
2621
|
try {
|
|
2424
2622
|
const result = daemon.federation.acceptPairing(req.body);
|
|
2425
2623
|
res.json(result);
|
|
@@ -2429,7 +2627,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2429
2627
|
});
|
|
2430
2628
|
|
|
2431
2629
|
// Unpair a peer
|
|
2432
|
-
app.delete('/api/federation/peers/:id', (req, res) => {
|
|
2630
|
+
app.delete('/api/federation/peers/:id', proOnly, (req, res) => {
|
|
2433
2631
|
try {
|
|
2434
2632
|
daemon.federation.unpair(req.params.id);
|
|
2435
2633
|
res.json({ ok: true });
|
|
@@ -2439,7 +2637,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2439
2637
|
});
|
|
2440
2638
|
|
|
2441
2639
|
// Receive a signed contract from a peer
|
|
2442
|
-
app.post('/api/federation/contract', (req, res) => {
|
|
2640
|
+
app.post('/api/federation/contract', proOnly, (req, res) => {
|
|
2443
2641
|
try {
|
|
2444
2642
|
const { senderId, payload, signature } = req.body;
|
|
2445
2643
|
if (!senderId || !payload || !signature) {
|
|
@@ -2453,7 +2651,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2453
2651
|
});
|
|
2454
2652
|
|
|
2455
2653
|
// Send a contract to a peer (local agents call this)
|
|
2456
|
-
app.post('/api/federation/contract/send', async (req, res) => {
|
|
2654
|
+
app.post('/api/federation/contract/send', proOnly, async (req, res) => {
|
|
2457
2655
|
try {
|
|
2458
2656
|
const { peerId, contract } = req.body;
|
|
2459
2657
|
if (!peerId || !contract) {
|
|
@@ -2473,6 +2671,260 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2473
2671
|
res.json(daemon.audit.recent(limit));
|
|
2474
2672
|
});
|
|
2475
2673
|
|
|
2674
|
+
// --- Repo Import ---
|
|
2675
|
+
|
|
2676
|
+
app.post('/api/repos/preview', async (req, res) => {
|
|
2677
|
+
try {
|
|
2678
|
+
const { repoUrl } = req.body;
|
|
2679
|
+
if (!repoUrl || typeof repoUrl !== 'string') {
|
|
2680
|
+
return res.status(400).json({ error: 'repoUrl is required (string)' });
|
|
2681
|
+
}
|
|
2682
|
+
const result = await daemon.repoImporter.preview(repoUrl);
|
|
2683
|
+
res.json(result);
|
|
2684
|
+
} catch (err) {
|
|
2685
|
+
res.status(400).json({ error: err.message });
|
|
2686
|
+
}
|
|
2687
|
+
});
|
|
2688
|
+
|
|
2689
|
+
app.post('/api/repos/import', async (req, res) => {
|
|
2690
|
+
try {
|
|
2691
|
+
const { repoUrl, targetPath, createTeam, teamName } = req.body;
|
|
2692
|
+
if (!repoUrl || typeof repoUrl !== 'string') {
|
|
2693
|
+
return res.status(400).json({ error: 'repoUrl is required (string)' });
|
|
2694
|
+
}
|
|
2695
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
2696
|
+
return res.status(400).json({ error: 'targetPath is required (string)' });
|
|
2697
|
+
}
|
|
2698
|
+
|
|
2699
|
+
// Resolve shell shortcuts — GUI sends ~/... and ./...
|
|
2700
|
+
let resolvedPath = targetPath;
|
|
2701
|
+
if (resolvedPath.startsWith('~/') || resolvedPath === '~') {
|
|
2702
|
+
resolvedPath = resolve(process.env.HOME || '/tmp', resolvedPath.slice(2));
|
|
2703
|
+
} else if (!resolvedPath.startsWith('/')) {
|
|
2704
|
+
resolvedPath = resolve(daemon.projectDir, resolvedPath);
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2707
|
+
const result = await daemon.repoImporter.import(repoUrl, resolvedPath, {});
|
|
2708
|
+
|
|
2709
|
+
let teamId = null;
|
|
2710
|
+
if (createTeam) {
|
|
2711
|
+
try {
|
|
2712
|
+
const team = daemon.teams.create(teamName || result.stackInfo?.name || 'imported-repo');
|
|
2713
|
+
teamId = team.id;
|
|
2714
|
+
const manifest = daemon.repoImporter.getImport(result.importId);
|
|
2715
|
+
if (manifest) {
|
|
2716
|
+
manifest.teamId = teamId;
|
|
2717
|
+
daemon.repoImporter._saveManifest(manifest);
|
|
2718
|
+
}
|
|
2719
|
+
} catch { /* team creation is optional */ }
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
// Spawn setup agent
|
|
2723
|
+
let agentId = null;
|
|
2724
|
+
try {
|
|
2725
|
+
const setupPrompt = daemon.repoImporter.generateSetupPrompt(resolvedPath, result.stackInfo, '');
|
|
2726
|
+
const agent = await daemon.processes.spawn({
|
|
2727
|
+
role: 'fullstack',
|
|
2728
|
+
name: `setup-${result.importId.slice(0, 4)}`,
|
|
2729
|
+
workingDir: resolvedPath,
|
|
2730
|
+
prompt: setupPrompt,
|
|
2731
|
+
provider: daemon.config?.defaultProvider || 'claude-code',
|
|
2732
|
+
});
|
|
2733
|
+
agentId = agent.id;
|
|
2734
|
+
const manifest = daemon.repoImporter.getImport(result.importId);
|
|
2735
|
+
if (manifest) {
|
|
2736
|
+
manifest.agents.push(agentId);
|
|
2737
|
+
daemon.repoImporter._saveManifest(manifest);
|
|
2738
|
+
}
|
|
2739
|
+
} catch { /* agent spawn is best-effort */ }
|
|
2740
|
+
|
|
2741
|
+
res.json({ importId: result.importId, path: result.path, agentId, teamId });
|
|
2742
|
+
} catch (err) {
|
|
2743
|
+
res.status(400).json({ error: err.message });
|
|
2744
|
+
}
|
|
2745
|
+
});
|
|
2746
|
+
|
|
2747
|
+
app.get('/api/repos/imported', (req, res) => {
|
|
2748
|
+
res.json(daemon.repoImporter.getImported());
|
|
2749
|
+
});
|
|
2750
|
+
|
|
2751
|
+
app.get('/api/repos/:id', (req, res) => {
|
|
2752
|
+
const manifest = daemon.repoImporter.getImport(req.params.id);
|
|
2753
|
+
if (!manifest) return res.status(404).json({ error: 'Import not found' });
|
|
2754
|
+
res.json(manifest);
|
|
2755
|
+
});
|
|
2756
|
+
|
|
2757
|
+
app.get('/api/repos/:id/sandbox', (req, res) => {
|
|
2758
|
+
const manifest = daemon.repoImporter.getImport(req.params.id);
|
|
2759
|
+
if (!manifest) return res.status(404).json({ error: 'Import not found' });
|
|
2760
|
+
res.json(manifest);
|
|
2761
|
+
});
|
|
2762
|
+
|
|
2763
|
+
app.post('/api/repos/:id/process', (req, res) => {
|
|
2764
|
+
try {
|
|
2765
|
+
const { pid, command } = req.body;
|
|
2766
|
+
daemon.repoImporter.recordProcess(req.params.id, pid, command);
|
|
2767
|
+
res.json({ ok: true });
|
|
2768
|
+
} catch (err) {
|
|
2769
|
+
res.status(400).json({ error: err.message });
|
|
2770
|
+
}
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
app.delete('/api/repos/:id/remove', async (req, res) => {
|
|
2774
|
+
try {
|
|
2775
|
+
await daemon.repoImporter.softRemove(req.params.id);
|
|
2776
|
+
res.json({ ok: true });
|
|
2777
|
+
} catch (err) {
|
|
2778
|
+
res.status(400).json({ error: err.message });
|
|
2779
|
+
}
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
app.delete('/api/repos/:id/nuke', async (req, res) => {
|
|
2783
|
+
try {
|
|
2784
|
+
const deleteFiles = req.query.deleteFiles !== 'false';
|
|
2785
|
+
await daemon.repoImporter.hardNuke(req.params.id, { deleteFiles });
|
|
2786
|
+
res.json({ ok: true });
|
|
2787
|
+
} catch (err) {
|
|
2788
|
+
res.status(400).json({ error: err.message });
|
|
2789
|
+
}
|
|
2790
|
+
});
|
|
2791
|
+
|
|
2792
|
+
// --- Personalities ---
|
|
2793
|
+
|
|
2794
|
+
app.get('/api/personalities', (req, res) => {
|
|
2795
|
+
const dir = resolve(daemon.grooveDir, 'personalities');
|
|
2796
|
+
mkdirSync(dir, { recursive: true });
|
|
2797
|
+
try {
|
|
2798
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.md'));
|
|
2799
|
+
const personalities = files.map(f => ({
|
|
2800
|
+
name: f.replace(/\.md$/, ''),
|
|
2801
|
+
}));
|
|
2802
|
+
res.json({ personalities });
|
|
2803
|
+
} catch {
|
|
2804
|
+
res.json({ personalities: [] });
|
|
2805
|
+
}
|
|
2806
|
+
});
|
|
2807
|
+
|
|
2808
|
+
app.get('/api/personalities/:name', (req, res) => {
|
|
2809
|
+
const name = req.params.name.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
2810
|
+
if (!name) return res.status(400).json({ error: 'Invalid name' });
|
|
2811
|
+
const file = resolve(daemon.grooveDir, 'personalities', `${name}.md`);
|
|
2812
|
+
if (!existsSync(file)) return res.status(404).json({ error: 'Personality not found' });
|
|
2813
|
+
res.json({ name, content: readFileSync(file, 'utf8') });
|
|
2814
|
+
});
|
|
2815
|
+
|
|
2816
|
+
app.put('/api/personalities/:name', (req, res) => {
|
|
2817
|
+
const name = req.params.name.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
2818
|
+
if (!name) return res.status(400).json({ error: 'Invalid name' });
|
|
2819
|
+
const content = typeof req.body?.content === 'string' ? req.body.content.slice(0, 10000) : '';
|
|
2820
|
+
const dir = resolve(daemon.grooveDir, 'personalities');
|
|
2821
|
+
mkdirSync(dir, { recursive: true });
|
|
2822
|
+
writeFileSync(resolve(dir, `${name}.md`), content, { mode: 0o600 });
|
|
2823
|
+
daemon.audit.log('personality.update', { name });
|
|
2824
|
+
res.json({ name, content });
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
app.post('/api/personalities/:name/clone', (req, res) => {
|
|
2828
|
+
const source = req.params.name.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
2829
|
+
const target = (req.body?.name || '').replace(/[^a-zA-Z0-9_-]/g, '');
|
|
2830
|
+
if (!source || !target) return res.status(400).json({ error: 'Source and target name required' });
|
|
2831
|
+
const dir = resolve(daemon.grooveDir, 'personalities');
|
|
2832
|
+
const sourceFile = resolve(dir, `${source}.md`);
|
|
2833
|
+
if (!existsSync(sourceFile)) return res.status(404).json({ error: 'Source personality not found' });
|
|
2834
|
+
copyFileSync(sourceFile, resolve(dir, `${target}.md`));
|
|
2835
|
+
daemon.audit.log('personality.clone', { source, target });
|
|
2836
|
+
res.json({ name: target, clonedFrom: source });
|
|
2837
|
+
});
|
|
2838
|
+
|
|
2839
|
+
// --- Tunnels (Remote Access) ---
|
|
2840
|
+
|
|
2841
|
+
app.get('/api/tunnels', proOnly, (req, res) => {
|
|
2842
|
+
res.json(daemon.tunnelManager.getSaved());
|
|
2843
|
+
});
|
|
2844
|
+
|
|
2845
|
+
app.post('/api/tunnels', proOnly, (req, res) => {
|
|
2846
|
+
try {
|
|
2847
|
+
const { name, host, user, port, sshKeyPath, autoStart, autoConnect } = req.body;
|
|
2848
|
+
if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name is required (string)' });
|
|
2849
|
+
if (!host || typeof host !== 'string') return res.status(400).json({ error: 'host is required (string)' });
|
|
2850
|
+
const result = daemon.tunnelManager.save({ name, host, user, port, sshKeyPath, autoStart, autoConnect });
|
|
2851
|
+
res.json(result);
|
|
2852
|
+
} catch (err) {
|
|
2853
|
+
res.status(400).json({ error: err.message });
|
|
2854
|
+
}
|
|
2855
|
+
});
|
|
2856
|
+
|
|
2857
|
+
app.patch('/api/tunnels/:id', proOnly, (req, res) => {
|
|
2858
|
+
try {
|
|
2859
|
+
const result = daemon.tunnelManager.update(req.params.id, req.body);
|
|
2860
|
+
res.json(result);
|
|
2861
|
+
} catch (err) {
|
|
2862
|
+
res.status(400).json({ error: err.message });
|
|
2863
|
+
}
|
|
2864
|
+
});
|
|
2865
|
+
|
|
2866
|
+
app.delete('/api/tunnels/:id', proOnly, (req, res) => {
|
|
2867
|
+
try {
|
|
2868
|
+
daemon.tunnelManager.delete(req.params.id);
|
|
2869
|
+
res.json({ ok: true });
|
|
2870
|
+
} catch (err) {
|
|
2871
|
+
res.status(400).json({ error: err.message });
|
|
2872
|
+
}
|
|
2873
|
+
});
|
|
2874
|
+
|
|
2875
|
+
app.post('/api/tunnels/:id/test', proOnly, async (req, res) => {
|
|
2876
|
+
try {
|
|
2877
|
+
const result = await daemon.tunnelManager.test(req.params.id);
|
|
2878
|
+
res.json(result);
|
|
2879
|
+
} catch (err) {
|
|
2880
|
+
res.status(400).json({ error: err.message });
|
|
2881
|
+
}
|
|
2882
|
+
});
|
|
2883
|
+
|
|
2884
|
+
app.post('/api/tunnels/:id/connect', proOnly, async (req, res) => {
|
|
2885
|
+
try {
|
|
2886
|
+
const result = await daemon.tunnelManager.connect(req.params.id);
|
|
2887
|
+
res.json(result);
|
|
2888
|
+
} catch (err) {
|
|
2889
|
+
const body = { error: err.message };
|
|
2890
|
+
if (err.testResult) body.testResult = err.testResult;
|
|
2891
|
+
res.status(400).json(body);
|
|
2892
|
+
}
|
|
2893
|
+
});
|
|
2894
|
+
|
|
2895
|
+
app.post('/api/tunnels/:id/disconnect', proOnly, async (req, res) => {
|
|
2896
|
+
try {
|
|
2897
|
+
await daemon.tunnelManager.disconnect(req.params.id);
|
|
2898
|
+
res.json({ ok: true });
|
|
2899
|
+
} catch (err) {
|
|
2900
|
+
res.status(400).json({ error: err.message });
|
|
2901
|
+
}
|
|
2902
|
+
});
|
|
2903
|
+
|
|
2904
|
+
app.post('/api/tunnels/:id/install', proOnly, async (req, res) => {
|
|
2905
|
+
try {
|
|
2906
|
+
const result = await daemon.tunnelManager.remoteInstall(req.params.id);
|
|
2907
|
+
res.json(result);
|
|
2908
|
+
} catch (err) {
|
|
2909
|
+
res.status(400).json({ error: err.message });
|
|
2910
|
+
}
|
|
2911
|
+
});
|
|
2912
|
+
|
|
2913
|
+
app.post('/api/tunnels/:id/start', proOnly, async (req, res) => {
|
|
2914
|
+
try {
|
|
2915
|
+
await daemon.tunnelManager.autoStart(req.params.id);
|
|
2916
|
+
res.json({ ok: true });
|
|
2917
|
+
} catch (err) {
|
|
2918
|
+
res.status(400).json({ error: err.message });
|
|
2919
|
+
}
|
|
2920
|
+
});
|
|
2921
|
+
|
|
2922
|
+
app.get('/api/tunnels/:id/status', proOnly, (req, res) => {
|
|
2923
|
+
const s = daemon.tunnelManager.getStatus(req.params.id);
|
|
2924
|
+
if (!s) return res.status(404).json({ error: 'Remote not found' });
|
|
2925
|
+
res.json(s);
|
|
2926
|
+
});
|
|
2927
|
+
|
|
2476
2928
|
// --- Config ---
|
|
2477
2929
|
|
|
2478
2930
|
app.get('/api/config', (req, res) => {
|