lynkr 7.2.5 → 8.0.1

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 (124) hide show
  1. package/README.md +3 -3
  2. package/config/model-tiers.json +89 -0
  3. package/install.sh +6 -1
  4. package/package.json +4 -2
  5. package/scripts/setup.js +0 -1
  6. package/src/agents/executor.js +14 -6
  7. package/src/api/middleware/session.js +15 -2
  8. package/src/api/openai-router.js +162 -37
  9. package/src/api/providers-handler.js +15 -1
  10. package/src/api/router.js +107 -2
  11. package/src/budget/index.js +4 -3
  12. package/src/clients/databricks.js +431 -234
  13. package/src/clients/gpt-utils.js +181 -0
  14. package/src/clients/ollama-utils.js +66 -140
  15. package/src/clients/routing.js +0 -1
  16. package/src/clients/standard-tools.js +99 -3
  17. package/src/config/index.js +133 -35
  18. package/src/context/toon.js +173 -0
  19. package/src/logger/index.js +23 -0
  20. package/src/orchestrator/index.js +688 -213
  21. package/src/routing/agentic-detector.js +320 -0
  22. package/src/routing/complexity-analyzer.js +202 -2
  23. package/src/routing/cost-optimizer.js +305 -0
  24. package/src/routing/index.js +168 -159
  25. package/src/routing/model-tiers.js +365 -0
  26. package/src/server.js +4 -14
  27. package/src/sessions/cleanup.js +3 -3
  28. package/src/sessions/record.js +10 -1
  29. package/src/sessions/store.js +7 -2
  30. package/src/tools/agent-task.js +48 -1
  31. package/src/tools/index.js +19 -2
  32. package/src/tools/lazy-loader.js +7 -0
  33. package/src/tools/tinyfish.js +358 -0
  34. package/src/tools/truncate.js +1 -0
  35. package/.github/FUNDING.yml +0 -15
  36. package/.github/workflows/README.md +0 -215
  37. package/.github/workflows/ci.yml +0 -69
  38. package/.github/workflows/index.yml +0 -62
  39. package/.github/workflows/web-tools-tests.yml +0 -56
  40. package/CITATIONS.bib +0 -6
  41. package/CLAWROUTER_ROUTING_PLAN.md +0 -910
  42. package/DEPLOYMENT.md +0 -1001
  43. package/LYNKR-TUI-PLAN.md +0 -984
  44. package/PERFORMANCE-REPORT.md +0 -866
  45. package/PLAN-per-client-model-routing.md +0 -252
  46. package/ROUTER_COMPARISON.md +0 -173
  47. package/TIER_ROUTING_PLAN.md +0 -771
  48. package/docs/42642f749da6234f41b6b425c3bb07c9.txt +0 -1
  49. package/docs/BingSiteAuth.xml +0 -4
  50. package/docs/docs-style.css +0 -478
  51. package/docs/docs.html +0 -197
  52. package/docs/google5be250e608e6da39.html +0 -1
  53. package/docs/index.html +0 -577
  54. package/docs/index.md +0 -577
  55. package/docs/robots.txt +0 -4
  56. package/docs/sitemap.xml +0 -44
  57. package/docs/style.css +0 -1223
  58. package/documentation/README.md +0 -100
  59. package/documentation/api.md +0 -806
  60. package/documentation/claude-code-cli.md +0 -672
  61. package/documentation/codex-cli.md +0 -397
  62. package/documentation/contributing.md +0 -571
  63. package/documentation/cursor-integration.md +0 -731
  64. package/documentation/docker.md +0 -867
  65. package/documentation/embeddings.md +0 -760
  66. package/documentation/faq.md +0 -659
  67. package/documentation/features.md +0 -396
  68. package/documentation/headroom.md +0 -519
  69. package/documentation/installation.md +0 -706
  70. package/documentation/memory-system.md +0 -476
  71. package/documentation/production.md +0 -601
  72. package/documentation/providers.md +0 -906
  73. package/documentation/testing.md +0 -629
  74. package/documentation/token-optimization.md +0 -323
  75. package/documentation/tools.md +0 -697
  76. package/documentation/troubleshooting.md +0 -893
  77. package/final-test.js +0 -33
  78. package/headroom-sidecar/config.py +0 -93
  79. package/headroom-sidecar/requirements.txt +0 -14
  80. package/headroom-sidecar/server.py +0 -451
  81. package/monitor-agents.sh +0 -31
  82. package/scripts/audit-log-reader.js +0 -399
  83. package/scripts/compact-dictionary.js +0 -204
  84. package/scripts/test-deduplication.js +0 -448
  85. package/src/db/database.sqlite +0 -0
  86. package/test/README.md +0 -212
  87. package/test/azure-openai-config.test.js +0 -204
  88. package/test/azure-openai-error-resilience.test.js +0 -238
  89. package/test/azure-openai-format-conversion.test.js +0 -354
  90. package/test/azure-openai-integration.test.js +0 -281
  91. package/test/azure-openai-routing.test.js +0 -177
  92. package/test/azure-openai-streaming.test.js +0 -171
  93. package/test/bedrock-integration.test.js +0 -471
  94. package/test/comprehensive-test-suite.js +0 -928
  95. package/test/config-validation.test.js +0 -207
  96. package/test/cursor-integration.test.js +0 -484
  97. package/test/format-conversion.test.js +0 -578
  98. package/test/hybrid-routing-integration.test.js +0 -254
  99. package/test/hybrid-routing-performance.test.js +0 -418
  100. package/test/llamacpp-integration.test.js +0 -863
  101. package/test/lmstudio-integration.test.js +0 -335
  102. package/test/memory/extractor.test.js +0 -398
  103. package/test/memory/retriever.test.js +0 -613
  104. package/test/memory/retriever.test.js.bak +0 -585
  105. package/test/memory/search.test.js +0 -537
  106. package/test/memory/search.test.js.bak +0 -389
  107. package/test/memory/store.test.js +0 -344
  108. package/test/memory/store.test.js.bak +0 -312
  109. package/test/memory/surprise.test.js +0 -300
  110. package/test/memory-performance.test.js +0 -472
  111. package/test/openai-integration.test.js +0 -686
  112. package/test/openrouter-error-resilience.test.js +0 -418
  113. package/test/passthrough-mode.test.js +0 -385
  114. package/test/performance-benchmark.js +0 -351
  115. package/test/performance-tests.js +0 -528
  116. package/test/routing.test.js +0 -219
  117. package/test/web-tools.test.js +0 -329
  118. package/test-agents-simple.js +0 -43
  119. package/test-cli-connection.sh +0 -33
  120. package/test-learning-unit.js +0 -126
  121. package/test-learning.js +0 -112
  122. package/test-parallel-agents.sh +0 -124
  123. package/test-parallel-direct.js +0 -155
  124. package/test-subagents.sh +0 -117
@@ -0,0 +1,181 @@
1
+ /**
2
+ * GPT-specific utilities for handling tool calls and responses
3
+ * All settings are hardcoded - no env vars required
4
+ *
5
+ * This module addresses GPT model compatibility issues when using Azure OpenAI
6
+ * through Lynkr proxy with Claude Code:
7
+ * - GPT doesn't interpret "0 files found" as a final answer
8
+ * - GPT retries the same tool expecting different results
9
+ * - GPT needs explicit guidance on tool result interpretation
10
+ */
11
+
12
+ const logger = require("../logger");
13
+
14
+ // Hardcoded GPT settings - optimized for GPT model behavior
15
+ const GPT_SETTINGS = {
16
+ toolLoopThreshold: 2, // Lower than Claude's 3 to catch loops earlier
17
+ enhancedFormatting: true, // Always format results explicitly for GPT
18
+ similarityThreshold: 0.8, // For detecting similar (not just identical) tool calls
19
+ };
20
+
21
+ // Provider identifiers that use GPT models
22
+ const GPT_PROVIDERS = ['azure-openai', 'openai'];
23
+
24
+ /**
25
+ * Check if a provider uses GPT models
26
+ * @param {string} provider - Provider type (e.g., 'azure-openai', 'databricks')
27
+ * @returns {boolean} - True if provider uses GPT models
28
+ */
29
+ function isGPTProvider(provider) {
30
+ if (!provider) return false;
31
+ return GPT_PROVIDERS.includes(provider.toLowerCase());
32
+ }
33
+
34
+ /**
35
+ * Get the tool loop threshold for GPT models
36
+ * @returns {number} - Threshold (2 for GPT, lower than Claude's 3)
37
+ */
38
+ function getGPTToolLoopThreshold() {
39
+ return GPT_SETTINGS.toolLoopThreshold;
40
+ }
41
+
42
+ /**
43
+ * Format tool result with explicit structure for GPT models
44
+ * GPT models need clear, unambiguous formatting to understand tool results
45
+ *
46
+ * @param {string} toolName - Name of the tool that was called
47
+ * @param {string} content - The tool result content
48
+ * @param {Object} args - The arguments passed to the tool
49
+ * @returns {string} - Formatted result with explicit status and instructions
50
+ */
51
+ function formatToolResultForGPT(toolName, content, args) {
52
+ // Handle empty/no results explicitly - add clear messaging to prevent retries
53
+ const isEmpty = !content ||
54
+ content.trim() === '' ||
55
+ content.includes('0 files found') ||
56
+ content.includes('No matches found') ||
57
+ content.includes('No results') ||
58
+ content.includes('Found 0') ||
59
+ /^Found \d+ files?\.$/.test(content.trim()) && content.includes('Found 0');
60
+
61
+ if (isEmpty) {
62
+ // Only format empty results - add explicit "don't retry" instruction
63
+ return `Tool "${toolName}" completed with no results found.
64
+ Query: ${JSON.stringify(args)}
65
+
66
+ This is a FINAL result - do not retry this query. Respond to the user based on this outcome.`;
67
+ }
68
+
69
+ // For successful results, return content as-is (don't add markers that might confuse GPT)
70
+ return content;
71
+ }
72
+
73
+ /**
74
+ * Get system prompt addendum for GPT models
75
+ * This teaches GPT how to properly interpret and use tools
76
+ *
77
+ * @returns {string} - System prompt instructions for GPT
78
+ */
79
+ function getGPTSystemPromptAddendum() {
80
+ return `Use the Bash tool with ls command for listing files. After any tool returns results, respond to the user.`;
81
+ }
82
+
83
+ /**
84
+ * Calculate string similarity using Jaccard index
85
+ * Used to detect semantically similar tool calls
86
+ *
87
+ * @param {string} s1 - First string
88
+ * @param {string} s2 - Second string
89
+ * @returns {number} - Similarity score between 0 and 1
90
+ */
91
+ function stringSimilarity(s1, s2) {
92
+ if (!s1 || !s2) return 0;
93
+ if (s1 === s2) return 1;
94
+
95
+ // Tokenize by whitespace and common delimiters
96
+ const tokenize = (s) => new Set(
97
+ s.toLowerCase()
98
+ .split(/[\s\-_\/\.\,\:\;]+/)
99
+ .filter(t => t.length > 0)
100
+ );
101
+
102
+ const set1 = tokenize(s1);
103
+ const set2 = tokenize(s2);
104
+
105
+ const intersection = new Set([...set1].filter(x => set2.has(x)));
106
+ const union = new Set([...set1, ...set2]);
107
+
108
+ return union.size > 0 ? intersection.size / union.size : 0;
109
+ }
110
+
111
+ /**
112
+ * Check if two tool calls are semantically similar
113
+ * GPT often retries with slightly different parameters that are functionally equivalent
114
+ *
115
+ * @param {Object} call1 - First tool call {name, arguments}
116
+ * @param {Object} call2 - Second tool call {name, arguments}
117
+ * @returns {boolean} - True if calls are similar enough to be considered duplicates
118
+ */
119
+ function areSimilarToolCalls(call1, call2) {
120
+ if (!call1 || !call2) return false;
121
+
122
+ // Must be the same tool
123
+ const name1 = call1.function?.name ?? call1.name;
124
+ const name2 = call2.function?.name ?? call2.name;
125
+ if (name1 !== name2) return false;
126
+
127
+ // Get arguments
128
+ const args1 = call1.function?.arguments ?? call1.arguments ?? call1.input ?? {};
129
+ const args2 = call2.function?.arguments ?? call2.arguments ?? call2.input ?? {};
130
+
131
+ // Stringify for comparison
132
+ const argsStr1 = typeof args1 === 'string' ? args1 : JSON.stringify(args1);
133
+ const argsStr2 = typeof args2 === 'string' ? args2 : JSON.stringify(args2);
134
+
135
+ // Exact match
136
+ if (argsStr1 === argsStr2) return true;
137
+
138
+ // For search-related tools, check semantic similarity
139
+ const searchTools = ['grep', 'glob', 'search', 'find', 'read', 'bash', 'shell'];
140
+ const toolName = (name1 || '').toLowerCase();
141
+ const isSearchTool = searchTools.some(t => toolName.includes(t));
142
+
143
+ if (isSearchTool) {
144
+ const similarity = stringSimilarity(argsStr1, argsStr2);
145
+ if (similarity >= GPT_SETTINGS.similarityThreshold) {
146
+ logger.debug({
147
+ tool: name1,
148
+ similarity,
149
+ threshold: GPT_SETTINGS.similarityThreshold,
150
+ args1: argsStr1.substring(0, 100),
151
+ args2: argsStr2.substring(0, 100),
152
+ }, "Similar tool call detected");
153
+ return true;
154
+ }
155
+ }
156
+
157
+ return false;
158
+ }
159
+
160
+ /**
161
+ * Get a signature for a tool call (for tracking in history)
162
+ * @param {Object} call - Tool call object
163
+ * @returns {string} - Unique signature for the call
164
+ */
165
+ function getToolCallSignature(call) {
166
+ const name = call.function?.name ?? call.name ?? 'unknown';
167
+ const args = call.function?.arguments ?? call.arguments ?? call.input ?? {};
168
+ const argsStr = typeof args === 'string' ? args : JSON.stringify(args);
169
+ return `${name}:${argsStr}`;
170
+ }
171
+
172
+ module.exports = {
173
+ GPT_SETTINGS,
174
+ isGPTProvider,
175
+ getGPTToolLoopThreshold,
176
+ formatToolResultForGPT,
177
+ getGPTSystemPromptAddendum,
178
+ stringSimilarity,
179
+ areSimilarToolCalls,
180
+ getToolCallSignature,
181
+ };
@@ -10,12 +10,21 @@ const modelCapabilitiesCache = new Map();
10
10
  const TOOL_CAPABLE_MODELS = new Set([
11
11
  "llama3.1",
12
12
  "llama3.2",
13
+ "llama3.3",
13
14
  "qwen2.5",
15
+ "qwen3",
14
16
  "mistral",
15
17
  "mistral-nemo",
16
18
  "firefunction-v2",
17
19
  "kimi-k2.5",
18
- "nemotron"
20
+ "nemotron",
21
+ "glm-4",
22
+ "glm-4.5",
23
+ "glm-4.7",
24
+ "glm-5",
25
+ "gpt-oss",
26
+ "minimax",
27
+ "deepseek-r1",
19
28
  ]);
20
29
 
21
30
  /**
@@ -55,25 +64,60 @@ async function checkOllamaToolSupport(modelName = config.ollama?.model) {
55
64
  return supportsTools;
56
65
  }
57
66
 
67
+ // --- Endpoint detection: Anthropic (/v1/messages) vs legacy (/api/chat) ---
68
+
69
+ // null = not probed yet, true = Anthropic available, false = use legacy
70
+ let anthropicEndpointAvailable = null;
71
+
72
+ /**
73
+ * Probe whether Ollama exposes the Anthropic-compatible /v1/messages endpoint (v0.14.0+).
74
+ * Result is cached for the process lifetime.
75
+ */
76
+ async function hasAnthropicEndpoint(baseUrl) {
77
+ if (anthropicEndpointAvailable !== null) return anthropicEndpointAvailable;
78
+
79
+ try {
80
+ // Send a minimal request — we only care about whether the route exists
81
+ const res = await fetch(`${baseUrl}/v1/messages`, {
82
+ method: "POST",
83
+ headers: {
84
+ "Content-Type": "application/json",
85
+ "anthropic-version": "2023-06-01",
86
+ },
87
+ body: JSON.stringify({
88
+ model: "probe",
89
+ max_tokens: 1,
90
+ messages: [{ role: "user", content: "hi" }],
91
+ }),
92
+ });
93
+
94
+ // 404 → endpoint doesn't exist (old Ollama)
95
+ // Any other status (200, 400, 500) → endpoint exists
96
+ anthropicEndpointAvailable = res.status !== 404;
97
+ logger.info(
98
+ { available: anthropicEndpointAvailable, status: res.status },
99
+ anthropicEndpointAvailable
100
+ ? "Ollama Anthropic API detected (/v1/messages) — using native passthrough"
101
+ : "Ollama Anthropic API not available — falling back to legacy /api/chat (upgrade to Ollama v0.14.0+ for best results)"
102
+ );
103
+ } catch (err) {
104
+ // Network error — assume legacy
105
+ anthropicEndpointAvailable = false;
106
+ logger.warn({ error: err.message }, "Failed to probe Ollama Anthropic endpoint, using legacy /api/chat");
107
+ }
108
+
109
+ return anthropicEndpointAvailable;
110
+ }
111
+
112
+ // Exposed for tests
113
+ function resetEndpointCache() {
114
+ anthropicEndpointAvailable = null;
115
+ }
116
+
117
+ // --- Legacy format conversion (for Ollama < v0.14.0 using /api/chat) ---
118
+
58
119
  /**
59
- * Convert Anthropic tool format to Ollama format
60
- *
61
- * Anthropic format:
62
- * {
63
- * name: "get_weather",
64
- * description: "Get weather",
65
- * input_schema: { type: "object", properties: {...}, required: [...] }
66
- * }
67
- *
68
- * Ollama format:
69
- * {
70
- * type: "function",
71
- * function: {
72
- * name: "get_weather",
73
- * description: "Get weather",
74
- * parameters: { type: "object", properties: {...}, required: [...] }
75
- * }
76
- * }
120
+ * Convert Anthropic tool format to Ollama/OpenAI function-calling format
77
121
  */
78
122
  function convertAnthropicToolsToOllama(anthropicTools) {
79
123
  if (!Array.isArray(anthropicTools) || anthropicTools.length === 0) {
@@ -93,128 +137,10 @@ function convertAnthropicToolsToOllama(anthropicTools) {
93
137
  }));
94
138
  }
95
139
 
96
- /**
97
- * Convert Ollama tool call response to Anthropic format
98
- *
99
- * Ollama format (actual):
100
- * {
101
- * message: {
102
- * role: "assistant",
103
- * content: "",
104
- * tool_calls: [{
105
- * function: {
106
- * name: "get_weather",
107
- * arguments: { location: "SF" } // Already parsed object
108
- * }
109
- * }]
110
- * }
111
- * }
112
- *
113
- * Anthropic format:
114
- * {
115
- * content: [{
116
- * type: "tool_use",
117
- * id: "toolu_123",
118
- * name: "get_weather",
119
- * input: { location: "SF" }
120
- * }],
121
- * stop_reason: "tool_use"
122
- * }
123
- */
124
- function convertOllamaToolCallsToAnthropic(ollamaResponse) {
125
- const message = ollamaResponse?.message || {};
126
- const toolCalls = message.tool_calls || [];
127
- const textContent = message.content || "";
128
-
129
- const contentBlocks = [];
130
-
131
- // Add text content if present
132
- if (textContent && textContent.trim()) {
133
- contentBlocks.push({
134
- type: "text",
135
- text: textContent,
136
- });
137
- }
138
-
139
- // Add tool calls
140
- for (const toolCall of toolCalls) {
141
- const func = toolCall.function || {};
142
- let input = {};
143
-
144
- // Handle arguments - can be string JSON or already parsed object
145
- if (func.arguments) {
146
- if (typeof func.arguments === "string") {
147
- try {
148
- input = JSON.parse(func.arguments);
149
- } catch (err) {
150
- logger.warn({
151
- error: err.message,
152
- arguments: func.arguments
153
- }, "Failed to parse Ollama tool arguments string");
154
- input = {};
155
- }
156
- } else if (typeof func.arguments === "object") {
157
- // Already an object, use directly
158
- input = func.arguments;
159
- }
160
- }
161
-
162
- // Generate tool use ID (Ollama may or may not provide one)
163
- const toolUseId = toolCall.id || `toolu_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
164
-
165
- contentBlocks.push({
166
- type: "tool_use",
167
- id: toolUseId,
168
- name: func.name || "unknown",
169
- input,
170
- });
171
- }
172
-
173
- // Determine stop reason
174
- const stopReason = toolCalls.length > 0 ? "tool_use" : "end_turn";
175
-
176
- return {
177
- contentBlocks,
178
- stopReason,
179
- };
180
- }
181
-
182
- /**
183
- * Build complete Anthropic response from Ollama with tool calls
184
- */
185
- function buildAnthropicResponseFromOllama(ollamaResponse, requestedModel) {
186
- const { contentBlocks, stopReason } = convertOllamaToolCallsToAnthropic(ollamaResponse);
187
-
188
- // Ensure at least one content block
189
- const finalContent = contentBlocks.length > 0
190
- ? contentBlocks
191
- : [{ type: "text", text: "" }];
192
-
193
- // Extract token counts
194
- const inputTokens = ollamaResponse.prompt_eval_count || 0;
195
- const outputTokens = ollamaResponse.eval_count || 0;
196
-
197
- return {
198
- id: `msg_${Date.now()}`,
199
- type: "message",
200
- role: "assistant",
201
- model: requestedModel,
202
- content: finalContent,
203
- stop_reason: stopReason,
204
- stop_sequence: null,
205
- usage: {
206
- input_tokens: inputTokens,
207
- output_tokens: outputTokens,
208
- cache_creation_input_tokens: 0,
209
- cache_read_input_tokens: 0,
210
- },
211
- };
212
- }
213
-
214
140
  module.exports = {
215
141
  checkOllamaToolSupport,
216
- convertAnthropicToolsToOllama,
217
- convertOllamaToolCallsToAnthropic,
218
- buildAnthropicResponseFromOllama,
219
142
  modelNameSupportsTools,
143
+ hasAnthropicEndpoint,
144
+ resetEndpointCache,
145
+ convertAnthropicToolsToOllama,
220
146
  };
@@ -14,7 +14,6 @@ const smartRouting = require('../routing');
14
14
 
15
15
  // Re-export all functions from smart routing
16
16
  module.exports = {
17
- determineProvider: smartRouting.determineProvider,
18
17
  determineProviderSmart: smartRouting.determineProviderSmart,
19
18
  isFallbackEnabled: smartRouting.isFallbackEnabled,
20
19
  getFallbackProvider: smartRouting.getFallbackProvider,
@@ -76,7 +76,7 @@ const STANDARD_TOOLS = [
76
76
  },
77
77
  {
78
78
  name: "Bash",
79
- description: "Executes a bash command in a persistent shell session. Use for terminal operations like git, npm, docker, etc. DO NOT use for file operations - use specialized tools instead.",
79
+ description: "Executes a bash command in a persistent shell session. Use for terminal operations like git, npm, docker, listing files (ls), etc. PREFERRED for listing directory contents - use 'ls' command. DO NOT use for reading file contents - use Read tool instead.",
80
80
  input_schema: {
81
81
  type: "object",
82
82
  properties: {
@@ -98,7 +98,7 @@ const STANDARD_TOOLS = [
98
98
  },
99
99
  {
100
100
  name: "Glob",
101
- description: "Fast file pattern matching tool. Supports glob patterns like '**/*.js' or 'src/**/*.ts'. Returns matching file paths sorted by modification time.",
101
+ description: "File pattern matching for finding files by name pattern. Use ONLY when you need to find files matching a specific pattern like '**/*.js'. For simple directory listing, use Bash with 'ls' instead.",
102
102
  input_schema: {
103
103
  type: "object",
104
104
  properties: {
@@ -145,6 +145,66 @@ const STANDARD_TOOLS = [
145
145
  required: ["pattern"]
146
146
  }
147
147
  },
148
+ {
149
+ name: "MultiEdit",
150
+ description: "Makes multiple edits to a single file in one atomic operation. More efficient than calling Edit multiple times. Each edit is an exact string replacement.",
151
+ input_schema: {
152
+ type: "object",
153
+ properties: {
154
+ file_path: {
155
+ type: "string",
156
+ description: "Relative path within workspace. DO NOT use absolute paths."
157
+ },
158
+ edits: {
159
+ type: "array",
160
+ description: "Array of edits to apply to the file",
161
+ items: {
162
+ type: "object",
163
+ properties: {
164
+ old_string: {
165
+ type: "string",
166
+ description: "The text to replace"
167
+ },
168
+ new_string: {
169
+ type: "string",
170
+ description: "The text to replace it with"
171
+ }
172
+ },
173
+ required: ["old_string", "new_string"]
174
+ }
175
+ }
176
+ },
177
+ required: ["file_path", "edits"]
178
+ }
179
+ },
180
+ {
181
+ name: "LS",
182
+ description: "Lists files and directories in a given path. Returns a structured listing with file types and sizes. Use for quick directory overview.",
183
+ input_schema: {
184
+ type: "object",
185
+ properties: {
186
+ path: {
187
+ type: "string",
188
+ description: "The directory to list. Defaults to current working directory."
189
+ }
190
+ },
191
+ required: []
192
+ }
193
+ },
194
+ {
195
+ name: "NotebookRead",
196
+ description: "Reads and displays the contents of a Jupyter notebook (.ipynb file), including all cells with their outputs, combining code, text, and visualizations.",
197
+ input_schema: {
198
+ type: "object",
199
+ properties: {
200
+ notebook_path: {
201
+ type: "string",
202
+ description: "Relative path to the Jupyter notebook (e.g., 'analysis.ipynb'). DO NOT use absolute paths."
203
+ }
204
+ },
205
+ required: ["notebook_path"]
206
+ }
207
+ },
148
208
  {
149
209
  name: "TodoWrite",
150
210
  description: "Create and manage a structured task list for tracking progress and organizing complex tasks. Use proactively for multi-step tasks or when user provides multiple tasks.",
@@ -320,6 +380,29 @@ EXAMPLE: User says "explore this project" → Call Task with subagent_type="Expl
320
380
  required: ["url", "prompt"]
321
381
  }
322
382
  },
383
+ {
384
+ name: "WebAgent",
385
+ description: "Launches a browser agent to navigate a website and accomplish a goal. Use when you need to interact with dynamic web content (click buttons, fill forms, extract data from JS-rendered pages) beyond what a simple HTTP fetch can do. Returns structured JSON. Takes 10-60 seconds.",
386
+ input_schema: {
387
+ type: "object",
388
+ properties: {
389
+ url: {
390
+ type: "string",
391
+ description: "Target URL to navigate to"
392
+ },
393
+ goal: {
394
+ type: "string",
395
+ description: "What to accomplish on the page. Be specific about what data to extract or actions to take."
396
+ },
397
+ browser_profile: {
398
+ type: "string",
399
+ enum: ["lite", "stealth"],
400
+ description: "lite (default, faster) or stealth (for bot-protected sites)"
401
+ }
402
+ },
403
+ required: ["url", "goal"]
404
+ }
405
+ },
323
406
  {
324
407
  name: "NotebookEdit",
325
408
  description: "Completely replaces the contents of a specific cell in a Jupyter notebook (.ipynb file). Use for editing interactive documents that combine code, text, and visualizations.",
@@ -354,4 +437,17 @@ EXAMPLE: User says "explore this project" → Call Task with subagent_type="Expl
354
437
  }
355
438
  ];
356
439
 
357
- module.exports = { STANDARD_TOOLS };
440
+ // Pre-computed name list to avoid re-mapping on every log call
441
+ const STANDARD_TOOL_NAMES = STANDARD_TOOLS.map(t => t.name);
442
+
443
+ // Tools that cannot work through a proxy (require bidirectional user interaction).
444
+ // All other tools are safe — per-client filtering via CLIENT_TOOL_MAPPINGS in
445
+ // openai-router.js handles excluding tools that specific clients don't support
446
+ // (e.g. Codex has no equivalent for Task, WebFetch, NotebookEdit).
447
+ const IDE_UNSUPPORTED_TOOLS = new Set(['AskUserQuestion']);
448
+
449
+ // Filtered tool set for IDE clients — excludes tools with no IDE equivalent
450
+ const IDE_SAFE_TOOLS = STANDARD_TOOLS.filter(t => !IDE_UNSUPPORTED_TOOLS.has(t.name));
451
+ const IDE_SAFE_TOOL_NAMES = IDE_SAFE_TOOLS.map(t => t.name);
452
+
453
+ module.exports = { STANDARD_TOOLS, STANDARD_TOOL_NAMES, IDE_SAFE_TOOLS, IDE_SAFE_TOOL_NAMES, IDE_UNSUPPORTED_TOOLS };