oh-my-opencode 5.0.0-beta.10 → 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/dist/cli/index.js +24 -85
- package/dist/cli-node/index.js +24 -85
- package/dist/index.js +70 -92
- 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 +9 -70
- package/package.json +14 -14
- package/packages/lsp-core/src/lsp/client-wrapper-outside-cwd.test.ts +78 -0
- package/packages/lsp-core/src/lsp/client-wrapper.test.ts +6 -6
- package/packages/lsp-core/src/lsp/client-wrapper.ts +5 -6
- package/packages/lsp-core/src/lsp/outside-context-workspace.ts +33 -0
- package/packages/lsp-core/src/lsp/workspace-markers.ts +1 -0
- package/packages/lsp-daemon/dist/cli.js +94 -59
- package/packages/lsp-daemon/dist/client.js +72 -37
- package/packages/lsp-daemon/dist/index.js +78 -43
- package/packages/lsp-tools-mcp/dist/cli.js +68 -33
- package/packages/lsp-tools-mcp/dist/mcp.js +68 -33
- package/packages/lsp-tools-mcp/dist/tools.js +68 -33
- 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/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/lsp/dist/.omo-runtime-manifest.json +3 -3
- package/packages/omo-codex/plugin/components/lsp/dist/cli.js +83 -54
- 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/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/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/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- 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/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 +13 -13
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
|
@@ -2,5 +2,6 @@ import type { CategoryConfig } from "../../config/schema";
|
|
|
2
2
|
export declare const DEFAULT_CATEGORIES: Record<string, CategoryConfig>;
|
|
3
3
|
export declare const CATEGORY_PROMPT_APPENDS: Record<string, string>;
|
|
4
4
|
export declare const CATEGORY_DESCRIPTIONS: Record<string, string>;
|
|
5
|
+
export declare const CATEGORY_CALLER_GUIDANCE: Record<string, string | undefined>;
|
|
5
6
|
export declare const CATEGORY_PROMPT_APPEND_RESOLVERS: Record<string, (model: string | undefined) => string>;
|
|
6
7
|
export declare const BUILTIN_CATEGORY_REQUIRES_MODEL: Record<string, string>;
|
|
@@ -6,4 +6,6 @@ export interface DelegateTaskPresentation {
|
|
|
6
6
|
categoryExamples: string;
|
|
7
7
|
description: string;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
type DelegateTaskPresentationOptions = Pick<DelegateTaskToolOptions, "availableCategories" | "availableSkills" | "userCategories">;
|
|
10
|
+
export declare function createDelegateTaskPresentation(options: DelegateTaskPresentationOptions): DelegateTaskPresentation;
|
|
11
|
+
export {};
|
package/dist/tui.js
CHANGED
|
@@ -78184,24 +78184,14 @@ var init_agent_display_names = __esm(() => {
|
|
|
78184
78184
|
// packages/omo-opencode/src/tools/delegate-task/anthropic-categories.ts
|
|
78185
78185
|
var UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND = `<Category_Context>
|
|
78186
78186
|
You are working on tasks that don't fit specific categories but require substantial effort.
|
|
78187
|
-
|
|
78188
|
-
<Selection_Gate>
|
|
78189
|
-
BEFORE selecting this category, VERIFY ALL conditions:
|
|
78190
|
-
1. Task does NOT fit: quick (trivial), visual-engineering (UI), ultrabrain (deep logic), artistry (creative), writing (docs)
|
|
78191
|
-
2. Task requires substantial effort across multiple systems/modules
|
|
78192
|
-
3. Changes have broad impact or require careful coordination
|
|
78193
|
-
4. NOT just "complex" - must be genuinely unclassifiable AND high-effort
|
|
78194
|
-
|
|
78195
|
-
If task fits ANY other category, DO NOT select unspecified-high.
|
|
78196
|
-
If task is unclassifiable but moderate-effort, use unspecified-low instead.
|
|
78197
|
-
</Selection_Gate>
|
|
78198
|
-
</Category_Context>`, ANTHROPIC_CATEGORIES;
|
|
78187
|
+
</Category_Context>`, UNSPECIFIED_HIGH_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Use only when no specialist category fits and substantial effort spans systems/modules with broad impact. Use unspecified-low for contained moderate work.</Selection_Gate>`, ANTHROPIC_CATEGORIES;
|
|
78199
78188
|
var init_anthropic_categories = __esm(() => {
|
|
78200
78189
|
ANTHROPIC_CATEGORIES = [
|
|
78201
78190
|
{
|
|
78202
78191
|
name: "unspecified-high",
|
|
78203
78192
|
config: { model: "kimi-for-coding/k3", variant: "max" },
|
|
78204
78193
|
description: "Tasks that don't fit other categories, high effort required",
|
|
78194
|
+
callerGuidance: UNSPECIFIED_HIGH_CATEGORY_CALLER_GUIDANCE,
|
|
78205
78195
|
promptAppend: UNSPECIFIED_HIGH_CATEGORY_PROMPT_APPEND
|
|
78206
78196
|
}
|
|
78207
78197
|
];
|
|
@@ -78458,64 +78448,10 @@ Approach:
|
|
|
78458
78448
|
- Minimal viable implementation
|
|
78459
78449
|
- Skip unnecessary abstractions
|
|
78460
78450
|
- Direct and concise
|
|
78461
|
-
</Category_Context>
|
|
78462
|
-
|
|
78463
|
-
<Caller_Warning>
|
|
78464
|
-
THIS CATEGORY USES A SMALLER/FASTER MODEL (gpt-5.6-luna-fast).
|
|
78465
|
-
|
|
78466
|
-
The model executing this task is optimized for speed over depth. Your prompt MUST be:
|
|
78467
|
-
|
|
78468
|
-
**EXHAUSTIVELY EXPLICIT** - Leave NOTHING to interpretation:
|
|
78469
|
-
1. MUST DO: List every required action as atomic, numbered steps
|
|
78470
|
-
2. MUST NOT DO: Explicitly forbid likely mistakes and deviations
|
|
78471
|
-
3. EXPECTED OUTPUT: Describe exact success criteria with concrete examples
|
|
78472
|
-
|
|
78473
|
-
**WHY THIS MATTERS:**
|
|
78474
|
-
- Smaller models benefit from explicit guardrails
|
|
78475
|
-
- Vague instructions may lead to unpredictable results
|
|
78476
|
-
- Implicit expectations may be missed
|
|
78477
|
-
**PROMPT STRUCTURE (MANDATORY):**
|
|
78478
|
-
\`\`\`
|
|
78479
|
-
TASK: [One-sentence goal]
|
|
78480
|
-
|
|
78481
|
-
MUST DO:
|
|
78482
|
-
1. [Specific action with exact details]
|
|
78483
|
-
2. [Another specific action]
|
|
78484
|
-
...
|
|
78485
|
-
|
|
78486
|
-
MUST NOT DO:
|
|
78487
|
-
- [Forbidden action + why]
|
|
78488
|
-
- [Another forbidden action]
|
|
78489
|
-
...
|
|
78490
|
-
|
|
78491
|
-
EXPECTED OUTPUT:
|
|
78492
|
-
- [Exact deliverable description]
|
|
78493
|
-
- [Success criteria / verification method]
|
|
78494
|
-
\`\`\`
|
|
78495
|
-
|
|
78496
|
-
If your prompt lacks this structure, REWRITE IT before delegating.
|
|
78497
|
-
</Caller_Warning>`, UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND = `<Category_Context>
|
|
78451
|
+
</Category_Context>`, QUICK_CATEGORY_CALLER_GUIDANCE = `<Caller_Warning>Small/fast model: before delegating, write an explicit prompt with numbered must-do steps, forbidden deviations, and concrete success criteria.</Caller_Warning>`, UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND = `<Category_Context>
|
|
78498
78452
|
You are working on tasks that don't fit specific categories but require moderate effort.
|
|
78499
|
-
|
|
78500
|
-
<
|
|
78501
|
-
BEFORE selecting this category, VERIFY ALL conditions:
|
|
78502
|
-
1. Task does NOT fit: quick (trivial), visual-engineering (UI), ultrabrain (deep logic), artistry (creative), writing (docs)
|
|
78503
|
-
2. Task requires more than trivial effort but is NOT system-wide
|
|
78504
|
-
3. Scope is contained within a few files/modules
|
|
78505
|
-
|
|
78506
|
-
If task fits ANY other category, DO NOT select unspecified-low.
|
|
78507
|
-
This is NOT a default choice - it's for genuinely unclassifiable moderate-effort work.
|
|
78508
|
-
</Selection_Gate>
|
|
78509
|
-
</Category_Context>
|
|
78510
|
-
|
|
78511
|
-
<Caller_Warning>
|
|
78512
|
-
THIS CATEGORY USES GROK 4.6 (xhigh).
|
|
78513
|
-
|
|
78514
|
-
**PROVIDE CLEAR STRUCTURE:**
|
|
78515
|
-
1. MUST DO: Enumerate required actions explicitly
|
|
78516
|
-
2. MUST NOT DO: State forbidden actions to prevent scope creep
|
|
78517
|
-
3. EXPECTED OUTPUT: Define concrete success criteria
|
|
78518
|
-
</Caller_Warning>`, OPENAI_CATEGORIES;
|
|
78453
|
+
</Category_Context>`, UNSPECIFIED_LOW_CATEGORY_CALLER_GUIDANCE = `<Selection_Gate>Use only when no specialist category fits, effort is moderate, and scope stays within a few files/modules. Prefer any matching specialist category.</Selection_Gate>
|
|
78454
|
+
<Caller_Warning>Provide explicit must-do steps, forbidden scope, and concrete success criteria.</Caller_Warning>`, OPENAI_CATEGORIES;
|
|
78519
78455
|
var init_openai_categories = __esm(() => {
|
|
78520
78456
|
init_types6();
|
|
78521
78457
|
OPENAI_CATEGORIES = [
|
|
@@ -78537,12 +78473,14 @@ var init_openai_categories = __esm(() => {
|
|
|
78537
78473
|
name: "quick",
|
|
78538
78474
|
config: { model: "kimi-for-coding/kimi-for-coding-highspeed" },
|
|
78539
78475
|
description: "Trivial tasks - single file changes, typo fixes, simple modifications",
|
|
78476
|
+
callerGuidance: QUICK_CATEGORY_CALLER_GUIDANCE,
|
|
78540
78477
|
promptAppend: QUICK_CATEGORY_PROMPT_APPEND
|
|
78541
78478
|
},
|
|
78542
78479
|
{
|
|
78543
78480
|
name: "unspecified-low",
|
|
78544
78481
|
config: { model: "xai/grok-4.6", variant: "xhigh" },
|
|
78545
78482
|
description: "Tasks that don't fit other categories, low effort required",
|
|
78483
|
+
callerGuidance: UNSPECIFIED_LOW_CATEGORY_CALLER_GUIDANCE,
|
|
78546
78484
|
promptAppend: UNSPECIFIED_LOW_CATEGORY_PROMPT_APPEND
|
|
78547
78485
|
}
|
|
78548
78486
|
];
|
|
@@ -78552,7 +78490,7 @@ var init_openai_categories = __esm(() => {
|
|
|
78552
78490
|
function buildCategoryRecord(selector) {
|
|
78553
78491
|
return Object.fromEntries(BUILTIN_CATEGORIES.map((definition2) => [definition2.name, selector(definition2)]));
|
|
78554
78492
|
}
|
|
78555
|
-
var BUILTIN_CATEGORIES, DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, CATEGORY_PROMPT_APPEND_RESOLVERS, BUILTIN_CATEGORY_REQUIRES_MODEL;
|
|
78493
|
+
var BUILTIN_CATEGORIES, DEFAULT_CATEGORIES, CATEGORY_PROMPT_APPENDS, CATEGORY_DESCRIPTIONS, CATEGORY_CALLER_GUIDANCE, CATEGORY_PROMPT_APPEND_RESOLVERS, BUILTIN_CATEGORY_REQUIRES_MODEL;
|
|
78556
78494
|
var init_builtin_categories = __esm(() => {
|
|
78557
78495
|
init_anthropic_categories();
|
|
78558
78496
|
init_google_categories();
|
|
@@ -78567,6 +78505,7 @@ var init_builtin_categories = __esm(() => {
|
|
|
78567
78505
|
DEFAULT_CATEGORIES = buildCategoryRecord((definition2) => definition2.config);
|
|
78568
78506
|
CATEGORY_PROMPT_APPENDS = buildCategoryRecord((definition2) => definition2.promptAppend);
|
|
78569
78507
|
CATEGORY_DESCRIPTIONS = buildCategoryRecord((definition2) => definition2.description);
|
|
78508
|
+
CATEGORY_CALLER_GUIDANCE = buildCategoryRecord((definition2) => definition2.callerGuidance);
|
|
78570
78509
|
CATEGORY_PROMPT_APPEND_RESOLVERS = Object.fromEntries(BUILTIN_CATEGORIES.filter((definition2) => definition2.resolvePromptAppend !== undefined).map((definition2) => [definition2.name, definition2.resolvePromptAppend]));
|
|
78571
78510
|
BUILTIN_CATEGORY_REQUIRES_MODEL = Object.fromEntries(BUILTIN_CATEGORIES.filter((definition2) => definition2.requiresModel !== undefined).map((definition2) => [definition2.name, definition2.requiresModel]));
|
|
78572
78511
|
});
|
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",
|
|
@@ -181,7 +181,7 @@
|
|
|
181
181
|
"zod": "^4.4.3"
|
|
182
182
|
},
|
|
183
183
|
"devDependencies": {
|
|
184
|
-
"@code-yeongyu/senpi": "2026.8.18-
|
|
184
|
+
"@code-yeongyu/senpi": "2026.8.18-3",
|
|
185
185
|
"@oh-my-opencode/agents-md-core": "workspace:*",
|
|
186
186
|
"@oh-my-opencode/ast-grep-mcp": "workspace:*",
|
|
187
187
|
"@oh-my-opencode/boulder-state": "workspace:*",
|
|
@@ -222,18 +222,18 @@
|
|
|
222
222
|
"typescript": "^7.0.2"
|
|
223
223
|
},
|
|
224
224
|
"optionalDependencies": {
|
|
225
|
-
"oh-my-opencode-darwin-arm64": "5.0.0-beta.
|
|
226
|
-
"oh-my-opencode-darwin-x64": "5.0.0-beta.
|
|
227
|
-
"oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.
|
|
228
|
-
"oh-my-opencode-linux-arm64": "5.0.0-beta.
|
|
229
|
-
"oh-my-opencode-linux-arm64-musl": "5.0.0-beta.
|
|
230
|
-
"oh-my-opencode-linux-x64": "5.0.0-beta.
|
|
231
|
-
"oh-my-opencode-linux-x64-baseline": "5.0.0-beta.
|
|
232
|
-
"oh-my-opencode-linux-x64-musl": "5.0.0-beta.
|
|
233
|
-
"oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.
|
|
234
|
-
"oh-my-opencode-windows-arm64": "5.0.0-beta.
|
|
235
|
-
"oh-my-opencode-windows-x64": "5.0.0-beta.
|
|
236
|
-
"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"
|
|
237
237
|
},
|
|
238
238
|
"overrides": {
|
|
239
239
|
"@earendil-works/pi-agent-core": "0.84.2",
|
|
@@ -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
|
+
});
|
|
@@ -36,17 +36,17 @@ describe("LSP client path confinement", () => {
|
|
|
36
36
|
expect(workspace).toBe(realpathSync(root));
|
|
37
37
|
});
|
|
38
38
|
|
|
39
|
-
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", () => {
|
|
40
40
|
const root = tempRoot("lsp-client-wrapper-cwd-");
|
|
41
41
|
const outside = tempRoot("lsp-client-wrapper-outside-");
|
|
42
42
|
mkdirSync(join(outside, ".git"), { recursive: true });
|
|
43
43
|
writeFileSync(join(outside, "file.ts"), "export const outside = true;\n");
|
|
44
44
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
).
|
|
45
|
+
const workspace = runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
|
|
46
|
+
findWorkspaceRoot(join(outside, "file.ts")),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(workspace).toBe(realpathSync(outside));
|
|
50
50
|
});
|
|
51
51
|
|
|
52
52
|
it("#given a symlink inside cwd that points outside #when resolving read path #then keeps lexical path and cwd root", () => {
|
|
@@ -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 {
|
|
@@ -39,6 +39,8 @@ export function findWorkspaceRoot(filePath: string): string {
|
|
|
39
39
|
dir = dirname(dir);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
if (!isPathInside(cwd, abs)) return findWorkspaceRootOutsideContext(dir);
|
|
43
|
+
|
|
42
44
|
const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
|
|
43
45
|
while (isPathInside(cwd, dir)) {
|
|
44
46
|
const canonicalDir = existingDirectoryInsideContext(dir, cwd);
|
|
@@ -64,10 +66,7 @@ export function resolveReadablePathInsideContext(filePath: string): string {
|
|
|
64
66
|
const rebased = rebaseThroughCanonicalAncestor(abs, cwd);
|
|
65
67
|
if (rebased !== undefined) return rebased;
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
if (isPathInside(cwd, canonical)) return canonical;
|
|
69
|
-
|
|
70
|
-
throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
|
|
69
|
+
return canonicalizeExistingOrNearestAncestor(abs);
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
export function resolvePathInsideContext(filePath: string): string {
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const WORKSPACE_MARKERS = [".git", "package.json", "pyproject.toml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle"] as const;
|