opencodekit 0.23.1 → 0.23.3
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/dist/index.js +354 -825
- package/dist/template/.opencode/AGENTS.md +15 -2
- package/dist/template/.opencode/command/init.md +198 -34
- package/dist/template/.opencode/context/fallow.md +137 -0
- package/dist/template/.opencode/opencode.json +12 -315
- package/dist/template/.opencode/plugin/codesearch.ts +730 -0
- package/dist/template/.opencode/plugin/memory/compile.ts +171 -186
- package/dist/template/.opencode/plugin/memory/index-generator.ts +118 -133
- package/dist/template/.opencode/plugin/memory/lint.ts +253 -275
- package/dist/template/.opencode/plugin/memory/tools.ts +224 -268
- package/dist/template/.opencode/plugin/memory/validate.ts +154 -164
- package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search-preview.ts +13 -30
- package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search-shared.ts +25 -0
- package/dist/template/.opencode/plugin/sdk/copilot/responses/tool/web-search.ts +17 -34
- package/dist/template/.opencode/plugin/session-summary.ts +0 -2
- package/dist/template/.opencode/plugin/srcwalk.ts +646 -667
- package/dist/template/.opencode/skill/code-navigation/SKILL.md +10 -10
- package/dist/template/.opencode/skill/code-review-and-quality/SKILL.md +1 -1
- package/dist/template/.opencode/skill/condition-based-waiting/example.ts +15 -2
- package/dist/template/.opencode/skill/debugging-and-error-recovery/SKILL.md +1 -1
- package/dist/template/.opencode/skill/deep-module-design/SKILL.md +1 -1
- package/dist/template/.opencode/skill/fallow/SKILL.md +409 -0
- package/dist/template/.opencode/skill/fallow/references/cli-reference.md +1905 -0
- package/dist/template/.opencode/skill/fallow/references/gotchas.md +644 -0
- package/dist/template/.opencode/skill/fallow/references/patterns.md +791 -0
- package/dist/template/.opencode/skill/planning-and-task-breakdown/SKILL.md +1 -1
- package/dist/template/.opencode/skill/srcwalk/SKILL.md +10 -13
- package/dist/template/.opencode/skill/ubiquitous-language/SKILL.md +1 -1
- package/dist/template/.opencode/tool/grepsearch.ts +92 -103
- package/package.json +1 -1
|
@@ -5,9 +5,7 @@
|
|
|
5
5
|
* Wraps available CLI tools (rg/grep, find, cat, ls) into unified srcwalk_* tools.
|
|
6
6
|
*
|
|
7
7
|
* Tools (matching pikit srcwalk extension surface):
|
|
8
|
-
* - srcwalk_search — Search symbols/text/regex in codebase
|
|
9
8
|
* - srcwalk_read — Read files with optional section/range
|
|
10
|
-
* - srcwalk_files — Find files by glob pattern
|
|
11
9
|
* - srcwalk_deps — Show imports and dependents for a file
|
|
12
10
|
* - srcwalk_map — Directory tree overview
|
|
13
11
|
* - srcwalk_callers — Reverse call graph (grep-based)
|
|
@@ -16,14 +14,13 @@
|
|
|
16
14
|
* - srcwalk_impact — Heuristic blast-radius triage
|
|
17
15
|
*/
|
|
18
16
|
|
|
19
|
-
import {
|
|
17
|
+
import { execFileSync } from "node:child_process";
|
|
20
18
|
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
21
19
|
import { readdir } from "node:fs/promises";
|
|
22
20
|
import path from "node:path";
|
|
23
21
|
import { tool } from "@opencode-ai/plugin/tool";
|
|
24
22
|
import type { Plugin } from "@opencode-ai/plugin";
|
|
25
23
|
|
|
26
|
-
const BIN_RG = "rg";
|
|
27
24
|
const TIMEOUT_MS = 15_000;
|
|
28
25
|
const MAX_BUFFER = 5 * 1024 * 1024;
|
|
29
26
|
|
|
@@ -31,637 +28,613 @@ const MAX_BUFFER = 5 * 1024 * 1024;
|
|
|
31
28
|
// Helpers
|
|
32
29
|
// ---------------------------------------------------------------------------
|
|
33
30
|
|
|
34
|
-
function run(
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
execFileSync(BIN_RG, ["--version"], { encoding: "utf-8", timeout: 1000, stdio: "ignore" });
|
|
57
|
-
return true;
|
|
58
|
-
} catch {
|
|
59
|
-
return false;
|
|
60
|
-
}
|
|
31
|
+
function run(
|
|
32
|
+
cmd: string,
|
|
33
|
+
args: string[],
|
|
34
|
+
cwd?: string,
|
|
35
|
+
): { stdout: string; stderr: string; code: number } {
|
|
36
|
+
try {
|
|
37
|
+
const result = execFileSync(cmd, args, {
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
timeout: TIMEOUT_MS,
|
|
40
|
+
maxBuffer: MAX_BUFFER,
|
|
41
|
+
cwd: cwd ?? process.cwd(),
|
|
42
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
43
|
+
});
|
|
44
|
+
return { stdout: result.stdout ?? "", stderr: result.stderr ?? "", code: 0 };
|
|
45
|
+
} catch (err: unknown) {
|
|
46
|
+
const e = err as { stdout?: string; stderr?: string; status?: number; message?: string };
|
|
47
|
+
return {
|
|
48
|
+
stdout: e.stdout ?? "",
|
|
49
|
+
stderr: e.stderr ?? "",
|
|
50
|
+
code: e.status ?? 1,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
61
53
|
}
|
|
62
54
|
|
|
63
55
|
function plural(n: number, word: string): string {
|
|
64
|
-
|
|
56
|
+
if (n === 1) return `${n} ${word}`;
|
|
57
|
+
if (
|
|
58
|
+
word.endsWith("ch") ||
|
|
59
|
+
word.endsWith("s") ||
|
|
60
|
+
word.endsWith("sh") ||
|
|
61
|
+
word.endsWith("x") ||
|
|
62
|
+
word.endsWith("z")
|
|
63
|
+
) {
|
|
64
|
+
return `${n} ${word}es`;
|
|
65
|
+
}
|
|
66
|
+
if (
|
|
67
|
+
word.endsWith("y") &&
|
|
68
|
+
word.length > 1 &&
|
|
69
|
+
!["a", "e", "i", "o", "u"].includes(word[word.length - 2])
|
|
70
|
+
) {
|
|
71
|
+
return `${n} ${word.slice(0, -1)}ies`;
|
|
72
|
+
}
|
|
73
|
+
return `${n} ${word}s`;
|
|
65
74
|
}
|
|
66
75
|
|
|
67
76
|
// ---------------------------------------------------------------------------
|
|
68
77
|
// Plugin
|
|
69
78
|
// ---------------------------------------------------------------------------
|
|
70
79
|
|
|
71
|
-
interface PluginConfig {
|
|
72
|
-
directory?: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
80
|
const srcwalkTools = {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
const dir = path.dirname(filePath);
|
|
634
|
-
if (!dirCounts[dir]) dirCounts[dir] = { files: 0, total: 0 };
|
|
635
|
-
dirCounts[dir].files++;
|
|
636
|
-
dirCounts[dir].total += count;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
const totalOccurrences = lines.length;
|
|
640
|
-
const totalFiles = Object.keys(fileCounts).length;
|
|
641
|
-
|
|
642
|
-
const output: string[] = [
|
|
643
|
-
`## Impact: \`${symbol}\``,
|
|
644
|
-
`Total: ${plural(totalOccurrences, "occurrence")} across ${plural(totalFiles, "file")}\n`,
|
|
645
|
-
`### By directory`,
|
|
646
|
-
];
|
|
647
|
-
|
|
648
|
-
const sortedDirs = Object.entries(dirCounts).sort((a, b) => b[1].total - a[1].total);
|
|
649
|
-
for (const [dir, info] of sortedDirs.slice(0, 15)) {
|
|
650
|
-
const relDir = path.relative(context.directory, dir) || ".";
|
|
651
|
-
output.push(` ${relDir}/ — ${plural(info.total, "occurrence")} in ${plural(info.files, "file")}`);
|
|
652
|
-
}
|
|
653
|
-
|
|
654
|
-
const topFiles = Object.entries(fileCounts).sort((a, b) => b[1] - a[1]).slice(0, 20);
|
|
655
|
-
output.push(`\n### Top files`);
|
|
656
|
-
for (const [filePath, count] of topFiles) {
|
|
657
|
-
const relPath = path.relative(context.directory, filePath);
|
|
658
|
-
output.push(` ${relPath} (${plural(count, "occurrence")})`);
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
output.push(`\n_Heuristic: name-matched, not proof. Follow up with srcwalk_callers for exact call sites._`);
|
|
662
|
-
return output.join("\n");
|
|
663
|
-
},
|
|
664
|
-
}),
|
|
81
|
+
// -----------------------------------------------------------------------
|
|
82
|
+
// srcwalk_read: Read files
|
|
83
|
+
// -----------------------------------------------------------------------
|
|
84
|
+
srcwalk_read: tool({
|
|
85
|
+
description: `Read a file with optional section (line range, symbol, or path:line format).\n\nSmall files return full content; large files support outlining.\nUse path:start-end for range reads (e.g. "src/app.ts:44-89").`,
|
|
86
|
+
args: {
|
|
87
|
+
path: tool.schema
|
|
88
|
+
.string()
|
|
89
|
+
.describe("File path to read (supports path:line or path:start-end)"),
|
|
90
|
+
section: tool.schema
|
|
91
|
+
.string()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe("Line range '44-89' or heading/symbol name"),
|
|
94
|
+
full: tool.schema.boolean().optional().describe("Force full content"),
|
|
95
|
+
},
|
|
96
|
+
execute: async (args, context) => {
|
|
97
|
+
const fileArg = String(args.path);
|
|
98
|
+
const fullFilePath = path.resolve(context.directory, fileArg);
|
|
99
|
+
|
|
100
|
+
// Parse path:line or path:start-end shortcut
|
|
101
|
+
let startLine: number | undefined;
|
|
102
|
+
let endLine: number | undefined;
|
|
103
|
+
|
|
104
|
+
const rangeMatch = fileArg.match(/^(.+?):(\d+)(?:-(\d+))?$/);
|
|
105
|
+
if (rangeMatch) {
|
|
106
|
+
const relPath = rangeMatch[1];
|
|
107
|
+
const resolved = path.resolve(context.directory, relPath);
|
|
108
|
+
startLine = parseInt(rangeMatch[2], 10);
|
|
109
|
+
endLine = rangeMatch[3] ? parseInt(rangeMatch[3], 10) : startLine;
|
|
110
|
+
return readFileRange(resolved, startLine, endLine, relPath);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!existsSync(fullFilePath)) return `File not found: ${fileArg}`;
|
|
114
|
+
const stats = statSync(fullFilePath);
|
|
115
|
+
|
|
116
|
+
// If section specified, try grep for symbol/heading
|
|
117
|
+
if (args.section) {
|
|
118
|
+
const section = String(args.section);
|
|
119
|
+
// Check if it's a line range
|
|
120
|
+
const lineMatch = section.match(/^(\d+)(?:-(\d+))?$/);
|
|
121
|
+
if (lineMatch) {
|
|
122
|
+
const start = parseInt(lineMatch[1], 10);
|
|
123
|
+
const end = lineMatch[2] ? parseInt(lineMatch[2], 10) : start + 30;
|
|
124
|
+
return readFileRange(fullFilePath, start, end);
|
|
125
|
+
}
|
|
126
|
+
// Symbol/heading: use grep to find location, then read around it
|
|
127
|
+
const grepArgs = [
|
|
128
|
+
"-n",
|
|
129
|
+
"--color=never",
|
|
130
|
+
"-E",
|
|
131
|
+
`^(function |const |let |class |interface |type |export |## |### )?.*${section}`,
|
|
132
|
+
fullFilePath,
|
|
133
|
+
];
|
|
134
|
+
const result = run("grep", grepArgs);
|
|
135
|
+
if (result.stdout) {
|
|
136
|
+
const firstMatch = result.stdout.split("\n")[0];
|
|
137
|
+
const lineNum = parseInt(firstMatch.split(":")[0], 10);
|
|
138
|
+
if (!isNaN(lineNum)) {
|
|
139
|
+
return readFileRange(fullFilePath, Math.max(1, lineNum - 3), lineNum + 40);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return `Section "${section}" not found in ${fileArg}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Full content for small files
|
|
146
|
+
if (stats.size < 50 * 1024 || args.full) {
|
|
147
|
+
const content = readFileSync(fullFilePath, "utf-8");
|
|
148
|
+
const lines = content.split("\n");
|
|
149
|
+
if (lines.length > 2000)
|
|
150
|
+
return `[File too large: ${plural(lines.length, "line")}. Showing first 2000 lines]\n\n${lines.slice(0, 2000).join("\n")}`;
|
|
151
|
+
return content;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Large file: outline
|
|
155
|
+
const content = readFileSync(fullFilePath, "utf-8");
|
|
156
|
+
const lines = content.split("\n");
|
|
157
|
+
const headings: string[] = [];
|
|
158
|
+
for (let i = 0; i < Math.min(lines.length, 5000); i++) {
|
|
159
|
+
const l = lines[i].trim();
|
|
160
|
+
if (
|
|
161
|
+
l.match(
|
|
162
|
+
/^(export\s+)?(function|class|interface|type|const|enum|def|struct|impl|pub\s+fn)\s/,
|
|
163
|
+
) ||
|
|
164
|
+
l.match(/^(##|###)\s/) ||
|
|
165
|
+
l.match(/^\w+\s*[:=]/)
|
|
166
|
+
) {
|
|
167
|
+
headings.push(` ${i + 1}: ${l.slice(0, 120)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
const header = `[File: ${fileArg} — ${plural(lines.length, "line")}, ${(stats.size / 1024).toFixed(1)}KB]\n\n`;
|
|
171
|
+
const outline =
|
|
172
|
+
headings.length > 0
|
|
173
|
+
? `Outline (${plural(headings.length, "entry")}):\n${headings.slice(0, 50).join("\n")}\n\nUse path:line or section to read a specific range.`
|
|
174
|
+
: `Use path:line to read a specific range (e.g., ${fileArg}:1-${Math.min(50, lines.length)}).`;
|
|
175
|
+
return header + outline;
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
|
|
179
|
+
// -----------------------------------------------------------------------
|
|
180
|
+
// srcwalk_deps: Import analysis
|
|
181
|
+
// -----------------------------------------------------------------------
|
|
182
|
+
srcwalk_deps: tool({
|
|
183
|
+
description: `Show what imports a file (dependents) and what a file imports (dependencies).\nBlast-radius check before breaking changes.`,
|
|
184
|
+
args: {
|
|
185
|
+
path: tool.schema.string().describe("File path to analyze"),
|
|
186
|
+
scope: tool.schema.string().optional().describe("Search scope (default: project root)"),
|
|
187
|
+
},
|
|
188
|
+
execute: async (args, context) => {
|
|
189
|
+
const filePath = String(args.path);
|
|
190
|
+
const absPath = path.resolve(context.directory, filePath);
|
|
191
|
+
if (!existsSync(absPath)) return `File not found: ${filePath}`;
|
|
192
|
+
|
|
193
|
+
const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
|
|
194
|
+
const fileName = path.basename(filePath, path.extname(filePath));
|
|
195
|
+
|
|
196
|
+
// What this file imports
|
|
197
|
+
const content = readFileSync(absPath, "utf-8");
|
|
198
|
+
const importLines: string[] = [];
|
|
199
|
+
for (const line of content.split("\n")) {
|
|
200
|
+
const m = line.match(
|
|
201
|
+
/(?:import|require)\s+.*?from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/,
|
|
202
|
+
);
|
|
203
|
+
if (m) importLines.push(` ${line.trim()}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// What imports this file (grep for the module name)
|
|
207
|
+
const searchName = fileName;
|
|
208
|
+
const grepResult = run("grep", [
|
|
209
|
+
"-rn",
|
|
210
|
+
"--color=never",
|
|
211
|
+
"-E",
|
|
212
|
+
`from ['"](\\./|\\.\\./|.*/)${searchName}['"]|require\\(['"](\\./|\\.\\./|.*/)${searchName}['"]`,
|
|
213
|
+
scopeDir,
|
|
214
|
+
"--include=*.ts",
|
|
215
|
+
"--include=*.tsx",
|
|
216
|
+
"--include=*.js",
|
|
217
|
+
"--include=*.jsx",
|
|
218
|
+
"--include=*.mjs",
|
|
219
|
+
]);
|
|
220
|
+
const importers = grepResult.stdout.split("\n").filter(Boolean).slice(0, 30);
|
|
221
|
+
|
|
222
|
+
const result: string[] = [`## Dependencies for ${filePath}\n`];
|
|
223
|
+
result.push(`### Imports (${plural(importLines.length, "module")})`);
|
|
224
|
+
if (importLines.length === 0) result.push(" (none)");
|
|
225
|
+
else result.push(...importLines);
|
|
226
|
+
|
|
227
|
+
result.push(`\n### Importers (${plural(importers.length, "file")})`);
|
|
228
|
+
if (importers.length === 0) result.push(" (no files import this module)");
|
|
229
|
+
else
|
|
230
|
+
result.push(
|
|
231
|
+
...importers.map(
|
|
232
|
+
(l) => ` ${path.relative(context.directory, l.split(":")[0])}:${l.split(":")[1]}`,
|
|
233
|
+
),
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
return result.join("\n");
|
|
237
|
+
},
|
|
238
|
+
}),
|
|
239
|
+
|
|
240
|
+
// -----------------------------------------------------------------------
|
|
241
|
+
// srcwalk_map: Directory overview
|
|
242
|
+
// -----------------------------------------------------------------------
|
|
243
|
+
srcwalk_map: tool({
|
|
244
|
+
description: `Token-annotated directory skeleton. Shows repo structure with file sizes and token estimates. Good for understanding codebase shape.`,
|
|
245
|
+
args: {
|
|
246
|
+
scope: tool.schema.string().optional().describe("Directory to map (default: project root)"),
|
|
247
|
+
depth: tool.schema.number().optional().describe("Max directory depth (default: 3)"),
|
|
248
|
+
},
|
|
249
|
+
execute: async (args, context) => {
|
|
250
|
+
const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
|
|
251
|
+
const maxDepth = args.depth ?? 3;
|
|
252
|
+
|
|
253
|
+
const treeArgs = ["-L", String(maxDepth), "--dirsfirst"];
|
|
254
|
+
// Use tree if available, else ls -R
|
|
255
|
+
const treeResult = run("tree", [
|
|
256
|
+
...treeArgs,
|
|
257
|
+
"-I",
|
|
258
|
+
".git|node_modules|dist|build|coverage|.next",
|
|
259
|
+
scopeDir,
|
|
260
|
+
]);
|
|
261
|
+
if (treeResult.code === 0) {
|
|
262
|
+
return treeResult.stdout.slice(0, 10_000);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// Fallback: simple directory listing
|
|
266
|
+
const result: string[] = [
|
|
267
|
+
`## Directory: ${path.relative(context.directory, scopeDir) || "."}\n`,
|
|
268
|
+
];
|
|
269
|
+
await listDirRecursive(scopeDir, "", maxDepth, result);
|
|
270
|
+
return result.join("\n");
|
|
271
|
+
},
|
|
272
|
+
}),
|
|
273
|
+
|
|
274
|
+
// -----------------------------------------------------------------------
|
|
275
|
+
// srcwalk_callers: Reverse call graph
|
|
276
|
+
// -----------------------------------------------------------------------
|
|
277
|
+
srcwalk_callers: tool({
|
|
278
|
+
description: `Reverse call graph — find what calls a function.\nGrep-based: searches for symbol usage across the codebase.\nUse depth for transitive callers (multi-hop).`,
|
|
279
|
+
args: {
|
|
280
|
+
symbol: tool.schema.string().describe("Function/symbol name"),
|
|
281
|
+
scope: tool.schema.string().optional().describe("Search scope"),
|
|
282
|
+
depth: tool.schema.number().optional().describe("BFS hop depth (default: 1, max: 3)"),
|
|
283
|
+
filter: tool.schema.string().optional().describe("Optional filter (e.g. path:api)"),
|
|
284
|
+
},
|
|
285
|
+
execute: async (args, context) => {
|
|
286
|
+
const symbol = String(args.symbol ?? "").trim();
|
|
287
|
+
if (!symbol) return "symbol is required.";
|
|
288
|
+
const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
|
|
289
|
+
const depth = Math.min(args.depth ?? 1, 3);
|
|
290
|
+
|
|
291
|
+
// Search for direct calls: symbol( or .symbol or symbol.
|
|
292
|
+
const grepArgs = [
|
|
293
|
+
"-rn",
|
|
294
|
+
"--color=never",
|
|
295
|
+
"-E",
|
|
296
|
+
`[.\\s]${symbol}\\s*\\(|[.]${symbol}\\b|\\b${symbol}\\.`,
|
|
297
|
+
scopeDir,
|
|
298
|
+
"--include=*.ts",
|
|
299
|
+
"--include=*.tsx",
|
|
300
|
+
"--include=*.js",
|
|
301
|
+
"--include=*.jsx",
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
if (args.filter) {
|
|
305
|
+
const filterStr = String(args.filter);
|
|
306
|
+
if (filterStr.startsWith("path:")) {
|
|
307
|
+
grepArgs.push(path.join(scopeDir, filterStr.slice(5)));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const result = run("grep", grepArgs);
|
|
312
|
+
const lines = result.stdout.split("\n").filter(Boolean).slice(0, 50);
|
|
313
|
+
|
|
314
|
+
if (lines.length === 0) return `No callers found for "${symbol}".`;
|
|
315
|
+
|
|
316
|
+
const output: string[] = [
|
|
317
|
+
`## Callers of \`${symbol}\` (${plural(lines.length, "result")})${depth > 1 ? ` (depth: ${depth})` : ""}\n`,
|
|
318
|
+
];
|
|
319
|
+
for (const line of lines) {
|
|
320
|
+
const parts = line.split(":");
|
|
321
|
+
if (parts.length >= 2) {
|
|
322
|
+
const relPath = path.relative(context.directory, parts[0]);
|
|
323
|
+
output.push(` ${relPath}:${parts[1]}: ${parts.slice(2).join(":").trim().slice(0, 150)}`);
|
|
324
|
+
} else {
|
|
325
|
+
output.push(` ${line.slice(0, 200)}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (depth > 1) {
|
|
330
|
+
output.push(`\n_Note: Multi-hop depth (${depth}) requires re-running on each caller._`);
|
|
331
|
+
}
|
|
332
|
+
return output.join("\n");
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
335
|
+
|
|
336
|
+
// -----------------------------------------------------------------------
|
|
337
|
+
// srcwalk_callees: Forward call graph
|
|
338
|
+
// -----------------------------------------------------------------------
|
|
339
|
+
srcwalk_callees: tool({
|
|
340
|
+
description: `Forward call graph — what does this function call?\nReads the function body and extracts call sites.`,
|
|
341
|
+
args: {
|
|
342
|
+
symbol: tool.schema.string().describe("Function/symbol name"),
|
|
343
|
+
scope: tool.schema.string().optional().describe("Scope directory"),
|
|
344
|
+
detailed: tool.schema
|
|
345
|
+
.boolean()
|
|
346
|
+
.optional()
|
|
347
|
+
.describe("Show ordered call sites with argument slots"),
|
|
348
|
+
},
|
|
349
|
+
execute: async (args, context) => {
|
|
350
|
+
const symbol = String(args.symbol ?? "").trim();
|
|
351
|
+
if (!symbol) return "symbol is required.";
|
|
352
|
+
const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
|
|
353
|
+
|
|
354
|
+
// Find the function definition
|
|
355
|
+
const defArgs = [
|
|
356
|
+
"-rn",
|
|
357
|
+
"--color=never",
|
|
358
|
+
"-E",
|
|
359
|
+
`(export\\s+)?(function|const|let|async\\s+function)\\s+${symbol}\\b|${symbol}\\s*[:=]\\s*(async\\s+)?\\(`,
|
|
360
|
+
scopeDir,
|
|
361
|
+
"--include=*.ts",
|
|
362
|
+
"--include=*.tsx",
|
|
363
|
+
"--include=*.js",
|
|
364
|
+
"--include=*.jsx",
|
|
365
|
+
];
|
|
366
|
+
const defResult = run("grep", defArgs);
|
|
367
|
+
const defLines = defResult.stdout.split("\n").filter(Boolean).slice(0, 5);
|
|
368
|
+
|
|
369
|
+
if (defLines.length === 0) {
|
|
370
|
+
return `Definition not found for "${symbol}". Cannot trace callees without finding the function body.`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const output: string[] = [`## Callees of \`${symbol}\`\n`];
|
|
374
|
+
|
|
375
|
+
// For each definition location, show the function and extract calls
|
|
376
|
+
for (const def of defLines) {
|
|
377
|
+
const parts = def.split(":");
|
|
378
|
+
if (parts.length >= 2) {
|
|
379
|
+
const relPath = path.relative(context.directory, parts[0]);
|
|
380
|
+
const lineNum = parseInt(parts[1], 10);
|
|
381
|
+
output.push(`**Definition:** ${relPath}:${lineNum}`);
|
|
382
|
+
|
|
383
|
+
// Read function body to extract calls
|
|
384
|
+
const filePath = path.resolve(context.directory, parts[0]);
|
|
385
|
+
if (existsSync(filePath)) {
|
|
386
|
+
const fileLines = readFileSync(filePath, "utf-8").split("\n");
|
|
387
|
+
let braceCount = 0;
|
|
388
|
+
let inFunc = false;
|
|
389
|
+
const calls: string[] = [];
|
|
390
|
+
|
|
391
|
+
for (let i = lineNum - 1; i < Math.min(lineNum + 80, fileLines.length); i++) {
|
|
392
|
+
const line = fileLines[i];
|
|
393
|
+
if (!inFunc) {
|
|
394
|
+
if (line.includes("{")) {
|
|
395
|
+
inFunc = true;
|
|
396
|
+
braceCount = (line.match(/{/g) || []).length;
|
|
397
|
+
braceCount -= (line.match(/}/g) || []).length;
|
|
398
|
+
}
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
braceCount += (line.match(/{/g) || []).length;
|
|
402
|
+
braceCount -= (line.match(/}/g) || []).length;
|
|
403
|
+
|
|
404
|
+
// Extract function calls
|
|
405
|
+
const callMatch = [...line.matchAll(/(?<![.\w])(\w+)\s*\(/g)];
|
|
406
|
+
for (const m of callMatch) {
|
|
407
|
+
const name = m[1];
|
|
408
|
+
if (
|
|
409
|
+
![
|
|
410
|
+
"if",
|
|
411
|
+
"for",
|
|
412
|
+
"while",
|
|
413
|
+
"switch",
|
|
414
|
+
"catch",
|
|
415
|
+
"typeof",
|
|
416
|
+
"instanceof",
|
|
417
|
+
"return",
|
|
418
|
+
"throw",
|
|
419
|
+
"new",
|
|
420
|
+
"delete",
|
|
421
|
+
"await",
|
|
422
|
+
"yield",
|
|
423
|
+
].includes(name)
|
|
424
|
+
) {
|
|
425
|
+
const argStart = line.indexOf("(", m.index!);
|
|
426
|
+
const argEnd = line.indexOf(")", argStart);
|
|
427
|
+
const args_s =
|
|
428
|
+
argEnd > argStart
|
|
429
|
+
? line
|
|
430
|
+
.slice(argStart + 1, argEnd)
|
|
431
|
+
.trim()
|
|
432
|
+
.slice(0, 60)
|
|
433
|
+
: "";
|
|
434
|
+
calls.push(` ${args.detailed ? `${name}(${args_s})` : `${name}()`}`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (braceCount <= 0) break;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
if (calls.length > 0) {
|
|
442
|
+
output.push(...calls);
|
|
443
|
+
} else {
|
|
444
|
+
output.push(" (no internal calls found)");
|
|
445
|
+
}
|
|
446
|
+
output.push("");
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return output.join("\n");
|
|
452
|
+
},
|
|
453
|
+
}),
|
|
454
|
+
|
|
455
|
+
// -----------------------------------------------------------------------
|
|
456
|
+
// srcwalk_flow: Compact orientation
|
|
457
|
+
// -----------------------------------------------------------------------
|
|
458
|
+
srcwalk_flow: tool({
|
|
459
|
+
description: `Compact function orientation — ordered callees + direct callers.\nQuick understanding of a function's role in the call graph.`,
|
|
460
|
+
args: {
|
|
461
|
+
symbol: tool.schema.string().describe("Symbol name to analyze"),
|
|
462
|
+
scope: tool.schema.string().optional().describe("Search scope"),
|
|
463
|
+
},
|
|
464
|
+
execute: async (args, context) => {
|
|
465
|
+
const symbol = String(args.symbol ?? "").trim();
|
|
466
|
+
if (!symbol) return "symbol is required.";
|
|
467
|
+
const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
|
|
468
|
+
|
|
469
|
+
// Get callers
|
|
470
|
+
const callersResult = run("grep", [
|
|
471
|
+
"-rn",
|
|
472
|
+
"--color=never",
|
|
473
|
+
"-E",
|
|
474
|
+
`[.\\s]${symbol}\\s*\\(|[.]${symbol}\\b`,
|
|
475
|
+
scopeDir,
|
|
476
|
+
"--include=*.ts",
|
|
477
|
+
"--include=*.tsx",
|
|
478
|
+
"--include=*.js",
|
|
479
|
+
"--include=*.jsx",
|
|
480
|
+
]);
|
|
481
|
+
const callers = callersResult.stdout.split("\n").filter(Boolean).slice(0, 15);
|
|
482
|
+
|
|
483
|
+
// Get callees by reading function body
|
|
484
|
+
const defResult = run("grep", [
|
|
485
|
+
"-rn",
|
|
486
|
+
"--color=never",
|
|
487
|
+
"-E",
|
|
488
|
+
`(function|const|let)\\s+${symbol}\\b|${symbol}\\s*[:=]\\s*(async\\s+)?\\(`,
|
|
489
|
+
scopeDir,
|
|
490
|
+
"--include=*.ts",
|
|
491
|
+
"--include=*.tsx",
|
|
492
|
+
"--include=*.js",
|
|
493
|
+
"--include=*.jsx",
|
|
494
|
+
]);
|
|
495
|
+
const defLine = defResult.stdout.split("\n").filter(Boolean)[0];
|
|
496
|
+
let callees: string[] = [];
|
|
497
|
+
|
|
498
|
+
if (defLine) {
|
|
499
|
+
const parts = defLine.split(":");
|
|
500
|
+
if (parts.length >= 2) {
|
|
501
|
+
const fp = path.resolve(context.directory, parts[0]);
|
|
502
|
+
const ln = parseInt(parts[1], 10);
|
|
503
|
+
if (existsSync(fp)) {
|
|
504
|
+
const fileLines = readFileSync(fp, "utf-8").split("\n");
|
|
505
|
+
let bc = 0,
|
|
506
|
+
inF = false;
|
|
507
|
+
for (let i = ln - 1; i < Math.min(ln + 60, fileLines.length); i++) {
|
|
508
|
+
const l = fileLines[i];
|
|
509
|
+
if (!inF) {
|
|
510
|
+
if (l.includes("{")) {
|
|
511
|
+
inF = true;
|
|
512
|
+
bc = (l.match(/{/g) || []).length - (l.match(/}/g) || []).length;
|
|
513
|
+
}
|
|
514
|
+
continue;
|
|
515
|
+
}
|
|
516
|
+
bc += (l.match(/{/g) || []).length - (l.match(/}/g) || []).length;
|
|
517
|
+
for (const m of l.matchAll(/(\w+)\s*\(/g)) {
|
|
518
|
+
if (
|
|
519
|
+
![
|
|
520
|
+
"if",
|
|
521
|
+
"for",
|
|
522
|
+
"while",
|
|
523
|
+
"switch",
|
|
524
|
+
"catch",
|
|
525
|
+
"typeof",
|
|
526
|
+
"instanceof",
|
|
527
|
+
"return",
|
|
528
|
+
"throw",
|
|
529
|
+
"new",
|
|
530
|
+
"delete",
|
|
531
|
+
"await",
|
|
532
|
+
"yield",
|
|
533
|
+
].includes(m[1])
|
|
534
|
+
)
|
|
535
|
+
callees.push(m[1]);
|
|
536
|
+
}
|
|
537
|
+
if (bc <= 0) break;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
const output: string[] = [`## Flow: \`${symbol}\``];
|
|
544
|
+
output.push(`\n**Callers (${plural(callers.length, "file")}):`);
|
|
545
|
+
if (callers.length === 0) output.push(" (none)");
|
|
546
|
+
else
|
|
547
|
+
output.push(
|
|
548
|
+
...callers
|
|
549
|
+
.slice(0, 10)
|
|
550
|
+
.map(
|
|
551
|
+
(l) => ` ${path.relative(context.directory, l.split(":")[0])}:${l.split(":")[1]}`,
|
|
552
|
+
),
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
output.push(`\n**Callees (${plural(callees.length, "call")}):`);
|
|
556
|
+
if (callees.length === 0) output.push(" (none)");
|
|
557
|
+
else output.push(...[...new Set(callees)].slice(0, 20).map((c) => ` ${c}()`));
|
|
558
|
+
return output.join("\n");
|
|
559
|
+
},
|
|
560
|
+
}),
|
|
561
|
+
|
|
562
|
+
// -----------------------------------------------------------------------
|
|
563
|
+
// srcwalk_impact: Heuristic blast-radius
|
|
564
|
+
// -----------------------------------------------------------------------
|
|
565
|
+
srcwalk_impact: tool({
|
|
566
|
+
description: `Heuristic blast-radius triage — broad 'what might be affected?' starting point.\nName-matched, not proof. Use as starting point before verifying with srcwalk_callers or exact reads.`,
|
|
567
|
+
args: {
|
|
568
|
+
symbol: tool.schema.string().describe("Symbol name to triage"),
|
|
569
|
+
scope: tool.schema.string().optional().describe("Search scope"),
|
|
570
|
+
},
|
|
571
|
+
execute: async (args, context) => {
|
|
572
|
+
const symbol = String(args.symbol ?? "").trim();
|
|
573
|
+
if (!symbol) return "symbol is required.";
|
|
574
|
+
const scopeDir = path.resolve(context.directory, args.scope ? String(args.scope) : "");
|
|
575
|
+
|
|
576
|
+
// Count usages per directory
|
|
577
|
+
const grepResult = run("grep", [
|
|
578
|
+
"-rn",
|
|
579
|
+
"--color=never",
|
|
580
|
+
"-E",
|
|
581
|
+
`\\b${symbol}\\b`,
|
|
582
|
+
scopeDir,
|
|
583
|
+
"--include=*.ts",
|
|
584
|
+
"--include=*.tsx",
|
|
585
|
+
"--include=*.js",
|
|
586
|
+
"--include=*.jsx",
|
|
587
|
+
]);
|
|
588
|
+
const lines = grepResult.stdout.split("\n").filter(Boolean);
|
|
589
|
+
|
|
590
|
+
// Group by file
|
|
591
|
+
const fileCounts: Record<string, number> = {};
|
|
592
|
+
for (const line of lines) {
|
|
593
|
+
const filePath = line.split(":")[0];
|
|
594
|
+
fileCounts[filePath] = (fileCounts[filePath] || 0) + 1;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Group by directory
|
|
598
|
+
const dirCounts: Record<string, { files: number; total: number }> = {};
|
|
599
|
+
for (const [filePath, count] of Object.entries(fileCounts)) {
|
|
600
|
+
const dir = path.dirname(filePath);
|
|
601
|
+
if (!dirCounts[dir]) dirCounts[dir] = { files: 0, total: 0 };
|
|
602
|
+
dirCounts[dir].files++;
|
|
603
|
+
dirCounts[dir].total += count;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
const totalOccurrences = lines.length;
|
|
607
|
+
const totalFiles = Object.keys(fileCounts).length;
|
|
608
|
+
|
|
609
|
+
const output: string[] = [
|
|
610
|
+
`## Impact: \`${symbol}\``,
|
|
611
|
+
`Total: ${plural(totalOccurrences, "occurrence")} across ${plural(totalFiles, "file")}\n`,
|
|
612
|
+
`### By directory`,
|
|
613
|
+
];
|
|
614
|
+
|
|
615
|
+
const sortedDirs = Object.entries(dirCounts).sort((a, b) => b[1].total - a[1].total);
|
|
616
|
+
for (const [dir, info] of sortedDirs.slice(0, 15)) {
|
|
617
|
+
const relDir = path.relative(context.directory, dir) || ".";
|
|
618
|
+
output.push(
|
|
619
|
+
` ${relDir}/ — ${plural(info.total, "occurrence")} in ${plural(info.files, "file")}`,
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
const topFiles = Object.entries(fileCounts)
|
|
624
|
+
.sort((a, b) => b[1] - a[1])
|
|
625
|
+
.slice(0, 20);
|
|
626
|
+
output.push(`\n### Top files`);
|
|
627
|
+
for (const [filePath, count] of topFiles) {
|
|
628
|
+
const relPath = path.relative(context.directory, filePath);
|
|
629
|
+
output.push(` ${relPath} (${plural(count, "occurrence")})`);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
output.push(
|
|
633
|
+
`\n_Heuristic: name-matched, not proof. Follow up with srcwalk_callers for exact call sites._`,
|
|
634
|
+
);
|
|
635
|
+
return output.join("\n");
|
|
636
|
+
},
|
|
637
|
+
}),
|
|
665
638
|
};
|
|
666
639
|
|
|
667
640
|
// ---------------------------------------------------------------------------
|
|
@@ -669,53 +642,59 @@ const srcwalkTools = {
|
|
|
669
642
|
// ---------------------------------------------------------------------------
|
|
670
643
|
|
|
671
644
|
function readFileRange(filePath: string, start: number, end: number, displayPath?: string): string {
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
645
|
+
try {
|
|
646
|
+
const content = readFileSync(filePath, "utf-8");
|
|
647
|
+
const lines = content.split("\n");
|
|
648
|
+
const from = Math.max(0, start - 1);
|
|
649
|
+
const to = Math.min(lines.length, end);
|
|
650
|
+
const result: string[] = [`File: ${displayPath ?? filePath} (lines ${start}-${end}):\n`];
|
|
651
|
+
for (let i = from; i < to; i++) {
|
|
652
|
+
result.push(`${i + 1}: ${lines[i]}`);
|
|
653
|
+
}
|
|
654
|
+
return result.join("\n");
|
|
655
|
+
} catch (err) {
|
|
656
|
+
return `Error reading file: ${err instanceof Error ? err.message : String(err)}`;
|
|
657
|
+
}
|
|
685
658
|
}
|
|
686
659
|
|
|
687
|
-
async function listDirRecursive(
|
|
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
|
-
|
|
660
|
+
async function listDirRecursive(
|
|
661
|
+
dir: string,
|
|
662
|
+
prefix: string,
|
|
663
|
+
maxDepth: number,
|
|
664
|
+
output: string[],
|
|
665
|
+
): Promise<void> {
|
|
666
|
+
if (maxDepth <= 0) return;
|
|
667
|
+
try {
|
|
668
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
669
|
+
const skipDirs = new Set([".git", "node_modules", "dist", "build", "coverage", ".next"]);
|
|
670
|
+
const dirs = entries
|
|
671
|
+
.filter((e) => e.isDirectory() && !skipDirs.has(e.name))
|
|
672
|
+
.sort((a, b) => a.name.localeCompare(b.name));
|
|
673
|
+
const files = entries.filter((e) => e.isFile()).sort((a, b) => a.name.localeCompare(b.name));
|
|
674
|
+
|
|
675
|
+
for (const d of dirs) {
|
|
676
|
+
output.push(`${prefix}${d.name}/`);
|
|
677
|
+
await listDirRecursive(path.join(dir, d.name), prefix + " ", maxDepth - 1, output);
|
|
678
|
+
}
|
|
679
|
+
for (const f of files) {
|
|
680
|
+
const fp = path.join(dir, f.name);
|
|
681
|
+
try {
|
|
682
|
+
const size = statSync(fp).size;
|
|
683
|
+
const tokenEst = Math.ceil(size / 4);
|
|
684
|
+
output.push(`${prefix}${f.name} (~${tokenEst.toLocaleString()} tokens)`);
|
|
685
|
+
} catch {
|
|
686
|
+
output.push(`${prefix}${f.name}`);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
} catch {
|
|
690
|
+
// Permission denied, skip
|
|
691
|
+
}
|
|
713
692
|
}
|
|
714
693
|
|
|
715
694
|
export const SrcwalkPlugin: Plugin = async () => {
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
695
|
+
return {
|
|
696
|
+
tool: srcwalkTools,
|
|
697
|
+
};
|
|
719
698
|
};
|
|
720
699
|
|
|
721
700
|
export default SrcwalkPlugin;
|