one-code-extension 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -11,37 +11,44 @@
|
|
|
11
11
|
* silently selected `anthropic/claude-haiku-4-5` — shipping that user's prompts to
|
|
12
12
|
* a vendor they had not chosen for this session, through a component with no UI.
|
|
13
13
|
*
|
|
14
|
-
* So: **never leave the session's provider
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* then a configured default, then something sensible in-provider, then the session
|
|
21
|
-
* model itself.
|
|
14
|
+
* So: **never leave the session's provider or route.** Automatic selection uses
|
|
15
|
+
* only contained models. It also requires a catalog context window at least as
|
|
16
|
+
* large as the session's, because the classifier receives the full transcript;
|
|
17
|
+
* if that cannot be proved, it screens on the session model instead. And it
|
|
18
|
+
* never screens with a model in a lower tier than the session's, or one that is
|
|
19
|
+
* not strictly cheaper per classifier call (a tie keeps the session).
|
|
22
20
|
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
* "Cheaper" is judged on what a classifier call costs, not on list input
|
|
22
|
+
* price: a call is mostly cached input (`CLASSIFIER_CALL_TOKENS`), so the
|
|
23
|
+
* cache-read price dominates. By input price DeepSeek V3.2 screened its own
|
|
24
|
+
* sessions and timed out (findings §61); per call V4.1 Flash is cheaper and
|
|
25
|
+
* answers in seconds.
|
|
26
|
+
*
|
|
27
|
+
* An explicit `autoMode.classifierModel` may name any provider, because naming
|
|
28
|
+
* it is choosing it: it leads the chain, with notices for a cross-provider
|
|
29
|
+
* choice and warnings for what it costs (`lib/model-choice-warnings.ts`).
|
|
27
30
|
*/
|
|
28
31
|
|
|
29
32
|
import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
33
|
+
import { type ChosenModelWarning, chosenModelWarnings } from "../lib/model-choice-warnings.ts";
|
|
30
34
|
import {
|
|
31
35
|
crossesProvider,
|
|
32
36
|
findConfigured,
|
|
37
|
+
hasCatalogContextWindow,
|
|
38
|
+
isMovingAlias,
|
|
33
39
|
isStaleContainmentStamp,
|
|
34
40
|
modelIdentity,
|
|
35
41
|
modelSpec as spec,
|
|
42
|
+
classifierPrice,
|
|
43
|
+
modelsContainedToSession,
|
|
36
44
|
} from "../lib/model-policy.ts";
|
|
37
|
-
import {
|
|
38
|
-
|
|
39
|
-
export { findConfigured } from "../lib/model-policy.ts";
|
|
45
|
+
import { pairedModel } from "../lib/model-pairings.ts";
|
|
46
|
+
import { intrinsicTier, sameTierContainedCandidates } from "../lib/model-tier.ts";
|
|
40
47
|
|
|
41
48
|
export interface Candidate {
|
|
42
49
|
model: Model<Api>;
|
|
43
50
|
/** How this candidate was arrived at, for the notice shown to the user. */
|
|
44
|
-
source: "configured" | "economical" | "session";
|
|
51
|
+
source: "configured" | "paired" | "economical" | "session";
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
export interface SelectInput {
|
|
@@ -51,142 +58,158 @@ export interface SelectInput {
|
|
|
51
58
|
/** `autoMode.classifierModel`, if set. May name any provider. */
|
|
52
59
|
configured?: string;
|
|
53
60
|
/**
|
|
54
|
-
* The containment identity
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* shape as the subagent `subagentModelSetFor` stamp. Undefined for a hand-edited
|
|
59
|
-
* setting.
|
|
61
|
+
* The containment identity the `classifierModel` setting was stamped for by
|
|
62
|
+
* `/auto-mode model`. When it differs from this session's, a cross-provider
|
|
63
|
+
* setting is stale (set for a session since left) and is replaced, with a
|
|
64
|
+
* warning. Undefined for a hand-edited setting.
|
|
60
65
|
*/
|
|
61
66
|
configuredSetForContainment?: string;
|
|
67
|
+
/** `suggestNewerModels` in ~/.onecode/settings.json; false drops the chosen model's newer-model warning. */
|
|
68
|
+
suggestNewer?: boolean;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Why automatic selection had to keep the session model. */
|
|
72
|
+
export type ClassifierFallbackReason = "unknown-session-context-window" | "paired-session" | "no-qualifying-model";
|
|
73
|
+
|
|
74
|
+
/** Structured selection fallback, retained with the chain for permission UI and logging. */
|
|
75
|
+
export interface ClassifierFallback {
|
|
76
|
+
reason: ClassifierFallbackReason;
|
|
77
|
+
text: string;
|
|
62
78
|
}
|
|
63
79
|
|
|
64
80
|
/** A user-facing selection notice, tagged so an informational one is not shown as a warning. */
|
|
65
81
|
export interface ClassifierNotice {
|
|
66
82
|
level: "info" | "warning";
|
|
67
83
|
text: string;
|
|
84
|
+
/** Present for a session fallback, so renderers need not parse `text`. */
|
|
85
|
+
fallbackReason?: ClassifierFallbackReason;
|
|
86
|
+
/** Present for a warning about the user's chosen classifier (`lib/model-choice-warnings.ts`). */
|
|
87
|
+
choiceWarning?: ChosenModelWarning["kind"];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface ClassifierChain {
|
|
91
|
+
candidates: Candidate[];
|
|
92
|
+
notices: ClassifierNotice[];
|
|
93
|
+
fallback?: ClassifierFallback;
|
|
68
94
|
}
|
|
69
95
|
|
|
70
96
|
/**
|
|
71
|
-
* The ordered
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
* `fallbackModels`.
|
|
76
|
-
*
|
|
77
|
-
* The chain always ends at the session model when there is one, so auto mode
|
|
78
|
-
* degrades to "correct but not cheap" rather than to "broken".
|
|
79
|
-
*
|
|
80
|
-
* The automatic pick is the SAME floor-gated selector the subagent default uses
|
|
81
|
-
* (`capableContainedCandidates`): the cheapest same-provider model (cheap →
|
|
82
|
-
* workhorse → frontier, never `tiny`) at or above `automaticTierFloor` — a
|
|
83
|
-
* session on a workhorse-or-better model is screened by a workhorse-or-better
|
|
84
|
-
* model, Claude Code's `min(main, sonnet)`. `docs/decisions/auto-mode.md`
|
|
85
|
-
* measured the same `rm -rf` grading stage-1 62 on Sonnet and ~22 on Haiku —
|
|
86
|
-
* "a weak classifier is a weak boundary" — yet until 2026-09-05 the selector
|
|
87
|
-
* picked Haiku even when the session itself was Sonnet, so the gate's threshold
|
|
88
|
-
* behaviour depended on which model happened to be cheapest
|
|
89
|
-
* (PERMISSIONS-REVIEW-2026-09-05 M6). A cheap session keeps a cheap screener
|
|
90
|
-
* (nothing cheaper and capable exists), and `autoMode.classifierModel`
|
|
91
|
-
* overrides either way. The `tiny` exclusion is the capability floor
|
|
92
|
-
* `auto-mode.md` recorded as still-missing. Since 2026-09-11 subagents share
|
|
93
|
-
* the floor, so a session screens and delegates on one model.
|
|
94
|
-
*
|
|
95
|
-
* The floor governs the AUTOMATIC pick only; the terminal fallback is never
|
|
96
|
-
* refused, so the chain is empty only when there is no session model and nothing
|
|
97
|
-
* available at all.
|
|
97
|
+
* The ordered fallback chain for the permission classifier. The user's
|
|
98
|
+
* `autoMode.classifierModel` leads when it resolves and is not stale; its
|
|
99
|
+
* warnings ride the notices. Then the automatic chain
|
|
100
|
+
* (`automaticClassifierCandidates`), which ends at the session model.
|
|
98
101
|
*/
|
|
99
|
-
export function classifierCandidates({
|
|
100
|
-
available,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
102
|
+
export function classifierCandidates({ available, sessionModel, configured, configuredSetForContainment, suggestNewer = true }: SelectInput): ClassifierChain {
|
|
103
|
+
const automatic = automaticClassifierCandidates(available, sessionModel);
|
|
104
|
+
if (!configured) return automatic;
|
|
105
|
+
/** The setting does not apply: the automatic chain, led by a warning saying why. */
|
|
106
|
+
const automaticBecause = (text: string): ClassifierChain => ({ ...automatic, notices: [{ level: "warning", text }, ...automatic.notices] });
|
|
107
|
+
const resolved = findConfigured(available, configured);
|
|
108
|
+
if (!resolved) {
|
|
109
|
+
return automaticBecause(
|
|
110
|
+
`autoMode.classifierModel ${configured} is not an available model (unknown id, or no credentials for its provider); auto mode chooses automatically instead.`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
106
113
|
const notices: ClassifierNotice[] = [];
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
// with a warning (parity with the subagent setting); a genuine cross-provider
|
|
116
|
-
// choice made for THIS session is honored with an announcement, because the
|
|
117
|
-
// classifier reads the user's prompts and CLAUDE.md.
|
|
118
|
-
const resolved = configured ? findConfigured(available, configured) : undefined;
|
|
119
|
-
if (resolved) {
|
|
120
|
-
if (sessionModel && crossesProvider(resolved, sessionModel)) {
|
|
121
|
-
if (isStaleContainmentStamp(configuredSetForContainment, sessionModel)) {
|
|
122
|
-
notices.push({
|
|
123
|
-
level: "warning",
|
|
124
|
-
text:
|
|
125
|
-
`autoMode.classifierModel ${spec(resolved)} was set for a different provider than this session (${spec(sessionModel)}); ` +
|
|
126
|
-
"a same-provider model screens calls instead. Re-set it with /auto-mode model on this session to use it here.",
|
|
127
|
-
});
|
|
128
|
-
} else {
|
|
129
|
-
// Informational, not a warning: the user deliberately set this for this
|
|
130
|
-
// session, so it is doing exactly what they asked.
|
|
131
|
-
notices.push({
|
|
132
|
-
level: "info",
|
|
133
|
-
text:
|
|
134
|
-
`autoMode.classifierModel ${spec(resolved)} is a different provider than this session (${spec(sessionModel)}) — ` +
|
|
135
|
-
"it reads your prompts and CLAUDE.md, so those go to that provider. It was set for this session, so it is honored.",
|
|
136
|
-
});
|
|
137
|
-
push(resolved, "configured");
|
|
138
|
-
}
|
|
139
|
-
} else {
|
|
140
|
-
push(resolved, "configured");
|
|
114
|
+
if (sessionModel && crossesProvider(resolved, sessionModel)) {
|
|
115
|
+
// Naming a provider is choosing it, but a setting stamped for a provider
|
|
116
|
+
// this session has since left is stale (parity with the subagent setting).
|
|
117
|
+
if (isStaleContainmentStamp(configuredSetForContainment, sessionModel)) {
|
|
118
|
+
return automaticBecause(
|
|
119
|
+
`autoMode.classifierModel ${spec(resolved)} was set for a different provider than this session (${spec(sessionModel)}); ` +
|
|
120
|
+
"a same-provider model screens calls instead. Re-set it with /auto-mode model on this session to use it here.",
|
|
121
|
+
);
|
|
141
122
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
// 2. The cheapest capable same-provider model, no dearer than the work being
|
|
145
|
-
// screened — screening a call more expensively than making it is
|
|
146
|
-
// indefensible. The shared gate excludes `tiny`, unpriced/opaque rows, and
|
|
147
|
-
// the session model itself, so an unpriced provider yields nothing here and
|
|
148
|
-
// the session model (step 3) screens the calls.
|
|
149
|
-
// With an Artificial Analysis snapshot (an optional user key —
|
|
150
|
-
// lib/capability-index.ts) the floor is MEASURED: a candidate whose coding
|
|
151
|
-
// index reaches min(session, Sonnet 5), on the thinking-off variant the
|
|
152
|
-
// classifier actually runs, screens — Claude Code's min(main, Sonnet) taken
|
|
153
|
-
// literally, so a flash-class model that measurably matches its vendor's
|
|
154
|
-
// flagship may screen it. A measured failure is never admitted; an
|
|
155
|
-
// unscored candidate (no key, no confirmed match) is judged by the
|
|
156
|
-
// name-class tier floor below, exactly as before the snapshot existed.
|
|
157
|
-
if (sessionModel) {
|
|
158
|
-
for (const model of capableContainedCandidates(available, sessionModel, "classifier")) push(model, "economical");
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// 3. The session's own model: always correct, just not cheap. Terminal
|
|
162
|
-
// fallback, so the gate degrades to screening on it rather than to broken.
|
|
163
|
-
// No tier floor here: a floor that refused a `tiny` session model would
|
|
164
|
-
// disarm auto mode on exactly the sessions least able to do without it,
|
|
165
|
-
// on a price-and-name heuristic rather than measured competence. The
|
|
166
|
-
// circumvention of a permission rule that motivated one is handled by
|
|
167
|
-
// giving the classifier the user's own deny rules to judge by effect
|
|
168
|
-
// (permissions/rule-prose.ts), not by picking a different screener.
|
|
169
|
-
push(sessionModel, "session");
|
|
170
|
-
|
|
171
|
-
// Nothing configured and no session model (a headless run with a bare
|
|
172
|
-
// registry) — take anything available rather than refusing outright.
|
|
173
|
-
if (candidates.length === 0) push(available[0], "session");
|
|
174
|
-
|
|
175
|
-
// A configured spec that matched nothing available would otherwise fall through
|
|
176
|
-
// in silence, leaving the user believing their setting is in force.
|
|
177
|
-
if (configured && !resolved) {
|
|
178
|
-
const instead = candidates[0];
|
|
123
|
+
// Informational: the user chose this for this session.
|
|
179
124
|
notices.push({
|
|
180
|
-
level: "
|
|
125
|
+
level: "info",
|
|
181
126
|
text:
|
|
182
|
-
`autoMode.classifierModel
|
|
183
|
-
|
|
127
|
+
`autoMode.classifierModel ${spec(resolved)} is a different provider than this session (${spec(sessionModel)}): ` +
|
|
128
|
+
"it reads your messages and CLAUDE.md, so those go to that provider. It was set for this session, so it is used.",
|
|
184
129
|
});
|
|
185
130
|
}
|
|
186
|
-
|
|
131
|
+
for (const warning of chosenModelWarnings({ available, sessionModel, chosen: resolved, role: "classifier", suggestNewer })) {
|
|
132
|
+
notices.push({ level: "warning", text: `${warning.text} ${warning.fix}`, choiceWarning: warning.kind });
|
|
133
|
+
}
|
|
134
|
+
const candidates: Candidate[] = [{ model: resolved, source: "configured" }];
|
|
135
|
+
for (const entry of automatic.candidates) {
|
|
136
|
+
if (entry.model.provider !== resolved.provider || entry.model.id !== resolved.id) candidates.push(entry);
|
|
137
|
+
}
|
|
138
|
+
// The automatic chain's "why the session screens" notice does not describe a
|
|
139
|
+
// chain the user's choice leads, so it is dropped with its fallback.
|
|
187
140
|
return { candidates, notices };
|
|
188
141
|
}
|
|
189
142
|
|
|
143
|
+
/**
|
|
144
|
+
* The automatic chain. For a session model the pairing table names
|
|
145
|
+
* (`lib/model-pairings.ts`), its pick leads (or the session alone screens);
|
|
146
|
+
* otherwise the cost algorithm below. Entries stay on the session provider/route, sit in the
|
|
147
|
+
* session's tier or above (`model-tier.ts`, from the public catalogs), cost
|
|
148
|
+
* strictly less than the session model, are not experimental builds or moving
|
|
149
|
+
* `-latest` aliases (what answers one changes without notice), and have
|
|
150
|
+
* a known catalog window at least as large as the session's: the same rule as
|
|
151
|
+
* the subagent default (`sameTierContainedCandidates`), priced per classifier
|
|
152
|
+
* call, plus the window. The
|
|
153
|
+
* cheapest qualifying model screens first, with the session retained as an
|
|
154
|
+
* availability fallback.
|
|
155
|
+
*/
|
|
156
|
+
function automaticClassifierCandidates(available: Model<Api>[], sessionModel: Model<Api> | undefined): ClassifierChain {
|
|
157
|
+
// A classifier gets the whole user transcript. It must stay on the session's
|
|
158
|
+
// provider/route, sit in the session's tier, and be able to receive every
|
|
159
|
+
// token the session catalog says the main model can receive. Catalog
|
|
160
|
+
// omissions are unsafe to guess at: an unknown candidate window is ineligible.
|
|
161
|
+
if (!sessionModel) return { candidates: [], notices: [] };
|
|
162
|
+
const sessionOnly = (fallback: ClassifierFallback): ClassifierChain => ({
|
|
163
|
+
candidates: [{ model: sessionModel, source: "session" }],
|
|
164
|
+
notices: [{ level: "info", text: fallback.text, fallbackReason: fallback.reason }],
|
|
165
|
+
fallback,
|
|
166
|
+
});
|
|
167
|
+
if (!hasCatalogContextWindow(sessionModel)) {
|
|
168
|
+
return sessionOnly({
|
|
169
|
+
reason: "unknown-session-context-window",
|
|
170
|
+
text: `Auto mode is screening calls with ${spec(sessionModel)}, this session's model, because its catalog context window is unknown and no alternate classifier can be verified to contain it.`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// The pairing table speaks first for the models it names (lib/model-pairings.ts),
|
|
174
|
+
// under the same window and moving-alias gates; it may also keep the session.
|
|
175
|
+
const fits = (model: Model<Api>) => !isMovingAlias(model.id) && hasCatalogContextWindow(model) && model.contextWindow >= sessionModel.contextWindow;
|
|
176
|
+
const contained = modelsContainedToSession(available, sessionModel);
|
|
177
|
+
const price = classifierPrice(sessionModel);
|
|
178
|
+
const paired = pairedModel("classifier", available, sessionModel, { contained, price, accept: fits });
|
|
179
|
+
if (paired?.kind === "session") {
|
|
180
|
+
return sessionOnly({
|
|
181
|
+
reason: "paired-session",
|
|
182
|
+
text: `Auto mode is screening calls with ${spec(sessionModel)}, this session's model: One Code's model pairing table finds no smaller model of its vendor close enough in capability to screen for it.`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// Never weaker than the session, strictly cheaper per call (the user,
|
|
186
|
+
// 2026-10-05: Qwen 3.8 27B drew a 2.4T model at five times its input price;
|
|
187
|
+
// 2026-10-07: a same-price older model saves nothing; 2026-10-12: judged on
|
|
188
|
+
// a call's own cost shape), and an unpriced session cannot prove an
|
|
189
|
+
// alternate cheaper. The classifier adds a window that contains the
|
|
190
|
+
// session's and never a moving alias (experimental builds are never
|
|
191
|
+
// auto-selectable at all). A session with no catalog cache-read price is
|
|
192
|
+
// compared by list input price (classifierPrice).
|
|
193
|
+
const cheaper = sameTierContainedCandidates(available, sessionModel, { contained, price }).filter(fits);
|
|
194
|
+
// The cost algorithm's picks stay behind the table's, in case the provider
|
|
195
|
+
// lists the paired model but refuses it to this account at call time.
|
|
196
|
+
if (paired) {
|
|
197
|
+
const backups = cheaper.filter((model) => model !== paired.model).map((model): Candidate => ({ model, source: "economical" }));
|
|
198
|
+
return { candidates: [{ model: paired.model, source: "paired" }, ...backups, { model: sessionModel, source: "session" }], notices: [] };
|
|
199
|
+
}
|
|
200
|
+
if (cheaper.length === 0) {
|
|
201
|
+
return sessionOnly({
|
|
202
|
+
reason: "no-qualifying-model",
|
|
203
|
+
text: `Auto mode is screening calls with ${spec(sessionModel)}, this session's model, because no cheaper same-provider/route model in its ${intrinsicTier(sessionModel)} tier or above contains its ${sessionModel.contextWindow}-token catalog context window.`,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
// The session stays last as the availability fallback.
|
|
207
|
+
return {
|
|
208
|
+
candidates: [...cheaper.map((model): Candidate => ({ model, source: "economical" })), { model: sessionModel, source: "session" }],
|
|
209
|
+
notices: [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
190
213
|
/**
|
|
191
214
|
* Whether an error means "this model is not usable on this account" — in which
|
|
192
215
|
* case stepping to the next candidate is right — as opposed to a transient
|
|
@@ -222,11 +245,13 @@ export function describeCandidate(candidate: Candidate): string {
|
|
|
222
245
|
const where = modelIdentity(candidate.model).profile ?? candidate.model.provider;
|
|
223
246
|
switch (candidate.source) {
|
|
224
247
|
case "configured":
|
|
225
|
-
return `${name} (
|
|
248
|
+
return `${name} (autoMode.classifierModel)`;
|
|
249
|
+
case "paired":
|
|
250
|
+
return `${name} (paired with the session model in One Code's model pairing table)`;
|
|
226
251
|
case "economical":
|
|
227
|
-
return `${name} (cheapest model within ${where}
|
|
252
|
+
return `${name} (cheapest model within ${where} in the session's tier or above that contains the session catalog context window)`;
|
|
228
253
|
case "session":
|
|
229
|
-
return `${name} (this session's model
|
|
254
|
+
return `${name} (this session's model)`;
|
|
230
255
|
}
|
|
231
256
|
}
|
|
232
257
|
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* and every caller treats that as "not provably contained".
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { lstatSync, readlinkSync } from "node:fs";
|
|
18
|
+
import { type Dirent, lstatSync, readdirSync, readlinkSync } from "node:fs";
|
|
19
19
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
20
20
|
import { comparablePath, expandTilde, gitBashPathToNative, isPathAtOrUnder, tryRealpath } from "../lib/paths.ts";
|
|
21
21
|
|
|
@@ -41,6 +41,19 @@ export function pathArgument(input: Record<string, unknown> | undefined): string
|
|
|
41
41
|
return typeof path === "string" ? path : undefined;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
/**
|
|
45
|
+
* The fix to name when a call spells its target in two path fields with
|
|
46
|
+
* different values (`path` and `file_path`), undefined otherwise. The tool
|
|
47
|
+
* validator keeps both and the tool reads only one, so a gate that picked the
|
|
48
|
+
* other judged a file the call never touches
|
|
49
|
+
* (AUTO-MODE-SECURITY-REVIEW-2026-09-24 H1).
|
|
50
|
+
*/
|
|
51
|
+
export function conflictingPathArguments(input: Record<string, unknown> | undefined): string | undefined {
|
|
52
|
+
const fields = ["path", "file_path", "notebook_path"].filter((key) => input?.[key] !== undefined);
|
|
53
|
+
if (new Set(fields.map((key) => input?.[key])).size < 2) return undefined;
|
|
54
|
+
return `This call names its target in ${fields.map((key) => `\`${key}\``).join(" and ")} with different values. Pass the target once, in the field the tool's schema defines.`;
|
|
55
|
+
}
|
|
56
|
+
|
|
44
57
|
/** The shared comparison form (lib/paths.ts): `/` separators, case-folded on darwin/win32. */
|
|
45
58
|
const normalize = comparablePath;
|
|
46
59
|
|
|
@@ -50,21 +63,32 @@ const normalize = comparablePath;
|
|
|
50
63
|
* resolved — callers must not treat that as contained.
|
|
51
64
|
*/
|
|
52
65
|
export function resolveForContainment(target: string): string | undefined {
|
|
66
|
+
const resolved = resolveThroughLinks(target);
|
|
67
|
+
return resolved === undefined ? undefined : normalize(resolved);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* {@link resolveForContainment} in the filesystem's own spelling (not
|
|
72
|
+
* case-folded), for showing where a path lands.
|
|
73
|
+
*/
|
|
74
|
+
export function resolveThroughLinks(target: string, hops = 0): string | undefined {
|
|
53
75
|
const absolute = resolve(target);
|
|
54
76
|
|
|
55
77
|
const direct = tryRealpath(absolute);
|
|
56
|
-
if (direct) return
|
|
78
|
+
if (direct) return direct;
|
|
57
79
|
|
|
58
80
|
// The leaf may be a dangling symlink: realpath fails on it, but writes still
|
|
59
81
|
// follow it. Read the link and resolve its target instead of assuming the
|
|
60
82
|
// literal path (N9).
|
|
61
83
|
try {
|
|
62
84
|
const stats = lstatSync(absolute);
|
|
63
|
-
|
|
85
|
+
// A link cycle (`a -> a`) stops at the kernel's usual 40 hops, then
|
|
86
|
+
// resolves like any other leaf, below, instead of overflowing the stack.
|
|
87
|
+
if (stats.isSymbolicLink() && hops < 40) {
|
|
64
88
|
const linkTarget = readlinkSync(absolute);
|
|
65
89
|
const resolvedTarget = isAbsolute(linkTarget) ? linkTarget : resolve(dirname(absolute), linkTarget);
|
|
66
90
|
// One hop is enough: the target's own realpath covers the rest of a chain.
|
|
67
|
-
return
|
|
91
|
+
return resolveThroughLinks(resolvedTarget, hops + 1) ?? resolvedTarget;
|
|
68
92
|
}
|
|
69
93
|
} catch {
|
|
70
94
|
// Does not exist yet — fall through to ancestor resolution below.
|
|
@@ -79,7 +103,7 @@ export function resolveForContainment(target: string): string | undefined {
|
|
|
79
103
|
if (parent === cursor) return undefined;
|
|
80
104
|
tail.unshift(basename(cursor));
|
|
81
105
|
const realParent = tryRealpath(parent);
|
|
82
|
-
if (realParent) return
|
|
106
|
+
if (realParent) return join(realParent, ...tail);
|
|
83
107
|
cursor = parent;
|
|
84
108
|
}
|
|
85
109
|
}
|
|
@@ -117,3 +141,31 @@ export function toAbsolute(cwd: string, token: string, home: string): string {
|
|
|
117
141
|
export function toAbsoluteBash(cwd: string, token: string, home: string): string {
|
|
118
142
|
return toAbsolute(cwd, gitBashPathToNative(token), home);
|
|
119
143
|
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Whether moving, copying or removing `source` touches a path `guarded`
|
|
147
|
+
* flags beneath it: walks `source` without following symlinks, at most
|
|
148
|
+
* `budget` entries, and tests each entry's counterpart under `target` (the
|
|
149
|
+
* same path for a removal). A walk that runs out of budget counts as touching
|
|
150
|
+
* one, so the command is judged, not cleared. A file source walks nothing.
|
|
151
|
+
* `guarded` also gets the source's directory entry (a symbolic link is one).
|
|
152
|
+
*/
|
|
153
|
+
export function touchesGuardedPath(source: string, target: string, guarded: (path: string, entry: Dirent) => boolean, budget = 5_000): boolean {
|
|
154
|
+
const stack = [""];
|
|
155
|
+
let seen = 0;
|
|
156
|
+
while (stack.length > 0) {
|
|
157
|
+
const relative = stack.pop() as string;
|
|
158
|
+
let entries;
|
|
159
|
+
try {
|
|
160
|
+
entries = readdirSync(join(source, relative), { withFileTypes: true });
|
|
161
|
+
} catch {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
for (const entry of entries) {
|
|
165
|
+
const child = join(relative, entry.name);
|
|
166
|
+
if (++seen > budget || guarded(join(target, child), entry)) return true;
|
|
167
|
+
if (entry.isDirectory()) stack.push(child);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
@@ -12,6 +12,27 @@
|
|
|
12
12
|
export const CONSECUTIVE_BLOCK_LIMIT = 3;
|
|
13
13
|
export const TOTAL_BLOCK_LIMIT = 20;
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* How long a timed resume prompt waits before denying the call, as Claude
|
|
17
|
+
* Code's denial-limit fallback does (2.1.282: 120 s, overridden by
|
|
18
|
+
* `CLAUDE_CODE_TICKLISH_WHISPER_TIMEOUT_MS`), so an unattended session is not
|
|
19
|
+
* held by a prompt nobody will answer.
|
|
20
|
+
*/
|
|
21
|
+
export const UNATTENDED_PROMPT_TIMEOUT_MS = 120_000;
|
|
22
|
+
|
|
23
|
+
export function unattendedPromptTimeoutMs(env: NodeJS.ProcessEnv = process.env): number {
|
|
24
|
+
const value = Number(env.CLAUDE_CODE_TICKLISH_WHISPER_TIMEOUT_MS);
|
|
25
|
+
return Number.isFinite(value) && value > 0 ? value : UNATTENDED_PROMPT_TIMEOUT_MS;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Claude Code's warning line on a timed prompt, in its "about N minutes" form (the dialog adds a live countdown). */
|
|
29
|
+
export function unattendedPromptNotice(timeoutMs: number): string {
|
|
30
|
+
const minutes = Math.round(timeoutMs / 60_000);
|
|
31
|
+
const seconds = Math.max(1, Math.round(timeoutMs / 1000));
|
|
32
|
+
const left = timeoutMs >= 60_000 ? `about ${minutes} ${minutes === 1 ? "minute" : "minutes"}` : `about ${seconds} ${seconds === 1 ? "second" : "seconds"}`;
|
|
33
|
+
return `⚠ One Code will automatically deny this request in ${left}, to avoid blocking progress on an unattended session`;
|
|
34
|
+
}
|
|
35
|
+
|
|
15
36
|
export interface Denial {
|
|
16
37
|
toolName: string;
|
|
17
38
|
/** The command or path the call targeted, for the /permissions listing. */
|
|
@@ -33,6 +54,10 @@ export class PauseTracker {
|
|
|
33
54
|
private consecutive = 0;
|
|
34
55
|
private total = 0;
|
|
35
56
|
private paused = false;
|
|
57
|
+
/** Which limit tripped the current pause. */
|
|
58
|
+
private trippedBy: "consecutive" | "total" | undefined;
|
|
59
|
+
/** The timed resume prompt was shown in this block streak (Claude Code's `timedFallbackShown`). */
|
|
60
|
+
private timedShown = false;
|
|
36
61
|
private readonly denials: Denial[] = [];
|
|
37
62
|
|
|
38
63
|
/** Record a classifier block. Returns true if this one tripped the pause. */
|
|
@@ -44,10 +69,12 @@ export class PauseTracker {
|
|
|
44
69
|
|
|
45
70
|
if (this.consecutive >= CONSECUTIVE_BLOCK_LIMIT) {
|
|
46
71
|
this.paused = true;
|
|
72
|
+
this.trippedBy = "consecutive";
|
|
47
73
|
return true;
|
|
48
74
|
}
|
|
49
75
|
if (this.total >= TOTAL_BLOCK_LIMIT) {
|
|
50
76
|
this.paused = true;
|
|
77
|
+
this.trippedBy = "total";
|
|
51
78
|
// The total counter resets when it is what triggered the fallback.
|
|
52
79
|
// Without this, resuming leaves it at the limit and the very next block
|
|
53
80
|
// re-pauses immediately, making the resume single-use.
|
|
@@ -57,9 +84,33 @@ export class PauseTracker {
|
|
|
57
84
|
return false;
|
|
58
85
|
}
|
|
59
86
|
|
|
60
|
-
/**
|
|
61
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Any allowed call in auto mode breaks a consecutive-block run, as in Claude
|
|
89
|
+
* Code, whose denial count resets on every allow in auto mode (2.1.282).
|
|
90
|
+
* Claude Code has no pause state: a block falls back to a prompt only while
|
|
91
|
+
* the count is at the limit, so an allow ends a pause the consecutive limit
|
|
92
|
+
* tripped. A 20-total pause still waits for the user. Returns true when this
|
|
93
|
+
* allow ended a pause.
|
|
94
|
+
*/
|
|
95
|
+
recordAllow(): boolean {
|
|
62
96
|
this.consecutive = 0;
|
|
97
|
+
this.timedShown = false;
|
|
98
|
+
if (!this.paused || this.trippedBy !== "consecutive") return false;
|
|
99
|
+
this.paused = false;
|
|
100
|
+
this.trippedBy = undefined;
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Whether this resume prompt auto-denies after `unattendedPromptTimeoutMs`,
|
|
106
|
+
* and marks it shown. As in Claude Code, only a pause the consecutive limit
|
|
107
|
+
* tripped is timed, and only once until the streak breaks: the next prompt
|
|
108
|
+
* in the same streak waits for the user. A 20-total pause always waits.
|
|
109
|
+
*/
|
|
110
|
+
takeTimedPrompt(): boolean {
|
|
111
|
+
if (!this.paused || this.trippedBy !== "consecutive" || this.timedShown) return false;
|
|
112
|
+
this.timedShown = true;
|
|
113
|
+
return true;
|
|
63
114
|
}
|
|
64
115
|
|
|
65
116
|
/**
|
|
@@ -70,6 +121,8 @@ export class PauseTracker {
|
|
|
70
121
|
resume(): void {
|
|
71
122
|
this.paused = false;
|
|
72
123
|
this.consecutive = 0;
|
|
124
|
+
this.trippedBy = undefined;
|
|
125
|
+
this.timedShown = false;
|
|
73
126
|
}
|
|
74
127
|
|
|
75
128
|
isPaused(): boolean {
|
|
@@ -81,6 +134,8 @@ export class PauseTracker {
|
|
|
81
134
|
this.consecutive = 0;
|
|
82
135
|
this.total = 0;
|
|
83
136
|
this.paused = false;
|
|
137
|
+
this.trippedBy = undefined;
|
|
138
|
+
this.timedShown = false;
|
|
84
139
|
this.denials.length = 0;
|
|
85
140
|
}
|
|
86
141
|
|