codemode-lsp 0.1.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +11 -5
  2. package/dist/index.js +172 -14
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -65,14 +65,20 @@ returns `{ result, logs, changes }`:
65
65
  - **changes** — every file that hit disk, as `{ file, kind, diff }` with a
66
66
  unified diff against the pre-script content. Empty for read-only scripts.
67
67
 
68
- The API surface is 16 functions plus `getDiagnostics`: 8 read ops (`readFile`,
68
+ The API surface is 18 functions plus `getDiagnostics`: 10 read ops (`readFile`,
69
69
  `getSymbolBody`, `getSymbols`, `findSymbol`, `findReferences`,
70
- `goToDefinition`, `searchText`, `listFiles`) and 7 write ops (`renameSymbol`,
71
- `replaceSymbolBody`, `insertBeforeSymbol`, `insertAfterSymbol`,
72
- `deleteSymbol`, `writeFile`, `deleteFile`). Symbols are addressed by
70
+ `goToDefinition`, `incomingCalls`, `outgoingCalls`, `searchText`, `listFiles`)
71
+ and 7 write ops (`renameSymbol`, `replaceSymbolBody`, `insertBeforeSymbol`,
72
+ `insertAfterSymbol`, `deleteSymbol`, `writeFile`, `deleteFile`). The call
73
+ hierarchy ops return only true calls — attributed to the enclosing function,
74
+ resolved across modules — where `findReferences` mixes calls with imports and
75
+ re-exports. Symbols are addressed by
73
76
  slash-separated paths (`MyClass/myMethod`) discovered via `getSymbols`. The
74
77
  full type definitions are embedded in the tool description, generated straight
75
- from the source (`bun run generate:types`).
78
+ from the source (`bun run generate:types`). If a client truncates the
79
+ description, the script `await lsp.help()` returns the complete reference —
80
+ the description's first lines advertise this, so an agent can always recover
81
+ the full API without probing.
76
82
 
77
83
  See `PRD.md` for the complete spec.
78
84
 
package/dist/index.js CHANGED
@@ -39506,16 +39506,26 @@ ${prefix}has no matching children.` : "")}`);
39506
39506
  }
39507
39507
  return resolved;
39508
39508
  }
39509
- function containingSymbolPath(symbols, position) {
39509
+ var CALLABLE_KIND_NAMES = new Set(["function", "method", "constructor"]);
39510
+ function containingFunctionPath(symbols, position) {
39511
+ const chain = [];
39510
39512
  function visit(candidates) {
39511
39513
  for (const candidate of candidates) {
39512
39514
  if (!positionContains(candidate.range, position))
39513
39515
  continue;
39514
- return visit(candidate.children) ?? candidate;
39516
+ chain.push(candidate);
39517
+ visit(candidate.children);
39518
+ return;
39515
39519
  }
39516
- return;
39517
39520
  }
39518
- return visit(buildResolvedSymbolTree(addressableSymbols(symbols)))?.path;
39521
+ visit(buildResolvedSymbolTree(addressableSymbols(symbols)));
39522
+ for (let i = chain.length - 1;i >= 0; i -= 1) {
39523
+ const candidate = chain[i];
39524
+ if (candidate && CALLABLE_KIND_NAMES.has(candidate.kind)) {
39525
+ return candidate.path;
39526
+ }
39527
+ }
39528
+ return chain[chain.length - 1]?.path;
39519
39529
  }
39520
39530
  function symbolPathForRange(symbols, range) {
39521
39531
  function visit(candidates) {
@@ -39859,6 +39869,76 @@ class LspApi {
39859
39869
  }
39860
39870
  return this.locationToApiLocation(first);
39861
39871
  }
39872
+ async incomingCalls(file2, symbolPath) {
39873
+ const item = await this.callHierarchyItemFor("incomingCalls(file, symbolPath)", 'await lsp.incomingCalls("src/auth.ts", "AuthService/validate")', file2, symbolPath);
39874
+ const calls = await this.client.incomingCalls(item);
39875
+ const results = [];
39876
+ for (const call of calls) {
39877
+ const info = await this.callHierarchyItemToInfo(call.from, call.fromRanges, call.from.uri);
39878
+ if (info)
39879
+ results.push(info);
39880
+ }
39881
+ return results;
39882
+ }
39883
+ async outgoingCalls(file2, symbolPath) {
39884
+ const item = await this.callHierarchyItemFor("outgoingCalls(file, symbolPath)", 'await lsp.outgoingCalls("src/payments.ts", "recordPayment")', file2, symbolPath);
39885
+ const calls = await this.client.outgoingCalls(item);
39886
+ const results = [];
39887
+ for (const call of calls) {
39888
+ const info = await this.callHierarchyItemToInfo(call.to, call.fromRanges, item.uri);
39889
+ if (info)
39890
+ results.push(info);
39891
+ }
39892
+ return results;
39893
+ }
39894
+ async callHierarchyItemFor(signature, example, file2, symbolPath) {
39895
+ this.requireStrings(signature, example, { file: file2, symbolPath });
39896
+ const resolved = this.resolveWorkspacePath(file2);
39897
+ const symbol2 = resolveSymbolPath({
39898
+ file: resolved.relPath,
39899
+ symbolPath,
39900
+ symbols: await this.documentSymbols(resolved)
39901
+ });
39902
+ const items = await this.client.prepareCallHierarchy({
39903
+ uri: resolved.uri,
39904
+ position: symbol2.selectionRange.start
39905
+ });
39906
+ const item = items[0];
39907
+ if (!item) {
39908
+ throw new Error(`"${symbolPath}" in "${resolved.relPath}" has no call hierarchy — the symbol must be callable (a function, method, or constructor; its kind is "${symbol2.kind}"). Pick a function-like symbol from getSymbols("${resolved.relPath}").`);
39909
+ }
39910
+ return item;
39911
+ }
39912
+ async callHierarchyItemToInfo(item, fromRanges, rangesUri) {
39913
+ const itemPath = this.workspacePathFromUri(item.uri);
39914
+ if (!itemPath)
39915
+ return;
39916
+ const rangesPath = this.workspacePathFromUri(rangesUri);
39917
+ const rangesText = rangesPath ? this.readTextSafe(rangesPath) : "";
39918
+ let symbolPath;
39919
+ try {
39920
+ const symbols = await this.documentSymbols(itemPath);
39921
+ symbolPath = symbolPathForRange(symbols, item.selectionRange) ?? containingFunctionPath(symbols, item.selectionRange.start);
39922
+ } catch {
39923
+ symbolPath = undefined;
39924
+ }
39925
+ return {
39926
+ file: itemPath.relPath,
39927
+ symbolPath: symbolPath ?? item.name,
39928
+ name: item.name,
39929
+ kind: symbolKindName(item.kind),
39930
+ callSites: [
39931
+ ...new Map(fromRanges.map((range) => [
39932
+ `${range.start.line}:${range.start.character}`,
39933
+ {
39934
+ line: range.start.line + 1,
39935
+ column: range.start.character + 1,
39936
+ context: rangesPath ? lineContext(rangesText, range.start.line) : ""
39937
+ }
39938
+ ])).values()
39939
+ ]
39940
+ };
39941
+ }
39862
39942
  async searchText(pattern, glob) {
39863
39943
  this.requireStrings("searchText(pattern, glob?)", 'await lsp.searchText("new NotFoundError\\\\(", "src/**") — the pattern is a regex; escape metacharacters for literal text', { pattern });
39864
39944
  if (glob !== undefined && glob !== null && typeof glob !== "string") {
@@ -40203,7 +40283,7 @@ ${text}`
40203
40283
  async containingPathForLocation(workspacePath, location) {
40204
40284
  try {
40205
40285
  const symbols = await this.documentSymbols(workspacePath);
40206
- return containingSymbolPath(symbols, location.range.start);
40286
+ return containingFunctionPath(symbols, location.range.start);
40207
40287
  } catch {
40208
40288
  return;
40209
40289
  }
@@ -40397,6 +40477,21 @@ class LspClient {
40397
40477
  const result = await this.request("textDocument/definition", { textDocument: { uri: params.uri }, position: params.position });
40398
40478
  return normalizeDefinition(result);
40399
40479
  }
40480
+ async prepareCallHierarchy(params) {
40481
+ await this.ensureAlive();
40482
+ const result = await this.request("textDocument/prepareCallHierarchy", { textDocument: { uri: params.uri }, position: params.position });
40483
+ return result ?? [];
40484
+ }
40485
+ async incomingCalls(item) {
40486
+ await this.ensureAlive();
40487
+ const result = await this.request("callHierarchy/incomingCalls", { item });
40488
+ return result ?? [];
40489
+ }
40490
+ async outgoingCalls(item) {
40491
+ await this.ensureAlive();
40492
+ const result = await this.request("callHierarchy/outgoingCalls", { item });
40493
+ return result ?? [];
40494
+ }
40400
40495
  openTextDocument(abs) {
40401
40496
  const uri = pathToFileURL2(abs).href;
40402
40497
  if (this.openVersions.has(uri))
@@ -40606,6 +40701,7 @@ class LspClient {
40606
40701
  definition: { linkSupport: false },
40607
40702
  references: {},
40608
40703
  rename: { prepareSupport: true },
40704
+ callHierarchy: {},
40609
40705
  synchronization: { didSave: false, willSave: false },
40610
40706
  publishDiagnostics: {}
40611
40707
  },
@@ -46035,6 +46131,8 @@ var READ_OP_NAMES = [
46035
46131
  "findSymbol",
46036
46132
  "findReferences",
46037
46133
  "goToDefinition",
46134
+ "incomingCalls",
46135
+ "outgoingCalls",
46038
46136
  "searchText",
46039
46137
  "listFiles",
46040
46138
  "getDiagnostics"
@@ -46274,6 +46372,8 @@ async function runSandbox(code, options) {
46274
46372
  };
46275
46373
  const opNames = options.readonly ? [...READ_OP_NAMES] : [...READ_OP_NAMES, ...WRITE_OP_NAMES];
46276
46374
  sandbox.lsp = buildTracedLsp(options.lsp, trace, makeSandboxError, wrapAsync, opNames);
46375
+ const helpText = options.helpText ?? "No extended help is available; the tool description is the full reference.";
46376
+ sandbox.lsp.help = wrapAsync(async () => helpText);
46277
46377
  const { source, uncapturedLastStatement } = normalizeCode(code);
46278
46378
  const wrapped = `(async () => {
46279
46379
  ${source}
@@ -46358,12 +46458,32 @@ interface Reference {
46358
46458
  column: number;
46359
46459
  /** The actual line of code containing the reference. */
46360
46460
  context: string;
46361
- /** Path of the symbol containing the reference (a reusable handle); "" at top level. */
46461
+ /** Path of the nearest enclosing function/method (a reusable handle); "" at top level. */
46362
46462
  symbolPath: string;
46363
46463
  /** Always false in v1 — the language server does not classify accesses. */
46364
46464
  isWriteAccess: boolean;
46365
46465
  }
46366
46466
 
46467
+ interface CallInfo {
46468
+ /** The other function: the caller (incomingCalls) or the callee (outgoingCalls). */
46469
+ file: string;
46470
+ /** Its symbol path — round-trips into other lsp.* calls together with \`file\`. */
46471
+ symbolPath: string;
46472
+ name: string;
46473
+ kind: string;
46474
+ /** Call sites in the caller's body (so in \`file\` for incomingCalls, in the queried symbol's file for outgoingCalls). */
46475
+ callSites: CallSite[];
46476
+ }
46477
+
46478
+ interface CallSite {
46479
+ /** 1-based. */
46480
+ line: number;
46481
+ /** 1-based. */
46482
+ column: number;
46483
+ /** The line of code containing the call. */
46484
+ context: string;
46485
+ }
46486
+
46367
46487
  interface Location {
46368
46488
  file: string;
46369
46489
  /** 1-based. */
@@ -46416,12 +46536,16 @@ var LSP_READ_OP_SIGNATURES = ` /** File contents as a raw string (no line numbe
46416
46536
  getSymbolBody(file: string, symbolPath: string): Promise<string>;
46417
46537
  /** Document symbol tree (file outline). Every path is a usable handle. */
46418
46538
  getSymbols(file: string): Promise<SymbolInfo[]>;
46419
- /** Workspace-wide symbol search; the index warms lazily, so early calls may be empty getSymbols(file) is exhaustive. */
46539
+ /** Workspace-wide symbol search; matches substrings, so filter for exact \`name\`. The index warms lazily early calls may be empty; getSymbols(file) is exhaustive. */
46420
46540
  findSymbol(query: string): Promise<WorkspaceSymbolInfo[]>;
46421
46541
  /** All references to a symbol across the workspace (incl. the declaration). */
46422
46542
  findReferences(file: string, symbolPath: string): Promise<Reference[]>;
46423
- /** Jump to a symbol's definition. */
46543
+ /** Jump to the definition of a symbol DEFINED in \`file\` — imports/callees in a body are not addressable; resolve those names with findSymbol. */
46424
46544
  goToDefinition(file: string, symbolPath: string): Promise<Location>;
46545
+ /** Functions that CALL this symbol — true calls only (no imports/re-exports), each attributed to its enclosing function with exact call sites. */
46546
+ incomingCalls(file: string, symbolPath: string): Promise<CallInfo[]>;
46547
+ /** Functions this symbol's body CALLS, each resolved to its definition (follows imports across modules); workspace functions only — library calls are omitted. */
46548
+ outgoingCalls(file: string, symbolPath: string): Promise<CallInfo[]>;
46425
46549
  /** Regex search across project files — escape metacharacters for literal text; optional second arg is a glob string. */
46426
46550
  searchText(pattern: string, glob?: string): Promise<SearchResult[]>;
46427
46551
  /** Project files matching a glob; no glob = all files, a directory name means everything under it. */
@@ -46455,6 +46579,17 @@ for (const file of files) {
46455
46579
  outline.push({ file, symbols: symbols.map((s) => s.kind + " " + s.path) });
46456
46580
  }
46457
46581
  outline;`
46582
+ },
46583
+ {
46584
+ title: "Trace the call graph",
46585
+ writes: false,
46586
+ code: `// Who calls findUser, and what does findUser call?
46587
+ const callers = await lsp.incomingCalls("src/users.ts", "findUser");
46588
+ const callees = await lsp.outgoingCalls("src/users.ts", "findUser");
46589
+ ({
46590
+ calledBy: callers.map((c) => c.file + "::" + c.symbolPath),
46591
+ calls: callees.map((c) => c.file + "::" + c.symbolPath),
46592
+ });`
46458
46593
  },
46459
46594
  {
46460
46595
  title: "Batch refactor: migrate every caller",
@@ -46490,10 +46625,16 @@ function renderLspTypes(readonly3) {
46490
46625
  const interfaces = readonly3 ? LSP_COMMON_INTERFACES : `${LSP_COMMON_INTERFACES}
46491
46626
 
46492
46627
  ${LSP_WRITE_INTERFACES}`;
46493
- const ops = readonly3 ? LSP_READ_OP_SIGNATURES : `${LSP_READ_OP_SIGNATURES}
46628
+ const helpOp = ` /** This full API reference as a string — call it if this description was truncated. */
46629
+ help(): Promise<string>;`;
46630
+ const ops = readonly3 ? `${LSP_READ_OP_SIGNATURES}
46631
+
46632
+ ${helpOp}` : `${LSP_READ_OP_SIGNATURES}
46494
46633
 
46495
46634
  // Write operations — buffered, applied atomically when the script succeeds.
46496
- ${LSP_WRITE_OP_SIGNATURES}`;
46635
+ ${LSP_WRITE_OP_SIGNATURES}
46636
+
46637
+ ${helpOp}`;
46497
46638
  return `declare const lsp: {
46498
46639
  ${ops}
46499
46640
  };
@@ -46501,6 +46642,8 @@ ${ops}
46501
46642
  ${interfaces}`;
46502
46643
  }
46503
46644
  var TEMPLATE = `Execute JavaScript to perform semantic code operations via LSP (TypeScript).
46645
+ If this description looks truncated, run the script \`await lsp.help()\` — it
46646
+ returns this complete reference. Ops — {{opInventory}}.
46504
46647
 
46505
46648
  Write one script that chains lsp.* calls — filter, loop, and branch in code
46506
46649
  instead of across many tool calls. The sandbox provides \`lsp\`,
@@ -46530,6 +46673,17 @@ script's last expression, JSON-serialized.
46530
46673
  the write ops.
46531
46674
  - \`searchText\` patterns are regexes — escape metacharacters for literal text:
46532
46675
  \`searchText("new NotFoundError\\\\(")\`.
46676
+ - To trace calls, use \`incomingCalls\`/\`outgoingCalls\` — they return only TRUE
46677
+ calls, attributed to the enclosing function. \`findReferences\` mixes calls
46678
+ with imports, re-exports, and type references.
46679
+ - \`goToDefinition\` only addresses symbols DEFINED in the given file — it cannot
46680
+ follow an imported or called name into another module. To resolve a name to
46681
+ its definition anywhere in the workspace, use \`findSymbol(name)\` and filter
46682
+ for an exact \`name\` match (it also returns substring matches), or
46683
+ \`outgoingCalls\` on the containing function.
46684
+ - \`result\` is capped at 50,000 chars (logs at 10,000). Aggregate INSIDE the
46685
+ script — return counts, top-N lists, and compact summaries, never a raw
46686
+ inventory of files/symbols/references.
46533
46687
  - File paths are relative to the workspace root; anything outside it is
46534
46688
  rejected.
46535
46689
  - Diagnostics cover files touched this session only, never the whole project.
@@ -46558,7 +46712,8 @@ ${example.code}
46558
46712
  \`\`\``).join(`
46559
46713
 
46560
46714
  `);
46561
- return TEMPLATE.replace("{{types}}", renderLspTypes(readonly3)).replace("{{writeSemantics}}", readonly3 ? READONLY_SEMANTICS : WRITE_SEMANTICS).replace("{{examples}}", examples);
46715
+ const opInventory = readonly3 ? `read: ${READ_OP_NAMES.join(", ")}` : `read: ${READ_OP_NAMES.join(", ")}; write: ${WRITE_OP_NAMES.join(", ")}`;
46716
+ return TEMPLATE.replace("{{opInventory}}", opInventory).replace("{{types}}", renderLspTypes(readonly3)).replace("{{writeSemantics}}", readonly3 ? READONLY_SEMANTICS : WRITE_SEMANTICS).replace("{{examples}}", examples);
46562
46717
  }
46563
46718
 
46564
46719
  // src/mcp-server.ts
@@ -46595,7 +46750,8 @@ function createExecuteRunner(deps) {
46595
46750
  const { result, logs } = await runSandbox(code, {
46596
46751
  lsp: deps.api,
46597
46752
  timeoutMs: deps.timeoutMs,
46598
- readonly: deps.readonly
46753
+ readonly: deps.readonly,
46754
+ helpText: deps.helpText ?? buildToolDescription(deps.readonly ?? false)
46599
46755
  });
46600
46756
  const flushed = buffer.flush();
46601
46757
  const changes = flushed.map((change) => ({
@@ -46659,20 +46815,22 @@ function createServer(options = {}) {
46659
46815
  warmup.catch(() => {
46660
46816
  return;
46661
46817
  });
46818
+ const description = buildToolDescription(readonly3);
46662
46819
  const { execute } = createExecuteRunner({
46663
46820
  api: api2,
46664
46821
  client,
46665
46822
  timeoutMs,
46666
46823
  warmup,
46667
46824
  rootDir,
46668
- readonly: readonly3
46825
+ readonly: readonly3,
46826
+ helpText: description
46669
46827
  });
46670
46828
  const server = new McpServer({
46671
46829
  name: "codemode-lsp",
46672
46830
  version: "0.1.0"
46673
46831
  });
46674
46832
  server.registerTool("execute", {
46675
- description: buildToolDescription(readonly3),
46833
+ description,
46676
46834
  inputSchema: {
46677
46835
  code: exports_external.string().describe("JavaScript to run in the LSP sandbox. The last expression is the return value.")
46678
46836
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codemode-lsp",
3
- "version": "0.1.2",
3
+ "version": "0.2.0",
4
4
  "description": "MCP server exposing a single code-mode tool backed by LSP",
5
5
  "keywords": [
6
6
  "mcp",