@newsails/veil-cli 1.0.1
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/.veil/agents/analyst/AGENT.md +21 -0
- package/.veil/agents/analyst/agent.json +23 -0
- package/.veil/agents/assistant/AGENT.md +15 -0
- package/.veil/agents/assistant/agent.json +19 -0
- package/.veil/agents/coder/AGENT.md +18 -0
- package/.veil/agents/coder/agent.json +19 -0
- package/.veil/agents/hello/AGENT.md +5 -0
- package/.veil/agents/hello/agent.json +13 -0
- package/.veil/agents/writer/AGENT.md +12 -0
- package/.veil/agents/writer/agent.json +17 -0
- package/.veil/memory/MEMORY.md +343 -0
- package/.veil/memory/agents/analyst/MEMORY.md +55 -0
- package/.veil/memory/agents/hello/MEMORY.md +12 -0
- package/.veil/runtime.pid +1 -0
- package/.veil/settings.json +10 -0
- package/.veil-studio/studio.db +0 -0
- package/.veil-studio/studio.db-shm +0 -0
- package/.veil-studio/studio.db-wal +0 -0
- package/PLAN/01-vision.md +26 -0
- package/PLAN/02-tech-stack.md +94 -0
- package/PLAN/03-agents.md +232 -0
- package/PLAN/04-runtime.md +171 -0
- package/PLAN/05-tools.md +211 -0
- package/PLAN/06-communication.md +243 -0
- package/PLAN/07-storage.md +218 -0
- package/PLAN/08-api-cli.md +153 -0
- package/PLAN/09-permissions.md +108 -0
- package/PLAN/10-ably.md +105 -0
- package/PLAN/11-file-formats.md +442 -0
- package/PLAN/12-folder-structure.md +205 -0
- package/PLAN/13-operations.md +212 -0
- package/PLAN/README.md +23 -0
- package/README.md +128 -0
- package/REPORT.md +174 -0
- package/TODO.md +45 -0
- package/ai-tests/FRONTEND_PROMPT.md +220 -0
- package/ai-tests/Research & Planning.md +814 -0
- package/ai-tests/prompt-001-basic-api.md +230 -0
- package/ai-tests/prompt-002-basic-flows.md +230 -0
- package/ai-tests/prompt-003-agent-behaviors.md +220 -0
- package/api/middleware.js +60 -0
- package/api/routes/agents.js +193 -0
- package/api/routes/chat.js +93 -0
- package/api/routes/completions.js +122 -0
- package/api/routes/daemons.js +80 -0
- package/api/routes/memory.js +169 -0
- package/api/routes/models.js +40 -0
- package/api/routes/remote-methods.js +74 -0
- package/api/routes/sessions.js +208 -0
- package/api/routes/settings.js +108 -0
- package/api/routes/system.js +50 -0
- package/api/routes/tasks.js +270 -0
- package/api/server.js +120 -0
- package/cli/formatter.js +70 -0
- package/cli/index.js +443 -0
- package/cli/parser.js +113 -0
- package/config/config.json +10 -0
- package/config/models.json +6826 -0
- package/core/agent.js +329 -0
- package/core/cancel.js +38 -0
- package/core/compaction.js +176 -0
- package/core/events.js +13 -0
- package/core/loop.js +564 -0
- package/core/memory.js +51 -0
- package/core/prompt.js +185 -0
- package/core/queue.js +96 -0
- package/core/registry.js +291 -0
- package/core/remote-methods.js +124 -0
- package/core/router.js +386 -0
- package/core/running-sessions.js +18 -0
- package/docs/api/01-system.md +84 -0
- package/docs/api/02-agents.md +374 -0
- package/docs/api/03-chat.md +269 -0
- package/docs/api/04-tasks.md +470 -0
- package/docs/api/05-sessions.md +444 -0
- package/docs/api/06-daemons.md +142 -0
- package/docs/api/07-memory.md +186 -0
- package/docs/api/08-settings.md +133 -0
- package/docs/api/09-models.md +119 -0
- package/docs/api/09-websocket.md +350 -0
- package/docs/api/10-completions.md +134 -0
- package/docs/api/README.md +116 -0
- package/docs/guide/01-quickstart.md +220 -0
- package/docs/guide/02-folder-structure.md +185 -0
- package/docs/guide/03-configuration.md +252 -0
- package/docs/guide/04-agents.md +267 -0
- package/docs/guide/05-cli.md +290 -0
- package/docs/guide/06-tools.md +643 -0
- package/docs/guide/07-permissions.md +236 -0
- package/docs/guide/08-memory.md +139 -0
- package/docs/guide/09-multi-agent.md +271 -0
- package/docs/guide/10-daemons.md +226 -0
- package/docs/guide/README.md +53 -0
- package/docs/index.html +623 -0
- package/examples/README.md +151 -0
- package/examples/agents/assistant/AGENT.md +31 -0
- package/examples/agents/assistant/SOUL.md +9 -0
- package/examples/agents/assistant/agent.json +74 -0
- package/examples/agents/hello/AGENT.md +15 -0
- package/examples/agents/hello/agent.json +14 -0
- package/examples/agents/monitor/AGENT.md +51 -0
- package/examples/agents/monitor/agent.json +33 -0
- package/examples/agents/monitor/heartbeats/monitor.md +24 -0
- package/examples/agents/orchestrator/AGENT.md +70 -0
- package/examples/agents/orchestrator/agent.json +30 -0
- package/examples/agents/researcher/AGENT.md +52 -0
- package/examples/agents/researcher/agent.json +49 -0
- package/examples/agents/researcher/skills/web-research.md +28 -0
- package/examples/skills/code-review.md +72 -0
- package/examples/skills/summarise.md +59 -0
- package/examples/skills/web-research.md +42 -0
- package/examples/tools/word-count/index.js +27 -0
- package/examples/tools/word-count/tool.json +18 -0
- package/infrastructure/database.js +563 -0
- package/infrastructure/scheduler.js +122 -0
- package/llm/client.js +206 -0
- package/migrations/001-initial.sql +121 -0
- package/migrations/002-debuggability.sql +13 -0
- package/migrations/003-drop-orphaned-columns.sql +72 -0
- package/migrations/004-session-message-token-fields.sql +78 -0
- package/migrations/005-session-thinking.sql +5 -0
- package/package.json +30 -0
- package/schemas/agent.json +143 -0
- package/schemas/settings.json +111 -0
- package/scripts/fetch-models.js +93 -0
- package/session-debug-scenario.md +248 -0
- package/settings/fields.js +52 -0
- package/system-prompts/base-core.md +7 -0
- package/system-prompts/environment.md +13 -0
- package/system-prompts/reminders/anti-drift.md +6 -0
- package/system-prompts/reminders/stall-recovery.md +10 -0
- package/system-prompts/safety-rules.md +25 -0
- package/system-prompts/task-heuristics.md +27 -0
- package/test/client.js +71 -0
- package/test/integration/01-health.test.js +25 -0
- package/test/integration/02-agents.test.js +80 -0
- package/test/integration/03-chat-hello.test.js +48 -0
- package/test/integration/04-chat-multiturn.test.js +61 -0
- package/test/integration/05-chat-writer.test.js +48 -0
- package/test/integration/06-task-basic.test.js +68 -0
- package/test/integration/07-task-tools.test.js +74 -0
- package/test/integration/08-task-code-analysis.test.js +69 -0
- package/test/integration/09-memory-analyst.test.js +63 -0
- package/test/integration/10-task-advanced.test.js +85 -0
- package/test/integration/11-sessions-advanced.test.js +84 -0
- package/test/integration/12-assistant-chat-tools.test.js +75 -0
- package/test/integration/13-edge-cases.test.js +99 -0
- package/test/integration/14-cancel.test.js +62 -0
- package/test/integration/15-debug.test.js +106 -0
- package/test/integration/16-memory-api.test.js +83 -0
- package/test/integration/17-settings-api.test.js +41 -0
- package/test/integration/18-tool-search-activation.test.js +119 -0
- package/test/results/.gitkeep +0 -0
- package/test/runner.js +206 -0
- package/test/smoke.js +216 -0
- package/tools/agent_message.js +85 -0
- package/tools/agent_send.js +80 -0
- package/tools/agent_spawn.js +44 -0
- package/tools/bash.js +49 -0
- package/tools/edit_file.js +41 -0
- package/tools/glob.js +64 -0
- package/tools/grep.js +82 -0
- package/tools/list_dir.js +63 -0
- package/tools/log_write.js +31 -0
- package/tools/memory_read.js +38 -0
- package/tools/memory_search.js +65 -0
- package/tools/memory_write.js +42 -0
- package/tools/read_file.js +48 -0
- package/tools/sleep.js +22 -0
- package/tools/task_create.js +41 -0
- package/tools/task_respond.js +37 -0
- package/tools/task_spawn.js +64 -0
- package/tools/task_status.js +39 -0
- package/tools/task_subscribe.js +37 -0
- package/tools/todo_read.js +26 -0
- package/tools/todo_write.js +38 -0
- package/tools/tool_activate.js +24 -0
- package/tools/tool_search.js +24 -0
- package/tools/web_fetch.js +50 -0
- package/tools/web_search.js +52 -0
- package/tools/write_file.js +28 -0
- package/ui/api.js +190 -0
- package/ui/app.js +281 -0
- package/ui/index.html +382 -0
- package/ui/views/agents.js +377 -0
- package/ui/views/chat.js +610 -0
- package/ui/views/connection.js +96 -0
- package/ui/views/daemons.js +129 -0
- package/ui/views/feed.js +194 -0
- package/ui/views/memory.js +263 -0
- package/ui/views/models.js +146 -0
- package/ui/views/sessions.js +314 -0
- package/ui/views/settings.js +142 -0
- package/ui/views/tasks.js +415 -0
- package/utils/context.js +49 -0
- package/utils/id.js +16 -0
- package/utils/models.js +88 -0
- package/utils/paths.js +213 -0
- package/utils/settings.js +172 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const router = express.Router({ mergeParams: true });
|
|
7
|
+
const { sendError } = require('../middleware');
|
|
8
|
+
const context = require('../../utils/context');
|
|
9
|
+
const paths = require('../../utils/paths');
|
|
10
|
+
|
|
11
|
+
const SAFE_FILE_RE = /^[\w\-]+\.md$/;
|
|
12
|
+
|
|
13
|
+
function safeFile(file) {
|
|
14
|
+
return SAFE_FILE_RE.test(file);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function listMdFiles(dir) {
|
|
18
|
+
if (!fs.existsSync(dir)) return [];
|
|
19
|
+
return fs.readdirSync(dir)
|
|
20
|
+
.filter(f => f.endsWith('.md'))
|
|
21
|
+
.map(f => {
|
|
22
|
+
const stat = fs.statSync(path.join(dir, f));
|
|
23
|
+
return { name: f, size: stat.size, modified: stat.mtime.toISOString() };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── Global memory routes (/memory/*) ──────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
// GET /memory — list global memory files
|
|
30
|
+
router.get('/memory', (req, res, next) => {
|
|
31
|
+
try {
|
|
32
|
+
const cwd = context.getCwd();
|
|
33
|
+
const dir = paths.getProjectMemoryDir(cwd);
|
|
34
|
+
const files = listMdFiles(dir);
|
|
35
|
+
res.json({ files });
|
|
36
|
+
} catch (err) {
|
|
37
|
+
next(err);
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// GET /memory/:file — read a global memory file
|
|
42
|
+
router.get('/memory/:file', (req, res, next) => {
|
|
43
|
+
try {
|
|
44
|
+
if (!safeFile(req.params.file)) {
|
|
45
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'File name must match [a-z0-9A-Z_-]+.md');
|
|
46
|
+
}
|
|
47
|
+
const cwd = context.getCwd();
|
|
48
|
+
const filePath = path.join(paths.getProjectMemoryDir(cwd), req.params.file);
|
|
49
|
+
if (!fs.existsSync(filePath)) {
|
|
50
|
+
return sendError(res, 404, 'NOT_FOUND', `Memory file not found: ${req.params.file}`);
|
|
51
|
+
}
|
|
52
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
53
|
+
res.json({ file: req.params.file, content });
|
|
54
|
+
} catch (err) {
|
|
55
|
+
next(err);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// PUT /memory/:file — write/replace a global memory file
|
|
60
|
+
router.put('/memory/:file', (req, res, next) => {
|
|
61
|
+
try {
|
|
62
|
+
if (!safeFile(req.params.file)) {
|
|
63
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'File name must match [a-z0-9A-Z_-]+.md');
|
|
64
|
+
}
|
|
65
|
+
const { content } = req.body;
|
|
66
|
+
if (typeof content !== 'string') {
|
|
67
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'content must be a string');
|
|
68
|
+
}
|
|
69
|
+
const cwd = context.getCwd();
|
|
70
|
+
const dir = paths.getProjectMemoryDir(cwd);
|
|
71
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
72
|
+
const filePath = path.join(dir, req.params.file);
|
|
73
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
74
|
+
res.json({ file: req.params.file, size: Buffer.byteLength(content, 'utf8') });
|
|
75
|
+
} catch (err) {
|
|
76
|
+
next(err);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// DELETE /memory/:file — delete a global memory file
|
|
81
|
+
router.delete('/memory/:file', (req, res, next) => {
|
|
82
|
+
try {
|
|
83
|
+
if (!safeFile(req.params.file)) {
|
|
84
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'File name must match [a-z0-9A-Z_-]+.md');
|
|
85
|
+
}
|
|
86
|
+
const cwd = context.getCwd();
|
|
87
|
+
const filePath = path.join(paths.getProjectMemoryDir(cwd), req.params.file);
|
|
88
|
+
if (!fs.existsSync(filePath)) {
|
|
89
|
+
return sendError(res, 404, 'NOT_FOUND', `Memory file not found: ${req.params.file}`);
|
|
90
|
+
}
|
|
91
|
+
fs.unlinkSync(filePath);
|
|
92
|
+
res.json({ file: req.params.file, status: 'deleted' });
|
|
93
|
+
} catch (err) {
|
|
94
|
+
next(err);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// ── Agent memory routes (/agents/:name/memory/*) ─────────────────────────────
|
|
99
|
+
|
|
100
|
+
// GET /agents/:name/memory — list agent memory files
|
|
101
|
+
router.get('/:name/memory', (req, res, next) => {
|
|
102
|
+
try {
|
|
103
|
+
const cwd = context.getCwd();
|
|
104
|
+
const dir = paths.getAgentMemoryDir(cwd, req.params.name);
|
|
105
|
+
const files = listMdFiles(dir);
|
|
106
|
+
res.json({ agentName: req.params.name, files });
|
|
107
|
+
} catch (err) {
|
|
108
|
+
next(err);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// GET /agents/:name/memory/:file — read a specific agent memory file
|
|
113
|
+
router.get('/:name/memory/:file', (req, res, next) => {
|
|
114
|
+
try {
|
|
115
|
+
if (!safeFile(req.params.file)) {
|
|
116
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'File name must match [a-z0-9_-]+.md');
|
|
117
|
+
}
|
|
118
|
+
const cwd = context.getCwd();
|
|
119
|
+
const filePath = path.join(paths.getAgentMemoryDir(cwd, req.params.name), req.params.file);
|
|
120
|
+
if (!fs.existsSync(filePath)) {
|
|
121
|
+
return sendError(res, 404, 'NOT_FOUND', `Memory file not found: ${req.params.file}`);
|
|
122
|
+
}
|
|
123
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
124
|
+
res.json({ agentName: req.params.name, file: req.params.file, content });
|
|
125
|
+
} catch (err) {
|
|
126
|
+
next(err);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// PUT /agents/:name/memory/:file — write/replace an agent memory file
|
|
131
|
+
router.put('/:name/memory/:file', (req, res, next) => {
|
|
132
|
+
try {
|
|
133
|
+
if (!safeFile(req.params.file)) {
|
|
134
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'File name must match [a-z0-9_-]+.md');
|
|
135
|
+
}
|
|
136
|
+
const { content } = req.body;
|
|
137
|
+
if (typeof content !== 'string') {
|
|
138
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'content must be a string');
|
|
139
|
+
}
|
|
140
|
+
const cwd = context.getCwd();
|
|
141
|
+
const dir = paths.getAgentMemoryDir(cwd, req.params.name);
|
|
142
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
143
|
+
const filePath = path.join(dir, req.params.file);
|
|
144
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
145
|
+
res.json({ agentName: req.params.name, file: req.params.file, size: Buffer.byteLength(content, 'utf8') });
|
|
146
|
+
} catch (err) {
|
|
147
|
+
next(err);
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// DELETE /agents/:name/memory/:file — delete an agent memory file
|
|
152
|
+
router.delete('/:name/memory/:file', (req, res, next) => {
|
|
153
|
+
try {
|
|
154
|
+
if (!safeFile(req.params.file)) {
|
|
155
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'File name must match [a-z0-9_-]+.md');
|
|
156
|
+
}
|
|
157
|
+
const cwd = context.getCwd();
|
|
158
|
+
const filePath = path.join(paths.getAgentMemoryDir(cwd, req.params.name), req.params.file);
|
|
159
|
+
if (!fs.existsSync(filePath)) {
|
|
160
|
+
return sendError(res, 404, 'NOT_FOUND', `Memory file not found: ${req.params.file}`);
|
|
161
|
+
}
|
|
162
|
+
fs.unlinkSync(filePath);
|
|
163
|
+
res.json({ agentName: req.params.name, file: req.params.file, status: 'deleted' });
|
|
164
|
+
} catch (err) {
|
|
165
|
+
next(err);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
module.exports = router;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const { listModels, getModel, invalidateCache } = require('../../utils/models');
|
|
6
|
+
const { sendError } = require('../middleware');
|
|
7
|
+
|
|
8
|
+
// GET /models — list all known models
|
|
9
|
+
router.get('/', (req, res) => {
|
|
10
|
+
res.json(listModels());
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
// GET /models/:id — get a single model by id (e.g. "anthropic/claude-sonnet-4.6")
|
|
14
|
+
router.get('/:provider/:name', (req, res) => {
|
|
15
|
+
const modelId = `${req.params.provider}/${req.params.name}`;
|
|
16
|
+
const model = getModel(modelId);
|
|
17
|
+
if (!model) return sendError(res, 404, 'MODEL_NOT_FOUND', `Model "${modelId}" not found`);
|
|
18
|
+
res.json({ id: modelId, ...model });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// POST /models/refresh — re-fetch models from OpenRouter and reload cache
|
|
22
|
+
router.post('/refresh', async (req, res, next) => {
|
|
23
|
+
try {
|
|
24
|
+
const { execFile } = require('child_process');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const scriptPath = path.join(__dirname, '..', '..', 'scripts', 'fetch-models.js');
|
|
27
|
+
await new Promise((resolve, reject) => {
|
|
28
|
+
execFile(process.execPath, [scriptPath], { timeout: 30000 }, (err, stdout, stderr) => {
|
|
29
|
+
if (err) return reject(new Error(stderr || err.message));
|
|
30
|
+
resolve(stdout);
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
invalidateCache();
|
|
34
|
+
res.json({ refreshed: true, ...listModels() });
|
|
35
|
+
} catch (err) {
|
|
36
|
+
next(err);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
module.exports = router;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const rm = require('../../core/remote-methods');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /remote-methods
|
|
9
|
+
*
|
|
10
|
+
* SSE stream. Sends all currently-pending methods on connect, then streams
|
|
11
|
+
* new `method.pending` and `method.done` events in real time.
|
|
12
|
+
*
|
|
13
|
+
* Event shapes:
|
|
14
|
+
* { type: "method.pending", id, method, data, createdAt }
|
|
15
|
+
* { type: "method.done", id, timedOut: bool }
|
|
16
|
+
*
|
|
17
|
+
* The client MUST implement reconnection logic (EventSource does this
|
|
18
|
+
* automatically). UI-side deduplication via method `id` is recommended.
|
|
19
|
+
*/
|
|
20
|
+
router.get('/', (req, res) => {
|
|
21
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
22
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
23
|
+
res.setHeader('Connection', 'keep-alive');
|
|
24
|
+
res.setHeader('X-Accel-Buffering', 'no'); // disable nginx buffering if proxied
|
|
25
|
+
res.flushHeaders();
|
|
26
|
+
|
|
27
|
+
// Send a keepalive comment immediately so the client knows the stream is live
|
|
28
|
+
res.write(': connected\n\n');
|
|
29
|
+
|
|
30
|
+
rm.addClient(res);
|
|
31
|
+
|
|
32
|
+
// Keepalive ping every 20s to prevent proxies from closing idle connections
|
|
33
|
+
const ping = setInterval(() => {
|
|
34
|
+
try { res.write(': ping\n\n'); } catch { clearInterval(ping); }
|
|
35
|
+
}, 20_000);
|
|
36
|
+
|
|
37
|
+
req.on('close', () => {
|
|
38
|
+
clearInterval(ping);
|
|
39
|
+
rm.removeClient(res);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* POST /remote-methods/:id/result
|
|
45
|
+
*
|
|
46
|
+
* Deliver the UI's response for a pending method call.
|
|
47
|
+
*
|
|
48
|
+
* Body: { result: <any JSON value> }
|
|
49
|
+
*
|
|
50
|
+
* Responses:
|
|
51
|
+
* 200 { ok: true } — result accepted, waiting tool unblocked
|
|
52
|
+
* 409 { error: "not_found" } — id unknown (already resolved, timed out, or invalid)
|
|
53
|
+
*/
|
|
54
|
+
router.post('/:id/result', (req, res) => {
|
|
55
|
+
const { id } = req.params;
|
|
56
|
+
const result = req.body?.result;
|
|
57
|
+
|
|
58
|
+
const status = rm.resolve(id, result);
|
|
59
|
+
|
|
60
|
+
if (status === 'not_found') {
|
|
61
|
+
return res.status(409).json({ error: 'not_found', message: `No pending remote method with id "${id}". It may have already been resolved or timed out.` });
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return res.json({ ok: true, id });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* GET /remote-methods/pending (diagnostic — lists pending IDs)
|
|
69
|
+
*/
|
|
70
|
+
router.get('/pending', (req, res) => {
|
|
71
|
+
res.json({ pending: rm.pendingIds() });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
module.exports = router;
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const { sendError } = require('../middleware');
|
|
6
|
+
const context = require('../../utils/context');
|
|
7
|
+
const db = require('../../infrastructure/database');
|
|
8
|
+
const { loadAgent } = require('../../core/agent');
|
|
9
|
+
const { assembleSystemPrompt } = require('../../core/prompt');
|
|
10
|
+
const { loadSettings } = require('../../utils/settings');
|
|
11
|
+
const eventBus = require('../../core/events');
|
|
12
|
+
|
|
13
|
+
const VALID_MODES = ['chat', 'task', 'daemon', 'subagent'];
|
|
14
|
+
|
|
15
|
+
// POST /sessions — create a new session without sending a message
|
|
16
|
+
router.post('/', (req, res, next) => {
|
|
17
|
+
try {
|
|
18
|
+
const { agentName, mode = 'chat', model, model_thinking } = req.body || {};
|
|
19
|
+
|
|
20
|
+
if (!agentName) return sendError(res, 400, 'VALIDATION_ERROR', '"agentName" is required');
|
|
21
|
+
if (!VALID_MODES.includes(mode)) {
|
|
22
|
+
return sendError(res, 400, 'VALIDATION_ERROR', `"mode" must be one of: ${VALID_MODES.join(', ')}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const cwd = context.getCwd();
|
|
26
|
+
|
|
27
|
+
let agent;
|
|
28
|
+
try {
|
|
29
|
+
agent = loadAgent({ cwd, name: agentName });
|
|
30
|
+
} catch {
|
|
31
|
+
return sendError(res, 404, 'AGENT_NOT_FOUND', `Agent "${agentName}" not found`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const resolvedModel = model || agent.model || null;
|
|
35
|
+
const resolvedThinking = model_thinking !== undefined ? model_thinking : (agent.thinking || null);
|
|
36
|
+
const sessionId = db.createSession({ agentName, mode, instanceFolder: cwd, model: resolvedModel, modelThinking: resolvedThinking });
|
|
37
|
+
|
|
38
|
+
// Inject system prompt as first message so the session is ready to use
|
|
39
|
+
try {
|
|
40
|
+
const settings = loadSettings({ cwd });
|
|
41
|
+
const systemPrompt = assembleSystemPrompt({ agent, mode, sessionId, cwd, settings });
|
|
42
|
+
db.addMessage({ sessionId, role: 'system', content: systemPrompt });
|
|
43
|
+
} catch { /* non-fatal — session is still usable without system prompt */ }
|
|
44
|
+
|
|
45
|
+
const session = db.getSession(sessionId);
|
|
46
|
+
res.status(201).json({ sessionId, session });
|
|
47
|
+
} catch (err) {
|
|
48
|
+
next(err);
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// GET /sessions
|
|
53
|
+
router.get('/', (req, res, next) => {
|
|
54
|
+
try {
|
|
55
|
+
const cwd = context.getCwd();
|
|
56
|
+
const { agentName, mode, status, limit, cursor } = req.query;
|
|
57
|
+
const sessions = db.listSessions({
|
|
58
|
+
instanceFolder: cwd,
|
|
59
|
+
agentName,
|
|
60
|
+
status,
|
|
61
|
+
limit: limit ? parseInt(limit, 10) : 20,
|
|
62
|
+
cursor,
|
|
63
|
+
});
|
|
64
|
+
res.json({ sessions });
|
|
65
|
+
} catch (err) {
|
|
66
|
+
next(err);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// GET /sessions/:id
|
|
71
|
+
router.get('/:id', (req, res, next) => {
|
|
72
|
+
try {
|
|
73
|
+
const session = db.getSession(req.params.id);
|
|
74
|
+
if (!session) return sendError(res, 404, 'SESSION_NOT_FOUND', `Session not found: ${req.params.id}`);
|
|
75
|
+
res.json({ session });
|
|
76
|
+
} catch (err) {
|
|
77
|
+
next(err);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// GET /sessions/:id/messages
|
|
82
|
+
router.get('/:id/messages', (req, res, next) => {
|
|
83
|
+
try {
|
|
84
|
+
const session = db.getSession(req.params.id);
|
|
85
|
+
if (!session) return sendError(res, 404, 'SESSION_NOT_FOUND', `Session not found: ${req.params.id}`);
|
|
86
|
+
const { limit, offset } = req.query;
|
|
87
|
+
const messages = db.getMessages(req.params.id, {
|
|
88
|
+
limit: limit ? parseInt(limit, 10) : 100,
|
|
89
|
+
offset: offset ? parseInt(offset, 10) : 0,
|
|
90
|
+
});
|
|
91
|
+
res.json({ sessionId: req.params.id, messages });
|
|
92
|
+
} catch (err) {
|
|
93
|
+
next(err);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// GET /sessions/:id/stream — SSE real-time event stream for a session
|
|
98
|
+
router.get('/:id/stream', (req, res, next) => {
|
|
99
|
+
try {
|
|
100
|
+
const session = db.getSession(req.params.id);
|
|
101
|
+
if (!session) return sendError(res, 404, 'SESSION_NOT_FOUND', `Session not found: ${req.params.id}`);
|
|
102
|
+
|
|
103
|
+
const sessionId = req.params.id;
|
|
104
|
+
|
|
105
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
106
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
107
|
+
res.setHeader('Connection', 'keep-alive');
|
|
108
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
109
|
+
res.flushHeaders();
|
|
110
|
+
|
|
111
|
+
function send(eventType, data) {
|
|
112
|
+
try {
|
|
113
|
+
res.write(`event: ${eventType}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
114
|
+
} catch { /* client disconnected */ }
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Send current session state immediately
|
|
118
|
+
send('session', { sessionId, status: session.status, agentName: session.agent_name, mode: session.mode });
|
|
119
|
+
|
|
120
|
+
function onEvent(msg) {
|
|
121
|
+
if (msg.sessionId !== sessionId) return;
|
|
122
|
+
send(msg.type, { sessionId, agentName: msg.agentName, event: msg.event, timestamp: msg.event?.timestamp });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
eventBus.on('event', onEvent);
|
|
126
|
+
|
|
127
|
+
// Keepalive ping every 15s to prevent proxy timeouts
|
|
128
|
+
const keepalive = setInterval(() => {
|
|
129
|
+
try { res.write(': keepalive\n\n'); } catch { cleanup(); }
|
|
130
|
+
}, 15000);
|
|
131
|
+
|
|
132
|
+
function cleanup() {
|
|
133
|
+
clearInterval(keepalive);
|
|
134
|
+
eventBus.off('event', onEvent);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
req.on('close', cleanup);
|
|
138
|
+
req.on('error', cleanup);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
next(err);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// GET /sessions/:id/context — inspect the full message history (truncated for readability)
|
|
145
|
+
router.get('/:id/context', (req, res, next) => {
|
|
146
|
+
try {
|
|
147
|
+
const session = db.getSession(req.params.id);
|
|
148
|
+
if (!session) return sendError(res, 404, 'SESSION_NOT_FOUND', `Session not found: ${req.params.id}`);
|
|
149
|
+
const messages = db.getMessages(req.params.id);
|
|
150
|
+
res.json({
|
|
151
|
+
sessionId: req.params.id,
|
|
152
|
+
agentName: session.agent_name,
|
|
153
|
+
mode: session.mode,
|
|
154
|
+
messageCount: messages.length,
|
|
155
|
+
messages: messages.map(m => ({
|
|
156
|
+
id: m.id,
|
|
157
|
+
role: m.role,
|
|
158
|
+
content: m.content || null,
|
|
159
|
+
tool_calls: m.tool_calls || undefined,
|
|
160
|
+
tool_call_id: m.tool_call_id || undefined,
|
|
161
|
+
created_at: m.created_at,
|
|
162
|
+
})),
|
|
163
|
+
});
|
|
164
|
+
} catch (err) {
|
|
165
|
+
next(err);
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// POST /sessions/:id/reset — clear messages but keep session alive
|
|
170
|
+
router.post('/:id/reset', (req, res, next) => {
|
|
171
|
+
try {
|
|
172
|
+
const session = db.getSession(req.params.id);
|
|
173
|
+
if (!session) return sendError(res, 404, 'SESSION_NOT_FOUND', `Session not found: ${req.params.id}`);
|
|
174
|
+
if (session.status === 'closed') return sendError(res, 400, 'SESSION_CLOSED', 'Cannot reset a closed session');
|
|
175
|
+
const cwd = context.getCwd();
|
|
176
|
+
db.resetSession(req.params.id);
|
|
177
|
+
// Re-inject fresh system prompt so agent has a clean start
|
|
178
|
+
try {
|
|
179
|
+
const agent = loadAgent({ cwd, name: session.agent_name });
|
|
180
|
+
const systemPrompt = assembleSystemPrompt({ agent, mode: session.mode, sessionId: req.params.id, cwd, settings: {} });
|
|
181
|
+
db.addMessage({ sessionId: req.params.id, role: 'system', content: systemPrompt });
|
|
182
|
+
} catch { /* if agent load fails, reset without re-injecting system prompt */ }
|
|
183
|
+
res.json({ sessionId: req.params.id, status: 'reset', messageCount: 1 });
|
|
184
|
+
} catch (err) {
|
|
185
|
+
next(err);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
// DELETE /sessions/:id — soft close or hard delete (?hard=true)
|
|
190
|
+
router.delete('/:id', (req, res, next) => {
|
|
191
|
+
try {
|
|
192
|
+
const session = db.getSession(req.params.id);
|
|
193
|
+
if (!session) return sendError(res, 404, 'SESSION_NOT_FOUND', `Session not found: ${req.params.id}`);
|
|
194
|
+
if (req.query.hard === 'true') {
|
|
195
|
+
db.deleteSession(req.params.id);
|
|
196
|
+
eventBus.emit('event', { type: 'session.deleted', sessionId: req.params.id, agentName: session.agent_name, event: { timestamp: Date.now() } });
|
|
197
|
+
res.json({ sessionId: req.params.id, status: 'deleted' });
|
|
198
|
+
} else {
|
|
199
|
+
db.closeSession(req.params.id);
|
|
200
|
+
eventBus.emit('event', { type: 'session.closed', sessionId: req.params.id, agentName: session.agent_name, event: { timestamp: Date.now() } });
|
|
201
|
+
res.json({ sessionId: req.params.id, status: 'closed' });
|
|
202
|
+
}
|
|
203
|
+
} catch (err) {
|
|
204
|
+
next(err);
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
module.exports = router;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const router = express.Router();
|
|
6
|
+
const Ajv = require('ajv');
|
|
7
|
+
const addFormats = require('ajv-formats');
|
|
8
|
+
const { sendError } = require('../middleware');
|
|
9
|
+
const context = require('../../utils/context');
|
|
10
|
+
const { loadSettings, readSettingsForLevel, settingsPathForLevel, VALID_LEVELS } = require('../../utils/settings');
|
|
11
|
+
const paths = require('../../utils/paths');
|
|
12
|
+
|
|
13
|
+
const ajv = new Ajv({ allErrors: true });
|
|
14
|
+
addFormats(ajv);
|
|
15
|
+
|
|
16
|
+
let _settingsValidator = null;
|
|
17
|
+
function getValidator() {
|
|
18
|
+
if (_settingsValidator) return _settingsValidator;
|
|
19
|
+
try {
|
|
20
|
+
const schema = require('../../schemas/settings.json');
|
|
21
|
+
_settingsValidator = ajv.compile(schema);
|
|
22
|
+
} catch {
|
|
23
|
+
_settingsValidator = null;
|
|
24
|
+
}
|
|
25
|
+
return _settingsValidator;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Redact sensitive fields (api_key values) from the settings object.
|
|
30
|
+
* @param {Object} obj
|
|
31
|
+
* @returns {Object}
|
|
32
|
+
*/
|
|
33
|
+
function redactSecrets(obj) {
|
|
34
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
35
|
+
const result = Array.isArray(obj) ? [] : {};
|
|
36
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
37
|
+
if (k === 'api_key' && typeof v === 'string' && v.length > 8) {
|
|
38
|
+
result[k] = v.slice(0, 3) + '…' + v.slice(-4);
|
|
39
|
+
} else if (typeof v === 'object' && v !== null) {
|
|
40
|
+
result[k] = redactSecrets(v);
|
|
41
|
+
} else {
|
|
42
|
+
result[k] = v;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// GET /settings — return settings, optionally scoped to a level
|
|
49
|
+
// ?level=merged (default) | project | global | local
|
|
50
|
+
router.get('/', (req, res, next) => {
|
|
51
|
+
try {
|
|
52
|
+
const level = req.query.level || 'merged';
|
|
53
|
+
if (!VALID_LEVELS.includes(level)) {
|
|
54
|
+
return sendError(res, 400, 'INVALID_LEVEL', `Invalid level "${level}". Must be one of: ${VALID_LEVELS.join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cwd = context.getCwd();
|
|
58
|
+
const { settings, exists, path: filePath } = readSettingsForLevel({ cwd, level });
|
|
59
|
+
res.json({ level, exists, path: filePath, settings: redactSecrets(settings) });
|
|
60
|
+
} catch (err) {
|
|
61
|
+
next(err);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// PUT /settings — validate and write settings at the specified level, then live-reload
|
|
66
|
+
// ?level=project (default) | global | local ("merged" is read-only)
|
|
67
|
+
router.put('/', (req, res, next) => {
|
|
68
|
+
try {
|
|
69
|
+
const level = req.query.level || 'project';
|
|
70
|
+
if (level === 'merged') {
|
|
71
|
+
return sendError(res, 400, 'INVALID_LEVEL', '"merged" is a read-only view and cannot be written. Use "project", "global", or "local".');
|
|
72
|
+
}
|
|
73
|
+
if (!VALID_LEVELS.includes(level)) {
|
|
74
|
+
return sendError(res, 400, 'INVALID_LEVEL', `Invalid level "${level}". Must be one of: ${VALID_LEVELS.filter(l => l !== 'merged').join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const body = req.body;
|
|
78
|
+
if (!body || typeof body !== 'object') {
|
|
79
|
+
return sendError(res, 400, 'VALIDATION_ERROR', 'Request body must be a JSON object');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const validate = getValidator();
|
|
83
|
+
if (validate) {
|
|
84
|
+
const valid = validate(body);
|
|
85
|
+
if (!valid) {
|
|
86
|
+
const msgs = validate.errors.map(e => `${e.instancePath || '(root)'}: ${e.message}`).join('; ');
|
|
87
|
+
return sendError(res, 400, 'VALIDATION_ERROR', `Invalid settings: ${msgs}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cwd = context.getCwd();
|
|
92
|
+
const settingsPath = settingsPathForLevel(level, cwd);
|
|
93
|
+
const dir = require('path').dirname(settingsPath);
|
|
94
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
95
|
+
|
|
96
|
+
fs.writeFileSync(settingsPath, JSON.stringify(body, null, 2), 'utf8');
|
|
97
|
+
|
|
98
|
+
// Live reload — update in-memory merged settings for all subsequent requests
|
|
99
|
+
const newSettings = loadSettings({ cwd });
|
|
100
|
+
req.app.locals.settings = newSettings;
|
|
101
|
+
|
|
102
|
+
res.json({ status: 'updated', level, path: settingsPath, settings: redactSecrets(newSettings) });
|
|
103
|
+
} catch (err) {
|
|
104
|
+
next(err);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
module.exports = router;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const express = require('express');
|
|
4
|
+
const router = express.Router();
|
|
5
|
+
const context = require('../../utils/context');
|
|
6
|
+
const db = require('../../infrastructure/database');
|
|
7
|
+
|
|
8
|
+
const startTime = Date.now();
|
|
9
|
+
|
|
10
|
+
// GET /health
|
|
11
|
+
router.get('/health', (req, res) => {
|
|
12
|
+
res.json({ status: 'ok', version: context.getVersion(), uptime: Math.floor((Date.now() - startTime) / 1000) });
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// GET /status
|
|
16
|
+
router.get('/status', (req, res, next) => {
|
|
17
|
+
try {
|
|
18
|
+
const cwd = context.getCwd();
|
|
19
|
+
const { listAgents } = require('../../core/agent');
|
|
20
|
+
const agents = listAgents({ cwd });
|
|
21
|
+
const scheduler = req.app.locals.scheduler;
|
|
22
|
+
const daemons = scheduler ? scheduler.listDaemons() : [];
|
|
23
|
+
|
|
24
|
+
const activeSessions = db.listSessions({ instanceFolder: cwd, status: 'active', limit: 1000 });
|
|
25
|
+
const pendingTasks = db.listTasks({ instanceFolder: cwd, status: 'pending', limit: 1000 });
|
|
26
|
+
const processingTasks = db.listTasks({ instanceFolder: cwd, status: 'processing', limit: 1000 });
|
|
27
|
+
|
|
28
|
+
res.json({
|
|
29
|
+
status: 'ok',
|
|
30
|
+
version: context.getVersion(),
|
|
31
|
+
uptime: Math.floor((Date.now() - startTime) / 1000),
|
|
32
|
+
cwd,
|
|
33
|
+
agents: agents.length,
|
|
34
|
+
activeSessions: activeSessions.length,
|
|
35
|
+
pendingTasks: pendingTasks.length,
|
|
36
|
+
processingTasks: processingTasks.length,
|
|
37
|
+
daemons: daemons.length,
|
|
38
|
+
});
|
|
39
|
+
} catch (err) {
|
|
40
|
+
next(err);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// POST /shutdown
|
|
45
|
+
router.post('/shutdown', (req, res) => {
|
|
46
|
+
res.json({ status: 'shutting_down' });
|
|
47
|
+
setTimeout(() => process.exit(0), 500);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
module.exports = router;
|