oh-my-opencode 4.18.0 → 4.18.2
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/.agents/skills/codex-qa/scripts/lsp-e2e.sh +3677 -0
- package/.agents/skills/opencode-qa/scripts/lsp-e2e.sh +3154 -0
- package/.agents/skills/work-with-pr/SKILL.md +16 -37
- package/.agents/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/.opencode/skills/work-with-pr/SKILL.md +16 -37
- package/.opencode/skills/work-with-pr-workspace/evals/evals.json +3 -3
- package/bin/AGENTS.md +33 -0
- package/dist/cli/index.js +500 -158
- package/dist/cli-node/index.js +500 -158
- package/dist/hooks/anthropic-context-window-limit-recovery/empty-content-recovery-sdk.d.ts +6 -0
- package/dist/hooks/category-skill-reminder/hook.d.ts +9 -1
- package/dist/hooks/comment-checker/hook.d.ts +8 -1
- package/dist/hooks/todo-continuation-enforcer/types.d.ts +3 -0
- package/dist/index.js +956 -702
- package/dist/plugin/messages-transform.d.ts +1 -0
- package/dist/plugin-handlers/prometheus-agent-config-builder.d.ts +2 -0
- package/dist/tui.js +43 -4
- package/package.json +16 -16
- package/packages/git-bash-mcp/dist/cli.js +81 -19
- package/packages/lsp-core/package.json +4 -0
- package/packages/lsp-core/src/index.ts +1 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.test.ts +18 -0
- package/packages/lsp-core/src/lsp/cleanup-errors.ts +12 -3
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +261 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +63 -0
- package/packages/lsp-core/src/lsp/client-wrapper.ts +35 -5
- package/packages/lsp-core/src/lsp/client.ts +262 -80
- package/packages/lsp-core/src/lsp/config-loader.ts +5 -17
- package/packages/lsp-core/src/lsp/connection.ts +12 -6
- package/packages/lsp-core/src/lsp/directory-diagnostics.test.ts +221 -0
- package/packages/lsp-core/src/lsp/directory-diagnostics.ts +61 -28
- package/packages/lsp-core/src/lsp/errors.ts +11 -0
- package/packages/lsp-core/src/lsp/fixtures/diagnostics-freshness-contract-probe.ts +283 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-contract-probe.ts +196 -0
- package/packages/lsp-core/src/lsp/fixtures/workspace-edit-server.mjs +215 -0
- package/packages/lsp-core/src/lsp/formatters.ts +3 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection-cancellation.test.ts +97 -0
- package/packages/lsp-core/src/lsp/json-rpc-connection.ts +73 -5
- package/packages/lsp-core/src/lsp/server-install-state.ts +3 -6
- package/packages/lsp-core/src/lsp/transport-protocol.ts +52 -0
- package/packages/lsp-core/src/lsp/transport.ts +96 -70
- package/packages/lsp-core/src/lsp/workspace-apply-edit-failure.ts +19 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-lease.integration.test.ts +214 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-sync.integration.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit-test-support.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-apply-edit.integration.test.ts +163 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.test.ts +67 -0
- package/packages/lsp-core/src/lsp/workspace-document-state.ts +368 -0
- package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +113 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.test.ts +140 -0
- package/packages/lsp-core/src/lsp/workspace-edit-commit.ts +220 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.test.ts +56 -0
- package/packages/lsp-core/src/lsp/workspace-edit-contract-evidence.ts +30 -0
- package/packages/lsp-core/src/lsp/workspace-edit-fingerprint.ts +44 -0
- package/packages/lsp-core/src/lsp/workspace-edit-options.test.ts +147 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parse-helpers.ts +59 -0
- package/packages/lsp-core/src/lsp/workspace-edit-parser.ts +130 -0
- package/packages/lsp-core/src/lsp/workspace-edit-path.ts +98 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan-types.ts +60 -0
- package/packages/lsp-core/src/lsp/workspace-edit-plan.ts +73 -0
- package/packages/lsp-core/src/lsp/workspace-edit-prevalidation.test.ts +174 -0
- package/packages/lsp-core/src/lsp/workspace-edit-resource-parser.ts +89 -0
- package/packages/lsp-core/src/lsp/workspace-edit-simulation.ts +183 -0
- package/packages/lsp-core/src/lsp/workspace-edit-snapshot.ts +53 -0
- package/packages/lsp-core/src/lsp/workspace-edit-text.ts +125 -0
- package/packages/lsp-core/src/lsp/workspace-edit-types.ts +121 -0
- package/packages/lsp-core/src/lsp/workspace-edit.characterization.test.ts +95 -0
- package/packages/lsp-core/src/lsp/workspace-edit.ts +49 -200
- package/packages/lsp-core/src/lsp/workspace-mutation-controller.ts +182 -0
- package/packages/lsp-core/src/mcp.ts +18 -7
- package/packages/lsp-core/src/missing-dependency-result.test.ts +105 -0
- package/packages/lsp-core/src/missing-dependency-result.ts +57 -0
- package/packages/lsp-core/src/post-edit/index.ts +1 -0
- package/packages/lsp-core/src/post-edit/orchestration.test.ts +157 -0
- package/packages/lsp-core/src/post-edit/orchestration.ts +178 -0
- package/packages/lsp-core/src/request-context.test.ts +171 -0
- package/packages/lsp-core/src/request-context.ts +222 -9
- package/packages/lsp-core/src/tool-surface.test.ts +4 -1
- package/packages/lsp-core/src/tools/diagnostics.ts +32 -13
- package/packages/lsp-core/src/tools/navigation.ts +12 -12
- package/packages/lsp-core/src/tools/rename.ts +10 -15
- package/packages/lsp-core/src/tools/symbols.ts +11 -11
- package/packages/lsp-core/src/tools/types.ts +2 -1
- package/packages/lsp-daemon/dist/cli.js +3330 -764
- package/packages/lsp-daemon/dist/client.d.ts +105 -0
- package/packages/lsp-daemon/dist/client.js +5995 -0
- package/packages/lsp-daemon/dist/daemon-client.d.ts +12 -7
- package/packages/lsp-daemon/dist/daemon-client.js +139 -32
- package/packages/lsp-daemon/dist/daemon-server.d.ts +1 -0
- package/packages/lsp-daemon/dist/daemon-server.js +40 -15
- package/packages/lsp-daemon/dist/ensure-daemon.d.ts +10 -8
- package/packages/lsp-daemon/dist/ensure-daemon.js +135 -51
- package/packages/lsp-daemon/dist/index.d.ts +2 -2
- package/packages/lsp-daemon/dist/index.js +3093 -786
- package/packages/lsp-daemon/dist/ipc-protocol.d.ts +46 -0
- package/packages/lsp-daemon/dist/ipc-protocol.js +187 -0
- package/packages/lsp-daemon/dist/lock.js +14 -4
- package/packages/lsp-daemon/dist/ownership.d.ts +49 -0
- package/packages/lsp-daemon/dist/ownership.js +168 -0
- package/packages/lsp-daemon/dist/paths.d.ts +33 -9
- package/packages/lsp-daemon/dist/paths.js +72 -33
- package/packages/lsp-daemon/dist/proxy.d.ts +5 -0
- package/packages/lsp-daemon/dist/proxy.js +123 -16
- package/packages/lsp-daemon/dist/request-routing.d.ts +7 -2
- package/packages/lsp-daemon/dist/request-routing.js +71 -22
- package/packages/lsp-daemon/dist/run-daemon.js +9 -2
- package/packages/lsp-daemon/dist/runtime-contract.d.ts +21 -0
- package/packages/lsp-daemon/dist/runtime-contract.js +58 -0
- package/packages/lsp-daemon/dist/socket-jsonrpc.js +6 -1
- package/packages/lsp-daemon/package.json +12 -3
- package/packages/lsp-tools-mcp/dist/cli.js +2189 -454
- package/packages/lsp-tools-mcp/dist/lsp/manager.js +1741 -148
- package/packages/lsp-tools-mcp/dist/mcp.js +2206 -471
- package/packages/lsp-tools-mcp/dist/request-context.js +176 -6
- package/packages/lsp-tools-mcp/dist/tools.js +2119 -447
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/.mcp.json +2 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +100 -28
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +100 -28
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
- package/packages/omo-codex/plugin/components/codegraph/src/mcp-bridge.ts +21 -9
- package/packages/omo-codex/plugin/components/codegraph/test/mcp-bridge-fixtures.ts +35 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge-lifecycle.test.ts +69 -0
- package/packages/omo-codex/plugin/components/codegraph/test/serve-mcp-bridge.test.ts +57 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/.mcp.json +2 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +55 -0
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +3033 -936
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook-cli.js +0 -4
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.d.ts +5 -2
- package/packages/omo-codex/plugin/components/lsp/dist/codex-hook.js +41 -62
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.d.ts +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/daemon-cli-path.js +24 -15
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.d.ts +3 -7
- package/packages/omo-codex/plugin/components/lsp/dist/lsp-session-state.js +23 -49
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +3 -2
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.mjs +31 -1
- package/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-daemon.test.mjs +76 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.mjs +201 -0
- package/packages/omo-codex/plugin/components/lsp/scripts/build-runtime.test.mjs +55 -0
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook-cli.ts +0 -4
- package/packages/omo-codex/plugin/components/lsp/src/codex-hook.ts +49 -71
- package/packages/omo-codex/plugin/components/lsp/src/daemon-cli-path.ts +26 -15
- package/packages/omo-codex/plugin/components/lsp/src/lsp-session-state.ts +26 -64
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook-unavailable.test.ts +16 -17
- package/packages/omo-codex/plugin/components/lsp/test/codex-hook.test.ts +30 -4
- package/packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts +20 -5
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.5.md +1 -1
- package/packages/omo-codex/plugin/components/rules/bundled-rules/hephaestus/gpt-5.6.md +5 -3
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/README.md +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/directive.md +3 -3
- package/packages/omo-codex/plugin/components/start-work-continuation/dist/cli.js +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts +1 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts +15 -1
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +20 -0
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/directive.md +50 -33
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ultrawork/SKILL.md +50 -33
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +50 -33
- package/packages/omo-codex/plugin/components/ulw-loop/dist/cli.js +3 -3
- package/packages/omo-codex/plugin/components/ulw-loop/dist/stop-resume-hook.js +5 -6
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/components/ulw-loop/src/stop-resume-hook.ts +5 -6
- package/packages/omo-codex/plugin/components/ulw-loop/test/stop-resume-hook.test.ts +2 -2
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +26 -14
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/scripts/build-bundled-mcp-runtimes.mjs +2 -3
- package/packages/omo-codex/plugin/scripts/build-components.mjs +13 -1
- package/packages/omo-codex/plugin/scripts/sync-skills.mjs +9 -1
- package/packages/omo-codex/plugin/skills/review-work/SKILL.md +7 -0
- package/packages/omo-codex/plugin/skills/start-work/SKILL.md +2 -1
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +50 -33
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +1 -1
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +6 -5
- package/packages/omo-codex/plugin/test/aggregate-build.test.mjs +8 -0
- package/packages/omo-codex/plugin/test/component-bundled-cli.test.mjs +128 -15
- package/packages/omo-codex/plugin/test/install-time-build-runtime.test.mjs +10 -0
- package/packages/omo-codex/plugin/test/lsp-prebuild-layouts.test.mjs +2 -0
- package/packages/omo-codex/plugin/test/mcp-research-servers.test.mjs +1 -0
- package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +7 -0
- package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +34 -3
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +328 -63
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
|
|
3
|
+
const DEFAULT_MAX_CONCURRENCY = 4;
|
|
4
|
+
const CLEAN_DIAGNOSTICS_TEXT = "No diagnostics found";
|
|
5
|
+
|
|
6
|
+
export type PostEditDiagnosticsOutcome =
|
|
7
|
+
| string
|
|
8
|
+
| {
|
|
9
|
+
readonly kind: "not_configured";
|
|
10
|
+
readonly extension: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type DiagnosticsRunner = (filePath: string) => Promise<PostEditDiagnosticsOutcome>;
|
|
14
|
+
|
|
15
|
+
export interface PostEditDiagnosticsBlock {
|
|
16
|
+
readonly filePath: string;
|
|
17
|
+
readonly diagnostics: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PostEditDiagnosticsObservation {
|
|
21
|
+
readonly filePath: string;
|
|
22
|
+
readonly kind: "block" | "clean" | "not_configured";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PostEditDiagnosticsResult {
|
|
26
|
+
readonly blocks: readonly PostEditDiagnosticsBlock[];
|
|
27
|
+
readonly observations: readonly PostEditDiagnosticsObservation[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PostEditNotConfiguredCache {
|
|
31
|
+
readonly notConfiguredExtensions: Set<string>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface CollectPostEditDiagnosticsInput {
|
|
35
|
+
readonly filePaths: readonly string[];
|
|
36
|
+
readonly runDiagnostics: DiagnosticsRunner;
|
|
37
|
+
readonly cache?: PostEditNotConfiguredCache;
|
|
38
|
+
readonly maxConcurrency?: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface IndexedFilePath {
|
|
42
|
+
readonly index: number;
|
|
43
|
+
readonly filePath: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface IndexedDiagnostics {
|
|
47
|
+
readonly index: number;
|
|
48
|
+
readonly filePath: string;
|
|
49
|
+
readonly outcome: PostEditDiagnosticsOutcome;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function createPostEditNotConfiguredCache(): PostEditNotConfiguredCache {
|
|
53
|
+
return { notConfiguredExtensions: new Set() };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resetPostEditNotConfiguredCache(cache: PostEditNotConfiguredCache): void {
|
|
57
|
+
cache.notConfiguredExtensions.clear();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export async function collectPostEditDiagnostics(
|
|
61
|
+
input: CollectPostEditDiagnosticsInput,
|
|
62
|
+
): Promise<PostEditDiagnosticsResult> {
|
|
63
|
+
const cache = input.cache === undefined ? createPostEditNotConfiguredCache() : input.cache;
|
|
64
|
+
const filePaths = firstSeenDiagnosticTargets(input.filePaths, cache);
|
|
65
|
+
const results = await runBoundedDiagnostics(filePaths, input.runDiagnostics, input.maxConcurrency);
|
|
66
|
+
const blocks: PostEditDiagnosticsBlock[] = [];
|
|
67
|
+
const observations: PostEditDiagnosticsObservation[] = [];
|
|
68
|
+
|
|
69
|
+
for (const result of results) {
|
|
70
|
+
const classification = classifyDiagnostics(result.outcome);
|
|
71
|
+
switch (classification.kind) {
|
|
72
|
+
case "clean":
|
|
73
|
+
observations.push({ filePath: result.filePath, kind: "clean" });
|
|
74
|
+
break;
|
|
75
|
+
case "not_configured":
|
|
76
|
+
cache.notConfiguredExtensions.add(classification.extension);
|
|
77
|
+
observations.push({ filePath: result.filePath, kind: "not_configured" });
|
|
78
|
+
break;
|
|
79
|
+
case "block":
|
|
80
|
+
blocks.push({ filePath: result.filePath, diagnostics: classification.diagnostics });
|
|
81
|
+
observations.push({ filePath: result.filePath, kind: "block" });
|
|
82
|
+
break;
|
|
83
|
+
default: {
|
|
84
|
+
const exhaustive: never = classification;
|
|
85
|
+
return exhaustive;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return { blocks, observations };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function firstSeenDiagnosticTargets(
|
|
94
|
+
filePaths: readonly string[],
|
|
95
|
+
cache: PostEditNotConfiguredCache,
|
|
96
|
+
): readonly IndexedFilePath[] {
|
|
97
|
+
const seen = new Set<string>();
|
|
98
|
+
const targets: IndexedFilePath[] = [];
|
|
99
|
+
for (const filePath of filePaths) {
|
|
100
|
+
if (filePath.length === 0 || seen.has(filePath)) continue;
|
|
101
|
+
seen.add(filePath);
|
|
102
|
+
const extension = extensionKey(filePath);
|
|
103
|
+
if (extension !== undefined && cache.notConfiguredExtensions.has(extension)) continue;
|
|
104
|
+
targets.push({ index: targets.length, filePath });
|
|
105
|
+
}
|
|
106
|
+
return targets;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function runBoundedDiagnostics(
|
|
110
|
+
filePaths: readonly IndexedFilePath[],
|
|
111
|
+
runDiagnostics: DiagnosticsRunner,
|
|
112
|
+
maxConcurrency: number | undefined,
|
|
113
|
+
): Promise<readonly IndexedDiagnostics[]> {
|
|
114
|
+
const results: IndexedDiagnostics[] = [];
|
|
115
|
+
const workerCount = Math.min(Math.max(1, maxConcurrency ?? DEFAULT_MAX_CONCURRENCY), filePaths.length);
|
|
116
|
+
let nextIndex = 0;
|
|
117
|
+
|
|
118
|
+
async function worker(): Promise<void> {
|
|
119
|
+
for (;;) {
|
|
120
|
+
const target = filePaths[nextIndex];
|
|
121
|
+
nextIndex += 1;
|
|
122
|
+
if (target === undefined) return;
|
|
123
|
+
results[target.index] = {
|
|
124
|
+
index: target.index,
|
|
125
|
+
filePath: target.filePath,
|
|
126
|
+
outcome: await collectFileDiagnostics(target.filePath, runDiagnostics),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
132
|
+
return results.filter((result) => result !== undefined);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function collectFileDiagnostics(
|
|
136
|
+
filePath: string,
|
|
137
|
+
runDiagnostics: DiagnosticsRunner,
|
|
138
|
+
): Promise<PostEditDiagnosticsOutcome> {
|
|
139
|
+
try {
|
|
140
|
+
return normalizeDiagnosticsOutcome(await runDiagnostics(filePath));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
if (error instanceof Error) return formatDiagnosticsError(error);
|
|
143
|
+
return normalizeDiagnosticsText(String(error));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function normalizeDiagnosticsOutcome(outcome: PostEditDiagnosticsOutcome): PostEditDiagnosticsOutcome {
|
|
148
|
+
if (typeof outcome === "string") return normalizeDiagnosticsText(outcome);
|
|
149
|
+
return outcome;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeDiagnosticsText(text: string): string {
|
|
153
|
+
return text.trim();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function formatDiagnosticsError(error: Error): string {
|
|
157
|
+
const message = normalizeDiagnosticsText(error.message);
|
|
158
|
+
return message.length > 0 ? message : normalizeDiagnosticsText(String(error));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function classifyDiagnostics(
|
|
162
|
+
outcome: PostEditDiagnosticsOutcome,
|
|
163
|
+
):
|
|
164
|
+
| { readonly kind: "clean" }
|
|
165
|
+
| { readonly kind: "not_configured"; readonly extension: string }
|
|
166
|
+
| { readonly kind: "block"; readonly diagnostics: string } {
|
|
167
|
+
if (typeof outcome !== "string") {
|
|
168
|
+
return { kind: "not_configured", extension: outcome.extension };
|
|
169
|
+
}
|
|
170
|
+
const diagnostics = outcome;
|
|
171
|
+
if (diagnostics.length === 0 || diagnostics === CLEAN_DIAGNOSTICS_TEXT) return { kind: "clean" };
|
|
172
|
+
return { kind: "block", diagnostics };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function extensionKey(filePath: string): string | undefined {
|
|
176
|
+
const extension = extname(filePath).toLowerCase();
|
|
177
|
+
return extension.length === 0 ? undefined : extension;
|
|
178
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir, tmpdir } from "node:os";
|
|
3
|
+
import { delimiter, join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
createStandaloneMcpRequestContext,
|
|
8
|
+
LspRequestContextUnavailableError,
|
|
9
|
+
LspRequestContextParseError,
|
|
10
|
+
parseLspRequestContext,
|
|
11
|
+
runWithRequestContext,
|
|
12
|
+
lspRequestContext,
|
|
13
|
+
} from "./request-context.js";
|
|
14
|
+
|
|
15
|
+
const tempDirectories: string[] = [];
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const directory of tempDirectories.splice(0)) {
|
|
19
|
+
rmSync(directory, { recursive: true, force: true });
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
function tempRoot(prefix: string): string {
|
|
24
|
+
const root = mkdtempSync(join(homedir(), prefix));
|
|
25
|
+
tempDirectories.push(root);
|
|
26
|
+
return root;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function tempSystemRoot(prefix: string): string {
|
|
30
|
+
const root = mkdtempSync(join(tmpdir(), prefix));
|
|
31
|
+
tempDirectories.push(root);
|
|
32
|
+
return root;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("LspRequestContext", () => {
|
|
36
|
+
it("#given exact typed context #when parsed #then canonicalizes cwd and preserves typed paths", () => {
|
|
37
|
+
const root = tempRoot("lsp-context-root-");
|
|
38
|
+
const projectPath = join(root, ".codex", "lsp-client.json");
|
|
39
|
+
const userPath = join(homedir(), ".codex", "lsp-client.json");
|
|
40
|
+
const decisionsPath = join(homedir(), ".codex", "lsp-install-decisions.json");
|
|
41
|
+
mkdirSync(join(root, ".codex"), { recursive: true });
|
|
42
|
+
|
|
43
|
+
const context = parseLspRequestContext({
|
|
44
|
+
cwd: join(root, "."),
|
|
45
|
+
projectConfigPaths: [projectPath],
|
|
46
|
+
userConfigPath: userPath,
|
|
47
|
+
installDecisionsPath: decisionsPath,
|
|
48
|
+
capabilities: { installDecisionTool: false },
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(context).toEqual({
|
|
52
|
+
cwd: root,
|
|
53
|
+
projectConfigPaths: [projectPath],
|
|
54
|
+
userConfigPath: userPath,
|
|
55
|
+
installDecisionsPath: decisionsPath,
|
|
56
|
+
capabilities: { installDecisionTool: false },
|
|
57
|
+
});
|
|
58
|
+
expect(runWithRequestContext(context, () => lspRequestContext())).toEqual(context);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("#given no active context #when requested #then throws an actionable typed error", () => {
|
|
62
|
+
expect(() => lspRequestContext()).toThrow(LspRequestContextUnavailableError);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("#given unknown field #when parsed #then rejects before lookup", () => {
|
|
66
|
+
const root = tempRoot("lsp-context-unknown-");
|
|
67
|
+
const projectPath = join(root, ".codex", "lsp-client.json");
|
|
68
|
+
|
|
69
|
+
expect(() =>
|
|
70
|
+
parseLspRequestContext({
|
|
71
|
+
cwd: root,
|
|
72
|
+
projectConfigPaths: [projectPath],
|
|
73
|
+
userConfigPath: join(homedir(), ".codex", "lsp-client.json"),
|
|
74
|
+
installDecisionsPath: join(homedir(), ".codex", "lsp-install-decisions.json"),
|
|
75
|
+
capabilities: { installDecisionTool: true },
|
|
76
|
+
env: {},
|
|
77
|
+
}),
|
|
78
|
+
).toThrow(LspRequestContextParseError);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("#given project path outside cwd #when parsed #then rejects before config loading", () => {
|
|
82
|
+
const root = tempRoot("lsp-context-confined-");
|
|
83
|
+
const outside = tempRoot("lsp-context-outside-");
|
|
84
|
+
|
|
85
|
+
expect(() =>
|
|
86
|
+
parseLspRequestContext({
|
|
87
|
+
cwd: root,
|
|
88
|
+
projectConfigPaths: [join(outside, "lsp-client.json")],
|
|
89
|
+
userConfigPath: join(homedir(), ".codex", "lsp-client.json"),
|
|
90
|
+
installDecisionsPath: join(homedir(), ".codex", "lsp-install-decisions.json"),
|
|
91
|
+
capabilities: { installDecisionTool: true },
|
|
92
|
+
}),
|
|
93
|
+
).toThrow(LspRequestContextParseError);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("#given equivalent macOS var spelling #when parsed #then canonicalizes to one identity", () => {
|
|
97
|
+
const systemRoot = tempSystemRoot("lsp-context-private-var-");
|
|
98
|
+
const canonicalRoot = realpathSync(systemRoot);
|
|
99
|
+
const aliasRoot = canonicalRoot.startsWith("/private/var/") ? canonicalRoot.replace("/private/var/", "/var/") : systemRoot;
|
|
100
|
+
mkdirSync(join(canonicalRoot, ".codex"), { recursive: true });
|
|
101
|
+
|
|
102
|
+
const context = parseLspRequestContext({
|
|
103
|
+
cwd: aliasRoot,
|
|
104
|
+
projectConfigPaths: [join(aliasRoot, ".codex", "lsp-client.json")],
|
|
105
|
+
userConfigPath: join(homedir(), ".codex", "lsp-client.json"),
|
|
106
|
+
installDecisionsPath: join(homedir(), ".codex", "lsp-install-decisions.json"),
|
|
107
|
+
capabilities: { installDecisionTool: true },
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(context.cwd).toBe(canonicalRoot);
|
|
111
|
+
expect(context.projectConfigPaths).toEqual([join(canonicalRoot, ".codex", "lsp-client.json")]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("#given missing standard project config inside cwd #when parsed #then accepts and preserves the intended suffix", () => {
|
|
115
|
+
const root = tempRoot("lsp-context-missing-config-");
|
|
116
|
+
mkdirSync(join(root, ".codex"), { recursive: true });
|
|
117
|
+
|
|
118
|
+
const context = parseLspRequestContext({
|
|
119
|
+
cwd: root,
|
|
120
|
+
projectConfigPaths: [join(root, ".codex", "missing-lsp-client.json")],
|
|
121
|
+
userConfigPath: join(homedir(), ".codex", "lsp-client.json"),
|
|
122
|
+
installDecisionsPath: join(homedir(), ".codex", "lsp-install-decisions.json"),
|
|
123
|
+
capabilities: { installDecisionTool: true },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(context.projectConfigPaths).toEqual([join(root, ".codex", "missing-lsp-client.json")]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("#given symlink project config escaping cwd #when parsed #then rejects the escape", () => {
|
|
130
|
+
const root = tempRoot("lsp-context-symlink-root-");
|
|
131
|
+
const outside = tempRoot("lsp-context-symlink-outside-");
|
|
132
|
+
const outsideConfig = join(outside, "lsp-client.json");
|
|
133
|
+
writeFileSync(outsideConfig, "{}");
|
|
134
|
+
const linkedConfig = join(root, "linked-lsp-client.json");
|
|
135
|
+
symlinkSync(outsideConfig, linkedConfig);
|
|
136
|
+
|
|
137
|
+
expect(() =>
|
|
138
|
+
parseLspRequestContext({
|
|
139
|
+
cwd: root,
|
|
140
|
+
projectConfigPaths: [linkedConfig],
|
|
141
|
+
userConfigPath: join(homedir(), ".codex", "lsp-client.json"),
|
|
142
|
+
installDecisionsPath: join(homedir(), ".codex", "lsp-install-decisions.json"),
|
|
143
|
+
capabilities: { installDecisionTool: true },
|
|
144
|
+
}),
|
|
145
|
+
).toThrow(LspRequestContextParseError);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("#given standalone MCP env #when translated #then resolves exact defaults and relative paths", () => {
|
|
149
|
+
const root = tempRoot("lsp-context-standalone-");
|
|
150
|
+
const home = tempRoot("lsp-context-home-");
|
|
151
|
+
const absoluteProjectPath = join(root, ".omo", "lsp-client.json");
|
|
152
|
+
|
|
153
|
+
const context = createStandaloneMcpRequestContext({
|
|
154
|
+
cwd: root,
|
|
155
|
+
homeDir: home,
|
|
156
|
+
env: {
|
|
157
|
+
LSP_TOOLS_MCP_PROJECT_CONFIG: ["relative.json", "", absoluteProjectPath].join(delimiter),
|
|
158
|
+
LSP_TOOLS_MCP_USER_CONFIG: "user-lsp.json",
|
|
159
|
+
LSP_TOOLS_MCP_INSTALL_DECISIONS: "decisions.json",
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
expect(context).toEqual({
|
|
164
|
+
cwd: root,
|
|
165
|
+
projectConfigPaths: [join(root, "relative.json"), absoluteProjectPath],
|
|
166
|
+
userConfigPath: join(home, "user-lsp.json"),
|
|
167
|
+
installDecisionsPath: join(home, "decisions.json"),
|
|
168
|
+
capabilities: { installDecisionTool: true },
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
@@ -1,22 +1,235 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
2
5
|
|
|
3
|
-
export interface
|
|
4
|
-
cwd
|
|
5
|
-
|
|
6
|
+
export interface LspRequestContext {
|
|
7
|
+
readonly cwd: string;
|
|
8
|
+
readonly projectConfigPaths: readonly string[];
|
|
9
|
+
readonly userConfigPath: string;
|
|
10
|
+
readonly installDecisionsPath: string;
|
|
11
|
+
readonly capabilities: {
|
|
12
|
+
readonly installDecisionTool: boolean;
|
|
13
|
+
};
|
|
6
14
|
}
|
|
7
15
|
|
|
8
|
-
|
|
16
|
+
export type RequestContext = LspRequestContext;
|
|
9
17
|
|
|
10
|
-
export
|
|
18
|
+
export interface StandaloneMcpRequestContextInput {
|
|
19
|
+
readonly cwd?: string;
|
|
20
|
+
readonly env?: Record<string, string | undefined>;
|
|
21
|
+
readonly homeDir?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class LspRequestContextParseError extends Error {
|
|
25
|
+
override readonly name = "LspRequestContextParseError";
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
readonly code: string,
|
|
29
|
+
message: string,
|
|
30
|
+
) {
|
|
31
|
+
super(message);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export class LspRequestContextUnavailableError extends Error {
|
|
36
|
+
override readonly name = "LspRequestContextUnavailableError";
|
|
37
|
+
|
|
38
|
+
constructor() {
|
|
39
|
+
super(
|
|
40
|
+
"LSP request context is required. Standalone MCP startup must install one with runWithRequestContext(createStandaloneMcpRequestContext()).",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const storage = new AsyncLocalStorage<LspRequestContext>();
|
|
46
|
+
const CONTEXT_FIELDS = new Set(["cwd", "projectConfigPaths", "userConfigPath", "installDecisionsPath", "capabilities"]);
|
|
47
|
+
const CAPABILITY_FIELDS = new Set(["installDecisionTool"]);
|
|
48
|
+
|
|
49
|
+
export function runWithRequestContext<T>(context: LspRequestContext, fn: () => T): T {
|
|
11
50
|
return storage.run(context, fn);
|
|
12
51
|
}
|
|
13
52
|
|
|
53
|
+
export function lspRequestContext(): LspRequestContext {
|
|
54
|
+
const context = storage.getStore();
|
|
55
|
+
if (!context) throw new LspRequestContextUnavailableError();
|
|
56
|
+
return context;
|
|
57
|
+
}
|
|
58
|
+
|
|
14
59
|
export function contextCwd(): string {
|
|
15
|
-
return
|
|
60
|
+
return lspRequestContext().cwd;
|
|
16
61
|
}
|
|
17
62
|
|
|
18
63
|
export function contextEnv(key: string): string | undefined {
|
|
19
|
-
const
|
|
20
|
-
if (
|
|
21
|
-
return
|
|
64
|
+
const context = lspRequestContext();
|
|
65
|
+
if (key === "LSP_TOOLS_MCP_PROJECT_CONFIG") return context.projectConfigPaths.join(delimiter);
|
|
66
|
+
if (key === "LSP_TOOLS_MCP_USER_CONFIG") return context.userConfigPath;
|
|
67
|
+
if (key === "LSP_TOOLS_MCP_INSTALL_DECISIONS") return context.installDecisionsPath;
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createStandaloneMcpRequestContext(
|
|
72
|
+
input: StandaloneMcpRequestContextInput = {},
|
|
73
|
+
): LspRequestContext {
|
|
74
|
+
const env = input.env ?? process.env;
|
|
75
|
+
const cwd = canonicalCwd(input.cwd ?? process.cwd());
|
|
76
|
+
const home = input.homeDir ?? homedir();
|
|
77
|
+
const projectConfigPaths = translateProjectConfigEnv(env["LSP_TOOLS_MCP_PROJECT_CONFIG"], cwd);
|
|
78
|
+
const userConfigPath = translateHomeConfigEnv(env["LSP_TOOLS_MCP_USER_CONFIG"], home, ".codex/lsp-client.json");
|
|
79
|
+
const installDecisionsPath = translateHomeConfigEnv(
|
|
80
|
+
env["LSP_TOOLS_MCP_INSTALL_DECISIONS"],
|
|
81
|
+
home,
|
|
82
|
+
".codex/lsp-install-decisions.json",
|
|
83
|
+
);
|
|
84
|
+
return parseLspRequestContext({
|
|
85
|
+
cwd,
|
|
86
|
+
projectConfigPaths,
|
|
87
|
+
userConfigPath,
|
|
88
|
+
installDecisionsPath,
|
|
89
|
+
capabilities: { installDecisionTool: true },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function parseLspRequestContext(value: unknown): LspRequestContext {
|
|
94
|
+
if (!isRecord(value)) {
|
|
95
|
+
throw new LspRequestContextParseError("invalid_context", "LSP request context must be an object.");
|
|
96
|
+
}
|
|
97
|
+
rejectUnknownFields(value, CONTEXT_FIELDS, "context");
|
|
98
|
+
|
|
99
|
+
const cwd = stringField(value, "cwd");
|
|
100
|
+
const projectConfigPaths = stringArrayField(value, "projectConfigPaths");
|
|
101
|
+
const userConfigPath = stringField(value, "userConfigPath");
|
|
102
|
+
const installDecisionsPath = stringField(value, "installDecisionsPath");
|
|
103
|
+
const capabilities = capabilitiesField(value["capabilities"]);
|
|
104
|
+
const canonical = canonicalCwd(cwd);
|
|
105
|
+
|
|
106
|
+
for (const path of projectConfigPaths) {
|
|
107
|
+
requireAbsolutePath(path, "projectConfigPaths");
|
|
108
|
+
const projectPath = canonicalizeExistingOrNearestAncestor(path);
|
|
109
|
+
if (!isPathInside(canonical, projectPath)) {
|
|
110
|
+
throw new LspRequestContextParseError(
|
|
111
|
+
"project_config_outside_cwd",
|
|
112
|
+
`Project LSP config path must be inside cwd: ${path}`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
requireAbsolutePath(userConfigPath, "userConfigPath");
|
|
117
|
+
requireAbsolutePath(installDecisionsPath, "installDecisionsPath");
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
cwd: canonical,
|
|
121
|
+
projectConfigPaths: projectConfigPaths.map((path) => canonicalizeExistingOrNearestAncestor(path)),
|
|
122
|
+
userConfigPath,
|
|
123
|
+
installDecisionsPath,
|
|
124
|
+
capabilities,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function translateProjectConfigEnv(value: string | undefined, cwd: string): readonly string[] {
|
|
129
|
+
if (value === undefined || value.length === 0) return [join(cwd, ".codex", "lsp-client.json")];
|
|
130
|
+
return value
|
|
131
|
+
.split(delimiter)
|
|
132
|
+
.filter((entry) => entry.length > 0)
|
|
133
|
+
.map((entry) => (isAbsolute(entry) ? entry : join(cwd, entry)));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function translateHomeConfigEnv(value: string | undefined, home: string, fallback: string): string {
|
|
137
|
+
if (value === undefined || value.length === 0) return join(home, fallback);
|
|
138
|
+
return isAbsolute(value) ? value : join(home, value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function canonicalCwd(cwd: string): string {
|
|
142
|
+
const resolved = resolve(cwd);
|
|
143
|
+
if (!existsSync(resolved) || !statSync(resolved).isDirectory()) {
|
|
144
|
+
throw new LspRequestContextParseError("invalid_cwd", `LSP request cwd must be an existing directory: ${cwd}`);
|
|
145
|
+
}
|
|
146
|
+
return realpathSync(resolved);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function canonicalizeExistingOrNearestAncestor(path: string): string {
|
|
150
|
+
let current = resolve(path);
|
|
151
|
+
const suffix: string[] = [];
|
|
152
|
+
while (true) {
|
|
153
|
+
try {
|
|
154
|
+
const existing = realpathSync(current);
|
|
155
|
+
return suffix.length === 0 ? existing : join(existing, ...suffix);
|
|
156
|
+
} catch (error) {
|
|
157
|
+
if (!isMissingPathError(error)) throw error;
|
|
158
|
+
const parent = dirname(current);
|
|
159
|
+
if (parent === current) throw error;
|
|
160
|
+
suffix.unshift(basename(current));
|
|
161
|
+
current = parent;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function capabilitiesField(value: unknown): LspRequestContext["capabilities"] {
|
|
167
|
+
if (!isRecord(value)) {
|
|
168
|
+
throw new LspRequestContextParseError("invalid_capabilities", "LSP request capabilities must be an object.");
|
|
169
|
+
}
|
|
170
|
+
rejectUnknownFields(value, CAPABILITY_FIELDS, "capabilities");
|
|
171
|
+
const installDecisionTool = value["installDecisionTool"];
|
|
172
|
+
if (typeof installDecisionTool !== "boolean") {
|
|
173
|
+
throw new LspRequestContextParseError(
|
|
174
|
+
"invalid_install_decision_capability",
|
|
175
|
+
"LSP request capabilities.installDecisionTool must be a boolean.",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
return { installDecisionTool };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function stringField(value: Record<string, unknown>, field: string): string {
|
|
182
|
+
const fieldValue = value[field];
|
|
183
|
+
if (typeof fieldValue !== "string" || fieldValue.length === 0) {
|
|
184
|
+
throw new LspRequestContextParseError("invalid_field", `LSP request context.${field} must be a non-empty string.`);
|
|
185
|
+
}
|
|
186
|
+
return fieldValue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function stringArrayField(value: Record<string, unknown>, field: string): readonly string[] {
|
|
190
|
+
const fieldValue = value[field];
|
|
191
|
+
if (!Array.isArray(fieldValue) || !fieldValue.every((item) => typeof item === "string" && item.length > 0)) {
|
|
192
|
+
throw new LspRequestContextParseError(
|
|
193
|
+
"invalid_field",
|
|
194
|
+
`LSP request context.${field} must be a non-empty string array.`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
return fieldValue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function requireAbsolutePath(path: string, field: string): void {
|
|
201
|
+
if (!isAbsolute(path)) {
|
|
202
|
+
throw new LspRequestContextParseError("relative_path", `LSP request context.${field} must be absolute: ${path}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function isPathInside(parent: string, child: string): boolean {
|
|
207
|
+
const childPath = resolve(child);
|
|
208
|
+
const relativePath = relative(parent, childPath);
|
|
209
|
+
return relativePath === "" || (!relativePath.startsWith("..") && !isAbsolute(relativePath));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isMissingPathError(error: unknown): boolean {
|
|
213
|
+
const code = errorCode(error);
|
|
214
|
+
return code === "ENOENT" || code === "ENOTDIR";
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function rejectUnknownFields(value: Record<string, unknown>, allowed: ReadonlySet<string>, scope: string): void {
|
|
218
|
+
const unknown = Object.keys(value).filter((key) => !allowed.has(key));
|
|
219
|
+
if (unknown.length > 0) {
|
|
220
|
+
throw new LspRequestContextParseError(
|
|
221
|
+
"unknown_field",
|
|
222
|
+
`Unknown LSP request ${scope} field: ${unknown.join(", ")}`,
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
228
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function errorCode(error: unknown): string | undefined {
|
|
232
|
+
if (!error || typeof error !== "object" || !("code" in error)) return undefined;
|
|
233
|
+
const code = Reflect.get(error, "code");
|
|
234
|
+
return typeof code === "string" ? code : undefined;
|
|
22
235
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { createStandaloneMcpRequestContext, runWithRequestContext } from "./request-context.js";
|
|
3
4
|
import { coerceToolArguments, executeLspTool, LSP_MCP_TOOLS } from "./tools.js";
|
|
4
5
|
|
|
5
6
|
const expectedToolSurface = [
|
|
@@ -142,7 +143,9 @@ describe("LSP core tool surface", () => {
|
|
|
142
143
|
|
|
143
144
|
test("#given legacy tool aliases #when executed #then aliases are callable but not listed", async () => {
|
|
144
145
|
// given / when
|
|
145
|
-
const result = await
|
|
146
|
+
const result = await runWithRequestContext(createStandaloneMcpRequestContext(), () =>
|
|
147
|
+
executeLspTool("lsp_diagnostics", { filePath: "module.wat" }),
|
|
148
|
+
);
|
|
146
149
|
|
|
147
150
|
// then
|
|
148
151
|
expect(result.content[0]?.text).toContain("No LSP server configured for extension: .wat");
|
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
import { isDirectoryPath, withLspClient } from "../lsp/client-wrapper.js";
|
|
1
|
+
import { isDirectoryPath, resolvePathInsideContext, withLspClient } from "../lsp/client-wrapper.js";
|
|
4
2
|
import { DEFAULT_MAX_DIAGNOSTICS } from "../lsp/constants.js";
|
|
5
3
|
import { aggregateDiagnosticsForDirectory } from "../lsp/directory-diagnostics.js";
|
|
6
4
|
import { filterDiagnosticsBySeverity, formatDiagnostic } from "../lsp/formatters.js";
|
|
7
5
|
import { inferExtensionFromDirectory } from "../lsp/infer-extension.js";
|
|
8
6
|
import type { Diagnostic } from "../lsp/types.js";
|
|
9
7
|
import { missingDependencyResult } from "../missing-dependency-result.js";
|
|
10
|
-
import { contextCwd } from "../request-context.js";
|
|
11
8
|
import { clientOptions, requireString, severityFilter } from "./parameters.js";
|
|
12
9
|
import { text } from "./result.js";
|
|
13
10
|
import type { LspDiagnosticsDetails, ToolExecutionResult } from "./types.js";
|
|
@@ -26,7 +23,7 @@ export async function executeLspDiagnostics(
|
|
|
26
23
|
const severity = severityFilter(params);
|
|
27
24
|
|
|
28
25
|
try {
|
|
29
|
-
const absPath =
|
|
26
|
+
const absPath = resolvePathInsideContext(filePath);
|
|
30
27
|
if (isDirectoryPath(absPath)) {
|
|
31
28
|
const extension = inferExtensionFromDirectory(absPath);
|
|
32
29
|
if (!extension) {
|
|
@@ -44,24 +41,46 @@ export async function executeLspDiagnostics(
|
|
|
44
41
|
return text(message, details);
|
|
45
42
|
}
|
|
46
43
|
|
|
47
|
-
|
|
44
|
+
const output = await aggregateDiagnosticsForDirectory(
|
|
45
|
+
absPath,
|
|
46
|
+
extension,
|
|
47
|
+
severity,
|
|
48
|
+
undefined,
|
|
49
|
+
signal === undefined ? {} : { signal },
|
|
50
|
+
);
|
|
48
51
|
const details: LspDiagnosticsDetails = {
|
|
49
52
|
filePath,
|
|
50
53
|
severity,
|
|
51
54
|
mode: "directory",
|
|
52
55
|
diagnostics: [],
|
|
53
|
-
totalDiagnostics:
|
|
56
|
+
totalDiagnostics: output.totalDiagnostics,
|
|
54
57
|
truncated: false,
|
|
58
|
+
fileFailures: [...output.fileFailures],
|
|
55
59
|
};
|
|
56
|
-
return text(output, details);
|
|
60
|
+
return text(output.output, details);
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
const result = await withLspClient(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
64
|
+
filePath,
|
|
65
|
+
async (client) => client.diagnostics(filePath, signal),
|
|
66
|
+
"diagnostics",
|
|
67
|
+
clientOptions(signal),
|
|
68
|
+
);
|
|
69
|
+
if (result.transientError) {
|
|
70
|
+
const message = result.transientError.message;
|
|
71
|
+
const details: LspDiagnosticsDetails = {
|
|
72
|
+
filePath,
|
|
73
|
+
severity,
|
|
74
|
+
mode: "file",
|
|
75
|
+
diagnostics: [],
|
|
76
|
+
totalDiagnostics: 0,
|
|
77
|
+
truncated: false,
|
|
78
|
+
error: message,
|
|
79
|
+
errorKind: result.transientError.kind,
|
|
80
|
+
};
|
|
81
|
+
return text(message, details, true);
|
|
82
|
+
}
|
|
83
|
+
|
|
65
84
|
const diagnostics = filterDiagnosticsBySeverity(asDiagnosticArray(result), severity);
|
|
66
85
|
const total = diagnostics.length;
|
|
67
86
|
const truncated = total > DEFAULT_MAX_DIAGNOSTICS;
|