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
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PowerShell's own parser as a service: one long-lived parse server per
|
|
3
|
+
* process, run on the PowerShell the tool itself runs, so the gate reads a
|
|
4
|
+
* line exactly as it will execute (decisions/windows.md "The PowerShell
|
|
5
|
+
* pre-gate reads PowerShell's own parse"; the tree-sitter grammars it replaces
|
|
6
|
+
* are measured in findings §41).
|
|
7
|
+
*
|
|
8
|
+
* {@link PowerShellParser.parse} never rejects. Anything short of a clean
|
|
9
|
+
* answer, whether the server could not start, timed out, crashed, runs in
|
|
10
|
+
* Constrained Language Mode or stopped for good after too many crashes,
|
|
11
|
+
* resolves to `{ ok: false, reason }`, and the caller treats the line as
|
|
12
|
+
* unparsed (the classifier decides). A parse with errors is still `ok: true`:
|
|
13
|
+
* the tree and its `errors` are PowerShell's answer; judging them is the
|
|
14
|
+
* caller's job.
|
|
15
|
+
*
|
|
16
|
+
* Lifecycle. The server starts on the first request and stops after
|
|
17
|
+
* `idleMs` without one; the next request starts it again. A crash, or a
|
|
18
|
+
* request that times out (the server is then killed), counts toward
|
|
19
|
+
* `maxRestarts`; past it the parser stays unavailable for the life of the
|
|
20
|
+
* process. The child, its pipes and every timer are unref'd, so an idle
|
|
21
|
+
* server never holds a finished one-shot run open; a caller that awaits a
|
|
22
|
+
* parse in one holds its own ref (`withKeepAlive`). The server also exits on
|
|
23
|
+
* its own when this process ends, because its stdin closes.
|
|
24
|
+
*
|
|
25
|
+
* Pure: no pi imports. The caller supplies `spawnServer`, which starts the
|
|
26
|
+
* right executable with the bootstrap after `-Command`, so the BI fork can use
|
|
27
|
+
* its own launcher.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import type { ChildProcess } from "node:child_process";
|
|
31
|
+
import { POWERSHELL_PARSE_BOOTSTRAP, POWERSHELL_PARSE_WALKER } from "./powershell-parse-script.ts";
|
|
32
|
+
import { unrefHandle } from "./process-tree.ts";
|
|
33
|
+
|
|
34
|
+
/** One syntax-tree node, as the walker reports it (powershell-parse-script.ts). */
|
|
35
|
+
export interface PowerShellAstNode {
|
|
36
|
+
/** The .NET type name: `CommandAst`, `StringConstantExpressionAst`, … */
|
|
37
|
+
type: string;
|
|
38
|
+
/** Index of the parent node in `nodes`; -1 for the root. */
|
|
39
|
+
parent: number;
|
|
40
|
+
/** Extent as UTF-16 offsets into the command (JS string indices). */
|
|
41
|
+
start: number;
|
|
42
|
+
end: number;
|
|
43
|
+
/** CommandAst: the static command name (null when not static). CommandParameterAst: the parameter. VariableExpressionAst: the variable path. Type nodes: the type's full name. */
|
|
44
|
+
name?: string | null;
|
|
45
|
+
/** String and constant nodes: the value PowerShell resolved (quotes and escapes applied). */
|
|
46
|
+
value?: string | null;
|
|
47
|
+
/** String nodes: `BareWord`, `SingleQuoted`, `DoubleQuoted`, `SingleQuotedHereString`, `DoubleQuotedHereString`. */
|
|
48
|
+
kind?: string;
|
|
49
|
+
/** CommandAst: the invocation operator (`Unknown`, `Ampersand`, `Dot`). Operator nodes: the operator. */
|
|
50
|
+
operator?: string;
|
|
51
|
+
/** VariableExpressionAst: `@name` splatting. */
|
|
52
|
+
splatted?: boolean;
|
|
53
|
+
/** Redirections: the stream redirected (`Output`, `Error`, `All`, …) and, for a merge, where to. */
|
|
54
|
+
from?: string;
|
|
55
|
+
to?: string;
|
|
56
|
+
/** FileRedirectionAst: `>>`. */
|
|
57
|
+
append?: boolean;
|
|
58
|
+
/** Pipelines and pipeline chains: a trailing `&` (PowerShell 7). */
|
|
59
|
+
background?: boolean;
|
|
60
|
+
/** Member access: `::`. */
|
|
61
|
+
static?: boolean;
|
|
62
|
+
/** CommandAst with a static name that resolves in this PowerShell: the resolved command's type (`Cmdlet`, `Application`, `Function`, …), after an alias. */
|
|
63
|
+
commandType?: string;
|
|
64
|
+
/** CommandAst: the name was an alias. */
|
|
65
|
+
alias?: boolean;
|
|
66
|
+
/** CommandAst: the resolved cmdlet's name, or an application's full path. */
|
|
67
|
+
resolvedName?: string;
|
|
68
|
+
/** CommandAst: the resolved command's module (empty for an application). */
|
|
69
|
+
module?: string;
|
|
70
|
+
/**
|
|
71
|
+
* CommandAst of a cmdlet: PowerShell's static parameter binding. `value` is
|
|
72
|
+
* the bound argument's node index, -1 for a switch given alone, and -2 for a
|
|
73
|
+
* value the binder built itself: the arguments a remaining-arguments
|
|
74
|
+
* parameter collects (`Write-Output a b`), whose node indices are
|
|
75
|
+
* `elements` (-1 for one that matched no argument).
|
|
76
|
+
*/
|
|
77
|
+
bindings?: Array<{ parameter: string; value: number; elements?: number[] }>;
|
|
78
|
+
/** CommandAst of a cmdlet: the arguments or parameters the binder could not bind (an unknown or ambiguous name, a surplus positional, a repeat). */
|
|
79
|
+
bindingErrors?: string[];
|
|
80
|
+
/** CommandAst of a cmdlet: the static binder is missing or threw. */
|
|
81
|
+
bindingUnavailable?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface PowerShellParse {
|
|
85
|
+
/** The tree in pre-order, root first. */
|
|
86
|
+
nodes: PowerShellAstNode[];
|
|
87
|
+
/** Each parse error's ErrorId. PowerShell runs nothing from a line that has one. */
|
|
88
|
+
errors: string[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export type PowerShellParseOutcome = { ok: true; parse: PowerShellParse } | { ok: false; reason: string };
|
|
92
|
+
|
|
93
|
+
export interface PowerShellParserOptions {
|
|
94
|
+
/** Start the server: the PowerShell executable with `bootstrap` as the argument after `-Command`, stdio all piped. */
|
|
95
|
+
spawnServer: (bootstrap: string) => ChildProcess;
|
|
96
|
+
/** How long the first request waits while the server starts. */
|
|
97
|
+
startTimeoutMs?: number;
|
|
98
|
+
/** How long a request waits once the server is ready. */
|
|
99
|
+
requestTimeoutMs?: number;
|
|
100
|
+
/** Crashes and timeouts tolerated before the parser stays unavailable. */
|
|
101
|
+
maxRestarts?: number;
|
|
102
|
+
/** Stop the server after this long without a request. */
|
|
103
|
+
idleMs?: number;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const POWERSHELL_PARSE_START_TIMEOUT_MS = 5_000;
|
|
107
|
+
export const POWERSHELL_PARSE_REQUEST_TIMEOUT_MS = 2_000;
|
|
108
|
+
export const POWERSHELL_PARSE_MAX_RESTARTS = 3;
|
|
109
|
+
export const POWERSHELL_PARSE_IDLE_MS = 10 * 60_000;
|
|
110
|
+
|
|
111
|
+
interface Pending {
|
|
112
|
+
resolve: (outcome: PowerShellParseOutcome) => void;
|
|
113
|
+
timer: ReturnType<typeof setTimeout>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
interface Server {
|
|
117
|
+
child: ChildProcess;
|
|
118
|
+
ready: boolean;
|
|
119
|
+
/** Set once the server is gone or being stopped; its late output and exit are ignored. */
|
|
120
|
+
stopped: boolean;
|
|
121
|
+
/** Why it stopped. */
|
|
122
|
+
reason?: string;
|
|
123
|
+
buffer: string;
|
|
124
|
+
pending: Map<number, Pending>;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const encode = (text: string) => Buffer.from(text, "utf8").toString("base64");
|
|
128
|
+
|
|
129
|
+
export class PowerShellParser {
|
|
130
|
+
private readonly options: Required<PowerShellParserOptions>;
|
|
131
|
+
private server: Server | undefined;
|
|
132
|
+
private nextId = 1;
|
|
133
|
+
private failures = 0;
|
|
134
|
+
/** Why the parser is unavailable for good; set past `maxRestarts`, or when the server runs in a restricted language mode. */
|
|
135
|
+
private disabled: string | undefined;
|
|
136
|
+
private idleTimer: ReturnType<typeof setTimeout> | undefined;
|
|
137
|
+
|
|
138
|
+
constructor(options: PowerShellParserOptions) {
|
|
139
|
+
this.options = {
|
|
140
|
+
startTimeoutMs: POWERSHELL_PARSE_START_TIMEOUT_MS,
|
|
141
|
+
requestTimeoutMs: POWERSHELL_PARSE_REQUEST_TIMEOUT_MS,
|
|
142
|
+
maxRestarts: POWERSHELL_PARSE_MAX_RESTARTS,
|
|
143
|
+
idleMs: POWERSHELL_PARSE_IDLE_MS,
|
|
144
|
+
...options,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Why the parser cannot answer at all, or undefined while it can (or can still start). */
|
|
149
|
+
unavailable(): string | undefined {
|
|
150
|
+
return this.disabled;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Parse `command` with PowerShell's own parser. Never rejects. */
|
|
154
|
+
parse(command: string): Promise<PowerShellParseOutcome> {
|
|
155
|
+
if (this.disabled) return Promise.resolve({ ok: false, reason: this.disabled });
|
|
156
|
+
let server: Server;
|
|
157
|
+
try {
|
|
158
|
+
server = this.server ?? this.start();
|
|
159
|
+
} catch (error) {
|
|
160
|
+
return Promise.resolve({ ok: false, reason: this.fail(`PowerShell could not start: ${message(error)}`) });
|
|
161
|
+
}
|
|
162
|
+
// A server can fail while it starts (a spawn error, a restricted language
|
|
163
|
+
// mode) before this request is registered with it.
|
|
164
|
+
if (server.stopped) return Promise.resolve({ ok: false, reason: this.disabled ?? server.reason ?? "the PowerShell parser stopped" });
|
|
165
|
+
this.touch();
|
|
166
|
+
const id = this.nextId++;
|
|
167
|
+
return new Promise((resolve) => {
|
|
168
|
+
const timeoutMs = server.ready ? this.options.requestTimeoutMs : this.options.startTimeoutMs;
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
server.pending.delete(id);
|
|
171
|
+
resolve({ ok: false, reason: `the PowerShell parser did not answer within ${timeoutMs} ms` });
|
|
172
|
+
this.crashed(server, `the PowerShell parser timed out after ${timeoutMs} ms`);
|
|
173
|
+
}, timeoutMs);
|
|
174
|
+
unrefHandle(timer);
|
|
175
|
+
server.pending.set(id, { resolve, timer });
|
|
176
|
+
server.child.stdin?.write(`${id} ${encode(command)}\n`);
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Stop the server. The next request starts a new one. */
|
|
181
|
+
stop(): void {
|
|
182
|
+
const server = this.server;
|
|
183
|
+
if (!server) return;
|
|
184
|
+
this.server = undefined;
|
|
185
|
+
this.clearIdle();
|
|
186
|
+
this.settleAll(server, "the PowerShell parser was stopped");
|
|
187
|
+
server.stopped = true;
|
|
188
|
+
server.child.stdin?.end();
|
|
189
|
+
server.child.kill();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
private start(): Server {
|
|
193
|
+
const child = this.options.spawnServer(POWERSHELL_PARSE_BOOTSTRAP);
|
|
194
|
+
const server: Server = { child, ready: false, stopped: false, buffer: "", pending: new Map() };
|
|
195
|
+
this.server = server;
|
|
196
|
+
child.stdin?.on("error", () => {});
|
|
197
|
+
child.stdout?.setEncoding("ascii");
|
|
198
|
+
child.stdout?.on("data", (chunk: string) => this.receive(server, chunk));
|
|
199
|
+
// PowerShell's own error output is not part of the protocol; drain it so
|
|
200
|
+
// the pipe never fills.
|
|
201
|
+
child.stderr?.on("data", () => {});
|
|
202
|
+
child.on("error", (error) => this.crashed(server, `PowerShell could not start: ${message(error)}`));
|
|
203
|
+
child.on("exit", (code, signal) => this.crashed(server, `the PowerShell parser exited (${signal ?? `code ${code}`})`));
|
|
204
|
+
unrefHandle(child);
|
|
205
|
+
unrefHandle(child.stdin);
|
|
206
|
+
unrefHandle(child.stdout);
|
|
207
|
+
unrefHandle(child.stderr);
|
|
208
|
+
child.stdin?.write(`${encode(POWERSHELL_PARSE_WALKER)}\n`);
|
|
209
|
+
return server;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
private receive(server: Server, chunk: string): void {
|
|
213
|
+
if (server.stopped) return;
|
|
214
|
+
server.buffer += chunk;
|
|
215
|
+
for (let newline = server.buffer.indexOf("\n"); newline >= 0; newline = server.buffer.indexOf("\n")) {
|
|
216
|
+
const line = server.buffer.slice(0, newline).trim();
|
|
217
|
+
server.buffer = server.buffer.slice(newline + 1);
|
|
218
|
+
if (line) this.record(server, line);
|
|
219
|
+
if (server.stopped) return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
private record(server: Server, line: string): void {
|
|
224
|
+
let record: Record<string, unknown>;
|
|
225
|
+
try {
|
|
226
|
+
record = JSON.parse(Buffer.from(line, "base64").toString("utf8")) as Record<string, unknown>;
|
|
227
|
+
} catch {
|
|
228
|
+
this.crashed(server, "the PowerShell parser wrote a line that is not part of its protocol");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (record.id === 0) {
|
|
232
|
+
if (record.language !== "FullLanguage") {
|
|
233
|
+
// Constrained Language Mode (or stricter) is a machine policy, so a
|
|
234
|
+
// restart cannot change it.
|
|
235
|
+
this.disabled = `PowerShell runs in ${String(record.language)} mode, where the parser is not available`;
|
|
236
|
+
this.crashed(server, this.disabled);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
// Failures are not reset here: a line that hangs the walker would
|
|
240
|
+
// otherwise restart it forever.
|
|
241
|
+
server.ready = true;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const pending = typeof record.id === "number" ? server.pending.get(record.id) : undefined;
|
|
245
|
+
if (!pending) return;
|
|
246
|
+
server.pending.delete(record.id as number);
|
|
247
|
+
clearTimeout(pending.timer);
|
|
248
|
+
if (typeof record.failure === "string") {
|
|
249
|
+
pending.resolve({ ok: false, reason: `the PowerShell parser failed: ${record.failure}` });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
pending.resolve({ ok: true, parse: { nodes: asArray(record.nodes).map(normalizeNode), errors: asArray(record.errors).map(String) } });
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The server is gone or unusable: settle what waits on it and count the failure. */
|
|
256
|
+
private crashed(server: Server, reason: string): void {
|
|
257
|
+
if (server.stopped) return;
|
|
258
|
+
server.stopped = true;
|
|
259
|
+
server.reason = reason;
|
|
260
|
+
if (this.server === server) this.server = undefined;
|
|
261
|
+
this.clearIdle();
|
|
262
|
+
this.settleAll(server, reason);
|
|
263
|
+
server.child.stdin?.end();
|
|
264
|
+
server.child.kill();
|
|
265
|
+
this.fail(reason);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
private fail(reason: string): string {
|
|
269
|
+
this.failures++;
|
|
270
|
+
if (!this.disabled && this.failures > this.options.maxRestarts) {
|
|
271
|
+
this.disabled = `the PowerShell parser is off after ${this.failures} failures (last: ${reason})`;
|
|
272
|
+
}
|
|
273
|
+
return reason;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
private settleAll(server: Server, reason: string): void {
|
|
277
|
+
for (const pending of server.pending.values()) {
|
|
278
|
+
clearTimeout(pending.timer);
|
|
279
|
+
pending.resolve({ ok: false, reason });
|
|
280
|
+
}
|
|
281
|
+
server.pending.clear();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private touch(): void {
|
|
285
|
+
this.clearIdle();
|
|
286
|
+
this.idleTimer = setTimeout(() => this.stop(), this.options.idleMs);
|
|
287
|
+
unrefHandle(this.idleTimer);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
private clearIdle(): void {
|
|
291
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
292
|
+
this.idleTimer = undefined;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* A node with its list fields made lists: ConvertTo-Json may write a
|
|
298
|
+
* one-element list as the element. A binding list that is not one after
|
|
299
|
+
* that marks the binding unavailable, which the gate refuses.
|
|
300
|
+
*/
|
|
301
|
+
function normalizeNode(value: unknown): PowerShellAstNode {
|
|
302
|
+
const node = { ...(value as PowerShellAstNode) };
|
|
303
|
+
if (node.bindingErrors !== undefined) node.bindingErrors = asArray(node.bindingErrors).map(String);
|
|
304
|
+
if (node.bindings !== undefined) {
|
|
305
|
+
const bindings = asArray(node.bindings) as Array<{ parameter?: unknown; value?: unknown; elements?: unknown }>;
|
|
306
|
+
if (bindings.every((b) => b !== null && typeof b === "object" && typeof b.parameter === "string" && typeof b.value === "number")) {
|
|
307
|
+
node.bindings = bindings.map((b) => ({ parameter: b.parameter as string, value: b.value as number, ...(b.elements === undefined ? {} : { elements: asArray(b.elements).map(Number) }) }));
|
|
308
|
+
} else {
|
|
309
|
+
delete node.bindings;
|
|
310
|
+
node.bindingUnavailable = true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return node;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function asArray(value: unknown): unknown[] {
|
|
317
|
+
if (Array.isArray(value)) return value;
|
|
318
|
+
// ConvertTo-Json writes a one-element array as that element on some
|
|
319
|
+
// PowerShell versions, and an empty one as null.
|
|
320
|
+
return value === undefined || value === null ? [] : [value];
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function message(error: unknown): string {
|
|
324
|
+
return error instanceof Error ? error.message : String(error);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const STATE_KEY = Symbol.for("one-code.powershell-parser");
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The process-wide parser for one PowerShell executable. It lives on
|
|
331
|
+
* `globalThis`, not in module state: jiti gives each extension, and each
|
|
332
|
+
* in-process child session, its own instance of this module (findings §3), and
|
|
333
|
+
* one server per process is the point.
|
|
334
|
+
*/
|
|
335
|
+
export function sharedPowerShellParser(executable: string, create: () => PowerShellParser): PowerShellParser {
|
|
336
|
+
const store = globalThis as { [STATE_KEY]?: Map<string, PowerShellParser> };
|
|
337
|
+
const parsers = (store[STATE_KEY] ??= new Map());
|
|
338
|
+
let parser = parsers.get(executable);
|
|
339
|
+
if (!parser) {
|
|
340
|
+
parser = create();
|
|
341
|
+
parsers.set(executable, parser);
|
|
342
|
+
}
|
|
343
|
+
return parser;
|
|
344
|
+
}
|
|
@@ -14,9 +14,22 @@
|
|
|
14
14
|
* The producer spawns `detached` (own process group; `detachedSpawnOptions`)
|
|
15
15
|
* and stops it with `killProcessTree`, which signals the negative pid — the
|
|
16
16
|
* whole group — falling back to the child alone when the group is already gone.
|
|
17
|
-
* `stopProcessTree` adds the grace period: SIGTERM now, SIGKILL
|
|
18
|
-
*
|
|
19
|
-
* which
|
|
17
|
+
* `stopProcessTree` adds the grace period: SIGTERM now, SIGKILL to the group
|
|
18
|
+
* when it ends, whether or not the leader is still alive. The leader is often
|
|
19
|
+
* only the `bash -c` wrapper, which dies on TERM at once while the command it
|
|
20
|
+
* forked (a dev server in its graceful-shutdown handler) keeps the group and
|
|
21
|
+
* its port; a leader-only check skipped the SIGKILL exactly then
|
|
22
|
+
* (LIFECYCLE-BACKGROUND-REVIEW-2026-09-26 M1, measured). A group whose SIGKILL
|
|
23
|
+
* is still pending when the process exits (pi's quit calls `process.exit`,
|
|
24
|
+
* and the grace timer is unref'd) is SIGKILLed from a `process.on("exit")`
|
|
25
|
+
* hook, so nothing One Code stopped outlives it.
|
|
26
|
+
*
|
|
27
|
+
* `rememberProcessGroup` owns a background shell's group for the lifetime of
|
|
28
|
+
* this process, including descendants left by a successfully exited leader.
|
|
29
|
+
* Session replacement must not kill those intentionally launched daemons.
|
|
30
|
+
* Each module instance keeps its own groups through process listeners (never
|
|
31
|
+
* through a session context or cross-extension module state), reaps empty
|
|
32
|
+
* groups, and sends TERM synchronously on process exit or termination signals.
|
|
20
33
|
*
|
|
21
34
|
* `waitForChildExit` is the counterpart on the waiting side: it settles on
|
|
22
35
|
* `exit` plus a short stdio grace, never on `close` alone — a descendant the
|
|
@@ -37,6 +50,16 @@ export const EXIT_STDIO_GRACE_MS = 200;
|
|
|
37
50
|
/** ...and the most it may keep arriving in total: a straggler writing continuously must not keep re-arming the grace. */
|
|
38
51
|
export const EXIT_STDIO_MAX_MS = 2_000;
|
|
39
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Unref a child process, one of its stdio streams or a timer, so it never
|
|
55
|
+
* holds the event loop open on its own (a finished one-shot run can exit);
|
|
56
|
+
* a caller awaiting it holds its own ref. Child pipes are sockets at run
|
|
57
|
+
* time and carry `unref()` even though their stream types do not declare it.
|
|
58
|
+
*/
|
|
59
|
+
export function unrefHandle(handle: unknown): void {
|
|
60
|
+
(handle as { unref?: () => void } | null | undefined)?.unref?.();
|
|
61
|
+
}
|
|
62
|
+
|
|
40
63
|
/**
|
|
41
64
|
* `detached` everywhere but Windows, where process groups do not exist — except
|
|
42
65
|
* for a child that must outlive an exiting parent (a fire-and-forget hook),
|
|
@@ -95,20 +118,120 @@ export function killProcessTree(child: ChildProcess, signal: NodeJS.Signals = "S
|
|
|
95
118
|
}
|
|
96
119
|
}
|
|
97
120
|
|
|
121
|
+
/** Groups owned by this producer, retained by process listeners across session replacement. */
|
|
122
|
+
const rememberedGroups = new Set<ChildProcess>();
|
|
123
|
+
let groupReaper: NodeJS.Timeout | undefined;
|
|
124
|
+
const terminationSignals = ["SIGINT", "SIGTERM", "SIGHUP"] as const;
|
|
125
|
+
const groupSignalHandlers = new Map<NodeJS.Signals, () => void>();
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Whether a remembered tree still needs killing at exit. Windows has no
|
|
129
|
+
* process groups, so only a live leader is worth a `taskkill /T`; it is judged
|
|
130
|
+
* by the child's own exit state, because once the leader exits Windows may
|
|
131
|
+
* hand its pid to an unrelated process that a pid probe would then find. On
|
|
132
|
+
* POSIX a group id stays reserved while any member lives, and the reaper
|
|
133
|
+
* forgets an emptied group within a second, far sooner than sequential pid
|
|
134
|
+
* allocation wraps around to it.
|
|
135
|
+
*/
|
|
136
|
+
export function rememberedGroupAlive(child: ChildProcess): boolean {
|
|
137
|
+
if (child.pid == null) return false;
|
|
138
|
+
if (process.platform === "win32") return child.exitCode === null && child.signalCode === null;
|
|
139
|
+
try {
|
|
140
|
+
process.kill(-child.pid, 0);
|
|
141
|
+
return true;
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return (error as NodeJS.ErrnoException).code !== "ESRCH";
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function releaseGroupListeners(): void {
|
|
148
|
+
if (groupReaper) clearInterval(groupReaper);
|
|
149
|
+
groupReaper = undefined;
|
|
150
|
+
process.removeListener("exit", terminateRememberedGroups);
|
|
151
|
+
for (const [signal, handler] of groupSignalHandlers) process.removeListener(signal, handler);
|
|
152
|
+
groupSignalHandlers.clear();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Exit handlers cannot await a grace period. Existing explicit stops retain their TERM/KILL escalation below. */
|
|
156
|
+
function terminateRememberedGroups(): void {
|
|
157
|
+
for (const child of rememberedGroups) {
|
|
158
|
+
if (rememberedGroupAlive(child)) killProcessTree(child, "SIGTERM");
|
|
159
|
+
}
|
|
160
|
+
rememberedGroups.clear();
|
|
161
|
+
releaseGroupListeners();
|
|
162
|
+
}
|
|
163
|
+
|
|
98
164
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
165
|
+
* Remember a detached background tree from spawn until its group becomes empty.
|
|
166
|
+
* Register before awaiting its leader, so even immediate process exit covers
|
|
167
|
+
* running work. The unref'd reaper cannot keep a print run alive. No session
|
|
168
|
+
* shutdown handler touches this set; process listeners keep its module instance
|
|
169
|
+
* alive even when jiti builds a new extension instance after /clear.
|
|
170
|
+
*/
|
|
171
|
+
export function rememberProcessGroup(child: ChildProcess): void {
|
|
172
|
+
if (child.pid == null || rememberedGroups.has(child)) return;
|
|
173
|
+
rememberedGroups.add(child);
|
|
174
|
+
if (groupReaper) return;
|
|
175
|
+
process.on("exit", terminateRememberedGroups);
|
|
176
|
+
for (const signal of terminationSignals) {
|
|
177
|
+
const handler = () => {
|
|
178
|
+
terminateRememberedGroups();
|
|
179
|
+
// Do not swallow Node's default signal termination when there is no
|
|
180
|
+
// pi handler. With isolated producers, the last listener re-raises it.
|
|
181
|
+
if (process.listenerCount(signal) === 0) process.kill(process.pid, signal);
|
|
182
|
+
};
|
|
183
|
+
groupSignalHandlers.set(signal, handler);
|
|
184
|
+
// Run before signal-exit observers: after we remove ourselves they must
|
|
185
|
+
// still see the original listener count and preserve default termination.
|
|
186
|
+
process.prependOnceListener(signal, handler);
|
|
187
|
+
}
|
|
188
|
+
groupReaper = setInterval(() => {
|
|
189
|
+
for (const child of rememberedGroups) {
|
|
190
|
+
if (!rememberedGroupAlive(child)) rememberedGroups.delete(child);
|
|
191
|
+
}
|
|
192
|
+
if (rememberedGroups.size === 0) releaseGroupListeners();
|
|
193
|
+
}, 1_000);
|
|
194
|
+
groupReaper.unref?.();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Trees sent SIGTERM whose SIGKILL has not run yet. Keyed by the child, not
|
|
199
|
+
* its pid, so a pid the OS hands to a new child during the grace does not
|
|
200
|
+
* make that child look already scheduled. Per module instance on purpose
|
|
201
|
+
* (each extension force-kills its own children); one `exit` listener per
|
|
202
|
+
* instance, installed on first use.
|
|
203
|
+
*/
|
|
204
|
+
const pendingKills = new Set<ChildProcess>();
|
|
205
|
+
let exitHookInstalled = false;
|
|
206
|
+
|
|
207
|
+
/** The process is exiting: the grace timers will never fire, so force-kill every tree still pending. */
|
|
208
|
+
function killPendingAtExit(): void {
|
|
209
|
+
for (const child of pendingKills) killProcessTree(child, "SIGKILL");
|
|
210
|
+
pendingKills.clear();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* SIGTERM the tree, then SIGKILL the whole group after `graceMs`, whatever
|
|
215
|
+
* the leader's state: a group already gone makes that an ESRCH, and a live
|
|
216
|
+
* group keeps its id reserved, so the id cannot name anyone else. The timer
|
|
217
|
+
* is unref'd (it must never be what keeps a one-shot process alive); a
|
|
218
|
+
* process exit before it fires runs the SIGKILL from the `exit` hook.
|
|
101
219
|
*/
|
|
102
220
|
export function stopProcessTree(child: ChildProcess, graceMs: number): void {
|
|
103
221
|
killProcessTree(child, "SIGTERM");
|
|
104
222
|
// taskkill /F is already forceful; there is no gentler first signal to grace.
|
|
105
223
|
if (process.platform === "win32") return;
|
|
106
|
-
if (child.
|
|
224
|
+
if (child.pid == null || pendingKills.has(child)) return;
|
|
225
|
+
pendingKills.add(child);
|
|
226
|
+
if (!exitHookInstalled) {
|
|
227
|
+
exitHookInstalled = true;
|
|
228
|
+
process.on("exit", killPendingAtExit);
|
|
229
|
+
}
|
|
107
230
|
const timer = setTimeout(() => {
|
|
108
|
-
if (
|
|
231
|
+
if (!pendingKills.delete(child)) return;
|
|
232
|
+
killProcessTree(child, "SIGKILL");
|
|
109
233
|
}, graceMs);
|
|
110
234
|
timer.unref?.();
|
|
111
|
-
child.once("exit", () => clearTimeout(timer));
|
|
112
235
|
}
|
|
113
236
|
|
|
114
237
|
export interface ChildExit {
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi's system-prompt options for a session no `prompt()` has run in yet.
|
|
3
|
+
*
|
|
4
|
+
* A turn an extension opens from idle skips `before_agent_start`, the one
|
|
5
|
+
* event that carries the options (upstream ask #17). After the first typed
|
|
6
|
+
* prompt the extensions rebuild from what that event saw (system-prompt's
|
|
7
|
+
* idle-turn.ts). Before it they have nothing, so a background completion that
|
|
8
|
+
* opens a session's first turn used to go out as a user message, which takes
|
|
9
|
+
* the prompt path and renders as a user bubble.
|
|
10
|
+
*
|
|
11
|
+
* A command handler's context does carry the options
|
|
12
|
+
* (`getSystemPromptOptions`, the same base object `before_agent_start`
|
|
13
|
+
* receives). So the command that starts background work announces them here:
|
|
14
|
+
* system-prompt builds the prompt from them, the skill extension lists its
|
|
15
|
+
* skills, and the notifier opens the turn with the notification itself as a
|
|
16
|
+
* custom message, rendered like every later one. Pure: no pi imports.
|
|
17
|
+
*
|
|
18
|
+
* The same module carries the fork's system-prompt request (below).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export const PROMPT_OPTIONS_CHANNEL = "one-code:prompt-options";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Whether a `before_agent_start` event opens a run the user typed. pi 1.2.0
|
|
25
|
+
* also fires the event for a run an extension opens from idle with
|
|
26
|
+
* `sendMessage(…, { triggerTurn: true })` (a notification, the Stop hook's
|
|
27
|
+
* continuation, a scheduled fire), marked `messageType: "custom"` and without
|
|
28
|
+
* `prompt`. Older pi fires it only for prompts and sends no `messageType`.
|
|
29
|
+
*/
|
|
30
|
+
export function isUserRun<E>(event: E): event is Exclude<E, { messageType: "custom" }> {
|
|
31
|
+
return (event as { messageType?: unknown } | undefined)?.messageType !== "custom";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The payload: pi's base options, and the session cwd they were read in. */
|
|
35
|
+
export interface PromptOptionsAnnouncement {
|
|
36
|
+
options: unknown;
|
|
37
|
+
cwd: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Emit the options of a command context. A context without
|
|
42
|
+
* `getSystemPromptOptions` (an event handler's, or an older pi) announces
|
|
43
|
+
* nothing, and the listeners keep their first-turn fallbacks.
|
|
44
|
+
*/
|
|
45
|
+
export function announcePromptOptions(events: { emit(channel: string, data: unknown): void }, ctx: unknown): void {
|
|
46
|
+
const context = ctx as { getSystemPromptOptions?: () => unknown; cwd?: string } | undefined;
|
|
47
|
+
if (typeof context?.getSystemPromptOptions !== "function") return;
|
|
48
|
+
let options: unknown;
|
|
49
|
+
try {
|
|
50
|
+
options = context.getSystemPromptOptions();
|
|
51
|
+
} catch {
|
|
52
|
+
return; // A torn-down session: nothing to announce.
|
|
53
|
+
}
|
|
54
|
+
if (!options || typeof options !== "object") return;
|
|
55
|
+
events.emit(PROMPT_OPTIONS_CHANNEL, { options, cwd: context.cwd ?? "" } satisfies PromptOptionsAnnouncement);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The system prompt a fork inherits. `ctx.getSystemPrompt()` is pi's own
|
|
60
|
+
* prompt whenever One Code's is not installed on pi's run: between turns (pi
|
|
61
|
+
* drops a run's options when it settles), before the first prompt, and in a
|
|
62
|
+
* turn opened from idle (installed on the wire only, by context_with_system).
|
|
63
|
+
* A fork started then ran on pi's stock prompt. The system-prompt extension
|
|
64
|
+
* answers this request synchronously with the prompt the session's next
|
|
65
|
+
* request carries; no answer (not loaded, a named agent's own prompt, nothing
|
|
66
|
+
* to build from) leaves the caller's fallback.
|
|
67
|
+
*/
|
|
68
|
+
export const SYSTEM_PROMPT_REQUEST_CHANNEL = "one-code:system-prompt-request";
|
|
69
|
+
|
|
70
|
+
export interface SystemPromptRequest {
|
|
71
|
+
/** The requester's context: cwd and model shape the prompt. */
|
|
72
|
+
ctx: unknown;
|
|
73
|
+
/** Set by the listener before it returns. */
|
|
74
|
+
prompt?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Ask for One Code's system prompt; `undefined` when nobody answered. */
|
|
78
|
+
export function requestSystemPrompt(events: { emit(channel: string, data: unknown): void }, ctx: unknown): string | undefined {
|
|
79
|
+
const request: SystemPromptRequest = { ctx };
|
|
80
|
+
events.emit(SYSTEM_PROMPT_REQUEST_CHANNEL, request);
|
|
81
|
+
return typeof request.prompt === "string" ? request.prompt : undefined;
|
|
82
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hold something for a message the user queued mid-turn until pi delivers it.
|
|
3
|
+
*
|
|
4
|
+
* Since pi 0.86 the `input` event also fires for a message queued while a turn
|
|
5
|
+
* runs (`streamingBehavior` set; interactive queue, RPC steer/follow_up). A
|
|
6
|
+
* one-shot reminder emitted there is taken by whatever the model reads next,
|
|
7
|
+
* usually the running tool's result, so it lands before the message it is
|
|
8
|
+
* about, a request or more early for a follow-up. A context message of its
|
|
9
|
+
* own is no better: pi drains one queued message per request, so it arrives a
|
|
10
|
+
* request late. Instead the caller holds the payload here, keyed by the typed
|
|
11
|
+
* text, and releases it from `message_start` when pi delivers the user message
|
|
12
|
+
* with that text (compare with pi-ai's `contentText(content, "")`, the
|
|
13
|
+
* comparison pi uses for its own queue). pi awaits `message_start` before the
|
|
14
|
+
* request that carries the message, so a `last-append` one-shot emitted then
|
|
15
|
+
* is pinned to that message.
|
|
16
|
+
*
|
|
17
|
+
* A queued message pi expands first (a prompt template) never matches, nor
|
|
18
|
+
* does one an extension replaces, unless its replacement carries the typed
|
|
19
|
+
* text (the skill extension's `details.input`, which hooks matches). Clear on `agent_settled` so its payload
|
|
20
|
+
* never rides a later, unrelated prompt, and on `session_start`.
|
|
21
|
+
*
|
|
22
|
+
* Pure (no pi imports); each extension keeps its own instance.
|
|
23
|
+
*/
|
|
24
|
+
export class QueuedDelivery<T> {
|
|
25
|
+
private entries: Array<{ text: string; payload: T }> = [];
|
|
26
|
+
|
|
27
|
+
hold(text: string, payload: T): void {
|
|
28
|
+
this.entries.push({ text, payload });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** The payload held for the delivered message with this text, oldest first; removed once released. */
|
|
32
|
+
release(text: string): T | undefined {
|
|
33
|
+
const index = this.entries.findIndex((entry) => entry.text === text);
|
|
34
|
+
return index === -1 ? undefined : this.entries.splice(index, 1)[0].payload;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get isEmpty(): boolean {
|
|
38
|
+
return this.entries.length === 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clear(): void {
|
|
42
|
+
this.entries = [];
|
|
43
|
+
}
|
|
44
|
+
}
|