wave-agent-sdk 0.0.2 → 0.0.3

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 (64) hide show
  1. package/dist/agent.d.ts +5 -1
  2. package/dist/agent.d.ts.map +1 -1
  3. package/dist/agent.js +46 -2
  4. package/dist/index.d.ts +0 -1
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +0 -1
  7. package/dist/managers/aiManager.d.ts.map +1 -1
  8. package/dist/managers/aiManager.js +4 -7
  9. package/dist/managers/messageManager.d.ts +8 -0
  10. package/dist/managers/messageManager.d.ts.map +1 -1
  11. package/dist/managers/messageManager.js +26 -2
  12. package/dist/managers/skillManager.d.ts +4 -5
  13. package/dist/managers/skillManager.d.ts.map +1 -1
  14. package/dist/managers/skillManager.js +6 -82
  15. package/dist/managers/subagentManager.d.ts +96 -0
  16. package/dist/managers/subagentManager.d.ts.map +1 -0
  17. package/dist/managers/subagentManager.js +261 -0
  18. package/dist/managers/toolManager.d.ts +33 -1
  19. package/dist/managers/toolManager.d.ts.map +1 -1
  20. package/dist/managers/toolManager.js +43 -5
  21. package/dist/services/aiService.d.ts.map +1 -1
  22. package/dist/services/aiService.js +40 -14
  23. package/dist/tools/grepTool.d.ts.map +1 -1
  24. package/dist/tools/grepTool.js +8 -6
  25. package/dist/tools/readTool.d.ts.map +1 -1
  26. package/dist/tools/readTool.js +36 -6
  27. package/dist/tools/skillTool.d.ts +8 -0
  28. package/dist/tools/skillTool.d.ts.map +1 -0
  29. package/dist/tools/skillTool.js +72 -0
  30. package/dist/tools/taskTool.d.ts +8 -0
  31. package/dist/tools/taskTool.d.ts.map +1 -0
  32. package/dist/tools/taskTool.js +109 -0
  33. package/dist/tools/todoWriteTool.d.ts +6 -0
  34. package/dist/tools/todoWriteTool.d.ts.map +1 -0
  35. package/dist/tools/todoWriteTool.js +203 -0
  36. package/dist/types.d.ts +8 -1
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/utils/fileFormat.d.ts +17 -0
  39. package/dist/utils/fileFormat.d.ts.map +1 -0
  40. package/dist/utils/fileFormat.js +35 -0
  41. package/dist/utils/messageOperations.d.ts +18 -0
  42. package/dist/utils/messageOperations.d.ts.map +1 -1
  43. package/dist/utils/messageOperations.js +43 -0
  44. package/dist/utils/subagentParser.d.ts +19 -0
  45. package/dist/utils/subagentParser.d.ts.map +1 -0
  46. package/dist/utils/subagentParser.js +159 -0
  47. package/package.json +1 -1
  48. package/src/agent.ts +53 -3
  49. package/src/index.ts +0 -1
  50. package/src/managers/aiManager.ts +5 -8
  51. package/src/managers/messageManager.ts +55 -1
  52. package/src/managers/skillManager.ts +7 -96
  53. package/src/managers/subagentManager.ts +368 -0
  54. package/src/managers/toolManager.ts +50 -5
  55. package/src/services/aiService.ts +43 -15
  56. package/src/tools/grepTool.ts +9 -6
  57. package/src/tools/readTool.ts +40 -6
  58. package/src/tools/skillTool.ts +82 -0
  59. package/src/tools/taskTool.ts +128 -0
  60. package/src/tools/todoWriteTool.ts +232 -0
  61. package/src/types.ts +10 -1
  62. package/src/utils/fileFormat.ts +40 -0
  63. package/src/utils/messageOperations.ts +80 -0
  64. package/src/utils/subagentParser.ts +223 -0
@@ -0,0 +1,368 @@
1
+ import { randomUUID } from "crypto";
2
+ import type { SubagentConfiguration } from "../utils/subagentParser.js";
3
+ import type { Message, Logger, GatewayConfig, ModelConfig } from "../types.js";
4
+ import { AIManager } from "./aiManager.js";
5
+ import {
6
+ MessageManager,
7
+ type MessageManagerCallbacks,
8
+ } from "./messageManager.js";
9
+ import { ToolManager } from "./toolManager.js";
10
+
11
+ export interface SubagentInstance {
12
+ subagentId: string;
13
+ configuration: SubagentConfiguration;
14
+ aiManager: AIManager;
15
+ messageManager: MessageManager;
16
+ toolManager: ToolManager;
17
+ status: "initializing" | "active" | "completed" | "error" | "aborted";
18
+ taskDescription: string;
19
+ messages: Message[];
20
+ }
21
+
22
+ export interface SubagentManagerOptions {
23
+ workdir: string;
24
+ parentToolManager: ToolManager;
25
+ parentMessageManager: MessageManager;
26
+ logger?: Logger;
27
+ gatewayConfig: GatewayConfig;
28
+ modelConfig: ModelConfig;
29
+ tokenLimit: number;
30
+ }
31
+
32
+ export class SubagentManager {
33
+ private instances = new Map<string, SubagentInstance>();
34
+ private cachedConfigurations: SubagentConfiguration[] | null = null;
35
+
36
+ private workdir: string;
37
+ private parentToolManager: ToolManager;
38
+ private parentMessageManager: MessageManager;
39
+ private logger?: Logger;
40
+ private gatewayConfig: GatewayConfig;
41
+ private modelConfig: ModelConfig;
42
+ private tokenLimit: number;
43
+
44
+ constructor(options: SubagentManagerOptions) {
45
+ this.workdir = options.workdir;
46
+ this.parentToolManager = options.parentToolManager;
47
+ this.parentMessageManager = options.parentMessageManager;
48
+ this.logger = options.logger;
49
+ this.gatewayConfig = options.gatewayConfig;
50
+ this.modelConfig = options.modelConfig;
51
+ this.tokenLimit = options.tokenLimit;
52
+ }
53
+
54
+ /**
55
+ * Initialize the SubagentManager by loading and caching configurations
56
+ */
57
+ async initialize(): Promise<void> {
58
+ await this.loadConfigurations();
59
+ }
60
+
61
+ /**
62
+ * Load all available subagent configurations and cache them
63
+ */
64
+ async loadConfigurations(): Promise<SubagentConfiguration[]> {
65
+ if (this.cachedConfigurations === null) {
66
+ const { loadSubagentConfigurations } = await import(
67
+ "../utils/subagentParser.js"
68
+ );
69
+ this.cachedConfigurations = await loadSubagentConfigurations(
70
+ this.workdir,
71
+ );
72
+ }
73
+ return this.cachedConfigurations;
74
+ }
75
+
76
+ /**
77
+ * Get cached configurations synchronously (must call loadConfigurations first)
78
+ */
79
+ getConfigurations(): SubagentConfiguration[] {
80
+ if (this.cachedConfigurations === null) {
81
+ throw new Error(
82
+ "SubagentManager not initialized. Call loadConfigurations() first.",
83
+ );
84
+ }
85
+ return this.cachedConfigurations;
86
+ }
87
+
88
+ /**
89
+ * Find subagent by exact name match
90
+ */
91
+ async findSubagent(name: string) {
92
+ const { findSubagentByName } = await import("../utils/subagentParser.js");
93
+ return findSubagentByName(name, this.workdir);
94
+ }
95
+
96
+ /**
97
+ * Create a new subagent instance with isolated managers
98
+ */
99
+ async createInstance(
100
+ configuration: SubagentConfiguration,
101
+ taskDescription: string,
102
+ ): Promise<SubagentInstance> {
103
+ if (
104
+ !this.parentToolManager ||
105
+ !this.gatewayConfig ||
106
+ !this.modelConfig ||
107
+ !this.tokenLimit
108
+ ) {
109
+ throw new Error(
110
+ "SubagentManager not properly initialized - call initialize() first",
111
+ );
112
+ }
113
+
114
+ const subagentId = randomUUID();
115
+
116
+ // Create isolated MessageManager for the subagent
117
+ const subagentCallbacks: MessageManagerCallbacks = {
118
+ // These callbacks will be handled by the parent agent
119
+ onMessagesChange: (messages: Message[]) => {
120
+ const instance = this.instances.get(subagentId);
121
+ if (instance) {
122
+ instance.messages = messages;
123
+ // Update parent's subagent block with latest messages
124
+ this.parentMessageManager.updateSubagentBlock(subagentId, {
125
+ messages: messages,
126
+ });
127
+ }
128
+ },
129
+ };
130
+
131
+ const messageManager = new MessageManager({
132
+ callbacks: subagentCallbacks,
133
+ workdir: this.workdir,
134
+ logger: this.logger,
135
+ });
136
+
137
+ // Use the parent tool manager directly - tool restrictions will be handled by allowedTools parameter
138
+ const toolManager = this.parentToolManager;
139
+
140
+ // Determine model to use
141
+ const modelToUse =
142
+ configuration.model && configuration.model !== "inherit"
143
+ ? configuration.model
144
+ : this.modelConfig.agentModel;
145
+
146
+ // Create isolated AIManager for the subagent
147
+ const aiManager = new AIManager({
148
+ messageManager,
149
+ toolManager,
150
+ logger: this.logger,
151
+ workdir: this.workdir,
152
+ systemPrompt: configuration.systemPrompt,
153
+ gatewayConfig: this.gatewayConfig,
154
+ modelConfig: {
155
+ ...this.modelConfig,
156
+ agentModel: modelToUse,
157
+ },
158
+ tokenLimit: this.tokenLimit,
159
+ });
160
+
161
+ const instance: SubagentInstance = {
162
+ subagentId,
163
+ configuration,
164
+ aiManager,
165
+ messageManager,
166
+ toolManager,
167
+ status: "initializing",
168
+ taskDescription,
169
+ messages: [],
170
+ };
171
+
172
+ this.instances.set(subagentId, instance);
173
+
174
+ // Create subagent block in parent message manager
175
+ this.parentMessageManager.addSubagentBlock(
176
+ subagentId,
177
+ configuration.name,
178
+ "active",
179
+ [],
180
+ );
181
+
182
+ return instance;
183
+ }
184
+
185
+ /**
186
+ * Execute task using subagent instance
187
+ *
188
+ * IMPORTANT: This method automatically filters out the Task tool from allowedTools
189
+ * to prevent subagents from spawning other subagents (infinite recursion protection)
190
+ */
191
+ async executeTask(
192
+ instance: SubagentInstance,
193
+ prompt: string,
194
+ ): Promise<string> {
195
+ try {
196
+ // Set status to active and update parent
197
+ this.updateInstanceStatus(instance.subagentId, "active");
198
+ this.parentMessageManager.updateSubagentBlock(instance.subagentId, {
199
+ status: "active",
200
+ });
201
+
202
+ // Add the user's prompt as a message
203
+ instance.messageManager.addUserMessage(prompt);
204
+
205
+ // Create allowed tools list - always exclude Task tool to prevent subagent recursion
206
+ let allowedTools = instance.configuration.tools;
207
+
208
+ // Always filter out the Task tool to prevent subagents from creating sub-subagents
209
+ if (allowedTools) {
210
+ allowedTools = allowedTools.filter((tool) => tool !== "Task");
211
+ } else {
212
+ // If no tools specified, get all tools except Task
213
+ const allTools = instance.toolManager.list().map((tool) => tool.name);
214
+ allowedTools = allTools.filter((tool) => tool !== "Task");
215
+ }
216
+
217
+ // Execute the AI request with tool restrictions
218
+ await instance.aiManager.sendAIMessage({
219
+ allowedTools,
220
+ model:
221
+ instance.configuration.model !== "inherit"
222
+ ? instance.configuration.model
223
+ : undefined,
224
+ });
225
+
226
+ // Get the latest messages to extract the response
227
+ const messages = instance.messageManager.getMessages();
228
+ const lastAssistantMessage = messages
229
+ .filter((msg) => msg.role === "assistant")
230
+ .pop();
231
+
232
+ if (!lastAssistantMessage) {
233
+ throw new Error("No response from subagent");
234
+ }
235
+
236
+ // Extract text content from the last assistant message
237
+ const textBlocks = lastAssistantMessage.blocks.filter(
238
+ (block) => block.type === "text",
239
+ );
240
+ const response = textBlocks.map((block) => block.content).join("\n");
241
+
242
+ // Update status to completed and update parent with final messages
243
+ this.updateInstanceStatus(instance.subagentId, "completed");
244
+ this.parentMessageManager.updateSubagentBlock(instance.subagentId, {
245
+ status: "completed",
246
+ messages: messages,
247
+ });
248
+
249
+ return response || "Task completed with no text response";
250
+ } catch (error) {
251
+ this.updateInstanceStatus(instance.subagentId, "error");
252
+ this.parentMessageManager.updateSubagentBlock(instance.subagentId, {
253
+ status: "error",
254
+ });
255
+ throw error;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Get instance by subagent ID
261
+ */
262
+ getInstance(subagentId: string): SubagentInstance | null {
263
+ return this.instances.get(subagentId) || null;
264
+ }
265
+
266
+ /**
267
+ * Update instance status
268
+ */
269
+ updateInstanceStatus(
270
+ subagentId: string,
271
+ status: SubagentInstance["status"],
272
+ ): void {
273
+ const instance = this.instances.get(subagentId);
274
+ if (instance) {
275
+ instance.status = status;
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Add message to instance
281
+ */
282
+ addMessageToInstance(subagentId: string, message: Message): void {
283
+ const instance = this.instances.get(subagentId);
284
+ if (instance) {
285
+ instance.messages.push(message);
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Abort a running subagent instance
291
+ */
292
+ abortInstance(subagentId: string): boolean {
293
+ const instance = this.instances.get(subagentId);
294
+ if (!instance) {
295
+ return false;
296
+ }
297
+
298
+ // Only abort active or initializing instances
299
+ if (instance.status !== "active" && instance.status !== "initializing") {
300
+ return false;
301
+ }
302
+
303
+ try {
304
+ // Abort the AI manager operations
305
+ instance.aiManager.abortAIMessage();
306
+
307
+ // Update status
308
+ this.updateInstanceStatus(subagentId, "aborted");
309
+ this.parentMessageManager.updateSubagentBlock(subagentId, {
310
+ status: "aborted",
311
+ messages: instance.messages,
312
+ });
313
+
314
+ this.logger?.info(`Aborted subagent instance: ${subagentId}`);
315
+ return true;
316
+ } catch (error) {
317
+ this.logger?.error(
318
+ `Failed to abort subagent instance ${subagentId}:`,
319
+ error,
320
+ );
321
+ return false;
322
+ }
323
+ }
324
+
325
+ /**
326
+ * Abort all active subagent instances
327
+ */
328
+ abortAllInstances(): void {
329
+ const activeInstances = this.getActiveInstances();
330
+ for (const instance of activeInstances) {
331
+ this.abortInstance(instance.subagentId);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Clean up completed, errored, or aborted instances
337
+ */
338
+ cleanupInstance(subagentId: string): void {
339
+ const instance = this.instances.get(subagentId);
340
+ if (
341
+ instance &&
342
+ (instance.status === "completed" ||
343
+ instance.status === "error" ||
344
+ instance.status === "aborted")
345
+ ) {
346
+ this.instances.delete(subagentId);
347
+ }
348
+ }
349
+
350
+ /**
351
+ * Get all active instances
352
+ */
353
+ getActiveInstances(): SubagentInstance[] {
354
+ return Array.from(this.instances.values()).filter(
355
+ (instance) =>
356
+ instance.status === "active" || instance.status === "initializing",
357
+ );
358
+ }
359
+
360
+ /**
361
+ * Clean up all instances (for session end)
362
+ */
363
+ cleanup(): void {
364
+ // Abort all active instances before cleanup
365
+ this.abortAllInstances();
366
+ this.instances.clear();
367
+ }
368
+ }
@@ -9,10 +9,14 @@ import { globTool } from "../tools/globTool.js";
9
9
  import { grepTool } from "../tools/grepTool.js";
10
10
  import { lsTool } from "../tools/lsTool.js";
11
11
  import { readTool } from "../tools/readTool.js";
12
- import { SkillManager } from "./skillManager.js";
12
+ import { todoWriteTool } from "../tools/todoWriteTool.js";
13
+ import { createTaskTool } from "../tools/taskTool.js";
14
+ import { createSkillTool } from "../tools/skillTool.js";
13
15
  import { McpManager } from "./mcpManager.js";
14
16
  import { ChatCompletionFunctionTool } from "openai/resources.js";
15
17
  import type { Logger } from "../types.js";
18
+ import type { SubagentManager } from "./subagentManager.js";
19
+ import type { SkillManager } from "./skillManager.js";
16
20
 
17
21
  export interface ToolManagerOptions {
18
22
  mcpManager: McpManager;
@@ -30,12 +34,42 @@ class ToolManager {
30
34
  constructor(options: ToolManagerOptions) {
31
35
  this.mcpManager = options.mcpManager;
32
36
  this.logger = options.logger;
37
+ }
33
38
 
34
- // Initialize built-in tools
35
- this.initializeBuiltInTools();
39
+ /**
40
+ * Register a new tool
41
+ */
42
+ public register(tool: ToolPlugin): void {
43
+ this.tools.set(tool.name, tool);
36
44
  }
37
45
 
38
- private initializeBuiltInTools(): void {
46
+ /**
47
+ * Initialize built-in tools. Can be called with dependencies for tools that require them.
48
+ *
49
+ * This method can be called multiple times safely. When called without dependencies,
50
+ * it registers basic tools (Bash, Read, Write, TodoWrite, etc.). When called with
51
+ * dependencies, it also registers tools that require managers (Task, Skill).
52
+ *
53
+ * @param deps Optional dependencies for advanced tools
54
+ * @param deps.subagentManager SubagentManager instance for Task tool
55
+ * @param deps.skillManager SkillManager instance for Skill tool
56
+ *
57
+ * @example
58
+ * ```typescript
59
+ * // Initialize basic tools only
60
+ * toolManager.initializeBuiltInTools();
61
+ *
62
+ * // Initialize all tools including those requiring dependencies
63
+ * toolManager.initializeBuiltInTools({
64
+ * subagentManager: mySubagentManager,
65
+ * skillManager: mySkillManager
66
+ * });
67
+ * ```
68
+ */
69
+ public initializeBuiltInTools(deps?: {
70
+ subagentManager?: SubagentManager;
71
+ skillManager?: SkillManager;
72
+ }): void {
39
73
  const builtInTools = [
40
74
  bashTool,
41
75
  bashOutputTool,
@@ -48,12 +82,23 @@ class ToolManager {
48
82
  grepTool,
49
83
  lsTool,
50
84
  readTool,
51
- new SkillManager({ logger: this.logger }).createTool(),
85
+ todoWriteTool,
52
86
  ];
53
87
 
54
88
  for (const tool of builtInTools) {
55
89
  this.tools.set(tool.name, tool);
56
90
  }
91
+
92
+ // Register tools that require dependencies
93
+ if (deps?.subagentManager) {
94
+ const taskTool = createTaskTool(deps.subagentManager);
95
+ this.tools.set(taskTool.name, taskTool);
96
+ }
97
+
98
+ if (deps?.skillManager) {
99
+ const skillTool = createSkillTool(deps.skillManager);
100
+ this.tools.set(skillTool.name, skillTool);
101
+ }
57
102
  }
58
103
 
59
104
  async execute(
@@ -6,6 +6,31 @@ import {
6
6
  ChatCompletionFunctionTool,
7
7
  } from "openai/resources.js";
8
8
  import type { GatewayConfig, ModelConfig } from "../types.js";
9
+ import * as os from "os";
10
+ import * as fs from "fs";
11
+ import * as path from "path";
12
+
13
+ /**
14
+ * Check if a directory is a git repository
15
+ * @param dirPath Directory path to check
16
+ * @returns "Yes" if it's a git repo, "No" otherwise
17
+ */
18
+ function isGitRepository(dirPath: string): string {
19
+ try {
20
+ // Check if .git directory exists in current directory or any parent directory
21
+ let currentPath = path.resolve(dirPath);
22
+ while (currentPath !== path.dirname(currentPath)) {
23
+ const gitPath = path.join(currentPath, ".git");
24
+ if (fs.existsSync(gitPath)) {
25
+ return "Yes";
26
+ }
27
+ currentPath = path.dirname(currentPath);
28
+ }
29
+ return "No";
30
+ } catch {
31
+ return "No";
32
+ }
33
+ }
9
34
 
10
35
  /**
11
36
  * OpenAI model configuration type, based on OpenAI parameters but excluding messages
@@ -89,23 +114,26 @@ export async function callAgent(
89
114
  });
90
115
 
91
116
  // Build system prompt content
92
- let systemContent: string;
93
-
94
- if (systemPrompt) {
95
- // Use custom system prompt if provided
96
- systemContent = systemPrompt;
97
- } else {
98
- // Use default system prompt
99
- systemContent = `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.
100
-
101
- ## Current Working Directory
102
- ${workdir}
117
+ let systemContent =
118
+ systemPrompt ||
119
+ `You are an interactive CLI tool that helps users with software engineering tasks. Use the instructions below and the tools available to you to assist the user.`;
120
+
121
+ // Always add environment information
122
+ systemContent += `
123
+
124
+ Here is useful information about the environment you are running in:
125
+ <env>
126
+ Working directory: ${workdir}
127
+ Is directory a git repo: ${isGitRepository(workdir)}
128
+ Platform: ${os.platform()}
129
+ OS Version: ${os.type()} ${os.release()}
130
+ Today's date: ${new Date().toISOString().split("T")[0]}
131
+ </env>
103
132
  `;
104
133
 
105
- // If there is memory content, add it to the system prompt
106
- if (memory && memory.trim()) {
107
- systemContent += `\n\n## Memory Context\n\nThe following is important context and memory from previous interactions:\n\n${memory}`;
108
- }
134
+ // If there is memory content, add it to the system prompt
135
+ if (memory && memory.trim()) {
136
+ systemContent += `\n## Memory Context\n\nThe following is important context and memory from previous interactions:\n\n${memory}`;
109
137
  }
110
138
 
111
139
  // Add system prompt
@@ -71,7 +71,7 @@ export const grepTool: ToolPlugin = {
71
71
  head_limit: {
72
72
  type: "number",
73
73
  description:
74
- 'Limit output to first N lines/entries, equivalent to "| head -N". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). When unspecified, shows all results from ripgrep.',
74
+ 'Limit output to first N lines/entries, equivalent to "| head -N". Works across all output modes: content (limits output lines), files_with_matches (limits file paths), count (limits count entries). Defaults to 100 to prevent excessive token usage.',
75
75
  },
76
76
  multiline: {
77
77
  type: "boolean",
@@ -209,12 +209,15 @@ export const grepTool: ToolPlugin = {
209
209
  };
210
210
  }
211
211
 
212
- // Apply head_limit
212
+ // Apply head_limit with default fallback
213
213
  let finalOutput = output;
214
214
  let lines = output.split("\n");
215
215
 
216
- if (headLimit && headLimit > 0 && lines.length > headLimit) {
217
- lines = lines.slice(0, headLimit);
216
+ // Set default head_limit if not specified to prevent excessive token usage
217
+ const effectiveHeadLimit = headLimit || 100;
218
+
219
+ if (lines.length > effectiveHeadLimit) {
220
+ lines = lines.slice(0, effectiveHeadLimit);
218
221
  finalOutput = lines.join("\n");
219
222
  }
220
223
 
@@ -230,8 +233,8 @@ export const grepTool: ToolPlugin = {
230
233
  shortResult = `Found ${totalLines} matching line${totalLines === 1 ? "" : "s"}`;
231
234
  }
232
235
 
233
- if (headLimit && totalLines > headLimit) {
234
- shortResult += ` (showing first ${headLimit})`;
236
+ if (effectiveHeadLimit && totalLines > effectiveHeadLimit) {
237
+ shortResult += ` (showing first ${effectiveHeadLimit})`;
235
238
  }
236
239
 
237
240
  return {
@@ -1,6 +1,10 @@
1
1
  import { readFile } from "fs/promises";
2
2
  import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
3
3
  import { resolvePath, getDisplayPath } from "../utils/path.js";
4
+ import {
5
+ isBinaryDocument,
6
+ getBinaryDocumentError,
7
+ } from "../utils/fileFormat.js";
4
8
 
5
9
  /**
6
10
  * Read Tool Plugin - Read file content
@@ -12,7 +16,7 @@ export const readTool: ToolPlugin = {
12
16
  function: {
13
17
  name: "Read",
14
18
  description:
15
- "Reads a file from the local filesystem. You can access any file directly by using this tool.\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.\n- This tool can read PDF files (.pdf). PDFs are processed page by page, extracting both text and visual content for analysis.\n- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.\n- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.\n- You will regularly be asked to read screenshots. If the user provides a path to a screenshot ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths like /var/folders/123/abc/T/TemporaryItems/NSIRD_screencaptureui_ZfB1tD/Screenshot.png\n- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.",
19
+ "Reads a file from the local filesystem. You can access any file directly by using this tool.\nAssume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.\n\nUsage:\n- The file_path parameter must be an absolute path, not a relative path\n- By default, it reads up to 2000 lines starting from the beginning of the file\n- You can optionally specify a line offset and limit (especially handy for long files), but it's recommended to read the whole file by not providing these parameters\n- Any lines longer than 2000 characters will be truncated\n- Results are returned using cat -n format, with line numbers starting at 1\n- This tool allows Claude Code to read images (eg PNG, JPG, etc). When reading an image file the contents are presented visually as Claude Code is a multimodal LLM.\n- This tool can read Jupyter notebooks (.ipynb files) and returns all cells with their outputs, combining code, text, and visualizations.\n- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.\n- You will regularly be asked to read screenshots. If the user provides a path to a screenshot ALWAYS use this tool to view the file at the path. This tool will work with all temporary file paths like /var/folders/123/abc/T/TemporaryItems/NSIRD_screencaptureui_ZfB1tD/Screenshot.png\n- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.\n- Binary document formats (PDF, DOC, DOCX, XLS, XLSX, PPT, PPTX) are not supported and will return an error.",
16
20
  parameters: {
17
21
  type: "object",
18
22
  properties: {
@@ -51,6 +55,15 @@ export const readTool: ToolPlugin = {
51
55
  };
52
56
  }
53
57
 
58
+ // Check for binary document formats
59
+ if (isBinaryDocument(filePath)) {
60
+ return {
61
+ success: false,
62
+ content: "",
63
+ error: getBinaryDocumentError(filePath),
64
+ };
65
+ }
66
+
54
67
  try {
55
68
  // Note: New Read tool requires absolute paths, so we don't use resolvePath
56
69
  // But for compatibility, if it's not an absolute path, we still try to resolve
@@ -71,8 +84,19 @@ export const readTool: ToolPlugin = {
71
84
  };
72
85
  }
73
86
 
74
- const lines = fileContent.split("\n");
87
+ // Check content size limit (100KB)
88
+ const MAX_CONTENT_SIZE = 100 * 1024; // 100KB
89
+ let contentToProcess = fileContent;
90
+ let contentTruncated = false;
91
+
92
+ if (fileContent.length > MAX_CONTENT_SIZE) {
93
+ contentToProcess = fileContent.substring(0, MAX_CONTENT_SIZE);
94
+ contentTruncated = true;
95
+ }
96
+
97
+ const lines = contentToProcess.split("\n");
75
98
  const totalLines = lines.length;
99
+ const originalTotalLines = fileContent.split("\n").length;
76
100
 
77
101
  // Handle offset and limit
78
102
  let startLine = 1;
@@ -117,7 +141,10 @@ export const readTool: ToolPlugin = {
117
141
 
118
142
  // Add file information header
119
143
  let content = `File: ${filePath}\n`;
120
- if (startLine > 1 || endLine < totalLines) {
144
+ if (contentTruncated) {
145
+ content += `Content truncated at ${MAX_CONTENT_SIZE} bytes\n`;
146
+ content += `Lines ${startLine}-${endLine} of ${totalLines} (original file: ${originalTotalLines} lines)\n`;
147
+ } else if (startLine > 1 || endLine < totalLines) {
121
148
  content += `Lines ${startLine}-${endLine} of ${totalLines}\n`;
122
149
  } else {
123
150
  content += `Total lines: ${totalLines}\n`;
@@ -126,15 +153,22 @@ export const readTool: ToolPlugin = {
126
153
  content += formattedContent;
127
154
 
128
155
  // If only showing partial content, add prompt
129
- if (endLine < totalLines) {
156
+ if (endLine < totalLines || contentTruncated) {
130
157
  content += `\n${"─".repeat(50)}\n`;
131
- content += `... ${totalLines - endLine} more lines not shown`;
158
+ if (contentTruncated) {
159
+ content += `... content truncated due to size limit (${MAX_CONTENT_SIZE} bytes)`;
160
+ if (endLine < totalLines) {
161
+ content += ` and ${totalLines - endLine} more lines not shown`;
162
+ }
163
+ } else {
164
+ content += `... ${totalLines - endLine} more lines not shown`;
165
+ }
132
166
  }
133
167
 
134
168
  return {
135
169
  success: true,
136
170
  content,
137
- shortResult: `Read ${selectedLines.length} lines${totalLines > 2000 ? " (truncated)" : ""}`,
171
+ shortResult: `Read ${selectedLines.length} lines${totalLines > 2000 || contentTruncated ? " (truncated)" : ""}`,
138
172
  };
139
173
  } catch (error) {
140
174
  return {