@link-assistant/agent 0.27.0 → 0.27.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/README.md +47 -5
- package/package.json +41 -42
- package/src/cli/argv.ts +1 -1
- package/src/cli/cmd/run.ts +7 -1
- package/src/cli/continuous-mode.js +38 -12
- package/src/cli/event-handler.js +3 -1
- package/src/cli/model-config.js +20 -4
- package/src/cli/run-options.js +144 -78
- package/src/config/config.ts +306 -105
- package/src/config/defaults.ts +13 -5
- package/src/config/file-config.ts +16 -0
- package/src/file/ripgrep.ts +38 -5
- package/src/file/time.ts +86 -9
- package/src/index.js +39 -33
- package/src/mcp/index.ts +370 -106
- package/src/permission/index.ts +22 -0
- package/src/project/instance.ts +5 -1
- package/src/project/project.ts +35 -2
- package/src/provider/free-tier-error.ts +1 -1
- package/src/provider/kilo-deprecation.ts +30 -0
- package/src/provider/kilo.ts +25 -7
- package/src/provider/provider.ts +53 -22
- package/src/provider/retry-fetch.ts +5 -0
- package/src/provider/transform.ts +139 -17
- package/src/session/compaction.ts +18 -2
- package/src/session/index.ts +30 -3
- package/src/session/message-v2.ts +318 -32
- package/src/session/processor.ts +274 -140
- package/src/session/prompt.ts +154 -128
- package/src/session/retry.ts +26 -13
- package/src/session/summary.ts +4 -1
- package/src/tool/bash.ts +232 -45
- package/src/tool/bash.txt +1 -1
- package/src/tool/codesearch.ts +15 -78
- package/src/tool/edit.ts +111 -30
- package/src/tool/glob.ts +2 -1
- package/src/tool/grep.ts +175 -57
- package/src/tool/ls.ts +2 -1
- package/src/tool/multiedit.ts +3 -1
- package/src/tool/read.ts +169 -41
- package/src/tool/task.ts +26 -18
- package/src/tool/tool.ts +4 -1
- package/src/tool/webfetch.ts +68 -56
- package/src/tool/webfetch.txt +4 -4
- package/src/tool/websearch.ts +20 -75
- package/src/tool/websearch.txt +2 -0
- package/src/tool/write.ts +23 -6
- package/src/util/bom.ts +32 -0
- package/src/util/exa-mcp.ts +82 -0
- package/src/util/legacy-error-names.ts +39 -0
- package/src/util/read-bounded.ts +32 -0
- package/src/util/sse-usage-extractor.ts +67 -13
- package/src/util/timeout.ts +3 -4
- package/src/util/verbose-fetch.ts +40 -1
package/src/tool/bash.ts
CHANGED
|
@@ -6,12 +6,23 @@ import { Branding } from '../branding';
|
|
|
6
6
|
import { Log } from '../util/log';
|
|
7
7
|
import { Instance } from '../project/instance';
|
|
8
8
|
import { lazy } from '../util/lazy';
|
|
9
|
-
import { Language } from 'web-tree-sitter';
|
|
9
|
+
import { Language, type Node } from 'web-tree-sitter';
|
|
10
10
|
import { Permission } from '../permission';
|
|
11
11
|
import { fileURLToPath } from 'url';
|
|
12
|
+
import path from 'path';
|
|
12
13
|
import { processResult } from './process-result';
|
|
14
|
+
import { config } from '../config/config';
|
|
13
15
|
|
|
14
|
-
|
|
16
|
+
// Output is limited to its last MAX_LINES lines and MAX_BYTES bytes, and the
|
|
17
|
+
// metadata preview to its last MAX_METADATA_LENGTH characters (upstream
|
|
18
|
+
// 307251bf3c #22660, 754ea99d86 #39403, 2092350cfa #41007).
|
|
19
|
+
const MAX_BYTES = 30_000;
|
|
20
|
+
const MAX_LINES = 2000;
|
|
21
|
+
const MAX_METADATA_LENGTH = 30_000;
|
|
22
|
+
// --legacy-tool-output: the head of the output was kept, up to this many
|
|
23
|
+
// characters, followed by LEGACY_TRUNCATED.
|
|
24
|
+
const LEGACY_MAX_OUTPUT_LENGTH = 30_000;
|
|
25
|
+
const LEGACY_TRUNCATED = '\n\n(Output was truncated due to length limit)';
|
|
15
26
|
const DEFAULT_TIMEOUT = 1 * 60 * 1000;
|
|
16
27
|
const MAX_TIMEOUT = 10 * 60 * 1000;
|
|
17
28
|
const KILL_SIGNAL = 'SIGTERM';
|
|
@@ -30,6 +41,113 @@ const killWindowsTree = async (pid: number | undefined) => {
|
|
|
30
41
|
).catch(() => {});
|
|
31
42
|
};
|
|
32
43
|
|
|
44
|
+
// Shells known to run the POSIX `-c` scripts models write. Anything else in
|
|
45
|
+
// $SHELL (fish, nu, tcsh, csh, xonsh, ...) would misparse them
|
|
46
|
+
// (upstream b5d0c56b4c #3494, 238b907dd8 #5205).
|
|
47
|
+
const POSIX_SHELLS = new Set([
|
|
48
|
+
'sh',
|
|
49
|
+
'bash',
|
|
50
|
+
'zsh',
|
|
51
|
+
'dash',
|
|
52
|
+
'ksh',
|
|
53
|
+
'mksh',
|
|
54
|
+
'ash',
|
|
55
|
+
]);
|
|
56
|
+
const shells = new Map<string, string | undefined>();
|
|
57
|
+
|
|
58
|
+
// Resolve the shell commands run in (--bash-shell /
|
|
59
|
+
// LINK_ASSISTANT_AGENT_BASH_SHELL):
|
|
60
|
+
// - "auto" (default): $SHELL when it is a POSIX shell, otherwise zsh on
|
|
61
|
+
// macOS and bash elsewhere, falling back to /bin/sh;
|
|
62
|
+
// - "legacy": Node's platform shell (`spawn(command, { shell: true })`,
|
|
63
|
+
// i.e. /bin/sh), the behaviour before issue #335;
|
|
64
|
+
// - anything else: that shell, run with `-c`.
|
|
65
|
+
// Windows always keeps the platform shell.
|
|
66
|
+
export function selectShell(
|
|
67
|
+
mode: string = config.bashShell
|
|
68
|
+
): string | undefined {
|
|
69
|
+
if (process.platform === 'win32') return undefined;
|
|
70
|
+
const choice = (mode || 'auto').trim();
|
|
71
|
+
if (choice === 'legacy') return undefined;
|
|
72
|
+
const preferred = choice === 'auto' ? (process.env.SHELL ?? '') : choice;
|
|
73
|
+
const key = `${choice}\0${preferred}`;
|
|
74
|
+
if (shells.has(key)) return shells.get(key);
|
|
75
|
+
let shell: string | undefined;
|
|
76
|
+
if (choice !== 'auto') {
|
|
77
|
+
// An explicit shell is used as given; an unknown one fails loudly when
|
|
78
|
+
// spawned instead of silently running another shell.
|
|
79
|
+
shell = Bun.which(preferred) ?? preferred;
|
|
80
|
+
} else {
|
|
81
|
+
const name = path.basename(preferred).toLowerCase();
|
|
82
|
+
if (preferred && POSIX_SHELLS.has(name)) {
|
|
83
|
+
shell = Bun.which(preferred) ?? undefined;
|
|
84
|
+
}
|
|
85
|
+
shell ??=
|
|
86
|
+
process.platform === 'darwin'
|
|
87
|
+
? '/bin/zsh'
|
|
88
|
+
: (Bun.which('bash') ?? '/bin/sh');
|
|
89
|
+
}
|
|
90
|
+
shells.set(key, shell);
|
|
91
|
+
return shell;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const WRITE_REDIRECTS = new Set(['>', '>>', '>|', '&>', '&>>', '>&']);
|
|
95
|
+
|
|
96
|
+
// Output redirects that write a file. They are siblings of the command node,
|
|
97
|
+
// so the token scan below does not see them, and `echo x > file` would match
|
|
98
|
+
// an `echo*` allow rule in plan/readonly mode. Discarding output into
|
|
99
|
+
// /dev/null and duplicating descriptors (`2>&1`) write nothing.
|
|
100
|
+
// Adapted from upstream e7ff7143b6 (#6737), which has no readonly mode.
|
|
101
|
+
function writeRedirects(root: Node): string[] {
|
|
102
|
+
const found: string[] = [];
|
|
103
|
+
for (const node of root.descendantsOfType('file_redirect')) {
|
|
104
|
+
if (!node) continue;
|
|
105
|
+
let operator = '';
|
|
106
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
107
|
+
const child = node.child(i);
|
|
108
|
+
if (child && WRITE_REDIRECTS.has(child.type)) operator = child.type;
|
|
109
|
+
}
|
|
110
|
+
if (!operator) continue;
|
|
111
|
+
const destination =
|
|
112
|
+
node.childForFieldName('destination')?.text ?? node.lastChild?.text;
|
|
113
|
+
if (destination === '/dev/null') continue;
|
|
114
|
+
if (operator === '>&' && /^(\d+|-)$/.test(destination ?? '')) continue;
|
|
115
|
+
found.push(node.text);
|
|
116
|
+
}
|
|
117
|
+
return found;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Keep the end of the output, where errors are printed, cutting at line
|
|
121
|
+
// boundaries and never inside a UTF-8 sequence.
|
|
122
|
+
function tail(text: string, maxLines: number, maxBytes: number) {
|
|
123
|
+
const lines = text.split('\n');
|
|
124
|
+
if (lines.length <= maxLines && Buffer.byteLength(text, 'utf-8') <= maxBytes)
|
|
125
|
+
return { text, cut: false };
|
|
126
|
+
const out: string[] = [];
|
|
127
|
+
let bytes = 0;
|
|
128
|
+
for (let i = lines.length - 1; i >= 0 && out.length < maxLines; i--) {
|
|
129
|
+
const size =
|
|
130
|
+
Buffer.byteLength(lines[i], 'utf-8') + (out.length > 0 ? 1 : 0);
|
|
131
|
+
if (bytes + size > maxBytes) {
|
|
132
|
+
if (out.length === 0) {
|
|
133
|
+
const buf = Buffer.from(lines[i], 'utf-8');
|
|
134
|
+
let start = Math.max(0, buf.length - maxBytes);
|
|
135
|
+
while (start < buf.length && (buf[start] & 0xc0) === 0x80) start++;
|
|
136
|
+
out.unshift(buf.subarray(start).toString('utf-8'));
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
out.unshift(lines[i]);
|
|
141
|
+
bytes += size;
|
|
142
|
+
}
|
|
143
|
+
return { text: out.join('\n'), cut: true };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function preview(text: string) {
|
|
147
|
+
if (text.length <= MAX_METADATA_LENGTH) return text;
|
|
148
|
+
return '...\n\n' + text.slice(-MAX_METADATA_LENGTH);
|
|
149
|
+
}
|
|
150
|
+
|
|
33
151
|
const resolveWasm = (asset: string) => {
|
|
34
152
|
if (asset.startsWith('file://')) return fileURLToPath(asset);
|
|
35
153
|
if (asset.startsWith('/') || /^[a-z]:/i.test(asset)) return asset;
|
|
@@ -101,24 +219,34 @@ export const BashTool = Tool.define('bash', {
|
|
|
101
219
|
throw new Error('Failed to parse command');
|
|
102
220
|
}
|
|
103
221
|
const commands: string[][] = [];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
for (
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
child
|
|
112
|
-
child
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
222
|
+
// Trees live in the WASM heap and are not garbage-collected; free each
|
|
223
|
+
// one before a permission ask can wait (upstream d4bf70be06, #24861).
|
|
224
|
+
try {
|
|
225
|
+
for (const node of tree.rootNode.descendantsOfType('command')) {
|
|
226
|
+
if (!node) continue;
|
|
227
|
+
const command: string[] = [];
|
|
228
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
229
|
+
const child = node.child(i);
|
|
230
|
+
if (!child) continue;
|
|
231
|
+
if (
|
|
232
|
+
child.type !== 'command_name' &&
|
|
233
|
+
child.type !== 'word' &&
|
|
234
|
+
child.type !== 'string' &&
|
|
235
|
+
child.type !== 'raw_string' &&
|
|
236
|
+
child.type !== 'concatenation'
|
|
237
|
+
) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
command.push(child.text);
|
|
118
241
|
}
|
|
119
|
-
|
|
242
|
+
commands.push(command);
|
|
120
243
|
}
|
|
121
|
-
|
|
244
|
+
// A `>` head matches only the catch-all pattern, so a write redirect
|
|
245
|
+
// gets the mode's fallback action: deny in readonly, ask in plan.
|
|
246
|
+
const redirects = writeRedirects(tree.rootNode);
|
|
247
|
+
if (redirects.length > 0) commands.push(['>', ...redirects]);
|
|
248
|
+
} finally {
|
|
249
|
+
tree.delete();
|
|
122
250
|
}
|
|
123
251
|
await Permission.checkBashTokens({
|
|
124
252
|
commands,
|
|
@@ -132,7 +260,40 @@ export const BashTool = Tool.define('bash', {
|
|
|
132
260
|
const timeoutSignal = AbortSignal.timeout(timeout);
|
|
133
261
|
const cancel = AbortSignal.any([ctx.abort, timeoutSignal]);
|
|
134
262
|
|
|
135
|
-
|
|
263
|
+
// Only the last 2 * MAX_BYTES of output are kept in memory.
|
|
264
|
+
const keep = MAX_BYTES * 2;
|
|
265
|
+
const chunks: { text: string; size: number }[] = [];
|
|
266
|
+
let used = 0;
|
|
267
|
+
let dropped = false;
|
|
268
|
+
let last = '';
|
|
269
|
+
// --legacy-tool-output keeps the head instead, so only the first
|
|
270
|
+
// LEGACY_MAX_OUTPUT_LENGTH + 1 characters are needed.
|
|
271
|
+
const legacy = config.legacyToolOutput;
|
|
272
|
+
let head = '';
|
|
273
|
+
// A multi-byte character can be split across chunks of one stream.
|
|
274
|
+
const decoders = new Map<string, TextDecoder>();
|
|
275
|
+
const append = (text: string) => {
|
|
276
|
+
if (!text) return;
|
|
277
|
+
if (legacy && head.length <= LEGACY_MAX_OUTPUT_LENGTH) {
|
|
278
|
+
head += text.slice(0, LEGACY_MAX_OUTPUT_LENGTH + 1 - head.length);
|
|
279
|
+
}
|
|
280
|
+
const size = Buffer.byteLength(text, 'utf-8');
|
|
281
|
+
chunks.push({ text, size });
|
|
282
|
+
used += size;
|
|
283
|
+
while (used > keep && chunks.length > 1) {
|
|
284
|
+
used -= chunks.shift()!.size;
|
|
285
|
+
dropped = true;
|
|
286
|
+
}
|
|
287
|
+
// Each update is stored and published, so send a bounded preview
|
|
288
|
+
// instead of the whole output so far.
|
|
289
|
+
last = preview(last + text);
|
|
290
|
+
ctx.metadata({
|
|
291
|
+
metadata: {
|
|
292
|
+
output: last,
|
|
293
|
+
description: params.description,
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
};
|
|
136
297
|
|
|
137
298
|
// Initialize metadata with empty output
|
|
138
299
|
ctx.metadata({
|
|
@@ -150,50 +311,65 @@ export const BashTool = Tool.define('bash', {
|
|
|
150
311
|
// (link-foundation/command-stream#207).
|
|
151
312
|
// A cancelled command does not need to start at all.
|
|
152
313
|
if (!cancel.aborted) {
|
|
314
|
+
// The runner gets its own signal. command-stream 2.4.0 never removes
|
|
315
|
+
// the abort listener it adds to options.signal, so passing the turn's
|
|
316
|
+
// long-lived signal would keep every runner (and its output) reachable
|
|
317
|
+
// until the turn ends or the timeout fires. Only the link below touches
|
|
318
|
+
// `cancel`, and it is removed when the command finishes.
|
|
153
319
|
// On Windows the runner is stopped only after taskkill has stopped the
|
|
154
320
|
// shell's process tree.
|
|
155
|
-
const stop =
|
|
156
|
-
process.platform === 'win32' ? new AbortController() : undefined;
|
|
321
|
+
const stop = new AbortController();
|
|
157
322
|
|
|
158
|
-
// The file/args
|
|
159
|
-
//
|
|
160
|
-
// string spec would route simple commands and pipelines through
|
|
323
|
+
// The file/args forms run the command string in a real shell. A
|
|
324
|
+
// command string spec would route simple commands and pipelines through
|
|
161
325
|
// command-stream's JavaScript builtins (`ls`, `exit`, `cd`, ...) instead.
|
|
326
|
+
// Windows keeps Node's platform shell (`spawn(command, { shell: true
|
|
327
|
+
// })`); elsewhere the selected shell runs it with `-c`.
|
|
328
|
+
const shell = selectShell();
|
|
162
329
|
const command = new ProcessRunner(
|
|
163
|
-
|
|
330
|
+
shell
|
|
331
|
+
? { mode: 'exec', file: shell, args: ['-c', params.command] }
|
|
332
|
+
: { mode: 'shell', file: params.command, args: [] },
|
|
164
333
|
{
|
|
165
334
|
cwd: Instance.directory,
|
|
166
335
|
env: { ...process.env },
|
|
167
|
-
signal: stop
|
|
336
|
+
signal: stop.signal,
|
|
168
337
|
killSignal: KILL_SIGNAL,
|
|
169
338
|
killGrace: 200,
|
|
170
339
|
mirror: false,
|
|
171
|
-
|
|
340
|
+
// Output is read from stream() into the bounded buffer above;
|
|
341
|
+
// capturing would also keep the whole output in the runner.
|
|
342
|
+
capture: false,
|
|
172
343
|
stdin: 'ignore',
|
|
173
344
|
}
|
|
174
345
|
);
|
|
175
346
|
|
|
176
347
|
const stopTree = () => {
|
|
177
|
-
|
|
348
|
+
if (process.platform !== 'win32') return stop.abort();
|
|
349
|
+
void killWindowsTree(command.child?.pid).finally(() => stop.abort());
|
|
178
350
|
};
|
|
179
|
-
|
|
351
|
+
cancel.addEventListener('abort', stopTree, { once: true });
|
|
180
352
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
353
|
+
try {
|
|
354
|
+
for await (const chunk of command.stream()) {
|
|
355
|
+
if (chunk.type === 'exit') {
|
|
356
|
+
exit = chunk.code;
|
|
357
|
+
} else {
|
|
358
|
+
let decoder = decoders.get(chunk.type);
|
|
359
|
+
if (!decoder) {
|
|
360
|
+
decoder = new TextDecoder();
|
|
361
|
+
decoders.set(chunk.type, decoder);
|
|
362
|
+
}
|
|
363
|
+
append(decoder.decode(chunk.data, { stream: true }));
|
|
364
|
+
}
|
|
192
365
|
}
|
|
366
|
+
for (const decoder of decoders.values()) append(decoder.decode());
|
|
367
|
+
} finally {
|
|
368
|
+
// ctx.abort outlives the call, so a listener left on it would keep
|
|
369
|
+
// this runner and its output reachable until the session ends.
|
|
370
|
+
cancel.removeEventListener('abort', stopTree);
|
|
193
371
|
}
|
|
194
372
|
|
|
195
|
-
cancel.removeEventListener('abort', stopTree);
|
|
196
|
-
|
|
197
373
|
// command-stream reports a shell that died from a signal as exit 0
|
|
198
374
|
// (link-foundation/command-stream#208).
|
|
199
375
|
// Its result keeps the native child, which has the signal name.
|
|
@@ -211,9 +387,20 @@ export const BashTool = Tool.define('bash', {
|
|
|
211
387
|
if (timedOut || aborted) signal = KILL_SIGNAL;
|
|
212
388
|
if (signal) exit = null;
|
|
213
389
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
output
|
|
390
|
+
let output: string;
|
|
391
|
+
if (legacy) {
|
|
392
|
+
output =
|
|
393
|
+
head.length > LEGACY_MAX_OUTPUT_LENGTH
|
|
394
|
+
? head.slice(0, LEGACY_MAX_OUTPUT_LENGTH) + LEGACY_TRUNCATED
|
|
395
|
+
: head;
|
|
396
|
+
} else {
|
|
397
|
+
const end = tail(
|
|
398
|
+
chunks.map((item) => item.text).join(''),
|
|
399
|
+
MAX_LINES,
|
|
400
|
+
MAX_BYTES
|
|
401
|
+
);
|
|
402
|
+
output =
|
|
403
|
+
(dropped || end.cut ? '...output truncated...\n\n' : '') + end.text;
|
|
217
404
|
}
|
|
218
405
|
|
|
219
406
|
if (timedOut) {
|
package/src/tool/bash.txt
CHANGED
|
@@ -20,7 +20,7 @@ Usage notes:
|
|
|
20
20
|
- The command argument is required.
|
|
21
21
|
- You can specify an optional timeout in milliseconds (up to 600000ms / 10 minutes). If not specified, commands will timeout after 120000ms (2 minutes).
|
|
22
22
|
- It is very helpful if you write a clear, concise description of what this command does in 5-10 words.
|
|
23
|
-
- If the output exceeds 30000
|
|
23
|
+
- If the output exceeds 2000 lines or 30000 bytes, the beginning of the output is dropped and only the end, where errors usually are, is returned to you.
|
|
24
24
|
- VERY IMPORTANT: You MUST avoid using search commands like `find` and `grep`. Instead use Grep, Glob, or Task to search. You MUST avoid read tools like `cat`, `head`, `tail`, and `ls`, and use Read and List to read files.
|
|
25
25
|
- If you _still_ need to run `grep`, STOP. ALWAYS USE ripgrep at `rg` (or /usr/bin/rg) first, which all opencode users have pre-installed.
|
|
26
26
|
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings).
|
package/src/tool/codesearch.ts
CHANGED
|
@@ -2,16 +2,10 @@ import z from 'zod';
|
|
|
2
2
|
import { Tool } from './tool';
|
|
3
3
|
import DESCRIPTION from './codesearch.txt';
|
|
4
4
|
import { createVerboseFetch } from '../util/verbose-fetch';
|
|
5
|
+
import { ExaMcp } from '../util/exa-mcp';
|
|
5
6
|
|
|
6
7
|
const verboseFetch = createVerboseFetch(fetch, { caller: 'codesearch' });
|
|
7
8
|
|
|
8
|
-
const API_CONFIG = {
|
|
9
|
-
BASE_URL: 'https://mcp.exa.ai',
|
|
10
|
-
ENDPOINTS: {
|
|
11
|
-
CONTEXT: '/mcp',
|
|
12
|
-
},
|
|
13
|
-
} as const;
|
|
14
|
-
|
|
15
9
|
interface McpCodeRequest {
|
|
16
10
|
jsonrpc: string;
|
|
17
11
|
id: number;
|
|
@@ -25,16 +19,6 @@ interface McpCodeRequest {
|
|
|
25
19
|
};
|
|
26
20
|
}
|
|
27
21
|
|
|
28
|
-
interface McpCodeResponse {
|
|
29
|
-
jsonrpc: string;
|
|
30
|
-
result: {
|
|
31
|
-
content: Array<{
|
|
32
|
-
type: string;
|
|
33
|
-
text: string;
|
|
34
|
-
}>;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
22
|
export const CodeSearchTool = Tool.define('codesearch', {
|
|
39
23
|
description: DESCRIPTION,
|
|
40
24
|
parameters: z.object({
|
|
@@ -67,67 +51,20 @@ export const CodeSearchTool = Tool.define('codesearch', {
|
|
|
67
51
|
},
|
|
68
52
|
};
|
|
69
53
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
const response = await verboseFetch(
|
|
80
|
-
`${API_CONFIG.BASE_URL}${API_CONFIG.ENDPOINTS.CONTEXT}`,
|
|
81
|
-
{
|
|
82
|
-
method: 'POST',
|
|
83
|
-
headers,
|
|
84
|
-
body: JSON.stringify(codeRequest),
|
|
85
|
-
signal: AbortSignal.any([controller.signal, ctx.abort]),
|
|
86
|
-
}
|
|
87
|
-
);
|
|
54
|
+
const output = await ExaMcp.call({
|
|
55
|
+
fetch: verboseFetch,
|
|
56
|
+
body: codeRequest,
|
|
57
|
+
timeout: 30000,
|
|
58
|
+
abort: ctx.abort,
|
|
59
|
+
label: 'Code search',
|
|
60
|
+
});
|
|
88
61
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const responseText = await response.text();
|
|
97
|
-
|
|
98
|
-
// Parse SSE response
|
|
99
|
-
const lines = responseText.split('\n');
|
|
100
|
-
for (const line of lines) {
|
|
101
|
-
if (line.startsWith('data: ')) {
|
|
102
|
-
const data: McpCodeResponse = JSON.parse(line.substring(6));
|
|
103
|
-
if (
|
|
104
|
-
data.result &&
|
|
105
|
-
data.result.content &&
|
|
106
|
-
data.result.content.length > 0
|
|
107
|
-
) {
|
|
108
|
-
return {
|
|
109
|
-
output: data.result.content[0].text,
|
|
110
|
-
title: `Code search: ${params.query}`,
|
|
111
|
-
metadata: {},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return {
|
|
118
|
-
output:
|
|
119
|
-
'No code snippets or documentation found. Please try a different query, be more specific about the library or programming concept, or check the spelling of framework names.',
|
|
120
|
-
title: `Code search: ${params.query}`,
|
|
121
|
-
metadata: {},
|
|
122
|
-
};
|
|
123
|
-
} catch (error) {
|
|
124
|
-
clearTimeout(timeoutId);
|
|
125
|
-
|
|
126
|
-
if (error instanceof Error && error.name === 'AbortError') {
|
|
127
|
-
throw new Error('Code search request timed out');
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
throw error;
|
|
131
|
-
}
|
|
62
|
+
return {
|
|
63
|
+
output:
|
|
64
|
+
output ??
|
|
65
|
+
'No code snippets or documentation found. Please try a different query, be more specific about the library or programming concept, or check the spelling of framework names.',
|
|
66
|
+
title: `Code search: ${params.query}`,
|
|
67
|
+
metadata: {},
|
|
68
|
+
};
|
|
132
69
|
},
|
|
133
70
|
});
|