groove-dev 0.27.143 → 0.27.145
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/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +1086 -6532
- package/node_modules/@groove-dev/daemon/src/conversations.js +18 -48
- package/node_modules/@groove-dev/daemon/src/gateways/manager.js +35 -1
- package/node_modules/@groove-dev/daemon/src/index.js +3 -0
- package/node_modules/@groove-dev/daemon/src/journalist.js +23 -13
- package/node_modules/@groove-dev/daemon/src/mlx-server.js +365 -0
- package/node_modules/@groove-dev/daemon/src/model-lab.js +308 -12
- package/node_modules/@groove-dev/daemon/src/pm.js +1 -1
- package/node_modules/@groove-dev/daemon/src/process.js +2 -2
- package/node_modules/@groove-dev/daemon/src/providers/local.js +36 -8
- package/node_modules/@groove-dev/daemon/src/registry.js +21 -5
- package/node_modules/@groove-dev/daemon/src/routes/agents.js +812 -0
- package/node_modules/@groove-dev/daemon/src/routes/coordination.js +318 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +751 -0
- package/node_modules/@groove-dev/daemon/src/routes/integrations.js +485 -0
- package/node_modules/@groove-dev/daemon/src/routes/network.js +1784 -0
- package/node_modules/@groove-dev/daemon/src/routes/providers.js +755 -0
- package/node_modules/@groove-dev/daemon/src/routes/schedules.js +110 -0
- package/node_modules/@groove-dev/daemon/src/routes/teams.js +650 -0
- package/node_modules/@groove-dev/daemon/src/scheduler.js +456 -24
- package/node_modules/@groove-dev/daemon/src/teams.js +1 -1
- package/node_modules/@groove-dev/daemon/src/validate.js +38 -1
- package/node_modules/@groove-dev/daemon/templates/mlx-setup.json +12 -0
- package/node_modules/@groove-dev/daemon/templates/tgi-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/templates/vllm-setup.json +1 -1
- package/node_modules/@groove-dev/daemon/test/introducer.test.js +3 -3
- package/node_modules/@groove-dev/daemon/test/journalist.test.js +7 -10
- package/node_modules/@groove-dev/daemon/test/registry.test.js +38 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-Bxc0gU06.js +1006 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-C0pztKBn.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/{app.jsx → App.jsx} +0 -2
- package/node_modules/@groove-dev/gui/src/app.css +35 -0
- package/node_modules/@groove-dev/gui/src/components/agents/agent-config.jsx +1 -128
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +210 -112
- package/node_modules/@groove-dev/gui/src/components/agents/agent-node.jsx +8 -13
- package/node_modules/@groove-dev/gui/src/components/agents/agent-panel.jsx +2 -70
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +159 -122
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/node_modules/@groove-dev/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/node_modules/@groove-dev/gui/src/components/automations/automation-card.jsx +274 -0
- package/node_modules/@groove-dev/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +68 -66
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +4 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/node_modules/@groove-dev/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/node_modules/@groove-dev/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/node_modules/@groove-dev/gui/src/components/dashboard/kpi-card.jsx +3 -3
- 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/dashboard/token-chart.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +39 -31
- package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/node_modules/@groove-dev/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/node_modules/@groove-dev/gui/src/components/lab/parameter-panel.jsx +200 -18
- package/node_modules/@groove-dev/gui/src/components/lab/preset-manager.jsx +17 -14
- package/node_modules/@groove-dev/gui/src/components/lab/runtime-config.jsx +335 -152
- package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +10 -8
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +2 -4
- package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/node_modules/@groove-dev/gui/src/components/network/network-health.jsx +2 -2
- package/node_modules/@groove-dev/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/node_modules/@groove-dev/gui/src/components/ui/sheet.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +8 -8
- package/node_modules/@groove-dev/gui/src/lib/cron.js +64 -0
- package/node_modules/@groove-dev/gui/src/lib/status.js +25 -24
- package/node_modules/@groove-dev/gui/src/lib/theme-hex.js +1 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +51 -3144
- package/node_modules/@groove-dev/gui/src/stores/helpers.js +10 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/agents-slice.js +459 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/automations-slice.js +96 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/chat-slice.js +226 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/editor-slice.js +285 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/network-slice.js +361 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/preview-slice.js +109 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/providers-slice.js +897 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/teams-slice.js +413 -0
- package/node_modules/@groove-dev/gui/src/stores/slices/ui-slice.js +98 -0
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +5 -5
- package/node_modules/@groove-dev/gui/src/views/dashboard.jsx +12 -13
- package/node_modules/@groove-dev/gui/src/views/marketplace.jsx +191 -3
- package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +54 -12
- package/node_modules/@groove-dev/gui/src/views/models.jsx +419 -496
- package/node_modules/@groove-dev/gui/src/views/network.jsx +3 -3
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +81 -94
- package/node_modules/@groove-dev/gui/src/views/teams.jsx +40 -483
- package/node_modules/axios/CHANGELOG.md +260 -0
- package/node_modules/axios/README.md +595 -223
- package/node_modules/axios/dist/axios.js +1460 -1090
- package/node_modules/axios/dist/axios.js.map +1 -1
- package/node_modules/axios/dist/axios.min.js +3 -3
- package/node_modules/axios/dist/axios.min.js.map +1 -1
- package/node_modules/axios/dist/browser/axios.cjs +1560 -1132
- package/node_modules/axios/dist/browser/axios.cjs.map +1 -1
- package/node_modules/axios/dist/esm/axios.js +1557 -1128
- package/node_modules/axios/dist/esm/axios.js.map +1 -1
- package/node_modules/axios/dist/esm/axios.min.js +2 -2
- package/node_modules/axios/dist/esm/axios.min.js.map +1 -1
- package/node_modules/axios/dist/node/axios.cjs +1594 -1057
- package/node_modules/axios/dist/node/axios.cjs.map +1 -1
- package/node_modules/axios/index.d.cts +40 -41
- package/node_modules/axios/index.d.ts +151 -227
- package/node_modules/axios/index.js +2 -0
- package/node_modules/axios/lib/adapters/adapters.js +4 -2
- package/node_modules/axios/lib/adapters/fetch.js +147 -16
- package/node_modules/axios/lib/adapters/http.js +306 -58
- package/node_modules/axios/lib/adapters/xhr.js +6 -2
- package/node_modules/axios/lib/core/Axios.js +7 -3
- package/node_modules/axios/lib/core/AxiosError.js +120 -34
- package/node_modules/axios/lib/core/AxiosHeaders.js +27 -25
- package/node_modules/axios/lib/core/buildFullPath.js +1 -1
- package/node_modules/axios/lib/core/dispatchRequest.js +19 -7
- package/node_modules/axios/lib/core/mergeConfig.js +21 -4
- package/node_modules/axios/lib/core/settle.js +7 -11
- package/node_modules/axios/lib/defaults/index.js +14 -9
- package/node_modules/axios/lib/env/data.js +1 -1
- package/node_modules/axios/lib/helpers/AxiosURLSearchParams.js +1 -2
- package/node_modules/axios/lib/helpers/buildURL.js +1 -1
- package/node_modules/axios/lib/helpers/cookies.js +14 -2
- package/node_modules/axios/lib/helpers/estimateDataURLDecodedBytes.js +28 -1
- package/node_modules/axios/lib/helpers/formDataToJSON.js +3 -1
- package/node_modules/axios/lib/helpers/formDataToStream.js +3 -2
- package/node_modules/axios/lib/helpers/parseProtocol.js +1 -1
- package/node_modules/axios/lib/helpers/progressEventReducer.js +5 -5
- package/node_modules/axios/lib/helpers/resolveConfig.js +54 -18
- package/node_modules/axios/lib/helpers/shouldBypassProxy.js +74 -2
- package/node_modules/axios/lib/helpers/toFormData.js +10 -2
- package/node_modules/axios/lib/helpers/validator.js +3 -1
- package/node_modules/axios/lib/utils.js +33 -21
- package/node_modules/axios/package.json +17 -24
- package/node_modules/follow-redirects/README.md +7 -5
- package/node_modules/follow-redirects/index.js +24 -1
- package/node_modules/follow-redirects/package.json +1 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +1086 -6532
- package/packages/daemon/src/conversations.js +18 -48
- package/packages/daemon/src/gateways/manager.js +35 -1
- package/packages/daemon/src/index.js +3 -0
- package/packages/daemon/src/journalist.js +23 -13
- package/packages/daemon/src/mlx-server.js +365 -0
- package/packages/daemon/src/model-lab.js +308 -12
- package/packages/daemon/src/pm.js +1 -1
- package/packages/daemon/src/process.js +2 -2
- package/packages/daemon/src/providers/local.js +36 -8
- package/packages/daemon/src/registry.js +21 -5
- package/packages/daemon/src/routes/agents.js +812 -0
- package/packages/daemon/src/routes/coordination.js +318 -0
- package/packages/daemon/src/routes/files.js +751 -0
- package/packages/daemon/src/routes/integrations.js +485 -0
- package/packages/daemon/src/routes/network.js +1784 -0
- package/packages/daemon/src/routes/providers.js +755 -0
- package/packages/daemon/src/routes/schedules.js +110 -0
- package/packages/daemon/src/routes/teams.js +650 -0
- package/packages/daemon/src/scheduler.js +456 -24
- package/packages/daemon/src/teams.js +1 -1
- package/packages/daemon/src/validate.js +38 -1
- package/packages/daemon/templates/mlx-setup.json +12 -0
- package/packages/daemon/templates/tgi-setup.json +1 -1
- package/packages/daemon/templates/vllm-setup.json +1 -1
- package/packages/gui/dist/assets/index-Bxc0gU06.js +1006 -0
- package/packages/gui/dist/assets/index-C0pztKBn.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/{app.jsx → App.jsx} +0 -2
- package/packages/gui/src/app.css +35 -0
- package/packages/gui/src/components/agents/agent-config.jsx +1 -128
- package/packages/gui/src/components/agents/agent-feed.jsx +210 -112
- package/packages/gui/src/components/agents/agent-node.jsx +8 -13
- package/packages/gui/src/components/agents/agent-panel.jsx +2 -70
- package/packages/gui/src/components/agents/code-review.jsx +159 -122
- package/packages/gui/src/components/agents/diff-viewer.jsx +23 -23
- package/packages/gui/src/components/agents/journalist-panel.jsx +1 -1
- package/packages/gui/src/components/agents/spawn-wizard.jsx +2 -135
- package/packages/gui/src/components/automations/automation-card.jsx +274 -0
- package/packages/gui/src/components/automations/automation-wizard.jsx +1136 -0
- package/packages/gui/src/components/chat/chat-header.jsx +2 -0
- package/packages/gui/src/components/chat/chat-input.jsx +68 -66
- package/packages/gui/src/components/chat/chat-view.jsx +4 -8
- package/packages/gui/src/components/dashboard/activity-feed.jsx +3 -3
- package/packages/gui/src/components/dashboard/cache-ring.jsx +5 -5
- package/packages/gui/src/components/dashboard/context-gauges.jsx +6 -8
- package/packages/gui/src/components/dashboard/fleet-panel.jsx +8 -14
- package/packages/gui/src/components/dashboard/intel-panel.jsx +238 -656
- package/packages/gui/src/components/dashboard/kpi-card.jsx +3 -3
- 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/dashboard/token-chart.jsx +4 -4
- package/packages/gui/src/components/lab/chat-playground.jsx +39 -31
- package/packages/gui/src/components/lab/lab-assistant.jsx +316 -82
- package/packages/gui/src/components/lab/metrics-panel.jsx +187 -32
- package/packages/gui/src/components/lab/parameter-panel.jsx +200 -18
- package/packages/gui/src/components/lab/preset-manager.jsx +17 -14
- package/packages/gui/src/components/lab/runtime-config.jsx +335 -152
- package/packages/gui/src/components/lab/system-prompt-editor.jsx +10 -8
- package/packages/gui/src/components/layout/activity-bar.jsx +2 -4
- package/packages/gui/src/components/layout/terminal-panel.jsx +4 -2
- package/packages/gui/src/components/layout/welcome-splash.jsx +137 -108
- package/packages/gui/src/components/network/network-health.jsx +2 -2
- package/packages/gui/src/components/network/performance-dashboard.jsx +4 -4
- package/packages/gui/src/components/settings/ssh-wizard.jsx +81 -99
- package/packages/gui/src/components/ui/sheet.jsx +5 -2
- package/packages/gui/src/components/ui/slider.jsx +8 -8
- package/packages/gui/src/lib/cron.js +64 -0
- package/packages/gui/src/lib/status.js +25 -24
- package/packages/gui/src/lib/theme-hex.js +1 -0
- package/packages/gui/src/stores/groove.js +51 -3144
- package/packages/gui/src/stores/helpers.js +10 -0
- package/packages/gui/src/stores/slices/agents-slice.js +459 -0
- package/packages/gui/src/stores/slices/automations-slice.js +96 -0
- package/packages/gui/src/stores/slices/chat-slice.js +226 -0
- package/packages/gui/src/stores/slices/editor-slice.js +285 -0
- package/packages/gui/src/stores/slices/marketplace-slice.js +461 -0
- package/packages/gui/src/stores/slices/network-slice.js +361 -0
- package/packages/gui/src/stores/slices/preview-slice.js +109 -0
- package/packages/gui/src/stores/slices/providers-slice.js +897 -0
- package/packages/gui/src/stores/slices/teams-slice.js +413 -0
- package/packages/gui/src/stores/slices/ui-slice.js +98 -0
- package/packages/gui/src/views/agents.jsx +5 -5
- package/packages/gui/src/views/dashboard.jsx +12 -13
- package/packages/gui/src/views/marketplace.jsx +191 -3
- package/packages/gui/src/views/model-lab.jsx +54 -12
- package/packages/gui/src/views/models.jsx +419 -496
- package/packages/gui/src/views/network.jsx +3 -3
- package/packages/gui/src/views/settings.jsx +81 -94
- package/packages/gui/src/views/teams.jsx +40 -483
- package/SECURITY_SWEEP.md +0 -228
- package/TRAINING_DATA_v4.md +0 -6
- package/node_modules/@groove-dev/gui/dist/assets/index-CCVvAoQn.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-DGIv_TRm.js +0 -984
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +0 -379
- package/node_modules/@groove-dev/gui/src/views/preview.jsx +0 -6
- package/node_modules/@groove-dev/gui/src/views/subscription-panel.jsx +0 -327
- package/packages/gui/dist/assets/index-CCVvAoQn.css +0 -1
- package/packages/gui/dist/assets/index-DGIv_TRm.js +0 -984
- package/packages/gui/src/components/agents/agent-chat.jsx +0 -379
- package/packages/gui/src/views/preview.jsx +0 -6
- package/packages/gui/src/views/subscription-panel.jsx +0 -327
- package/test.py +0 -571
|
@@ -0,0 +1,812 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
|
|
3
|
+
import { resolve } from 'path';
|
|
4
|
+
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync, mkdirSync } from 'fs';
|
|
5
|
+
import { validateAgentConfig, validateReasoningEffort, validateVerbosity } from '../validate.js';
|
|
6
|
+
import { ROLE_INTEGRATIONS, wrapWithRoleReminder } from '../process.js';
|
|
7
|
+
import { getProvider } from '../providers/index.js';
|
|
8
|
+
|
|
9
|
+
export function registerAgentRoutes(app, daemon) {
|
|
10
|
+
// List all agents
|
|
11
|
+
app.get('/api/agents', (req, res) => {
|
|
12
|
+
res.json(daemon.registry.getAll());
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// Get single agent
|
|
16
|
+
app.get('/api/agents/:id', (req, res) => {
|
|
17
|
+
const agent = daemon.registry.get(req.params.id);
|
|
18
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
19
|
+
res.json(agent);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Spawn a new agent
|
|
23
|
+
app.post('/api/agents', async (req, res) => {
|
|
24
|
+
try {
|
|
25
|
+
const config = validateAgentConfig(req.body);
|
|
26
|
+
config.teamId = req.body.teamId || daemon.teams.getDefault()?.id || null;
|
|
27
|
+
// Inherit team working directory if agent doesn't specify one
|
|
28
|
+
if (!config.workingDir) {
|
|
29
|
+
const team = daemon.teams.get(config.teamId);
|
|
30
|
+
if (team?.workingDir) config.workingDir = team.workingDir;
|
|
31
|
+
}
|
|
32
|
+
// Inherit configured defaults if the request didn't pick them
|
|
33
|
+
if (!config.provider && daemon.config?.defaultProvider) {
|
|
34
|
+
config.provider = daemon.config.defaultProvider;
|
|
35
|
+
}
|
|
36
|
+
if (!config.model && daemon.config?.defaultModel) {
|
|
37
|
+
config.model = daemon.config.defaultModel;
|
|
38
|
+
}
|
|
39
|
+
const agent = await daemon.processes.spawn(config);
|
|
40
|
+
daemon.audit.log('agent.spawn', { id: agent.id, role: agent.role, provider: agent.provider });
|
|
41
|
+
res.status(201).json(agent);
|
|
42
|
+
} catch (err) {
|
|
43
|
+
res.status(400).json({ error: err.message });
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Update agent
|
|
48
|
+
app.patch('/api/agents/:id', (req, res) => {
|
|
49
|
+
const agent = daemon.registry.update(req.params.id, req.body);
|
|
50
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
51
|
+
res.json(agent);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Kill an agent (add ?purge=true to also remove from registry)
|
|
55
|
+
app.delete('/api/agents/:id', async (req, res) => {
|
|
56
|
+
try {
|
|
57
|
+
const agent = daemon.registry.get(req.params.id);
|
|
58
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
59
|
+
|
|
60
|
+
// Always attempt kill — handles race where GUI sees 'running' but daemon
|
|
61
|
+
// already marked the agent completed (common with fast non-interactive
|
|
62
|
+
// providers like Gemini). processes.kill() is a no-op when no handle exists.
|
|
63
|
+
await daemon.processes.kill(req.params.id);
|
|
64
|
+
|
|
65
|
+
// Only purge from registry when explicitly requested.
|
|
66
|
+
// Killed/completed agents stay visible so the user can review output.
|
|
67
|
+
const purge = req.query.purge === 'true';
|
|
68
|
+
if (purge) {
|
|
69
|
+
daemon.registry.remove(req.params.id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
daemon.audit.log('agent.kill', { id: agent.id, role: agent.role, purged: purge });
|
|
73
|
+
res.json({ ok: true, purged: purge });
|
|
74
|
+
} catch (err) {
|
|
75
|
+
res.status(400).json({ error: err.message });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Kill all agents and purge registry (used by groove nuke)
|
|
80
|
+
app.delete('/api/agents', async (req, res) => {
|
|
81
|
+
const count = daemon.processes.getRunningCount();
|
|
82
|
+
await daemon.processes.killAll();
|
|
83
|
+
// Purge all agents from registry — kill() no longer does this automatically
|
|
84
|
+
for (const agent of daemon.registry.getAll()) {
|
|
85
|
+
daemon.registry.remove(agent.id);
|
|
86
|
+
}
|
|
87
|
+
daemon.audit.log('agent.kill_all', { count });
|
|
88
|
+
res.json({ ok: true });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// --- Role-to-Integration Mapping ---
|
|
92
|
+
|
|
93
|
+
app.get('/api/roles/integrations', (req, res) => {
|
|
94
|
+
const roleFilter = req.query.role;
|
|
95
|
+
const entries = roleFilter ? { [roleFilter]: ROLE_INTEGRATIONS[roleFilter] || [] } : ROLE_INTEGRATIONS;
|
|
96
|
+
const result = {};
|
|
97
|
+
for (const [role, ids] of Object.entries(entries)) {
|
|
98
|
+
result[role] = (ids || []).map((id) => {
|
|
99
|
+
const status = daemon.integrations.getStatus(id);
|
|
100
|
+
const entry = daemon.integrations.registry.find((r) => r.id === id);
|
|
101
|
+
return {
|
|
102
|
+
id,
|
|
103
|
+
name: entry?.name || id,
|
|
104
|
+
installed: status?.installed || false,
|
|
105
|
+
configured: status?.configured || false,
|
|
106
|
+
authenticated: status?.authenticated || false,
|
|
107
|
+
};
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (roleFilter) return res.json(result[roleFilter] || []);
|
|
111
|
+
res.json(result);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
app.post('/api/agents/preflight', (req, res) => {
|
|
115
|
+
const { role, integrations } = req.body || {};
|
|
116
|
+
if (!role || !Array.isArray(integrations)) {
|
|
117
|
+
return res.status(400).json({ error: 'role and integrations[] required' });
|
|
118
|
+
}
|
|
119
|
+
const issues = [];
|
|
120
|
+
for (const id of integrations) {
|
|
121
|
+
const status = daemon.integrations.getStatus(id);
|
|
122
|
+
const entry = daemon.integrations.registry.find((r) => r.id === id);
|
|
123
|
+
const name = entry?.name || id;
|
|
124
|
+
if (!status || !status.installed) {
|
|
125
|
+
issues.push({ integrationId: id, name, problem: 'not_installed' });
|
|
126
|
+
} else if (!status.configured) {
|
|
127
|
+
issues.push({ integrationId: id, name, problem: 'not_configured' });
|
|
128
|
+
} else if (!status.authenticated) {
|
|
129
|
+
issues.push({ integrationId: id, name, problem: 'not_authenticated' });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
res.json({ ready: issues.length === 0, issues });
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// --- Agent Integration Attach/Detach ---
|
|
136
|
+
|
|
137
|
+
app.post('/api/agents/:id/integrations/:integrationId', (req, res) => {
|
|
138
|
+
const agent = daemon.registry.get(req.params.id);
|
|
139
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
140
|
+
|
|
141
|
+
const integrationId = req.params.integrationId;
|
|
142
|
+
const status = daemon.integrations.getStatus(integrationId);
|
|
143
|
+
if (!status || !status.installed) {
|
|
144
|
+
return res.status(400).json({ error: `Integration not installed: ${integrationId}` });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const integrations = new Set(agent.integrations || []);
|
|
148
|
+
integrations.add(integrationId);
|
|
149
|
+
const updated = Array.from(integrations);
|
|
150
|
+
|
|
151
|
+
daemon.registry.update(req.params.id, { integrations: updated });
|
|
152
|
+
daemon.integrations.writeMcpJson(daemon.integrations.getActiveIntegrations());
|
|
153
|
+
daemon.integrations.refreshMcpJson();
|
|
154
|
+
daemon.audit.log('agent.integration.attach', { agentId: req.params.id, integrationId });
|
|
155
|
+
daemon.broadcast({ type: 'agent:integration:attach', agentId: req.params.id, integrationId });
|
|
156
|
+
res.json({ ok: true, integrations: updated });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
app.delete('/api/agents/:id/integrations/:integrationId', (req, res) => {
|
|
160
|
+
const agent = daemon.registry.get(req.params.id);
|
|
161
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
162
|
+
|
|
163
|
+
const integrationId = req.params.integrationId;
|
|
164
|
+
const integrations = (agent.integrations || []).filter((id) => id !== integrationId);
|
|
165
|
+
|
|
166
|
+
daemon.registry.update(req.params.id, { integrations });
|
|
167
|
+
daemon.integrations.refreshMcpJson();
|
|
168
|
+
daemon.audit.log('agent.integration.detach', { agentId: req.params.id, integrationId });
|
|
169
|
+
daemon.broadcast({ type: 'agent:integration:detach', agentId: req.params.id, integrationId });
|
|
170
|
+
res.json({ ok: true, integrations });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
// --- Agent Routing ---
|
|
174
|
+
|
|
175
|
+
app.post('/api/agents/:id/routing', (req, res) => {
|
|
176
|
+
daemon.router.setMode(req.params.id, req.body.mode, {
|
|
177
|
+
fixedModel: req.body.fixedModel,
|
|
178
|
+
floorModel: req.body.floorModel,
|
|
179
|
+
});
|
|
180
|
+
res.json(daemon.router.getMode(req.params.id));
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
app.get('/api/agents/:id/routing/recommend', (req, res) => {
|
|
184
|
+
const rec = daemon.router.recommend(req.params.id);
|
|
185
|
+
if (!rec) return res.status(404).json({ error: 'Agent not found' });
|
|
186
|
+
res.json(rec);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// Downshift suggestion — NEVER auto-applied. User must accept via UI.
|
|
190
|
+
// Returns null (204) when classifier has no strong suggestion.
|
|
191
|
+
app.get('/api/agents/:id/routing/suggestion', (req, res) => {
|
|
192
|
+
const suggestion = daemon.router.getSuggestion(req.params.id);
|
|
193
|
+
if (!suggestion) return res.status(204).send();
|
|
194
|
+
res.json(suggestion);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// --- Conversations ---
|
|
198
|
+
|
|
199
|
+
app.get('/api/conversations', (req, res) => {
|
|
200
|
+
res.json({ conversations: daemon.conversations.list() });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
app.post('/api/conversations', async (req, res) => {
|
|
204
|
+
try {
|
|
205
|
+
const { provider, model, title, mode, reasoning_effort, verbosity } = req.body;
|
|
206
|
+
if (provider && typeof provider !== 'string') {
|
|
207
|
+
return res.status(400).json({ error: 'provider must be a string' });
|
|
208
|
+
}
|
|
209
|
+
if (mode && mode !== 'api' && mode !== 'agent') {
|
|
210
|
+
return res.status(400).json({ error: 'mode must be "api" or "agent"' });
|
|
211
|
+
}
|
|
212
|
+
const validatedEffort = validateReasoningEffort(reasoning_effort);
|
|
213
|
+
const validatedVerbosity = validateVerbosity(verbosity);
|
|
214
|
+
const conversation = await daemon.conversations.create(provider, model, title, mode || 'api', {
|
|
215
|
+
reasoningEffort: validatedEffort,
|
|
216
|
+
verbosity: validatedVerbosity,
|
|
217
|
+
});
|
|
218
|
+
daemon.audit.log('conversation.create', { id: conversation.id, provider, model, mode: conversation.mode });
|
|
219
|
+
res.status(201).json(conversation);
|
|
220
|
+
} catch (err) {
|
|
221
|
+
res.status(400).json({ error: err.message });
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
app.get('/api/conversations/:id', (req, res) => {
|
|
226
|
+
const conversation = daemon.conversations.get(req.params.id);
|
|
227
|
+
if (!conversation) return res.status(404).json({ error: 'Conversation not found' });
|
|
228
|
+
res.json(conversation);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
app.patch('/api/conversations/:id', async (req, res) => {
|
|
232
|
+
try {
|
|
233
|
+
const conv = daemon.conversations.get(req.params.id);
|
|
234
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
235
|
+
if (req.body.title !== undefined) daemon.conversations.rename(req.params.id, req.body.title);
|
|
236
|
+
if (req.body.pinned !== undefined) daemon.conversations.pin(req.params.id, req.body.pinned);
|
|
237
|
+
if (req.body.archived !== undefined) daemon.conversations.archive(req.params.id, req.body.archived);
|
|
238
|
+
if (req.body.model !== undefined || req.body.provider !== undefined) {
|
|
239
|
+
const newProvider = req.body.provider || conv.provider;
|
|
240
|
+
const newModel = req.body.model || conv.model;
|
|
241
|
+
daemon.conversations.updateModel(req.params.id, newProvider, newModel);
|
|
242
|
+
}
|
|
243
|
+
if (req.body.mode !== undefined) {
|
|
244
|
+
if (req.body.mode !== 'api' && req.body.mode !== 'agent') {
|
|
245
|
+
return res.status(400).json({ error: 'mode must be "api" or "agent"' });
|
|
246
|
+
}
|
|
247
|
+
await daemon.conversations.setMode(req.params.id, req.body.mode);
|
|
248
|
+
}
|
|
249
|
+
if (req.body.reasoning_effort !== undefined || req.body.verbosity !== undefined) {
|
|
250
|
+
const validatedEffort = req.body.reasoning_effort !== undefined ? validateReasoningEffort(req.body.reasoning_effort) : undefined;
|
|
251
|
+
const validatedVerbosity = req.body.verbosity !== undefined ? validateVerbosity(req.body.verbosity) : undefined;
|
|
252
|
+
daemon.conversations.updateReasoningSettings(req.params.id, validatedEffort, validatedVerbosity);
|
|
253
|
+
}
|
|
254
|
+
daemon.audit.log('conversation.update', { id: req.params.id, provider: req.body.provider, model: req.body.model, mode: req.body.mode });
|
|
255
|
+
res.json(daemon.conversations.get(req.params.id));
|
|
256
|
+
} catch (err) {
|
|
257
|
+
res.status(400).json({ error: err.message });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
app.delete('/api/conversations/:id', async (req, res) => {
|
|
262
|
+
try {
|
|
263
|
+
const conv = daemon.conversations.get(req.params.id);
|
|
264
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
265
|
+
await daemon.conversations.delete(req.params.id);
|
|
266
|
+
daemon.audit.log('conversation.delete', { id: req.params.id });
|
|
267
|
+
res.json({ ok: true });
|
|
268
|
+
} catch (err) {
|
|
269
|
+
res.status(400).json({ error: err.message });
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
app.post('/api/conversations/:id/message', async (req, res) => {
|
|
274
|
+
try {
|
|
275
|
+
const { message, history, reasoning_effort, verbosity } = req.body;
|
|
276
|
+
if (!message || typeof message !== 'string' || !message.trim()) {
|
|
277
|
+
return res.status(400).json({ error: 'message is required' });
|
|
278
|
+
}
|
|
279
|
+
const validatedEffort = validateReasoningEffort(reasoning_effort);
|
|
280
|
+
const validatedVerbosity = validateVerbosity(verbosity);
|
|
281
|
+
|
|
282
|
+
const conv = daemon.conversations.get(req.params.id);
|
|
283
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
284
|
+
|
|
285
|
+
daemon.conversations.autoTitle(req.params.id, message.trim());
|
|
286
|
+
daemon.conversations.touchUpdatedAt(req.params.id);
|
|
287
|
+
|
|
288
|
+
await daemon.conversations.sendMessage(req.params.id, message.trim(), history || [], {
|
|
289
|
+
reasoningEffort: validatedEffort,
|
|
290
|
+
verbosity: validatedVerbosity,
|
|
291
|
+
});
|
|
292
|
+
daemon.audit.log('conversation.message', { id: req.params.id, mode: conv.mode || 'api' });
|
|
293
|
+
res.json({ status: 'streaming', mode: conv.mode || 'api' });
|
|
294
|
+
} catch (err) {
|
|
295
|
+
res.status(400).json({ error: err.message });
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
app.post('/api/conversations/:id/stop', (req, res) => {
|
|
300
|
+
try {
|
|
301
|
+
const conv = daemon.conversations.get(req.params.id);
|
|
302
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
303
|
+
daemon.conversations.stopStreaming(req.params.id);
|
|
304
|
+
res.json({ ok: true });
|
|
305
|
+
} catch (err) {
|
|
306
|
+
res.status(400).json({ error: err.message });
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// --- Image Generation ---
|
|
311
|
+
|
|
312
|
+
app.post('/api/conversations/:id/generate-image', async (req, res) => {
|
|
313
|
+
try {
|
|
314
|
+
const { prompt, model, size, quality } = req.body;
|
|
315
|
+
if (!prompt || typeof prompt !== 'string' || !prompt.trim()) {
|
|
316
|
+
return res.status(400).json({ error: 'prompt is required' });
|
|
317
|
+
}
|
|
318
|
+
const conv = daemon.conversations.get(req.params.id);
|
|
319
|
+
if (!conv) return res.status(404).json({ error: 'Conversation not found' });
|
|
320
|
+
|
|
321
|
+
let providerName = conv.provider;
|
|
322
|
+
let provider = getProvider(providerName);
|
|
323
|
+
|
|
324
|
+
// If a specific image model was requested, find the right provider
|
|
325
|
+
if (model) {
|
|
326
|
+
const imageProviders = ['codex', 'grok', 'nano-banana'];
|
|
327
|
+
for (const pid of imageProviders) {
|
|
328
|
+
const p = getProvider(pid);
|
|
329
|
+
if (p?.constructor.models.some((m) => m.id === model)) {
|
|
330
|
+
provider = p;
|
|
331
|
+
providerName = pid;
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (!provider?.generateImage) {
|
|
338
|
+
return res.status(400).json({ error: 'Provider does not support image generation' });
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const apiKey = daemon.conversations._getApiKey(providerName);
|
|
342
|
+
if (!apiKey) {
|
|
343
|
+
return res.status(400).json({ error: `No API key configured for ${providerName}` });
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
daemon.broadcast({
|
|
347
|
+
type: 'conversation:image-progress',
|
|
348
|
+
data: { conversationId: req.params.id, status: 'generating', prompt: prompt.trim() },
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
const result = await provider.generateImage(prompt.trim(), { model, size, quality, apiKey });
|
|
352
|
+
|
|
353
|
+
daemon.broadcast({
|
|
354
|
+
type: 'conversation:image',
|
|
355
|
+
data: { conversationId: req.params.id, ...result, prompt: prompt.trim() },
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
daemon.conversations.touchUpdatedAt(req.params.id);
|
|
359
|
+
daemon.audit.log('conversation.image', { id: req.params.id, model: result.model, provider: result.provider });
|
|
360
|
+
res.json(result);
|
|
361
|
+
} catch (err) {
|
|
362
|
+
daemon.broadcast({
|
|
363
|
+
type: 'conversation:image-progress',
|
|
364
|
+
data: { conversationId: req.params.id, status: 'error', error: err.message },
|
|
365
|
+
});
|
|
366
|
+
res.status(500).json({ error: err.message });
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// Stop an agent's current work without killing the agent
|
|
371
|
+
app.post('/api/agents/:id/stop', async (req, res) => {
|
|
372
|
+
try {
|
|
373
|
+
const agent = daemon.registry.get(req.params.id);
|
|
374
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
375
|
+
await daemon.processes.stop(req.params.id);
|
|
376
|
+
daemon.audit.log('agent.stop', { id: req.params.id, name: agent.name });
|
|
377
|
+
res.json({ id: req.params.id, status: 'stopped' });
|
|
378
|
+
} catch (err) {
|
|
379
|
+
res.status(500).json({ error: err.message });
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// Rotate an agent
|
|
384
|
+
app.post('/api/agents/:id/rotate', async (req, res) => {
|
|
385
|
+
try {
|
|
386
|
+
const oldAgent = daemon.registry.get(req.params.id);
|
|
387
|
+
const newAgent = await daemon.rotator.rotate(req.params.id);
|
|
388
|
+
daemon.audit.log('agent.rotate', { oldId: req.params.id, newId: newAgent.id, role: oldAgent?.role });
|
|
389
|
+
res.json(newAgent);
|
|
390
|
+
} catch (err) {
|
|
391
|
+
res.status(400).json({ error: err.message });
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Instruct an agent — send message to agent loop, resume session, or rotate
|
|
396
|
+
// Agent loop = direct message to running loop (local models)
|
|
397
|
+
// Resume = zero cold-start (uses --resume SESSION_ID)
|
|
398
|
+
// Rotation = full handoff brief (only for degradation or no session)
|
|
399
|
+
app.post('/api/agents/:id/instruct', async (req, res) => {
|
|
400
|
+
try {
|
|
401
|
+
const { message, codeContext } = req.body;
|
|
402
|
+
if (!message || typeof message !== 'string' || !message.trim()) {
|
|
403
|
+
return res.status(400).json({ error: 'message is required' });
|
|
404
|
+
}
|
|
405
|
+
const agent = daemon.registry.get(req.params.id);
|
|
406
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
407
|
+
|
|
408
|
+
// Build the final instruction, optionally enriched with code context
|
|
409
|
+
let finalMessage = message.trim();
|
|
410
|
+
if (codeContext && typeof codeContext === 'object') {
|
|
411
|
+
const { filePath, lineStart, lineEnd, selectedCode } = codeContext;
|
|
412
|
+
if (filePath && typeof filePath === 'string' && selectedCode && typeof selectedCode === 'string') {
|
|
413
|
+
const start = Number.isFinite(lineStart) ? lineStart : '?';
|
|
414
|
+
const end = Number.isFinite(lineEnd) ? lineEnd : '?';
|
|
415
|
+
finalMessage = `${finalMessage}\n\nCode context from ${filePath} (lines ${start}-${end}):\n\`\`\`\n${selectedCode}\n\`\`\``;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Record user feedback so the journalist can include it in future agent context
|
|
420
|
+
if (daemon.journalist) daemon.journalist.recordUserFeedback(agent, finalMessage);
|
|
421
|
+
|
|
422
|
+
// Agent loop path — send message directly to the running loop
|
|
423
|
+
const wrappedMessage = wrapWithRoleReminder(agent.role, finalMessage);
|
|
424
|
+
if (daemon.processes.hasAgentLoop(req.params.id)) {
|
|
425
|
+
const sent = await daemon.processes.sendMessage(req.params.id, wrappedMessage);
|
|
426
|
+
if (sent) {
|
|
427
|
+
daemon.audit.log('agent.chat', { id: req.params.id });
|
|
428
|
+
return res.json({ id: agent.id, status: 'message_sent' });
|
|
429
|
+
}
|
|
430
|
+
// Loop exists but not running — fall through to resume/rotate
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// One-shot providers (groove-network): kill any running instance and
|
|
434
|
+
// respawn with the user's message as --prompt. No handoff brief, no
|
|
435
|
+
// session resume, no message queue — each chat message is a fresh spawn.
|
|
436
|
+
const provider = getProvider(agent.provider);
|
|
437
|
+
if (provider?.constructor?.isOneShot) {
|
|
438
|
+
const oldConfig = { ...agent };
|
|
439
|
+
if (daemon.processes.isRunning(req.params.id)) {
|
|
440
|
+
await daemon.processes.kill(req.params.id);
|
|
441
|
+
}
|
|
442
|
+
daemon.registry.remove(req.params.id);
|
|
443
|
+
daemon.locks.release(req.params.id);
|
|
444
|
+
|
|
445
|
+
const newAgent = await daemon.processes.spawn({
|
|
446
|
+
role: oldConfig.role,
|
|
447
|
+
scope: oldConfig.scope,
|
|
448
|
+
provider: oldConfig.provider,
|
|
449
|
+
model: oldConfig.model,
|
|
450
|
+
prompt: finalMessage,
|
|
451
|
+
permission: oldConfig.permission || 'full',
|
|
452
|
+
workingDir: oldConfig.workingDir,
|
|
453
|
+
name: oldConfig.name,
|
|
454
|
+
teamId: oldConfig.teamId,
|
|
455
|
+
});
|
|
456
|
+
daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
|
|
457
|
+
return res.json(newAgent);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Non-interactive CLI providers (e.g. Gemini): respawn with the new
|
|
461
|
+
// message as the prompt, preserving original introContext. These providers
|
|
462
|
+
// run one prompt per spawn and cannot resume sessions.
|
|
463
|
+
if (provider?.constructor?.nonInteractive && !daemon.processes.isRunning(req.params.id)) {
|
|
464
|
+
const oldConfig = { ...agent };
|
|
465
|
+
daemon.registry.remove(req.params.id);
|
|
466
|
+
daemon.locks.release(req.params.id);
|
|
467
|
+
|
|
468
|
+
const newAgent = await daemon.processes.spawn({
|
|
469
|
+
role: oldConfig.role,
|
|
470
|
+
scope: oldConfig.scope,
|
|
471
|
+
provider: oldConfig.provider,
|
|
472
|
+
model: oldConfig.model,
|
|
473
|
+
prompt: finalMessage,
|
|
474
|
+
introContext: oldConfig.introContext,
|
|
475
|
+
permission: oldConfig.permission || 'full',
|
|
476
|
+
workingDir: oldConfig.workingDir,
|
|
477
|
+
name: oldConfig.name,
|
|
478
|
+
teamId: oldConfig.teamId,
|
|
479
|
+
});
|
|
480
|
+
daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed: false });
|
|
481
|
+
return res.json(newAgent);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Running CLI agent (no loop) — queue the message for delivery after
|
|
485
|
+
// the current task completes instead of killing and respawning.
|
|
486
|
+
if (daemon.processes.isRunning(req.params.id)) {
|
|
487
|
+
daemon.processes.queueMessage(req.params.id, wrappedMessage);
|
|
488
|
+
daemon.audit.log('agent.chat.queued', { id: req.params.id });
|
|
489
|
+
return res.json({ id: agent.id, status: 'message_queued' });
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// CLI agent path — session resume or rotation.
|
|
493
|
+
// Force rotation (fresh session + handoff brief) past the resume ceiling:
|
|
494
|
+
// reviving a >5M-token claude session has crashed the CLI mid-HTTP-parse
|
|
495
|
+
// (V8 fatal in JsonStringifier) — the rotator's handoff brief sidesteps that.
|
|
496
|
+
const SESSION_RESUME_CEILING = 5_000_000;
|
|
497
|
+
const resumed = !!agent.sessionId && (agent.tokensUsed || 0) < SESSION_RESUME_CEILING;
|
|
498
|
+
const newAgent = resumed
|
|
499
|
+
? await daemon.processes.resume(req.params.id, wrappedMessage)
|
|
500
|
+
: await daemon.rotator.rotate(req.params.id, { additionalPrompt: wrappedMessage });
|
|
501
|
+
|
|
502
|
+
daemon.audit.log('agent.instruct', { id: req.params.id, newId: newAgent.id, resumed });
|
|
503
|
+
res.json(newAgent);
|
|
504
|
+
} catch (err) {
|
|
505
|
+
res.status(400).json({ error: err.message });
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
// Query an agent (headless one-shot, agent keeps running)
|
|
510
|
+
// For agent loop agents: sends message directly to the loop
|
|
511
|
+
app.post('/api/agents/:id/query', async (req, res) => {
|
|
512
|
+
try {
|
|
513
|
+
const { message } = req.body;
|
|
514
|
+
if (!message || typeof message !== 'string' || !message.trim()) {
|
|
515
|
+
return res.status(400).json({ error: 'message is required' });
|
|
516
|
+
}
|
|
517
|
+
const agent = daemon.registry.get(req.params.id);
|
|
518
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
519
|
+
|
|
520
|
+
// Agent loop agents: send message directly (they're interactive)
|
|
521
|
+
if (daemon.processes.hasAgentLoop(req.params.id)) {
|
|
522
|
+
const sent = await daemon.processes.sendMessage(req.params.id, message.trim());
|
|
523
|
+
return res.json({ response: sent ? 'Message sent to agent' : 'Agent not running', agentId: agent.id, agentName: agent.name });
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Build context about the agent's work
|
|
527
|
+
const activity = daemon.classifier?.agentWindows?.[agent.id] || [];
|
|
528
|
+
const recentActivity = activity.slice(-20).map((e) => e.data || e.text || '').join('\n');
|
|
529
|
+
|
|
530
|
+
// Truncate the agent's original prompt to avoid massive payloads
|
|
531
|
+
const taskSummary = agent.prompt ? agent.prompt.slice(0, 500) : '';
|
|
532
|
+
const prompt = [
|
|
533
|
+
`You are answering a question about agent "${agent.name}" (role: ${agent.role}).`,
|
|
534
|
+
`Provider: ${agent.provider}, Tokens used: ${agent.tokensUsed || 0}`,
|
|
535
|
+
taskSummary ? `Task summary: ${taskSummary}` : '',
|
|
536
|
+
recentActivity ? `\nRecent activity:\n${recentActivity}` : '',
|
|
537
|
+
`\nUser question: ${message.trim()}`,
|
|
538
|
+
'\nAnswer concisely based on the agent context above.',
|
|
539
|
+
].filter(Boolean).join('\n');
|
|
540
|
+
|
|
541
|
+
const response = await daemon.journalist.callHeadless(prompt, { trackAs: '__agent_qa__' });
|
|
542
|
+
res.json({ response, agentId: agent.id, agentName: agent.name });
|
|
543
|
+
} catch (err) {
|
|
544
|
+
res.status(400).json({ error: err.message });
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
|
|
548
|
+
// Upload file to agent's working directory
|
|
549
|
+
app.post('/api/agents/:id/upload', (req, res) => {
|
|
550
|
+
const agent = daemon.registry.get(req.params.id);
|
|
551
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
552
|
+
|
|
553
|
+
const { filename, content } = req.body;
|
|
554
|
+
if (!filename || !content) return res.status(400).json({ error: 'filename and content required' });
|
|
555
|
+
|
|
556
|
+
// Sanitize filename — strict allowlist, no path traversal
|
|
557
|
+
const safeName = String(filename).replace(/[^a-zA-Z0-9._-]/g, '_').replace(/^\.+/, '');
|
|
558
|
+
if (!safeName) return res.status(400).json({ error: 'Invalid filename' });
|
|
559
|
+
|
|
560
|
+
const dir = agent.workingDir || daemon.projectDir;
|
|
561
|
+
const filePath = resolve(dir, safeName);
|
|
562
|
+
|
|
563
|
+
// Ensure file stays within working directory
|
|
564
|
+
if (!filePath.startsWith(dir)) {
|
|
565
|
+
return res.status(400).json({ error: 'Path traversal detected' });
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
try {
|
|
569
|
+
mkdirSync(dir, { recursive: true });
|
|
570
|
+
const buffer = Buffer.from(content, 'base64');
|
|
571
|
+
writeFileSync(filePath, buffer);
|
|
572
|
+
daemon.audit.log('file.upload', { agentId: agent.id, filename: safeName, size: buffer.length });
|
|
573
|
+
res.json({ ok: true, path: safeName, size: buffer.length });
|
|
574
|
+
} catch (err) {
|
|
575
|
+
res.status(500).json({ error: `Upload failed: ${err.message}` });
|
|
576
|
+
}
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// List MD files for an agent (from its working directory + .groove)
|
|
580
|
+
app.get('/api/agents/:id/mdfiles', (req, res) => {
|
|
581
|
+
const agent = daemon.registry.get(req.params.id);
|
|
582
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
583
|
+
|
|
584
|
+
const dir = agent.workingDir || daemon.projectDir;
|
|
585
|
+
const files = [];
|
|
586
|
+
|
|
587
|
+
// Scan working directory for .md files (top level + .groove/)
|
|
588
|
+
try {
|
|
589
|
+
for (const entry of readdirSync(dir)) {
|
|
590
|
+
if (entry.endsWith('.md') && !entry.startsWith('.')) {
|
|
591
|
+
const fullPath = resolve(dir, entry);
|
|
592
|
+
if (statSync(fullPath).isFile()) {
|
|
593
|
+
files.push({ name: entry, path: entry, size: statSync(fullPath).size, source: 'project' });
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const grooveDir = resolve(dir, '.groove');
|
|
598
|
+
if (existsSync(grooveDir)) {
|
|
599
|
+
for (const entry of readdirSync(grooveDir)) {
|
|
600
|
+
if (entry.endsWith('.md')) {
|
|
601
|
+
const fullPath = resolve(grooveDir, entry);
|
|
602
|
+
if (statSync(fullPath).isFile()) {
|
|
603
|
+
files.push({ name: entry, path: `.groove/${entry}`, size: statSync(fullPath).size, source: 'project' });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
} catch { /* dir might not exist */ }
|
|
609
|
+
|
|
610
|
+
// Include personality file from .groove/personalities/
|
|
611
|
+
try {
|
|
612
|
+
const personalityFile = resolve(daemon.grooveDir, 'personalities', `${agent.name}.md`);
|
|
613
|
+
if (existsSync(personalityFile)) {
|
|
614
|
+
const size = statSync(personalityFile).size;
|
|
615
|
+
files.unshift({ name: 'personality.md', path: '__personality__', size, source: 'personality' });
|
|
616
|
+
}
|
|
617
|
+
} catch { /* ignore */ }
|
|
618
|
+
|
|
619
|
+
// Include user-created agent files from .groove/agent-files/<name>/
|
|
620
|
+
try {
|
|
621
|
+
const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
|
|
622
|
+
if (existsSync(agentFilesDir)) {
|
|
623
|
+
for (const entry of readdirSync(agentFilesDir)) {
|
|
624
|
+
if (entry.endsWith('.md')) {
|
|
625
|
+
const fullPath = resolve(agentFilesDir, entry);
|
|
626
|
+
if (statSync(fullPath).isFile()) {
|
|
627
|
+
files.push({ name: entry, path: `__user__/${entry}`, size: statSync(fullPath).size, source: 'user' });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
} catch { /* ignore */ }
|
|
633
|
+
|
|
634
|
+
res.json({ files, workingDir: dir });
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
// Read a specific MD file for an agent
|
|
638
|
+
app.get('/api/agents/:id/mdfiles/read', (req, res) => {
|
|
639
|
+
const agent = daemon.registry.get(req.params.id);
|
|
640
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
641
|
+
|
|
642
|
+
const dir = agent.workingDir || daemon.projectDir;
|
|
643
|
+
const relPath = req.query.path;
|
|
644
|
+
if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
645
|
+
|
|
646
|
+
if (relPath === '__personality__') {
|
|
647
|
+
const personalityFile = resolve(daemon.grooveDir, 'personalities', `${agent.name}.md`);
|
|
648
|
+
if (existsSync(personalityFile)) {
|
|
649
|
+
return res.json({ content: readFileSync(personalityFile, 'utf8') });
|
|
650
|
+
}
|
|
651
|
+
return res.json({ content: '' });
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
if (relPath.startsWith('__user__/')) {
|
|
655
|
+
const fileName = relPath.slice('__user__/'.length);
|
|
656
|
+
if (!fileName || fileName.includes('/') || fileName.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
657
|
+
const filePath = resolve(daemon.grooveDir, 'agent-files', agent.name, fileName);
|
|
658
|
+
if (existsSync(filePath)) return res.json({ content: readFileSync(filePath, 'utf8') });
|
|
659
|
+
return res.json({ content: '' });
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const fullPath = resolve(dir, relPath);
|
|
663
|
+
if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
667
|
+
res.json({ path: relPath, content });
|
|
668
|
+
} catch {
|
|
669
|
+
res.status(404).json({ error: 'File not found' });
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// Save a MD file for an agent
|
|
674
|
+
app.put('/api/agents/:id/mdfiles/write', (req, res) => {
|
|
675
|
+
const agent = daemon.registry.get(req.params.id);
|
|
676
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
677
|
+
|
|
678
|
+
const dir = agent.workingDir || daemon.projectDir;
|
|
679
|
+
const { path: relPath, content } = req.body;
|
|
680
|
+
if (!relPath || relPath.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
681
|
+
if (typeof content !== 'string') return res.status(400).json({ error: 'Content required' });
|
|
682
|
+
|
|
683
|
+
if (relPath === '__personality__') {
|
|
684
|
+
const personalityDir = resolve(daemon.grooveDir, 'personalities');
|
|
685
|
+
mkdirSync(personalityDir, { recursive: true });
|
|
686
|
+
writeFileSync(resolve(personalityDir, `${agent.name}.md`), content || '', { mode: 0o600 });
|
|
687
|
+
daemon.audit.log('personality.update', { name: agent.name, agentId: agent.id });
|
|
688
|
+
return res.json({ saved: true });
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (relPath.startsWith('__user__/')) {
|
|
692
|
+
const fileName = relPath.slice('__user__/'.length);
|
|
693
|
+
if (!fileName || fileName.includes('/') || fileName.includes('..')) return res.status(400).json({ error: 'Invalid path' });
|
|
694
|
+
const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
|
|
695
|
+
mkdirSync(agentFilesDir, { recursive: true });
|
|
696
|
+
writeFileSync(resolve(agentFilesDir, fileName), content || '', { mode: 0o600 });
|
|
697
|
+
daemon.audit.log('mdfile.write.user', { agentId: agent.id, name: fileName });
|
|
698
|
+
return res.json({ saved: true });
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const fullPath = resolve(dir, relPath);
|
|
702
|
+
if (!fullPath.startsWith(dir)) return res.status(400).json({ error: 'Path traversal' });
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
writeFileSync(fullPath, content, 'utf8');
|
|
706
|
+
daemon.audit.log('mdfile.write', { agentId: agent.id, path: relPath });
|
|
707
|
+
res.json({ ok: true });
|
|
708
|
+
} catch (err) {
|
|
709
|
+
res.status(500).json({ error: err.message });
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
// Create a new MD file for an agent
|
|
714
|
+
app.post('/api/agents/:id/mdfiles/create', (req, res) => {
|
|
715
|
+
const agent = daemon.registry.get(req.params.id);
|
|
716
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
717
|
+
let name = req.body?.name;
|
|
718
|
+
if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name required' });
|
|
719
|
+
name = name.replace(/[^a-zA-Z0-9_-]/g, '-').slice(0, 64);
|
|
720
|
+
if (!name) return res.status(400).json({ error: 'Invalid name' });
|
|
721
|
+
if (!name.endsWith('.md')) name += '.md';
|
|
722
|
+
const agentFilesDir = resolve(daemon.grooveDir, 'agent-files', agent.name);
|
|
723
|
+
mkdirSync(agentFilesDir, { recursive: true });
|
|
724
|
+
const filePath = resolve(agentFilesDir, name);
|
|
725
|
+
if (existsSync(filePath)) return res.status(409).json({ error: 'File already exists' });
|
|
726
|
+
writeFileSync(filePath, '', { mode: 0o600 });
|
|
727
|
+
daemon.audit.log('mdfile.create', { agentId: agent.id, name });
|
|
728
|
+
res.json({ name, path: `__user__/${name}` });
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
// --- Agent Skills (attach/detach) ---
|
|
732
|
+
|
|
733
|
+
app.post('/api/agents/:agentId/skills/:skillId', (req, res) => {
|
|
734
|
+
const agent = daemon.registry.get(req.params.agentId);
|
|
735
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
736
|
+
const skillId = req.params.skillId;
|
|
737
|
+
if (!daemon.skills.getContent(skillId)) {
|
|
738
|
+
return res.status(400).json({ error: 'Skill not installed. Install it first.' });
|
|
739
|
+
}
|
|
740
|
+
const skills = agent.skills || [];
|
|
741
|
+
if (skills.includes(skillId)) {
|
|
742
|
+
return res.json({ id: agent.id, skills });
|
|
743
|
+
}
|
|
744
|
+
daemon.registry.update(agent.id, { skills: [...skills, skillId] });
|
|
745
|
+
daemon.audit.log('skill.attach', { agentId: agent.id, skillId });
|
|
746
|
+
res.json({ id: agent.id, skills: [...skills, skillId] });
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
app.delete('/api/agents/:agentId/skills/:skillId', (req, res) => {
|
|
750
|
+
const agent = daemon.registry.get(req.params.agentId);
|
|
751
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
752
|
+
const skills = (agent.skills || []).filter((s) => s !== req.params.skillId);
|
|
753
|
+
daemon.registry.update(agent.id, { skills });
|
|
754
|
+
daemon.audit.log('skill.detach', { agentId: agent.id, skillId: req.params.skillId });
|
|
755
|
+
res.json({ id: agent.id, skills });
|
|
756
|
+
});
|
|
757
|
+
|
|
758
|
+
// --- Agent Repos (attach/detach) ---
|
|
759
|
+
|
|
760
|
+
app.post('/api/agents/:agentId/repos/:importId', (req, res) => {
|
|
761
|
+
const agent = daemon.registry.get(req.params.agentId);
|
|
762
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
763
|
+
const importId = req.params.importId;
|
|
764
|
+
const manifest = daemon.repoImporter.getImport(importId);
|
|
765
|
+
if (!manifest || manifest.status !== 'active') {
|
|
766
|
+
return res.status(400).json({ error: 'Repo not found or not active' });
|
|
767
|
+
}
|
|
768
|
+
const repos = agent.repos || [];
|
|
769
|
+
if (repos.includes(importId)) {
|
|
770
|
+
return res.json({ id: agent.id, repos });
|
|
771
|
+
}
|
|
772
|
+
daemon.registry.update(agent.id, { repos: [...repos, importId] });
|
|
773
|
+
daemon.audit.log('repo.attach', { agentId: agent.id, importId });
|
|
774
|
+
res.json({ id: agent.id, repos: [...repos, importId] });
|
|
775
|
+
});
|
|
776
|
+
|
|
777
|
+
app.delete('/api/agents/:agentId/repos/:importId', (req, res) => {
|
|
778
|
+
const agent = daemon.registry.get(req.params.agentId);
|
|
779
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
780
|
+
const repos = (agent.repos || []).filter((r) => r !== req.params.importId);
|
|
781
|
+
daemon.registry.update(agent.id, { repos });
|
|
782
|
+
daemon.audit.log('repo.detach', { agentId: agent.id, importId: req.params.importId });
|
|
783
|
+
res.json({ id: agent.id, repos });
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
// --- Agent Integrations (attach/detach) ---
|
|
787
|
+
|
|
788
|
+
app.post('/api/agents/:agentId/integrations/:integrationId', (req, res) => {
|
|
789
|
+
const agent = daemon.registry.get(req.params.agentId);
|
|
790
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
791
|
+
const integrationId = req.params.integrationId;
|
|
792
|
+
if (!daemon.integrations._isInstalled(integrationId)) {
|
|
793
|
+
return res.status(400).json({ error: 'Integration not installed. Install it first.' });
|
|
794
|
+
}
|
|
795
|
+
const integrations = agent.integrations || [];
|
|
796
|
+
if (integrations.includes(integrationId)) {
|
|
797
|
+
return res.json({ id: agent.id, integrations });
|
|
798
|
+
}
|
|
799
|
+
daemon.registry.update(agent.id, { integrations: [...integrations, integrationId] });
|
|
800
|
+
daemon.audit.log('integration.attach', { agentId: agent.id, integrationId });
|
|
801
|
+
res.json({ id: agent.id, integrations: [...integrations, integrationId] });
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
app.delete('/api/agents/:agentId/integrations/:integrationId', (req, res) => {
|
|
805
|
+
const agent = daemon.registry.get(req.params.agentId);
|
|
806
|
+
if (!agent) return res.status(404).json({ error: 'Agent not found' });
|
|
807
|
+
const integrations = (agent.integrations || []).filter((s) => s !== req.params.integrationId);
|
|
808
|
+
daemon.registry.update(agent.id, { integrations });
|
|
809
|
+
daemon.audit.log('integration.detach', { agentId: agent.id, integrationId: req.params.integrationId });
|
|
810
|
+
res.json({ id: agent.id, integrations });
|
|
811
|
+
});
|
|
812
|
+
}
|