skyloom 1.14.6 → 1.15.0
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/.github/workflows/ci.yml +2 -2
- package/.github/workflows/publish.yml +74 -0
- package/CONVERSION_PLAN.md +191 -191
- package/README.md +523 -220
- package/config/default.yaml +46 -43
- package/config/models.yaml +928 -155
- package/config/providers.yaml +109 -6
- package/dist/agents/snow.d.ts +2 -0
- package/dist/agents/snow.d.ts.map +1 -1
- package/dist/agents/snow.js +36 -5
- package/dist/agents/snow.js.map +1 -1
- package/dist/cli/loom_chat.d.ts.map +1 -1
- package/dist/cli/loom_chat.js +207 -1
- package/dist/cli/loom_chat.js.map +1 -1
- package/dist/cli/main.js +190 -40
- package/dist/cli/main.js.map +1 -1
- package/dist/cli/tui.d.ts.map +1 -1
- package/dist/cli/tui.js +6 -31
- package/dist/cli/tui.js.map +1 -1
- package/dist/core/agent.d.ts +6 -4
- package/dist/core/agent.d.ts.map +1 -1
- package/dist/core/agent.js +61 -20
- package/dist/core/agent.js.map +1 -1
- package/dist/core/catalog.d.ts.map +1 -1
- package/dist/core/catalog.js +30 -9
- package/dist/core/catalog.js.map +1 -1
- package/dist/core/commands.d.ts +110 -0
- package/dist/core/commands.d.ts.map +1 -0
- package/dist/core/commands.js +633 -0
- package/dist/core/commands.js.map +1 -0
- package/dist/core/concurrency.d.ts +38 -0
- package/dist/core/concurrency.d.ts.map +1 -0
- package/dist/core/concurrency.js +65 -0
- package/dist/core/concurrency.js.map +1 -0
- package/dist/core/factory.js +16 -16
- package/dist/core/file_checkpoint.d.ts +9 -0
- package/dist/core/file_checkpoint.d.ts.map +1 -1
- package/dist/core/file_checkpoint.js +33 -1
- package/dist/core/file_checkpoint.js.map +1 -1
- package/dist/core/llm.d.ts.map +1 -1
- package/dist/core/llm.js +66 -13
- package/dist/core/llm.js.map +1 -1
- package/dist/core/memory.js +51 -51
- package/dist/core/schemas.d.ts +16 -0
- package/dist/core/schemas.d.ts.map +1 -1
- package/dist/core/schemas.js +32 -0
- package/dist/core/schemas.js.map +1 -1
- package/dist/core/security.d.ts.map +1 -1
- package/dist/core/security.js +27 -0
- package/dist/core/security.js.map +1 -1
- package/dist/core/skymd.js +14 -14
- package/dist/core/trace.d.ts +105 -0
- package/dist/core/trace.d.ts.map +1 -0
- package/dist/core/trace.js +213 -0
- package/dist/core/trace.js.map +1 -0
- package/dist/tools/builtin.d.ts +2 -6
- package/dist/tools/builtin.d.ts.map +1 -1
- package/dist/tools/builtin.js +180 -125
- package/dist/tools/builtin.js.map +1 -1
- package/dist/tools/extra.d.ts +13 -0
- package/dist/tools/extra.d.ts.map +1 -0
- package/dist/tools/extra.js +827 -0
- package/dist/tools/extra.js.map +1 -0
- package/dist/tools/guards.d.ts +12 -0
- package/dist/tools/guards.d.ts.map +1 -0
- package/dist/tools/guards.js +143 -0
- package/dist/tools/guards.js.map +1 -0
- package/dist/tools/model_tool.d.ts.map +1 -1
- package/dist/tools/model_tool.js +24 -4
- package/dist/tools/model_tool.js.map +1 -1
- package/dist/web/markdown.d.ts +32 -0
- package/dist/web/markdown.d.ts.map +1 -0
- package/dist/web/markdown.js +202 -0
- package/dist/web/markdown.js.map +1 -0
- package/dist/web/server.d.ts +4 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +14 -582
- package/dist/web/server.js.map +1 -1
- package/dist/web/ui.d.ts +31 -0
- package/dist/web/ui.d.ts.map +1 -0
- package/dist/web/ui.js +1009 -0
- package/dist/web/ui.js.map +1 -0
- package/docs/AESTHETIC_DESIGN.md +152 -152
- package/docs/OPTIMIZATION_PLAN.md +178 -178
- package/package.json +68 -68
- package/src/agents/snow.ts +38 -5
- package/src/cli/commands_md.ts +112 -112
- package/src/cli/input_macros.ts +83 -83
- package/src/cli/loom.ts +1041 -1041
- package/src/cli/loom_chat.ts +772 -603
- package/src/cli/main.ts +853 -723
- package/src/cli/tui.ts +264 -289
- package/src/core/agent/guard.ts +133 -133
- package/src/core/agent/task.ts +100 -100
- package/src/core/agent.ts +1630 -1590
- package/src/core/agent_helpers.ts +500 -500
- package/src/core/bus.ts +221 -221
- package/src/core/cache.ts +153 -153
- package/src/core/catalog.ts +199 -178
- package/src/core/circuit_breaker.ts +119 -119
- package/src/core/commands.ts +704 -0
- package/src/core/concurrency.ts +73 -0
- package/src/core/config.ts +365 -365
- package/src/core/constants.ts +95 -95
- package/src/core/factory.ts +656 -656
- package/src/core/file_checkpoint.ts +163 -136
- package/src/core/hooks.ts +126 -126
- package/src/core/llm.ts +972 -915
- package/src/core/logger.ts +143 -143
- package/src/core/mcp.ts +1001 -1001
- package/src/core/memory.ts +1201 -1201
- package/src/core/middleware.ts +350 -350
- package/src/core/model_config.ts +159 -159
- package/src/core/pipelines.ts +424 -424
- package/src/core/schemas.ts +319 -282
- package/src/core/security.ts +27 -0
- package/src/core/semantic.ts +211 -211
- package/src/core/skill.ts +384 -384
- package/src/core/skymd.ts +143 -143
- package/src/core/theme.ts +65 -65
- package/src/core/tool.ts +457 -457
- package/src/core/trace.ts +236 -0
- package/src/core/verify.ts +71 -71
- package/src/plugins/loader.ts +91 -91
- package/src/skills/loader.ts +75 -75
- package/src/tools/builtin.ts +571 -493
- package/src/tools/computer.ts +279 -279
- package/src/tools/extra.ts +662 -0
- package/src/tools/guards.ts +82 -0
- package/src/tools/model_tool.ts +93 -74
- package/src/tools/todo.ts +76 -76
- package/src/web/markdown.ts +193 -0
- package/src/web/server.ts +117 -693
- package/src/web/ui.ts +949 -0
- package/tests/agent.test.ts +211 -159
- package/tests/agent_helpers.test.ts +48 -48
- package/tests/catalog.test.ts +86 -86
- package/tests/checkpoint_commands.test.ts +124 -124
- package/tests/claude_compat.test.ts +110 -110
- package/tests/commands.test.ts +103 -0
- package/tests/concurrency.test.ts +102 -0
- package/tests/config.test.ts +41 -41
- package/tests/extra_tools.test.ts +212 -0
- package/tests/fence_plugin.test.ts +52 -52
- package/tests/guard.test.ts +75 -75
- package/tests/loom.test.ts +337 -337
- package/tests/memory.test.ts +170 -170
- package/tests/model_config.test.ts +109 -109
- package/tests/skymd.test.ts +146 -146
- package/tests/ssrf.test.ts +38 -38
- package/tests/structured_retry.test.ts +87 -0
- package/tests/task.test.ts +60 -60
- package/tests/todo_toolstats.test.ts +94 -94
- package/tests/trace.test.ts +128 -0
- package/tests/tui.test.ts +67 -67
- package/tests/web.test.ts +169 -0
- package/tsconfig.json +38 -38
package/src/tools/builtin.ts
CHANGED
|
@@ -1,493 +1,571 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Built-in tool registration — registers all default tools.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as fs from 'fs';
|
|
6
|
-
import * as
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const
|
|
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
|
-
{ name: '
|
|
222
|
-
{ name: '
|
|
223
|
-
],
|
|
224
|
-
handler: async (params) => {
|
|
225
|
-
const
|
|
226
|
-
const fenced = fenceCheck(
|
|
227
|
-
|
|
228
|
-
try {
|
|
229
|
-
|
|
230
|
-
const
|
|
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
|
-
return
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
registry.register({
|
|
331
|
-
name: '
|
|
332
|
-
description: '
|
|
333
|
-
parameters: [
|
|
334
|
-
{ name: '
|
|
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
|
-
const
|
|
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
|
-
|
|
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
|
-
const
|
|
473
|
-
try {
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Built-in tool registration — registers all default tools.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import type { ToolRegistry } from '../core/tool';
|
|
8
|
+
import { getLogger } from '../core/logger';
|
|
9
|
+
import { registerComputerTools } from './computer';
|
|
10
|
+
import { registerExtraTools } from './extra';
|
|
11
|
+
import { isPrivateIp, assertFetchAllowed, fenceRoot, fenceCheck } from './guards';
|
|
12
|
+
|
|
13
|
+
// Re-exported so existing importers/tests keep resolving these from builtin.
|
|
14
|
+
export { isPrivateIp, assertFetchAllowed, fenceRoot, fenceCheck };
|
|
15
|
+
|
|
16
|
+
const log = getLogger('builtin-tools');
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/* ── Web search helpers ───────────────────────────────────────────────────
|
|
20
|
+
Multi-engine fallback. DuckDuckGo's Instant Answer JSON API only returns
|
|
21
|
+
"abstracts" and is blank for ~90% of real queries; HTML scraping is what
|
|
22
|
+
actually works. In CN networks, DDG/Bing may be unreachable — Baidu/Sogou
|
|
23
|
+
serve as fallbacks. Each parser is intentionally tolerant: HTML changes
|
|
24
|
+
over time, so we extract loosely and let the engine list provide redundancy.
|
|
25
|
+
────────────────────────────────────────────────────────────────────────── */
|
|
26
|
+
interface SearchResult { title: string; url: string; snippet: string }
|
|
27
|
+
|
|
28
|
+
const SEARCH_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36';
|
|
29
|
+
|
|
30
|
+
async function fetchHtml(url: string, timeoutMs = 12000): Promise<string> {
|
|
31
|
+
const controller = new AbortController();
|
|
32
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
33
|
+
try {
|
|
34
|
+
const res = await fetch(url, {
|
|
35
|
+
headers: {
|
|
36
|
+
'User-Agent': SEARCH_UA,
|
|
37
|
+
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
|
38
|
+
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
|
39
|
+
},
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
});
|
|
42
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
43
|
+
return await res.text();
|
|
44
|
+
} finally {
|
|
45
|
+
clearTimeout(timer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function decodeHtmlEntities(s: string): string {
|
|
50
|
+
return s
|
|
51
|
+
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
52
|
+
.replace(/"/g, '"').replace(/'/g, "'").replace(/ /g, ' ')
|
|
53
|
+
.replace(/&#(\d+);/g, (_, n) => String.fromCharCode(parseInt(n, 10)))
|
|
54
|
+
.replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCharCode(parseInt(n, 16)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function stripTags(s: string): string {
|
|
58
|
+
return decodeHtmlEntities(s.replace(/<[^>]+>/g, '')).replace(/\s+/g, ' ').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function unwrapDdgRedirect(href: string): string {
|
|
62
|
+
// DuckDuckGo HTML wraps results in /l/?uddg=<encoded-url>
|
|
63
|
+
const m = href.match(/[?&]uddg=([^&]+)/);
|
|
64
|
+
if (m) { try { return decodeURIComponent(m[1]); } catch { /* fall through */ } }
|
|
65
|
+
if (href.startsWith('//')) return 'https:' + href;
|
|
66
|
+
return href;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function unwrapBaiduRedirect(href: string): string {
|
|
70
|
+
// Baidu uses opaque /link?url=... redirects; we can't resolve without another request.
|
|
71
|
+
// Return as-is; consumer can still click through.
|
|
72
|
+
return href;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function searchDuckDuckGo(query: string, max: number): Promise<SearchResult[]> {
|
|
76
|
+
const html = await fetchHtml(`https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`);
|
|
77
|
+
const results: SearchResult[] = [];
|
|
78
|
+
const re = /<a[^>]+class="[^"]*result__a[^"]*"[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>[\s\S]*?<a[^>]+class="[^"]*result__snippet[^"]*"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
79
|
+
let m: RegExpExecArray | null;
|
|
80
|
+
while ((m = re.exec(html)) && results.length < max) {
|
|
81
|
+
results.push({ url: unwrapDdgRedirect(m[1]), title: stripTags(m[2]), snippet: stripTags(m[3]) });
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function searchBing(query: string, max: number): Promise<SearchResult[]> {
|
|
87
|
+
const html = await fetchHtml(`https://www.bing.com/search?q=${encodeURIComponent(query)}&setlang=zh-cn`);
|
|
88
|
+
const results: SearchResult[] = [];
|
|
89
|
+
const liRe = /<li class="b_algo"[\s\S]*?<\/li>/gi;
|
|
90
|
+
const items = html.match(liRe) || [];
|
|
91
|
+
for (const item of items) {
|
|
92
|
+
if (results.length >= max) break;
|
|
93
|
+
const a = item.match(/<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i);
|
|
94
|
+
if (!a) continue;
|
|
95
|
+
const snipMatch =
|
|
96
|
+
item.match(/<p class="b_lineclamp[^"]*"[^>]*>([\s\S]*?)<\/p>/i) ||
|
|
97
|
+
item.match(/<div class="b_caption"[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/i) ||
|
|
98
|
+
item.match(/<p[^>]*>([\s\S]*?)<\/p>/i);
|
|
99
|
+
results.push({ url: a[1], title: stripTags(a[2]), snippet: snipMatch ? stripTags(snipMatch[1]) : '' });
|
|
100
|
+
}
|
|
101
|
+
return results;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function searchBaidu(query: string, max: number): Promise<SearchResult[]> {
|
|
105
|
+
const html = await fetchHtml(`https://www.baidu.com/s?wd=${encodeURIComponent(query)}`);
|
|
106
|
+
const results: SearchResult[] = [];
|
|
107
|
+
// Baidu nests divs aggressively; anchor on <h3> ... <a href>...</a> and look
|
|
108
|
+
// for the nearest abstract block following.
|
|
109
|
+
const re = /<h3[^>]*>[\s\S]{0,500}?<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/gi;
|
|
110
|
+
let m: RegExpExecArray | null;
|
|
111
|
+
while ((m = re.exec(html)) && results.length < max) {
|
|
112
|
+
const url = unwrapBaiduRedirect(m[1]);
|
|
113
|
+
const title = stripTags(m[2]);
|
|
114
|
+
if (!title || !/^https?:\/\//.test(url)) continue;
|
|
115
|
+
const after = html.slice(re.lastIndex, re.lastIndex + 4000);
|
|
116
|
+
const snipMatch =
|
|
117
|
+
after.match(/<span class="content-right[^"]*"[^>]*>([\s\S]*?)<\/span>/i) ||
|
|
118
|
+
after.match(/<div class="c-abstract[^"]*"[^>]*>([\s\S]*?)<\/div>/i) ||
|
|
119
|
+
after.match(/<span[^>]*content[^"]*"[^>]*>([\s\S]{20,400}?)<\/span>/i) ||
|
|
120
|
+
after.match(/<p[^>]*>([\s\S]{20,400}?)<\/p>/i);
|
|
121
|
+
results.push({ url, title, snippet: snipMatch ? stripTags(snipMatch[1]) : '' });
|
|
122
|
+
}
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function searchSogou(query: string, max: number): Promise<SearchResult[]> {
|
|
127
|
+
const html = await fetchHtml(`https://www.sogou.com/web?query=${encodeURIComponent(query)}`);
|
|
128
|
+
const results: SearchResult[] = [];
|
|
129
|
+
const divRe = /<div[^>]+class="vrwrap"[\s\S]*?(?=<div[^>]+class="vrwrap"|$)/gi;
|
|
130
|
+
const items = html.match(divRe) || [];
|
|
131
|
+
for (const item of items) {
|
|
132
|
+
if (results.length >= max) break;
|
|
133
|
+
const a = item.match(/<h3[^>]*>[\s\S]*?<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i);
|
|
134
|
+
if (!a) continue;
|
|
135
|
+
let url = a[1];
|
|
136
|
+
if (url.startsWith('/link?')) url = 'https://www.sogou.com' + url;
|
|
137
|
+
const snipMatch =
|
|
138
|
+
item.match(/<div[^>]+class="(?:str_info|fz-mid|space-txt)[^"]*"[^>]*>([\s\S]*?)<\/div>/i) ||
|
|
139
|
+
item.match(/<p[^>]*>([\s\S]{20,400}?)<\/p>/i);
|
|
140
|
+
results.push({ url, title: stripTags(a[2]), snippet: snipMatch ? stripTags(snipMatch[1]) : '' });
|
|
141
|
+
}
|
|
142
|
+
return results;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function runSearchEngine(engine: string, query: string, max: number): Promise<SearchResult[]> {
|
|
146
|
+
let results: SearchResult[];
|
|
147
|
+
switch (engine) {
|
|
148
|
+
case 'duckduckgo': case 'ddg': results = await searchDuckDuckGo(query, max); break;
|
|
149
|
+
case 'bing': results = await searchBing(query, max); break;
|
|
150
|
+
case 'baidu': results = await searchBaidu(query, max); break;
|
|
151
|
+
case 'sogou': results = await searchSogou(query, max); break;
|
|
152
|
+
default: throw new Error(`unknown search engine: ${engine}`);
|
|
153
|
+
}
|
|
154
|
+
// Drop placeholder/JS-anchor entries from inline answer cards.
|
|
155
|
+
return results.filter((r) => r.title && /^https?:\/\//i.test(r.url));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Register all built-in tools into the given registry.
|
|
160
|
+
*/
|
|
161
|
+
export function registerBuiltinTools(registry: ToolRegistry): void {
|
|
162
|
+
// Register computer-operation + extended-capability tools
|
|
163
|
+
registerComputerTools(registry);
|
|
164
|
+
registerExtraTools(registry);
|
|
165
|
+
// ── File Tools ──
|
|
166
|
+
|
|
167
|
+
registry.register({
|
|
168
|
+
name: 'read_file',
|
|
169
|
+
description: 'Read the contents of a file. Large files are paged: pass offset (1-based start line) and limit (line count) to read further sections; use grep to locate the right offset first.',
|
|
170
|
+
parameters: [
|
|
171
|
+
{ name: 'path', type: 'string', description: 'Absolute or relative path to the file', required: true },
|
|
172
|
+
{ name: 'offset', type: 'number', description: '1-based line number to start from (default 1)', required: false },
|
|
173
|
+
{ name: 'limit', type: 'number', description: 'Max lines to return (default 800)', required: false },
|
|
174
|
+
],
|
|
175
|
+
handler: async (params) => {
|
|
176
|
+
const filePath = path.resolve(params.path as string);
|
|
177
|
+
const fenced = fenceCheck(filePath); if (fenced) return fenced;
|
|
178
|
+
if (!fs.existsSync(filePath)) return `Error: File not found: ${filePath}`;
|
|
179
|
+
try {
|
|
180
|
+
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
181
|
+
const offset = Math.max(1, Number(params.offset) || 1);
|
|
182
|
+
const limit = Math.max(1, Math.min(Number(params.limit) || 800, 4000));
|
|
183
|
+
const slice = lines.slice(offset - 1, offset - 1 + limit);
|
|
184
|
+
const remaining = lines.length - (offset - 1 + slice.length);
|
|
185
|
+
const tail = remaining > 0
|
|
186
|
+
? `\n…[还有 ${remaining} 行 — read_file(path, offset=${offset + slice.length}) 继续]`
|
|
187
|
+
: '';
|
|
188
|
+
const range = offset > 1 || remaining > 0 ? ` lines ${offset}-${offset + slice.length - 1}/${lines.length}` : '';
|
|
189
|
+
return `Successfully read ${filePath}${range}:\n${slice.join('\n')}${tail}`;
|
|
190
|
+
} catch (e) {
|
|
191
|
+
return `Error reading file: ${e}`;
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
registry.register({
|
|
197
|
+
name: 'write_file',
|
|
198
|
+
description: 'Write content to a file at the given path. Creates directories if needed.',
|
|
199
|
+
parameters: [
|
|
200
|
+
{ name: 'path', type: 'string', description: 'Absolute or relative path to write to', required: true },
|
|
201
|
+
{ name: 'content', type: 'string', description: 'Content to write to the file', required: true },
|
|
202
|
+
],
|
|
203
|
+
handler: async (params) => {
|
|
204
|
+
const filePath = path.resolve(params.path as string);
|
|
205
|
+
const fenced = fenceCheck(filePath); if (fenced) return fenced;
|
|
206
|
+
try {
|
|
207
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
208
|
+
fs.writeFileSync(filePath, params.content as string, 'utf-8');
|
|
209
|
+
return `Successfully wrote ${Buffer.byteLength(params.content as string, 'utf-8')} bytes to ${filePath}`;
|
|
210
|
+
} catch (e) {
|
|
211
|
+
return `Error writing file: ${e}`;
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
registry.register({
|
|
217
|
+
name: 'edit_file',
|
|
218
|
+
description: 'Edit a file by replacing old_text with new_text. Use this for targeted edits.',
|
|
219
|
+
parameters: [
|
|
220
|
+
{ name: 'path', type: 'string', description: 'Path to the file to edit', required: true },
|
|
221
|
+
{ name: 'old_text', type: 'string', description: 'Text to search for and replace', required: true },
|
|
222
|
+
{ name: 'new_text', type: 'string', description: 'Text to replace with', required: true },
|
|
223
|
+
],
|
|
224
|
+
handler: async (params) => {
|
|
225
|
+
const filePath = path.resolve(params.path as string);
|
|
226
|
+
const fenced = fenceCheck(filePath); if (fenced) return fenced;
|
|
227
|
+
if (!fs.existsSync(filePath)) return `Error: File not found: ${filePath}`;
|
|
228
|
+
try {
|
|
229
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
230
|
+
const oldText = params.old_text as string;
|
|
231
|
+
const newText = params.new_text as string;
|
|
232
|
+
if (!content.includes(oldText)) {
|
|
233
|
+
return `Error: old_text not found in file. Searched for: ${oldText.slice(0, 50)}...`;
|
|
234
|
+
}
|
|
235
|
+
content = content.replace(oldText, newText);
|
|
236
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
237
|
+
return `Successfully edited ${filePath}`;
|
|
238
|
+
} catch (e) {
|
|
239
|
+
return `Error editing file: ${e}`;
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
registry.register({
|
|
245
|
+
name: 'delete_file',
|
|
246
|
+
description: 'Delete a file at the given path.',
|
|
247
|
+
parameters: [
|
|
248
|
+
{ name: 'path', type: 'string', description: 'Path to the file to delete', required: true },
|
|
249
|
+
],
|
|
250
|
+
handler: async (params) => {
|
|
251
|
+
const filePath = path.resolve(params.path as string);
|
|
252
|
+
const fenced = fenceCheck(filePath); if (fenced) return fenced;
|
|
253
|
+
if (!fs.existsSync(filePath)) return `Error: File not found: ${filePath}`;
|
|
254
|
+
try {
|
|
255
|
+
fs.unlinkSync(filePath);
|
|
256
|
+
return `Successfully deleted ${filePath}`;
|
|
257
|
+
} catch (e) {
|
|
258
|
+
return `Error deleting file: ${e}`;
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
registry.register({
|
|
264
|
+
name: 'list_directory',
|
|
265
|
+
description: 'List files and directories at the given path.',
|
|
266
|
+
parameters: [
|
|
267
|
+
{ name: 'path', type: 'string', description: 'Path to list', required: true },
|
|
268
|
+
],
|
|
269
|
+
handler: async (params) => {
|
|
270
|
+
const dirPath = path.resolve(params.path as string);
|
|
271
|
+
const fenced = fenceCheck(dirPath); if (fenced) return fenced;
|
|
272
|
+
if (!fs.existsSync(dirPath)) return `Error: Directory not found: ${dirPath}`;
|
|
273
|
+
try {
|
|
274
|
+
const entries = fs.readdirSync(dirPath);
|
|
275
|
+
return entries.map(e => {
|
|
276
|
+
const stat = fs.statSync(path.join(dirPath, e));
|
|
277
|
+
return `${stat.isDirectory() ? '[DIR]' : '[FILE]'} ${e}`;
|
|
278
|
+
}).join('\n');
|
|
279
|
+
} catch (e) {
|
|
280
|
+
return `Error listing directory: ${e}`;
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
registry.register({
|
|
286
|
+
name: 'file_search',
|
|
287
|
+
description: 'Search for files matching a glob pattern.',
|
|
288
|
+
parameters: [
|
|
289
|
+
{ name: 'pattern', type: 'string', description: 'Glob pattern to match (e.g. "**/*.ts")', required: true },
|
|
290
|
+
{ name: 'directory', type: 'string', description: 'Directory to search in (default: cwd)', required: false },
|
|
291
|
+
],
|
|
292
|
+
handler: async (params) => {
|
|
293
|
+
const dir = params.directory ? path.resolve(params.directory as string) : process.cwd();
|
|
294
|
+
const fenced = fenceCheck(dir); if (fenced) return fenced;
|
|
295
|
+
const pattern = params.pattern as string;
|
|
296
|
+
try {
|
|
297
|
+
const { globSync } = require('glob');
|
|
298
|
+
const results = globSync(pattern, { cwd: dir, nodir: true });
|
|
299
|
+
if (results.length === 0) return 'No files found matching the pattern.';
|
|
300
|
+
return results.slice(0, 200).join('\n') + (results.length > 200 ? `\n... and ${results.length - 200} more` : '');
|
|
301
|
+
} catch (e) {
|
|
302
|
+
return `Error searching files: ${e}`;
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ── Shell Tool ──
|
|
308
|
+
|
|
309
|
+
registry.register({
|
|
310
|
+
name: 'run_bash',
|
|
311
|
+
description: 'Execute a shell command and return its output.',
|
|
312
|
+
parameters: [
|
|
313
|
+
{ name: 'command', type: 'string', description: 'Command to execute', required: true },
|
|
314
|
+
{ name: 'timeout', type: 'number', description: 'Timeout in milliseconds (default: 30000)', required: false },
|
|
315
|
+
],
|
|
316
|
+
handler: async (params) => {
|
|
317
|
+
const cmd = params.command as string;
|
|
318
|
+
const timeout = (params.timeout as number) || 30000;
|
|
319
|
+
try {
|
|
320
|
+
const { runInSandbox, formatSandboxResult } = require('../core/sandbox');
|
|
321
|
+
const result = runInSandbox(cmd, { timeoutMs: timeout });
|
|
322
|
+
return formatSandboxResult(result);
|
|
323
|
+
} catch (e: any) { return `Error: ${e.message || e}`; }
|
|
324
|
+
},
|
|
325
|
+
dangerous: true,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ── HTTP Tools ──
|
|
329
|
+
|
|
330
|
+
registry.register({
|
|
331
|
+
name: 'http_get',
|
|
332
|
+
description: 'Make an HTTP GET request to a URL.',
|
|
333
|
+
parameters: [
|
|
334
|
+
{ name: 'url', type: 'string', description: 'URL to fetch', required: true },
|
|
335
|
+
],
|
|
336
|
+
handler: async (params) => {
|
|
337
|
+
try {
|
|
338
|
+
await assertFetchAllowed(params.url as string);
|
|
339
|
+
const response = await fetch(params.url as string);
|
|
340
|
+
const text = await response.text();
|
|
341
|
+
return `Status: ${response.status}\n\n${text.slice(0, 10000)}${text.length > 10000 ? '\n...[truncated]' : ''}`;
|
|
342
|
+
} catch (e) {
|
|
343
|
+
return `Error fetching URL: ${e instanceof Error ? e.message : e}`;
|
|
344
|
+
}
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ── Task Management ──
|
|
349
|
+
|
|
350
|
+
registry.register({
|
|
351
|
+
name: 'task_done',
|
|
352
|
+
description: 'Signal that the current task is complete and provide a summary. Call this when you have finished the work.',
|
|
353
|
+
parameters: [
|
|
354
|
+
{ name: 'summary', type: 'string', description: 'Summary of what was accomplished', required: false },
|
|
355
|
+
],
|
|
356
|
+
handler: async (_params) => {
|
|
357
|
+
return '__TASK_DONE__';
|
|
358
|
+
},
|
|
359
|
+
cacheable: false,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ── Search Tool ──
|
|
363
|
+
|
|
364
|
+
registry.register({
|
|
365
|
+
name: 'web_search',
|
|
366
|
+
description: 'Search the web for information. Returns search results with titles, URLs and snippets.',
|
|
367
|
+
parameters: [
|
|
368
|
+
{ name: 'query', type: 'string', description: 'Search query', required: true },
|
|
369
|
+
{ name: 'engine', type: 'string', description: 'Optional engine: duckduckgo|bing|baidu|sogou. Default: auto (tries each until one returns results)', required: false },
|
|
370
|
+
{ name: 'max_results', type: 'number', description: 'Max results to return (default 8, capped at 20)', required: false },
|
|
371
|
+
],
|
|
372
|
+
handler: async (params) => {
|
|
373
|
+
const query = String(params.query || '').trim();
|
|
374
|
+
if (!query) return 'Error: query is required';
|
|
375
|
+
const max = Math.max(1, Math.min(20, Math.floor(Number(params.max_results) || 8)));
|
|
376
|
+
const explicit = String(params.engine || '').trim().toLowerCase();
|
|
377
|
+
const envEngine = String(process.env.SKYLOOM_SEARCH_ENGINE || '').trim().toLowerCase();
|
|
378
|
+
const order = explicit
|
|
379
|
+
? [explicit]
|
|
380
|
+
: envEngine
|
|
381
|
+
? [envEngine, 'duckduckgo', 'bing', 'baidu', 'sogou']
|
|
382
|
+
: ['duckduckgo', 'bing', 'baidu', 'sogou'];
|
|
383
|
+
const seen = new Set<string>();
|
|
384
|
+
const tried: string[] = [];
|
|
385
|
+
for (const eng of order) {
|
|
386
|
+
if (seen.has(eng)) continue;
|
|
387
|
+
seen.add(eng);
|
|
388
|
+
tried.push(eng);
|
|
389
|
+
try {
|
|
390
|
+
const results = await runSearchEngine(eng, query, max);
|
|
391
|
+
if (results && results.length > 0) {
|
|
392
|
+
const head = `Search results (${eng}, ${results.length}):`;
|
|
393
|
+
const body = results
|
|
394
|
+
.map((r, i) => `${i + 1}. ${r.title}\n ${r.url}${r.snippet ? `\n ${r.snippet}` : ''}`)
|
|
395
|
+
.join('\n');
|
|
396
|
+
return `${head}\n${body}`;
|
|
397
|
+
}
|
|
398
|
+
} catch (e: any) {
|
|
399
|
+
log.warn('web_search_engine_failed', { engine: eng, error: String(e?.message || e) });
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
return `No search results found (tried: ${tried.join(', ')}). Set SKYLOOM_SEARCH_ENGINE to pin an engine, or try a different query.`;
|
|
403
|
+
},
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
// ── Memory Tools ──
|
|
407
|
+
|
|
408
|
+
registry.register({
|
|
409
|
+
name: 'remember_fact',
|
|
410
|
+
description: 'Store a fact about the user or project in long-term memory.',
|
|
411
|
+
parameters: [
|
|
412
|
+
{ name: 'key', type: 'string', description: 'Fact key (snake_case)', required: true },
|
|
413
|
+
{ name: 'value', type: 'string', description: 'Fact value', required: true },
|
|
414
|
+
{ name: 'category', type: 'string', description: 'Category (e.g. user_pref, project_info)', required: false },
|
|
415
|
+
],
|
|
416
|
+
handler: async (_params) => {
|
|
417
|
+
return 'Fact stored. (Memory integration at agent level.)';
|
|
418
|
+
},
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
registry.register({
|
|
422
|
+
name: 'recall_facts',
|
|
423
|
+
description: 'Recall stored facts from long-term memory.',
|
|
424
|
+
parameters: [
|
|
425
|
+
{ name: 'query', type: 'string', description: 'Search query to match against stored facts', required: true },
|
|
426
|
+
],
|
|
427
|
+
handler: async (_params) => {
|
|
428
|
+
return 'Recall handled at agent level.';
|
|
429
|
+
},
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// ── Git Tools ──
|
|
433
|
+
|
|
434
|
+
registry.register({
|
|
435
|
+
name: 'git_status',
|
|
436
|
+
description: 'Show the working tree status.',
|
|
437
|
+
parameters: [],
|
|
438
|
+
handler: async () => {
|
|
439
|
+
const { execSync } = require('child_process');
|
|
440
|
+
try {
|
|
441
|
+
return execSync('git status', { encoding: 'utf-8' });
|
|
442
|
+
} catch (e: any) {
|
|
443
|
+
return `Error: ${e.message || e}`;
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
registry.register({
|
|
449
|
+
name: 'git_diff',
|
|
450
|
+
description: 'Show changes between commits, commit and working tree, etc.',
|
|
451
|
+
parameters: [
|
|
452
|
+
{ name: 'staged', type: 'boolean', description: 'Show staged changes only', required: false },
|
|
453
|
+
],
|
|
454
|
+
handler: async (params) => {
|
|
455
|
+
const { execSync } = require('child_process');
|
|
456
|
+
try {
|
|
457
|
+
const args = params.staged ? '--staged' : '';
|
|
458
|
+
return execSync(`git diff ${args}`, { encoding: 'utf-8' });
|
|
459
|
+
} catch (e: any) {
|
|
460
|
+
return `Error: ${e.message || e}`;
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
registry.register({
|
|
466
|
+
name: 'git_log',
|
|
467
|
+
description: 'Show commit logs.',
|
|
468
|
+
parameters: [
|
|
469
|
+
{ name: 'max_count', type: 'number', description: 'Number of commits to show (default: 10)', required: false },
|
|
470
|
+
],
|
|
471
|
+
handler: async (params) => {
|
|
472
|
+
const { execFileSync } = require('child_process');
|
|
473
|
+
try {
|
|
474
|
+
const n = Math.max(1, Math.min(1000, Math.floor(Number(params.max_count) || 10)));
|
|
475
|
+
return execFileSync('git', ['log', '--oneline', `-${n}`], { encoding: 'utf-8' });
|
|
476
|
+
} catch (e: any) {
|
|
477
|
+
return `Error: ${e.message || e}`;
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
registry.register({
|
|
483
|
+
name: 'git_commit',
|
|
484
|
+
description: 'Create a new commit with the given message.',
|
|
485
|
+
parameters: [
|
|
486
|
+
{ name: 'message', type: 'string', description: 'Commit message', required: true },
|
|
487
|
+
],
|
|
488
|
+
handler: async (params) => {
|
|
489
|
+
const { execFileSync } = require('child_process');
|
|
490
|
+
try {
|
|
491
|
+
const msg = String(params.message ?? '');
|
|
492
|
+
// execFileSync passes the message as a single argv entry — no shell, so
|
|
493
|
+
// backticks / $() / ; in the message cannot be interpreted.
|
|
494
|
+
execFileSync('git', ['commit', '-m', msg], { encoding: 'utf-8' });
|
|
495
|
+
return 'Commit created successfully.';
|
|
496
|
+
} catch (e: any) {
|
|
497
|
+
return `Error: ${e.message || e}`;
|
|
498
|
+
}
|
|
499
|
+
},
|
|
500
|
+
dangerous: true,
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
// ── Utility Tools ──
|
|
504
|
+
|
|
505
|
+
registry.register({
|
|
506
|
+
name: 'grep',
|
|
507
|
+
description: 'Search for a pattern in files using ripgrep or grep.',
|
|
508
|
+
parameters: [
|
|
509
|
+
{ name: 'pattern', type: 'string', description: 'Regex pattern to search for', required: true },
|
|
510
|
+
{ name: 'path', type: 'string', description: 'Directory to search in', required: false },
|
|
511
|
+
],
|
|
512
|
+
handler: async (params) => {
|
|
513
|
+
const { execFileSync } = require('child_process');
|
|
514
|
+
const searchDir = params.path ? path.resolve(params.path as string) : process.cwd();
|
|
515
|
+
const fenced = fenceCheck(searchDir); if (fenced) return fenced;
|
|
516
|
+
const pat = String(params.pattern || '');
|
|
517
|
+
// No shell: pattern and directory are passed as argv entries, and `--`
|
|
518
|
+
// stops a leading `-` in the pattern from being read as a flag. This is a
|
|
519
|
+
// non-dangerous (auto-approved) tool, so shell injection here would have
|
|
520
|
+
// bypassed the tool-approval gate entirely.
|
|
521
|
+
const variants: [string, string[]][] = [
|
|
522
|
+
['rg', ['-n', '--', pat, searchDir]],
|
|
523
|
+
['grep', ['-rn', '--', pat, searchDir]],
|
|
524
|
+
];
|
|
525
|
+
for (const [bin, args] of variants) {
|
|
526
|
+
try {
|
|
527
|
+
const out = execFileSync(bin, args, { encoding: 'utf-8', maxBuffer: 1024 * 1024 });
|
|
528
|
+
return out || 'No matches found.';
|
|
529
|
+
} catch (e: any) {
|
|
530
|
+
// exit status 1 = ran successfully, zero matches; anything else
|
|
531
|
+
// (e.g. binary not installed) falls through to the next variant.
|
|
532
|
+
if (e?.status === 1) return 'No matches found.';
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return 'No matches found.';
|
|
536
|
+
},
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
registry.register({
|
|
540
|
+
name: 'tree',
|
|
541
|
+
description: 'Display directory tree structure.',
|
|
542
|
+
parameters: [
|
|
543
|
+
{ name: 'directory', type: 'string', description: 'Directory to show tree for', required: false },
|
|
544
|
+
{ name: 'depth', type: 'number', description: 'Maximum depth (default: 3)', required: false },
|
|
545
|
+
],
|
|
546
|
+
handler: async (params) => {
|
|
547
|
+
const { execFileSync } = require('child_process');
|
|
548
|
+
const treeDir = params.directory ? path.resolve(params.directory as string) : process.cwd();
|
|
549
|
+
const fenced = fenceCheck(treeDir); if (fenced) return fenced;
|
|
550
|
+
const depth = Math.max(1, Math.min(20, Math.floor(Number(params.depth) || 3)));
|
|
551
|
+
try {
|
|
552
|
+
// No shell: directory passed as an argv entry, depth clamped to an int.
|
|
553
|
+
const out = execFileSync('tree', [treeDir, '-L', String(depth), '--charset=utf-8'], { encoding: 'utf-8' });
|
|
554
|
+
return out;
|
|
555
|
+
} catch {
|
|
556
|
+
return 'Directory tree unavailable.';
|
|
557
|
+
}
|
|
558
|
+
},
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
log.info('builtin_tools_registered', { count: registry.listNames().length });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
let _httpClient: any = null;
|
|
565
|
+
|
|
566
|
+
export async function closeHttpClient(): Promise<void> {
|
|
567
|
+
if (_httpClient) {
|
|
568
|
+
try { await _httpClient.close?.(); } catch { /* ignore */ }
|
|
569
|
+
_httpClient = null;
|
|
570
|
+
}
|
|
571
|
+
}
|