lynkr 9.4.0 → 9.4.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.
- package/package.json +1 -1
- package/src/api/router.js +11 -0
- package/src/dashboard/router.js +1 -1
- package/src/tools/smart-selection.js +104 -281
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lynkr",
|
|
3
|
-
"version": "9.4.
|
|
3
|
+
"version": "9.4.1",
|
|
4
4
|
"description": "Self-hosted LLM gateway and tier-routing proxy for Claude Code, Cursor, and Codex. Routes across Ollama, AWS Bedrock, OpenRouter, Databricks, Azure OpenAI, llama.cpp, and LM Studio with prompt caching, MCP tools, and 60-80% cost savings.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
package/src/api/router.js
CHANGED
|
@@ -764,6 +764,17 @@ router.post("/v1/messages", rateLimiter, async (req, res, next) => {
|
|
|
764
764
|
const parsed = JSON.parse(text);
|
|
765
765
|
if (parsed && typeof parsed === 'object' && parsed.type === 'message') {
|
|
766
766
|
parsed.lynkr_interaction = interaction;
|
|
767
|
+
// Inject a visible text block into content so Claude Code renders it.
|
|
768
|
+
if (Array.isArray(parsed.content)) {
|
|
769
|
+
const lines = [
|
|
770
|
+
`╭─ Lynkr ${'─'.repeat(40)}`,
|
|
771
|
+
`│ Tier ${interaction.tier ?? '—'} → ${interaction.model ?? '—'} (${interaction.provider ?? '—'})`,
|
|
772
|
+
`│ Score ${interaction.complexity_score ?? '—'}/100 · Risk: ${interaction.risk ?? '—'} · Savings: ~${interaction.estimated_savings_percent ?? 0}%`,
|
|
773
|
+
`│ Route ${interaction.mode ?? '—'} — ${interaction.headline ?? ''}`,
|
|
774
|
+
`╰${'─'.repeat(46)}`,
|
|
775
|
+
];
|
|
776
|
+
parsed.content.unshift({ type: 'text', text: lines.join('\n') });
|
|
777
|
+
}
|
|
767
778
|
finalBody = JSON.stringify(parsed);
|
|
768
779
|
}
|
|
769
780
|
}
|
package/src/dashboard/router.js
CHANGED
|
@@ -4,7 +4,7 @@ const api = require('./api');
|
|
|
4
4
|
|
|
5
5
|
const router = express.Router();
|
|
6
6
|
|
|
7
|
-
router.get('/',
|
|
7
|
+
router.get(['/', ''], (_req, res) => res.sendFile(path.join(__dirname, '../../public/dashboard.html')));
|
|
8
8
|
router.get('/api/overview', api.overview);
|
|
9
9
|
router.get('/api/usage', api.usage);
|
|
10
10
|
router.get('/api/routing', api.routing);
|
|
@@ -1,347 +1,170 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Smart Tool Selection
|
|
2
|
+
* Smart Tool Selection — Conservative Stripping
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Strategy: instead of predicting which tools ARE needed (brittle regex),
|
|
5
|
+
* only strip groups we are CERTAIN are irrelevant based on clear absence
|
|
6
|
+
* of intent signals.
|
|
6
7
|
*
|
|
7
|
-
*
|
|
8
|
+
* Rules:
|
|
9
|
+
* 1. Greeting → strip everything
|
|
10
|
+
* 2. No write intent → strip Write / Edit / NotebookEdit
|
|
11
|
+
* 3. No execution intent → strip Bash / KillShell
|
|
12
|
+
* 4. No web intent → strip WebSearch / WebFetch
|
|
13
|
+
*
|
|
14
|
+
* File ops (Read, Grep, Glob) are NEVER stripped — they are the most
|
|
15
|
+
* broadly useful and the most commonly needed unexpectedly.
|
|
8
16
|
*/
|
|
9
17
|
|
|
10
18
|
const logger = require('../logger');
|
|
11
19
|
|
|
12
|
-
// Strip system-reminder blocks injected by the CLI before classification
|
|
13
20
|
const SYSTEM_REMINDER_PATTERN = /<system-reminder>[\s\S]*?<\/system-reminder>/g;
|
|
14
21
|
|
|
15
|
-
//
|
|
22
|
+
// Clear greeting — strip all tools
|
|
16
23
|
const GREETING_PATTERN = /^(hi|hello|hey|good morning|good afternoon|good evening|howdy|greetings|sup|yo)[\s\.\!\?]*$/i;
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
24
|
+
const TECHNICAL_KEYWORDS = /code|function|class|file|module|import|export|async|await|promise|api|database|server|component|variable|array|object|\.[a-z]{1,5}\b|npm|git|docker|python|node|bash|run|install/i;
|
|
25
|
+
|
|
26
|
+
// Intent signals — absence means we strip that group
|
|
27
|
+
const WRITE_INTENT = /write|create\b|add to|update|modify|change|fix|delete|remove|insert|append|replace|save|edit|refactor|rename|move|reorganize|rewrite|implement|generate|produce|scaffold/i;
|
|
28
|
+
const EXECUTE_INTENT = /run|execute|test|compile|build|deploy|start|install|launch|boot|npm|yarn|pnpm|git|python|node|docker|bash|sh\b|cmd|script|make|cargo|go run/i;
|
|
29
|
+
const WEB_INTENT = /search online|search the web|search google|look up online|browse|website|https?:\/\//i;
|
|
30
|
+
|
|
31
|
+
// Tools always kept (file search is never useless)
|
|
32
|
+
const ALWAYS_KEEP = new Set([
|
|
33
|
+
'Read', 'Grep', 'Glob',
|
|
34
|
+
'Task', 'TaskOutput', 'TodoWrite', 'TodoRead',
|
|
35
|
+
'AskUserQuestion', 'Skill',
|
|
36
|
+
'EnterPlanMode', 'ExitPlanMode',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Conditional strips: group → intent pattern that must be present to keep it
|
|
40
|
+
const CONDITIONAL_GROUPS = [
|
|
41
|
+
{ names: ['Write', 'Edit', 'NotebookEdit'], intent: WRITE_INTENT },
|
|
42
|
+
{ names: ['Bash', 'KillShell'], intent: EXECUTE_INTENT },
|
|
43
|
+
{ names: ['WebSearch', 'WebFetch'], intent: WEB_INTENT },
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// Legacy map kept for telemetry label compatibility
|
|
30
47
|
const TOOL_SELECTION_MAP = {
|
|
31
|
-
conversational:
|
|
32
|
-
simple_qa:
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
'Read', 'Grep', 'Glob',
|
|
36
|
-
|
|
37
|
-
],
|
|
38
|
-
|
|
39
|
-
file_reading: [
|
|
40
|
-
'Read', 'Grep', 'Glob' // Read-only tools
|
|
41
|
-
],
|
|
42
|
-
|
|
43
|
-
file_modification: [
|
|
44
|
-
'Read', 'Write', 'Edit', // Full I/O
|
|
45
|
-
'Grep', 'Glob', 'Bash' // Support tools
|
|
46
|
-
],
|
|
47
|
-
|
|
48
|
-
code_execution: [
|
|
49
|
-
'Read', 'Write', 'Edit', // File operations
|
|
50
|
-
'Bash', 'Grep', 'Glob' // Execution + search
|
|
51
|
-
],
|
|
52
|
-
|
|
53
|
-
coding: [
|
|
54
|
-
'Read', 'Write', 'Edit', // Core file ops
|
|
55
|
-
'Bash', 'Grep', 'Glob' // Support tools
|
|
56
|
-
],
|
|
57
|
-
|
|
58
|
-
complex_task: [
|
|
59
|
-
'Read', 'Write', 'Edit', // Tier 1
|
|
60
|
-
'Bash', 'Grep', 'Glob', // Tier 1
|
|
61
|
-
'WebSearch', 'WebFetch', // Tier 2
|
|
62
|
-
'Task', 'TodoWrite', 'AskUserQuestion' // Tier 3+4
|
|
63
|
-
]
|
|
48
|
+
conversational: [],
|
|
49
|
+
simple_qa: [],
|
|
50
|
+
file_reading: ['Read', 'Grep', 'Glob'],
|
|
51
|
+
file_modification: ['Read', 'Write', 'Edit', 'Grep', 'Glob', 'Bash'],
|
|
52
|
+
code_execution: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob'],
|
|
53
|
+
coding: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob'],
|
|
54
|
+
research: ['Read', 'Grep', 'Glob', 'WebSearch', 'WebFetch'],
|
|
55
|
+
complex_task: ['Read', 'Write', 'Edit', 'Bash', 'Grep', 'Glob', 'WebSearch', 'WebFetch', 'Task', 'TodoWrite', 'AskUserQuestion'],
|
|
64
56
|
};
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
* Extract content from last user message
|
|
68
|
-
*/
|
|
69
|
-
function getLastUserMessage(payload) {
|
|
70
|
-
if (!Array.isArray(payload.messages) || payload.messages.length === 0) {
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
58
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
73
59
|
|
|
74
|
-
|
|
60
|
+
function getLastUserContent(payload) {
|
|
61
|
+
if (!Array.isArray(payload.messages)) return '';
|
|
75
62
|
for (let i = payload.messages.length - 1; i >= 0; i--) {
|
|
76
63
|
const msg = payload.messages[i];
|
|
77
|
-
if (msg?.role
|
|
78
|
-
|
|
64
|
+
if (msg?.role !== 'user') continue;
|
|
65
|
+
let text = '';
|
|
66
|
+
if (typeof msg.content === 'string') {
|
|
67
|
+
text = msg.content;
|
|
68
|
+
} else if (Array.isArray(msg.content)) {
|
|
69
|
+
text = msg.content.filter(b => b?.type === 'text').map(b => b.text || '').join(' ');
|
|
79
70
|
}
|
|
71
|
+
return text.replace(SYSTEM_REMINDER_PATTERN, '').trim();
|
|
80
72
|
}
|
|
81
|
-
|
|
82
|
-
return null;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Extract text content from message (handles string or array format)
|
|
87
|
-
*/
|
|
88
|
-
function extractContent(message) {
|
|
89
|
-
if (!message) return '';
|
|
90
|
-
|
|
91
|
-
if (typeof message.content === 'string') {
|
|
92
|
-
return message.content;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
if (Array.isArray(message.content)) {
|
|
96
|
-
return message.content
|
|
97
|
-
.filter(block => block?.type === 'text')
|
|
98
|
-
.map(block => block.text || '')
|
|
99
|
-
.join(' ');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
73
|
return '';
|
|
103
74
|
}
|
|
104
75
|
|
|
105
|
-
/**
|
|
106
|
-
* Check if content matches greeting patterns
|
|
107
|
-
*/
|
|
108
76
|
function isGreeting(content) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Check if content is short and non-technical
|
|
114
|
-
*/
|
|
115
|
-
function isShortNonTechnical(content) {
|
|
116
|
-
const trimmed = content.trim();
|
|
117
|
-
return trimmed.length < 20 && !TECHNICAL_KEYWORDS.test(trimmed);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Check if content is a simple question
|
|
122
|
-
*/
|
|
123
|
-
function isSimpleQuestion(content) {
|
|
124
|
-
return QUESTION_PATTERN.test(content.trim());
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Check for technical keywords
|
|
129
|
-
*/
|
|
130
|
-
function hasTechnicalKeywords(content) {
|
|
131
|
-
return TECHNICAL_KEYWORDS.test(content);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Check for explanation/research keywords
|
|
136
|
-
*/
|
|
137
|
-
function hasExplanationKeywords(content) {
|
|
138
|
-
return EXPLANATION_PATTERN.test(content);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* Check for web/search keywords
|
|
143
|
-
*/
|
|
144
|
-
function hasWebKeywords(content) {
|
|
145
|
-
return WEB_PATTERN.test(content);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Check for file reading keywords
|
|
150
|
-
*/
|
|
151
|
-
function hasReadKeywords(content) {
|
|
152
|
-
return READ_PATTERN.test(content);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* Check for file writing/modification keywords
|
|
157
|
-
*/
|
|
158
|
-
function hasWriteKeywords(content) {
|
|
159
|
-
return WRITE_PATTERN.test(content);
|
|
77
|
+
const t = content.trim();
|
|
78
|
+
return GREETING_PATTERN.test(t) || (t.length < 20 && !TECHNICAL_KEYWORDS.test(t));
|
|
160
79
|
}
|
|
161
80
|
|
|
162
|
-
|
|
163
|
-
* Check for edit/refactor keywords
|
|
164
|
-
*/
|
|
165
|
-
function hasEditKeywords(content) {
|
|
166
|
-
return EDIT_PATTERN.test(content);
|
|
167
|
-
}
|
|
81
|
+
// ─── Classifier (conservative) ───────────────────────────────────────────────
|
|
168
82
|
|
|
169
83
|
/**
|
|
170
|
-
*
|
|
171
|
-
|
|
172
|
-
function hasExecutionKeywords(content) {
|
|
173
|
-
return EXECUTION_PATTERN.test(content);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* Check for complex task keywords
|
|
178
|
-
*/
|
|
179
|
-
function hasComplexKeywords(content) {
|
|
180
|
-
return COMPLEX_PATTERN.test(content);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Classify request type based on content analysis
|
|
185
|
-
*
|
|
186
|
-
* @param {Object} payload - Request payload with messages
|
|
187
|
-
* @returns {Object} Classification result { type, confidence, keywords }
|
|
84
|
+
* Classify request and compute which tool groups to strip.
|
|
85
|
+
* Returns a classification object for logging/telemetry compatibility.
|
|
188
86
|
*/
|
|
189
87
|
function classifyRequestType(payload) {
|
|
190
|
-
const
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return { type: 'coding', confidence: 0.5, keywords: [] };
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const rawContent = extractContent(lastMessage);
|
|
197
|
-
// Strip <system-reminder> blocks before classification to prevent
|
|
198
|
-
// CLI-injected keywords (search, explain, documentation) from polluting results
|
|
199
|
-
const content = rawContent.replace(SYSTEM_REMINDER_PATTERN, '').trim();
|
|
200
|
-
const contentLower = content.toLowerCase();
|
|
201
|
-
const messageCount = payload.messages?.length ?? 0;
|
|
202
|
-
|
|
203
|
-
// 1. Conversational (no tools)
|
|
204
|
-
if (isGreeting(contentLower)) {
|
|
205
|
-
return { type: 'conversational', confidence: 1.0, keywords: ['greeting'] };
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (isShortNonTechnical(contentLower)) {
|
|
209
|
-
return { type: 'conversational', confidence: 0.8, keywords: ['short', 'non-technical'] };
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// 2. Simple Q&A (no tools)
|
|
213
|
-
if (isSimpleQuestion(contentLower) && !hasTechnicalKeywords(contentLower)) {
|
|
214
|
-
return { type: 'simple_qa', confidence: 0.9, keywords: ['question', 'non-technical'] };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// 3. Research/Explanation (minimal tools)
|
|
218
|
-
if (hasExplanationKeywords(contentLower)) {
|
|
219
|
-
return { type: 'research', confidence: 0.85, keywords: ['explanation'] };
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (hasWebKeywords(contentLower)) {
|
|
223
|
-
return { type: 'research', confidence: 0.9, keywords: ['web', 'search'] };
|
|
224
|
-
}
|
|
88
|
+
const content = getLastUserContent(payload);
|
|
89
|
+
const lower = content.toLowerCase();
|
|
90
|
+
const msgCount = payload.messages?.length ?? 0;
|
|
225
91
|
|
|
226
|
-
//
|
|
227
|
-
if (
|
|
228
|
-
return { type: '
|
|
92
|
+
// Greeting → strip everything
|
|
93
|
+
if (isGreeting(lower)) {
|
|
94
|
+
return { type: 'conversational', confidence: 1.0, keywords: ['greeting'], _stripped: ['Write', 'Edit', 'NotebookEdit', 'Bash', 'KillShell', 'WebSearch', 'WebFetch'] };
|
|
229
95
|
}
|
|
230
96
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
97
|
+
const stripped = [];
|
|
98
|
+
for (const { names, intent } of CONDITIONAL_GROUPS) {
|
|
99
|
+
if (!intent.test(lower)) stripped.push(...names);
|
|
234
100
|
}
|
|
235
101
|
|
|
236
|
-
//
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
102
|
+
// Derive a label for telemetry
|
|
103
|
+
const hasWrite = WRITE_INTENT.test(lower);
|
|
104
|
+
const hasExec = EXECUTE_INTENT.test(lower);
|
|
105
|
+
const hasWeb = WEB_INTENT.test(lower);
|
|
240
106
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
// Long conversations likely need more tools
|
|
247
|
-
if (messageCount > 10) {
|
|
248
|
-
return { type: 'complex_task', confidence: 0.7, keywords: ['long_conversation'] };
|
|
249
|
-
}
|
|
107
|
+
const type = hasWrite || hasExec ? 'file_modification'
|
|
108
|
+
: hasWeb ? 'research'
|
|
109
|
+
: msgCount > 10 ? 'complex_task'
|
|
110
|
+
: 'file_reading';
|
|
250
111
|
|
|
251
|
-
|
|
252
|
-
return { type: 'coding', confidence: 0.6, keywords: ['default'] };
|
|
112
|
+
return { type, confidence: 0.9, keywords: ['conservative'], _stripped: stripped };
|
|
253
113
|
}
|
|
254
114
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
*/
|
|
115
|
+
// ─── Tool filter ─────────────────────────────────────────────────────────────
|
|
116
|
+
|
|
258
117
|
function estimateToolTokens(tools) {
|
|
259
118
|
if (!Array.isArray(tools)) return 0;
|
|
260
|
-
|
|
261
|
-
// Average: ~175 tokens per tool (based on STANDARD_TOOLS analysis)
|
|
262
119
|
return tools.length * 175;
|
|
263
120
|
}
|
|
264
121
|
|
|
265
122
|
/**
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
* @param {Array} tools - Available tools
|
|
269
|
-
* @param {Object} classification - Classification result from classifyRequestType
|
|
270
|
-
* @param {Object} options - Selection options (provider, tokenBudget, config)
|
|
271
|
-
* @returns {Array} Filtered list of relevant tools
|
|
123
|
+
* Apply conservative stripping to the tool list.
|
|
272
124
|
*/
|
|
273
125
|
function selectToolsSmartly(tools, classification, options = {}) {
|
|
274
|
-
if (!Array.isArray(tools) || tools.length === 0)
|
|
275
|
-
return tools;
|
|
276
|
-
}
|
|
126
|
+
if (!Array.isArray(tools) || tools.length === 0) return tools;
|
|
277
127
|
|
|
278
|
-
const { provider = 'databricks'
|
|
279
|
-
const
|
|
128
|
+
const { provider = 'databricks' } = options;
|
|
129
|
+
const strippedNames = new Set(classification._stripped ?? []);
|
|
280
130
|
|
|
281
|
-
//
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
// Filter to relevant tools only (case-insensitive match so external clients
|
|
286
|
-
// using lowercase names like Pi's `bash`/`read` aren't filtered out)
|
|
287
|
-
let selectedTools = tools.filter(tool => relevantLower.has(String(tool.name || '').toLowerCase()));
|
|
288
|
-
|
|
289
|
-
// If nothing matched, the caller is using a tool ecosystem we don't recognize
|
|
290
|
-
// (e.g. Pi's read/write/edit/bash). Pass tools through untouched rather than
|
|
291
|
-
// deleting them — otherwise the LLM gets no schema and hallucinates defaults.
|
|
292
|
-
if (selectedTools.length === 0) {
|
|
293
|
-
return tools;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
// Mode-specific adjustments
|
|
297
|
-
if (config.mode === 'aggressive') {
|
|
298
|
-
// Aggressive: Further reduce tools for ambiguous cases
|
|
299
|
-
if (classification.confidence < 0.7 && selectedTools.length > 4) {
|
|
300
|
-
selectedTools = selectedTools.slice(0, 4);
|
|
301
|
-
}
|
|
302
|
-
} else if (config.mode === 'conservative') {
|
|
303
|
-
// Conservative: Include one extra tier of tools for safety
|
|
304
|
-
if (requestType === 'file_reading' && !relevantToolNames.includes('Bash')) {
|
|
305
|
-
const bashTool = tools.find(t => t.name === 'Bash');
|
|
306
|
-
if (bashTool) selectedTools.push(bashTool);
|
|
307
|
-
}
|
|
131
|
+
// Greeting: strip everything
|
|
132
|
+
if (classification.type === 'conversational') {
|
|
133
|
+
return [];
|
|
308
134
|
}
|
|
309
135
|
|
|
310
|
-
//
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const estimatedTokens = estimateToolTokens(selectedTools);
|
|
317
|
-
if (estimatedTokens > tokenBudget) {
|
|
318
|
-
const targetCount = Math.floor(tokenBudget / 175);
|
|
319
|
-
selectedTools = selectedTools.slice(0, Math.max(targetCount, 0));
|
|
320
|
-
}
|
|
136
|
+
// Strip only the flagged groups; always keep ALWAYS_KEEP tools
|
|
137
|
+
let selected = tools.filter(tool => {
|
|
138
|
+
const name = String(tool.name || '');
|
|
139
|
+
if (ALWAYS_KEEP.has(name)) return true;
|
|
140
|
+
return !strippedNames.has(name);
|
|
141
|
+
});
|
|
321
142
|
|
|
322
|
-
//
|
|
323
|
-
if (
|
|
324
|
-
const minimalTools = ['Read', 'Write', 'Edit', 'Bash'];
|
|
325
|
-
selectedTools = selectedTools.filter(t => minimalTools.includes(t.name));
|
|
326
|
-
}
|
|
143
|
+
// Safety: if we somehow stripped everything, return full list
|
|
144
|
+
if (selected.length === 0) return tools;
|
|
327
145
|
|
|
328
|
-
// Code Mode
|
|
146
|
+
// Code Mode meta-tools always included
|
|
329
147
|
const codeConfig = require('../config');
|
|
330
148
|
if (codeConfig.mcp?.codeMode?.enabled) {
|
|
331
149
|
const codeModeNames = new Set(['mcp_list_tools', 'mcp_tool_info', 'mcp_tool_docs', 'mcp_execute']);
|
|
332
150
|
for (const tool of tools) {
|
|
333
|
-
if (codeModeNames.has(tool.name) && !
|
|
334
|
-
|
|
151
|
+
if (codeModeNames.has(tool.name) && !selected.some(t => t.name === tool.name)) {
|
|
152
|
+
selected.push(tool);
|
|
335
153
|
}
|
|
336
154
|
}
|
|
337
155
|
}
|
|
338
156
|
|
|
339
|
-
|
|
157
|
+
// Ollama has a smaller context — cap at 10 tools
|
|
158
|
+
if (provider === 'ollama' && selected.length > 10) {
|
|
159
|
+
selected = selected.slice(0, 10);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return selected;
|
|
340
163
|
}
|
|
341
164
|
|
|
342
165
|
module.exports = {
|
|
343
166
|
classifyRequestType,
|
|
344
167
|
selectToolsSmartly,
|
|
345
168
|
estimateToolTokens,
|
|
346
|
-
TOOL_SELECTION_MAP
|
|
169
|
+
TOOL_SELECTION_MAP,
|
|
347
170
|
};
|