archtracker-mcp 0.3.1 → 0.4.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 +8 -4
- package/dist/cli/index.js +127 -144
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +5 -10
- package/dist/index.js +59 -128
- package/dist/index.js.map +1 -1
- package/dist/mcp/index.js +115 -140
- package/dist/mcp/index.js.map +1 -1
- package/package.json +2 -5
- package/scripts/postinstall.cjs +0 -45
- package/scripts/screenshot.mjs +0 -121
package/dist/mcp/index.js
CHANGED
|
@@ -4,125 +4,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { z as z2 } from "zod";
|
|
5
5
|
|
|
6
6
|
// src/analyzer/analyze.ts
|
|
7
|
-
import { resolve as
|
|
8
|
-
|
|
9
|
-
// src/analyzer/engines/dependency-cruiser.ts
|
|
10
|
-
import { resolve } from "path";
|
|
11
|
-
import { cruise } from "dependency-cruiser";
|
|
12
|
-
var DEFAULT_EXCLUDE = [
|
|
13
|
-
"node_modules",
|
|
14
|
-
"\\.d\\.ts$",
|
|
15
|
-
"dist",
|
|
16
|
-
"build",
|
|
17
|
-
"coverage",
|
|
18
|
-
"\\.archtracker"
|
|
19
|
-
];
|
|
20
|
-
var DependencyCruiserEngine = class {
|
|
21
|
-
async analyze(rootDir, options = {}) {
|
|
22
|
-
const {
|
|
23
|
-
exclude = [],
|
|
24
|
-
maxDepth = 0,
|
|
25
|
-
tsConfigPath,
|
|
26
|
-
includeTypeOnly = true
|
|
27
|
-
} = options;
|
|
28
|
-
const absRootDir = resolve(rootDir);
|
|
29
|
-
const allExclude = [...DEFAULT_EXCLUDE, ...exclude];
|
|
30
|
-
const excludePattern = allExclude.join("|");
|
|
31
|
-
const cruiseOptions = {
|
|
32
|
-
baseDir: absRootDir,
|
|
33
|
-
exclude: { path: excludePattern },
|
|
34
|
-
doNotFollow: { path: "node_modules" },
|
|
35
|
-
maxDepth,
|
|
36
|
-
tsPreCompilationDeps: includeTypeOnly ? true : false,
|
|
37
|
-
combinedDependencies: false
|
|
38
|
-
};
|
|
39
|
-
if (tsConfigPath) {
|
|
40
|
-
cruiseOptions.tsConfig = { fileName: tsConfigPath };
|
|
41
|
-
}
|
|
42
|
-
let result;
|
|
43
|
-
try {
|
|
44
|
-
result = await cruise(["."], cruiseOptions);
|
|
45
|
-
} catch (error) {
|
|
46
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
47
|
-
throw new Error(`dependency-cruiser failed: ${message}`, {
|
|
48
|
-
cause: error
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
if (result.exitCode !== 0 && !result.output) {
|
|
52
|
-
throw new Error(`Analysis exited with code ${result.exitCode}`);
|
|
53
|
-
}
|
|
54
|
-
const cruiseResult = result.output;
|
|
55
|
-
return this.buildGraph(absRootDir, cruiseResult);
|
|
56
|
-
}
|
|
57
|
-
buildGraph(rootDir, cruiseResult) {
|
|
58
|
-
const files = {};
|
|
59
|
-
const edges = [];
|
|
60
|
-
const circularSet = /* @__PURE__ */ new Set();
|
|
61
|
-
const circularDependencies = [];
|
|
62
|
-
for (const mod of cruiseResult.modules) {
|
|
63
|
-
if (this.isExternalModule(mod)) continue;
|
|
64
|
-
files[mod.source] = {
|
|
65
|
-
path: mod.source,
|
|
66
|
-
exists: !mod.couldNotResolve,
|
|
67
|
-
dependencies: [],
|
|
68
|
-
dependents: []
|
|
69
|
-
};
|
|
70
|
-
}
|
|
71
|
-
for (const mod of cruiseResult.modules) {
|
|
72
|
-
for (const dep of mod.dependencies) {
|
|
73
|
-
if (dep.couldNotResolve || dep.coreModule) continue;
|
|
74
|
-
if (!files[mod.source] || this.isExternalDep(dep)) continue;
|
|
75
|
-
const edgeType = dep.typeOnly ? "type-only" : dep.dynamic ? "dynamic" : "static";
|
|
76
|
-
edges.push({ source: mod.source, target: dep.resolved, type: edgeType });
|
|
77
|
-
if (files[mod.source]) {
|
|
78
|
-
files[mod.source].dependencies.push(dep.resolved);
|
|
79
|
-
}
|
|
80
|
-
if (files[dep.resolved]) {
|
|
81
|
-
files[dep.resolved].dependents.push(mod.source);
|
|
82
|
-
}
|
|
83
|
-
if (dep.circular && dep.cycle) {
|
|
84
|
-
const cyclePath = dep.cycle.map((c) => c.name);
|
|
85
|
-
const cycleKey = [...cyclePath].sort().join("\u2192");
|
|
86
|
-
if (!circularSet.has(cycleKey)) {
|
|
87
|
-
circularSet.add(cycleKey);
|
|
88
|
-
circularDependencies.push({ cycle: cyclePath });
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return {
|
|
94
|
-
rootDir,
|
|
95
|
-
files,
|
|
96
|
-
edges,
|
|
97
|
-
circularDependencies,
|
|
98
|
-
totalFiles: Object.keys(files).length,
|
|
99
|
-
totalEdges: edges.length
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
isExternalModule(mod) {
|
|
103
|
-
if (mod.coreModule) return true;
|
|
104
|
-
const depTypes = mod.dependencyTypes ?? [];
|
|
105
|
-
if (depTypes.some((t2) => t2.startsWith("npm") || t2 === "core")) return true;
|
|
106
|
-
return isExternalPath(mod.source);
|
|
107
|
-
}
|
|
108
|
-
isExternalDep(dep) {
|
|
109
|
-
if (dep.coreModule) return true;
|
|
110
|
-
if (dep.dependencyTypes.some((t2) => t2.startsWith("npm") || t2 === "core"))
|
|
111
|
-
return true;
|
|
112
|
-
return isExternalPath(dep.resolved);
|
|
113
|
-
}
|
|
114
|
-
};
|
|
115
|
-
function isExternalPath(source) {
|
|
116
|
-
if (source.startsWith("@")) return true;
|
|
117
|
-
if (!source.includes("/") && !source.includes("\\") && !source.includes("."))
|
|
118
|
-
return true;
|
|
119
|
-
if (source.startsWith("node:")) return true;
|
|
120
|
-
return false;
|
|
121
|
-
}
|
|
7
|
+
import { resolve as resolve3 } from "path";
|
|
8
|
+
import { stat as stat3 } from "fs/promises";
|
|
122
9
|
|
|
123
10
|
// src/analyzer/engines/regex-engine.ts
|
|
124
11
|
import { readdir, readFile } from "fs/promises";
|
|
125
|
-
import { join, relative, resolve
|
|
12
|
+
import { join, relative, resolve } from "path";
|
|
126
13
|
|
|
127
14
|
// src/analyzer/engines/cycle.ts
|
|
128
15
|
function detectCycles(edges) {
|
|
@@ -680,7 +567,7 @@ var RegexEngine = class {
|
|
|
680
567
|
this.config = config;
|
|
681
568
|
}
|
|
682
569
|
async analyze(rootDir, options = {}) {
|
|
683
|
-
const absRootDir =
|
|
570
|
+
const absRootDir = resolve(rootDir);
|
|
684
571
|
const excludePatterns = [
|
|
685
572
|
...this.config.defaultExclude ?? [],
|
|
686
573
|
...options.exclude ?? [],
|
|
@@ -927,7 +814,26 @@ async function scanExtensions(dir, counts, maxDepth, currentDepth) {
|
|
|
927
814
|
|
|
928
815
|
// src/analyzer/engines/languages.ts
|
|
929
816
|
import { readFileSync } from "fs";
|
|
930
|
-
import { join as join3, dirname, resolve as
|
|
817
|
+
import { join as join3, dirname, resolve as resolve2 } from "path";
|
|
818
|
+
|
|
819
|
+
// src/analyzer/engines/types.ts
|
|
820
|
+
var LANGUAGE_IDS = [
|
|
821
|
+
"javascript",
|
|
822
|
+
"python",
|
|
823
|
+
"rust",
|
|
824
|
+
"go",
|
|
825
|
+
"java",
|
|
826
|
+
"c-cpp",
|
|
827
|
+
"c-sharp",
|
|
828
|
+
"ruby",
|
|
829
|
+
"php",
|
|
830
|
+
"swift",
|
|
831
|
+
"kotlin",
|
|
832
|
+
"dart",
|
|
833
|
+
"scala"
|
|
834
|
+
];
|
|
835
|
+
|
|
836
|
+
// src/analyzer/engines/languages.ts
|
|
931
837
|
var python = {
|
|
932
838
|
id: "python",
|
|
933
839
|
extensions: [".py"],
|
|
@@ -1191,7 +1097,7 @@ var cCpp = {
|
|
|
1191
1097
|
{ regex: /^#include\s+"([^"]+)"/gm }
|
|
1192
1098
|
],
|
|
1193
1099
|
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1194
|
-
const fromSource =
|
|
1100
|
+
const fromSource = resolve2(dirname(sourceFile), importPath);
|
|
1195
1101
|
if (projectFiles.has(fromSource)) return fromSource;
|
|
1196
1102
|
const fromRoot = join3(rootDir, importPath);
|
|
1197
1103
|
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
@@ -1215,7 +1121,7 @@ var ruby = {
|
|
|
1215
1121
|
],
|
|
1216
1122
|
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1217
1123
|
const withExt = importPath.endsWith(".rb") ? importPath : importPath + ".rb";
|
|
1218
|
-
const fromSource =
|
|
1124
|
+
const fromSource = resolve2(dirname(sourceFile), withExt);
|
|
1219
1125
|
if (projectFiles.has(fromSource)) return fromSource;
|
|
1220
1126
|
const fromRoot = join3(rootDir, withExt);
|
|
1221
1127
|
if (projectFiles.has(fromRoot)) return fromRoot;
|
|
@@ -1238,7 +1144,7 @@ var php = {
|
|
|
1238
1144
|
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1239
1145
|
if (importPath.includes("/") || importPath.endsWith(".php")) {
|
|
1240
1146
|
const withExt = importPath.endsWith(".php") ? importPath : importPath + ".php";
|
|
1241
|
-
const fromSource =
|
|
1147
|
+
const fromSource = resolve2(dirname(sourceFile), withExt);
|
|
1242
1148
|
if (projectFiles.has(fromSource)) return fromSource;
|
|
1243
1149
|
const fromRoot2 = join3(rootDir, withExt);
|
|
1244
1150
|
if (projectFiles.has(fromRoot2)) return fromRoot2;
|
|
@@ -1358,7 +1264,7 @@ var dart = {
|
|
|
1358
1264
|
if (projectFiles.has(full)) return full;
|
|
1359
1265
|
return null;
|
|
1360
1266
|
}
|
|
1361
|
-
const resolved =
|
|
1267
|
+
const resolved = resolve2(dirname(sourceFile), importPath);
|
|
1362
1268
|
if (projectFiles.has(resolved)) return resolved;
|
|
1363
1269
|
return null;
|
|
1364
1270
|
},
|
|
@@ -1422,9 +1328,47 @@ var scala = {
|
|
|
1422
1328
|
},
|
|
1423
1329
|
defaultExclude: ["target", "\\.bsp", "\\.metals", "\\.bloop"]
|
|
1424
1330
|
};
|
|
1331
|
+
var javascript = {
|
|
1332
|
+
id: "javascript",
|
|
1333
|
+
extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"],
|
|
1334
|
+
commentStyle: "c-style",
|
|
1335
|
+
importPatterns: [
|
|
1336
|
+
// ES6: import [type] [stuff from] "path"
|
|
1337
|
+
{ regex: /import\s+(?:type\s+)?(?:[\w*{}\s,]+\s+from\s+)?["']([^"']+)["']/g },
|
|
1338
|
+
// Dynamic: import("path")
|
|
1339
|
+
{ regex: /\bimport\s*\(\s*["']([^"']+)["']\s*\)/g },
|
|
1340
|
+
// Re-export: export [type] { stuff } from "path" / export * from "path"
|
|
1341
|
+
{ regex: /export\s+(?:type\s+)?(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?)\s+from\s+["']([^"']+)["']/g },
|
|
1342
|
+
// CommonJS: require("path")
|
|
1343
|
+
{ regex: /\brequire\s*\(\s*["']([^"']+)["']\s*\)/g }
|
|
1344
|
+
],
|
|
1345
|
+
resolveImport(importPath, sourceFile, rootDir, projectFiles) {
|
|
1346
|
+
if (importPath.startsWith("node:")) return null;
|
|
1347
|
+
if (!importPath.startsWith(".")) return null;
|
|
1348
|
+
const resolved = resolve2(dirname(sourceFile), importPath);
|
|
1349
|
+
if (projectFiles.has(resolved)) return resolved;
|
|
1350
|
+
if (resolved.endsWith(".js")) {
|
|
1351
|
+
const tsPath = resolved.slice(0, -3) + ".ts";
|
|
1352
|
+
if (projectFiles.has(tsPath)) return tsPath;
|
|
1353
|
+
const tsxPath = resolved.slice(0, -3) + ".tsx";
|
|
1354
|
+
if (projectFiles.has(tsxPath)) return tsxPath;
|
|
1355
|
+
}
|
|
1356
|
+
if (resolved.endsWith(".jsx")) {
|
|
1357
|
+
const tsxPath = resolved.slice(0, -4) + ".tsx";
|
|
1358
|
+
if (projectFiles.has(tsxPath)) return tsxPath;
|
|
1359
|
+
}
|
|
1360
|
+
for (const ext of [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"]) {
|
|
1361
|
+
if (projectFiles.has(resolved + ext)) return resolved + ext;
|
|
1362
|
+
}
|
|
1363
|
+
for (const idx of ["/index.ts", "/index.tsx", "/index.js", "/index.jsx"]) {
|
|
1364
|
+
if (projectFiles.has(resolved + idx)) return resolved + idx;
|
|
1365
|
+
}
|
|
1366
|
+
return null;
|
|
1367
|
+
},
|
|
1368
|
+
defaultExclude: ["node_modules", "\\.d\\.ts$", "dist", "build", "coverage"]
|
|
1369
|
+
};
|
|
1425
1370
|
var LANGUAGE_CONFIGS = {
|
|
1426
|
-
javascript
|
|
1427
|
-
// handled by DependencyCruiserEngine
|
|
1371
|
+
javascript,
|
|
1428
1372
|
python,
|
|
1429
1373
|
rust,
|
|
1430
1374
|
go,
|
|
@@ -1444,12 +1388,18 @@ function getLanguageConfig(id) {
|
|
|
1444
1388
|
|
|
1445
1389
|
// src/analyzer/analyze.ts
|
|
1446
1390
|
async function analyzeProject(rootDir, options = {}) {
|
|
1447
|
-
const absRootDir =
|
|
1448
|
-
const language = options.language ?? await detectLanguage(absRootDir);
|
|
1391
|
+
const absRootDir = resolve3(rootDir);
|
|
1449
1392
|
try {
|
|
1450
|
-
|
|
1451
|
-
|
|
1393
|
+
const s = await stat3(absRootDir);
|
|
1394
|
+
if (!s.isDirectory()) {
|
|
1395
|
+
throw new AnalyzerError(`Not a directory: ${absRootDir}`);
|
|
1452
1396
|
}
|
|
1397
|
+
} catch (error) {
|
|
1398
|
+
if (error instanceof AnalyzerError) throw error;
|
|
1399
|
+
throw new AnalyzerError(`Directory not found: ${absRootDir}`, { cause: error });
|
|
1400
|
+
}
|
|
1401
|
+
const language = options.language ?? await detectLanguage(absRootDir);
|
|
1402
|
+
try {
|
|
1453
1403
|
const config = getLanguageConfig(language);
|
|
1454
1404
|
if (!config) {
|
|
1455
1405
|
throw new AnalyzerError(`No analyzer config for language: ${language}`);
|
|
@@ -1964,10 +1914,10 @@ function arraysEqual(a, b) {
|
|
|
1964
1914
|
}
|
|
1965
1915
|
|
|
1966
1916
|
// src/utils/path-guard.ts
|
|
1967
|
-
import { resolve as
|
|
1917
|
+
import { resolve as resolve4 } from "path";
|
|
1968
1918
|
function validatePath(inputPath, boundary) {
|
|
1969
|
-
const resolved =
|
|
1970
|
-
const root = boundary ?
|
|
1919
|
+
const resolved = resolve4(inputPath);
|
|
1920
|
+
const root = boundary ? resolve4(boundary) : process.cwd();
|
|
1971
1921
|
if (!resolved.startsWith(root)) {
|
|
1972
1922
|
throw new PathTraversalError(
|
|
1973
1923
|
t("pathGuard.traversal", { input: inputPath, resolved, boundary: root })
|
|
@@ -1982,19 +1932,44 @@ var PathTraversalError = class extends Error {
|
|
|
1982
1932
|
}
|
|
1983
1933
|
};
|
|
1984
1934
|
|
|
1935
|
+
// src/utils/version.ts
|
|
1936
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
1937
|
+
import { join as join5, dirname as dirname2 } from "path";
|
|
1938
|
+
import { fileURLToPath } from "url";
|
|
1939
|
+
function loadVersion() {
|
|
1940
|
+
let dir = dirname2(fileURLToPath(import.meta.url));
|
|
1941
|
+
for (let i = 0; i < 5; i++) {
|
|
1942
|
+
try {
|
|
1943
|
+
const pkg = JSON.parse(readFileSync2(join5(dir, "package.json"), "utf-8"));
|
|
1944
|
+
return pkg.version;
|
|
1945
|
+
} catch {
|
|
1946
|
+
dir = dirname2(dir);
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
return "0.0.0";
|
|
1950
|
+
}
|
|
1951
|
+
var VERSION = loadVersion();
|
|
1952
|
+
|
|
1985
1953
|
// src/mcp/index.ts
|
|
1986
1954
|
var server = new McpServer({
|
|
1987
1955
|
name: "archtracker",
|
|
1988
|
-
version:
|
|
1956
|
+
version: VERSION
|
|
1989
1957
|
});
|
|
1958
|
+
var languageEnum = z2.enum(LANGUAGE_IDS);
|
|
1959
|
+
var LANG_DISPLAY = {
|
|
1960
|
+
javascript: "JS/TS",
|
|
1961
|
+
"c-cpp": "C/C++",
|
|
1962
|
+
"c-sharp": "C#"
|
|
1963
|
+
};
|
|
1964
|
+
var languageList = LANGUAGE_IDS.map((id) => LANG_DISPLAY[id] ?? id.charAt(0).toUpperCase() + id.slice(1)).join(", ");
|
|
1990
1965
|
server.tool(
|
|
1991
1966
|
"generate_map",
|
|
1992
|
-
|
|
1967
|
+
`Analyze dependency graph of a directory and return file import/export structure as JSON. Supports ${languageList}.`,
|
|
1993
1968
|
{
|
|
1994
1969
|
targetDir: z2.string().default("src").describe("Target directory path (default: src)"),
|
|
1995
1970
|
exclude: z2.array(z2.string()).optional().describe("Array of regex patterns to exclude (e.g. ['test', 'mock'])"),
|
|
1996
1971
|
maxDepth: z2.number().int().min(0).optional().describe("Max analysis depth (0 = unlimited)"),
|
|
1997
|
-
language:
|
|
1972
|
+
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
1998
1973
|
},
|
|
1999
1974
|
async ({ targetDir, exclude, maxDepth, language }) => {
|
|
2000
1975
|
try {
|
|
@@ -2017,14 +1992,14 @@ server.tool(
|
|
|
2017
1992
|
);
|
|
2018
1993
|
server.tool(
|
|
2019
1994
|
"analyze_existing_architecture",
|
|
2020
|
-
|
|
1995
|
+
`Comprehensive architecture analysis for existing projects. Shows critical components, circular dependencies, orphan files, coupling hotspots, and directory breakdown. Supports ${LANGUAGE_IDS.length} languages.`,
|
|
2021
1996
|
{
|
|
2022
1997
|
targetDir: z2.string().default("src").describe("Target directory path (default: src)"),
|
|
2023
1998
|
exclude: z2.array(z2.string()).optional().describe("Array of regex patterns to exclude"),
|
|
2024
1999
|
topN: z2.number().int().min(1).max(50).optional().describe("Number of top items to show in each section (default: 10)"),
|
|
2025
2000
|
saveSnapshot: z2.boolean().optional().describe("Also save a snapshot after analysis (default: false)"),
|
|
2026
2001
|
projectRoot: z2.string().default(".").describe("Project root (needed only when saveSnapshot is true)"),
|
|
2027
|
-
language:
|
|
2002
|
+
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2028
2003
|
},
|
|
2029
2004
|
async ({ targetDir, exclude, topN, saveSnapshot: doSave, projectRoot, language }) => {
|
|
2030
2005
|
try {
|
|
@@ -2051,7 +2026,7 @@ server.tool(
|
|
|
2051
2026
|
{
|
|
2052
2027
|
targetDir: z2.string().default("src").describe("Target directory path"),
|
|
2053
2028
|
projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
2054
|
-
language:
|
|
2029
|
+
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2055
2030
|
},
|
|
2056
2031
|
async ({ targetDir, projectRoot, language }) => {
|
|
2057
2032
|
try {
|
|
@@ -2081,7 +2056,7 @@ server.tool(
|
|
|
2081
2056
|
{
|
|
2082
2057
|
targetDir: z2.string().default("src").describe("Target directory path"),
|
|
2083
2058
|
projectRoot: z2.string().default(".").describe("Project root (where .archtracker is placed)"),
|
|
2084
|
-
language:
|
|
2059
|
+
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2085
2060
|
},
|
|
2086
2061
|
async ({ targetDir, projectRoot, language }) => {
|
|
2087
2062
|
try {
|
|
@@ -2119,7 +2094,7 @@ server.tool(
|
|
|
2119
2094
|
{
|
|
2120
2095
|
targetDir: z2.string().default("src").describe("Target directory path"),
|
|
2121
2096
|
projectRoot: z2.string().default(".").describe("Project root"),
|
|
2122
|
-
language:
|
|
2097
|
+
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2123
2098
|
},
|
|
2124
2099
|
async ({ targetDir, projectRoot, language }) => {
|
|
2125
2100
|
try {
|
|
@@ -2179,7 +2154,7 @@ server.tool(
|
|
|
2179
2154
|
targetDir: z2.string().default("src").describe("Target directory path"),
|
|
2180
2155
|
projectRoot: z2.string().default(".").describe("Project root"),
|
|
2181
2156
|
limit: z2.number().int().min(1).max(50).optional().describe("Max results (default: 10)"),
|
|
2182
|
-
language:
|
|
2157
|
+
language: languageEnum.optional().describe("Target language (auto-detected if omitted)")
|
|
2183
2158
|
},
|
|
2184
2159
|
async ({ query, mode, targetDir, projectRoot, limit, language }) => {
|
|
2185
2160
|
try {
|