@sashabogi/argus-mcp 1.2.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +403 -156
- package/dist/cli.mjs +286 -45
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +70 -1
- package/dist/index.mjs +252 -0
- package/dist/index.mjs.map +1 -1
- package/dist/mcp.mjs +1080 -35
- package/dist/mcp.mjs.map +1 -1
- package/package.json +5 -1
package/dist/mcp.mjs
CHANGED
|
@@ -1,9 +1,219 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
var init_esm_shims = __esm({
|
|
14
|
+
"node_modules/tsup/assets/esm_shims.js"() {
|
|
15
|
+
"use strict";
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// src/core/semantic-search.ts
|
|
20
|
+
var semantic_search_exports = {};
|
|
21
|
+
__export(semantic_search_exports, {
|
|
22
|
+
SemanticIndex: () => SemanticIndex
|
|
23
|
+
});
|
|
24
|
+
import Database from "better-sqlite3";
|
|
25
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync5 } from "fs";
|
|
26
|
+
import { dirname as dirname2 } from "path";
|
|
27
|
+
var SemanticIndex;
|
|
28
|
+
var init_semantic_search = __esm({
|
|
29
|
+
"src/core/semantic-search.ts"() {
|
|
30
|
+
"use strict";
|
|
31
|
+
init_esm_shims();
|
|
32
|
+
SemanticIndex = class {
|
|
33
|
+
db;
|
|
34
|
+
initialized = false;
|
|
35
|
+
constructor(dbPath) {
|
|
36
|
+
const dir = dirname2(dbPath);
|
|
37
|
+
if (!existsSync4(dir)) {
|
|
38
|
+
mkdirSync2(dir, { recursive: true });
|
|
39
|
+
}
|
|
40
|
+
this.db = new Database(dbPath);
|
|
41
|
+
this.initialize();
|
|
42
|
+
}
|
|
43
|
+
initialize() {
|
|
44
|
+
if (this.initialized) return;
|
|
45
|
+
this.db.exec(`
|
|
46
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS code_index USING fts5(
|
|
47
|
+
file,
|
|
48
|
+
symbol,
|
|
49
|
+
content,
|
|
50
|
+
type,
|
|
51
|
+
tokenize='porter unicode61'
|
|
52
|
+
);
|
|
53
|
+
`);
|
|
54
|
+
this.db.exec(`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS index_metadata (
|
|
56
|
+
key TEXT PRIMARY KEY,
|
|
57
|
+
value TEXT
|
|
58
|
+
);
|
|
59
|
+
`);
|
|
60
|
+
this.initialized = true;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Clear the index and rebuild from scratch
|
|
64
|
+
*/
|
|
65
|
+
clear() {
|
|
66
|
+
this.db.exec("DELETE FROM code_index");
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Index a file's symbols and content
|
|
70
|
+
*/
|
|
71
|
+
indexFile(file, symbols) {
|
|
72
|
+
const insert = this.db.prepare(`
|
|
73
|
+
INSERT INTO code_index (file, symbol, content, type)
|
|
74
|
+
VALUES (?, ?, ?, ?)
|
|
75
|
+
`);
|
|
76
|
+
const tx = this.db.transaction(() => {
|
|
77
|
+
for (const sym of symbols) {
|
|
78
|
+
insert.run(file, sym.name, sym.content, sym.type);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
tx();
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Index content from a snapshot file
|
|
85
|
+
*/
|
|
86
|
+
indexFromSnapshot(snapshotPath) {
|
|
87
|
+
const content = readFileSync5(snapshotPath, "utf-8");
|
|
88
|
+
this.clear();
|
|
89
|
+
let filesIndexed = 0;
|
|
90
|
+
let symbolsIndexed = 0;
|
|
91
|
+
const fileRegex = /^FILE: \.\/(.+)$/gm;
|
|
92
|
+
const files = [];
|
|
93
|
+
let match;
|
|
94
|
+
while ((match = fileRegex.exec(content)) !== null) {
|
|
95
|
+
if (files.length > 0) {
|
|
96
|
+
files[files.length - 1].end = match.index;
|
|
97
|
+
}
|
|
98
|
+
files.push({ path: match[1], start: match.index, end: content.length });
|
|
99
|
+
}
|
|
100
|
+
const metadataStart = content.indexOf("\nMETADATA:");
|
|
101
|
+
if (metadataStart !== -1 && files.length > 0) {
|
|
102
|
+
files[files.length - 1].end = metadataStart;
|
|
103
|
+
}
|
|
104
|
+
for (const file of files) {
|
|
105
|
+
const fileContent = content.slice(file.start, file.end);
|
|
106
|
+
const lines = fileContent.split("\n").slice(2);
|
|
107
|
+
const symbols = [];
|
|
108
|
+
for (let i = 0; i < lines.length; i++) {
|
|
109
|
+
const line = lines[i];
|
|
110
|
+
const funcMatch = line.match(/(?:export\s+)?(?:async\s+)?function\s+(\w+)/);
|
|
111
|
+
if (funcMatch) {
|
|
112
|
+
symbols.push({
|
|
113
|
+
name: funcMatch[1],
|
|
114
|
+
content: lines.slice(i, Math.min(i + 10, lines.length)).join("\n"),
|
|
115
|
+
type: "function"
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
const arrowMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*=>/);
|
|
119
|
+
if (arrowMatch) {
|
|
120
|
+
symbols.push({
|
|
121
|
+
name: arrowMatch[1],
|
|
122
|
+
content: lines.slice(i, Math.min(i + 10, lines.length)).join("\n"),
|
|
123
|
+
type: "function"
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
const classMatch = line.match(/(?:export\s+)?class\s+(\w+)/);
|
|
127
|
+
if (classMatch) {
|
|
128
|
+
symbols.push({
|
|
129
|
+
name: classMatch[1],
|
|
130
|
+
content: lines.slice(i, Math.min(i + 15, lines.length)).join("\n"),
|
|
131
|
+
type: "class"
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
const typeMatch = line.match(/(?:export\s+)?(?:type|interface)\s+(\w+)/);
|
|
135
|
+
if (typeMatch) {
|
|
136
|
+
symbols.push({
|
|
137
|
+
name: typeMatch[1],
|
|
138
|
+
content: lines.slice(i, Math.min(i + 10, lines.length)).join("\n"),
|
|
139
|
+
type: "type"
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const constMatch = line.match(/(?:export\s+)?const\s+(\w+)\s*=\s*(?![^(]*=>)/);
|
|
143
|
+
if (constMatch && !arrowMatch) {
|
|
144
|
+
symbols.push({
|
|
145
|
+
name: constMatch[1],
|
|
146
|
+
content: lines.slice(i, Math.min(i + 5, lines.length)).join("\n"),
|
|
147
|
+
type: "const"
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (symbols.length > 0) {
|
|
152
|
+
this.indexFile(file.path, symbols);
|
|
153
|
+
filesIndexed++;
|
|
154
|
+
symbolsIndexed += symbols.length;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
this.db.prepare(`
|
|
158
|
+
INSERT OR REPLACE INTO index_metadata (key, value) VALUES (?, ?)
|
|
159
|
+
`).run("last_indexed", (/* @__PURE__ */ new Date()).toISOString());
|
|
160
|
+
this.db.prepare(`
|
|
161
|
+
INSERT OR REPLACE INTO index_metadata (key, value) VALUES (?, ?)
|
|
162
|
+
`).run("snapshot_path", snapshotPath);
|
|
163
|
+
return { filesIndexed, symbolsIndexed };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Search the index
|
|
167
|
+
*/
|
|
168
|
+
search(query, limit = 20) {
|
|
169
|
+
const ftsQuery = query.split(/\s+/).map((term) => `${term}*`).join(" ");
|
|
170
|
+
try {
|
|
171
|
+
const stmt = this.db.prepare(`
|
|
172
|
+
SELECT file, symbol, content, type, rank
|
|
173
|
+
FROM code_index
|
|
174
|
+
WHERE code_index MATCH ?
|
|
175
|
+
ORDER BY rank
|
|
176
|
+
LIMIT ?
|
|
177
|
+
`);
|
|
178
|
+
return stmt.all(ftsQuery, limit);
|
|
179
|
+
} catch {
|
|
180
|
+
const stmt = this.db.prepare(`
|
|
181
|
+
SELECT file, symbol, content, type, 0 as rank
|
|
182
|
+
FROM code_index
|
|
183
|
+
WHERE symbol LIKE ? OR content LIKE ?
|
|
184
|
+
ORDER BY symbol
|
|
185
|
+
LIMIT ?
|
|
186
|
+
`);
|
|
187
|
+
const likePattern = `%${query}%`;
|
|
188
|
+
return stmt.all(likePattern, likePattern, limit);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Get index statistics
|
|
193
|
+
*/
|
|
194
|
+
getStats() {
|
|
195
|
+
const countResult = this.db.prepare("SELECT COUNT(*) as count FROM code_index").get();
|
|
196
|
+
const lastIndexed = this.db.prepare("SELECT value FROM index_metadata WHERE key = 'last_indexed'").get();
|
|
197
|
+
const snapshotPath = this.db.prepare("SELECT value FROM index_metadata WHERE key = 'snapshot_path'").get();
|
|
198
|
+
return {
|
|
199
|
+
totalSymbols: countResult.count,
|
|
200
|
+
lastIndexed: lastIndexed?.value || null,
|
|
201
|
+
snapshotPath: snapshotPath?.value || null
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
close() {
|
|
205
|
+
this.db.close();
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
});
|
|
2
210
|
|
|
3
211
|
// src/mcp.ts
|
|
212
|
+
init_esm_shims();
|
|
4
213
|
import { createInterface } from "readline";
|
|
5
214
|
|
|
6
215
|
// src/core/config.ts
|
|
216
|
+
init_esm_shims();
|
|
7
217
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
218
|
import { homedir } from "os";
|
|
9
219
|
import { join } from "path";
|
|
@@ -79,7 +289,14 @@ function validateConfig(config2) {
|
|
|
79
289
|
return errors;
|
|
80
290
|
}
|
|
81
291
|
|
|
292
|
+
// src/core/enhanced-snapshot.ts
|
|
293
|
+
init_esm_shims();
|
|
294
|
+
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
295
|
+
import { join as join3, dirname, extname as extname2, basename } from "path";
|
|
296
|
+
import { execSync } from "child_process";
|
|
297
|
+
|
|
82
298
|
// src/core/snapshot.ts
|
|
299
|
+
init_esm_shims();
|
|
83
300
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
84
301
|
import { join as join2, relative, extname } from "path";
|
|
85
302
|
var DEFAULT_OPTIONS = {
|
|
@@ -203,10 +420,404 @@ function createSnapshot(projectPath, outputPath, options = {}) {
|
|
|
203
420
|
};
|
|
204
421
|
}
|
|
205
422
|
|
|
423
|
+
// src/core/enhanced-snapshot.ts
|
|
424
|
+
function parseImports(content, filePath) {
|
|
425
|
+
const imports = [];
|
|
426
|
+
const lines = content.split("\n");
|
|
427
|
+
const patterns = [
|
|
428
|
+
// import { a, b } from 'module'
|
|
429
|
+
/import\s+(?:type\s+)?{([^}]+)}\s+from\s+['"]([^'"]+)['"]/g,
|
|
430
|
+
// import * as name from 'module'
|
|
431
|
+
/import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
|
432
|
+
// import defaultExport from 'module'
|
|
433
|
+
/import\s+(?:type\s+)?(\w+)\s+from\s+['"]([^'"]+)['"]/g,
|
|
434
|
+
// import 'module' (side-effect)
|
|
435
|
+
/import\s+['"]([^'"]+)['"]/g,
|
|
436
|
+
// require('module')
|
|
437
|
+
/(?:const|let|var)\s+(?:{([^}]+)}|(\w+))\s*=\s*require\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
438
|
+
];
|
|
439
|
+
for (const line of lines) {
|
|
440
|
+
const trimmed = line.trim();
|
|
441
|
+
if (!trimmed.startsWith("import") && !trimmed.includes("require(")) continue;
|
|
442
|
+
let match = /import\s+(type\s+)?{([^}]+)}\s+from\s+['"]([^'"]+)['"]/.exec(trimmed);
|
|
443
|
+
if (match) {
|
|
444
|
+
const isType = !!match[1];
|
|
445
|
+
const symbols = match[2].split(",").map((s) => s.trim().split(/\s+as\s+/)[0].trim()).filter(Boolean);
|
|
446
|
+
const target = match[3];
|
|
447
|
+
imports.push({
|
|
448
|
+
source: filePath,
|
|
449
|
+
target,
|
|
450
|
+
symbols,
|
|
451
|
+
isDefault: false,
|
|
452
|
+
isType
|
|
453
|
+
});
|
|
454
|
+
continue;
|
|
455
|
+
}
|
|
456
|
+
match = /import\s+\*\s+as\s+(\w+)\s+from\s+['"]([^'"]+)['"]/.exec(trimmed);
|
|
457
|
+
if (match) {
|
|
458
|
+
imports.push({
|
|
459
|
+
source: filePath,
|
|
460
|
+
target: match[2],
|
|
461
|
+
symbols: ["*"],
|
|
462
|
+
isDefault: false,
|
|
463
|
+
isType: false
|
|
464
|
+
});
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
match = /import\s+(type\s+)?(\w+)\s+from\s+['"]([^'"]+)['"]/.exec(trimmed);
|
|
468
|
+
if (match && !trimmed.includes("{")) {
|
|
469
|
+
imports.push({
|
|
470
|
+
source: filePath,
|
|
471
|
+
target: match[3],
|
|
472
|
+
symbols: [match[2]],
|
|
473
|
+
isDefault: true,
|
|
474
|
+
isType: !!match[1]
|
|
475
|
+
});
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
match = /^import\s+['"]([^'"]+)['"]/.exec(trimmed);
|
|
479
|
+
if (match) {
|
|
480
|
+
imports.push({
|
|
481
|
+
source: filePath,
|
|
482
|
+
target: match[1],
|
|
483
|
+
symbols: [],
|
|
484
|
+
isDefault: false,
|
|
485
|
+
isType: false
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
return imports;
|
|
490
|
+
}
|
|
491
|
+
function parseExports(content, filePath) {
|
|
492
|
+
const exports = [];
|
|
493
|
+
const lines = content.split("\n");
|
|
494
|
+
for (let i = 0; i < lines.length; i++) {
|
|
495
|
+
const line = lines[i];
|
|
496
|
+
const trimmed = line.trim();
|
|
497
|
+
let match = /export\s+(?:async\s+)?function\s+(\w+)\s*(\([^)]*\))/.exec(trimmed);
|
|
498
|
+
if (match) {
|
|
499
|
+
exports.push({
|
|
500
|
+
file: filePath,
|
|
501
|
+
symbol: match[1],
|
|
502
|
+
type: "function",
|
|
503
|
+
signature: `function ${match[1]}${match[2]}`,
|
|
504
|
+
line: i + 1
|
|
505
|
+
});
|
|
506
|
+
continue;
|
|
507
|
+
}
|
|
508
|
+
match = /export\s+class\s+(\w+)/.exec(trimmed);
|
|
509
|
+
if (match) {
|
|
510
|
+
exports.push({
|
|
511
|
+
file: filePath,
|
|
512
|
+
symbol: match[1],
|
|
513
|
+
type: "class",
|
|
514
|
+
line: i + 1
|
|
515
|
+
});
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
match = /export\s+(const|let|var)\s+(\w+)/.exec(trimmed);
|
|
519
|
+
if (match) {
|
|
520
|
+
exports.push({
|
|
521
|
+
file: filePath,
|
|
522
|
+
symbol: match[2],
|
|
523
|
+
type: match[1],
|
|
524
|
+
line: i + 1
|
|
525
|
+
});
|
|
526
|
+
continue;
|
|
527
|
+
}
|
|
528
|
+
match = /export\s+(type|interface)\s+(\w+)/.exec(trimmed);
|
|
529
|
+
if (match) {
|
|
530
|
+
exports.push({
|
|
531
|
+
file: filePath,
|
|
532
|
+
symbol: match[2],
|
|
533
|
+
type: match[1],
|
|
534
|
+
line: i + 1
|
|
535
|
+
});
|
|
536
|
+
continue;
|
|
537
|
+
}
|
|
538
|
+
match = /export\s+enum\s+(\w+)/.exec(trimmed);
|
|
539
|
+
if (match) {
|
|
540
|
+
exports.push({
|
|
541
|
+
file: filePath,
|
|
542
|
+
symbol: match[1],
|
|
543
|
+
type: "enum",
|
|
544
|
+
line: i + 1
|
|
545
|
+
});
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
if (/export\s+default/.test(trimmed)) {
|
|
549
|
+
match = /export\s+default\s+(?:function\s+)?(\w+)?/.exec(trimmed);
|
|
550
|
+
exports.push({
|
|
551
|
+
file: filePath,
|
|
552
|
+
symbol: match?.[1] || "default",
|
|
553
|
+
type: "default",
|
|
554
|
+
line: i + 1
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
return exports;
|
|
559
|
+
}
|
|
560
|
+
function calculateComplexity(content) {
|
|
561
|
+
const patterns = [
|
|
562
|
+
/\bif\s*\(/g,
|
|
563
|
+
/\belse\s+if\s*\(/g,
|
|
564
|
+
/\bwhile\s*\(/g,
|
|
565
|
+
/\bfor\s*\(/g,
|
|
566
|
+
/\bcase\s+/g,
|
|
567
|
+
/\?\s*.*\s*:/g,
|
|
568
|
+
/\&\&/g,
|
|
569
|
+
/\|\|/g,
|
|
570
|
+
/\bcatch\s*\(/g
|
|
571
|
+
];
|
|
572
|
+
let complexity = 1;
|
|
573
|
+
for (const pattern of patterns) {
|
|
574
|
+
const matches = content.match(pattern);
|
|
575
|
+
if (matches) complexity += matches.length;
|
|
576
|
+
}
|
|
577
|
+
return complexity;
|
|
578
|
+
}
|
|
579
|
+
function getComplexityLevel(score) {
|
|
580
|
+
if (score <= 10) return "low";
|
|
581
|
+
if (score <= 20) return "medium";
|
|
582
|
+
return "high";
|
|
583
|
+
}
|
|
584
|
+
function mapTestFiles(files) {
|
|
585
|
+
const testMap = {};
|
|
586
|
+
const testPatterns = [
|
|
587
|
+
// Same directory patterns
|
|
588
|
+
(src) => src.replace(/\.tsx?$/, ".test.ts"),
|
|
589
|
+
(src) => src.replace(/\.tsx?$/, ".test.tsx"),
|
|
590
|
+
(src) => src.replace(/\.tsx?$/, ".spec.ts"),
|
|
591
|
+
(src) => src.replace(/\.tsx?$/, ".spec.tsx"),
|
|
592
|
+
(src) => src.replace(/\.jsx?$/, ".test.js"),
|
|
593
|
+
(src) => src.replace(/\.jsx?$/, ".test.jsx"),
|
|
594
|
+
(src) => src.replace(/\.jsx?$/, ".spec.js"),
|
|
595
|
+
(src) => src.replace(/\.jsx?$/, ".spec.jsx"),
|
|
596
|
+
// __tests__ directory pattern
|
|
597
|
+
(src) => {
|
|
598
|
+
const dir = dirname(src);
|
|
599
|
+
const base = basename(src).replace(/\.(tsx?|jsx?)$/, "");
|
|
600
|
+
return join3(dir, "__tests__", `${base}.test.ts`);
|
|
601
|
+
},
|
|
602
|
+
(src) => {
|
|
603
|
+
const dir = dirname(src);
|
|
604
|
+
const base = basename(src).replace(/\.(tsx?|jsx?)$/, "");
|
|
605
|
+
return join3(dir, "__tests__", `${base}.test.tsx`);
|
|
606
|
+
},
|
|
607
|
+
// test/ directory pattern
|
|
608
|
+
(src) => src.replace(/^src\//, "test/").replace(/\.(tsx?|jsx?)$/, ".test.ts"),
|
|
609
|
+
(src) => src.replace(/^src\//, "tests/").replace(/\.(tsx?|jsx?)$/, ".test.ts")
|
|
610
|
+
];
|
|
611
|
+
const fileSet = new Set(files);
|
|
612
|
+
for (const file of files) {
|
|
613
|
+
if (file.includes(".test.") || file.includes(".spec.") || file.includes("__tests__")) continue;
|
|
614
|
+
if (!/\.(tsx?|jsx?)$/.test(file)) continue;
|
|
615
|
+
const tests = [];
|
|
616
|
+
for (const pattern of testPatterns) {
|
|
617
|
+
const testPath = pattern(file);
|
|
618
|
+
if (testPath !== file && fileSet.has(testPath)) {
|
|
619
|
+
tests.push(testPath);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (tests.length > 0) {
|
|
623
|
+
testMap[file] = [...new Set(tests)];
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return testMap;
|
|
627
|
+
}
|
|
628
|
+
function getRecentChanges(projectPath) {
|
|
629
|
+
try {
|
|
630
|
+
execSync("git rev-parse --git-dir", { cwd: projectPath, encoding: "utf-8", stdio: "pipe" });
|
|
631
|
+
const output = execSync(
|
|
632
|
+
'git log --since="7 days ago" --name-only --format="COMMIT_AUTHOR:%an" --diff-filter=ACMR',
|
|
633
|
+
{ cwd: projectPath, encoding: "utf-8", maxBuffer: 10 * 1024 * 1024, stdio: "pipe" }
|
|
634
|
+
);
|
|
635
|
+
if (!output.trim()) return [];
|
|
636
|
+
const fileStats = {};
|
|
637
|
+
let currentAuthor = "";
|
|
638
|
+
let currentCommitId = 0;
|
|
639
|
+
for (const line of output.split("\n")) {
|
|
640
|
+
const trimmed = line.trim();
|
|
641
|
+
if (!trimmed) {
|
|
642
|
+
currentCommitId++;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (trimmed.startsWith("COMMIT_AUTHOR:")) {
|
|
646
|
+
currentAuthor = trimmed.replace("COMMIT_AUTHOR:", "");
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const file = trimmed;
|
|
650
|
+
if (!fileStats[file]) {
|
|
651
|
+
fileStats[file] = { commits: /* @__PURE__ */ new Set(), authors: /* @__PURE__ */ new Set() };
|
|
652
|
+
}
|
|
653
|
+
fileStats[file].commits.add(`${currentCommitId}`);
|
|
654
|
+
if (currentAuthor) {
|
|
655
|
+
fileStats[file].authors.add(currentAuthor);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
const result = Object.entries(fileStats).map(([file, stats]) => ({
|
|
659
|
+
file,
|
|
660
|
+
commits: stats.commits.size,
|
|
661
|
+
authors: stats.authors.size
|
|
662
|
+
})).sort((a, b) => b.commits - a.commits);
|
|
663
|
+
return result;
|
|
664
|
+
} catch {
|
|
665
|
+
return null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
function resolveImportPath(importPath, fromFile, projectFiles) {
|
|
669
|
+
if (!importPath.startsWith(".")) return void 0;
|
|
670
|
+
const fromDir = dirname(fromFile);
|
|
671
|
+
let resolved = join3(fromDir, importPath);
|
|
672
|
+
const extensions = [".ts", ".tsx", ".js", ".jsx", "", "/index.ts", "/index.tsx", "/index.js", "/index.jsx"];
|
|
673
|
+
for (const ext of extensions) {
|
|
674
|
+
const candidate = resolved + ext;
|
|
675
|
+
if (projectFiles.includes(candidate) || projectFiles.includes("./" + candidate)) {
|
|
676
|
+
return candidate;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return void 0;
|
|
680
|
+
}
|
|
681
|
+
function createEnhancedSnapshot(projectPath, outputPath, options = {}) {
|
|
682
|
+
const baseResult = createSnapshot(projectPath, outputPath, options);
|
|
683
|
+
const allImports = [];
|
|
684
|
+
const allExports = [];
|
|
685
|
+
const fileIndex = {};
|
|
686
|
+
const projectFiles = baseResult.files.map((f) => "./" + f);
|
|
687
|
+
for (const relPath of baseResult.files) {
|
|
688
|
+
const fullPath = join3(projectPath, relPath);
|
|
689
|
+
const ext = extname2(relPath).toLowerCase();
|
|
690
|
+
if (![".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"].includes(ext)) {
|
|
691
|
+
continue;
|
|
692
|
+
}
|
|
693
|
+
try {
|
|
694
|
+
const content = readFileSync3(fullPath, "utf-8");
|
|
695
|
+
const imports = parseImports(content, relPath);
|
|
696
|
+
const exports = parseExports(content, relPath);
|
|
697
|
+
for (const imp of imports) {
|
|
698
|
+
imp.resolved = resolveImportPath(imp.target, relPath, projectFiles);
|
|
699
|
+
}
|
|
700
|
+
allImports.push(...imports);
|
|
701
|
+
allExports.push(...exports);
|
|
702
|
+
fileIndex[relPath] = {
|
|
703
|
+
path: relPath,
|
|
704
|
+
imports,
|
|
705
|
+
exports,
|
|
706
|
+
size: content.length,
|
|
707
|
+
lines: content.split("\n").length
|
|
708
|
+
};
|
|
709
|
+
} catch {
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
const importGraph = {};
|
|
713
|
+
for (const imp of allImports) {
|
|
714
|
+
if (imp.resolved) {
|
|
715
|
+
if (!importGraph[imp.source]) importGraph[imp.source] = [];
|
|
716
|
+
if (!importGraph[imp.source].includes(imp.resolved)) {
|
|
717
|
+
importGraph[imp.source].push(imp.resolved);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
const exportGraph = {};
|
|
722
|
+
for (const imp of allImports) {
|
|
723
|
+
if (imp.resolved) {
|
|
724
|
+
if (!exportGraph[imp.resolved]) exportGraph[imp.resolved] = [];
|
|
725
|
+
if (!exportGraph[imp.resolved].includes(imp.source)) {
|
|
726
|
+
exportGraph[imp.resolved].push(imp.source);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
const symbolIndex = {};
|
|
731
|
+
for (const exp of allExports) {
|
|
732
|
+
if (!symbolIndex[exp.symbol]) symbolIndex[exp.symbol] = [];
|
|
733
|
+
if (!symbolIndex[exp.symbol].includes(exp.file)) {
|
|
734
|
+
symbolIndex[exp.symbol].push(exp.file);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
const complexityScores = [];
|
|
738
|
+
for (const [relPath, metadata] of Object.entries(fileIndex)) {
|
|
739
|
+
const fullPath = join3(projectPath, relPath);
|
|
740
|
+
try {
|
|
741
|
+
const content = readFileSync3(fullPath, "utf-8");
|
|
742
|
+
const score = calculateComplexity(content);
|
|
743
|
+
complexityScores.push({
|
|
744
|
+
file: relPath,
|
|
745
|
+
score,
|
|
746
|
+
level: getComplexityLevel(score)
|
|
747
|
+
});
|
|
748
|
+
} catch {
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
complexityScores.sort((a, b) => b.score - a.score);
|
|
752
|
+
const testFileMap = mapTestFiles(baseResult.files);
|
|
753
|
+
const recentChanges = getRecentChanges(projectPath);
|
|
754
|
+
const metadataSection = `
|
|
755
|
+
|
|
756
|
+
================================================================================
|
|
757
|
+
METADATA: IMPORT GRAPH
|
|
758
|
+
================================================================================
|
|
759
|
+
${Object.entries(importGraph).map(([file, imports]) => `${file}:
|
|
760
|
+
${imports.map((i) => ` \u2192 ${i}`).join("\n")}`).join("\n\n")}
|
|
761
|
+
|
|
762
|
+
================================================================================
|
|
763
|
+
METADATA: EXPORT INDEX
|
|
764
|
+
================================================================================
|
|
765
|
+
${Object.entries(symbolIndex).map(([symbol, files]) => `${symbol}: ${files.join(", ")}`).join("\n")}
|
|
766
|
+
|
|
767
|
+
================================================================================
|
|
768
|
+
METADATA: FILE EXPORTS
|
|
769
|
+
================================================================================
|
|
770
|
+
${allExports.map((e) => `${e.file}:${e.line} - ${e.type} ${e.symbol}${e.signature ? ` ${e.signature}` : ""}`).join("\n")}
|
|
771
|
+
|
|
772
|
+
================================================================================
|
|
773
|
+
METADATA: WHO IMPORTS WHOM
|
|
774
|
+
================================================================================
|
|
775
|
+
${Object.entries(exportGraph).map(([file, importers]) => `${file} is imported by:
|
|
776
|
+
${importers.map((i) => ` \u2190 ${i}`).join("\n")}`).join("\n\n")}
|
|
777
|
+
|
|
778
|
+
================================================================================
|
|
779
|
+
METADATA: COMPLEXITY SCORES
|
|
780
|
+
================================================================================
|
|
781
|
+
${complexityScores.map((c) => `${c.file}: ${c.score} (${c.level})`).join("\n")}
|
|
782
|
+
|
|
783
|
+
================================================================================
|
|
784
|
+
METADATA: TEST COVERAGE MAP
|
|
785
|
+
================================================================================
|
|
786
|
+
${Object.entries(testFileMap).length > 0 ? Object.entries(testFileMap).map(([src, tests]) => `${src} -> ${tests.join(", ")}`).join("\n") : "(no test file mappings found)"}
|
|
787
|
+
${baseResult.files.filter(
|
|
788
|
+
(f) => /\.(tsx?|jsx?)$/.test(f) && !f.includes(".test.") && !f.includes(".spec.") && !f.includes("__tests__") && !testFileMap[f]
|
|
789
|
+
).map((f) => `${f} -> (no tests)`).join("\n")}
|
|
790
|
+
${recentChanges !== null ? `
|
|
791
|
+
|
|
792
|
+
================================================================================
|
|
793
|
+
METADATA: RECENT CHANGES (last 7 days)
|
|
794
|
+
================================================================================
|
|
795
|
+
${recentChanges.length > 0 ? recentChanges.map((c) => `${c.file}: ${c.commits} commit${c.commits !== 1 ? "s" : ""}, ${c.authors} author${c.authors !== 1 ? "s" : ""}`).join("\n") : "(no changes in the last 7 days)"}` : ""}
|
|
796
|
+
`;
|
|
797
|
+
const existingContent = readFileSync3(outputPath, "utf-8");
|
|
798
|
+
writeFileSync3(outputPath, existingContent + metadataSection);
|
|
799
|
+
return {
|
|
800
|
+
...baseResult,
|
|
801
|
+
metadata: {
|
|
802
|
+
imports: allImports,
|
|
803
|
+
exports: allExports,
|
|
804
|
+
fileIndex,
|
|
805
|
+
importGraph,
|
|
806
|
+
exportGraph,
|
|
807
|
+
symbolIndex,
|
|
808
|
+
complexityScores,
|
|
809
|
+
testFileMap,
|
|
810
|
+
recentChanges
|
|
811
|
+
}
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
|
|
206
815
|
// src/core/engine.ts
|
|
207
|
-
|
|
816
|
+
init_esm_shims();
|
|
817
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
208
818
|
|
|
209
819
|
// src/core/prompts.ts
|
|
820
|
+
init_esm_shims();
|
|
210
821
|
var NUCLEUS_COMMANDS = `
|
|
211
822
|
COMMANDS (output ONE per turn):
|
|
212
823
|
(grep "pattern") - Find lines matching regex
|
|
@@ -562,7 +1173,7 @@ async function analyze(provider2, documentPath, query, options = {}) {
|
|
|
562
1173
|
onProgress
|
|
563
1174
|
} = options;
|
|
564
1175
|
const dynamicLimit = Math.min(getTurnLimit(query), maxTurns);
|
|
565
|
-
const content =
|
|
1176
|
+
const content = readFileSync4(documentPath, "utf-8");
|
|
566
1177
|
const fileCount = (content.match(/^FILE:/gm) || []).length;
|
|
567
1178
|
const lineCount = content.split("\n").length;
|
|
568
1179
|
const bindings = /* @__PURE__ */ new Map();
|
|
@@ -676,7 +1287,7 @@ ${truncatedResult}`;
|
|
|
676
1287
|
};
|
|
677
1288
|
}
|
|
678
1289
|
function searchDocument(documentPath, pattern, options = {}) {
|
|
679
|
-
const content =
|
|
1290
|
+
const content = readFileSync4(documentPath, "utf-8");
|
|
680
1291
|
const flags = options.caseInsensitive ? "gi" : "g";
|
|
681
1292
|
const regex = new RegExp(pattern, flags);
|
|
682
1293
|
const lines = content.split("\n");
|
|
@@ -703,7 +1314,11 @@ function searchDocument(documentPath, pattern, options = {}) {
|
|
|
703
1314
|
return matches;
|
|
704
1315
|
}
|
|
705
1316
|
|
|
1317
|
+
// src/providers/index.ts
|
|
1318
|
+
init_esm_shims();
|
|
1319
|
+
|
|
706
1320
|
// src/providers/openai-compatible.ts
|
|
1321
|
+
init_esm_shims();
|
|
707
1322
|
var OpenAICompatibleProvider = class {
|
|
708
1323
|
name;
|
|
709
1324
|
config;
|
|
@@ -787,6 +1402,7 @@ function createDeepSeekProvider(config2) {
|
|
|
787
1402
|
}
|
|
788
1403
|
|
|
789
1404
|
// src/providers/ollama.ts
|
|
1405
|
+
init_esm_shims();
|
|
790
1406
|
var OllamaProvider = class {
|
|
791
1407
|
name = "Ollama";
|
|
792
1408
|
config;
|
|
@@ -865,6 +1481,7 @@ function createOllamaProvider(config2) {
|
|
|
865
1481
|
}
|
|
866
1482
|
|
|
867
1483
|
// src/providers/anthropic.ts
|
|
1484
|
+
init_esm_shims();
|
|
868
1485
|
var AnthropicProvider = class {
|
|
869
1486
|
name = "Anthropic";
|
|
870
1487
|
config;
|
|
@@ -960,10 +1577,150 @@ function createProviderByType(type, config2) {
|
|
|
960
1577
|
}
|
|
961
1578
|
|
|
962
1579
|
// src/mcp.ts
|
|
963
|
-
import { existsSync as
|
|
1580
|
+
import { existsSync as existsSync5, statSync as statSync2, mkdtempSync, unlinkSync, readFileSync as readFileSync6 } from "fs";
|
|
964
1581
|
import { tmpdir } from "os";
|
|
965
|
-
import { join as
|
|
1582
|
+
import { join as join4, resolve } from "path";
|
|
1583
|
+
var DEFAULT_FIND_FILES_LIMIT = 100;
|
|
1584
|
+
var MAX_FIND_FILES_LIMIT = 500;
|
|
1585
|
+
var DEFAULT_SEARCH_RESULTS = 50;
|
|
1586
|
+
var MAX_SEARCH_RESULTS = 200;
|
|
1587
|
+
var MAX_PATTERN_LENGTH = 500;
|
|
1588
|
+
var MAX_WILDCARDS = 20;
|
|
1589
|
+
var WORKER_URL = process.env.ARGUS_WORKER_URL || "http://localhost:37778";
|
|
1590
|
+
var workerAvailable = false;
|
|
1591
|
+
async function checkWorkerHealth() {
|
|
1592
|
+
try {
|
|
1593
|
+
const controller = new AbortController();
|
|
1594
|
+
const timeout = setTimeout(() => controller.abort(), 1e3);
|
|
1595
|
+
const response = await fetch(`${WORKER_URL}/health`, {
|
|
1596
|
+
signal: controller.signal
|
|
1597
|
+
});
|
|
1598
|
+
clearTimeout(timeout);
|
|
1599
|
+
return response.ok;
|
|
1600
|
+
} catch {
|
|
1601
|
+
return false;
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
checkWorkerHealth().then((available) => {
|
|
1605
|
+
workerAvailable = available;
|
|
1606
|
+
});
|
|
966
1607
|
var TOOLS = [
|
|
1608
|
+
{
|
|
1609
|
+
name: "__ARGUS_GUIDE",
|
|
1610
|
+
description: `ARGUS CODEBASE INTELLIGENCE - Follow this workflow for codebase questions:
|
|
1611
|
+
|
|
1612
|
+
STEP 1: Check for snapshot
|
|
1613
|
+
- Look for .argus/snapshot.txt in the project root
|
|
1614
|
+
- If missing, use create_snapshot first (saves to .argus/snapshot.txt)
|
|
1615
|
+
- Snapshots survive context compaction - create once, use forever
|
|
1616
|
+
|
|
1617
|
+
STEP 2: Use zero-cost tools first (NO AI tokens consumed)
|
|
1618
|
+
- search_codebase: Fast regex search, returns file:line:content
|
|
1619
|
+
- find_symbol: Locate where functions/types/classes are exported
|
|
1620
|
+
- find_importers: Find all files that depend on a given file
|
|
1621
|
+
- get_file_deps: See what modules a file imports
|
|
1622
|
+
- get_context: Get lines of code around a specific location
|
|
1623
|
+
|
|
1624
|
+
STEP 3: Use AI analysis only when zero-cost tools are insufficient
|
|
1625
|
+
- analyze_codebase: Deep reasoning across entire codebase (~500 tokens)
|
|
1626
|
+
- Use for architecture questions, pattern finding, complex relationships
|
|
1627
|
+
|
|
1628
|
+
EFFICIENCY MATRIX:
|
|
1629
|
+
| Question Type | Tool | Token Cost |
|
|
1630
|
+
|---------------------------|-------------------------|------------|
|
|
1631
|
+
| "Where is X defined?" | find_symbol | 0 |
|
|
1632
|
+
| "What uses this file?" | find_importers | 0 |
|
|
1633
|
+
| "Find all TODO comments" | search_codebase | 0 |
|
|
1634
|
+
| "Show context around L42" | get_context | 0 |
|
|
1635
|
+
| "How does auth work?" | analyze_codebase | ~500 |
|
|
1636
|
+
|
|
1637
|
+
SNAPSHOT FRESHNESS:
|
|
1638
|
+
- Snapshots don't auto-update (yet)
|
|
1639
|
+
- Re-run create_snapshot if files have changed significantly
|
|
1640
|
+
- Check snapshot timestamp in header to assess freshness`,
|
|
1641
|
+
inputSchema: {
|
|
1642
|
+
type: "object",
|
|
1643
|
+
properties: {},
|
|
1644
|
+
required: []
|
|
1645
|
+
}
|
|
1646
|
+
},
|
|
1647
|
+
{
|
|
1648
|
+
name: "get_context",
|
|
1649
|
+
description: `Get lines of code around a specific location. Zero AI cost.
|
|
1650
|
+
|
|
1651
|
+
Use AFTER search_codebase when you need more context around a match.
|
|
1652
|
+
Much more efficient than reading the entire file.
|
|
1653
|
+
|
|
1654
|
+
Example workflow:
|
|
1655
|
+
1. search_codebase("handleAuth") -> finds src/auth.ts:42
|
|
1656
|
+
2. get_context(file="src/auth.ts", line=42, before=10, after=20)
|
|
1657
|
+
|
|
1658
|
+
Returns the surrounding code with proper line numbers.`,
|
|
1659
|
+
inputSchema: {
|
|
1660
|
+
type: "object",
|
|
1661
|
+
properties: {
|
|
1662
|
+
path: {
|
|
1663
|
+
type: "string",
|
|
1664
|
+
description: "Path to the snapshot file (.argus/snapshot.txt)"
|
|
1665
|
+
},
|
|
1666
|
+
file: {
|
|
1667
|
+
type: "string",
|
|
1668
|
+
description: 'File path within the snapshot (e.g., "src/auth.ts")'
|
|
1669
|
+
},
|
|
1670
|
+
line: {
|
|
1671
|
+
type: "number",
|
|
1672
|
+
description: "Center line number to get context around"
|
|
1673
|
+
},
|
|
1674
|
+
before: {
|
|
1675
|
+
type: "number",
|
|
1676
|
+
description: "Lines to include before the target line (default: 10)"
|
|
1677
|
+
},
|
|
1678
|
+
after: {
|
|
1679
|
+
type: "number",
|
|
1680
|
+
description: "Lines to include after the target line (default: 10)"
|
|
1681
|
+
}
|
|
1682
|
+
},
|
|
1683
|
+
required: ["path", "file", "line"]
|
|
1684
|
+
}
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
name: "find_files",
|
|
1688
|
+
description: `Find files matching a glob pattern. Ultra-low cost (~10 tokens per result).
|
|
1689
|
+
|
|
1690
|
+
Use for:
|
|
1691
|
+
- "What files are in src/components?"
|
|
1692
|
+
- "Find all test files"
|
|
1693
|
+
- "List files named auth*"
|
|
1694
|
+
|
|
1695
|
+
Patterns:
|
|
1696
|
+
- * matches any characters except /
|
|
1697
|
+
- ** matches any characters including /
|
|
1698
|
+
- ? matches single character
|
|
1699
|
+
|
|
1700
|
+
Returns file paths only - use get_context or search_codebase for content.`,
|
|
1701
|
+
inputSchema: {
|
|
1702
|
+
type: "object",
|
|
1703
|
+
properties: {
|
|
1704
|
+
path: {
|
|
1705
|
+
type: "string",
|
|
1706
|
+
description: "Path to the snapshot file (.argus/snapshot.txt)"
|
|
1707
|
+
},
|
|
1708
|
+
pattern: {
|
|
1709
|
+
type: "string",
|
|
1710
|
+
description: 'Glob pattern (e.g., "*.test.ts", "src/**/*.tsx", "**/*auth*")'
|
|
1711
|
+
},
|
|
1712
|
+
caseInsensitive: {
|
|
1713
|
+
type: "boolean",
|
|
1714
|
+
description: "Case-insensitive matching (default: true)"
|
|
1715
|
+
},
|
|
1716
|
+
limit: {
|
|
1717
|
+
type: "number",
|
|
1718
|
+
description: "Maximum results (default: 100, max: 500)"
|
|
1719
|
+
}
|
|
1720
|
+
},
|
|
1721
|
+
required: ["path", "pattern"]
|
|
1722
|
+
}
|
|
1723
|
+
},
|
|
967
1724
|
{
|
|
968
1725
|
name: "find_importers",
|
|
969
1726
|
description: `Find all files that import a given file or module. Zero AI cost.
|
|
@@ -973,7 +1730,7 @@ Use when you need to know:
|
|
|
973
1730
|
- Who uses this function/component?
|
|
974
1731
|
- Impact analysis before refactoring
|
|
975
1732
|
|
|
976
|
-
|
|
1733
|
+
Snapshots are enhanced by default and include this metadata.`,
|
|
977
1734
|
inputSchema: {
|
|
978
1735
|
type: "object",
|
|
979
1736
|
properties: {
|
|
@@ -998,7 +1755,7 @@ Use when you need to know:
|
|
|
998
1755
|
- Which file exports this component?
|
|
999
1756
|
- Find the source of a type
|
|
1000
1757
|
|
|
1001
|
-
|
|
1758
|
+
Snapshots are enhanced by default and include this metadata.`,
|
|
1002
1759
|
inputSchema: {
|
|
1003
1760
|
type: "object",
|
|
1004
1761
|
properties: {
|
|
@@ -1023,7 +1780,7 @@ Use when you need to understand:
|
|
|
1023
1780
|
- What modules need to be loaded?
|
|
1024
1781
|
- Trace the dependency chain
|
|
1025
1782
|
|
|
1026
|
-
|
|
1783
|
+
Snapshots are enhanced by default and include this metadata.`,
|
|
1027
1784
|
inputSchema: {
|
|
1028
1785
|
type: "object",
|
|
1029
1786
|
properties: {
|
|
@@ -1100,11 +1857,19 @@ Returns matching lines with line numbers - much faster than grep across many fil
|
|
|
1100
1857
|
},
|
|
1101
1858
|
caseInsensitive: {
|
|
1102
1859
|
type: "boolean",
|
|
1103
|
-
description: "Whether to ignore case (default:
|
|
1860
|
+
description: "Whether to ignore case (default: true)"
|
|
1104
1861
|
},
|
|
1105
1862
|
maxResults: {
|
|
1106
1863
|
type: "number",
|
|
1107
1864
|
description: "Maximum results to return (default: 50)"
|
|
1865
|
+
},
|
|
1866
|
+
offset: {
|
|
1867
|
+
type: "number",
|
|
1868
|
+
description: "Skip first N results for pagination (default: 0)"
|
|
1869
|
+
},
|
|
1870
|
+
contextChars: {
|
|
1871
|
+
type: "number",
|
|
1872
|
+
description: "Characters of context around match (default: 0 = full line)"
|
|
1108
1873
|
}
|
|
1109
1874
|
},
|
|
1110
1875
|
required: ["path", "pattern"]
|
|
@@ -1112,9 +1877,10 @@ Returns matching lines with line numbers - much faster than grep across many fil
|
|
|
1112
1877
|
},
|
|
1113
1878
|
{
|
|
1114
1879
|
name: "create_snapshot",
|
|
1115
|
-
description: `Create
|
|
1880
|
+
description: `Create an enhanced codebase snapshot for analysis. Run this ONCE per project, then use the snapshot for all queries.
|
|
1116
1881
|
|
|
1117
1882
|
The snapshot compiles all source files into a single optimized file that survives context compaction.
|
|
1883
|
+
Includes structural metadata (import graph, exports index) for zero-cost dependency queries.
|
|
1118
1884
|
Store at .argus/snapshot.txt so other tools can find it.
|
|
1119
1885
|
|
|
1120
1886
|
Run this when:
|
|
@@ -1140,6 +1906,38 @@ Run this when:
|
|
|
1140
1906
|
},
|
|
1141
1907
|
required: ["path"]
|
|
1142
1908
|
}
|
|
1909
|
+
},
|
|
1910
|
+
{
|
|
1911
|
+
name: "semantic_search",
|
|
1912
|
+
description: `Search code using natural language. Uses FTS5 full-text search.
|
|
1913
|
+
|
|
1914
|
+
More flexible than regex search - finds related concepts and partial matches.
|
|
1915
|
+
|
|
1916
|
+
Examples:
|
|
1917
|
+
- "authentication middleware"
|
|
1918
|
+
- "database connection"
|
|
1919
|
+
- "error handling"
|
|
1920
|
+
|
|
1921
|
+
Returns symbols (functions, classes, types) with snippets of their content.
|
|
1922
|
+
Requires an index - will auto-create from snapshot on first use.`,
|
|
1923
|
+
inputSchema: {
|
|
1924
|
+
type: "object",
|
|
1925
|
+
properties: {
|
|
1926
|
+
path: {
|
|
1927
|
+
type: "string",
|
|
1928
|
+
description: "Path to the project directory (must have .argus/snapshot.txt)"
|
|
1929
|
+
},
|
|
1930
|
+
query: {
|
|
1931
|
+
type: "string",
|
|
1932
|
+
description: "Natural language query or code terms"
|
|
1933
|
+
},
|
|
1934
|
+
limit: {
|
|
1935
|
+
type: "number",
|
|
1936
|
+
description: "Maximum results (default: 20)"
|
|
1937
|
+
}
|
|
1938
|
+
},
|
|
1939
|
+
required: ["path", "query"]
|
|
1940
|
+
}
|
|
1143
1941
|
}
|
|
1144
1942
|
];
|
|
1145
1943
|
var config;
|
|
@@ -1197,15 +1995,73 @@ function parseSnapshotMetadata(content) {
|
|
|
1197
1995
|
}
|
|
1198
1996
|
return { importGraph, exportGraph, symbolIndex, exports };
|
|
1199
1997
|
}
|
|
1998
|
+
async function searchWithWorker(snapshotPath, pattern, options) {
|
|
1999
|
+
if (!workerAvailable) return null;
|
|
2000
|
+
try {
|
|
2001
|
+
await fetch(`${WORKER_URL}/snapshot/load`, {
|
|
2002
|
+
method: "POST",
|
|
2003
|
+
headers: { "Content-Type": "application/json" },
|
|
2004
|
+
body: JSON.stringify({ path: snapshotPath })
|
|
2005
|
+
});
|
|
2006
|
+
const response = await fetch(`${WORKER_URL}/search`, {
|
|
2007
|
+
method: "POST",
|
|
2008
|
+
headers: { "Content-Type": "application/json" },
|
|
2009
|
+
body: JSON.stringify({ path: snapshotPath, pattern, options })
|
|
2010
|
+
});
|
|
2011
|
+
if (response.ok) {
|
|
2012
|
+
return await response.json();
|
|
2013
|
+
}
|
|
2014
|
+
} catch {
|
|
2015
|
+
}
|
|
2016
|
+
return null;
|
|
2017
|
+
}
|
|
1200
2018
|
async function handleToolCall(name, args) {
|
|
1201
2019
|
switch (name) {
|
|
2020
|
+
case "find_files": {
|
|
2021
|
+
const snapshotPath = resolve(args.path);
|
|
2022
|
+
const pattern = args.pattern;
|
|
2023
|
+
const caseInsensitive = args.caseInsensitive !== false;
|
|
2024
|
+
const limit = Math.min(args.limit || DEFAULT_FIND_FILES_LIMIT, MAX_FIND_FILES_LIMIT);
|
|
2025
|
+
if (!pattern || pattern.trim() === "") {
|
|
2026
|
+
throw new Error("Pattern cannot be empty");
|
|
2027
|
+
}
|
|
2028
|
+
if (pattern.length > MAX_PATTERN_LENGTH) {
|
|
2029
|
+
throw new Error(`Pattern too long (max ${MAX_PATTERN_LENGTH} characters)`);
|
|
2030
|
+
}
|
|
2031
|
+
const starCount = (pattern.match(/\*/g) || []).length;
|
|
2032
|
+
if (starCount > MAX_WILDCARDS) {
|
|
2033
|
+
throw new Error(`Too many wildcards in pattern (max ${MAX_WILDCARDS})`);
|
|
2034
|
+
}
|
|
2035
|
+
if (!existsSync5(snapshotPath)) {
|
|
2036
|
+
throw new Error(`Snapshot not found: ${snapshotPath}. Run 'argus snapshot' to create one.`);
|
|
2037
|
+
}
|
|
2038
|
+
const content = readFileSync6(snapshotPath, "utf-8");
|
|
2039
|
+
const fileRegex = /^FILE: \.\/(.+)$/gm;
|
|
2040
|
+
const files = [];
|
|
2041
|
+
let match;
|
|
2042
|
+
while ((match = fileRegex.exec(content)) !== null) {
|
|
2043
|
+
files.push(match[1]);
|
|
2044
|
+
}
|
|
2045
|
+
let regexPattern = pattern.replace(/[.+^${}()|[\]\\-]/g, "\\$&").replace(/\*\*/g, "<<<GLOBSTAR>>>").replace(/\*/g, "[^/]*?").replace(/<<<GLOBSTAR>>>/g, ".*?").replace(/\?/g, ".");
|
|
2046
|
+
const flags = caseInsensitive ? "i" : "";
|
|
2047
|
+
const regex = new RegExp(`^${regexPattern}$`, flags);
|
|
2048
|
+
const matching = files.filter((f) => regex.test(f));
|
|
2049
|
+
const limited = matching.slice(0, limit).sort();
|
|
2050
|
+
return {
|
|
2051
|
+
pattern,
|
|
2052
|
+
files: limited,
|
|
2053
|
+
count: limited.length,
|
|
2054
|
+
totalMatching: matching.length,
|
|
2055
|
+
hasMore: matching.length > limit
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
1202
2058
|
case "find_importers": {
|
|
1203
2059
|
const path = resolve(args.path);
|
|
1204
2060
|
const target = args.target;
|
|
1205
|
-
if (!
|
|
2061
|
+
if (!existsSync5(path)) {
|
|
1206
2062
|
throw new Error(`File not found: ${path}`);
|
|
1207
2063
|
}
|
|
1208
|
-
const content =
|
|
2064
|
+
const content = readFileSync6(path, "utf-8");
|
|
1209
2065
|
const metadata = parseSnapshotMetadata(content);
|
|
1210
2066
|
if (!metadata) {
|
|
1211
2067
|
throw new Error("This snapshot does not have metadata. Create with: argus snapshot --enhanced");
|
|
@@ -1236,10 +2092,10 @@ async function handleToolCall(name, args) {
|
|
|
1236
2092
|
case "find_symbol": {
|
|
1237
2093
|
const path = resolve(args.path);
|
|
1238
2094
|
const symbol = args.symbol;
|
|
1239
|
-
if (!
|
|
2095
|
+
if (!existsSync5(path)) {
|
|
1240
2096
|
throw new Error(`File not found: ${path}`);
|
|
1241
2097
|
}
|
|
1242
|
-
const content =
|
|
2098
|
+
const content = readFileSync6(path, "utf-8");
|
|
1243
2099
|
const metadata = parseSnapshotMetadata(content);
|
|
1244
2100
|
if (!metadata) {
|
|
1245
2101
|
throw new Error("This snapshot does not have metadata. Create with: argus snapshot --enhanced");
|
|
@@ -1256,10 +2112,10 @@ async function handleToolCall(name, args) {
|
|
|
1256
2112
|
case "get_file_deps": {
|
|
1257
2113
|
const path = resolve(args.path);
|
|
1258
2114
|
const file = args.file;
|
|
1259
|
-
if (!
|
|
2115
|
+
if (!existsSync5(path)) {
|
|
1260
2116
|
throw new Error(`File not found: ${path}`);
|
|
1261
2117
|
}
|
|
1262
|
-
const content =
|
|
2118
|
+
const content = readFileSync6(path, "utf-8");
|
|
1263
2119
|
const metadata = parseSnapshotMetadata(content);
|
|
1264
2120
|
if (!metadata) {
|
|
1265
2121
|
throw new Error("This snapshot does not have metadata. Create with: argus snapshot --enhanced");
|
|
@@ -1286,16 +2142,16 @@ async function handleToolCall(name, args) {
|
|
|
1286
2142
|
const path = resolve(args.path);
|
|
1287
2143
|
const query = args.query;
|
|
1288
2144
|
const maxTurns = args.maxTurns || 15;
|
|
1289
|
-
if (!
|
|
2145
|
+
if (!existsSync5(path)) {
|
|
1290
2146
|
throw new Error(`Path not found: ${path}`);
|
|
1291
2147
|
}
|
|
1292
2148
|
let snapshotPath = path;
|
|
1293
2149
|
let tempSnapshot = false;
|
|
1294
2150
|
const stats = statSync2(path);
|
|
1295
2151
|
if (stats.isDirectory()) {
|
|
1296
|
-
const tempDir = mkdtempSync(
|
|
1297
|
-
snapshotPath =
|
|
1298
|
-
|
|
2152
|
+
const tempDir = mkdtempSync(join4(tmpdir(), "argus-"));
|
|
2153
|
+
snapshotPath = join4(tempDir, "snapshot.txt");
|
|
2154
|
+
createEnhancedSnapshot(path, snapshotPath, {
|
|
1299
2155
|
extensions: config.defaults.snapshotExtensions,
|
|
1300
2156
|
excludePatterns: config.defaults.excludePatterns
|
|
1301
2157
|
});
|
|
@@ -1310,7 +2166,7 @@ async function handleToolCall(name, args) {
|
|
|
1310
2166
|
commands: result.commands
|
|
1311
2167
|
};
|
|
1312
2168
|
} finally {
|
|
1313
|
-
if (tempSnapshot &&
|
|
2169
|
+
if (tempSnapshot && existsSync5(snapshotPath)) {
|
|
1314
2170
|
unlinkSync(snapshotPath);
|
|
1315
2171
|
}
|
|
1316
2172
|
}
|
|
@@ -1318,29 +2174,159 @@ async function handleToolCall(name, args) {
|
|
|
1318
2174
|
case "search_codebase": {
|
|
1319
2175
|
const path = resolve(args.path);
|
|
1320
2176
|
const pattern = args.pattern;
|
|
1321
|
-
const caseInsensitive = args.caseInsensitive
|
|
1322
|
-
const maxResults = args.maxResults ||
|
|
1323
|
-
|
|
1324
|
-
|
|
2177
|
+
const caseInsensitive = args.caseInsensitive !== false;
|
|
2178
|
+
const maxResults = Math.min(args.maxResults || DEFAULT_SEARCH_RESULTS, MAX_SEARCH_RESULTS);
|
|
2179
|
+
const offset = args.offset || 0;
|
|
2180
|
+
const contextChars = args.contextChars || 0;
|
|
2181
|
+
if (!pattern || pattern.trim() === "") {
|
|
2182
|
+
throw new Error("Pattern cannot be empty");
|
|
1325
2183
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
2184
|
+
if (offset < 0 || !Number.isInteger(offset)) {
|
|
2185
|
+
throw new Error("Offset must be a non-negative integer");
|
|
2186
|
+
}
|
|
2187
|
+
if (contextChars < 0) {
|
|
2188
|
+
throw new Error("contextChars must be non-negative");
|
|
2189
|
+
}
|
|
2190
|
+
if (!existsSync5(path)) {
|
|
2191
|
+
throw new Error(`Snapshot not found: ${path}. Run 'argus snapshot' to create one.`);
|
|
2192
|
+
}
|
|
2193
|
+
const fetchLimit = offset + maxResults + 1;
|
|
2194
|
+
if (workerAvailable) {
|
|
2195
|
+
const workerResult = await searchWithWorker(path, pattern, {
|
|
2196
|
+
caseInsensitive,
|
|
2197
|
+
maxResults: fetchLimit,
|
|
2198
|
+
offset: 0
|
|
2199
|
+
});
|
|
2200
|
+
if (workerResult) {
|
|
2201
|
+
const hasMore2 = workerResult.matches.length === fetchLimit;
|
|
2202
|
+
const pageMatches2 = workerResult.matches.slice(offset, offset + maxResults);
|
|
2203
|
+
const formattedMatches2 = pageMatches2.map((m) => {
|
|
2204
|
+
let displayLine = m.line;
|
|
2205
|
+
if (contextChars > 0 && displayLine.length > contextChars) {
|
|
2206
|
+
const matchStart = displayLine.indexOf(m.match);
|
|
2207
|
+
if (matchStart !== -1) {
|
|
2208
|
+
const matchEnd = matchStart + m.match.length;
|
|
2209
|
+
const matchCenter = Math.floor((matchStart + matchEnd) / 2);
|
|
2210
|
+
const halfContext = Math.floor(contextChars / 2);
|
|
2211
|
+
let start = Math.max(0, matchCenter - halfContext);
|
|
2212
|
+
let end = start + contextChars;
|
|
2213
|
+
if (end > displayLine.length) {
|
|
2214
|
+
end = displayLine.length;
|
|
2215
|
+
start = Math.max(0, end - contextChars);
|
|
2216
|
+
}
|
|
2217
|
+
const prefix = start > 0 ? "..." : "";
|
|
2218
|
+
const suffix = end < displayLine.length ? "..." : "";
|
|
2219
|
+
displayLine = prefix + displayLine.slice(start, end) + suffix;
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
return { lineNum: m.lineNum, line: displayLine, match: m.match };
|
|
2223
|
+
});
|
|
2224
|
+
const response2 = {
|
|
2225
|
+
count: formattedMatches2.length,
|
|
2226
|
+
matches: formattedMatches2,
|
|
2227
|
+
_source: "worker"
|
|
2228
|
+
// Debug: show source
|
|
2229
|
+
};
|
|
2230
|
+
if (offset > 0 || hasMore2) {
|
|
2231
|
+
response2.offset = offset;
|
|
2232
|
+
response2.hasMore = hasMore2;
|
|
2233
|
+
response2.totalFound = hasMore2 ? `${offset + maxResults}+` : String(offset + formattedMatches2.length);
|
|
2234
|
+
if (hasMore2) {
|
|
2235
|
+
response2.nextOffset = offset + maxResults;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
return response2;
|
|
2239
|
+
}
|
|
2240
|
+
}
|
|
2241
|
+
const allMatches = searchDocument(path, pattern, {
|
|
2242
|
+
caseInsensitive,
|
|
2243
|
+
maxResults: fetchLimit
|
|
2244
|
+
});
|
|
2245
|
+
const hasMore = allMatches.length === fetchLimit;
|
|
2246
|
+
const pageMatches = allMatches.slice(offset, offset + maxResults);
|
|
2247
|
+
const formattedMatches = pageMatches.map((m) => {
|
|
2248
|
+
let displayLine = m.line.trim();
|
|
2249
|
+
if (contextChars > 0 && displayLine.length > contextChars) {
|
|
2250
|
+
const matchStart = displayLine.indexOf(m.match);
|
|
2251
|
+
if (matchStart !== -1) {
|
|
2252
|
+
const matchEnd = matchStart + m.match.length;
|
|
2253
|
+
const matchCenter = Math.floor((matchStart + matchEnd) / 2);
|
|
2254
|
+
const halfContext = Math.floor(contextChars / 2);
|
|
2255
|
+
let start = Math.max(0, matchCenter - halfContext);
|
|
2256
|
+
let end = start + contextChars;
|
|
2257
|
+
if (end > displayLine.length) {
|
|
2258
|
+
end = displayLine.length;
|
|
2259
|
+
start = Math.max(0, end - contextChars);
|
|
2260
|
+
}
|
|
2261
|
+
const prefix = start > 0 ? "..." : "";
|
|
2262
|
+
const suffix = end < displayLine.length ? "..." : "";
|
|
2263
|
+
displayLine = prefix + displayLine.slice(start, end) + suffix;
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
return {
|
|
1330
2267
|
lineNum: m.lineNum,
|
|
1331
|
-
line:
|
|
2268
|
+
line: displayLine,
|
|
1332
2269
|
match: m.match
|
|
1333
|
-
}
|
|
2270
|
+
};
|
|
2271
|
+
});
|
|
2272
|
+
const response = {
|
|
2273
|
+
count: formattedMatches.length,
|
|
2274
|
+
matches: formattedMatches
|
|
1334
2275
|
};
|
|
2276
|
+
if (offset > 0 || hasMore) {
|
|
2277
|
+
response.offset = offset;
|
|
2278
|
+
response.hasMore = hasMore;
|
|
2279
|
+
response.totalFound = hasMore ? `${offset + maxResults}+` : String(offset + formattedMatches.length);
|
|
2280
|
+
if (hasMore) {
|
|
2281
|
+
response.nextOffset = offset + maxResults;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
return response;
|
|
2285
|
+
}
|
|
2286
|
+
case "semantic_search": {
|
|
2287
|
+
const projectPath = resolve(args.path);
|
|
2288
|
+
const query = args.query;
|
|
2289
|
+
const limit = args.limit || 20;
|
|
2290
|
+
if (!query || query.trim() === "") {
|
|
2291
|
+
throw new Error("Query cannot be empty");
|
|
2292
|
+
}
|
|
2293
|
+
const snapshotPath = join4(projectPath, ".argus", "snapshot.txt");
|
|
2294
|
+
const indexPath = join4(projectPath, ".argus", "search.db");
|
|
2295
|
+
if (!existsSync5(snapshotPath)) {
|
|
2296
|
+
throw new Error(`Snapshot not found: ${snapshotPath}. Run 'argus snapshot' first.`);
|
|
2297
|
+
}
|
|
2298
|
+
const { SemanticIndex: SemanticIndex2 } = await Promise.resolve().then(() => (init_semantic_search(), semantic_search_exports));
|
|
2299
|
+
const index = new SemanticIndex2(indexPath);
|
|
2300
|
+
try {
|
|
2301
|
+
const stats = index.getStats();
|
|
2302
|
+
const snapshotMtime = statSync2(snapshotPath).mtimeMs;
|
|
2303
|
+
const needsReindex = !stats.lastIndexed || new Date(stats.lastIndexed).getTime() < snapshotMtime || stats.snapshotPath !== snapshotPath;
|
|
2304
|
+
if (needsReindex) {
|
|
2305
|
+
index.indexFromSnapshot(snapshotPath);
|
|
2306
|
+
}
|
|
2307
|
+
const results = index.search(query, limit);
|
|
2308
|
+
return {
|
|
2309
|
+
query,
|
|
2310
|
+
count: results.length,
|
|
2311
|
+
results: results.map((r) => ({
|
|
2312
|
+
file: r.file,
|
|
2313
|
+
symbol: r.symbol,
|
|
2314
|
+
type: r.type,
|
|
2315
|
+
snippet: r.content.split("\n").slice(0, 5).join("\n")
|
|
2316
|
+
}))
|
|
2317
|
+
};
|
|
2318
|
+
} finally {
|
|
2319
|
+
index.close();
|
|
2320
|
+
}
|
|
1335
2321
|
}
|
|
1336
2322
|
case "create_snapshot": {
|
|
1337
2323
|
const path = resolve(args.path);
|
|
1338
|
-
const outputPath = args.outputPath ? resolve(args.outputPath) :
|
|
2324
|
+
const outputPath = args.outputPath ? resolve(args.outputPath) : join4(tmpdir(), `argus-snapshot-${Date.now()}.txt`);
|
|
1339
2325
|
const extensions = args.extensions || config.defaults.snapshotExtensions;
|
|
1340
|
-
if (!
|
|
2326
|
+
if (!existsSync5(path)) {
|
|
1341
2327
|
throw new Error(`Path not found: ${path}`);
|
|
1342
2328
|
}
|
|
1343
|
-
const result =
|
|
2329
|
+
const result = createEnhancedSnapshot(path, outputPath, {
|
|
1344
2330
|
extensions,
|
|
1345
2331
|
excludePatterns: config.defaults.excludePatterns
|
|
1346
2332
|
});
|
|
@@ -1348,7 +2334,66 @@ async function handleToolCall(name, args) {
|
|
|
1348
2334
|
outputPath: result.outputPath,
|
|
1349
2335
|
fileCount: result.fileCount,
|
|
1350
2336
|
totalLines: result.totalLines,
|
|
1351
|
-
totalSize: result.totalSize
|
|
2337
|
+
totalSize: result.totalSize,
|
|
2338
|
+
enhanced: true,
|
|
2339
|
+
metadata: "metadata" in result ? {
|
|
2340
|
+
imports: result.metadata.imports.length,
|
|
2341
|
+
exports: result.metadata.exports.length,
|
|
2342
|
+
symbols: Object.keys(result.metadata.symbolIndex).length
|
|
2343
|
+
} : void 0
|
|
2344
|
+
};
|
|
2345
|
+
}
|
|
2346
|
+
case "__ARGUS_GUIDE": {
|
|
2347
|
+
return {
|
|
2348
|
+
message: "This is a documentation tool. Read the description for Argus usage patterns.",
|
|
2349
|
+
tools: TOOLS.map((t) => ({ name: t.name, purpose: t.description.split("\n")[0] })),
|
|
2350
|
+
recommendation: "Start with search_codebase for most queries. Use analyze_codebase only for complex architecture questions."
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
case "get_context": {
|
|
2354
|
+
const snapshotPath = resolve(args.path);
|
|
2355
|
+
const targetFile = args.file;
|
|
2356
|
+
const targetLine = args.line;
|
|
2357
|
+
const beforeLines = args.before || 10;
|
|
2358
|
+
const afterLines = args.after || 10;
|
|
2359
|
+
if (!existsSync5(snapshotPath)) {
|
|
2360
|
+
throw new Error(`Snapshot not found: ${snapshotPath}`);
|
|
2361
|
+
}
|
|
2362
|
+
const content = readFileSync6(snapshotPath, "utf-8");
|
|
2363
|
+
const normalizedTarget = targetFile.replace(/^\.\//, "");
|
|
2364
|
+
const fileMarkerVariants = [
|
|
2365
|
+
`FILE: ./${normalizedTarget}`,
|
|
2366
|
+
`FILE: ${normalizedTarget}`
|
|
2367
|
+
];
|
|
2368
|
+
let fileStart = -1;
|
|
2369
|
+
for (const marker of fileMarkerVariants) {
|
|
2370
|
+
fileStart = content.indexOf(marker);
|
|
2371
|
+
if (fileStart !== -1) break;
|
|
2372
|
+
}
|
|
2373
|
+
if (fileStart === -1) {
|
|
2374
|
+
throw new Error(`File not found in snapshot: ${targetFile}`);
|
|
2375
|
+
}
|
|
2376
|
+
const nextFileStart = content.indexOf("\nFILE:", fileStart + 1);
|
|
2377
|
+
const metadataStart = content.indexOf("\nMETADATA:", fileStart);
|
|
2378
|
+
const fileEnd = Math.min(
|
|
2379
|
+
nextFileStart === -1 ? Infinity : nextFileStart,
|
|
2380
|
+
metadataStart === -1 ? Infinity : metadataStart
|
|
2381
|
+
);
|
|
2382
|
+
const fileContent = content.slice(fileStart, fileEnd === Infinity ? void 0 : fileEnd);
|
|
2383
|
+
const fileLines = fileContent.split("\n").slice(2);
|
|
2384
|
+
const startLine = Math.max(0, targetLine - beforeLines - 1);
|
|
2385
|
+
const endLine = Math.min(fileLines.length, targetLine + afterLines);
|
|
2386
|
+
const contextLines = fileLines.slice(startLine, endLine).map((line, idx) => {
|
|
2387
|
+
const lineNum = startLine + idx + 1;
|
|
2388
|
+
const marker = lineNum === targetLine ? ">>>" : " ";
|
|
2389
|
+
return `${marker} ${lineNum.toString().padStart(4)}: ${line}`;
|
|
2390
|
+
});
|
|
2391
|
+
return {
|
|
2392
|
+
file: targetFile,
|
|
2393
|
+
targetLine,
|
|
2394
|
+
range: { start: startLine + 1, end: endLine },
|
|
2395
|
+
content: contextLines.join("\n"),
|
|
2396
|
+
totalLines: fileLines.length
|
|
1352
2397
|
};
|
|
1353
2398
|
}
|
|
1354
2399
|
default:
|