@wrongstack/plug-lsp 0.7.4 → 0.7.6

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/index.js CHANGED
@@ -5,6 +5,7 @@ import { TOKENS, buildChildEnv, atomicWrite } from '@wrongstack/core';
5
5
  import { pathToFileURL, fileURLToPath } from 'url';
6
6
  import { EventEmitter } from 'events';
7
7
  import * as fs3 from 'fs';
8
+ import { IndexStore, buildIndexableText, buildBm25Index, tokenise, internalKindToLspKind, lspKindToInternalKind } from '@wrongstack/tools/codebase-index/index';
8
9
 
9
10
  // src/presets.ts
10
11
  var PRESETS = {
@@ -1426,6 +1427,298 @@ function formatActions(actions) {
1426
1427
  return actions.map((a, i) => `[${i}] ${a.kind ?? "action"} ${a.title}`).join("\n");
1427
1428
  }
1428
1429
 
1430
+ // src/constants.ts
1431
+ var LSP_CONSTANTS = Object.freeze({
1432
+ /** Default timeout for LSP tool operations (5 seconds). */
1433
+ TOOL_TIMEOUT_MS: 5e3
1434
+ });
1435
+
1436
+ // src/formatters/symbols.ts
1437
+ function formatDocumentSymbols(path8, symbols, cwd) {
1438
+ if (!symbols || symbols.length === 0) return "No symbols found.";
1439
+ const lines = [`${displayPath(path8, cwd)}:`];
1440
+ for (const sym of symbols) appendSymbol(lines, sym, 1, cwd);
1441
+ return lines.join("\n");
1442
+ }
1443
+ function formatWorkspaceSymbols(symbols, query, cwd, limit = 100) {
1444
+ if (!symbols || symbols.length === 0) return `No symbols matching "${query}".`;
1445
+ const lines = [`${symbols.length} symbols matching "${query}":`];
1446
+ for (const sym of symbols.slice(0, limit)) {
1447
+ lines.push(
1448
+ ` ${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`
1449
+ );
1450
+ }
1451
+ if (symbols.length > limit) lines.push(` ... truncated ${symbols.length - limit} more`);
1452
+ return lines.join("\n");
1453
+ }
1454
+ function appendSymbol(lines, sym, depth, cwd) {
1455
+ const indent = " ".repeat(depth);
1456
+ if ("selectionRange" in sym) {
1457
+ lines.push(
1458
+ `${indent}${kindName(sym.kind)} ${sym.name} (L${sym.selectionRange.start.line + 1})`
1459
+ );
1460
+ for (const child of sym.children ?? []) appendSymbol(lines, child, depth + 1, cwd);
1461
+ } else {
1462
+ lines.push(
1463
+ `${indent}${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`
1464
+ );
1465
+ }
1466
+ }
1467
+ function kindName(kind) {
1468
+ return [
1469
+ "file",
1470
+ "module",
1471
+ "namespace",
1472
+ "package",
1473
+ "class",
1474
+ "method",
1475
+ "property",
1476
+ "field",
1477
+ "constructor",
1478
+ "enum",
1479
+ "interface",
1480
+ "function",
1481
+ "variable",
1482
+ "constant",
1483
+ "string",
1484
+ "number",
1485
+ "boolean",
1486
+ "array",
1487
+ "object",
1488
+ "key",
1489
+ "null",
1490
+ "enumMember",
1491
+ "struct",
1492
+ "event",
1493
+ "operator",
1494
+ "typeParameter"
1495
+ ][kind - 1] ?? "symbol";
1496
+ }
1497
+ function formatCodebaseLspResults(output, cwd) {
1498
+ const { results, totalIndex, totalLsp, query, usedIndex, usedLsp } = output;
1499
+ if (results.length === 0) {
1500
+ const sources = [];
1501
+ if (usedIndex) sources.push(`index(${totalIndex})`);
1502
+ if (usedLsp) sources.push(`lsp(${totalLsp})`);
1503
+ return `No symbols matching "${query}". Searched: ${sources.join(", ") || "none"}.`;
1504
+ }
1505
+ const lines = [];
1506
+ lines.push(`${results.length} results for "${query}" (index:${totalIndex} lsp:${totalLsp}):`);
1507
+ for (const r of results) {
1508
+ const sourceTag = r.source === "index" ? "[index]" : `[lsp:${r.server ?? "?"}]`;
1509
+ const scoreTag = r.score !== void 0 ? ` score=${r.score.toFixed(2)}` : "";
1510
+ const location = `${displayPath(r.file, cwd)}:${r.line}`;
1511
+ if (r.snippet) {
1512
+ lines.push(` ${sourceTag} ${r.kind} ${r.name} ${location}${scoreTag}`);
1513
+ lines.push(` ${r.snippet}`);
1514
+ } else {
1515
+ lines.push(` ${sourceTag} ${r.kind} ${r.name} ${location}${scoreTag}`);
1516
+ }
1517
+ }
1518
+ return lines.join("\n");
1519
+ }
1520
+
1521
+ // src/tools/codebase-lsp-search.ts
1522
+ function createCodebaseLspSearchTool(deps) {
1523
+ return {
1524
+ name: "codebase-lsp-search",
1525
+ description: "Search code symbols using a fast SQLite+BM25 index, falling back to live LSP workspaceSymbol queries when needed.",
1526
+ usageHint: "Pass `query` to search. Use `limit` (default 20) to cap results. Set `preferLsp=true` to skip the index and query LSP servers directly for live precision.",
1527
+ inputSchema: {
1528
+ type: "object",
1529
+ properties: {
1530
+ query: { type: "string", description: "Search query string" },
1531
+ limit: {
1532
+ type: "integer",
1533
+ description: "Maximum number of results to return (default 20, max 100)",
1534
+ minimum: 1,
1535
+ maximum: 100
1536
+ },
1537
+ preferLsp: {
1538
+ type: "boolean",
1539
+ description: "If true, skip the index and query LSP servers directly. Useful for live precision when the index may be stale."
1540
+ }
1541
+ },
1542
+ required: ["query"]
1543
+ },
1544
+ permission: "auto",
1545
+ mutating: false,
1546
+ timeoutMs: LSP_CONSTANTS.TOOL_TIMEOUT_MS * 2,
1547
+ // Allow extra time for LSP round-robins
1548
+ async execute(input, ctx, opts) {
1549
+ try {
1550
+ const limit = Math.min(input.limit ?? 20, 100);
1551
+ const query = input.query ?? "";
1552
+ let indexResults = [];
1553
+ let totalIndex = 0;
1554
+ let usedIndex = false;
1555
+ let usedLsp = false;
1556
+ let lspResults = [];
1557
+ let totalLsp = 0;
1558
+ if (!input.preferLsp) {
1559
+ const indexOutcome = await searchIndex(ctx.projectRoot, query, limit);
1560
+ indexResults = indexOutcome.results;
1561
+ totalIndex = indexOutcome.total;
1562
+ usedIndex = true;
1563
+ }
1564
+ const needsLsp = input.preferLsp || indexResults.length === 0;
1565
+ if (needsLsp) {
1566
+ const lspOutcome = await searchLsp(deps, query, limit, opts.signal);
1567
+ lspResults = lspOutcome.results;
1568
+ totalLsp = lspOutcome.total;
1569
+ usedLsp = true;
1570
+ }
1571
+ const output = mergeResults(indexResults, lspResults, limit);
1572
+ const fullOutput = {
1573
+ results: output,
1574
+ totalIndex,
1575
+ totalLsp,
1576
+ query,
1577
+ usedIndex,
1578
+ usedLsp
1579
+ };
1580
+ return formatCodebaseLspResults(fullOutput, ctx.cwd);
1581
+ } catch (err) {
1582
+ return stringifyToolError(err);
1583
+ }
1584
+ }
1585
+ };
1586
+ }
1587
+ async function searchIndex(projectRoot, query, limit) {
1588
+ const store = new IndexStore(projectRoot);
1589
+ try {
1590
+ const candidates = store.search(query);
1591
+ if (candidates.length === 0) {
1592
+ return { results: [], total: 0 };
1593
+ }
1594
+ const indexable = candidates.map((c) => ({
1595
+ id: c.id,
1596
+ text: buildIndexableText(c.name, c.signature, c.docComment)
1597
+ }));
1598
+ const bm25 = buildBm25Index(indexable);
1599
+ const scored = bm25.score(query, (id) => candidates.some((c) => c.id === id));
1600
+ scored.sort((a, b) => b.score - a.score);
1601
+ const top = scored.slice(0, limit);
1602
+ const qTokens = tokenise(query);
1603
+ const results = top.map(({ id, score }) => {
1604
+ const c = candidates.find((c2) => c2.id === id);
1605
+ const lspKind = internalKindToLspKind(c.kind) ?? 0;
1606
+ const snippet = bm25.extractSnippet(id, qTokens);
1607
+ return {
1608
+ name: c.name,
1609
+ kind: c.kind,
1610
+ lspKind,
1611
+ file: c.file,
1612
+ line: c.line,
1613
+ source: "index",
1614
+ score,
1615
+ snippet
1616
+ };
1617
+ });
1618
+ return { results, total: candidates.length };
1619
+ } finally {
1620
+ store.close();
1621
+ }
1622
+ }
1623
+ async function searchLsp(deps, query, limit, signal) {
1624
+ const merged = [];
1625
+ const servers = deps.registry.list();
1626
+ const promises = [];
1627
+ for (const server of servers) {
1628
+ if (server.state !== "ready") continue;
1629
+ if (server.capabilities && !supportsWorkspaceSymbol(server.capabilities)) continue;
1630
+ promises.push(
1631
+ (async () => {
1632
+ try {
1633
+ const result = await server.workspaceSymbol(
1634
+ { query },
1635
+ LSP_CONSTANTS.TOOL_TIMEOUT_MS,
1636
+ signal
1637
+ );
1638
+ if (result) {
1639
+ for (const sym of result) {
1640
+ merged.push(sym);
1641
+ }
1642
+ }
1643
+ } catch {
1644
+ }
1645
+ })()
1646
+ );
1647
+ }
1648
+ await Promise.all(promises);
1649
+ const deduplicated = deduplicateByKey(
1650
+ merged.map((sym) => ({
1651
+ name: sym.name,
1652
+ kind: lspKindToInternalKind(sym.kind) ?? "symbol",
1653
+ lspKind: sym.kind,
1654
+ file: sym.location.uri.startsWith("file://") ? sym.location.uri.slice(7) : sym.location.uri,
1655
+ line: sym.location.range.start.line + 1,
1656
+ // convert to 1-based
1657
+ source: "lsp",
1658
+ server: serverNameFromConfig(deps, sym)
1659
+ }))
1660
+ );
1661
+ return {
1662
+ results: deduplicated.slice(0, limit),
1663
+ total: deduplicated.length
1664
+ };
1665
+ }
1666
+ function serverNameFromConfig(deps, sym) {
1667
+ const file = sym.location.uri;
1668
+ const ext = file.includes(".") ? file.split(".").pop().toLowerCase() : "";
1669
+ const langMap = {
1670
+ ts: ["typescript", "tsserver"],
1671
+ tsx: ["typescript", "tsserver"],
1672
+ js: ["javascript", "typescript"],
1673
+ jsx: ["javascript", "typescript"],
1674
+ py: ["python", "pyright"],
1675
+ go: ["go", "gopls"],
1676
+ rs: ["rust", "rust-analyzer"]
1677
+ };
1678
+ const langs = langMap[ext] ?? [ext];
1679
+ const servers = deps.registry.list();
1680
+ for (const lang of langs) {
1681
+ for (const server of servers) {
1682
+ if (server.state === "ready" && server.config.languages.some((l) => l.toLowerCase() === lang.toLowerCase())) {
1683
+ return server.name;
1684
+ }
1685
+ }
1686
+ }
1687
+ return servers.find((s) => s.state === "ready")?.name ?? "unknown";
1688
+ }
1689
+ function mergeResults(indexResults, lspResults, limit) {
1690
+ const seen = /* @__PURE__ */ new Map();
1691
+ for (const r of indexResults) {
1692
+ const key = `${r.file}:${r.line}:${r.name}`;
1693
+ seen.set(key, r);
1694
+ }
1695
+ for (const r of lspResults) {
1696
+ const key = `${r.file}:${r.line}:${r.name}`;
1697
+ if (!seen.has(key)) {
1698
+ seen.set(key, r);
1699
+ }
1700
+ }
1701
+ const merged = Array.from(seen.values());
1702
+ merged.sort((a, b) => {
1703
+ if (a.source === "index" && b.source !== "index") return -1;
1704
+ if (a.source !== "index" && b.source === "index") return 1;
1705
+ if (a.source === "index" && b.source === "index") {
1706
+ return (b.score ?? 0) - (a.score ?? 0);
1707
+ }
1708
+ return 0;
1709
+ });
1710
+ return merged.slice(0, limit);
1711
+ }
1712
+ function deduplicateByKey(items) {
1713
+ const seen = /* @__PURE__ */ new Set();
1714
+ return items.filter((item) => {
1715
+ const key = `${item.file}:${item.line}:${item.name}`;
1716
+ if (seen.has(key)) return false;
1717
+ seen.add(key);
1718
+ return true;
1719
+ });
1720
+ }
1721
+
1429
1722
  // src/formatters/location.ts
1430
1723
  function formatLocations(locations, cwd, limit = 100) {
1431
1724
  if (!locations || locations.length === 0) return "No locations found.";
@@ -1438,12 +1731,6 @@ function formatLocations(locations, cwd, limit = 100) {
1438
1731
  return lines.join("\n");
1439
1732
  }
1440
1733
 
1441
- // src/constants.ts
1442
- var LSP_CONSTANTS = Object.freeze({
1443
- /** Default timeout for LSP tool operations (5 seconds). */
1444
- TOOL_TIMEOUT_MS: 5e3
1445
- });
1446
-
1447
1734
  // src/tools/definition.ts
1448
1735
  function createDefinitionTool(deps) {
1449
1736
  return {
@@ -1697,68 +1984,6 @@ Applied: ${applied.edits} edits across ${applied.files.length} files.`;
1697
1984
  };
1698
1985
  }
1699
1986
 
1700
- // src/formatters/symbols.ts
1701
- function formatDocumentSymbols(path8, symbols, cwd) {
1702
- if (!symbols || symbols.length === 0) return "No symbols found.";
1703
- const lines = [`${displayPath(path8, cwd)}:`];
1704
- for (const sym of symbols) appendSymbol(lines, sym, 1, cwd);
1705
- return lines.join("\n");
1706
- }
1707
- function formatWorkspaceSymbols(symbols, query, cwd, limit = 100) {
1708
- if (!symbols || symbols.length === 0) return `No symbols matching "${query}".`;
1709
- const lines = [`${symbols.length} symbols matching "${query}":`];
1710
- for (const sym of symbols.slice(0, limit)) {
1711
- lines.push(
1712
- ` ${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`
1713
- );
1714
- }
1715
- if (symbols.length > limit) lines.push(` ... truncated ${symbols.length - limit} more`);
1716
- return lines.join("\n");
1717
- }
1718
- function appendSymbol(lines, sym, depth, cwd) {
1719
- const indent = " ".repeat(depth);
1720
- if ("selectionRange" in sym) {
1721
- lines.push(
1722
- `${indent}${kindName(sym.kind)} ${sym.name} (L${sym.selectionRange.start.line + 1})`
1723
- );
1724
- for (const child of sym.children ?? []) appendSymbol(lines, child, depth + 1, cwd);
1725
- } else {
1726
- lines.push(
1727
- `${indent}${kindName(sym.kind)} ${sym.name} ${displayPath(uriToPath(sym.location.uri), cwd)}:${sym.location.range.start.line + 1}`
1728
- );
1729
- }
1730
- }
1731
- function kindName(kind) {
1732
- return [
1733
- "file",
1734
- "module",
1735
- "namespace",
1736
- "package",
1737
- "class",
1738
- "method",
1739
- "property",
1740
- "field",
1741
- "constructor",
1742
- "enum",
1743
- "interface",
1744
- "function",
1745
- "variable",
1746
- "constant",
1747
- "string",
1748
- "number",
1749
- "boolean",
1750
- "array",
1751
- "object",
1752
- "key",
1753
- "null",
1754
- "enumMember",
1755
- "struct",
1756
- "event",
1757
- "operator",
1758
- "typeParameter"
1759
- ][kind - 1] ?? "symbol";
1760
- }
1761
-
1762
1987
  // src/tools/symbols.ts
1763
1988
  function createSymbolsTool(deps) {
1764
1989
  return {
@@ -1818,6 +2043,7 @@ function makeLSPTools(deps) {
1818
2043
  createReferencesTool(deps),
1819
2044
  createHoverTool(deps),
1820
2045
  createSymbolsTool(deps),
2046
+ createCodebaseLspSearchTool(deps),
1821
2047
  createRenameTool(deps),
1822
2048
  createCodeActionsTool(deps)
1823
2049
  ];