@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.
Files changed (199) hide show
  1. package/.veil/agents/analyst/AGENT.md +21 -0
  2. package/.veil/agents/analyst/agent.json +23 -0
  3. package/.veil/agents/assistant/AGENT.md +15 -0
  4. package/.veil/agents/assistant/agent.json +19 -0
  5. package/.veil/agents/coder/AGENT.md +18 -0
  6. package/.veil/agents/coder/agent.json +19 -0
  7. package/.veil/agents/hello/AGENT.md +5 -0
  8. package/.veil/agents/hello/agent.json +13 -0
  9. package/.veil/agents/writer/AGENT.md +12 -0
  10. package/.veil/agents/writer/agent.json +17 -0
  11. package/.veil/memory/MEMORY.md +343 -0
  12. package/.veil/memory/agents/analyst/MEMORY.md +55 -0
  13. package/.veil/memory/agents/hello/MEMORY.md +12 -0
  14. package/.veil/runtime.pid +1 -0
  15. package/.veil/settings.json +10 -0
  16. package/.veil-studio/studio.db +0 -0
  17. package/.veil-studio/studio.db-shm +0 -0
  18. package/.veil-studio/studio.db-wal +0 -0
  19. package/PLAN/01-vision.md +26 -0
  20. package/PLAN/02-tech-stack.md +94 -0
  21. package/PLAN/03-agents.md +232 -0
  22. package/PLAN/04-runtime.md +171 -0
  23. package/PLAN/05-tools.md +211 -0
  24. package/PLAN/06-communication.md +243 -0
  25. package/PLAN/07-storage.md +218 -0
  26. package/PLAN/08-api-cli.md +153 -0
  27. package/PLAN/09-permissions.md +108 -0
  28. package/PLAN/10-ably.md +105 -0
  29. package/PLAN/11-file-formats.md +442 -0
  30. package/PLAN/12-folder-structure.md +205 -0
  31. package/PLAN/13-operations.md +212 -0
  32. package/PLAN/README.md +23 -0
  33. package/README.md +128 -0
  34. package/REPORT.md +174 -0
  35. package/TODO.md +45 -0
  36. package/ai-tests/FRONTEND_PROMPT.md +220 -0
  37. package/ai-tests/Research & Planning.md +814 -0
  38. package/ai-tests/prompt-001-basic-api.md +230 -0
  39. package/ai-tests/prompt-002-basic-flows.md +230 -0
  40. package/ai-tests/prompt-003-agent-behaviors.md +220 -0
  41. package/api/middleware.js +60 -0
  42. package/api/routes/agents.js +193 -0
  43. package/api/routes/chat.js +93 -0
  44. package/api/routes/completions.js +122 -0
  45. package/api/routes/daemons.js +80 -0
  46. package/api/routes/memory.js +169 -0
  47. package/api/routes/models.js +40 -0
  48. package/api/routes/remote-methods.js +74 -0
  49. package/api/routes/sessions.js +208 -0
  50. package/api/routes/settings.js +108 -0
  51. package/api/routes/system.js +50 -0
  52. package/api/routes/tasks.js +270 -0
  53. package/api/server.js +120 -0
  54. package/cli/formatter.js +70 -0
  55. package/cli/index.js +443 -0
  56. package/cli/parser.js +113 -0
  57. package/config/config.json +10 -0
  58. package/config/models.json +6826 -0
  59. package/core/agent.js +329 -0
  60. package/core/cancel.js +38 -0
  61. package/core/compaction.js +176 -0
  62. package/core/events.js +13 -0
  63. package/core/loop.js +564 -0
  64. package/core/memory.js +51 -0
  65. package/core/prompt.js +185 -0
  66. package/core/queue.js +96 -0
  67. package/core/registry.js +291 -0
  68. package/core/remote-methods.js +124 -0
  69. package/core/router.js +386 -0
  70. package/core/running-sessions.js +18 -0
  71. package/docs/api/01-system.md +84 -0
  72. package/docs/api/02-agents.md +374 -0
  73. package/docs/api/03-chat.md +269 -0
  74. package/docs/api/04-tasks.md +470 -0
  75. package/docs/api/05-sessions.md +444 -0
  76. package/docs/api/06-daemons.md +142 -0
  77. package/docs/api/07-memory.md +186 -0
  78. package/docs/api/08-settings.md +133 -0
  79. package/docs/api/09-models.md +119 -0
  80. package/docs/api/09-websocket.md +350 -0
  81. package/docs/api/10-completions.md +134 -0
  82. package/docs/api/README.md +116 -0
  83. package/docs/guide/01-quickstart.md +220 -0
  84. package/docs/guide/02-folder-structure.md +185 -0
  85. package/docs/guide/03-configuration.md +252 -0
  86. package/docs/guide/04-agents.md +267 -0
  87. package/docs/guide/05-cli.md +290 -0
  88. package/docs/guide/06-tools.md +643 -0
  89. package/docs/guide/07-permissions.md +236 -0
  90. package/docs/guide/08-memory.md +139 -0
  91. package/docs/guide/09-multi-agent.md +271 -0
  92. package/docs/guide/10-daemons.md +226 -0
  93. package/docs/guide/README.md +53 -0
  94. package/docs/index.html +623 -0
  95. package/examples/README.md +151 -0
  96. package/examples/agents/assistant/AGENT.md +31 -0
  97. package/examples/agents/assistant/SOUL.md +9 -0
  98. package/examples/agents/assistant/agent.json +74 -0
  99. package/examples/agents/hello/AGENT.md +15 -0
  100. package/examples/agents/hello/agent.json +14 -0
  101. package/examples/agents/monitor/AGENT.md +51 -0
  102. package/examples/agents/monitor/agent.json +33 -0
  103. package/examples/agents/monitor/heartbeats/monitor.md +24 -0
  104. package/examples/agents/orchestrator/AGENT.md +70 -0
  105. package/examples/agents/orchestrator/agent.json +30 -0
  106. package/examples/agents/researcher/AGENT.md +52 -0
  107. package/examples/agents/researcher/agent.json +49 -0
  108. package/examples/agents/researcher/skills/web-research.md +28 -0
  109. package/examples/skills/code-review.md +72 -0
  110. package/examples/skills/summarise.md +59 -0
  111. package/examples/skills/web-research.md +42 -0
  112. package/examples/tools/word-count/index.js +27 -0
  113. package/examples/tools/word-count/tool.json +18 -0
  114. package/infrastructure/database.js +563 -0
  115. package/infrastructure/scheduler.js +122 -0
  116. package/llm/client.js +206 -0
  117. package/migrations/001-initial.sql +121 -0
  118. package/migrations/002-debuggability.sql +13 -0
  119. package/migrations/003-drop-orphaned-columns.sql +72 -0
  120. package/migrations/004-session-message-token-fields.sql +78 -0
  121. package/migrations/005-session-thinking.sql +5 -0
  122. package/package.json +30 -0
  123. package/schemas/agent.json +143 -0
  124. package/schemas/settings.json +111 -0
  125. package/scripts/fetch-models.js +93 -0
  126. package/session-debug-scenario.md +248 -0
  127. package/settings/fields.js +52 -0
  128. package/system-prompts/base-core.md +7 -0
  129. package/system-prompts/environment.md +13 -0
  130. package/system-prompts/reminders/anti-drift.md +6 -0
  131. package/system-prompts/reminders/stall-recovery.md +10 -0
  132. package/system-prompts/safety-rules.md +25 -0
  133. package/system-prompts/task-heuristics.md +27 -0
  134. package/test/client.js +71 -0
  135. package/test/integration/01-health.test.js +25 -0
  136. package/test/integration/02-agents.test.js +80 -0
  137. package/test/integration/03-chat-hello.test.js +48 -0
  138. package/test/integration/04-chat-multiturn.test.js +61 -0
  139. package/test/integration/05-chat-writer.test.js +48 -0
  140. package/test/integration/06-task-basic.test.js +68 -0
  141. package/test/integration/07-task-tools.test.js +74 -0
  142. package/test/integration/08-task-code-analysis.test.js +69 -0
  143. package/test/integration/09-memory-analyst.test.js +63 -0
  144. package/test/integration/10-task-advanced.test.js +85 -0
  145. package/test/integration/11-sessions-advanced.test.js +84 -0
  146. package/test/integration/12-assistant-chat-tools.test.js +75 -0
  147. package/test/integration/13-edge-cases.test.js +99 -0
  148. package/test/integration/14-cancel.test.js +62 -0
  149. package/test/integration/15-debug.test.js +106 -0
  150. package/test/integration/16-memory-api.test.js +83 -0
  151. package/test/integration/17-settings-api.test.js +41 -0
  152. package/test/integration/18-tool-search-activation.test.js +119 -0
  153. package/test/results/.gitkeep +0 -0
  154. package/test/runner.js +206 -0
  155. package/test/smoke.js +216 -0
  156. package/tools/agent_message.js +85 -0
  157. package/tools/agent_send.js +80 -0
  158. package/tools/agent_spawn.js +44 -0
  159. package/tools/bash.js +49 -0
  160. package/tools/edit_file.js +41 -0
  161. package/tools/glob.js +64 -0
  162. package/tools/grep.js +82 -0
  163. package/tools/list_dir.js +63 -0
  164. package/tools/log_write.js +31 -0
  165. package/tools/memory_read.js +38 -0
  166. package/tools/memory_search.js +65 -0
  167. package/tools/memory_write.js +42 -0
  168. package/tools/read_file.js +48 -0
  169. package/tools/sleep.js +22 -0
  170. package/tools/task_create.js +41 -0
  171. package/tools/task_respond.js +37 -0
  172. package/tools/task_spawn.js +64 -0
  173. package/tools/task_status.js +39 -0
  174. package/tools/task_subscribe.js +37 -0
  175. package/tools/todo_read.js +26 -0
  176. package/tools/todo_write.js +38 -0
  177. package/tools/tool_activate.js +24 -0
  178. package/tools/tool_search.js +24 -0
  179. package/tools/web_fetch.js +50 -0
  180. package/tools/web_search.js +52 -0
  181. package/tools/write_file.js +28 -0
  182. package/ui/api.js +190 -0
  183. package/ui/app.js +281 -0
  184. package/ui/index.html +382 -0
  185. package/ui/views/agents.js +377 -0
  186. package/ui/views/chat.js +610 -0
  187. package/ui/views/connection.js +96 -0
  188. package/ui/views/daemons.js +129 -0
  189. package/ui/views/feed.js +194 -0
  190. package/ui/views/memory.js +263 -0
  191. package/ui/views/models.js +146 -0
  192. package/ui/views/sessions.js +314 -0
  193. package/ui/views/settings.js +142 -0
  194. package/ui/views/tasks.js +415 -0
  195. package/utils/context.js +49 -0
  196. package/utils/id.js +16 -0
  197. package/utils/models.js +88 -0
  198. package/utils/paths.js +213 -0
  199. 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;