@kkelly-offical/kkcode 0.1.6 → 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 +674 -674
- package/README.md +452 -387
- package/package.json +50 -46
- package/src/agent/agent.mjs +19 -2
- package/src/agent/custom-agent-loader.mjs +6 -3
- package/src/agent/generator.mjs +2 -2
- package/src/agent/prompt/assistant.txt +12 -0
- package/src/agent/prompt/bug-hunter.txt +90 -0
- package/src/agent/prompt/frontend-designer.txt +58 -58
- package/src/agent/prompt/guide.txt +1 -1
- package/src/agent/prompt/longagent-blueprint-agent.txt +83 -83
- package/src/agent/prompt/longagent-coding-agent.txt +37 -37
- package/src/agent/prompt/longagent-debugging-agent.txt +46 -46
- package/src/agent/prompt/longagent-preview-agent.txt +63 -63
- package/src/command/custom-commands.mjs +2 -2
- package/src/commands/agent.mjs +1 -1
- package/src/commands/background.mjs +145 -4
- package/src/commands/chat.mjs +117 -76
- package/src/commands/config.mjs +148 -1
- package/src/commands/doctor.mjs +30 -6
- package/src/commands/init.mjs +32 -6
- package/src/commands/longagent.mjs +117 -0
- package/src/commands/mcp.mjs +275 -43
- package/src/commands/permission.mjs +1 -1
- package/src/commands/session.mjs +195 -140
- package/src/commands/skill.mjs +63 -0
- package/src/commands/theme.mjs +1 -1
- package/src/config/defaults.mjs +280 -260
- package/src/config/import-config.mjs +1 -1
- package/src/config/load-config.mjs +61 -4
- package/src/config/schema.mjs +591 -574
- package/src/context.mjs +4 -1
- package/src/core/constants.mjs +97 -91
- package/src/core/types.mjs +1 -1
- package/src/github/api.mjs +78 -78
- package/src/github/auth.mjs +294 -286
- package/src/github/flow.mjs +298 -298
- package/src/github/workspace.mjs +225 -212
- package/src/index.mjs +84 -82
- package/src/knowledge/frontend-aesthetics.txt +38 -38
- package/src/mcp/client-http.mjs +139 -141
- package/src/mcp/client-sse.mjs +297 -288
- package/src/mcp/client-stdio.mjs +534 -533
- package/src/mcp/constants.mjs +2 -2
- package/src/mcp/registry.mjs +498 -479
- package/src/mcp/stdio-framing.mjs +135 -133
- package/src/mcp/tool-result.mjs +24 -24
- package/src/observability/edit-diagnostics.mjs +449 -0
- package/src/observability/index.mjs +42 -42
- package/src/observability/metrics.mjs +165 -137
- package/src/observability/tracer.mjs +137 -137
- package/src/onboarding.mjs +209 -0
- package/src/orchestration/background-manager.mjs +567 -372
- package/src/orchestration/background-worker.mjs +419 -305
- package/src/orchestration/interruption-reason.mjs +21 -0
- package/src/orchestration/longagent-manager.mjs +197 -171
- package/src/orchestration/stage-scheduler.mjs +733 -728
- package/src/orchestration/subagent-router.mjs +7 -1
- package/src/orchestration/task-scheduler.mjs +219 -7
- package/src/permission/engine.mjs +1 -1
- package/src/permission/exec-policy.mjs +370 -370
- package/src/permission/file-edit-policy.mjs +108 -0
- package/src/permission/prompt.mjs +1 -1
- package/src/permission/rules.mjs +116 -7
- package/src/plugin/builtin-hooks/post-edit-format.mjs +2 -1
- package/src/plugin/builtin-hooks/post-edit-typecheck.mjs +104 -40
- package/src/plugin/hook-bus.mjs +19 -5
- package/src/plugin/manifest-loader.mjs +222 -0
- package/src/provider/anthropic.mjs +396 -390
- package/src/provider/ollama.mjs +7 -1
- package/src/provider/openai.mjs +382 -340
- package/src/provider/retry-policy.mjs +74 -68
- package/src/provider/router.mjs +242 -241
- package/src/provider/sse.mjs +104 -104
- package/src/provider/wizard.mjs +556 -0
- package/src/repl/capability-facade.mjs +30 -0
- package/src/repl/command-surface.mjs +23 -0
- package/src/repl/controller-entry.mjs +40 -0
- package/src/repl/core-shell.mjs +208 -0
- package/src/repl/dialog-router.mjs +87 -0
- package/src/repl/input-engine.mjs +76 -0
- package/src/repl/keymap.mjs +7 -0
- package/src/repl/operator-surface.mjs +15 -0
- package/src/repl/permission-flow.mjs +49 -0
- package/src/repl/runtime-facade.mjs +36 -0
- package/src/repl/slash-router.mjs +62 -0
- package/src/repl/state-store.mjs +29 -0
- package/src/repl/turn-controller.mjs +58 -0
- package/src/repl/verification.mjs +23 -0
- package/src/repl.mjs +3368 -2929
- package/src/rules/load-rules.mjs +3 -3
- package/src/runtime.mjs +1 -1
- package/src/session/agent-transaction.mjs +86 -0
- package/src/session/checkpoint.mjs +302 -302
- package/src/session/compaction.mjs +36 -14
- package/src/session/engine.mjs +417 -227
- package/src/session/longagent-4stage.mjs +467 -460
- package/src/session/longagent-hybrid.mjs +1344 -1081
- package/src/session/longagent-plan.mjs +376 -365
- package/src/session/longagent-project-memory.mjs +53 -53
- package/src/session/longagent-scaffold.mjs +291 -291
- package/src/session/longagent-task-bus.mjs +138 -54
- package/src/session/longagent-utils.mjs +828 -472
- package/src/session/longagent.mjs +911 -884
- package/src/session/loop.mjs +1005 -905
- package/src/session/prompt/agent.txt +25 -0
- package/src/session/prompt/anthropic.txt +150 -150
- package/src/session/prompt/beast.txt +1 -1
- package/src/session/prompt/plan.txt +28 -6
- package/src/session/prompt/qwen.txt +46 -46
- package/src/session/recovery.mjs +21 -0
- package/src/session/rollback.mjs +197 -0
- package/src/session/routing-observability.mjs +72 -0
- package/src/session/runtime-state.mjs +47 -0
- package/src/session/store.mjs +523 -510
- package/src/session/system-prompt.mjs +56 -8
- package/src/session/task-validator.mjs +267 -267
- package/src/session/usability-gates.mjs +2 -2
- package/src/skill/builtin/commit.mjs +64 -64
- package/src/skill/builtin/design.mjs +76 -76
- package/src/skill/generator.mjs +18 -2
- package/src/skill/registry.mjs +642 -390
- package/src/storage/audit-store.mjs +18 -11
- package/src/storage/event-log.mjs +7 -1
- package/src/storage/ghost-commit-store.mjs +243 -245
- package/src/storage/paths.mjs +13 -0
- package/src/theme/default-theme.mjs +1 -1
- package/src/theme/markdown.mjs +4 -0
- package/src/theme/schema.mjs +1 -1
- package/src/theme/status-bar.mjs +162 -158
- package/src/tool/audit-wrapper.mjs +18 -2
- package/src/tool/edit-transaction.mjs +23 -0
- package/src/tool/executor.mjs +26 -1
- package/src/tool/file-read-state.mjs +65 -0
- package/src/tool/git-auto.mjs +526 -526
- package/src/tool/git-full-auto.mjs +487 -478
- package/src/tool/mutation-guard.mjs +54 -0
- package/src/tool/prompt/edit.txt +3 -3
- package/src/tool/prompt/multiedit.txt +1 -0
- package/src/tool/prompt/notebookedit.txt +2 -1
- package/src/tool/prompt/patch.txt +25 -24
- package/src/tool/prompt/read.txt +3 -3
- package/src/tool/prompt/sysinfo.txt +29 -0
- package/src/tool/prompt/task.txt +66 -4
- package/src/tool/prompt/write.txt +2 -2
- package/src/tool/question-prompt.mjs +17 -4
- package/src/tool/registry.mjs +1701 -1343
- package/src/tool/task-tool.mjs +14 -6
- package/src/ui/activity-renderer.mjs +667 -664
- package/src/ui/repl-background-panel.mjs +7 -0
- package/src/ui/repl-capability-panel.mjs +9 -0
- package/src/ui/repl-dashboard.mjs +54 -4
- package/src/ui/repl-help.mjs +110 -0
- package/src/ui/repl-operator-panel.mjs +12 -0
- package/src/ui/repl-route-feedback.mjs +35 -0
- package/src/ui/repl-status-view.mjs +76 -0
- package/src/ui/repl-task-panel.mjs +5 -0
- package/src/ui/repl-transcript-panel.mjs +56 -0
- package/src/ui/repl-turn-summary.mjs +135 -0
- package/src/usage/pricing.mjs +122 -121
- package/src/usage/usage-meter.mjs +1 -0
- package/src/util/git.mjs +562 -519
- package/src/util/template.mjs +6 -1
package/src/tool/registry.mjs
CHANGED
|
@@ -1,1343 +1,1701 @@
|
|
|
1
|
-
import path from "node:path"
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function
|
|
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
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
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
|
-
const
|
|
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
|
-
const
|
|
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
|
-
await
|
|
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
|
-
|
|
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
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
const
|
|
875
|
-
name: "
|
|
876
|
-
description: "
|
|
877
|
-
inputSchema: {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
const
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
}
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
},
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
}
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
}
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
}
|
|
1
|
+
import path from "node:path"
|
|
2
|
+
import os from "node:os"
|
|
3
|
+
import { readdir, readFile } from "node:fs/promises"
|
|
4
|
+
import { access, stat, statfs, unlink } from "node:fs/promises"
|
|
5
|
+
import { exec as execCb, spawn } from "node:child_process"
|
|
6
|
+
import { promisify } from "node:util"
|
|
7
|
+
import { pathToFileURL } from "node:url"
|
|
8
|
+
import { atomicWriteFile, replaceInFileTransactional, replaceAllInFileTransactional, diffLineCount, buildStructuredPatch } from "./edit-transaction.mjs"
|
|
9
|
+
import { withFileLock } from "./file-lock-manager.mjs"
|
|
10
|
+
import { BackgroundManager } from "../orchestration/background-manager.mjs"
|
|
11
|
+
import { createTaskTool } from "./task-tool.mjs"
|
|
12
|
+
import { McpRegistry } from "../mcp/registry.mjs"
|
|
13
|
+
import { SkillRegistry } from "../skill/registry.mjs"
|
|
14
|
+
import { askQuestionInteractive } from "./question-prompt.mjs"
|
|
15
|
+
import { checkBashAllowed } from "../permission/exec-policy.mjs"
|
|
16
|
+
import { gitAutoTools } from "./git-auto.mjs"
|
|
17
|
+
import { gitFullAutoTools } from "./git-full-auto.mjs"
|
|
18
|
+
import { markFileRead, refreshFileReadStateFromDisk } from "./file-read-state.mjs"
|
|
19
|
+
import { validateExistingFileMutation } from "./mutation-guard.mjs"
|
|
20
|
+
import { buildMutationObservability } from "../observability/edit-diagnostics.mjs"
|
|
21
|
+
|
|
22
|
+
const exec = promisify(execCb)
|
|
23
|
+
|
|
24
|
+
const state = {
|
|
25
|
+
initialized: false,
|
|
26
|
+
tools: [],
|
|
27
|
+
loadedAt: 0,
|
|
28
|
+
lastSignature: "",
|
|
29
|
+
lastCwd: "",
|
|
30
|
+
lastConfig: null,
|
|
31
|
+
refreshing: false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function schema(type, description) {
|
|
35
|
+
return { type, description }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function safeStringify(value) {
|
|
39
|
+
if (typeof value === "string") return value
|
|
40
|
+
return JSON.stringify(value, null, 2)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function signatureFor(config = {}, cwd = process.cwd()) {
|
|
44
|
+
const payload = {
|
|
45
|
+
cwd,
|
|
46
|
+
tool: config.tool || {},
|
|
47
|
+
mcp: config.mcp || {},
|
|
48
|
+
runtime: config.runtime || {}
|
|
49
|
+
}
|
|
50
|
+
return JSON.stringify(payload)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function exists(target) {
|
|
54
|
+
try {
|
|
55
|
+
await access(target)
|
|
56
|
+
return true
|
|
57
|
+
} catch {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function listDir(dir) {
|
|
63
|
+
const items = await readdir(dir, { withFileTypes: true })
|
|
64
|
+
return items.map((item) => `${item.isDirectory() ? "d" : "f"} ${item.name}`).join("\n")
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function formatBytes(bytes) {
|
|
68
|
+
const value = Number(bytes || 0)
|
|
69
|
+
if (!Number.isFinite(value) || value <= 0) return "0 B"
|
|
70
|
+
const units = ["B", "KB", "MB", "GB", "TB", "PB"]
|
|
71
|
+
let size = value
|
|
72
|
+
let unitIndex = 0
|
|
73
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
74
|
+
size /= 1024
|
|
75
|
+
unitIndex += 1
|
|
76
|
+
}
|
|
77
|
+
const decimals = size >= 10 || unitIndex === 0 ? 0 : 1
|
|
78
|
+
return `${size.toFixed(decimals)} ${units[unitIndex]}`
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function detectShellInfo() {
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
return process.env.ComSpec || process.env.SHELL || "powershell/cmd"
|
|
84
|
+
}
|
|
85
|
+
return process.env.SHELL || "/bin/sh"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function detectGitRepo(cwd) {
|
|
89
|
+
try {
|
|
90
|
+
await exec("git rev-parse --is-inside-work-tree", { cwd, timeout: 3000 })
|
|
91
|
+
return true
|
|
92
|
+
} catch {
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function detectPackageManagers(cwd) {
|
|
98
|
+
const candidates = [
|
|
99
|
+
["npm", "package-lock.json"],
|
|
100
|
+
["pnpm", "pnpm-lock.yaml"],
|
|
101
|
+
["yarn", "yarn.lock"],
|
|
102
|
+
["bun", "bun.lockb"]
|
|
103
|
+
]
|
|
104
|
+
const present = []
|
|
105
|
+
for (const [name, file] of candidates) {
|
|
106
|
+
if (await exists(path.join(cwd, file))) present.push(name)
|
|
107
|
+
}
|
|
108
|
+
return present
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function assertWithinCwd(resolved, cwd) {
|
|
112
|
+
const normalCwd = path.resolve(cwd)
|
|
113
|
+
if (!resolved.startsWith(normalCwd + path.sep) && resolved !== normalCwd) {
|
|
114
|
+
throw new Error(`path traversal blocked: ${resolved} is outside working directory`)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function runRg(args, cwd, timeoutMs = 30000) {
|
|
119
|
+
return new Promise((resolve) => {
|
|
120
|
+
let stdout = "", stderr = "", done = false
|
|
121
|
+
const child = spawn("rg", ["--no-config", ...args], {
|
|
122
|
+
cwd, windowsHide: true, stdio: ["ignore", "pipe", "pipe"]
|
|
123
|
+
})
|
|
124
|
+
const timer = setTimeout(() => {
|
|
125
|
+
if (done) return
|
|
126
|
+
done = true
|
|
127
|
+
child.kill("SIGTERM")
|
|
128
|
+
setTimeout(() => { try { child.kill("SIGKILL") } catch {} }, 2000).unref()
|
|
129
|
+
resolve({ ok: false, stdout, stderr: "search timed out" })
|
|
130
|
+
}, timeoutMs)
|
|
131
|
+
child.stdout.on("data", (b) => { stdout += b })
|
|
132
|
+
child.stderr.on("data", (b) => { stderr += b })
|
|
133
|
+
child.on("error", (e) => {
|
|
134
|
+
if (done) return; done = true; clearTimeout(timer)
|
|
135
|
+
resolve({ ok: false, stdout, stderr: e.message })
|
|
136
|
+
})
|
|
137
|
+
child.on("close", (code) => {
|
|
138
|
+
if (done) return; done = true; clearTimeout(timer)
|
|
139
|
+
resolve({ ok: code === 0 || code === 1, stdout: stdout.trim(), stderr: stderr.trim() })
|
|
140
|
+
})
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function runGlob(pattern, cwd, searchPath) {
|
|
145
|
+
if (!pattern) return "pattern is required"
|
|
146
|
+
const target = searchPath ? path.resolve(cwd, searchPath) : "."
|
|
147
|
+
const { stdout } = await runRg(["--files", "--glob", pattern, target], cwd, 15000)
|
|
148
|
+
const text = stdout.trim()
|
|
149
|
+
if (!text) return "no files matched"
|
|
150
|
+
const lines = text.split("\n").filter(Boolean)
|
|
151
|
+
if (lines.length > 200) {
|
|
152
|
+
return lines.slice(0, 200).join("\n") + `\n... (+${lines.length - 200} more files)`
|
|
153
|
+
}
|
|
154
|
+
return `${lines.length} file(s):\n${text}`
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function runGrep(pattern, cwd, options = {}) {
|
|
158
|
+
if (!pattern) return "pattern is required"
|
|
159
|
+
const args = []
|
|
160
|
+
if (options.multiline) args.push("-U", "--multiline-dotall")
|
|
161
|
+
if (options.outputMode === "count") args.push("-c")
|
|
162
|
+
else if (options.outputMode === "files") args.push("-l")
|
|
163
|
+
else args.push("-n")
|
|
164
|
+
if (options.beforeContext) args.push("-B", String(options.beforeContext))
|
|
165
|
+
if (options.afterContext) args.push("-A", String(options.afterContext))
|
|
166
|
+
if (options.context) args.push("-C", String(options.context))
|
|
167
|
+
if (options.type) args.push("--type", options.type)
|
|
168
|
+
if (options.glob) args.push("--glob", options.glob)
|
|
169
|
+
if (options.maxCount) args.push("-m", String(options.maxCount))
|
|
170
|
+
if (options.ignoreCase) args.push("-i")
|
|
171
|
+
args.push(pattern)
|
|
172
|
+
args.push(options.path ? path.resolve(cwd, options.path) : ".")
|
|
173
|
+
const { stdout, stderr } = await runRg(args, cwd)
|
|
174
|
+
let text = stdout.trim()
|
|
175
|
+
if (!text && stderr) text = `[search error] ${stderr}`
|
|
176
|
+
if (text && (options.offset || options.headLimit)) {
|
|
177
|
+
const lines = text.split("\n")
|
|
178
|
+
const start = options.offset || 0
|
|
179
|
+
const limit = options.headLimit || lines.length
|
|
180
|
+
text = lines.slice(start, start + limit).join("\n")
|
|
181
|
+
}
|
|
182
|
+
return text || "no matches"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const LONG_RUNNING_PATTERNS = [
|
|
186
|
+
/\bnpm\s+run\s+dev\b/i,
|
|
187
|
+
/\bnpm\s+run\s+start\b/i,
|
|
188
|
+
/\bnpm\s+start\b/i,
|
|
189
|
+
/\byarn\s+dev\b/i,
|
|
190
|
+
/\byarn\s+start\b/i,
|
|
191
|
+
/\bpnpm\s+dev\b/i,
|
|
192
|
+
/\bpnpm\s+start\b/i,
|
|
193
|
+
/\bnpx\s+vite\b/i,
|
|
194
|
+
/\bnpx\s+next\s+dev\b/i,
|
|
195
|
+
/\bnpx\s+serve\b/i,
|
|
196
|
+
/\bnode\s+.*server/i,
|
|
197
|
+
/\bwebpack\s+serve\b/i,
|
|
198
|
+
/\bwebpack\s+--watch\b/i,
|
|
199
|
+
/\bjest\s+--watch\b/i,
|
|
200
|
+
/\bvitest(?!\s+--run)\b.*(?!--run)/i,
|
|
201
|
+
/\bnodemon\b/i,
|
|
202
|
+
/\btsc\s+--watch\b/i,
|
|
203
|
+
/\btailwindcss\s+--watch\b/i,
|
|
204
|
+
/\bnpm\s+run\s+serve\b/i,
|
|
205
|
+
/\bnpm\s+run\s+watch\b/i
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
const BASH_TIMEOUT_MS = 120_000
|
|
209
|
+
const IS_WIN = process.platform === "win32"
|
|
210
|
+
function wrapCmd(cmd) { return IS_WIN ? `chcp 65001 >nul & ${cmd}` : cmd }
|
|
211
|
+
|
|
212
|
+
function isLongRunningCommand(command) {
|
|
213
|
+
const cmd = String(command || "").trim()
|
|
214
|
+
return LONG_RUNNING_PATTERNS.some((re) => re.test(cmd))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function runBash(command, cwd, timeoutMs = BASH_TIMEOUT_MS) {
|
|
218
|
+
if (isLongRunningCommand(command)) {
|
|
219
|
+
return `[blocked] "${command}" looks like a long-running/dev-server command that would block execution. Please tell the user to run it manually in their terminal, or use run_in_background: true.`
|
|
220
|
+
}
|
|
221
|
+
const out = await exec(wrapCmd(command), { cwd, timeout: timeoutMs, encoding: "utf8" }).catch((error) => {
|
|
222
|
+
if (error.killed || error.signal === "SIGTERM") {
|
|
223
|
+
return {
|
|
224
|
+
stdout: error.stdout ?? "",
|
|
225
|
+
stderr: `${error.stderr || ""}\n[timeout] command killed after ${timeoutMs / 1000}s`
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
stdout: error.stdout ?? "",
|
|
230
|
+
stderr: error.stderr ?? error.message
|
|
231
|
+
}
|
|
232
|
+
})
|
|
233
|
+
const raw = `${out.stdout || ""}${out.stderr || ""}`.trim() || "(empty output)"
|
|
234
|
+
if (raw.length > 30000) return raw.slice(0, 30000) + `\n\n[truncated] output exceeded 30000 chars (total: ${raw.length})`
|
|
235
|
+
return raw
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function lockOptions(ctx = {}) {
|
|
239
|
+
const mode = String(ctx?.config?.tool?.write_lock?.mode || "file_lock")
|
|
240
|
+
const waitTimeoutMs = Math.max(0, Number(ctx?.config?.tool?.write_lock?.wait_timeout_ms || 120000))
|
|
241
|
+
const owner = String(ctx?.taskId || ctx?.sessionId || ctx?.turnId || "kkcode")
|
|
242
|
+
return { mode, waitTimeoutMs, owner }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function mutationMetadata({
|
|
246
|
+
operation,
|
|
247
|
+
filePath,
|
|
248
|
+
originalContent = null,
|
|
249
|
+
updatedContent = null,
|
|
250
|
+
structuredPatch = [],
|
|
251
|
+
addedLines = 0,
|
|
252
|
+
removedLines = 0,
|
|
253
|
+
stageId = null,
|
|
254
|
+
taskId = null
|
|
255
|
+
}) {
|
|
256
|
+
return {
|
|
257
|
+
fileChanges: [{
|
|
258
|
+
path: filePath,
|
|
259
|
+
tool: operation,
|
|
260
|
+
addedLines,
|
|
261
|
+
removedLines,
|
|
262
|
+
stageId,
|
|
263
|
+
taskId
|
|
264
|
+
}],
|
|
265
|
+
mutation: {
|
|
266
|
+
operation,
|
|
267
|
+
filePath,
|
|
268
|
+
originalContent,
|
|
269
|
+
updatedContent,
|
|
270
|
+
structuredPatch,
|
|
271
|
+
addedLines,
|
|
272
|
+
removedLines
|
|
273
|
+
},
|
|
274
|
+
observability: buildMutationObservability({
|
|
275
|
+
fileChanges: [{
|
|
276
|
+
path: filePath,
|
|
277
|
+
tool: operation,
|
|
278
|
+
addedLines,
|
|
279
|
+
removedLines,
|
|
280
|
+
stageId,
|
|
281
|
+
taskId
|
|
282
|
+
}],
|
|
283
|
+
mutation: {
|
|
284
|
+
operation,
|
|
285
|
+
filePath,
|
|
286
|
+
originalContent,
|
|
287
|
+
updatedContent,
|
|
288
|
+
structuredPatch,
|
|
289
|
+
addedLines,
|
|
290
|
+
removedLines
|
|
291
|
+
}
|
|
292
|
+
})
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function loadDynamicTools(dirs) {
|
|
297
|
+
const loaded = []
|
|
298
|
+
for (const dir of dirs) {
|
|
299
|
+
const absolute = path.resolve(dir)
|
|
300
|
+
if (!(await exists(absolute))) continue
|
|
301
|
+
const entries = await readdir(absolute, { withFileTypes: true })
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
if (!entry.isFile()) continue
|
|
304
|
+
if (![".mjs", ".js"].includes(path.extname(entry.name).toLowerCase())) continue
|
|
305
|
+
const file = path.join(absolute, entry.name)
|
|
306
|
+
try {
|
|
307
|
+
const mod = await import(pathToFileURL(file).href)
|
|
308
|
+
const def = mod.default || mod.tool || mod
|
|
309
|
+
if (!def || typeof def !== "object" || typeof def.name !== "string" || typeof def.execute !== "function") {
|
|
310
|
+
continue
|
|
311
|
+
}
|
|
312
|
+
loaded.push({
|
|
313
|
+
name: def.name,
|
|
314
|
+
description: def.description || `dynamic tool from ${file}`,
|
|
315
|
+
inputSchema: def.inputSchema || { type: "object", properties: {}, required: [] },
|
|
316
|
+
execute: def.execute
|
|
317
|
+
})
|
|
318
|
+
} catch {
|
|
319
|
+
// ignore invalid tool module
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return loaded
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function builtinTools(config) {
|
|
327
|
+
const listTool = {
|
|
328
|
+
name: "list",
|
|
329
|
+
description: "List files and subdirectories in a directory. Returns entry names with type prefix (d=directory, f=file). Use this for quick directory overview; use `glob` for recursive pattern matching.",
|
|
330
|
+
inputSchema: {
|
|
331
|
+
type: "object",
|
|
332
|
+
properties: { path: schema("string", "directory path") },
|
|
333
|
+
required: []
|
|
334
|
+
},
|
|
335
|
+
async execute(args, ctx) {
|
|
336
|
+
const target = path.resolve(ctx.cwd, args.path || ".")
|
|
337
|
+
return listDir(target)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const sysinfoTool = {
|
|
342
|
+
name: "sysinfo",
|
|
343
|
+
description: "Return structured, read-only system and runtime information for the current machine/workspace. Good for OS/runtime/workspace/cpu/memory/disk summaries without relying on raw shell output.",
|
|
344
|
+
inputSchema: {
|
|
345
|
+
type: "object",
|
|
346
|
+
properties: {
|
|
347
|
+
sections: {
|
|
348
|
+
type: "array",
|
|
349
|
+
description: "optional sections to return: os, runtime, workspace, cpu, memory, disk",
|
|
350
|
+
items: { type: "string" }
|
|
351
|
+
},
|
|
352
|
+
path: schema("string", "optional workspace path for disk/workspace inspection (default: cwd)")
|
|
353
|
+
},
|
|
354
|
+
required: []
|
|
355
|
+
},
|
|
356
|
+
async execute(args, ctx) {
|
|
357
|
+
const targetPath = path.resolve(ctx.cwd, String(args.path || "."))
|
|
358
|
+
assertWithinCwd(targetPath, ctx.cwd)
|
|
359
|
+
const requestedSections = Array.isArray(args.sections) && args.sections.length
|
|
360
|
+
? args.sections.map((item) => String(item || "").trim().toLowerCase()).filter(Boolean)
|
|
361
|
+
: ["os", "runtime", "workspace", "cpu", "memory", "disk"]
|
|
362
|
+
const sectionSet = new Set(requestedSections)
|
|
363
|
+
|
|
364
|
+
const result = {
|
|
365
|
+
generatedAt: new Date().toISOString(),
|
|
366
|
+
path: targetPath,
|
|
367
|
+
sections: {}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (sectionSet.has("os")) {
|
|
371
|
+
result.sections.os = {
|
|
372
|
+
platform: process.platform,
|
|
373
|
+
arch: process.arch,
|
|
374
|
+
hostname: os.hostname(),
|
|
375
|
+
release: os.release(),
|
|
376
|
+
version: typeof os.version === "function" ? os.version() : null
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
if (sectionSet.has("runtime")) {
|
|
381
|
+
result.sections.runtime = {
|
|
382
|
+
nodeVersion: process.version,
|
|
383
|
+
shell: detectShellInfo(),
|
|
384
|
+
pid: process.pid,
|
|
385
|
+
uptimeSeconds: Math.round(process.uptime()),
|
|
386
|
+
uptimeHuman: `${Math.round(process.uptime())}s`
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (sectionSet.has("workspace")) {
|
|
391
|
+
const packageManagers = await detectPackageManagers(targetPath)
|
|
392
|
+
result.sections.workspace = {
|
|
393
|
+
cwd: targetPath,
|
|
394
|
+
isGitRepo: await detectGitRepo(targetPath),
|
|
395
|
+
packageManagers,
|
|
396
|
+
hasPackageJson: await exists(path.join(targetPath, "package.json")),
|
|
397
|
+
hasNodeModules: await exists(path.join(targetPath, "node_modules"))
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (sectionSet.has("cpu")) {
|
|
402
|
+
const cpus = os.cpus() || []
|
|
403
|
+
result.sections.cpu = {
|
|
404
|
+
cores: cpus.length,
|
|
405
|
+
model: cpus[0]?.model || null,
|
|
406
|
+
loadAverage: typeof os.loadavg === "function" ? os.loadavg() : []
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (sectionSet.has("memory")) {
|
|
411
|
+
const total = os.totalmem()
|
|
412
|
+
const free = os.freemem()
|
|
413
|
+
result.sections.memory = {
|
|
414
|
+
totalBytes: total,
|
|
415
|
+
freeBytes: free,
|
|
416
|
+
usedBytes: Math.max(0, total - free),
|
|
417
|
+
total: formatBytes(total),
|
|
418
|
+
free: formatBytes(free),
|
|
419
|
+
used: formatBytes(Math.max(0, total - free))
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
if (sectionSet.has("disk")) {
|
|
424
|
+
try {
|
|
425
|
+
const disk = await statfs(targetPath)
|
|
426
|
+
const blockSize = Number(disk.bsize || disk.frsize || 0)
|
|
427
|
+
const totalBytes = Number(disk.blocks || 0) * blockSize
|
|
428
|
+
const freeBytes = Number(disk.bavail || disk.bfree || 0) * blockSize
|
|
429
|
+
result.sections.disk = {
|
|
430
|
+
path: targetPath,
|
|
431
|
+
totalBytes,
|
|
432
|
+
freeBytes,
|
|
433
|
+
usedBytes: Math.max(0, totalBytes - freeBytes),
|
|
434
|
+
total: formatBytes(totalBytes),
|
|
435
|
+
free: formatBytes(freeBytes),
|
|
436
|
+
used: formatBytes(Math.max(0, totalBytes - freeBytes))
|
|
437
|
+
}
|
|
438
|
+
} catch (error) {
|
|
439
|
+
result.sections.disk = {
|
|
440
|
+
path: targetPath,
|
|
441
|
+
error: error.message
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
const summaryParts = []
|
|
447
|
+
if (result.sections.os) summaryParts.push(`${result.sections.os.platform}/${result.sections.os.arch}`)
|
|
448
|
+
if (result.sections.runtime) summaryParts.push(`node ${result.sections.runtime.nodeVersion}`)
|
|
449
|
+
if (result.sections.workspace) summaryParts.push(result.sections.workspace.isGitRepo ? "git repo" : "non-git cwd")
|
|
450
|
+
if (result.sections.memory) summaryParts.push(`mem ${result.sections.memory.used}/${result.sections.memory.total}`)
|
|
451
|
+
if (result.sections.disk?.total) summaryParts.push(`disk ${result.sections.disk.used}/${result.sections.disk.total}`)
|
|
452
|
+
result.summary = summaryParts.join(" · ")
|
|
453
|
+
|
|
454
|
+
return result
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const IMAGE_EXTENSIONS = new Set([".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".bmp", ".ico"])
|
|
459
|
+
const IMAGE_MIME = { ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".svg": "image/svg+xml", ".webp": "image/webp", ".bmp": "image/bmp", ".ico": "image/x-icon" }
|
|
460
|
+
|
|
461
|
+
function readNotebook(raw) {
|
|
462
|
+
const notebook = JSON.parse(raw)
|
|
463
|
+
if (!notebook.cells || !Array.isArray(notebook.cells)) return "Not a valid .ipynb file (missing cells array)"
|
|
464
|
+
const lines = []
|
|
465
|
+
notebook.cells.forEach((cell, i) => {
|
|
466
|
+
const type = cell.cell_type || "unknown"
|
|
467
|
+
lines.push(`--- Cell ${i} [${type}] ---`)
|
|
468
|
+
const source = Array.isArray(cell.source) ? cell.source.join("") : String(cell.source || "")
|
|
469
|
+
lines.push(source)
|
|
470
|
+
if (cell.outputs && cell.outputs.length > 0) {
|
|
471
|
+
lines.push("[Output]:")
|
|
472
|
+
for (const out of cell.outputs) {
|
|
473
|
+
if (out.text) lines.push(Array.isArray(out.text) ? out.text.join("") : String(out.text))
|
|
474
|
+
else if (out.data?.["text/plain"]) {
|
|
475
|
+
const plain = out.data["text/plain"]
|
|
476
|
+
lines.push(Array.isArray(plain) ? plain.join("") : String(plain))
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
lines.push("")
|
|
481
|
+
})
|
|
482
|
+
return lines.join("\n")
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function extractPdfText(buffer) {
|
|
486
|
+
// Basic PDF text extraction: find text between BT/ET operators and parenthesized strings
|
|
487
|
+
const str = buffer.toString("latin1")
|
|
488
|
+
const texts = []
|
|
489
|
+
const tjRegex = /\(([^)]*)\)/g
|
|
490
|
+
// Extract strings from content streams
|
|
491
|
+
let match
|
|
492
|
+
while ((match = tjRegex.exec(str)) !== null) {
|
|
493
|
+
const decoded = match[1]
|
|
494
|
+
.replace(/\\n/g, "\n").replace(/\\r/g, "\r")
|
|
495
|
+
.replace(/\\t/g, "\t").replace(/\\\\/g, "\\")
|
|
496
|
+
.replace(/\\([()])/g, "$1")
|
|
497
|
+
if (decoded.trim()) texts.push(decoded)
|
|
498
|
+
}
|
|
499
|
+
if (texts.length === 0) return "(PDF contains no extractable text — may be image-based or encrypted)"
|
|
500
|
+
return texts.join(" ").replace(/\s+/g, " ").trim()
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const readTool = {
|
|
504
|
+
name: "read",
|
|
505
|
+
description: "Read file content with line numbers. Supports text files, images (PNG/JPG/GIF/SVG/WebP/BMP/ICO as base64), PDF (text extraction), and Jupyter notebooks (.ipynb cell parsing). Use `offset` and `limit` to read specific line ranges. ALWAYS use this instead of `bash` with cat/head/tail. Existing-file write/edit/patch/notebookedit flows require a recent read first.",
|
|
506
|
+
inputSchema: {
|
|
507
|
+
type: "object",
|
|
508
|
+
properties: {
|
|
509
|
+
path: schema("string", "file path"),
|
|
510
|
+
offset: schema("number", "start line number (1-based, optional)"),
|
|
511
|
+
limit: schema("number", "max lines to return (optional)"),
|
|
512
|
+
encoding: schema("string", "file encoding (default: utf8)"),
|
|
513
|
+
pages: schema("string", "page range for PDF files, e.g. '1-5' (optional)")
|
|
514
|
+
},
|
|
515
|
+
required: ["path"]
|
|
516
|
+
},
|
|
517
|
+
async execute(args, ctx) {
|
|
518
|
+
const target = path.resolve(ctx.cwd, args.path)
|
|
519
|
+
assertWithinCwd(target, ctx.cwd)
|
|
520
|
+
const ext = path.extname(target).toLowerCase()
|
|
521
|
+
|
|
522
|
+
// Image files: return base64 data URI
|
|
523
|
+
if (IMAGE_EXTENSIONS.has(ext)) {
|
|
524
|
+
const buffer = await readFile(target)
|
|
525
|
+
const base64 = buffer.toString("base64")
|
|
526
|
+
const mime = IMAGE_MIME[ext] || "application/octet-stream"
|
|
527
|
+
return {
|
|
528
|
+
type: "image",
|
|
529
|
+
output: `Image file: ${args.path} (${buffer.length} bytes, ${mime})`,
|
|
530
|
+
data: `data:${mime};base64,${base64}`
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// PDF files: extract text
|
|
535
|
+
if (ext === ".pdf") {
|
|
536
|
+
const buffer = await readFile(target)
|
|
537
|
+
return extractPdfText(buffer)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Jupyter notebooks: parse cells
|
|
541
|
+
if (ext === ".ipynb") {
|
|
542
|
+
const raw = await readFile(target, "utf8")
|
|
543
|
+
const fileStat = await stat(target)
|
|
544
|
+
markFileRead(target, {
|
|
545
|
+
content: raw,
|
|
546
|
+
timestamp: fileStat.mtimeMs,
|
|
547
|
+
isPartialView: false
|
|
548
|
+
})
|
|
549
|
+
return readNotebook(raw)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Default: text file with line numbers
|
|
553
|
+
const encoding = args.encoding || "utf8"
|
|
554
|
+
const content = await readFile(target, encoding)
|
|
555
|
+
const fileStat = await stat(target)
|
|
556
|
+
const allLines = content.split("\n")
|
|
557
|
+
const start = Math.max(0, (Number(args.offset) || 1) - 1)
|
|
558
|
+
const count = Number(args.limit) || Math.min(allLines.length, 2000)
|
|
559
|
+
const slice = allLines.slice(start, start + count)
|
|
560
|
+
const isPartialView = start > 0 || start + count < allLines.length
|
|
561
|
+
markFileRead(target, {
|
|
562
|
+
content: isPartialView ? slice.join("\n") : content,
|
|
563
|
+
timestamp: fileStat.mtimeMs,
|
|
564
|
+
offset: isPartialView ? start + 1 : undefined,
|
|
565
|
+
limit: isPartialView ? count : undefined,
|
|
566
|
+
isPartialView
|
|
567
|
+
})
|
|
568
|
+
const numbered = slice.map((line, i) => {
|
|
569
|
+
const num = String(start + i + 1).padStart(6)
|
|
570
|
+
const truncated = line.length > 2000 ? line.slice(0, 2000) + "... (truncated)" : line
|
|
571
|
+
return `${num}→${truncated}`
|
|
572
|
+
})
|
|
573
|
+
return numbered.join("\n")
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const writeTool = {
|
|
578
|
+
name: "write",
|
|
579
|
+
description: "Create or overwrite a file atomically. Auto-creates parent directories. Supports three modes: 'overwrite' (default, full replacement), 'append' (add to end of file), 'insert' (insert at a specific line). Existing-file writes require a recent full read first. For large files (200+ lines), use mode='append' to build incrementally across multiple calls to avoid output truncation. Use `edit` instead when only a small part of an existing file needs to change.",
|
|
580
|
+
inputSchema: {
|
|
581
|
+
type: "object",
|
|
582
|
+
properties: {
|
|
583
|
+
path: schema("string", "file path"),
|
|
584
|
+
content: schema("string", "file content to write"),
|
|
585
|
+
mode: schema("string", "write mode: 'overwrite' (default), 'append' (add to end), 'insert' (insert at line number)"),
|
|
586
|
+
insert_at_line: schema("number", "1-based line number for insert mode. Content is inserted BEFORE this line.")
|
|
587
|
+
},
|
|
588
|
+
required: ["path", "content"]
|
|
589
|
+
},
|
|
590
|
+
async execute(args, ctx) {
|
|
591
|
+
const target = path.resolve(ctx.cwd, args.path)
|
|
592
|
+
assertWithinCwd(target, ctx.cwd)
|
|
593
|
+
const content = String(args.content ?? "")
|
|
594
|
+
const mode = String(args.mode || "overwrite")
|
|
595
|
+
|
|
596
|
+
// Guard: detect empty/parse-error writes that would destroy existing content
|
|
597
|
+
if (args.__parse_error) {
|
|
598
|
+
return {
|
|
599
|
+
output: `error: tool call arguments were corrupted (JSON parse failed). The write was NOT executed. This usually means the response was truncated — try using write with mode="append" to build the file incrementally.`,
|
|
600
|
+
metadata: { blocked: true, reason: "parse_error" }
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
if (!content && !args.content && mode === "overwrite") {
|
|
604
|
+
return {
|
|
605
|
+
output: `error: content is empty or missing. The write was NOT executed. If you intended to create an empty file, pass content as an empty string explicitly.`,
|
|
606
|
+
metadata: { blocked: true, reason: "empty_content" }
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
if (await exists(target)) {
|
|
611
|
+
const validation = await validateExistingFileMutation({
|
|
612
|
+
targetPath: target,
|
|
613
|
+
displayPath: String(args.path || target),
|
|
614
|
+
operation: "writing to it",
|
|
615
|
+
requireFullRead: true
|
|
616
|
+
})
|
|
617
|
+
if (!validation.ok) {
|
|
618
|
+
return {
|
|
619
|
+
output: validation.message,
|
|
620
|
+
metadata: { blocked: true, reason: validation.reason, fileChanges: [] }
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
let previous = ""
|
|
626
|
+
const options = lockOptions(ctx)
|
|
627
|
+
|
|
628
|
+
const runWrite = async () => {
|
|
629
|
+
try {
|
|
630
|
+
previous = await readFile(target, "utf8")
|
|
631
|
+
} catch {
|
|
632
|
+
previous = ""
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
if (mode === "append") {
|
|
636
|
+
const separator = previous && !previous.endsWith("\n") ? "\n" : ""
|
|
637
|
+
await atomicWriteFile(target, previous + separator + content)
|
|
638
|
+
} else if (mode === "insert") {
|
|
639
|
+
const lineNum = Math.max(1, Number(args.insert_at_line) || 1)
|
|
640
|
+
const lines = previous ? previous.split("\n") : []
|
|
641
|
+
const insertIdx = Math.min(lineNum - 1, lines.length)
|
|
642
|
+
const newLines = content.split("\n")
|
|
643
|
+
lines.splice(insertIdx, 0, ...newLines)
|
|
644
|
+
await atomicWriteFile(target, lines.join("\n"))
|
|
645
|
+
} else {
|
|
646
|
+
// overwrite (default)
|
|
647
|
+
await atomicWriteFile(target, content)
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (options.mode === "file_lock") {
|
|
652
|
+
await withFileLock({
|
|
653
|
+
targetPath: target,
|
|
654
|
+
owner: options.owner,
|
|
655
|
+
waitTimeoutMs: options.waitTimeoutMs,
|
|
656
|
+
run: runWrite
|
|
657
|
+
})
|
|
658
|
+
} else {
|
|
659
|
+
await runWrite()
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
let finalContent
|
|
663
|
+
try { finalContent = await readFile(target, "utf8") } catch { finalContent = content }
|
|
664
|
+
await refreshFileReadStateFromDisk(target, { content: finalContent }).catch(() => {})
|
|
665
|
+
const diff = diffLineCount(previous, finalContent)
|
|
666
|
+
const modeLabel = mode === "append" ? "appended" : mode === "insert" ? "inserted" : "written"
|
|
667
|
+
return {
|
|
668
|
+
output: `${modeLabel}: ${target}`,
|
|
669
|
+
metadata: mutationMetadata({
|
|
670
|
+
operation: "write",
|
|
671
|
+
filePath: String(args.path || target),
|
|
672
|
+
originalContent: previous,
|
|
673
|
+
updatedContent: finalContent,
|
|
674
|
+
structuredPatch: buildStructuredPatch(previous, finalContent),
|
|
675
|
+
addedLines: diff.added,
|
|
676
|
+
removedLines: diff.removed,
|
|
677
|
+
stageId: ctx.stageId || null,
|
|
678
|
+
taskId: ctx.logicalTaskId || ctx.taskId || null
|
|
679
|
+
})
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const editTool = {
|
|
685
|
+
name: "edit",
|
|
686
|
+
description: "Replace a specific text snippet in an existing file. Transactional with automatic rollback on failure. You MUST `read` the file first — edits on unread or stale files are rejected. Provide enough surrounding context in `before` to ensure a unique match. Set `replace_all: true` to replace ALL occurrences.",
|
|
687
|
+
inputSchema: {
|
|
688
|
+
type: "object",
|
|
689
|
+
properties: {
|
|
690
|
+
path: schema("string", "file path"),
|
|
691
|
+
before: schema("string", "target snippet"),
|
|
692
|
+
after: schema("string", "replacement snippet"),
|
|
693
|
+
replace_all: schema("boolean", "replace all occurrences instead of requiring unique match (default: false)")
|
|
694
|
+
},
|
|
695
|
+
required: ["path", "before", "after"]
|
|
696
|
+
},
|
|
697
|
+
async execute(args, ctx) {
|
|
698
|
+
const target = path.resolve(ctx.cwd, args.path)
|
|
699
|
+
assertWithinCwd(target, ctx.cwd)
|
|
700
|
+
if (await exists(target)) {
|
|
701
|
+
const validation = await validateExistingFileMutation({
|
|
702
|
+
targetPath: target,
|
|
703
|
+
displayPath: String(args.path || target),
|
|
704
|
+
operation: "editing it"
|
|
705
|
+
})
|
|
706
|
+
if (!validation.ok) {
|
|
707
|
+
return {
|
|
708
|
+
output: validation.message,
|
|
709
|
+
metadata: { blocked: true, reason: validation.reason, fileChanges: [] }
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
const options = lockOptions(ctx)
|
|
714
|
+
const runEdit = async () =>
|
|
715
|
+
args.replace_all
|
|
716
|
+
? replaceAllInFileTransactional(target, String(args.before), String(args.after))
|
|
717
|
+
: replaceInFileTransactional(target, String(args.before), String(args.after))
|
|
718
|
+
const result = options.mode === "file_lock"
|
|
719
|
+
? await withFileLock({
|
|
720
|
+
targetPath: target,
|
|
721
|
+
owner: options.owner,
|
|
722
|
+
waitTimeoutMs: options.waitTimeoutMs,
|
|
723
|
+
run: runEdit
|
|
724
|
+
})
|
|
725
|
+
: await runEdit()
|
|
726
|
+
const updatedContent = await readFile(target, "utf8").catch(() => null)
|
|
727
|
+
await refreshFileReadStateFromDisk(target, { content: updatedContent ?? undefined }).catch(() => {})
|
|
728
|
+
return {
|
|
729
|
+
output: result.output,
|
|
730
|
+
metadata: mutationMetadata({
|
|
731
|
+
operation: "edit",
|
|
732
|
+
filePath: String(args.path || target),
|
|
733
|
+
originalContent: String(args.before),
|
|
734
|
+
updatedContent: String(args.after),
|
|
735
|
+
structuredPatch: buildStructuredPatch(String(args.before), String(args.after)),
|
|
736
|
+
addedLines: Number(result.addedLines || 0),
|
|
737
|
+
removedLines: Number(result.removedLines || 0),
|
|
738
|
+
stageId: ctx.stageId || null,
|
|
739
|
+
taskId: ctx.logicalTaskId || ctx.taskId || null
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
const globTool = {
|
|
746
|
+
name: "glob",
|
|
747
|
+
description: "Find files by glob pattern recursively. Use this instead of `bash` with find/ls. Optionally specify a `path` to search within a specific directory. Returns up to 200 matching file paths.",
|
|
748
|
+
inputSchema: {
|
|
749
|
+
type: "object",
|
|
750
|
+
properties: {
|
|
751
|
+
pattern: schema("string", "glob pattern, e.g. **/*.mjs, src/**/*.ts"),
|
|
752
|
+
path: schema("string", "directory to search in (default: cwd)")
|
|
753
|
+
},
|
|
754
|
+
required: ["pattern"]
|
|
755
|
+
},
|
|
756
|
+
async execute(args, ctx) {
|
|
757
|
+
return runGlob(String(args.pattern || ""), ctx.cwd, args.path || null)
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const grepTool = {
|
|
762
|
+
name: "grep",
|
|
763
|
+
description: "Search file contents by regex pattern. Use this instead of `bash` with grep/rg. Supports searching within a specific file or directory via `path`, output modes (content/files/count), multiline matching, context lines, and pagination.",
|
|
764
|
+
inputSchema: {
|
|
765
|
+
type: "object",
|
|
766
|
+
properties: {
|
|
767
|
+
pattern: schema("string", "regex or string pattern"),
|
|
768
|
+
path: schema("string", "file or directory to search in (default: cwd). Use this to search within a specific file."),
|
|
769
|
+
output_mode: schema("string", "output mode: 'content' (lines with numbers), 'files' (file paths only, default), 'count' (match counts per file)"),
|
|
770
|
+
type: schema("string", "file type filter, e.g. js, ts, py (optional)"),
|
|
771
|
+
glob: schema("string", "glob filter, e.g. *.mjs, src/**/*.ts (optional)"),
|
|
772
|
+
maxCount: schema("number", "max matches per file (optional)"),
|
|
773
|
+
context: schema("number", "lines of context around match, -C (optional)"),
|
|
774
|
+
before_context: schema("number", "lines before each match, -B (optional)"),
|
|
775
|
+
after_context: schema("number", "lines after each match, -A (optional)"),
|
|
776
|
+
ignoreCase: schema("boolean", "case insensitive search (optional)"),
|
|
777
|
+
multiline: schema("boolean", "enable cross-line matching (optional)"),
|
|
778
|
+
head_limit: schema("number", "limit output to first N lines/entries (optional)"),
|
|
779
|
+
offset: schema("number", "skip first N lines/entries before head_limit (optional)")
|
|
780
|
+
},
|
|
781
|
+
required: ["pattern"]
|
|
782
|
+
},
|
|
783
|
+
async execute(args, ctx) {
|
|
784
|
+
return runGrep(String(args.pattern || ""), ctx.cwd, {
|
|
785
|
+
path: args.path || null,
|
|
786
|
+
outputMode: args.output_mode || "files",
|
|
787
|
+
type: args.type || null,
|
|
788
|
+
glob: args.glob || null,
|
|
789
|
+
maxCount: args.maxCount || null,
|
|
790
|
+
context: args.context || null,
|
|
791
|
+
beforeContext: args.before_context || null,
|
|
792
|
+
afterContext: args.after_context || null,
|
|
793
|
+
ignoreCase: !!args.ignoreCase,
|
|
794
|
+
multiline: !!args.multiline,
|
|
795
|
+
headLimit: args.head_limit || null,
|
|
796
|
+
offset: args.offset || null
|
|
797
|
+
})
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
const bashTool = {
|
|
802
|
+
name: "bash",
|
|
803
|
+
description: "Run a shell command in cwd. ONLY use for commands that have no dedicated tool (e.g. git, npm, pip, docker). Do NOT use for: reading files (use `read`), searching files (use `grep`/`glob`), writing files (use `write`/`edit`). Long-running commands are blocked unless run_in_background is true.",
|
|
804
|
+
inputSchema: {
|
|
805
|
+
type: "object",
|
|
806
|
+
properties: {
|
|
807
|
+
command: schema("string", "shell command"),
|
|
808
|
+
timeout: schema("number", "timeout in ms (default 120000, max 600000)"),
|
|
809
|
+
description: schema("string", "human-readable description of what this command does (optional)"),
|
|
810
|
+
run_in_background: schema("boolean", "run as background task, returns task_id immediately (optional)")
|
|
811
|
+
},
|
|
812
|
+
required: ["command"]
|
|
813
|
+
},
|
|
814
|
+
async execute(args, ctx) {
|
|
815
|
+
const command = String(args.command || "")
|
|
816
|
+
const configBashTimeout = Number(ctx.config?.tool?.bash_timeout_ms || BASH_TIMEOUT_MS)
|
|
817
|
+
const timeoutMs = Math.min(Math.max(Number(args.timeout) || configBashTimeout, 1000), 600_000)
|
|
818
|
+
|
|
819
|
+
// 执行策略检查
|
|
820
|
+
const policyCheck = checkBashAllowed(command, ctx.config)
|
|
821
|
+
if (!policyCheck.allowed) {
|
|
822
|
+
return {
|
|
823
|
+
ok: false,
|
|
824
|
+
blocked: true,
|
|
825
|
+
error: "execution_policy_violation",
|
|
826
|
+
message: policyCheck.reason,
|
|
827
|
+
suggestion: "Use git_snapshot to create temporary snapshots, then manually commit when satisfied."
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (args.run_in_background) {
|
|
832
|
+
if (isLongRunningCommand(command)) {
|
|
833
|
+
return `[blocked] "${command}" appears to be a long-running command. Run it manually in your terminal.`
|
|
834
|
+
}
|
|
835
|
+
// Launch as background task
|
|
836
|
+
const task = await BackgroundManager.launch({
|
|
837
|
+
description: args.description || command,
|
|
838
|
+
payload: { command, cwd: ctx.cwd },
|
|
839
|
+
run: async () => {
|
|
840
|
+
const out = await exec(wrapCmd(command), { cwd: ctx.cwd, timeout: 600_000, encoding: "utf8" })
|
|
841
|
+
.catch(e => ({ stdout: e.stdout ?? "", stderr: e.stderr ?? e.message }))
|
|
842
|
+
return `${out.stdout || ""}${out.stderr || ""}`.trim() || "(empty output)"
|
|
843
|
+
},
|
|
844
|
+
config: ctx.config
|
|
845
|
+
})
|
|
846
|
+
return `background task launched: ${task.id}\nUse background_output to check results.`
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
return runBash(command, ctx.cwd, timeoutMs)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
const outputTool = {
|
|
854
|
+
name: "background_output",
|
|
855
|
+
description: "Retrieve status, logs, and result of a background task launched via `task` with `run_in_background: true`. Returns the task object including status and output.",
|
|
856
|
+
inputSchema: {
|
|
857
|
+
type: "object",
|
|
858
|
+
properties: {
|
|
859
|
+
task_id: schema("string", "background task id")
|
|
860
|
+
},
|
|
861
|
+
required: ["task_id"]
|
|
862
|
+
},
|
|
863
|
+
async execute(args) {
|
|
864
|
+
const task = await BackgroundManager.get(String(args.task_id || ""))
|
|
865
|
+
if (!task) return "background task not found"
|
|
866
|
+
return {
|
|
867
|
+
...BackgroundManager.summarize(task),
|
|
868
|
+
result: task.result,
|
|
869
|
+
error: task.error || null
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
const taskListTool = {
|
|
875
|
+
name: "task_list",
|
|
876
|
+
description: "List delegated background tasks with concise lifecycle summaries.",
|
|
877
|
+
inputSchema: { type: "object", properties: {}, required: [] },
|
|
878
|
+
async execute() {
|
|
879
|
+
const tasks = await BackgroundManager.list()
|
|
880
|
+
return tasks.map((task) => BackgroundManager.summarize(task))
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const taskGetTool = {
|
|
885
|
+
name: "task_get",
|
|
886
|
+
description: "Retrieve one delegated background task summary and result payload by task_id.",
|
|
887
|
+
inputSchema: {
|
|
888
|
+
type: "object",
|
|
889
|
+
properties: {
|
|
890
|
+
task_id: schema("string", "background task id")
|
|
891
|
+
},
|
|
892
|
+
required: ["task_id"]
|
|
893
|
+
},
|
|
894
|
+
async execute(args) {
|
|
895
|
+
const task = await BackgroundManager.get(String(args.task_id || ""))
|
|
896
|
+
if (!task) return "background task not found"
|
|
897
|
+
return {
|
|
898
|
+
...BackgroundManager.summarize(task),
|
|
899
|
+
result: task.result,
|
|
900
|
+
error: task.error || null
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
const taskStopTool = {
|
|
906
|
+
name: "task_stop",
|
|
907
|
+
description: "Cancel a delegated background task by task_id.",
|
|
908
|
+
inputSchema: {
|
|
909
|
+
type: "object",
|
|
910
|
+
properties: {
|
|
911
|
+
task_id: schema("string", "background task id")
|
|
912
|
+
},
|
|
913
|
+
required: ["task_id"]
|
|
914
|
+
},
|
|
915
|
+
async execute(args) {
|
|
916
|
+
const ok = await BackgroundManager.cancel(String(args.task_id || ""))
|
|
917
|
+
return ok ? "cancel requested" : "background task not found"
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
const taskOutputTool = {
|
|
922
|
+
name: "task_output",
|
|
923
|
+
description: "Retrieve delegated background task output with summary, result payload, and next-action guidance.",
|
|
924
|
+
inputSchema: {
|
|
925
|
+
type: "object",
|
|
926
|
+
properties: {
|
|
927
|
+
task_id: schema("string", "background task id")
|
|
928
|
+
},
|
|
929
|
+
required: ["task_id"]
|
|
930
|
+
},
|
|
931
|
+
async execute(args) {
|
|
932
|
+
const task = await BackgroundManager.get(String(args.task_id || ""))
|
|
933
|
+
if (!task) return "background task not found"
|
|
934
|
+
return {
|
|
935
|
+
...BackgroundManager.summarize(task),
|
|
936
|
+
result: task.result,
|
|
937
|
+
error: task.error || null
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
const cancelTool = {
|
|
943
|
+
name: "background_cancel",
|
|
944
|
+
description: "Cancel a running background task by its task_id. Only works on tasks launched via `task` with `run_in_background: true`.",
|
|
945
|
+
inputSchema: {
|
|
946
|
+
type: "object",
|
|
947
|
+
properties: {
|
|
948
|
+
task_id: schema("string", "background task id")
|
|
949
|
+
},
|
|
950
|
+
required: ["task_id"]
|
|
951
|
+
},
|
|
952
|
+
async execute(args) {
|
|
953
|
+
const ok = await BackgroundManager.cancel(String(args.task_id || ""))
|
|
954
|
+
return ok ? "cancel requested" : "background task not found"
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const todowriteTool = {
|
|
959
|
+
name: "todowrite",
|
|
960
|
+
description: "Create or update a structured task list for tracking multi-step work. ALWAYS create a todo list before starting any task with 2+ steps. Mark items in_progress/completed as you work. Only ONE item should be in_progress at a time.",
|
|
961
|
+
inputSchema: {
|
|
962
|
+
type: "object",
|
|
963
|
+
properties: {
|
|
964
|
+
todos: {
|
|
965
|
+
type: "array",
|
|
966
|
+
description: "The updated todo list",
|
|
967
|
+
items: {
|
|
968
|
+
type: "object",
|
|
969
|
+
properties: {
|
|
970
|
+
content: schema("string", "task description in imperative form (e.g. 'Run tests')"),
|
|
971
|
+
activeForm: schema("string", "present continuous form shown during execution (e.g. 'Running tests')"),
|
|
972
|
+
status: { type: "string", enum: ["pending", "in_progress", "completed"], description: "task status" }
|
|
973
|
+
},
|
|
974
|
+
required: ["content", "status"]
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
},
|
|
978
|
+
required: ["todos"]
|
|
979
|
+
},
|
|
980
|
+
async execute(args, ctx) {
|
|
981
|
+
const todos = args.todos || []
|
|
982
|
+
ctx._todoState = todos
|
|
983
|
+
const summary = todos.map((t) => {
|
|
984
|
+
const active = t.status === "in_progress" && t.activeForm ? ` (${t.activeForm})` : ""
|
|
985
|
+
return `[${t.status}] ${t.content}${active}`
|
|
986
|
+
}).join("\n")
|
|
987
|
+
return `Todo list updated (${todos.length} items):\n${summary}`
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
const questionTool = {
|
|
992
|
+
name: "question",
|
|
993
|
+
description: "Ask the user one or more structured questions and wait for their answers. Use when you need user input to proceed — e.g. ambiguous requirements, implementation choices, or missing information. Supports predefined options, multi-select, and custom text input. Returns actual user answers.",
|
|
994
|
+
inputSchema: {
|
|
995
|
+
type: "object",
|
|
996
|
+
properties: {
|
|
997
|
+
questions: {
|
|
998
|
+
type: "array",
|
|
999
|
+
description: "questions to ask the user",
|
|
1000
|
+
items: {
|
|
1001
|
+
type: "object",
|
|
1002
|
+
properties: {
|
|
1003
|
+
id: schema("string", "unique question identifier"),
|
|
1004
|
+
text: schema("string", "question text"),
|
|
1005
|
+
header: schema("string", "short label for tab chip (max 12 chars)"),
|
|
1006
|
+
description: schema("string", "supplementary description (optional)"),
|
|
1007
|
+
options: {
|
|
1008
|
+
type: "array",
|
|
1009
|
+
items: {
|
|
1010
|
+
type: "object",
|
|
1011
|
+
properties: {
|
|
1012
|
+
label: schema("string", "option display text"),
|
|
1013
|
+
value: schema("string", "option value (defaults to label)"),
|
|
1014
|
+
description: schema("string", "option description (optional)")
|
|
1015
|
+
},
|
|
1016
|
+
required: ["label"]
|
|
1017
|
+
},
|
|
1018
|
+
description: "predefined choices (optional)"
|
|
1019
|
+
},
|
|
1020
|
+
multi: schema("boolean", "allow multiple selections (default false)"),
|
|
1021
|
+
allowCustom: schema("boolean", "allow custom text input (default true)")
|
|
1022
|
+
},
|
|
1023
|
+
required: ["id", "text"]
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
},
|
|
1027
|
+
required: ["questions"]
|
|
1028
|
+
},
|
|
1029
|
+
async execute(args) {
|
|
1030
|
+
if (args && args._allowQuestion === false) {
|
|
1031
|
+
return "question tool disabled in this phase"
|
|
1032
|
+
}
|
|
1033
|
+
const questions = Array.isArray(args.questions) ? args.questions : []
|
|
1034
|
+
if (questions.length === 0) {
|
|
1035
|
+
return "error: at least one question is required"
|
|
1036
|
+
}
|
|
1037
|
+
// Normalize questions
|
|
1038
|
+
const normalized = questions.map((q, i) => ({
|
|
1039
|
+
id: String(q.id || `q${i}`),
|
|
1040
|
+
text: String(q.text || ""),
|
|
1041
|
+
description: q.description ? String(q.description) : "",
|
|
1042
|
+
options: Array.isArray(q.options) ? q.options.map((o) => ({
|
|
1043
|
+
label: String(o.label || ""),
|
|
1044
|
+
value: String(o.value || o.label || ""),
|
|
1045
|
+
description: o.description ? String(o.description) : ""
|
|
1046
|
+
})) : [],
|
|
1047
|
+
multi: !!q.multi,
|
|
1048
|
+
allowCustom: q.allowCustom !== false
|
|
1049
|
+
}))
|
|
1050
|
+
const answers = await askQuestionInteractive({ questions: normalized })
|
|
1051
|
+
// Format response
|
|
1052
|
+
const lines = normalized.map((q) => {
|
|
1053
|
+
const answer = answers[q.id] ?? "(skipped)"
|
|
1054
|
+
return `[${q.id}] ${q.text} → ${answer}`
|
|
1055
|
+
})
|
|
1056
|
+
return lines.join("\n")
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
const webfetchTool = {
|
|
1061
|
+
name: "webfetch",
|
|
1062
|
+
description: "Fetch content from a public URL and return it as text. HTML is converted to markdown. Content over 50KB is truncated. Only use for public, unauthenticated URLs. Do NOT use for local file reading — use `read` instead.",
|
|
1063
|
+
inputSchema: {
|
|
1064
|
+
type: "object",
|
|
1065
|
+
properties: {
|
|
1066
|
+
url: schema("string", "URL to fetch"),
|
|
1067
|
+
prompt: schema("string", "optional processing instruction")
|
|
1068
|
+
},
|
|
1069
|
+
required: ["url"]
|
|
1070
|
+
},
|
|
1071
|
+
async execute(args) {
|
|
1072
|
+
const url = String(args.url || "")
|
|
1073
|
+
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
1074
|
+
return "error: URL must start with http:// or https://"
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
const response = await fetch(url, {
|
|
1078
|
+
headers: { "user-agent": "kkcode/0.1" },
|
|
1079
|
+
signal: AbortSignal.timeout(30000)
|
|
1080
|
+
})
|
|
1081
|
+
if (!response.ok) return `error: HTTP ${response.status}`
|
|
1082
|
+
const text = await response.text()
|
|
1083
|
+
const truncated = text.length > 50000 ? text.slice(0, 50000) + "\n...(truncated)" : text
|
|
1084
|
+
return truncated
|
|
1085
|
+
} catch (error) {
|
|
1086
|
+
return `error: ${error.message}`
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
const skillTool = {
|
|
1092
|
+
name: "skill",
|
|
1093
|
+
description: "Invoke a registered skill by name. Skills are pre-built prompt templates or programmable modules that provide specialized capabilities. Use this when a task matches an available skill listed in the system prompt, or when the user mentions a slash command like '/commit'.",
|
|
1094
|
+
inputSchema: {
|
|
1095
|
+
type: "object",
|
|
1096
|
+
properties: {
|
|
1097
|
+
skill: schema("string", "skill name without '/' prefix (e.g. 'commit', 'init', 'frontend')"),
|
|
1098
|
+
args: schema("string", "optional arguments to pass to the skill (e.g. 'vue' for /init vue)")
|
|
1099
|
+
},
|
|
1100
|
+
required: ["skill"]
|
|
1101
|
+
},
|
|
1102
|
+
async execute(args, ctx) {
|
|
1103
|
+
const name = String(args.skill || "").trim()
|
|
1104
|
+
if (!name) return "error: skill name is required"
|
|
1105
|
+
if (!SkillRegistry.isReady()) return "error: skill registry not initialized"
|
|
1106
|
+
const skill = SkillRegistry.get(name)
|
|
1107
|
+
if (!skill) {
|
|
1108
|
+
const available = SkillRegistry.list().map(s => s.name).join(", ")
|
|
1109
|
+
return `error: skill "${name}" not found. Available: ${available}`
|
|
1110
|
+
}
|
|
1111
|
+
const result = await SkillRegistry.execute(name, String(args.args || ""), {
|
|
1112
|
+
cwd: ctx.cwd,
|
|
1113
|
+
mode: ctx.mode || "agent",
|
|
1114
|
+
model: ctx.model || "",
|
|
1115
|
+
provider: ctx.provider || "",
|
|
1116
|
+
config: ctx.config || null
|
|
1117
|
+
})
|
|
1118
|
+
if (!result) return `skill /${name} returned no output`
|
|
1119
|
+
// contextFork skills return { prompt, contextFork, model }
|
|
1120
|
+
if (typeof result === "object" && result.contextFork) {
|
|
1121
|
+
return result.prompt || ""
|
|
1122
|
+
}
|
|
1123
|
+
return result
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
const EXA_MCP_URL = "https://mcp.exa.ai/mcp"
|
|
1128
|
+
const EXA_TIMEOUT_MS = 25000
|
|
1129
|
+
|
|
1130
|
+
async function callExaMcp(toolName, args, signal) {
|
|
1131
|
+
const body = JSON.stringify({
|
|
1132
|
+
jsonrpc: "2.0",
|
|
1133
|
+
id: 1,
|
|
1134
|
+
method: "tools/call",
|
|
1135
|
+
params: { name: toolName, arguments: args }
|
|
1136
|
+
})
|
|
1137
|
+
const response = await fetch(EXA_MCP_URL, {
|
|
1138
|
+
method: "POST",
|
|
1139
|
+
headers: { "Content-Type": "application/json", Accept: "application/json, text/event-stream" },
|
|
1140
|
+
body,
|
|
1141
|
+
signal: signal || AbortSignal.timeout(EXA_TIMEOUT_MS)
|
|
1142
|
+
})
|
|
1143
|
+
if (!response.ok) {
|
|
1144
|
+
const err = await response.text().catch(() => "")
|
|
1145
|
+
throw new Error(`Exa search error (${response.status}): ${err}`)
|
|
1146
|
+
}
|
|
1147
|
+
const text = await response.text()
|
|
1148
|
+
for (const line of text.split("\n")) {
|
|
1149
|
+
if (line.startsWith("data: ")) {
|
|
1150
|
+
const data = JSON.parse(line.slice(6))
|
|
1151
|
+
if (data.result?.content?.[0]?.text) return data.result.content[0].text
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
return null
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
const websearchTool = {
|
|
1158
|
+
name: "websearch",
|
|
1159
|
+
description: "Search the web for up-to-date information. Use this PROACTIVELY when you are unsure about facts, APIs, library versions, error messages, or anything beyond your training data. Reduces hallucination by grounding answers in real search results. Returns relevant web page content.",
|
|
1160
|
+
inputSchema: {
|
|
1161
|
+
type: "object",
|
|
1162
|
+
properties: {
|
|
1163
|
+
query: schema("string", "search query"),
|
|
1164
|
+
numResults: schema("number", "number of results to return (default: 5)"),
|
|
1165
|
+
type: schema("string", "search type: 'auto' (default), 'fast' (quick), 'deep' (comprehensive)")
|
|
1166
|
+
},
|
|
1167
|
+
required: ["query"]
|
|
1168
|
+
},
|
|
1169
|
+
async execute(args, ctx) {
|
|
1170
|
+
const query = String(args.query || "").trim()
|
|
1171
|
+
if (!query) return "error: query is required"
|
|
1172
|
+
try {
|
|
1173
|
+
const result = await callExaMcp("web_search_exa", {
|
|
1174
|
+
query,
|
|
1175
|
+
numResults: Number(args.numResults) || 5,
|
|
1176
|
+
type: args.type || "auto",
|
|
1177
|
+
livecrawl: "fallback"
|
|
1178
|
+
}, ctx.signal)
|
|
1179
|
+
return result || "No results found. Try a different query."
|
|
1180
|
+
} catch (error) {
|
|
1181
|
+
if (error.name === "AbortError" || error.name === "TimeoutError") return "error: search request timed out"
|
|
1182
|
+
return `error: ${error.message}`
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
const codesearchTool = {
|
|
1188
|
+
name: "codesearch",
|
|
1189
|
+
description: "Search for code examples, API documentation, and SDK usage. Use this PROACTIVELY when working with unfamiliar libraries, frameworks, or APIs. Returns relevant code snippets and documentation from the web. Especially useful for: correct API signatures, configuration examples, migration guides, and best practices.",
|
|
1190
|
+
inputSchema: {
|
|
1191
|
+
type: "object",
|
|
1192
|
+
properties: {
|
|
1193
|
+
query: schema("string", "search query for APIs, libraries, SDKs (e.g. 'Express.js middleware', 'React useState hook')"),
|
|
1194
|
+
tokensNum: schema("number", "amount of context to return, 1000-50000 (default: 5000)")
|
|
1195
|
+
},
|
|
1196
|
+
required: ["query"]
|
|
1197
|
+
},
|
|
1198
|
+
async execute(args, ctx) {
|
|
1199
|
+
const query = String(args.query || "").trim()
|
|
1200
|
+
if (!query) return "error: query is required"
|
|
1201
|
+
try {
|
|
1202
|
+
const result = await callExaMcp("get_code_context_exa", {
|
|
1203
|
+
query,
|
|
1204
|
+
tokensNum: Math.min(Math.max(Number(args.tokensNum) || 5000, 1000), 50000)
|
|
1205
|
+
}, ctx.signal)
|
|
1206
|
+
return result || "No code context found. Try a more specific query."
|
|
1207
|
+
} catch (error) {
|
|
1208
|
+
if (error.name === "AbortError" || error.name === "TimeoutError") return "error: code search request timed out"
|
|
1209
|
+
return `error: ${error.message}`
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
const multieditTool = {
|
|
1215
|
+
name: "multiedit",
|
|
1216
|
+
description: "Apply multiple file edits atomically in a single operation. All changes succeed together or are rolled back entirely. Use this instead of multiple sequential `edit` calls when modifying related code across files (e.g. renaming an export and updating all imports). Each file must have been `read` first.",
|
|
1217
|
+
inputSchema: {
|
|
1218
|
+
type: "object",
|
|
1219
|
+
properties: {
|
|
1220
|
+
changes: {
|
|
1221
|
+
type: "array",
|
|
1222
|
+
description: "list of file changes to apply atomically",
|
|
1223
|
+
items: {
|
|
1224
|
+
type: "object",
|
|
1225
|
+
properties: {
|
|
1226
|
+
path: schema("string", "file path"),
|
|
1227
|
+
before: schema("string", "text to find (required for edits, omit for new file creation)"),
|
|
1228
|
+
after: schema("string", "replacement text (for edits) or full content (for new files)"),
|
|
1229
|
+
replace_all: schema("boolean", "replace all occurrences of before (default: false)")
|
|
1230
|
+
},
|
|
1231
|
+
required: ["path", "after"]
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
},
|
|
1235
|
+
required: ["changes"]
|
|
1236
|
+
},
|
|
1237
|
+
async execute(args, ctx) {
|
|
1238
|
+
const changes = Array.isArray(args.changes) ? args.changes : []
|
|
1239
|
+
if (!changes.length) return "error: at least one change is required"
|
|
1240
|
+
|
|
1241
|
+
// Phase 1: validate all changes and collect original content for rollback
|
|
1242
|
+
const snapshots = [] // { path, original, isNew }
|
|
1243
|
+
const resolved = []
|
|
1244
|
+
for (const change of changes) {
|
|
1245
|
+
const target = path.resolve(ctx.cwd, change.path)
|
|
1246
|
+
const originalExists = await exists(target)
|
|
1247
|
+
const hasBefore = Object.prototype.hasOwnProperty.call(change, "before")
|
|
1248
|
+
const isCreate = !originalExists && !hasBefore
|
|
1249
|
+
if (originalExists && !hasBefore) {
|
|
1250
|
+
return `error: "${change.path}" already exists. Provide a "before" snippet for existing-file multiedit changes.`
|
|
1251
|
+
}
|
|
1252
|
+
let original = null
|
|
1253
|
+
try {
|
|
1254
|
+
original = await readFile(target, "utf8")
|
|
1255
|
+
} catch { /* new file */ }
|
|
1256
|
+
|
|
1257
|
+
if (!isCreate && original === null) {
|
|
1258
|
+
return `error: "${change.path}" does not exist. Omit "before" only for new-file creation.`
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
if (!isCreate && original !== null) {
|
|
1262
|
+
const validation = await validateExistingFileMutation({
|
|
1263
|
+
targetPath: target,
|
|
1264
|
+
displayPath: String(change.path || target),
|
|
1265
|
+
operation: "applying this multiedit change"
|
|
1266
|
+
})
|
|
1267
|
+
if (!validation.ok) return validation.message
|
|
1268
|
+
const matches = (original || "").split(change.before).length - 1
|
|
1269
|
+
if (matches === 0) return `error: no match for "before" in ${change.path}. Re-read the file and check your snippet.`
|
|
1270
|
+
if (matches > 1 && !change.replace_all) return `error: ${matches} matches in ${change.path} — set replace_all: true or provide more context.`
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
snapshots.push({ path: target, original, isNew: original === null })
|
|
1274
|
+
resolved.push({ target, ...change, isCreate })
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
// Phase 2: apply all changes
|
|
1278
|
+
const applied = []
|
|
1279
|
+
try {
|
|
1280
|
+
for (const change of resolved) {
|
|
1281
|
+
if (change.isCreate) {
|
|
1282
|
+
await atomicWriteFile(change.target, String(change.after))
|
|
1283
|
+
} else {
|
|
1284
|
+
const snap = snapshots.find(s => s.path === change.target)
|
|
1285
|
+
const content = snap?.original ?? await readFile(change.target, "utf8")
|
|
1286
|
+
const next = change.replace_all
|
|
1287
|
+
? content.replaceAll(change.before, change.after)
|
|
1288
|
+
: content.replace(change.before, change.after)
|
|
1289
|
+
await atomicWriteFile(change.target, next)
|
|
1290
|
+
}
|
|
1291
|
+
await refreshFileReadStateFromDisk(change.target).catch(() => {})
|
|
1292
|
+
applied.push(change.target)
|
|
1293
|
+
}
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
// Rollback all applied changes
|
|
1296
|
+
for (let i = applied.length - 1; i >= 0; i--) {
|
|
1297
|
+
const snap = snapshots.find(s => s.path === applied[i])
|
|
1298
|
+
if (!snap) continue
|
|
1299
|
+
try {
|
|
1300
|
+
if (snap.isNew) {
|
|
1301
|
+
await unlink(applied[i]).catch(() => {})
|
|
1302
|
+
} else if (snap.original !== null) {
|
|
1303
|
+
await atomicWriteFile(applied[i], snap.original)
|
|
1304
|
+
}
|
|
1305
|
+
} catch { /* best effort rollback */ }
|
|
1306
|
+
}
|
|
1307
|
+
return `error: failed at ${applied.length + 1}/${resolved.length} — all changes rolled back. Cause: ${error.message}`
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
// Phase 3: summarize
|
|
1311
|
+
const summary = resolved.map(c => ` ${c.isCreate ? "+" : "~"} ${c.path}`).join("\n")
|
|
1312
|
+
return {
|
|
1313
|
+
output: `${resolved.length} file(s) updated atomically:\n${summary}`,
|
|
1314
|
+
metadata: {
|
|
1315
|
+
fileChanges: resolved.map(c => ({
|
|
1316
|
+
path: String(c.path || c.target),
|
|
1317
|
+
tool: "multiedit",
|
|
1318
|
+
stageId: ctx.stageId || null,
|
|
1319
|
+
taskId: ctx.logicalTaskId || ctx.taskId || null
|
|
1320
|
+
})),
|
|
1321
|
+
mutations: resolved.map((c) => {
|
|
1322
|
+
const snap = snapshots.find((s) => s.path === c.target)
|
|
1323
|
+
const originalContent = snap?.original ?? null
|
|
1324
|
+
const updatedContent = c.isCreate
|
|
1325
|
+
? String(c.after)
|
|
1326
|
+
: c.replace_all
|
|
1327
|
+
? String(originalContent ?? "").replaceAll(String(c.before), String(c.after))
|
|
1328
|
+
: String(originalContent ?? "").replace(String(c.before), String(c.after))
|
|
1329
|
+
const diff = diffLineCount(originalContent ?? "", updatedContent)
|
|
1330
|
+
return {
|
|
1331
|
+
operation: "multiedit",
|
|
1332
|
+
filePath: String(c.path || c.target),
|
|
1333
|
+
originalContent,
|
|
1334
|
+
updatedContent,
|
|
1335
|
+
structuredPatch: buildStructuredPatch(originalContent ?? "", updatedContent),
|
|
1336
|
+
addedLines: diff.added,
|
|
1337
|
+
removedLines: diff.removed
|
|
1338
|
+
}
|
|
1339
|
+
})
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
const enterPlanTool = {
|
|
1346
|
+
name: "enter_plan",
|
|
1347
|
+
description: "Enter planning mode. Use this PROACTIVELY when the task is non-trivial and requires architectural decisions, multi-file changes, or when multiple valid approaches exist. After calling this, outline your plan, then call `exit_plan` to present it to the user for approval.",
|
|
1348
|
+
inputSchema: {
|
|
1349
|
+
type: "object",
|
|
1350
|
+
properties: {
|
|
1351
|
+
reason: schema("string", "why planning is needed (shown to user)")
|
|
1352
|
+
},
|
|
1353
|
+
required: []
|
|
1354
|
+
},
|
|
1355
|
+
async execute(args, ctx) {
|
|
1356
|
+
ctx._planMode = true
|
|
1357
|
+
return `Planning mode entered. Outline your plan now, then call exit_plan to present it for user approval.${args.reason ? ` Reason: ${args.reason}` : ""}`
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
const exitPlanTool = {
|
|
1362
|
+
name: "exit_plan",
|
|
1363
|
+
description: "Present your plan to the user for approval. The user will see the plan and can approve, reject, or request changes. Only call this after enter_plan and after you have outlined a complete plan in your response.",
|
|
1364
|
+
inputSchema: {
|
|
1365
|
+
type: "object",
|
|
1366
|
+
properties: {
|
|
1367
|
+
plan: schema("string", "the complete plan text to present to the user"),
|
|
1368
|
+
files: {
|
|
1369
|
+
type: "array", items: { type: "string" },
|
|
1370
|
+
description: "list of files that will be created or modified"
|
|
1371
|
+
}
|
|
1372
|
+
},
|
|
1373
|
+
required: ["plan"]
|
|
1374
|
+
},
|
|
1375
|
+
async execute(args, ctx) {
|
|
1376
|
+
if (!ctx._planMode) {
|
|
1377
|
+
return {
|
|
1378
|
+
output: "Cannot exit plan mode — you are not currently in plan mode. Call enter_plan first.",
|
|
1379
|
+
metadata: {}
|
|
1380
|
+
}
|
|
1381
|
+
}
|
|
1382
|
+
ctx._planMode = false
|
|
1383
|
+
return {
|
|
1384
|
+
output: "Plan submitted for user approval.",
|
|
1385
|
+
metadata: {
|
|
1386
|
+
planApproval: true,
|
|
1387
|
+
plan: String(args.plan || ""),
|
|
1388
|
+
files: Array.isArray(args.files) ? args.files : []
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
const notebookeditTool = {
|
|
1395
|
+
name: "notebookedit",
|
|
1396
|
+
description: "Edit a Jupyter notebook (.ipynb) cell. Supports replace, insert, and delete operations on individual cells. Use this instead of `write` when modifying notebooks — it preserves cell metadata and outputs. Notebooks must be read first and stale notebooks are rejected.",
|
|
1397
|
+
inputSchema: {
|
|
1398
|
+
type: "object",
|
|
1399
|
+
properties: {
|
|
1400
|
+
path: schema("string", "notebook file path (.ipynb)"),
|
|
1401
|
+
cell_number: schema("number", "0-indexed cell number to operate on (default: 0)"),
|
|
1402
|
+
new_source: schema("string", "new cell source content"),
|
|
1403
|
+
cell_type: { type: "string", enum: ["code", "markdown"], description: "cell type (required for insert)" },
|
|
1404
|
+
edit_mode: { type: "string", enum: ["replace", "insert", "delete"], description: "operation type (default: replace)" }
|
|
1405
|
+
},
|
|
1406
|
+
required: ["path", "new_source"]
|
|
1407
|
+
},
|
|
1408
|
+
async execute(args, ctx) {
|
|
1409
|
+
const target = path.resolve(ctx.cwd, args.path)
|
|
1410
|
+
if (await exists(target)) {
|
|
1411
|
+
const validation = await validateExistingFileMutation({
|
|
1412
|
+
targetPath: target,
|
|
1413
|
+
displayPath: String(args.path || target),
|
|
1414
|
+
operation: "editing the notebook",
|
|
1415
|
+
requireFullRead: true
|
|
1416
|
+
})
|
|
1417
|
+
if (!validation.ok) {
|
|
1418
|
+
return {
|
|
1419
|
+
output: validation.message,
|
|
1420
|
+
metadata: { blocked: true, reason: validation.reason, fileChanges: [] }
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
const raw = await readFile(target, "utf8")
|
|
1425
|
+
const notebook = JSON.parse(raw)
|
|
1426
|
+
if (!notebook.cells || !Array.isArray(notebook.cells)) {
|
|
1427
|
+
return "error: not a valid .ipynb file (missing cells array)"
|
|
1428
|
+
}
|
|
1429
|
+
const mode = args.edit_mode || "replace"
|
|
1430
|
+
const cellNum = Number(args.cell_number ?? 0)
|
|
1431
|
+
const source = String(args.new_source ?? "")
|
|
1432
|
+
const sourceLines = source.split("\n").map((line, i, arr) => i < arr.length - 1 ? line + "\n" : line)
|
|
1433
|
+
|
|
1434
|
+
if (mode === "insert") {
|
|
1435
|
+
const cellType = args.cell_type
|
|
1436
|
+
if (!cellType || !["code", "markdown"].includes(cellType)) {
|
|
1437
|
+
return "error: cell_type is required for insert mode (must be 'code' or 'markdown')"
|
|
1438
|
+
}
|
|
1439
|
+
const newCell = {
|
|
1440
|
+
cell_type: cellType,
|
|
1441
|
+
metadata: {},
|
|
1442
|
+
source: sourceLines
|
|
1443
|
+
}
|
|
1444
|
+
if (cellType === "code") {
|
|
1445
|
+
newCell.execution_count = null
|
|
1446
|
+
newCell.outputs = []
|
|
1447
|
+
}
|
|
1448
|
+
const insertAt = cellNum < 0 ? 0 : Math.min(cellNum + 1, notebook.cells.length)
|
|
1449
|
+
notebook.cells.splice(insertAt, 0, newCell)
|
|
1450
|
+
} else if (mode === "delete") {
|
|
1451
|
+
if (cellNum < 0 || cellNum >= notebook.cells.length) {
|
|
1452
|
+
return `error: cell_number ${cellNum} out of range (0-${notebook.cells.length - 1})`
|
|
1453
|
+
}
|
|
1454
|
+
notebook.cells.splice(cellNum, 1)
|
|
1455
|
+
} else {
|
|
1456
|
+
// replace
|
|
1457
|
+
if (cellNum < 0 || cellNum >= notebook.cells.length) {
|
|
1458
|
+
return `error: cell_number ${cellNum} out of range (0-${notebook.cells.length - 1})`
|
|
1459
|
+
}
|
|
1460
|
+
const cell = notebook.cells[cellNum]
|
|
1461
|
+
cell.source = sourceLines
|
|
1462
|
+
if (args.cell_type && args.cell_type !== cell.cell_type) {
|
|
1463
|
+
cell.cell_type = args.cell_type
|
|
1464
|
+
if (args.cell_type === "markdown") {
|
|
1465
|
+
delete cell.execution_count
|
|
1466
|
+
delete cell.outputs
|
|
1467
|
+
} else if (args.cell_type === "code") {
|
|
1468
|
+
cell.execution_count = null
|
|
1469
|
+
cell.outputs = []
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
const finalNotebook = JSON.stringify(notebook, null, 1) + "\n"
|
|
1475
|
+
await atomicWriteFile(target, finalNotebook)
|
|
1476
|
+
await refreshFileReadStateFromDisk(target, { content: finalNotebook }).catch(() => {})
|
|
1477
|
+
const actionLabel = mode === "insert" ? "inserted" : mode === "delete" ? "deleted" : "replaced"
|
|
1478
|
+
return {
|
|
1479
|
+
output: `${actionLabel} cell ${cellNum} in ${args.path} (${notebook.cells.length} cells total)`,
|
|
1480
|
+
metadata: mutationMetadata({
|
|
1481
|
+
operation: "notebookedit",
|
|
1482
|
+
filePath: String(args.path || target),
|
|
1483
|
+
originalContent: raw,
|
|
1484
|
+
updatedContent: finalNotebook,
|
|
1485
|
+
structuredPatch: buildStructuredPatch(raw, finalNotebook),
|
|
1486
|
+
addedLines: 0,
|
|
1487
|
+
removedLines: 0,
|
|
1488
|
+
stageId: ctx.stageId || null,
|
|
1489
|
+
taskId: ctx.logicalTaskId || ctx.taskId || null
|
|
1490
|
+
})
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
const patchTool = {
|
|
1496
|
+
name: "patch",
|
|
1497
|
+
description: "Replace a range of lines in a file by line numbers. Read the file first with `read` (use offset/limit for large files) to see line numbers, then specify the line range to replace. Lines are 1-based and inclusive. Ideal for modifying specific sections of large files without needing to match exact text.",
|
|
1498
|
+
inputSchema: {
|
|
1499
|
+
type: "object",
|
|
1500
|
+
properties: {
|
|
1501
|
+
path: schema("string", "file path"),
|
|
1502
|
+
start_line: schema("number", "first line to replace (1-based, inclusive)"),
|
|
1503
|
+
end_line: schema("number", "last line to replace (1-based, inclusive)"),
|
|
1504
|
+
content: schema("string", "replacement content (replaces the line range). Empty string deletes lines.")
|
|
1505
|
+
},
|
|
1506
|
+
required: ["path", "start_line", "end_line", "content"]
|
|
1507
|
+
},
|
|
1508
|
+
async execute(args, ctx) {
|
|
1509
|
+
const target = path.resolve(ctx.cwd, args.path)
|
|
1510
|
+
|
|
1511
|
+
if (await exists(target)) {
|
|
1512
|
+
const validation = await validateExistingFileMutation({
|
|
1513
|
+
targetPath: target,
|
|
1514
|
+
displayPath: String(args.path || target),
|
|
1515
|
+
operation: "patching it"
|
|
1516
|
+
})
|
|
1517
|
+
if (!validation.ok) {
|
|
1518
|
+
return {
|
|
1519
|
+
output: validation.message,
|
|
1520
|
+
metadata: { blocked: true, reason: validation.reason, fileChanges: [] }
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
const startLine = Math.max(1, Number(args.start_line) || 1)
|
|
1526
|
+
const endLine = Math.max(startLine, Number(args.end_line) || startLine)
|
|
1527
|
+
const content = String(args.content ?? "")
|
|
1528
|
+
|
|
1529
|
+
const options = lockOptions(ctx)
|
|
1530
|
+
let result
|
|
1531
|
+
const runPatch = async () => {
|
|
1532
|
+
const existing = await readFile(target, "utf8")
|
|
1533
|
+
const lines = existing.split("\n")
|
|
1534
|
+
if (startLine > lines.length) {
|
|
1535
|
+
throw new Error(`start_line ${startLine} exceeds file length (${lines.length} lines)`)
|
|
1536
|
+
}
|
|
1537
|
+
const startIdx = startLine - 1
|
|
1538
|
+
const endIdx = Math.min(endLine, lines.length)
|
|
1539
|
+
const newLines = content === "" ? [] : content.split("\n")
|
|
1540
|
+
lines.splice(startIdx, endIdx - startIdx, ...newLines)
|
|
1541
|
+
const final = lines.join("\n")
|
|
1542
|
+
await atomicWriteFile(target, final)
|
|
1543
|
+
return { removedCount: endIdx - startIdx, insertedCount: newLines.length, previous: existing, final }
|
|
1544
|
+
}
|
|
1545
|
+
|
|
1546
|
+
if (options.mode === "file_lock") {
|
|
1547
|
+
result = await withFileLock({ targetPath: target, owner: options.owner, waitTimeoutMs: options.waitTimeoutMs, run: runPatch })
|
|
1548
|
+
} else {
|
|
1549
|
+
result = await runPatch()
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
await refreshFileReadStateFromDisk(target, { content: result.final }).catch(() => {})
|
|
1553
|
+
return {
|
|
1554
|
+
output: `patched ${args.path}: replaced lines ${startLine}-${endLine} (removed ${result.removedCount}, inserted ${result.insertedCount})`,
|
|
1555
|
+
metadata: mutationMetadata({
|
|
1556
|
+
operation: "patch",
|
|
1557
|
+
filePath: String(args.path || target),
|
|
1558
|
+
originalContent: result.previous,
|
|
1559
|
+
updatedContent: result.final,
|
|
1560
|
+
structuredPatch: buildStructuredPatch(result.previous, result.final, { oldStart: startLine, newStart: startLine }),
|
|
1561
|
+
addedLines: result.insertedCount,
|
|
1562
|
+
removedLines: result.removedCount,
|
|
1563
|
+
stageId: ctx.stageId || null,
|
|
1564
|
+
taskId: ctx.logicalTaskId || ctx.taskId || null
|
|
1565
|
+
})
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
const gitTools = config?.git_auto?.enabled !== false ? gitAutoTools : []
|
|
1571
|
+
const gitFullAutoToolsList = config?.git_auto?.full_auto === true ? gitFullAutoTools : []
|
|
1572
|
+
|
|
1573
|
+
return [listTool, sysinfoTool, readTool, writeTool, editTool, patchTool, multieditTool, globTool, grepTool, bashTool, createTaskTool(), outputTool, cancelTool, taskListTool, taskGetTool, taskStopTool, taskOutputTool, todowriteTool, questionTool, skillTool, webfetchTool, websearchTool, codesearchTool, notebookeditTool, enterPlanTool, exitPlanTool, ...gitTools, ...gitFullAutoToolsList]
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function mcpTools() {
|
|
1577
|
+
return McpRegistry.listTools().map((tool) => ({
|
|
1578
|
+
name: tool.id,
|
|
1579
|
+
description: `[mcp:${tool.server}] ${tool.description}`,
|
|
1580
|
+
inputSchema: tool.inputSchema,
|
|
1581
|
+
async execute(args, ctx) {
|
|
1582
|
+
try {
|
|
1583
|
+
const result = await McpRegistry.callTool(tool.id, args || {}, ctx.signal || null)
|
|
1584
|
+
return result.output
|
|
1585
|
+
} catch (error) {
|
|
1586
|
+
const reason = error.reason || "unknown"
|
|
1587
|
+
const server = error.server || tool.server
|
|
1588
|
+
return `[MCP Error: ${server} ${reason}] ${error.message}`
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
}))
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function toolAllowedByMode(toolName, mode) {
|
|
1595
|
+
if (mode === "plan") {
|
|
1596
|
+
return !["write", "edit", "patch", "multiedit", "notebookedit", "bash", "task", "git_snapshot", "git_restore", "git_apply_patch", "git_delete_snapshot"].includes(toolName)
|
|
1597
|
+
}
|
|
1598
|
+
return true
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
export const ToolRegistry = {
|
|
1602
|
+
async initialize({ config = {}, cwd = process.cwd(), force = false } = {}) {
|
|
1603
|
+
const ttlMs = Math.max(0, Number(config.runtime?.tool_registry_cache_ttl_ms || 30000))
|
|
1604
|
+
const sig = signatureFor(config, cwd)
|
|
1605
|
+
const cacheValid =
|
|
1606
|
+
state.initialized &&
|
|
1607
|
+
!force &&
|
|
1608
|
+
state.lastSignature === sig &&
|
|
1609
|
+
state.lastCwd === cwd &&
|
|
1610
|
+
Date.now() - state.loadedAt <= ttlMs
|
|
1611
|
+
if (cacheValid) return
|
|
1612
|
+
|
|
1613
|
+
const tools = []
|
|
1614
|
+
|
|
1615
|
+
if (config.tool?.sources?.builtin !== false) {
|
|
1616
|
+
tools.push(...builtinTools(config))
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
if (config.tool?.sources?.local !== false) {
|
|
1620
|
+
const localDirs = (config.tool?.local_dirs || []).map((dir) => path.resolve(cwd, dir))
|
|
1621
|
+
tools.push(...(await loadDynamicTools(localDirs)))
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
if (config.tool?.sources?.plugin !== false) {
|
|
1625
|
+
const pluginDirs = (config.tool?.plugin_dirs || []).map((dir) => path.resolve(cwd, dir))
|
|
1626
|
+
tools.push(...(await loadDynamicTools(pluginDirs)))
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
if (config.tool && config.tool?.sources?.mcp !== false) {
|
|
1630
|
+
await McpRegistry.initialize(config, { cwd })
|
|
1631
|
+
tools.push(...mcpTools())
|
|
1632
|
+
}
|
|
1633
|
+
|
|
1634
|
+
state.tools = tools
|
|
1635
|
+
state.initialized = true
|
|
1636
|
+
state.loadedAt = Date.now()
|
|
1637
|
+
state.lastSignature = sig
|
|
1638
|
+
state.lastCwd = cwd
|
|
1639
|
+
state.lastConfig = config
|
|
1640
|
+
},
|
|
1641
|
+
|
|
1642
|
+
isReady() {
|
|
1643
|
+
return state.initialized
|
|
1644
|
+
},
|
|
1645
|
+
|
|
1646
|
+
async list({ mode, cwd = process.cwd(), config = undefined } = {}) {
|
|
1647
|
+
const resolvedConfig = config === undefined ? state.lastConfig || {} : config
|
|
1648
|
+
if (!state.initialized) {
|
|
1649
|
+
await this.initialize({ config: resolvedConfig, cwd })
|
|
1650
|
+
} else {
|
|
1651
|
+
await this.initialize({ config: resolvedConfig, cwd, force: false })
|
|
1652
|
+
}
|
|
1653
|
+
return state.tools
|
|
1654
|
+
.filter((tool) => toolAllowedByMode(tool.name, mode))
|
|
1655
|
+
.map((tool) => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema }))
|
|
1656
|
+
},
|
|
1657
|
+
|
|
1658
|
+
async get(toolName) {
|
|
1659
|
+
return state.tools.find((tool) => tool.name === toolName) || null
|
|
1660
|
+
},
|
|
1661
|
+
|
|
1662
|
+
async call(toolName, args, ctx) {
|
|
1663
|
+
const tool = await this.get(toolName)
|
|
1664
|
+
if (!tool) {
|
|
1665
|
+
return {
|
|
1666
|
+
name: toolName,
|
|
1667
|
+
status: "error",
|
|
1668
|
+
output: `unknown tool: ${toolName}`,
|
|
1669
|
+
error: `unknown tool: ${toolName}`
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
try {
|
|
1673
|
+
const output = await tool.execute(args || {}, ctx)
|
|
1674
|
+
return {
|
|
1675
|
+
name: toolName,
|
|
1676
|
+
status: "completed",
|
|
1677
|
+
output: safeStringify(output)
|
|
1678
|
+
}
|
|
1679
|
+
} catch (error) {
|
|
1680
|
+
return {
|
|
1681
|
+
name: toolName,
|
|
1682
|
+
status: "error",
|
|
1683
|
+
output: error.message,
|
|
1684
|
+
error: error.message
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
},
|
|
1688
|
+
|
|
1689
|
+
refreshMcpTools() {
|
|
1690
|
+
if (!state.initialized || state.refreshing) return
|
|
1691
|
+
state.refreshing = true
|
|
1692
|
+
try {
|
|
1693
|
+
// Atomic replacement: build new list, then assign once
|
|
1694
|
+
const nonMcp = state.tools.filter((t) => !t.name.startsWith("mcp_"))
|
|
1695
|
+
const newMcpTools = mcpTools()
|
|
1696
|
+
state.tools = [...nonMcp, ...newMcpTools]
|
|
1697
|
+
} finally {
|
|
1698
|
+
state.refreshing = false
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
}
|