oh-my-opencode 5.0.0-beta.1 → 5.0.0-beta.11
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/command/publish.md +44 -16
- package/.agents/skills/publish/SKILL.md +44 -16
- package/.agents/skills/work-with-pr/SKILL.md +37 -23
- package/.opencode/command/publish.md +44 -16
- package/.opencode/skills/work-with-pr/SKILL.md +37 -23
- package/README.md +12 -1
- package/dist/agents/atlas/agent.d.ts +0 -1
- package/dist/agents/sisyphus/grok-4.d.ts +20 -0
- package/dist/agents/sisyphus/index.d.ts +2 -0
- package/dist/agents/sisyphus-agent-config.d.ts +6 -0
- package/dist/agents/sisyphus-agent-factory.d.ts +1 -1
- package/dist/agents/sisyphus-runtime-prompt-reconciler.d.ts +15 -4
- package/dist/agents/types.d.ts +2 -2
- package/dist/cli/index.js +805 -534
- package/dist/cli/run/on-complete-hook.d.ts +2 -0
- package/dist/cli-node/index.js +805 -534
- package/dist/hooks/atlas/final-wave-approval-gate.test-support.d.ts +50 -0
- package/dist/hooks/atlas/system-reminder-templates.d.ts +0 -1
- package/dist/index.js +1566 -1217
- package/dist/shared/normalize-sdk-response.d.ts +1 -0
- package/dist/shared/shell-env.d.ts +1 -1
- package/dist/skills/coding-agent-sessions/SKILL.md +3 -2
- package/dist/skills/coding-agent-sessions/references/all-platforms.md +1 -1
- package/dist/skills/coding-agent-sessions/references/senpi.md +4 -4
- package/dist/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +1 -1
- package/dist/skills/frontend/SKILL.md +10 -7
- package/dist/skills/frontend/references/design/_INDEX.md +1 -0
- package/dist/skills/frontend/references/design/stylegallery.md +80 -0
- package/dist/skills/ultimate-browsing/ATTRIBUTION.md +37 -10
- package/dist/skills/ultimate-browsing/engine/AGENTS.md +179 -0
- package/dist/skills/ultimate-browsing/engine/templates/package.json +1 -1
- package/dist/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
- package/dist/skills/ulw-plan/SKILL.md +2 -2
- package/dist/skills/ulw-plan/references/full-workflow.md +27 -3
- package/dist/skills/ulw-plan/references/intent-clear.md +2 -1
- package/dist/skills/ulw-plan/references/intent-unclear.md +3 -3
- package/dist/tools/delegate-task/builtin-categories.d.ts +1 -0
- package/dist/tools/delegate-task/builtin-category-definition.d.ts +1 -0
- package/dist/tools/delegate-task/tool-description.d.ts +3 -1
- package/dist/tui.js +184 -83
- package/package.json +21 -19
- package/packages/lsp-core/src/lsp/client-diagnostics-concurrency.integration.test.ts +44 -0
- package/packages/lsp-core/src/lsp/client-diagnostics-freshness.integration.test.ts +0 -28
- package/packages/lsp-core/src/lsp/client-wrapper-outside-cwd.test.ts +78 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +66 -13
- package/packages/lsp-core/src/lsp/client-wrapper.ts +70 -18
- package/packages/lsp-core/src/lsp/outside-context-workspace.ts +33 -0
- package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +20 -1
- package/packages/lsp-core/src/lsp/workspace-markers.ts +1 -0
- package/packages/lsp-core/src/tools/diagnostics.ts +3 -3
- package/packages/lsp-core/src/tools/navigation.ts +4 -2
- package/packages/lsp-core/src/tools/rename.ts +4 -2
- package/packages/lsp-core/src/tools/symbols.ts +1 -1
- package/packages/lsp-daemon/dist/cli.js +160 -71
- package/packages/lsp-daemon/dist/client.js +135 -46
- package/packages/lsp-daemon/dist/index.js +143 -54
- package/packages/lsp-tools-mcp/dist/cli.js +131 -42
- package/packages/lsp-tools-mcp/dist/mcp.js +131 -42
- package/packages/lsp-tools-mcp/dist/tools.js +131 -42
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -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 +118 -8
- package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +118 -8
- package/packages/omo-codex/plugin/components/codegraph/package.json +1 -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/lazycodex-executor-verify/test/codex-hook.test.ts +3 -17
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +147 -64
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- 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/rules/test/bundled-rules-priority.test.ts +11 -16
- package/packages/omo-codex/plugin/components/rules/test/bundled-rules.test.ts +16 -23
- package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-budget.test.ts +9 -7
- package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-context.test.ts +0 -6
- package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-dedup.test.ts +6 -4
- package/packages/omo-codex/plugin/components/rules/test/codex-hook-post-compact-directive.test.ts +12 -9
- package/packages/omo-codex/plugin/components/rules/test/codex-hook.test.ts +28 -37
- package/packages/omo-codex/plugin/components/rules/test/formatter.test.ts +37 -69
- package/packages/omo-codex/plugin/components/rules/test/hook-output.test.ts +2 -3
- package/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts +1 -15
- package/packages/omo-codex/plugin/components/start-work-continuation/AGENTS.md +3 -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/test/cli.test.ts +0 -3
- package/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts +2 -16
- 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/teammode/test/thread-title-hook.test.ts +3 -9
- package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +24 -12
- package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +24 -12
- 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 +6 -0
- 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 +6 -0
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/SKILL.md +2 -2
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/full-workflow.md +27 -3
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-clear.md +2 -1
- package/packages/omo-codex/plugin/components/ultrawork/skills/ulw-plan/references/intent-unclear.md +3 -3
- package/packages/omo-codex/plugin/components/ultrawork/test/codex-hook.test.ts +0 -136
- package/packages/omo-codex/plugin/components/ultrawork/test/skill-pointer.test.ts +0 -2
- package/packages/omo-codex/plugin/components/ulw-loop/directive.md +6 -0
- 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 +3 -2
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/define-goal.md +108 -0
- package/packages/omo-codex/plugin/components/ulw-loop/skills/ulw-loop/references/full-workflow.md +1 -0
- package/packages/omo-codex/plugin/components/ulw-loop/test/checkpoint-continuation.test.ts +0 -1
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-goal-instruction.test.ts +2 -2
- package/packages/omo-codex/plugin/components/ulw-loop/test/codex-hook.test.ts +0 -3
- package/packages/omo-codex/plugin/components/ulw-loop/test/package-smoke.test.ts +2 -35
- package/packages/omo-codex/plugin/components/ulw-loop/test/ultrawork-directive.test.ts +4 -5
- 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 +20 -20
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/coding-agent-sessions/SKILL.md +3 -2
- package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/all-platforms.md +1 -1
- package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/senpi.md +4 -4
- package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +1 -1
- package/packages/omo-codex/plugin/skills/frontend/SKILL.md +10 -7
- package/packages/omo-codex/plugin/skills/frontend/references/design/_INDEX.md +1 -0
- package/packages/omo-codex/plugin/skills/frontend/references/design/stylegallery.md +80 -0
- package/packages/omo-codex/plugin/skills/ultimate-browsing/ATTRIBUTION.md +37 -10
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/AGENTS.md +179 -0
- package/packages/omo-codex/plugin/skills/ultimate-browsing/engine/templates/package.json +1 -1
- package/packages/omo-codex/plugin/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
- package/packages/omo-codex/plugin/skills/ultrawork/SKILL.md +6 -0
- package/packages/omo-codex/plugin/skills/ulw-loop/SKILL.md +3 -2
- package/packages/omo-codex/plugin/skills/ulw-loop/references/define-goal.md +108 -0
- package/packages/omo-codex/plugin/skills/ulw-loop/references/full-workflow.md +1 -0
- package/packages/omo-codex/plugin/skills/ulw-plan/SKILL.md +2 -2
- package/packages/omo-codex/plugin/skills/ulw-plan/references/full-workflow.md +27 -3
- package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-clear.md +2 -1
- package/packages/omo-codex/plugin/skills/ulw-plan/references/intent-unclear.md +3 -3
- package/packages/omo-codex/plugin/test/aggregate-agents.test.mjs +19 -173
- package/packages/omo-codex/plugin/test/aggregate-hooks.test.mjs +4 -24
- package/packages/omo-codex/plugin/test/aggregate-plugin-fixture.mjs +175 -13
- package/packages/omo-codex/plugin/test/aggregate.test.mjs +78 -2
- package/packages/omo-codex/plugin/test/auto-update-release-notes.test.mjs +19 -33
- package/packages/omo-codex/plugin/test/lcx-contribute-bug-fix-template.test.mjs +21 -27
- package/packages/omo-codex/plugin/test/scaffold-plan.test.mjs +0 -36
- package/packages/omo-codex/plugin/test/sync-skills-codex-compatibility.test.mjs +101 -0
- package/packages/omo-codex/plugin/test/sync-skills.test.mjs +1 -119
- package/packages/omo-codex/plugin/test/teammode-archive-ambiguity.test.mjs +0 -40
- package/packages/omo-codex/plugin/test/teammode-communication.test.mjs +6 -62
- package/packages/omo-codex/plugin/test/teammode-thread-links.test.mjs +3 -36
- package/packages/omo-codex/plugin/test/teammode-transport.test.mjs +0 -44
- package/packages/omo-codex/plugin/test/teammode-worktree.test.mjs +2 -6
- package/packages/omo-codex/plugin/test/ultrawork-skill-pointer.test.mjs +0 -3
- package/packages/omo-codex/plugin/test/ulw-plan-review-state-contract.test.mjs +0 -3
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +57 -19
- package/packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs +7 -2
- package/packages/shared-skills/skills/coding-agent-sessions/SKILL.md +3 -2
- package/packages/shared-skills/skills/coding-agent-sessions/references/all-platforms.md +1 -1
- package/packages/shared-skills/skills/coding-agent-sessions/references/senpi.md +4 -4
- package/packages/shared-skills/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +1 -1
- package/packages/shared-skills/skills/frontend/SKILL.md +10 -7
- package/packages/shared-skills/skills/frontend/references/design/_INDEX.md +1 -0
- package/packages/shared-skills/skills/frontend/references/design/stylegallery.md +80 -0
- package/packages/shared-skills/skills/ultimate-browsing/ATTRIBUTION.md +37 -10
- package/packages/shared-skills/skills/ultimate-browsing/engine/AGENTS.md +179 -0
- package/packages/shared-skills/skills/ultimate-browsing/engine/templates/package.json +1 -1
- package/packages/shared-skills/skills/ultimate-browsing/references/chrome-stealth.md +11 -11
- package/packages/shared-skills/skills/ulw-plan/SKILL.md +2 -2
- package/packages/shared-skills/skills/ulw-plan/references/full-workflow.md +27 -3
- package/packages/shared-skills/skills/ulw-plan/references/intent-clear.md +2 -1
- package/packages/shared-skills/skills/ulw-plan/references/intent-unclear.md +3 -3
- package/dist/tools/call-omo-agent/background-agent-executor.d.ts +0 -5
- package/packages/omo-codex/plugin/test/aggregate-skills.test.mjs +0 -92
- package/packages/omo-codex/plugin/test/sync-skills-orchestration.test.mjs +0 -314
- package/packages/omo-codex/plugin/test/ulw-plan-scope-contract.test.mjs +0 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oh-my-opencode",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.11",
|
|
4
4
|
"description": "The Best AI Agent Harness - Batteries-Included OpenCode Plugin with Multi-Model Orchestration, Parallel Background Agents, and Crafted LSP/AST Tools",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -136,6 +136,7 @@
|
|
|
136
136
|
"typecheck:script": "tsgo --noEmit -p script/tsconfig.json",
|
|
137
137
|
"test": "bun test",
|
|
138
138
|
"test:codex": "bun run build:codex-install && bun run build:git-bash-mcp && bun run build:lsp-tools-mcp && bun run build:lsp-daemon && npm --prefix packages/lsp-tools-mcp test && npm --prefix packages/omo-codex/plugin ci && npm --prefix packages/omo-codex/plugin/components/ulw-loop test && bun run --cwd packages/omo-codex/plugin build && npm --prefix packages/omo-codex/plugin/components/codegraph run typecheck && npm --prefix packages/omo-codex/plugin/components/codegraph test && node scripts/check-third-party-notices.mjs --ship && bun test packages/omo-opencode/src/cli/cli-installer.platform.test.ts packages/omo-codex/src/install/codex-cache.test.ts packages/omo-codex/src/install/codex-cleanup.test.ts packages/omo-codex/src/install/codex-config-agent-cleanup.test.ts packages/omo-codex/src/install/codex-config-autonomous-features.test.ts packages/omo-codex/src/install/codex-config-reasoning.test.ts packages/omo-codex/src/install/codex-config-toml.test.ts packages/omo-codex/src/install/codex-project-local-cleanup.test.ts packages/omo-codex/src/install/install-codex-project-local-cleanup.test.ts packages/omo-codex/src/install/install-codex.test.ts packages/omo-codex/src/install/install-codex-packaged.test.ts packages/omo-codex/src/install/link-cached-plugin-agents.test.ts packages/omo-codex/src/**/*.test.ts packages/utils/src/jsonc-parser.test.ts packages/utils/src/frontmatter.test.ts packages/hashline-core/src/hash-computation.test.ts packages/hashline-core/src/smoke-untested-modules.test.ts packages/rules-engine/src/index.test.ts packages/rules-engine/src/security-boundary.test.ts packages/agents-md-core/src/injector.test.ts packages/omo-codex/plugin/components/lsp/test/package-smoke.test.ts && node --test packages/omo-codex/plugin/test/*.test.mjs packages/omo-codex/scripts/install-cache-copy.test.mjs packages/omo-codex/scripts/install-cli-args.test.mjs packages/omo-codex/scripts/install-delegated-command.test.mjs packages/omo-codex/scripts/install-config-autonomous-features.test.mjs packages/omo-codex/scripts/install-config-autonomous.test.mjs packages/omo-codex/scripts/install-config-reasoning.test.mjs packages/omo-codex/scripts/install-config.test.mjs packages/omo-codex/scripts/install-hook-targets.test.mjs packages/omo-codex/scripts/install-project-local-cleanup.test.mjs packages/omo-codex/scripts/install-lazycodex-version-stamp.test.mjs packages/omo-codex/scripts/install-local-entrypoint.test.mjs packages/omo-codex/scripts/install-local-git-bash-preflight.test.mjs packages/omo-codex/scripts/install-local.test.mjs packages/omo-codex/scripts/install-marketplace-cache.test.mjs packages/omo-codex/scripts/install-mcp-context7-runtime.test.mjs packages/omo-codex/scripts/install-mcp-runtime.test.mjs packages/omo-codex/scripts/install-packaged-local.test.mjs packages/omo-codex/scripts/install-generated-bundle.test.mjs packages/omo-codex/scripts/install-agent-links.test.mjs packages/omo-codex/scripts/install-bin-links.test.mjs",
|
|
139
|
+
"test:fast": "bun run script/test-fast.ts",
|
|
139
140
|
"test:senpi": "bun run build:senpi-plugin && tsgo --noEmit -p packages/omo-senpi/tsconfig.json && bun test packages/omo-senpi",
|
|
140
141
|
"test:windows-codex": "bun run test:codex",
|
|
141
142
|
"build:git-bash-mcp": "bun run --cwd packages/git-bash-mcp build",
|
|
@@ -180,7 +181,7 @@
|
|
|
180
181
|
"zod": "^4.4.3"
|
|
181
182
|
},
|
|
182
183
|
"devDependencies": {
|
|
183
|
-
"@code-yeongyu/senpi": "2026.8.
|
|
184
|
+
"@code-yeongyu/senpi": "2026.8.18-3",
|
|
184
185
|
"@oh-my-opencode/agents-md-core": "workspace:*",
|
|
185
186
|
"@oh-my-opencode/ast-grep-mcp": "workspace:*",
|
|
186
187
|
"@oh-my-opencode/boulder-state": "workspace:*",
|
|
@@ -196,6 +197,7 @@
|
|
|
196
197
|
"@oh-my-opencode/model-core": "workspace:*",
|
|
197
198
|
"@oh-my-opencode/omo-codex": "workspace:*",
|
|
198
199
|
"@oh-my-opencode/omo-config-core": "workspace:*",
|
|
200
|
+
"@oh-my-opencode/omo-opencode": "workspace:*",
|
|
199
201
|
"@oh-my-opencode/omo-senpi": "workspace:*",
|
|
200
202
|
"@oh-my-opencode/openclaw-core": "workspace:*",
|
|
201
203
|
"@oh-my-opencode/pi-goal": "workspace:*",
|
|
@@ -211,7 +213,7 @@
|
|
|
211
213
|
"@oh-my-opencode/utils": "workspace:*",
|
|
212
214
|
"@types/js-yaml": "^4.0.9",
|
|
213
215
|
"@types/picomatch": "^4.0.3",
|
|
214
|
-
"@typescript/native-preview": "7.0.0-dev.
|
|
216
|
+
"@typescript/native-preview": "7.0.0-dev.20260707.2",
|
|
215
217
|
"@xterm/addon-unicode11": "^0.9.0",
|
|
216
218
|
"@xterm/xterm": "^6.0.0",
|
|
217
219
|
"bun-types": "1.3.14",
|
|
@@ -220,24 +222,24 @@
|
|
|
220
222
|
"typescript": "^7.0.2"
|
|
221
223
|
},
|
|
222
224
|
"optionalDependencies": {
|
|
223
|
-
"oh-my-opencode-darwin-arm64": "5.0.0-beta.
|
|
224
|
-
"oh-my-opencode-darwin-x64": "5.0.0-beta.
|
|
225
|
-
"oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.
|
|
226
|
-
"oh-my-opencode-linux-arm64": "5.0.0-beta.
|
|
227
|
-
"oh-my-opencode-linux-arm64-musl": "5.0.0-beta.
|
|
228
|
-
"oh-my-opencode-linux-x64": "5.0.0-beta.
|
|
229
|
-
"oh-my-opencode-linux-x64-baseline": "5.0.0-beta.
|
|
230
|
-
"oh-my-opencode-linux-x64-musl": "5.0.0-beta.
|
|
231
|
-
"oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.
|
|
232
|
-
"oh-my-opencode-windows-arm64": "5.0.0-beta.
|
|
233
|
-
"oh-my-opencode-windows-x64": "5.0.0-beta.
|
|
234
|
-
"oh-my-opencode-windows-x64-baseline": "5.0.0-beta.
|
|
225
|
+
"oh-my-opencode-darwin-arm64": "5.0.0-beta.11",
|
|
226
|
+
"oh-my-opencode-darwin-x64": "5.0.0-beta.11",
|
|
227
|
+
"oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.11",
|
|
228
|
+
"oh-my-opencode-linux-arm64": "5.0.0-beta.11",
|
|
229
|
+
"oh-my-opencode-linux-arm64-musl": "5.0.0-beta.11",
|
|
230
|
+
"oh-my-opencode-linux-x64": "5.0.0-beta.11",
|
|
231
|
+
"oh-my-opencode-linux-x64-baseline": "5.0.0-beta.11",
|
|
232
|
+
"oh-my-opencode-linux-x64-musl": "5.0.0-beta.11",
|
|
233
|
+
"oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.11",
|
|
234
|
+
"oh-my-opencode-windows-arm64": "5.0.0-beta.11",
|
|
235
|
+
"oh-my-opencode-windows-x64": "5.0.0-beta.11",
|
|
236
|
+
"oh-my-opencode-windows-x64-baseline": "5.0.0-beta.11"
|
|
235
237
|
},
|
|
236
238
|
"overrides": {
|
|
237
|
-
"@earendil-works/pi-agent-core": "0.
|
|
238
|
-
"@earendil-works/pi-ai": "0.
|
|
239
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
240
|
-
"@earendil-works/pi-tui": "0.
|
|
239
|
+
"@earendil-works/pi-agent-core": "0.84.2",
|
|
240
|
+
"@earendil-works/pi-ai": "0.84.2",
|
|
241
|
+
"@earendil-works/pi-coding-agent": "0.84.2",
|
|
242
|
+
"@earendil-works/pi-tui": "0.84.2",
|
|
241
243
|
"hono": "^4.12.18",
|
|
242
244
|
"@hono/node-server": "^1.19.13",
|
|
243
245
|
"express-rate-limit": "^8.5.1",
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createWorkspaceEditTestHarness,
|
|
5
|
+
diagnostic,
|
|
6
|
+
readEvents,
|
|
7
|
+
} from "./workspace-apply-edit-test-support.js";
|
|
8
|
+
|
|
9
|
+
const harness = createWorkspaceEditTestHarness();
|
|
10
|
+
|
|
11
|
+
afterEach(async () => {
|
|
12
|
+
await harness.cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("LspClient diagnostics concurrency", () => {
|
|
16
|
+
it("#given concurrent diagnostics on a cold file and an exact didOpen publish #when pull is not advertised #then one didOpen opens the file and both requests receive the current diagnostics", async () => {
|
|
17
|
+
const context = await harness.makeClient(
|
|
18
|
+
{
|
|
19
|
+
publishDiagnostics: [
|
|
20
|
+
{
|
|
21
|
+
trigger: "didOpen",
|
|
22
|
+
version: 1,
|
|
23
|
+
diagnostics: [diagnostic("exact-current")],
|
|
24
|
+
awaitClientDelivery: true,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{ diagnosticsFreshnessTimeoutMs: 500, versionlessPublishQuiescenceMs: 5 },
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
const [first, second] = await Promise.all([
|
|
32
|
+
context.client.diagnostics(context.source),
|
|
33
|
+
context.client.diagnostics(context.source),
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
expect(first.items).toEqual([diagnostic("exact-current")]);
|
|
37
|
+
expect(second.items).toEqual([diagnostic("exact-current")]);
|
|
38
|
+
expect(
|
|
39
|
+
readEvents(context.events).filter(
|
|
40
|
+
(event) => event.type === "clientNotification" && event.method === "textDocument/didOpen",
|
|
41
|
+
),
|
|
42
|
+
).toHaveLength(1);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -16,34 +16,6 @@ afterEach(async () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
describe("LspClient diagnostics freshness", () => {
|
|
19
|
-
it("#given concurrent diagnostics on a cold file and an exact didOpen publish #when pull is not advertised #then one didOpen opens the file and both requests receive the current diagnostics", async () => {
|
|
20
|
-
const context = await harness.makeClient(
|
|
21
|
-
{
|
|
22
|
-
publishDiagnostics: [
|
|
23
|
-
{
|
|
24
|
-
trigger: "didOpen",
|
|
25
|
-
version: 1,
|
|
26
|
-
diagnostics: [diagnostic("exact-current")],
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
{ diagnosticsFreshnessTimeoutMs: 50, versionlessPublishQuiescenceMs: 5 },
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const [first, second] = await Promise.all([
|
|
34
|
-
context.client.diagnostics(context.source),
|
|
35
|
-
context.client.diagnostics(context.source),
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
expect(first.items).toEqual([diagnostic("exact-current")]);
|
|
39
|
-
expect(second.items).toEqual([diagnostic("exact-current")]);
|
|
40
|
-
expect(
|
|
41
|
-
readEvents(context.events).filter(
|
|
42
|
-
(event) => event.type === "clientNotification" && event.method === "textDocument/didOpen",
|
|
43
|
-
),
|
|
44
|
-
).toHaveLength(1);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
19
|
it("#given a versionless publish that arrives after the current change #when no newer eligible publish arrives before quiescence #then diagnostics wait for that quiescence window and return the versionless payload", async () => {
|
|
48
20
|
const context = await harness.makeClient(
|
|
49
21
|
{
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
5
|
+
|
|
6
|
+
import { createStandaloneMcpRequestContext, runWithRequestContext } from "../request-context.js";
|
|
7
|
+
import { findWorkspaceRoot, resolveReadablePathInsideContext, resolvePathInsideContext } from "./client-wrapper.js";
|
|
8
|
+
import { LspInvalidPathError } from "./errors.js";
|
|
9
|
+
|
|
10
|
+
const tempDirectories: string[] = [];
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
for (const directory of tempDirectories.splice(0)) {
|
|
14
|
+
rmSync(directory, { recursive: true, force: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function tempRoot(prefix: string): string {
|
|
19
|
+
const root = mkdtempSync(join(tmpdir(), prefix));
|
|
20
|
+
tempDirectories.push(root);
|
|
21
|
+
return root;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
describe("LSP read-only access outside request cwd", () => {
|
|
25
|
+
it("#given an absolute file outside cwd #when resolving a read path #then returns the canonical path", () => {
|
|
26
|
+
const root = tempRoot("lsp-outcwd-root-");
|
|
27
|
+
const outside = tempRoot("lsp-outcwd-outside-");
|
|
28
|
+
const outsideFile = join(outside, "probe.ts");
|
|
29
|
+
writeFileSync(outsideFile, "export const value = 1;\n");
|
|
30
|
+
|
|
31
|
+
const resolved = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
32
|
+
resolveReadablePathInsideContext(outsideFile),
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
expect(resolved).toBe(realpathSync(outsideFile));
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it("#given an outside file inside its own marked project #when inferring workspace #then uses that project root", () => {
|
|
39
|
+
const root = tempRoot("lsp-outcwd-marker-root-");
|
|
40
|
+
const outside = tempRoot("lsp-outcwd-marker-outside-");
|
|
41
|
+
mkdirSync(join(outside, ".git"), { recursive: true });
|
|
42
|
+
mkdirSync(join(outside, "src"), { recursive: true });
|
|
43
|
+
const outsideFile = join(outside, "src", "probe.ts");
|
|
44
|
+
writeFileSync(outsideFile, "export const value = 1;\n");
|
|
45
|
+
|
|
46
|
+
const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
47
|
+
findWorkspaceRoot(outsideFile),
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
expect(workspace).toBe(realpathSync(outside));
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("#given an outside file without any marker #when inferring workspace #then falls back to its own directory", () => {
|
|
54
|
+
const root = tempRoot("lsp-outcwd-nomarker-root-");
|
|
55
|
+
const outside = tempRoot("lsp-outcwd-nomarker-outside-");
|
|
56
|
+
const outsideFile = join(outside, "probe.ts");
|
|
57
|
+
writeFileSync(outsideFile, "export const value = 1;\n");
|
|
58
|
+
|
|
59
|
+
const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
60
|
+
findWorkspaceRoot(outsideFile),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
expect(workspace).toBe(realpathSync(outside));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("#given an outside file #when resolving a mutating path #then still rejects to keep writes confined", () => {
|
|
67
|
+
const root = tempRoot("lsp-outcwd-write-root-");
|
|
68
|
+
const outside = tempRoot("lsp-outcwd-write-outside-");
|
|
69
|
+
const outsideFile = join(outside, "probe.ts");
|
|
70
|
+
writeFileSync(outsideFile, "export const value = 1;\n");
|
|
71
|
+
|
|
72
|
+
expect(() =>
|
|
73
|
+
runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
74
|
+
resolvePathInsideContext(outsideFile),
|
|
75
|
+
),
|
|
76
|
+
).toThrow(LspInvalidPathError);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -4,8 +4,9 @@ import { join } from "node:path";
|
|
|
4
4
|
import { afterEach, describe, expect, it } from "bun:test";
|
|
5
5
|
|
|
6
6
|
import { createStandaloneMcpRequestContext, runWithRequestContext } from "../request-context.js";
|
|
7
|
-
import { findWorkspaceRoot,
|
|
7
|
+
import { findWorkspaceRoot, resolveReadablePathInsideContext, withLspClient } from "./client-wrapper.js";
|
|
8
8
|
import { LspInvalidPathError } from "./errors.js";
|
|
9
|
+
import { LspManager } from "./manager.js";
|
|
9
10
|
|
|
10
11
|
const tempDirectories: string[] = [];
|
|
11
12
|
|
|
@@ -35,29 +36,81 @@ describe("LSP client path confinement", () => {
|
|
|
35
36
|
expect(workspace).toBe(realpathSync(root));
|
|
36
37
|
});
|
|
37
38
|
|
|
38
|
-
it("#given an absolute file outside context cwd #when resolving #then
|
|
39
|
+
it("#given an absolute file outside context cwd #when resolving #then infers the file's own project root", () => {
|
|
39
40
|
const root = tempRoot("lsp-client-wrapper-cwd-");
|
|
40
41
|
const outside = tempRoot("lsp-client-wrapper-outside-");
|
|
41
42
|
mkdirSync(join(outside, ".git"), { recursive: true });
|
|
42
43
|
writeFileSync(join(outside, "file.ts"), "export const outside = true;\n");
|
|
43
44
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
).
|
|
45
|
+
const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
46
|
+
findWorkspaceRoot(join(outside, "file.ts")),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(workspace).toBe(realpathSync(outside));
|
|
49
50
|
});
|
|
50
51
|
|
|
51
|
-
it("#given a symlink inside cwd that points outside #when resolving #then
|
|
52
|
+
it("#given a symlink inside cwd that points outside #when resolving read path #then keeps lexical path and cwd root", () => {
|
|
52
53
|
const root = tempRoot("lsp-client-wrapper-symlink-root-");
|
|
53
54
|
const outside = tempRoot("lsp-client-wrapper-symlink-outside-");
|
|
55
|
+
mkdirSync(join(root, ".git"), { recursive: true });
|
|
56
|
+
mkdirSync(join(outside, ".git"), { recursive: true });
|
|
57
|
+
writeFileSync(join(outside, "file.ts"), "export const outside = true;\n");
|
|
58
|
+
symlinkSync(outside, join(root, "linked"));
|
|
59
|
+
|
|
60
|
+
const result = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () => ({
|
|
61
|
+
filePath: resolveReadablePathInsideContext("linked/file.ts"),
|
|
62
|
+
workspaceRoot: findWorkspaceRoot("linked/file.ts"),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
expect(result.filePath).toBe(join(realpathSync(root), "linked", "file.ts"));
|
|
66
|
+
expect(result.workspaceRoot).toBe(realpathSync(root));
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("#given an absolute macOS alias path through an outside symlink #when resolving read path #then preserves the lexical suffix", () => {
|
|
70
|
+
const root = tempRoot("lsp-client-wrapper-alias-root-");
|
|
71
|
+
const outside = tempRoot("lsp-client-wrapper-alias-outside-");
|
|
72
|
+
mkdirSync(join(root, ".git"), { recursive: true });
|
|
73
|
+
writeFileSync(join(outside, "file.ts"), "export const outside = true;\n");
|
|
74
|
+
symlinkSync(outside, join(root, "linked"));
|
|
75
|
+
|
|
76
|
+
const result = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
77
|
+
resolveReadablePathInsideContext(join(root, "linked", "file.ts")),
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
expect(result).toBe(join(realpathSync(root), "linked", "file.ts"));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("#given missing parent directories without markers #when resolving workspace #then uses an existing directory", () => {
|
|
84
|
+
const root = tempRoot("lsp-client-wrapper-missing-root-");
|
|
85
|
+
const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
86
|
+
findWorkspaceRoot("missing/deep/file.ts"),
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
expect(workspace).toBe(realpathSync(root));
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("#given a symlink inside cwd that points outside #when starting rename #then rejects before client acquisition", async () => {
|
|
93
|
+
const root = tempRoot("lsp-client-wrapper-rename-root-");
|
|
94
|
+
const outside = tempRoot("lsp-client-wrapper-rename-outside-");
|
|
54
95
|
writeFileSync(join(outside, "file.ts"), "export const outside = true;\n");
|
|
55
96
|
symlinkSync(outside, join(root, "linked"));
|
|
97
|
+
let clientFactoryCalls = 0;
|
|
98
|
+
const manager = new LspManager({
|
|
99
|
+
clientFactory: () => {
|
|
100
|
+
clientFactoryCalls += 1;
|
|
101
|
+
throw new Error("client acquisition must not start");
|
|
102
|
+
},
|
|
103
|
+
});
|
|
56
104
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
105
|
+
try {
|
|
106
|
+
await expect(
|
|
107
|
+
runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
108
|
+
withLspClient("linked/file.ts", async () => undefined, "rename", { manager }),
|
|
109
|
+
),
|
|
110
|
+
).rejects.toThrow(LspInvalidPathError);
|
|
111
|
+
expect(clientFactoryCalls).toBe(0);
|
|
112
|
+
} finally {
|
|
113
|
+
await manager.stopAll();
|
|
114
|
+
}
|
|
62
115
|
});
|
|
63
116
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, statSync } from "node:fs";
|
|
2
|
-
import { dirname, join, resolve } from "node:path";
|
|
1
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
canonicalizeExistingOrNearestAncestor,
|
|
5
5
|
contextCwd,
|
|
@@ -16,11 +16,11 @@ import {
|
|
|
16
16
|
LspServerLookupError,
|
|
17
17
|
} from "./errors.js";
|
|
18
18
|
import { getLspManager, type LspManager } from "./manager.js";
|
|
19
|
+
import { findWorkspaceRootOutsideContext } from "./outside-context-workspace.js";
|
|
19
20
|
import { loadInstallDecision } from "./server-install-state.js";
|
|
20
21
|
import { findServerForExtension } from "./server-resolution.js";
|
|
21
22
|
import type { ServerLookupResult } from "./types.js";
|
|
22
|
-
|
|
23
|
-
const WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"];
|
|
23
|
+
import { WORKSPACE_MARKERS } from "./workspace-markers.js";
|
|
24
24
|
|
|
25
25
|
export function isDirectoryPath(filePath: string): boolean {
|
|
26
26
|
try {
|
|
@@ -31,30 +31,47 @@ export function isDirectoryPath(filePath: string): boolean {
|
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
export function findWorkspaceRoot(filePath: string): string {
|
|
34
|
-
const
|
|
34
|
+
const cwd = contextCwd();
|
|
35
|
+
const abs = resolveReadablePathInsideContext(filePath);
|
|
35
36
|
let dir = abs;
|
|
36
37
|
|
|
37
38
|
if (!isDirectoryPath(dir)) {
|
|
38
39
|
dir = dirname(dir);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
if (!isPathInside(cwd, abs)) return findWorkspaceRootOutsideContext(dir);
|
|
43
|
+
|
|
44
|
+
const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
|
|
45
|
+
while (isPathInside(cwd, dir)) {
|
|
46
|
+
const canonicalDir = existingDirectoryInsideContext(dir, cwd);
|
|
47
|
+
if (canonicalDir !== undefined) {
|
|
48
|
+
for (const marker of WORKSPACE_MARKERS) {
|
|
49
|
+
if (existsSync(join(dir, marker))) {
|
|
50
|
+
return canonicalDir;
|
|
51
|
+
}
|
|
46
52
|
}
|
|
47
53
|
}
|
|
48
|
-
|
|
54
|
+
if (dir === cwd) break;
|
|
49
55
|
dir = dirname(dir);
|
|
50
56
|
}
|
|
51
57
|
|
|
52
|
-
return
|
|
58
|
+
return fallbackRoot;
|
|
53
59
|
}
|
|
54
60
|
|
|
55
|
-
export function
|
|
61
|
+
export function resolveReadablePathInsideContext(filePath: string): string {
|
|
56
62
|
const cwd = contextCwd();
|
|
57
63
|
const abs = resolve(cwd, filePath);
|
|
64
|
+
if (isPathInside(cwd, abs)) return abs;
|
|
65
|
+
|
|
66
|
+
const rebased = rebaseThroughCanonicalAncestor(abs, cwd);
|
|
67
|
+
if (rebased !== undefined) return rebased;
|
|
68
|
+
|
|
69
|
+
return canonicalizeExistingOrNearestAncestor(abs);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function resolvePathInsideContext(filePath: string): string {
|
|
73
|
+
const cwd = contextCwd();
|
|
74
|
+
const abs = resolveReadablePathInsideContext(filePath);
|
|
58
75
|
const canonical = canonicalizeExistingOrNearestAncestor(abs);
|
|
59
76
|
if (!isPathInside(cwd, canonical)) {
|
|
60
77
|
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
@@ -62,6 +79,39 @@ export function resolvePathInsideContext(filePath: string): string {
|
|
|
62
79
|
return canonical;
|
|
63
80
|
}
|
|
64
81
|
|
|
82
|
+
function rebaseThroughCanonicalAncestor(path: string, cwd: string): string | undefined {
|
|
83
|
+
let current = path;
|
|
84
|
+
const suffix: string[] = [];
|
|
85
|
+
while (true) {
|
|
86
|
+
if (existsSync(current)) {
|
|
87
|
+
const canonical = realpathSync(current);
|
|
88
|
+
if (isPathInside(cwd, canonical)) return suffix.length === 0 ? canonical : join(canonical, ...suffix);
|
|
89
|
+
}
|
|
90
|
+
const parent = dirname(current);
|
|
91
|
+
if (parent === current) return undefined;
|
|
92
|
+
suffix.unshift(basename(current));
|
|
93
|
+
current = parent;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function existingDirectoryInsideContext(directory: string, cwd: string): string | undefined {
|
|
98
|
+
if (!existsSync(directory)) return undefined;
|
|
99
|
+
const canonical = realpathSync(directory);
|
|
100
|
+
if (!statSync(canonical).isDirectory()) return undefined;
|
|
101
|
+
return isPathInside(cwd, canonical) ? canonical : undefined;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function nearestExistingDirectoryInsideContext(directory: string, cwd: string): string | undefined {
|
|
105
|
+
let current = directory;
|
|
106
|
+
while (isPathInside(cwd, current)) {
|
|
107
|
+
const canonical = existingDirectoryInsideContext(current, cwd);
|
|
108
|
+
if (canonical !== undefined) return canonical;
|
|
109
|
+
if (current === cwd) return undefined;
|
|
110
|
+
current = dirname(current);
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
65
115
|
export function formatServerLookupError(result: Exclude<ServerLookupResult, { status: "found" }>): string {
|
|
66
116
|
if (result.status === "not_installed") {
|
|
67
117
|
return formatNotInstalled(result);
|
|
@@ -143,7 +193,7 @@ export interface WithLspClientOptions {
|
|
|
143
193
|
manager?: LspManager;
|
|
144
194
|
}
|
|
145
195
|
|
|
146
|
-
const
|
|
196
|
+
const READ_ONLY_TOOLS = new Set([
|
|
147
197
|
"diagnostics",
|
|
148
198
|
"definition",
|
|
149
199
|
"references",
|
|
@@ -154,11 +204,13 @@ const READ_ONLY_RETRY_TOOLS = new Set([
|
|
|
154
204
|
|
|
155
205
|
export async function withLspClient<T>(
|
|
156
206
|
filePath: string,
|
|
157
|
-
fn: (client: LspClient, workspaceRoot: string) => Promise<T>,
|
|
207
|
+
fn: (client: LspClient, workspaceRoot: string, resolvedFilePath: string) => Promise<T>,
|
|
158
208
|
toolName: string,
|
|
159
209
|
options: WithLspClientOptions = {},
|
|
160
210
|
): Promise<T> {
|
|
161
|
-
const absPath =
|
|
211
|
+
const absPath = READ_ONLY_TOOLS.has(toolName)
|
|
212
|
+
? resolveReadablePathInsideContext(filePath)
|
|
213
|
+
: resolvePathInsideContext(filePath);
|
|
162
214
|
|
|
163
215
|
if (isDirectoryPath(absPath)) {
|
|
164
216
|
throw new LspInvalidPathError(
|
|
@@ -181,9 +233,9 @@ export async function withLspClient<T>(
|
|
|
181
233
|
const client = await manager.getClient(root, server, options.signal);
|
|
182
234
|
|
|
183
235
|
try {
|
|
184
|
-
return await fn(client, root);
|
|
236
|
+
return await fn(client, root, absPath);
|
|
185
237
|
} catch (err) {
|
|
186
|
-
if (allowRetry &&
|
|
238
|
+
if (allowRetry && READ_ONLY_TOOLS.has(toolName) && isLspDeadConnectionError(err)) {
|
|
187
239
|
manager.invalidateClient(root, server.id, client);
|
|
188
240
|
return acquireAndCall(false);
|
|
189
241
|
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import { WORKSPACE_MARKERS } from "./workspace-markers.js";
|
|
5
|
+
|
|
6
|
+
/** Read-only tools accept out-of-cwd files, so cwd cannot bound this search; the fallback keeps the server scoped to the file's own directory. */
|
|
7
|
+
export function findWorkspaceRootOutsideContext(directory: string): string {
|
|
8
|
+
const marked = nearestMarkedAncestor(directory);
|
|
9
|
+
if (marked !== undefined) return marked;
|
|
10
|
+
return realpathSync(nearestExistingAncestor(directory));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function nearestMarkedAncestor(directory: string): string | undefined {
|
|
14
|
+
let current = directory;
|
|
15
|
+
for (;;) {
|
|
16
|
+
if (existsSync(current) && WORKSPACE_MARKERS.some((marker) => existsSync(join(current, marker)))) {
|
|
17
|
+
return realpathSync(current);
|
|
18
|
+
}
|
|
19
|
+
const parent = dirname(current);
|
|
20
|
+
if (parent === current) return undefined;
|
|
21
|
+
current = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function nearestExistingAncestor(directory: string): string {
|
|
26
|
+
let current = directory;
|
|
27
|
+
while (!existsSync(current)) {
|
|
28
|
+
const parent = dirname(current);
|
|
29
|
+
if (parent === current) return current;
|
|
30
|
+
current = parent;
|
|
31
|
+
}
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { mkdtempSync, readFileSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
@@ -87,6 +87,25 @@ describe("workspace edit adversarial inputs", () => {
|
|
|
87
87
|
expect(readFileSync(outsideFile, "utf-8")).toBe("outside bytes\n");
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
it("#given an in-workspace symlink to an outside file #when applying an edit #then rejects without modifying target", () => {
|
|
91
|
+
const workspace = makeTempDirectory("lsp-workspace-");
|
|
92
|
+
const outside = makeTempDirectory("lsp-outside-");
|
|
93
|
+
const outsideFile = join(outside, "outside.ts");
|
|
94
|
+
const canonicalWorkspace = realpathSync(workspace);
|
|
95
|
+
const linkedDirectory = join(canonicalWorkspace, "linked");
|
|
96
|
+
writeFileSync(outsideFile, "outside bytes\n", "utf-8");
|
|
97
|
+
symlinkSync(outside, linkedDirectory);
|
|
98
|
+
|
|
99
|
+
const result = applyWorkspaceEdit(
|
|
100
|
+
{ changes: { [pathToFileURL(join(linkedDirectory, "outside.ts")).href]: [insertion()] } },
|
|
101
|
+
{ workspaceRoot: canonicalWorkspace },
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(result.success).toBe(false);
|
|
105
|
+
expect(result.errors.join("\n")).toContain("outside workspace");
|
|
106
|
+
expect(readFileSync(outsideFile, "utf-8")).toBe("outside bytes\n");
|
|
107
|
+
});
|
|
108
|
+
|
|
90
109
|
it("#given annotations or mixed edit representations #when planned #then unsupported claims are rejected", () => {
|
|
91
110
|
const workspace = makeTempDirectory("lsp-workspace-");
|
|
92
111
|
const unsupported = [
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"] as const;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDirectoryPath,
|
|
1
|
+
import { isDirectoryPath, resolveReadablePathInsideContext, withLspClient } from "../lsp/client-wrapper.js";
|
|
2
2
|
import { DEFAULT_MAX_DIAGNOSTICS } from "../lsp/constants.js";
|
|
3
3
|
import { aggregateDiagnosticsForDirectory } from "../lsp/directory-diagnostics.js";
|
|
4
4
|
import { filterDiagnosticsBySeverity, formatDiagnostic } from "../lsp/formatters.js";
|
|
@@ -23,7 +23,7 @@ export async function executeLspDiagnostics(
|
|
|
23
23
|
const severity = severityFilter(params);
|
|
24
24
|
|
|
25
25
|
try {
|
|
26
|
-
const absPath =
|
|
26
|
+
const absPath = resolveReadablePathInsideContext(filePath);
|
|
27
27
|
if (isDirectoryPath(absPath)) {
|
|
28
28
|
const extension = inferExtensionFromDirectory(absPath);
|
|
29
29
|
if (!extension) {
|
|
@@ -62,7 +62,7 @@ export async function executeLspDiagnostics(
|
|
|
62
62
|
|
|
63
63
|
const result = await withLspClient(
|
|
64
64
|
filePath,
|
|
65
|
-
async (client) => client.diagnostics(
|
|
65
|
+
async (client, _workspaceRoot, resolvedFilePath) => client.diagnostics(resolvedFilePath, signal),
|
|
66
66
|
"diagnostics",
|
|
67
67
|
clientOptions(signal),
|
|
68
68
|
);
|
|
@@ -17,7 +17,8 @@ export async function executeLspGotoDefinition(
|
|
|
17
17
|
try {
|
|
18
18
|
const result = await withLspClient(
|
|
19
19
|
filePath,
|
|
20
|
-
async (client
|
|
20
|
+
async (client, _workspaceRoot, resolvedFilePath) =>
|
|
21
|
+
client.definition(resolvedFilePath, line, character, signal),
|
|
21
22
|
"definition",
|
|
22
23
|
clientOptions(signal),
|
|
23
24
|
);
|
|
@@ -49,7 +50,8 @@ export async function executeLspFindReferences(
|
|
|
49
50
|
try {
|
|
50
51
|
const result = await withLspClient(
|
|
51
52
|
filePath,
|
|
52
|
-
async (client
|
|
53
|
+
async (client, _workspaceRoot, resolvedFilePath) =>
|
|
54
|
+
client.references(resolvedFilePath, line, character, includeDeclaration, signal),
|
|
53
55
|
"references",
|
|
54
56
|
clientOptions(signal),
|
|
55
57
|
);
|
|
@@ -16,7 +16,8 @@ export async function executeLspPrepareRename(
|
|
|
16
16
|
try {
|
|
17
17
|
const result = await withLspClient(
|
|
18
18
|
filePath,
|
|
19
|
-
async (client
|
|
19
|
+
async (client, _workspaceRoot, resolvedFilePath) =>
|
|
20
|
+
client.prepareRename(resolvedFilePath, line, character, signal),
|
|
20
21
|
"prepareRename",
|
|
21
22
|
clientOptions(signal),
|
|
22
23
|
);
|
|
@@ -46,7 +47,8 @@ export async function executeLspRename(
|
|
|
46
47
|
try {
|
|
47
48
|
const result = await withLspClient(
|
|
48
49
|
filePath,
|
|
49
|
-
async (client
|
|
50
|
+
async (client, _workspaceRoot, resolvedFilePath) =>
|
|
51
|
+
client.rename(resolvedFilePath, line, character, newName, signal),
|
|
50
52
|
"rename",
|
|
51
53
|
clientOptions(signal),
|
|
52
54
|
);
|
|
@@ -47,7 +47,7 @@ export async function executeLspSymbols(
|
|
|
47
47
|
|
|
48
48
|
const symbols = await withLspClient(
|
|
49
49
|
filePath,
|
|
50
|
-
async (client) => client.documentSymbols(
|
|
50
|
+
async (client, _workspaceRoot, resolvedFilePath) => client.documentSymbols(resolvedFilePath, signal),
|
|
51
51
|
"documentSymbols",
|
|
52
52
|
clientOptions(signal),
|
|
53
53
|
);
|