oh-my-opencode 5.0.0-beta.4 → 5.0.0-beta.6

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.
Files changed (86) hide show
  1. package/dist/cli/index.js +496 -377
  2. package/dist/cli-node/index.js +496 -377
  3. package/dist/index.js +1136 -1104
  4. package/dist/skills/coding-agent-sessions/SKILL.md +3 -2
  5. package/dist/skills/coding-agent-sessions/references/all-platforms.md +1 -1
  6. package/dist/skills/coding-agent-sessions/references/senpi.md +4 -4
  7. package/dist/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +1 -1
  8. package/dist/tui.js +20 -1
  9. package/package.json +15 -14
  10. package/packages/lsp-core/src/lsp/client-wrapper.test.ts +60 -7
  11. package/packages/lsp-core/src/lsp/client-wrapper.ts +69 -16
  12. package/packages/lsp-core/src/lsp/workspace-edit-adversarial.test.ts +20 -1
  13. package/packages/lsp-core/src/tools/diagnostics.ts +3 -3
  14. package/packages/lsp-core/src/tools/navigation.ts +4 -2
  15. package/packages/lsp-core/src/tools/rename.ts +4 -2
  16. package/packages/lsp-core/src/tools/symbols.ts +1 -1
  17. package/packages/lsp-daemon/dist/cli.js +83 -29
  18. package/packages/lsp-daemon/dist/client.js +76 -22
  19. package/packages/lsp-daemon/dist/index.js +81 -27
  20. package/packages/lsp-tools-mcp/dist/cli.js +76 -22
  21. package/packages/lsp-tools-mcp/dist/mcp.js +76 -22
  22. package/packages/lsp-tools-mcp/dist/tools.js +76 -22
  23. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  24. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  25. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/codegraph/dist/cli.js +14 -1
  27. package/packages/omo-codex/plugin/components/codegraph/dist/serve.js +14 -1
  28. package/packages/omo-codex/plugin/components/codegraph/package.json +1 -1
  29. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  30. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  31. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  32. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  33. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  34. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  35. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +3 -3
  36. package/packages/omo-codex/plugin/components/lsp/dist/cli.js +83 -29
  37. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  38. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  39. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  40. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json +2 -2
  42. package/packages/omo-codex/plugin/components/start-work-continuation/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/telemetry/dist/cli.js +24 -12
  46. package/packages/omo-codex/plugin/components/telemetry/dist/posthog.js +24 -12
  47. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  48. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  49. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  50. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  51. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +4 -4
  52. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-codegraph-init-guidance.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/session-start-checking-codegraph-bootstrap.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/stop-checking-start-work-continuation.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  71. package/packages/omo-codex/plugin/hooks/subagent-stop-checking-start-work-continuation.json +1 -1
  72. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  73. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  76. package/packages/omo-codex/plugin/package-lock.json +13 -13
  77. package/packages/omo-codex/plugin/package.json +1 -1
  78. package/packages/omo-codex/plugin/skills/coding-agent-sessions/SKILL.md +3 -2
  79. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/all-platforms.md +1 -1
  80. package/packages/omo-codex/plugin/skills/coding-agent-sessions/references/senpi.md +4 -4
  81. package/packages/omo-codex/plugin/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +1 -1
  82. package/packages/omo-codex/scripts/install-dist/install-local.mjs +39 -16
  83. package/packages/shared-skills/skills/coding-agent-sessions/SKILL.md +3 -2
  84. package/packages/shared-skills/skills/coding-agent-sessions/references/all-platforms.md +1 -1
  85. package/packages/shared-skills/skills/coding-agent-sessions/references/senpi.md +4 -4
  86. package/packages/shared-skills/skills/coding-agent-sessions/scripts/agent_sessions/pi_family.py +1 -1
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: coding-agent-sessions
3
- description: "MUST USE when asked to find, read, list, search, inspect, fetch, export, or reconstruct coding-agent sessions across Codex, Claude Code/Desktop, OpenCode, Senpi/pi, oh-my-pi (omp), gajae-code (gjc), OpenClaw, Factory Droid, Amp, Gemini/Kimi/Qwen CLIs, Codebuff, Roo/Kilo/Cline, Kodu, Cursor CLI, Aider, Aside browser-agent sessions, or unknown local agent logs. Covers transcripts, session IDs, rollout JSONL, state SQLite, Claude projects/pre-compact histories, OpenCode messages/parts, child/subagent linkage, cwd/model/time/token filters, archives, and cost clues. Expands fuzzy recall into parallel query lanes and first probes known stores so absent platforms are skipped cheaply. Triggers: coding agent sessions, Codex/Claude/OpenCode/Senpi/pi/oh-my-pi/omp/gajae-code/gjc/OpenClaw/Droid/Amp/Kodu/Cursor/Aider/Aside sessions, transcript search, session history, session ID, read transcript, token usage, subagent sessions, what did I do yesterday, did we already do this."
3
+ description: "MUST USE when asked to find, read, list, search, inspect, fetch, export, or reconstruct coding-agent sessions across Codex, Claude Code/Desktop, OpenCode, OMO/Senpi/pi, oh-my-pi (omp), gajae-code (gjc), OpenClaw, Factory Droid, Amp, Gemini/Kimi/Qwen CLIs, Codebuff, Roo/Kilo/Cline, Kodu, Cursor CLI, Aider, Aside browser-agent sessions, or unknown local agent logs. Covers transcripts, session IDs, rollout JSONL, state SQLite, Claude projects/pre-compact histories, OpenCode messages/parts, child/subagent linkage, cwd/model/time/token filters, archives, and cost clues. Expands fuzzy recall into parallel query lanes and first probes known stores so absent platforms are skipped cheaply. Triggers: coding agent sessions, Codex/Claude/OpenCode/OMO/Senpi/pi/oh-my-pi/omp/gajae-code/gjc/OpenClaw/Droid/Amp/Kodu/Cursor/Aider/Aside sessions, transcript search, session history, session ID, read transcript, token usage, subagent sessions, what did I do yesterday, did we already do this."
4
4
  ---
5
5
 
6
6
  # Coding Agent Sessions
@@ -15,7 +15,7 @@ Find local coding-agent sessions across agent products before answering from mem
15
15
  |---|---|
16
16
  | Codex / OpenAI Codex CLI | `references/codex.md` |
17
17
  | Claude Code / Claude Desktop histories | `references/claude.md` |
18
- | Senpi / pi coding-agent logs | `references/senpi.md` |
18
+ | OMO / Senpi / pi coding-agent logs | `references/senpi.md` |
19
19
  | oh-my-pi (`omp`, `~/.omp`) and gajae-code (`gjc`, `~/.gjc`) logs | `references/senpi.md` |
20
20
  | OpenCode / oh-my-openagent (formerly oh-my-opencode) storage | `references/opencode.md` |
21
21
  | OpenClaw, Droid, Amp, Gemini, Kimi, Qwen, Codebuff, Roo/Kilo/Cline, Kodu, Cursor CLI, Aider, Kiro, Goose, Hermes, Crush, Zed, Aside | `references/all-platforms.md` |
@@ -120,6 +120,7 @@ Use `references/codex.md` for Codex storage details.
120
120
  |---|---|
121
121
  | Missing Codex sessions | Set `CODEX_HOME` or pass `--root /path/to/.codex`. |
122
122
  | Missing oh-my-pi / gajae-code sessions | Those stores live in `~/.omp/agent/sessions` and `~/.gjc/agent/sessions`. For a custom `PI_CONFIG_DIR` / `PI_CODING_AGENT_DIR`, pass that agent dir with `--root`. |
123
+ | Missing OMO / Senpi sessions | The `senpi` platform searches `~/.omo/agent/sessions`, `~/.senpi/agent/sessions`, and `~/.pi/agent/sessions` (plus matching profile roots). Pass a nonstandard agent directory with `--root`. |
123
124
  | Missing OpenCode sessions | Pass the data dir that contains `messages/` and `parts/`, often `~/.opencode` or `~/.local/share/opencode`. |
124
125
  | Missing Claude sessions | Search `~/.claude/projects`, `~/.claude/transcripts`, and `~/.claude/pre-compact-session-histories`; use `--root` for nonstandard config dirs. |
125
126
  | Missing Aside sessions | The Aside browser agent stores per-user data under `~/.aside/u/<n>/` (`sessions/<date>_<id>/messages.jsonl` transcripts + a `state.db` index; `agents/*/sessions/` is just a hardlink mirror). Pass `--root` for a nonstandard `.aside` dir or an exported user dir. |
@@ -10,7 +10,7 @@ Registered platform keys: `codex`, `claude`, `senpi`, `oh-my-pi`, `gajae-code`,
10
10
  |---|---|---|
11
11
  | Codex | `$CODEX_HOME`, `~/.codex` | `%CODEX_HOME%`, `%USERPROFILE%\.codex` |
12
12
  | Claude | `~/.claude` | `%USERPROFILE%\.claude`, `%APPDATA%\Claude` |
13
- | Senpi / pi | `~/.senpi/agent`, `~/.pi/agent` | `%USERPROFILE%\.senpi\agent`, `%USERPROFILE%\.pi\agent` |
13
+ | OMO / Senpi / pi | `~/.omo/agent`, `~/.senpi/agent`, `~/.pi/agent` | `%USERPROFILE%\.omo\agent`, `%USERPROFILE%\.senpi\agent`, `%USERPROFILE%\.pi\agent` |
14
14
  | oh-my-pi (`omp`) | `~/.omp/agent`, `~/.omp/profiles/*/agent`, `$XDG_DATA_HOME/omp` | `%USERPROFILE%\.omp\agent` |
15
15
  | gajae-code (`gjc`) | `~/.gjc/agent`, `~/.gjc/profiles/*/agent`, `$XDG_DATA_HOME/gjc` | `%USERPROFILE%\.gjc\agent` |
16
16
  | OpenCode | `$OPENCODE_HOME`, `~/.opencode`, `~/.local/share/opencode` | `%OPENCODE_HOME%`, `%APPDATA%\opencode`, `%USERPROFILE%\.opencode` |
@@ -1,10 +1,10 @@
1
- # Senpi / pi Family Coding-Agent Sessions
1
+ # OMO / Senpi / pi Family Coding-Agent Sessions
2
2
 
3
- The pi family (Senpi, oh-my-pi, gajae-code) shares one session format, so one scanner serves all three under separate platform keys.
3
+ The pi family (OMO/Senpi, oh-my-pi, gajae-code) shares one session format, so one scanner serves all three under separate platform keys.
4
4
 
5
5
  | Platform key | Aliases | Config root | Sessions |
6
6
  |---|---|---|---|
7
- | `senpi` | - | `~/.senpi`, `~/.pi` | `<root>/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl` |
7
+ | `senpi` | - | `~/.omo`, `~/.senpi`, `~/.pi` | `<root>/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl` |
8
8
  | `oh-my-pi` | `omp`, `ohmypi` | `~/.omp` | `~/.omp/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl` |
9
9
  | `gajae-code` | `gjc`, `gajae` | `~/.gjc` | `~/.gjc/agent/sessions/<encoded-cwd>/<timestamp>_<uuid>.jsonl` |
10
10
 
@@ -14,7 +14,7 @@ Extra roots scanned for every pi-family platform:
14
14
  - XDG stores (macOS/Linux, default profile): `$XDG_DATA_HOME/<app>/sessions/**` and `$XDG_DATA_HOME/<app>/profiles/<profile>/sessions/**`, where `<app>` is `senpi`, `omp`, or `gjc`. XDG flattens the `agent/` path segment.
15
15
  - A custom `PI_CONFIG_DIR` / `PI_CODING_AGENT_DIR` / `GJC_CONFIG_DIR` store: pass that agent directory with `--root`.
16
16
 
17
- `~/.senpi/agent/settings.json`, `models.json`, and `auth.json` provide environment context; oh-my-pi and gajae-code keep the same files plus `config.yml` and `models.yml` next to their sessions directory.
17
+ `~/.omo/agent` and `~/.senpi/agent` are the current and legacy OMO/Senpi stores. Their `settings.json`, `models.json`, and `auth.json` files provide environment context; oh-my-pi and gajae-code keep the same files plus `config.yml` and `models.yml` next to their sessions directory.
18
18
 
19
19
  Common event types:
20
20
 
@@ -5,7 +5,7 @@ from pathlib import Path
5
5
  from .transcript import env_path, existing, jsonl_parallel, recent, stem_id
6
6
  from .types import Session
7
7
 
8
- SENPI_CONFIG_DIRS = (".senpi", ".pi")
8
+ SENPI_CONFIG_DIRS = (".omo", ".senpi", ".pi")
9
9
  OH_MY_PI_CONFIG_DIRS = (".omp",)
10
10
  GAJAE_CODE_CONFIG_DIRS = (".gjc",)
11
11
 
package/dist/tui.js CHANGED
@@ -19060,6 +19060,19 @@ var init_team = __esm(() => {
19060
19060
  OmoTeamsConfigLayerSchema = record(string2(), OmoTeamSpecLayerSchema);
19061
19061
  });
19062
19062
 
19063
+ // packages/omo-config-core/src/schema/telemetry.ts
19064
+ var OmoTelemetrySettingsShape, OmoTelemetrySettingsLayerSchema, OmoTelemetrySettingsSchema;
19065
+ var init_telemetry = __esm(() => {
19066
+ init_zod();
19067
+ OmoTelemetrySettingsShape = {
19068
+ enabled: boolean2()
19069
+ };
19070
+ OmoTelemetrySettingsLayerSchema = object(OmoTelemetrySettingsShape).partial().strict();
19071
+ OmoTelemetrySettingsSchema = OmoTelemetrySettingsLayerSchema.extend({
19072
+ enabled: boolean2().default(true)
19073
+ }).strict();
19074
+ });
19075
+
19063
19076
  // packages/omo-config-core/src/schema/config.ts
19064
19077
  var OmoOpenCodeHarnessConfigSchema, OmoTypedHarnessConfigSchema, OmoConfigProfileSchema, OmoConfigSchema, OmoConfigLayerSchema;
19065
19078
  var init_config = __esm(() => {
@@ -19071,6 +19084,7 @@ var init_config = __esm(() => {
19071
19084
  init_model_catalog();
19072
19085
  init_task();
19073
19086
  init_team();
19087
+ init_telemetry();
19074
19088
  OmoOpenCodeHarnessConfigSchema = record(string2(), unknown());
19075
19089
  OmoTypedHarnessConfigSchema = object({
19076
19090
  categories: OmoCategoriesConfigSchema.optional(),
@@ -19079,7 +19093,8 @@ var init_config = __esm(() => {
19079
19093
  task: OmoTaskSettingsLayerSchema.optional(),
19080
19094
  teams: OmoTeamsConfigLayerSchema.optional(),
19081
19095
  models: OmoModelCatalogLayerSchema.optional(),
19082
- memory: OmoMemorySettingsLayerSchema.optional()
19096
+ memory: OmoMemorySettingsLayerSchema.optional(),
19097
+ telemetry: OmoTelemetrySettingsLayerSchema.optional()
19083
19098
  }).strict();
19084
19099
  OmoConfigProfileSchema = object({
19085
19100
  categories: OmoCategoriesConfigSchema.optional(),
@@ -19089,6 +19104,7 @@ var init_config = __esm(() => {
19089
19104
  teams: OmoTeamsConfigLayerSchema.optional(),
19090
19105
  models: OmoModelCatalogLayerSchema.optional(),
19091
19106
  memory: OmoMemorySettingsLayerSchema.optional(),
19107
+ telemetry: OmoTelemetrySettingsLayerSchema.optional(),
19092
19108
  "[opencode]": OmoOpenCodeHarnessConfigSchema.optional(),
19093
19109
  "[senpi]": OmoTypedHarnessConfigSchema.optional(),
19094
19110
  "[codex]": OmoTypedHarnessConfigSchema.optional()
@@ -19102,6 +19118,7 @@ var init_config = __esm(() => {
19102
19118
  teams: OmoTeamsConfigSchema.optional(),
19103
19119
  models: OmoModelCatalogSchema.optional(),
19104
19120
  memory: OmoMemorySettingsSchema.optional(),
19121
+ telemetry: OmoTelemetrySettingsSchema.optional(),
19105
19122
  "[opencode]": OmoOpenCodeHarnessConfigSchema.optional(),
19106
19123
  "[senpi]": OmoTypedHarnessConfigSchema.optional(),
19107
19124
  "[codex]": OmoTypedHarnessConfigSchema.optional(),
@@ -19118,6 +19135,7 @@ var init_config = __esm(() => {
19118
19135
  teams: OmoTeamsConfigLayerSchema.optional(),
19119
19136
  models: OmoModelCatalogLayerSchema.optional(),
19120
19137
  memory: OmoMemorySettingsLayerSchema.optional(),
19138
+ telemetry: OmoTelemetrySettingsLayerSchema.optional(),
19121
19139
  "[opencode]": OmoOpenCodeHarnessConfigSchema.optional(),
19122
19140
  "[senpi]": OmoTypedHarnessConfigSchema.optional(),
19123
19141
  "[codex]": OmoTypedHarnessConfigSchema.optional(),
@@ -19140,6 +19158,7 @@ var init_schema = __esm(() => {
19140
19158
  init_model_ref();
19141
19159
  init_task();
19142
19160
  init_team();
19161
+ init_telemetry();
19143
19162
  });
19144
19163
 
19145
19164
  // packages/omo-config-core/src/loader/merge.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-opencode",
3
- "version": "5.0.0-beta.4",
3
+ "version": "5.0.0-beta.6",
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",
@@ -180,7 +180,7 @@
180
180
  "zod": "^4.4.3"
181
181
  },
182
182
  "devDependencies": {
183
- "@code-yeongyu/senpi": "2026.8.11",
183
+ "@code-yeongyu/senpi": "2026.8.11-4",
184
184
  "@oh-my-opencode/agents-md-core": "workspace:*",
185
185
  "@oh-my-opencode/ast-grep-mcp": "workspace:*",
186
186
  "@oh-my-opencode/boulder-state": "workspace:*",
@@ -196,6 +196,7 @@
196
196
  "@oh-my-opencode/model-core": "workspace:*",
197
197
  "@oh-my-opencode/omo-codex": "workspace:*",
198
198
  "@oh-my-opencode/omo-config-core": "workspace:*",
199
+ "@oh-my-opencode/omo-opencode": "workspace:*",
199
200
  "@oh-my-opencode/omo-senpi": "workspace:*",
200
201
  "@oh-my-opencode/openclaw-core": "workspace:*",
201
202
  "@oh-my-opencode/pi-goal": "workspace:*",
@@ -220,18 +221,18 @@
220
221
  "typescript": "^7.0.2"
221
222
  },
222
223
  "optionalDependencies": {
223
- "oh-my-opencode-darwin-arm64": "5.0.0-beta.4",
224
- "oh-my-opencode-darwin-x64": "5.0.0-beta.4",
225
- "oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.4",
226
- "oh-my-opencode-linux-arm64": "5.0.0-beta.4",
227
- "oh-my-opencode-linux-arm64-musl": "5.0.0-beta.4",
228
- "oh-my-opencode-linux-x64": "5.0.0-beta.4",
229
- "oh-my-opencode-linux-x64-baseline": "5.0.0-beta.4",
230
- "oh-my-opencode-linux-x64-musl": "5.0.0-beta.4",
231
- "oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.4",
232
- "oh-my-opencode-windows-arm64": "5.0.0-beta.4",
233
- "oh-my-opencode-windows-x64": "5.0.0-beta.4",
234
- "oh-my-opencode-windows-x64-baseline": "5.0.0-beta.4"
224
+ "oh-my-opencode-darwin-arm64": "5.0.0-beta.6",
225
+ "oh-my-opencode-darwin-x64": "5.0.0-beta.6",
226
+ "oh-my-opencode-darwin-x64-baseline": "5.0.0-beta.6",
227
+ "oh-my-opencode-linux-arm64": "5.0.0-beta.6",
228
+ "oh-my-opencode-linux-arm64-musl": "5.0.0-beta.6",
229
+ "oh-my-opencode-linux-x64": "5.0.0-beta.6",
230
+ "oh-my-opencode-linux-x64-baseline": "5.0.0-beta.6",
231
+ "oh-my-opencode-linux-x64-musl": "5.0.0-beta.6",
232
+ "oh-my-opencode-linux-x64-musl-baseline": "5.0.0-beta.6",
233
+ "oh-my-opencode-windows-arm64": "5.0.0-beta.6",
234
+ "oh-my-opencode-windows-x64": "5.0.0-beta.6",
235
+ "oh-my-opencode-windows-x64-baseline": "5.0.0-beta.6"
235
236
  },
236
237
  "overrides": {
237
238
  "@earendil-works/pi-agent-core": "0.80.3",
@@ -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, resolvePathInsideContext } from "./client-wrapper.js";
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
 
@@ -48,16 +49,68 @@ describe("LSP client path confinement", () => {
48
49
  ).toThrow(LspInvalidPathError);
49
50
  });
50
51
 
51
- it("#given a symlink inside cwd that points outside #when resolving #then rejects the escape", () => {
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 });
54
57
  writeFileSync(join(outside, "file.ts"), "export const outside = true;\n");
55
58
  symlinkSync(outside, join(root, "linked"));
56
59
 
57
- expect(() =>
58
- runWithRequestContext(createStandaloneMcpRequestContext({ cwd: root }), () =>
59
- resolvePathInsideContext("linked/file.ts"),
60
- ),
61
- ).toThrow(LspInvalidPathError);
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-");
95
+ writeFileSync(join(outside, "file.ts"), "export const outside = true;\n");
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
+ });
104
+
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,
@@ -31,30 +31,48 @@ export function isDirectoryPath(filePath: string): boolean {
31
31
  }
32
32
 
33
33
  export function findWorkspaceRoot(filePath: string): string {
34
- const abs = resolvePathInsideContext(filePath);
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
- let prevDir = "";
42
- while (dir !== prevDir) {
43
- for (const marker of WORKSPACE_MARKERS) {
44
- if (existsSync(join(dir, marker))) {
45
- return dir;
42
+ const fallbackRoot = nearestExistingDirectoryInsideContext(dir, cwd) ?? cwd;
43
+ while (isPathInside(cwd, dir)) {
44
+ const canonicalDir = existingDirectoryInsideContext(dir, cwd);
45
+ if (canonicalDir !== undefined) {
46
+ for (const marker of WORKSPACE_MARKERS) {
47
+ if (existsSync(join(dir, marker))) {
48
+ return canonicalDir;
49
+ }
46
50
  }
47
51
  }
48
- prevDir = dir;
52
+ if (dir === cwd) break;
49
53
  dir = dirname(dir);
50
54
  }
51
55
 
52
- return dirname(abs);
56
+ return fallbackRoot;
53
57
  }
54
58
 
55
- export function resolvePathInsideContext(filePath: string): string {
59
+ export function resolveReadablePathInsideContext(filePath: string): string {
56
60
  const cwd = contextCwd();
57
61
  const abs = resolve(cwd, filePath);
62
+ if (isPathInside(cwd, abs)) return abs;
63
+
64
+ const rebased = rebaseThroughCanonicalAncestor(abs, cwd);
65
+ if (rebased !== undefined) return rebased;
66
+
67
+ const canonical = canonicalizeExistingOrNearestAncestor(abs);
68
+ if (isPathInside(cwd, canonical)) return canonical;
69
+
70
+ throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
71
+ }
72
+
73
+ export function resolvePathInsideContext(filePath: string): string {
74
+ const cwd = contextCwd();
75
+ const abs = resolveReadablePathInsideContext(filePath);
58
76
  const canonical = canonicalizeExistingOrNearestAncestor(abs);
59
77
  if (!isPathInside(cwd, canonical)) {
60
78
  throw new LspInvalidPathError(`LSP file path must be inside request cwd: ${filePath}`);
@@ -62,6 +80,39 @@ export function resolvePathInsideContext(filePath: string): string {
62
80
  return canonical;
63
81
  }
64
82
 
83
+ function rebaseThroughCanonicalAncestor(path: string, cwd: string): string | undefined {
84
+ let current = path;
85
+ const suffix: string[] = [];
86
+ while (true) {
87
+ if (existsSync(current)) {
88
+ const canonical = realpathSync(current);
89
+ if (isPathInside(cwd, canonical)) return suffix.length === 0 ? canonical : join(canonical, ...suffix);
90
+ }
91
+ const parent = dirname(current);
92
+ if (parent === current) return undefined;
93
+ suffix.unshift(basename(current));
94
+ current = parent;
95
+ }
96
+ }
97
+
98
+ function existingDirectoryInsideContext(directory: string, cwd: string): string | undefined {
99
+ if (!existsSync(directory)) return undefined;
100
+ const canonical = realpathSync(directory);
101
+ if (!statSync(canonical).isDirectory()) return undefined;
102
+ return isPathInside(cwd, canonical) ? canonical : undefined;
103
+ }
104
+
105
+ function nearestExistingDirectoryInsideContext(directory: string, cwd: string): string | undefined {
106
+ let current = directory;
107
+ while (isPathInside(cwd, current)) {
108
+ const canonical = existingDirectoryInsideContext(current, cwd);
109
+ if (canonical !== undefined) return canonical;
110
+ if (current === cwd) return undefined;
111
+ current = dirname(current);
112
+ }
113
+ return undefined;
114
+ }
115
+
65
116
  export function formatServerLookupError(result: Exclude<ServerLookupResult, { status: "found" }>): string {
66
117
  if (result.status === "not_installed") {
67
118
  return formatNotInstalled(result);
@@ -143,7 +194,7 @@ export interface WithLspClientOptions {
143
194
  manager?: LspManager;
144
195
  }
145
196
 
146
- const READ_ONLY_RETRY_TOOLS = new Set([
197
+ const READ_ONLY_TOOLS = new Set([
147
198
  "diagnostics",
148
199
  "definition",
149
200
  "references",
@@ -154,11 +205,13 @@ const READ_ONLY_RETRY_TOOLS = new Set([
154
205
 
155
206
  export async function withLspClient<T>(
156
207
  filePath: string,
157
- fn: (client: LspClient, workspaceRoot: string) => Promise<T>,
208
+ fn: (client: LspClient, workspaceRoot: string, resolvedFilePath: string) => Promise<T>,
158
209
  toolName: string,
159
210
  options: WithLspClientOptions = {},
160
211
  ): Promise<T> {
161
- const absPath = resolvePathInsideContext(filePath);
212
+ const absPath = READ_ONLY_TOOLS.has(toolName)
213
+ ? resolveReadablePathInsideContext(filePath)
214
+ : resolvePathInsideContext(filePath);
162
215
 
163
216
  if (isDirectoryPath(absPath)) {
164
217
  throw new LspInvalidPathError(
@@ -181,9 +234,9 @@ export async function withLspClient<T>(
181
234
  const client = await manager.getClient(root, server, options.signal);
182
235
 
183
236
  try {
184
- return await fn(client, root);
237
+ return await fn(client, root, absPath);
185
238
  } catch (err) {
186
- if (allowRetry && READ_ONLY_RETRY_TOOLS.has(toolName) && isLspDeadConnectionError(err)) {
239
+ if (allowRetry && READ_ONLY_TOOLS.has(toolName) && isLspDeadConnectionError(err)) {
187
240
  manager.invalidateClient(root, server.id, client);
188
241
  return acquireAndCall(false);
189
242
  }
@@ -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 = [
@@ -1,4 +1,4 @@
1
- import { isDirectoryPath, resolvePathInsideContext, withLspClient } from "../lsp/client-wrapper.js";
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 = resolvePathInsideContext(filePath);
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(filePath, signal),
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) => client.definition(filePath, line, character, signal),
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) => client.references(filePath, line, character, includeDeclaration, signal),
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) => client.prepareRename(filePath, line, character, signal),
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) => client.rename(filePath, line, character, newName, signal),
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(filePath, signal),
50
+ async (client, _workspaceRoot, resolvedFilePath) => client.documentSymbols(resolvedFilePath, signal),
51
51
  "documentSymbols",
52
52
  clientOptions(signal),
53
53
  );