@sashabogi/argus-mcp 1.2.3 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 existsSync4, statSync as statSync2, mkdtempSync, unlinkSync, readFileSync as readFileSync5 } from "fs";
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: false)"
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 (!existsSync4(path)) {
2061
+ if (!existsSync5(path)) {
1456
2062
  throw new Error(`File not found: ${path}`);
1457
2063
  }
1458
- const content = readFileSync5(path, "utf-8");
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 (!existsSync4(path)) {
2095
+ if (!existsSync5(path)) {
1490
2096
  throw new Error(`File not found: ${path}`);
1491
2097
  }
1492
- const content = readFileSync5(path, "utf-8");
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 (!existsSync4(path)) {
2115
+ if (!existsSync5(path)) {
1510
2116
  throw new Error(`File not found: ${path}`);
1511
2117
  }
1512
- const content = readFileSync5(path, "utf-8");
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 (!existsSync4(path)) {
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 && existsSync4(snapshotPath)) {
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 || false;
1572
- const maxResults = args.maxResults || 50;
1573
- if (!existsSync4(path)) {
1574
- throw new Error(`File not found: ${path}`);
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
- const matches = searchDocument(path, pattern, { caseInsensitive, maxResults });
1577
- return {
1578
- count: matches.length,
1579
- matches: matches.map((m) => ({
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: m.line.trim(),
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 (!existsSync4(path)) {
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
  }