muonroi-cli 1.8.5 → 1.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.
- package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
- package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
- package/dist/src/agent-harness/mock-model.d.ts +10 -0
- package/dist/src/agent-harness/mock-model.js +6 -2
- package/dist/src/chat/chat-keychain.d.ts +7 -12
- package/dist/src/chat/chat-keychain.js +19 -86
- package/dist/src/cli/keys.d.ts +8 -45
- package/dist/src/cli/keys.js +18 -324
- package/dist/src/council/clarifier.d.ts +4 -2
- package/dist/src/council/clarifier.js +158 -36
- package/dist/src/council/debate-planner.js +3 -2
- package/dist/src/council/debate.js +59 -5
- package/dist/src/council/index.d.ts +23 -0
- package/dist/src/council/index.js +37 -2
- package/dist/src/council/llm.d.ts +62 -0
- package/dist/src/council/llm.js +123 -23
- package/dist/src/council/panel-select.js +13 -3
- package/dist/src/council/planner.js +17 -0
- package/dist/src/council/preflight.d.ts +10 -0
- package/dist/src/council/preflight.js +36 -0
- package/dist/src/council/prompts.d.ts +9 -1
- package/dist/src/council/prompts.js +23 -6
- package/dist/src/council/types.d.ts +10 -0
- package/dist/src/ee/auth.d.ts +19 -0
- package/dist/src/ee/auth.js +39 -0
- package/dist/src/ee/client.js +28 -3
- package/dist/src/ee/ee-onboarding.js +6 -26
- package/dist/src/flow/compaction/compress.d.ts +2 -2
- package/dist/src/flow/compaction/compress.js +21 -8
- package/dist/src/flow/compaction/extract.d.ts +3 -3
- package/dist/src/flow/compaction/extract.js +6 -6
- package/dist/src/flow/compaction/index.d.ts +2 -1
- package/dist/src/flow/compaction/index.js +29 -3
- package/dist/src/flow/compaction/progress.d.ts +35 -0
- package/dist/src/flow/compaction/progress.js +35 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/flags.d.ts +11 -0
- package/dist/src/gsd/flags.js +19 -0
- package/dist/src/gsd/plan-council.js +104 -72
- package/dist/src/gsd/verdict-schema.d.ts +1 -1
- package/dist/src/headless/council-answers.js +4 -0
- package/dist/src/index.js +129 -260
- package/dist/src/lsp/builtins.js +3 -1
- package/dist/src/lsp/manager.d.ts +5 -1
- package/dist/src/lsp/manager.js +249 -3
- package/dist/src/lsp/npm-cache.d.ts +11 -1
- package/dist/src/lsp/npm-cache.js +17 -1
- package/dist/src/lsp/runtime.d.ts +6 -1
- package/dist/src/lsp/runtime.js +17 -1
- package/dist/src/lsp/types.d.ts +83 -1
- package/dist/src/lsp/types.js +10 -0
- package/dist/src/mcp/client-pool.js +43 -15
- package/dist/src/mcp/lsp-tools.d.ts +5 -1
- package/dist/src/mcp/lsp-tools.js +93 -2
- package/dist/src/mcp/mcp-keychain.d.ts +3 -5
- package/dist/src/mcp/mcp-keychain.js +9 -49
- package/dist/src/mcp/setup-guide-text.d.ts +1 -1
- package/dist/src/mcp/setup-guide-text.js +22 -2
- package/dist/src/mcp/tools-server.d.ts +10 -0
- package/dist/src/mcp/tools-server.js +10 -2
- package/dist/src/models/catalog.json +19 -19
- package/dist/src/orchestrator/ask-user.d.ts +61 -0
- package/dist/src/orchestrator/ask-user.js +65 -0
- package/dist/src/orchestrator/compaction.d.ts +2 -3
- package/dist/src/orchestrator/compaction.js +8 -8
- package/dist/src/orchestrator/council-manager.js +9 -8
- package/dist/src/orchestrator/council-request.d.ts +49 -0
- package/dist/src/orchestrator/council-request.js +62 -0
- package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
- package/dist/src/orchestrator/interactive-pause.js +36 -0
- package/dist/src/orchestrator/message-processor.d.ts +4 -0
- package/dist/src/orchestrator/message-processor.js +26 -8
- package/dist/src/orchestrator/orchestrator.d.ts +25 -0
- package/dist/src/orchestrator/orchestrator.js +204 -50
- package/dist/src/orchestrator/preprocessor.js +2 -2
- package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
- package/dist/src/orchestrator/safety-askcard.js +5 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
- package/dist/src/orchestrator/safety-intercept.js +7 -0
- package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
- package/dist/src/orchestrator/stall-watchdog.js +24 -3
- package/dist/src/orchestrator/stream-runner.d.ts +13 -3
- package/dist/src/orchestrator/stream-runner.js +54 -21
- package/dist/src/orchestrator/tool-engine.d.ts +19 -0
- package/dist/src/orchestrator/tool-engine.js +241 -25
- package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
- package/dist/src/orchestrator/turn-watchdog.js +38 -9
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +6 -4
- package/dist/src/pil/discovery.d.ts +1 -1
- package/dist/src/pil/discovery.js +2 -2
- package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
- package/dist/src/pil/layer1_5-complexity-size.js +31 -5
- package/dist/src/pil/llm-classify.d.ts +78 -3
- package/dist/src/pil/llm-classify.js +351 -111
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +7 -0
- package/dist/src/pil/pipeline.js +2 -0
- package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
- package/dist/src/pil/repo-grounding-probe.js +136 -0
- package/dist/src/pil/repo-structure-hints.d.ts +7 -0
- package/dist/src/pil/repo-structure-hints.js +45 -0
- package/dist/src/product-loop/artifact-io.js +4 -0
- package/dist/src/product-loop/criteria-seed.d.ts +51 -0
- package/dist/src/product-loop/criteria-seed.js +200 -0
- package/dist/src/product-loop/discovery-interview.d.ts +9 -0
- package/dist/src/product-loop/discovery-interview.js +37 -18
- package/dist/src/product-loop/discovery-recommender.js +2 -1
- package/dist/src/product-loop/discovery-schema.js +14 -1
- package/dist/src/product-loop/discovery-triage.d.ts +23 -0
- package/dist/src/product-loop/discovery-triage.js +109 -0
- package/dist/src/product-loop/gather.js +150 -2
- package/dist/src/product-loop/index.js +7 -0
- package/dist/src/product-loop/loop-driver.js +21 -8
- package/dist/src/product-loop/phase-plan.d.ts +16 -0
- package/dist/src/product-loop/phase-plan.js +42 -4
- package/dist/src/product-loop/phase-rituals.d.ts +3 -0
- package/dist/src/product-loop/phase-rituals.js +8 -3
- package/dist/src/product-loop/phase-runner.js +30 -11
- package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
- package/dist/src/product-loop/plan-adherence-review.js +144 -0
- package/dist/src/product-loop/sprint-runner.d.ts +62 -0
- package/dist/src/product-loop/sprint-runner.js +309 -8
- package/dist/src/product-loop/types.d.ts +25 -0
- package/dist/src/providers/anthropic.d.ts +9 -8
- package/dist/src/providers/anthropic.js +13 -47
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
- package/dist/src/providers/auth/grok-oauth.js +30 -5
- package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
- package/dist/src/providers/auth/openai-oauth.js +14 -0
- package/dist/src/providers/auth/token-store.d.ts +9 -9
- package/dist/src/providers/auth/token-store.js +8 -70
- package/dist/src/providers/auth/types.d.ts +8 -0
- package/dist/src/providers/env-store.d.ts +17 -0
- package/dist/src/providers/env-store.js +228 -0
- package/dist/src/providers/keychain.d.ts +21 -17
- package/dist/src/providers/keychain.js +124 -135
- package/dist/src/providers/runtime.d.ts +24 -9
- package/dist/src/providers/runtime.js +48 -37
- package/dist/src/providers/strategies/thinking-mode.js +9 -1
- package/dist/src/providers/strategies/xai.strategy.js +27 -0
- package/dist/src/providers/warm.d.ts +65 -0
- package/dist/src/providers/warm.js +145 -0
- package/dist/src/self-qa/agentic-loop.js +3 -2
- package/dist/src/storage/transcript.js +56 -2
- package/dist/src/tools/git-safety.d.ts +19 -0
- package/dist/src/tools/git-safety.js +168 -0
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +76 -1
- package/dist/src/tools/registry.d.ts +17 -0
- package/dist/src/tools/registry.js +116 -1
- package/dist/src/types/index.d.ts +29 -1
- package/dist/src/ui/app.js +67 -4
- package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
- package/dist/src/ui/components/agent-rail-activities.js +47 -0
- package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
- package/dist/src/ui/components/compact-progress-card.js +42 -0
- package/dist/src/ui/components/council-phase-timeline.js +17 -2
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.d.ts +15 -0
- package/dist/src/ui/components/message-view.js +50 -1
- package/dist/src/ui/components/tool-group.d.ts +15 -3
- package/dist/src/ui/components/tool-group.js +69 -11
- package/dist/src/ui/containers/modals-layer.d.ts +2 -1
- package/dist/src/ui/containers/modals-layer.js +2 -2
- package/dist/src/ui/council-harness-event.d.ts +57 -0
- package/dist/src/ui/council-harness-event.js +46 -0
- package/dist/src/ui/heartbeat-debug.d.ts +29 -0
- package/dist/src/ui/heartbeat-debug.js +45 -0
- package/dist/src/ui/modals/api-key-modal.js +1 -1
- package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
- package/dist/src/ui/modals/model-picker-modal.js +8 -10
- package/dist/src/ui/slash/ee.js +81 -0
- package/dist/src/ui/slash/menu-items.js +11 -2
- package/dist/src/ui/use-app-logic.js +354 -224
- package/dist/src/ui/utils/agent-activities.d.ts +39 -0
- package/dist/src/ui/utils/agent-activities.js +96 -0
- package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
- package/dist/src/ui/utils/group-tool-entries.js +111 -0
- package/dist/src/ui/utils/tool-summary.d.ts +21 -0
- package/dist/src/ui/utils/tool-summary.js +91 -0
- package/dist/src/utils/event-loop-monitor.d.ts +85 -0
- package/dist/src/utils/event-loop-monitor.js +107 -0
- package/dist/src/utils/llm-deadline.d.ts +14 -0
- package/dist/src/utils/llm-deadline.js +19 -0
- package/dist/src/utils/loop-profiler.d.ts +102 -0
- package/dist/src/utils/loop-profiler.js +202 -0
- package/dist/src/utils/settings.d.ts +27 -0
- package/dist/src/utils/settings.js +38 -2
- package/dist/src/utils/side-question.d.ts +1 -2
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/verify/entrypoint.js +51 -16
- package/dist/src/verify/orchestrator.d.ts +1 -1
- package/dist/src/verify/orchestrator.js +20 -3
- package/package.json +1 -2
- package/dist/src/cli/bw-vault.d.ts +0 -55
- package/dist/src/cli/bw-vault.js +0 -133
- package/dist/src/mcp/ee-tools.d.ts +0 -46
- package/dist/src/mcp/ee-tools.js +0 -194
package/dist/src/lsp/manager.js
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
import { readFile } from "fs/promises";
|
|
2
2
|
import path from "path";
|
|
3
|
-
import { pathToFileURL } from "url";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "url";
|
|
4
4
|
import { createRuntimeLspDefinitions } from "./builtins.js";
|
|
5
5
|
import { createLspClientSession } from "./client.js";
|
|
6
6
|
export function createWorkspaceLspManager(cwd, settings, options = {}) {
|
|
7
7
|
const definitions = createRuntimeLspDefinitions(cwd, settings);
|
|
8
8
|
const clients = new Map();
|
|
9
|
+
// Servers we have already reported as not installed. Without this the "no
|
|
10
|
+
// command" path is silent: a Go/Rust/C++/Java file just gets no LSP and the
|
|
11
|
+
// caller sees "unavailable" with no way to learn WHICH server is missing.
|
|
12
|
+
const reportedMissing = new Set();
|
|
9
13
|
const createClient = options.createClient ??
|
|
10
14
|
(async ({ serverId, root, definition, settings: normalizedSettings }) => {
|
|
11
15
|
const launch = await definition.resolveLaunch(root, normalizedSettings);
|
|
12
|
-
if (!launch?.command)
|
|
16
|
+
if (!launch?.command) {
|
|
17
|
+
if (!reportedMissing.has(serverId)) {
|
|
18
|
+
reportedMissing.add(serverId);
|
|
19
|
+
console.error(`[lsp] no language server found for "${serverId}" (${definition.extensions.join(", ")}). ` +
|
|
20
|
+
`Install it and make sure it is on PATH, or set lsp.builtins.${serverId}.command in settings.`);
|
|
21
|
+
}
|
|
13
22
|
return null;
|
|
23
|
+
}
|
|
14
24
|
return createLspClientSession({
|
|
15
25
|
serverId,
|
|
16
26
|
root,
|
|
@@ -96,7 +106,10 @@ export function createWorkspaceLspManager(cwd, settings, options = {}) {
|
|
|
96
106
|
diagnostics: client.getDiagnostics(filePath),
|
|
97
107
|
};
|
|
98
108
|
}
|
|
99
|
-
catch {
|
|
109
|
+
catch (err) {
|
|
110
|
+
// Dropping the client silently here is what let a dead language server
|
|
111
|
+
// look like "no diagnostics" for the whole session.
|
|
112
|
+
console.error(`[lsp:${definition.id}] sync failed for ${filePath}, dropping client: ${err instanceof Error ? err.message : String(err)}`);
|
|
100
113
|
clients.delete(key);
|
|
101
114
|
return null;
|
|
102
115
|
}
|
|
@@ -167,11 +180,238 @@ export function createWorkspaceLspManager(cwd, settings, options = {}) {
|
|
|
167
180
|
await Promise.allSettled(active.map((entry) => entry.client.stop()));
|
|
168
181
|
clients.clear();
|
|
169
182
|
}
|
|
183
|
+
// ── Sprint 1: impact/readiness contract (SLICE1-BUILD-NOTE.md) ─────────────
|
|
184
|
+
const TOKEN_BUDGET_CAP = 500; // hard cap (spec §49-54)
|
|
185
|
+
const REF_TOKEN_COST = 30; // per-reference estimate (spec §53)
|
|
186
|
+
const MAX_REFS = Math.floor(TOKEN_BUDGET_CAP / REF_TOKEN_COST); // 16 — truncation threshold
|
|
187
|
+
/** Error-level = severity ≤ 1 (Error). Warnings/infos ignored (spec §21-25). */
|
|
188
|
+
function errorLevel(diags) {
|
|
189
|
+
return diags.filter((d) => (d.severity ?? 1) <= 1);
|
|
190
|
+
}
|
|
191
|
+
/** tokenBudgetUsed = elapsed estimate + refs×30, clamped ≤500 (spec §49-54). */
|
|
192
|
+
function tokenBudget(elapsedMs, refCount) {
|
|
193
|
+
const elapsedTokens = Math.min(200, Math.round(elapsedMs / 25));
|
|
194
|
+
return Math.min(TOKEN_BUDGET_CAP, elapsedTokens + refCount * REF_TOKEN_COST);
|
|
195
|
+
}
|
|
196
|
+
/** Top-2 distinct error messages joined "; ", ≤120 chars; "none" when clean (spec §42-47). */
|
|
197
|
+
function suggestedGuardFrom(errs) {
|
|
198
|
+
if (errs.length === 0)
|
|
199
|
+
return "none";
|
|
200
|
+
const seen = new Set();
|
|
201
|
+
const messages = [];
|
|
202
|
+
for (const d of errs) {
|
|
203
|
+
const cat = (d.source ?? d.code ?? d.message.split(/[:.]/)[0] ?? "error").toString().trim();
|
|
204
|
+
const key = `${cat}:${d.message.trim()}`;
|
|
205
|
+
if (seen.has(key))
|
|
206
|
+
continue;
|
|
207
|
+
seen.add(key);
|
|
208
|
+
messages.push(d.message.trim());
|
|
209
|
+
if (messages.length === 2)
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
return messages.join("; ").slice(0, 120) || "none";
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* didOpen `filePath` on every client that is about to be asked for its
|
|
216
|
+
* diagnostics, so the server actually analyses it. Returns true when the file
|
|
217
|
+
* cannot be read — nothing to diagnose, and reporting it "clean" would lie.
|
|
218
|
+
*/
|
|
219
|
+
async function openForDiagnostics(filePath, records) {
|
|
220
|
+
let content;
|
|
221
|
+
try {
|
|
222
|
+
content = await readFile(filePath, "utf8");
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
console.error(`[lsp] cannot read ${filePath} for diagnostics: ${err instanceof Error ? err.message : String(err)}`);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
const extension = path.extname(filePath).toLowerCase();
|
|
229
|
+
await Promise.all(records.map(async ({ key, definition, client }) => {
|
|
230
|
+
const languageId = definition.languageIds[extension] ?? (extension.slice(1) || "plaintext");
|
|
231
|
+
try {
|
|
232
|
+
await client.openOrChangeFile(filePath, languageId, content);
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
// Drop the client so the next call re-spawns it; the diagnostics wait
|
|
236
|
+
// below will surface this as a failure rather than a clean verdict.
|
|
237
|
+
console.error(`[lsp:${definition.id}] didOpen failed for ${filePath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
238
|
+
clients.delete(key);
|
|
239
|
+
}
|
|
240
|
+
}));
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
async function waitForDiagnostics(filePath, timeout) {
|
|
244
|
+
const clampedTimeout = Math.min(Math.max(timeout, 0), 5000);
|
|
245
|
+
const t0 = Date.now();
|
|
246
|
+
const records = await getClientsForFile(filePath);
|
|
247
|
+
if (records.length === 0) {
|
|
248
|
+
return {
|
|
249
|
+
diagnostics: [],
|
|
250
|
+
lspStatus: "unavailable",
|
|
251
|
+
clean: false,
|
|
252
|
+
metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, 0) },
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
// A language server only publishes diagnostics for files it has been told
|
|
256
|
+
// about. Without this didOpen, waiting on a file the server has never seen
|
|
257
|
+
// waits out the full timeout, gets nothing, and reports lspStatus "ok" /
|
|
258
|
+
// clean: true — a green verdict on a file that was never analysed at all.
|
|
259
|
+
// (Proven: a file with two tsc type errors reported clean.) Callers that
|
|
260
|
+
// already synced the buffer (syncFile) are unaffected: openOrChangeFile is
|
|
261
|
+
// idempotent, and diagnostics already cached short-circuit the wait below.
|
|
262
|
+
const unreadable = await openForDiagnostics(filePath, records);
|
|
263
|
+
if (unreadable) {
|
|
264
|
+
return {
|
|
265
|
+
diagnostics: [],
|
|
266
|
+
lspStatus: "unavailable",
|
|
267
|
+
clean: false,
|
|
268
|
+
metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, 0) },
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
let anyTimedOut = false;
|
|
272
|
+
let anyFailed = false;
|
|
273
|
+
const allDiagnostics = [];
|
|
274
|
+
for (const record of records) {
|
|
275
|
+
try {
|
|
276
|
+
const diags = await withRequestTimeout(record.client.waitForDiagnostics(filePath, clampedTimeout), clampedTimeout);
|
|
277
|
+
allDiagnostics.push(...diags);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
if (err instanceof LspRequestTimeoutError) {
|
|
281
|
+
anyTimedOut = true;
|
|
282
|
+
allDiagnostics.push(...record.client.getDiagnostics(filePath));
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
anyFailed = true;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const lspStatus = anyTimedOut
|
|
290
|
+
? "partial"
|
|
291
|
+
: anyFailed && allDiagnostics.length === 0
|
|
292
|
+
? "unavailable"
|
|
293
|
+
: "ok";
|
|
294
|
+
// clean: true only when we actually have data and zero error-level diags (spec §85).
|
|
295
|
+
const clean = lspStatus !== "unavailable" && errorLevel(allDiagnostics).length === 0;
|
|
296
|
+
return {
|
|
297
|
+
diagnostics: allDiagnostics,
|
|
298
|
+
lspStatus,
|
|
299
|
+
clean,
|
|
300
|
+
metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, 0) },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
async function impactOfChange(filePath, _query, timeout) {
|
|
304
|
+
const t0 = Date.now();
|
|
305
|
+
const diag = await waitForDiagnostics(filePath, timeout ?? 1500);
|
|
306
|
+
if (diag.lspStatus === "unavailable") {
|
|
307
|
+
const errs = errorLevel(diag.diagnostics);
|
|
308
|
+
return {
|
|
309
|
+
references: [],
|
|
310
|
+
diagnostics: diag.diagnostics,
|
|
311
|
+
referencesComplete: false,
|
|
312
|
+
safeToRename: false,
|
|
313
|
+
clean: false,
|
|
314
|
+
suggestedGuard: suggestedGuardFrom(errs),
|
|
315
|
+
degraded: "lsp_unavailable",
|
|
316
|
+
lspStatus: "unavailable",
|
|
317
|
+
metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, 0) },
|
|
318
|
+
};
|
|
319
|
+
}
|
|
320
|
+
const records = await getClientsForFile(filePath);
|
|
321
|
+
const rawReferences = [];
|
|
322
|
+
for (const record of records) {
|
|
323
|
+
try {
|
|
324
|
+
const uri = pathToFileURL(path.resolve(filePath)).href;
|
|
325
|
+
const params = {
|
|
326
|
+
textDocument: { uri },
|
|
327
|
+
position: { line: 0, character: 0 },
|
|
328
|
+
context: { includeDeclaration: true },
|
|
329
|
+
};
|
|
330
|
+
const refs = await record.client.sendRequest("textDocument/references", params);
|
|
331
|
+
if (Array.isArray(refs)) {
|
|
332
|
+
for (const ref of refs) {
|
|
333
|
+
const r = ref;
|
|
334
|
+
if (r?.uri && r?.range) {
|
|
335
|
+
rawReferences.push({
|
|
336
|
+
uri: r.uri,
|
|
337
|
+
range: {
|
|
338
|
+
start: { line: r.range.start.line + 1, character: r.range.start.character + 1 },
|
|
339
|
+
end: { line: r.range.end.line + 1, character: r.range.end.character + 1 },
|
|
340
|
+
},
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
// Individual reference query failure: continue with partial results
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// Truncate so references×30 stays within budget headroom (spec §38, §53).
|
|
351
|
+
const truncated = rawReferences.length > MAX_REFS;
|
|
352
|
+
const references = truncated ? rawReferences.slice(0, MAX_REFS) : rawReferences;
|
|
353
|
+
const referencesComplete = !truncated;
|
|
354
|
+
// Frozen union: the symbol file's waited diagnostics + each unique referenced
|
|
355
|
+
// file's already-published diagnostics (cheap getDiagnostics, no extra waits).
|
|
356
|
+
// clean is computed over this union of error-level diagnostics (spec §19-25).
|
|
357
|
+
const unionDiags = [...diag.diagnostics];
|
|
358
|
+
const seenFiles = new Set([path.resolve(filePath)]);
|
|
359
|
+
for (const ref of references) {
|
|
360
|
+
let refPath;
|
|
361
|
+
try {
|
|
362
|
+
refPath = fileURLToPath(ref.uri);
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (seenFiles.has(refPath))
|
|
368
|
+
continue;
|
|
369
|
+
seenFiles.add(refPath);
|
|
370
|
+
for (const record of records)
|
|
371
|
+
unionDiags.push(...record.client.getDiagnostics(refPath));
|
|
372
|
+
}
|
|
373
|
+
const unionErrors = errorLevel(unionDiags);
|
|
374
|
+
const clean = unionErrors.length === 0;
|
|
375
|
+
// Precedence: lsp_unavailable (handled above) > diagnostics_timeout > refs_truncated > none.
|
|
376
|
+
const degraded = diag.lspStatus === "partial" ? "diagnostics_timeout" : truncated ? "refs_truncated" : "none";
|
|
377
|
+
// safeToRename only when refs are complete, the union is clean, and nothing degraded (spec §27-31).
|
|
378
|
+
const safeToRename = referencesComplete && clean && degraded === "none" && diag.lspStatus === "ok";
|
|
379
|
+
return {
|
|
380
|
+
references,
|
|
381
|
+
diagnostics: diag.diagnostics,
|
|
382
|
+
referencesComplete,
|
|
383
|
+
safeToRename,
|
|
384
|
+
clean,
|
|
385
|
+
suggestedGuard: suggestedGuardFrom(unionErrors),
|
|
386
|
+
degraded,
|
|
387
|
+
lspStatus: diag.lspStatus,
|
|
388
|
+
metadata: { tokenBudgetUsed: tokenBudget(Date.now() - t0, references.length) },
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
async function lspMutationPreview(_filePath, _change) {
|
|
392
|
+
// Fixed stub schema — no workspaceEdit, no apply path (spec §55-71).
|
|
393
|
+
return { op: "allowlist", dryRunResult: { proposedEdits: [], tokenEstimate: 0 }, schemaVersion: "1.0" };
|
|
394
|
+
}
|
|
395
|
+
async function lspBeforeGrep(filePath, query) {
|
|
396
|
+
const result = await impactOfChange(filePath, query);
|
|
397
|
+
// Policy keys on lspStatus: grep fallback allowed unless the LSP was fully ok (spec §73-77).
|
|
398
|
+
if (result.lspStatus !== "ok") {
|
|
399
|
+
return { kind: "allow", reason: `LSP ${result.lspStatus}; fall back to grep` };
|
|
400
|
+
}
|
|
401
|
+
if (result.safeToRename) {
|
|
402
|
+
return { kind: "allow", reason: "LSP ok and rename is safe" };
|
|
403
|
+
}
|
|
404
|
+
return { kind: "enrich", enrichWith: result };
|
|
405
|
+
}
|
|
170
406
|
return {
|
|
171
407
|
touchFile,
|
|
172
408
|
syncFile,
|
|
173
409
|
query,
|
|
174
410
|
close,
|
|
411
|
+
waitForDiagnostics,
|
|
412
|
+
impactOfChange,
|
|
413
|
+
lspMutationPreview,
|
|
414
|
+
lspBeforeGrep,
|
|
175
415
|
};
|
|
176
416
|
}
|
|
177
417
|
class LspRequestTimeoutError extends Error {
|
|
@@ -211,6 +451,8 @@ function getOperationMethod(operation) {
|
|
|
211
451
|
case "incomingCalls":
|
|
212
452
|
case "outgoingCalls":
|
|
213
453
|
return "textDocument/prepareCallHierarchy";
|
|
454
|
+
case "waitForDiagnostics":
|
|
455
|
+
return "textDocument/diagnostic";
|
|
214
456
|
}
|
|
215
457
|
}
|
|
216
458
|
function createOperationParams(input, absolutePath) {
|
|
@@ -244,6 +486,10 @@ function createOperationParams(input, absolutePath) {
|
|
|
244
486
|
return {
|
|
245
487
|
query: input.query ?? "",
|
|
246
488
|
};
|
|
489
|
+
case "waitForDiagnostics":
|
|
490
|
+
return {
|
|
491
|
+
textDocument: { uri },
|
|
492
|
+
};
|
|
247
493
|
}
|
|
248
494
|
}
|
|
249
495
|
export function summarizeLspDiagnostics(diagnostics) {
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Absolute path to a language-server binary in the cache, installing the package
|
|
3
|
+
* on first use.
|
|
4
|
+
*
|
|
5
|
+
* `preferredBin` is the binary the CALLER needs, which is not always the one
|
|
6
|
+
* named after the package: `pyright` ships both `pyright` (the batch CLI) and
|
|
7
|
+
* `pyright-langserver` (the LSP server). Choosing by package name alone launched
|
|
8
|
+
* the CLI with `--stdio`, which exits 4 with "pyright --help for usage" — so
|
|
9
|
+
* Python LSP never started for anyone relying on auto-install.
|
|
10
|
+
*/
|
|
11
|
+
export declare function lspNpmWhich(pkg: string, preferredBin?: string): Promise<string | null>;
|
|
2
12
|
export declare function lspNpmAdd(pkg: string): Promise<string>;
|
|
@@ -10,13 +10,27 @@ function packageDir(pkg) {
|
|
|
10
10
|
: pkg;
|
|
11
11
|
return path.join(CACHE_ROOT, sanitized);
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Absolute path to a language-server binary in the cache, installing the package
|
|
15
|
+
* on first use.
|
|
16
|
+
*
|
|
17
|
+
* `preferredBin` is the binary the CALLER needs, which is not always the one
|
|
18
|
+
* named after the package: `pyright` ships both `pyright` (the batch CLI) and
|
|
19
|
+
* `pyright-langserver` (the LSP server). Choosing by package name alone launched
|
|
20
|
+
* the CLI with `--stdio`, which exits 4 with "pyright --help for usage" — so
|
|
21
|
+
* Python LSP never started for anyone relying on auto-install.
|
|
22
|
+
*/
|
|
23
|
+
export async function lspNpmWhich(pkg, preferredBin) {
|
|
14
24
|
const dir = packageDir(pkg);
|
|
15
25
|
const binDir = path.join(dir, "node_modules", ".bin");
|
|
16
26
|
const pick = async () => {
|
|
17
27
|
const files = await readdir(binDir).catch(() => []);
|
|
18
28
|
if (files.length === 0)
|
|
19
29
|
return undefined;
|
|
30
|
+
// The caller's choice wins whenever the package actually ships it. Match the
|
|
31
|
+
// extension-less shim: Windows also lists .cmd/.ps1 wrappers for each bin.
|
|
32
|
+
if (preferredBin && files.includes(preferredBin))
|
|
33
|
+
return preferredBin;
|
|
20
34
|
if (files.length === 1)
|
|
21
35
|
return files[0];
|
|
22
36
|
const pkgJsonPath = path.join(dir, "node_modules", pkg, "package.json");
|
|
@@ -29,6 +43,8 @@ export async function lspNpmWhich(pkg) {
|
|
|
29
43
|
const keys = Object.keys(bin);
|
|
30
44
|
if (keys.length === 1)
|
|
31
45
|
return keys[0];
|
|
46
|
+
if (preferredBin && bin[preferredBin])
|
|
47
|
+
return preferredBin;
|
|
32
48
|
return bin[unscoped] ? unscoped : keys[0];
|
|
33
49
|
}
|
|
34
50
|
return files[0];
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type WorkspaceLspManager } from "./manager.js";
|
|
2
|
+
import type { ImpactOfChangeResult, LspDiagnosticFile, LspQueryInput, LspQueryResult, LspToolResponse, MutationPreviewResult } from "./types.js";
|
|
2
3
|
export declare function queryLsp(cwd: string, input: LspQueryInput): Promise<LspToolResponse>;
|
|
4
|
+
export declare function waitForDiagnosticsLsp(cwd: string, filePath: string, timeout?: number): Promise<LspQueryResult>;
|
|
5
|
+
export declare function impactOfChangeLsp(cwd: string, filePath: string, query?: string): Promise<ImpactOfChangeResult>;
|
|
6
|
+
export declare function lspMutationPreview(cwd: string, filePath: string, change: string): Promise<MutationPreviewResult>;
|
|
3
7
|
export declare function syncFileWithLsp(cwd: string, filePath: string, content: string, save?: boolean, waitForDiagnostics?: boolean, diagnosticsTimeoutMs?: number): Promise<LspDiagnosticFile[]>;
|
|
4
8
|
export declare function isLspToolEnabled(_cwd: string): boolean;
|
|
5
9
|
export declare function summarizeDiagnostics(diagnostics: LspDiagnosticFile[]): string | null;
|
|
6
10
|
export declare function shutdownWorkspaceLspManager(cwd: string): Promise<void>;
|
|
11
|
+
export declare function getOrCreateManager(cwd: string): WorkspaceLspManager;
|
package/dist/src/lsp/runtime.js
CHANGED
|
@@ -10,6 +10,22 @@ export async function queryLsp(cwd, input) {
|
|
|
10
10
|
filePath: path.isAbsolute(input.filePath) ? input.filePath : path.resolve(cwd, input.filePath),
|
|
11
11
|
});
|
|
12
12
|
}
|
|
13
|
+
// Default timeout (ms) when a caller omits one. manager.waitForDiagnostics now
|
|
14
|
+
// takes a REQUIRED timeout (Sprint-1 readiness contract, commit 4af6efb1) and no
|
|
15
|
+
// longer defaults internally, so the public wrapper supplies the documented 1500ms.
|
|
16
|
+
const DEFAULT_WAIT_DIAGNOSTICS_MS = 1500;
|
|
17
|
+
export async function waitForDiagnosticsLsp(cwd, filePath, timeout) {
|
|
18
|
+
const manager = getOrCreateManager(cwd);
|
|
19
|
+
return manager.waitForDiagnostics(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), timeout ?? DEFAULT_WAIT_DIAGNOSTICS_MS);
|
|
20
|
+
}
|
|
21
|
+
export async function impactOfChangeLsp(cwd, filePath, query) {
|
|
22
|
+
const manager = getOrCreateManager(cwd);
|
|
23
|
+
return manager.impactOfChange(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), query);
|
|
24
|
+
}
|
|
25
|
+
export async function lspMutationPreview(cwd, filePath, change) {
|
|
26
|
+
const manager = getOrCreateManager(cwd);
|
|
27
|
+
return manager.lspMutationPreview(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), change);
|
|
28
|
+
}
|
|
13
29
|
export async function syncFileWithLsp(cwd, filePath, content, save = true, waitForDiagnostics = true, diagnosticsTimeoutMs) {
|
|
14
30
|
const manager = getOrCreateManager(cwd);
|
|
15
31
|
return manager.syncFile(path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath), content, save, waitForDiagnostics, diagnosticsTimeoutMs);
|
|
@@ -29,7 +45,7 @@ export async function shutdownWorkspaceLspManager(cwd) {
|
|
|
29
45
|
managers.delete(key);
|
|
30
46
|
await manager.close();
|
|
31
47
|
}
|
|
32
|
-
function getOrCreateManager(cwd) {
|
|
48
|
+
export function getOrCreateManager(cwd) {
|
|
33
49
|
const key = resolveManagerKey(cwd);
|
|
34
50
|
const existing = managers.get(key);
|
|
35
51
|
if (existing)
|
package/dist/src/lsp/types.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const LSP_TOOL_OPERATIONS: readonly ["goToDefinition", "findReferences", "hover", "documentSymbol", "workspaceSymbol", "goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls"];
|
|
1
|
+
export declare const LSP_TOOL_OPERATIONS: readonly ["goToDefinition", "findReferences", "hover", "documentSymbol", "workspaceSymbol", "goToImplementation", "prepareCallHierarchy", "incomingCalls", "outgoingCalls", "waitForDiagnostics"];
|
|
2
2
|
export type LspToolOperation = (typeof LSP_TOOL_OPERATIONS)[number];
|
|
3
3
|
export type LspBuiltInServerId = "typescript" | "pyright" | "gopls" | "rust-analyzer" | "bash-language-server" | "yaml-language-server" | "clangd" | "jdtls" | "sourcekit-lsp" | "csharp-ls";
|
|
4
4
|
export interface LspPosition {
|
|
@@ -78,6 +78,88 @@ export interface LspQueryInput {
|
|
|
78
78
|
character?: number;
|
|
79
79
|
query?: string;
|
|
80
80
|
}
|
|
81
|
+
/**
|
|
82
|
+
* LSP readiness status (SLICE1-BUILD-NOTE.md §79-86, §73-77).
|
|
83
|
+
* - `ok` — diagnostics resolved within the ≤5s budget
|
|
84
|
+
* - `partial` — one or more waits timed out; stale diagnostics were used
|
|
85
|
+
* - `unavailable` — no server could be reached or the query threw
|
|
86
|
+
* The lsp-before-grep policy keys on this: grep fallback is allowed when
|
|
87
|
+
* `lspStatus !== 'ok'`.
|
|
88
|
+
*/
|
|
89
|
+
export type LspStatus = "ok" | "partial" | "unavailable";
|
|
90
|
+
/**
|
|
91
|
+
* Reason a composite result is degraded (SLICE1-BUILD-NOTE.md §33-40).
|
|
92
|
+
* Precedence when several apply: lsp_unavailable > diagnostics_timeout >
|
|
93
|
+
* refs_truncated > none.
|
|
94
|
+
*/
|
|
95
|
+
export type DegradedReason = "none" | "refs_truncated" | "diagnostics_timeout" | "lsp_unavailable";
|
|
96
|
+
/** Per-call token-budget accounting; `tokenBudgetUsed` is hard-capped ≤500. */
|
|
97
|
+
export interface LspResultMetadata {
|
|
98
|
+
/** Elapsed + reference estimate, clamped to ≤500 (SLICE1-BUILD-NOTE.md §49-54). */
|
|
99
|
+
tokenBudgetUsed: number;
|
|
100
|
+
}
|
|
101
|
+
/** `waitForDiagnostics` result (SLICE1-BUILD-NOTE.md §79-86). */
|
|
102
|
+
export interface LspQueryResult {
|
|
103
|
+
diagnostics: LspDiagnostic[];
|
|
104
|
+
lspStatus: LspStatus;
|
|
105
|
+
/** True only when zero error-level (severity ≤ 1) diagnostics exist for the file. */
|
|
106
|
+
clean: boolean;
|
|
107
|
+
metadata: LspResultMetadata;
|
|
108
|
+
}
|
|
109
|
+
/** `impact_of_change` composite result (SLICE1-BUILD-NOTE.md §10, §19-54). */
|
|
110
|
+
export interface ImpactOfChangeResult {
|
|
111
|
+
references: LspLocation[];
|
|
112
|
+
diagnostics: LspDiagnostic[];
|
|
113
|
+
/** False when the reference list was truncated to stay within token budget. */
|
|
114
|
+
referencesComplete: boolean;
|
|
115
|
+
/**
|
|
116
|
+
* True only when `referencesComplete === true` AND the frozen union (symbol
|
|
117
|
+
* file + every referenced file) has zero error-level diagnostics AND the LSP
|
|
118
|
+
* was fully available. `false` is the safe default → agents grep-fallback.
|
|
119
|
+
*/
|
|
120
|
+
safeToRename: boolean;
|
|
121
|
+
/** Zero error-level diagnostics on the union of the symbol file + reference files. */
|
|
122
|
+
clean: boolean;
|
|
123
|
+
/** Top-2 error category messages joined with "; ", ≤120 chars; "none" when clean. */
|
|
124
|
+
suggestedGuard: string;
|
|
125
|
+
degraded: DegradedReason;
|
|
126
|
+
lspStatus: LspStatus;
|
|
127
|
+
metadata: LspResultMetadata;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* `lsp_mutation_preview` stub (SLICE1-BUILD-NOTE.md §55-71). Fixed schema, no
|
|
131
|
+
* `workspaceEdit`, no apply path — real diff computation is Slice 2.
|
|
132
|
+
*/
|
|
133
|
+
export interface MutationPreviewResult {
|
|
134
|
+
readonly op: "allowlist";
|
|
135
|
+
readonly dryRunResult: {
|
|
136
|
+
readonly proposedEdits: [];
|
|
137
|
+
readonly tokenEstimate: number;
|
|
138
|
+
};
|
|
139
|
+
readonly schemaVersion: "1.0";
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Tagged error kind for LSP operation failures.
|
|
143
|
+
* Throw-only canonical failure for slice 1 ── every failure flows
|
|
144
|
+
* through a single syntactic choke point (the projection's try/catch).
|
|
145
|
+
*/
|
|
146
|
+
export type LspErrorKind = "no_server" | "request_timeout" | "request_failed" | "lsp_disabled" | "unknown";
|
|
147
|
+
/** Tagged‑union Error used by manager methods. */
|
|
148
|
+
export declare class LspError extends Error {
|
|
149
|
+
readonly kind: LspErrorKind;
|
|
150
|
+
constructor(kind: LspErrorKind, message: string);
|
|
151
|
+
}
|
|
152
|
+
/** Policy decision the lsp‑before‑grep gate returns. */
|
|
153
|
+
export type PolicyAction = {
|
|
154
|
+
kind: "allow";
|
|
155
|
+
reason: string;
|
|
156
|
+
} | {
|
|
157
|
+
kind: "block";
|
|
158
|
+
reason: string;
|
|
159
|
+
} | {
|
|
160
|
+
kind: "enrich";
|
|
161
|
+
enrichWith: ImpactOfChangeResult;
|
|
162
|
+
};
|
|
81
163
|
export interface LspToolResponse {
|
|
82
164
|
success: boolean;
|
|
83
165
|
output: string;
|
package/dist/src/lsp/types.js
CHANGED
|
@@ -8,5 +8,15 @@ export const LSP_TOOL_OPERATIONS = [
|
|
|
8
8
|
"prepareCallHierarchy",
|
|
9
9
|
"incomingCalls",
|
|
10
10
|
"outgoingCalls",
|
|
11
|
+
"waitForDiagnostics",
|
|
11
12
|
];
|
|
13
|
+
/** Tagged‑union Error used by manager methods. */
|
|
14
|
+
export class LspError extends Error {
|
|
15
|
+
kind;
|
|
16
|
+
constructor(kind, message) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "LspError";
|
|
19
|
+
this.kind = kind;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
12
22
|
//# sourceMappingURL=types.js.map
|
|
@@ -238,29 +238,57 @@ export async function acquireMcpTools(servers, opts) {
|
|
|
238
238
|
*/
|
|
239
239
|
export async function warmMcpClients(servers, syncAndLog = false) {
|
|
240
240
|
const validServers = servers.filter((s) => s.enabled && validateMcpServerConfig(s).ok);
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
// Fire-and-forget background warmup — no user-facing output.
|
|
242
|
+
if (!syncAndLog) {
|
|
243
|
+
void Promise.all(validServers.map((s) => getOrConnect(s).catch((e) => {
|
|
244
|
+
// Intentionally non-fatal: getOrConnect evicts the failed entry so a
|
|
245
|
+
// real turn retries. Surface at debug level only (No Silent Catch).
|
|
246
|
+
if (process.env.MUONROI_DEBUG_MCP) {
|
|
247
|
+
console.error(`[mcp:warm] background warmup failed for ${s.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
248
|
+
}
|
|
249
|
+
})));
|
|
250
|
+
return;
|
|
243
251
|
}
|
|
244
|
-
|
|
252
|
+
if (validServers.length === 0)
|
|
253
|
+
return;
|
|
254
|
+
// Synchronous warmup (fresh start): render ONE compact in-place progress line
|
|
255
|
+
// instead of a line per server — e.g. "⏳ Starting MCP servers (0/4): a, b, c".
|
|
256
|
+
const total = validServers.length;
|
|
257
|
+
const names = validServers.map((s) => s.id).join(", ");
|
|
258
|
+
const isTTY = Boolean(process.stdout.isTTY);
|
|
259
|
+
let done = 0;
|
|
260
|
+
const failures = [];
|
|
261
|
+
const render = () => {
|
|
262
|
+
// \r + clear-line (\x1b[2K) rewrites the same terminal row each tick.
|
|
263
|
+
process.stdout.write(`\r\x1b[2K⏳ Starting MCP servers (${done}/${total}): ${names}`);
|
|
264
|
+
};
|
|
265
|
+
if (isTTY)
|
|
266
|
+
render();
|
|
267
|
+
else
|
|
268
|
+
console.log(`[MCP] Starting ${total} server(s): ${names}`);
|
|
269
|
+
await Promise.all(validServers.map(async (s) => {
|
|
245
270
|
try {
|
|
246
271
|
await getOrConnect(s);
|
|
247
|
-
if (syncAndLog) {
|
|
248
|
-
console.log(`[MCP] ✅ ${s.id}: healthy`);
|
|
249
|
-
}
|
|
250
272
|
}
|
|
251
273
|
catch (e) {
|
|
252
|
-
|
|
253
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
254
|
-
console.log(`[MCP] ❌ ${s.id}: failed - ${msg}`);
|
|
255
|
-
}
|
|
274
|
+
failures.push(`${s.id}: ${e instanceof Error ? e.message : String(e)}`);
|
|
256
275
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
276
|
+
finally {
|
|
277
|
+
done += 1;
|
|
278
|
+
if (isTTY)
|
|
279
|
+
render();
|
|
280
|
+
}
|
|
281
|
+
}));
|
|
282
|
+
const ok = total - failures.length;
|
|
283
|
+
if (isTTY) {
|
|
284
|
+
process.stdout.write(`\r\x1b[2K✅ MCP servers ready (${ok}/${total})\n`);
|
|
260
285
|
}
|
|
261
286
|
else {
|
|
262
|
-
|
|
263
|
-
|
|
287
|
+
console.log(`[MCP] ${ok}/${total} server(s) ready`);
|
|
288
|
+
}
|
|
289
|
+
// Compact failure summary (one line, not per-server spam) — keeps errors visible.
|
|
290
|
+
if (failures.length > 0) {
|
|
291
|
+
console.warn(`[MCP] ⚠️ ${failures.length} unavailable: ${failures.join(" | ")}`);
|
|
264
292
|
}
|
|
265
293
|
}
|
|
266
294
|
/** Tear down every pooled client. Call on orchestrator/process shutdown. */
|
|
@@ -8,9 +8,13 @@
|
|
|
8
8
|
* deps are injected so unit tests never spawn a real language server.
|
|
9
9
|
*/
|
|
10
10
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
-
import { type LspQueryInput, type LspToolResponse } from "../lsp/types.js";
|
|
11
|
+
import { type ImpactOfChangeResult, type LspQueryInput, type LspQueryResult, type LspToolResponse, type MutationPreviewResult, type PolicyAction } from "../lsp/types.js";
|
|
12
12
|
export interface LspToolDeps {
|
|
13
13
|
query?: (cwd: string, input: LspQueryInput) => Promise<LspToolResponse>;
|
|
14
14
|
enabled?: (cwd: string) => boolean | Promise<boolean>;
|
|
15
|
+
waitForDiagnostics?: (cwd: string, filePath: string, timeout?: number) => Promise<LspQueryResult>;
|
|
16
|
+
impactOfChange?: (cwd: string, filePath: string, query?: string) => Promise<ImpactOfChangeResult>;
|
|
17
|
+
lspMutationPreview?: (cwd: string, filePath: string, change: string) => Promise<MutationPreviewResult>;
|
|
18
|
+
lspBeforeGrep?: (cwd: string, filePath: string, query?: string) => Promise<PolicyAction>;
|
|
15
19
|
}
|
|
16
20
|
export declare function registerLspTools(server: McpServer, deps?: LspToolDeps): void;
|