wave-agent-sdk 0.11.6 → 0.11.7

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 (105) hide show
  1. package/builtin/skills/init/SKILL.md +2 -0
  2. package/builtin/skills/settings/SKILLS.md +3 -2
  3. package/builtin/skills/settings/SUBAGENTS.md +1 -3
  4. package/dist/agent.d.ts +6 -0
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +18 -1
  7. package/dist/constants/tools.d.ts +1 -1
  8. package/dist/constants/tools.d.ts.map +1 -1
  9. package/dist/constants/tools.js +1 -1
  10. package/dist/managers/MemoryRuleManager.d.ts.map +1 -1
  11. package/dist/managers/MemoryRuleManager.js +1 -9
  12. package/dist/managers/aiManager.d.ts.map +1 -1
  13. package/dist/managers/aiManager.js +22 -3
  14. package/dist/managers/messageManager.d.ts +13 -5
  15. package/dist/managers/messageManager.d.ts.map +1 -1
  16. package/dist/managers/messageManager.js +62 -34
  17. package/dist/managers/pluginManager.d.ts.map +1 -1
  18. package/dist/managers/pluginManager.js +4 -2
  19. package/dist/managers/slashCommandManager.d.ts +2 -0
  20. package/dist/managers/slashCommandManager.d.ts.map +1 -1
  21. package/dist/managers/slashCommandManager.js +98 -4
  22. package/dist/managers/toolManager.d.ts.map +1 -1
  23. package/dist/managers/toolManager.js +8 -2
  24. package/dist/prompts/index.d.ts +2 -0
  25. package/dist/prompts/index.d.ts.map +1 -1
  26. package/dist/prompts/index.js +5 -0
  27. package/dist/services/GitService.d.ts +1 -0
  28. package/dist/services/GitService.d.ts.map +1 -1
  29. package/dist/services/GitService.js +16 -0
  30. package/dist/services/MarketplaceService.d.ts +7 -0
  31. package/dist/services/MarketplaceService.d.ts.map +1 -1
  32. package/dist/services/MarketplaceService.js +321 -252
  33. package/dist/services/aiService.d.ts +34 -0
  34. package/dist/services/aiService.d.ts.map +1 -1
  35. package/dist/services/aiService.js +124 -1
  36. package/dist/services/initializationService.d.ts.map +1 -1
  37. package/dist/services/initializationService.js +18 -0
  38. package/dist/tools/agentTool.js +3 -3
  39. package/dist/tools/bashTool.d.ts.map +1 -1
  40. package/dist/tools/bashTool.js +4 -4
  41. package/dist/tools/editTool.d.ts.map +1 -1
  42. package/dist/tools/editTool.js +2 -0
  43. package/dist/tools/globTool.d.ts.map +1 -1
  44. package/dist/tools/globTool.js +15 -3
  45. package/dist/tools/grepTool.d.ts.map +1 -1
  46. package/dist/tools/grepTool.js +38 -12
  47. package/dist/tools/readTool.d.ts.map +1 -1
  48. package/dist/tools/readTool.js +61 -0
  49. package/dist/tools/skillTool.js +2 -2
  50. package/dist/tools/types.d.ts +16 -0
  51. package/dist/tools/types.d.ts.map +1 -1
  52. package/dist/tools/webFetchTool.d.ts +3 -0
  53. package/dist/tools/webFetchTool.d.ts.map +1 -0
  54. package/dist/tools/webFetchTool.js +171 -0
  55. package/dist/tools/writeTool.d.ts.map +1 -1
  56. package/dist/tools/writeTool.js +2 -0
  57. package/dist/types/commands.d.ts +1 -1
  58. package/dist/types/commands.d.ts.map +1 -1
  59. package/dist/types/messaging.d.ts +1 -0
  60. package/dist/types/messaging.d.ts.map +1 -1
  61. package/dist/utils/bashParser.d.ts +14 -0
  62. package/dist/utils/bashParser.d.ts.map +1 -1
  63. package/dist/utils/bashParser.js +243 -142
  64. package/dist/utils/convertMessagesForAPI.d.ts.map +1 -1
  65. package/dist/utils/convertMessagesForAPI.js +7 -0
  66. package/dist/utils/fileUtils.d.ts +8 -0
  67. package/dist/utils/fileUtils.d.ts.map +1 -1
  68. package/dist/utils/fileUtils.js +52 -0
  69. package/dist/utils/messageOperations.d.ts +12 -3
  70. package/dist/utils/messageOperations.d.ts.map +1 -1
  71. package/dist/utils/messageOperations.js +77 -9
  72. package/package.json +4 -2
  73. package/src/agent.ts +19 -1
  74. package/src/constants/tools.ts +1 -1
  75. package/src/managers/MemoryRuleManager.ts +1 -10
  76. package/src/managers/aiManager.ts +23 -3
  77. package/src/managers/messageManager.ts +76 -38
  78. package/src/managers/pluginManager.ts +4 -2
  79. package/src/managers/slashCommandManager.ts +130 -4
  80. package/src/managers/toolManager.ts +11 -2
  81. package/src/prompts/index.ts +6 -0
  82. package/src/services/GitService.ts +20 -0
  83. package/src/services/MarketplaceService.ts +397 -324
  84. package/src/services/aiService.ts +197 -1
  85. package/src/services/initializationService.ts +38 -0
  86. package/src/tools/agentTool.ts +3 -3
  87. package/src/tools/bashTool.ts +3 -4
  88. package/src/tools/editTool.ts +3 -0
  89. package/src/tools/globTool.ts +16 -3
  90. package/src/tools/grepTool.ts +41 -13
  91. package/src/tools/readTool.ts +69 -0
  92. package/src/tools/skillTool.ts +2 -2
  93. package/src/tools/types.ts +13 -0
  94. package/src/tools/webFetchTool.ts +194 -0
  95. package/src/tools/writeTool.ts +3 -0
  96. package/src/types/commands.ts +1 -1
  97. package/src/types/messaging.ts +1 -0
  98. package/src/utils/bashParser.ts +268 -157
  99. package/src/utils/convertMessagesForAPI.ts +8 -0
  100. package/src/utils/fileUtils.ts +69 -0
  101. package/src/utils/messageOperations.ts +84 -9
  102. package/dist/tools/taskOutputTool.d.ts +0 -3
  103. package/dist/tools/taskOutputTool.d.ts.map +0 -1
  104. package/dist/tools/taskOutputTool.js +0 -198
  105. package/src/tools/taskOutputTool.ts +0 -222
@@ -22,7 +22,11 @@ import * as os from "os";
22
22
  import * as fs from "fs";
23
23
  import * as path from "path";
24
24
 
25
- import { COMPRESS_MESSAGES_SYSTEM_PROMPT } from "../prompts/index.js";
25
+ import {
26
+ COMPRESS_MESSAGES_SYSTEM_PROMPT,
27
+ WEB_CONTENT_SYSTEM_PROMPT,
28
+ BTW_SYSTEM_PROMPT,
29
+ } from "../prompts/index.js";
26
30
 
27
31
  /**
28
32
  * Interface for debug data saved during 400 errors
@@ -829,3 +833,195 @@ export async function compressMessages(
829
833
  throw error;
830
834
  }
831
835
  }
836
+
837
+ export interface ProcessWebContentOptions {
838
+ // Resolved configuration
839
+ gatewayConfig: GatewayConfig;
840
+ modelConfig: ModelConfig;
841
+
842
+ // Parameters
843
+ content: string;
844
+ prompt: string;
845
+ abortSignal?: AbortSignal;
846
+ model?: string;
847
+ }
848
+
849
+ export interface ProcessWebContentResult {
850
+ content: string;
851
+ usage?: {
852
+ prompt_tokens: number;
853
+ completion_tokens: number;
854
+ total_tokens: number;
855
+ };
856
+ }
857
+
858
+ export async function processWebContent(
859
+ options: ProcessWebContentOptions,
860
+ ): Promise<ProcessWebContentResult> {
861
+ const { gatewayConfig, modelConfig, content, prompt, abortSignal } = options;
862
+
863
+ // Apply global 1 QPS rate limit
864
+ if (
865
+ process.env.NODE_ENV !== "test" ||
866
+ modelConfig.model === "rate-limit-test"
867
+ ) {
868
+ await acquireSlot(abortSignal);
869
+ }
870
+
871
+ // Create OpenAI client with injected configuration
872
+ const openai = new OpenAIClient({
873
+ apiKey: gatewayConfig.apiKey,
874
+ baseURL: gatewayConfig.baseURL,
875
+ defaultHeaders: gatewayConfig.defaultHeaders,
876
+ fetchOptions: gatewayConfig.fetchOptions,
877
+ fetch: gatewayConfig.fetch,
878
+ });
879
+
880
+ // Get model configuration - use injected agent model
881
+ const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
882
+ temperature: 0.1,
883
+ max_tokens: 4096,
884
+ });
885
+
886
+ try {
887
+ const response = await openai.chat.completions.create(
888
+ {
889
+ ...openaiModelConfig,
890
+ messages: [
891
+ {
892
+ role: "system",
893
+ content: WEB_CONTENT_SYSTEM_PROMPT,
894
+ },
895
+ {
896
+ role: "user",
897
+ content: `Web Content:\n\n${content}\n\nUser Prompt: ${prompt}`,
898
+ },
899
+ ],
900
+ },
901
+ {
902
+ signal: abortSignal,
903
+ },
904
+ );
905
+
906
+ const result = response.choices[0]?.message?.content?.trim();
907
+ if (!result) {
908
+ throw new Error("Failed to process web content: Empty response from AI");
909
+ }
910
+ const usage = response.usage
911
+ ? {
912
+ prompt_tokens: response.usage.prompt_tokens,
913
+ completion_tokens: response.usage.completion_tokens,
914
+ total_tokens: response.usage.total_tokens,
915
+ }
916
+ : undefined;
917
+
918
+ return {
919
+ content: result,
920
+ usage,
921
+ };
922
+ } catch (error) {
923
+ if ((error as Error).name === "AbortError") {
924
+ logger.info("Web content processing request was aborted");
925
+ throw new Error("Web content processing request was aborted");
926
+ }
927
+ logger.error("Failed to process web content:", error);
928
+ throw error;
929
+ }
930
+ }
931
+
932
+ export interface BtwOptions {
933
+ // Resolved configuration
934
+ gatewayConfig: GatewayConfig;
935
+ modelConfig: ModelConfig;
936
+
937
+ // Parameters
938
+ messages: ChatCompletionMessageParam[];
939
+ question: string;
940
+ abortSignal?: AbortSignal;
941
+ model?: string;
942
+ }
943
+
944
+ export interface BtwResult {
945
+ content: string;
946
+ usage?: {
947
+ prompt_tokens: number;
948
+ completion_tokens: number;
949
+ total_tokens: number;
950
+ };
951
+ }
952
+
953
+ export async function btw(options: BtwOptions): Promise<BtwResult> {
954
+ const { gatewayConfig, modelConfig, messages, question, abortSignal } =
955
+ options;
956
+
957
+ // Apply global 1 QPS rate limit
958
+ if (
959
+ process.env.NODE_ENV !== "test" ||
960
+ modelConfig.model === "rate-limit-test"
961
+ ) {
962
+ await acquireSlot(abortSignal);
963
+ }
964
+
965
+ // Create OpenAI client with injected configuration
966
+ const openai = new OpenAIClient({
967
+ apiKey: gatewayConfig.apiKey,
968
+ baseURL: gatewayConfig.baseURL,
969
+ defaultHeaders: gatewayConfig.defaultHeaders,
970
+ fetchOptions: gatewayConfig.fetchOptions,
971
+ fetch: gatewayConfig.fetch,
972
+ });
973
+
974
+ // Get model configuration - use injected agent model
975
+ const openaiModelConfig = getModelConfig(options.model || modelConfig.model, {
976
+ temperature: 0.1,
977
+ max_tokens: 4096,
978
+ });
979
+
980
+ try {
981
+ const response = await openai.chat.completions.create(
982
+ {
983
+ ...openaiModelConfig,
984
+ messages: [
985
+ {
986
+ role: "system",
987
+ content: BTW_SYSTEM_PROMPT,
988
+ },
989
+ ...messages,
990
+ {
991
+ role: "user",
992
+ content: question,
993
+ },
994
+ ],
995
+ },
996
+ {
997
+ signal: abortSignal,
998
+ },
999
+ );
1000
+
1001
+ const result = response.choices[0]?.message?.content?.trim();
1002
+ if (!result) {
1003
+ throw new Error(
1004
+ "Failed to process side question: Empty response from AI",
1005
+ );
1006
+ }
1007
+ const usage = response.usage
1008
+ ? {
1009
+ prompt_tokens: response.usage.prompt_tokens,
1010
+ completion_tokens: response.usage.completion_tokens,
1011
+ total_tokens: response.usage.total_tokens,
1012
+ }
1013
+ : undefined;
1014
+
1015
+ return {
1016
+ content: result,
1017
+ usage,
1018
+ };
1019
+ } catch (error) {
1020
+ if ((error as Error).name === "AbortError") {
1021
+ logger.info("Side question request was aborted");
1022
+ throw new Error("Side question request was aborted");
1023
+ }
1024
+ logger.error("Failed to process side question:", error);
1025
+ throw error;
1026
+ }
1027
+ }
@@ -78,8 +78,11 @@ export class InitializationService {
78
78
  resolveAndValidateConfig,
79
79
  } = context;
80
80
 
81
+ const startTime = performance.now();
82
+
81
83
  // Initialize managers first
82
84
  try {
85
+ const phaseStart = performance.now();
83
86
  // Initialize SkillManager
84
87
  await skillManager.initialize();
85
88
 
@@ -100,6 +103,9 @@ export class InitializationService {
100
103
  slashCommandManager.registerSkillCommands(
101
104
  skillManager.getAvailableSkills(),
102
105
  );
106
+ logger?.debug(
107
+ `Initialization Phase [Managers and Tools] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
108
+ );
103
109
  } catch (error) {
104
110
  logger?.error("Failed to initialize managers and tools:", error);
105
111
  // Don't throw error to prevent app startup failure
@@ -107,10 +113,14 @@ export class InitializationService {
107
113
 
108
114
  // Initialize MCP servers with auto-connect
109
115
  try {
116
+ const phaseStart = performance.now();
110
117
  await mcpManager.initialize(workdir, true);
111
118
  if (lspManager instanceof LspManager) {
112
119
  await lspManager.initialize(workdir);
113
120
  }
121
+ logger?.debug(
122
+ `Initialization Phase [MCP and LSP] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
123
+ );
114
124
  } catch (error) {
115
125
  logger?.error("Failed to initialize MCP servers:", error);
116
126
  // Don't throw error to prevent app startup failure
@@ -118,6 +128,7 @@ export class InitializationService {
118
128
 
119
129
  // Initialize hooks configuration
120
130
  try {
131
+ const phaseStart = performance.now();
121
132
  // Load hooks configuration using ConfigurationService
122
133
  const configResult =
123
134
  await configurationService.loadMergedConfiguration(workdir);
@@ -158,6 +169,9 @@ export class InitializationService {
158
169
  }
159
170
  }
160
171
  }
172
+ logger?.debug(
173
+ `Initialization Phase [Hooks Configuration] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
174
+ );
161
175
  } catch (error) {
162
176
  logger?.error("Failed to initialize hooks system:", error);
163
177
  // Don't throw error to prevent app startup failure
@@ -196,6 +210,7 @@ export class InitializationService {
196
210
 
197
211
  // Initialize auto-memory directory
198
212
  try {
213
+ const phaseStart = performance.now();
199
214
  if (configurationService.resolveAutoMemoryEnabled()) {
200
215
  const memoryService =
201
216
  container.get<import("./memory.js").MemoryService>("MemoryService");
@@ -209,6 +224,9 @@ export class InitializationService {
209
224
  }
210
225
  }
211
226
  }
227
+ logger?.debug(
228
+ `Initialization Phase [Auto-memory Initialization] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
229
+ );
212
230
  } catch (error) {
213
231
  logger?.error("Failed to initialize auto-memory directory:", error);
214
232
  }
@@ -218,14 +236,22 @@ export class InitializationService {
218
236
 
219
237
  // Discover modular memory rules
220
238
  try {
239
+ const phaseStart = performance.now();
221
240
  await memoryRuleManager.discoverRules();
241
+ logger?.debug(
242
+ `Initialization Phase [Memory Rules Discovery] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
243
+ );
222
244
  } catch (error) {
223
245
  logger?.error("Failed to discover memory rules:", error);
224
246
  }
225
247
 
226
248
  // Initialize live configuration reload
227
249
  try {
250
+ const phaseStart = performance.now();
228
251
  await liveConfigManager.initialize();
252
+ logger?.debug(
253
+ `Initialization Phase [Live Config Initialization] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
254
+ );
229
255
  } catch (error) {
230
256
  logger?.error("Failed to initialize live configuration reload:", error);
231
257
  // Don't throw error to prevent app startup failure - continue without live reload
@@ -233,6 +259,7 @@ export class InitializationService {
233
259
 
234
260
  // Load memory files during initialization
235
261
  try {
262
+ const phaseStart = performance.now();
236
263
  const memoryService = container.get<MemoryService>("MemoryService");
237
264
  if (!memoryService) {
238
265
  throw new Error("MemoryService not found in container");
@@ -256,6 +283,9 @@ export class InitializationService {
256
283
  logger?.warn("Failed to load user memory file:", error);
257
284
  setUserMemory("");
258
285
  }
286
+ logger?.debug(
287
+ `Initialization Phase [Memory Files Loading] took ${(performance.now() - phaseStart).toFixed(2)}ms`,
288
+ );
259
289
  } catch (error) {
260
290
  // Ensure memory is always initialized even if loading fails
261
291
  setProjectMemory("");
@@ -265,6 +295,7 @@ export class InitializationService {
265
295
  }
266
296
 
267
297
  // Handle session restoration or set provided messages
298
+ const sessionPhaseStart = performance.now();
268
299
  if (options?.messages) {
269
300
  // If messages are provided, use them directly (useful for testing)
270
301
  messageManager.setMessages(options.messages);
@@ -293,5 +324,12 @@ export class InitializationService {
293
324
  agentOptions.callbacks?.onTasksChange?.(tasks);
294
325
  }
295
326
  }
327
+ logger?.debug(
328
+ `Initialization Phase [Session Restoration] took ${(performance.now() - sessionPhaseStart).toFixed(2)}ms`,
329
+ );
330
+
331
+ logger?.debug(
332
+ `Total Initialization took ${(performance.now() - startTime).toFixed(2)}ms`,
333
+ );
296
334
  }
297
335
  }
@@ -36,7 +36,7 @@ export const agentTool: ToolPlugin = {
36
36
  run_in_background: {
37
37
  type: "boolean",
38
38
  description:
39
- "Set to true to run this command in the background. Use TaskOutput to read the output later.",
39
+ "Set to true to run this command in the background. Use Read to read the output later.",
40
40
  },
41
41
  },
42
42
  required: ["description", "prompt", "subagent_type"],
@@ -154,7 +154,7 @@ When using the Agent tool, you must specify a subagent_type parameter to select
154
154
  if (run_in_background || isBackgrounded) return;
155
155
 
156
156
  const messages = instance.messageManager.getMessages();
157
- const tokens = instance.messageManager.getlatestTotalTokens();
157
+ const tokens = instance.messageManager.getLatestTotalTokens();
158
158
  const lastTools = instance.lastTools;
159
159
 
160
160
  const toolCount = countToolBlocks(messages);
@@ -224,7 +224,7 @@ When using the Agent tool, you must specify a subagent_type parameter to select
224
224
  subagentManager.cleanupInstance(instance.subagentId);
225
225
 
226
226
  const messages = instance.messageManager.getMessages();
227
- const tokens = instance.messageManager.getlatestTotalTokens();
227
+ const tokens = instance.messageManager.getLatestTotalTokens();
228
228
  const toolCount = countToolBlocks(messages);
229
229
  const summary = formatToolTokenSummary(toolCount, tokens);
230
230
 
@@ -7,7 +7,6 @@ import { stripAnsiColors } from "../utils/stringUtils.js";
7
7
  import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
8
8
  import {
9
9
  BASH_TOOL_NAME,
10
- TASK_OUTPUT_TOOL_NAME,
11
10
  GLOB_TOOL_NAME,
12
11
  GREP_TOOL_NAME,
13
12
  READ_TOOL_NAME,
@@ -87,7 +86,7 @@ export const bashTool: ToolPlugin = {
87
86
  },
88
87
  run_in_background: {
89
88
  type: "boolean",
90
- description: `Set to true to run this command in the background. Use ${TASK_OUTPUT_TOOL_NAME} to read the output later.`,
89
+ description: `Set to true to run this command in the background. Use ${READ_TOOL_NAME} to read the output later.`,
91
90
  },
92
91
  },
93
92
  required: ["command"],
@@ -120,7 +119,7 @@ Usage notes:
120
119
  - You can specify an optional timeout in milliseconds (up to ${BASH_DEFAULT_TIMEOUT_MS}ms / ${BASH_DEFAULT_TIMEOUT_MS / 60000} minutes). If not specified, commands will timeout after ${BASH_DEFAULT_TIMEOUT_MS}ms (${BASH_DEFAULT_TIMEOUT_MS / 60000} minutes).
121
120
  - It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
122
121
  - If the output exceeds ${MAX_OUTPUT_LENGTH} characters, output will be truncated and the full output will be persisted to a temporary file.
123
- - You can use the \`run_in_background\` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the ${BASH_TOOL_NAME} tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.
122
+ - You can use the \`run_in_background\` parameter to run the command in the background, which allows you to continue working while the command runs. You can monitor the output using the ${READ_TOOL_NAME} tool as it becomes available. You do not need to use '&' at the end of the command when using this parameter.
124
123
  - Avoid using ${BASH_TOOL_NAME} with the \`find\`, \`sed\`, \`awk\`, or \`echo\` commands, unless explicitly instructed or when these commands are truly necessary for the task. Instead, always prefer using the dedicated tools for these commands:
125
124
  - File search: Use ${GLOB_TOOL_NAME} (NOT find or ls)
126
125
  - Content search: Use ${GREP_TOOL_NAME}
@@ -226,7 +225,7 @@ Usage notes:
226
225
  const outputPath = task?.outputPath;
227
226
  return {
228
227
  success: true,
229
- content: `Command started in background with ID: ${taskId}.${outputPath ? ` Real-time output: ${outputPath}` : ` Use TaskOutput tool with task_id="${taskId}" to monitor output.`}`,
228
+ content: `Command started in background with ID: ${taskId}.${outputPath ? ` Real-time output: ${outputPath}` : ` Use ${READ_TOOL_NAME} tool with task_id="${taskId}" to monitor output.`}`,
230
229
  shortResult: `Background process ${taskId} started`,
231
230
  };
232
231
  }
@@ -106,6 +106,9 @@ Usage:
106
106
  };
107
107
  }
108
108
 
109
+ // Touch file to track it in context
110
+ context.messageManager?.touchFile(filePath);
111
+
109
112
  try {
110
113
  const resolvedPath = resolvePath(filePath, context.workdir);
111
114
 
@@ -32,6 +32,10 @@ export const globTool: ToolPlugin = {
32
32
  description:
33
33
  'The directory to search in. If not specified, the current working directory will be used. IMPORTANT: Omit this field to use the default directory. DO NOT enter "undefined" or "null" - simply omit it for the default behavior. Must be a valid directory path if provided.',
34
34
  },
35
+ limit: {
36
+ type: "number",
37
+ description: "Maximum number of files to return. Defaults to 100.",
38
+ },
35
39
  },
36
40
  required: ["pattern"],
37
41
  },
@@ -50,6 +54,8 @@ export const globTool: ToolPlugin = {
50
54
  ): Promise<ToolResult> => {
51
55
  const pattern = args.pattern as string;
52
56
  const searchPath = args.path as string;
57
+ const limit = (args.limit as number) || MAX_GLOB_RESULTS;
58
+ const startTime = Date.now();
53
59
 
54
60
  if (!pattern || typeof pattern !== "string") {
55
61
  return {
@@ -113,22 +119,29 @@ export const globTool: ToolPlugin = {
113
119
  .map((item) => item.path);
114
120
 
115
121
  const totalCount = sortedFiles.length;
116
- const finalFiles = sortedFiles.slice(0, MAX_GLOB_RESULTS);
122
+ const finalFiles = sortedFiles.slice(0, limit);
117
123
 
118
124
  // Format output
119
125
  const output = finalFiles
120
126
  .map((file, index) => `${index + 1}. ${file}`)
121
127
  .join("\n");
122
128
 
123
- const isTruncated = totalCount > MAX_GLOB_RESULTS;
129
+ const isTruncated = totalCount > limit;
124
130
  const shortResult = isTruncated
125
- ? `Found ${totalCount} files (showing first ${MAX_GLOB_RESULTS})`
131
+ ? `Found ${totalCount} files (showing first ${limit})`
126
132
  : `Found ${totalCount} file${totalCount === 1 ? "" : "s"}`;
127
133
 
134
+ const durationMs = Date.now() - startTime;
135
+
128
136
  return {
129
137
  success: true,
130
138
  content: output,
131
139
  shortResult,
140
+ metadata: {
141
+ durationMs,
142
+ numFiles: totalCount,
143
+ truncated: isTruncated,
144
+ },
132
145
  };
133
146
  } catch (error) {
134
147
  return {
@@ -81,6 +81,15 @@ export const grepTool: ToolPlugin = {
81
81
  description:
82
82
  "Enable multiline mode where . matches newlines and patterns can span lines (rg -U --multiline-dotall). Default: false.",
83
83
  },
84
+ offset: {
85
+ type: "number",
86
+ description: "The number of matches to skip.",
87
+ },
88
+ context: {
89
+ type: "number",
90
+ description:
91
+ 'Alias for -C. Number of lines to show before and after each match. Requires output_mode: "content", ignored otherwise.',
92
+ },
84
93
  },
85
94
  required: ["pattern"],
86
95
  },
@@ -113,6 +122,8 @@ export const grepTool: ToolPlugin = {
113
122
  const fileType = args.type as string;
114
123
  const headLimit = args.head_limit as number;
115
124
  const multiline = args.multiline as boolean;
125
+ const offset = args.offset as number;
126
+ const contextArg = args.context as number;
116
127
 
117
128
  if (!pattern || typeof pattern !== "string") {
118
129
  return {
@@ -165,8 +176,9 @@ export const grepTool: ToolPlugin = {
165
176
 
166
177
  // Context lines (only effective in content mode)
167
178
  if (outputMode === "content") {
168
- if (contextAround) {
169
- rgArgs.push("-C", contextAround.toString());
179
+ const effectiveContext = contextArg ?? contextAround;
180
+ if (effectiveContext) {
181
+ rgArgs.push("-C", effectiveContext.toString());
170
182
  } else {
171
183
  if (contextBefore) {
172
184
  rgArgs.push("-B", contextBefore.toString());
@@ -215,41 +227,57 @@ export const grepTool: ToolPlugin = {
215
227
  content:
216
228
  "No matches found. Suggestion: specify the 'path' field to search in ignored or other directories (e.g., 'node_modules'), as the default search path is the current working directory and respects .gitignore.",
217
229
  shortResult: "No matches found",
230
+ metadata: {
231
+ numMatches: 0,
232
+ },
218
233
  };
219
234
  }
220
235
 
221
- // Apply head_limit with default fallback
222
- let finalOutput = output;
223
236
  let lines = output.split("\n");
237
+ const totalMatches = lines.length;
224
238
 
225
- // Set default head_limit if not specified to prevent excessive token usage
226
- const effectiveHeadLimit = headLimit || 0;
239
+ // Apply offset
240
+ const effectiveOffset = offset || 0;
241
+ if (effectiveOffset > 0) {
242
+ lines = lines.slice(effectiveOffset);
243
+ }
227
244
 
245
+ // Apply head_limit
246
+ const effectiveHeadLimit = headLimit || 0;
247
+ let truncated = false;
228
248
  if (effectiveHeadLimit > 0 && lines.length > effectiveHeadLimit) {
229
249
  lines = lines.slice(0, effectiveHeadLimit);
230
- finalOutput = lines.join("\n");
250
+ truncated = true;
231
251
  }
232
252
 
253
+ const finalOutput = lines.join("\n");
254
+
233
255
  // Generate short result
234
256
  let shortResult: string;
235
- const totalLines = output.split("\n").length;
257
+ const numMatches = lines.length;
236
258
 
237
259
  if (outputMode === "files_with_matches") {
238
- shortResult = `Found ${totalLines} file${totalLines === 1 ? "" : "s"}`;
260
+ shortResult = `Found ${numMatches} file${numMatches === 1 ? "" : "s"}`;
239
261
  } else if (outputMode === "count") {
240
- shortResult = `Match counts for ${totalLines} file${totalLines === 1 ? "" : "s"}`;
262
+ shortResult = `Match counts for ${numMatches} file${numMatches === 1 ? "" : "s"}`;
241
263
  } else {
242
- shortResult = `Found ${totalLines} matching line${totalLines === 1 ? "" : "s"}`;
264
+ shortResult = `Found ${numMatches} matching line${numMatches === 1 ? "" : "s"}`;
243
265
  }
244
266
 
245
- if (effectiveHeadLimit && totalLines > effectiveHeadLimit) {
246
- shortResult += ` (showing first ${effectiveHeadLimit})`;
267
+ if (effectiveOffset > 0 || truncated) {
268
+ shortResult += ` (showing ${numMatches} of ${totalMatches})`;
247
269
  }
248
270
 
249
271
  return {
250
272
  success: true,
251
273
  content: finalOutput,
252
274
  shortResult,
275
+ metadata: {
276
+ numMatches: totalMatches,
277
+ truncated,
278
+ appliedLimit: effectiveHeadLimit,
279
+ appliedOffset: effectiveOffset,
280
+ },
253
281
  };
254
282
  } catch (error) {
255
283
  return {
@@ -1,5 +1,6 @@
1
1
  import { readFile, stat } from "fs/promises";
2
2
  import { extname } from "path";
3
+ import { createHash } from "crypto";
3
4
  import { logger } from "../utils/globalLogger.js";
4
5
  import type { ToolPlugin, ToolResult, ToolContext } from "./types.js";
5
6
  import { resolvePath, getDisplayPath } from "../utils/path.js";
@@ -117,6 +118,10 @@ async function processImageFile(
117
118
  mediaType: mimeType,
118
119
  },
119
120
  ],
121
+ metadata: {
122
+ type: "image",
123
+ mimeType,
124
+ },
120
125
  };
121
126
  } catch (error) {
122
127
  return {
@@ -190,12 +195,19 @@ Usage:
190
195
  };
191
196
  }
192
197
 
198
+ // Touch file to track it in context
199
+ context.messageManager?.touchFile(filePath);
200
+
193
201
  // Check for binary document formats
194
202
  if (isBinaryDocument(filePath)) {
203
+ const isPdf = filePath.toLowerCase().endsWith(".pdf");
195
204
  return {
196
205
  success: false,
197
206
  content: "",
198
207
  error: getBinaryDocumentError(filePath),
208
+ metadata: {
209
+ type: isPdf ? "pdf" : "binary",
210
+ },
199
211
  };
200
212
  }
201
213
 
@@ -230,8 +242,54 @@ Usage:
230
242
  ? filePath
231
243
  : resolvePath(filePath, context.workdir);
232
244
 
245
+ const stats = await stat(actualFilePath);
246
+
247
+ // Deduplication
248
+ if (context.readFileState) {
249
+ const state = context.readFileState.get(actualFilePath);
250
+ if (state && state.mtime === stats.mtime.getTime()) {
251
+ return {
252
+ success: true,
253
+ content: `File ${filePath} has not changed since last read.`,
254
+ shortResult: "File unchanged",
255
+ metadata: {
256
+ type: "file_unchanged",
257
+ },
258
+ };
259
+ }
260
+ }
261
+
262
+ // Resource Limits
263
+ const maxSizeBytes =
264
+ context.fileReadingLimits?.maxSizeBytes ?? 1024 * 1024; // Default 1MB
265
+ if (
266
+ stats.size > maxSizeBytes &&
267
+ typeof offset !== "number" &&
268
+ typeof limit !== "number"
269
+ ) {
270
+ return {
271
+ success: false,
272
+ content: "",
273
+ error: `File size (${(stats.size / 1024).toFixed(2)}KB) exceeds limit (${(maxSizeBytes / 1024).toFixed(2)}KB). Please use offset and limit to read a portion of the file.`,
274
+ metadata: {
275
+ type: "error_limit_exceeded",
276
+ size: stats.size,
277
+ limit: maxSizeBytes,
278
+ },
279
+ };
280
+ }
281
+
233
282
  const fileContent = await readFile(actualFilePath, "utf-8");
234
283
 
284
+ // Update readFileState
285
+ if (context.readFileState) {
286
+ const hash = createHash("sha256").update(fileContent).digest("hex");
287
+ context.readFileState.set(actualFilePath, {
288
+ mtime: stats.mtime.getTime(),
289
+ hash,
290
+ });
291
+ }
292
+
235
293
  // Check if file is empty
236
294
  if (fileContent.length === 0) {
237
295
  logger.warn(`File ${filePath} exists but has empty contents`);
@@ -240,6 +298,10 @@ Usage:
240
298
  content:
241
299
  "⚠️ System reminder: This file exists but has empty contents.",
242
300
  shortResult: "Empty file",
301
+ metadata: {
302
+ type: "text",
303
+ isEmpty: true,
304
+ },
243
305
  };
244
306
  }
245
307
 
@@ -306,6 +368,13 @@ Usage:
306
368
  success: true,
307
369
  content,
308
370
  shortResult: `Read ${selectedLines.length} lines${totalLines > 2000 ? " (truncated)" : ""}`,
371
+ metadata: {
372
+ type: "text",
373
+ totalLines,
374
+ startLine,
375
+ endLine,
376
+ truncated: endLine < totalLines,
377
+ },
309
378
  };
310
379
  } catch (error) {
311
380
  return {