one-code-extension 0.4.0 → 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 +219 -219
- 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 +66 -34
- 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 +19 -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
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Cross-extension "a setting you own just changed on disk" channels (pure
|
|
3
3
|
* constants). jiti isolates module state, so when one extension writes another
|
|
4
|
-
* extension's setting — `/doctor preset` writing the subagent default and
|
|
5
|
-
* auto-mode classifier model — it announces the write here and the
|
|
6
|
-
* re-reads its file and republishes its live status. No payload: the
|
|
7
|
-
* own loader is the source of truth.
|
|
4
|
+
* extension's setting — `/doctor preset` writing the subagent default and
|
|
5
|
+
* clearing the auto-mode classifier model — it announces the write here and the
|
|
6
|
+
* owner re-reads its file and republishes its live status. No payload: the
|
|
7
|
+
* owner's own loader is the source of truth.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/** `subagentModel` in ~/.onecode/settings.json changed; the subagents extension re-resolves and re-emits its status. */
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's semantic exit codes (pure): a nonzero exit that is not an
|
|
3
|
+
* error, such as grep's "no matches" or diff's "files differ", which Claude
|
|
4
|
+
* Code reports as plain output with no `Exit code N` line. Bash and
|
|
5
|
+
* PowerShell keep separate tables and separate last-command rules.
|
|
6
|
+
*/
|
|
7
|
+
import { parseBash } from "./bash-parser.ts";
|
|
8
|
+
|
|
9
|
+
type BashNode = NonNullable<ReturnType<typeof parseBash>>["rootNode"];
|
|
10
|
+
const CONTAINERS = new Set(["program", "list", "pipeline"]);
|
|
11
|
+
const OPERATORS = new Set(["&&", "||", "|", ";", "&", "|&", "\n"]);
|
|
12
|
+
const EXIT_ONE_SUCCESSES = new Set(["grep", "rg", "egrep", "fgrep", "find", "diff", "test", "["]);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The rightmost command's text, or undefined when an `&&` precedes it: a failed
|
|
16
|
+
* earlier step can explain the exit, not the final search. Redirects and
|
|
17
|
+
* comments are skipped, as Claude Code's last-command walk does.
|
|
18
|
+
*/
|
|
19
|
+
function lastCommand(root: BashNode): string | undefined {
|
|
20
|
+
let node: BashNode | undefined = root;
|
|
21
|
+
while (node && (CONTAINERS.has(node.type) || node.type === "redirected_statement")) {
|
|
22
|
+
const children: BashNode[] = node.children;
|
|
23
|
+
const last = children.findLastIndex((child) => !OPERATORS.has(child.type) && child.type !== "comment" && !child.type.endsWith("_redirect"));
|
|
24
|
+
if (node.type === "list" && children[last - 1]?.type === "&&") return undefined;
|
|
25
|
+
node = children[last];
|
|
26
|
+
}
|
|
27
|
+
return node?.text;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** git's global options that take their value as the next word (`--git-dir=x` is one word). */
|
|
31
|
+
const GIT_VALUE_OPTIONS = new Set(["-C", "-c", "--git-dir", "--work-tree", "--namespace", "--super-prefix", "--config-env", "--attr-source"]);
|
|
32
|
+
|
|
33
|
+
/** git's subcommand among the words after `git`: the first that is no option or an option's value. */
|
|
34
|
+
function gitSubcommandOf(args: readonly string[]): string | undefined {
|
|
35
|
+
for (let i = 0; i < args.length; i++) {
|
|
36
|
+
if (!args[i].startsWith("-")) return args[i];
|
|
37
|
+
if (GIT_VALUE_OPTIONS.has(args[i])) i++;
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Bash: exit 1 from a final grep, rg, egrep, fgrep, find, diff, test, `[` or git grep/diff. */
|
|
43
|
+
export function bashExitOneIsSuccess(command: string): boolean {
|
|
44
|
+
if (command.length > 10_000) return false;
|
|
45
|
+
const tree = parseBash(command);
|
|
46
|
+
if (!tree) return false;
|
|
47
|
+
try {
|
|
48
|
+
if (tree.rootNode.type !== "program") return false;
|
|
49
|
+
const last = lastCommand(tree.rootNode);
|
|
50
|
+
if (last === undefined) return false;
|
|
51
|
+
// Claude Code matches the literal first word: no unwrapping of env, assignments or quotes.
|
|
52
|
+
const [head, ...args] = last.trim().split(/\s+/);
|
|
53
|
+
if (head !== "git") return EXIT_ONE_SUCCESSES.has(head);
|
|
54
|
+
const sub = gitSubcommandOf(args);
|
|
55
|
+
return sub === "grep" || sub === "diff";
|
|
56
|
+
} finally {
|
|
57
|
+
tree.delete();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// PowerShell. The last segment splits on `;`, `|`, newlines, `&&` and a
|
|
62
|
+
// spaced `&`, outside quotes and comments; `&&` before it means an earlier
|
|
63
|
+
// step can explain the exit.
|
|
64
|
+
type Segment = { text: string; andGated: boolean };
|
|
65
|
+
|
|
66
|
+
function lastSegment(command: string): Segment {
|
|
67
|
+
const segments: Segment[] = [];
|
|
68
|
+
let start = 0;
|
|
69
|
+
let andGated = false;
|
|
70
|
+
let inSingle = false;
|
|
71
|
+
let inDouble = false;
|
|
72
|
+
for (let i = 0; i < command.length; i++) {
|
|
73
|
+
const c = command[i];
|
|
74
|
+
if (inSingle) {
|
|
75
|
+
if (c === "'") inSingle = false;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (inDouble) {
|
|
79
|
+
if (c === "`") i++;
|
|
80
|
+
else if (c === '"') inDouble = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (c === "#" && (i === 0 || " \t\n\r".includes(command[i - 1]))) {
|
|
84
|
+
segments.push({ text: command.slice(start, i), andGated });
|
|
85
|
+
while (i + 1 < command.length && command[i + 1] !== "\n" && command[i + 1] !== "\r") i++;
|
|
86
|
+
start = i + 1;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (c === "'") {
|
|
90
|
+
inSingle = true;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (c === '"') {
|
|
94
|
+
inDouble = true;
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (c === ";" || c === "|" || c === "\n" || c === "\r") {
|
|
98
|
+
segments.push({ text: command.slice(start, i), andGated });
|
|
99
|
+
start = i + 1;
|
|
100
|
+
if (c !== "|") andGated = false;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (c === "&") {
|
|
104
|
+
if (command[i + 1] === "&") {
|
|
105
|
+
segments.push({ text: command.slice(start, i), andGated });
|
|
106
|
+
i++;
|
|
107
|
+
start = i + 1;
|
|
108
|
+
andGated = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const before = command[i - 1];
|
|
112
|
+
if ((before === " " || before === "\t") && command.slice(start, i).trim() !== "") {
|
|
113
|
+
segments.push({ text: command.slice(start, i), andGated });
|
|
114
|
+
start = i + 1;
|
|
115
|
+
andGated = false;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
segments.push({ text: command.slice(start), andGated });
|
|
120
|
+
return segments.findLast((segment) => segment.text.trim()) ?? { text: command, andGated: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A segment's first word, unquoted, and the text after it; a leading `&` or `.` call operator is dropped. */
|
|
124
|
+
function splitFirstWord(segment: string): { word: string; rest: string } {
|
|
125
|
+
const line = segment.trim().replace(/^[&.]\s+/, "");
|
|
126
|
+
const quoted = /^"([^"]*)"|^'([^']*)'/.exec(line);
|
|
127
|
+
if (quoted) return { word: quoted[1] ?? quoted[2], rest: line.slice(quoted[0].length) };
|
|
128
|
+
const raw = line.split(/\s+/)[0] ?? "";
|
|
129
|
+
return { word: raw.replace(/^["']|["']$/g, ""), rest: line.slice(raw.length) };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The command's file name, lowercased, without a native .exe/.cmd/.bat extension. */
|
|
133
|
+
function baseCommand(word: string): { base: string; nativeExt: string | undefined } {
|
|
134
|
+
const file = (word.split(/[\\/]/).pop() || word).toLowerCase();
|
|
135
|
+
const nativeExt = ["exe", "cmd", "bat"].find((ext) => file.endsWith(`.${ext}`));
|
|
136
|
+
return { base: nativeExt ? file.slice(0, -(nativeExt.length + 1)) : file, nativeExt };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const exitOneOk = (code: number) => code === 0 || code === 1;
|
|
140
|
+
const POWERSHELL_SEMANTICS = new Map<string, (code: number) => boolean>([
|
|
141
|
+
["grep", exitOneOk],
|
|
142
|
+
["rg", exitOneOk],
|
|
143
|
+
["egrep", exitOneOk],
|
|
144
|
+
["fgrep", exitOneOk],
|
|
145
|
+
["findstr", exitOneOk],
|
|
146
|
+
["robocopy", (code) => code >= 0 && code < 8],
|
|
147
|
+
]);
|
|
148
|
+
// Only as `<name>.exe` with output: bare `where` is Where-Object, bare `fc` Format-Custom.
|
|
149
|
+
const POWERSHELL_EXE_WITH_OUTPUT = new Map<string, (code: number) => boolean>([
|
|
150
|
+
["where", exitOneOk],
|
|
151
|
+
["fc", exitOneOk],
|
|
152
|
+
["diff", exitOneOk],
|
|
153
|
+
]);
|
|
154
|
+
const NOT_RECOGNIZED = /is not recognized as (a name of a cmdlet|the name of a cmdlet|an? internal)/i;
|
|
155
|
+
|
|
156
|
+
/** PowerShell: a nonzero exit the final command's table calls success. `output` is stdout and stderr merged. */
|
|
157
|
+
export function powershellExitIsSuccess(command: string, exitCode: number, output: string): boolean {
|
|
158
|
+
if (exitCode === 0) return true;
|
|
159
|
+
const { text, andGated } = lastSegment(command);
|
|
160
|
+
const { word, rest } = splitFirstWord(text);
|
|
161
|
+
const { base, nativeExt } = baseCommand(word);
|
|
162
|
+
const sub = base === "git" ? gitSubcommandOf(rest.trim().split(/\s+/).filter(Boolean)) : undefined;
|
|
163
|
+
const ok =
|
|
164
|
+
sub === "grep" || sub === "diff"
|
|
165
|
+
? exitOneOk
|
|
166
|
+
: ((nativeExt === "exe" && output.trim() !== "" ? POWERSHELL_EXE_WITH_OUTPUT.get(base) : undefined) ?? POWERSHELL_SEMANTICS.get(base));
|
|
167
|
+
if (!ok?.(exitCode)) return false;
|
|
168
|
+
// A command that never ran, or findstr's own error, is a failure after all.
|
|
169
|
+
return !(andGated || NOT_RECOGNIZED.test(output) || /CommandNotFoundException/.test(output) || /^FINDSTR: /m.test(output));
|
|
170
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Quoting one word for a shell command line. Pure. */
|
|
2
|
+
|
|
3
|
+
/** A POSIX shell single-quoted word: the text exactly, whatever it holds. */
|
|
4
|
+
export function shellQuote(word: string): string {
|
|
5
|
+
return `'${word.replace(/'/g, "'\\''")}'`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/** PowerShell single-quoted literal: only `'` needs escaping, as `''`. */
|
|
9
|
+
export function powershellQuote(word: string): string {
|
|
10
|
+
return `'${word.replace(/'/g, "''")}'`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code's shell result text, made from pi's (pure). pi ends a failing
|
|
3
|
+
* command's output with `\n\nCommand exited with code N` and keeps the
|
|
4
|
+
* output's trailing newline; Claude Code starts a failure with `Exit code N`
|
|
5
|
+
* on its own line and trims the output's leading blank lines and trailing
|
|
6
|
+
* whitespace, and an empty success reads `(<tool> completed with no output)`.
|
|
7
|
+
* A nonzero exit Claude Code's semantics call success (grep's no match, a
|
|
8
|
+
* diff that found differences) reads as a success. pi's truncation notes
|
|
9
|
+
* stay where they are, at the end of the output.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { emptyToolResultText } from "./empty-tool-result.ts";
|
|
13
|
+
import { bashExitOneIsSuccess, powershellExitIsSuccess } from "./shell-exit-semantics.ts";
|
|
14
|
+
|
|
15
|
+
type Block = { type: string; text?: string };
|
|
16
|
+
type ShellResult = { content?: Block[]; structuredContent?: unknown; isError?: boolean };
|
|
17
|
+
|
|
18
|
+
/** pi's text for an empty output. */
|
|
19
|
+
const PI_NO_OUTPUT = "(no output)";
|
|
20
|
+
|
|
21
|
+
function successText(output: string, toolName: string): string {
|
|
22
|
+
if (output === PI_NO_OUTPUT) return emptyToolResultText(toolName);
|
|
23
|
+
return output.replace(/^(\s*\n)+/, "").trimEnd() || emptyToolResultText(toolName);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The output before pi's `Command exited with code N` trailer, or undefined when the text is not pi's failure shape. */
|
|
27
|
+
function failureOutput(text: string, exitCode: number): string | undefined {
|
|
28
|
+
const status = `Command exited with code ${exitCode}`;
|
|
29
|
+
if (text !== status && !text.endsWith(`\n\n${status}`)) return undefined;
|
|
30
|
+
return text.slice(0, Math.max(0, text.length - status.length - 2));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Claude Code's failure text: `Exit code N`, then the output on its own lines. */
|
|
34
|
+
function failureText(output: string, exitCode: number): string {
|
|
35
|
+
return (output === PI_NO_OUTPUT || output === "" ? `Exit code ${exitCode}` : `Exit code ${exitCode}\n${output}`).trimEnd();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Claude Code's text for one foreground result, or undefined to keep pi's. */
|
|
39
|
+
export function claudeCodeShellText(text: string, exitCode: unknown, toolName: string): string | undefined {
|
|
40
|
+
if (typeof exitCode !== "number") return undefined;
|
|
41
|
+
if (exitCode === 0) return successText(text, toolName);
|
|
42
|
+
const output = failureOutput(text, exitCode);
|
|
43
|
+
return output === undefined ? undefined : failureText(output, exitCode);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function semanticSuccess(toolName: string, command: string, exitCode: number, output: string): boolean {
|
|
47
|
+
if (toolName === "bash") return exitCode === 1 && bashExitOneIsSuccess(command);
|
|
48
|
+
if (toolName === "powershell") return powershellExitIsSuccess(command, exitCode, output === PI_NO_OUTPUT ? "" : output);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** A foreground result with its first text block in Claude Code's words; other results pass through. */
|
|
53
|
+
export function withClaudeCodeShellText<R extends ShellResult>(result: R, toolName: string, command: string): R {
|
|
54
|
+
const first = result?.content?.[0];
|
|
55
|
+
if (first?.type !== "text" || typeof first.text !== "string") return result;
|
|
56
|
+
const exitCode = (result.structuredContent as { exit_code?: unknown } | undefined)?.exit_code;
|
|
57
|
+
const rest = (result.content ?? []).slice(1);
|
|
58
|
+
let text: string | undefined;
|
|
59
|
+
if (typeof exitCode === "number" && exitCode !== 0) {
|
|
60
|
+
const output = failureOutput(first.text, exitCode);
|
|
61
|
+
if (output !== undefined && semanticSuccess(toolName, command, exitCode, output)) {
|
|
62
|
+
return { ...result, content: [{ ...first, text: successText(output, toolName) }, ...rest], isError: false };
|
|
63
|
+
}
|
|
64
|
+
text = output === undefined ? undefined : failureText(output, exitCode);
|
|
65
|
+
} else {
|
|
66
|
+
text = claudeCodeShellText(first.text, exitCode, toolName);
|
|
67
|
+
}
|
|
68
|
+
if (text === undefined || text === first.text) return result;
|
|
69
|
+
return { ...result, content: [{ ...first, text }, ...rest] };
|
|
70
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Which shell One Code spawns, and how — the one place that knows about Git
|
|
3
3
|
* Bash on Windows and about PowerShell.
|
|
4
4
|
*
|
|
5
|
-
* Claude Code's shape (findings §22, docs/decisions/windows.md): Git for
|
|
5
|
+
* Claude Code's shape (findings §22, working-docs/decisions/windows.md): Git for
|
|
6
6
|
* Windows is optional. When a bash is available it drives the `bash` tool,
|
|
7
7
|
* hooks and background shells; `CLAUDE_CODE_GIT_BASH_PATH` (process env, or
|
|
8
8
|
* Claude Code's user-settings `env` block) names a custom bash and is ignored
|
|
@@ -29,9 +29,11 @@ import { type ChildProcess, spawn, type SpawnOptions } from "node:child_process"
|
|
|
29
29
|
import { constants, existsSync } from "node:fs";
|
|
30
30
|
import { access } from "node:fs/promises";
|
|
31
31
|
import { homedir } from "node:os";
|
|
32
|
-
import { basename } from "node:path";
|
|
33
|
-
import { getPowerShellConfig, getShellConfig } from "@earendil-works/pi-coding-agent";
|
|
32
|
+
import { basename, delimiter, join } from "node:path";
|
|
33
|
+
import { getAgentDir, getPowerShellConfig, getShellConfig } from "@earendil-works/pi-coding-agent";
|
|
34
|
+
import { childProcessEnv } from "./app-launch.mjs";
|
|
34
35
|
import { readSettingsEnv } from "./claude-settings.ts";
|
|
36
|
+
import { PowerShellParser, sharedPowerShellParser } from "./powershell-parser.ts";
|
|
35
37
|
import { detachedSpawnOptions, killProcessTree, waitForChildExit } from "./process-tree.ts";
|
|
36
38
|
import { whichOnPath } from "./which.ts";
|
|
37
39
|
|
|
@@ -52,6 +54,20 @@ export const POWERSHELL_ARGS = ["-NoProfile", "-NonInteractive", "-ExecutionPoli
|
|
|
52
54
|
/** pi's UTF-8 output prefix for PowerShell (core/tools/powershell.ts), so Windows PowerShell 5.1 does not emit UTF-16. */
|
|
53
55
|
export const POWERSHELL_UTF8_PREFIX = "try { [Console]::OutputEncoding=[System.Text.Encoding]::UTF8 } catch {}\n";
|
|
54
56
|
|
|
57
|
+
/**
|
|
58
|
+
* What the PowerShell tool prepends to every command, foreground and
|
|
59
|
+
* background: pi's UTF-8 line, then UTF-8 for `Out-File` and `$OutputEncoding`
|
|
60
|
+
* and plain-text rendering, as Claude Code's PowerShell tool runs commands.
|
|
61
|
+
* `$PSStyle.OutputRendering = 'PlainText'` keeps pwsh 7's formatter from
|
|
62
|
+
* colouring table headers, which otherwise reached the model as raw
|
|
63
|
+
* `\x1b[32;1m` escapes.
|
|
64
|
+
*/
|
|
65
|
+
export const POWERSHELL_TOOL_PREFIX =
|
|
66
|
+
// One line, so an error's "At line:N" stays one off the command's own line, as with pi's prefix alone.
|
|
67
|
+
`${POWERSHELL_UTF8_PREFIX.trimEnd()}; try { $PSDefaultParameterValues['Out-File:Encoding'] = 'utf8' } catch {}; ` +
|
|
68
|
+
"if ($ExecutionContext.SessionState.LanguageMode -eq 'FullLanguage') { try { $OutputEncoding = [System.Text.UTF8Encoding]::new() } catch {}; " +
|
|
69
|
+
"if ($null -ne $PSStyle) { try { $PSStyle.OutputRendering = 'PlainText' } catch {} } };\n";
|
|
70
|
+
|
|
55
71
|
export const GIT_BASH_PATH_VAR = "CLAUDE_CODE_GIT_BASH_PATH";
|
|
56
72
|
|
|
57
73
|
// ---------------------------------------------------------------------------
|
|
@@ -187,6 +203,27 @@ export function powerShellSpawn(): ShellSpawn | undefined {
|
|
|
187
203
|
return cachedPowerShell.spawn;
|
|
188
204
|
}
|
|
189
205
|
|
|
206
|
+
/**
|
|
207
|
+
* PowerShell's own parser, served by the same executable and arguments the
|
|
208
|
+
* PowerShell tool runs (lib/powershell-parser.ts), one server per process.
|
|
209
|
+
* Undefined when no PowerShell resolves. It resolves command names on the
|
|
210
|
+
* foreground tool's PATH, pi's managed-tools directory first (piShellEnv):
|
|
211
|
+
* a `findstr.exe` there shadows Windows' own where the command runs. The
|
|
212
|
+
* background spawn's PATH lacks that directory, so a name resolved to
|
|
213
|
+
* System32 here resolves there too.
|
|
214
|
+
*/
|
|
215
|
+
export function powerShellParser(spec: ShellSpawn | undefined = powerShellSpawn()): PowerShellParser | undefined {
|
|
216
|
+
if (!spec) return undefined;
|
|
217
|
+
return sharedPowerShellParser(
|
|
218
|
+
spec.shell,
|
|
219
|
+
() =>
|
|
220
|
+
new PowerShellParser({
|
|
221
|
+
spawnServer: (bootstrap) =>
|
|
222
|
+
spawnShellCommand(spec, bootstrap, { env: piShellEnv(join(getAgentDir(), "bin")), stdio: ["pipe", "pipe", "pipe"] }),
|
|
223
|
+
}),
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
190
227
|
/** Whether the resolved PowerShell is Windows PowerShell 5.1 (`powershell.exe`) or PowerShell 7+ (`pwsh`). */
|
|
191
228
|
export function powerShellEdition(spawnSpec: ShellSpawn | undefined): "core" | "desktop" | "unknown" {
|
|
192
229
|
if (!spawnSpec) return "unknown";
|
|
@@ -204,16 +241,20 @@ export function powerShellEdition(spawnSpec: ShellSpawn | undefined): "core" | "
|
|
|
204
241
|
* shell's arguments (`bash -c <cmd>`, `pwsh … -Command <cmd>`); stdin transport
|
|
205
242
|
* writes it to the child's stdin (pi's legacy-WSL bash), which needs stdin
|
|
206
243
|
* piped — callers that must feed their own stdin (hooks) reject that form.
|
|
244
|
+
* The child's environment is `options.env` (default `process.env`) with the
|
|
245
|
+
* bundled app's launcher variables put back to the user's own values
|
|
246
|
+
* (app-launch.mjs childProcessEnv), so a `pi` run from a command is the user's pi.
|
|
207
247
|
*/
|
|
208
248
|
export function spawnShellCommand(spec: ShellSpawn, command: string, options: SpawnOptions): ChildProcess {
|
|
249
|
+
const env = childProcessEnv(options.env ?? process.env);
|
|
209
250
|
if (spec.commandTransport === "stdin") {
|
|
210
251
|
const stdio = Array.isArray(options.stdio) ? ["pipe", ...options.stdio.slice(1)] : ["pipe", "pipe", "pipe"];
|
|
211
|
-
const child = spawn(spec.shell, spec.args, { ...options, stdio: stdio as SpawnOptions["stdio"], windowsHide: true });
|
|
252
|
+
const child = spawn(spec.shell, spec.args, { ...options, env, stdio: stdio as SpawnOptions["stdio"], windowsHide: true });
|
|
212
253
|
child.stdin?.on("error", () => {});
|
|
213
254
|
child.stdin?.end(command);
|
|
214
255
|
return child;
|
|
215
256
|
}
|
|
216
|
-
return spawn(spec.shell, [...spec.args, command], { ...options, windowsHide: true });
|
|
257
|
+
return spawn(spec.shell, [...spec.args, command], { ...options, env, windowsHide: true });
|
|
217
258
|
}
|
|
218
259
|
|
|
219
260
|
// ---------------------------------------------------------------------------
|
|
@@ -228,6 +269,29 @@ export interface ShellOperations {
|
|
|
228
269
|
) => Promise<{ exitCode: number | null }>;
|
|
229
270
|
}
|
|
230
271
|
|
|
272
|
+
/**
|
|
273
|
+
* pi's shell environment (utils/shell.ts `getShellEnv`, not exported): `env`
|
|
274
|
+
* with pi's managed-tools directory (`<agentDir>/bin`) first on PATH, the PATH
|
|
275
|
+
* key found in any case.
|
|
276
|
+
*/
|
|
277
|
+
export function piShellEnv(binDir: string, env: NodeJS.ProcessEnv = process.env): NodeJS.ProcessEnv {
|
|
278
|
+
const pathKey = Object.keys(env).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
279
|
+
const current = env[pathKey] ?? "";
|
|
280
|
+
if (current.split(delimiter).includes(binDir)) return { ...env };
|
|
281
|
+
return { ...env, [pathKey]: [binDir, current].filter(Boolean).join(delimiter) };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* `operations` with every command's environment passed through
|
|
286
|
+
* childProcessEnv; `defaultEnv` stands in when the caller passes none (pi's
|
|
287
|
+
* user `!` commands never do, and its local operations then use its shell env).
|
|
288
|
+
*/
|
|
289
|
+
export function withChildProcessEnv(operations: ShellOperations, defaultEnv: () => NodeJS.ProcessEnv): ShellOperations {
|
|
290
|
+
return {
|
|
291
|
+
exec: (command, cwd, options) => operations.exec(command, cwd, { ...options, env: childProcessEnv(options.env ?? defaultEnv()) }),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
231
295
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
232
296
|
|
|
233
297
|
/** pi's timeout validation (seconds → ms), same messages. */
|
|
@@ -263,7 +327,7 @@ export function createPowerShellOperations(resolve: () => ShellSpawn | undefined
|
|
|
263
327
|
} catch {
|
|
264
328
|
throw new Error(`Working directory does not exist: ${cwd}\nCannot execute PowerShell commands.`);
|
|
265
329
|
}
|
|
266
|
-
const child = spawnShellCommand(spec, `${
|
|
330
|
+
const child = spawnShellCommand(spec, `${POWERSHELL_TOOL_PREFIX}${command}`, {
|
|
267
331
|
cwd,
|
|
268
332
|
...detachedSpawnOptions(),
|
|
269
333
|
env: env ?? process.env,
|
|
@@ -13,24 +13,34 @@
|
|
|
13
13
|
|
|
14
14
|
import { mkdirSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
|
-
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import type { ExtensionAPI, ExtensionContext, ExtensionToolContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
import type { Static, TObject } from "typebox";
|
|
18
18
|
import { generateTaskId, TASK_REGISTER_CHANNEL } from "../background/registry.ts";
|
|
19
19
|
import { type BashFinishSummary, runBackgroundBashBlocking, startBackgroundBash, tailCap } from "../bash/background.ts";
|
|
20
20
|
import { createTaskNotifier, oneShotNote, sessionOutlivesTurn, shellSummary, type TaskStatus, taskNotification } from "./notifications.ts";
|
|
21
21
|
import { commandToEvaluate, trackOriginalCommands } from "./original-command.ts";
|
|
22
|
-
import { persistIfLarge, sessionResultsDir } from "./persisted-output.ts";
|
|
22
|
+
import { persistedFilePreview, persistIfLarge, sessionResultsDir } from "./persisted-output.ts";
|
|
23
|
+
import { withClaudeCodeShellText } from "./shell-result.ts";
|
|
24
|
+
import { bashParserReady } from "./bash-parser.ts";
|
|
23
25
|
import type { ShellSpawn } from "./shell-spawn.ts";
|
|
26
|
+
import { keepSpillReadable } from "./spill-file.ts";
|
|
24
27
|
import { ccWrapBuiltinRenderers, linesComponent, resultLines } from "./tui-render.ts";
|
|
28
|
+
import { registerVariantTool } from "./tool-variants.ts";
|
|
25
29
|
|
|
26
30
|
// The completion notification carries status + exit code + where the output is,
|
|
27
31
|
// plus only a short tail: a finished build used to push 30 KB (~8k tokens) into
|
|
28
32
|
// context unasked (review T7). The full tail stays behind task_output / the log.
|
|
29
33
|
export const NOTIFY_OUTPUT_CAP = 2_000;
|
|
30
|
-
/**
|
|
31
|
-
export const
|
|
34
|
+
/** Claude Code's inline cap for a shell's output; past it the output is persisted (file + head preview), never cut. */
|
|
35
|
+
export const INLINE_OUTPUT_CAP = 30_000;
|
|
32
36
|
/** Claude Code's cap; the tools' `timeout` is milliseconds, as CC's is. */
|
|
33
37
|
export const MAX_TIMEOUT_MS = 600_000;
|
|
38
|
+
/**
|
|
39
|
+
* Claude Code's default for a command that names no `timeout`, which both
|
|
40
|
+
* tool descriptions promise. pi's executor has no default, so without this a
|
|
41
|
+
* foreground `npm run dev` ran until Esc, and forever in a `-p` run.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
34
44
|
|
|
35
45
|
/** The parameter shape both shell tools share (descriptions differ per tool). */
|
|
36
46
|
export interface ShellToolParams {
|
|
@@ -53,7 +63,7 @@ export interface ShellToolBase {
|
|
|
53
63
|
|
|
54
64
|
/** pi's shell tool executor: `execute(toolCallId, { command, timeout: seconds }, signal, onUpdate, ctx)`. */
|
|
55
65
|
export interface ShellToolExecutor {
|
|
56
|
-
execute: (toolCallId: string, params: { command: string; timeout?: number }, signal: AbortSignal | undefined, onUpdate: any, ctx:
|
|
66
|
+
execute: (toolCallId: string, params: { command: string; timeout?: number }, signal: AbortSignal | undefined, onUpdate: any, ctx: ExtensionToolContext) => Promise<any>;
|
|
57
67
|
}
|
|
58
68
|
|
|
59
69
|
/** What one shell tool's definition supplies; everything else is shared. */
|
|
@@ -98,10 +108,20 @@ export function finishLine(summary: BashFinishSummary, timeoutSeconds?: number):
|
|
|
98
108
|
return `failed (${detail ?? `exit code ${summary.exitCode}`})`;
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Why a one-shot `run_in_background` call was stopped: a one-shot session
|
|
113
|
+
* cannot keep a command running past the run, so the call is held to the
|
|
114
|
+
* foreground deadline.
|
|
115
|
+
*/
|
|
116
|
+
export function oneShotDeadlineNote(timeoutMs: number, explicit: boolean): string {
|
|
117
|
+
const which = explicit ? `its ${timeoutMs} ms timeout` : `the default ${timeoutMs} ms timeout (no \`timeout\` was given)`;
|
|
118
|
+
return `It was stopped at ${which}: a one-shot session cannot keep a background command running after the run ends, so a server or watcher started this way stops at the deadline. Pass a longer \`timeout\` (max ${MAX_TIMEOUT_MS}) for a long build.`;
|
|
119
|
+
}
|
|
120
|
+
|
|
101
121
|
/** `<sessionDir>/bash/<taskId>/output.log` — one spool location for every shell tool (the shell panel reads it). */
|
|
102
122
|
export function taskLogPath(ctx: ExtensionContext, taskId: string): string | undefined {
|
|
103
123
|
try {
|
|
104
|
-
const dir = join(ctx
|
|
124
|
+
const dir = join(sessionResultsDir(ctx), "bash", taskId);
|
|
105
125
|
mkdirSync(dir, { recursive: true });
|
|
106
126
|
return join(dir, "output.log");
|
|
107
127
|
} catch {
|
|
@@ -109,7 +129,13 @@ export function taskLogPath(ctx: ExtensionContext, taskId: string): string | und
|
|
|
109
129
|
}
|
|
110
130
|
}
|
|
111
131
|
|
|
112
|
-
|
|
132
|
+
/**
|
|
133
|
+
* Register the shell tool; the returned setter registers it again with a new
|
|
134
|
+
* description (a no-op when the text is unchanged), for a description that
|
|
135
|
+
* follows the session (lib/tool-variants.ts). Everything else — the notifier,
|
|
136
|
+
* the original-command tracker, execute — is built once.
|
|
137
|
+
*/
|
|
138
|
+
export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: ShellToolSpec<P>): (description: string) => void {
|
|
113
139
|
// A completion whose output task_output just returned is withdrawn (CC's
|
|
114
140
|
// delivered_as_tool_result) — the model already has it.
|
|
115
141
|
const notifyTask = createTaskNotifier(pi, { withdrawOnDelivery: true });
|
|
@@ -124,7 +150,7 @@ export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: She
|
|
|
124
150
|
// "Took Ns" that counted permission-prompt wait disappears.
|
|
125
151
|
const wrapped = ccWrapBuiltinRenderers<{ command?: string }>(spec.ccLabel, spec.base, { title: (a) => a?.command });
|
|
126
152
|
|
|
127
|
-
|
|
153
|
+
const definition = {
|
|
128
154
|
name: spec.name,
|
|
129
155
|
label: spec.base.label,
|
|
130
156
|
description: spec.description,
|
|
@@ -162,14 +188,24 @@ export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: She
|
|
|
162
188
|
if (guardReason) return { content: [{ type: "text" as const, text: guardReason }], isError: true, details: {} };
|
|
163
189
|
// pi's executor takes seconds; the model-facing unit is milliseconds
|
|
164
190
|
// (Claude Code's), so a habitual `timeout: 120000` is 2 minutes, not
|
|
165
|
-
// 33 hours (review T11).
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
: undefined;
|
|
191
|
+
// 33 hours (review T11). A command that names none gets Claude Code's
|
|
192
|
+
// default, which is what the descriptions promise.
|
|
193
|
+
const explicitTimeout = params.timeout !== undefined && Number.isFinite(params.timeout) && params.timeout > 0;
|
|
194
|
+
const timeoutSeconds = (explicitTimeout ? Math.min(params.timeout!, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS) / 1000;
|
|
170
195
|
|
|
171
196
|
if (!params.run_in_background) {
|
|
172
|
-
|
|
197
|
+
// pi spills a long output to os.tmpdir(), outside every readable root;
|
|
198
|
+
// move it where the model's read of it is a working-space read, and
|
|
199
|
+
// cut the output Claude Code's way (its head, not pi's tail). Then
|
|
200
|
+
// Claude Code's result text (`Exit code N` first, trimmed output).
|
|
201
|
+
const result = await keepSpillReadable(
|
|
202
|
+
() => spec.foreground(ctx.cwd).execute(toolCallId, { command: params.command, timeout: timeoutSeconds }, signal, onUpdate, ctx),
|
|
203
|
+
sessionResultsDir(ctx),
|
|
204
|
+
{ inlineMax: INLINE_OUTPUT_CAP, id: `${spec.name}-${toolCallId}` },
|
|
205
|
+
);
|
|
206
|
+
// The no-match check parses the command; only bash results need the grammar.
|
|
207
|
+
if (spec.name === "bash") await bashParserReady();
|
|
208
|
+
return withClaudeCodeShellText(result, spec.name, originalCommand);
|
|
173
209
|
}
|
|
174
210
|
|
|
175
211
|
const shell = spec.backgroundShell();
|
|
@@ -189,48 +225,61 @@ export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: She
|
|
|
189
225
|
// detached task would be orphaned, and its completion callback would
|
|
190
226
|
// call sendMessage on the disposed session and crash pi (measured,
|
|
191
227
|
// STEERING-REVIEW-2026-09-05 H3). Run blocking there — the same rule the
|
|
192
|
-
// Agent tool applies — and return the output in the result.
|
|
228
|
+
// Agent tool applies — and return the output in the result. The call
|
|
229
|
+
// holds the turn like a foreground command, so it gets the foreground
|
|
230
|
+
// deadline (the model's `timeout`, else the default): with none, a
|
|
231
|
+
// server started this way hung the run forever. A detached shell below
|
|
232
|
+
// keeps having no deadline (findings §31).
|
|
193
233
|
if (!sessionOutlivesTurn(ctx.mode)) {
|
|
194
234
|
const summary = await runBackgroundBashBlocking({ id, command, description, cwd: ctx.cwd, timeoutSeconds, logPath, shell }, signal);
|
|
195
|
-
const
|
|
235
|
+
const spool = summary.logPath && Buffer.byteLength(summary.output, "utf8") > INLINE_OUTPUT_CAP
|
|
236
|
+
? persistedFilePreview(summary.logPath)
|
|
237
|
+
: undefined;
|
|
238
|
+
const output = spool ?? persistIfLarge(summary.output, { dir: sessionResultsDir(ctx), id: `${spec.name}-${id}`, maxBytes: INLINE_OUTPUT_CAP });
|
|
239
|
+
const deadline = summary.timedOut ? ` ${oneShotDeadlineNote(timeoutSeconds * 1000, explicitTimeout)}` : "";
|
|
196
240
|
return {
|
|
197
241
|
content: [
|
|
198
242
|
{
|
|
199
243
|
type: "text" as const,
|
|
200
|
-
text: `${spec.ccLabel} task ${id} (${description}) ${finishLine(summary, timeoutSeconds)}. ${oneShotNote("command")}${logPath ? ` Log: ${logPath}.` : ""}\n\n${output}`,
|
|
244
|
+
text: `${spec.ccLabel} task ${id} (${description}) ${finishLine(summary, timeoutSeconds)}. ${oneShotNote("command")}${deadline}${summary.logPath ? ` Log: ${summary.logPath}.` : ""}\n\n${output}`,
|
|
201
245
|
},
|
|
202
246
|
],
|
|
203
247
|
// No taskId: nothing was registered behind task_output/task_stop, and
|
|
204
248
|
// its presence is what renderResult reads as "running in the background".
|
|
205
|
-
details: { logPath },
|
|
206
|
-
isError: summary.
|
|
249
|
+
details: { logPath: summary.logPath },
|
|
250
|
+
isError: shellFinish(summary, timeoutSeconds).status === "failed",
|
|
207
251
|
};
|
|
208
252
|
}
|
|
209
253
|
|
|
254
|
+
// No timeout here: Claude Code clears a command's timeout the
|
|
255
|
+
// moment it goes to the background, so a detached shell has no
|
|
256
|
+
// deadline (findings §31). Models habitually send `timeout: 600000`
|
|
257
|
+
// next to `run_in_background`, which used to kill a dev server at
|
|
258
|
+
// exactly 10 minutes. The one-shot path above keeps the timeout
|
|
259
|
+
// because there the command holds the turn.
|
|
210
260
|
const task = startBackgroundBash({
|
|
211
261
|
id,
|
|
212
262
|
command,
|
|
213
263
|
description,
|
|
214
264
|
cwd: ctx.cwd,
|
|
215
|
-
timeoutSeconds,
|
|
216
265
|
logPath,
|
|
217
266
|
shell,
|
|
218
267
|
onFinished: (finishedTask, summary) => {
|
|
219
268
|
// Claude Code's shell completion names the output file and nothing
|
|
220
269
|
// else — the model reads it (or task_output) when it needs the text.
|
|
221
270
|
// Without a spool file the tail rides `<result>` so nothing is lost.
|
|
222
|
-
const { status, detail } = shellFinish(summary
|
|
271
|
+
const { status, detail } = shellFinish(summary);
|
|
223
272
|
notify(
|
|
224
273
|
taskNotification({
|
|
225
274
|
kind: "shell",
|
|
226
275
|
taskId: id,
|
|
227
276
|
toolUseId: toolCallId,
|
|
228
|
-
outputFile: logPath,
|
|
277
|
+
outputFile: finishedTask.logPath,
|
|
229
278
|
status,
|
|
230
279
|
summary: shellSummary(description, status, summary.exitCode, detail),
|
|
231
|
-
result: logPath ? undefined : tailCap(summary.output, NOTIFY_OUTPUT_CAP).trim() || undefined,
|
|
280
|
+
result: finishedTask.logPath ? undefined : tailCap(summary.output, NOTIFY_OUTPUT_CAP).trim() || undefined,
|
|
232
281
|
}),
|
|
233
|
-
{ taskId: id, status: finishedTask.status, exitCode: summary.exitCode, logPath },
|
|
282
|
+
{ taskId: id, status: finishedTask.status, exitCode: summary.exitCode, logPath: finishedTask.logPath },
|
|
234
283
|
);
|
|
235
284
|
},
|
|
236
285
|
});
|
|
@@ -248,5 +297,6 @@ export function registerShellTool<P extends TObject>(pi: ExtensionAPI, spec: She
|
|
|
248
297
|
details: { taskId: id, logPath },
|
|
249
298
|
};
|
|
250
299
|
},
|
|
251
|
-
} as ToolDefinition<P
|
|
300
|
+
} as ToolDefinition<P>;
|
|
301
|
+
return registerVariantTool(pi, spec.description, (description) => ({ ...definition, description }) as unknown as ToolDefinition);
|
|
252
302
|
}
|