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.
Files changed (2) hide show
  1. package/dist/bin/repowise.js +491 -291
  2. package/package.json +1 -1
@@ -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
- execSync(`which ${command}`, { stdio: "ignore", timeout: 200 });
1146
- found = true;
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 { ok: false, reason: "unsupported-language" };
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).join(", ");
1228
- console.warn(`[lsp:${language}] no server available on PATH \u2014 tried: ${tried}`);
1229
- return { ok: false, reason: "no-server-available" };
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 { ok: false, reason: got.reason };
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 { ok: false, reason: got.reason };
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 { ok: false, reason: got.reason };
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 { ok: false, reason: got.reason };
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 { ok: false, reason: got.reason };
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 { ok: false, reason: got.reason };
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 { ok: false, reason: "no-server-available" };
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 { ok: false, reason: got.reason };
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: "Regex-match symbol names across the graph.",
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((q) => {
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 (q.tool) {
4941
+ switch (toolName) {
4872
4942
  case "find_symbol":
4873
- return { ok: true, tool: q.tool, result: findSymbol(graph, q.params) };
4943
+ return { ok: true, tool: toolName, result: findSymbol(graph, args) };
4874
4944
  case "get_symbol":
4875
- return { ok: true, tool: q.tool, result: getSymbol(graph, q.params) };
4945
+ return { ok: true, tool: toolName, result: getSymbol(graph, args) };
4876
4946
  case "get_impact":
4877
- return { ok: true, tool: q.tool, result: getImpact(graph, q.params) };
4947
+ return { ok: true, tool: toolName, result: getImpact(graph, args) };
4878
4948
  case "list_edges":
4879
- return { ok: true, tool: q.tool, result: listEdges(graph, q.params) };
4949
+ return { ok: true, tool: toolName, result: listEdges(graph, args) };
4880
4950
  case "search_pattern":
4881
- return { ok: true, tool: q.tool, result: searchPattern(graph, q.params) };
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: q.tool,
5002
+ tool: toolName,
4887
5003
  error: err instanceof Error ? err.message : String(err)
4888
5004
  };
4889
5005
  }
4890
5006
  });
4891
- return { results, freshness: freshnessEnvelope(graph), droppedCount };
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) => sendJson(res, 500, { error: err instanceof Error ? err.message : String(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(msg) {
5141
- return { __validationError: msg };
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: false,
5257
- status: 400,
5258
- body: {
5398
+ ok: true,
5399
+ value: {
5259
5400
  error: "unknown_repo",
5260
- message: `no workspace root registered for repoId ${session.repoId}`
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 { ok: false, status: 503, body: { error: "graph_not_loaded" } };
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 { ok: false, status: 400, body: { error: raw.__validationError } };
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: typeof result.body["error"] === "string" ? result.body["error"] : "tool_rejected"
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, 504, { error: "tool-timeout", tool });
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, 500, {
5353
- error: "tool_error",
5354
- tool,
5355
- message: safeMsg
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 (existsSync(gitignorePath)) {
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 tools = [
10628
- "find_symbol",
10629
- "get_symbol",
10630
- "get_impact",
10631
- "list_edges",
10632
- "search_pattern",
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
- if (!sessionState.sessionId) {
10812
+ async function connect() {
10653
10813
  const connectResp = await postJson(`${endpoint}/mcp/connect`, { repoId }, headers);
10654
- if (!connectResp.sessionId) {
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 = connectResp.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 result = await postJson(
10676
- `${endpoint}/mcp/tools/${name}`,
10677
- { sessionId: sessionState.sessionId, ...args },
10678
- headers
10679
- );
10680
- return { jsonrpc: "2.0", id: rpcId, result };
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";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repowisestage",
3
- "version": "0.0.46",
3
+ "version": "0.0.48",
4
4
  "type": "module",
5
5
  "description": "AI-optimized codebase context generator",
6
6
  "bin": {