one-code-extension 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The workspace directories in force when a session starts, announced by the
|
|
3
|
+
* permissions extension over `pi.events` (jiti isolation, findings §3) so the
|
|
4
|
+
* system prompt can list them the way Claude Code's environment block does
|
|
5
|
+
* (` - Additional working directories:`). It is emitted once, at
|
|
6
|
+
* session_start: the system prompt stays byte-stable for the session, and a
|
|
7
|
+
* directory added later reaches the model through the /permissions or
|
|
8
|
+
* /add-dir command output instead.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const WORKSPACE_CHANNEL = "one-code:workspace-dirs";
|
|
12
|
+
|
|
13
|
+
export interface WorkspaceAnnouncement {
|
|
14
|
+
/** Absolute, resolved paths, in the order the gate applies them. */
|
|
15
|
+
dirs: string[];
|
|
16
|
+
}
|
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
* worktree extension emits on every state change — enter, exit, and the
|
|
5
5
|
* session_start/session_tree restore; the footer shows the worktree's path
|
|
6
6
|
* and branch in place of the process cwd while one is active (Claude Code's
|
|
7
|
-
* footer does the same)
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* footer does the same), cron fires and monitors run there, and subagents
|
|
8
|
+
* and workflow agents are spawned there (`sessionWorkCwd`). The bus does not
|
|
9
|
+
* replay: a late subscriber sees the next change, which is fine here — every
|
|
10
|
+
* subscriber subscribes at load, and the first emit is the session_start
|
|
11
|
+
* restore.
|
|
10
12
|
*/
|
|
11
13
|
|
|
12
14
|
export const WORKTREE_CHANNEL = "one-code:worktree";
|
|
@@ -15,4 +17,30 @@ export const WORKTREE_CHANNEL = "one-code:worktree";
|
|
|
15
17
|
export interface WorktreeLocation {
|
|
16
18
|
path: string;
|
|
17
19
|
branch?: string;
|
|
20
|
+
/** Root of the shared checkout the worktree belongs to, for the child sessions' git-isolation guard. */
|
|
21
|
+
sharedRoot?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Where the session's work happens: the entered worktree while one is active,
|
|
26
|
+
* else the session's own cwd. A removed worktree must fail at that path, never
|
|
27
|
+
* silently redirect work into the original checkout. The worktree extension
|
|
28
|
+
* announces a missing path on restore and blocks calls until an active missing
|
|
29
|
+
* worktree is explicitly left. pi fixes a session's cwd at creation, so a child
|
|
30
|
+
* spawned from `ctx.cwd` would work in the checkout the user meant to protect.
|
|
31
|
+
*/
|
|
32
|
+
export function sessionWorkCwd(entered: WorktreeLocation | null | undefined, sessionCwd: string): string {
|
|
33
|
+
return entered?.path || sessionCwd;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Subscribe once at load and get `sessionWorkCwd` for the latest announcement:
|
|
38
|
+
* `(ctx.cwd) => where this session's work happens now`.
|
|
39
|
+
*/
|
|
40
|
+
export function followWorkCwd(events: { on(channel: string, handler: (data: unknown) => void): unknown }): (sessionCwd: string) => string {
|
|
41
|
+
let entered: WorktreeLocation | null = null;
|
|
42
|
+
events.on(WORKTREE_CHANNEL, (data) => {
|
|
43
|
+
entered = data as WorktreeLocation | null;
|
|
44
|
+
});
|
|
45
|
+
return (sessionCwd) => sessionWorkCwd(entered, sessionCwd);
|
|
18
46
|
}
|
|
@@ -6,11 +6,14 @@
|
|
|
6
6
|
*
|
|
7
7
|
* The registry exists because the association can only resolve at call time:
|
|
8
8
|
* these worktrees live in tmpdirs (not statically recognizable paths), and the
|
|
9
|
-
*
|
|
9
|
+
* guard judges each call's `ctx.cwd`, not the cwd its child's loader was built
|
|
10
|
+
* with (one loader per child session, lib/agent-loader.ts openChildSession).
|
|
10
11
|
* createWorktree/cleanupWorktree (subagents/worktree.ts) register and release
|
|
11
12
|
* entries; reconstructed run records re-register kept worktrees after a
|
|
12
|
-
* process restart (subagents/index.ts); the
|
|
13
|
-
*
|
|
13
|
+
* process restart (subagents/index.ts); the worktree the main session entered
|
|
14
|
+
* with `enter_worktree` is registered too (`followEnteredWorktree`), so a child
|
|
15
|
+
* spawned there is guarded like an isolated one; the guard resolves each bash
|
|
16
|
+
* call's cwd against them.
|
|
14
17
|
*
|
|
15
18
|
* Module state is deliberate and safe despite jiti isolation: registration
|
|
16
19
|
* (worktree creation) and lookup (the guard factory) always happen inside the
|
|
@@ -22,9 +25,11 @@
|
|
|
22
25
|
import { homedir } from "node:os";
|
|
23
26
|
import { isAbsolute, join, relative } from "node:path";
|
|
24
27
|
import type { InlineExtension } from "@earendil-works/pi-coding-agent";
|
|
28
|
+
import { findProjectRoot, repositoryWorktrees } from "./git.ts";
|
|
29
|
+
import { WORKTREE_CHANNEL, type WorktreeLocation } from "./worktree-channel.ts";
|
|
25
30
|
import { isWithin, isWritingTool, pathArgument, resolveForContainment, toAbsolute } from "../auto-mode/paths.ts";
|
|
26
31
|
import { analyzeShellCommand } from "../auto-mode/shell-analysis.ts";
|
|
27
|
-
import { worktreeBashGuardReason } from "../worktree/guards.ts";
|
|
32
|
+
import { namesGit, unlistedWorktreesReason, worktreeBashGuardReason } from "../worktree/guards.ts";
|
|
28
33
|
|
|
29
34
|
export interface WorktreeIsolation {
|
|
30
35
|
/** The isolated worktree the run works in. */
|
|
@@ -61,11 +66,35 @@ export function releaseWorktreeIsolation(worktreePath: string): void {
|
|
|
61
66
|
active.delete(worktreePath);
|
|
62
67
|
}
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Follow the main session's `enter_worktree` state over `WORKTREE_CHANNEL`
|
|
71
|
+
* and return a getter for it. The entered worktree is registered in this
|
|
72
|
+
* extension's registry, so a child spawned there gets the git-isolation and
|
|
73
|
+
* write guards. The entry stays after the session exits the worktree: a child
|
|
74
|
+
* started there may still be running, and a registration for a directory no
|
|
75
|
+
* later child runs in is inert.
|
|
76
|
+
*/
|
|
77
|
+
export function followEnteredWorktree(events: { on(channel: string, handler: (data: unknown) => void): unknown }): () => WorktreeLocation | undefined {
|
|
78
|
+
let entered: WorktreeLocation | undefined;
|
|
79
|
+
events.on(WORKTREE_CHANNEL, (data) => {
|
|
80
|
+
const location = data as WorktreeLocation | null | undefined;
|
|
81
|
+
entered = location?.path ? location : undefined;
|
|
82
|
+
if (entered) registerWorktreeIsolation(entered.path, entered.sharedRoot ?? findProjectRoot(entered.path) ?? entered.path);
|
|
83
|
+
});
|
|
84
|
+
return () => entered;
|
|
85
|
+
}
|
|
86
|
+
|
|
64
87
|
/** The registered isolation worktree `cwd` lives in, if any. */
|
|
65
88
|
export function worktreeIsolationFor(cwd: string): WorktreeIsolation | undefined {
|
|
66
89
|
for (const isolation of active.values()) {
|
|
67
90
|
if (isWithin(isolation.worktreePath, cwd)) return isolation;
|
|
68
91
|
}
|
|
92
|
+
if (active.size === 0) return undefined;
|
|
93
|
+
const resolved = resolveForContainment(cwd);
|
|
94
|
+
if (resolved === undefined) return undefined;
|
|
95
|
+
for (const isolation of active.values()) {
|
|
96
|
+
if (isWithin(isolation.resolvedWorktreePath, resolved)) return isolation;
|
|
97
|
+
}
|
|
69
98
|
return undefined;
|
|
70
99
|
}
|
|
71
100
|
|
|
@@ -79,10 +108,8 @@ export function worktreeIsolationFor(cwd: string): WorktreeIsolation | undefined
|
|
|
79
108
|
export function sharedCheckoutWriteTarget(target: string, cwd: string, isolation: WorktreeIsolation, home = homedir()): string | undefined {
|
|
80
109
|
const { worktreePath, sharedRoot, resolvedWorktreePath, resolvedSharedRoot } = isolation;
|
|
81
110
|
const absolute = toAbsolute(cwd, target, home);
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
if (isWithin(worktreePath, absolute)) return undefined;
|
|
85
|
-
// Resolved so a write is judged by where it lands, not how it is spelled.
|
|
111
|
+
// Resolve even paths spelled under the worktree: a symlink there can point
|
|
112
|
+
// back into the shared checkout.
|
|
86
113
|
const resolved = resolveForContainment(absolute) ?? absolute;
|
|
87
114
|
if (isWithin(resolvedWorktreePath, resolved) || !isWithin(resolvedSharedRoot, resolved)) return undefined;
|
|
88
115
|
// The tail keeps the model's spelling when the path was written under the
|
|
@@ -151,15 +178,18 @@ export function worktreeGuardFactory(cwd: string): InlineExtension {
|
|
|
151
178
|
name: "agent-worktree-guard",
|
|
152
179
|
hidden: true,
|
|
153
180
|
factory: (pi) => {
|
|
154
|
-
pi.on("tool_call", (event, ctx) => {
|
|
181
|
+
pi.on("tool_call", async (event, ctx) => {
|
|
155
182
|
const runCwd = ctx?.cwd ?? cwd;
|
|
156
183
|
const isolation = worktreeIsolationFor(runCwd);
|
|
157
184
|
if (!isolation) return undefined;
|
|
158
185
|
const input = (event.input ?? {}) as Record<string, unknown>;
|
|
159
186
|
if (event.toolName === "bash") {
|
|
160
187
|
if (typeof input.command !== "string") return undefined;
|
|
188
|
+
// Every worktree of the repository, as the main session's guard checks: a linked one may live outside the shared root.
|
|
189
|
+
const others = namesGit(input.command) ? await repositoryWorktrees(isolation.worktreePath) : [];
|
|
190
|
+
if (others === undefined) return { block: true, reason: unlistedWorktreesReason(isolation.worktreePath) };
|
|
161
191
|
const reason =
|
|
162
|
-
worktreeBashGuardReason({ command: input.command, worktreePath: isolation.worktreePath, sharedRoot: isolation.sharedRoot }) ??
|
|
192
|
+
worktreeBashGuardReason({ command: input.command, worktreePath: isolation.worktreePath, sharedRoot: isolation.sharedRoot, otherWorktrees: others }) ??
|
|
163
193
|
worktreeBashWriteGuardReason({ command: input.command, cwd: runCwd, isolation });
|
|
164
194
|
return reason ? { block: true, reason } : undefined;
|
|
165
195
|
}
|
package/extensions/lsp/client.ts
CHANGED
|
@@ -19,8 +19,9 @@ import { type ChildProcess, spawn } from "node:child_process";
|
|
|
19
19
|
import { readFileSync } from "node:fs";
|
|
20
20
|
import { pathToFileURL } from "node:url";
|
|
21
21
|
import type { LspDiagnostic } from "./format.ts";
|
|
22
|
+
import { childProcessEnv } from "../lib/app-launch.mjs";
|
|
22
23
|
import { commandLaunch } from "../lib/command-launch.ts";
|
|
23
|
-
import { killProcessTree } from "../lib/process-tree.ts";
|
|
24
|
+
import { killProcessTree, unrefHandle } from "../lib/process-tree.ts";
|
|
24
25
|
import { createReaderState, encodeMessage, type JsonRpcMessage, readMessages } from "./protocol.ts";
|
|
25
26
|
|
|
26
27
|
const INITIALIZE_TIMEOUT_MS = 15_000;
|
|
@@ -57,10 +58,6 @@ export function configurationResponse(settings: unknown, params: unknown): unkno
|
|
|
57
58
|
return Array.from({ length: count }, () => settings ?? {});
|
|
58
59
|
}
|
|
59
60
|
|
|
60
|
-
function unrefStream(stream: unknown): void {
|
|
61
|
-
(stream as { unref?: () => void } | null | undefined)?.unref?.();
|
|
62
|
-
}
|
|
63
|
-
|
|
64
61
|
interface OpenDocument {
|
|
65
62
|
version: number;
|
|
66
63
|
text: string;
|
|
@@ -101,7 +98,9 @@ export class LspClient {
|
|
|
101
98
|
async start(): Promise<void> {
|
|
102
99
|
if (this.child) return;
|
|
103
100
|
|
|
104
|
-
|
|
101
|
+
// The user's own environment under the bundled app (lib/app-launch.mjs).
|
|
102
|
+
const base = childProcessEnv(process.env);
|
|
103
|
+
const env = this.options.env ? { ...base, ...this.options.env } : base;
|
|
105
104
|
// On Windows an npm-installed server is a .cmd shim, which a bare spawn
|
|
106
105
|
// cannot start (lib/command-launch.ts); failure messages keep naming the command.
|
|
107
106
|
const launch = commandLaunch(this.config.command, this.config.args, env);
|
|
@@ -118,9 +117,9 @@ export class LspClient {
|
|
|
118
117
|
// are sockets at runtime, so they carry unref() even though the stream
|
|
119
118
|
// types don't declare it.
|
|
120
119
|
child.unref();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
120
|
+
unrefHandle(child.stdout);
|
|
121
|
+
unrefHandle(child.stderr);
|
|
122
|
+
unrefHandle(child.stdin);
|
|
124
123
|
|
|
125
124
|
let stderr = "";
|
|
126
125
|
child.stderr?.on("data", (chunk: Buffer) => {
|
package/extensions/lsp/index.ts
CHANGED
|
@@ -47,6 +47,9 @@ import {
|
|
|
47
47
|
import { findProjectRoot, serverForPath, typescriptPreflight } from "./servers.ts";
|
|
48
48
|
import { computeDelta, DeliveredTracker, fingerprintDiagnostic, formatNewDiagnostics, markDelivered } from "./watcher.ts";
|
|
49
49
|
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
50
|
+
import { singleFlight } from "../lib/single-flight.ts";
|
|
51
|
+
import { findProjectRoot as findRepoRoot } from "../lib/git.ts";
|
|
52
|
+
import { createLspTrustGate, PROJECT_CODE_REASON, PROJECT_CODE_SERVERS } from "./trust.ts";
|
|
50
53
|
|
|
51
54
|
/** Everything needed to spawn/reuse the server responsible for a path. */
|
|
52
55
|
interface ResolvedTarget {
|
|
@@ -133,12 +136,28 @@ export default function lspExtension(pi: ExtensionAPI) {
|
|
|
133
136
|
const MAX_RESPAWNS = 2;
|
|
134
137
|
const respawns = new Map<string, number>();
|
|
135
138
|
|
|
136
|
-
|
|
139
|
+
/**
|
|
140
|
+
* One startup per server key at a time: overlapping calls (two edits, or a
|
|
141
|
+
* diagnostics wait racing an edit) share it, since both would otherwise
|
|
142
|
+
* pass the empty-map check across the trust and backoff awaits and spawn a
|
|
143
|
+
* second server whose predecessor shutdown can no longer reach.
|
|
144
|
+
*/
|
|
145
|
+
const startOnce = singleFlight<LspClient | undefined>();
|
|
146
|
+
const clientFor = (target: ResolvedTarget, ctx: ExtensionContext): Promise<LspClient | undefined> => {
|
|
147
|
+
const running = clients.get(target.key);
|
|
148
|
+
if (running?.isRunning) return Promise.resolve(running);
|
|
149
|
+
return startOnce(target.key, () => startClient(target, ctx));
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const startClient = async (target: ResolvedTarget, ctx: ExtensionContext): Promise<LspClient | undefined> => {
|
|
137
153
|
const { key } = target;
|
|
138
154
|
if (startFailures.has(key)) return undefined;
|
|
139
155
|
|
|
140
156
|
const existing = clients.get(key);
|
|
141
157
|
if (existing?.isRunning) return existing;
|
|
158
|
+
// A server that runs project code starts only in a trusted project
|
|
159
|
+
// (trust.ts); checked here so no caller can spawn one without it.
|
|
160
|
+
if (!(await projectCodeAllowed(target, ctx)) || ctx.signal?.aborted) return undefined;
|
|
142
161
|
if (existing) {
|
|
143
162
|
// Crashed mid-session. Respawn with a short backoff, up to MAX_RESPAWNS
|
|
144
163
|
// times; after that record why so downstream reports the real cause (a
|
|
@@ -155,6 +174,8 @@ export default function lspExtension(pi: ExtensionAPI) {
|
|
|
155
174
|
await new Promise((resolve) => setTimeout(resolve, 500 * count));
|
|
156
175
|
}
|
|
157
176
|
|
|
177
|
+
if (ctx.signal?.aborted) return undefined;
|
|
178
|
+
|
|
158
179
|
// Keyed on the command being spawned, not on where the config came from —
|
|
159
180
|
// a plugin configuring typescript-language-server hits the same TS7 trap.
|
|
160
181
|
if (target.command === "typescript-language-server") {
|
|
@@ -181,6 +202,33 @@ export default function lspExtension(pi: ExtensionAPI) {
|
|
|
181
202
|
}
|
|
182
203
|
};
|
|
183
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Consent for a built-in server that runs the project's own code
|
|
207
|
+
* (trust.ts): trusted project → start; otherwise ask once per project in an
|
|
208
|
+
* interactive session, and with no UI or a "no" leave it off with a
|
|
209
|
+
* not-started failure the one-time notice and /lsp report.
|
|
210
|
+
*/
|
|
211
|
+
const trustGate = createLspTrustGate({ projectRoot: (dir) => findRepoRoot(dir) ?? dir });
|
|
212
|
+
const NOT_TRUSTED = "not started:";
|
|
213
|
+
const projectCodeAllowed = async (target: ResolvedTarget, ctx: ExtensionContext): Promise<boolean> => {
|
|
214
|
+
if (target.plugin || !PROJECT_CODE_SERVERS.has(target.command)) return true;
|
|
215
|
+
const why = PROJECT_CODE_REASON[target.command] ?? "runs this project's code";
|
|
216
|
+
const confirm = ctx.hasUI
|
|
217
|
+
? async (root: string) =>
|
|
218
|
+
(await ctx.ui.confirm(
|
|
219
|
+
`Start ${target.command} in this project?`,
|
|
220
|
+
`${target.command} ${why}. Allow it only for a project you trust.\n\nThe answer for ${root} is remembered; /lsp trust allows it later.`,
|
|
221
|
+
{ signal: ctx.signal },
|
|
222
|
+
)) === true
|
|
223
|
+
: undefined;
|
|
224
|
+
if (await trustGate.allowed(target.root, confirm, ctx.signal)) return true;
|
|
225
|
+
if (ctx.signal?.aborted) return false;
|
|
226
|
+
if (!startFailures.has(target.key)) {
|
|
227
|
+
startFailures.set(target.key, `${NOT_TRUSTED} ${target.command} ${why}, and this project is not trusted (run /lsp trust to allow it)`);
|
|
228
|
+
}
|
|
229
|
+
return false;
|
|
230
|
+
};
|
|
231
|
+
|
|
184
232
|
/** One-time notice per server key so a missing server doesn't degrade silently. */
|
|
185
233
|
const reportFailureOnce = (target: ResolvedTarget, notify: (message: string) => void) => {
|
|
186
234
|
const failure = startFailures.get(target.key);
|
|
@@ -270,7 +318,7 @@ export default function lspExtension(pi: ExtensionAPI) {
|
|
|
270
318
|
// pending work, and everything the client holds is unref'd —
|
|
271
319
|
// without a ref the loop drains and pi exits mid-tool (keep-alive.ts).
|
|
272
320
|
await withKeepAlive(async () => {
|
|
273
|
-
const client = await clientFor(target);
|
|
321
|
+
const client = await clientFor(target, ctx);
|
|
274
322
|
if (client) {
|
|
275
323
|
tracker.clear(pathToUri(path));
|
|
276
324
|
forceDeltaScan();
|
|
@@ -321,10 +369,13 @@ export default function lspExtension(pi: ExtensionAPI) {
|
|
|
321
369
|
// One keep-alive spans both awaits (same pattern as the tool_result hook).
|
|
322
370
|
const fetched = target
|
|
323
371
|
? await withKeepAlive(async () => {
|
|
324
|
-
const client = await clientFor(target);
|
|
372
|
+
const client = await clientFor(target, ctx);
|
|
325
373
|
return client && { all: await client.getDiagnostics(path, target.languageId) };
|
|
326
374
|
})
|
|
327
375
|
: undefined;
|
|
376
|
+
if (ctx.signal?.aborted) {
|
|
377
|
+
return { content: [{ type: "text", text: "Diagnostics cancelled because the turn was stopped." }], details: { available: false }, isError: true };
|
|
378
|
+
}
|
|
328
379
|
if (!target || !fetched) {
|
|
329
380
|
const failure = target ? startFailures.get(target.key) : undefined;
|
|
330
381
|
return {
|
|
@@ -363,8 +414,23 @@ export default function lspExtension(pi: ExtensionAPI) {
|
|
|
363
414
|
});
|
|
364
415
|
|
|
365
416
|
registerLocalCommand(pi, "lsp", {
|
|
366
|
-
description: "Show language server status",
|
|
417
|
+
description: "Show language server status: /lsp [trust]",
|
|
418
|
+
argumentHint: "[trust]",
|
|
419
|
+
getArgumentCompletions: () => [
|
|
420
|
+
{ value: "trust", label: "let servers that run this project's code start here (rust-analyzer, jdtls)" },
|
|
421
|
+
],
|
|
367
422
|
handler: async (args, ctx) => {
|
|
423
|
+
if (args.trim() === "trust") {
|
|
424
|
+
const root = trustGate.trust(ctx.cwd);
|
|
425
|
+
for (const [key, failure] of startFailures) {
|
|
426
|
+
if (failure.startsWith(NOT_TRUSTED)) {
|
|
427
|
+
startFailures.delete(key);
|
|
428
|
+
warned.delete(key);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
ctx.ui.notify(`Trusted ${root}: rust-analyzer and jdtls may start here. They run this project's build code.`, "info");
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
368
434
|
const lines = [...clients.entries()].map(
|
|
369
435
|
([key, client]) => `${client.isRunning ? "running" : "stopped"} ${key} (${client.diagnosticsCount} diagnostics)`,
|
|
370
436
|
);
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* process exits 0 mid-await, silently ending the run right after
|
|
8
8
|
* tool_execution_start. Wrap each outermost LS await in this; keeping the ref
|
|
9
9
|
* at the call sites (not inside the client) preserves drain-on-idle for
|
|
10
|
-
* everything not actively awaited. docs/one-shot-lsp-event-loop-drain.md.
|
|
10
|
+
* everything not actively awaited. working-docs/one-shot-lsp-event-loop-drain.md.
|
|
11
11
|
*/
|
|
12
12
|
export async function withKeepAlive<T>(work: () => Promise<T>): Promise<T> {
|
|
13
13
|
const keepAlive = setInterval(() => {}, 1_000);
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent before a language server that runs the project's own code (pure fs).
|
|
3
|
+
*
|
|
4
|
+
* The LSP extension starts a server after any edit, outside the permission
|
|
5
|
+
* gate. Most servers only parse, but some execute the project as part of their
|
|
6
|
+
* job: rust-analyzer runs `build.rs` build scripts and proc macros (and cargo
|
|
7
|
+
* honours a `rust-toolchain.toml` that can name any toolchain binary), and
|
|
8
|
+
* jdtls imports Gradle and Maven builds by running them. In an untrusted
|
|
9
|
+
* checkout, an approved edit to one `.rs` file therefore also ran the
|
|
10
|
+
* repository's build script (SECURITY-REVIEW-2026-09-23 M4). Those servers now
|
|
11
|
+
* start only in a project the user has trusted: once per project root,
|
|
12
|
+
* persisted under `~/.onecode`, asked on first use in an interactive session
|
|
13
|
+
* or granted with `/lsp trust`. With no UI they stay off (fails closed, like
|
|
14
|
+
* project hooks and `.mcp.json` servers). Plugin-provided servers never ask:
|
|
15
|
+
* installing the plugin was the consent.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readJsonFile, writeJsonAtomic } from "../lib/atomic-write.ts";
|
|
19
|
+
import { consentStorePath } from "../lib/consent-stores.ts";
|
|
20
|
+
import { comparablePath, isPathAtOrUnder } from "../lib/paths.ts";
|
|
21
|
+
import { singleFlight } from "../lib/single-flight.ts";
|
|
22
|
+
|
|
23
|
+
/** Built-in server commands that execute project code. */
|
|
24
|
+
export const PROJECT_CODE_SERVERS: ReadonlySet<string> = new Set(["rust-analyzer", "jdtls"]);
|
|
25
|
+
|
|
26
|
+
/** What each of those runs, for the consent dialog and the not-started notice. */
|
|
27
|
+
export const PROJECT_CODE_REASON: Record<string, string> = {
|
|
28
|
+
"rust-analyzer": "runs this project's build scripts and proc macros",
|
|
29
|
+
jdtls: "runs this project's Gradle or Maven build to import it",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
interface TrustStore {
|
|
33
|
+
version: 1;
|
|
34
|
+
roots: Record<string, { trustedAt: string }>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function lspTrustStorePath(): string {
|
|
38
|
+
return consentStorePath("lsp");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function readStore(storePath: string): TrustStore {
|
|
42
|
+
const store = readJsonFile<TrustStore>(storePath);
|
|
43
|
+
return store && typeof store === "object" && store.roots && typeof store.roots === "object" ? store : { version: 1, roots: {} };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Whether `serverRoot` lies at or under a trusted project root. */
|
|
47
|
+
export function isLspRootTrusted(serverRoot: string, storePath = lspTrustStorePath()): boolean {
|
|
48
|
+
const target = comparablePath(serverRoot);
|
|
49
|
+
return Object.keys(readStore(storePath).roots).some((root) => isPathAtOrUnder(target, comparablePath(root)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Trust a project root (and everything under it) for project-code servers. */
|
|
53
|
+
export function persistLspTrust(projectRoot: string, storePath = lspTrustStorePath()): void {
|
|
54
|
+
const store = readStore(storePath);
|
|
55
|
+
store.roots[projectRoot] = { trustedAt: new Date().toISOString() };
|
|
56
|
+
try {
|
|
57
|
+
writeJsonAtomic(storePath, store, { mode: 0o600 });
|
|
58
|
+
} catch {
|
|
59
|
+
// Trust still holds for this session through the caller's own memory.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The per-session trust decision for project-code servers. Every answer is
|
|
65
|
+
* keyed by the project the SERVER runs in (the repository holding its root),
|
|
66
|
+
* never the session's cwd: trusting the current checkout must not start a
|
|
67
|
+
* server rooted in another one (PR #8 review). One question per project is in
|
|
68
|
+
* flight at a time; a "yes" is persisted, a "no" holds for the session.
|
|
69
|
+
*/
|
|
70
|
+
export function createLspTrustGate(deps: {
|
|
71
|
+
/** The project a directory belongs to (its repository root, or itself). */
|
|
72
|
+
projectRoot: (dir: string) => string;
|
|
73
|
+
isTrusted?: (serverRoot: string) => boolean;
|
|
74
|
+
persist?: (projectRoot: string) => void;
|
|
75
|
+
}) {
|
|
76
|
+
const { projectRoot, isTrusted = (root: string) => isLspRootTrusted(root), persist = (root: string) => persistLspTrust(root) } = deps;
|
|
77
|
+
const sessionTrust = new Map<string, boolean>();
|
|
78
|
+
/** Server roots found trusted on disk this process, so the store is read once per root. */
|
|
79
|
+
const trustedServerRoots = new Set<string>();
|
|
80
|
+
/** Undefined: the caller that opened the dialog aborted, so nobody decided. */
|
|
81
|
+
const askOnce = singleFlight<boolean | undefined>();
|
|
82
|
+
const gate = {
|
|
83
|
+
/**
|
|
84
|
+
* Whether a server rooted at `serverRoot` may start. Without `confirm`
|
|
85
|
+
* (no UI), or after a "no" for its project, only stored trust counts.
|
|
86
|
+
*/
|
|
87
|
+
async allowed(serverRoot: string, confirm?: (projectRoot: string) => Promise<boolean>, signal?: AbortSignal): Promise<boolean> {
|
|
88
|
+
if (signal?.aborted) return false;
|
|
89
|
+
const root = projectRoot(serverRoot);
|
|
90
|
+
if (sessionTrust.get(root) === true || trustedServerRoots.has(serverRoot)) return true;
|
|
91
|
+
// The project root is what a "yes" persists; a linked worktree outside
|
|
92
|
+
// its main checkout is not under it, so both are checked.
|
|
93
|
+
if (isTrusted(serverRoot) || isTrusted(root)) {
|
|
94
|
+
trustedServerRoots.add(serverRoot);
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (sessionTrust.get(root) !== undefined || !confirm) return false;
|
|
98
|
+
const decided = await askOnce(root, async () => {
|
|
99
|
+
const ok = await confirm(root);
|
|
100
|
+
// Aborting the turn neither grants trust nor remembers a refusal.
|
|
101
|
+
if (signal?.aborted) return undefined;
|
|
102
|
+
sessionTrust.set(root, ok);
|
|
103
|
+
if (ok) persist(root);
|
|
104
|
+
return ok;
|
|
105
|
+
});
|
|
106
|
+
if (signal?.aborted) return false;
|
|
107
|
+
// The opener aborted without an answer: a caller still live asks again.
|
|
108
|
+
return decided ?? gate.allowed(serverRoot, confirm, signal);
|
|
109
|
+
},
|
|
110
|
+
/** `/lsp trust`: trust the project `dir` belongs to, persisted. Returns that project root. */
|
|
111
|
+
trust(dir: string): string {
|
|
112
|
+
const root = projectRoot(dir);
|
|
113
|
+
persist(root);
|
|
114
|
+
sessionTrust.set(root, true);
|
|
115
|
+
return root;
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
return gate;
|
|
119
|
+
}
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*
|
|
15
15
|
* One deliberate divergence: file headers are cwd-relative paths, not
|
|
16
16
|
* basenames — two files with the same basename would otherwise be
|
|
17
|
-
* indistinguishable in one block (docs/decisions/lsp.md).
|
|
17
|
+
* indistinguishable in one block (working-docs/decisions/lsp.md).
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { relative } from "node:path";
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { mcpFailuresReminder, mcpInstructionsReminder } from "./client.ts";
|
|
13
|
+
import { countNoun } from "../lib/tui-render.ts";
|
|
13
14
|
|
|
14
15
|
export interface McpAnnounced {
|
|
15
16
|
/** Servers whose instructions the model has been given, with the text it saw. */
|
|
@@ -38,13 +39,79 @@ export function emptyAnnounced(): McpAnnounced {
|
|
|
38
39
|
return { instructed: new Map(), failed: new Map() };
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Store and compare the exact bounded values the reminder puts on the wire.
|
|
44
|
+
* A changing tail beyond either cap is not a model-visible capability change
|
|
45
|
+
* and must not become a false resume addendum.
|
|
46
|
+
*/
|
|
47
|
+
function displayedInstructions(name: string, instructions: string): string {
|
|
48
|
+
const rendered = mcpInstructionsReminder([{ server: { name }, instructions }]);
|
|
49
|
+
const prefix = "# MCP Server Instructions\n\nThe following MCP servers have provided instructions for how to use their tools and resources:\n\n" + `## ${name}\n`;
|
|
50
|
+
return rendered?.startsWith(prefix) ? rendered.slice(prefix.length) : instructions;
|
|
51
|
+
}
|
|
52
|
+
function displayedFailure(error: string): string {
|
|
53
|
+
return error.split("\n")[0].slice(0, 300);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Maps are serialized as entry arrays so arbitrary server instructions/errors round-trip exactly. */
|
|
57
|
+
export interface McpAnnouncedBaseline { instructed: Array<[string, string]>; failed: Array<[string, string]> }
|
|
58
|
+
export function serializeMcpAnnounced(announced: McpAnnounced): McpAnnouncedBaseline {
|
|
59
|
+
return { instructed: [...announced.instructed], failed: [...announced.failed] };
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Best-effort compatibility reader for stacks written before structured MCP
|
|
63
|
+
* baselines. With the configured server `names`, a section starts only at a
|
|
64
|
+
* `## <name>` heading, so a `## ` heading inside a server's instructions stays
|
|
65
|
+
* part of them instead of reading as a server of its own.
|
|
66
|
+
*/
|
|
67
|
+
export function mcpAnnouncedFromReminders(instructions: string | undefined, failures: string | undefined, names?: Iterable<string>): McpAnnounced {
|
|
68
|
+
const announced = emptyAnnounced();
|
|
69
|
+
const known = names ? new Set(names) : undefined;
|
|
70
|
+
const instructionPrefix = "# MCP Server Instructions\n\nThe following MCP servers have provided instructions for how to use their tools and resources:\n\n";
|
|
71
|
+
if (instructions?.startsWith(instructionPrefix)) {
|
|
72
|
+
const sections: string[] = [];
|
|
73
|
+
for (const section of instructions.slice(instructionPrefix.length).split("\n\n## ")) {
|
|
74
|
+
const normalized = section.startsWith("## ") ? section.slice(3) : section;
|
|
75
|
+
const newline = normalized.indexOf("\n");
|
|
76
|
+
const heading = newline > 0 ? normalized.slice(0, newline) : normalized;
|
|
77
|
+
if (known && sections.length > 0 && !known.has(heading)) sections[sections.length - 1] += `\n\n## ${normalized}`;
|
|
78
|
+
else sections.push(normalized);
|
|
79
|
+
}
|
|
80
|
+
for (const section of sections) {
|
|
81
|
+
const newline = section.indexOf("\n");
|
|
82
|
+
if (newline > 0) announced.instructed.set(section.slice(0, newline), section.slice(newline + 1));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const failurePrefix = "The following MCP servers are configured but failed to connect — their tools (typically named mcp__<server>__*) are unavailable for this session:\n";
|
|
86
|
+
if (failures?.startsWith(failurePrefix)) {
|
|
87
|
+
for (const line of failures.slice(failurePrefix.length).split("\n")) {
|
|
88
|
+
if (!line.startsWith("- ")) break;
|
|
89
|
+
const colon = line.indexOf(": ", 2);
|
|
90
|
+
if (colon > 2) announced.failed.set(line.slice(2, colon), line.slice(colon + 2));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return announced;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function mcpAnnouncedBaseline(value: unknown): McpAnnounced | undefined {
|
|
97
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
98
|
+
const input = value as Record<string, unknown>;
|
|
99
|
+
const pairs = (item: unknown): Array<[string, string]> | undefined =>
|
|
100
|
+
Array.isArray(item) && item.every((pair) => Array.isArray(pair) && pair.length === 2 && typeof pair[0] === "string" && typeof pair[1] === "string")
|
|
101
|
+
? item as Array<[string, string]>
|
|
102
|
+
: undefined;
|
|
103
|
+
const instructed = pairs(input.instructed);
|
|
104
|
+
const failed = pairs(input.failed);
|
|
105
|
+
return instructed && failed ? { instructed: new Map(instructed), failed: new Map(failed) } : undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
41
108
|
/** What message 1 says after it was (re)written from `snapshot`, before the first request. */
|
|
42
109
|
export function announcedFrom(snapshot: McpSnapshot): McpAnnounced {
|
|
43
110
|
return {
|
|
44
111
|
instructed: new Map(
|
|
45
|
-
snapshot.connected.filter((c) => c.instructions).map((c) => [c.name, c.instructions as string]),
|
|
112
|
+
snapshot.connected.filter((c) => c.instructions).map((c) => [c.name, displayedInstructions(c.name, c.instructions as string)]),
|
|
46
113
|
),
|
|
47
|
-
failed: new Map(snapshot.failed.map((f) => [f.name, f.error])),
|
|
114
|
+
failed: new Map(snapshot.failed.map((f) => [f.name, displayedFailure(f.error)])),
|
|
48
115
|
};
|
|
49
116
|
}
|
|
50
117
|
|
|
@@ -53,9 +120,11 @@ export function mcpDelta(announced: McpAnnounced, snapshot: McpSnapshot): McpDel
|
|
|
53
120
|
const failedNames = new Set(snapshot.failed.map((f) => f.name));
|
|
54
121
|
return {
|
|
55
122
|
newInstructions: snapshot.connected
|
|
56
|
-
.filter((c) => c.instructions && announced.instructed.get(c.name) !== c.instructions)
|
|
57
|
-
.map((c) => ({ name: c.name, instructions: c.instructions as string })),
|
|
58
|
-
newFailures: snapshot.failed
|
|
123
|
+
.filter((c) => c.instructions && announced.instructed.get(c.name) !== displayedInstructions(c.name, c.instructions as string))
|
|
124
|
+
.map((c) => ({ name: c.name, instructions: displayedInstructions(c.name, c.instructions as string) })),
|
|
125
|
+
newFailures: snapshot.failed
|
|
126
|
+
.filter((f) => announced.failed.get(f.name) !== displayedFailure(f.error))
|
|
127
|
+
.map((f) => ({ name: f.name, error: displayedFailure(f.error) })),
|
|
59
128
|
recovered: [...announced.failed.keys()].filter((name) => connectedNames.has(name)),
|
|
60
129
|
dropped: [...announced.instructed.keys()].filter((name) => !connectedNames.has(name) && !failedNames.has(name)),
|
|
61
130
|
};
|
|
@@ -97,3 +166,16 @@ export function mcpChangeNotices(delta: McpDelta): string[] {
|
|
|
97
166
|
}
|
|
98
167
|
return notices;
|
|
99
168
|
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The user's startup notices, Claude Code's wording (`useMcpConnectivityStatus`):
|
|
172
|
+
* `2 MCP servers failed · /mcp` and `1 MCP server needs auth · /mcp`, with each
|
|
173
|
+
* server's error left to /mcp. Printing every server's raw error ("fetch
|
|
174
|
+
* failed" per remote server when offline) buried the prompt.
|
|
175
|
+
*/
|
|
176
|
+
export function mcpStartupNotices(failedServers: number, needsAuth: number): string[] {
|
|
177
|
+
const out: string[] = [];
|
|
178
|
+
if (failedServers > 0) out.push(`${countNoun(failedServers, "MCP server")} failed · /mcp`);
|
|
179
|
+
if (needsAuth > 0) out.push(`${countNoun(needsAuth, "MCP server")} ${needsAuth === 1 ? "needs" : "need"} auth · /mcp`);
|
|
180
|
+
return out;
|
|
181
|
+
}
|