@sashabogi/argus-mcp 1.2.3 → 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 +272 -36
- 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 +812 -23
- 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";
|
|
@@ -80,10 +290,13 @@ function validateConfig(config2) {
|
|
|
80
290
|
}
|
|
81
291
|
|
|
82
292
|
// src/core/enhanced-snapshot.ts
|
|
293
|
+
init_esm_shims();
|
|
83
294
|
import { readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
|
|
84
|
-
import { join as join3, dirname, extname as extname2 } from "path";
|
|
295
|
+
import { join as join3, dirname, extname as extname2, basename } from "path";
|
|
296
|
+
import { execSync } from "child_process";
|
|
85
297
|
|
|
86
298
|
// src/core/snapshot.ts
|
|
299
|
+
init_esm_shims();
|
|
87
300
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
88
301
|
import { join as join2, relative, extname } from "path";
|
|
89
302
|
var DEFAULT_OPTIONS = {
|
|
@@ -344,6 +557,114 @@ function parseExports(content, filePath) {
|
|
|
344
557
|
}
|
|
345
558
|
return exports;
|
|
346
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
|
+
}
|
|
347
668
|
function resolveImportPath(importPath, fromFile, projectFiles) {
|
|
348
669
|
if (!importPath.startsWith(".")) return void 0;
|
|
349
670
|
const fromDir = dirname(fromFile);
|
|
@@ -413,6 +734,23 @@ function createEnhancedSnapshot(projectPath, outputPath, options = {}) {
|
|
|
413
734
|
symbolIndex[exp.symbol].push(exp.file);
|
|
414
735
|
}
|
|
415
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);
|
|
416
754
|
const metadataSection = `
|
|
417
755
|
|
|
418
756
|
================================================================================
|
|
@@ -436,6 +774,25 @@ METADATA: WHO IMPORTS WHOM
|
|
|
436
774
|
================================================================================
|
|
437
775
|
${Object.entries(exportGraph).map(([file, importers]) => `${file} is imported by:
|
|
438
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)"}` : ""}
|
|
439
796
|
`;
|
|
440
797
|
const existingContent = readFileSync3(outputPath, "utf-8");
|
|
441
798
|
writeFileSync3(outputPath, existingContent + metadataSection);
|
|
@@ -447,15 +804,20 @@ ${importers.map((i) => ` \u2190 ${i}`).join("\n")}`).join("\n\n")}
|
|
|
447
804
|
fileIndex,
|
|
448
805
|
importGraph,
|
|
449
806
|
exportGraph,
|
|
450
|
-
symbolIndex
|
|
807
|
+
symbolIndex,
|
|
808
|
+
complexityScores,
|
|
809
|
+
testFileMap,
|
|
810
|
+
recentChanges
|
|
451
811
|
}
|
|
452
812
|
};
|
|
453
813
|
}
|
|
454
814
|
|
|
455
815
|
// src/core/engine.ts
|
|
816
|
+
init_esm_shims();
|
|
456
817
|
import { readFileSync as readFileSync4 } from "fs";
|
|
457
818
|
|
|
458
819
|
// src/core/prompts.ts
|
|
820
|
+
init_esm_shims();
|
|
459
821
|
var NUCLEUS_COMMANDS = `
|
|
460
822
|
COMMANDS (output ONE per turn):
|
|
461
823
|
(grep "pattern") - Find lines matching regex
|
|
@@ -952,7 +1314,11 @@ function searchDocument(documentPath, pattern, options = {}) {
|
|
|
952
1314
|
return matches;
|
|
953
1315
|
}
|
|
954
1316
|
|
|
1317
|
+
// src/providers/index.ts
|
|
1318
|
+
init_esm_shims();
|
|
1319
|
+
|
|
955
1320
|
// src/providers/openai-compatible.ts
|
|
1321
|
+
init_esm_shims();
|
|
956
1322
|
var OpenAICompatibleProvider = class {
|
|
957
1323
|
name;
|
|
958
1324
|
config;
|
|
@@ -1036,6 +1402,7 @@ function createDeepSeekProvider(config2) {
|
|
|
1036
1402
|
}
|
|
1037
1403
|
|
|
1038
1404
|
// src/providers/ollama.ts
|
|
1405
|
+
init_esm_shims();
|
|
1039
1406
|
var OllamaProvider = class {
|
|
1040
1407
|
name = "Ollama";
|
|
1041
1408
|
config;
|
|
@@ -1114,6 +1481,7 @@ function createOllamaProvider(config2) {
|
|
|
1114
1481
|
}
|
|
1115
1482
|
|
|
1116
1483
|
// src/providers/anthropic.ts
|
|
1484
|
+
init_esm_shims();
|
|
1117
1485
|
var AnthropicProvider = class {
|
|
1118
1486
|
name = "Anthropic";
|
|
1119
1487
|
config;
|
|
@@ -1209,10 +1577,150 @@ function createProviderByType(type, config2) {
|
|
|
1209
1577
|
}
|
|
1210
1578
|
|
|
1211
1579
|
// src/mcp.ts
|
|
1212
|
-
import { existsSync as
|
|
1580
|
+
import { existsSync as existsSync5, statSync as statSync2, mkdtempSync, unlinkSync, readFileSync as readFileSync6 } from "fs";
|
|
1213
1581
|
import { tmpdir } from "os";
|
|
1214
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
|
+
});
|
|
1215
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
|
+
},
|
|
1216
1724
|
{
|
|
1217
1725
|
name: "find_importers",
|
|
1218
1726
|
description: `Find all files that import a given file or module. Zero AI cost.
|
|
@@ -1349,11 +1857,19 @@ Returns matching lines with line numbers - much faster than grep across many fil
|
|
|
1349
1857
|
},
|
|
1350
1858
|
caseInsensitive: {
|
|
1351
1859
|
type: "boolean",
|
|
1352
|
-
description: "Whether to ignore case (default:
|
|
1860
|
+
description: "Whether to ignore case (default: true)"
|
|
1353
1861
|
},
|
|
1354
1862
|
maxResults: {
|
|
1355
1863
|
type: "number",
|
|
1356
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)"
|
|
1357
1873
|
}
|
|
1358
1874
|
},
|
|
1359
1875
|
required: ["path", "pattern"]
|
|
@@ -1390,6 +1906,38 @@ Run this when:
|
|
|
1390
1906
|
},
|
|
1391
1907
|
required: ["path"]
|
|
1392
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
|
+
}
|
|
1393
1941
|
}
|
|
1394
1942
|
];
|
|
1395
1943
|
var config;
|
|
@@ -1447,15 +1995,73 @@ function parseSnapshotMetadata(content) {
|
|
|
1447
1995
|
}
|
|
1448
1996
|
return { importGraph, exportGraph, symbolIndex, exports };
|
|
1449
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
|
+
}
|
|
1450
2018
|
async function handleToolCall(name, args) {
|
|
1451
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
|
+
}
|
|
1452
2058
|
case "find_importers": {
|
|
1453
2059
|
const path = resolve(args.path);
|
|
1454
2060
|
const target = args.target;
|
|
1455
|
-
if (!
|
|
2061
|
+
if (!existsSync5(path)) {
|
|
1456
2062
|
throw new Error(`File not found: ${path}`);
|
|
1457
2063
|
}
|
|
1458
|
-
const content =
|
|
2064
|
+
const content = readFileSync6(path, "utf-8");
|
|
1459
2065
|
const metadata = parseSnapshotMetadata(content);
|
|
1460
2066
|
if (!metadata) {
|
|
1461
2067
|
throw new Error("This snapshot does not have metadata. Create with: argus snapshot --enhanced");
|
|
@@ -1486,10 +2092,10 @@ async function handleToolCall(name, args) {
|
|
|
1486
2092
|
case "find_symbol": {
|
|
1487
2093
|
const path = resolve(args.path);
|
|
1488
2094
|
const symbol = args.symbol;
|
|
1489
|
-
if (!
|
|
2095
|
+
if (!existsSync5(path)) {
|
|
1490
2096
|
throw new Error(`File not found: ${path}`);
|
|
1491
2097
|
}
|
|
1492
|
-
const content =
|
|
2098
|
+
const content = readFileSync6(path, "utf-8");
|
|
1493
2099
|
const metadata = parseSnapshotMetadata(content);
|
|
1494
2100
|
if (!metadata) {
|
|
1495
2101
|
throw new Error("This snapshot does not have metadata. Create with: argus snapshot --enhanced");
|
|
@@ -1506,10 +2112,10 @@ async function handleToolCall(name, args) {
|
|
|
1506
2112
|
case "get_file_deps": {
|
|
1507
2113
|
const path = resolve(args.path);
|
|
1508
2114
|
const file = args.file;
|
|
1509
|
-
if (!
|
|
2115
|
+
if (!existsSync5(path)) {
|
|
1510
2116
|
throw new Error(`File not found: ${path}`);
|
|
1511
2117
|
}
|
|
1512
|
-
const content =
|
|
2118
|
+
const content = readFileSync6(path, "utf-8");
|
|
1513
2119
|
const metadata = parseSnapshotMetadata(content);
|
|
1514
2120
|
if (!metadata) {
|
|
1515
2121
|
throw new Error("This snapshot does not have metadata. Create with: argus snapshot --enhanced");
|
|
@@ -1536,7 +2142,7 @@ async function handleToolCall(name, args) {
|
|
|
1536
2142
|
const path = resolve(args.path);
|
|
1537
2143
|
const query = args.query;
|
|
1538
2144
|
const maxTurns = args.maxTurns || 15;
|
|
1539
|
-
if (!
|
|
2145
|
+
if (!existsSync5(path)) {
|
|
1540
2146
|
throw new Error(`Path not found: ${path}`);
|
|
1541
2147
|
}
|
|
1542
2148
|
let snapshotPath = path;
|
|
@@ -1560,7 +2166,7 @@ async function handleToolCall(name, args) {
|
|
|
1560
2166
|
commands: result.commands
|
|
1561
2167
|
};
|
|
1562
2168
|
} finally {
|
|
1563
|
-
if (tempSnapshot &&
|
|
2169
|
+
if (tempSnapshot && existsSync5(snapshotPath)) {
|
|
1564
2170
|
unlinkSync(snapshotPath);
|
|
1565
2171
|
}
|
|
1566
2172
|
}
|
|
@@ -1568,26 +2174,156 @@ async function handleToolCall(name, args) {
|
|
|
1568
2174
|
case "search_codebase": {
|
|
1569
2175
|
const path = resolve(args.path);
|
|
1570
2176
|
const pattern = args.pattern;
|
|
1571
|
-
const caseInsensitive = args.caseInsensitive
|
|
1572
|
-
const maxResults = args.maxResults ||
|
|
1573
|
-
|
|
1574
|
-
|
|
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");
|
|
1575
2183
|
}
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
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 {
|
|
1580
2267
|
lineNum: m.lineNum,
|
|
1581
|
-
line:
|
|
2268
|
+
line: displayLine,
|
|
1582
2269
|
match: m.match
|
|
1583
|
-
}
|
|
2270
|
+
};
|
|
2271
|
+
});
|
|
2272
|
+
const response = {
|
|
2273
|
+
count: formattedMatches.length,
|
|
2274
|
+
matches: formattedMatches
|
|
1584
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
|
+
}
|
|
1585
2321
|
}
|
|
1586
2322
|
case "create_snapshot": {
|
|
1587
2323
|
const path = resolve(args.path);
|
|
1588
2324
|
const outputPath = args.outputPath ? resolve(args.outputPath) : join4(tmpdir(), `argus-snapshot-${Date.now()}.txt`);
|
|
1589
2325
|
const extensions = args.extensions || config.defaults.snapshotExtensions;
|
|
1590
|
-
if (!
|
|
2326
|
+
if (!existsSync5(path)) {
|
|
1591
2327
|
throw new Error(`Path not found: ${path}`);
|
|
1592
2328
|
}
|
|
1593
2329
|
const result = createEnhancedSnapshot(path, outputPath, {
|
|
@@ -1607,6 +2343,59 @@ async function handleToolCall(name, args) {
|
|
|
1607
2343
|
} : void 0
|
|
1608
2344
|
};
|
|
1609
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
|
|
2397
|
+
};
|
|
2398
|
+
}
|
|
1610
2399
|
default:
|
|
1611
2400
|
throw new Error(`Unknown tool: ${name}`);
|
|
1612
2401
|
}
|