ashlrcode 1.0.0

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 (133) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +295 -0
  3. package/package.json +46 -0
  4. package/src/__tests__/branded-types.test.ts +47 -0
  5. package/src/__tests__/context.test.ts +163 -0
  6. package/src/__tests__/cost-tracker.test.ts +274 -0
  7. package/src/__tests__/cron.test.ts +197 -0
  8. package/src/__tests__/dream.test.ts +204 -0
  9. package/src/__tests__/error-handler.test.ts +192 -0
  10. package/src/__tests__/features.test.ts +69 -0
  11. package/src/__tests__/file-history.test.ts +177 -0
  12. package/src/__tests__/hooks.test.ts +145 -0
  13. package/src/__tests__/keybindings.test.ts +159 -0
  14. package/src/__tests__/model-patches.test.ts +82 -0
  15. package/src/__tests__/permissions-rules.test.ts +121 -0
  16. package/src/__tests__/permissions.test.ts +108 -0
  17. package/src/__tests__/project-config.test.ts +63 -0
  18. package/src/__tests__/retry.test.ts +321 -0
  19. package/src/__tests__/router.test.ts +158 -0
  20. package/src/__tests__/session-compact.test.ts +191 -0
  21. package/src/__tests__/session.test.ts +145 -0
  22. package/src/__tests__/skill-registry.test.ts +130 -0
  23. package/src/__tests__/speculation.test.ts +196 -0
  24. package/src/__tests__/tasks-v2.test.ts +267 -0
  25. package/src/__tests__/telemetry.test.ts +149 -0
  26. package/src/__tests__/tool-executor.test.ts +141 -0
  27. package/src/__tests__/tool-registry.test.ts +166 -0
  28. package/src/__tests__/undercover.test.ts +93 -0
  29. package/src/__tests__/workflow.test.ts +195 -0
  30. package/src/agent/async-context.ts +64 -0
  31. package/src/agent/context.ts +245 -0
  32. package/src/agent/cron.ts +189 -0
  33. package/src/agent/dream.ts +165 -0
  34. package/src/agent/error-handler.ts +108 -0
  35. package/src/agent/ipc.ts +256 -0
  36. package/src/agent/kairos.ts +207 -0
  37. package/src/agent/loop.ts +314 -0
  38. package/src/agent/model-patches.ts +68 -0
  39. package/src/agent/speculation.ts +219 -0
  40. package/src/agent/sub-agent.ts +125 -0
  41. package/src/agent/system-prompt.ts +231 -0
  42. package/src/agent/team.ts +220 -0
  43. package/src/agent/tool-executor.ts +162 -0
  44. package/src/agent/workflow.ts +189 -0
  45. package/src/agent/worktree-manager.ts +86 -0
  46. package/src/autopilot/queue.ts +186 -0
  47. package/src/autopilot/scanner.ts +245 -0
  48. package/src/autopilot/types.ts +58 -0
  49. package/src/bridge/bridge-client.ts +57 -0
  50. package/src/bridge/bridge-server.ts +81 -0
  51. package/src/cli.ts +1120 -0
  52. package/src/config/features.ts +51 -0
  53. package/src/config/git.ts +137 -0
  54. package/src/config/hooks.ts +201 -0
  55. package/src/config/permissions.ts +251 -0
  56. package/src/config/project-config.ts +63 -0
  57. package/src/config/remote-settings.ts +163 -0
  58. package/src/config/settings-sync.ts +170 -0
  59. package/src/config/settings.ts +113 -0
  60. package/src/config/undercover.ts +76 -0
  61. package/src/config/upgrade-notice.ts +65 -0
  62. package/src/mcp/client.ts +197 -0
  63. package/src/mcp/manager.ts +125 -0
  64. package/src/mcp/oauth.ts +252 -0
  65. package/src/mcp/types.ts +61 -0
  66. package/src/persistence/memory.ts +129 -0
  67. package/src/persistence/session.ts +289 -0
  68. package/src/planning/plan-mode.ts +128 -0
  69. package/src/planning/plan-tools.ts +138 -0
  70. package/src/providers/anthropic.ts +177 -0
  71. package/src/providers/cost-tracker.ts +184 -0
  72. package/src/providers/retry.ts +264 -0
  73. package/src/providers/router.ts +159 -0
  74. package/src/providers/types.ts +79 -0
  75. package/src/providers/xai.ts +217 -0
  76. package/src/repl.tsx +1384 -0
  77. package/src/setup.ts +119 -0
  78. package/src/skills/loader.ts +78 -0
  79. package/src/skills/registry.ts +78 -0
  80. package/src/skills/types.ts +11 -0
  81. package/src/state/file-history.ts +264 -0
  82. package/src/telemetry/event-log.ts +116 -0
  83. package/src/tools/agent.ts +133 -0
  84. package/src/tools/ask-user.ts +229 -0
  85. package/src/tools/bash.ts +146 -0
  86. package/src/tools/config.ts +147 -0
  87. package/src/tools/diff.ts +137 -0
  88. package/src/tools/file-edit.ts +123 -0
  89. package/src/tools/file-read.ts +82 -0
  90. package/src/tools/file-write.ts +82 -0
  91. package/src/tools/glob.ts +76 -0
  92. package/src/tools/grep.ts +187 -0
  93. package/src/tools/ls.ts +77 -0
  94. package/src/tools/lsp.ts +375 -0
  95. package/src/tools/mcp-resources.ts +83 -0
  96. package/src/tools/mcp-tool.ts +47 -0
  97. package/src/tools/memory.ts +148 -0
  98. package/src/tools/notebook-edit.ts +133 -0
  99. package/src/tools/peers.ts +113 -0
  100. package/src/tools/powershell.ts +83 -0
  101. package/src/tools/registry.ts +114 -0
  102. package/src/tools/send-message.ts +75 -0
  103. package/src/tools/sleep.ts +50 -0
  104. package/src/tools/snip.ts +143 -0
  105. package/src/tools/tasks.ts +349 -0
  106. package/src/tools/team.ts +309 -0
  107. package/src/tools/todo-write.ts +93 -0
  108. package/src/tools/tool-search.ts +83 -0
  109. package/src/tools/types.ts +52 -0
  110. package/src/tools/web-browser.ts +263 -0
  111. package/src/tools/web-fetch.ts +118 -0
  112. package/src/tools/web-search.ts +107 -0
  113. package/src/tools/workflow.ts +188 -0
  114. package/src/tools/worktree.ts +143 -0
  115. package/src/types/branded.ts +22 -0
  116. package/src/ui/App.tsx +184 -0
  117. package/src/ui/BuddyPanel.tsx +52 -0
  118. package/src/ui/PermissionPrompt.tsx +29 -0
  119. package/src/ui/banner.ts +217 -0
  120. package/src/ui/buddy-ai.ts +108 -0
  121. package/src/ui/buddy.ts +466 -0
  122. package/src/ui/context-bar.ts +60 -0
  123. package/src/ui/effort.ts +65 -0
  124. package/src/ui/keybindings.ts +143 -0
  125. package/src/ui/markdown.ts +271 -0
  126. package/src/ui/message-renderer.ts +73 -0
  127. package/src/ui/mode.ts +80 -0
  128. package/src/ui/notifications.ts +57 -0
  129. package/src/ui/speech-bubble.ts +95 -0
  130. package/src/ui/spinner.ts +116 -0
  131. package/src/ui/theme.ts +98 -0
  132. package/src/version.ts +5 -0
  133. package/src/voice/voice-mode.ts +169 -0
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Memory management tools — save, list, and delete project memories.
3
+ *
4
+ * Memories persist across sessions in ~/.ashlrcode/memory/<project-hash>/
5
+ */
6
+
7
+ import type { Tool, ToolContext } from "./types.ts";
8
+ import {
9
+ loadMemories,
10
+ saveMemory,
11
+ deleteMemory,
12
+ type MemoryEntry,
13
+ } from "../persistence/memory.ts";
14
+
15
+ export const memorySaveTool: Tool = {
16
+ name: "MemorySave",
17
+
18
+ prompt() {
19
+ return `Save a memory for this project. Memories persist across sessions and are loaded into context automatically.
20
+
21
+ Memory types:
22
+ - user: Information about the user (role, preferences, knowledge)
23
+ - feedback: Guidance on how to approach work (corrections, confirmations)
24
+ - project: Ongoing work context (goals, status, decisions)
25
+ - reference: Pointers to external resources (URLs, tools, docs)
26
+
27
+ Use when the user asks you to "remember" something, or when you learn important context that should persist.`;
28
+ },
29
+
30
+ inputSchema() {
31
+ return {
32
+ type: "object",
33
+ properties: {
34
+ name: {
35
+ type: "string",
36
+ description: "Short name for the memory",
37
+ },
38
+ description: {
39
+ type: "string",
40
+ description: "One-line description (used to decide relevance in future)",
41
+ },
42
+ type: {
43
+ type: "string",
44
+ enum: ["user", "feedback", "project", "reference"],
45
+ description: "Memory type",
46
+ },
47
+ content: {
48
+ type: "string",
49
+ description: "The memory content (markdown)",
50
+ },
51
+ },
52
+ required: ["name", "type", "content"],
53
+ };
54
+ },
55
+
56
+ isReadOnly() { return false; },
57
+ isDestructive() { return false; },
58
+ isConcurrencySafe() { return false; },
59
+
60
+ validateInput(input) {
61
+ if (!input.name) return "name is required";
62
+ if (!input.type) return "type is required";
63
+ if (!input.content) return "content is required";
64
+ return null;
65
+ },
66
+
67
+ async call(input, context) {
68
+ const filePath = await saveMemory(context.cwd, {
69
+ name: input.name as string,
70
+ description: (input.description as string) ?? "",
71
+ type: input.type as MemoryEntry["type"],
72
+ content: input.content as string,
73
+ });
74
+ return `Memory saved: ${input.name} → ${filePath}`;
75
+ },
76
+ };
77
+
78
+ export const memoryListTool: Tool = {
79
+ name: "MemoryList",
80
+
81
+ prompt() {
82
+ return "List all memories saved for this project. Shows name, type, and description.";
83
+ },
84
+
85
+ inputSchema() {
86
+ return {
87
+ type: "object",
88
+ properties: {},
89
+ required: [],
90
+ };
91
+ },
92
+
93
+ isReadOnly() { return true; },
94
+ isDestructive() { return false; },
95
+ isConcurrencySafe() { return true; },
96
+
97
+ validateInput() { return null; },
98
+
99
+ async call(_input, context) {
100
+ const memories = await loadMemories(context.cwd);
101
+ if (memories.length === 0) {
102
+ return "No memories saved for this project.";
103
+ }
104
+
105
+ const lines = memories.map((m) =>
106
+ `- **${m.name}** (${m.type}): ${m.description || m.content.slice(0, 80)}`
107
+ );
108
+ return `${memories.length} memories:\n${lines.join("\n")}`;
109
+ },
110
+ };
111
+
112
+ export const memoryDeleteTool: Tool = {
113
+ name: "MemoryDelete",
114
+
115
+ prompt() {
116
+ return "Delete a memory by name.";
117
+ },
118
+
119
+ inputSchema() {
120
+ return {
121
+ type: "object",
122
+ properties: {
123
+ name: {
124
+ type: "string",
125
+ description: "Name of the memory to delete",
126
+ },
127
+ },
128
+ required: ["name"],
129
+ };
130
+ },
131
+
132
+ isReadOnly() { return false; },
133
+ isDestructive() { return true; },
134
+ isConcurrencySafe() { return false; },
135
+
136
+ validateInput(input) {
137
+ if (!input.name) return "name is required";
138
+ return null;
139
+ },
140
+
141
+ async call(input, context) {
142
+ const deleted = await deleteMemory(context.cwd, input.name as string);
143
+ if (deleted) {
144
+ return `Memory deleted: ${input.name}`;
145
+ }
146
+ return `Memory not found: ${input.name}`;
147
+ },
148
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * NotebookEdit tool — edit Jupyter notebook cells.
3
+ */
4
+
5
+ import { readFile, writeFile } from "fs/promises";
6
+ import { existsSync } from "fs";
7
+ import { resolve } from "path";
8
+ import type { Tool, ToolContext } from "./types.ts";
9
+ import { fileHistory } from "../state/file-history.ts";
10
+
11
+ interface NotebookCell {
12
+ cell_type: "code" | "markdown" | "raw";
13
+ source: string[];
14
+ metadata?: Record<string, unknown>;
15
+ outputs?: unknown[];
16
+ execution_count?: number | null;
17
+ }
18
+
19
+ interface Notebook {
20
+ cells: NotebookCell[];
21
+ metadata: Record<string, unknown>;
22
+ nbformat: number;
23
+ nbformat_minor: number;
24
+ }
25
+
26
+ export const notebookEditTool: Tool = {
27
+ name: "NotebookEdit",
28
+
29
+ prompt() {
30
+ return `Edit a Jupyter notebook (.ipynb) cell. Operations:
31
+ - replace: Replace cell content at a given index
32
+ - insert: Insert a new cell at a given index
33
+ - delete: Delete a cell at a given index`;
34
+ },
35
+
36
+ inputSchema() {
37
+ return {
38
+ type: "object",
39
+ properties: {
40
+ file_path: {
41
+ type: "string",
42
+ description: "Path to the .ipynb file",
43
+ },
44
+ operation: {
45
+ type: "string",
46
+ enum: ["replace", "insert", "delete"],
47
+ description: "Operation to perform",
48
+ },
49
+ cell_index: {
50
+ type: "number",
51
+ description: "Cell index (0-based)",
52
+ },
53
+ cell_type: {
54
+ type: "string",
55
+ enum: ["code", "markdown"],
56
+ description: "Cell type (for insert/replace)",
57
+ },
58
+ content: {
59
+ type: "string",
60
+ description: "New cell content (for insert/replace)",
61
+ },
62
+ },
63
+ required: ["file_path", "operation", "cell_index"],
64
+ };
65
+ },
66
+
67
+ isReadOnly() { return false; },
68
+ isDestructive() { return false; },
69
+ isConcurrencySafe() { return false; },
70
+
71
+ validateInput(input) {
72
+ if (!input.file_path) return "file_path is required";
73
+ if (!input.operation) return "operation is required";
74
+ if (input.cell_index === undefined) return "cell_index is required";
75
+ const op = input.operation as string;
76
+ if (op !== "delete" && !input.content) return "content is required for insert/replace";
77
+ return null;
78
+ },
79
+
80
+ async call(input, context) {
81
+ const filePath = resolve(context.cwd, input.file_path as string);
82
+ if (!existsSync(filePath)) return `File not found: ${filePath}`;
83
+
84
+ await fileHistory.snapshot(filePath);
85
+
86
+ const raw = await readFile(filePath, "utf-8");
87
+ const notebook = JSON.parse(raw) as Notebook;
88
+ const idx = input.cell_index as number;
89
+ const op = input.operation as string;
90
+
91
+ if (idx < 0 || (op !== "insert" && idx >= notebook.cells.length)) {
92
+ return `Cell index ${idx} out of range (${notebook.cells.length} cells)`;
93
+ }
94
+
95
+ switch (op) {
96
+ case "replace": {
97
+ const cellType = (input.cell_type as string) ?? notebook.cells[idx]!.cell_type;
98
+ const content = input.content as string;
99
+ notebook.cells[idx] = {
100
+ cell_type: cellType as NotebookCell["cell_type"],
101
+ source: content.split("\n").map((line, i, arr) =>
102
+ i < arr.length - 1 ? line + "\n" : line
103
+ ),
104
+ metadata: {},
105
+ ...(cellType === "code" ? { outputs: [], execution_count: null } : {}),
106
+ };
107
+ break;
108
+ }
109
+
110
+ case "insert": {
111
+ const cellType = (input.cell_type as string) ?? "code";
112
+ const content = input.content as string;
113
+ const newCell: NotebookCell = {
114
+ cell_type: cellType as NotebookCell["cell_type"],
115
+ source: content.split("\n").map((line, i, arr) =>
116
+ i < arr.length - 1 ? line + "\n" : line
117
+ ),
118
+ metadata: {},
119
+ ...(cellType === "code" ? { outputs: [], execution_count: null } : {}),
120
+ };
121
+ notebook.cells.splice(idx, 0, newCell);
122
+ break;
123
+ }
124
+
125
+ case "delete":
126
+ notebook.cells.splice(idx, 1);
127
+ break;
128
+ }
129
+
130
+ await writeFile(filePath, JSON.stringify(notebook, null, 1) + "\n", "utf-8");
131
+ return `${op} cell ${idx} in ${filePath} (${notebook.cells.length} cells total)`;
132
+ },
133
+ };
@@ -0,0 +1,113 @@
1
+ /**
2
+ * ListPeers tool — discover and communicate with other running AshlrCode instances.
3
+ *
4
+ * Actions:
5
+ * list — show all active peers (pid, cwd, session)
6
+ * send — send a message to a peer by ID
7
+ * inbox — read received messages
8
+ */
9
+
10
+ import type { Tool } from "./types.ts";
11
+ import { listPeers, sendToPeer, readInbox, getPeerId } from "../agent/ipc.ts";
12
+
13
+ export const listPeersTool: Tool = {
14
+ name: "ListPeers",
15
+
16
+ prompt() {
17
+ return (
18
+ "List other running AshlrCode instances and communicate with them via IPC. " +
19
+ "Use action 'list' to discover peers, 'send' to message a peer, 'inbox' to read received messages."
20
+ );
21
+ },
22
+
23
+ inputSchema() {
24
+ return {
25
+ type: "object",
26
+ properties: {
27
+ action: {
28
+ type: "string",
29
+ enum: ["list", "send", "inbox"],
30
+ description: "Action to perform: list peers, send a message, or check inbox",
31
+ },
32
+ peerId: {
33
+ type: "string",
34
+ description: "Target peer ID (required for 'send')",
35
+ },
36
+ message: {
37
+ type: "string",
38
+ description: "Message content (required for 'send')",
39
+ },
40
+ },
41
+ required: ["action"],
42
+ };
43
+ },
44
+
45
+ isReadOnly() {
46
+ return true;
47
+ },
48
+
49
+ isDestructive() {
50
+ return false;
51
+ },
52
+
53
+ isConcurrencySafe() {
54
+ return true;
55
+ },
56
+
57
+ validateInput(input) {
58
+ const action = input.action as string | undefined;
59
+ if (!action || !["list", "send", "inbox"].includes(action)) {
60
+ return "action must be one of: list, send, inbox";
61
+ }
62
+ if (action === "send") {
63
+ if (!input.peerId || typeof input.peerId !== "string") {
64
+ return "peerId is required for send action";
65
+ }
66
+ if (!input.message || typeof input.message !== "string") {
67
+ return "message is required for send action";
68
+ }
69
+ }
70
+ return null;
71
+ },
72
+
73
+ async call(input) {
74
+ const action = input.action as string;
75
+
76
+ if (action === "list") {
77
+ const peers = await listPeers();
78
+ const myId = getPeerId();
79
+ if (peers.length === 0) {
80
+ return "No AshlrCode instances running (IPC server may not be started).";
81
+ }
82
+ const lines = peers.map((p) => {
83
+ const self = p.id === myId ? " (self)" : "";
84
+ return ` ${p.id}${self} pid=${p.pid} cwd=${p.cwd} session=${p.sessionId} started=${p.startedAt}`;
85
+ });
86
+ return `Active peers (${peers.length}):\n${lines.join("\n")}`;
87
+ }
88
+
89
+ if (action === "inbox") {
90
+ const msgs = readInbox();
91
+ if (msgs.length === 0) {
92
+ return "Inbox is empty — no messages received.";
93
+ }
94
+ const lines = msgs.map(
95
+ (m) => ` [${m.timestamp}] from=${m.from} type=${m.type}: ${m.payload}`,
96
+ );
97
+ return `${msgs.length} message(s):\n${lines.join("\n")}`;
98
+ }
99
+
100
+ if (action === "send") {
101
+ const ok = await sendToPeer(
102
+ input.peerId as string,
103
+ "message",
104
+ input.message as string,
105
+ );
106
+ return ok
107
+ ? "Message sent successfully."
108
+ : "Failed to send — peer not found or unreachable.";
109
+ }
110
+
111
+ return "Unknown action.";
112
+ },
113
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * PowerShellTool — execute PowerShell commands on Windows.
3
+ */
4
+
5
+ import chalk from "chalk";
6
+ import type { Tool, ToolContext } from "./types.ts";
7
+
8
+ const DEFAULT_TIMEOUT = 120_000;
9
+
10
+ export const powershellTool: Tool = {
11
+ name: "PowerShell",
12
+
13
+ prompt() {
14
+ return "Execute a PowerShell command on Windows. Use for system commands, file operations, and Windows-specific tasks.";
15
+ },
16
+
17
+ inputSchema() {
18
+ return {
19
+ type: "object",
20
+ properties: {
21
+ command: { type: "string", description: "The PowerShell command to execute" },
22
+ timeout: { type: "number", description: "Timeout in milliseconds (default: 120000)" },
23
+ },
24
+ required: ["command"],
25
+ };
26
+ },
27
+
28
+ isReadOnly() { return false; },
29
+ isDestructive() { return true; },
30
+ isConcurrencySafe() { return false; },
31
+
32
+ validateInput(input) {
33
+ if (!input.command || typeof input.command !== "string") return "command is required";
34
+ return null;
35
+ },
36
+
37
+ async call(input, context) {
38
+ const command = input.command as string;
39
+ const timeout = (input.timeout as number) ?? DEFAULT_TIMEOUT;
40
+
41
+ const shell = process.platform === "win32" ? "powershell.exe" : "pwsh";
42
+ const proc = Bun.spawn([shell, "-NoProfile", "-NonInteractive", "-Command", command], {
43
+ cwd: context.cwd,
44
+ stdout: "pipe",
45
+ stderr: "pipe",
46
+ env: { ...process.env },
47
+ });
48
+
49
+ const timeoutId = setTimeout(() => proc.kill(), timeout);
50
+ const stderrPromise = new Response(proc.stderr).text();
51
+ const reader = proc.stdout.getReader();
52
+ const decoder = new TextDecoder();
53
+
54
+ try {
55
+ let stdout = "";
56
+ while (true) {
57
+ const { done, value } = await reader.read();
58
+ if (done) break;
59
+ stdout += decoder.decode(value, { stream: true });
60
+ }
61
+
62
+ const stderr = await stderrPromise;
63
+ const exitCode = await proc.exited;
64
+ clearTimeout(timeoutId);
65
+
66
+ let result = "";
67
+ if (stdout) result += stdout;
68
+ if (stderr) result += (result ? "\n" : "") + stderr;
69
+ if (exitCode !== 0) result += `\nExit code: ${exitCode}`;
70
+
71
+ if (result.length > 50_000) {
72
+ result = result.slice(0, 20_000) + `\n\n[... truncated ${result.length - 40_000} chars ...]\n\n` + result.slice(-20_000);
73
+ }
74
+
75
+ return result || "(no output)";
76
+ } catch {
77
+ clearTimeout(timeoutId);
78
+ try { reader.releaseLock(); } catch {}
79
+ try { await stderrPromise; } catch {}
80
+ return "Command timed out";
81
+ }
82
+ },
83
+ };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Tool registry — manages available tools and dispatches calls.
3
+ * Integrates with hook system for pre/post tool execution.
4
+ */
5
+
6
+ import type { Tool, ToolContext } from "./types.ts";
7
+ import type { ToolDefinition } from "../providers/types.ts";
8
+ import { toolToDefinition } from "./types.ts";
9
+ import { runPreToolHooks, runPostToolHooks, type HooksConfig } from "../config/hooks.ts";
10
+ import { checkRules } from "../config/permissions.ts";
11
+
12
+ function formatInputPreview(toolName: string, input: Record<string, unknown>): string {
13
+ switch (toolName) {
14
+ case "Bash":
15
+ return `Run: ${input.command}`;
16
+ case "Write":
17
+ return `Write to: ${input.file_path}`;
18
+ case "Edit":
19
+ return `Edit: ${input.file_path}`;
20
+ default:
21
+ return JSON.stringify(input).slice(0, 100);
22
+ }
23
+ }
24
+
25
+ export class ToolRegistry {
26
+ private tools = new Map<string, Tool>();
27
+ private hooks: HooksConfig = {};
28
+
29
+ register(tool: Tool): void {
30
+ this.tools.set(tool.name, tool);
31
+ }
32
+
33
+ setHooks(hooks: HooksConfig): void {
34
+ this.hooks = hooks;
35
+ }
36
+
37
+ get(name: string): Tool | undefined {
38
+ return this.tools.get(name);
39
+ }
40
+
41
+ getAll(): Tool[] {
42
+ return Array.from(this.tools.values());
43
+ }
44
+
45
+ getDefinitions(): ToolDefinition[] {
46
+ return this.getAll().map(toolToDefinition);
47
+ }
48
+
49
+ /** Get only read-only tools (for plan mode) */
50
+ getReadOnlyDefinitions(): ToolDefinition[] {
51
+ return this.getAll()
52
+ .filter((t) => t.isReadOnly())
53
+ .map(toolToDefinition);
54
+ }
55
+
56
+ async execute(
57
+ toolName: string,
58
+ input: Record<string, unknown>,
59
+ context: ToolContext
60
+ ): Promise<{ result: string; isError: boolean }> {
61
+ const tool = this.tools.get(toolName);
62
+ if (!tool) {
63
+ return { result: `Unknown tool: ${toolName}`, isError: true };
64
+ }
65
+
66
+ // Validate input
67
+ const validationError = tool.validateInput(input);
68
+ if (validationError) {
69
+ return { result: `Validation error: ${validationError}`, isError: true };
70
+ }
71
+
72
+ // Check permissions for non-read-only tools (before hooks, so hooks
73
+ // don't execute shell commands for tools the user would deny)
74
+ if (!tool.isReadOnly()) {
75
+ const ruleResult = checkRules(toolName, input);
76
+ if (ruleResult === "deny") {
77
+ return { result: "Denied by permission rule", isError: true };
78
+ }
79
+ if (ruleResult !== "allow") {
80
+ const inputPreview = formatInputPreview(toolName, input);
81
+ const allowed = await context.requestPermission(toolName, inputPreview);
82
+ if (!allowed) {
83
+ return { result: "Permission denied by user", isError: true };
84
+ }
85
+ }
86
+ }
87
+
88
+ // Tool-specific permission check
89
+ if (tool.checkPermissions) {
90
+ const permError = tool.checkPermissions(input, context);
91
+ if (permError) {
92
+ return { result: `Permission denied: ${permError}`, isError: true };
93
+ }
94
+ }
95
+
96
+ // Run pre-tool hooks (after permission check)
97
+ const hookResult = await runPreToolHooks(this.hooks, toolName, input);
98
+ if (hookResult.action === "deny") {
99
+ return { result: hookResult.message ?? "Denied by hook", isError: true };
100
+ }
101
+
102
+ try {
103
+ const result = await tool.call(input, context);
104
+
105
+ // Run post-tool hooks (fire and forget)
106
+ runPostToolHooks(this.hooks, toolName, input, result).catch(() => {});
107
+
108
+ return { result, isError: false };
109
+ } catch (err) {
110
+ const message = err instanceof Error ? err.message : String(err);
111
+ return { result: `Error: ${message}`, isError: true };
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * SendMessage tool — send messages between agents.
3
+ *
4
+ * Enables agent-to-agent communication for team coordination.
5
+ */
6
+
7
+ import type { Tool, ToolContext } from "./types.ts";
8
+
9
+ // Simple in-memory message inbox
10
+ interface Message {
11
+ from: string;
12
+ to: string;
13
+ content: string;
14
+ timestamp: string;
15
+ }
16
+
17
+ const inbox: Message[] = [];
18
+
19
+ export function getMessages(agentName: string): Message[] {
20
+ return inbox.filter((m) => m.to === agentName);
21
+ }
22
+
23
+ export function clearMessages(agentName: string): void {
24
+ const toRemove = inbox.filter((m) => m.to === agentName);
25
+ for (const msg of toRemove) {
26
+ const idx = inbox.indexOf(msg);
27
+ if (idx >= 0) inbox.splice(idx, 1);
28
+ }
29
+ }
30
+
31
+ export const sendMessageTool: Tool = {
32
+ name: "SendMessage",
33
+
34
+ prompt() {
35
+ return "Send a message to another agent. Used for agent-to-agent communication when coordinating work across sub-agents.";
36
+ },
37
+
38
+ inputSchema() {
39
+ return {
40
+ type: "object",
41
+ properties: {
42
+ to: {
43
+ type: "string",
44
+ description: "Name or ID of the recipient agent",
45
+ },
46
+ content: {
47
+ type: "string",
48
+ description: "Message content",
49
+ },
50
+ },
51
+ required: ["to", "content"],
52
+ };
53
+ },
54
+
55
+ isReadOnly() { return true; },
56
+ isDestructive() { return false; },
57
+ isConcurrencySafe() { return true; },
58
+
59
+ validateInput(input) {
60
+ if (!input.to) return "to is required";
61
+ if (!input.content) return "content is required";
62
+ return null;
63
+ },
64
+
65
+ async call(input, _context) {
66
+ const msg: Message = {
67
+ from: "main",
68
+ to: input.to as string,
69
+ content: input.content as string,
70
+ timestamp: new Date().toISOString(),
71
+ };
72
+ inbox.push(msg);
73
+ return `Message sent to ${msg.to}`;
74
+ },
75
+ };