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.
- package/README.md +11 -5
- package/dist/index.js +172 -14
- 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
|
|
68
|
+
The API surface is 18 functions plus `getDiagnostics`: 10 read ops (`readFile`,
|
|
69
69
|
`getSymbolBody`, `getSymbols`, `findSymbol`, `findReferences`,
|
|
70
|
-
`goToDefinition`, `searchText`, `listFiles`)
|
|
71
|
-
`
|
|
72
|
-
`deleteSymbol`, `writeFile`, `deleteFile`).
|
|
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
|
-
|
|
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
|
-
|
|
39516
|
+
chain.push(candidate);
|
|
39517
|
+
visit(candidate.children);
|
|
39518
|
+
return;
|
|
39515
39519
|
}
|
|
39516
|
-
return;
|
|
39517
39520
|
}
|
|
39518
|
-
|
|
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
|
|
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
|
|
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;
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
}
|