repowisestage 0.0.46 → 0.0.48
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/bin/repowise.js +491 -291
- package/package.json +1 -1
package/dist/bin/repowise.js
CHANGED
|
@@ -721,6 +721,200 @@ var init_registry = __esm({
|
|
|
721
721
|
}
|
|
722
722
|
});
|
|
723
723
|
|
|
724
|
+
// ../listener/dist/lsp/installer.js
|
|
725
|
+
import { promises as fs } from "fs";
|
|
726
|
+
import { join as join14, dirname as dirname5 } from "path";
|
|
727
|
+
import { spawn as spawn3 } from "child_process";
|
|
728
|
+
function getLspInstallDir() {
|
|
729
|
+
return join14(getConfigDir(), "lsp-servers");
|
|
730
|
+
}
|
|
731
|
+
function getLspBinDir() {
|
|
732
|
+
return join14(getLspInstallDir(), "node_modules", ".bin");
|
|
733
|
+
}
|
|
734
|
+
async function ensureNpmLspInstalled(config2) {
|
|
735
|
+
if (!config2.npmPackage)
|
|
736
|
+
return { installed: false, skipped: "no-npm-package" };
|
|
737
|
+
const installDir = getLspInstallDir();
|
|
738
|
+
const binPath = join14(installDir, "node_modules", ".bin", config2.command);
|
|
739
|
+
if (await pathExists(binPath)) {
|
|
740
|
+
return { installed: false, skipped: "already-present" };
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
await fs.mkdir(installDir, { recursive: true });
|
|
744
|
+
const pkgJsonPath = join14(installDir, "package.json");
|
|
745
|
+
if (!await pathExists(pkgJsonPath)) {
|
|
746
|
+
await fs.writeFile(pkgJsonPath, JSON.stringify({ name: "repowise-lsp-servers", private: true, version: "0.0.0" }, null, 2), "utf-8");
|
|
747
|
+
}
|
|
748
|
+
await runNpmInstall(installDir, config2.npmPackage);
|
|
749
|
+
if (!await pathExists(binPath)) {
|
|
750
|
+
return {
|
|
751
|
+
installed: false,
|
|
752
|
+
error: `npm install completed but ${config2.command} still not at ${binPath}`
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
return { installed: true };
|
|
756
|
+
} catch (err) {
|
|
757
|
+
return {
|
|
758
|
+
installed: false,
|
|
759
|
+
error: err instanceof Error ? err.message : String(err)
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
async function resolveNpmCommand() {
|
|
764
|
+
const nodeDir = dirname5(process.execPath);
|
|
765
|
+
for (const candidate of [join14(nodeDir, "npm"), join14(nodeDir, "npm.cmd")]) {
|
|
766
|
+
try {
|
|
767
|
+
await fs.access(candidate);
|
|
768
|
+
return candidate;
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
for (const candidate of ["/opt/homebrew/bin/npm", "/usr/local/bin/npm", "/usr/bin/npm"]) {
|
|
773
|
+
try {
|
|
774
|
+
await fs.access(candidate);
|
|
775
|
+
return candidate;
|
|
776
|
+
} catch {
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
return "npm";
|
|
780
|
+
}
|
|
781
|
+
async function runNpmInstall(cwd, pkg2) {
|
|
782
|
+
const npmCmd = await resolveNpmCommand();
|
|
783
|
+
const nodeDir = dirname5(process.execPath);
|
|
784
|
+
const augmentedPath = process.env.PATH ? `${nodeDir}:${process.env.PATH}` : nodeDir;
|
|
785
|
+
return new Promise((resolve4, reject) => {
|
|
786
|
+
const child = spawn3(npmCmd, ["install", "--no-audit", "--no-fund", "--silent", pkg2], {
|
|
787
|
+
cwd,
|
|
788
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
789
|
+
env: { ...process.env, PATH: augmentedPath }
|
|
790
|
+
});
|
|
791
|
+
let stderr = "";
|
|
792
|
+
child.stderr.on("data", (chunk) => {
|
|
793
|
+
stderr += chunk.toString();
|
|
794
|
+
});
|
|
795
|
+
child.on("error", (err) => reject(err));
|
|
796
|
+
child.on("close", (code) => {
|
|
797
|
+
if (code === 0) {
|
|
798
|
+
resolve4();
|
|
799
|
+
} else {
|
|
800
|
+
reject(new Error(`npm install ${pkg2} exited ${(code ?? -1).toString()}: ${sanitizeNpmStderr(stderr)}`));
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
function sanitizeNpmStderr(raw) {
|
|
806
|
+
const truncated = raw.split("\n").slice(0, 3).join(" ").trim();
|
|
807
|
+
return truncated.replace(/(https?:\/\/)[^@\s/]*@/g, (_match, scheme) => `${scheme}<redacted>@`);
|
|
808
|
+
}
|
|
809
|
+
async function pathExists(p) {
|
|
810
|
+
try {
|
|
811
|
+
await fs.access(p);
|
|
812
|
+
return true;
|
|
813
|
+
} catch {
|
|
814
|
+
return false;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async function detectRepoLanguages(repoRoot) {
|
|
818
|
+
const found = /* @__PURE__ */ new Set();
|
|
819
|
+
let inspected = 0;
|
|
820
|
+
const MAX_INSPECT = 200;
|
|
821
|
+
async function inspect(dir) {
|
|
822
|
+
if (inspected >= MAX_INSPECT)
|
|
823
|
+
return;
|
|
824
|
+
let entries;
|
|
825
|
+
try {
|
|
826
|
+
entries = await fs.readdir(dir);
|
|
827
|
+
} catch {
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
for (const name of entries) {
|
|
831
|
+
if (inspected >= MAX_INSPECT)
|
|
832
|
+
return;
|
|
833
|
+
if (name.startsWith("."))
|
|
834
|
+
continue;
|
|
835
|
+
if (name === "node_modules" || name === "dist" || name === "build")
|
|
836
|
+
continue;
|
|
837
|
+
const lang = detectLanguage(name);
|
|
838
|
+
if (lang) {
|
|
839
|
+
found.add(lang);
|
|
840
|
+
inspected += 1;
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
await inspect(repoRoot);
|
|
845
|
+
let topEntries = [];
|
|
846
|
+
try {
|
|
847
|
+
topEntries = await fs.readdir(repoRoot);
|
|
848
|
+
} catch {
|
|
849
|
+
return found;
|
|
850
|
+
}
|
|
851
|
+
for (const name of topEntries) {
|
|
852
|
+
if (name.startsWith("."))
|
|
853
|
+
continue;
|
|
854
|
+
if (name === "node_modules" || name === "dist" || name === "build")
|
|
855
|
+
continue;
|
|
856
|
+
const sub = join14(repoRoot, name);
|
|
857
|
+
try {
|
|
858
|
+
const stat7 = await fs.stat(sub);
|
|
859
|
+
if (stat7.isDirectory())
|
|
860
|
+
await inspect(sub);
|
|
861
|
+
} catch {
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return found;
|
|
865
|
+
}
|
|
866
|
+
async function prepareLspServersForRepos(repos) {
|
|
867
|
+
const detected = /* @__PURE__ */ new Set();
|
|
868
|
+
for (const r of repos) {
|
|
869
|
+
if (!r.localPath)
|
|
870
|
+
continue;
|
|
871
|
+
try {
|
|
872
|
+
const stat7 = await fs.stat(r.localPath);
|
|
873
|
+
if (!stat7.isDirectory())
|
|
874
|
+
continue;
|
|
875
|
+
} catch {
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
try {
|
|
879
|
+
const langs = await detectRepoLanguages(r.localPath);
|
|
880
|
+
for (const l of langs)
|
|
881
|
+
detected.add(l);
|
|
882
|
+
} catch {
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
const results = [];
|
|
886
|
+
for (const language of detected) {
|
|
887
|
+
const configs = LSP_REGISTRY[language];
|
|
888
|
+
const npmConfig = configs.find((c) => c.npmPackage);
|
|
889
|
+
if (!npmConfig) {
|
|
890
|
+
results.push({
|
|
891
|
+
language,
|
|
892
|
+
installed: false,
|
|
893
|
+
alreadyPresent: false,
|
|
894
|
+
skippedNoNpmPackage: true,
|
|
895
|
+
hint: configs[0]?.installHint
|
|
896
|
+
});
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const outcome = await ensureNpmLspInstalled(npmConfig);
|
|
900
|
+
results.push({
|
|
901
|
+
language,
|
|
902
|
+
installed: outcome.installed,
|
|
903
|
+
alreadyPresent: outcome.skipped === "already-present",
|
|
904
|
+
skippedNoNpmPackage: false,
|
|
905
|
+
error: outcome.error
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
return results;
|
|
909
|
+
}
|
|
910
|
+
var init_installer = __esm({
|
|
911
|
+
"../listener/dist/lsp/installer.js"() {
|
|
912
|
+
"use strict";
|
|
913
|
+
init_config_dir();
|
|
914
|
+
init_registry();
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
|
|
724
918
|
// ../../packages/shared/src/types/typed-resolution.ts
|
|
725
919
|
function typedResolutionKey(filePath, propertyName, line, column) {
|
|
726
920
|
return `${filePath}\0${propertyName}\0${line.toString()}\0${column.toString()}`;
|
|
@@ -1122,6 +1316,7 @@ var init_sidecar_client = __esm({
|
|
|
1122
1316
|
// ../listener/dist/lsp/lsp-tools.js
|
|
1123
1317
|
var lsp_tools_exports = {};
|
|
1124
1318
|
__export(lsp_tools_exports, {
|
|
1319
|
+
emptyLspResult: () => emptyLspResult,
|
|
1125
1320
|
lspCallHierarchy: () => lspCallHierarchy,
|
|
1126
1321
|
lspDefinition: () => lspDefinition,
|
|
1127
1322
|
lspDocumentSymbol: () => lspDocumentSymbol,
|
|
@@ -1132,7 +1327,8 @@ __export(lsp_tools_exports, {
|
|
|
1132
1327
|
lspWorkspaceSymbol: () => lspWorkspaceSymbol
|
|
1133
1328
|
});
|
|
1134
1329
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1135
|
-
import { relative as pathRelative, resolve as pathResolve3 } from "path";
|
|
1330
|
+
import { relative as pathRelative, resolve as pathResolve3, join as pathJoin } from "path";
|
|
1331
|
+
import { existsSync } from "fs";
|
|
1136
1332
|
import { execSync } from "child_process";
|
|
1137
1333
|
function probeBinary(command) {
|
|
1138
1334
|
if (process.env["VITEST"])
|
|
@@ -1142,8 +1338,12 @@ function probeBinary(command) {
|
|
|
1142
1338
|
return cached;
|
|
1143
1339
|
let found = false;
|
|
1144
1340
|
try {
|
|
1145
|
-
|
|
1146
|
-
|
|
1341
|
+
if (existsSync(pathJoin(getLspBinDir(), command))) {
|
|
1342
|
+
found = true;
|
|
1343
|
+
} else {
|
|
1344
|
+
execSync(`which ${command}`, { stdio: "ignore", timeout: 200 });
|
|
1345
|
+
found = true;
|
|
1346
|
+
}
|
|
1147
1347
|
} catch {
|
|
1148
1348
|
found = false;
|
|
1149
1349
|
}
|
|
@@ -1158,6 +1358,25 @@ function pickAvailableConfig(configs, isAvailable) {
|
|
|
1158
1358
|
}
|
|
1159
1359
|
return null;
|
|
1160
1360
|
}
|
|
1361
|
+
function emptyLspResult(reason, details) {
|
|
1362
|
+
if (reason === "unsupported-language") {
|
|
1363
|
+
return {
|
|
1364
|
+
ok: false,
|
|
1365
|
+
reason,
|
|
1366
|
+
isError: true,
|
|
1367
|
+
error: "unsupported-language" + (details?.language ? `: ${details.language}` : "") + " \u2014 no LSP server registered for this file extension.",
|
|
1368
|
+
hint: "RepoWise auto-installs LSP servers for known languages (TS/JS, Python, Go, Rust, Ruby, etc.). For an unsupported language fall back to graph-only tools: `find_symbol`, `find_callers`, `find_references`, `get_call_graph`."
|
|
1369
|
+
};
|
|
1370
|
+
}
|
|
1371
|
+
const triedFrag = details?.tried && details.tried.length > 0 ? ` (tried: ${details.tried.join(", ")})` : "";
|
|
1372
|
+
return {
|
|
1373
|
+
ok: false,
|
|
1374
|
+
reason,
|
|
1375
|
+
isError: true,
|
|
1376
|
+
error: `no-server-available${triedFrag} \u2014 the LSP binary for this language isn't on PATH or in the listener's install dir.`,
|
|
1377
|
+
hint: "The listener auto-installs npm-based LSP servers (pyright, typescript-language-server, etc.) on boot. If the install failed, check listener stderr for the underlying npm error. Non-npm servers (gopls, rust-analyzer, sourcekit-lsp, ruby-lsp) need manual install \u2014 see the listener log for the per-language hint. Until then, use graph-only tools (`find_symbol`, `find_callers`, etc.) for this file."
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1161
1380
|
function toOneIndexed(pos) {
|
|
1162
1381
|
return { line: pos.line + 1, column: pos.character + 1 };
|
|
1163
1382
|
}
|
|
@@ -1220,13 +1439,13 @@ function normalizeLocations(raw, repoRoot) {
|
|
|
1220
1439
|
async function getSession(deps, file) {
|
|
1221
1440
|
const language = detectLanguage(file);
|
|
1222
1441
|
if (!language)
|
|
1223
|
-
return
|
|
1442
|
+
return emptyLspResult("unsupported-language");
|
|
1224
1443
|
const configs = LSP_REGISTRY[language];
|
|
1225
1444
|
const config2 = pickAvailableConfig(configs, deps.binaryAvailable);
|
|
1226
1445
|
if (!config2) {
|
|
1227
|
-
const tried = configs.map((c) => c.command)
|
|
1228
|
-
console.warn(`[lsp:${language}] no server available on PATH \u2014 tried: ${tried}`);
|
|
1229
|
-
return
|
|
1446
|
+
const tried = configs.map((c) => c.command);
|
|
1447
|
+
console.warn(`[lsp:${language}] no server available on PATH \u2014 tried: ${tried.join(", ")}`);
|
|
1448
|
+
return emptyLspResult("no-server-available", { language, tried });
|
|
1230
1449
|
}
|
|
1231
1450
|
const session = await deps.workspaces.getOrOpen({
|
|
1232
1451
|
repoRoot: deps.repoRoot,
|
|
@@ -1239,7 +1458,7 @@ async function getSession(deps, file) {
|
|
|
1239
1458
|
async function lspDefinition(deps, req) {
|
|
1240
1459
|
const got = await getSession(deps, req.file);
|
|
1241
1460
|
if (!got.ok)
|
|
1242
|
-
return
|
|
1461
|
+
return got;
|
|
1243
1462
|
const result = await got.session.client.request("textDocument/definition", {
|
|
1244
1463
|
textDocument: { uri: got.uri },
|
|
1245
1464
|
position: toZeroIndexed(req.position)
|
|
@@ -1249,7 +1468,7 @@ async function lspDefinition(deps, req) {
|
|
|
1249
1468
|
async function lspReferences(deps, req) {
|
|
1250
1469
|
const got = await getSession(deps, req.file);
|
|
1251
1470
|
if (!got.ok)
|
|
1252
|
-
return
|
|
1471
|
+
return got;
|
|
1253
1472
|
const result = await got.session.client.request("textDocument/references", {
|
|
1254
1473
|
textDocument: { uri: got.uri },
|
|
1255
1474
|
position: toZeroIndexed(req.position),
|
|
@@ -1260,7 +1479,7 @@ async function lspReferences(deps, req) {
|
|
|
1260
1479
|
async function lspHover(deps, req) {
|
|
1261
1480
|
const got = await getSession(deps, req.file);
|
|
1262
1481
|
if (!got.ok)
|
|
1263
|
-
return
|
|
1482
|
+
return got;
|
|
1264
1483
|
const result = await got.session.client.request("textDocument/hover", {
|
|
1265
1484
|
textDocument: { uri: got.uri },
|
|
1266
1485
|
position: toZeroIndexed(req.position)
|
|
@@ -1303,7 +1522,7 @@ function normalizeHoverContents(contents) {
|
|
|
1303
1522
|
async function lspCallHierarchy(deps, req) {
|
|
1304
1523
|
const got = await getSession(deps, req.file);
|
|
1305
1524
|
if (!got.ok)
|
|
1306
|
-
return
|
|
1525
|
+
return got;
|
|
1307
1526
|
const limit = req.limit ?? 200;
|
|
1308
1527
|
const prepared = await got.session.client.request("textDocument/prepareCallHierarchy", {
|
|
1309
1528
|
textDocument: { uri: got.uri },
|
|
@@ -1342,7 +1561,7 @@ async function lspCallHierarchy(deps, req) {
|
|
|
1342
1561
|
async function lspImplementation(deps, req) {
|
|
1343
1562
|
const got = await getSession(deps, req.file);
|
|
1344
1563
|
if (!got.ok)
|
|
1345
|
-
return
|
|
1564
|
+
return got;
|
|
1346
1565
|
const result = await got.session.client.request("textDocument/implementation", {
|
|
1347
1566
|
textDocument: { uri: got.uri },
|
|
1348
1567
|
position: toZeroIndexed(req.position)
|
|
@@ -1352,7 +1571,7 @@ async function lspImplementation(deps, req) {
|
|
|
1352
1571
|
async function lspTypeHierarchy(deps, req) {
|
|
1353
1572
|
const got = await getSession(deps, req.file);
|
|
1354
1573
|
if (!got.ok)
|
|
1355
|
-
return
|
|
1574
|
+
return got;
|
|
1356
1575
|
const limit = req.limit ?? 200;
|
|
1357
1576
|
const prepared = await got.session.client.request("textDocument/prepareTypeHierarchy", {
|
|
1358
1577
|
textDocument: { uri: got.uri },
|
|
@@ -1388,8 +1607,11 @@ async function lspTypeHierarchy(deps, req) {
|
|
|
1388
1607
|
}
|
|
1389
1608
|
async function lspWorkspaceSymbol(deps, req) {
|
|
1390
1609
|
const sessions = deps.workspaces.activeSessions().filter((s) => !req.language || s.language === req.language);
|
|
1391
|
-
if (sessions.length === 0)
|
|
1392
|
-
return
|
|
1610
|
+
if (sessions.length === 0) {
|
|
1611
|
+
return emptyLspResult("no-server-available", {
|
|
1612
|
+
language: req.language
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1393
1615
|
const symbols = [];
|
|
1394
1616
|
for (const session of sessions) {
|
|
1395
1617
|
const raw = await session.client.request("workspace/symbol", { query: req.query });
|
|
@@ -1417,7 +1639,7 @@ async function lspWorkspaceSymbol(deps, req) {
|
|
|
1417
1639
|
async function lspDocumentSymbol(deps, req) {
|
|
1418
1640
|
const got = await getSession(deps, req.file);
|
|
1419
1641
|
if (!got.ok)
|
|
1420
|
-
return
|
|
1642
|
+
return got;
|
|
1421
1643
|
const raw = await got.session.client.request("textDocument/documentSymbol", {
|
|
1422
1644
|
textDocument: { uri: got.uri }
|
|
1423
1645
|
});
|
|
@@ -1465,6 +1687,7 @@ var init_lsp_tools = __esm({
|
|
|
1465
1687
|
"../listener/dist/lsp/lsp-tools.js"() {
|
|
1466
1688
|
"use strict";
|
|
1467
1689
|
init_registry();
|
|
1690
|
+
init_installer();
|
|
1468
1691
|
binaryProbeCache = /* @__PURE__ */ new Map();
|
|
1469
1692
|
}
|
|
1470
1693
|
});
|
|
@@ -3042,196 +3265,8 @@ var LspClient = class extends EventEmitter {
|
|
|
3042
3265
|
}
|
|
3043
3266
|
};
|
|
3044
3267
|
|
|
3045
|
-
// ../listener/dist/lsp/installer.js
|
|
3046
|
-
init_config_dir();
|
|
3047
|
-
init_registry();
|
|
3048
|
-
import { promises as fs } from "fs";
|
|
3049
|
-
import { join as join14, dirname as dirname5 } from "path";
|
|
3050
|
-
import { spawn as spawn3 } from "child_process";
|
|
3051
|
-
function getLspInstallDir() {
|
|
3052
|
-
return join14(getConfigDir(), "lsp-servers");
|
|
3053
|
-
}
|
|
3054
|
-
function getLspBinDir() {
|
|
3055
|
-
return join14(getLspInstallDir(), "node_modules", ".bin");
|
|
3056
|
-
}
|
|
3057
|
-
async function ensureNpmLspInstalled(config2) {
|
|
3058
|
-
if (!config2.npmPackage)
|
|
3059
|
-
return { installed: false, skipped: "no-npm-package" };
|
|
3060
|
-
const installDir = getLspInstallDir();
|
|
3061
|
-
const binPath = join14(installDir, "node_modules", ".bin", config2.command);
|
|
3062
|
-
if (await pathExists(binPath)) {
|
|
3063
|
-
return { installed: false, skipped: "already-present" };
|
|
3064
|
-
}
|
|
3065
|
-
try {
|
|
3066
|
-
await fs.mkdir(installDir, { recursive: true });
|
|
3067
|
-
const pkgJsonPath = join14(installDir, "package.json");
|
|
3068
|
-
if (!await pathExists(pkgJsonPath)) {
|
|
3069
|
-
await fs.writeFile(pkgJsonPath, JSON.stringify({ name: "repowise-lsp-servers", private: true, version: "0.0.0" }, null, 2), "utf-8");
|
|
3070
|
-
}
|
|
3071
|
-
await runNpmInstall(installDir, config2.npmPackage);
|
|
3072
|
-
if (!await pathExists(binPath)) {
|
|
3073
|
-
return {
|
|
3074
|
-
installed: false,
|
|
3075
|
-
error: `npm install completed but ${config2.command} still not at ${binPath}`
|
|
3076
|
-
};
|
|
3077
|
-
}
|
|
3078
|
-
return { installed: true };
|
|
3079
|
-
} catch (err) {
|
|
3080
|
-
return {
|
|
3081
|
-
installed: false,
|
|
3082
|
-
error: err instanceof Error ? err.message : String(err)
|
|
3083
|
-
};
|
|
3084
|
-
}
|
|
3085
|
-
}
|
|
3086
|
-
async function resolveNpmCommand() {
|
|
3087
|
-
const nodeDir = dirname5(process.execPath);
|
|
3088
|
-
for (const candidate of [join14(nodeDir, "npm"), join14(nodeDir, "npm.cmd")]) {
|
|
3089
|
-
try {
|
|
3090
|
-
await fs.access(candidate);
|
|
3091
|
-
return candidate;
|
|
3092
|
-
} catch {
|
|
3093
|
-
}
|
|
3094
|
-
}
|
|
3095
|
-
for (const candidate of ["/opt/homebrew/bin/npm", "/usr/local/bin/npm", "/usr/bin/npm"]) {
|
|
3096
|
-
try {
|
|
3097
|
-
await fs.access(candidate);
|
|
3098
|
-
return candidate;
|
|
3099
|
-
} catch {
|
|
3100
|
-
}
|
|
3101
|
-
}
|
|
3102
|
-
return "npm";
|
|
3103
|
-
}
|
|
3104
|
-
async function runNpmInstall(cwd, pkg2) {
|
|
3105
|
-
const npmCmd = await resolveNpmCommand();
|
|
3106
|
-
const nodeDir = dirname5(process.execPath);
|
|
3107
|
-
const augmentedPath = process.env.PATH ? `${nodeDir}:${process.env.PATH}` : nodeDir;
|
|
3108
|
-
return new Promise((resolve4, reject) => {
|
|
3109
|
-
const child = spawn3(npmCmd, ["install", "--no-audit", "--no-fund", "--silent", pkg2], {
|
|
3110
|
-
cwd,
|
|
3111
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
3112
|
-
env: { ...process.env, PATH: augmentedPath }
|
|
3113
|
-
});
|
|
3114
|
-
let stderr = "";
|
|
3115
|
-
child.stderr.on("data", (chunk) => {
|
|
3116
|
-
stderr += chunk.toString();
|
|
3117
|
-
});
|
|
3118
|
-
child.on("error", (err) => reject(err));
|
|
3119
|
-
child.on("close", (code) => {
|
|
3120
|
-
if (code === 0) {
|
|
3121
|
-
resolve4();
|
|
3122
|
-
} else {
|
|
3123
|
-
reject(new Error(`npm install ${pkg2} exited ${(code ?? -1).toString()}: ${sanitizeNpmStderr(stderr)}`));
|
|
3124
|
-
}
|
|
3125
|
-
});
|
|
3126
|
-
});
|
|
3127
|
-
}
|
|
3128
|
-
function sanitizeNpmStderr(raw) {
|
|
3129
|
-
const truncated = raw.split("\n").slice(0, 3).join(" ").trim();
|
|
3130
|
-
return truncated.replace(/(https?:\/\/)[^@\s/]*@/g, (_match, scheme) => `${scheme}<redacted>@`);
|
|
3131
|
-
}
|
|
3132
|
-
async function pathExists(p) {
|
|
3133
|
-
try {
|
|
3134
|
-
await fs.access(p);
|
|
3135
|
-
return true;
|
|
3136
|
-
} catch {
|
|
3137
|
-
return false;
|
|
3138
|
-
}
|
|
3139
|
-
}
|
|
3140
|
-
async function detectRepoLanguages(repoRoot) {
|
|
3141
|
-
const found = /* @__PURE__ */ new Set();
|
|
3142
|
-
let inspected = 0;
|
|
3143
|
-
const MAX_INSPECT = 200;
|
|
3144
|
-
async function inspect(dir) {
|
|
3145
|
-
if (inspected >= MAX_INSPECT)
|
|
3146
|
-
return;
|
|
3147
|
-
let entries;
|
|
3148
|
-
try {
|
|
3149
|
-
entries = await fs.readdir(dir);
|
|
3150
|
-
} catch {
|
|
3151
|
-
return;
|
|
3152
|
-
}
|
|
3153
|
-
for (const name of entries) {
|
|
3154
|
-
if (inspected >= MAX_INSPECT)
|
|
3155
|
-
return;
|
|
3156
|
-
if (name.startsWith("."))
|
|
3157
|
-
continue;
|
|
3158
|
-
if (name === "node_modules" || name === "dist" || name === "build")
|
|
3159
|
-
continue;
|
|
3160
|
-
const lang = detectLanguage(name);
|
|
3161
|
-
if (lang) {
|
|
3162
|
-
found.add(lang);
|
|
3163
|
-
inspected += 1;
|
|
3164
|
-
}
|
|
3165
|
-
}
|
|
3166
|
-
}
|
|
3167
|
-
await inspect(repoRoot);
|
|
3168
|
-
let topEntries = [];
|
|
3169
|
-
try {
|
|
3170
|
-
topEntries = await fs.readdir(repoRoot);
|
|
3171
|
-
} catch {
|
|
3172
|
-
return found;
|
|
3173
|
-
}
|
|
3174
|
-
for (const name of topEntries) {
|
|
3175
|
-
if (name.startsWith("."))
|
|
3176
|
-
continue;
|
|
3177
|
-
if (name === "node_modules" || name === "dist" || name === "build")
|
|
3178
|
-
continue;
|
|
3179
|
-
const sub = join14(repoRoot, name);
|
|
3180
|
-
try {
|
|
3181
|
-
const stat7 = await fs.stat(sub);
|
|
3182
|
-
if (stat7.isDirectory())
|
|
3183
|
-
await inspect(sub);
|
|
3184
|
-
} catch {
|
|
3185
|
-
}
|
|
3186
|
-
}
|
|
3187
|
-
return found;
|
|
3188
|
-
}
|
|
3189
|
-
async function prepareLspServersForRepos(repos) {
|
|
3190
|
-
const detected = /* @__PURE__ */ new Set();
|
|
3191
|
-
for (const r of repos) {
|
|
3192
|
-
if (!r.localPath)
|
|
3193
|
-
continue;
|
|
3194
|
-
try {
|
|
3195
|
-
const stat7 = await fs.stat(r.localPath);
|
|
3196
|
-
if (!stat7.isDirectory())
|
|
3197
|
-
continue;
|
|
3198
|
-
} catch {
|
|
3199
|
-
continue;
|
|
3200
|
-
}
|
|
3201
|
-
try {
|
|
3202
|
-
const langs = await detectRepoLanguages(r.localPath);
|
|
3203
|
-
for (const l of langs)
|
|
3204
|
-
detected.add(l);
|
|
3205
|
-
} catch {
|
|
3206
|
-
}
|
|
3207
|
-
}
|
|
3208
|
-
const results = [];
|
|
3209
|
-
for (const language of detected) {
|
|
3210
|
-
const configs = LSP_REGISTRY[language];
|
|
3211
|
-
const npmConfig = configs.find((c) => c.npmPackage);
|
|
3212
|
-
if (!npmConfig) {
|
|
3213
|
-
results.push({
|
|
3214
|
-
language,
|
|
3215
|
-
installed: false,
|
|
3216
|
-
alreadyPresent: false,
|
|
3217
|
-
skippedNoNpmPackage: true,
|
|
3218
|
-
hint: configs[0]?.installHint
|
|
3219
|
-
});
|
|
3220
|
-
continue;
|
|
3221
|
-
}
|
|
3222
|
-
const outcome = await ensureNpmLspInstalled(npmConfig);
|
|
3223
|
-
results.push({
|
|
3224
|
-
language,
|
|
3225
|
-
installed: outcome.installed,
|
|
3226
|
-
alreadyPresent: outcome.skipped === "already-present",
|
|
3227
|
-
skippedNoNpmPackage: false,
|
|
3228
|
-
error: outcome.error
|
|
3229
|
-
});
|
|
3230
|
-
}
|
|
3231
|
-
return results;
|
|
3232
|
-
}
|
|
3233
|
-
|
|
3234
3268
|
// ../listener/dist/lsp/workspace-session.js
|
|
3269
|
+
init_installer();
|
|
3235
3270
|
function keyOf(args) {
|
|
3236
3271
|
return `${args.repoRoot}\0${args.language}`;
|
|
3237
3272
|
}
|
|
@@ -4321,8 +4356,8 @@ var TOOL_CATALOG = [
|
|
|
4321
4356
|
},
|
|
4322
4357
|
{
|
|
4323
4358
|
name: "search_pattern",
|
|
4324
|
-
description: "
|
|
4325
|
-
whenToUse: 'Use when the user asks for a pattern rather than a literal substring \u2014 e.g. "anything ending in `Repository`", "every `handle*` function". For plain substring search use `find_symbol`.',
|
|
4359
|
+
description: "JavaScript-flavor regex match against symbol names. Uses `new RegExp(pattern, flags)` \u2014 no Python-only constructs (`(?P<name>...)`, `(?i)`-style inline flags, `\\A` / `\\Z` anchors).",
|
|
4360
|
+
whenToUse: 'Use when the user asks for a pattern rather than a literal substring \u2014 e.g. "anything ending in `Repository`", "every `handle*` function". For plain substring search use `find_symbol`. For case-insensitive matching set `flags: "i"`, NOT a `(?i)` prefix.',
|
|
4326
4361
|
inputSchema: {
|
|
4327
4362
|
type: "object",
|
|
4328
4363
|
properties: {
|
|
@@ -4789,16 +4824,31 @@ function listEdges(graphJson, req) {
|
|
|
4789
4824
|
freshness: freshnessEnvelope(graph)
|
|
4790
4825
|
};
|
|
4791
4826
|
}
|
|
4827
|
+
function detectCommonRegexMistake(pattern) {
|
|
4828
|
+
if (/\(\?P</.test(pattern)) {
|
|
4829
|
+
return "Python named groups `(?P<name>\u2026)` aren't supported. Use `(?<name>\u2026)` (JS named-group syntax) or just `(\u2026)`.";
|
|
4830
|
+
}
|
|
4831
|
+
if (/^\(\?[ims]+\)/.test(pattern)) {
|
|
4832
|
+
return 'Python-style inline flags `(?i)` aren\'t supported. Pass them as the `flags` parameter, e.g. `flags: "i"`.';
|
|
4833
|
+
}
|
|
4834
|
+
if (/\\A|\\Z/.test(pattern)) {
|
|
4835
|
+
return "Python anchors `\\A` / `\\Z` are valid JS regex but match LITERAL `A` / `Z`, not start-of-string / end-of-string. Use `^` / `$` (JS anchors) with the `m` flag for multiline.";
|
|
4836
|
+
}
|
|
4837
|
+
return void 0;
|
|
4838
|
+
}
|
|
4792
4839
|
var MAX_PATTERN_LENGTH = 200;
|
|
4793
4840
|
var REDOS_PATTERN = /(\([^)]*[+*]\)|\[[^\]]*[+*]\])[+*?]/;
|
|
4794
4841
|
function searchPattern(graphJson, req) {
|
|
4795
4842
|
const graph = graphJson;
|
|
4796
4843
|
if (!req.pattern || req.pattern.length > MAX_PATTERN_LENGTH) {
|
|
4844
|
+
const reason = !req.pattern ? "Empty pattern \u2014 pass a non-empty regex string." : `Pattern exceeds ${MAX_PATTERN_LENGTH.toString()} chars (${req.pattern.length.toString()} chars). Use a tighter expression or filter via \`kind\` / \`language\` / \`file\`.`;
|
|
4797
4845
|
return {
|
|
4798
4846
|
matches: [],
|
|
4799
4847
|
coverage: coverageEnvelope(graph, 0),
|
|
4800
4848
|
freshness: freshnessEnvelope(graph),
|
|
4801
|
-
unsafePattern: true
|
|
4849
|
+
unsafePattern: true,
|
|
4850
|
+
error: reason,
|
|
4851
|
+
isError: true
|
|
4802
4852
|
};
|
|
4803
4853
|
}
|
|
4804
4854
|
if (REDOS_PATTERN.test(req.pattern)) {
|
|
@@ -4806,19 +4856,26 @@ function searchPattern(graphJson, req) {
|
|
|
4806
4856
|
matches: [],
|
|
4807
4857
|
coverage: coverageEnvelope(graph, 0),
|
|
4808
4858
|
freshness: freshnessEnvelope(graph),
|
|
4809
|
-
unsafePattern: true
|
|
4859
|
+
unsafePattern: true,
|
|
4860
|
+
error: "Pattern rejected by ReDoS guard \u2014 nested quantifiers like `(a+)+` or `[\u2026]*+` can cause catastrophic backtracking. Flatten the quantifiers or split into multiple calls.",
|
|
4861
|
+
isError: true
|
|
4810
4862
|
};
|
|
4811
4863
|
}
|
|
4812
4864
|
const flags = (req.flags ?? "").replace(/g/g, "");
|
|
4813
4865
|
let re;
|
|
4814
4866
|
try {
|
|
4815
4867
|
re = new RegExp(req.pattern, flags);
|
|
4816
|
-
} catch {
|
|
4868
|
+
} catch (err) {
|
|
4869
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
4870
|
+
const hint = detectCommonRegexMistake(req.pattern);
|
|
4817
4871
|
return {
|
|
4818
4872
|
matches: [],
|
|
4819
4873
|
coverage: coverageEnvelope(graph, 0),
|
|
4820
4874
|
freshness: freshnessEnvelope(graph),
|
|
4821
|
-
invalidPattern: true
|
|
4875
|
+
invalidPattern: true,
|
|
4876
|
+
error: msg,
|
|
4877
|
+
...hint ? { hint } : {},
|
|
4878
|
+
isError: true
|
|
4822
4879
|
};
|
|
4823
4880
|
}
|
|
4824
4881
|
const kinds = normaliseFilter(req.kind);
|
|
@@ -4854,10 +4911,12 @@ function searchPattern(graphJson, req) {
|
|
|
4854
4911
|
return bScore - aScore;
|
|
4855
4912
|
return a.id.localeCompare(b.id);
|
|
4856
4913
|
});
|
|
4914
|
+
const proactiveHint = matches.length === 0 ? detectCommonRegexMistake(req.pattern) : void 0;
|
|
4857
4915
|
return {
|
|
4858
4916
|
matches: matches.slice(0, limit),
|
|
4859
4917
|
coverage: coverageEnvelope(graph, matches.length),
|
|
4860
|
-
freshness: freshnessEnvelope(graph)
|
|
4918
|
+
freshness: freshnessEnvelope(graph),
|
|
4919
|
+
...proactiveHint ? { hint: proactiveHint } : {}
|
|
4861
4920
|
};
|
|
4862
4921
|
}
|
|
4863
4922
|
function batchQuery(graphJson, req) {
|
|
@@ -4866,29 +4925,94 @@ function batchQuery(graphJson, req) {
|
|
|
4866
4925
|
const allQueries = req.queries ?? [];
|
|
4867
4926
|
const queries = allQueries.slice(0, BATCH_CAP);
|
|
4868
4927
|
const droppedCount = Math.max(0, allQueries.length - queries.length);
|
|
4869
|
-
const results = queries.map((
|
|
4928
|
+
const results = queries.map((rawQ) => {
|
|
4929
|
+
const q = rawQ;
|
|
4930
|
+
const toolName = q.tool ?? "";
|
|
4931
|
+
const params = q.params ?? q.arguments;
|
|
4932
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
4933
|
+
return {
|
|
4934
|
+
ok: false,
|
|
4935
|
+
tool: toolName,
|
|
4936
|
+
error: "params required (object) \u2014 pass `params` or `arguments` per sub-query"
|
|
4937
|
+
};
|
|
4938
|
+
}
|
|
4939
|
+
const args = params;
|
|
4870
4940
|
try {
|
|
4871
|
-
switch (
|
|
4941
|
+
switch (toolName) {
|
|
4872
4942
|
case "find_symbol":
|
|
4873
|
-
return { ok: true, tool:
|
|
4943
|
+
return { ok: true, tool: toolName, result: findSymbol(graph, args) };
|
|
4874
4944
|
case "get_symbol":
|
|
4875
|
-
return { ok: true, tool:
|
|
4945
|
+
return { ok: true, tool: toolName, result: getSymbol(graph, args) };
|
|
4876
4946
|
case "get_impact":
|
|
4877
|
-
return { ok: true, tool:
|
|
4947
|
+
return { ok: true, tool: toolName, result: getImpact(graph, args) };
|
|
4878
4948
|
case "list_edges":
|
|
4879
|
-
return { ok: true, tool:
|
|
4949
|
+
return { ok: true, tool: toolName, result: listEdges(graph, args) };
|
|
4880
4950
|
case "search_pattern":
|
|
4881
|
-
return {
|
|
4951
|
+
return {
|
|
4952
|
+
ok: true,
|
|
4953
|
+
tool: toolName,
|
|
4954
|
+
result: searchPattern(graph, args)
|
|
4955
|
+
};
|
|
4956
|
+
// The 7 graph tools below were advertised by the schema's
|
|
4957
|
+
// whenToUse but missing from the switch — Claude calling
|
|
4958
|
+
// batch_query with `find_callers` etc. used to hit the
|
|
4959
|
+
// "unsupported sub-query tool" branch and bail. Wired up here
|
|
4960
|
+
// so the catalog promise matches the implementation.
|
|
4961
|
+
case "find_callers":
|
|
4962
|
+
return {
|
|
4963
|
+
ok: true,
|
|
4964
|
+
tool: toolName,
|
|
4965
|
+
result: findCallers(graph, args)
|
|
4966
|
+
};
|
|
4967
|
+
case "find_references":
|
|
4968
|
+
return {
|
|
4969
|
+
ok: true,
|
|
4970
|
+
tool: toolName,
|
|
4971
|
+
result: findReferences(graph, args)
|
|
4972
|
+
};
|
|
4973
|
+
case "get_deps":
|
|
4974
|
+
return { ok: true, tool: toolName, result: getDeps(graph, args) };
|
|
4975
|
+
case "get_call_graph":
|
|
4976
|
+
return {
|
|
4977
|
+
ok: true,
|
|
4978
|
+
tool: toolName,
|
|
4979
|
+
result: getCallGraph(graph, args)
|
|
4980
|
+
};
|
|
4981
|
+
case "find_tests_for_symbol":
|
|
4982
|
+
return {
|
|
4983
|
+
ok: true,
|
|
4984
|
+
tool: toolName,
|
|
4985
|
+
result: findTestsForSymbol(graph, args)
|
|
4986
|
+
};
|
|
4987
|
+
case "get_todos":
|
|
4988
|
+
return { ok: true, tool: toolName, result: getTodos(graph, args) };
|
|
4989
|
+
case "get_freshness":
|
|
4990
|
+
return { ok: true, tool: toolName, result: getFreshness(graph) };
|
|
4991
|
+
default:
|
|
4992
|
+
return {
|
|
4993
|
+
ok: false,
|
|
4994
|
+
tool: toolName,
|
|
4995
|
+
error: `unsupported sub-query tool: ${String(toolName)}`,
|
|
4996
|
+
hint: `Supported tools: find_symbol, get_symbol, get_impact, list_edges, search_pattern, find_callers, find_references, get_deps, get_call_graph, find_tests_for_symbol, get_todos, get_freshness.`
|
|
4997
|
+
};
|
|
4882
4998
|
}
|
|
4883
4999
|
} catch (err) {
|
|
4884
5000
|
return {
|
|
4885
5001
|
ok: false,
|
|
4886
|
-
tool:
|
|
5002
|
+
tool: toolName,
|
|
4887
5003
|
error: err instanceof Error ? err.message : String(err)
|
|
4888
5004
|
};
|
|
4889
5005
|
}
|
|
4890
5006
|
});
|
|
4891
|
-
|
|
5007
|
+
const failedCount = results.filter((r) => !r.ok).length;
|
|
5008
|
+
const isError = queries.length > 0 && failedCount === queries.length;
|
|
5009
|
+
return {
|
|
5010
|
+
results,
|
|
5011
|
+
freshness: freshnessEnvelope(graph),
|
|
5012
|
+
droppedCount,
|
|
5013
|
+
failedCount,
|
|
5014
|
+
...isError ? { isError: true } : {}
|
|
5015
|
+
};
|
|
4892
5016
|
}
|
|
4893
5017
|
function findCallers(graphJson, req) {
|
|
4894
5018
|
const graph = graphJson;
|
|
@@ -5121,7 +5245,14 @@ function handleRequest(req, res, options, sessions, secretCtx) {
|
|
|
5121
5245
|
sessions.set(sessionId, { repoId: body.repoId });
|
|
5122
5246
|
return sendJson(res, 200, { sessionId });
|
|
5123
5247
|
});
|
|
5124
|
-
}).catch((err) =>
|
|
5248
|
+
}).catch((err) => {
|
|
5249
|
+
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
5250
|
+
const safeMsg = stripAbsolutePaths(rawMsg);
|
|
5251
|
+
if (err?.code === "ENOENT") {
|
|
5252
|
+
return sendJson(res, 404, { error: "unknown_repo" });
|
|
5253
|
+
}
|
|
5254
|
+
sendJson(res, 500, { error: safeMsg });
|
|
5255
|
+
});
|
|
5125
5256
|
return;
|
|
5126
5257
|
}
|
|
5127
5258
|
if (method === "POST" && url === "/mcp/disconnect") {
|
|
@@ -5137,62 +5268,73 @@ function handleRequest(req, res, options, sessions, secretCtx) {
|
|
|
5137
5268
|
}).catch((err) => sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) }));
|
|
5138
5269
|
return;
|
|
5139
5270
|
}
|
|
5140
|
-
function validationError(
|
|
5141
|
-
return { __validationError:
|
|
5271
|
+
function validationError(error, hint) {
|
|
5272
|
+
return { __validationError: { error, hint } };
|
|
5142
5273
|
}
|
|
5143
5274
|
function isValidationError(v) {
|
|
5144
5275
|
return typeof v === "object" && v !== null && "__validationError" in v;
|
|
5145
5276
|
}
|
|
5277
|
+
const HINT_USE_FIND_SYMBOL_FIRST = "To get a valid `id`, call `find_symbol` (fuzzy name match) or `search_pattern` (regex) first; pass the resulting `matches[].id` here.";
|
|
5146
5278
|
const toolDispatch = {
|
|
5147
5279
|
"/mcp/tools/find_symbol": (g, b) => {
|
|
5148
|
-
if (typeof b["query"] !== "string")
|
|
5149
|
-
return validationError("query required (string)");
|
|
5280
|
+
if (typeof b["query"] !== "string") {
|
|
5281
|
+
return validationError("query required (string)", "Pass a name or substring to fuzzy-match against. For regex use `search_pattern`.");
|
|
5282
|
+
}
|
|
5150
5283
|
return findSymbol(g, b);
|
|
5151
5284
|
},
|
|
5152
5285
|
"/mcp/tools/get_symbol": (g, b) => {
|
|
5153
|
-
if (typeof b["id"] !== "string")
|
|
5154
|
-
return validationError("id required (string)");
|
|
5286
|
+
if (typeof b["id"] !== "string") {
|
|
5287
|
+
return validationError("id required (string)", HINT_USE_FIND_SYMBOL_FIRST);
|
|
5288
|
+
}
|
|
5155
5289
|
return getSymbol(g, b);
|
|
5156
5290
|
},
|
|
5157
5291
|
"/mcp/tools/get_impact": (g, b) => {
|
|
5158
|
-
if (typeof b["id"] !== "string")
|
|
5159
|
-
return validationError("id required (string)");
|
|
5292
|
+
if (typeof b["id"] !== "string") {
|
|
5293
|
+
return validationError("id required (string)", HINT_USE_FIND_SYMBOL_FIRST);
|
|
5294
|
+
}
|
|
5160
5295
|
return getImpact(g, b);
|
|
5161
5296
|
},
|
|
5162
5297
|
"/mcp/tools/list_edges": (g, b) => listEdges(g, b),
|
|
5163
5298
|
"/mcp/tools/search_pattern": (g, b) => {
|
|
5164
|
-
if (typeof b["pattern"] !== "string")
|
|
5165
|
-
return validationError("pattern required (string)");
|
|
5299
|
+
if (typeof b["pattern"] !== "string") {
|
|
5300
|
+
return validationError("pattern required (string)", "Pattern is a JavaScript regex string (no Python `(?P<>` / `(?i)` syntax). For literal substring search use `find_symbol` instead.");
|
|
5301
|
+
}
|
|
5166
5302
|
return searchPattern(g, b);
|
|
5167
5303
|
},
|
|
5168
5304
|
"/mcp/tools/batch_query": (g, b) => {
|
|
5169
|
-
if (!Array.isArray(b["queries"]))
|
|
5170
|
-
return validationError("queries required (array)");
|
|
5305
|
+
if (!Array.isArray(b["queries"])) {
|
|
5306
|
+
return validationError("queries required (array)", "Pass an array of `{tool, params}` (or `{tool, arguments}`) objects. Up to 10 sub-queries per batch. See the schema for valid `tool` values.");
|
|
5307
|
+
}
|
|
5171
5308
|
return batchQuery(g, b);
|
|
5172
5309
|
},
|
|
5173
5310
|
"/mcp/tools/find_callers": (g, b) => {
|
|
5174
|
-
if (typeof b["id"] !== "string")
|
|
5175
|
-
return validationError("id required (string)");
|
|
5311
|
+
if (typeof b["id"] !== "string") {
|
|
5312
|
+
return validationError("id required (string)", HINT_USE_FIND_SYMBOL_FIRST);
|
|
5313
|
+
}
|
|
5176
5314
|
return findCallers(g, b);
|
|
5177
5315
|
},
|
|
5178
5316
|
"/mcp/tools/find_references": (g, b) => {
|
|
5179
|
-
if (typeof b["id"] !== "string")
|
|
5180
|
-
return validationError("id required (string)");
|
|
5317
|
+
if (typeof b["id"] !== "string") {
|
|
5318
|
+
return validationError("id required (string)", HINT_USE_FIND_SYMBOL_FIRST);
|
|
5319
|
+
}
|
|
5181
5320
|
return findReferences(g, b);
|
|
5182
5321
|
},
|
|
5183
5322
|
"/mcp/tools/get_deps": (g, b) => {
|
|
5184
|
-
if (typeof b["file"] !== "string")
|
|
5185
|
-
return validationError("file required (string)");
|
|
5323
|
+
if (typeof b["file"] !== "string") {
|
|
5324
|
+
return validationError("file required (string)", "Pass a repo-relative file path (e.g., `src/main.py`, NOT an absolute path or symbol id).");
|
|
5325
|
+
}
|
|
5186
5326
|
return getDeps(g, b);
|
|
5187
5327
|
},
|
|
5188
5328
|
"/mcp/tools/get_call_graph": (g, b) => {
|
|
5189
|
-
if (typeof b["id"] !== "string")
|
|
5190
|
-
return validationError("id required (string)");
|
|
5329
|
+
if (typeof b["id"] !== "string") {
|
|
5330
|
+
return validationError("id required (string)", HINT_USE_FIND_SYMBOL_FIRST);
|
|
5331
|
+
}
|
|
5191
5332
|
return getCallGraph(g, b);
|
|
5192
5333
|
},
|
|
5193
5334
|
"/mcp/tools/find_tests_for_symbol": (g, b) => {
|
|
5194
|
-
if (typeof b["id"] !== "string")
|
|
5195
|
-
return validationError("id required (string)");
|
|
5335
|
+
if (typeof b["id"] !== "string") {
|
|
5336
|
+
return validationError("id required (string)", HINT_USE_FIND_SYMBOL_FIRST);
|
|
5337
|
+
}
|
|
5196
5338
|
return findTestsForSymbol(g, b);
|
|
5197
5339
|
},
|
|
5198
5340
|
"/mcp/tools/get_todos": (g, b) => getTodos(g, b),
|
|
@@ -5253,11 +5395,11 @@ function handleRequest(req, res, options, sessions, secretCtx) {
|
|
|
5253
5395
|
const root = getRepoIdToRoot().get(session.repoId) ?? fallbackRoot;
|
|
5254
5396
|
if (!root) {
|
|
5255
5397
|
return {
|
|
5256
|
-
ok:
|
|
5257
|
-
|
|
5258
|
-
body: {
|
|
5398
|
+
ok: true,
|
|
5399
|
+
value: {
|
|
5259
5400
|
error: "unknown_repo",
|
|
5260
|
-
|
|
5401
|
+
hint: `LSP tool dispatch needs a workspace root for repoId ${session.repoId}, but none is registered. This usually means the listener hasn't yet reconciled the repo's localPath. Use graph-only tools (find_symbol, find_callers, etc.) until reconcile completes.`,
|
|
5402
|
+
isError: true
|
|
5261
5403
|
}
|
|
5262
5404
|
};
|
|
5263
5405
|
}
|
|
@@ -5271,11 +5413,25 @@ function handleRequest(req, res, options, sessions, secretCtx) {
|
|
|
5271
5413
|
dispatchSessionTool(req, res, url, sessions, options.mcpLogger, async (session, body) => {
|
|
5272
5414
|
const graph = options.graphCache.peek(session.repoId);
|
|
5273
5415
|
if (!graph) {
|
|
5274
|
-
return {
|
|
5416
|
+
return {
|
|
5417
|
+
ok: true,
|
|
5418
|
+
value: {
|
|
5419
|
+
error: "graph_not_loaded",
|
|
5420
|
+
hint: "The graph for this repo is still downloading or has been invalidated. Retry in a few seconds; if the problem persists, the listener may not have completed the initial sync.",
|
|
5421
|
+
isError: true
|
|
5422
|
+
}
|
|
5423
|
+
};
|
|
5275
5424
|
}
|
|
5276
5425
|
const raw = handler(graph, body);
|
|
5277
5426
|
if (isValidationError(raw)) {
|
|
5278
|
-
return {
|
|
5427
|
+
return {
|
|
5428
|
+
ok: true,
|
|
5429
|
+
value: {
|
|
5430
|
+
error: raw.__validationError.error,
|
|
5431
|
+
hint: raw.__validationError.hint,
|
|
5432
|
+
isError: true
|
|
5433
|
+
}
|
|
5434
|
+
};
|
|
5279
5435
|
}
|
|
5280
5436
|
const resolved = await Promise.race([
|
|
5281
5437
|
raw instanceof Promise ? raw : Promise.resolve(raw),
|
|
@@ -5305,6 +5461,7 @@ function dispatchSessionTool(req, res, url, sessions, logger, handler) {
|
|
|
5305
5461
|
latencyMs: Date.now() - startedAt,
|
|
5306
5462
|
error: "unknown_session"
|
|
5307
5463
|
});
|
|
5464
|
+
logToStdout("rejected", tool, null, Date.now() - startedAt, "unknown_session");
|
|
5308
5465
|
return sendJson(res, 404, { error: "unknown_session" });
|
|
5309
5466
|
}
|
|
5310
5467
|
try {
|
|
@@ -5319,8 +5476,10 @@ function dispatchSessionTool(req, res, url, sessions, logger, handler) {
|
|
|
5319
5476
|
status: "ok",
|
|
5320
5477
|
latencyMs: Date.now() - startedAt
|
|
5321
5478
|
});
|
|
5479
|
+
logToStdout("ok", tool, session.repoId, Date.now() - startedAt);
|
|
5322
5480
|
return sendJson(res, 200, result.value);
|
|
5323
5481
|
}
|
|
5482
|
+
const rejectErr = typeof result.body["error"] === "string" ? result.body["error"] : "tool_rejected";
|
|
5324
5483
|
recordLog(logger, {
|
|
5325
5484
|
ts: new Date(startedAt).toISOString(),
|
|
5326
5485
|
sessionId: body.sessionId,
|
|
@@ -5329,8 +5488,9 @@ function dispatchSessionTool(req, res, url, sessions, logger, handler) {
|
|
|
5329
5488
|
args: redactArgs(body),
|
|
5330
5489
|
status: "rejected",
|
|
5331
5490
|
latencyMs: Date.now() - startedAt,
|
|
5332
|
-
error:
|
|
5491
|
+
error: rejectErr
|
|
5333
5492
|
});
|
|
5493
|
+
logToStdout("rejected", tool, session.repoId, Date.now() - startedAt, rejectErr);
|
|
5334
5494
|
return sendJson(res, result.status, result.body);
|
|
5335
5495
|
} catch (err) {
|
|
5336
5496
|
const isTimeout = err instanceof ToolTimeoutError;
|
|
@@ -5346,13 +5506,20 @@ function dispatchSessionTool(req, res, url, sessions, logger, handler) {
|
|
|
5346
5506
|
latencyMs: Date.now() - startedAt,
|
|
5347
5507
|
error: safeMsg
|
|
5348
5508
|
});
|
|
5509
|
+
logToStdout("error", tool, session.repoId, Date.now() - startedAt, safeMsg);
|
|
5349
5510
|
if (isTimeout) {
|
|
5350
|
-
return sendJson(res,
|
|
5511
|
+
return sendJson(res, 200, {
|
|
5512
|
+
error: `tool-timeout: ${tool} exceeded its execution budget`,
|
|
5513
|
+
hint: "The tool was killed at its per-tool timeout. Try a narrower query (smaller limit, depth, or filter) or split into multiple calls.",
|
|
5514
|
+
isError: true,
|
|
5515
|
+
tool
|
|
5516
|
+
});
|
|
5351
5517
|
}
|
|
5352
|
-
return sendJson(res,
|
|
5353
|
-
error:
|
|
5354
|
-
tool,
|
|
5355
|
-
|
|
5518
|
+
return sendJson(res, 200, {
|
|
5519
|
+
error: `tool_error: ${safeMsg}`,
|
|
5520
|
+
hint: "The tool threw an unexpected exception. This is a server-side bug \u2014 file an issue with the tool name and the error text.",
|
|
5521
|
+
isError: true,
|
|
5522
|
+
tool
|
|
5356
5523
|
});
|
|
5357
5524
|
}
|
|
5358
5525
|
}).catch((err) => {
|
|
@@ -5365,6 +5532,11 @@ function recordLog(logger, entry) {
|
|
|
5365
5532
|
return;
|
|
5366
5533
|
void logger.append(entry);
|
|
5367
5534
|
}
|
|
5535
|
+
function logToStdout(status2, tool, repoId, latencyMs, error) {
|
|
5536
|
+
const repoFrag = repoId ? `repo=${repoId}` : "repo=\u2014";
|
|
5537
|
+
const errFrag = error ? ` error=${error}` : "";
|
|
5538
|
+
console.log(`[mcp] ${status2} ${tool} ${repoFrag} ${latencyMs}ms${errFrag}`);
|
|
5539
|
+
}
|
|
5368
5540
|
var REDACT_FIELDS = /* @__PURE__ */ new Set([
|
|
5369
5541
|
"query",
|
|
5370
5542
|
"pattern",
|
|
@@ -6044,6 +6216,9 @@ async function runAutoConfig(opts) {
|
|
|
6044
6216
|
return results;
|
|
6045
6217
|
}
|
|
6046
6218
|
|
|
6219
|
+
// ../listener/dist/main.js
|
|
6220
|
+
init_installer();
|
|
6221
|
+
|
|
6047
6222
|
// ../listener/dist/typed-resolution/resolver-loop.js
|
|
6048
6223
|
init_src();
|
|
6049
6224
|
init_registry();
|
|
@@ -7727,11 +7902,11 @@ async function writeClaudeSubagentHook(repoRoot, contextFolder) {
|
|
|
7727
7902
|
}
|
|
7728
7903
|
|
|
7729
7904
|
// src/lib/gitignore.ts
|
|
7730
|
-
import { readFileSync as readFileSync2, writeFileSync, existsSync } from "fs";
|
|
7905
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
7731
7906
|
import { join as join32 } from "path";
|
|
7732
7907
|
function ensureGitignore(repoRoot, entry) {
|
|
7733
7908
|
const gitignorePath = join32(repoRoot, ".gitignore");
|
|
7734
|
-
if (
|
|
7909
|
+
if (existsSync2(gitignorePath)) {
|
|
7735
7910
|
const content = readFileSync2(gitignorePath, "utf-8");
|
|
7736
7911
|
const lines = content.split("\n").map((l) => l.trim());
|
|
7737
7912
|
if (lines.includes(entry) || lines.includes(entry + "/")) {
|
|
@@ -10521,6 +10696,7 @@ async function mcpShim(opts) {
|
|
|
10521
10696
|
const stderr = opts.stderr ?? process.stderr;
|
|
10522
10697
|
const maxBytes = opts.maxRequestBytes ?? DEFAULT_MAX;
|
|
10523
10698
|
const postJson = opts.postJson ?? defaultPostJson;
|
|
10699
|
+
const getJson = opts.getJson ?? defaultGetJson;
|
|
10524
10700
|
const sessionState = { sessionId: null };
|
|
10525
10701
|
const rl = createInterface2({ input: stdin, crlfDelay: Infinity });
|
|
10526
10702
|
for await (const rawLine of rl) {
|
|
@@ -10566,6 +10742,7 @@ async function mcpShim(opts) {
|
|
|
10566
10742
|
opts.repoId,
|
|
10567
10743
|
parsed,
|
|
10568
10744
|
postJson,
|
|
10745
|
+
getJson,
|
|
10569
10746
|
headers,
|
|
10570
10747
|
sessionState
|
|
10571
10748
|
);
|
|
@@ -10608,7 +10785,7 @@ function writeJson(stream, value) {
|
|
|
10608
10785
|
stream.write(`${JSON.stringify(value)}
|
|
10609
10786
|
`);
|
|
10610
10787
|
}
|
|
10611
|
-
async function routeMessage(endpoint, repoId, message, postJson, headers, sessionState) {
|
|
10788
|
+
async function routeMessage(endpoint, repoId, message, postJson, getJson, headers, sessionState) {
|
|
10612
10789
|
const msg = message;
|
|
10613
10790
|
const rpcId = msg.id;
|
|
10614
10791
|
const method = msg.method;
|
|
@@ -10624,41 +10801,28 @@ async function routeMessage(endpoint, repoId, message, postJson, headers, sessio
|
|
|
10624
10801
|
};
|
|
10625
10802
|
}
|
|
10626
10803
|
if (method === "tools/list") {
|
|
10627
|
-
const
|
|
10628
|
-
|
|
10629
|
-
|
|
10630
|
-
"
|
|
10631
|
-
"
|
|
10632
|
-
|
|
10633
|
-
"batch_query",
|
|
10634
|
-
"find_callers",
|
|
10635
|
-
"find_references",
|
|
10636
|
-
"get_deps",
|
|
10637
|
-
"get_call_graph",
|
|
10638
|
-
"find_tests_for_symbol",
|
|
10639
|
-
"get_todos",
|
|
10640
|
-
"get_freshness",
|
|
10641
|
-
"lsp_definition",
|
|
10642
|
-
"lsp_references",
|
|
10643
|
-
"lsp_hover",
|
|
10644
|
-
"lsp_workspace_symbol",
|
|
10645
|
-
"lsp_document_symbol",
|
|
10646
|
-
"lsp_call_hierarchy",
|
|
10647
|
-
"lsp_implementation",
|
|
10648
|
-
"lsp_type_hierarchy"
|
|
10649
|
-
].map((name) => ({ name, description: "", inputSchema: { type: "object" } }));
|
|
10804
|
+
const catalog = await getJson(`${endpoint}/mcp/tools`, headers);
|
|
10805
|
+
const tools = (catalog.tools ?? []).map((t) => ({
|
|
10806
|
+
name: t.name,
|
|
10807
|
+
description: [t.description, t.whenToUse].filter((s) => s && s.length > 0).join("\n\n"),
|
|
10808
|
+
inputSchema: t.inputSchema ?? { type: "object" }
|
|
10809
|
+
}));
|
|
10650
10810
|
return { jsonrpc: "2.0", id: rpcId, result: { tools } };
|
|
10651
10811
|
}
|
|
10652
|
-
|
|
10812
|
+
async function connect() {
|
|
10653
10813
|
const connectResp = await postJson(`${endpoint}/mcp/connect`, { repoId }, headers);
|
|
10654
|
-
|
|
10814
|
+
return connectResp.sessionId ?? null;
|
|
10815
|
+
}
|
|
10816
|
+
if (!sessionState.sessionId) {
|
|
10817
|
+
const sid = await connect();
|
|
10818
|
+
if (!sid) {
|
|
10655
10819
|
return {
|
|
10656
10820
|
jsonrpc: "2.0",
|
|
10657
10821
|
id: rpcId,
|
|
10658
10822
|
error: { code: -32002, message: "failed to establish MCP session" }
|
|
10659
10823
|
};
|
|
10660
10824
|
}
|
|
10661
|
-
sessionState.sessionId =
|
|
10825
|
+
sessionState.sessionId = sid;
|
|
10662
10826
|
}
|
|
10663
10827
|
if (method === "tools/call") {
|
|
10664
10828
|
const params = msg.params ?? {};
|
|
@@ -10672,12 +10836,35 @@ async function routeMessage(endpoint, repoId, message, postJson, headers, sessio
|
|
|
10672
10836
|
error: { code: -32602, message: "tools/call missing name" }
|
|
10673
10837
|
};
|
|
10674
10838
|
}
|
|
10675
|
-
const
|
|
10676
|
-
|
|
10677
|
-
|
|
10678
|
-
|
|
10679
|
-
)
|
|
10680
|
-
|
|
10839
|
+
const callTool = (sid) => postJson(`${endpoint}/mcp/tools/${name}`, { sessionId: sid, ...args }, headers);
|
|
10840
|
+
let result;
|
|
10841
|
+
try {
|
|
10842
|
+
result = await callTool(sessionState.sessionId);
|
|
10843
|
+
} catch (err) {
|
|
10844
|
+
const detail = err.message;
|
|
10845
|
+
const isStaleSession = /HTTP (401|404)/.test(detail) || /session/i.test(detail) || /unknown_session/i.test(detail);
|
|
10846
|
+
if (!isStaleSession) throw err;
|
|
10847
|
+
sessionState.sessionId = null;
|
|
10848
|
+
const fresh = await connect();
|
|
10849
|
+
if (!fresh) {
|
|
10850
|
+
return {
|
|
10851
|
+
jsonrpc: "2.0",
|
|
10852
|
+
id: rpcId,
|
|
10853
|
+
error: { code: -32002, message: "failed to establish MCP session (after retry)" }
|
|
10854
|
+
};
|
|
10855
|
+
}
|
|
10856
|
+
sessionState.sessionId = fresh;
|
|
10857
|
+
result = await callTool(fresh);
|
|
10858
|
+
}
|
|
10859
|
+
const inner = result ?? {};
|
|
10860
|
+
return {
|
|
10861
|
+
jsonrpc: "2.0",
|
|
10862
|
+
id: rpcId,
|
|
10863
|
+
result: {
|
|
10864
|
+
content: [{ type: "text", text: JSON.stringify(result) }],
|
|
10865
|
+
...inner.isError === true ? { isError: true } : {}
|
|
10866
|
+
}
|
|
10867
|
+
};
|
|
10681
10868
|
}
|
|
10682
10869
|
return {
|
|
10683
10870
|
jsonrpc: "2.0",
|
|
@@ -10702,6 +10889,19 @@ async function defaultPostJson(url, body, headers = { "content-type": "applicati
|
|
|
10702
10889
|
}
|
|
10703
10890
|
return await res.json();
|
|
10704
10891
|
}
|
|
10892
|
+
async function defaultGetJson(url, headers = {}) {
|
|
10893
|
+
const res = await fetch(url, { method: "GET", headers });
|
|
10894
|
+
if (!res.ok) {
|
|
10895
|
+
let detail = "";
|
|
10896
|
+
try {
|
|
10897
|
+
const errBody = await res.json();
|
|
10898
|
+
detail = errBody.message ?? errBody.error ?? "";
|
|
10899
|
+
} catch {
|
|
10900
|
+
}
|
|
10901
|
+
throw new Error(`HTTP ${res.status.toString()}${detail ? `: ${detail}` : ""}`);
|
|
10902
|
+
}
|
|
10903
|
+
return await res.json();
|
|
10904
|
+
}
|
|
10705
10905
|
|
|
10706
10906
|
// src/commands/mcp-serve.ts
|
|
10707
10907
|
import { createInterface as createInterface3 } from "readline";
|