gsd-pi 2.8.3 → 2.9.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 (169) hide show
  1. package/README.md +2 -1
  2. package/dist/cli.js +5 -0
  3. package/dist/loader.js +1 -1
  4. package/dist/update-check.d.ts +24 -0
  5. package/dist/update-check.js +93 -0
  6. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  7. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  8. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js +758 -0
  9. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  10. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  11. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  12. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js +267 -0
  13. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  14. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  15. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  16. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  17. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  18. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  19. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  20. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  21. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  22. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  23. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  24. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js +709 -0
  25. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  26. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  27. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  28. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  29. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  30. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  31. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  32. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  33. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  34. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  35. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  36. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js +64 -0
  37. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  38. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  39. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  40. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  41. package/node_modules/@gsd/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  42. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  43. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js +1 -0
  44. package/node_modules/@gsd/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  45. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  46. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  47. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js +4 -0
  48. package/node_modules/@gsd/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  49. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  50. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  51. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  52. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  53. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  54. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  55. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js +46 -1
  56. package/node_modules/@gsd/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  57. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/client.ts +880 -0
  58. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/config.ts +325 -0
  59. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  60. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  61. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  62. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/index.ts +943 -0
  63. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  64. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  65. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  66. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/types.ts +421 -0
  67. package/node_modules/@gsd/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  68. package/node_modules/@gsd/pi-coding-agent/src/core/slash-commands.ts +1 -0
  69. package/node_modules/@gsd/pi-coding-agent/src/core/tools/index.ts +10 -0
  70. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  71. package/node_modules/@gsd/pi-coding-agent/src/modes/interactive/interactive-mode.ts +59 -2
  72. package/package.json +1 -1
  73. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts +46 -0
  74. package/packages/pi-coding-agent/dist/core/lsp/client.d.ts.map +1 -0
  75. package/packages/pi-coding-agent/dist/core/lsp/client.js +758 -0
  76. package/packages/pi-coding-agent/dist/core/lsp/client.js.map +1 -0
  77. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts +23 -0
  78. package/packages/pi-coding-agent/dist/core/lsp/config.d.ts.map +1 -0
  79. package/packages/pi-coding-agent/dist/core/lsp/config.js +267 -0
  80. package/packages/pi-coding-agent/dist/core/lsp/config.js.map +1 -0
  81. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts +17 -0
  82. package/packages/pi-coding-agent/dist/core/lsp/edits.d.ts.map +1 -0
  83. package/packages/pi-coding-agent/dist/core/lsp/edits.js +101 -0
  84. package/packages/pi-coding-agent/dist/core/lsp/edits.js.map +1 -0
  85. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts +15 -0
  86. package/packages/pi-coding-agent/dist/core/lsp/helpers.d.ts.map +1 -0
  87. package/packages/pi-coding-agent/dist/core/lsp/helpers.js +46 -0
  88. package/packages/pi-coding-agent/dist/core/lsp/helpers.js.map +1 -0
  89. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts +35 -0
  90. package/packages/pi-coding-agent/dist/core/lsp/index.d.ts.map +1 -0
  91. package/packages/pi-coding-agent/dist/core/lsp/index.js +709 -0
  92. package/packages/pi-coding-agent/dist/core/lsp/index.js.map +1 -0
  93. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts +2 -0
  94. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.d.ts.map +1 -0
  95. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js +308 -0
  96. package/packages/pi-coding-agent/dist/core/lsp/lsp-integration.test.js.map +1 -0
  97. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts +34 -0
  98. package/packages/pi-coding-agent/dist/core/lsp/lspmux.d.ts.map +1 -0
  99. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js +136 -0
  100. package/packages/pi-coding-agent/dist/core/lsp/lspmux.js.map +1 -0
  101. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts +262 -0
  102. package/packages/pi-coding-agent/dist/core/lsp/types.d.ts.map +1 -0
  103. package/packages/pi-coding-agent/dist/core/lsp/types.js +64 -0
  104. package/packages/pi-coding-agent/dist/core/lsp/types.js.map +1 -0
  105. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts +50 -0
  106. package/packages/pi-coding-agent/dist/core/lsp/utils.d.ts.map +1 -0
  107. package/packages/pi-coding-agent/dist/core/lsp/utils.js +574 -0
  108. package/packages/pi-coding-agent/dist/core/lsp/utils.js.map +1 -0
  109. package/packages/pi-coding-agent/dist/core/slash-commands.d.ts.map +1 -1
  110. package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -0
  111. package/packages/pi-coding-agent/dist/core/slash-commands.js.map +1 -1
  112. package/packages/pi-coding-agent/dist/core/tools/index.d.ts +13 -0
  113. package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
  114. package/packages/pi-coding-agent/dist/core/tools/index.js +4 -0
  115. package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
  116. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +10 -1
  117. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  118. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +2 -2
  119. package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
  120. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts +2 -0
  121. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  122. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +46 -1
  123. package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
  124. package/packages/pi-coding-agent/src/core/lsp/client.ts +880 -0
  125. package/packages/pi-coding-agent/src/core/lsp/config.ts +325 -0
  126. package/packages/pi-coding-agent/src/core/lsp/defaults.json +456 -0
  127. package/packages/pi-coding-agent/src/core/lsp/edits.ts +109 -0
  128. package/packages/pi-coding-agent/src/core/lsp/helpers.ts +54 -0
  129. package/packages/pi-coding-agent/src/core/lsp/index.ts +943 -0
  130. package/packages/pi-coding-agent/src/core/lsp/lsp-integration.test.ts +407 -0
  131. package/packages/pi-coding-agent/src/core/lsp/lsp.md +33 -0
  132. package/packages/pi-coding-agent/src/core/lsp/lspmux.ts +199 -0
  133. package/packages/pi-coding-agent/src/core/lsp/types.ts +421 -0
  134. package/packages/pi-coding-agent/src/core/lsp/utils.ts +682 -0
  135. package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -0
  136. package/packages/pi-coding-agent/src/core/tools/index.ts +10 -0
  137. package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +2 -2
  138. package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +59 -2
  139. package/src/resources/extensions/ask-user-questions.ts +2 -2
  140. package/src/resources/extensions/bg-shell/index.ts +34 -37
  141. package/src/resources/extensions/browser-tools/core.d.ts +205 -0
  142. package/src/resources/extensions/browser-tools/index.ts +2 -2
  143. package/src/resources/extensions/browser-tools/refs.ts +1 -1
  144. package/src/resources/extensions/browser-tools/tools/session.ts +1 -1
  145. package/src/resources/extensions/context7/index.ts +2 -2
  146. package/src/resources/extensions/get-secrets-from-user.ts +3 -2
  147. package/src/resources/extensions/google-search/index.ts +1 -1
  148. package/src/resources/extensions/gsd/auto.ts +41 -4
  149. package/src/resources/extensions/gsd/commands.ts +218 -3
  150. package/src/resources/extensions/gsd/doctor.ts +1 -1
  151. package/src/resources/extensions/gsd/git-service.ts +116 -4
  152. package/src/resources/extensions/gsd/guided-flow.ts +19 -9
  153. package/src/resources/extensions/gsd/index.ts +17 -7
  154. package/src/resources/extensions/gsd/preferences.ts +1 -1
  155. package/src/resources/extensions/gsd/tests/git-service.test.ts +226 -0
  156. package/src/resources/extensions/gsd/tests/migrate-command.test.ts +2 -2
  157. package/src/resources/extensions/gsd/tests/migrate-transformer.test.ts +1 -1
  158. package/src/resources/extensions/gsd/tests/migrate-writer-integration.test.ts +10 -10
  159. package/src/resources/extensions/gsd/tests/next-milestone-id.test.ts +87 -0
  160. package/src/resources/extensions/gsd/tests/worktree.test.ts +352 -0
  161. package/src/resources/extensions/gsd/types.ts +1 -0
  162. package/src/resources/extensions/gsd/worktree.ts +20 -1
  163. package/src/resources/extensions/mac-tools/index.ts +1 -1
  164. package/src/resources/extensions/search-the-web/format.ts +1 -1
  165. package/src/resources/extensions/search-the-web/index.ts +5 -5
  166. package/src/resources/extensions/search-the-web/tool-fetch-page.ts +7 -7
  167. package/src/resources/extensions/search-the-web/tool-llm-context.ts +11 -11
  168. package/src/resources/extensions/search-the-web/tool-search.ts +10 -10
  169. package/src/resources/extensions/shared/interview-ui.ts +2 -2
@@ -0,0 +1,407 @@
1
+ /**
2
+ * Integration test for the LSP tool port.
3
+ *
4
+ * Spins up typescript-language-server against a temp TypeScript project
5
+ * and exercises: initialize, didOpen, hover, definition, references,
6
+ * documentSymbol, diagnostics, and shutdown.
7
+ *
8
+ * Run: node --experimental-strip-types --test src/core/lsp/lsp-integration.test.ts
9
+ * (from packages/pi-coding-agent/)
10
+ */
11
+ import test from "node:test";
12
+ import assert from "node:assert/strict";
13
+ import { spawn } from "node:child_process";
14
+ import * as fs from "node:fs";
15
+ import * as path from "node:path";
16
+ import * as os from "node:os";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // Helpers — lightweight JSON-RPC over stdio (no dependency on our LSP code)
20
+ // ---------------------------------------------------------------------------
21
+
22
+ interface JsonRpcRequest {
23
+ jsonrpc: "2.0";
24
+ id: number;
25
+ method: string;
26
+ params: unknown;
27
+ }
28
+
29
+ interface JsonRpcNotification {
30
+ jsonrpc: "2.0";
31
+ method: string;
32
+ params?: unknown;
33
+ }
34
+
35
+ interface JsonRpcResponse {
36
+ jsonrpc: "2.0";
37
+ id?: number;
38
+ result?: unknown;
39
+ error?: { code: number; message: string };
40
+ }
41
+
42
+ function encodeMessage(msg: JsonRpcRequest | JsonRpcNotification | JsonRpcResponse): string {
43
+ const body = JSON.stringify(msg);
44
+ return `Content-Length: ${Buffer.byteLength(body, "utf-8")}\r\n\r\n${body}`;
45
+ }
46
+
47
+ /**
48
+ * Minimal LSP harness: spawns a language server, sends requests, collects responses.
49
+ */
50
+ class LspHarness {
51
+ private proc;
52
+ private nextId = 1;
53
+ private buffer = Buffer.alloc(0);
54
+ private pending = new Map<number, { resolve: (v: unknown) => void; reject: (e: Error) => void }>();
55
+ private notifications: Array<{ method: string; params: unknown }> = [];
56
+
57
+ constructor(command: string, args: string[], cwd: string) {
58
+ this.proc = spawn(command, args, {
59
+ cwd,
60
+ stdio: ["pipe", "pipe", "pipe"],
61
+ });
62
+
63
+ this.proc.stdout!.on("data", (chunk: Buffer) => {
64
+ this.buffer = Buffer.concat([this.buffer, chunk]);
65
+ this.drain();
66
+ });
67
+
68
+ this.proc.stderr!.on("data", (chunk: Buffer) => {
69
+ // Swallow stderr (server logs)
70
+ });
71
+ }
72
+
73
+ private drain(): void {
74
+ while (true) {
75
+ const headerEnd = this.findHeaderEnd();
76
+ if (headerEnd === -1) return;
77
+
78
+ const headerText = this.buffer.subarray(0, headerEnd).toString("utf-8");
79
+ const match = headerText.match(/Content-Length:\s*(\d+)/i);
80
+ if (!match) return;
81
+
82
+ const contentLength = parseInt(match[1], 10);
83
+ const messageStart = headerEnd + 4; // past \r\n\r\n
84
+ const messageEnd = messageStart + contentLength;
85
+ if (this.buffer.length < messageEnd) return;
86
+
87
+ const body = this.buffer.subarray(messageStart, messageEnd).toString("utf-8");
88
+ this.buffer = Buffer.from(this.buffer.subarray(messageEnd));
89
+
90
+ const msg = JSON.parse(body) as JsonRpcResponse & { method?: string; params?: unknown };
91
+
92
+ if (msg.id !== undefined && this.pending.has(msg.id)) {
93
+ const p = this.pending.get(msg.id)!;
94
+ this.pending.delete(msg.id);
95
+ if (msg.error) {
96
+ p.reject(new Error(`LSP error ${msg.error.code}: ${msg.error.message}`));
97
+ } else {
98
+ p.resolve(msg.result);
99
+ }
100
+ } else if (msg.method) {
101
+ // Server request or notification
102
+ this.notifications.push({ method: msg.method, params: msg.params });
103
+ // Auto-respond to server requests that have an id
104
+ if (msg.id !== undefined) {
105
+ this.respond(msg.id, null);
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ private findHeaderEnd(): number {
112
+ for (let i = 0; i < this.buffer.length - 3; i++) {
113
+ if (
114
+ this.buffer[i] === 13 &&
115
+ this.buffer[i + 1] === 10 &&
116
+ this.buffer[i + 2] === 13 &&
117
+ this.buffer[i + 3] === 10
118
+ ) {
119
+ return i;
120
+ }
121
+ }
122
+ return -1;
123
+ }
124
+
125
+ private respond(id: number, result: unknown): void {
126
+ const msg: JsonRpcResponse = { jsonrpc: "2.0", id, result };
127
+ this.proc.stdin!.write(encodeMessage(msg));
128
+ }
129
+
130
+ async request(method: string, params: unknown, timeoutMs = 15000): Promise<unknown> {
131
+ const id = this.nextId++;
132
+ const msg: JsonRpcRequest = { jsonrpc: "2.0", id, method, params };
133
+ this.proc.stdin!.write(encodeMessage(msg));
134
+
135
+ return new Promise<unknown>((resolve, reject) => {
136
+ const timer = setTimeout(() => {
137
+ this.pending.delete(id);
138
+ reject(new Error(`Request ${method} timed out after ${timeoutMs}ms`));
139
+ }, timeoutMs);
140
+
141
+ this.pending.set(id, {
142
+ resolve: (v) => {
143
+ clearTimeout(timer);
144
+ resolve(v);
145
+ },
146
+ reject: (e) => {
147
+ clearTimeout(timer);
148
+ reject(e);
149
+ },
150
+ });
151
+ });
152
+ }
153
+
154
+ notify(method: string, params: unknown): void {
155
+ const msg: JsonRpcNotification = { jsonrpc: "2.0", method, params };
156
+ this.proc.stdin!.write(encodeMessage(msg));
157
+ }
158
+
159
+ getNotifications(method?: string): Array<{ method: string; params: unknown }> {
160
+ if (!method) return this.notifications;
161
+ return this.notifications.filter((n) => n.method === method);
162
+ }
163
+
164
+ async shutdown(): Promise<void> {
165
+ try {
166
+ await this.request("shutdown", null, 5000);
167
+ this.notify("exit", null);
168
+ } catch {
169
+ // Best effort
170
+ }
171
+ this.proc.kill();
172
+ }
173
+ }
174
+
175
+ // ---------------------------------------------------------------------------
176
+ // Test fixtures
177
+ // ---------------------------------------------------------------------------
178
+
179
+ function createTempProject(): { dir: string; cleanup: () => void } {
180
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "lsp-test-"));
181
+
182
+ // tsconfig.json
183
+ fs.writeFileSync(
184
+ path.join(dir, "tsconfig.json"),
185
+ JSON.stringify(
186
+ {
187
+ compilerOptions: {
188
+ target: "ES2022",
189
+ module: "commonjs",
190
+ strict: true,
191
+ outDir: "./dist",
192
+ rootDir: "./src",
193
+ },
194
+ include: ["src/**/*.ts"],
195
+ },
196
+ null,
197
+ 2,
198
+ ),
199
+ );
200
+
201
+ // package.json
202
+ fs.writeFileSync(
203
+ path.join(dir, "package.json"),
204
+ JSON.stringify({ name: "lsp-test-project", version: "1.0.0" }, null, 2),
205
+ );
206
+
207
+ fs.mkdirSync(path.join(dir, "src"));
208
+
209
+ // src/math.ts — module with exported functions
210
+ fs.writeFileSync(
211
+ path.join(dir, "src", "math.ts"),
212
+ `export function add(a: number, b: number): number {
213
+ return a + b;
214
+ }
215
+
216
+ export function subtract(a: number, b: number): number {
217
+ return a - b;
218
+ }
219
+
220
+ export interface Calculator {
221
+ add(a: number, b: number): number;
222
+ subtract(a: number, b: number): number;
223
+ }
224
+ `,
225
+ );
226
+
227
+ // src/main.ts — imports from math, has a type error
228
+ fs.writeFileSync(
229
+ path.join(dir, "src", "main.ts"),
230
+ `import { add, subtract, Calculator } from "./math";
231
+
232
+ const result: number = add(1, 2);
233
+ const diff: number = subtract(5, 3);
234
+
235
+ // Intentional type error: string assigned to number
236
+ const bad: number = "not a number";
237
+
238
+ export function compute(calc: Calculator): number {
239
+ return calc.add(1, 2) + calc.subtract(5, 3);
240
+ }
241
+ `,
242
+ );
243
+
244
+ return {
245
+ dir,
246
+ cleanup: () => fs.rmSync(dir, { recursive: true, force: true }),
247
+ };
248
+ }
249
+
250
+ function fileToUri(filePath: string): string {
251
+ return `file://${path.resolve(filePath)}`;
252
+ }
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // Tests
256
+ // ---------------------------------------------------------------------------
257
+
258
+ test("LSP integration: typescript-language-server", async (t) => {
259
+ const { dir, cleanup } = createTempProject();
260
+ const mainPath = path.join(dir, "src", "main.ts");
261
+ const mathPath = path.join(dir, "src", "math.ts");
262
+ const mainUri = fileToUri(mainPath);
263
+ const mathUri = fileToUri(mathPath);
264
+
265
+ const lsp = new LspHarness("typescript-language-server", ["--stdio"], dir);
266
+
267
+ try {
268
+ // ---- Initialize ----
269
+ await t.test("initialize handshake", async () => {
270
+ const result = (await lsp.request("initialize", {
271
+ processId: process.pid,
272
+ rootUri: fileToUri(dir),
273
+ rootPath: dir,
274
+ capabilities: {
275
+ textDocument: {
276
+ hover: { contentFormat: ["markdown", "plaintext"] },
277
+ definition: { linkSupport: true },
278
+ references: {},
279
+ documentSymbol: { hierarchicalDocumentSymbolSupport: true },
280
+ publishDiagnostics: { relatedInformation: true },
281
+ },
282
+ },
283
+ workspaceFolders: [{ uri: fileToUri(dir), name: "test" }],
284
+ })) as { capabilities?: Record<string, unknown> };
285
+
286
+ assert.ok(result, "initialize should return a result");
287
+ assert.ok(result.capabilities, "result should have capabilities");
288
+ assert.ok(result.capabilities.hoverProvider !== undefined, "should support hover");
289
+ assert.ok(result.capabilities.definitionProvider !== undefined, "should support definition");
290
+ });
291
+
292
+ lsp.notify("initialized", {});
293
+
294
+ // Open both files
295
+ const mainContent = fs.readFileSync(mainPath, "utf-8");
296
+ const mathContent = fs.readFileSync(mathPath, "utf-8");
297
+
298
+ lsp.notify("textDocument/didOpen", {
299
+ textDocument: { uri: mainUri, languageId: "typescript", version: 1, text: mainContent },
300
+ });
301
+ lsp.notify("textDocument/didOpen", {
302
+ textDocument: { uri: mathUri, languageId: "typescript", version: 1, text: mathContent },
303
+ });
304
+
305
+ // Give the server time to index
306
+ await new Promise((r) => setTimeout(r, 3000));
307
+
308
+ // ---- Hover ----
309
+ await t.test("hover on 'add' call", async () => {
310
+ const result = (await lsp.request("textDocument/hover", {
311
+ textDocument: { uri: mainUri },
312
+ position: { line: 2, character: 24 }, // on 'add' in "add(1, 2)"
313
+ })) as { contents?: unknown } | null;
314
+
315
+ assert.ok(result, "hover should return a result");
316
+ assert.ok(result.contents, "hover should have contents");
317
+ const text = JSON.stringify(result.contents);
318
+ assert.ok(
319
+ text.includes("add") || text.includes("number"),
320
+ `hover text should mention 'add' or 'number', got: ${text.slice(0, 200)}`,
321
+ );
322
+ });
323
+
324
+ // ---- Go to Definition ----
325
+ await t.test("go to definition of 'add'", async () => {
326
+ const result = (await lsp.request("textDocument/definition", {
327
+ textDocument: { uri: mainUri },
328
+ position: { line: 2, character: 24 }, // on 'add'
329
+ })) as unknown;
330
+
331
+ assert.ok(result, "definition should return a result");
332
+ const locations = Array.isArray(result) ? result : [result];
333
+ assert.ok(locations.length > 0, "should find at least one definition");
334
+ // Response can be Location (uri) or LocationLink (targetUri)
335
+ const loc = locations[0] as Record<string, unknown>;
336
+ const uri = (loc.uri ?? loc.targetUri) as string;
337
+ assert.ok(uri, `definition should have uri or targetUri, got keys: ${Object.keys(loc).join(", ")}`);
338
+ assert.ok(
339
+ uri.includes("math.ts"),
340
+ `definition should point to math.ts, got: ${uri}`,
341
+ );
342
+ });
343
+
344
+ // ---- References ----
345
+ await t.test("find references of 'add'", async () => {
346
+ const result = (await lsp.request("textDocument/references", {
347
+ textDocument: { uri: mathUri },
348
+ position: { line: 0, character: 16 }, // on 'add' definition
349
+ context: { includeDeclaration: true },
350
+ })) as Array<{ uri: string; range: unknown }> | null;
351
+
352
+ assert.ok(result, "references should return a result");
353
+ assert.ok(result.length >= 2, `should find at least 2 references (decl + usage), got ${result.length}`);
354
+ });
355
+
356
+ // ---- Document Symbols ----
357
+ await t.test("document symbols in math.ts", async () => {
358
+ const result = (await lsp.request("textDocument/documentSymbol", {
359
+ textDocument: { uri: mathUri },
360
+ })) as Array<{ name: string; kind: number }> | null;
361
+
362
+ assert.ok(result, "documentSymbol should return a result");
363
+ assert.ok(result.length >= 2, `should find at least 2 symbols, got ${result.length}`);
364
+ const names = result.map((s) => s.name);
365
+ assert.ok(names.includes("add"), `symbols should include 'add', got: ${names.join(", ")}`);
366
+ assert.ok(names.includes("subtract"), `symbols should include 'subtract', got: ${names.join(", ")}`);
367
+ });
368
+
369
+ // ---- Diagnostics (published via notification) ----
370
+ await t.test("diagnostics for type error", async () => {
371
+ // Wait a bit more for diagnostics to arrive
372
+ await new Promise((r) => setTimeout(r, 2000));
373
+
374
+ const diagNotifications = lsp.getNotifications("textDocument/publishDiagnostics");
375
+ const mainDiags = diagNotifications.filter(
376
+ (n) => (n.params as { uri: string }).uri === mainUri,
377
+ );
378
+
379
+ assert.ok(mainDiags.length > 0, "should receive diagnostics for main.ts");
380
+
381
+ const lastDiag = mainDiags[mainDiags.length - 1];
382
+ const diagnostics = (lastDiag.params as { diagnostics: Array<{ message: string; range: unknown }> })
383
+ .diagnostics;
384
+
385
+ // Should catch the type error: string assigned to number
386
+ const typeError = diagnostics.find(
387
+ (d) => d.message.includes("not assignable") || d.message.includes("Type"),
388
+ );
389
+ assert.ok(
390
+ typeError,
391
+ `should find type error diagnostic, got: ${diagnostics.map((d) => d.message).join("; ")}`,
392
+ );
393
+ });
394
+
395
+ // ---- Shutdown ----
396
+ await t.test("clean shutdown", async () => {
397
+ // Should not throw
398
+ await lsp.shutdown();
399
+ });
400
+ } catch (err) {
401
+ await lsp.shutdown().catch(() => {});
402
+ cleanup();
403
+ throw err;
404
+ }
405
+
406
+ cleanup();
407
+ });
@@ -0,0 +1,33 @@
1
+ Interacts with Language Server Protocol servers for code intelligence.
2
+
3
+ <operations>
4
+ - `diagnostics`: Get errors/warnings for file, glob, or entire workspace (no file)
5
+ - `definition`: Go to symbol definition → file path + position + 3-line source context
6
+ - `type_definition`: Go to symbol type definition → file path + position + 3-line source context
7
+ - `implementation`: Find concrete implementations → file path + position + 3-line source context
8
+ - `references`: Find references → locations with 3-line source context (first 50), remaining location-only
9
+ - `hover`: Get type info and documentation → type signature + docs
10
+ - `symbols`: List symbols in file, or search workspace (with query, no file)
11
+ - `rename`: Rename symbol across codebase → preview or apply edits
12
+ - `code_actions`: List available quick-fixes/refactors/import actions; apply one when `apply: true` and `query` matches title or index
13
+ - `status`: Show active language servers
14
+ - `reload`: Restart the language server
15
+ </operations>
16
+
17
+ <parameters>
18
+ - `file`: File path; for diagnostics it may be a glob pattern (e.g., `src/**/*.ts`)
19
+ - `line`: 1-indexed line number for position-based actions
20
+ - `symbol`: Substring on the target line used to resolve column automatically
21
+ - `occurrence`: 1-indexed match index when `symbol` appears multiple times on the same line
22
+ - `query`: Symbol search query, code-action kind filter (list mode), or code-action selector (apply mode)
23
+ - `new_name`: Required for rename
24
+ - `apply`: Apply edits for rename/code_actions (default true for rename, list mode for code_actions unless explicitly true)
25
+ - `timeout`: Request timeout in seconds (clamped to 5-60, default 20)
26
+ </parameters>
27
+
28
+ <caution>
29
+ - Requires running LSP server for target language
30
+ - Some operations require file to be saved to disk
31
+ - Diagnostics glob mode samples up to 20 files per request to avoid long-running stalls on broad patterns
32
+ - When `symbol` is provided for position-based actions, missing symbols or out-of-bounds `occurrence` values return an explicit error instead of silently falling back
33
+ </caution>
@@ -0,0 +1,199 @@
1
+ import { execSync, spawn } from "node:child_process";
2
+ import * as fsPromises from "node:fs/promises";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+
6
+ /**
7
+ * lspmux integration for LSP server multiplexing.
8
+ *
9
+ * When lspmux is available and running, this module wraps supported LSP server
10
+ * commands to use lspmux client mode, enabling server instance sharing across
11
+ * multiple editor windows.
12
+ *
13
+ * Integration is transparent: if lspmux is unavailable, falls back to direct spawning.
14
+ */
15
+
16
+ // =============================================================================
17
+ // Types
18
+ // =============================================================================
19
+
20
+ interface LspmuxConfig {
21
+ instance_timeout?: number;
22
+ gc_interval?: number;
23
+ listen?: [string, number] | string;
24
+ connect?: [string, number] | string;
25
+ log_filters?: string;
26
+ pass_environment?: string[];
27
+ }
28
+
29
+ interface LspmuxState {
30
+ available: boolean;
31
+ running: boolean;
32
+ binaryPath: string | null;
33
+ config: LspmuxConfig | null;
34
+ }
35
+
36
+ // =============================================================================
37
+ // Constants
38
+ // =============================================================================
39
+
40
+ const DEFAULT_SUPPORTED_SERVERS = new Set([
41
+ "rust-analyzer",
42
+ ]);
43
+
44
+ const LIVENESS_TIMEOUT_MS = 1000;
45
+ const STATE_CACHE_TTL_MS = 5 * 60 * 1000;
46
+
47
+ // =============================================================================
48
+ // Helpers
49
+ // =============================================================================
50
+
51
+ function which(command: string): string | null {
52
+ try {
53
+ return execSync(`which ${command}`, { encoding: "utf-8" }).trim() || null;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ // =============================================================================
60
+ // Config Path
61
+ // =============================================================================
62
+
63
+ function getConfigPath(): string {
64
+ const home = os.homedir();
65
+ switch (os.platform()) {
66
+ case "win32":
67
+ return path.join(process.env.APPDATA ?? path.join(home, "AppData", "Roaming"), "lspmux", "config.toml");
68
+ case "darwin":
69
+ return path.join(home, "Library", "Application Support", "lspmux", "config.toml");
70
+ default:
71
+ return path.join(process.env.XDG_CONFIG_HOME ?? path.join(home, ".config"), "lspmux", "config.toml");
72
+ }
73
+ }
74
+
75
+ // =============================================================================
76
+ // State Management
77
+ // =============================================================================
78
+
79
+ let cachedState: LspmuxState | null = null;
80
+ let cacheTimestamp = 0;
81
+
82
+ async function parseConfig(): Promise<LspmuxConfig | null> {
83
+ try {
84
+ const configPath = getConfigPath();
85
+ // lspmux config uses TOML, but since we're stripping TOML support,
86
+ // attempt a simple key=value parse for the config file.
87
+ // If the config file exists but can't be parsed, return null.
88
+ try {
89
+ await fsPromises.access(configPath);
90
+ } catch {
91
+ return null;
92
+ }
93
+ // Config exists but we can't parse TOML without a dependency.
94
+ // Return an empty config object to indicate the file exists.
95
+ return {} as LspmuxConfig;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+
101
+ async function checkServerRunning(binaryPath: string): Promise<boolean> {
102
+ try {
103
+ const proc = spawn(binaryPath, ["status"], {
104
+ stdio: ["ignore", "pipe", "pipe"],
105
+ });
106
+
107
+ const exited = await Promise.race([
108
+ new Promise<number>((resolve) => {
109
+ proc.on("exit", (code: number | null) => resolve(code ?? 1));
110
+ }),
111
+ new Promise<null>(resolve => setTimeout(() => resolve(null), LIVENESS_TIMEOUT_MS)),
112
+ ]);
113
+
114
+ if (exited === null) {
115
+ proc.kill();
116
+ return false;
117
+ }
118
+
119
+ return exited === 0;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ export async function detectLspmux(): Promise<LspmuxState> {
126
+ const now = Date.now();
127
+ if (cachedState && now - cacheTimestamp < STATE_CACHE_TTL_MS) {
128
+ return cachedState;
129
+ }
130
+
131
+ if (process.env.PI_DISABLE_LSPMUX === "1" || process.env.GSD_DISABLE_LSPMUX === "1") {
132
+ cachedState = { available: false, running: false, binaryPath: null, config: null };
133
+ cacheTimestamp = now;
134
+ return cachedState;
135
+ }
136
+
137
+ const binaryPath = which("lspmux");
138
+ if (!binaryPath) {
139
+ cachedState = { available: false, running: false, binaryPath: null, config: null };
140
+ cacheTimestamp = now;
141
+ return cachedState;
142
+ }
143
+
144
+ const [config, running] = await Promise.all([parseConfig(), checkServerRunning(binaryPath)]);
145
+
146
+ cachedState = { available: true, running, binaryPath, config };
147
+ cacheTimestamp = now;
148
+
149
+ return cachedState;
150
+ }
151
+
152
+ // =============================================================================
153
+ // Command Wrapping
154
+ // =============================================================================
155
+
156
+ export function isLspmuxSupported(command: string): boolean {
157
+ const baseName = command.split("/").pop() ?? command;
158
+ return DEFAULT_SUPPORTED_SERVERS.has(baseName);
159
+ }
160
+
161
+ export interface LspmuxWrappedCommand {
162
+ command: string;
163
+ args: string[];
164
+ env?: Record<string, string>;
165
+ }
166
+
167
+ export function wrapWithLspmux(
168
+ originalCommand: string,
169
+ originalArgs: string[] | undefined,
170
+ state: LspmuxState,
171
+ ): LspmuxWrappedCommand {
172
+ if (!state.available || !state.running || !state.binaryPath) {
173
+ return { command: originalCommand, args: originalArgs ?? [] };
174
+ }
175
+
176
+ if (!isLspmuxSupported(originalCommand)) {
177
+ return { command: originalCommand, args: originalArgs ?? [] };
178
+ }
179
+
180
+ const baseName = originalCommand.split("/").pop() ?? originalCommand;
181
+ const isDefaultRustAnalyzer = baseName === "rust-analyzer" && originalCommand === "rust-analyzer";
182
+ const hasArgs = originalArgs && originalArgs.length > 0;
183
+
184
+ if (isDefaultRustAnalyzer && !hasArgs) {
185
+ return { command: state.binaryPath, args: [] };
186
+ }
187
+
188
+ const args = hasArgs ? ["client", "--", ...originalArgs] : ["client"];
189
+ return {
190
+ command: state.binaryPath,
191
+ args,
192
+ env: { LSPMUX_SERVER: originalCommand },
193
+ };
194
+ }
195
+
196
+ export async function getLspmuxCommand(command: string, args?: string[]): Promise<LspmuxWrappedCommand> {
197
+ const state = await detectLspmux();
198
+ return wrapWithLspmux(command, args, state);
199
+ }