one-code-extension 0.4.1 → 0.6.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 +40 -28
- package/docs/guide/doctor.md +20 -13
- package/docs/guide/hooks.md +18 -3
- package/docs/guide/installation.md +26 -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 +124 -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 +642 -0
- package/extensions/background/index.ts +614 -248
- package/extensions/background/loop-fire.ts +173 -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 +56 -0
- package/extensions/background/wakeup.ts +178 -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 +66 -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 +250 -78
- package/extensions/btw/panel.ts +228 -100
- package/extensions/btw/prompt.ts +41 -4
- package/extensions/cache-marks/index.ts +42 -0
- package/extensions/claude-compat/index.ts +7 -11
- package/extensions/claude-context/index.ts +436 -63
- package/extensions/compaction/cut.ts +157 -0
- package/extensions/compaction/index.ts +219 -75
- package/extensions/compaction/kept-thinking.ts +62 -0
- package/extensions/compaction/prompt.ts +15 -12
- package/extensions/context-budget/budget.ts +16 -2
- package/extensions/context-budget/index.ts +35 -15
- package/extensions/context-management/index.ts +36 -28
- 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 +141 -25
- 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 +420 -42
- package/extensions/file-tracker/replay.ts +137 -24
- package/extensions/file-tracker/restore.ts +66 -0
- package/extensions/file-tracker/results.ts +66 -0
- package/extensions/file-tracker/shell-reads.ts +211 -0
- package/extensions/file-tracker/tracker.ts +45 -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 +209 -58
- package/extensions/hooks/matcher.ts +3 -0
- package/extensions/hooks/prompt-context.ts +59 -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 +26 -12
- 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/ledger/index.ts +46 -0
- package/extensions/ledger/wire.ts +154 -0
- package/extensions/lib/agent-cron.ts +146 -0
- package/extensions/lib/agent-loader.ts +18 -3
- package/extensions/lib/agent-view.ts +85 -0
- package/extensions/lib/anthropic-auth.ts +98 -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 +52 -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 +97 -0
- package/extensions/lib/covered-messages.ts +23 -0
- package/extensions/lib/deferred.ts +359 -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/ledger/fold-memo.ts +49 -0
- package/extensions/lib/ledger/fold.ts +265 -0
- package/extensions/lib/ledger/layout.ts +72 -0
- package/extensions/lib/ledger/plan.ts +102 -0
- package/extensions/lib/ledger/records.ts +276 -0
- package/extensions/lib/ledger/state.ts +47 -0
- package/extensions/lib/ledger/validate.ts +141 -0
- package/extensions/lib/ledger/wire.ts +632 -0
- package/extensions/lib/local-command.ts +71 -19
- 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 +178 -0
- package/extensions/lib/model-catalog/models-dev.json +18533 -0
- package/extensions/lib/model-catalog/openrouter.json +5050 -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 +357 -73
- 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 +38 -2
- package/extensions/lib/plan-mode-channels.ts +42 -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/previous-mark.ts +101 -0
- package/extensions/lib/process-state.ts +48 -0
- package/extensions/lib/process-tree.ts +132 -8
- package/extensions/lib/prompt-options.ts +82 -0
- package/extensions/lib/queued-delivery.ts +44 -0
- package/extensions/lib/reminders.ts +370 -358
- package/extensions/lib/replaced-builtins.mjs +199 -0
- package/extensions/lib/replay-call.ts +70 -0
- package/extensions/lib/request-replay.ts +547 -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 +24 -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 +112 -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 +129 -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 +159 -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 +76 -12
- 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 +90 -43
- 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 +220 -121
- 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 +164 -73
- 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/model-default/index.ts +86 -19
- 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 +1426 -398
- package/extensions/permissions/matcher.ts +425 -74
- package/extensions/permissions/modes.ts +11 -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 +276 -92
- package/extensions/plan-mode/reminder.ts +89 -21
- package/extensions/plan-mode/run-state.ts +26 -0
- 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 +64 -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 +14 -7
- package/extensions/recap/index.ts +73 -71
- 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/announce.ts +30 -0
- package/extensions/skill/code-review.ts +97 -0
- package/extensions/skill/index.ts +469 -132
- package/extensions/skill/invoke.ts +118 -75
- 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 +146 -31
- package/extensions/subagents/default-model.ts +2 -3
- package/extensions/subagents/delegation-steer.ts +4 -4
- package/extensions/subagents/fork-cache.ts +82 -0
- package/extensions/subagents/guide-agent.ts +189 -0
- package/extensions/subagents/hand-back-review.ts +3 -0
- package/extensions/subagents/index.ts +1594 -852
- package/extensions/subagents/live-runs.ts +26 -6
- package/extensions/subagents/model-select.ts +38 -25
- package/extensions/subagents/outcome.ts +5 -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/parent-mode.ts +50 -0
- package/extensions/subagents/pending-claim.ts +18 -1
- package/extensions/subagents/runner.ts +135 -80
- package/extensions/subagents/runs.ts +44 -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 +165 -47
- package/extensions/system-prompt/template.ts +34 -51
- 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 +204 -70
- 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/index.ts +366 -144
- 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 +108 -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 +157 -162
- package/extensions/workflow/records.ts +1 -0
- package/extensions/workflow/run-manager.ts +33 -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 +162 -44
- package/extensions/worktree/powershell-guards.ts +167 -0
- package/extensions/worktree/rewrite.ts +14 -16
- package/package.json +21 -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
- package/extensions/tool-search/announce.ts +0 -42
|
@@ -2,24 +2,30 @@
|
|
|
2
2
|
* skill extension — Claude Code's Skill tool.
|
|
3
3
|
*
|
|
4
4
|
* pi's own mechanism lists skills in the system prompt and expects the model to
|
|
5
|
-
* `read` the SKILL.md path. Claude Code instead exposes a `skill` tool
|
|
6
|
-
*
|
|
7
|
-
* skill can be invoked by name — including plugin
|
|
5
|
+
* `read` the SKILL.md path. Claude Code instead exposes a `skill` tool, which
|
|
6
|
+
* answers "Launching skill: <name>" with the skill's instructions beside it.
|
|
7
|
+
* This adds that tool, so a skill can be invoked by name — including plugin
|
|
8
|
+
* skills as `<plugin>:<skill>` — and expands a skill the user types into its
|
|
9
|
+
* instructions before the model sees the message, as Claude Code does.
|
|
8
10
|
*
|
|
9
11
|
* Skills discovered by pi (which includes `~/.claude/skills` and
|
|
10
12
|
* `.claude/skills` thanks to the claude-compat extension) are read from
|
|
11
13
|
* `before_agent_start`'s systemPromptOptions rather than rediscovered here.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
|
-
import { readFileSync } from "node:fs";
|
|
16
|
+
import { readFileSync, statSync } from "node:fs";
|
|
15
17
|
import os from "node:os";
|
|
18
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
19
|
+
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
16
20
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
-
import { getAgentDir
|
|
21
|
+
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
18
22
|
import { Type } from "typebox";
|
|
23
|
+
import { canShowCustomUi, notifyOrPrint, notifyRpcReadOnly } from "../lib/headless-output.ts";
|
|
19
24
|
import { pluginRoot } from "../lib/plugin-root.ts";
|
|
20
25
|
import { defaultDiscoverRoots, discoverPlugins } from "../lib/plugins.ts";
|
|
21
|
-
import { awaitOneShotTurn } from "../lib/notifications.ts";
|
|
22
|
-
import {
|
|
26
|
+
import { awaitOneShotTurn, sessionOutlivesTurn } from "../lib/notifications.ts";
|
|
27
|
+
import { PROMPT_OPTIONS_CHANNEL, type PromptOptionsAnnouncement } from "../lib/prompt-options.ts";
|
|
28
|
+
import { CONTEXT_ORDER, REMINDER_CHANNEL, RENDER_CHANNEL, type RenderRequest } from "../lib/reminders.ts";
|
|
23
29
|
import {
|
|
24
30
|
nextSkillState,
|
|
25
31
|
readSkillStates,
|
|
@@ -30,22 +36,34 @@ import {
|
|
|
30
36
|
skillStateFor,
|
|
31
37
|
} from "../lib/skill-overrides.ts";
|
|
32
38
|
import { BUNDLED_SKILLS_DIR, estimateSkillTokens, promptTemplateNames, scanSkills, scopeForPath } from "../lib/skill-scan.ts";
|
|
33
|
-
import {
|
|
39
|
+
import { sessionModelTier } from "../lib/session-model-tier.ts";
|
|
40
|
+
import { readUsage, recordUsage, usageKey } from "../lib/usage-tracker.ts";
|
|
34
41
|
import { boundedDockHeight, ccToolRenderers, safeThemeBold, safeThemePaint, truncateLine } from "../lib/tui-render.ts";
|
|
35
42
|
import {
|
|
36
43
|
bareSkillMatches,
|
|
37
|
-
|
|
44
|
+
commandDisplayLine,
|
|
45
|
+
launchingSkill,
|
|
46
|
+
missingArgumentsNote,
|
|
38
47
|
parseSkillCommand,
|
|
39
|
-
redactOffSkillMessages,
|
|
40
48
|
resolveSkill,
|
|
49
|
+
SKILL_INVOCATION_TYPE,
|
|
50
|
+
type SkillInvocationDetails,
|
|
41
51
|
skillCommandCandidates,
|
|
52
|
+
skillPromptText,
|
|
53
|
+
typedSkillContent,
|
|
54
|
+
withoutDuplicateSkillCommands,
|
|
42
55
|
} from "./invoke.ts";
|
|
56
|
+
import { codeReviewBody } from "./code-review.ts";
|
|
43
57
|
import { decodeSkillsKey } from "./panel/keys.ts";
|
|
44
|
-
import { skillListingText } from "./listing.ts";
|
|
58
|
+
import { frontmatterFlag, readableSkillListing, skillListingBudget, skillListingText, usageScore, withoutPiSkillsBlock } from "./listing.ts";
|
|
45
59
|
import { renderSkillsPanel, type SkillsPaint } from "./panel/render.ts";
|
|
46
|
-
import {
|
|
60
|
+
import { listableSkillNames, SKILL_LISTING_HEADER } from "./announce.ts";
|
|
61
|
+
import { applySkillsKey, followRow, initialSkillsState, type SkillsRow, visibleRows } from "./panel/state.ts";
|
|
62
|
+
import { announceArgumentHint, type CommandHint, frontmatterCommandHint } from "../lib/argument-hints.ts";
|
|
63
|
+
import { parseSlashCommand, SKILL_BODY_CHANNEL, type SkillBodyQuery, SLASH_EXPAND_CHANNEL, type SlashExpandQuery } from "../lib/skill-body.ts";
|
|
47
64
|
import { parseFrontmatterLoosely } from "../lib/frontmatter.ts";
|
|
48
65
|
import { registerLocalCommand } from "../lib/local-command.ts";
|
|
66
|
+
import { panelUi } from "../lib/panel-focus.ts";
|
|
49
67
|
|
|
50
68
|
interface IndexedSkill {
|
|
51
69
|
name: string;
|
|
@@ -55,22 +73,102 @@ interface IndexedSkill {
|
|
|
55
73
|
scope: SkillScope;
|
|
56
74
|
state: SkillState;
|
|
57
75
|
pluginName?: string;
|
|
76
|
+
/** The `when_to_use` frontmatter, which Claude Code lists after the description. */
|
|
77
|
+
whenToUse?: string;
|
|
78
|
+
/**
|
|
79
|
+
* A plugin command (`<plugin>:<command>`): listed for the model and
|
|
80
|
+
* loadable through the tool, as in Claude Code; typed, it stays the
|
|
81
|
+
* plugins extension's command.
|
|
82
|
+
*/
|
|
83
|
+
kind?: "command";
|
|
84
|
+
/**
|
|
85
|
+
* Frontmatter `disable-model-invocation: true`: only the user starts it (a
|
|
86
|
+
* typed `/<name>`); it is left out of the model's listing and the `skill`
|
|
87
|
+
* tool refuses it, as Claude Code's Skill tool does.
|
|
88
|
+
*/
|
|
89
|
+
disableModelInvocation?: boolean;
|
|
58
90
|
}
|
|
59
91
|
|
|
92
|
+
|
|
60
93
|
/** Bounded dock like the /plugins panel — keeps the transcript visible above. */
|
|
61
94
|
const SKILLS_PANEL_MAX_HEIGHT = 24;
|
|
62
95
|
|
|
96
|
+
const BUNDLED_ROOT = resolve(BUNDLED_SKILLS_DIR);
|
|
97
|
+
/** A skill in One Code's own catalog, like one built into Claude Code: listed last, kept whole over budget, no folder named. */
|
|
98
|
+
const isBundled = (path: string): boolean => resolve(path).startsWith(BUNDLED_ROOT + sep);
|
|
99
|
+
const CODE_REVIEW_PATH = join(BUNDLED_ROOT, "code-review", "SKILL.md");
|
|
100
|
+
const CODE_REVIEW_CELLS = join(BUNDLED_ROOT, "code-review", "cells");
|
|
101
|
+
|
|
63
102
|
export default function skillExtension(pi: ExtensionAPI) {
|
|
103
|
+
const requestTier = sessionModelTier(pi);
|
|
104
|
+
// pi draws a user message as markdown, so a typed skill, `/init` and
|
|
105
|
+
// `/doctor` showed their whole expanded prompt; show the command line.
|
|
106
|
+
pi.registerMarkdownTransformer((markdown, context) =>
|
|
107
|
+
context.messageType === "user" ? (commandDisplayLine(markdown) ?? markdown) : markdown,
|
|
108
|
+
);
|
|
64
109
|
/** pi resolves skills per turn; cache the latest list for the tool to use. */
|
|
65
110
|
let piSkills: IndexedSkill[] = [];
|
|
66
111
|
/** Session cwd, so project-level enabledPlugins settings apply to plugin skills. */
|
|
67
112
|
let sessionCwd: string | undefined;
|
|
113
|
+
/** The session's model: its context window sizes the listing, its tier picks code-review's body. */
|
|
114
|
+
let sessionModel: Model<Api> | undefined;
|
|
68
115
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
116
|
+
/**
|
|
117
|
+
* True once a `prompt()` has run (the only path that emits
|
|
118
|
+
* before_agent_start). Until then an idle skill delivery goes out as a user
|
|
119
|
+
* message: pi's `sendMessage(…, {triggerTurn})` skips that preamble, so a
|
|
120
|
+
* session opening with `/loop …` or `/simplify` ran its first request
|
|
121
|
+
* without our system prompt or reminder stack (lib/notifications.ts,
|
|
122
|
+
* "First turn of a session"; upstream_prs.md #17). Later turns get the
|
|
123
|
+
* prompt from system-prompt's context_with_system handler (idle-turn.ts).
|
|
124
|
+
*/
|
|
125
|
+
let prompted = false;
|
|
126
|
+
/**
|
|
127
|
+
* A one-shot run (`-p`, `--mode json`) ends when its prompt returns, so the
|
|
128
|
+
* bare `/<name>` skill forms are taken by the `input` hook there instead of
|
|
129
|
+
* registered as commands: a command can only start a second prompt, which
|
|
130
|
+
* the run does not wait for.
|
|
131
|
+
*/
|
|
132
|
+
let oneShot = false;
|
|
133
|
+
/** The bare skill names a one-shot run takes in the `input` hook. */
|
|
134
|
+
const oneShotSkillNames = new Set<string>();
|
|
135
|
+
|
|
136
|
+
// A factory re-run builds each session's instance, so this is belt and braces
|
|
137
|
+
// (lib/notifications.ts resets its twin the same way).
|
|
138
|
+
pi.on("session_start", (_event, ctx) => {
|
|
139
|
+
prompted = false;
|
|
140
|
+
oneShot = !sessionOutlivesTurn(ctx.mode);
|
|
141
|
+
announcedSkills = false;
|
|
142
|
+
sessionUsage = undefined;
|
|
143
|
+
sessionModel = ctx.model;
|
|
144
|
+
});
|
|
145
|
+
/** The index as the current run's first request read it; a new run, or pi's skills for it, reads it again. */
|
|
146
|
+
let runIndex: IndexedSkill[] | undefined;
|
|
147
|
+
pi.on("agent_start", () => {
|
|
148
|
+
runIndex = undefined;
|
|
149
|
+
});
|
|
150
|
+
// Inside an epoch the listing on message 1 is the record: a skill that became
|
|
151
|
+
// listable since rides the request's tail with only its own line, decided
|
|
152
|
+
// when the request goes out from what the epoch has told.
|
|
153
|
+
pi.events.on(RENDER_CHANNEL, (data) => {
|
|
154
|
+
const request = data as RenderRequest;
|
|
155
|
+
if (request.epoch) return;
|
|
156
|
+
const told = request.told.get("skills") ?? new Set<string>();
|
|
157
|
+
// Read once per run: a run makes many requests, and the index reads every skill file.
|
|
158
|
+
const all = (runIndex ??= index());
|
|
159
|
+
const added = new Set(listableNow(all).filter((name) => !told.has(name)));
|
|
160
|
+
if (added.size === 0) return;
|
|
161
|
+
const text = listingText(all.filter((skill) => added.has(skill.name)));
|
|
162
|
+
if (text) request.blocks.push({ text, facts: [{ kind: "skills", names: [...added] }] });
|
|
163
|
+
});
|
|
164
|
+
pi.on("model_select", (event) => {
|
|
165
|
+
sessionModel = event.model;
|
|
166
|
+
});
|
|
167
|
+
/** Index the skills pi resolved for this turn and list them for the model. */
|
|
168
|
+
const adoptPiSkills = (skills: unknown[], cwd: string) => {
|
|
169
|
+
sessionCwd = cwd;
|
|
72
170
|
piSkills = skills.map((skill) => {
|
|
73
|
-
const record = skill as unknown as { name: string; description?: string; path?: string; filePath?: string };
|
|
171
|
+
const record = skill as unknown as { name: string; description?: string; path?: string; filePath?: string; disableModelInvocation?: boolean };
|
|
74
172
|
const path = record.path ?? record.filePath ?? "";
|
|
75
173
|
return {
|
|
76
174
|
name: record.name,
|
|
@@ -79,20 +177,65 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
79
177
|
source: "project" as const,
|
|
80
178
|
scope: scopeForPath(path, os.homedir(), getAgentDir()),
|
|
81
179
|
state: "on" as const, // resolved per index() call below
|
|
180
|
+
// pi reads only a YAML `true`; Claude Code also takes the string "true".
|
|
181
|
+
disableModelInvocation: record.disableModelInvocation === true || (path ? readModelInvocationDisabled(path) : false),
|
|
82
182
|
};
|
|
83
183
|
});
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
184
|
+
announceSkills();
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Claude Code lists skills as a <system-reminder> on the first user message
|
|
188
|
+
* (its block 3), framed for the Skill tool — not in the system prompt. A
|
|
189
|
+
* session without that tool (an agent limited to read and bash, a main
|
|
190
|
+
* session started with fewer tools) gets the same block framed for reading
|
|
191
|
+
* the file, since pi's own section is dropped from every prompt below.
|
|
192
|
+
* `skills` is the whole index or, for a later notice, only the new ones.
|
|
193
|
+
*/
|
|
194
|
+
const listingText = (skills: IndexedSkill[]): string | undefined => {
|
|
195
|
+
const tools = pi.getActiveTools();
|
|
196
|
+
if (!tools.includes("skill")) return readableSkillListing(skills, tools.includes("read") ? "the read tool" : "the shell");
|
|
197
|
+
const listing = describe(skills);
|
|
198
|
+
return listing === "(no skills available)" ? undefined : `${SKILL_LISTING_HEADER}\n\n${listing}`;
|
|
199
|
+
};
|
|
200
|
+
const listableNow = (skills: IndexedSkill[]) => listableSkillNames(skills, !pi.getActiveTools().includes("skill"));
|
|
201
|
+
/** The listing as live state: what an epoch's message 1 says (the render step above announces later skills). */
|
|
202
|
+
const announceSkills = () => {
|
|
203
|
+
const all = index();
|
|
204
|
+
const text = listingText(all);
|
|
205
|
+
if (!text) {
|
|
206
|
+
pi.events.emit(REMINDER_CHANNEL, { key: "skills", remove: true });
|
|
207
|
+
return;
|
|
95
208
|
}
|
|
209
|
+
pi.events.emit(REMINDER_CHANNEL, {
|
|
210
|
+
text,
|
|
211
|
+
key: "skills",
|
|
212
|
+
placement: "first-prepend",
|
|
213
|
+
order: CONTEXT_ORDER.skills,
|
|
214
|
+
facts: [{ kind: "skills", names: listableNow(all) }],
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
pi.on("before_agent_start", (event, ctx) => {
|
|
218
|
+
prompted = true;
|
|
219
|
+
sessionModel = ctx.model ?? sessionModel;
|
|
220
|
+
adoptPiSkills(event.systemPromptOptions.skills ?? [], ctx.cwd);
|
|
221
|
+
runIndex = undefined;
|
|
222
|
+
// The listing above is the one skills instruction: drop pi's own section
|
|
223
|
+
// from the prompt (a child agent's own prompt carries it; the main
|
|
224
|
+
// session's prompt is rebuilt without it).
|
|
225
|
+
if (typeof event.systemPrompt !== "string") return;
|
|
226
|
+
const stripped = withoutPiSkillsBlock(event.systemPrompt);
|
|
227
|
+
return stripped === event.systemPrompt ? undefined : { systemPrompt: stripped };
|
|
228
|
+
});
|
|
229
|
+
// The first turn a background completion opens (lib/prompt-options.ts)
|
|
230
|
+
// carries the same listing a typed first prompt would.
|
|
231
|
+
// The first announcement wins, as in system-prompt: both describe one turn.
|
|
232
|
+
let announcedSkills = false;
|
|
233
|
+
pi.events.on(PROMPT_OPTIONS_CHANNEL, (data) => {
|
|
234
|
+
const announced = data as PromptOptionsAnnouncement | undefined;
|
|
235
|
+
const skills = (announced?.options as { skills?: unknown[] } | undefined)?.skills;
|
|
236
|
+
if (prompted || announcedSkills || !announced?.cwd || !Array.isArray(skills)) return;
|
|
237
|
+
announcedSkills = true;
|
|
238
|
+
adoptPiSkills(skills, announced.cwd);
|
|
96
239
|
});
|
|
97
240
|
|
|
98
241
|
/**
|
|
@@ -102,24 +245,57 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
102
245
|
* stable for the session anyway (the listing must stay byte-stable for the
|
|
103
246
|
* cache prefix).
|
|
104
247
|
*/
|
|
105
|
-
const
|
|
248
|
+
const frontmatterCache = new Map<string, Record<string, unknown> | undefined>();
|
|
249
|
+
/** A SKILL.md's frontmatter, read once per path: the description and the argument hint come from it. */
|
|
250
|
+
const readFrontmatter = (path: string): Record<string, unknown> | undefined => {
|
|
251
|
+
if (frontmatterCache.has(path)) return frontmatterCache.get(path);
|
|
252
|
+
let frontmatter: Record<string, unknown> | undefined;
|
|
253
|
+
try {
|
|
254
|
+
frontmatter = parseFrontmatterLoosely(readFileSync(path, "utf-8")).frontmatter;
|
|
255
|
+
} catch {
|
|
256
|
+
frontmatter = undefined;
|
|
257
|
+
}
|
|
258
|
+
frontmatterCache.set(path, frontmatter);
|
|
259
|
+
return frontmatter;
|
|
260
|
+
};
|
|
106
261
|
const readDescription = (path: string): string | undefined => {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
262
|
+
const description = readFrontmatter(path)?.description;
|
|
263
|
+
return typeof description === "string" ? description : undefined;
|
|
264
|
+
};
|
|
265
|
+
const readWhenToUse = (path: string): string | undefined => {
|
|
266
|
+
const whenToUse = readFrontmatter(path)?.when_to_use;
|
|
267
|
+
return typeof whenToUse === "string" && whenToUse.trim() ? whenToUse : undefined;
|
|
111
268
|
};
|
|
112
|
-
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* A SKILL.md's `disable-model-invocation` flag, read apart from the
|
|
272
|
+
* description cache and again whenever the file changes: a flag added
|
|
273
|
+
* mid-session must stop the model loading the skill, while the listing's
|
|
274
|
+
* descriptions stay byte-stable.
|
|
275
|
+
*/
|
|
276
|
+
const invocationFlagCache = new Map<string, { mtimeMs: number; disabled: boolean }>();
|
|
277
|
+
const readModelInvocationDisabled = (path: string): boolean => {
|
|
278
|
+
let mtimeMs = -1;
|
|
113
279
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
280
|
+
mtimeMs = statSync(path).mtimeMs;
|
|
281
|
+
} catch {
|
|
282
|
+
// Unreadable: parsed (and failed) below, cached under -1.
|
|
283
|
+
}
|
|
284
|
+
const cached = invocationFlagCache.get(path);
|
|
285
|
+
if (cached && cached.mtimeMs === mtimeMs) return cached.disabled;
|
|
286
|
+
let disabled = false;
|
|
287
|
+
try {
|
|
288
|
+
disabled = frontmatterFlag(parseFrontmatterLoosely(readFileSync(path, "utf-8")).frontmatter?.["disable-model-invocation"]);
|
|
118
289
|
} catch {
|
|
119
|
-
|
|
290
|
+
// No file, no flag.
|
|
120
291
|
}
|
|
292
|
+
invocationFlagCache.set(path, { mtimeMs, disabled });
|
|
293
|
+
return disabled;
|
|
121
294
|
};
|
|
122
295
|
|
|
296
|
+
/** A SKILL.md's `argument-hint`, the prompt's placeholder after its command (lib/argument-hints.ts). */
|
|
297
|
+
const readArgumentHint = (path: string): CommandHint | undefined => frontmatterCommandHint(readFrontmatter(path));
|
|
298
|
+
|
|
123
299
|
// Per-skill availability comes from the skill-overrides store (the /skills
|
|
124
300
|
// panel cycles it, the /plugins panel manages plugin skills). The listing
|
|
125
301
|
// and the tool both re-read it, so a change applies live.
|
|
@@ -130,7 +306,11 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
130
306
|
// the /skills command/panel falls back to a disk scan (reading each
|
|
131
307
|
// SKILL.md's frontmatter for its description) — that's the gap that made a
|
|
132
308
|
// fresh session's /skills show only plugin skills.
|
|
133
|
-
|
|
309
|
+
//
|
|
310
|
+
// The order is Claude Code's: project and user skills, then plugin commands
|
|
311
|
+
// and plugin skills, then the bundled catalog.
|
|
312
|
+
/** Every listable entry; `withCommands: false` leaves out plugin commands (and their frontmatter reads). */
|
|
313
|
+
const index = (cwd = sessionCwd, withCommands = true): IndexedSkill[] => {
|
|
134
314
|
const agentDir = getAgentDir();
|
|
135
315
|
const home = os.homedir();
|
|
136
316
|
const states = readSkillStates(pluginRoot(agentDir));
|
|
@@ -145,34 +325,78 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
145
325
|
source: "project" as const,
|
|
146
326
|
scope: skill.scope,
|
|
147
327
|
state: "on" as const,
|
|
328
|
+
disableModelInvocation: readModelInvocationDisabled(skill.path),
|
|
148
329
|
}));
|
|
149
330
|
const project = base.map((skill) => ({
|
|
150
331
|
...skill,
|
|
332
|
+
whenToUse: readWhenToUse(skill.path),
|
|
151
333
|
state: skillStateFor(states, skillOverrideKey(skill.scope, skill.name)),
|
|
152
334
|
}));
|
|
153
335
|
// Plugin skills from discoverPlugins are already filtered to enabled by
|
|
154
336
|
// the same store; anything it returns is fully available (plugin skills
|
|
155
337
|
// aren't governed by skillOverrides — managed via /plugins).
|
|
156
|
-
const
|
|
338
|
+
const discovered = discoverPlugins(defaultDiscoverRoots(agentDir, cwd, home));
|
|
339
|
+
const plugin = discovered.skills.map((skill) => ({
|
|
157
340
|
name: skill.name,
|
|
158
341
|
description: readDescription(skill.path),
|
|
342
|
+
whenToUse: readWhenToUse(skill.path),
|
|
159
343
|
path: skill.path,
|
|
160
344
|
source: "plugin" as const,
|
|
161
345
|
scope: "plugin" as const,
|
|
162
346
|
state: "on" as const,
|
|
163
347
|
pluginName: skill.plugin,
|
|
348
|
+
disableModelInvocation: readModelInvocationDisabled(skill.path),
|
|
164
349
|
}));
|
|
165
|
-
|
|
350
|
+
// A plugin command is listed when it describes itself, as in Claude Code.
|
|
351
|
+
const commands = !withCommands ? [] : discovered.commands.flatMap((command) => {
|
|
352
|
+
const description = readDescription(command.path);
|
|
353
|
+
const whenToUse = readWhenToUse(command.path);
|
|
354
|
+
if (!description?.trim() && !whenToUse) return [];
|
|
355
|
+
return [
|
|
356
|
+
{
|
|
357
|
+
name: command.name,
|
|
358
|
+
description,
|
|
359
|
+
whenToUse,
|
|
360
|
+
path: command.path,
|
|
361
|
+
source: "plugin" as const,
|
|
362
|
+
scope: "plugin" as const,
|
|
363
|
+
state: "on" as const,
|
|
364
|
+
pluginName: command.plugin,
|
|
365
|
+
kind: "command" as const,
|
|
366
|
+
disableModelInvocation: readModelInvocationDisabled(command.path),
|
|
367
|
+
},
|
|
368
|
+
];
|
|
369
|
+
});
|
|
370
|
+
return [...project.filter((skill) => !isBundled(skill.path)), ...commands, ...plugin, ...project.filter((skill) => isBundled(skill.path))];
|
|
166
371
|
};
|
|
372
|
+
/** Skills proper: the /skills panel and the typed forms leave plugin commands to the plugins extension. */
|
|
373
|
+
const skillsOnly = (cwd = sessionCwd): IndexedSkill[] => index(cwd, false);
|
|
167
374
|
|
|
168
|
-
|
|
375
|
+
/**
|
|
376
|
+
* Usage as the session found it: read once, so the listing's ranking (and
|
|
377
|
+
* with it message 1 of the cached prefix) does not move when a skill is
|
|
378
|
+
* used mid-session or a day's decay crosses a threshold.
|
|
379
|
+
*/
|
|
380
|
+
let sessionUsage: { usage: ReturnType<typeof readUsage>; now: Date } | undefined;
|
|
381
|
+
|
|
382
|
+
/** The listing, within Claude Code's budget for the session model's context window. */
|
|
383
|
+
const describe = (source: IndexedSkill[] = index()) => {
|
|
384
|
+
sessionUsage ??= { usage: readUsage(pluginRoot(getAgentDir())), now: new Date() };
|
|
385
|
+
const { usage, now } = sessionUsage;
|
|
386
|
+
const skills = source.map((skill) => ({
|
|
387
|
+
...skill,
|
|
388
|
+
bundled: isBundled(skill.path),
|
|
389
|
+
usage: usageScore(usage[usageKey(skill.kind ?? "skill", skill.name)], now),
|
|
390
|
+
}));
|
|
391
|
+
return skillListingText(skills, skillListingBudget(sessionModel?.contextWindow));
|
|
392
|
+
};
|
|
169
393
|
|
|
170
394
|
pi.registerTool({
|
|
171
395
|
name: "skill",
|
|
172
396
|
label: "Skill",
|
|
173
397
|
...ccToolRenderers<{ skill?: string; args?: string }>("Skill", {
|
|
174
398
|
title: (a) => (a ? [a.skill, a.args].filter(Boolean).join(" ") : undefined),
|
|
175
|
-
// The
|
|
399
|
+
// The skill's text goes to the model; the transcript needs one line.
|
|
176
400
|
result: (_r, a, isError) => (isError ? undefined : a?.skill ? `Loaded ${a.skill}` : undefined),
|
|
177
401
|
}),
|
|
178
402
|
description:
|
|
@@ -183,12 +407,12 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
183
407
|
args: Type.Optional(Type.String({ description: "Arguments to pass through to the skill" })),
|
|
184
408
|
list: Type.Optional(Type.Boolean({ description: "List available skills instead of invoking one" })),
|
|
185
409
|
}),
|
|
186
|
-
async execute(_toolCallId, params) {
|
|
410
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
187
411
|
// `list`, or a bare call with no invoke signal, browses the catalog. A
|
|
188
412
|
// call that passed `args` but no `skill` is an invocation that forgot to
|
|
189
413
|
// name the skill — fail loudly rather than silently returning the
|
|
190
414
|
// catalog, which a weak model reads as a non-sequitur (same archetype as
|
|
191
|
-
// the subagent tool; see docs/decisions/subagents-workflows.md).
|
|
415
|
+
// the subagent tool; see working-docs/decisions/subagents-workflows.md).
|
|
192
416
|
const all = index();
|
|
193
417
|
if (params.list || (!params.skill && params.args == null)) {
|
|
194
418
|
return {
|
|
@@ -228,10 +452,11 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
228
452
|
};
|
|
229
453
|
}
|
|
230
454
|
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
455
|
+
// An off skill is refused (matching Claude Code, where invoking an off
|
|
456
|
+
// skill by name returns the skillOverrides error). A user-only skill is
|
|
457
|
+
// hidden from the model and runs only as the user's `/<name>`, which does
|
|
458
|
+
// not come through this tool; a listing frozen before the user hid it can
|
|
459
|
+
// still name it, so the tool refuses it too (./announce.ts).
|
|
235
460
|
if (found.state === "off") {
|
|
236
461
|
const where = found.source === "plugin" ? "/plugins" : "/skills";
|
|
237
462
|
return {
|
|
@@ -240,11 +465,29 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
240
465
|
isError: true,
|
|
241
466
|
};
|
|
242
467
|
}
|
|
468
|
+
if (found.state === "user-only") {
|
|
469
|
+
return {
|
|
470
|
+
content: [{ type: "text", text: `Skill "${found.name}" is set to user-only — only the user can run it, by typing /${found.name}.` }],
|
|
471
|
+
details: { skill: found.name, state: found.state } as Record<string, unknown>,
|
|
472
|
+
isError: true,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
243
475
|
|
|
244
|
-
|
|
476
|
+
// Only the user may start a skill marked `disable-model-invocation`
|
|
477
|
+
// (a typed `/<name>` runs through deliverSkill, not this tool).
|
|
478
|
+
if (found.disableModelInvocation || readModelInvocationDisabled(found.path)) {
|
|
479
|
+
return {
|
|
480
|
+
content: [{ type: "text", text: `Skill ${wanted} cannot be used with Skill tool due to disable-model-invocation` }],
|
|
481
|
+
details: { skill: found.name } as Record<string, unknown>,
|
|
482
|
+
isError: true,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
let promptText: string;
|
|
245
487
|
try {
|
|
246
488
|
const parsed = parseFrontmatterLoosely(readFileSync(found.path, "utf-8")) as { body: string };
|
|
247
|
-
body = parsed.body.trim();
|
|
489
|
+
const { body, args } = skillBody(found, parsed.body.trim(), params.args?.trim() ?? "", ctx.cwd);
|
|
490
|
+
promptText = skillPromptText(body, args, baseDirFor(found));
|
|
248
491
|
} catch (error) {
|
|
249
492
|
return {
|
|
250
493
|
content: [{ type: "text", text: `Could not read skill "${found.name}": ${(error as Error).message}` }],
|
|
@@ -253,76 +496,130 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
253
496
|
};
|
|
254
497
|
}
|
|
255
498
|
|
|
256
|
-
recordUsage(pluginRoot(getAgentDir()), "skill", found.name);
|
|
499
|
+
recordUsage(pluginRoot(getAgentDir()), found.kind ?? "skill", found.name);
|
|
257
500
|
|
|
258
|
-
//
|
|
259
|
-
//
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"Follow these instructions for the current task.",
|
|
265
|
-
]
|
|
266
|
-
.filter(Boolean)
|
|
267
|
-
.join("\n");
|
|
501
|
+
// A skill that takes arguments, called without any, returns its
|
|
502
|
+
// no-argument body; say so, since a model previewing the skill would
|
|
503
|
+
// otherwise follow it (invoke.ts missingArgumentsNote).
|
|
504
|
+
const hint = params.args?.trim() ? undefined : readArgumentHint(found.path);
|
|
505
|
+
const hintText = hint?.hint ?? hint?.argNames?.map((name) => `<${name}>`).join(" ");
|
|
506
|
+
const note = hintText ? missingArgumentsNote(hintText) : undefined;
|
|
268
507
|
|
|
508
|
+
// Claude Code's shape: the result says the skill is launching, and its
|
|
509
|
+
// text follows as a block of its own.
|
|
269
510
|
return {
|
|
270
|
-
content: [
|
|
511
|
+
content: [
|
|
512
|
+
{ type: "text", text: launchingSkill(found.name) },
|
|
513
|
+
{ type: "text", text: note ? `${note.before}\n\n${promptText}\n\n${note.after}` : promptText },
|
|
514
|
+
],
|
|
271
515
|
details: { skill: found.name, path: found.path } as Record<string, unknown>,
|
|
272
516
|
};
|
|
273
517
|
},
|
|
274
518
|
});
|
|
275
519
|
|
|
276
520
|
/**
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
280
|
-
* and the command would exit silently. `console.error` writes to stderr, so it
|
|
281
|
-
* never corrupts the `--mode json` event stream on stdout.
|
|
521
|
+
* A skill's body and the arguments still to append: a generated body
|
|
522
|
+
* (lib/skill-body.ts, `/loop`) carries its arguments, a file's does not.
|
|
523
|
+
* The bundled code-review picks its body by tier and effort (code-review.ts).
|
|
282
524
|
*/
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
525
|
+
const skillBody = (found: IndexedSkill, fileBody: string, args: string, cwd: string): { body: string; args: string } => {
|
|
526
|
+
if (found.kind === "command") return { body: fileBody, args };
|
|
527
|
+
if (resolve(found.path) === CODE_REVIEW_PATH) {
|
|
528
|
+
try {
|
|
529
|
+
const review = codeReviewBody(CODE_REVIEW_CELLS, requestTier(), pi.getThinkingLevel(), args);
|
|
530
|
+
return { body: review.body, args: review.args };
|
|
531
|
+
} catch {
|
|
532
|
+
// A cell file missing from an install: the SKILL.md body (the medium cell) still reviews.
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
const query: SkillBodyQuery = { skill: found.name, path: found.path, args, cwd };
|
|
536
|
+
pi.events.emit(SKILL_BODY_CHANNEL, query);
|
|
537
|
+
return query.body !== undefined ? { body: query.body, args: "" } : { body: fileBody, args };
|
|
290
538
|
};
|
|
539
|
+
/** The folder Claude Code names before a skill's text; none for One Code's own catalog. */
|
|
540
|
+
const baseDirFor = (found: IndexedSkill): string | undefined => (found.kind === "command" || isBundled(found.path) ? undefined : dirname(found.path));
|
|
541
|
+
|
|
542
|
+
// A fired scheduled prompt that is a skill's slash command runs that skill,
|
|
543
|
+
// as Claude Code's queue runs a fired `/babysit-prs` (lib/skill-body.ts).
|
|
544
|
+
pi.events.on(SLASH_EXPAND_CHANNEL, (data) => {
|
|
545
|
+
const query = data as SlashExpandQuery;
|
|
546
|
+
const command = parseSlashCommand(query.text);
|
|
547
|
+
if (!command) return;
|
|
548
|
+
const found = resolveSkill(skillsOnly(query.cwd), command.name.replace(/^skill:/, ""));
|
|
549
|
+
if (!found || found.state === "off") return;
|
|
550
|
+
let fileBody: string;
|
|
551
|
+
try {
|
|
552
|
+
fileBody = parseFrontmatterLoosely(readFileSync(found.path, "utf-8")).body.trim();
|
|
553
|
+
} catch {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
recordUsage(pluginRoot(getAgentDir()), "skill", found.name);
|
|
557
|
+
const { body, args } = skillBody(found, fileBody, command.args, query.cwd);
|
|
558
|
+
query.expanded = typedSkillContent(found.name, command.args, skillPromptText(body, args, baseDirFor(found))).join("");
|
|
559
|
+
});
|
|
560
|
+
|
|
561
|
+
/** The `input` hook's answer that replaces the typed prompt with the skill's message. */
|
|
562
|
+
type SkillTransform = { action: "transform"; text: string; images?: Array<{ type: "image"; data: string; mimeType: string }> };
|
|
291
563
|
|
|
292
564
|
/**
|
|
293
565
|
* Run a resolved skill the way a user-typed command does: refuse an "off"
|
|
294
566
|
* skill (in EVERY mode — falling through would hand `/skill:` to pi's native
|
|
295
567
|
* expansion, which knows nothing of the overrides store and would run it; a
|
|
296
568
|
* one-shot run gets the refusal on stderr instead of a UI notice, never as
|
|
297
|
-
* silent execution), else
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
* regardless of `display`), but nothing renders
|
|
301
|
-
*
|
|
569
|
+
* silent execution), else deliver Claude Code's typed-skill message (the
|
|
570
|
+
* command breadcrumb and the skill's text) as a hidden custom message: the
|
|
571
|
+
* model receives it as a user message (convertToLlm maps a custom message
|
|
572
|
+
* to one regardless of `display`), but nothing renders and no `skill` call
|
|
573
|
+
* is needed. Shared by the `/skill:<name>` interception, the typed
|
|
574
|
+
* `/<plugin>:<skill>` form and the bare `/<name>` commands.
|
|
302
575
|
*/
|
|
303
576
|
const deliverSkill = async (
|
|
304
577
|
found: IndexedSkill,
|
|
305
578
|
args: string,
|
|
306
579
|
ctx: ExtensionContext & { waitForIdle?: () => Promise<void> },
|
|
307
|
-
extra: {
|
|
308
|
-
|
|
580
|
+
extra: {
|
|
581
|
+
images?: Array<{ type: "image"; data: string; mimeType: string }>;
|
|
582
|
+
streamingBehavior?: "steer" | "followUp";
|
|
583
|
+
input?: string;
|
|
584
|
+
/** Called from the `input` hook, which can hand the message back as the prompt's own text. */
|
|
585
|
+
fromInput?: boolean;
|
|
586
|
+
} = {},
|
|
587
|
+
): Promise<"handled" | "unavailable" | SkillTransform> => {
|
|
309
588
|
if (found.state === "off") {
|
|
310
589
|
const where = found.source === "plugin" ? "/plugins" : "/skills";
|
|
311
590
|
notifyOrPrint(ctx, `Skill "${found.name}" is turned off — enable it from ${where} to run it.`, "warning");
|
|
312
591
|
return "handled";
|
|
313
592
|
}
|
|
314
|
-
let
|
|
593
|
+
let fileBody: string;
|
|
315
594
|
try {
|
|
316
|
-
|
|
595
|
+
fileBody = parseFrontmatterLoosely(readFileSync(found.path, "utf-8")).body.trim();
|
|
317
596
|
} catch {
|
|
318
597
|
return "unavailable";
|
|
319
598
|
}
|
|
320
599
|
recordUsage(pluginRoot(getAgentDir()), "skill", found.name);
|
|
321
|
-
const
|
|
322
|
-
//
|
|
323
|
-
|
|
600
|
+
const { body, args: bodyArgs } = skillBody(found, fileBody, args, ctx.cwd);
|
|
601
|
+
// Claude Code's typed-skill message: the command breadcrumb, then the
|
|
602
|
+
// skill's text; any attached images ride after them.
|
|
603
|
+
const [breadcrumb, text] = typedSkillContent(found.name, args, skillPromptText(body, bodyArgs, baseDirFor(found)));
|
|
604
|
+
const content = [{ type: "text" as const, text: breadcrumb }, { type: "text" as const, text }, ...(extra.images ?? [])];
|
|
605
|
+
if (!prompted && !extra.streamingBehavior) {
|
|
606
|
+
// From the input hook the message becomes the typed prompt's own text
|
|
607
|
+
// (the same bytes sendUserMessage would send: its text parts joined by a
|
|
608
|
+
// newline). sendUserMessage starts a second prompt that is still in its
|
|
609
|
+
// async preflight when this handler returns, so the session looks idle,
|
|
610
|
+
// and a one-shot run disposed the session before that turn ran.
|
|
611
|
+
if (extra.fromInput) return { action: "transform", text: `${breadcrumb}\n${text}`, images: extra.images };
|
|
612
|
+
pi.sendUserMessage(content, { deliverAs: "followUp" });
|
|
613
|
+
await awaitOneShotTurn(ctx);
|
|
614
|
+
return "handled";
|
|
615
|
+
}
|
|
324
616
|
pi.sendMessage(
|
|
325
|
-
{
|
|
617
|
+
{
|
|
618
|
+
customType: SKILL_INVOCATION_TYPE,
|
|
619
|
+
content,
|
|
620
|
+
display: false,
|
|
621
|
+
details: { skill: found.name, args, ...(extra.input !== undefined ? { input: extra.input } : {}) } satisfies SkillInvocationDetails,
|
|
622
|
+
},
|
|
326
623
|
{ triggerTurn: true, ...(extra.streamingBehavior ? { deliverAs: extra.streamingBehavior } : {}) },
|
|
327
624
|
);
|
|
328
625
|
// The turn we just triggered rides a fire-and-forget pi.sendMessage. In a
|
|
@@ -339,21 +636,52 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
339
636
|
// the transcript as a new user turn. Intercept it here, suppress pi's
|
|
340
637
|
// expansion (return "handled"), and deliver through deliverSkill. An unknown
|
|
341
638
|
// name, an ambiguous plugin match, or an unreadable file falls through to
|
|
342
|
-
// pi's native handling.
|
|
343
|
-
//
|
|
344
|
-
//
|
|
639
|
+
// pi's native handling. Since pi 0.86.0 (faa9863cb) this also runs for a
|
|
640
|
+
// message queued mid-turn and for RPC steer/followUp (`streamingBehavior` is
|
|
641
|
+
// set), so an off skill is refused before it enters the queue. On older pi
|
|
642
|
+
// those paths skip `input`; the context-hook redaction below covers them.
|
|
643
|
+
//
|
|
644
|
+
// A typed `/<plugin>:<skill>` has no command of its own (plugin skills get
|
|
645
|
+
// no bare alias), so it reached the model as text and the model called
|
|
646
|
+
// `skill`; it is expanded here too, as Claude Code expands it. Only an exact
|
|
647
|
+
// plugin-skill name is taken; anything else stays the user's text.
|
|
648
|
+
//
|
|
649
|
+
// In a one-shot run the bare `/<name>` forms are not commands (see
|
|
650
|
+
// registerSkillCommands) and are taken here too, so the first skill becomes
|
|
651
|
+
// the prompt's own text and the run waits for its turn.
|
|
345
652
|
pi.on("input", async (event, ctx) => {
|
|
346
|
-
const
|
|
653
|
+
const typed = parseSkillCommand(event.text) ?? oneShotBareSkill(event.text);
|
|
654
|
+
const plugin = typed ? undefined : typedPluginSkill(event.text);
|
|
655
|
+
const cmd = typed ?? plugin?.command;
|
|
347
656
|
if (!cmd) return { action: "continue" };
|
|
348
|
-
const found = resolveSkill(
|
|
657
|
+
const found = plugin?.skill ?? resolveSkill(skillsOnly(ctx.cwd), cmd.name);
|
|
349
658
|
if (!found) return { action: "continue" };
|
|
350
659
|
const outcome = await deliverSkill(found, cmd.args, ctx, {
|
|
351
660
|
images: event.images as Array<{ type: "image"; data: string; mimeType: string }> | undefined,
|
|
352
661
|
streamingBehavior: event.streamingBehavior,
|
|
662
|
+
input: event.text,
|
|
663
|
+
fromInput: true,
|
|
353
664
|
});
|
|
665
|
+
if (typeof outcome === "object") return outcome;
|
|
354
666
|
return { action: outcome === "handled" ? "handled" : "continue" };
|
|
355
667
|
});
|
|
356
668
|
|
|
669
|
+
/** A typed bare `/<name> [args]` naming a skill whose command a one-shot run left unregistered. */
|
|
670
|
+
const oneShotBareSkill = (text: string): { name: string; args: string } | undefined => {
|
|
671
|
+
if (!oneShot || !text.startsWith("/")) return undefined;
|
|
672
|
+
const command = parseSlashCommand(text);
|
|
673
|
+
return command && oneShotSkillNames.has(command.name) ? command : undefined;
|
|
674
|
+
};
|
|
675
|
+
|
|
676
|
+
/** A typed `/<plugin>:<skill> [args]` naming a plugin skill exactly, and that skill. */
|
|
677
|
+
const typedPluginSkill = (text: string): { command: { name: string; args: string }; skill: IndexedSkill } | undefined => {
|
|
678
|
+
if (!text.startsWith("/")) return undefined;
|
|
679
|
+
const command = parseSlashCommand(text);
|
|
680
|
+
if (!command?.name.includes(":")) return undefined;
|
|
681
|
+
const skill = skillsOnly().find((candidate) => candidate.source === "plugin" && candidate.name === command.name);
|
|
682
|
+
return skill ? { command, skill } : undefined;
|
|
683
|
+
};
|
|
684
|
+
|
|
357
685
|
// Claude Code invokes a user/project skill as a bare `/<name>` (only plugin
|
|
358
686
|
// skills carry a `<plugin>:` prefix); pi registers `/skill:<name>`. Register
|
|
359
687
|
// each project/user/bundled skill as a bare extension command too, so
|
|
@@ -370,7 +698,7 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
370
698
|
// be unregistered, so the handler resolves the skill afresh at invocation:
|
|
371
699
|
// a skill deleted or turned off since registration is refused, never run.
|
|
372
700
|
const registeredSkillCommands = new Set<string>();
|
|
373
|
-
const registerSkillCommands = (cwd: string | undefined) => {
|
|
701
|
+
const registerSkillCommands = (cwd: string | undefined, skills: IndexedSkill[] = skillsOnly(cwd)) => {
|
|
374
702
|
let taken: string[];
|
|
375
703
|
try {
|
|
376
704
|
taken = pi.getCommands().map((command) => command.name);
|
|
@@ -378,12 +706,18 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
378
706
|
return; // not bound yet (load time) — session_start retries
|
|
379
707
|
}
|
|
380
708
|
const templates = promptTemplateNames(cwd ?? process.cwd(), os.homedir(), getAgentDir());
|
|
381
|
-
for (const skill of skillCommandCandidates(
|
|
709
|
+
for (const skill of skillCommandCandidates(skills, [...taken, ...templates, ...registeredSkillCommands, ...oneShotSkillNames])) {
|
|
710
|
+
if (oneShot) {
|
|
711
|
+
oneShotSkillNames.add(skill.name);
|
|
712
|
+
continue;
|
|
713
|
+
}
|
|
382
714
|
registeredSkillCommands.add(skill.name);
|
|
715
|
+
const hint = readArgumentHint(skill.path);
|
|
716
|
+
if (hint) announceArgumentHint(pi, skill.name, hint);
|
|
383
717
|
pi.registerCommand(skill.name, {
|
|
384
718
|
description: skill.description ? `${skill.description} (skill)` : `Run the ${skill.name} skill`,
|
|
385
719
|
handler: async (args, ctx) => {
|
|
386
|
-
const found = resolveSkill(
|
|
720
|
+
const found = resolveSkill(skillsOnly(ctx.cwd), skill.name);
|
|
387
721
|
if (!found || (await deliverSkill(found, args.trim(), ctx)) === "unavailable") {
|
|
388
722
|
notifyOrPrint(ctx, `Skill "${skill.name}" is no longer available (its SKILL.md was removed or is unreadable).`, "error");
|
|
389
723
|
}
|
|
@@ -391,40 +725,45 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
391
725
|
});
|
|
392
726
|
}
|
|
393
727
|
};
|
|
394
|
-
pi.on("session_start", (_event, ctx) =>
|
|
728
|
+
pi.on("session_start", (_event, ctx) => {
|
|
729
|
+
const skills = skillsOnly(ctx.cwd);
|
|
730
|
+
registerSkillCommands(ctx.cwd, skills);
|
|
731
|
+
// pi lists every skill a second time as `/skill:<name>`; drop that entry
|
|
732
|
+
// where the bare command exists (withoutDuplicateSkillCommands).
|
|
733
|
+
if (ctx.hasUI && ctx.mode === "tui") {
|
|
734
|
+
ctx.ui.addAutocompleteProvider((current) => ({
|
|
735
|
+
...current,
|
|
736
|
+
getSuggestions: async (lines, cursorLine, cursorCol, options) => {
|
|
737
|
+
const suggestions = await current.getSuggestions(lines, cursorLine, cursorCol, options);
|
|
738
|
+
if (!suggestions) return suggestions;
|
|
739
|
+
const items = withoutDuplicateSkillCommands(suggestions.items, suggestions.prefix, registeredSkillCommands);
|
|
740
|
+
return items.length > 0 ? { ...suggestions, items } : null;
|
|
741
|
+
},
|
|
742
|
+
applyCompletion: (...args) => current.applyCompletion(...args),
|
|
743
|
+
...(current.shouldTriggerFileCompletion ? { shouldTriggerFileCompletion: (...args) => current.shouldTriggerFileCompletion!(...args) } : {}),
|
|
744
|
+
}));
|
|
745
|
+
}
|
|
746
|
+
// pi's own `/skill:<name>` form, which pi lists only after the first turn.
|
|
747
|
+
for (const skill of skills) {
|
|
748
|
+
const hint = skill.source === "plugin" ? undefined : readArgumentHint(skill.path);
|
|
749
|
+
if (hint) announceArgumentHint(pi, `skill:${skill.name}`, hint);
|
|
750
|
+
}
|
|
751
|
+
});
|
|
395
752
|
// pi's per-turn resolution (frontmatter `name`, description required) can
|
|
396
753
|
// surface skills the pre-turn disk scan missed; late registrations still
|
|
397
754
|
// execute (autocomplete lists them after a /reload). Gated on an unseen
|
|
398
755
|
// name so the steady state costs one Set lookup per skill per turn, not a
|
|
399
756
|
// re-index (before_agent_start runs every turn).
|
|
400
757
|
pi.on("before_agent_start", (_event, ctx) => {
|
|
401
|
-
if (piSkills.some((skill) => !registeredSkillCommands.has(skill.name))) registerSkillCommands(ctx.cwd);
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
// Fail-closed backstop for the `input` interception above: pi's steer() and
|
|
405
|
-
// followUp() expand `/skill:<name>` natively WITHOUT firing an `input` event
|
|
406
|
-
// (queued interactive messages after the first, RPC steer/followUp), so a
|
|
407
|
-
// turned-off skill's instructions can land in the session history anyway.
|
|
408
|
-
// Strip them from every outgoing request instead — the wire copy carries the
|
|
409
|
-
// refusal notice, the session file keeps the original bytes, and untouched
|
|
410
|
-
// requests return undefined so the message array stays byte-identical
|
|
411
|
-
// (prompt-cache stable). Override states are read lazily, only when a
|
|
412
|
-
// message actually contains a `<skill>` block.
|
|
413
|
-
pi.on("context", (event) => {
|
|
414
|
-
let states: Map<string, string> | undefined;
|
|
415
|
-
const isOff = (name: string) => {
|
|
416
|
-
states ??= new Map(index().map((skill) => [skill.name, skill.state]));
|
|
417
|
-
return states.get(name) === "off";
|
|
418
|
-
};
|
|
419
|
-
const redacted = redactOffSkillMessages(event.messages, isOff);
|
|
420
|
-
return redacted ? { messages: redacted } : undefined;
|
|
758
|
+
if (piSkills.some((skill) => !registeredSkillCommands.has(skill.name) && !oneShotSkillNames.has(skill.name))) registerSkillCommands(ctx.cwd);
|
|
421
759
|
});
|
|
422
760
|
|
|
423
761
|
const buildSkillsRows = (cwd: string | undefined): SkillsRow[] =>
|
|
424
|
-
|
|
762
|
+
skillsOnly(cwd).map((skill) => ({
|
|
425
763
|
key: skillOverrideKey(skill.scope, skill.name),
|
|
426
764
|
name: skill.name,
|
|
427
|
-
scope
|
|
765
|
+
// The override key keeps the path's scope; the row names the catalog.
|
|
766
|
+
scope: isBundled(skill.path) ? ("bundled" as const) : skill.scope,
|
|
428
767
|
tokens: estimateSkillTokens(skill.path),
|
|
429
768
|
state: skill.state,
|
|
430
769
|
locked: skill.source === "plugin",
|
|
@@ -436,7 +775,7 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
436
775
|
// the skill-overrides store live. Plugin skills show locked (managed via
|
|
437
776
|
// /plugins). Pure state/render live in ./panel; this owns repaint + writes.
|
|
438
777
|
const openSkillsPanel = async (ctx: ExtensionContext): Promise<void> => {
|
|
439
|
-
await ctx.ui.custom<null>((tui, theme, _keybindings, done) => {
|
|
778
|
+
await panelUi(pi.events, ctx.ui).custom<null>((tui, theme, _keybindings, done) => {
|
|
440
779
|
const paint: SkillsPaint = { fg: safeThemePaint(theme), bold: safeThemeBold(theme) };
|
|
441
780
|
const state = initialSkillsState();
|
|
442
781
|
let rows = buildSkillsRows(ctx.cwd);
|
|
@@ -462,7 +801,6 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
462
801
|
if (!key) return;
|
|
463
802
|
const before = visibleRows(rows, state);
|
|
464
803
|
const anchorKey = before[state.cursor]?.key;
|
|
465
|
-
const sortBefore = state.sort;
|
|
466
804
|
const effect = applySkillsKey(state, key, before);
|
|
467
805
|
notice = undefined;
|
|
468
806
|
if (effect?.kind === "close") {
|
|
@@ -478,13 +816,8 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
478
816
|
} else if (effect?.kind === "locked") {
|
|
479
817
|
notice = `${effect.row.name} is a plugin skill — manage it from /plugins.`;
|
|
480
818
|
}
|
|
481
|
-
// A sort toggle
|
|
482
|
-
|
|
483
|
-
// that slid into its index.
|
|
484
|
-
if (state.sort !== sortBefore && anchorKey) {
|
|
485
|
-
const idx = visibleRows(rows, state).findIndex((row) => row.key === anchorKey);
|
|
486
|
-
if (idx >= 0) state.cursor = idx;
|
|
487
|
-
}
|
|
819
|
+
// A sort toggle or a cycle under the state sort reorders the list.
|
|
820
|
+
if ((key.kind === "text" || effect?.kind === "cycle") && anchorKey) followRow(state, visibleRows(rows, state), anchorKey);
|
|
488
821
|
repaint();
|
|
489
822
|
},
|
|
490
823
|
invalidate: () => {
|
|
@@ -495,17 +828,20 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
495
828
|
};
|
|
496
829
|
|
|
497
830
|
registerLocalCommand(pi, "skills", {
|
|
831
|
+
reportsResult: true,
|
|
498
832
|
description: "View and manage skills (on / name-only / user-only / off)",
|
|
499
833
|
handler: async (args, ctx) => {
|
|
500
|
-
if (ctx
|
|
834
|
+
if (canShowCustomUi(ctx)) {
|
|
501
835
|
await openSkillsPanel(ctx);
|
|
502
|
-
return;
|
|
836
|
+
return "Skills dialog dismissed";
|
|
503
837
|
}
|
|
838
|
+
const readOnly = notifyRpcReadOnly(ctx, "/skills", "change skill states");
|
|
839
|
+
const withNotice = (text: string) => (readOnly ? `${readOnly}\n${text}` : text);
|
|
504
840
|
// Non-interactive fallback: a flat listing with each skill's state.
|
|
505
|
-
const all =
|
|
841
|
+
const all = skillsOnly(ctx.cwd);
|
|
506
842
|
if (all.length === 0) {
|
|
507
843
|
ctx.ui.notify("No skills available.", "info");
|
|
508
|
-
return;
|
|
844
|
+
return withNotice("No skills available.");
|
|
509
845
|
}
|
|
510
846
|
const lines = all.map((skill) => {
|
|
511
847
|
const label = skill.source === "plugin" ? "plugin" : skill.state;
|
|
@@ -513,6 +849,7 @@ export default function skillExtension(pi: ExtensionAPI) {
|
|
|
513
849
|
return `- ${skill.name} [${label}]${desc}`;
|
|
514
850
|
});
|
|
515
851
|
ctx.ui.notify(`Skills:\n${lines.join("\n")}`, "info");
|
|
852
|
+
return withNotice(`Skills:\n${lines.join("\n")}`);
|
|
516
853
|
},
|
|
517
854
|
});
|
|
518
855
|
}
|