@mrclrchtr/supi-lsp 1.6.0 → 1.7.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-core",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "SuPi core — shared infrastructure for SuPi extensions (XML context tags, config system)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,7 +20,8 @@
20
20
  ],
21
21
  "peerDependencies": {
22
22
  "@earendil-works/pi-coding-agent": "*",
23
- "@earendil-works/pi-tui": "*"
23
+ "@earendil-works/pi-tui": "*",
24
+ "typebox": "*"
24
25
  },
25
26
  "peerDependenciesMeta": {
26
27
  "@earendil-works/pi-coding-agent": {
@@ -28,6 +29,9 @@
28
29
  },
29
30
  "@earendil-works/pi-tui": {
30
31
  "optional": true
32
+ },
33
+ "typebox": {
34
+ "optional": true
31
35
  }
32
36
  },
33
37
  "main": "src/api.ts",
@@ -1,11 +1,12 @@
1
1
  // supi-core — shared infrastructure for SuPi extensions.
2
2
  // Provides XML context tag wrapping, unified config system, context-message utilities,
3
- // and settings registry for supi-wide TUI settings.
3
+ // settings registry for supi-wide TUI settings, and a shared tool-spec/registration framework.
4
4
 
5
5
  export type { SupiConfigLocation, SupiConfigOptions } from "./config/config.ts";
6
6
  export {
7
7
  loadSupiConfig,
8
8
  loadSupiConfigForScope,
9
+ readJsonFile,
9
10
  removeSupiConfigKey,
10
11
  writeSupiConfig,
11
12
  } from "./config/config.ts";
@@ -83,3 +84,13 @@ export {
83
84
  signalWaiting,
84
85
  WAITING_SYMBOL,
85
86
  } from "./terminal.ts";
87
+ export type { SuiPiToolPromptSurface, SuiPiToolSpec, ToolExecuteFn } from "./tool-framework.ts";
88
+ export {
89
+ CharacterParam,
90
+ derivePromptSurface,
91
+ FileParam,
92
+ LineParam,
93
+ MaxResultsParam,
94
+ registerSuiPiTools,
95
+ SymbolParam,
96
+ } from "./tool-framework.ts";
@@ -20,7 +20,7 @@ function getProjectConfigPath(cwd: string): string {
20
20
  return path.join(cwd, PROJECT_CONFIG_DIR, CONFIG_FILE);
21
21
  }
22
22
 
23
- function readJsonFile(filePath: string): Record<string, unknown> | null {
23
+ export function readJsonFile(filePath: string): Record<string, unknown> | null {
24
24
  let content: string;
25
25
  try {
26
26
  content = fs.readFileSync(filePath, "utf-8");
@@ -1,11 +1,12 @@
1
1
  // supi-core — shared infrastructure for SuPi extensions.
2
2
  // Provides XML context tag wrapping, unified config system, context-message utilities,
3
- // and settings registry for supi-wide TUI settings.
3
+ // settings registry for supi-wide TUI settings, and a shared tool-spec/registration framework.
4
4
 
5
5
  export type { SupiConfigLocation, SupiConfigOptions } from "./config/config.ts";
6
6
  export {
7
7
  loadSupiConfig,
8
8
  loadSupiConfigForScope,
9
+ readJsonFile,
9
10
  removeSupiConfigKey,
10
11
  writeSupiConfig,
11
12
  } from "./config/config.ts";
@@ -83,3 +84,13 @@ export {
83
84
  signalWaiting,
84
85
  WAITING_SYMBOL,
85
86
  } from "./terminal.ts";
87
+ export type { SuiPiToolPromptSurface, SuiPiToolSpec, ToolExecuteFn } from "./tool-framework.ts";
88
+ export {
89
+ CharacterParam,
90
+ derivePromptSurface,
91
+ FileParam,
92
+ LineParam,
93
+ MaxResultsParam,
94
+ registerSuiPiTools,
95
+ SymbolParam,
96
+ } from "./tool-framework.ts";
@@ -0,0 +1,116 @@
1
+ // Shared tool framework for SuPi extensions.
2
+ //
3
+ // Provides a standard ToolSpec→PromptSurface→registerTool pipeline so
4
+ // individual packages do not duplicate spec interfaces, guidance derivation,
5
+ // registration loops, or common TypeBox parameter schemas.
6
+
7
+ import type {
8
+ AgentToolResult,
9
+ AgentToolUpdateCallback,
10
+ ExtensionAPI,
11
+ ExtensionContext,
12
+ } from "@earendil-works/pi-coding-agent";
13
+ import { type TSchema, Type } from "typebox";
14
+
15
+ // ---------------------------------------------------------------------------
16
+ // Types
17
+ // ---------------------------------------------------------------------------
18
+
19
+ /** Minimum contract for a SuPi tool definition. */
20
+ export interface SuiPiToolSpec {
21
+ name: string;
22
+ label: string;
23
+ description: string;
24
+ promptSnippet: string;
25
+ promptGuidelines: string[];
26
+ parameters: TSchema;
27
+ }
28
+
29
+ /** Derived prompt surface — what pi flattens into the system prompt. */
30
+ export interface SuiPiToolPromptSurface {
31
+ description: string;
32
+ promptSnippet: string;
33
+ promptGuidelines: string[];
34
+ }
35
+
36
+ // ---------------------------------------------------------------------------
37
+ // Guidance derivation
38
+ // ---------------------------------------------------------------------------
39
+
40
+ /**
41
+ * Static derivation: copies spec fields into a prompt surface.
42
+ *
43
+ * Packages that need dynamic guidance (e.g. server-coverage injection) should
44
+ * build their own surfaces, optionally starting from the output of this helper.
45
+ */
46
+ export function derivePromptSurface(spec: SuiPiToolSpec): SuiPiToolPromptSurface {
47
+ return {
48
+ description: spec.description,
49
+ promptSnippet: spec.promptSnippet,
50
+ promptGuidelines: [...spec.promptGuidelines],
51
+ };
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Registration
56
+ // ---------------------------------------------------------------------------
57
+
58
+ // biome-ignore lint/complexity/useMaxParams: matches pi ToolDefinition.execute signature
59
+ export type ToolExecuteFn = (
60
+ toolCallId: string,
61
+ params: unknown,
62
+ signal: AbortSignal | undefined,
63
+ onUpdate: AgentToolUpdateCallback<Record<string, unknown>> | undefined,
64
+ ctx: ExtensionContext,
65
+ ) => Promise<AgentToolResult<Record<string, unknown>>>;
66
+
67
+ /**
68
+ * Register a set of tools from specs + pre-derived surfaces.
69
+ *
70
+ * `createExecute` receives the spec and returns a pi-compatible execute
71
+ * function. This keeps execute-logic package-local while the framework owns
72
+ * the declarative surface and registration boilerplate.
73
+ */
74
+ export function registerSuiPiTools(
75
+ pi: ExtensionAPI,
76
+ specs: readonly SuiPiToolSpec[],
77
+ surfaces: Record<string, SuiPiToolPromptSurface>,
78
+ createExecute: (spec: SuiPiToolSpec) => ToolExecuteFn,
79
+ ): void {
80
+ for (const spec of specs) {
81
+ const surface = surfaces[spec.name];
82
+ pi.registerTool({
83
+ name: spec.name,
84
+ label: spec.label,
85
+ description: surface?.description ?? spec.description,
86
+ promptSnippet: surface?.promptSnippet ?? spec.promptSnippet,
87
+ promptGuidelines: surface?.promptGuidelines ?? [...spec.promptGuidelines],
88
+ parameters: spec.parameters,
89
+ execute: createExecute(spec),
90
+ });
91
+ }
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Shared parameter builders
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /** File path (relative or absolute). */
99
+ export const FileParam = Type.String({ description: "File path (relative or absolute)" });
100
+
101
+ /** 1-based line number. */
102
+ export const LineParam = Type.Number({ description: "1-based line number", minimum: 1 });
103
+
104
+ /** 1-based character column (UTF-16). */
105
+ export const CharacterParam = Type.Number({
106
+ description: "1-based column number (UTF-16)",
107
+ minimum: 1,
108
+ });
109
+
110
+ /** Symbol name for discovery-based resolution. */
111
+ export const SymbolParam = Type.String({
112
+ description: "Symbol name for discovery-based resolution",
113
+ });
114
+
115
+ /** Maximum results to return. */
116
+ export const MaxResultsParam = Type.Number({ description: "Maximum results to return" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrclrchtr/supi-lsp",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "SuPi LSP extension — Language Server Protocol integration for pi",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,10 +23,16 @@
23
23
  "dependencies": {
24
24
  "ignore": "^7.0.5",
25
25
  "typescript": "6.0.3",
26
- "@mrclrchtr/supi-core": "1.6.0"
26
+ "vscode-jsonrpc": "^8.2.1",
27
+ "vscode-languageserver-protocol": "^3.17.5",
28
+ "vscode-languageserver-types": "^3.17.5",
29
+ "@mrclrchtr/supi-core": "1.7.0"
27
30
  },
28
31
  "bundledDependencies": [
29
- "@mrclrchtr/supi-core"
32
+ "@mrclrchtr/supi-core",
33
+ "vscode-jsonrpc",
34
+ "vscode-languageserver-protocol",
35
+ "vscode-languageserver-types"
30
36
  ],
31
37
  "peerDependencies": {
32
38
  "@earendil-works/pi-ai": "*",
@@ -173,7 +173,13 @@ export class LspClient {
173
173
  setTimeout(() => reject(new Error("shutdown timeout")), SHUTDOWN_TIMEOUT_MS),
174
174
  ),
175
175
  ]);
176
- this.rpc.sendNotification("exit");
176
+ // Flush the final exit notification before disposing the transport.
177
+ await Promise.race([
178
+ this.rpc.sendNotification("exit"),
179
+ new Promise((_, reject) =>
180
+ setTimeout(() => reject(new Error("exit notification timeout")), SHUTDOWN_TIMEOUT_MS),
181
+ ),
182
+ ]);
177
183
  } catch {
178
184
  // Timeout or error — force kill
179
185
  }
@@ -333,10 +339,7 @@ export class LspClient {
333
339
 
334
340
  /** Check if server supports pull diagnostics. */
335
341
  get hasDiagnosticProvider(): boolean {
336
- return (
337
- this.capabilities?.diagnosticProvider !== undefined &&
338
- this.capabilities.diagnosticProvider !== false
339
- );
342
+ return this.capabilities?.diagnosticProvider !== undefined;
340
343
  }
341
344
 
342
345
  /** Notify the server that watched workspace files changed. */
@@ -1,16 +1,18 @@
1
- // JSON-RPC 2.0 transport over stdio with Content-Length header framing.
1
+ // JSON-RPC 2.0 transport thin wrapper around vscode-jsonrpc.
2
+ // Handles Content-Length framing, request/response correlation, timeouts,
3
+ // and notification/request dispatching through vscode-jsonrpc's MessageConnection.
2
4
 
3
5
  import type { Readable, Writable } from "node:stream";
4
- import type {
5
- JsonRpcId,
6
- JsonRpcMessage,
7
- JsonRpcNotification,
8
- JsonRpcRequest,
9
- JsonRpcResponse,
10
- } from "../config/types.ts";
11
-
12
- const CONTENT_LENGTH = "Content-Length: ";
13
- const HEADER_DELIMITER = "\r\n\r\n";
6
+ import {
7
+ CancellationTokenSource,
8
+ createMessageConnection,
9
+ type MessageConnection,
10
+ NullLogger,
11
+ ResponseError,
12
+ StreamMessageReader,
13
+ StreamMessageWriter,
14
+ } from "vscode-jsonrpc/node";
15
+
14
16
  const DEFAULT_TIMEOUT_MS = 30_000;
15
17
 
16
18
  // ── Types ─────────────────────────────────────────────────────────────
@@ -18,29 +20,15 @@ const DEFAULT_TIMEOUT_MS = 30_000;
18
20
  export type NotificationHandler = (method: string, params: unknown) => void;
19
21
  export type RequestHandler = (method: string, params: unknown) => Promise<unknown> | unknown;
20
22
 
21
- interface PendingRequest {
22
- resolve: (result: unknown) => void;
23
- reject: (error: Error) => void;
24
- timer: ReturnType<typeof setTimeout>;
25
- }
23
+ /** Re-export ResponseError so callers don't need a separate vscode-jsonrpc import. */
24
+ const JsonRpcRequestError = ResponseError;
26
25
 
27
- export class JsonRpcRequestError extends Error {
28
- constructor(
29
- readonly code: number,
30
- message: string,
31
- readonly data?: unknown,
32
- ) {
33
- super(message);
34
- this.name = "JsonRpcRequestError";
35
- }
36
- }
26
+ export { JsonRpcRequestError };
37
27
 
38
28
  // ── JsonRpcClient ─────────────────────────────────────────────────────
39
29
 
40
30
  export class JsonRpcClient {
41
- private nextId = 1;
42
- private buffer = Buffer.alloc(0);
43
- private pending = new Map<JsonRpcId, PendingRequest>();
31
+ private connection: MessageConnection | null = null;
44
32
  private notificationHandler: NotificationHandler | null = null;
45
33
  private requestHandler: RequestHandler | null = null;
46
34
  private closed = false;
@@ -52,9 +40,31 @@ export class JsonRpcClient {
52
40
  options?: { timeoutMs?: number },
53
41
  ) {
54
42
  this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
55
- this.input.on("data", (chunk: Buffer) => this.onData(chunk));
56
- this.input.on("end", () => this.onClose());
57
- this.input.on("error", () => this.onClose());
43
+
44
+ const reader = new StreamMessageReader(this.input);
45
+ const writer = new StreamMessageWriter(this.output);
46
+
47
+ this.connection = createMessageConnection(reader, writer, NullLogger);
48
+
49
+ // Register catch-all notification handler
50
+ this.connection.onNotification((method, params) => {
51
+ this.notificationHandler?.(method, params);
52
+ });
53
+
54
+ // Register catch-all request handler for server-initiated requests
55
+ this.connection.onRequest(async (method, params, _token) => {
56
+ if (!this.requestHandler) {
57
+ throw new JsonRpcRequestError(-32601, `Method not found: ${method}`);
58
+ }
59
+ return this.requestHandler(method, params);
60
+ });
61
+
62
+ // Handle connection close
63
+ this.connection.onClose(() => {
64
+ this.closed = true;
65
+ });
66
+
67
+ this.connection.listen();
58
68
  }
59
69
 
60
70
  /** Register a handler for server notifications (no id). */
@@ -73,176 +83,55 @@ export class JsonRpcClient {
73
83
  params?: unknown,
74
84
  options?: { timeoutMs?: number },
75
85
  ): Promise<unknown> {
76
- if (this.closed) {
86
+ if (this.closed || !this.connection) {
77
87
  return Promise.reject(new Error("JSON-RPC client is closed"));
78
88
  }
79
89
 
80
- const id = this.nextId++;
81
90
  const timeoutMs = options?.timeoutMs ?? this.timeoutMs;
82
- const promise = new Promise<unknown>((resolve, reject) => {
83
- const timer = setTimeout(() => {
84
- this.pending.delete(id);
85
- reject(new Error(`Request ${method} (id=${id}) timed out after ${timeoutMs}ms`));
86
- }, timeoutMs);
87
-
88
- this.pending.set(id, { resolve, reject, timer });
89
- });
90
-
91
- const msg: JsonRpcRequest = { jsonrpc: "2.0", id, method, params };
92
- this.writeMessage(msg);
93
-
94
- // Prevent unhandled rejection when dispose() rejects orphaned promises
91
+ const tokenSource = new CancellationTokenSource();
92
+
93
+ const timer = setTimeout(() => tokenSource.cancel(), timeoutMs);
94
+
95
+ const request = this.connection.sendRequest(method, params, tokenSource.token);
96
+
97
+ // Race the request against a timeout so callers don't hang forever.
98
+ // The CancellationToken is also passed to sendRequest so the connection
99
+ // can short-circuit writes and cleanup when the token fires.
100
+ const promise = Promise.race([
101
+ request,
102
+ new Promise<never>((_, reject) =>
103
+ setTimeout(
104
+ () => reject(new Error(`Request ${method} timed out after ${timeoutMs}ms`)),
105
+ timeoutMs,
106
+ ),
107
+ ),
108
+ ]).finally(() => clearTimeout(timer));
109
+
110
+ // Prevent unhandled rejection when dispose() cancels requests
95
111
  promise.catch(() => {});
96
112
  return promise;
97
113
  }
98
114
 
99
- /** Send a notification (no response expected). */
100
- sendNotification(method: string, params?: unknown): void {
101
- if (this.closed) return;
102
- const msg: JsonRpcNotification = { jsonrpc: "2.0", method, params };
103
- this.writeMessage(msg);
115
+ /**
116
+ * Send a notification (no response expected).
117
+ *
118
+ * Returns the underlying write promise so ordering-sensitive cleanup paths
119
+ * can await the final flush. A no-op catch is still attached to prevent
120
+ * unhandled rejections when callers intentionally fire-and-forget.
121
+ */
122
+ sendNotification(method: string, params?: unknown): Promise<void> {
123
+ if (this.closed || !this.connection) return Promise.resolve();
124
+ const promise = this.connection.sendNotification(method, params);
125
+ promise.catch(() => {});
126
+ return promise;
104
127
  }
105
128
 
106
- /** Clean up all pending requests. */
129
+ /** Clean up the connection. */
107
130
  dispose(): void {
108
131
  this.closed = true;
109
- for (const [id, p] of this.pending) {
110
- clearTimeout(p.timer);
111
- p.reject(new Error("JSON-RPC client disposed"));
112
- this.pending.delete(id);
113
- }
114
- }
115
-
116
- // ── Private ───────────────────────────────────────────────────────
117
-
118
- private writeMessage(msg: JsonRpcMessage): void {
119
- const body = JSON.stringify(msg);
120
- const contentLength = Buffer.byteLength(body, "utf-8");
121
- const header = `${CONTENT_LENGTH}${contentLength}${HEADER_DELIMITER}`;
122
- this.output.write(header + body);
123
- }
124
-
125
- private onData(chunk: Buffer): void {
126
- this.buffer = Buffer.concat([this.buffer, chunk]);
127
- this.processBuffer();
128
- }
129
-
130
- private processBuffer(): void {
131
- while (true) {
132
- // Look for header delimiter
133
- const headerEnd = this.buffer.indexOf(HEADER_DELIMITER);
134
- if (headerEnd === -1) return;
135
-
136
- // Parse Content-Length from headers
137
- const headerText = this.buffer.subarray(0, headerEnd).toString("utf-8");
138
- const contentLength = parseContentLength(headerText);
139
- if (contentLength === null) {
140
- // Malformed header — skip past delimiter and try again
141
- this.buffer = this.buffer.subarray(headerEnd + HEADER_DELIMITER.length);
142
- continue;
143
- }
144
-
145
- // Check if we have the full body
146
- const bodyStart = headerEnd + HEADER_DELIMITER.length;
147
- const messageEnd = bodyStart + contentLength;
148
- if (this.buffer.length < messageEnd) {
149
- return; // Need more data — partial message
150
- }
151
-
152
- // Extract and parse the body
153
- const body = this.buffer.subarray(bodyStart, messageEnd).toString("utf-8");
154
- this.buffer = this.buffer.subarray(messageEnd);
155
-
156
- try {
157
- const msg = JSON.parse(body) as JsonRpcMessage;
158
- this.handleMessage(msg);
159
- } catch {
160
- // Malformed JSON — skip
161
- }
162
- }
163
- }
164
-
165
- private handleMessage(msg: JsonRpcMessage): void {
166
- // Response (has id, has result or error)
167
- if ("id" in msg && msg.id != null && ("result" in msg || "error" in msg)) {
168
- const response = msg as JsonRpcResponse;
169
- const id = response.id;
170
- if (id === null) return;
171
- const pending = this.pending.get(id);
172
- if (pending) {
173
- this.pending.delete(id);
174
- clearTimeout(pending.timer);
175
- if (response.error) {
176
- pending.reject(new Error(`LSP error ${response.error.code}: ${response.error.message}`));
177
- } else {
178
- pending.resolve(response.result);
179
- }
180
- }
181
- return;
182
- }
183
-
184
- // Notification (no id)
185
- if ("method" in msg && !("id" in msg)) {
186
- const notification = msg as JsonRpcNotification;
187
- this.notificationHandler?.(notification.method, notification.params);
188
- return;
189
- }
190
-
191
- // Request from server (has id + method)
192
- if ("method" in msg && "id" in msg && msg.id != null) {
193
- void this.handleInboundRequest(msg as JsonRpcRequest);
194
- }
195
- }
196
-
197
- private async handleInboundRequest(request: JsonRpcRequest): Promise<void> {
198
- if (this.closed) return;
199
-
200
- try {
201
- if (!this.requestHandler) {
202
- throw new JsonRpcRequestError(-32601, `Method not found: ${request.method}`);
203
- }
204
-
205
- const result = await this.requestHandler(request.method, request.params);
206
- this.writeMessage({
207
- jsonrpc: "2.0",
208
- id: request.id,
209
- result: result ?? null,
210
- } satisfies JsonRpcResponse);
211
- } catch (error) {
212
- const failure =
213
- error instanceof JsonRpcRequestError
214
- ? error
215
- : new JsonRpcRequestError(-32603, error instanceof Error ? error.message : String(error));
216
- this.writeMessage({
217
- jsonrpc: "2.0",
218
- id: request.id,
219
- error: {
220
- code: failure.code,
221
- message: failure.message,
222
- ...(failure.data !== undefined ? { data: failure.data } : {}),
223
- },
224
- } satisfies JsonRpcResponse);
225
- }
226
- }
227
-
228
- private onClose(): void {
229
- this.closed = true;
230
- for (const [id, p] of this.pending) {
231
- clearTimeout(p.timer);
232
- p.reject(new Error("JSON-RPC connection closed"));
233
- this.pending.delete(id);
234
- }
235
- }
236
- }
237
-
238
- // ── Helpers ───────────────────────────────────────────────────────────
239
-
240
- function parseContentLength(header: string): number | null {
241
- for (const line of header.split("\r\n")) {
242
- if (line.startsWith(CONTENT_LENGTH)) {
243
- const value = parseInt(line.slice(CONTENT_LENGTH.length), 10);
244
- if (Number.isFinite(value) && value >= 0) return value;
132
+ if (this.connection) {
133
+ this.connection.dispose();
134
+ this.connection = null;
245
135
  }
246
136
  }
247
- return null;
248
137
  }
@@ -0,0 +1,38 @@
1
+ // SuPi-specific server configuration types — not part of the LSP specification.
2
+ // These are our own types for server discovery, configuration, and status tracking.
3
+
4
+ export interface ServerConfig {
5
+ command: string;
6
+ args?: string[];
7
+ fileTypes: string[];
8
+ rootMarkers: string[];
9
+ enabled?: boolean;
10
+ initializationOptions?: unknown;
11
+ }
12
+
13
+ /** LSP configuration keyed by language name (e.g. `typescript`, `python`). */
14
+ export interface LspConfig {
15
+ servers: Record<string, ServerConfig>;
16
+ }
17
+
18
+ export interface DetectedProjectServer {
19
+ name: string;
20
+ root: string;
21
+ fileTypes: string[];
22
+ }
23
+
24
+ export interface ProjectServerInfo extends DetectedProjectServer {
25
+ status: "running" | "error" | "unavailable";
26
+ supportedActions: string[];
27
+ openFiles: string[];
28
+ }
29
+
30
+ /** A language whose source files are present but the server binary is missing. */
31
+ export interface MissingServer {
32
+ /** Language name (e.g. "python", "rust"). */
33
+ name: string;
34
+ /** Server command that was not found on PATH. */
35
+ command: string;
36
+ /** File extensions found in the project (subset of server.fileTypes). */
37
+ foundExtensions: string[];
38
+ }
@@ -1,353 +1,56 @@
1
- // LSP protocol types — minimal subset needed for our client.
2
- // Based on the Language Server Protocol specification.
3
- // biome-ignore-all lint/nursery/noExcessiveLinesPerFile: protocol types are intentionally centralized in one catalog file.
4
-
5
- // ── Positions & Ranges ────────────────────────────────────────────────
6
-
7
- /** 0-based line and character offset. */
8
- export interface Position {
9
- line: number;
10
- character: number;
11
- }
12
-
13
- export interface Range {
14
- start: Position;
15
- end: Position;
16
- }
17
-
18
- export interface Location {
19
- uri: string;
20
- range: Range;
21
- }
22
-
23
- export interface LocationLink {
24
- originSelectionRange?: Range;
25
- targetUri: string;
26
- targetRange: Range;
27
- targetSelectionRange: Range;
28
- }
29
-
30
- // ── Text Edits ────────────────────────────────────────────────────────
31
-
32
- export interface TextEdit {
33
- range: Range;
34
- newText: string;
35
- }
36
-
37
- export interface TextDocumentEdit {
38
- textDocument: { uri: string; version?: number | null };
39
- edits: TextEdit[];
40
- }
41
-
42
- export interface WorkspaceEdit {
43
- changes?: Record<string, TextEdit[]>;
44
- documentChanges?: TextDocumentEdit[];
45
- }
46
-
47
- // ── Diagnostics ───────────────────────────────────────────────────────
48
-
49
- export const DiagnosticSeverity = {
50
- Error: 1,
51
- Warning: 2,
52
- Information: 3,
53
- Hint: 4,
54
- } as const;
55
- export type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof DiagnosticSeverity];
56
-
57
- export interface DiagnosticRelatedInformation {
58
- location: Location;
59
- message: string;
60
- }
61
-
62
- export interface Diagnostic {
63
- range: Range;
64
- severity?: DiagnosticSeverity;
65
- code?: number | string;
66
- codeDescription?: { href: string };
67
- source?: string;
68
- message: string;
69
- relatedInformation?: DiagnosticRelatedInformation[];
70
- }
71
-
72
- // ── Hover ─────────────────────────────────────────────────────────────
73
-
74
- export interface MarkupContent {
75
- kind: "plaintext" | "markdown";
76
- value: string;
77
- }
78
-
79
- export type MarkedString = string | { language: string; value: string };
80
-
81
- export interface Hover {
82
- contents: MarkupContent | MarkedString | MarkedString[];
83
- range?: Range;
84
- }
85
-
86
- // ── Symbols ───────────────────────────────────────────────────────────
87
-
88
- export const SymbolKind = {
89
- File: 1,
90
- Module: 2,
91
- Namespace: 3,
92
- Package: 4,
93
- Class: 5,
94
- Method: 6,
95
- Property: 7,
96
- Field: 8,
97
- Constructor: 9,
98
- Enum: 10,
99
- Interface: 11,
100
- Function: 12,
101
- Variable: 13,
102
- Constant: 14,
103
- String: 15,
104
- Number: 16,
105
- Boolean: 17,
106
- Array: 18,
107
- Object: 19,
108
- Key: 20,
109
- Null: 21,
110
- EnumMember: 22,
111
- Struct: 23,
112
- Event: 24,
113
- Operator: 25,
114
- TypeParameter: 26,
115
- } as const;
116
- export type SymbolKind = (typeof SymbolKind)[keyof typeof SymbolKind];
117
-
118
- export interface DocumentSymbol {
119
- name: string;
120
- detail?: string;
121
- kind: SymbolKind;
122
- range: Range;
123
- selectionRange: Range;
124
- children?: DocumentSymbol[];
125
- }
126
-
127
- export interface SymbolInformation {
128
- name: string;
129
- kind: SymbolKind;
130
- location: Location;
131
- containerName?: string;
132
- }
133
-
134
- export interface WorkspaceSymbol {
135
- name: string;
136
- kind: SymbolKind;
137
- location: Location;
138
- containerName?: string;
139
- /** LSP 3.17+ extra data for resolve support */
140
- data?: unknown;
141
- }
142
-
143
- // ── Code Actions ──────────────────────────────────────────────────────
144
-
145
- export interface CodeActionContext {
146
- diagnostics: Diagnostic[];
147
- only?: string[];
148
- }
149
-
150
- export interface Command {
151
- title: string;
152
- command: string;
153
- arguments?: unknown[];
154
- }
155
-
156
- export interface CodeAction {
157
- title: string;
158
- kind?: string;
159
- diagnostics?: Diagnostic[];
160
- isPreferred?: boolean;
161
- edit?: WorkspaceEdit;
162
- command?: Command;
163
- }
164
-
165
- // ── Publish Diagnostics ───────────────────────────────────────────────
166
-
167
- export interface PublishDiagnosticsParams {
168
- uri: string;
169
- version?: number;
170
- diagnostics: Diagnostic[];
171
- }
172
-
173
- export const FileChangeType = {
174
- Created: 1,
175
- Changed: 2,
176
- Deleted: 3,
177
- } as const;
178
- export type FileChangeType = (typeof FileChangeType)[keyof typeof FileChangeType];
179
-
180
- export interface FileEvent {
181
- uri: string;
182
- type: FileChangeType;
183
- }
184
-
185
- export interface DidChangeWatchedFilesParams {
186
- changes: FileEvent[];
187
- }
188
-
189
- // ── LSP 3.17 Pull Diagnostics ─────────────────────────────────────────
190
-
191
- export interface DocumentDiagnosticParams {
192
- textDocument: TextDocumentIdentifier;
193
- identifier?: string;
194
- previousResultId?: string;
195
- workDoneToken?: unknown;
196
- partialResultToken?: unknown;
197
- }
198
-
199
- /** LSP 3.17 document diagnostic report shape used by textDocument/diagnostic. */
200
- export type DocumentDiagnosticReport =
201
- | RelatedFullDocumentDiagnosticReport
202
- | RelatedUnchangedDocumentDiagnosticReport;
203
-
204
- /** Full document diagnostic report, optionally carrying related document reports. */
205
- export interface RelatedFullDocumentDiagnosticReport extends FullDocumentDiagnosticReport {
206
- relatedDocuments?: Record<
207
- string,
208
- FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport
209
- >;
210
- }
211
-
212
- /** Unchanged document diagnostic report, optionally carrying related document reports. */
213
- export interface RelatedUnchangedDocumentDiagnosticReport
214
- extends UnchangedDocumentDiagnosticReport {
215
- relatedDocuments?: Record<
216
- string,
217
- FullDocumentDiagnosticReport | UnchangedDocumentDiagnosticReport
218
- >;
219
- }
220
-
221
- /** Full diagnostic payload for a document. */
222
- export interface FullDocumentDiagnosticReport {
223
- kind: "full";
224
- resultId?: string;
225
- items: Diagnostic[];
226
- }
227
-
228
- /** Result-id-only report indicating a document's diagnostics are unchanged. */
229
- export interface UnchangedDocumentDiagnosticReport {
230
- kind: "unchanged";
231
- resultId: string;
232
- }
233
-
234
- /** Client capability for pull diagnostics. */
235
- export interface ClientDiagnosticCapabilities {
236
- dynamicRegistration?: boolean;
237
- relatedDocumentSupport?: boolean;
238
- }
239
-
240
- // ── Initialize ────────────────────────────────────────────────────────
241
-
242
- export interface InitializeParams {
243
- processId: number | null;
244
- rootUri: string | null;
245
- capabilities: ClientCapabilities;
246
- initializationOptions?: unknown;
247
- }
248
-
249
- export interface ClientCapabilities {
250
- textDocument?: {
251
- synchronization?: {
252
- didSave?: boolean;
253
- dynamicRegistration?: boolean;
254
- };
255
- hover?: {
256
- contentFormat?: string[];
257
- dynamicRegistration?: boolean;
258
- };
259
- definition?: {
260
- dynamicRegistration?: boolean;
261
- linkSupport?: boolean;
262
- };
263
- references?: {
264
- dynamicRegistration?: boolean;
265
- };
266
- documentSymbol?: {
267
- dynamicRegistration?: boolean;
268
- hierarchicalDocumentSymbolSupport?: boolean;
269
- };
270
- rename?: {
271
- dynamicRegistration?: boolean;
272
- prepareSupport?: boolean;
273
- };
274
- codeAction?: {
275
- dynamicRegistration?: boolean;
276
- codeActionLiteralSupport?: {
277
- codeActionKind: { valueSet: string[] };
278
- };
279
- };
280
- publishDiagnostics?: {
281
- relatedInformation?: boolean;
282
- versionSupport?: boolean;
283
- };
284
- /** LSP 3.17+ pull diagnostic capability */
285
- diagnostic?: ClientDiagnosticCapabilities;
286
- };
287
- workspace?: {
288
- workspaceFolders?: boolean;
289
- diagnostics?: {
290
- refreshSupport?: boolean;
291
- };
292
- };
293
- }
294
-
295
- export interface InitializeResult {
296
- capabilities: ServerCapabilities;
297
- }
298
-
299
- export interface ServerCapabilities {
300
- textDocumentSync?: number | { openClose?: boolean; change?: number };
301
- hoverProvider?: boolean;
302
- definitionProvider?: boolean;
303
- referencesProvider?: boolean;
304
- documentSymbolProvider?: boolean;
305
- workspaceSymbolProvider?: boolean;
306
- renameProvider?: boolean | { prepareProvider?: boolean };
307
- codeActionProvider?: boolean | { codeActionKinds?: string[] };
308
- implementationProvider?: boolean;
309
- /** LSP 3.17+ pull diagnostic support */
310
- diagnosticProvider?:
311
- | boolean
312
- | {
313
- /** Document diagnostic provider */
314
- documentIdentifierProvider?: boolean | { workDoneProgress?: boolean };
315
- /** Workspace diagnostic provider */
316
- workspaceDiagnostics?: boolean | { workDoneProgress?: boolean };
317
- /** Identifier for result sets */
318
- identifierSet?: boolean;
319
- /** Inter-file dependency support */
320
- interFileDependencies?: boolean;
321
- /** Workspace-wide multi-file support */
322
- workspaceDiagnosticsSupport?: boolean;
323
- };
324
- }
325
-
326
- // ── Text Document Items ───────────────────────────────────────────────
327
-
328
- export interface TextDocumentIdentifier {
329
- uri: string;
330
- }
331
-
332
- export interface TextDocumentItem {
333
- uri: string;
334
- languageId: string;
335
- version: number;
336
- text: string;
337
- }
338
-
339
- export interface VersionedTextDocumentIdentifier {
340
- uri: string;
341
- version: number;
342
- }
343
-
344
- export interface TextDocumentPositionParams {
345
- textDocument: TextDocumentIdentifier;
346
- position: Position;
347
- }
348
-
349
- // ── JSON-RPC ──────────────────────────────────────────────────────────
350
-
1
+ // LSP protocol types — re-exported from vscode-languageserver-* packages.
2
+ // These are the canonical type definitions maintained by Microsoft alongside the LSP spec.
3
+
4
+ // ── Protocol types from vscode-languageserver-protocol ───────────────
5
+ export {
6
+ type ClientCapabilities,
7
+ DidChangeWatchedFilesParams,
8
+ DocumentDiagnosticParams,
9
+ type DocumentDiagnosticReport,
10
+ FileChangeType,
11
+ type FileEvent,
12
+ type FullDocumentDiagnosticReport,
13
+ type InitializeParams,
14
+ type InitializeResult,
15
+ PublishDiagnosticsParams,
16
+ type RelatedFullDocumentDiagnosticReport,
17
+ type RelatedUnchangedDocumentDiagnosticReport,
18
+ type ServerCapabilities,
19
+ TextDocumentPositionParams,
20
+ type UnchangedDocumentDiagnosticReport,
21
+ } from "vscode-languageserver-protocol";
22
+ // ── Core data types from vscode-languageserver-types ─────────────────
23
+ export {
24
+ CodeAction,
25
+ type CodeActionContext,
26
+ Command,
27
+ Diagnostic,
28
+ DiagnosticRelatedInformation,
29
+ DiagnosticSeverity,
30
+ DocumentSymbol,
31
+ Hover,
32
+ Location,
33
+ LocationLink,
34
+ MarkedString,
35
+ MarkupContent,
36
+ Position,
37
+ Range,
38
+ SymbolInformation,
39
+ SymbolKind,
40
+ TextDocumentEdit,
41
+ TextDocumentIdentifier,
42
+ TextDocumentItem,
43
+ TextEdit,
44
+ VersionedTextDocumentIdentifier,
45
+ WorkspaceEdit,
46
+ WorkspaceSymbol,
47
+ } from "vscode-languageserver-types";
48
+
49
+ // Alias for backward compatibility — our code uses ClientDiagnosticCapabilities
50
+ import type { DiagnosticClientCapabilities } from "vscode-languageserver-protocol";
51
+ export type ClientDiagnosticCapabilities = DiagnosticClientCapabilities;
52
+
53
+ // ── JSON-RPC types (local — replaced by vscode-jsonrpc in transport task) ──
351
54
  export type JsonRpcId = number | string;
352
55
 
353
56
  export interface JsonRpcRequest {
@@ -372,40 +75,11 @@ export interface JsonRpcNotification {
372
75
 
373
76
  export type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
374
77
 
375
- // ── Server Configuration ──────────────────────────────────────────────
376
-
377
- export interface ServerConfig {
378
- command: string;
379
- args?: string[];
380
- fileTypes: string[];
381
- rootMarkers: string[];
382
- enabled?: boolean;
383
- initializationOptions?: unknown;
384
- }
385
-
386
- /** LSP configuration keyed by language name (e.g. `typescript`, `python`). */
387
- export interface LspConfig {
388
- servers: Record<string, ServerConfig>;
389
- }
390
-
391
- export interface DetectedProjectServer {
392
- name: string;
393
- root: string;
394
- fileTypes: string[];
395
- }
396
-
397
- export interface ProjectServerInfo extends DetectedProjectServer {
398
- status: "running" | "error" | "unavailable";
399
- supportedActions: string[];
400
- openFiles: string[];
401
- }
402
-
403
- /** A language whose source files are present but the server binary is missing. */
404
- export interface MissingServer {
405
- /** Language name (e.g. "python", "rust"). */
406
- name: string;
407
- /** Server command that was not found on PATH. */
408
- command: string;
409
- /** File extensions found in the project (subset of server.fileTypes). */
410
- foundExtensions: string[];
411
- }
78
+ // ── SuPi-specific server config ──────────────────────────────────────
79
+ export type {
80
+ DetectedProjectServer,
81
+ LspConfig,
82
+ MissingServer,
83
+ ProjectServerInfo,
84
+ ServerConfig,
85
+ } from "./server-config.ts";
package/src/format.ts CHANGED
@@ -11,6 +11,7 @@ import type {
11
11
  MarkupContent,
12
12
  SymbolInformation,
13
13
  WorkspaceEdit,
14
+ WorkspaceSymbol,
14
15
  } from "./config/types.ts";
15
16
  import { isProjectSource } from "./summary.ts";
16
17
  import { uriToFile } from "./utils.ts";
@@ -145,8 +146,8 @@ export function formatDocumentSymbols(symbols: DocumentSymbol[], indent: number)
145
146
  }
146
147
 
147
148
  export function formatSymbolInformation(symbols: SymbolInformation[], cwd: string): string {
148
- const projectSyms: SymbolInformation[] = [];
149
- const externalSyms: SymbolInformation[] = [];
149
+ const projectSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
150
+ const externalSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
150
151
  for (const sym of symbols) {
151
152
  if (isProjectSource(uriToFile(sym.location.uri), cwd)) {
152
153
  projectSyms.push(sym);
@@ -160,7 +161,7 @@ export function formatSymbolInformation(symbols: SymbolInformation[], cwd: strin
160
161
  for (const sym of symbolsToShow) {
161
162
  const kind = symbolKindName(sym.kind);
162
163
  const file = relPath(uriToFile(sym.location.uri), cwd);
163
- const line = sym.location.range.start.line + 1;
164
+ const line = "range" in sym.location ? sym.location.range.start.line + 1 : "?";
164
165
  const container = sym.containerName ? ` (in ${sym.containerName})` : "";
165
166
  lines.push(`- ${kind} **${sym.name}**${container} — ${file}:${line}`);
166
167
  }
@@ -183,6 +184,7 @@ interface EditEntry {
183
184
  edits: Array<{ range: { start: { line: number } }; newText: string }>;
184
185
  }
185
186
 
187
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pre-existing — not introduced by this change
186
188
  function partitionWorkspaceEdit(
187
189
  edit: WorkspaceEdit,
188
190
  cwd: string,
@@ -203,6 +205,7 @@ function partitionWorkspaceEdit(
203
205
 
204
206
  if (edit.documentChanges) {
205
207
  for (const dc of edit.documentChanges) {
208
+ if (!("textDocument" in dc)) continue;
206
209
  const filePath = uriToFile(dc.textDocument.uri);
207
210
  if (isProjectSource(filePath, cwd)) {
208
211
  projectChanges.push({ file: relPath(filePath, cwd), edits: dc.edits });
@@ -258,11 +261,15 @@ export function formatCodeActions(actions: CodeAction[]): string {
258
261
 
259
262
  // ── Workspace Symbols ─────────────────────────────────────────────────
260
263
 
261
- export function formatWorkspaceSymbols(symbols: SymbolInformation[], cwd: string): string {
264
+ // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: pre-existing threshold exceeded by widened parameter type for WorkspaceSymbol compatibility
265
+ export function formatWorkspaceSymbols(
266
+ symbols: (SymbolInformation | WorkspaceSymbol)[],
267
+ cwd: string,
268
+ ): string {
262
269
  if (symbols.length === 0) return "No symbols found.";
263
270
 
264
- const projectSyms: SymbolInformation[] = [];
265
- const externalSyms: SymbolInformation[] = [];
271
+ const projectSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
272
+ const externalSyms: (SymbolInformation | WorkspaceSymbol)[] = [];
266
273
  for (const sym of symbols) {
267
274
  if (isProjectSource(uriToFile(sym.location.uri), cwd)) {
268
275
  projectSyms.push(sym);
@@ -279,8 +286,9 @@ export function formatWorkspaceSymbols(symbols: SymbolInformation[], cwd: string
279
286
  for (const sym of projectSyms) {
280
287
  const kind = symbolKindName(sym.kind);
281
288
  const file = relPath(uriToFile(sym.location.uri), cwd);
282
- const line = sym.location.range.start.line + 1;
283
- const col = sym.location.range.start.character + 1;
289
+ const loc = "range" in sym.location ? sym.location.range : null;
290
+ const line = loc ? loc.start.line + 1 : "?";
291
+ const col = loc ? loc.start.character + 1 : "?";
284
292
  const container = sym.containerName ? ` — ${sym.containerName}` : "";
285
293
  lines.push(`- **${sym.name}** (${kind})${container} — ${file}:${line}:${col}`);
286
294
  }
package/src/lsp.ts CHANGED
@@ -16,7 +16,7 @@ import type {
16
16
  import { pruneAndReorderContextMessages, restorePromptContent } from "@mrclrchtr/supi-core/api";
17
17
  import { loadConfig, resolveLanguageAlias } from "./config/config.ts";
18
18
  import { clearTsconfigCache } from "./config/tsconfig-scope.ts";
19
- import { FileChangeType } from "./config/types.ts";
19
+ import { FileChangeType, type FileEvent } from "./config/types.ts";
20
20
  import {
21
21
  diagnosticsContextFingerprint,
22
22
  formatDiagnosticsContext,
@@ -228,7 +228,7 @@ function recoverWorkspaceChangesFromToolResult(
228
228
  if (shouldInvalidateTsconfigScopeCache(resolvedPath)) {
229
229
  clearTsconfigCache();
230
230
  }
231
- const fileEvent = { uri: fileToUri(resolvedPath), type: FileChangeType.Changed };
231
+ const fileEvent: FileEvent = { uri: fileToUri(resolvedPath), type: FileChangeType.Changed };
232
232
 
233
233
  // Sentinel files (package.json, tsconfig.json, lockfiles, .d.ts)
234
234
  if (isWorkspaceRecoveryTrigger(resolvedPath, cwd)) {
@@ -1,5 +1,5 @@
1
1
  import type { LspClient } from "../client/client.ts";
2
- import type { ProjectServerInfo } from "../config/types.ts";
2
+ import type { ProjectServerInfo } from "../config/server-config.ts";
3
3
  import { displayRelativeFilePath } from "../summary.ts";
4
4
  import { getSupportedLspServerActions } from "../tool/tool-specs.ts";
5
5
 
@@ -2,7 +2,7 @@
2
2
  // Extracted from lsp.ts to keep file sizes within Biome limits.
3
3
 
4
4
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
5
- import type { DetectedProjectServer, ProjectServerInfo } from "../config/types.ts";
5
+ import type { DetectedProjectServer, ProjectServerInfo } from "../config/server-config.ts";
6
6
  import type { LspManager } from "../manager/manager.ts";
7
7
  import { LSP_TOOL_NAMES } from "../tool/names.ts";
8
8
  import type { LspInspectorState } from "../ui/ui.ts";
@@ -1,7 +1,7 @@
1
1
  // Prompt guidance and tool descriptions for the expert LSP toolset.
2
2
 
3
3
  import * as path from "node:path";
4
- import type { ProjectServerInfo } from "../config/types.ts";
4
+ import type { ProjectServerInfo } from "../config/server-config.ts";
5
5
  import { LSP_LOOKUP_TOOL, type LspToolName } from "./names.ts";
6
6
  import { LSP_TOOL_DEFINITION_SPECS } from "./tool-specs.ts";
7
7
 
@@ -103,7 +103,7 @@ export const LSP_TOOL_DEFINITION_SPECS = [
103
103
  basePromptGuidelines: [
104
104
  'Use lsp_lookup with `kind: "hover"` for semantic type or symbol information at a known `file`, `line`, and `character`.',
105
105
  'Use lsp_lookup with `kind: "definition"`, `"references"`, or `"implementation"` for semantic navigation at a known position.',
106
- "Use lsp_lookup after code_intel or tree_sitter has already narrowed the target file and position.",
106
+ "Use lsp_lookup after code_brief, code_map, or tree_sitter has already narrowed the target file and position.",
107
107
  ],
108
108
  parameters: LookupParameters,
109
109
  run: (service, cwd, params) =>