@ruvnet/open-claude-code 2.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 (63) hide show
  1. package/package.json +27 -0
  2. package/src/agents/loader.mjs +89 -0
  3. package/src/agents/parser.mjs +98 -0
  4. package/src/agents/teams.mjs +123 -0
  5. package/src/auth/oauth.mjs +220 -0
  6. package/src/config/cli-args.mjs +139 -0
  7. package/src/config/env.mjs +261 -0
  8. package/src/config/settings.mjs +119 -0
  9. package/src/core/agent-loop.mjs +462 -0
  10. package/src/core/cache.mjs +105 -0
  11. package/src/core/checkpoints.mjs +141 -0
  12. package/src/core/context-manager.mjs +198 -0
  13. package/src/core/providers.mjs +219 -0
  14. package/src/core/rate-limiter.mjs +119 -0
  15. package/src/core/scheduler.mjs +173 -0
  16. package/src/core/session.mjs +143 -0
  17. package/src/core/streaming.mjs +182 -0
  18. package/src/core/system-prompt.mjs +135 -0
  19. package/src/hooks/engine.mjs +162 -0
  20. package/src/index.mjs +220 -0
  21. package/src/mcp/client.mjs +253 -0
  22. package/src/mcp/transport-shttp.mjs +130 -0
  23. package/src/mcp/transport-sse.mjs +131 -0
  24. package/src/mcp/transport-ws.mjs +134 -0
  25. package/src/permissions/checker.mjs +57 -0
  26. package/src/permissions/injection-check.mjs +60 -0
  27. package/src/permissions/path-check.mjs +102 -0
  28. package/src/permissions/prompt.mjs +73 -0
  29. package/src/permissions/sandbox.mjs +112 -0
  30. package/src/plugins/loader.mjs +138 -0
  31. package/src/skills/loader.mjs +147 -0
  32. package/src/skills/runner.mjs +55 -0
  33. package/src/telemetry/index.mjs +96 -0
  34. package/src/tools/agent.mjs +127 -0
  35. package/src/tools/ask-user.mjs +61 -0
  36. package/src/tools/bash.mjs +148 -0
  37. package/src/tools/cron-create.mjs +120 -0
  38. package/src/tools/cron-delete.mjs +49 -0
  39. package/src/tools/cron-list.mjs +37 -0
  40. package/src/tools/edit.mjs +82 -0
  41. package/src/tools/enter-worktree.mjs +69 -0
  42. package/src/tools/exit-worktree.mjs +57 -0
  43. package/src/tools/glob.mjs +117 -0
  44. package/src/tools/grep.mjs +127 -0
  45. package/src/tools/ls.mjs +58 -0
  46. package/src/tools/lsp.mjs +115 -0
  47. package/src/tools/multi-edit.mjs +94 -0
  48. package/src/tools/notebook-edit.mjs +96 -0
  49. package/src/tools/read-mcp-resource.mjs +57 -0
  50. package/src/tools/read.mjs +138 -0
  51. package/src/tools/registry.mjs +114 -0
  52. package/src/tools/remote-trigger.mjs +84 -0
  53. package/src/tools/send-message.mjs +64 -0
  54. package/src/tools/skill.mjs +52 -0
  55. package/src/tools/todo-write.mjs +68 -0
  56. package/src/tools/tool-search.mjs +77 -0
  57. package/src/tools/web-fetch.mjs +65 -0
  58. package/src/tools/web-search.mjs +89 -0
  59. package/src/tools/write.mjs +55 -0
  60. package/src/ui/commands.mjs +499 -0
  61. package/src/ui/ink-app.mjs +231 -0
  62. package/src/ui/repl.mjs +144 -0
  63. package/test/test.mjs +1857 -0
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Tool Registry — validateInput/call interface.
3
+ * Mirrors Claude Code's tool dispatch system.
4
+ * Registers all 25+ built-in tools.
5
+ */
6
+
7
+ import { BashTool } from './bash.mjs';
8
+ import { ReadTool } from './read.mjs';
9
+ import { EditTool } from './edit.mjs';
10
+ import { WriteTool } from './write.mjs';
11
+ import { GlobTool } from './glob.mjs';
12
+ import { GrepTool } from './grep.mjs';
13
+ import { AgentTool } from './agent.mjs';
14
+ import { WebFetchTool } from './web-fetch.mjs';
15
+ import { WebSearchTool } from './web-search.mjs';
16
+ import { TodoWriteTool } from './todo-write.mjs';
17
+ import { NotebookEditTool } from './notebook-edit.mjs';
18
+ import { MultiEditTool } from './multi-edit.mjs';
19
+ import { LsTool } from './ls.mjs';
20
+ import { ToolSearchTool } from './tool-search.mjs';
21
+ import { AskUserTool } from './ask-user.mjs';
22
+ import { EnterWorktreeTool } from './enter-worktree.mjs';
23
+ import { ExitWorktreeTool } from './exit-worktree.mjs';
24
+ import { SkillTool } from './skill.mjs';
25
+ import { SendMessageTool } from './send-message.mjs';
26
+ import { RemoteTriggerTool } from './remote-trigger.mjs';
27
+ import { CronCreateTool } from './cron-create.mjs';
28
+ import { CronDeleteTool } from './cron-delete.mjs';
29
+ import { CronListTool } from './cron-list.mjs';
30
+ import { LspTool } from './lsp.mjs';
31
+ import { ReadMcpResourceTool } from './read-mcp-resource.mjs';
32
+
33
+ const BUILTIN_TOOLS = [
34
+ BashTool,
35
+ ReadTool,
36
+ EditTool,
37
+ WriteTool,
38
+ GlobTool,
39
+ GrepTool,
40
+ AgentTool,
41
+ WebFetchTool,
42
+ WebSearchTool,
43
+ TodoWriteTool,
44
+ NotebookEditTool,
45
+ MultiEditTool,
46
+ LsTool,
47
+ ToolSearchTool,
48
+ AskUserTool,
49
+ EnterWorktreeTool,
50
+ ExitWorktreeTool,
51
+ SkillTool,
52
+ SendMessageTool,
53
+ RemoteTriggerTool,
54
+ CronCreateTool,
55
+ CronDeleteTool,
56
+ CronListTool,
57
+ LspTool,
58
+ ReadMcpResourceTool,
59
+ ];
60
+
61
+ export function createToolRegistry() {
62
+ const tools = new Map();
63
+ for (const Tool of BUILTIN_TOOLS) {
64
+ tools.set(Tool.name, Tool);
65
+ }
66
+
67
+ const registry = {
68
+ list() {
69
+ return [...tools.values()].map(t => ({
70
+ name: t.name,
71
+ description: t.description,
72
+ input_schema: t.inputSchema,
73
+ }));
74
+ },
75
+
76
+ async call(name, input) {
77
+ const tool = tools.get(name);
78
+ if (!tool) throw new Error(`Unknown tool: ${name}`);
79
+ const errors = tool.validateInput?.(input) || [];
80
+ if (errors.length > 0) return `Validation error: ${errors.join(', ')}`;
81
+ return tool.call(input);
82
+ },
83
+
84
+ register(tool) {
85
+ tools.set(tool.name, tool);
86
+ },
87
+
88
+ get(name) {
89
+ return tools.get(name);
90
+ },
91
+
92
+ has(name) {
93
+ return tools.has(name);
94
+ },
95
+
96
+ registerMcpTools(mcpTools, callFn) {
97
+ ToolSearchTool._mcpTools = mcpTools;
98
+
99
+ for (const mcpTool of mcpTools) {
100
+ const wrapper = {
101
+ name: mcpTool.name,
102
+ description: mcpTool.description || '',
103
+ inputSchema: mcpTool.inputSchema || { type: 'object', properties: {} },
104
+ validateInput() { return []; },
105
+ async call(input) { return callFn(mcpTool.name, input); },
106
+ };
107
+ tools.set(mcpTool.name, wrapper);
108
+ }
109
+ },
110
+ };
111
+
112
+ ToolSearchTool._registry = registry;
113
+ return registry;
114
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * RemoteTrigger Tool — trigger remote execution of a task.
3
+ *
4
+ * Sends a task to a remote agent endpoint for execution.
5
+ * Used for distributed agent workflows and scheduled tasks.
6
+ */
7
+
8
+ export const RemoteTriggerTool = {
9
+ name: 'RemoteTrigger',
10
+ description: 'Trigger remote execution of a task on a remote agent.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ endpoint: {
15
+ type: 'string',
16
+ description: 'Remote agent endpoint URL',
17
+ },
18
+ task: {
19
+ type: 'string',
20
+ description: 'Task description or prompt to execute remotely',
21
+ },
22
+ timeout: {
23
+ type: 'number',
24
+ description: 'Timeout in milliseconds (default: 300000)',
25
+ },
26
+ async: {
27
+ type: 'boolean',
28
+ description: 'Fire-and-forget mode (default: false)',
29
+ },
30
+ },
31
+ required: ['task'],
32
+ },
33
+
34
+ validateInput(input) {
35
+ return input.task ? [] : ['task is required'];
36
+ },
37
+
38
+ async call(input) {
39
+ const endpoint = input.endpoint || process.env.REMOTE_AGENT_URL;
40
+ if (!endpoint) {
41
+ return 'No remote endpoint configured. Set endpoint parameter or REMOTE_AGENT_URL env var.';
42
+ }
43
+
44
+ try {
45
+ const controller = new AbortController();
46
+ const timeout = setTimeout(
47
+ () => controller.abort(),
48
+ input.timeout || 300000
49
+ );
50
+
51
+ const res = await fetch(endpoint, {
52
+ method: 'POST',
53
+ headers: {
54
+ 'Content-Type': 'application/json',
55
+ ...(process.env.REMOTE_AGENT_TOKEN && {
56
+ Authorization: `Bearer ${process.env.REMOTE_AGENT_TOKEN}`,
57
+ }),
58
+ },
59
+ body: JSON.stringify({
60
+ task: input.task,
61
+ async: input.async || false,
62
+ }),
63
+ signal: controller.signal,
64
+ });
65
+
66
+ clearTimeout(timeout);
67
+
68
+ if (!res.ok) {
69
+ return `Remote error: HTTP ${res.status} ${res.statusText}`;
70
+ }
71
+
72
+ if (input.async) {
73
+ return `Task submitted to ${endpoint} (async mode)`;
74
+ }
75
+
76
+ const data = await res.json();
77
+ return typeof data.result === 'string'
78
+ ? data.result
79
+ : JSON.stringify(data, null, 2);
80
+ } catch (err) {
81
+ return `Remote trigger error: ${err.message}`;
82
+ }
83
+ },
84
+ };
@@ -0,0 +1,64 @@
1
+ /**
2
+ * SendMessage Tool — send a message to a teammate agent.
3
+ *
4
+ * Used in multi-agent (teams) mode to communicate between agents.
5
+ * Messages are stored in a shared message queue.
6
+ */
7
+
8
+ const messageQueue = new Map(); // agentId -> messages[]
9
+
10
+ export const SendMessageTool = {
11
+ name: 'SendMessage',
12
+ description: 'Send a message to a teammate agent in multi-agent mode.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ to: {
17
+ type: 'string',
18
+ description: 'Target agent ID or name',
19
+ },
20
+ content: {
21
+ type: 'string',
22
+ description: 'Message content to send',
23
+ },
24
+ type: {
25
+ type: 'string',
26
+ enum: ['request', 'response', 'notification', 'handoff'],
27
+ description: 'Message type (default: notification)',
28
+ },
29
+ },
30
+ required: ['to', 'content'],
31
+ },
32
+
33
+ validateInput(input) {
34
+ const errors = [];
35
+ if (!input.to) errors.push('to is required');
36
+ if (!input.content) errors.push('content is required');
37
+ return errors;
38
+ },
39
+
40
+ async call(input) {
41
+ const msg = {
42
+ id: `msg_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
43
+ from: process.env.AGENT_ID || 'main',
44
+ to: input.to,
45
+ type: input.type || 'notification',
46
+ content: input.content,
47
+ timestamp: new Date().toISOString(),
48
+ };
49
+
50
+ if (!messageQueue.has(input.to)) {
51
+ messageQueue.set(input.to, []);
52
+ }
53
+ messageQueue.get(input.to).push(msg);
54
+
55
+ return `Message sent to "${input.to}" (id: ${msg.id}, type: ${msg.type})`;
56
+ },
57
+
58
+ // Helper: receive messages for an agent
59
+ receive(agentId) {
60
+ const messages = messageQueue.get(agentId) || [];
61
+ messageQueue.set(agentId, []);
62
+ return messages;
63
+ },
64
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Skill Tool — invoke a loaded skill by name.
3
+ *
4
+ * Skills are loaded from .claude/skills/{name}/SKILL.md and provide
5
+ * specialized prompts injected into the conversation.
6
+ */
7
+
8
+ export const SkillTool = {
9
+ name: 'Skill',
10
+ description: 'Invoke a skill within the conversation. Skills provide specialized capabilities.',
11
+ inputSchema: {
12
+ type: 'object',
13
+ properties: {
14
+ skill: {
15
+ type: 'string',
16
+ description: 'The skill name to invoke (e.g., "commit", "review-pr")',
17
+ },
18
+ args: {
19
+ type: 'string',
20
+ description: 'Optional arguments for the skill',
21
+ },
22
+ },
23
+ required: ['skill'],
24
+ },
25
+
26
+ // Set by skills loader
27
+ _skillsLoader: null,
28
+
29
+ validateInput(input) {
30
+ return input.skill ? [] : ['skill name is required'];
31
+ },
32
+
33
+ async call(input) {
34
+ if (!this._skillsLoader) {
35
+ return 'Skills system not initialized. No skills available.';
36
+ }
37
+
38
+ try {
39
+ const skill = this._skillsLoader.get(input.skill);
40
+ if (!skill) {
41
+ const available = this._skillsLoader.list();
42
+ const names = available.map(s => s.name).join(', ');
43
+ return `Unknown skill: "${input.skill}". Available: ${names || 'none'}`;
44
+ }
45
+
46
+ const result = await this._skillsLoader.run(input.skill, input.args);
47
+ return result;
48
+ } catch (err) {
49
+ return `Skill error: ${err.message}`;
50
+ }
51
+ },
52
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * TodoWrite Tool — in-memory task management.
3
+ *
4
+ * Maintains a task list that the agent can use to track work items.
5
+ * State persists for the duration of the session.
6
+ */
7
+
8
+ const todos = [];
9
+ let nextId = 1;
10
+
11
+ export const TodoWriteTool = {
12
+ name: 'TodoWrite',
13
+ description: 'Manage a task list. Supports add, update, complete, and list operations.',
14
+ inputSchema: {
15
+ type: 'object',
16
+ properties: {
17
+ todos: {
18
+ type: 'array',
19
+ items: {
20
+ type: 'object',
21
+ properties: {
22
+ id: { type: 'string', description: 'Task ID (auto-assigned if adding)' },
23
+ content: { type: 'string', description: 'Task description' },
24
+ status: {
25
+ type: 'string',
26
+ enum: ['pending', 'in_progress', 'completed'],
27
+ description: 'Task status',
28
+ },
29
+ priority: {
30
+ type: 'string',
31
+ enum: ['high', 'medium', 'low'],
32
+ description: 'Task priority',
33
+ },
34
+ },
35
+ required: ['content', 'status'],
36
+ },
37
+ description: 'Array of todo items to write (replaces all todos)',
38
+ },
39
+ },
40
+ required: ['todos'],
41
+ },
42
+
43
+ validateInput(input) {
44
+ if (!input.todos || !Array.isArray(input.todos)) return ['todos must be an array'];
45
+ return [];
46
+ },
47
+
48
+ async call(input) {
49
+ // Replace entire todo list (matches Claude Code behavior)
50
+ todos.length = 0;
51
+ nextId = 1;
52
+
53
+ for (const item of input.todos) {
54
+ todos.push({
55
+ id: item.id || String(nextId++),
56
+ content: item.content,
57
+ status: item.status || 'pending',
58
+ priority: item.priority || 'medium',
59
+ });
60
+ }
61
+
62
+ const summary = todos.map(t =>
63
+ `[${t.status === 'completed' ? 'x' : t.status === 'in_progress' ? '~' : ' '}] ${t.id}. ${t.content} (${t.priority})`
64
+ ).join('\n');
65
+
66
+ return `Updated ${todos.length} todos:\n${summary}`;
67
+ },
68
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * ToolSearch Tool — deferred tool loading / search.
3
+ *
4
+ * Allows the agent to discover and load tools that are not
5
+ * in the default set. Supports searching MCP servers and
6
+ * deferred tool definitions.
7
+ */
8
+
9
+ export const ToolSearchTool = {
10
+ name: 'ToolSearch',
11
+ description: 'Search for and load deferred tools by name or keyword.',
12
+ inputSchema: {
13
+ type: 'object',
14
+ properties: {
15
+ query: {
16
+ type: 'string',
17
+ description: 'Tool name or search keywords',
18
+ },
19
+ max_results: {
20
+ type: 'number',
21
+ description: 'Maximum results to return (default: 5)',
22
+ },
23
+ },
24
+ required: ['query'],
25
+ },
26
+
27
+ // Will be set by the registry when MCP tools are available
28
+ _mcpTools: [],
29
+ _registry: null,
30
+
31
+ validateInput(input) {
32
+ return input.query ? [] : ['query is required'];
33
+ },
34
+
35
+ async call(input) {
36
+ const query = input.query.toLowerCase();
37
+ const maxResults = input.max_results || 5;
38
+ const matches = [];
39
+
40
+ // Search MCP tools
41
+ for (const tool of this._mcpTools) {
42
+ const name = (tool.name || '').toLowerCase();
43
+ const desc = (tool.description || '').toLowerCase();
44
+ if (name.includes(query) || desc.includes(query)) {
45
+ matches.push({
46
+ name: tool.name,
47
+ description: tool.description,
48
+ source: 'mcp',
49
+ });
50
+ }
51
+ }
52
+
53
+ // Search registry tools
54
+ if (this._registry) {
55
+ for (const tool of this._registry.list()) {
56
+ const name = (tool.name || '').toLowerCase();
57
+ const desc = (tool.description || '').toLowerCase();
58
+ if (name.includes(query) || desc.includes(query)) {
59
+ matches.push({
60
+ name: tool.name,
61
+ description: tool.description,
62
+ source: 'builtin',
63
+ });
64
+ }
65
+ }
66
+ }
67
+
68
+ if (matches.length === 0) {
69
+ return `No tools found matching "${input.query}"`;
70
+ }
71
+
72
+ const limited = matches.slice(0, maxResults);
73
+ return limited.map(m =>
74
+ `[${m.source}] ${m.name}: ${m.description}`
75
+ ).join('\n');
76
+ },
77
+ };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Web Fetch Tool — fetch URL content using built-in Node.js fetch.
3
+ */
4
+
5
+ export const WebFetchTool = {
6
+ name: 'WebFetch',
7
+ description: 'Fetch content from a URL. Returns the response body as text.',
8
+ inputSchema: {
9
+ type: 'object',
10
+ properties: {
11
+ url: { type: 'string', description: 'The URL to fetch' },
12
+ headers: {
13
+ type: 'object',
14
+ description: 'Optional HTTP headers',
15
+ },
16
+ max_length: {
17
+ type: 'number',
18
+ description: 'Max response length in characters (default: 50000)',
19
+ },
20
+ },
21
+ required: ['url'],
22
+ },
23
+
24
+ validateInput(input) {
25
+ const errors = [];
26
+ if (!input.url) errors.push('url is required');
27
+ try {
28
+ new URL(input.url);
29
+ } catch {
30
+ errors.push('url must be a valid URL');
31
+ }
32
+ return errors;
33
+ },
34
+
35
+ async call(input) {
36
+ const maxLength = input.max_length || 50000;
37
+
38
+ try {
39
+ const controller = new AbortController();
40
+ const timeout = setTimeout(() => controller.abort(), 30000);
41
+
42
+ const res = await fetch(input.url, {
43
+ headers: input.headers || {},
44
+ signal: controller.signal,
45
+ redirect: 'follow',
46
+ });
47
+
48
+ clearTimeout(timeout);
49
+
50
+ if (!res.ok) {
51
+ return `HTTP ${res.status}: ${res.statusText}`;
52
+ }
53
+
54
+ const contentType = res.headers.get('content-type') || '';
55
+ const text = await res.text();
56
+ const truncated = text.length > maxLength
57
+ ? text.slice(0, maxLength) + `\n...[truncated at ${maxLength} chars]`
58
+ : text;
59
+
60
+ return `Content-Type: ${contentType}\nLength: ${text.length}\n\n${truncated}`;
61
+ } catch (err) {
62
+ return `Fetch error: ${err.message}`;
63
+ }
64
+ },
65
+ };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Web Search Tool — search the web via configurable API backends.
3
+ *
4
+ * Supports:
5
+ * - Brave Search API (BRAVE_API_KEY)
6
+ * - SearXNG (self-hosted, SEARXNG_URL)
7
+ * - Fallback: returns instructions to use web-fetch instead
8
+ */
9
+
10
+ export const WebSearchTool = {
11
+ name: 'WebSearch',
12
+ description: 'Search the web for information. Requires BRAVE_API_KEY or SEARXNG_URL.',
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ query: { type: 'string', description: 'Search query' },
17
+ limit: { type: 'number', description: 'Max results (default: 5)' },
18
+ },
19
+ required: ['query'],
20
+ },
21
+
22
+ validateInput(input) {
23
+ return input.query ? [] : ['query is required'];
24
+ },
25
+
26
+ async call(input) {
27
+ const limit = input.limit || 5;
28
+
29
+ // Try Brave Search API
30
+ const braveKey = process.env.BRAVE_API_KEY;
31
+ if (braveKey) {
32
+ return searchBrave(input.query, limit, braveKey);
33
+ }
34
+
35
+ // Try SearXNG
36
+ const searxUrl = process.env.SEARXNG_URL;
37
+ if (searxUrl) {
38
+ return searchSearxng(input.query, limit, searxUrl);
39
+ }
40
+
41
+ return 'No search API configured. Set BRAVE_API_KEY or SEARXNG_URL environment variable. ' +
42
+ 'Alternatively, use the WebFetch tool to fetch specific URLs directly.';
43
+ },
44
+ };
45
+
46
+ async function searchBrave(query, limit, apiKey) {
47
+ try {
48
+ const url = `https://api.search.brave.com/res/v1/web/search?q=${encodeURIComponent(query)}&count=${limit}`;
49
+ const res = await fetch(url, {
50
+ headers: {
51
+ 'Accept': 'application/json',
52
+ 'X-Subscription-Token': apiKey,
53
+ },
54
+ });
55
+
56
+ if (!res.ok) return `Brave API error: ${res.status}`;
57
+
58
+ const data = await res.json();
59
+ const results = (data.web?.results || []).slice(0, limit);
60
+
61
+ if (results.length === 0) return 'No results found.';
62
+
63
+ return results.map((r, i) =>
64
+ `${i + 1}. ${r.title}\n ${r.url}\n ${r.description || ''}`
65
+ ).join('\n\n');
66
+ } catch (err) {
67
+ return `Search error: ${err.message}`;
68
+ }
69
+ }
70
+
71
+ async function searchSearxng(query, limit, baseUrl) {
72
+ try {
73
+ const url = `${baseUrl}/search?q=${encodeURIComponent(query)}&format=json&categories=general`;
74
+ const res = await fetch(url);
75
+
76
+ if (!res.ok) return `SearXNG error: ${res.status}`;
77
+
78
+ const data = await res.json();
79
+ const results = (data.results || []).slice(0, limit);
80
+
81
+ if (results.length === 0) return 'No results found.';
82
+
83
+ return results.map((r, i) =>
84
+ `${i + 1}. ${r.title}\n ${r.url}\n ${r.content || ''}`
85
+ ).join('\n\n');
86
+ } catch (err) {
87
+ return `Search error: ${err.message}`;
88
+ }
89
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Write Tool — matches Claude Code's exact behavior.
3
+ *
4
+ * Features:
5
+ * - Creates parent directories if needed
6
+ * - Requires Read first for existing file overwrites
7
+ * - No README creation unless explicitly asked
8
+ */
9
+ import fs from 'fs';
10
+ import path from 'path';
11
+ import { hasBeenRead, markRead } from './read.mjs';
12
+
13
+ export const WriteTool = {
14
+ name: 'Write',
15
+ description: 'Write content to a file. Creates parent dirs if needed.',
16
+ inputSchema: {
17
+ type: 'object',
18
+ properties: {
19
+ file_path: { type: 'string', description: 'Absolute path to the file' },
20
+ content: { type: 'string', description: 'The content to write' },
21
+ },
22
+ required: ['file_path', 'content'],
23
+ },
24
+ validateInput(input) {
25
+ const errors = [];
26
+ if (!input.file_path) errors.push('file_path required');
27
+ return errors;
28
+ },
29
+ async call(input) {
30
+ const filePath = path.resolve(input.file_path);
31
+
32
+ // Check if file already exists — require Read first for overwrites
33
+ if (fs.existsSync(filePath)) {
34
+ if (!hasBeenRead(filePath)) {
35
+ return `Error: File ${filePath} already exists. You must Read it first before overwriting.`;
36
+ }
37
+ }
38
+
39
+ // Create parent directory if it doesn't exist
40
+ const dir = path.dirname(filePath);
41
+ try {
42
+ fs.mkdirSync(dir, { recursive: true });
43
+ } catch (e) {
44
+ return `Error creating directory ${dir}: ${e.message}`;
45
+ }
46
+
47
+ try {
48
+ fs.writeFileSync(filePath, input.content);
49
+ markRead(filePath); // Mark as read after writing
50
+ return `File written: ${filePath}`;
51
+ } catch (e) {
52
+ return `Error writing file: ${e.message}`;
53
+ }
54
+ },
55
+ };