one-code-extension 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -5
- package/agents/general-purpose.md +2 -2
- package/docs/guide/README.md +2 -1
- package/docs/guide/bring-your-claude-code-setup.md +60 -9
- package/docs/guide/configuration.md +48 -14
- package/docs/guide/differences-from-claude-code.md +32 -23
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +27 -4
- package/docs/guide/permissions-modes-and-auto-mode.md +225 -49
- package/docs/guide/providers-and-models.md +137 -54
- package/docs/guide/reference.md +27 -15
- package/docs/guide/sessions-and-context.md +69 -16
- package/docs/guide/skills-plugins-and-mcp.md +57 -20
- package/docs/guide/subagents-and-workflows.md +95 -29
- package/docs/guide/tasks-and-background-work.md +101 -26
- package/docs/guide/terminal-interface.md +24 -12
- package/docs/guide/tools.md +46 -5
- package/docs/guide/troubleshooting.md +13 -9
- package/docs/guide/windows.md +58 -31
- package/extensions/artifacts/gallery.ts +227 -0
- package/extensions/artifacts/index.ts +288 -0
- package/extensions/artifacts/store.ts +570 -0
- package/extensions/artifacts/viewer.ts +199 -0
- package/extensions/ask-user/description.ts +67 -0
- package/extensions/ask-user/dialogs.ts +80 -0
- package/extensions/ask-user/index.ts +37 -33
- package/extensions/ask-user/questions.ts +32 -7
- package/extensions/ask-user/widget.ts +45 -25
- package/extensions/auto-mode/actions.ts +6 -0
- package/extensions/auto-mode/cache.ts +88 -0
- package/extensions/auto-mode/classifier-prompt.ts +5 -4
- package/extensions/auto-mode/classifier.ts +290 -65
- package/extensions/auto-mode/config.ts +72 -20
- package/extensions/auto-mode/decision-log.ts +2 -0
- package/extensions/auto-mode/defaults.ts +1 -1
- package/extensions/auto-mode/denials.ts +123 -0
- package/extensions/auto-mode/git-checkout-programs.ts +187 -0
- package/extensions/auto-mode/git-status-meta.ts +159 -0
- package/extensions/auto-mode/history.ts +189 -0
- package/extensions/auto-mode/instructions.ts +58 -52
- package/extensions/auto-mode/model-select.ts +159 -134
- package/extensions/auto-mode/paths.ts +57 -5
- package/extensions/auto-mode/pause.ts +57 -2
- package/extensions/auto-mode/prompt.ts +37 -21
- package/extensions/auto-mode/read-only-options.ts +724 -0
- package/extensions/auto-mode/resolved-paths-meta.ts +133 -0
- package/extensions/auto-mode/rules.ts +18 -1
- package/extensions/auto-mode/safety-floor.ts +361 -18
- package/extensions/auto-mode/setup-run.ts +20 -14
- package/extensions/auto-mode/shell-analysis.ts +764 -490
- package/extensions/auto-mode/shell-parse.ts +885 -0
- package/extensions/auto-mode/system-note.ts +33 -0
- package/extensions/auto-mode/transcript.ts +80 -51
- package/extensions/background/cron.ts +515 -0
- package/extensions/background/index.ts +514 -236
- package/extensions/background/loop-fire.ts +136 -0
- package/extensions/background/loop-preamble.ts +59 -0
- package/extensions/background/loop-skill.ts +163 -0
- package/extensions/background/monitor-batch.ts +53 -0
- package/extensions/background/noop-fold.ts +128 -0
- package/extensions/background/switch-guard.ts +52 -0
- package/extensions/background/wakeup.ts +159 -55
- package/extensions/bash/background.ts +53 -17
- package/extensions/bash/child.ts +7 -0
- package/extensions/bash/description.ts +188 -0
- package/extensions/bash/guards.ts +15 -67
- package/extensions/bash/index.ts +88 -35
- package/extensions/branding/compaction-keep.ts +63 -0
- package/extensions/branding/declined.ts +22 -0
- package/extensions/branding/index.ts +133 -9
- package/extensions/branding/prompt-editor.ts +53 -6
- package/extensions/branding/queued-edit.ts +31 -0
- package/extensions/branding/replaced-builtins.ts +68 -0
- package/extensions/branding/startup.ts +33 -71
- package/extensions/btw/index.ts +229 -71
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +446 -58
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +216 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/index.ts +24 -5
- package/extensions/context-management/index.ts +25 -0
- package/extensions/doctor/build.ts +59 -8
- package/extensions/doctor/builtins.ts +84 -0
- package/extensions/doctor/cli.ts +7 -1
- package/extensions/doctor/compat.ts +88 -23
- package/extensions/doctor/dependencies.ts +41 -5
- package/extensions/doctor/fix-prompt.ts +9 -2
- package/extensions/doctor/index.ts +102 -58
- package/extensions/doctor/models.ts +102 -63
- package/extensions/doctor/presets.ts +42 -27
- package/extensions/doctor/report.ts +19 -2
- package/extensions/doctor/viewer.ts +49 -27
- package/extensions/effort/index.ts +72 -19
- package/extensions/effort/settings.ts +21 -0
- package/extensions/effort/slider.ts +24 -20
- package/extensions/exit/index.ts +32 -1
- package/extensions/file-tracker/index.ts +337 -26
- package/extensions/file-tracker/replay.ts +96 -10
- package/extensions/file-tracker/restore.ts +63 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +32 -15
- package/extensions/footer/footer-line.ts +45 -24
- package/extensions/footer/index.ts +47 -2
- package/extensions/footer/pr.ts +34 -5
- package/extensions/hooks/compaction.ts +7 -0
- package/extensions/hooks/executor.ts +32 -30
- package/extensions/hooks/index.ts +198 -31
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/protocol.ts +41 -3
- package/extensions/hooks/settings.ts +50 -19
- package/extensions/hooks/subagent-bridge.ts +2 -0
- package/extensions/hooks/trust.ts +13 -5
- package/extensions/idle-compact/index.ts +119 -0
- package/extensions/idle-compact/policy.ts +143 -0
- package/extensions/init/index.ts +11 -10
- package/extensions/init/prompt.ts +25 -32
- package/extensions/interrupted/index.ts +13 -1
- package/extensions/kept-thinking/index.ts +27 -0
- package/extensions/lib/agent-cron.ts +130 -0
- package/extensions/lib/agent-loader.ts +15 -1
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-payload.ts +82 -1
- package/extensions/lib/anthropic-server-call.ts +230 -0
- package/extensions/lib/anthropic-server-tools.ts +485 -0
- package/extensions/lib/app-launch.mjs +94 -0
- package/extensions/lib/argument-hints.ts +190 -0
- package/extensions/lib/atomic-write.ts +6 -2
- package/extensions/lib/auto-memory.ts +75 -0
- package/extensions/lib/bash-parser.ts +84 -0
- package/extensions/lib/btw-fork.ts +93 -0
- package/extensions/lib/child-extensions.ts +42 -0
- package/extensions/lib/child-writes.ts +45 -0
- package/extensions/lib/claude-context.ts +515 -100
- package/extensions/lib/claude-external-includes.ts +68 -0
- package/extensions/lib/claude-id.ts +22 -0
- package/extensions/lib/claude-rules.ts +323 -0
- package/extensions/lib/claude-settings.ts +76 -4
- package/extensions/lib/compaction-boundary.ts +36 -0
- package/extensions/lib/compaction-keep.mjs +83 -0
- package/extensions/lib/config-mode.ts +112 -0
- package/extensions/lib/consent-dialogs.ts +63 -0
- package/extensions/lib/consent-preview.ts +5 -1
- package/extensions/lib/consent-stores.ts +48 -0
- package/extensions/lib/context-facts.ts +78 -0
- package/extensions/lib/context-stack.ts +81 -0
- package/extensions/lib/deferred.ts +219 -25
- package/extensions/lib/dock-rows.ts +60 -0
- package/extensions/lib/empty-tool-result.ts +27 -0
- package/extensions/lib/environment-block.ts +76 -0
- package/extensions/lib/fetch-timeout.ts +24 -6
- package/extensions/{system-prompt → lib}/git-status.ts +51 -13
- package/extensions/lib/git.ts +75 -13
- package/extensions/lib/grammars/LICENSE-tree-sitter-bash +21 -0
- package/extensions/lib/grammars/README.md +2 -0
- package/extensions/lib/grammars/tree-sitter-bash.wasm +0 -0
- package/extensions/lib/guide-docs.ts +45 -0
- package/extensions/lib/headless-output.ts +46 -0
- package/extensions/lib/hook-gate.ts +7 -7
- package/extensions/lib/interrupt.ts +20 -4
- package/extensions/lib/key-input.ts +455 -0
- package/extensions/lib/keys.ts +14 -1
- package/extensions/lib/local-command.ts +47 -13
- package/extensions/lib/mcp-overrides.ts +1 -1
- package/extensions/lib/mcp-share.ts +46 -0
- package/extensions/lib/memory-content.ts +146 -0
- package/extensions/lib/memory-index.ts +18 -0
- package/extensions/lib/memory.ts +134 -123
- package/extensions/lib/model-catalog/huggingface.json +174 -0
- package/extensions/lib/model-catalog/models-dev.json +18297 -0
- package/extensions/lib/model-catalog/openrouter.json +5252 -0
- package/extensions/lib/model-catalog-data.ts +513 -0
- package/extensions/lib/model-catalog.ts +462 -0
- package/extensions/lib/model-choice-warnings.ts +137 -0
- package/extensions/lib/model-pairings.ts +152 -0
- package/extensions/{auto-mode → lib}/model-picker.ts +26 -43
- package/extensions/lib/model-policy.ts +112 -15
- package/extensions/lib/model-tier.ts +283 -355
- package/extensions/lib/model-unusable.ts +1 -1
- package/extensions/lib/newer-model.ts +100 -0
- package/extensions/lib/notices.ts +87 -0
- package/extensions/lib/notifications.ts +308 -64
- package/extensions/lib/one-code-settings.ts +74 -8
- package/extensions/lib/open-browser.ts +63 -0
- package/extensions/lib/openrouter-generation.ts +94 -0
- package/extensions/lib/operation-deadline.ts +30 -0
- package/extensions/lib/original-command.ts +4 -4
- package/extensions/lib/package-version.ts +1 -1
- package/extensions/lib/panel-focus.ts +134 -0
- package/extensions/lib/paths.ts +17 -2
- package/extensions/lib/permission-gate.ts +72 -6
- package/extensions/lib/persisted-output.ts +60 -8
- package/extensions/lib/pi-ai-estimate.ts +19 -3
- package/extensions/lib/pi-version.ts +21 -2
- package/extensions/lib/plan-mode-channels.ts +20 -5
- package/extensions/lib/plugins.ts +26 -17
- package/extensions/lib/powershell-parse-script.ts +208 -0
- package/extensions/lib/powershell-parser.ts +344 -0
- package/extensions/lib/process-tree.ts +131 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +364 -58
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +439 -0
- package/extensions/lib/scratchpad.ts +81 -14
- package/extensions/lib/send-now-channel.ts +7 -0
- package/extensions/lib/session-model-tier.ts +51 -0
- package/extensions/lib/session-work.ts +64 -0
- package/extensions/lib/settings-channels.ts +4 -4
- package/extensions/lib/shell-exit-semantics.ts +170 -0
- package/extensions/lib/shell-quote.ts +11 -0
- package/extensions/lib/shell-result.ts +70 -0
- package/extensions/lib/shell-spawn.ts +70 -6
- package/extensions/lib/shell-tool.ts +75 -25
- package/extensions/lib/side-call-cache.ts +124 -0
- package/extensions/lib/side-call-run.ts +96 -0
- package/extensions/lib/side-call-usage.ts +38 -0
- package/extensions/lib/side-call.ts +13 -1
- package/extensions/lib/single-flight.ts +18 -0
- package/extensions/lib/skill-body.ts +42 -0
- package/extensions/lib/skill-scan.ts +63 -24
- package/extensions/lib/spill-file.ts +140 -0
- package/extensions/lib/subagent-handback.ts +113 -0
- package/extensions/lib/system-role.ts +156 -0
- package/extensions/lib/terminal-text.ts +114 -0
- package/extensions/lib/text-width.ts +47 -16
- package/extensions/lib/timer-ops.ts +20 -0
- package/extensions/lib/tool-additions.ts +149 -0
- package/extensions/lib/tool-call-corruption.ts +134 -0
- package/extensions/lib/tool-path.ts +66 -0
- package/extensions/lib/tool-variants.ts +69 -0
- package/extensions/lib/tui-render.ts +255 -27
- package/extensions/lib/turn-budget-layout.ts +143 -0
- package/extensions/lib/update-check.mjs +15 -2
- package/extensions/lib/usage-bus.ts +1 -1
- package/extensions/lib/user-denial.ts +38 -0
- package/extensions/lib/workspace-channel.ts +16 -0
- package/extensions/lib/worktree-channel.ts +31 -3
- package/extensions/lib/worktree-isolation.ts +40 -10
- package/extensions/lsp/client.ts +8 -9
- package/extensions/lsp/index.ts +70 -4
- package/extensions/lsp/keep-alive.ts +1 -1
- package/extensions/lsp/trust.ts +119 -0
- package/extensions/lsp/watcher.ts +1 -1
- package/extensions/mcp/announce.ts +87 -5
- package/extensions/mcp/client.ts +161 -48
- package/extensions/mcp/config.ts +132 -8
- package/extensions/mcp/discovery.ts +19 -0
- package/extensions/mcp/index.ts +209 -71
- package/extensions/mcp/oauth/browser.ts +38 -17
- package/extensions/mcp/oauth/flow.ts +25 -17
- package/extensions/mcp/oauth/provider.ts +14 -8
- package/extensions/mcp/oauth/store.ts +34 -16
- package/extensions/mcp/panel/keys.ts +12 -11
- package/extensions/mcp/panel/render.ts +9 -6
- package/extensions/mcp/schema.ts +104 -5
- package/extensions/mcp/trust.ts +151 -67
- package/extensions/memory/entries.ts +23 -14
- package/extensions/memory/index.ts +134 -28
- package/extensions/memory/open-external.ts +33 -18
- package/extensions/memory/panel.ts +135 -26
- package/extensions/newer-model/index.ts +31 -0
- package/extensions/notebook/description.ts +18 -0
- package/extensions/notebook/index.ts +6 -5
- package/extensions/notebook/notebook.ts +1 -1
- package/extensions/permissions/index.ts +1300 -381
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +6 -4
- package/extensions/permissions/panel/host.ts +130 -0
- package/extensions/permissions/panel/keys.ts +48 -0
- package/extensions/permissions/panel/render.ts +450 -0
- package/extensions/permissions/panel/state.ts +550 -0
- package/extensions/permissions/powershell-cmdlets.ts +198 -0
- package/extensions/permissions/powershell-paths.ts +197 -0
- package/extensions/permissions/powershell-rules.ts +207 -280
- package/extensions/permissions/powershell-tree.ts +557 -0
- package/extensions/permissions/project-trust.ts +27 -11
- package/extensions/permissions/protected-paths.ts +14 -2
- package/extensions/permissions/session-grant.ts +6 -0
- package/extensions/permissions/settings.ts +166 -31
- package/extensions/permissions/subagent-gate.ts +25 -0
- package/extensions/permissions/workspace.ts +78 -0
- package/extensions/plan-mode/index.ts +156 -69
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/texts.ts +177 -0
- package/extensions/plan-mode/viewer.ts +92 -27
- package/extensions/plugins/counts.ts +3 -3
- package/extensions/plugins/index.ts +61 -46
- package/extensions/plugins/install/install.ts +16 -0
- package/extensions/plugins/marketplace/sync.ts +9 -3
- package/extensions/plugins/marketplace/types.ts +3 -1
- package/extensions/plugins/panel/keys.ts +21 -24
- package/extensions/plugins/panel/render.ts +12 -6
- package/extensions/plugins/shell-expand.ts +107 -0
- package/extensions/plugins/template.ts +32 -15
- package/extensions/powershell/child.ts +7 -0
- package/extensions/powershell/description.ts +38 -6
- package/extensions/powershell/guards.ts +1 -1
- package/extensions/powershell/index.ts +12 -7
- package/extensions/recap/index.ts +65 -60
- package/extensions/recap/prompt.ts +2 -2
- package/extensions/recap/scheduler.ts +3 -5
- package/extensions/search-tools/grep-merge.ts +39 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/send-now/chord.ts +62 -0
- package/extensions/send-now/index.ts +133 -0
- package/extensions/session-title/index.ts +3 -0
- package/extensions/session-title/title.ts +4 -1
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +421 -101
- package/extensions/skill/invoke.ts +128 -18
- package/extensions/skill/listing.ts +133 -9
- package/extensions/skill/panel/keys.ts +15 -16
- package/extensions/skill/panel/render.ts +5 -2
- package/extensions/skill/panel/state.ts +12 -1
- package/extensions/subagents/agent-args.ts +39 -0
- package/extensions/subagents/agent-description.ts +132 -0
- package/extensions/subagents/agents.ts +116 -23
- package/extensions/subagents/announce.ts +69 -0
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +76 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1493 -824
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +2 -2
- package/extensions/subagents/panel-keys.ts +73 -31
- package/extensions/subagents/panel-render.ts +120 -96
- package/extensions/subagents/panel-widget.ts +60 -89
- package/extensions/subagents/runner.ts +108 -80
- package/extensions/subagents/runs.ts +39 -5
- package/extensions/subagents/send-to-main-tool.ts +1 -1
- package/extensions/subagents/session-turns.ts +32 -8
- package/extensions/subagents/shell-panel.ts +36 -178
- package/extensions/subagents/tasks-dialog.ts +170 -0
- package/extensions/subagents/viewport-mirror.ts +95 -0
- package/extensions/subagents/worktree.ts +57 -13
- package/extensions/system-prompt/environment.ts +19 -19
- package/extensions/system-prompt/idle-turn.ts +65 -0
- package/extensions/system-prompt/index.ts +131 -47
- package/extensions/system-prompt/template.ts +24 -50
- package/extensions/system-prompt/tiers/common.ts +23 -64
- package/extensions/system-prompt/tiers/frontier.ts +32 -11
- package/extensions/system-prompt/tiers/low.ts +67 -45
- package/extensions/system-prompt/tiers/mid.ts +96 -38
- package/extensions/system-reminder/index.ts +175 -13
- package/extensions/tasks/descriptions.ts +190 -0
- package/extensions/tasks/index.ts +120 -41
- package/extensions/tasks/store.ts +83 -16
- package/extensions/tool-call-corruption/child.ts +7 -0
- package/extensions/tool-call-corruption/index.ts +237 -0
- package/extensions/tool-search/announce.ts +16 -0
- package/extensions/tool-search/index.ts +189 -41
- package/extensions/tool-style/index.ts +68 -10
- package/extensions/tool-style/write-description.ts +40 -0
- package/extensions/tool-style/write-render.ts +237 -0
- package/extensions/turn-duration/index.ts +7 -3
- package/extensions/turn-duration/line.ts +47 -4
- package/extensions/web/backends.ts +6 -9
- package/extensions/web/description.ts +50 -0
- package/extensions/web/index.ts +118 -53
- package/extensions/web/native-call.ts +15 -0
- package/extensions/web/native-results.ts +41 -0
- package/extensions/web-fetch/description.ts +42 -0
- package/extensions/web-fetch/extract.ts +53 -6
- package/extensions/web-fetch/index.ts +125 -76
- package/extensions/web-fetch/page-content.ts +19 -0
- package/extensions/web-fetch/response.ts +49 -0
- package/extensions/web-fetch/summarize.ts +9 -10
- package/extensions/workflow/agent-session.ts +105 -18
- package/extensions/workflow/args.ts +24 -0
- package/extensions/workflow/description.ts +67 -0
- package/extensions/workflow/globals.ts +17 -3
- package/extensions/workflow/index.ts +145 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +29 -6
- package/extensions/workflow/saved-workflows.ts +3 -2
- package/extensions/workflow/types.ts +22 -1
- package/extensions/workflow/viewer.ts +54 -24
- package/extensions/workflow/widget.ts +20 -0
- package/extensions/worktree/descriptions.ts +91 -0
- package/extensions/worktree/guards.ts +316 -54
- package/extensions/worktree/index.ts +121 -39
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +18 -7
- package/skills/artifact-design/SKILL.md +85 -0
- package/skills/code-review/SKILL.md +20 -20
- package/skills/code-review/cells/arguments-and-flags.md +19 -0
- package/skills/code-review/cells/high-3x5-recall-verify.md +137 -0
- package/skills/code-review/cells/high-8-inline.md +122 -0
- package/skills/code-review/cells/low-1-pass-max4.md +27 -0
- package/skills/code-review/cells/low-1-pass-min-files-4.md +28 -0
- package/skills/code-review/cells/max-5x5-verify-sweep.md +162 -0
- package/skills/code-review/cells/medium-3x5-verify.md +132 -0
- package/skills/code-review/cells/minimal-prompt.md +32 -0
- package/skills/code-review/cells/xhigh-10-inline-sweep.md +149 -0
- package/skills/code-review/cells/xhigh-5x5-verify-sweep.md +162 -0
- package/skills/fewer-permission-prompts/SKILL.md +1 -1
- package/skills/loop/SKILL.md +11 -0
- package/skills/security-review/SKILL.md +2 -5
- package/skills/simplify/SKILL.md +7 -5
- package/skills/workflow-authoring/SKILL.md +143 -0
- package/themes/onecode-light.json +1 -1
- package/themes/onecode.json +1 -1
- package/types/app.d.ts +58 -0
- package/types/community-packages.d.ts +26 -1
- package/extensions/auto-mode/recoverability.ts +0 -229
- package/extensions/lib/capability-index.ts +0 -466
- package/extensions/lib/model-facts.generated.json +0 -2675
- package/extensions/lib/model-facts.ts +0 -162
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anthropic cache placement for standalone side calls. pi-ai marks the final
|
|
3
|
+
* user question, which means every different question writes a fresh cache
|
|
4
|
+
* entry. Move that marker onto the preceding conversation message instead,
|
|
5
|
+
* without changing any message text. Call this from an `anthropic-messages`
|
|
6
|
+
* payload hook, or an `openai-completions` one: Chat Completions carries the
|
|
7
|
+
* same `cache_control` markers when the model takes Anthropic's format
|
|
8
|
+
* (OpenRouter's `anthropic/*`), and has none to move otherwise.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/** The APIs whose standalone side-call payload this placement applies to. */
|
|
12
|
+
export const SIDE_CALL_CACHE_APIS: ReadonlySet<string> = new Set(["anthropic-messages", "openai-completions"]);
|
|
13
|
+
|
|
14
|
+
/** Anthropic permits at most four cache-control markers per request. */
|
|
15
|
+
export const MAX_SIDE_CALL_CACHE_MARKERS = 4;
|
|
16
|
+
|
|
17
|
+
type WireRecord = Record<string, unknown>;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Move pi-ai's final-question cache marker to the last preceding message and
|
|
21
|
+
* remove the earliest markers past Anthropic's four-marker limit. Unsupported
|
|
22
|
+
* payload shapes are left unchanged rather than being normalised.
|
|
23
|
+
*/
|
|
24
|
+
export function cacheSideCallConversation(payload: unknown): void {
|
|
25
|
+
if (!isRecord(payload) || !Array.isArray(payload.messages)) return;
|
|
26
|
+
const messages = payload.messages;
|
|
27
|
+
const questionIndex = messages.length - 1;
|
|
28
|
+
const question = asMessage(messages[questionIndex]);
|
|
29
|
+
if (!question || question.role !== "user") return;
|
|
30
|
+
const questionBlocks = blocks(question.content);
|
|
31
|
+
const sourceIndex = questionBlocks.findLastIndex((block) => block.cache_control !== undefined);
|
|
32
|
+
if (sourceIndex === -1) return;
|
|
33
|
+
|
|
34
|
+
const target = precedingCacheableBlock(messages, questionIndex);
|
|
35
|
+
if (!target) return;
|
|
36
|
+
const marker = questionBlocks[sourceIndex].cache_control;
|
|
37
|
+
const nextQuestionBlocks = [...questionBlocks];
|
|
38
|
+
const { cache_control: _, ...unmarked } = questionBlocks[sourceIndex];
|
|
39
|
+
nextQuestionBlocks[sourceIndex] = unmarked;
|
|
40
|
+
const nextTargetBlocks = [...target.blocks];
|
|
41
|
+
nextTargetBlocks[target.blockIndex] = { ...target.blocks[target.blockIndex], cache_control: marker };
|
|
42
|
+
const nextMessages = [...messages];
|
|
43
|
+
nextMessages[questionIndex] = { ...question, content: nextQuestionBlocks };
|
|
44
|
+
nextMessages[target.messageIndex] = { ...target.message, content: nextTargetBlocks };
|
|
45
|
+
payload.messages = nextMessages;
|
|
46
|
+
capMarkers(payload);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface Target {
|
|
50
|
+
messageIndex: number;
|
|
51
|
+
message: WireRecord;
|
|
52
|
+
blocks: WireRecord[];
|
|
53
|
+
blockIndex: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The last prior wire block Anthropic accepts as a message cache breakpoint. */
|
|
57
|
+
function precedingCacheableBlock(messages: unknown[], questionIndex: number): Target | undefined {
|
|
58
|
+
for (let messageIndex = questionIndex - 1; messageIndex >= 0; messageIndex--) {
|
|
59
|
+
const message = asMessage(messages[messageIndex]);
|
|
60
|
+
if (!message) continue;
|
|
61
|
+
// pi-ai keeps earlier user text as a string, only converting the final
|
|
62
|
+
// marked message to blocks. Convert this target exactly as pi-ai does so
|
|
63
|
+
// the newest conversation turn, rather than an older block message, gets
|
|
64
|
+
// the cache boundary. The text itself remains byte-for-byte unchanged.
|
|
65
|
+
// Chat Completions also sends assistant and tool text as strings; an
|
|
66
|
+
// empty one (a tool-call-only reply) cannot carry a marker, as in pi-ai.
|
|
67
|
+
if (typeof message.content === "string") {
|
|
68
|
+
if (message.content.length === 0) continue;
|
|
69
|
+
return { messageIndex, message, blocks: [{ type: "text", text: message.content }], blockIndex: 0 };
|
|
70
|
+
}
|
|
71
|
+
const content = blocks(message.content);
|
|
72
|
+
const blockIndex = content.findLastIndex(isCacheableBlock);
|
|
73
|
+
if (blockIndex !== -1) return { messageIndex, message, blocks: content, blockIndex };
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function asMessage(value: unknown): WireRecord | undefined {
|
|
79
|
+
return isRecord(value) && typeof value.role === "string" ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function blocks(value: unknown): WireRecord[] {
|
|
83
|
+
return Array.isArray(value) && value.every(isRecord) ? value : [];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** This is pi-ai's own list from its Anthropic message converter. */
|
|
87
|
+
function isCacheableBlock(block: WireRecord): boolean {
|
|
88
|
+
return (
|
|
89
|
+
block.type === "text" ||
|
|
90
|
+
block.type === "image" ||
|
|
91
|
+
block.type === "tool_result" ||
|
|
92
|
+
block.type === "tool_addition" ||
|
|
93
|
+
block.type === "tool_removal"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Drop the earliest actual Anthropic markers in wire order: tools, system, messages. */
|
|
98
|
+
function capMarkers(payload: WireRecord): void {
|
|
99
|
+
const markers = [
|
|
100
|
+
...topLevelMarkers(payload.tools),
|
|
101
|
+
...topLevelMarkers(payload.system),
|
|
102
|
+
...messageMarkers(payload.messages),
|
|
103
|
+
];
|
|
104
|
+
for (const marker of markers.slice(0, Math.max(0, markers.length - MAX_SIDE_CALL_CACHE_MARKERS))) delete marker.cache_control;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Tool definitions and top-level system blocks can each carry one marker. */
|
|
108
|
+
function topLevelMarkers(value: unknown): WireRecord[] {
|
|
109
|
+
return Array.isArray(value) ? value.filter((entry): entry is WireRecord => isRecord(entry) && entry.cache_control !== undefined) : [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Only a message content block is an Anthropic conversation marker. Never inspect block payloads. */
|
|
113
|
+
function messageMarkers(value: unknown): WireRecord[] {
|
|
114
|
+
if (!Array.isArray(value)) return [];
|
|
115
|
+
return value.flatMap((message) => (isRecord(message) ? blocks(message.content).filter((block) => block.cache_control !== undefined) : []));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function isRecord(value: unknown): value is WireRecord {
|
|
119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
/** The conversation cache placement a side call passes to `runSideCall` (`side-call-run.ts`). */
|
|
124
|
+
export const CONVERSATION_CACHE_PLACEMENT = { apis: SIDE_CALL_CACHE_APIS, place: cacheSideCallConversation } as const;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One standalone side call: a single `completeSimple` request outside the main
|
|
3
|
+
* agent loop, shared by `/btw`, the recap and the web_fetch reader. The
|
|
4
|
+
* auto-mode classifier has its own transport and stays out of this.
|
|
5
|
+
*
|
|
6
|
+
* Every side call takes the same steps: splice the provider's base URL from the
|
|
7
|
+
* resolved auth into the model, combine the caller's abort signal with a
|
|
8
|
+
* per-attempt timeout, key the request to the session (`<session id>:<kind>`,
|
|
9
|
+
* stable across calls, so the provider's prompt cache is reused), apply the
|
|
10
|
+
* caller's cache placement on the APIs it covers, log the attempt
|
|
11
|
+
* (`CC_SIDE_CALL_LOG`), retry with thinking on when the model cannot disable
|
|
12
|
+
* it, and report usage for the footer cost. The request on the wire is exactly
|
|
13
|
+
* what each extension built inline before (`side-call-run-wire.test.ts`).
|
|
14
|
+
*
|
|
15
|
+
* Kept apart from `side-call.ts`, which stays free of runtime pi imports for
|
|
16
|
+
* the pure prompt modules that use it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { type Api, type AssistantMessage, type Context, cleanupSessionResources, type Model, type SimpleStreamOptions, type ThinkingLevel } from "@earendil-works/pi-ai";
|
|
20
|
+
import { completeSimple } from "@earendil-works/pi-ai/compat";
|
|
21
|
+
import { withReasoningFallback } from "./model-policy.ts";
|
|
22
|
+
import { SIDE_CALL_KINDS, type SideCallLogContext, logSideCallUsage } from "./side-call-usage.ts";
|
|
23
|
+
|
|
24
|
+
/** A payload hook that places cache breakpoints, and the APIs whose payload shape it handles. */
|
|
25
|
+
export interface SideCallCachePlacement {
|
|
26
|
+
apis: ReadonlySet<string>;
|
|
27
|
+
place: (payload: unknown) => void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SideCallRun {
|
|
31
|
+
/** Names the call in the cache key and the side-call log. */
|
|
32
|
+
kind: SideCallLogContext["kind"];
|
|
33
|
+
model: Model<Api>;
|
|
34
|
+
/** The model registry's resolved auth for `model` (the caller handles a failed lookup). */
|
|
35
|
+
auth: { apiKey?: string; headers?: SimpleStreamOptions["headers"]; env?: Record<string, string>; baseUrl?: string };
|
|
36
|
+
/** The session's id; the call's cache key is `<sessionId>:<kind>`. */
|
|
37
|
+
sessionId: string;
|
|
38
|
+
context: Context;
|
|
39
|
+
signal?: AbortSignal;
|
|
40
|
+
/** Applies to each attempt on its own, so a reasoning retry gets a full timeout. */
|
|
41
|
+
timeoutMs: number;
|
|
42
|
+
maxTokens: number;
|
|
43
|
+
/** Cache placement for the payload; none leaves pi-ai's own markers. */
|
|
44
|
+
cache?: SideCallCachePlacement;
|
|
45
|
+
/** Per-session memo of a model's required thinking level (`withReasoningFallback`). */
|
|
46
|
+
learnedReasoning?: Map<string, ThinkingLevel>;
|
|
47
|
+
/** Each attempt's usage, for the footer's all-in cost. */
|
|
48
|
+
onUsage: (usage: unknown) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type SideCallKind = SideCallLogContext["kind"];
|
|
52
|
+
|
|
53
|
+
/** A side call's cache key, which pi-ai also takes as its `sessionId`. */
|
|
54
|
+
function sideCallKey(sessionId: string, kind: SideCallKind): string {
|
|
55
|
+
return `${sessionId}:${kind}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Close what pi-ai holds open under a session's side-call keys, every kind's
|
|
60
|
+
* (a key never used costs nothing). On openai-codex it keeps one WebSocket per
|
|
61
|
+
* `sessionId` with a 5-minute idle timer holding the process, and pi's own
|
|
62
|
+
* dispose closes only the session's id, so a `pi -p` run waited out the timer
|
|
63
|
+
* after its answer (findings §59). Every extension that runs a side call calls
|
|
64
|
+
* it as the session ends, so a new kind is released with no further change.
|
|
65
|
+
*/
|
|
66
|
+
export function releaseSideCalls(sessionId: string): void {
|
|
67
|
+
for (const kind of SIDE_CALL_KINDS) cleanupSessionResources(sideCallKey(sessionId, kind));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Run the side call and return the final reply; a provider failure is in its `stopReason`, as with `completeSimple`. */
|
|
71
|
+
export async function runSideCall(run: SideCallRun): Promise<AssistantMessage> {
|
|
72
|
+
const { kind, model, auth, context } = run;
|
|
73
|
+
const sessionId = sideCallKey(run.sessionId, kind);
|
|
74
|
+
const target = auth.baseUrl ? ({ ...model, baseUrl: auth.baseUrl } as Model<Api>) : model;
|
|
75
|
+
const onPayload = run.cache?.apis.has(model.api) ? run.cache.place : undefined;
|
|
76
|
+
return withReasoningFallback(
|
|
77
|
+
model,
|
|
78
|
+
async (reasoning) => {
|
|
79
|
+
const timeout = AbortSignal.timeout(run.timeoutMs);
|
|
80
|
+
const reply = await completeSimple(target, context, {
|
|
81
|
+
apiKey: auth.apiKey,
|
|
82
|
+
headers: auth.headers,
|
|
83
|
+
env: auth.env,
|
|
84
|
+
signal: run.signal ? AbortSignal.any([run.signal, timeout]) : timeout,
|
|
85
|
+
maxTokens: run.maxTokens,
|
|
86
|
+
sessionId,
|
|
87
|
+
...(onPayload ? { onPayload } : {}),
|
|
88
|
+
...(reasoning ? { reasoning } : {}),
|
|
89
|
+
});
|
|
90
|
+
logSideCallUsage({ kind, model, sessionId, system: context.systemPrompt ?? "", messages: context.messages }, reply);
|
|
91
|
+
return reply;
|
|
92
|
+
},
|
|
93
|
+
run.learnedReasoning,
|
|
94
|
+
run.onUsage,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/** Opt-in side-call cache evidence, independent of the provider's transport. */
|
|
2
|
+
import { appendFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
/** Every side-call kind; releaseSideCalls closes each one's pi-ai key. */
|
|
5
|
+
export const SIDE_CALL_KINDS = ["reader", "btw", "recap"] as const;
|
|
6
|
+
|
|
7
|
+
export interface SideCallLogContext {
|
|
8
|
+
kind: (typeof SIDE_CALL_KINDS)[number];
|
|
9
|
+
model: { provider: string; id: string; api: string };
|
|
10
|
+
sessionId: string;
|
|
11
|
+
system: string;
|
|
12
|
+
messages: readonly unknown[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* CC_SIDE_CALL_LOG names a JSONL file containing prompt text and normalized
|
|
17
|
+
* usage, never credentials or response text. Like CC_AUTO_MODE_LOG, this is
|
|
18
|
+
* explicitly opt-in and never read back. Log every completed provider attempt,
|
|
19
|
+
* including reasoning retries; a probe must not mistake an error for a hit.
|
|
20
|
+
*/
|
|
21
|
+
export function logSideCallUsage(call: SideCallLogContext, reply: { usage?: unknown; stopReason: string }): void {
|
|
22
|
+
const path = process.env.CC_SIDE_CALL_LOG;
|
|
23
|
+
if (!path) return;
|
|
24
|
+
try {
|
|
25
|
+
appendFileSync(path, `${JSON.stringify({
|
|
26
|
+
kind: call.kind,
|
|
27
|
+
model: `${call.model.provider}/${call.model.id}`,
|
|
28
|
+
api: call.model.api,
|
|
29
|
+
sessionId: call.sessionId,
|
|
30
|
+
system: call.system,
|
|
31
|
+
messages: call.messages,
|
|
32
|
+
usage: reply.usage,
|
|
33
|
+
stopReason: reply.stopReason,
|
|
34
|
+
})}\n`);
|
|
35
|
+
} catch {
|
|
36
|
+
// A diagnostic log that cannot be written must not fail a side call.
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -40,6 +40,18 @@ export function trimToTurnBoundary<T extends { role: string }>(messages: readonl
|
|
|
40
40
|
return start === -1 ? [] : messages.slice(start);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* The conversation without pi's `system` transcript messages, for a capture of
|
|
45
|
+
* the `context` event. pi 0.86 hands `context` handlers a leading system message
|
|
46
|
+
* holding pi's own default prompt sections and every full tool schema; a side
|
|
47
|
+
* call that forwarded it had pi-ai replay those into its system prompt and tools
|
|
48
|
+
* (over the stubs), and it shifted every role-by-index comparison. pi 0.87 strips
|
|
49
|
+
* system messages before `context` handlers run, so there this is a no-op.
|
|
50
|
+
*/
|
|
51
|
+
export function withoutSystemMessages<T extends { role: string }>(messages: readonly T[]): T[] {
|
|
52
|
+
return messages.filter((message) => message.role !== "system");
|
|
53
|
+
}
|
|
54
|
+
|
|
43
55
|
/** The placeholder left in a message whose only content was an image we removed. */
|
|
44
56
|
export const IMAGE_OMITTED_TEXT = "[image omitted]";
|
|
45
57
|
|
|
@@ -55,7 +67,7 @@ export const IMAGE_OMITTED_TEXT = "[image omitted]";
|
|
|
55
67
|
* placeholder text block, so it stays a valid turn on strict providers. Purely
|
|
56
68
|
* structural (any `{ role, content }` message) to stay free of runtime pi
|
|
57
69
|
* imports; assistant messages carry no images and pass through untouched.
|
|
58
|
-
* See `docs/decisions/model-policy.md`.
|
|
70
|
+
* See `working-docs/decisions/model-policy.md`.
|
|
59
71
|
*/
|
|
60
72
|
export function stripImageBlocks<M extends { role: string; content: unknown }>(messages: readonly M[]): M[] {
|
|
61
73
|
return messages.map((message) => {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One in-flight run per key (pure): a call while a run for its key is pending
|
|
3
|
+
* shares that run's promise instead of starting another; once it settles, the
|
|
4
|
+
* next call starts fresh. Used where two overlapping callers would otherwise
|
|
5
|
+
* both pass an "is it there yet?" check across an await and duplicate work
|
|
6
|
+
* that must happen once (a language server spawn, a consent dialog).
|
|
7
|
+
*/
|
|
8
|
+
export function singleFlight<T>(): (key: string, run: () => Promise<T>) => Promise<T> {
|
|
9
|
+
const pending = new Map<string, Promise<T>>();
|
|
10
|
+
return (key, run) => {
|
|
11
|
+
let current = pending.get(key);
|
|
12
|
+
if (!current) {
|
|
13
|
+
current = run().finally(() => pending.delete(key));
|
|
14
|
+
pending.set(key, current);
|
|
15
|
+
}
|
|
16
|
+
return current;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills whose body is generated, not read from their SKILL.md: Claude Code's
|
|
3
|
+
* `/loop` builds a different prompt per argument shape (findings §21). The
|
|
4
|
+
* skill extension emits a `SkillBodyQuery` on SKILL_BODY_CHANNEL before it
|
|
5
|
+
* delivers a skill; the extension that owns the skill fills `body`
|
|
6
|
+
* synchronously, and the SKILL.md body is the fallback.
|
|
7
|
+
*
|
|
8
|
+
* A fired scheduled prompt that is a slash command (`/loop check the deploy`,
|
|
9
|
+
* `/babysit-prs`) runs the way Claude Code runs it, as if typed: the
|
|
10
|
+
* background extension emits a `SlashExpandQuery` on SLASH_EXPAND_CHANNEL
|
|
11
|
+
* and the skill extension fills `expanded` with the skill's block.
|
|
12
|
+
*
|
|
13
|
+
* Pure: no pi imports.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const SKILL_BODY_CHANNEL = "one-code:skill-body";
|
|
17
|
+
export const SLASH_EXPAND_CHANNEL = "one-code:slash-expand";
|
|
18
|
+
|
|
19
|
+
export interface SkillBodyQuery {
|
|
20
|
+
/** The skill's name. */
|
|
21
|
+
skill: string;
|
|
22
|
+
/** Its SKILL.md, so a provider answers only for its own file. */
|
|
23
|
+
path: string;
|
|
24
|
+
args: string;
|
|
25
|
+
cwd: string;
|
|
26
|
+
/** Set by the provider; the arguments are then part of it. */
|
|
27
|
+
body?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface SlashExpandQuery {
|
|
31
|
+
/** The fired prompt, starting with `/`. */
|
|
32
|
+
text: string;
|
|
33
|
+
cwd: string;
|
|
34
|
+
/** Set by the skill extension when the command is a skill. */
|
|
35
|
+
expanded?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** `/name rest` split, or undefined when `text` is not a slash command. */
|
|
39
|
+
export function parseSlashCommand(text: string): { name: string; args: string } | undefined {
|
|
40
|
+
const match = /^\/([^\s/][^\s]*)(?:\s+([\s\S]*))?$/.exec(text.trim());
|
|
41
|
+
return match ? { name: match[1], args: (match[2] ?? "").trim() } : undefined;
|
|
42
|
+
}
|
|
@@ -3,22 +3,26 @@
|
|
|
3
3
|
* command registration (pure fs). (The /plugins Installed tab builds its rows
|
|
4
4
|
* from discoverPlugins directly and no longer calls this.)
|
|
5
5
|
*
|
|
6
|
-
* Scans the SAME directories
|
|
7
|
-
*
|
|
8
|
-
* `<cwd>/.
|
|
9
|
-
*
|
|
10
|
-
*
|
|
6
|
+
* Scans the SAME directories pi loads, in pi's order — `<agentDir>/skills`
|
|
7
|
+
* (pi's own), then the ones claude-compat feeds it: `~/.claude/skills`,
|
|
8
|
+
* `~/.agents/skills`, `<cwd>/.claude/skills`, `<cwd>/.agents/skills`, and the
|
|
9
|
+
* bundled catalog last — and merges the plugin skills the caller resolved.
|
|
10
|
+
* As in pi, a name loads once (the first wins, so the bundled catalog loses a
|
|
11
|
+
* collision) and a file reached twice through a link counts once, so the
|
|
12
|
+
* banner, `/skills` and `/doctor` count the skills pi actually loads. Rows carry the SKILL.md path (for the ~token
|
|
11
13
|
* size estimate) and the scope used by skill-overrides keys. Before the first
|
|
12
14
|
* turn this scan is the only skill list One Code has (pi resolves skills per
|
|
13
15
|
* turn), so a root missing here is a skill with no `/name` command at startup.
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
|
-
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
18
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
17
19
|
import { dirname, join } from "node:path";
|
|
18
20
|
import { fileURLToPath } from "node:url";
|
|
19
21
|
import type { PluginSkill } from "./plugins.ts";
|
|
20
22
|
import type { SkillScope } from "./skill-overrides.ts";
|
|
21
|
-
import {
|
|
23
|
+
import { claudeSourcesOn, projectConfigDir, userConfigDir } from "./config-mode.ts";
|
|
24
|
+
import { claudeUserDir, isPathAtOrUnder, tryRealpath } from "./paths.ts";
|
|
25
|
+
import { parseFrontmatterLoosely } from "./frontmatter.ts";
|
|
22
26
|
|
|
23
27
|
/** The skill catalog shipped in this package: `<package>/skills` (Claude Code's self-contained built-in skills). */
|
|
24
28
|
export const BUNDLED_SKILLS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
|
|
@@ -31,16 +35,38 @@ export interface ScannedSkill {
|
|
|
31
35
|
|
|
32
36
|
/**
|
|
33
37
|
* The one scope-classification rule for a skill path — user skills live under
|
|
34
|
-
* `~/.claude/skills`
|
|
38
|
+
* `~/.claude/skills`, `~/.agents/skills` and `<agentDir>/skills`; everything else is project
|
|
35
39
|
* scope. Both the scanner and the skill extension classify through this, so a
|
|
36
40
|
* skill never shows different scopes in /plugins vs the skill tool.
|
|
37
41
|
*/
|
|
38
42
|
export function scopeForPath(path: string, home: string, agentDir: string): SkillScope {
|
|
39
|
-
|
|
40
|
-
return "project";
|
|
43
|
+
const roots = [join(claudeUserDir(home), "skills"), join(home, ".agents", "skills"), join(agentDir, "skills")];
|
|
44
|
+
return roots.some((root) => isPathAtOrUnder(path, root)) ? "user" : "project";
|
|
41
45
|
}
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The skill folders One Code adds to pi's own (`<agentDir>/skills`, which pi
|
|
49
|
+
* scans itself), in load order: Claude Code's two only in Claude-compatible
|
|
50
|
+
* mode (lib/config-mode.ts), the cross-tool `.agents` pair in both. `claudeDir`
|
|
51
|
+
* defaults to `~/.claude` (`CLAUDE_CONFIG_DIR`); `~/.agents` is never moved.
|
|
52
|
+
*/
|
|
53
|
+
export function skillSourceDirs(cwd: string, home: string, claudeDir: string = claudeUserDir(home)): Array<{ dir: string; scope: SkillScope }> {
|
|
54
|
+
const claude = claudeSourcesOn();
|
|
55
|
+
const dirs: Array<{ dir: string; scope: SkillScope } | false> = [
|
|
56
|
+
claude && { dir: join(claudeDir, "skills"), scope: "user" },
|
|
57
|
+
{ dir: join(home, ".agents", "skills"), scope: "user" },
|
|
58
|
+
claude && { dir: join(cwd, ".claude", "skills"), scope: "project" },
|
|
59
|
+
{ dir: join(cwd, ".agents", "skills"), scope: "project" },
|
|
60
|
+
];
|
|
61
|
+
return dirs.filter((d): d is { dir: string; scope: SkillScope } => d !== false);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The prompt-template folders: `~/.claude/commands` and `.claude/commands`, or their `.onecode` twins in independent mode. */
|
|
65
|
+
export function commandDirs(cwd: string, home: string, claudeDir: string = claudeUserDir(home)): string[] {
|
|
66
|
+
return [join(claudeSourcesOn() ? claudeDir : userConfigDir(home), "commands"), join(projectConfigDir(cwd), "commands")];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function scanDir(dir: string, scope: SkillScope, into: Map<string, ScannedSkill>, seenFiles: Set<string>): void {
|
|
44
70
|
if (!existsSync(dir)) return;
|
|
45
71
|
let entries: string[];
|
|
46
72
|
try {
|
|
@@ -51,9 +77,18 @@ function scanDir(dir: string, scope: SkillScope, into: Map<string, ScannedSkill>
|
|
|
51
77
|
for (const entry of entries) {
|
|
52
78
|
const skillFile = join(dir, entry, "SKILL.md");
|
|
53
79
|
// existsSync follows symlinked skill directories, which isDirectory() would miss.
|
|
54
|
-
if (existsSync(skillFile)
|
|
55
|
-
|
|
80
|
+
if (!existsSync(skillFile)) continue;
|
|
81
|
+
let name = entry;
|
|
82
|
+
try {
|
|
83
|
+
const declared = parseFrontmatterLoosely(readFileSync(skillFile, "utf-8")).frontmatter.name;
|
|
84
|
+
if (typeof declared === "string" && declared) name = declared;
|
|
85
|
+
} catch {
|
|
86
|
+
continue;
|
|
56
87
|
}
|
|
88
|
+
const real = tryRealpath(skillFile) ?? skillFile;
|
|
89
|
+
if (into.has(name) || seenFiles.has(real)) continue;
|
|
90
|
+
seenFiles.add(real);
|
|
91
|
+
into.set(name, { name, path: skillFile, scope });
|
|
57
92
|
}
|
|
58
93
|
}
|
|
59
94
|
|
|
@@ -65,12 +100,10 @@ export function scanSkills(
|
|
|
65
100
|
bundledSkillsDir?: string,
|
|
66
101
|
): ScannedSkill[] {
|
|
67
102
|
const skills = new Map<string, ScannedSkill>();
|
|
68
|
-
|
|
69
|
-
scanDir(join(
|
|
70
|
-
|
|
71
|
-
scanDir(
|
|
72
|
-
scanDir(join(agentDir, "skills"), "user", skills);
|
|
73
|
-
if (bundledSkillsDir) scanDir(bundledSkillsDir, scopeForPath(bundledSkillsDir, home, agentDir), skills);
|
|
103
|
+
const seenFiles = new Set<string>();
|
|
104
|
+
scanDir(join(agentDir, "skills"), "user", skills, seenFiles);
|
|
105
|
+
for (const { dir, scope } of skillSourceDirs(cwd, home)) scanDir(dir, scope, skills, seenFiles);
|
|
106
|
+
if (bundledSkillsDir) scanDir(bundledSkillsDir, scopeForPath(bundledSkillsDir, home, agentDir), skills, seenFiles);
|
|
74
107
|
for (const skill of pluginSkills) {
|
|
75
108
|
skills.set(`plugin:${skill.name}`, { name: skill.name, path: skill.path, scope: "plugin" });
|
|
76
109
|
}
|
|
@@ -80,14 +113,20 @@ export function scanSkills(
|
|
|
80
113
|
/**
|
|
81
114
|
* Names of the `.claude/commands` prompt templates (`<name>.md`) pi will expose
|
|
82
115
|
* as `/<name>` — the same dirs claude-compat feeds pi (`~/.claude/commands`,
|
|
83
|
-
* `<cwd>/.claude/commands
|
|
116
|
+
* `<cwd>/.claude/commands`, or their `.onecode` twins in independent mode)
|
|
117
|
+
* plus pi's own `<agentDir>/prompts`. pi resolves
|
|
84
118
|
* templates per turn, after `session_start`, so a bare skill command registered
|
|
85
119
|
* then would silently shadow a same-named template for the whole session;
|
|
86
120
|
* this pre-scan lets the registration skip those names.
|
|
87
121
|
*/
|
|
88
122
|
export function promptTemplateNames(cwd: string, home: string, agentDir: string): string[] {
|
|
89
|
-
|
|
90
|
-
|
|
123
|
+
return [...new Set(promptTemplateFiles(cwd, home, agentDir).map((file) => file.name))];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Every prompt-template file pi will resolve, with the command name each gives. */
|
|
127
|
+
export function promptTemplateFiles(cwd: string, home: string, agentDir: string): { name: string; path: string }[] {
|
|
128
|
+
const files: { name: string; path: string }[] = [];
|
|
129
|
+
for (const dir of [...commandDirs(cwd, home), join(agentDir, "prompts")]) {
|
|
91
130
|
if (!existsSync(dir)) continue;
|
|
92
131
|
let entries: string[];
|
|
93
132
|
try {
|
|
@@ -95,9 +134,9 @@ export function promptTemplateNames(cwd: string, home: string, agentDir: string)
|
|
|
95
134
|
} catch {
|
|
96
135
|
continue;
|
|
97
136
|
}
|
|
98
|
-
for (const entry of entries) if (entry.endsWith(".md"))
|
|
137
|
+
for (const entry of entries) if (entry.endsWith(".md")) files.push({ name: entry.slice(0, -3), path: join(dir, entry) });
|
|
99
138
|
}
|
|
100
|
-
return
|
|
139
|
+
return files;
|
|
101
140
|
}
|
|
102
141
|
|
|
103
142
|
/** Rough token estimate from the SKILL.md byte size (~4 bytes/token). */
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep pi's foreground shell spill file readable.
|
|
3
|
+
*
|
|
4
|
+
* When a foreground `bash` or `powershell` run prints more than pi keeps, pi
|
|
5
|
+
* writes the whole output to `<os.tmpdir()>/pi-<shell>-<16 hex>.log` and ends
|
|
6
|
+
* the text with `[… Full output: <path>]`, on success and on a failure: a
|
|
7
|
+
* non-zero exit is an error result from pi 0.99 on and a thrown error before
|
|
8
|
+
* it, and a timeout or an abort is always thrown. `os.tmpdir()` is outside
|
|
9
|
+
* every root the permission gate treats as readable, so the "full output is
|
|
10
|
+
* one read away" promise held only where outside reads skip the gate. So the
|
|
11
|
+
* file is moved into the session's persisted-results dir (a readable root)
|
|
12
|
+
* and the path in the text is rewritten to match.
|
|
13
|
+
*
|
|
14
|
+
* Only a file pi produced is moved: the path must sit directly in the temp
|
|
15
|
+
* dir and carry pi's name shape, so a command that prints a look-alike line
|
|
16
|
+
* cannot make the harness move some other file.
|
|
17
|
+
*
|
|
18
|
+
* The output is also cut Claude Code's way instead of pi's. pi
|
|
19
|
+
* keeps the last 2,000 lines or 50 KB and drops the head, so a batched
|
|
20
|
+
* command lost its first commands' output. Claude Code keeps the head: the
|
|
21
|
+
* whole output up to its inline cap, past it the persisted block naming the
|
|
22
|
+
* file, with the first 2 KB as the preview. pi's spill file holds the whole
|
|
23
|
+
* output, so the block names it and nothing is copied.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { copyFileSync, mkdirSync, readFileSync, renameSync, statSync, unlinkSync } from "node:fs";
|
|
27
|
+
import { tmpdir } from "node:os";
|
|
28
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
29
|
+
import { persistedFilePreview, persistIfLarge } from "./persisted-output.ts";
|
|
30
|
+
|
|
31
|
+
const MARKER = "Full output: ";
|
|
32
|
+
const PI_SPILL_NAME = /^pi-[a-z]+-[0-9a-f]{16}\.log$/;
|
|
33
|
+
/** The note pi ends a cut output with: `\n\n[Showing lines …` or `\n\n[Showing last …`. */
|
|
34
|
+
const PI_NOTE = "\n\n[Showing ";
|
|
35
|
+
/** pi's status after the output of a run that did not exit 0. */
|
|
36
|
+
const PI_STATUS = /(?:^|\n\n)Command (?:exited with code -?\d+|timed out after \S+ seconds|aborted|terminated without an exit code)$/;
|
|
37
|
+
|
|
38
|
+
/** How a shell's output is cut: whole up to `inlineMax` bytes, past it persisted as `<id>.txt` when pi did not spill it. */
|
|
39
|
+
export interface OutputBound {
|
|
40
|
+
inlineMax: number;
|
|
41
|
+
id: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The spill path pi named at the end of `text`, when it is one pi produced in `tmp`. Pure. */
|
|
45
|
+
export function piSpillPath(text: string, tmp: string = tmpdir()): string | undefined {
|
|
46
|
+
const at = text.lastIndexOf(MARKER);
|
|
47
|
+
if (at === -1) return undefined;
|
|
48
|
+
const close = text.indexOf("]", at + MARKER.length);
|
|
49
|
+
if (close === -1) return undefined;
|
|
50
|
+
const path = text.slice(at + MARKER.length, close);
|
|
51
|
+
if (!PI_SPILL_NAME.test(basename(path))) return undefined;
|
|
52
|
+
if (resolve(dirname(path)) !== resolve(tmp)) return undefined;
|
|
53
|
+
return path;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Move `from` into `dir`, keeping its name; the new path, or undefined when it could not move. */
|
|
57
|
+
function moveInto(from: string, dir: string): string | undefined {
|
|
58
|
+
const to = join(dir, basename(from));
|
|
59
|
+
try {
|
|
60
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
61
|
+
try {
|
|
62
|
+
renameSync(from, to);
|
|
63
|
+
} catch {
|
|
64
|
+
// A different filesystem: copy, then drop the original.
|
|
65
|
+
copyFileSync(from, to);
|
|
66
|
+
unlinkSync(from);
|
|
67
|
+
}
|
|
68
|
+
return to;
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* `text` with its spill file moved and pi's tail cut replaced by Claude
|
|
76
|
+
* Code's head-first one. A spill file that cannot be moved keeps pi's text;
|
|
77
|
+
* one that cannot be read, or a text not in pi's shape, keeps pi's text with
|
|
78
|
+
* only the path rewritten.
|
|
79
|
+
*/
|
|
80
|
+
export function boundSpillIn(text: string, resultsDir: string, bound: OutputBound, failed = true, tmp: string = tmpdir()): string {
|
|
81
|
+
// pi appends a status only to a run that did not exit 0; a successful
|
|
82
|
+
// command's own last line can look like one.
|
|
83
|
+
const status = failed ? PI_STATUS.exec(text) : null;
|
|
84
|
+
const end = status ? status.index : text.length;
|
|
85
|
+
const output = text.slice(0, end);
|
|
86
|
+
const from = piSpillPath(output, tmp);
|
|
87
|
+
if (!from) return persistIfLarge(output, { dir: resultsDir, id: bound.id, maxBytes: bound.inlineMax }) + text.slice(end);
|
|
88
|
+
const to = moveInto(from, resultsDir);
|
|
89
|
+
if (!to) return text;
|
|
90
|
+
// pi's note ends the output; anything else keeps pi's text.
|
|
91
|
+
const whole = output.includes(PI_NOTE) && output.endsWith(`${from}]`) ? wholeOutput(to, bound.inlineMax) : undefined;
|
|
92
|
+
return whole === undefined ? text.split(from).join(to) : whole + text.slice(end);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** The spill file inline while it fits `inlineMax`, past it the persisted block; undefined when unreadable. */
|
|
96
|
+
function wholeOutput(path: string, inlineMax: number): string | undefined {
|
|
97
|
+
try {
|
|
98
|
+
return statSync(path).size <= inlineMax ? readFileSync(path, "utf8") : persistedFilePreview(path);
|
|
99
|
+
} catch {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type TextResult = { content: Array<{ type: string; text?: string }>; details?: unknown };
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Run a foreground shell execute and keep its spill file readable: on a
|
|
108
|
+
* returned result the text and `details.fullOutputPath` are rewritten, and a
|
|
109
|
+
* thrown error is rethrown with its message rewritten, the output cut
|
|
110
|
+
* Claude Code's way (`bound`).
|
|
111
|
+
*/
|
|
112
|
+
export async function keepSpillReadable<R extends TextResult>(run: () => Promise<R>, resultsDir: string, bound: OutputBound): Promise<R> {
|
|
113
|
+
const rewrite = (text: string, failed: boolean) => boundSpillIn(text, resultsDir, bound, failed);
|
|
114
|
+
let result: R;
|
|
115
|
+
try {
|
|
116
|
+
result = await run();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
if (error instanceof Error) {
|
|
119
|
+
const message = rewrite(error.message, true);
|
|
120
|
+
if (message !== error.message) {
|
|
121
|
+
const moved = new Error(message);
|
|
122
|
+
moved.stack = error.stack;
|
|
123
|
+
throw moved;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
const details = result.details as { fullOutputPath?: unknown } | undefined;
|
|
129
|
+
const text = result.content.find((block) => block.type === "text" && typeof block.text === "string");
|
|
130
|
+
if (!text?.text) return result;
|
|
131
|
+
const rewritten = rewrite(text.text, (result as { isError?: boolean }).isError === true);
|
|
132
|
+
if (rewritten === text.text) return result;
|
|
133
|
+
const from = piSpillPath(text.text);
|
|
134
|
+
const to = from ? join(resultsDir, basename(from)) : undefined;
|
|
135
|
+
return {
|
|
136
|
+
...result,
|
|
137
|
+
content: result.content.map((block) => (block === text ? { ...block, text: rewritten } : block)),
|
|
138
|
+
details: details && typeof details === "object" && to ? { ...details, fullOutputPath: to } : result.details,
|
|
139
|
+
};
|
|
140
|
+
}
|