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,77 @@
1
+ /**
2
+ * LS tool — directory listing without Bash.
3
+ */
4
+
5
+ import { readdir, stat } from "fs/promises";
6
+ import { resolve, join } from "path";
7
+ import type { Tool, ToolContext } from "./types.ts";
8
+
9
+ export const lsTool: Tool = {
10
+ name: "LS",
11
+
12
+ prompt() {
13
+ return "List files and directories in a given path. Returns names with type indicators (/ for directories). Lighter than Bash ls.";
14
+ },
15
+
16
+ inputSchema() {
17
+ return {
18
+ type: "object",
19
+ properties: {
20
+ path: {
21
+ type: "string",
22
+ description: "Directory path to list (defaults to cwd)",
23
+ },
24
+ },
25
+ required: [],
26
+ };
27
+ },
28
+
29
+ isReadOnly() {
30
+ return true;
31
+ },
32
+ isDestructive() {
33
+ return false;
34
+ },
35
+ isConcurrencySafe() {
36
+ return true;
37
+ },
38
+
39
+ validateInput() {
40
+ return null;
41
+ },
42
+
43
+ async call(input, context) {
44
+ const dirPath = resolve(context.cwd, (input.path as string) ?? ".");
45
+
46
+ try {
47
+ const entries = await readdir(dirPath);
48
+ const details: string[] = [];
49
+
50
+ for (const entry of entries) {
51
+ if (entry.startsWith(".")) continue; // Skip hidden files
52
+ try {
53
+ const s = await stat(join(dirPath, entry));
54
+ const indicator = s.isDirectory() ? "/" : "";
55
+ const size = s.isDirectory() ? "" : ` (${formatSize(s.size)})`;
56
+ details.push(`${entry}${indicator}${size}`);
57
+ } catch {
58
+ details.push(entry);
59
+ }
60
+ }
61
+
62
+ if (details.length === 0) {
63
+ return `Empty directory: ${dirPath}`;
64
+ }
65
+
66
+ return details.join("\n");
67
+ } catch (err) {
68
+ return `Error listing ${dirPath}: ${err instanceof Error ? err.message : String(err)}`;
69
+ }
70
+ },
71
+ };
72
+
73
+ function formatSize(bytes: number): string {
74
+ if (bytes < 1024) return `${bytes}B`;
75
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
76
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
77
+ }
@@ -0,0 +1,375 @@
1
+ /**
2
+ * LSP Tool — Language Server Protocol integration.
3
+ *
4
+ * Provides go-to-definition, find-references, and hover info by
5
+ * communicating with language servers (TypeScript, Python, etc.)
6
+ */
7
+
8
+ import { resolve } from "path";
9
+ import { readFile } from "fs/promises";
10
+ import type { Tool, ToolContext } from "./types.ts";
11
+
12
+ // LSP server configs per language
13
+ interface LSPServerConfig {
14
+ command: string[];
15
+ initOptions?: Record<string, unknown>;
16
+ }
17
+
18
+ const SERVER_CONFIGS: Record<string, LSPServerConfig> = {
19
+ typescript: { command: ["npx", "typescript-language-server", "--stdio"] },
20
+ javascript: { command: ["npx", "typescript-language-server", "--stdio"] },
21
+ python: { command: ["pylsp"] },
22
+ rust: { command: ["rust-analyzer"] },
23
+ go: { command: ["gopls", "serve"] },
24
+ };
25
+
26
+ // Detect language from file extension
27
+ function detectLanguage(filePath: string): string | null {
28
+ const ext = filePath.split(".").pop()?.toLowerCase();
29
+ const langMap: Record<string, string> = {
30
+ ts: "typescript",
31
+ tsx: "typescript",
32
+ js: "javascript",
33
+ jsx: "javascript",
34
+ py: "python",
35
+ rs: "rust",
36
+ go: "go",
37
+ };
38
+ return langMap[ext ?? ""] ?? null;
39
+ }
40
+
41
+ /**
42
+ * Simple LSP client — sends requests via JSON-RPC over stdio.
43
+ * This is a lightweight implementation for basic operations.
44
+ */
45
+ class SimpleLSPClient {
46
+ // Use `any` for the subprocess — Bun's Subprocess type has complex
47
+ // conditional generics that don't resolve cleanly for stdin/stdout.
48
+ private proc: any = null;
49
+ private requestId = 0;
50
+ private pendingRequests = new Map<
51
+ number,
52
+ { resolve: (r: unknown) => void; reject: (e: Error) => void }
53
+ >();
54
+ private buffer = "";
55
+
56
+ async start(config: LSPServerConfig, cwd: string): Promise<void> {
57
+ this.proc = Bun.spawn(config.command, {
58
+ cwd,
59
+ stdin: "pipe",
60
+ stdout: "pipe",
61
+ stderr: "pipe",
62
+ });
63
+
64
+ // Read responses in background
65
+ this.readLoop();
66
+
67
+ // Initialize the language server
68
+ await this.request("initialize", {
69
+ processId: process.pid,
70
+ rootUri: `file://${cwd}`,
71
+ capabilities: {},
72
+ ...config.initOptions,
73
+ });
74
+
75
+ this.notify("initialized", {});
76
+ }
77
+
78
+ private async readLoop(): Promise<void> {
79
+ if (!this.proc) return;
80
+ const reader = this.proc.stdout.getReader();
81
+
82
+ try {
83
+ while (true) {
84
+ const { done, value } = await reader.read();
85
+ if (done) break;
86
+ this.buffer += Buffer.from(value).toString("utf-8");
87
+ this.processBuffer();
88
+ }
89
+ } catch {
90
+ // Stream closed — expected on shutdown
91
+ }
92
+ }
93
+
94
+ private processBuffer(): void {
95
+ while (true) {
96
+ const headerEnd = this.buffer.indexOf("\r\n\r\n");
97
+ if (headerEnd === -1) break;
98
+
99
+ const header = this.buffer.slice(0, headerEnd);
100
+ const match = header.match(/Content-Length:\s*(\d+)/i);
101
+ if (!match) {
102
+ this.buffer = this.buffer.slice(headerEnd + 4);
103
+ continue;
104
+ }
105
+
106
+ const contentLength = parseInt(match[1]!, 10);
107
+ const bodyStart = headerEnd + 4;
108
+
109
+ if (this.buffer.length < bodyStart + contentLength) break;
110
+
111
+ const body = this.buffer.slice(bodyStart, bodyStart + contentLength);
112
+ this.buffer = this.buffer.slice(bodyStart + contentLength);
113
+
114
+ try {
115
+ const msg = JSON.parse(body) as {
116
+ id?: number;
117
+ error?: { message: string };
118
+ result?: unknown;
119
+ };
120
+ if (msg.id !== undefined && this.pendingRequests.has(msg.id)) {
121
+ const pending = this.pendingRequests.get(msg.id)!;
122
+ this.pendingRequests.delete(msg.id);
123
+ if (msg.error) pending.reject(new Error(msg.error.message));
124
+ else pending.resolve(msg.result);
125
+ }
126
+ } catch {
127
+ // Malformed JSON — skip
128
+ }
129
+ }
130
+ }
131
+
132
+ async request(method: string, params: unknown): Promise<unknown> {
133
+ const id = ++this.requestId;
134
+ return new Promise((resolve, reject) => {
135
+ this.pendingRequests.set(id, { resolve, reject });
136
+ this.send({ jsonrpc: "2.0", id, method, params });
137
+
138
+ // Timeout after 10s
139
+ setTimeout(() => {
140
+ if (this.pendingRequests.has(id)) {
141
+ this.pendingRequests.delete(id);
142
+ reject(new Error(`LSP request timed out: ${method}`));
143
+ }
144
+ }, 10_000);
145
+ });
146
+ }
147
+
148
+ notify(method: string, params: unknown): void {
149
+ this.send({ jsonrpc: "2.0", method, params });
150
+ }
151
+
152
+ private send(msg: Record<string, unknown>): void {
153
+ if (!this.proc) return;
154
+ const json = JSON.stringify(msg);
155
+ const header = `Content-Length: ${Buffer.byteLength(json, "utf-8")}\r\n\r\n`;
156
+ const buf = Buffer.concat([Buffer.from(header), Buffer.from(json, "utf-8")]);
157
+ this.proc.stdin.write(buf);
158
+ }
159
+
160
+ async stop(): Promise<void> {
161
+ if (!this.proc) return;
162
+ // Reject all pending requests before shutting down
163
+ for (const [id, pending] of this.pendingRequests) {
164
+ pending.reject(new Error("LSP client stopped"));
165
+ }
166
+ this.pendingRequests.clear();
167
+ try {
168
+ await this.request("shutdown", null);
169
+ this.notify("exit", null);
170
+ this.proc.stdin.end();
171
+ } catch {
172
+ // Best-effort shutdown
173
+ }
174
+ try { this.proc.kill(); } catch {}
175
+ this.proc = null;
176
+ }
177
+ }
178
+
179
+ // Cache active LSP clients by language:cwd
180
+ const clients = new Map<string, SimpleLSPClient>();
181
+ const inFlight = new Map<string, Promise<SimpleLSPClient>>();
182
+
183
+ async function getClient(
184
+ language: string,
185
+ cwd: string,
186
+ ): Promise<SimpleLSPClient> {
187
+ const key = `${language}:${cwd}`;
188
+ if (clients.has(key)) return clients.get(key)!;
189
+ if (inFlight.has(key)) return inFlight.get(key)!;
190
+
191
+ const config = SERVER_CONFIGS[language];
192
+ if (!config) throw new Error(`No LSP server configured for ${language}`);
193
+
194
+ const p = (async () => {
195
+ const client = new SimpleLSPClient();
196
+ await client.start(config, cwd);
197
+ clients.set(key, client);
198
+ inFlight.delete(key);
199
+ return client;
200
+ })();
201
+ inFlight.set(key, p);
202
+ return p;
203
+ }
204
+
205
+ // LSP location result shapes
206
+ interface LSPLocation {
207
+ uri?: string;
208
+ targetUri?: string;
209
+ range?: { start?: { line?: number } };
210
+ targetRange?: { start?: { line?: number } };
211
+ }
212
+
213
+ interface LSPHoverResult {
214
+ contents?:
215
+ | string
216
+ | { value?: string }
217
+ | Array<string | { value?: string }>;
218
+ }
219
+
220
+ function formatLocations(result: unknown): string {
221
+ if (!result) return "No results found";
222
+ const locations = (
223
+ Array.isArray(result) ? result : [result]
224
+ ) as LSPLocation[];
225
+ return locations
226
+ .map((loc) => {
227
+ const path =
228
+ (loc.uri ?? loc.targetUri)?.replace("file://", "") ?? "unknown";
229
+ const range = loc.range ?? loc.targetRange;
230
+ const line = (range?.start?.line ?? 0) + 1;
231
+ return `${path}:${line}`;
232
+ })
233
+ .join("\n");
234
+ }
235
+
236
+ function formatHover(result: unknown): string {
237
+ const hover = result as LSPHoverResult | null;
238
+ if (!hover?.contents) return "No hover info available";
239
+ const contents = hover.contents;
240
+ if (typeof contents === "string") return contents;
241
+ if ("value" in contents && contents.value) return contents.value;
242
+ if (Array.isArray(contents))
243
+ return contents
244
+ .map((c) => (typeof c === "string" ? c : c.value ?? ""))
245
+ .join("\n");
246
+ return JSON.stringify(contents);
247
+ }
248
+
249
+ export const lspTool: Tool = {
250
+ name: "LSP",
251
+
252
+ prompt() {
253
+ return `Language Server Protocol integration. Use for:
254
+ - go-to-definition: Find where a symbol is defined
255
+ - find-references: Find all usages of a symbol
256
+ - hover: Get type info and documentation for a symbol
257
+
258
+ Supported languages: TypeScript, JavaScript, Python, Rust, Go.
259
+ Requires the language server to be installed (e.g., typescript-language-server for TS).`;
260
+ },
261
+
262
+ inputSchema() {
263
+ return {
264
+ type: "object",
265
+ properties: {
266
+ action: {
267
+ type: "string",
268
+ enum: ["definition", "references", "hover"],
269
+ description: "LSP operation to perform",
270
+ },
271
+ file: {
272
+ type: "string",
273
+ description: "File path containing the symbol",
274
+ },
275
+ line: {
276
+ type: "number",
277
+ description: "Line number (1-indexed)",
278
+ },
279
+ column: {
280
+ type: "number",
281
+ description: "Column number (1-indexed)",
282
+ },
283
+ },
284
+ required: ["action", "file", "line", "column"],
285
+ };
286
+ },
287
+
288
+ isReadOnly() {
289
+ return true;
290
+ },
291
+ isDestructive() {
292
+ return false;
293
+ },
294
+ isConcurrencySafe() {
295
+ return true;
296
+ },
297
+
298
+ validateInput(input) {
299
+ if (!input.action) return "action is required";
300
+ if (!["definition", "references", "hover"].includes(input.action as string))
301
+ return "action must be one of: definition, references, hover";
302
+ if (!input.file) return "file is required";
303
+ if (!input.line || !input.column) return "line and column are required";
304
+ return null;
305
+ },
306
+
307
+ async call(input, context) {
308
+ const file = input.file as string;
309
+ const line = (input.line as number) - 1; // LSP uses 0-indexed positions
310
+ const column = (input.column as number) - 1;
311
+ const action = input.action as string;
312
+
313
+ const fullPath = resolve(context.cwd, file);
314
+ const language = detectLanguage(fullPath);
315
+ if (!language)
316
+ return `Unsupported file type: ${file}. Supported: .ts, .tsx, .js, .jsx, .py, .rs, .go`;
317
+
318
+ try {
319
+ const client = await getClient(language, context.cwd);
320
+ const uri = `file://${fullPath}`;
321
+ const position = { line, character: column };
322
+
323
+ // Open the document so the server knows about it
324
+ const content = await readFile(fullPath, "utf-8");
325
+ client.notify("textDocument/didOpen", {
326
+ textDocument: {
327
+ uri,
328
+ languageId: language,
329
+ version: 1,
330
+ text: content,
331
+ },
332
+ });
333
+
334
+ switch (action) {
335
+ case "definition": {
336
+ const result = await client.request("textDocument/definition", {
337
+ textDocument: { uri },
338
+ position,
339
+ });
340
+ return formatLocations(result);
341
+ }
342
+
343
+ case "references": {
344
+ const result = await client.request("textDocument/references", {
345
+ textDocument: { uri },
346
+ position,
347
+ context: { includeDeclaration: true },
348
+ });
349
+ return formatLocations(result);
350
+ }
351
+
352
+ case "hover": {
353
+ const result = await client.request("textDocument/hover", {
354
+ textDocument: { uri },
355
+ position,
356
+ });
357
+ return formatHover(result);
358
+ }
359
+
360
+ default:
361
+ return `Unknown action: ${action}`;
362
+ }
363
+ } catch (err) {
364
+ return `LSP error: ${err instanceof Error ? err.message : String(err)}`;
365
+ }
366
+ },
367
+ };
368
+
369
+ /** Shut down all cached LSP clients. Call on process exit. */
370
+ export async function shutdownLSP(): Promise<void> {
371
+ for (const client of clients.values()) {
372
+ await client.stop().catch(() => {});
373
+ }
374
+ clients.clear();
375
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * ListMcpResources tool — enumerate tools and resources from connected MCP servers.
3
+ */
4
+
5
+ import type { Tool, ToolContext } from "./types.ts";
6
+ import type { MCPManager } from "../mcp/manager.ts";
7
+
8
+ // Module-level MCP manager reference (set during init)
9
+ let _mcpManager: MCPManager | null = null;
10
+
11
+ export function setMCPManager(manager: MCPManager): void {
12
+ _mcpManager = manager;
13
+ }
14
+
15
+ export const listMcpResourcesTool: Tool = {
16
+ name: "ListMcpResources",
17
+
18
+ prompt() {
19
+ return "List available MCP (Model Context Protocol) resources and tools from connected servers.";
20
+ },
21
+
22
+ inputSchema() {
23
+ return {
24
+ type: "object" as const,
25
+ properties: {
26
+ server: {
27
+ type: "string",
28
+ description: "Optional: filter by server name",
29
+ },
30
+ },
31
+ };
32
+ },
33
+
34
+ isReadOnly() {
35
+ return true;
36
+ },
37
+
38
+ isDestructive() {
39
+ return false;
40
+ },
41
+
42
+ isConcurrencySafe() {
43
+ return true;
44
+ },
45
+
46
+ validateInput() {
47
+ return null;
48
+ },
49
+
50
+ async call(input: Record<string, unknown>, _context: ToolContext): Promise<string> {
51
+ if (!_mcpManager) return "No MCP servers connected.";
52
+
53
+ try {
54
+ const serverNames = _mcpManager.getServerNames();
55
+ if (serverNames.length === 0) return "No MCP servers connected.";
56
+
57
+ const serverFilter = input.server as string | undefined;
58
+ const allTools = _mcpManager.getAllTools();
59
+ const lines: string[] = [];
60
+
61
+ for (const name of serverNames) {
62
+ if (serverFilter && !name.includes(serverFilter)) continue;
63
+
64
+ const serverTools = allTools.filter(t => t.serverName === name);
65
+ lines.push(`\n Server: ${name}`);
66
+
67
+ if (serverTools.length > 0) {
68
+ lines.push(` Tools (${serverTools.length}):`);
69
+ for (const { tool } of serverTools.slice(0, 10)) {
70
+ lines.push(` - ${tool.name}: ${(tool.description ?? "").slice(0, 60)}`);
71
+ }
72
+ if (serverTools.length > 10) {
73
+ lines.push(` ... and ${serverTools.length - 10} more`);
74
+ }
75
+ }
76
+ }
77
+
78
+ return lines.join("\n") || "No resources found.";
79
+ } catch (err) {
80
+ return `Error listing MCP resources: ${err instanceof Error ? err.message : String(err)}`;
81
+ }
82
+ },
83
+ };
@@ -0,0 +1,47 @@
1
+ /**
2
+ * MCP Tool wrapper — wraps an MCP server tool as an AshlrCode Tool.
3
+ *
4
+ * Tool naming: mcp__<server>__<tool>
5
+ */
6
+
7
+ import type { Tool, ToolContext } from "./types.ts";
8
+ import type { MCPManager } from "../mcp/manager.ts";
9
+ import type { MCPToolInfo } from "../mcp/types.ts";
10
+
11
+ export function createMCPTool(
12
+ serverName: string,
13
+ toolInfo: MCPToolInfo,
14
+ manager: MCPManager
15
+ ): Tool {
16
+ const fullName = `mcp__${serverName}__${toolInfo.name}`;
17
+
18
+ return {
19
+ name: fullName,
20
+
21
+ prompt() {
22
+ return toolInfo.description ?? `MCP tool: ${toolInfo.name} from ${serverName}`;
23
+ },
24
+
25
+ inputSchema() {
26
+ return toolInfo.inputSchema;
27
+ },
28
+
29
+ isReadOnly() {
30
+ return false; // Can't know, default to cautious
31
+ },
32
+ isDestructive() {
33
+ return false;
34
+ },
35
+ isConcurrencySafe() {
36
+ return true; // MCP tools are independent
37
+ },
38
+
39
+ validateInput() {
40
+ return null; // Schema validation happens on the MCP server
41
+ },
42
+
43
+ async call(input, _context) {
44
+ return await manager.callTool(serverName, toolInfo.name, input);
45
+ },
46
+ };
47
+ }