@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,80 @@
1
+ 'use strict';
2
+
3
+ const db = require('../infrastructure/database');
4
+ const { loadAgent } = require('../core/agent');
5
+ const runningSessions = require('../core/running-sessions');
6
+
7
+ const schema = {
8
+ name: 'agent_send',
9
+ description: 'Send a message to another agent without waiting for a response (fire-and-forget async).',
10
+ input_schema: {
11
+ type: 'object',
12
+ properties: {
13
+ agent: { type: 'string', description: 'Name of the target agent' },
14
+ sessionId: { type: 'string', description: 'Session ID of the target agent session to send the message to. Obtain this from agent_spawn.' },
15
+ message: { type: 'string', description: 'Message to send' },
16
+ followup: { type: 'boolean', description: 'If true, message is only delivered after the agent finishes its current task (default: false)' },
17
+ },
18
+ required: ['agent', 'sessionId', 'message'],
19
+ },
20
+ timeout: 5,
21
+ };
22
+
23
+ async function execute({ agent, message, sessionId, followup = false, _cwd, _settings, _agent: senderAgent }) {
24
+ try { loadAgent({ cwd: _cwd, name: agent }); } catch (err) {
25
+ return `Error: target agent "${agent}" not found — ${err.message}`;
26
+ }
27
+ const targetSession = db.getSession(sessionId);
28
+ if (!targetSession) return `Error: session "${sessionId}" not found.`;
29
+ if (targetSession.agent_name !== agent) return `Error: session "${sessionId}" belongs to agent "${targetSession.agent_name}", not "${agent}".`;
30
+
31
+ const fromAgent = senderAgent?.name || 'unknown';
32
+
33
+ // Idle chat session — fire-and-forget a new turn
34
+ if (!runningSessions.has(sessionId) && targetSession.mode === 'chat') {
35
+ const { runChat } = require('../core/router');
36
+ runChat({
37
+ agentName: agent,
38
+ message: `[Message from ${fromAgent}]: ${message}`,
39
+ sessionId,
40
+ cwd: _cwd,
41
+ settings: _settings,
42
+ }).catch(() => {});
43
+ return `Message sent to session "${sessionId}" (agent: ${agent}) — new chat turn triggered.`;
44
+ }
45
+
46
+ // Active session or non-chat idle session — inject via queue
47
+ const msgId = db.enqueueAgentMessage({
48
+ targetAgent: agent,
49
+ targetSessionId: sessionId,
50
+ fromAgent,
51
+ content: message,
52
+ followup,
53
+ });
54
+
55
+ // Background watcher: if the loop ends before picking up the message, fall back to runChat
56
+ if (targetSession.mode === 'chat') {
57
+ const wrappedMessage = `[Message from ${fromAgent}]: ${message}`;
58
+ const watchCwd = _cwd;
59
+ const watchSettings = _settings;
60
+ (async () => {
61
+ // Watch until the loop ends or the message is delivered — no hard deadline
62
+ while (runningSessions.has(sessionId)) {
63
+ await new Promise(r => setTimeout(r, 300));
64
+ const row = db.getDb().prepare('SELECT status FROM agent_messages WHERE id = ?').get(msgId);
65
+ if (!row || row.status !== 'pending') return; // delivered
66
+ }
67
+ // Loop ended — check if message was left undelivered
68
+ const row = db.getDb().prepare('SELECT status FROM agent_messages WHERE id = ?').get(msgId);
69
+ if (row && row.status === 'pending') {
70
+ db.getDb().prepare('DELETE FROM agent_messages WHERE id = ? AND status = ?').run(msgId, 'pending');
71
+ const { runChat } = require('../core/router');
72
+ runChat({ agentName: agent, message: wrappedMessage, sessionId, cwd: watchCwd, settings: watchSettings }).catch(() => {});
73
+ }
74
+ })().catch(() => {});
75
+ }
76
+
77
+ return `Message queued for session "${sessionId}" (agent: ${agent}, followup: ${followup}). It will be delivered at the next loop iteration of that session.`;
78
+ }
79
+
80
+ module.exports = { schema, execute };
@@ -0,0 +1,44 @@
1
+ 'use strict';
2
+
3
+ const { loadAgent } = require('../core/agent');
4
+
5
+ const schema = {
6
+ name: 'agent_spawn',
7
+ description: 'Start a new chat session with an agent and send the first message. Returns the sessionId and the agent response. Use the returned sessionId with agent_message for follow-up exchanges in the same conversation.',
8
+ input_schema: {
9
+ type: 'object',
10
+ properties: {
11
+ agent: { type: 'string', description: 'Name of the agent to start a session with' },
12
+ message: { type: 'string', description: 'First message to send to the agent' },
13
+ returnMode: { type: 'string', enum: ['full', 'responseOnly'], description: 'full = { sessionId, response } (default), responseOnly = response text only' },
14
+ },
15
+ required: ['agent', 'message'],
16
+ },
17
+ timeout: 86400,
18
+ };
19
+
20
+ async function execute({ agent, message, returnMode = 'full', _cwd, _settings }) {
21
+ try { loadAgent({ cwd: _cwd, name: agent }); } catch (err) {
22
+ return `Error: agent "${agent}" not found — ${err.message}`;
23
+ }
24
+
25
+ try {
26
+ const { runChat } = require('../core/router');
27
+ const result = await runChat({
28
+ agentName: agent,
29
+ message,
30
+ sessionId: undefined,
31
+ cwd: _cwd,
32
+ settings: _settings,
33
+ });
34
+
35
+ if (returnMode === 'responseOnly') {
36
+ return result.content || '(no response)';
37
+ }
38
+ return JSON.stringify({ sessionId: result.sessionId, response: result.content });
39
+ } catch (err) {
40
+ return `Error spawning session with "${agent}": ${err.message}`;
41
+ }
42
+ }
43
+
44
+ module.exports = { schema, execute };
package/tools/bash.js ADDED
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const { execSync } = require('child_process');
4
+
5
+ const schema = {
6
+ name: 'bash',
7
+ description: 'Execute a shell command and return its output. Use for running scripts, tests, builds, and system operations.',
8
+ input_schema: {
9
+ type: 'object',
10
+ properties: {
11
+ command: { type: 'string', description: 'The shell command to execute' },
12
+ timeout: { type: 'integer', description: 'Timeout in seconds (default: 30)', minimum: 1 },
13
+ workingDir: { type: 'string', description: 'Working directory for the command (default: project cwd)' },
14
+ },
15
+ required: ['command'],
16
+ },
17
+ timeout: 86400,
18
+ };
19
+
20
+ /**
21
+ * @param {{ command: string, timeout?: number, workingDir?: string, _cwd: string }} input
22
+ */
23
+ async function execute({ command, timeout = 30, workingDir, _cwd }) {
24
+ const cwd = workingDir || _cwd;
25
+ const timeoutMs = timeout * 1000;
26
+
27
+ try {
28
+ const output = execSync(command, {
29
+ cwd,
30
+ timeout: timeoutMs,
31
+ encoding: 'utf8',
32
+ maxBuffer: 10 * 1024 * 1024, // 10MB
33
+ env: process.env,
34
+ });
35
+ return output || '(no output)';
36
+ } catch (err) {
37
+ const stdout = err.stdout ? err.stdout.toString().trim() : '';
38
+ const stderr = err.stderr ? err.stderr.toString().trim() : '';
39
+ const exitCode = err.status || 1;
40
+
41
+ let result = `Exit code: ${exitCode}`;
42
+ if (stdout) result += `\nstdout:\n${stdout}`;
43
+ if (stderr) result += `\nstderr:\n${stderr}`;
44
+ if (err.killed) result = `Command timed out after ${timeout}s\n` + result;
45
+ return result;
46
+ }
47
+ }
48
+
49
+ module.exports = { schema, execute };
@@ -0,0 +1,41 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const schema = {
7
+ name: 'edit_file',
8
+ description: 'Search-and-replace edit on a file. Finds old_string and replaces with new_string. The old_string must match exactly (including whitespace). Read the file first before editing.',
9
+ input_schema: {
10
+ type: 'object',
11
+ properties: {
12
+ file: { type: 'string', description: 'Path to the file (relative to cwd or absolute)' },
13
+ old_string: { type: 'string', description: 'Exact string to find and replace' },
14
+ new_string: { type: 'string', description: 'String to replace it with' },
15
+ },
16
+ required: ['file', 'old_string', 'new_string'],
17
+ },
18
+ timeout: 10,
19
+ };
20
+
21
+ async function execute({ file, old_string, new_string, _cwd }) {
22
+ const filePath = path.isAbsolute(file) ? file : path.resolve(_cwd, file);
23
+
24
+ if (!fs.existsSync(filePath)) return `File not found: ${filePath}`;
25
+
26
+ const content = fs.readFileSync(filePath, 'utf8');
27
+ const occurrences = content.split(old_string).length - 1;
28
+
29
+ if (occurrences === 0) {
30
+ return `old_string not found in ${filePath}. The string must match exactly including whitespace and newlines.`;
31
+ }
32
+ if (occurrences > 1) {
33
+ return `old_string appears ${occurrences} times in ${filePath}. Provide a more unique string to avoid ambiguity.`;
34
+ }
35
+
36
+ const newContent = content.replace(old_string, new_string);
37
+ fs.writeFileSync(filePath, newContent, 'utf8');
38
+ return `Edited ${filePath}: replaced 1 occurrence`;
39
+ }
40
+
41
+ module.exports = { schema, execute };
package/tools/glob.js ADDED
@@ -0,0 +1,64 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const schema = {
7
+ name: 'glob',
8
+ description: 'Find files matching a glob pattern. Returns matching file paths.',
9
+ input_schema: {
10
+ type: 'object',
11
+ properties: {
12
+ pattern: { type: 'string', description: 'Glob pattern (e.g. "**/*.js", "src/**/*.test.js")' },
13
+ dir: { type: 'string', description: 'Base directory to search from (default: cwd)' },
14
+ ignore: { type: 'array', items: { type: 'string' }, description: 'Patterns to ignore (e.g. ["node_modules/**"])' },
15
+ },
16
+ required: ['pattern'],
17
+ },
18
+ timeout: 15,
19
+ };
20
+
21
+ /**
22
+ * Simple recursive glob matcher.
23
+ */
24
+ function matchGlob(filePath, pattern) {
25
+ const regexStr = pattern
26
+ .replace(/[.+^${}()|[\]\\]/g, '\\$&')
27
+ .replace(/\*\*/g, '__DS__')
28
+ .replace(/\*/g, '[^/]*')
29
+ .replace(/__DS__/g, '.*');
30
+ return new RegExp(`^${regexStr}$`).test(filePath);
31
+ }
32
+
33
+ function walkDir(dir, baseDir, ignore = []) {
34
+ const results = [];
35
+ let entries;
36
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return results; }
37
+
38
+ for (const entry of entries) {
39
+ const fullPath = path.join(dir, entry.name);
40
+ const relPath = path.relative(baseDir, fullPath).replace(/\\/g, '/');
41
+
42
+ if (ignore.some(p => matchGlob(relPath, p) || relPath.startsWith(p.replace('/**', '')))) continue;
43
+
44
+ if (entry.isDirectory()) {
45
+ results.push(...walkDir(fullPath, baseDir, ignore));
46
+ } else {
47
+ results.push(relPath);
48
+ }
49
+ }
50
+ return results;
51
+ }
52
+
53
+ async function execute({ pattern, dir, ignore = [], _cwd }) {
54
+ const baseDir = dir ? (path.isAbsolute(dir) ? dir : path.resolve(_cwd, dir)) : _cwd;
55
+ const defaultIgnore = ['node_modules/**', '.git/**', ...ignore];
56
+
57
+ const allFiles = walkDir(baseDir, baseDir, defaultIgnore);
58
+ const matches = allFiles.filter(f => matchGlob(f, pattern));
59
+
60
+ if (matches.length === 0) return `No files matched pattern: ${pattern}`;
61
+ return matches.join('\n');
62
+ }
63
+
64
+ module.exports = { schema, execute };
package/tools/grep.js ADDED
@@ -0,0 +1,82 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const schema = {
7
+ name: 'grep',
8
+ description: 'Search file contents for a pattern. Returns matching lines with file paths and line numbers.',
9
+ input_schema: {
10
+ type: 'object',
11
+ properties: {
12
+ pattern: { type: 'string', description: 'Search pattern (string or regex)' },
13
+ dir: { type: 'string', description: 'Directory to search (default: cwd)' },
14
+ file: { type: 'string', description: 'Specific file to search instead of directory' },
15
+ include: { type: 'string', description: 'File glob to include (e.g. "*.js")' },
16
+ ignoreCase: { type: 'boolean', description: 'Case-insensitive search (default: false)' },
17
+ maxResults: { type: 'integer', description: 'Maximum number of results (default: 50)', minimum: 1, maximum: 500 },
18
+ },
19
+ required: ['pattern'],
20
+ },
21
+ timeout: 20,
22
+ };
23
+
24
+ function matchesInclude(filename, include) {
25
+ if (!include) return true;
26
+ const pattern = include.replace(/\*/g, '.*').replace(/\?/g, '.');
27
+ return new RegExp(`^${pattern}$`).test(filename);
28
+ }
29
+
30
+ function searchFile(filePath, regex, results, maxResults) {
31
+ if (results.length >= maxResults) return;
32
+ let content;
33
+ try { content = fs.readFileSync(filePath, 'utf8'); } catch { return; }
34
+ const lines = content.split('\n');
35
+ for (let i = 0; i < lines.length; i++) {
36
+ if (results.length >= maxResults) break;
37
+ if (regex.test(lines[i])) {
38
+ results.push(`${filePath}:${i + 1}: ${lines[i]}`);
39
+ }
40
+ }
41
+ }
42
+
43
+ function walkDir(dir, regex, include, results, maxResults, ignoreList = ['node_modules', '.git']) {
44
+ if (results.length >= maxResults) return;
45
+ let entries;
46
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
47
+ for (const entry of entries) {
48
+ if (ignoreList.includes(entry.name)) continue;
49
+ const fullPath = path.join(dir, entry.name);
50
+ if (entry.isDirectory()) {
51
+ walkDir(fullPath, regex, include, results, maxResults, ignoreList);
52
+ } else if (matchesInclude(entry.name, include)) {
53
+ searchFile(fullPath, regex, results, maxResults);
54
+ }
55
+ }
56
+ }
57
+
58
+ async function execute({ pattern, dir, file, include, ignoreCase = false, maxResults = 50, _cwd }) {
59
+ const flags = ignoreCase ? 'gi' : 'g';
60
+ let regex;
61
+ try {
62
+ regex = new RegExp(pattern, flags);
63
+ } catch {
64
+ regex = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
65
+ }
66
+
67
+ const results = [];
68
+
69
+ if (file) {
70
+ const filePath = path.isAbsolute(file) ? file : path.resolve(_cwd, file);
71
+ searchFile(filePath, regex, results, maxResults);
72
+ } else {
73
+ const searchDir = dir ? (path.isAbsolute(dir) ? dir : path.resolve(_cwd, dir)) : _cwd;
74
+ walkDir(searchDir, regex, include, results, maxResults);
75
+ }
76
+
77
+ if (results.length === 0) return `No matches found for: ${pattern}`;
78
+ const truncated = results.length >= maxResults ? `\n(results truncated at ${maxResults})` : '';
79
+ return results.join('\n') + truncated;
80
+ }
81
+
82
+ module.exports = { schema, execute };
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const schema = {
7
+ name: 'list_dir',
8
+ description: 'List directory contents with file sizes and types.',
9
+ input_schema: {
10
+ type: 'object',
11
+ properties: {
12
+ dir: { type: 'string', description: 'Directory path (relative to cwd or absolute)' },
13
+ recursive: { type: 'boolean', description: 'List recursively (default: false)' },
14
+ },
15
+ required: ['dir'],
16
+ },
17
+ timeout: 10,
18
+ };
19
+
20
+ function listRecursive(dirPath, baseDir, depth = 0) {
21
+ const lines = [];
22
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
23
+ for (const entry of entries) {
24
+ const indent = ' '.repeat(depth);
25
+ const fullPath = path.join(dirPath, entry.name);
26
+ if (entry.isDirectory()) {
27
+ lines.push(`${indent}${entry.name}/`);
28
+ lines.push(...listRecursive(fullPath, baseDir, depth + 1));
29
+ } else {
30
+ const stat = fs.statSync(fullPath);
31
+ lines.push(`${indent}${entry.name} (${formatSize(stat.size)})`);
32
+ }
33
+ }
34
+ return lines;
35
+ }
36
+
37
+ function formatSize(bytes) {
38
+ if (bytes < 1024) return `${bytes}B`;
39
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
40
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
41
+ }
42
+
43
+ async function execute({ dir, recursive = false, _cwd }) {
44
+ const dirPath = path.isAbsolute(dir) ? dir : path.resolve(_cwd, dir);
45
+ if (!fs.existsSync(dirPath)) return `Directory not found: ${dirPath}`;
46
+ if (!fs.statSync(dirPath).isDirectory()) return `Not a directory: ${dirPath}`;
47
+
48
+ if (recursive) {
49
+ const lines = listRecursive(dirPath, dirPath);
50
+ return lines.length > 0 ? lines.join('\n') : '(empty directory)';
51
+ }
52
+
53
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
54
+ if (entries.length === 0) return '(empty directory)';
55
+
56
+ return entries.map(entry => {
57
+ if (entry.isDirectory()) return `${entry.name}/`;
58
+ const stat = fs.statSync(path.join(dirPath, entry.name));
59
+ return `${entry.name} (${formatSize(stat.size)})`;
60
+ }).join('\n');
61
+ }
62
+
63
+ module.exports = { schema, execute };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ const db = require('../infrastructure/database');
4
+
5
+ const schema = {
6
+ name: 'log_write',
7
+ description: 'Write a progress entry to the task log. Use to report milestones, status updates, and custom events.',
8
+ input_schema: {
9
+ type: 'object',
10
+ properties: {
11
+ message: { type: 'string', description: 'Log message' },
12
+ level: { type: 'string', enum: ['info', 'warn', 'error'], description: 'Log level (default: info)' },
13
+ metadata: { type: 'object', description: 'Optional structured metadata' },
14
+ },
15
+ required: ['message'],
16
+ },
17
+ timeout: 5,
18
+ };
19
+
20
+ async function execute({ message, level = 'info', metadata, _taskId }) {
21
+ if (_taskId) {
22
+ db.addTaskEvent({
23
+ taskId: _taskId,
24
+ type: 'custom',
25
+ data: { level, message, metadata: metadata || null },
26
+ });
27
+ }
28
+ return `Logged [${level}]: ${message}`;
29
+ }
30
+
31
+ module.exports = { schema, execute };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const paths = require('../utils/paths');
6
+
7
+ const schema = {
8
+ name: 'memory_read',
9
+ description: 'Read agent memory files. Defaults to MEMORY.md for the current agent.',
10
+ input_schema: {
11
+ type: 'object',
12
+ properties: {
13
+ file: { type: 'string', description: 'Memory file name (default: MEMORY.md). Can be a topic file like "research.md"' },
14
+ scope: { type: 'string', enum: ['agent', 'global'], description: 'Memory scope: "agent" (default) or "global" (project-level)' },
15
+ },
16
+ },
17
+ timeout: 10,
18
+ };
19
+
20
+ async function execute({ file = 'MEMORY.md', scope = 'agent', _cwd, _agent }) {
21
+ let memoryDir;
22
+
23
+ if (scope === 'global') {
24
+ memoryDir = paths.getProjectMemoryDir(_cwd);
25
+ } else {
26
+ memoryDir = paths.getAgentMemoryDir(_cwd, _agent.name);
27
+ }
28
+
29
+ const memoryFile = path.join(memoryDir, file);
30
+
31
+ if (!fs.existsSync(memoryFile)) {
32
+ return `Memory file not found: ${memoryFile} (no memories saved yet)`;
33
+ }
34
+
35
+ return fs.readFileSync(memoryFile, 'utf8');
36
+ }
37
+
38
+ module.exports = { schema, execute };
@@ -0,0 +1,65 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const paths = require('../utils/paths');
6
+
7
+ const schema = {
8
+ name: 'memory_search',
9
+ description: 'Search across project memory files for relevant content.',
10
+ input_schema: {
11
+ type: 'object',
12
+ properties: {
13
+ query: { type: 'string', description: 'Search query' },
14
+ scope: { type: 'string', enum: ['project', 'agent'], description: 'Search scope: "project" (all project memory) or "agent" (current agent only). Default: "project"' },
15
+ },
16
+ required: ['query'],
17
+ },
18
+ timeout: 10,
19
+ };
20
+
21
+ function searchInFile(filePath, queryWords) {
22
+ if (!fs.existsSync(filePath)) return [];
23
+ const content = fs.readFileSync(filePath, 'utf8');
24
+ const lines = content.split('\n');
25
+ const results = [];
26
+ for (let i = 0; i < lines.length; i++) {
27
+ const line = lines[i].toLowerCase();
28
+ if (queryWords.some(w => line.includes(w))) {
29
+ results.push(`${filePath}:${i + 1}: ${lines[i].trim()}`);
30
+ }
31
+ }
32
+ return results;
33
+ }
34
+
35
+ function searchDir(dir, queryWords, results, maxResults = 50) {
36
+ if (!fs.existsSync(dir)) return;
37
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
38
+ for (const entry of entries) {
39
+ if (results.length >= maxResults) break;
40
+ const fullPath = path.join(dir, entry.name);
41
+ if (entry.isDirectory()) {
42
+ searchDir(fullPath, queryWords, results, maxResults);
43
+ } else if (entry.name.endsWith('.md')) {
44
+ results.push(...searchInFile(fullPath, queryWords).slice(0, maxResults - results.length));
45
+ }
46
+ }
47
+ }
48
+
49
+ async function execute({ query, scope = 'project', _cwd, _agent }) {
50
+ const queryWords = query.toLowerCase().split(/\s+/).filter(w => w.length > 2);
51
+ if (queryWords.length === 0) return 'Query too short';
52
+
53
+ const results = [];
54
+
55
+ if (scope === 'project') {
56
+ searchDir(paths.getProjectMemoryDir(_cwd), queryWords, results);
57
+ } else {
58
+ searchDir(paths.getAgentMemoryDir(_cwd, _agent.name), queryWords, results);
59
+ }
60
+
61
+ if (results.length === 0) return `No memory matches found for: ${query}`;
62
+ return `Memory search results for "${query}":\n\n${results.join('\n')}`;
63
+ }
64
+
65
+ module.exports = { schema, execute };
@@ -0,0 +1,42 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const paths = require('../utils/paths');
6
+
7
+ const schema = {
8
+ name: 'memory_write',
9
+ description: 'Write to persistent memory. Agent-scoped by default. Use scope "global" to write to project-level memory.',
10
+ input_schema: {
11
+ type: 'object',
12
+ properties: {
13
+ content: { type: 'string', description: 'Content to append to memory' },
14
+ scope: { type: 'string', enum: ['agent', 'global'], description: 'Memory scope: "agent" (default) or "global" (project-level)' },
15
+ },
16
+ required: ['content'],
17
+ },
18
+ timeout: 10,
19
+ };
20
+
21
+ async function execute({ content, scope = 'agent', _cwd, _agent }) {
22
+ let memoryDir, memoryFile;
23
+
24
+ if (scope === 'global') {
25
+ memoryDir = paths.getProjectMemoryDir(_cwd);
26
+ memoryFile = path.join(memoryDir, 'MEMORY.md');
27
+ } else {
28
+ memoryDir = paths.getAgentMemoryDir(_cwd, _agent.name);
29
+ memoryFile = path.join(memoryDir, 'MEMORY.md');
30
+ }
31
+
32
+ fs.mkdirSync(memoryDir, { recursive: true });
33
+
34
+ const timestamp = new Date().toISOString().slice(0, 10);
35
+ const entry = `\n\n<!-- ${timestamp} -->\n${content.trim()}`;
36
+
37
+ fs.appendFileSync(memoryFile, entry, 'utf8');
38
+
39
+ return `Memory written to ${scope} scope (${memoryFile})`;
40
+ }
41
+
42
+ module.exports = { schema, execute };
@@ -0,0 +1,48 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const schema = {
7
+ name: 'read_file',
8
+ description: 'Read the contents of a file. Supports offset and limit for large files.',
9
+ input_schema: {
10
+ type: 'object',
11
+ properties: {
12
+ file: { type: 'string', description: 'Path to the file (relative to cwd or absolute)' },
13
+ offset: { type: 'integer', description: 'Line number to start reading from (1-indexed)', minimum: 1 },
14
+ limit: { type: 'integer', description: 'Maximum number of lines to read', minimum: 1 },
15
+ },
16
+ required: ['file'],
17
+ },
18
+ timeout: 10,
19
+ };
20
+
21
+ async function execute({ file, offset, limit, _cwd }) {
22
+ const filePath = path.isAbsolute(file) ? file : path.resolve(_cwd, file);
23
+
24
+ if (!fs.existsSync(filePath)) {
25
+ return `File not found: ${filePath}`;
26
+ }
27
+
28
+ const stat = fs.statSync(filePath);
29
+ if (stat.isDirectory()) return `Path is a directory: ${filePath}`;
30
+
31
+ const content = fs.readFileSync(filePath, 'utf8');
32
+ const lines = content.split('\n');
33
+ const total = lines.length;
34
+
35
+ let start = 0;
36
+ let end = lines.length;
37
+
38
+ if (offset !== undefined) start = Math.max(0, offset - 1);
39
+ if (limit !== undefined) end = Math.min(lines.length, start + limit);
40
+
41
+ const selected = lines.slice(start, end);
42
+ const numbered = selected.map((l, i) => `${String(start + i + 1).padStart(4)} | ${l}`).join('\n');
43
+
44
+ const header = (offset || limit) ? `Lines ${start + 1}-${start + selected.length} of ${total} total:\n` : '';
45
+ return header + numbered;
46
+ }
47
+
48
+ module.exports = { schema, execute };
package/tools/sleep.js ADDED
@@ -0,0 +1,22 @@
1
+ 'use strict';
2
+
3
+ const schema = {
4
+ name: 'sleep',
5
+ description: 'Wait for a specified duration. Counts against maxDurationSeconds but not maxIterations.',
6
+ input_schema: {
7
+ type: 'object',
8
+ properties: {
9
+ seconds: { type: 'number', description: 'Number of seconds to wait (max: 300)', minimum: 0.1, maximum: 300 },
10
+ },
11
+ required: ['seconds'],
12
+ },
13
+ timeout: 305,
14
+ };
15
+
16
+ async function execute({ seconds }) {
17
+ const ms = Math.min(seconds, 300) * 1000;
18
+ await new Promise(r => setTimeout(r, ms));
19
+ return `Slept for ${seconds}s`;
20
+ }
21
+
22
+ module.exports = { schema, execute };