@koriit/opencode-claude-bridge 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.
- package/LICENSE +21 -0
- package/README.md +264 -0
- package/package.json +54 -0
- package/src/config.ts +82 -0
- package/src/frontmatter.ts +159 -0
- package/src/index.ts +158 -0
- package/src/inject.ts +480 -0
- package/src/logger.ts +54 -0
- package/src/lsp-inject.ts +405 -0
- package/src/mcp-inject.ts +381 -0
- package/src/naming.ts +122 -0
- package/src/opencode-builtins.ts +98 -0
- package/src/selection.ts +122 -0
- package/src/skill-inject.ts +480 -0
- package/src/skill-scan.ts +349 -0
- package/src/types.ts +46 -0
- package/src/version.ts +114 -0
|
@@ -0,0 +1,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LSP injection from enabled Claude plugins into OpenCode's `cfg.lsp` record.
|
|
3
|
+
*
|
|
4
|
+
* Only runs when the global `allowLsp` toggle is on AND the user has not explicitly
|
|
5
|
+
* disabled all LSP via `cfg.lsp === false` (safe-by-default per §8).
|
|
6
|
+
*
|
|
7
|
+
* Source: each plugin's top-level `<installPath>/.lsp.json`.
|
|
8
|
+
* The file format mirrors the Claude LSP plugin schema (verified from claude-code
|
|
9
|
+
* `lspPluginIntegration.ts` and real installed LSP plugins):
|
|
10
|
+
*
|
|
11
|
+
* ```json
|
|
12
|
+
* {
|
|
13
|
+
* "<serverName>": {
|
|
14
|
+
* "command": "rust-analyzer",
|
|
15
|
+
* "args": ["--arg"],
|
|
16
|
+
* "extensionToLanguage": { ".rs": "rust" },
|
|
17
|
+
* "env": { "KEY": "value" },
|
|
18
|
+
* "initializationOptions": { ... }
|
|
19
|
+
* }
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* Mapping (Claude .lsp.json → OpenCode V1 cfg.lsp entry):
|
|
24
|
+
* - `command` (string) + `args` (array) → `command` (string array)
|
|
25
|
+
* - `extensionToLanguage` keys → `extensions` (array of file-extension strings)
|
|
26
|
+
* - `env` → `env`
|
|
27
|
+
* - `initializationOptions` or `settings` → `initialization`
|
|
28
|
+
* - `${CLAUDE_PLUGIN_ROOT}` resolved to `installPath` in command/args/env values
|
|
29
|
+
*
|
|
30
|
+
* SOURCE GAP NOTE: No real installed Claude plugin with LSP servers was available on
|
|
31
|
+
* this machine (the rust-analyzer-lsp@claude-plugins-official plugin has only README/LICENSE,
|
|
32
|
+
* no `.lsp.json`). The `.lsp.json` source convention is derived from claude-code source
|
|
33
|
+
* (`lspPluginIntegration.ts`) which explicitly checks `join(plugin.path, '.lsp.json')`.
|
|
34
|
+
* This is an Appendix-B–style open item: the mechanism is implemented and tested against
|
|
35
|
+
* a synthetic fixture, but real-world `.lsp.json` files have not been confirmed against
|
|
36
|
+
* a live installed plugin. Validate when an LSP plugin becomes available.
|
|
37
|
+
*
|
|
38
|
+
* `cfg.lsp === false` guard: if the user explicitly disabled all LSP servers, the bridge
|
|
39
|
+
* injects nothing and returns immediately (§6.5 — do not override user intent). This only
|
|
40
|
+
* skips LSP; commands, agents, skills, and MCP still inject as normal.
|
|
41
|
+
*
|
|
42
|
+
* Naming: base name `<plugin>-<server>`, then apply the §7 NameAllocator collision rules
|
|
43
|
+
* vs existing `cfg.lsp` keys (when `cfg.lsp` is an object).
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import fs from "node:fs/promises"
|
|
47
|
+
import path from "node:path"
|
|
48
|
+
import type { Config } from "@opencode-ai/plugin"
|
|
49
|
+
import { NameAllocator, splitPluginId } from "./naming.js"
|
|
50
|
+
import type { Logger } from "./logger.js"
|
|
51
|
+
import type { ClaudePlugin } from "./types.js"
|
|
52
|
+
|
|
53
|
+
// ── OpenCode V1 LSP entry shape ────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* OpenCode V1 LSP entry shape (verified against packages/core/src/v1/config/lsp.ts).
|
|
57
|
+
*
|
|
58
|
+
* `cfg.lsp[name]` can be `{ disabled: true }` or a live server definition.
|
|
59
|
+
* We only ever write live entries; we never write `{ disabled: true }`.
|
|
60
|
+
*/
|
|
61
|
+
export interface LspEntry {
|
|
62
|
+
command: string[]
|
|
63
|
+
extensions?: string[]
|
|
64
|
+
disabled?: boolean
|
|
65
|
+
env?: Record<string, string>
|
|
66
|
+
initialization?: Record<string, unknown>
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Config shape guard ────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
interface InjectableLspConfig {
|
|
72
|
+
lsp?: false | true | Record<string, LspEntry> | undefined
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Normalize `cfg.lsp` to a mutable object we can extend.
|
|
77
|
+
*
|
|
78
|
+
* Respects `cfg.lsp === false` — callers MUST check before calling this.
|
|
79
|
+
* `true` and `undefined` are normalized to `{}`.
|
|
80
|
+
* When already an object, returns it as-is.
|
|
81
|
+
*/
|
|
82
|
+
function guardLspConfig(cfg: InjectableLspConfig): Record<string, LspEntry> {
|
|
83
|
+
if (cfg.lsp === false) {
|
|
84
|
+
// Caller must not reach here; guard is defensive.
|
|
85
|
+
cfg.lsp = {}
|
|
86
|
+
} else if (cfg.lsp === undefined || cfg.lsp === true) {
|
|
87
|
+
cfg.lsp = {}
|
|
88
|
+
}
|
|
89
|
+
return cfg.lsp as Record<string, LspEntry>
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ── Claude .lsp.json schema ───────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Claude plugin LSP server definition as read from `.lsp.json`.
|
|
96
|
+
* Schema derived from claude-code `LspServerConfigSchema` (src/utils/plugins/schemas.ts).
|
|
97
|
+
*/
|
|
98
|
+
interface ClaudeLspServer {
|
|
99
|
+
command?: string
|
|
100
|
+
args?: string[]
|
|
101
|
+
extensionToLanguage?: Record<string, string>
|
|
102
|
+
env?: Record<string, string>
|
|
103
|
+
initializationOptions?: unknown
|
|
104
|
+
settings?: unknown
|
|
105
|
+
transport?: string
|
|
106
|
+
workspaceFolder?: string
|
|
107
|
+
[key: string]: unknown
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// ── Resolution helpers ────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Resolve `${CLAUDE_PLUGIN_ROOT}` in a string to the absolute `installPath`.
|
|
114
|
+
*/
|
|
115
|
+
function resolvePluginRoot(text: string, installPath: string): string {
|
|
116
|
+
return text.replaceAll("${CLAUDE_PLUGIN_ROOT}", installPath)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Resolve `${CLAUDE_PLUGIN_ROOT}` in all string values of a record.
|
|
121
|
+
*
|
|
122
|
+
* Non-string values (e.g. a numeric env value from a loose JSON file) are
|
|
123
|
+
* silently dropped rather than reaching `String.prototype.replaceAll` and
|
|
124
|
+
* throwing a TypeError that would kill the whole hook run (§10).
|
|
125
|
+
*/
|
|
126
|
+
function resolveRecordValues(
|
|
127
|
+
record: Record<string, unknown>,
|
|
128
|
+
installPath: string,
|
|
129
|
+
): Record<string, string> {
|
|
130
|
+
const out: Record<string, string> = {}
|
|
131
|
+
for (const [k, v] of Object.entries(record)) {
|
|
132
|
+
if (typeof v === "string") out[k] = resolvePluginRoot(v, installPath)
|
|
133
|
+
// Non-string values are dropped — not valid in the target string-record fields (env).
|
|
134
|
+
}
|
|
135
|
+
return out
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Claude → OpenCode mapping ─────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
/** Discriminated result from {@link mapClaudeLspServer}. */
|
|
141
|
+
export type MapLspResult =
|
|
142
|
+
| { ok: true; entry: LspEntry }
|
|
143
|
+
| { ok: false; reason: "missing-command" | "socket-transport" | "no-extensions" }
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Map a Claude `.lsp.json` server entry to an OpenCode V1 LSP entry.
|
|
147
|
+
*
|
|
148
|
+
* - `command` (string) + `args` (array) → `command` (string array)
|
|
149
|
+
* - `extensionToLanguage` keys → `extensions` (file extension strings, required)
|
|
150
|
+
* - `env` → `env` (with `${CLAUDE_PLUGIN_ROOT}` resolved)
|
|
151
|
+
* - `initializationOptions` or `settings` → `initialization` (best-effort)
|
|
152
|
+
* - `${CLAUDE_PLUGIN_ROOT}` resolved in command, args, env values
|
|
153
|
+
* - `workspaceFolder` is not mapped (no equivalent in OpenCode V1 LSP entry shape)
|
|
154
|
+
*
|
|
155
|
+
* Returns a discriminated result so callers emit precise per-reason warnings. The
|
|
156
|
+
* three rejection reasons are:
|
|
157
|
+
* - `"missing-command"`: no `command` string field
|
|
158
|
+
* - `"socket-transport"`: `transport: "socket"` is unsupported by OpenCode's LSP layer
|
|
159
|
+
* - `"no-extensions"`: no `.`-prefixed keys in `extensionToLanguage`; OpenCode V1
|
|
160
|
+
* requires `extensions` for any non-builtin cfg.lsp key
|
|
161
|
+
*/
|
|
162
|
+
export function mapClaudeLspServer(
|
|
163
|
+
server: ClaudeLspServer,
|
|
164
|
+
installPath: string,
|
|
165
|
+
): MapLspResult {
|
|
166
|
+
if (!server.command || typeof server.command !== "string") {
|
|
167
|
+
return { ok: false, reason: "missing-command" }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// OpenCode V1 LSP has no socket transport — silently misbehaves if injected as stdio.
|
|
171
|
+
if (server.transport === "socket") {
|
|
172
|
+
return { ok: false, reason: "socket-transport" }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const cmd = resolvePluginRoot(server.command, installPath)
|
|
176
|
+
const args = Array.isArray(server.args)
|
|
177
|
+
? server.args.map((a) =>
|
|
178
|
+
typeof a === "string" ? resolvePluginRoot(a, installPath) : String(a),
|
|
179
|
+
)
|
|
180
|
+
: []
|
|
181
|
+
|
|
182
|
+
const entry: LspEntry = { command: [cmd, ...args] }
|
|
183
|
+
|
|
184
|
+
// Extract extensions from extensionToLanguage keys. OpenCode V1 REQUIRES `extensions`
|
|
185
|
+
// for any non-builtin cfg.lsp key; all bridge-injected names are custom by construction.
|
|
186
|
+
if (server.extensionToLanguage && typeof server.extensionToLanguage === "object") {
|
|
187
|
+
const keys = Object.keys(server.extensionToLanguage).filter(
|
|
188
|
+
(k) => typeof k === "string" && k.startsWith("."),
|
|
189
|
+
)
|
|
190
|
+
if (keys.length > 0) entry.extensions = keys
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (!entry.extensions || entry.extensions.length === 0) {
|
|
194
|
+
return { ok: false, reason: "no-extensions" }
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (server.env && typeof server.env === "object") {
|
|
198
|
+
entry.env = resolveRecordValues(server.env as Record<string, unknown>, installPath)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// initializationOptions takes precedence over settings (both are best-effort).
|
|
202
|
+
const initSource =
|
|
203
|
+
server.initializationOptions !== undefined
|
|
204
|
+
? server.initializationOptions
|
|
205
|
+
: server.settings !== undefined
|
|
206
|
+
? server.settings
|
|
207
|
+
: undefined
|
|
208
|
+
|
|
209
|
+
if (initSource !== undefined && typeof initSource === "object" && initSource !== null) {
|
|
210
|
+
entry.initialization = initSource as Record<string, unknown>
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { ok: true, entry }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// ── Per-plugin injection ──────────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Read and parse `<installPath>/.lsp.json`. Returns the parsed servers record or
|
|
220
|
+
* `null` if the file is absent (silent) or unreadable (warns and skips).
|
|
221
|
+
*/
|
|
222
|
+
async function readLspJson(
|
|
223
|
+
installPath: string,
|
|
224
|
+
pluginId: string,
|
|
225
|
+
logger: Logger,
|
|
226
|
+
): Promise<Record<string, ClaudeLspServer> | null> {
|
|
227
|
+
const lspPath = path.join(installPath, ".lsp.json")
|
|
228
|
+
let raw: string
|
|
229
|
+
try {
|
|
230
|
+
raw = await fs.readFile(lspPath, "utf8")
|
|
231
|
+
} catch (err) {
|
|
232
|
+
const code = (err as NodeJS.ErrnoException).code
|
|
233
|
+
if (code === "ENOENT") return null // no LSP for this plugin — silent
|
|
234
|
+
logger.warn(
|
|
235
|
+
`could not read ".lsp.json" from plugin "${pluginId}" (${err instanceof Error ? err.message : String(err)}); skipping LSP for this plugin`,
|
|
236
|
+
)
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(raw) as unknown
|
|
242
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
243
|
+
logger.warn(
|
|
244
|
+
`".lsp.json" from plugin "${pluginId}" is not a JSON object; skipping LSP for this plugin`,
|
|
245
|
+
)
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
return parsed as Record<string, ClaudeLspServer>
|
|
249
|
+
} catch (err) {
|
|
250
|
+
logger.warn(
|
|
251
|
+
`could not parse ".lsp.json" from plugin "${pluginId}" (${err instanceof Error ? err.message : String(err)}); skipping LSP for this plugin`,
|
|
252
|
+
)
|
|
253
|
+
return null
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Inject LSP servers from one plugin's `.lsp.json` into `lspCfg`.
|
|
259
|
+
*/
|
|
260
|
+
async function injectPluginLsp(
|
|
261
|
+
plugin: ClaudePlugin,
|
|
262
|
+
lspCfg: Record<string, LspEntry>,
|
|
263
|
+
allocator: NameAllocator,
|
|
264
|
+
summary: LspInjectionSummary,
|
|
265
|
+
logger: Logger,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
const servers = await readLspJson(plugin.installPath, plugin.id, logger)
|
|
268
|
+
if (servers === null) return
|
|
269
|
+
|
|
270
|
+
const pluginPart = splitPluginId(plugin.id).plugin
|
|
271
|
+
|
|
272
|
+
for (const [serverName, serverDef] of Object.entries(servers)) {
|
|
273
|
+
if (typeof serverDef !== "object" || serverDef === null || Array.isArray(serverDef)) {
|
|
274
|
+
logger.warn(
|
|
275
|
+
`LSP server "${serverName}" in plugin "${plugin.id}" is not a valid object; skipping`,
|
|
276
|
+
)
|
|
277
|
+
continue
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const result = mapClaudeLspServer(serverDef, plugin.installPath)
|
|
281
|
+
if (!result.ok) {
|
|
282
|
+
// Precise per-reason warning — the discriminated result prevents misclassification
|
|
283
|
+
// if new rejection reasons are added to mapClaudeLspServer later.
|
|
284
|
+
if (result.reason === "socket-transport") {
|
|
285
|
+
logger.warn(
|
|
286
|
+
`LSP server "${serverName}" in plugin "${plugin.id}" uses socket transport which is not supported by the bridge; skipping`,
|
|
287
|
+
)
|
|
288
|
+
} else if (result.reason === "missing-command") {
|
|
289
|
+
logger.warn(
|
|
290
|
+
`LSP server "${serverName}" in plugin "${plugin.id}" is missing required "command" field; skipping`,
|
|
291
|
+
)
|
|
292
|
+
} else {
|
|
293
|
+
// "no-extensions": missing or empty extensionToLanguage — OpenCode requires extensions for custom servers.
|
|
294
|
+
logger.warn(
|
|
295
|
+
`LSP server "${serverName}" in plugin "${plugin.id}" has no "extensionToLanguage" mapping; OpenCode requires extensions for custom LSP servers — skipping`,
|
|
296
|
+
)
|
|
297
|
+
}
|
|
298
|
+
continue
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Base name is always "<plugin>-<server>" per §6.5 — intentional prefix.
|
|
302
|
+
// On collision the NameAllocator ladder escalates from this already-prefixed base.
|
|
303
|
+
// pluginPart is hoisted outside the loop (splitPluginId is loop-invariant).
|
|
304
|
+
const baseName = `${pluginPart}-${serverName}`
|
|
305
|
+
const { name, renamed } = allocator.claim(plugin.id, baseName)
|
|
306
|
+
|
|
307
|
+
lspCfg[name] = result.entry
|
|
308
|
+
summary.servers++
|
|
309
|
+
if (renamed) summary.renamed++
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Count LSP servers in a plugin's `.lsp.json` without any validation or warnings.
|
|
315
|
+
* Used only for the policy-gate summary when `allowLsp` is off.
|
|
316
|
+
* Returns 0 if the file is absent, unreadable, or malformed.
|
|
317
|
+
*/
|
|
318
|
+
async function countLspServersQuiet(installPath: string): Promise<number> {
|
|
319
|
+
const lspPath = path.join(installPath, ".lsp.json")
|
|
320
|
+
try {
|
|
321
|
+
const raw = await fs.readFile(lspPath, "utf8")
|
|
322
|
+
const parsed = JSON.parse(raw) as unknown
|
|
323
|
+
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
|
|
324
|
+
return Object.keys(parsed as object).length
|
|
325
|
+
}
|
|
326
|
+
return 0
|
|
327
|
+
} catch {
|
|
328
|
+
return 0
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ── Public API ────────────────────────────────────────────────────────────────
|
|
333
|
+
|
|
334
|
+
/** Summary counters for LSP injection collected across all plugins. */
|
|
335
|
+
export interface LspInjectionSummary {
|
|
336
|
+
servers: number
|
|
337
|
+
renamed: number
|
|
338
|
+
skippedPolicy: number
|
|
339
|
+
/** True when cfg.lsp === false and injection was skipped per user intent. */
|
|
340
|
+
skippedUserDisabled: boolean
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Inject LSP servers from all `plugins` into the mutable `cfg`.
|
|
345
|
+
*
|
|
346
|
+
* Guards:
|
|
347
|
+
* 1. When `allowLsp` is false, logs a policy summary and injects nothing.
|
|
348
|
+
* 2. When `cfg.lsp === false`, the user has explicitly disabled all LSP — inject
|
|
349
|
+
* nothing, but this ONLY skips LSP (commands/agents/skills/MCP still inject).
|
|
350
|
+
*
|
|
351
|
+
* Builds one `NameAllocator` seeded with existing `cfg.lsp` keys (when `cfg.lsp`
|
|
352
|
+
* is already an object). Processes plugins in the order given (caller passes
|
|
353
|
+
* sorted-by-id order).
|
|
354
|
+
*/
|
|
355
|
+
export async function injectLsp(
|
|
356
|
+
plugins: ClaudePlugin[],
|
|
357
|
+
cfg: Config,
|
|
358
|
+
allowLsp: boolean,
|
|
359
|
+
logger: Logger,
|
|
360
|
+
): Promise<LspInjectionSummary> {
|
|
361
|
+
const summary: LspInjectionSummary = {
|
|
362
|
+
servers: 0,
|
|
363
|
+
renamed: 0,
|
|
364
|
+
skippedPolicy: 0,
|
|
365
|
+
skippedUserDisabled: false,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const mutableCfg = cfg as unknown as InjectableLspConfig
|
|
369
|
+
|
|
370
|
+
// Guard 1: §6.5 — if user set cfg.lsp === false, respect their intent. Skip LSP only.
|
|
371
|
+
if (mutableCfg.lsp === false) {
|
|
372
|
+
summary.skippedUserDisabled = true
|
|
373
|
+
logger.info("skipped LSP injection (cfg.lsp is false — user disabled all LSP)")
|
|
374
|
+
return summary
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!allowLsp) {
|
|
378
|
+
// Policy gate: count potential servers without per-entry validation — a user who
|
|
379
|
+
// opted out of LSP should not see LSP-config warnings from plugins they never enabled.
|
|
380
|
+
let totalServers = 0
|
|
381
|
+
for (const plugin of plugins) {
|
|
382
|
+
totalServers += await countLspServersQuiet(plugin.installPath)
|
|
383
|
+
}
|
|
384
|
+
summary.skippedPolicy = totalServers
|
|
385
|
+
if (totalServers > 0) {
|
|
386
|
+
logger.info(`skipped ${totalServers} LSP server(s) (policy: allowLsp is off)`)
|
|
387
|
+
}
|
|
388
|
+
return summary
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (plugins.length === 0) return summary
|
|
392
|
+
|
|
393
|
+
// Normalize cfg.lsp to an object.
|
|
394
|
+
const lspCfg = guardLspConfig(mutableCfg)
|
|
395
|
+
|
|
396
|
+
// Seed the allocator with existing cfg.lsp keys.
|
|
397
|
+
const existingLsp = new Set<string>(Object.keys(lspCfg))
|
|
398
|
+
const allocator = new NameAllocator(existingLsp)
|
|
399
|
+
|
|
400
|
+
for (const plugin of plugins) {
|
|
401
|
+
await injectPluginLsp(plugin, lspCfg, allocator, summary, logger)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return summary
|
|
405
|
+
}
|