@mrclrchtr/supi-code-intelligence 0.1.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 (146) hide show
  1. package/README.md +212 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  18. package/node_modules/@mrclrchtr/supi-lsp/README.md +112 -0
  19. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  20. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/package.json +30 -0
  21. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  22. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  23. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  24. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  25. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  26. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  27. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  28. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  29. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  30. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  31. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  32. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  33. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  34. package/node_modules/@mrclrchtr/supi-lsp/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  35. package/node_modules/@mrclrchtr/supi-lsp/package.json +45 -0
  36. package/node_modules/@mrclrchtr/supi-lsp/src/capabilities.ts +62 -0
  37. package/node_modules/@mrclrchtr/supi-lsp/src/client/client-refresh.ts +229 -0
  38. package/node_modules/@mrclrchtr/supi-lsp/src/client/client.ts +545 -0
  39. package/node_modules/@mrclrchtr/supi-lsp/src/client/transport.ts +192 -0
  40. package/node_modules/@mrclrchtr/supi-lsp/src/config.ts +143 -0
  41. package/node_modules/@mrclrchtr/supi-lsp/src/defaults.json +82 -0
  42. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-augmentation.ts +82 -0
  43. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-display.ts +68 -0
  44. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostic-summary.ts +73 -0
  45. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/diagnostics.ts +98 -0
  46. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/stale-diagnostics.ts +47 -0
  47. package/node_modules/@mrclrchtr/supi-lsp/src/diagnostics/suppression-diagnostics.ts +58 -0
  48. package/node_modules/@mrclrchtr/supi-lsp/src/format.ts +359 -0
  49. package/node_modules/@mrclrchtr/supi-lsp/src/guidance.ts +163 -0
  50. package/node_modules/@mrclrchtr/supi-lsp/src/index.ts +17 -0
  51. package/node_modules/@mrclrchtr/supi-lsp/src/lsp-state.ts +82 -0
  52. package/node_modules/@mrclrchtr/supi-lsp/src/lsp.ts +470 -0
  53. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-client-state.ts +34 -0
  54. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-diagnostics.ts +139 -0
  55. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-helpers.ts +39 -0
  56. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-project-info.ts +46 -0
  57. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-types.ts +39 -0
  58. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-recovery.ts +83 -0
  59. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager-workspace-symbol.ts +18 -0
  60. package/node_modules/@mrclrchtr/supi-lsp/src/manager/manager.ts +550 -0
  61. package/node_modules/@mrclrchtr/supi-lsp/src/overrides.ts +173 -0
  62. package/node_modules/@mrclrchtr/supi-lsp/src/pattern-matcher.ts +197 -0
  63. package/node_modules/@mrclrchtr/supi-lsp/src/renderer.ts +120 -0
  64. package/node_modules/@mrclrchtr/supi-lsp/src/scanner.ts +153 -0
  65. package/node_modules/@mrclrchtr/supi-lsp/src/search-fallback.ts +98 -0
  66. package/node_modules/@mrclrchtr/supi-lsp/src/service-registry.ts +153 -0
  67. package/node_modules/@mrclrchtr/supi-lsp/src/settings-registration.ts +292 -0
  68. package/node_modules/@mrclrchtr/supi-lsp/src/summary.ts +153 -0
  69. package/node_modules/@mrclrchtr/supi-lsp/src/tool-actions.ts +430 -0
  70. package/node_modules/@mrclrchtr/supi-lsp/src/tree-persist.ts +48 -0
  71. package/node_modules/@mrclrchtr/supi-lsp/src/tsconfig-scope.ts +156 -0
  72. package/node_modules/@mrclrchtr/supi-lsp/src/types.ts +409 -0
  73. package/node_modules/@mrclrchtr/supi-lsp/src/ui.ts +358 -0
  74. package/node_modules/@mrclrchtr/supi-lsp/src/utils.ts +122 -0
  75. package/node_modules/@mrclrchtr/supi-lsp/src/workspace-sentinels.ts +114 -0
  76. package/node_modules/@mrclrchtr/supi-tree-sitter/README.md +97 -0
  77. package/node_modules/@mrclrchtr/supi-tree-sitter/package.json +67 -0
  78. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/.gitkeep +0 -0
  79. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm +0 -0
  80. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/bash/tree-sitter-bash.wasm.json +7 -0
  81. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm +0 -0
  82. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/c/tree-sitter-c.wasm.json +7 -0
  83. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm +0 -0
  84. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/cpp/tree-sitter-cpp.wasm.json +7 -0
  85. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm +0 -0
  86. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/go/tree-sitter-go.wasm.json +7 -0
  87. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm +0 -0
  88. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/html/tree-sitter-html.wasm.json +7 -0
  89. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm +0 -0
  90. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/java/tree-sitter-java.wasm.json +7 -0
  91. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm +0 -0
  92. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/javascript/tree-sitter-javascript.wasm.json +7 -0
  93. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm +0 -0
  94. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/kotlin/tree-sitter-kotlin.wasm.json +12 -0
  95. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm +0 -0
  96. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/python/tree-sitter-python.wasm.json +7 -0
  97. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm +0 -0
  98. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/r/tree-sitter-r.wasm.json +7 -0
  99. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm +0 -0
  100. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/ruby/tree-sitter-ruby.wasm.json +7 -0
  101. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm +0 -0
  102. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/rust/tree-sitter-rust.wasm.json +7 -0
  103. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm +0 -0
  104. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/sql/tree-sitter-sql.wasm.json +19 -0
  105. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm +0 -0
  106. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/tsx/tree-sitter-tsx.wasm.json +7 -0
  107. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm +0 -0
  108. package/node_modules/@mrclrchtr/supi-tree-sitter/resources/grammars/typescript/tree-sitter-typescript.wasm.json +7 -0
  109. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-kotlin-wasm.mjs +126 -0
  110. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/generate-sql-wasm.mjs +144 -0
  111. package/node_modules/@mrclrchtr/supi-tree-sitter/scripts/vendor-wasm.mjs +151 -0
  112. package/node_modules/@mrclrchtr/supi-tree-sitter/src/callees.ts +343 -0
  113. package/node_modules/@mrclrchtr/supi-tree-sitter/src/coordinates.ts +108 -0
  114. package/node_modules/@mrclrchtr/supi-tree-sitter/src/exports.ts +315 -0
  115. package/node_modules/@mrclrchtr/supi-tree-sitter/src/formatting.ts +104 -0
  116. package/node_modules/@mrclrchtr/supi-tree-sitter/src/imports.ts +42 -0
  117. package/node_modules/@mrclrchtr/supi-tree-sitter/src/index.ts +16 -0
  118. package/node_modules/@mrclrchtr/supi-tree-sitter/src/language.ts +116 -0
  119. package/node_modules/@mrclrchtr/supi-tree-sitter/src/node-at.ts +96 -0
  120. package/node_modules/@mrclrchtr/supi-tree-sitter/src/outline.ts +287 -0
  121. package/node_modules/@mrclrchtr/supi-tree-sitter/src/runtime.ts +237 -0
  122. package/node_modules/@mrclrchtr/supi-tree-sitter/src/session.ts +112 -0
  123. package/node_modules/@mrclrchtr/supi-tree-sitter/src/structure.ts +7 -0
  124. package/node_modules/@mrclrchtr/supi-tree-sitter/src/syntax-node.ts +13 -0
  125. package/node_modules/@mrclrchtr/supi-tree-sitter/src/tree-sitter.ts +306 -0
  126. package/node_modules/@mrclrchtr/supi-tree-sitter/src/types.ts +146 -0
  127. package/package.json +47 -0
  128. package/src/actions/affected-action.ts +310 -0
  129. package/src/actions/brief-action.ts +242 -0
  130. package/src/actions/callees-action.ts +134 -0
  131. package/src/actions/callers-action.ts +215 -0
  132. package/src/actions/implementations-action.ts +190 -0
  133. package/src/actions/index-action.ts +187 -0
  134. package/src/actions/pattern-action.ts +232 -0
  135. package/src/architecture.ts +367 -0
  136. package/src/brief-focused.ts +383 -0
  137. package/src/brief.ts +228 -0
  138. package/src/code-intelligence.ts +122 -0
  139. package/src/git-context.ts +65 -0
  140. package/src/guidance.ts +39 -0
  141. package/src/index.ts +28 -0
  142. package/src/resolve-target.ts +104 -0
  143. package/src/search-helpers.ts +283 -0
  144. package/src/target-resolution.ts +368 -0
  145. package/src/tool-actions.ts +109 -0
  146. package/src/types.ts +57 -0
@@ -0,0 +1,192 @@
1
+ // JSON-RPC 2.0 transport over stdio with Content-Length header framing.
2
+
3
+ import type { Readable, Writable } from "node:stream";
4
+ import type {
5
+ JsonRpcMessage,
6
+ JsonRpcNotification,
7
+ JsonRpcRequest,
8
+ JsonRpcResponse,
9
+ } from "../types.ts";
10
+
11
+ const CONTENT_LENGTH = "Content-Length: ";
12
+ const HEADER_DELIMITER = "\r\n\r\n";
13
+ const DEFAULT_TIMEOUT_MS = 30_000;
14
+
15
+ // ── Types ─────────────────────────────────────────────────────────────
16
+
17
+ export type NotificationHandler = (method: string, params: unknown) => void;
18
+
19
+ interface PendingRequest {
20
+ resolve: (result: unknown) => void;
21
+ reject: (error: Error) => void;
22
+ timer: ReturnType<typeof setTimeout>;
23
+ }
24
+
25
+ // ── JsonRpcClient ─────────────────────────────────────────────────────
26
+
27
+ export class JsonRpcClient {
28
+ private nextId = 1;
29
+ private buffer = Buffer.alloc(0);
30
+ private pending = new Map<number, PendingRequest>();
31
+ private notificationHandler: NotificationHandler | null = null;
32
+ private closed = false;
33
+ private readonly timeoutMs: number;
34
+
35
+ constructor(
36
+ private readonly input: Readable,
37
+ private readonly output: Writable,
38
+ options?: { timeoutMs?: number },
39
+ ) {
40
+ this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
41
+ this.input.on("data", (chunk: Buffer) => this.onData(chunk));
42
+ this.input.on("end", () => this.onClose());
43
+ this.input.on("error", () => this.onClose());
44
+ }
45
+
46
+ /** Register a handler for server notifications (no id). */
47
+ onNotification(handler: NotificationHandler): void {
48
+ this.notificationHandler = handler;
49
+ }
50
+
51
+ /** Send a request and wait for the correlated response, optionally overriding the timeout. */
52
+ sendRequest(
53
+ method: string,
54
+ params?: unknown,
55
+ options?: { timeoutMs?: number },
56
+ ): Promise<unknown> {
57
+ if (this.closed) {
58
+ return Promise.reject(new Error("JSON-RPC client is closed"));
59
+ }
60
+
61
+ const id = this.nextId++;
62
+ const timeoutMs = options?.timeoutMs ?? this.timeoutMs;
63
+ const promise = new Promise<unknown>((resolve, reject) => {
64
+ const timer = setTimeout(() => {
65
+ this.pending.delete(id);
66
+ reject(new Error(`Request ${method} (id=${id}) timed out after ${timeoutMs}ms`));
67
+ }, timeoutMs);
68
+
69
+ this.pending.set(id, { resolve, reject, timer });
70
+ });
71
+
72
+ const msg: JsonRpcRequest = { jsonrpc: "2.0", id, method, params };
73
+ this.writeMessage(msg);
74
+
75
+ // Prevent unhandled rejection when dispose() rejects orphaned promises
76
+ promise.catch(() => {});
77
+ return promise;
78
+ }
79
+
80
+ /** Send a notification (no response expected). */
81
+ sendNotification(method: string, params?: unknown): void {
82
+ if (this.closed) return;
83
+ const msg: JsonRpcNotification = { jsonrpc: "2.0", method, params };
84
+ this.writeMessage(msg);
85
+ }
86
+
87
+ /** Clean up all pending requests. */
88
+ dispose(): void {
89
+ this.closed = true;
90
+ for (const [id, p] of this.pending) {
91
+ clearTimeout(p.timer);
92
+ p.reject(new Error("JSON-RPC client disposed"));
93
+ this.pending.delete(id);
94
+ }
95
+ }
96
+
97
+ // ── Private ───────────────────────────────────────────────────────
98
+
99
+ private writeMessage(msg: JsonRpcMessage): void {
100
+ const body = JSON.stringify(msg);
101
+ const contentLength = Buffer.byteLength(body, "utf-8");
102
+ const header = `${CONTENT_LENGTH}${contentLength}${HEADER_DELIMITER}`;
103
+ this.output.write(header + body);
104
+ }
105
+
106
+ private onData(chunk: Buffer): void {
107
+ this.buffer = Buffer.concat([this.buffer, chunk]);
108
+ this.processBuffer();
109
+ }
110
+
111
+ private processBuffer(): void {
112
+ while (true) {
113
+ // Look for header delimiter
114
+ const headerEnd = this.buffer.indexOf(HEADER_DELIMITER);
115
+ if (headerEnd === -1) return;
116
+
117
+ // Parse Content-Length from headers
118
+ const headerText = this.buffer.subarray(0, headerEnd).toString("utf-8");
119
+ const contentLength = parseContentLength(headerText);
120
+ if (contentLength === null) {
121
+ // Malformed header — skip past delimiter and try again
122
+ this.buffer = this.buffer.subarray(headerEnd + HEADER_DELIMITER.length);
123
+ continue;
124
+ }
125
+
126
+ // Check if we have the full body
127
+ const bodyStart = headerEnd + HEADER_DELIMITER.length;
128
+ const messageEnd = bodyStart + contentLength;
129
+ if (this.buffer.length < messageEnd) {
130
+ return; // Need more data — partial message
131
+ }
132
+
133
+ // Extract and parse the body
134
+ const body = this.buffer.subarray(bodyStart, messageEnd).toString("utf-8");
135
+ this.buffer = this.buffer.subarray(messageEnd);
136
+
137
+ try {
138
+ const msg = JSON.parse(body) as JsonRpcMessage;
139
+ this.handleMessage(msg);
140
+ } catch {
141
+ // Malformed JSON — skip
142
+ }
143
+ }
144
+ }
145
+
146
+ private handleMessage(msg: JsonRpcMessage): void {
147
+ // Response (has id, has result or error)
148
+ if ("id" in msg && msg.id != null && ("result" in msg || "error" in msg)) {
149
+ const response = msg as JsonRpcResponse;
150
+ const pending = this.pending.get(response.id);
151
+ if (pending) {
152
+ this.pending.delete(response.id);
153
+ clearTimeout(pending.timer);
154
+ if (response.error) {
155
+ pending.reject(new Error(`LSP error ${response.error.code}: ${response.error.message}`));
156
+ } else {
157
+ pending.resolve(response.result);
158
+ }
159
+ }
160
+ return;
161
+ }
162
+
163
+ // Notification (no id)
164
+ if ("method" in msg && !("id" in msg)) {
165
+ const notification = msg as JsonRpcNotification;
166
+ this.notificationHandler?.(notification.method, notification.params);
167
+ }
168
+
169
+ // Request from server (has id + method) — we don't handle server→client requests yet
170
+ }
171
+
172
+ private onClose(): void {
173
+ this.closed = true;
174
+ for (const [id, p] of this.pending) {
175
+ clearTimeout(p.timer);
176
+ p.reject(new Error("JSON-RPC connection closed"));
177
+ this.pending.delete(id);
178
+ }
179
+ }
180
+ }
181
+
182
+ // ── Helpers ───────────────────────────────────────────────────────────
183
+
184
+ function parseContentLength(header: string): number | null {
185
+ for (const line of header.split("\r\n")) {
186
+ if (line.startsWith(CONTENT_LENGTH)) {
187
+ const value = parseInt(line.slice(CONTENT_LENGTH.length), 10);
188
+ if (Number.isFinite(value) && value >= 0) return value;
189
+ }
190
+ }
191
+ return null;
192
+ }
@@ -0,0 +1,143 @@
1
+ // LSP server configuration — load defaults, merge with supi config per language key.
2
+
3
+ import * as fs from "node:fs";
4
+ import * as path from "node:path";
5
+ import { loadSupiConfigForScope } from "@mrclrchtr/supi-core";
6
+ import type { LspConfig, ServerConfig } from "./types.ts";
7
+
8
+ // Load defaults at module level — resolve relative to this file.
9
+ // pi loads extensions via jiti, which always provides __dirname.
10
+ const DEFAULTS: LspConfig = JSON.parse(
11
+ fs.readFileSync(path.join(__dirname, "defaults.json"), "utf-8"),
12
+ ) as LspConfig;
13
+
14
+ // ── Public API ────────────────────────────────────────────────────────
15
+
16
+ export interface LoadConfigOptions {
17
+ homeDir?: string;
18
+ }
19
+
20
+ /** Map from language alias → canonical config key. */
21
+ export const LANGUAGE_ALIASES: Record<string, string> = {
22
+ cpp: "c",
23
+ };
24
+
25
+ /** Resolve a language name through aliases. */
26
+ export function resolveLanguageAlias(name: string): string {
27
+ return LANGUAGE_ALIASES[name] ?? name;
28
+ }
29
+
30
+ function resolveAliasesInOverrides(servers: Record<string, Partial<ServerConfig>>): void {
31
+ for (const [alias, target] of Object.entries(LANGUAGE_ALIASES)) {
32
+ if (servers[alias]) {
33
+ servers[target] = { ...(servers[target] ?? {}), ...servers[alias] };
34
+ delete servers[alias];
35
+ }
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Load LSP config: built-in defaults merged with per-language-key overrides
41
+ * from supi config (`~/.pi/agent/supi/config.json` and `.pi/supi/config.json`).
42
+ * Each language key merges individually; omitted fields fall back to defaults.
43
+ */
44
+ export function loadConfig(cwd: string, options?: LoadConfigOptions): LspConfig {
45
+ const defaults = DEFAULTS;
46
+
47
+ const globalLsp = loadSupiConfigForScope(
48
+ "lsp",
49
+ cwd,
50
+ { enabled: true, severity: 1, active: [], servers: {} as Record<string, ServerConfig> },
51
+ { scope: "global", homeDir: options?.homeDir },
52
+ );
53
+ const projectLsp = loadSupiConfigForScope(
54
+ "lsp",
55
+ cwd,
56
+ { enabled: true, severity: 1, active: [], servers: {} as Record<string, ServerConfig> },
57
+ { scope: "project" },
58
+ );
59
+
60
+ const merged = mergeServerConfigs(defaults.servers, globalLsp.servers, projectLsp.servers);
61
+
62
+ return { servers: merged };
63
+ }
64
+
65
+ /**
66
+ * Find which server config handles a given file extension.
67
+ * Returns [languageName, config] or null.
68
+ */
69
+ export function getServerForFile(
70
+ config: LspConfig,
71
+ filePath: string,
72
+ ): [string, ServerConfig] | null {
73
+ const ext = path.extname(filePath).slice(1).toLowerCase();
74
+ if (!ext) return null;
75
+
76
+ for (const [name, server] of Object.entries(config.servers)) {
77
+ if (server.fileTypes.includes(ext)) {
78
+ return [name, server];
79
+ }
80
+ }
81
+ return null;
82
+ }
83
+
84
+ // ── Private ───────────────────────────────────────────────────────────
85
+
86
+ function mergeServerConfigs(
87
+ defaults: Record<string, ServerConfig>,
88
+ globalOverrides: unknown,
89
+ projectOverrides: unknown,
90
+ ): Record<string, ServerConfig> {
91
+ const merged: Record<string, ServerConfig> = { ...defaults };
92
+
93
+ const globalServers = isServerRecord(globalOverrides) ? globalOverrides : {};
94
+ const projectServers = isServerRecord(projectOverrides) ? projectOverrides : {};
95
+
96
+ resolveAliasesInOverrides(globalServers);
97
+ resolveAliasesInOverrides(projectServers);
98
+
99
+ // Apply global per-key overrides against defaults
100
+ for (const [lang, override] of Object.entries(globalServers)) {
101
+ const result = mergeSingleServer(defaults[lang], override);
102
+ if (result) merged[lang] = result;
103
+ }
104
+
105
+ // Apply project per-key overrides against the result so far
106
+ for (const [lang, override] of Object.entries(projectServers)) {
107
+ const result = mergeSingleServer(merged[lang] ?? defaults[lang], override);
108
+ if (result) merged[lang] = result;
109
+ }
110
+
111
+ // Remove servers whose final merged config has enabled === false
112
+ for (const [lang, config] of Object.entries(merged)) {
113
+ if (config.enabled === false) {
114
+ delete merged[lang];
115
+ }
116
+ }
117
+
118
+ return merged;
119
+ }
120
+
121
+ function mergeSingleServer(
122
+ base: ServerConfig | undefined,
123
+ override: Partial<ServerConfig>,
124
+ ): ServerConfig | null {
125
+ if (!base) {
126
+ // New custom language — must have all required fields
127
+ if (
128
+ override.command &&
129
+ Array.isArray(override.fileTypes) &&
130
+ override.fileTypes.length > 0 &&
131
+ Array.isArray(override.rootMarkers) &&
132
+ override.rootMarkers.length > 0
133
+ ) {
134
+ return override as ServerConfig;
135
+ }
136
+ return null;
137
+ }
138
+ return { ...base, ...override };
139
+ }
140
+
141
+ function isServerRecord(value: unknown): value is Record<string, Partial<ServerConfig>> {
142
+ return typeof value === "object" && value !== null && !Array.isArray(value);
143
+ }
@@ -0,0 +1,82 @@
1
+ {
2
+ "servers": {
3
+ "typescript": {
4
+ "command": "typescript-language-server",
5
+ "args": ["--stdio"],
6
+ "fileTypes": ["ts", "tsx", "js", "jsx", "mts", "cts", "mjs", "cjs"],
7
+ "rootMarkers": ["tsconfig.json", "jsconfig.json", "package.json"]
8
+ },
9
+ "python": {
10
+ "command": "pyright-langserver",
11
+ "args": ["--stdio"],
12
+ "fileTypes": ["py", "pyi"],
13
+ "rootMarkers": [
14
+ "pyproject.toml",
15
+ "setup.py",
16
+ "setup.cfg",
17
+ "requirements.txt",
18
+ "pyrightconfig.json"
19
+ ]
20
+ },
21
+ "rust": {
22
+ "command": "rust-analyzer",
23
+ "args": [],
24
+ "fileTypes": ["rs"],
25
+ "rootMarkers": ["Cargo.toml"]
26
+ },
27
+ "go": {
28
+ "command": "gopls",
29
+ "args": ["serve"],
30
+ "fileTypes": ["go", "mod"],
31
+ "rootMarkers": ["go.mod", "go.sum"]
32
+ },
33
+ "c": {
34
+ "command": "clangd",
35
+ "args": ["--background-index"],
36
+ "fileTypes": ["c", "h", "cpp", "hpp", "cc", "cxx", "hxx", "c++", "h++"],
37
+ "rootMarkers": ["compile_commands.json", "CMakeLists.txt", ".clangd", "Makefile"]
38
+ },
39
+ "ruby": {
40
+ "command": "ruby-lsp",
41
+ "args": [],
42
+ "fileTypes": ["rb", "erb", "gemspec"],
43
+ "rootMarkers": ["Gemfile", ".ruby-version"]
44
+ },
45
+ "java": {
46
+ "command": "jdtls",
47
+ "args": [],
48
+ "fileTypes": ["java"],
49
+ "rootMarkers": ["pom.xml", "build.gradle"]
50
+ },
51
+ "kotlin": {
52
+ "command": "kotlin-lsp",
53
+ "args": [],
54
+ "fileTypes": ["kt", "kts"],
55
+ "rootMarkers": ["build.gradle.kts", "pom.xml"]
56
+ },
57
+ "bash": {
58
+ "command": "bash-language-server",
59
+ "args": ["start"],
60
+ "fileTypes": ["sh", "bash", "zsh", "ksh"],
61
+ "rootMarkers": []
62
+ },
63
+ "html": {
64
+ "command": "vscode-html-language-server",
65
+ "args": ["--stdio"],
66
+ "fileTypes": ["html", "htm", "xhtml"],
67
+ "rootMarkers": ["package.json"]
68
+ },
69
+ "sql": {
70
+ "command": "sql-language-server",
71
+ "args": ["up", "--method", "stdio"],
72
+ "fileTypes": ["sql"],
73
+ "rootMarkers": []
74
+ },
75
+ "r": {
76
+ "command": "R",
77
+ "args": ["--slave", "-e", "languageserver::run()"],
78
+ "fileTypes": ["r"],
79
+ "rootMarkers": ["DESCRIPTION", "renv.lock", ".Rprofile"]
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,82 @@
1
+ import * as path from "node:path";
2
+ import type { LspManager } from "../manager/manager.ts";
3
+ import type { Diagnostic, Hover, MarkedString, MarkupContent } from "../types.ts";
4
+
5
+ const AUGMENT_TIMEOUT_MS = 500;
6
+
7
+ /**
8
+ * Augment diagnostics with LSP hover and code_actions at the first severity-1 error.
9
+ * Silently returns null if LSP is unavailable, times out, or there are no errors.
10
+ */
11
+ export async function augmentDiagnostics(
12
+ filePath: string,
13
+ diags: Diagnostic[],
14
+ manager: LspManager,
15
+ _cwd: string,
16
+ ): Promise<string | null> {
17
+ const firstError = diags.find((d) => d.severity === 1);
18
+ if (!firstError) return null;
19
+
20
+ const resolvedPath = path.resolve(filePath);
21
+ const client = await manager.getClientForFile(filePath);
22
+ if (!client) return null;
23
+
24
+ const pos = firstError.range.start;
25
+
26
+ const [hoverResult, codeActionsResult] = await Promise.all([
27
+ withTimeout(client.hover(resolvedPath, pos), AUGMENT_TIMEOUT_MS),
28
+ withTimeout(
29
+ client.codeActions(resolvedPath, { start: pos, end: pos }, { diagnostics: [firstError] }),
30
+ AUGMENT_TIMEOUT_MS,
31
+ ),
32
+ ]);
33
+
34
+ const parts: string[] = [];
35
+
36
+ if (hoverResult) {
37
+ const hoverText = formatHoverForDiagnostics(hoverResult);
38
+ if (hoverText) parts.push(`💡 Hover info:\n${hoverText}`);
39
+ }
40
+
41
+ if (codeActionsResult && codeActionsResult.length > 0) {
42
+ const titles = codeActionsResult.map((a) => a.title).join(", ");
43
+ parts.push(`💡 Available fix: ${titles}`);
44
+ }
45
+
46
+ return parts.length > 0 ? parts.join("\n") : null;
47
+ }
48
+
49
+ /**
50
+ * Extract raw hover text for inline diagnostic augmentation.
51
+ * Intentionally strips markdown code-block framing (unlike formatHover)
52
+ * to keep augmentation concise and readable inside diagnostic output.
53
+ */
54
+ function formatHoverForDiagnostics(hover: Hover): string {
55
+ const contents = hover.contents;
56
+ let text = "";
57
+
58
+ if (typeof contents === "string") {
59
+ text = contents;
60
+ } else if ("value" in contents) {
61
+ const mc = contents as MarkupContent | { language: string; value: string };
62
+ if ("kind" in mc) text = mc.value;
63
+ else text = mc.value;
64
+ } else if (Array.isArray(contents)) {
65
+ text = (contents as MarkedString[])
66
+ .map((c) => (typeof c === "string" ? c : c.value))
67
+ .join("\n");
68
+ }
69
+
70
+ const lines = text.split("\n").slice(0, 3);
71
+ return lines.join("\n");
72
+ }
73
+
74
+ async function withTimeout<T>(promise: Promise<T | null>, ms: number): Promise<T | null> {
75
+ let timer: ReturnType<typeof setTimeout> | null = null;
76
+ const timeoutPromise = new Promise<null>((resolve) => {
77
+ timer = setTimeout(() => resolve(null), ms);
78
+ });
79
+ const result = await Promise.race([promise, timeoutPromise]);
80
+ if (timer) clearTimeout(timer);
81
+ return result;
82
+ }
@@ -0,0 +1,68 @@
1
+ import type { OutstandingDiagnosticSummaryEntry } from "../manager/manager-types.ts";
2
+ import type { Diagnostic } from "../types.ts";
3
+
4
+ export function formatDiagnosticsDisplayContent(
5
+ diagnostics: OutstandingDiagnosticSummaryEntry[],
6
+ detailed?: Array<{ file: string; diagnostics: Diagnostic[] }>,
7
+ ): string {
8
+ const totals = collectDisplayTotals(diagnostics);
9
+ const summary = buildDisplaySummary(totals);
10
+
11
+ if (!detailed || detailed.length === 0) return summary;
12
+
13
+ const detailLines = buildDisplayDetailLines(detailed);
14
+ return detailLines.length > 0 ? `${summary}\n${detailLines.join("\n")}` : summary;
15
+ }
16
+
17
+ function collectDisplayTotals(
18
+ diagnostics: Array<{ errors: number; warnings: number; information: number; hints: number }>,
19
+ ) {
20
+ return diagnostics.reduce(
21
+ (acc, d) => ({
22
+ errors: acc.errors + d.errors,
23
+ warnings: acc.warnings + d.warnings,
24
+ information: acc.information + d.information,
25
+ hints: acc.hints + d.hints,
26
+ }),
27
+ { errors: 0, warnings: 0, information: 0, hints: 0 },
28
+ );
29
+ }
30
+
31
+ function buildDisplaySummary(totals: {
32
+ errors: number;
33
+ warnings: number;
34
+ information: number;
35
+ hints: number;
36
+ }): string {
37
+ const parts: string[] = [];
38
+ if (totals.errors > 0) parts.push(`${totals.errors} error${totals.errors === 1 ? "" : "s"}`);
39
+ if (totals.warnings > 0)
40
+ parts.push(`${totals.warnings} warning${totals.warnings === 1 ? "" : "s"}`);
41
+ if (totals.information > 0)
42
+ parts.push(`${totals.information} info${totals.information === 1 ? "" : "s"}`);
43
+ if (totals.hints > 0) parts.push(`${totals.hints} hint${totals.hints === 1 ? "" : "s"}`);
44
+
45
+ return parts.length > 0
46
+ ? `LSP diagnostics injected (${parts.join(", ")})`
47
+ : "LSP diagnostics injected";
48
+ }
49
+
50
+ function buildDisplayDetailLines(
51
+ detailed: Array<{
52
+ file: string;
53
+ diagnostics: Diagnostic[];
54
+ }>,
55
+ ): string[] {
56
+ const lines: string[] = [];
57
+ for (const entry of detailed) {
58
+ for (const d of entry.diagnostics.slice(0, 3)) {
59
+ const line = d.range.start.line + 1;
60
+ const source = d.source ? ` ${d.source}` : "";
61
+ lines.push(` ${entry.file} L${line}${source}: ${d.message}`);
62
+ }
63
+ if (entry.diagnostics.length > 3) {
64
+ lines.push(` +${entry.diagnostics.length - 3} more`);
65
+ }
66
+ }
67
+ return lines;
68
+ }
@@ -0,0 +1,73 @@
1
+ import type { OutstandingDiagnosticSummaryEntry } from "../manager/manager-types.ts";
2
+ import { isGlobMatch } from "../pattern-matcher.ts";
3
+ import { displayRelativeFilePath, shouldIgnoreLspPath } from "../summary.ts";
4
+ import { type Diagnostic, DiagnosticSeverity } from "../types.ts";
5
+
6
+ export function collectDiagnosticSummaryCounts(
7
+ fileDiags: Map<string, { errors: number; warnings: number }>,
8
+ entry: { uri: string; diagnostics: Diagnostic[] },
9
+ cwd: string,
10
+ excludePatterns?: string[],
11
+ ): void {
12
+ const file = relativeFilePathFromUri(entry.uri, cwd);
13
+ if (shouldIgnoreLspPath(file, cwd)) return;
14
+ if (excludePatterns?.some((p) => isGlobMatch(file, p))) return;
15
+
16
+ const current = fileDiags.get(file) ?? { errors: 0, warnings: 0 };
17
+ for (const diagnostic of entry.diagnostics) {
18
+ if (diagnostic.severity === DiagnosticSeverity.Error) current.errors++;
19
+ else if (diagnostic.severity === DiagnosticSeverity.Warning) current.warnings++;
20
+ }
21
+ fileDiags.set(file, current);
22
+ }
23
+
24
+ export function createOutstandingDiagnosticSummary(
25
+ file: string,
26
+ ): OutstandingDiagnosticSummaryEntry {
27
+ return {
28
+ file,
29
+ total: 0,
30
+ errors: 0,
31
+ warnings: 0,
32
+ information: 0,
33
+ hints: 0,
34
+ };
35
+ }
36
+
37
+ export function accumulateOutstandingDiagnostics(
38
+ current: OutstandingDiagnosticSummaryEntry,
39
+ diagnostics: Diagnostic[],
40
+ maxSeverity: number,
41
+ ): OutstandingDiagnosticSummaryEntry {
42
+ const next = { ...current };
43
+
44
+ for (const diagnostic of diagnostics) {
45
+ if (!isDiagnosticWithinThreshold(diagnostic, maxSeverity)) continue;
46
+
47
+ next.total++;
48
+ incrementOutstandingDiagnosticCount(next, diagnostic.severity);
49
+ }
50
+
51
+ return next;
52
+ }
53
+
54
+ export function relativeFilePathFromUri(uri: string, cwd: string): string {
55
+ return displayRelativeFilePath(uri.replace("file://", ""), cwd);
56
+ }
57
+
58
+ function isDiagnosticWithinThreshold(
59
+ diagnostic: Diagnostic,
60
+ maxSeverity: number,
61
+ ): diagnostic is Diagnostic & { severity: number } {
62
+ return diagnostic.severity !== undefined && diagnostic.severity <= maxSeverity;
63
+ }
64
+
65
+ function incrementOutstandingDiagnosticCount(
66
+ entry: OutstandingDiagnosticSummaryEntry,
67
+ severity: number,
68
+ ): void {
69
+ if (severity === DiagnosticSeverity.Error) entry.errors++;
70
+ else if (severity === DiagnosticSeverity.Warning) entry.warnings++;
71
+ else if (severity === DiagnosticSeverity.Information) entry.information++;
72
+ else if (severity === DiagnosticSeverity.Hint) entry.hints++;
73
+ }