@wrongstack/plug-lsp 0.270.0 → 0.272.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 CHANGED
@@ -85,7 +85,7 @@ The primary interface for all LSP operations:
85
85
 
86
86
  ## Registered Tools
87
87
 
88
- The plugin registers 4 tools into WrongStack's tool system. These are kept because
88
+ The plugin registers 5 tools into WrongStack's tool system. These are kept because
89
89
  LSP provides genuinely unique data or capability the agent cannot replicate with
90
90
  basic tools (read, grep, edit) at comparable cost.
91
91
 
@@ -93,6 +93,7 @@ basic tools (read, grep, edit) at comparable cost.
93
93
  |---|---|---|
94
94
  | `lsp_diagnostics` | `auto` | Get type/lint diagnostics for a file or whole workspace |
95
95
  | `lsp_definition` | `auto` | Jump to the definition of a symbol (more precise than grep) |
96
+ | `lsp_completion` | `auto` | Semantic completions for a cursor location, including live editor content when provided |
96
97
  | `lsp_rename` | `confirm` | Safe semantic rename across the workspace |
97
98
  | `codebase-lsp-search` | `auto` | Fast symbol search via WrongStack's index, with LSP fallback |
98
99
 
@@ -236,7 +237,7 @@ packages/plug-lsp/src/
236
237
  │ ├── lsp-server.ts — per-server LSP client (one process per server)
237
238
  │ ├── connection.ts — JSON-RPC 2.0 over stdio transport
238
239
  │ └── lifecycle.ts — state machine for server lifecycle
239
- ├── tools/ — 4 LSP-backed agent tools (diagnostics, definition, rename, codebase-search)
240
+ ├── tools/ — 5 LSP-backed agent tools (diagnostics, definition, completion, rename, codebase-search)
240
241
  ├── registry.ts — manages all server instances
241
242
  ├── document-tracker.ts — tracks open/edited files across sessions
242
243
  ├── auto-discover.ts — PATH and node_modules discovery
package/dist/index.js CHANGED
@@ -593,7 +593,17 @@ var DocumentTracker = class {
593
593
  const absPath = this.resolve(filePath);
594
594
  const languageId = languageIdFor(absPath);
595
595
  if (!languageId) return;
596
- const text = knownText ?? await fs2.readFile(absPath, "utf8");
596
+ let text;
597
+ if (knownText !== void 0) {
598
+ text = knownText;
599
+ } else {
600
+ try {
601
+ text = await fs2.readFile(absPath, "utf8");
602
+ } catch (err) {
603
+ this.log.debug(`LSP tracker could not read file ${absPath}`, err);
604
+ return;
605
+ }
606
+ }
597
607
  let doc = this.docs.get(absPath);
598
608
  if (!doc) {
599
609
  doc = {
@@ -606,6 +616,14 @@ var DocumentTracker = class {
606
616
  };
607
617
  this.docs.set(absPath, doc);
608
618
  this.events?.emit("lsp.document.opened", { path: absPath, language: languageId });
619
+ } else if (knownText !== void 0 && knownText !== doc.text) {
620
+ doc.version++;
621
+ doc.text = knownText;
622
+ for (const server of this.registry().list()) {
623
+ if (server.state !== "ready" || !server.config.languages.includes(languageId)) continue;
624
+ if (!doc.serverNames.has(server.name)) continue;
625
+ server.notifyDidChange({ uri: doc.uri, version: doc.version }, knownText);
626
+ }
609
627
  }
610
628
  for (const server of this.registry().list()) {
611
629
  if (server.state !== "ready" || !server.config.languages.includes(languageId)) continue;
@@ -1012,6 +1030,9 @@ var LSPServer = class {
1012
1030
  async hover(params, timeoutMs, signal) {
1013
1031
  return await this.request("textDocument/hover", params, timeoutMs, signal);
1014
1032
  }
1033
+ async completion(params, timeoutMs, signal) {
1034
+ return await this.request("textDocument/completion", params, timeoutMs, signal);
1035
+ }
1015
1036
  async documentSymbol(params, timeoutMs, signal) {
1016
1037
  return await this.request("textDocument/documentSymbol", params, timeoutMs, signal);
1017
1038
  }
@@ -1898,6 +1919,9 @@ function formatCodebaseLspResults(output, cwd) {
1898
1919
  }
1899
1920
 
1900
1921
  // src/server/capabilities.ts
1922
+ function supportsCompletion(cap) {
1923
+ return !!cap.completionProvider;
1924
+ }
1901
1925
  function supportsDefinition(cap) {
1902
1926
  return !!cap.definitionProvider;
1903
1927
  }
@@ -2116,18 +2140,6 @@ function deduplicateByKey(items) {
2116
2140
  });
2117
2141
  }
2118
2142
 
2119
- // src/formatters/location.ts
2120
- function formatLocations(locations, cwd, limit = 100) {
2121
- if (!locations || locations.length === 0) return "No locations found.";
2122
- const lines = locations.slice(0, limit).map((loc) => {
2123
- const uri = "uri" in loc ? loc.uri : loc.targetUri;
2124
- const range = "range" in loc ? loc.range : loc.targetSelectionRange;
2125
- return `${displayPath(uriToPath(uri), cwd)}:${range.start.line + 1}:${range.start.character + 1}`;
2126
- });
2127
- if (locations.length > limit) lines.push(`... truncated ${locations.length - limit} more`);
2128
- return lines.join("\n");
2129
- }
2130
-
2131
2143
  // src/position.ts
2132
2144
  function humanToLSP(content, pos) {
2133
2145
  const lines = splitLines(content);
@@ -2153,6 +2165,143 @@ function clamp(n, min, max) {
2153
2165
  return Math.max(min, Math.min(max, n));
2154
2166
  }
2155
2167
 
2168
+ // src/tools/completion.ts
2169
+ function createCompletionTool(deps) {
2170
+ return {
2171
+ name: "lsp_completion",
2172
+ description: "Get semantic code completions from a configured language server.",
2173
+ usageHint: 'Use for context-aware completion at a cursor location. Lines and columns are 1-based; pass trigger_character for member access like ".".',
2174
+ inputSchema: {
2175
+ type: "object",
2176
+ properties: {
2177
+ path: { type: "string" },
2178
+ line: { type: "integer" },
2179
+ character: { type: "integer" },
2180
+ content: { type: "string", maxLength: 5e5 },
2181
+ limit: { type: "integer", minimum: 1, maximum: 100 },
2182
+ trigger_character: { type: "string" },
2183
+ format: { type: "string", enum: ["text", "json"] }
2184
+ },
2185
+ required: ["path", "line", "character"]
2186
+ },
2187
+ permission: "auto",
2188
+ mutating: false,
2189
+ timeoutMs: LSP_CONSTANTS.TOOL_TIMEOUT_MS,
2190
+ maxOutputBytes: 32768,
2191
+ async execute(input, ctx, opts) {
2192
+ try {
2193
+ const file = resolveInputPath(input.path, ctx);
2194
+ const server = await requireServer(deps.registry, file, opts.signal);
2195
+ if (server.capabilities && !supportsCompletion(server.capabilities)) {
2196
+ throw new LSPError(
2197
+ "LSP_CAPABILITY_MISSING" /* CapabilityMissing */,
2198
+ `Server "${server.name}" does not support completion`
2199
+ );
2200
+ }
2201
+ const content = typeof input.content === "string" ? input.content : await readDocumentContent(file, deps.tracker);
2202
+ await deps.tracker.open(file, content);
2203
+ const position = humanToLSP(content, { line: input.line, character: input.character });
2204
+ const result = await server.completion(
2205
+ {
2206
+ textDocument: { uri: pathToUri(file) },
2207
+ position,
2208
+ context: input.trigger_character ? { triggerKind: 2, triggerCharacter: input.trigger_character } : { triggerKind: 1 }
2209
+ },
2210
+ LSP_CONSTANTS.TOOL_TIMEOUT_MS,
2211
+ opts.signal
2212
+ );
2213
+ const items = collectCompletionItems(result, Math.min(input.limit ?? 25, 100));
2214
+ if (input.format === "json") {
2215
+ return JSON.stringify({
2216
+ items: items.map((item) => ({
2217
+ label: item.label,
2218
+ insertText: item.insertText ?? item.label,
2219
+ kind: item.kind ? completionKindName(item.kind) : void 0,
2220
+ detail: compact(item.detail),
2221
+ documentation: compact(documentationText(item.documentation))
2222
+ }))
2223
+ });
2224
+ }
2225
+ return formatCompletionItems(items, result);
2226
+ } catch (err) {
2227
+ return stringifyToolError(err);
2228
+ }
2229
+ }
2230
+ };
2231
+ }
2232
+ function collectCompletionItems(result, limit) {
2233
+ const items = Array.isArray(result) ? result : result?.items ?? [];
2234
+ return items.slice(0, limit);
2235
+ }
2236
+ function formatCompletionItems(visibleItems, result) {
2237
+ const items = Array.isArray(result) ? result : result?.items ?? [];
2238
+ if (items.length === 0) return "No completions found.";
2239
+ const lines = visibleItems.map((item, index) => {
2240
+ const label = item.label || item.insertText || "(unnamed)";
2241
+ const kind = item.kind ? completionKindName(item.kind) : "Completion";
2242
+ const detail = compact(item.detail);
2243
+ const docs = compact(documentationText(item.documentation));
2244
+ const suffix = [detail, docs].filter(Boolean).join(" \u2014 ");
2245
+ return `${index + 1}. ${label} [${kind}]${suffix ? ` \u2014 ${suffix}` : ""}`;
2246
+ });
2247
+ if (items.length > visibleItems.length) {
2248
+ lines.push(`... truncated ${items.length - visibleItems.length} more`);
2249
+ }
2250
+ return lines.join("\n");
2251
+ }
2252
+ function documentationText(value) {
2253
+ if (!value) return void 0;
2254
+ if (typeof value === "string") return value;
2255
+ return value.value;
2256
+ }
2257
+ function compact(value) {
2258
+ const cleaned = value?.replace(/\s*\r?\n\s*/g, " ").trim();
2259
+ if (!cleaned) return void 0;
2260
+ return cleaned.length <= 160 ? cleaned : `${cleaned.slice(0, 157)}...`;
2261
+ }
2262
+ function completionKindName(kind) {
2263
+ const names = {
2264
+ 1: "Text",
2265
+ 2: "Method",
2266
+ 3: "Function",
2267
+ 4: "Constructor",
2268
+ 5: "Field",
2269
+ 6: "Variable",
2270
+ 7: "Class",
2271
+ 8: "Interface",
2272
+ 9: "Module",
2273
+ 10: "Property",
2274
+ 11: "Unit",
2275
+ 12: "Value",
2276
+ 13: "Enum",
2277
+ 14: "Keyword",
2278
+ 15: "Snippet",
2279
+ 16: "Color",
2280
+ 17: "File",
2281
+ 18: "Reference",
2282
+ 19: "Folder",
2283
+ 20: "EnumMember",
2284
+ 21: "Constant",
2285
+ 22: "Struct",
2286
+ 23: "Event",
2287
+ 24: "Operator",
2288
+ 25: "TypeParameter"
2289
+ };
2290
+ return names[kind] ?? `Kind ${kind}`;
2291
+ }
2292
+
2293
+ // src/formatters/location.ts
2294
+ function formatLocations(locations, cwd, limit = 100) {
2295
+ if (!locations || locations.length === 0) return "No locations found.";
2296
+ const lines = locations.slice(0, limit).map((loc) => {
2297
+ const uri = "uri" in loc ? loc.uri : loc.targetUri;
2298
+ const range = "range" in loc ? loc.range : loc.targetSelectionRange;
2299
+ return `${displayPath(uriToPath(uri), cwd)}:${range.start.line + 1}:${range.start.character + 1}`;
2300
+ });
2301
+ if (locations.length > limit) lines.push(`... truncated ${locations.length - limit} more`);
2302
+ return lines.join("\n");
2303
+ }
2304
+
2156
2305
  // src/tools/definition.ts
2157
2306
  function createDefinitionTool(deps) {
2158
2307
  return {
@@ -2372,6 +2521,7 @@ function makeLSPTools(deps) {
2372
2521
  return [
2373
2522
  createDiagnosticsTool(deps),
2374
2523
  createDefinitionTool(deps),
2524
+ createCompletionTool(deps),
2375
2525
  createCodebaseLspSearchTool(deps),
2376
2526
  createRenameTool(deps)
2377
2527
  ];
@@ -2414,7 +2564,7 @@ var plugin = {
2414
2564
  void tracker.forceCloseAll().finally(() => registry.shutdown());
2415
2565
  }),
2416
2566
  api.events.on("tool.executed", (event) => {
2417
- void tracker.handleToolExecuted(event);
2567
+ void tracker.handleToolExecuted(event).catch((err) => api.log.debug("LSP tracker failed to handle tool event", err));
2418
2568
  })
2419
2569
  ];
2420
2570
  teardownState = {