groove-dev 0.27.8 → 0.27.12
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/groove-icon.png +0 -0
- package/node_modules/@groove-dev/daemon/src/api.js +460 -25
- 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 +67 -7
- 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/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 +4 -4
- 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 +25 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +139 -6
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +38 -39
- 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 +460 -25
- 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 +67 -7
- 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/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 +4 -4
- 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 +25 -0
- package/packages/gui/src/lib/status.js +1 -0
- package/packages/gui/src/stores/groove.js +139 -6
- package/packages/gui/src/views/dashboard.jsx +38 -39
- 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/integrations/FEDERATION_PLAN.md +0 -583
- package/integrations/VOICE_PLAN.md +0 -232
- package/node_modules/@groove-dev/gui/dist/assets/index-CwmR3-HY.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DiCjVtQL.js +0 -652
- package/packages/gui/dist/assets/index-CwmR3-HY.css +0 -1
- package/packages/gui/dist/assets/index-DiCjVtQL.js +0 -652
- 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')
|
|
@@ -2171,6 +2350,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2171
2350
|
permission: config.permission || existing.permission || 'auto',
|
|
2172
2351
|
workingDir: existing.workingDir || projectWorkingDir,
|
|
2173
2352
|
name: existing.name,
|
|
2353
|
+
integrationApproval: config.integrationApproval || existing.integrationApproval || undefined,
|
|
2174
2354
|
});
|
|
2175
2355
|
validated.teamId = defaultTeamId;
|
|
2176
2356
|
const newAgent = await daemon.processes.spawn(validated);
|
|
@@ -2192,6 +2372,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2192
2372
|
permission: config.permission || 'auto',
|
|
2193
2373
|
workingDir: config.workingDir || projectWorkingDir,
|
|
2194
2374
|
name: config.name || undefined,
|
|
2375
|
+
integrationApproval: config.integrationApproval || undefined,
|
|
2195
2376
|
});
|
|
2196
2377
|
validated.teamId = defaultTeamId;
|
|
2197
2378
|
const agent = await daemon.processes.spawn(validated);
|
|
@@ -2412,17 +2593,17 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2412
2593
|
// --- Federation ---
|
|
2413
2594
|
|
|
2414
2595
|
// Federation status
|
|
2415
|
-
app.get('/api/federation', (req, res) => {
|
|
2596
|
+
app.get('/api/federation', proOnly, (req, res) => {
|
|
2416
2597
|
res.json(daemon.federation.getStatus());
|
|
2417
2598
|
});
|
|
2418
2599
|
|
|
2419
2600
|
// List peers
|
|
2420
|
-
app.get('/api/federation/peers', (req, res) => {
|
|
2601
|
+
app.get('/api/federation/peers', proOnly, (req, res) => {
|
|
2421
2602
|
res.json(daemon.federation.getPeers());
|
|
2422
2603
|
});
|
|
2423
2604
|
|
|
2424
2605
|
// Initiate pairing (local CLI calls this with the remote URL)
|
|
2425
|
-
app.post('/api/federation/initiate', async (req, res) => {
|
|
2606
|
+
app.post('/api/federation/initiate', proOnly, async (req, res) => {
|
|
2426
2607
|
try {
|
|
2427
2608
|
const { remoteUrl } = req.body;
|
|
2428
2609
|
if (!remoteUrl || typeof remoteUrl !== 'string') {
|
|
@@ -2436,7 +2617,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2436
2617
|
});
|
|
2437
2618
|
|
|
2438
2619
|
// Accept pairing (remote daemon calls this during key exchange)
|
|
2439
|
-
app.post('/api/federation/pair', (req, res) => {
|
|
2620
|
+
app.post('/api/federation/pair', proOnly, (req, res) => {
|
|
2440
2621
|
try {
|
|
2441
2622
|
const result = daemon.federation.acceptPairing(req.body);
|
|
2442
2623
|
res.json(result);
|
|
@@ -2446,7 +2627,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2446
2627
|
});
|
|
2447
2628
|
|
|
2448
2629
|
// Unpair a peer
|
|
2449
|
-
app.delete('/api/federation/peers/:id', (req, res) => {
|
|
2630
|
+
app.delete('/api/federation/peers/:id', proOnly, (req, res) => {
|
|
2450
2631
|
try {
|
|
2451
2632
|
daemon.federation.unpair(req.params.id);
|
|
2452
2633
|
res.json({ ok: true });
|
|
@@ -2456,7 +2637,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2456
2637
|
});
|
|
2457
2638
|
|
|
2458
2639
|
// Receive a signed contract from a peer
|
|
2459
|
-
app.post('/api/federation/contract', (req, res) => {
|
|
2640
|
+
app.post('/api/federation/contract', proOnly, (req, res) => {
|
|
2460
2641
|
try {
|
|
2461
2642
|
const { senderId, payload, signature } = req.body;
|
|
2462
2643
|
if (!senderId || !payload || !signature) {
|
|
@@ -2470,7 +2651,7 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2470
2651
|
});
|
|
2471
2652
|
|
|
2472
2653
|
// Send a contract to a peer (local agents call this)
|
|
2473
|
-
app.post('/api/federation/contract/send', async (req, res) => {
|
|
2654
|
+
app.post('/api/federation/contract/send', proOnly, async (req, res) => {
|
|
2474
2655
|
try {
|
|
2475
2656
|
const { peerId, contract } = req.body;
|
|
2476
2657
|
if (!peerId || !contract) {
|
|
@@ -2490,6 +2671,260 @@ Keep responses concise. Help them think, don't lecture them about the system the
|
|
|
2490
2671
|
res.json(daemon.audit.recent(limit));
|
|
2491
2672
|
});
|
|
2492
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
|
+
|
|
2493
2928
|
// --- Config ---
|
|
2494
2929
|
|
|
2495
2930
|
app.get('/api/config', (req, res) => {
|