@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
package/core/prompt.js ADDED
@@ -0,0 +1,185 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const paths = require('../utils/paths');
7
+ const context = require('../utils/context');
8
+
9
+ const PROMPTS_DIR = path.join(__dirname, '..', 'system-prompts');
10
+
11
+ /**
12
+ * Read a system prompt template file.
13
+ * @param {string} filename - Relative to system-prompts/
14
+ * @returns {string}
15
+ */
16
+ function readPromptFile(filename) {
17
+ try {
18
+ return fs.readFileSync(path.join(PROMPTS_DIR, filename), 'utf8');
19
+ } catch {
20
+ return '';
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Replace template variables in a prompt string.
26
+ * @param {string} text
27
+ * @param {Object} vars - Key/value map of variables
28
+ * @returns {string}
29
+ */
30
+ function interpolate(text, vars) {
31
+ return text.replace(/\{\{(\w+)\}\}/g, (_, key) => {
32
+ return vars[key] !== undefined ? String(vars[key]) : `{{${key}}}`;
33
+ });
34
+ }
35
+
36
+ /**
37
+ * Read a markdown file safely.
38
+ * @param {string} filePath
39
+ * @returns {string}
40
+ */
41
+ function readMd(filePath) {
42
+ try {
43
+ return fs.readFileSync(filePath, 'utf8').trim();
44
+ } catch {
45
+ return '';
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Get git status for the project directory.
51
+ * @param {string} cwd
52
+ * @returns {string}
53
+ */
54
+ function getGitStatus(cwd) {
55
+ try {
56
+ const { execSync } = require('child_process');
57
+ const branch = execSync('git branch --show-current 2>/dev/null', { cwd, timeout: 3000 }).toString().trim();
58
+ const status = execSync('git status --short 2>/dev/null | head -20', { cwd, timeout: 3000 }).toString().trim();
59
+ if (!branch) return 'Not a git repository';
60
+ return `Branch: ${branch}\n${status || '(clean)'}`;
61
+ } catch {
62
+ return 'Not a git repository';
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Assemble the 7-layer system prompt for an agent session.
68
+ *
69
+ * @param {{ agent: Object, mode: string, sessionId: string, taskId?: string, cwd: string, settings: Object, memory?: string, todos?: Object[], taskBrief?: string }} opts
70
+ * @returns {string} Full system prompt text
71
+ */
72
+ function assembleSystemPrompt({ agent, mode, sessionId, taskId, cwd, settings, memory, todos, taskBrief }) {
73
+ const layers = [];
74
+ const version = context.getVersion();
75
+
76
+ // ── Layer 1: Base Core (static) ───────────────────────────────────────────
77
+ const baseCoreVars = {
78
+ VERSION: version,
79
+ AGENT_NAME: agent.name,
80
+ MODE: mode,
81
+ SESSION_ID: sessionId || '',
82
+ };
83
+ layers.push(interpolate(readPromptFile('base-core.md'), baseCoreVars));
84
+
85
+ // ── Layer 2: Safety & Rules (static) ─────────────────────────────────────
86
+ // layers.push(readPromptFile('safety-rules.md'));
87
+
88
+ // ── Layer 3: Environment (dynamic) ───────────────────────────────────────
89
+ const mcpServersList = settings.mcpServers && Object.keys(settings.mcpServers).length > 0
90
+ ? Object.keys(settings.mcpServers).map(s => `- ${s}`).join('\n')
91
+ : 'None configured';
92
+
93
+ const envVars = {
94
+ OS: `${os.type()} ${os.release()}`,
95
+ SHELL: process.env.SHELL || '/bin/sh',
96
+ CWD: cwd,
97
+ DATETIME: new Date().toISOString(),
98
+ DATEONLY: new Date().toISOString().split('T')[0],
99
+ NODE_VERSION: process.version,
100
+ GIT_STATUS: getGitStatus(cwd),
101
+ MCP_SERVERS: mcpServersList,
102
+ };
103
+ layers.push(interpolate(readPromptFile('environment.md'), envVars));
104
+
105
+ // ── Layer 4: Agent Instructions ───────────────────────────────────────────
106
+ const agentInstructions = [];
107
+
108
+ // Global project AGENT.md
109
+ const projectAgentMd = readMd(paths.getProjectAgentMdPath(cwd));
110
+ if (projectAgentMd) agentInstructions.push(`## Project Instructions\n\n${projectAgentMd}`);
111
+
112
+ // Agent-specific AGENT.md
113
+ if (agent.agentMd) agentInstructions.push(`## Agent Instructions\n\n${agent.agentMd}`);
114
+
115
+ // SOUL.md
116
+ if (agent.soulMd) agentInstructions.push(`## Persona\n\n${agent.soulMd}`);
117
+
118
+ // Memory
119
+ const agentMemoryPath = paths.getAgentMemoryDir(cwd, agent.name);
120
+ const agentMemoryMd = readMd(path.join(agentMemoryPath, 'MEMORY.md'));
121
+ const projectMemoryMd = readMd(path.join(paths.getProjectMemoryDir(cwd), 'MEMORY.md'));
122
+ if (agentMemoryMd) agentInstructions.push(`## Agent Memory\n\n${agentMemoryMd}`);
123
+ if (projectMemoryMd) agentInstructions.push(`## Project Memory\n\n${projectMemoryMd}`);
124
+ if (memory) agentInstructions.push(`## Injected Memory\n\n${memory}`);
125
+
126
+ if (agentInstructions.length > 0) {
127
+ layers.push(agentInstructions.join('\n\n---\n\n'));
128
+ }
129
+
130
+ // ── Layer 5: Tools & Capabilities ────────────────────────────────────────
131
+ // Tool descriptions are injected at the API level (tools parameter), not in system prompt.
132
+ // Only listing custom tools summary here as Tier 2.
133
+ // (Registry handles Tier 1 full schemas via buildToolsForLLM)
134
+
135
+ // ── Layer 6: Task Heuristics ──────────────────────────────────────────────
136
+ if (mode === 'task' || mode === 'daemon' || mode === 'subagent') {
137
+ layers.push(readPromptFile('task-heuristics.md'));
138
+ }
139
+
140
+ // ── Layer 7: Session Context ──────────────────────────────────────────────
141
+ const sessionCtx = [];
142
+ if (taskBrief) sessionCtx.push(`## Current Task\n\n${taskBrief}`);
143
+ if (todos && todos.length > 0) {
144
+ const todoList = todos.map(t => `- [${t.status === 'completed' ? 'x' : ' '}] ${t.content}`).join('\n');
145
+ sessionCtx.push(`## Active TODOs\n\n${todoList}`);
146
+ }
147
+ if (sessionCtx.length > 0) {
148
+ layers.push(sessionCtx.join('\n\n'));
149
+ }
150
+
151
+ return layers.filter(Boolean).join('\n\n---\n\n').trim();
152
+ }
153
+
154
+ /**
155
+ * Build a compaction prompt — summarizes conversation for context compression.
156
+ * @param {{ messages: Object[], taskBrief?: string }} opts
157
+ * @returns {string}
158
+ */
159
+ function buildCompactionPrompt({ messages, taskBrief }) {
160
+ return `You are summarizing a conversation to reduce context length.
161
+
162
+ ${taskBrief ? `The task was: ${taskBrief}\n\n` : ''}Summarize the conversation below, preserving:
163
+ - Key decisions made
164
+ - Important findings and facts discovered
165
+ - Files read or modified (with their paths)
166
+ - Active problems or unresolved issues
167
+ - Any errors encountered and how they were handled
168
+ - Current state and what step comes next
169
+
170
+ Be concise but complete. The summary will replace the full conversation history.
171
+
172
+ Format as structured markdown with clear sections.`;
173
+ }
174
+
175
+ /**
176
+ * Build a reminder injection for mid-conversation.
177
+ * @param {{ type: 'anti-drift'|'stall-recovery', vars?: Object }} opts
178
+ * @returns {string}
179
+ */
180
+ function buildReminder({ type, vars = {} }) {
181
+ const filename = `reminders/${type}.md`;
182
+ return interpolate(readPromptFile(filename), vars);
183
+ }
184
+
185
+ module.exports = { assembleSystemPrompt, buildCompactionPrompt, buildReminder };
package/core/queue.js ADDED
@@ -0,0 +1,96 @@
1
+ 'use strict';
2
+
3
+ const db = require('../infrastructure/database');
4
+
5
+ /**
6
+ * In-memory agent message queue helper.
7
+ * The actual queue is in SQLite (agent_messages table).
8
+ * This module provides higher-level queue operations.
9
+ */
10
+
11
+ /**
12
+ * Get and consume all non-followup pending messages for an agent.
13
+ * Returns them as user-role messages to inject into conversation.
14
+ * Messages with a target_session_id are only drained by that specific session.
15
+ * @param {string} agentName
16
+ * @param {string|null} sessionId - The current session ID (used to match target_session_id)
17
+ * @returns {{ messages: Object[], correlationIds: string[] }}
18
+ */
19
+ function drainNonFollowup(agentName, sessionId = null) {
20
+ const pending = db.getPendingAgentMessages(agentName);
21
+ const messages = [];
22
+ const correlationIds = [];
23
+
24
+ for (const msg of pending) {
25
+ if (msg.followup) continue;
26
+ if (msg.target_session_id && msg.target_session_id !== sessionId) continue;
27
+ messages.push({
28
+ role: 'user',
29
+ content: `[Message from ${msg.from_agent}]: ${msg.content}`,
30
+ _correlationId: msg.correlation_id || null,
31
+ _messageId: msg.id,
32
+ });
33
+ if (msg.correlation_id) correlationIds.push(msg.correlation_id);
34
+ db.markAgentMessageDelivered(msg.id);
35
+ }
36
+
37
+ return { messages, correlationIds };
38
+ }
39
+
40
+ /**
41
+ * Get and consume all followup pending messages for an agent.
42
+ * Messages with a target_session_id are only drained by that specific session.
43
+ * @param {string} agentName
44
+ * @param {string|null} sessionId - The current session ID (used to match target_session_id)
45
+ * @returns {Object[]}
46
+ */
47
+ function drainFollowup(agentName, sessionId = null) {
48
+ const pending = db.getPendingAgentMessages(agentName);
49
+ const messages = [];
50
+
51
+ for (const msg of pending) {
52
+ if (!msg.followup) continue;
53
+ if (msg.target_session_id && msg.target_session_id !== sessionId) continue;
54
+ messages.push({
55
+ role: 'user',
56
+ content: `[Message from ${msg.from_agent}]: ${msg.content}`,
57
+ });
58
+ db.markAgentMessageDelivered(msg.id);
59
+ }
60
+
61
+ return messages;
62
+ }
63
+
64
+ /**
65
+ * Post a response to a correlated agent_message (sync messaging).
66
+ * @param {string} correlationId
67
+ * @param {string} response
68
+ */
69
+ function postCorrelatedResponse(correlationId, response) {
70
+ if (!correlationId) return;
71
+ db.getDb().prepare(
72
+ "UPDATE agent_messages SET response = ?, delivered_at = ? WHERE correlation_id = ? AND status = 'delivered'"
73
+ ).run(response, new Date().toISOString(), correlationId);
74
+ }
75
+
76
+ /**
77
+ * Notify all durable subscribers of a completed task.
78
+ * Injects a message into each subscriber's session.
79
+ * @param {{ taskId: string, task: Object }} opts
80
+ */
81
+ function notifyTaskSubscribers({ taskId, task }) {
82
+ const subs = db.getPendingSubscriptions(taskId);
83
+ for (const sub of subs) {
84
+ const content = `[Task ${taskId} completed] Agent: ${task.agent_name}, Status: ${task.status}${task.output ? `\nOutput: ${String(task.output).slice(0, 500)}` : ''}`;
85
+ db.enqueueAgentMessage({
86
+ targetAgent: sub.subscriber_agent,
87
+ targetSessionId: sub.subscriber_session_id,
88
+ fromAgent: 'system',
89
+ content,
90
+ followup: false,
91
+ });
92
+ db.markSubscriptionDelivered(sub.id);
93
+ }
94
+ }
95
+
96
+ module.exports = { drainNonFollowup, drainFollowup, postCorrelatedResponse, notifyTaskSubscribers };
@@ -0,0 +1,291 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const Ajv = require('ajv');
6
+ const addFormats = require('ajv-formats');
7
+ const paths = require('../utils/paths');
8
+
9
+ const ajv = new Ajv({ allErrors: true });
10
+ addFormats(ajv);
11
+
12
+ // Cache of compiled validators per tool name
13
+ const _validators = new Map();
14
+
15
+ // Registry of all built-in tools: name → { schema, execute }
16
+ const _builtins = new Map();
17
+
18
+ /**
19
+ * Register a built-in tool.
20
+ * @param {string} name
21
+ * @param {{ schema: Object, execute: Function }} tool
22
+ */
23
+ function registerBuiltin(name, tool) {
24
+ if (!tool.schema || !tool.execute) {
25
+ throw new Error(`Tool "${name}" must have schema and execute`);
26
+ }
27
+ _builtins.set(name, tool);
28
+ if (tool.schema.input_schema) {
29
+ _validators.set(name, ajv.compile(tool.schema.input_schema));
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Load and register all built-in tools from the tools/ directory.
35
+ * @param {string} toolsDir - Absolute path to the tools/ directory
36
+ */
37
+ function loadBuiltinTools(toolsDir) {
38
+ const files = fs.readdirSync(toolsDir).filter(f => f.endsWith('.js'));
39
+ for (const file of files) {
40
+ try {
41
+ const tool = require(path.join(toolsDir, file));
42
+ if (tool.schema && tool.execute) {
43
+ registerBuiltin(tool.schema.name, tool);
44
+ }
45
+ } catch (err) {
46
+ console.error(`Failed to load built-in tool ${file}: ${err.message}`);
47
+ }
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Load a custom JS tool from a folder.
53
+ * @param {string} toolFolder - Absolute path to the tool folder
54
+ * @returns {{ schema: Object, execute: Function }|null}
55
+ */
56
+ function loadCustomTool(toolFolder) {
57
+ const manifestPath = path.join(toolFolder, 'tool.json');
58
+ const indexPath = path.join(toolFolder, 'index.js');
59
+ if (!fs.existsSync(manifestPath) || !fs.existsSync(indexPath)) return null;
60
+ try {
61
+ const schema = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
62
+ // Hot reload: delete require cache before loading
63
+ delete require.cache[require.resolve(indexPath)];
64
+ const execute = require(indexPath);
65
+ return { schema, execute };
66
+ } catch (err) {
67
+ console.error(`Failed to load custom tool at ${toolFolder}: ${err.message}`);
68
+ return null;
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Discover custom tools from a directory (each subdirectory is a tool).
74
+ * @param {string} dir
75
+ * @returns {Map<string, { schema: Object, execute: Function }>}
76
+ */
77
+ function discoverCustomTools(dir) {
78
+ const result = new Map();
79
+ if (!fs.existsSync(dir)) return result;
80
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
81
+ if (!entry.isDirectory()) continue;
82
+ const tool = loadCustomTool(path.join(dir, entry.name));
83
+ if (tool && tool.schema && tool.schema.name) {
84
+ result.set(tool.schema.name, tool);
85
+ }
86
+ }
87
+ return result;
88
+ }
89
+
90
+ /**
91
+ * Resolve a tool by name, following the resolution order:
92
+ * built-in → agent-level → project-level → global → not found
93
+ *
94
+ * @param {{ name: string, agentConfig: Object, cwd: string }} opts
95
+ * @returns {{ schema: Object, execute: Function }|null}
96
+ */
97
+ function resolveTool({ name, agentConfig, cwd }) {
98
+ // 1. Built-in
99
+ if (_builtins.has(name)) return _builtins.get(name);
100
+
101
+ // 2. Agent-level
102
+ if (agentConfig && agentConfig.agentFolder) {
103
+ const agentToolDir = path.join(agentConfig.agentFolder, 'tools', name);
104
+ const agentTool = loadCustomTool(agentToolDir);
105
+ if (agentTool) return agentTool;
106
+ }
107
+
108
+ // 3. Project-level
109
+ const projectTool = loadCustomTool(path.join(paths.getProjectToolsDir(cwd), name));
110
+ if (projectTool) return projectTool;
111
+
112
+ // 4. Global
113
+ const globalTool = loadCustomTool(path.join(paths.getGlobalToolsDir(), name));
114
+ if (globalTool) return globalTool;
115
+
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Build the tools list to send to the LLM.
121
+ * Tier 1 (built-in): full schemas
122
+ * Tier 2 (custom): names + one-line descriptions only (full schema loaded via tool_search)
123
+ *
124
+ * @param {{ agentConfig: Object, cwd: string, modeConfig?: Object }} opts
125
+ * @returns {Object[]} OpenAI-format tool definitions
126
+ */
127
+ function buildToolsForLLM({ agentConfig, cwd, modeConfig = {} }) {
128
+ const allowedTools = modeConfig.tools || [];
129
+ const disallowedTools = new Set(modeConfig.disallowedTools || []);
130
+
131
+ // Auto-disable memory tools if memory is disabled for this agent
132
+ const memoryEnabled = agentConfig?.memory?.enabled !== false;
133
+ if (!memoryEnabled) {
134
+ disallowedTools.add('memory_write');
135
+ disallowedTools.add('memory_read');
136
+ }
137
+
138
+ const result = [];
139
+
140
+ // Tier 1: built-in tools — full schemas
141
+ for (const [name, tool] of _builtins) {
142
+ if (disallowedTools.has(name)) continue;
143
+ if (allowedTools.length > 0 && !allowedTools.includes(name)) continue;
144
+ result.push(toolToOpenAIFormat(tool.schema));
145
+ }
146
+
147
+ return result;
148
+ }
149
+
150
+ /**
151
+ * Get names + descriptions of all custom tools (Tier 2 listing).
152
+ * @param {{ agentConfig: Object, cwd: string }} opts
153
+ * @returns {Array<{ name: string, description: string }>}
154
+ */
155
+ function listCustomToolSummaries({ agentConfig, cwd }) {
156
+ const summaries = [];
157
+
158
+ // Agent-level
159
+ if (agentConfig && agentConfig.agentFolder) {
160
+ const agentToolsDir = path.join(agentConfig.agentFolder, 'tools');
161
+ for (const [name, tool] of discoverCustomTools(agentToolsDir)) {
162
+ summaries.push({ name, description: tool.schema.description || '' });
163
+ }
164
+ }
165
+
166
+ // Project-level
167
+ for (const [name, tool] of discoverCustomTools(paths.getProjectToolsDir(cwd))) {
168
+ summaries.push({ name, description: tool.schema.description || '' });
169
+ }
170
+
171
+ // Global
172
+ for (const [name, tool] of discoverCustomTools(paths.getGlobalToolsDir())) {
173
+ summaries.push({ name, description: tool.schema.description || '' });
174
+ }
175
+
176
+ return summaries;
177
+ }
178
+
179
+ /**
180
+ * Search tools by query string (name or description match).
181
+ * Returns full schemas for matching tools.
182
+ * @param {{ query: string, agentConfig: Object, cwd: string }} opts
183
+ * @returns {Object[]} Full tool schemas
184
+ */
185
+ function searchTools({ query, agentConfig, cwd }) {
186
+ const lq = query.toLowerCase();
187
+ const results = [];
188
+
189
+ for (const [name, tool] of _builtins) {
190
+ if (name.includes(lq) || (tool.schema.description || '').toLowerCase().includes(lq)) {
191
+ results.push(tool.schema);
192
+ }
193
+ }
194
+
195
+ if (agentConfig && agentConfig.agentFolder) {
196
+ for (const [, tool] of discoverCustomTools(path.join(agentConfig.agentFolder, 'tools'))) {
197
+ const n = tool.schema.name || '';
198
+ if (n.includes(lq) || (tool.schema.description || '').toLowerCase().includes(lq)) {
199
+ results.push(tool.schema);
200
+ }
201
+ }
202
+ }
203
+
204
+ for (const [, tool] of discoverCustomTools(paths.getProjectToolsDir(cwd))) {
205
+ const n = tool.schema.name || '';
206
+ if (n.includes(lq) || (tool.schema.description || '').toLowerCase().includes(lq)) {
207
+ results.push(tool.schema);
208
+ }
209
+ }
210
+
211
+ return results;
212
+ }
213
+
214
+ /**
215
+ * Validate tool input arguments against the tool's JSON schema.
216
+ * @param {string} toolName
217
+ * @param {Object} input
218
+ * @returns {{ valid: boolean, errors?: string[] }}
219
+ */
220
+ function validateToolInput(toolName, input) {
221
+ const validator = _validators.get(toolName);
222
+ if (!validator) return { valid: true };
223
+ const valid = validator(input);
224
+ if (!valid) {
225
+ return { valid: false, errors: validator.errors.map(e => `${e.instancePath || '(root)'}: ${e.message}`) };
226
+ }
227
+ return { valid: true };
228
+ }
229
+
230
+ /**
231
+ * Convert our tool schema format to OpenAI function-calling format.
232
+ * @param {Object} schema
233
+ * @returns {Object}
234
+ */
235
+ function toolToOpenAIFormat(schema) {
236
+ return {
237
+ type: 'function',
238
+ function: {
239
+ name: schema.name,
240
+ description: schema.description || '',
241
+ parameters: schema.input_schema || { type: 'object', properties: {} },
242
+ },
243
+ };
244
+ }
245
+
246
+ /**
247
+ * Resolve a custom tool by name and return its OpenAI-format schema, applying
248
+ * the same whitelist/blocklist rules as buildToolsForLLM.
249
+ * Also registers an AJV validator for the tool if not already registered.
250
+ * Returns null if the tool is blocked, not in the whitelist, or not found.
251
+ *
252
+ * @param {string} name - Tool name to activate
253
+ * @param {Object} agentConfig
254
+ * @param {string} cwd
255
+ * @param {Object} modeConfig
256
+ * @returns {Object|null} OpenAI-format tool definition, or null
257
+ */
258
+ function activateToolByName(name, agentConfig, cwd, modeConfig = {}) {
259
+ const allowedTools = modeConfig.tools || [];
260
+ const disallowedTools = new Set(modeConfig.disallowedTools || []);
261
+
262
+ if (disallowedTools.has(name)) return null;
263
+ if (allowedTools.length > 0 && !allowedTools.includes(name)) return null;
264
+
265
+ const tool = resolveTool({ name, agentConfig, cwd });
266
+ if (!tool) return null;
267
+
268
+ if (tool.schema.input_schema && !_validators.has(name)) {
269
+ try { _validators.set(name, ajv.compile(tool.schema.input_schema)); } catch {}
270
+ }
271
+
272
+ return toolToOpenAIFormat(tool.schema);
273
+ }
274
+
275
+ /**
276
+ * Get all registered built-in tool names.
277
+ * @returns {string[]}
278
+ */
279
+ function getBuiltinNames() {
280
+ return Array.from(_builtins.keys());
281
+ }
282
+
283
+ module.exports = {
284
+ loadBuiltinTools,
285
+ resolveTool,
286
+ buildToolsForLLM,
287
+ listCustomToolSummaries,
288
+ searchTools,
289
+ validateToolInput,
290
+ activateToolByName,
291
+ };
@@ -0,0 +1,124 @@
1
+ 'use strict';
2
+
3
+ const { randomUUID } = require('crypto');
4
+
5
+ /**
6
+ * In-memory Remote Method Execution queue.
7
+ *
8
+ * Allows custom tools to call remoteMethodExecution({ method, data, timeoutMs })
9
+ * which blocks until a connected UI client posts a result via
10
+ * POST /remote-methods/:id/result.
11
+ *
12
+ * Methods do NOT survive a server restart (by design).
13
+ * SSE clients are notified of new pending methods and of completed ones
14
+ * (so secondary clients know to discard them).
15
+ */
16
+
17
+ /** @type {Map<string, { id: string, method: string, data: any, createdAt: string, resolve: Function, reject: Function, timer: NodeJS.Timeout }>} */
18
+ const _pending = new Map();
19
+
20
+ /** @type {Set<import('http').ServerResponse>} */
21
+ const _clients = new Set();
22
+
23
+ /**
24
+ * Send an SSE event to all connected clients.
25
+ * @param {{ type: string, [key: string]: any }} payload
26
+ */
27
+ function _broadcast(payload) {
28
+ const line = `data: ${JSON.stringify(payload)}\n\n`;
29
+ for (const res of _clients) {
30
+ try {
31
+ res.write(line);
32
+ } catch {
33
+ _clients.delete(res);
34
+ }
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Register a new SSE client. Immediately flushes all currently-pending
40
+ * methods so the client is fully caught up.
41
+ * @param {import('http').ServerResponse} res
42
+ */
43
+ function addClient(res) {
44
+ _clients.add(res);
45
+ // Flush current pending methods to the new client
46
+ for (const entry of _pending.values()) {
47
+ try {
48
+ res.write(`data: ${JSON.stringify({
49
+ type: 'method.pending',
50
+ id: entry.id,
51
+ method: entry.method,
52
+ data: entry.data,
53
+ createdAt: entry.createdAt,
54
+ })}\n\n`);
55
+ } catch { /* client may have already gone */ }
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Remove an SSE client (on disconnect).
61
+ * @param {import('http').ServerResponse} res
62
+ */
63
+ function removeClient(res) {
64
+ _clients.delete(res);
65
+ }
66
+
67
+ /**
68
+ * Enqueue a remote method call. Returns a Promise that resolves when the UI
69
+ * posts a result, or rejects on timeout.
70
+ *
71
+ * @param {{ method: string, data?: any, timeoutMs?: number }} opts
72
+ * @returns {Promise<any>} Resolves with whatever the UI sends as `result`
73
+ */
74
+ function enqueue({ method, data = null, timeoutMs = 120_000 }) {
75
+ const id = randomUUID();
76
+ const createdAt = new Date().toISOString();
77
+
78
+ return new Promise((resolve, reject) => {
79
+ const timer = setTimeout(() => {
80
+ if (_pending.has(id)) {
81
+ _pending.delete(id);
82
+ _broadcast({ type: 'method.done', id, timedOut: true });
83
+ reject(new Error(`remoteMethodExecution timed out after ${timeoutMs}ms (id=${id})`));
84
+ }
85
+ }, timeoutMs);
86
+
87
+ _pending.set(id, { id, method, data, createdAt, resolve, reject, timer });
88
+
89
+ // Notify all connected UIs
90
+ _broadcast({ type: 'method.pending', id, method, data, createdAt });
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Resolve a pending remote method call with a result from the UI.
96
+ *
97
+ * @param {string} id
98
+ * @param {any} result
99
+ * @returns {'ok'|'not_found'|'already_resolved'}
100
+ */
101
+ function resolve(id, result) {
102
+ const entry = _pending.get(id);
103
+ if (!entry) return 'not_found';
104
+
105
+ clearTimeout(entry.timer);
106
+ _pending.delete(id);
107
+
108
+ // Notify ALL clients (including the one that just responded) so multi-client
109
+ // setups can discard the pending method immediately.
110
+ _broadcast({ type: 'method.done', id, timedOut: false });
111
+
112
+ entry.resolve(result);
113
+ return 'ok';
114
+ }
115
+
116
+ /**
117
+ * Return a snapshot of all currently-pending method IDs (for diagnostics).
118
+ * @returns {string[]}
119
+ */
120
+ function pendingIds() {
121
+ return Array.from(_pending.keys());
122
+ }
123
+
124
+ module.exports = { addClient, removeClient, enqueue, resolve, pendingIds };