@fishdivinity/dsh-local-search 0.1.0 → 0.2.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/LICENSE +21 -21
- package/README-zh.md +167 -166
- package/README.md +177 -176
- package/cordis.patch.yml +9 -9
- package/icon.svg +35 -7
- package/lib/anytxt.js +272 -252
- package/lib/client.js +623 -0
- package/lib/config.js +101 -0
- package/lib/engines.js +85 -85
- package/lib/errors.js +18 -18
- package/lib/everything.js +277 -219
- package/lib/format.js +101 -92
- package/lib/index.js +77 -71
- package/lib/limits.js +14 -0
- package/lib/probe.js +88 -0
- package/lib/ref.js +38 -38
- package/lib/roots.js +114 -86
- package/lib/route.js +170 -0
- package/lib/tools.js +498 -506
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +86 -65
package/lib/tools.js
CHANGED
|
@@ -1,506 +1,498 @@
|
|
|
1
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
-
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
|
3
|
-
import { EngineError } from './errors.js'
|
|
4
|
-
import { engineNames, resolveContentEngine, resolveFilesEngine } from './engines.js'
|
|
5
|
-
import { formatContent, formatFiles, formatFragments, formatRead, formatStatus } from './format.js'
|
|
6
|
-
import { parseRef, assertRefEngine } from './ref.js'
|
|
7
|
-
import { defaultAllRoots, parseAllRoots, rootsFor } from './roots.js'
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (error instanceof
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return value
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function
|
|
50
|
-
return (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
if (!
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
description:
|
|
290
|
-
},
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
}
|
|
311
|
-
},
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}),
|
|
500
|
-
}
|
|
501
|
-
})
|
|
502
|
-
},
|
|
503
|
-
})
|
|
504
|
-
|
|
505
|
-
return [filesTool, contentTool, fragmentsTool, readTool, statusTool]
|
|
506
|
-
}
|
|
1
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
2
|
+
import { HarnessError } from '@deepseek-ai/dsh-llm'
|
|
3
|
+
import { EngineError } from './errors.js'
|
|
4
|
+
import { engineNames, resolveContentEngine, resolveFilesEngine } from './engines.js'
|
|
5
|
+
import { formatContent, formatFiles, formatFragments, formatRead, formatStatus } from './format.js'
|
|
6
|
+
import { parseRef, assertRefEngine } from './ref.js'
|
|
7
|
+
import { defaultAllRoots, parseAllRoots, rootsFor } from './roots.js'
|
|
8
|
+
import { readConfig } from './config.js'
|
|
9
|
+
import { probeEngines } from './probe.js'
|
|
10
|
+
import { MAX_LIMIT, MAX_READ } from './limits.js'
|
|
11
|
+
|
|
12
|
+
const STATUS_ENGINES = ['auto', ...new Set([...engineNames().files, ...engineNames().content])]
|
|
13
|
+
const MAX_FRAGMENTS = 20
|
|
14
|
+
|
|
15
|
+
function toHarnessError(error) {
|
|
16
|
+
if (error instanceof HarnessError) return error
|
|
17
|
+
if (error instanceof Error) {
|
|
18
|
+
return new HarnessError(error.message, error.code || 'LOCAL_SEARCH_ERROR', { cause: error })
|
|
19
|
+
}
|
|
20
|
+
return new HarnessError(String(error), 'LOCAL_SEARCH_ERROR')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function guard(fn) {
|
|
24
|
+
try {
|
|
25
|
+
return await fn()
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw toHarnessError(error)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Coerce an optional numeric argument into `[1, cap]`. A non-numeric argument
|
|
33
|
+
* falls back to the configured default instead of turning the window into NaN,
|
|
34
|
+
* which would silently return nothing.
|
|
35
|
+
* @param value - the argument as the model sent it
|
|
36
|
+
* @param fallback - default to use when it is not a finite number
|
|
37
|
+
* @param cap - inclusive upper bound
|
|
38
|
+
* @returns the usable count
|
|
39
|
+
*/
|
|
40
|
+
function clampCount(value, fallback, cap) {
|
|
41
|
+
const number = Number.isFinite(value) ? Math.floor(value) : fallback
|
|
42
|
+
return Math.max(1, Math.min(cap, number))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function clampLimit(value, fallback) {
|
|
46
|
+
return clampCount(value, fallback, MAX_LIMIT)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function clampOffset(value) {
|
|
50
|
+
return Math.max(0, Number.isFinite(value) ? Math.floor(value) : 0)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function requireQuery(query) {
|
|
54
|
+
const value = typeof query === 'string' ? query.trim() : ''
|
|
55
|
+
if (value.length === 0) throw new Error('query must be a non-empty string')
|
|
56
|
+
return value
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function validateEngine(engine, allowed) {
|
|
60
|
+
const value = engine || 'auto'
|
|
61
|
+
if (!allowed.includes(value)) {
|
|
62
|
+
throw new Error(`engine must be one of ${allowed.join(', ')}`)
|
|
63
|
+
}
|
|
64
|
+
return value
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function presentSearchCall(label) {
|
|
68
|
+
return (args) => ({
|
|
69
|
+
card: 'generic',
|
|
70
|
+
title: `${label}${args?.query ? `: ${args.query}` : ''}`,
|
|
71
|
+
kind: 'search',
|
|
72
|
+
rawInput: args?.query,
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function presentSearchResult(_args, result) {
|
|
77
|
+
if (result.isError) return undefined
|
|
78
|
+
const meta = result.meta
|
|
79
|
+
if (!meta || !Array.isArray(meta.paths)) return undefined
|
|
80
|
+
return {
|
|
81
|
+
card: 'search',
|
|
82
|
+
shape: 'paths',
|
|
83
|
+
paths: meta.paths,
|
|
84
|
+
truncated: Boolean(meta.truncated),
|
|
85
|
+
total: typeof meta.total === 'number' ? meta.total : meta.paths.length,
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const filesOutput = {
|
|
90
|
+
schema: {
|
|
91
|
+
type: 'object',
|
|
92
|
+
additionalProperties: false,
|
|
93
|
+
properties: {
|
|
94
|
+
items: {
|
|
95
|
+
type: 'array',
|
|
96
|
+
required: true,
|
|
97
|
+
items: {
|
|
98
|
+
type: 'object',
|
|
99
|
+
additionalProperties: false,
|
|
100
|
+
properties: {
|
|
101
|
+
path: { type: 'string', required: true },
|
|
102
|
+
size: { type: 'number' },
|
|
103
|
+
modified: { type: 'string' },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
truncated: { type: 'boolean', required: true },
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
render: (_args, value) => [{ type: 'text', text: formatFiles(value) }],
|
|
111
|
+
presentationMeta: (_args, value) => ({
|
|
112
|
+
paths: value.items.map((item) => item.path),
|
|
113
|
+
truncated: value.truncated,
|
|
114
|
+
total: value.items.length,
|
|
115
|
+
}),
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const contentOutput = {
|
|
119
|
+
schema: {
|
|
120
|
+
type: 'object',
|
|
121
|
+
additionalProperties: false,
|
|
122
|
+
properties: {
|
|
123
|
+
items: {
|
|
124
|
+
type: 'array',
|
|
125
|
+
required: true,
|
|
126
|
+
items: {
|
|
127
|
+
type: 'object',
|
|
128
|
+
additionalProperties: false,
|
|
129
|
+
properties: {
|
|
130
|
+
ref: { type: 'string', required: true },
|
|
131
|
+
engine: { type: 'string', required: true },
|
|
132
|
+
path: { type: 'string', required: true },
|
|
133
|
+
size: { type: 'number' },
|
|
134
|
+
modified: { type: 'string' },
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
total: { type: 'number', required: true },
|
|
139
|
+
truncated: { type: 'boolean', required: true },
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
render: (_args, value) => [{ type: 'text', text: formatContent(value) }],
|
|
143
|
+
presentationMeta: (_args, value) => ({
|
|
144
|
+
paths: value.items.map((item) => item.path),
|
|
145
|
+
truncated: value.truncated,
|
|
146
|
+
total: value.total,
|
|
147
|
+
}),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const fragmentsOutput = {
|
|
151
|
+
schema: {
|
|
152
|
+
type: 'object',
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
properties: {
|
|
155
|
+
fragments: {
|
|
156
|
+
type: 'array',
|
|
157
|
+
required: true,
|
|
158
|
+
items: { type: 'string' },
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
render: (_args, value) => [{ type: 'text', text: formatFragments(value) }],
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const readOutput = {
|
|
166
|
+
schema: {
|
|
167
|
+
type: 'object',
|
|
168
|
+
additionalProperties: false,
|
|
169
|
+
properties: {
|
|
170
|
+
text: { type: 'string', required: true },
|
|
171
|
+
totalChars: { type: 'number', required: true },
|
|
172
|
+
offset: { type: 'number', required: true },
|
|
173
|
+
returned: { type: 'number', required: true },
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
render: (_args, value) => [{ type: 'text', text: formatRead(value) }],
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const statusOutput = {
|
|
180
|
+
schema: {
|
|
181
|
+
type: 'object',
|
|
182
|
+
additionalProperties: false,
|
|
183
|
+
properties: {
|
|
184
|
+
text: { type: 'string', required: true },
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
render: (_args, value) => [{ type: 'text', text: value.text }],
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Build and register the five model-facing tools.
|
|
192
|
+
* @param {import('@deepseek-ai/cordis').Context} ctx
|
|
193
|
+
* @param {object} config
|
|
194
|
+
* @returns {Array<() => void>} tool disposers
|
|
195
|
+
*/
|
|
196
|
+
export function createTools(ctx, config) {
|
|
197
|
+
// Registration fixes the tool deadlines and the text describing defaults;
|
|
198
|
+
// everything a call acts on is read per operation, so a save on the config
|
|
199
|
+
// page applies to the next call instead of the next reload.
|
|
200
|
+
const initial = readConfig(config)
|
|
201
|
+
const names = engineNames()
|
|
202
|
+
|
|
203
|
+
async function resolveWorkspace(exec, cfg) {
|
|
204
|
+
if (cfg.workspaceRoot) return cfg.workspaceRoot
|
|
205
|
+
if (exec.agent && ctx.workingDirectory?.ensure) {
|
|
206
|
+
try {
|
|
207
|
+
return await ctx.workingDirectory.ensure(exec.agent, exec.signal)
|
|
208
|
+
} catch (error) {
|
|
209
|
+
const wrapped = new Error(`cannot resolve the session working directory: ${error.message}`)
|
|
210
|
+
wrapped.code = 'WORKSPACE_UNAVAILABLE'
|
|
211
|
+
wrapped.cause = error
|
|
212
|
+
throw wrapped
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return ctx.workingDirectory?.defaultDirectory || process.cwd()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const filesTool = defineTool({
|
|
219
|
+
name: 'local_search_files',
|
|
220
|
+
description:
|
|
221
|
+
'Use this when the user asks for files or folders by name/path, especially outside the current workspace or across drives. ' +
|
|
222
|
+
'It queries the Everything index (NTFS metadata only: names, paths, size, dates; it does NOT search file contents). ' +
|
|
223
|
+
'Do not use it for text inside PDF/Office documents (use local_search_content) or for source-code content search (use grep). ' +
|
|
224
|
+
'Default scope is the workspace; set scope="all" only when the user explicitly asks for a cross-drive/global search.',
|
|
225
|
+
parameters: {
|
|
226
|
+
query: {
|
|
227
|
+
type: 'string',
|
|
228
|
+
required: true,
|
|
229
|
+
description: 'Everything search text: wildcards, ext:, size:, dm:, path:, | and ! operators.',
|
|
230
|
+
},
|
|
231
|
+
scope: {
|
|
232
|
+
type: 'string',
|
|
233
|
+
description: 'workspace (default), work, personal, or all.',
|
|
234
|
+
},
|
|
235
|
+
path: {
|
|
236
|
+
type: 'string',
|
|
237
|
+
description: 'Optional absolute directory; overrides scope.',
|
|
238
|
+
},
|
|
239
|
+
engine: {
|
|
240
|
+
type: 'string',
|
|
241
|
+
description: `auto (default) or ${names.files.join(', ')}. Use auto unless the user asks for a specific engine.`,
|
|
242
|
+
},
|
|
243
|
+
limit: {
|
|
244
|
+
type: 'number',
|
|
245
|
+
description: `Max results (default ${initial.maxResults}; max ${MAX_LIMIT}).`,
|
|
246
|
+
},
|
|
247
|
+
},
|
|
248
|
+
timeoutMs: initial.timeoutMs,
|
|
249
|
+
output: filesOutput,
|
|
250
|
+
async execute(args, exec) {
|
|
251
|
+
return guard(async () => {
|
|
252
|
+
const cfg = readConfig(config)
|
|
253
|
+
const query = requireQuery(args.query)
|
|
254
|
+
const engine = resolveFilesEngine(args.engine)
|
|
255
|
+
const scope = args.scope || 'workspace'
|
|
256
|
+
const workspace = await resolveWorkspace(exec, cfg)
|
|
257
|
+
let dir
|
|
258
|
+
if (scope === 'all' && !args.path) {
|
|
259
|
+
dir = undefined
|
|
260
|
+
} else {
|
|
261
|
+
dir = rootsFor(scope, args.path, cfg, workspace)[0]
|
|
262
|
+
}
|
|
263
|
+
return await engine.search({
|
|
264
|
+
query,
|
|
265
|
+
dir,
|
|
266
|
+
limit: clampLimit(args.limit, cfg.maxResults),
|
|
267
|
+
esPath: cfg.esPath,
|
|
268
|
+
timeoutMs: initial.timeoutMs,
|
|
269
|
+
signal: exec.signal,
|
|
270
|
+
encoding: cfg.esEncoding,
|
|
271
|
+
})
|
|
272
|
+
})
|
|
273
|
+
},
|
|
274
|
+
presentCall: presentSearchCall('Everything search'),
|
|
275
|
+
presentResult: presentSearchResult,
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
const contentTool = defineTool({
|
|
279
|
+
name: 'local_search_content',
|
|
280
|
+
description:
|
|
281
|
+
'Use this when the user asks for text inside PDF, DOCX, XLSX, PPTX, EPUB, RTF, TXT or MD documents, or for a whole-disk content search. ' +
|
|
282
|
+
'It queries the Anytxt index and returns refs for local_search_fragments/local_search_read. ' +
|
|
283
|
+
'Do not use it for source-code search in the workspace (use grep; the index can be stale). ' +
|
|
284
|
+
'Default scope is the workspace; set scope="all" only when the user explicitly asks for a cross-drive/global search.',
|
|
285
|
+
parameters: {
|
|
286
|
+
query: {
|
|
287
|
+
type: 'string',
|
|
288
|
+
required: true,
|
|
289
|
+
description: 'Keywords; supports & | ! ( ) and "exact phrase".',
|
|
290
|
+
},
|
|
291
|
+
scope: {
|
|
292
|
+
type: 'string',
|
|
293
|
+
description: 'workspace (default), work, personal, or all.',
|
|
294
|
+
},
|
|
295
|
+
path: {
|
|
296
|
+
type: 'string',
|
|
297
|
+
description: 'Optional absolute directory; overrides scope.',
|
|
298
|
+
},
|
|
299
|
+
ext: {
|
|
300
|
+
type: 'string',
|
|
301
|
+
description: 'Optional extension filter, e.g. "pdf;docx".',
|
|
302
|
+
},
|
|
303
|
+
engine: {
|
|
304
|
+
type: 'string',
|
|
305
|
+
description: `auto (default) or ${names.content.join(', ')}. Use auto unless the user asks for a specific engine.`,
|
|
306
|
+
},
|
|
307
|
+
limit: {
|
|
308
|
+
type: 'number',
|
|
309
|
+
description: `Max files (default ${initial.maxResults}; max ${MAX_LIMIT}).`,
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
timeoutMs: initial.timeoutMs,
|
|
313
|
+
output: contentOutput,
|
|
314
|
+
async execute(args, exec) {
|
|
315
|
+
return guard(async () => {
|
|
316
|
+
const cfg = readConfig(config)
|
|
317
|
+
const query = requireQuery(args.query)
|
|
318
|
+
const engine = resolveContentEngine(args.engine)
|
|
319
|
+
const workspace = await resolveWorkspace(exec, cfg)
|
|
320
|
+
const roots = rootsFor(args.scope || 'workspace', args.path, cfg, workspace)
|
|
321
|
+
const result = await engine.search({
|
|
322
|
+
query,
|
|
323
|
+
roots,
|
|
324
|
+
ext: args.ext,
|
|
325
|
+
limit: clampLimit(args.limit, cfg.maxResults),
|
|
326
|
+
url: cfg.anytxtUrl,
|
|
327
|
+
timeoutMs: initial.timeoutMs,
|
|
328
|
+
signal: exec.signal,
|
|
329
|
+
})
|
|
330
|
+
return result
|
|
331
|
+
})
|
|
332
|
+
},
|
|
333
|
+
presentCall: presentSearchCall('Anytxt content search'),
|
|
334
|
+
presentResult: presentSearchResult,
|
|
335
|
+
})
|
|
336
|
+
|
|
337
|
+
const fragmentsTool = defineTool({
|
|
338
|
+
name: 'local_search_fragments',
|
|
339
|
+
description:
|
|
340
|
+
'Use this after local_search_content to get the text around a match inside one indexed file. ' +
|
|
341
|
+
'Returns a few snippets, not the whole document; use local_search_read for the full extracted text.',
|
|
342
|
+
parameters: {
|
|
343
|
+
ref: {
|
|
344
|
+
type: 'string',
|
|
345
|
+
required: true,
|
|
346
|
+
description: 'Reference returned by local_search_content.',
|
|
347
|
+
},
|
|
348
|
+
engine: {
|
|
349
|
+
type: 'string',
|
|
350
|
+
description: 'auto (default) or a content engine name; refs are self-describing, so usually omit.',
|
|
351
|
+
},
|
|
352
|
+
query: {
|
|
353
|
+
type: 'string',
|
|
354
|
+
required: true,
|
|
355
|
+
description: 'Keyword whose surrounding text should be returned.',
|
|
356
|
+
},
|
|
357
|
+
max: {
|
|
358
|
+
type: 'number',
|
|
359
|
+
description: `Max snippets (default 8; max ${MAX_FRAGMENTS}).`,
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
timeoutMs: initial.timeoutMs,
|
|
363
|
+
output: fragmentsOutput,
|
|
364
|
+
async execute(args, exec) {
|
|
365
|
+
return guard(async () => {
|
|
366
|
+
const cfg = readConfig(config)
|
|
367
|
+
if (typeof args.ref !== 'string' || args.ref.trim().length === 0) {
|
|
368
|
+
throw new Error('ref must be a non-empty string')
|
|
369
|
+
}
|
|
370
|
+
const query = requireQuery(args.query)
|
|
371
|
+
const parsed = parseRef(args.ref)
|
|
372
|
+
const requested = args.engine && args.engine !== 'auto' ? args.engine : parsed.engine
|
|
373
|
+
const engine = resolveContentEngine(requested)
|
|
374
|
+
assertRefEngine(parsed, engine.name)
|
|
375
|
+
if (typeof engine.fragments !== 'function') {
|
|
376
|
+
throw new EngineError(`content engine "${engine.name}" does not support fragments`, 'ENGINE_READ_UNSUPPORTED', {
|
|
377
|
+
hint: 'Use local_search_content or a different content engine.',
|
|
378
|
+
})
|
|
379
|
+
}
|
|
380
|
+
const max = clampCount(args.max, 8, MAX_FRAGMENTS)
|
|
381
|
+
const fragments = await engine.fragments({
|
|
382
|
+
ref: args.ref,
|
|
383
|
+
query,
|
|
384
|
+
url: cfg.anytxtUrl,
|
|
385
|
+
timeoutMs: initial.timeoutMs,
|
|
386
|
+
signal: exec.signal,
|
|
387
|
+
})
|
|
388
|
+
return { fragments: fragments.slice(0, max) }
|
|
389
|
+
})
|
|
390
|
+
},
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
const readTool = defineTool({
|
|
394
|
+
name: 'local_search_read',
|
|
395
|
+
description:
|
|
396
|
+
'Use this to read the extracted plain text of one indexed document by ref. The result is capped; ' +
|
|
397
|
+
'for normal text files in the workspace use read instead of this tool.',
|
|
398
|
+
parameters: {
|
|
399
|
+
ref: {
|
|
400
|
+
type: 'string',
|
|
401
|
+
required: true,
|
|
402
|
+
description: 'Reference returned by local_search_content.',
|
|
403
|
+
},
|
|
404
|
+
engine: {
|
|
405
|
+
type: 'string',
|
|
406
|
+
description: 'auto (default) or a content engine name; refs are self-describing, so usually omit.',
|
|
407
|
+
},
|
|
408
|
+
offset: {
|
|
409
|
+
type: 'number',
|
|
410
|
+
description: 'Character offset (default 0).',
|
|
411
|
+
},
|
|
412
|
+
max: {
|
|
413
|
+
type: 'number',
|
|
414
|
+
description: `Max characters (default ${initial.readMaxChars}; max ${MAX_READ}).`,
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
timeoutMs: initial.timeoutMs,
|
|
418
|
+
output: readOutput,
|
|
419
|
+
async execute(args, exec) {
|
|
420
|
+
return guard(async () => {
|
|
421
|
+
const cfg = readConfig(config)
|
|
422
|
+
if (typeof args.ref !== 'string' || args.ref.trim().length === 0) {
|
|
423
|
+
throw new Error('ref must be a non-empty string')
|
|
424
|
+
}
|
|
425
|
+
const max = clampCount(args.max, cfg.readMaxChars, MAX_READ)
|
|
426
|
+
const offset = clampOffset(args.offset)
|
|
427
|
+
const parsed = parseRef(args.ref)
|
|
428
|
+
const requested = args.engine && args.engine !== 'auto' ? args.engine : parsed.engine
|
|
429
|
+
const engine = resolveContentEngine(requested)
|
|
430
|
+
assertRefEngine(parsed, engine.name)
|
|
431
|
+
if (typeof engine.read !== 'function') {
|
|
432
|
+
throw new EngineError(`content engine "${engine.name}" does not support read`, 'ENGINE_READ_UNSUPPORTED', {
|
|
433
|
+
hint: 'Use local_search_content or a different content engine.',
|
|
434
|
+
})
|
|
435
|
+
}
|
|
436
|
+
const text = await engine.read({
|
|
437
|
+
ref: args.ref,
|
|
438
|
+
url: cfg.anytxtUrl,
|
|
439
|
+
timeoutMs: initial.timeoutMs,
|
|
440
|
+
signal: exec.signal,
|
|
441
|
+
})
|
|
442
|
+
const slice = text.slice(offset, offset + max)
|
|
443
|
+
return { text: slice, totalChars: text.length, offset, returned: slice.length }
|
|
444
|
+
})
|
|
445
|
+
},
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
const statusTool = defineTool({
|
|
449
|
+
name: 'local_search_status',
|
|
450
|
+
description:
|
|
451
|
+
'Use this when a search reports EVERYTHING_DOWN or ANYTXT_DOWN, or before a large batch, to check whether the Everything index and the Anytxt JSON-RPC service are reachable. ' +
|
|
452
|
+
'Returns both statuses plus the configured search roots.',
|
|
453
|
+
parameters: {
|
|
454
|
+
engine: {
|
|
455
|
+
type: 'string',
|
|
456
|
+
description: `auto (default), ${[...names.files, ...names.content].join(', ')}. Limit the probe to one engine.`,
|
|
457
|
+
},
|
|
458
|
+
},
|
|
459
|
+
timeoutMs: initial.timeoutMs,
|
|
460
|
+
output: statusOutput,
|
|
461
|
+
async execute(args, exec) {
|
|
462
|
+
return guard(async () => {
|
|
463
|
+
const cfg = readConfig(config)
|
|
464
|
+
const engine = validateEngine(args.engine, STATUS_ENGINES)
|
|
465
|
+
let workspace = cfg.workspaceRoot || ctx.workingDirectory?.defaultDirectory || process.cwd()
|
|
466
|
+
try {
|
|
467
|
+
workspace = await resolveWorkspace(exec, cfg)
|
|
468
|
+
} catch {
|
|
469
|
+
// Status must still report the engines even when the workspace is gone.
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// The same probe the configuration page's Test control asks for.
|
|
473
|
+
const { everything, anytxt } = await probeEngines(config, {
|
|
474
|
+
only: engine,
|
|
475
|
+
workspace,
|
|
476
|
+
signal: exec.signal,
|
|
477
|
+
})
|
|
478
|
+
|
|
479
|
+
const all = cfg.allRoots ? parseAllRoots(cfg.allRoots) : defaultAllRoots()
|
|
480
|
+
return {
|
|
481
|
+
text: formatStatus({
|
|
482
|
+
everything,
|
|
483
|
+
anytxt,
|
|
484
|
+
engines: engineNames(),
|
|
485
|
+
roots: {
|
|
486
|
+
workspace,
|
|
487
|
+
work: cfg.workRoot,
|
|
488
|
+
personal: cfg.personalRoot,
|
|
489
|
+
all,
|
|
490
|
+
},
|
|
491
|
+
}),
|
|
492
|
+
}
|
|
493
|
+
})
|
|
494
|
+
},
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
return [filesTool, contentTool, fragmentsTool, readTool, statusTool]
|
|
498
|
+
}
|