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
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
* Claude Code hook protocol (pure): the stdin payload shape, the stdout JSON
|
|
3
3
|
* envelope, and the mapping from a finished hook run to what One Code should do
|
|
4
4
|
* about it. Envelope semantics follow Claude Code's hooks reference; the
|
|
5
|
-
* fail-closed choices are One Code's own (see docs/decisions.md).
|
|
5
|
+
* fail-closed choices are One Code's own (see working-docs/decisions.md).
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { isAppendedReminderText } from "../lib/reminders.ts";
|
|
9
|
+
import { emptyToolResultText } from "../lib/empty-tool-result.ts";
|
|
10
|
+
import type { SessionBackgroundTask, SessionCron } from "../lib/session-work.ts";
|
|
9
11
|
|
|
10
12
|
/** The contiguous run of reminder/countdown blocks at the tail of a tool result (review M3). */
|
|
11
13
|
function trailingReminderBlocks<T extends { type: string }>(content: readonly T[]): T[] {
|
|
@@ -51,6 +53,10 @@ export interface HookStdinPayload {
|
|
|
51
53
|
tool_response?: { content: unknown; is_error: boolean };
|
|
52
54
|
prompt?: string;
|
|
53
55
|
stop_hook_active?: boolean;
|
|
56
|
+
/** Stop: in-flight background work (Claude Code's `background_tasks`). */
|
|
57
|
+
background_tasks?: SessionBackgroundTask[];
|
|
58
|
+
/** Stop: jobs that will wake the session later (Claude Code's `session_crons`). */
|
|
59
|
+
session_crons?: SessionCron[];
|
|
54
60
|
trigger?: "manual" | "auto";
|
|
55
61
|
/** PreCompact: the `/compact <instructions>` text, empty when none (Claude Code sends the string as is). */
|
|
56
62
|
custom_instructions?: string;
|
|
@@ -97,6 +103,8 @@ export function parseEnvelope(stdout: string): HookEnvelope | undefined {
|
|
|
97
103
|
/** What one finished hook run asks One Code to do. */
|
|
98
104
|
export interface HookOutcome {
|
|
99
105
|
block?: { reason: string };
|
|
106
|
+
/** Stop continue:false ends the turn rather than requesting another response. */
|
|
107
|
+
preventContinuation?: boolean;
|
|
100
108
|
updatedInput?: Record<string, unknown>;
|
|
101
109
|
updatedToolResult?: unknown;
|
|
102
110
|
additionalContext?: string;
|
|
@@ -104,8 +112,24 @@ export interface HookOutcome {
|
|
|
104
112
|
}
|
|
105
113
|
|
|
106
114
|
export interface FinishedRun {
|
|
115
|
+
/**
|
|
116
|
+
* null when the process was killed (timeout) or never spawned. Normalized on
|
|
117
|
+
* the timeout path rather than taken from `waitpid`: a group SIGKILL is not
|
|
118
|
+
* atomic, so the shell can be scheduled after its foreground child is killed
|
|
119
|
+
* and before its own signal lands, reap the child and exit(128+9) itself —
|
|
120
|
+
* `close` then reports a normal exit of 137 instead of death by signal
|
|
121
|
+
* (roughly 1% of timeouts under load, and never when the shell had exec'd
|
|
122
|
+
* away, leaving no shell to reap; findings §10.20). Callers should still
|
|
123
|
+
* prefer `timedOut`, which says what happened rather than what it looked
|
|
124
|
+
* like, but they no longer have to.
|
|
125
|
+
*/
|
|
107
126
|
exitCode: number | null;
|
|
108
127
|
timedOut: boolean;
|
|
128
|
+
/** The calling turn or child session was cancelled. */
|
|
129
|
+
aborted?: boolean;
|
|
130
|
+
/** Output is incomplete and must not be interpreted as a hook response. */
|
|
131
|
+
outputLimitExceeded?: boolean;
|
|
132
|
+
/** Set when the child could not be spawned at all. */
|
|
109
133
|
spawnError?: string;
|
|
110
134
|
stdout: string;
|
|
111
135
|
stderr: string;
|
|
@@ -117,9 +141,19 @@ const TIMEOUT_BLOCKS: ReadonlySet<CcHookEvent> = new Set(["PreToolUse", "UserPro
|
|
|
117
141
|
* Events where CC's plain (non-JSON) stdout becomes model-visible context
|
|
118
142
|
* rather than being discarded.
|
|
119
143
|
*/
|
|
120
|
-
const STDOUT_IS_CONTEXT: ReadonlySet<CcHookEvent> = new Set(["UserPromptSubmit", "SessionStart"]);
|
|
144
|
+
const STDOUT_IS_CONTEXT: ReadonlySet<CcHookEvent> = new Set(["UserPromptSubmit", "SessionStart", "PreCompact"]);
|
|
145
|
+
|
|
146
|
+
/** A cancelled run: the gating events block, the rest pass. */
|
|
147
|
+
export function cancelledOutcome(event: CcHookEvent): HookOutcome {
|
|
148
|
+
return TIMEOUT_BLOCKS.has(event) ? { block: { reason: "Hook cancelled" } } : {};
|
|
149
|
+
}
|
|
121
150
|
|
|
122
151
|
export function interpretHookResult(event: CcHookEvent, run: FinishedRun): HookOutcome {
|
|
152
|
+
if (run.aborted) return cancelledOutcome(event);
|
|
153
|
+
if (run.outputLimitExceeded) {
|
|
154
|
+
const reason = "Hook exceeded its output limit; reduce stdout/stderr to at most 1,000,000 bytes per stream.";
|
|
155
|
+
return TIMEOUT_BLOCKS.has(event) ? { block: { reason } } : { systemMessage: reason };
|
|
156
|
+
}
|
|
123
157
|
// Killed (timeout or otherwise): a null exit code must never read as a
|
|
124
158
|
// clean allow. Closed for the two events that gate something; open for the
|
|
125
159
|
// rest — the side effect already happened, or blocking would wedge exit.
|
|
@@ -146,6 +180,7 @@ export function interpretHookResult(event: CcHookEvent, run: FinishedRun): HookO
|
|
|
146
180
|
|
|
147
181
|
if (envelope.continue === false) {
|
|
148
182
|
outcome.block = { reason: envelope.stopReason?.trim() || "Hook stopped continuation" };
|
|
183
|
+
if (event === "Stop") outcome.preventContinuation = true;
|
|
149
184
|
}
|
|
150
185
|
const decision = specific?.permissionDecision;
|
|
151
186
|
if (decision === "deny" || decision === "ask") {
|
|
@@ -191,6 +226,7 @@ interface TextBlock {
|
|
|
191
226
|
export function applyPostToolUseOutcome<T extends { type: string }>(
|
|
192
227
|
content: readonly T[],
|
|
193
228
|
outcome: Pick<HookOutcome, "block" | "updatedToolResult" | "additionalContext">,
|
|
229
|
+
toolName: string,
|
|
194
230
|
): Array<T | TextBlock> | undefined {
|
|
195
231
|
if (!outcome.block && outcome.updatedToolResult === undefined && !outcome.additionalContext) return undefined;
|
|
196
232
|
let result: Array<T | TextBlock> = [...content];
|
|
@@ -201,7 +237,9 @@ export function applyPostToolUseOutcome<T extends { type: string }>(
|
|
|
201
237
|
// tool_result hook before ours). Replacing the content must not drop
|
|
202
238
|
// them, so keep the trailing run of reminder blocks (review M3).
|
|
203
239
|
const reminderTail = trailingReminderBlocks(content);
|
|
204
|
-
|
|
240
|
+
const text = typeof replacement === "string" ? replacement : JSON.stringify(replacement);
|
|
241
|
+
// The hook's own text: an empty one gets the marker for every tool, read included.
|
|
242
|
+
result = [{ type: "text", text: text.trim() ? text : emptyToolResultText(toolName) }, ...reminderTail];
|
|
205
243
|
}
|
|
206
244
|
if (outcome.block) result = [{ type: "text", text: `PostToolUse hook: ${outcome.block.reason}` }, ...result];
|
|
207
245
|
if (outcome.additionalContext) result = [...result, { type: "text", text: outcome.additionalContext }];
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Hook config discovery and merge (pure fs reads, no pi imports).
|
|
3
3
|
*
|
|
4
|
-
* Sources,
|
|
4
|
+
* Sources, in collection order: user ~/.claude/settings.json, managed
|
|
5
5
|
* settings (Claude Code's platform paths), project .claude/settings.json,
|
|
6
|
-
* project .claude/settings.local.json
|
|
6
|
+
* project .claude/settings.local.json (independent mode: One Code's user and
|
|
7
|
+
* per-repo settings, `hookSettingsPaths`). Project and local sources are
|
|
7
8
|
* *returned flagged, not filtered* — whether they run is a trust decision
|
|
8
9
|
* (hooks are arbitrary code execution) that index.ts applies via trust.ts;
|
|
9
10
|
* this module stays pure.
|
|
@@ -14,7 +15,10 @@
|
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
17
|
import { readFileSync, statSync } from "node:fs";
|
|
18
|
+
import os from "node:os";
|
|
17
19
|
import { join } from "node:path";
|
|
20
|
+
import { managedSettingsPaths, settingsSources } from "../lib/claude-settings.ts";
|
|
21
|
+
import { claudeSourcesOn } from "../lib/config-mode.ts";
|
|
18
22
|
import { CC_HOOK_EVENTS, type CcHookEvent } from "./protocol.ts";
|
|
19
23
|
|
|
20
24
|
/** Claude Code's per-hook `shell` field: which interpreter runs the command string. */
|
|
@@ -50,21 +54,23 @@ export interface HooksSource {
|
|
|
50
54
|
|
|
51
55
|
export interface LoadedHooks {
|
|
52
56
|
sources: HooksSource[];
|
|
57
|
+
/** Configured sources are filtered; collectors must also suppress plugin hooks when set. */
|
|
58
|
+
disabled?: "all" | "unmanaged";
|
|
53
59
|
/** Malformed entries are skipped and reported, never fatal. */
|
|
54
60
|
diagnostics: string[];
|
|
55
61
|
}
|
|
56
62
|
|
|
57
|
-
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
63
|
+
/**
|
|
64
|
+
* The settings files hooks are read from, in collection order. Independent
|
|
65
|
+
* mode (lib/config-mode.ts) reads One Code's user file and its per-repo file
|
|
66
|
+
* under `~/.onecode/projects/<slug>/` instead; neither ships in a repository,
|
|
67
|
+
* so both are user scope and need no project-trust prompt.
|
|
68
|
+
*/
|
|
69
|
+
export function hookSettingsPaths(claudeDir: string, cwd: string, home: string = os.homedir()): Array<{ scope: HookScope; path: string }> {
|
|
70
|
+
if (!claudeSourcesOn()) return settingsSources(cwd, home).map(([, path]) => ({ scope: "user" as const, path }));
|
|
65
71
|
return [
|
|
66
72
|
{ scope: "user", path: join(claudeDir, "settings.json") },
|
|
67
|
-
{ scope: "managed", path
|
|
73
|
+
...managedSettingsPaths().map((path) => ({ scope: "managed" as const, path })),
|
|
68
74
|
{ scope: "project", path: join(cwd, ".claude", "settings.json") },
|
|
69
75
|
{ scope: "local", path: join(cwd, ".claude", "settings.local.json") },
|
|
70
76
|
];
|
|
@@ -130,13 +136,14 @@ export function parseHooksBlock(raw: unknown, origin: string, diagnostics: strin
|
|
|
130
136
|
interface CacheEntry {
|
|
131
137
|
mtimeMs: number;
|
|
132
138
|
config: HooksFileConfig;
|
|
139
|
+
disableAllHooks?: boolean;
|
|
133
140
|
diagnostics: string[];
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
const cache = new Map<string, CacheEntry>();
|
|
137
144
|
|
|
138
|
-
/** Read one settings file's hooks
|
|
139
|
-
function readHooksFile(path: string, diagnostics: string[]):
|
|
145
|
+
/** Read one settings file's hooks and disabling flag, via the mtime cache. */
|
|
146
|
+
function readHooksFile(path: string, diagnostics: string[]): CacheEntry | undefined {
|
|
140
147
|
let mtimeMs: number;
|
|
141
148
|
try {
|
|
142
149
|
mtimeMs = statSync(path).mtimeMs;
|
|
@@ -147,27 +154,51 @@ function readHooksFile(path: string, diagnostics: string[]): HooksFileConfig | u
|
|
|
147
154
|
const cached = cache.get(path);
|
|
148
155
|
if (cached && cached.mtimeMs === mtimeMs) {
|
|
149
156
|
diagnostics.push(...cached.diagnostics);
|
|
150
|
-
return cached
|
|
157
|
+
return cached;
|
|
151
158
|
}
|
|
152
159
|
const fileDiagnostics: string[] = [];
|
|
153
160
|
let config: HooksFileConfig = {};
|
|
161
|
+
let disableAllHooks: boolean | undefined;
|
|
154
162
|
try {
|
|
155
|
-
const parsed = JSON.parse(readFileSync(path, "utf-8")) as { hooks?: unknown };
|
|
163
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8")) as { hooks?: unknown; disableAllHooks?: unknown };
|
|
156
164
|
config = parseHooksBlock(parsed.hooks, path, fileDiagnostics);
|
|
165
|
+
if (typeof parsed.disableAllHooks === "boolean") disableAllHooks = parsed.disableAllHooks;
|
|
157
166
|
} catch (error) {
|
|
158
167
|
fileDiagnostics.push(`${path}: unreadable settings file skipped (${error instanceof Error ? error.message : error})`);
|
|
159
168
|
}
|
|
160
|
-
|
|
169
|
+
const entry: CacheEntry = { mtimeMs, config, disableAllHooks, diagnostics: fileDiagnostics };
|
|
170
|
+
cache.set(path, entry);
|
|
161
171
|
diagnostics.push(...fileDiagnostics);
|
|
162
|
-
return
|
|
172
|
+
return entry;
|
|
163
173
|
}
|
|
164
174
|
|
|
165
175
|
export function loadHookSettings(claudeDir: string, cwd: string): LoadedHooks {
|
|
166
176
|
const diagnostics: string[] = [];
|
|
167
177
|
const sources: HooksSource[] = [];
|
|
178
|
+
let userDisableAllHooks: boolean | undefined;
|
|
179
|
+
let managedDisableAllHooks: boolean | undefined;
|
|
180
|
+
let repoDisableAllHooks: boolean | undefined;
|
|
168
181
|
for (const { scope, path } of hookSettingsPaths(claudeDir, cwd)) {
|
|
169
|
-
const
|
|
170
|
-
if (
|
|
182
|
+
const entry = readHooksFile(path, diagnostics);
|
|
183
|
+
if (!entry) continue;
|
|
184
|
+
if (entry.disableAllHooks !== undefined) {
|
|
185
|
+
if (scope === "managed") managedDisableAllHooks = entry.disableAllHooks;
|
|
186
|
+
else if (scope === "user") userDisableAllHooks = entry.disableAllHooks;
|
|
187
|
+
else repoDisableAllHooks = entry.disableAllHooks;
|
|
188
|
+
}
|
|
189
|
+
if (Object.keys(entry.config).length > 0) sources.push({ scope, path, config: entry.config });
|
|
190
|
+
}
|
|
191
|
+
// Collection order stays stable, but managed booleans have highest precedence.
|
|
192
|
+
// A non-managed disable must not turn off organization-managed hooks.
|
|
193
|
+
if (managedDisableAllHooks === true) return { sources: [], diagnostics, disabled: "all" };
|
|
194
|
+
if ((managedDisableAllHooks ?? userDisableAllHooks) === true) {
|
|
195
|
+
return { sources: sources.filter((source) => source.scope === "managed"), diagnostics, disabled: "unmanaged" };
|
|
196
|
+
}
|
|
197
|
+
// A repository's flag reaches only its own hooks: a cloned repo must not
|
|
198
|
+
// switch off the user's guard hooks, as project settings never set autoMode.
|
|
199
|
+
if (managedDisableAllHooks === undefined && repoDisableAllHooks === true) {
|
|
200
|
+
diagnostics.push("disableAllHooks in project settings turns off only the project's own hooks");
|
|
201
|
+
return { sources: sources.filter((source) => source.scope === "user" || source.scope === "managed"), diagnostics };
|
|
171
202
|
}
|
|
172
203
|
return { sources, diagnostics };
|
|
173
204
|
}
|
|
@@ -33,6 +33,8 @@ export interface ChildHookCall {
|
|
|
33
33
|
input: Record<string, unknown>;
|
|
34
34
|
/** The child's runtime cwd (a worktree, if isolated). */
|
|
35
35
|
cwd: string;
|
|
36
|
+
/** The child's turn signal; cancelling it must stop its hooks, not the parent's. */
|
|
37
|
+
signal?: AbortSignal;
|
|
36
38
|
/** The child session's id — becomes the payload's `session_id` and `agent_id`. */
|
|
37
39
|
sessionId?: string;
|
|
38
40
|
/** The child's session file, if persisted (payload `transcript_path`). */
|
|
@@ -17,9 +17,9 @@
|
|
|
17
17
|
|
|
18
18
|
import { createHash } from "node:crypto";
|
|
19
19
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { dirname
|
|
20
|
+
import { dirname } from "node:path";
|
|
21
21
|
import { boundConsentItems } from "../lib/consent-preview.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { consentStorePath } from "../lib/consent-stores.ts";
|
|
23
23
|
import type { HooksSource } from "./settings.ts";
|
|
24
24
|
|
|
25
25
|
interface ApprovalStore {
|
|
@@ -28,7 +28,7 @@ interface ApprovalStore {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
export function approvalStorePath(): string {
|
|
31
|
-
return
|
|
31
|
+
return consentStorePath("hooks");
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** Key order must not change the hash — two spellings of one config are one consent. */
|
|
@@ -98,6 +98,7 @@ export interface TrustDecisionDeps {
|
|
|
98
98
|
confirm: (title: string, message: string) => Promise<boolean | undefined>;
|
|
99
99
|
notify: (message: string) => void;
|
|
100
100
|
storePath?: string;
|
|
101
|
+
signal?: AbortSignal;
|
|
101
102
|
/**
|
|
102
103
|
* Answer from the remembered/stored approval only: neither prompt nor record
|
|
103
104
|
* a decision. For a dispatch at session shutdown — recording "declined" there
|
|
@@ -108,7 +109,8 @@ export interface TrustDecisionDeps {
|
|
|
108
109
|
|
|
109
110
|
/** Process-lifetime memory of declined/approved hashes per project, so a dispatch storm asks once. */
|
|
110
111
|
const sessionDecisions = new Map<string, boolean>();
|
|
111
|
-
|
|
112
|
+
/** Undefined: the dispatch that opened the prompt aborted, so nobody decided. */
|
|
113
|
+
const pendingPrompts = new Map<string, Promise<boolean | undefined>>();
|
|
112
114
|
|
|
113
115
|
/**
|
|
114
116
|
* Whether the project/local hook sources may run. Resolves without prompting
|
|
@@ -120,6 +122,7 @@ export async function projectHooksApproved(
|
|
|
120
122
|
projectSources: HooksSource[],
|
|
121
123
|
deps: TrustDecisionDeps,
|
|
122
124
|
): Promise<boolean> {
|
|
125
|
+
if (deps.signal?.aborted) return false;
|
|
123
126
|
if (projectSources.length === 0) return true;
|
|
124
127
|
const configHash = hashProjectHooks(projectSources);
|
|
125
128
|
const key = `${projectRoot}:${configHash}`;
|
|
@@ -145,6 +148,8 @@ export async function projectHooksApproved(
|
|
|
145
148
|
"Run this project's hooks?",
|
|
146
149
|
`This project's .claude settings define ${describeProjectHooks(projectSources)}\n\nThey run as shell commands on your machine. Approval is remembered until the hook config changes.`,
|
|
147
150
|
)) === true;
|
|
151
|
+
// An interrupted dialog is no trust decision, even if a reply raced abort.
|
|
152
|
+
if (deps.signal?.aborted) return undefined;
|
|
148
153
|
sessionDecisions.set(key, approved);
|
|
149
154
|
if (approved) persistApproval(projectRoot, configHash, deps.storePath);
|
|
150
155
|
else deps.notify("Project hooks disabled for this session (user/managed hooks still run).");
|
|
@@ -152,7 +157,10 @@ export async function projectHooksApproved(
|
|
|
152
157
|
})().finally(() => pendingPrompts.delete(key));
|
|
153
158
|
pendingPrompts.set(key, pending);
|
|
154
159
|
}
|
|
155
|
-
|
|
160
|
+
const decided = await pending;
|
|
161
|
+
if (deps.signal?.aborted) return false;
|
|
162
|
+
// The opener aborted without an answer: a dispatch still live asks again.
|
|
163
|
+
return decided ?? projectHooksApproved(projectRoot, projectSources, deps);
|
|
156
164
|
}
|
|
157
165
|
|
|
158
166
|
/** Test seam. */
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* idle-compact extension — Claude Code's compaction of an idle session before
|
|
3
|
+
* its one-hour prompt cache expires (rules and refusal order: policy.ts).
|
|
4
|
+
*
|
|
5
|
+
* Wiring: the timer follows the compaction extension's capture of the
|
|
6
|
+
* session's last request (`LAST_REQUEST_CHANNEL`), the very request its
|
|
7
|
+
* cache-reading replay sends. Each capture re-arms it when the body caches for
|
|
8
|
+
* one hour (`hasOneHourCache`); a cleared capture (a new session, a compaction,
|
|
9
|
+
* a branch switch) drops it, so the timer is armed only while a replay is
|
|
10
|
+
* possible. The reply's usage says whether the cache was warm, the response
|
|
11
|
+
* headers carry Anthropic's rate-limit status, and keystrokes
|
|
12
|
+
* (`onTerminalInput`) mark the user present.
|
|
13
|
+
*
|
|
14
|
+
* TUI only: RPC mode also runs the one-hour cache, but its host's input never
|
|
15
|
+
* reaches `onTerminalInput`, so the presence check could not hold a
|
|
16
|
+
* compaction off while someone is there.
|
|
17
|
+
*
|
|
18
|
+
* On fire it calls `ctx.compact()`; pi's TUI shows its usual "Compacting"
|
|
19
|
+
* indicator, Esc cancels, and on success a display-only entry carries Claude
|
|
20
|
+
* Code's notice. Failures are silent, as in Claude Code.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { type ExtensionAPI, type ExtensionContext, getAgentDir, SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
24
|
+
import { hasOneHourCache } from "../lib/anthropic-payload.ts";
|
|
25
|
+
import { modelSpec } from "../lib/model-policy.ts";
|
|
26
|
+
import { LAST_REQUEST_CHANNEL, type RequestCapture } from "../lib/request-replay.ts";
|
|
27
|
+
import { unrefTimers } from "../lib/timer-ops.ts";
|
|
28
|
+
import { dimMarkedLine, liveUiCtx } from "../lib/tui-render.ts";
|
|
29
|
+
import { type ArmedRequest, IDLE_COMPACT_NOTICE, IdleCompactTimer, type IdleCompactConfig, idleCompactConfig, idleRefusal } from "./policy.ts";
|
|
30
|
+
|
|
31
|
+
const ENTRY_TYPE = "one-code:idle-compact";
|
|
32
|
+
const NOTICE_MARK = "●";
|
|
33
|
+
const RATE_LIMIT_HEADER = "anthropic-ratelimit-unified-status";
|
|
34
|
+
|
|
35
|
+
/** pi's auto-compaction setting; unreadable settings mean pi's default, on. */
|
|
36
|
+
function autoCompactionOn(cwd: string): boolean {
|
|
37
|
+
try {
|
|
38
|
+
return SettingsManager.create(cwd, getAgentDir()).getCompactionEnabled();
|
|
39
|
+
} catch {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export default function idleCompactExtension(pi: ExtensionAPI) {
|
|
45
|
+
pi.registerEntryRenderer(ENTRY_TYPE, (_entry, _options, theme) => dimMarkedLine(theme, NOTICE_MARK, IDLE_COMPACT_NOTICE));
|
|
46
|
+
|
|
47
|
+
let sessionCtx: ExtensionContext | undefined;
|
|
48
|
+
let warm = false;
|
|
49
|
+
let rateLimitStatus: string | undefined;
|
|
50
|
+
let lastInteractionAt: number | undefined;
|
|
51
|
+
let inputHookRegistered = false;
|
|
52
|
+
|
|
53
|
+
const timer = new IdleCompactTimer(unrefTimers, Date.now, () => idleCompactConfig(process.env), (armed, dueAt, config) => fire(armed, dueAt, config));
|
|
54
|
+
|
|
55
|
+
function fire(armed: ArmedRequest, dueAt: number, config: IdleCompactConfig) {
|
|
56
|
+
const ctx = liveUiCtx(sessionCtx);
|
|
57
|
+
if (!ctx) return;
|
|
58
|
+
const refusal = idleRefusal(armed, {
|
|
59
|
+
now: Date.now(),
|
|
60
|
+
dueAt,
|
|
61
|
+
config,
|
|
62
|
+
compactionOn: process.env.CC_COMPACTION !== "0" && autoCompactionOn(ctx.cwd),
|
|
63
|
+
model: ctx.model ? modelSpec(ctx.model) : undefined,
|
|
64
|
+
thinking: pi.getThinkingLevel(),
|
|
65
|
+
warm,
|
|
66
|
+
contextTokens: ctx.getContextUsage()?.tokens ?? 0,
|
|
67
|
+
rateLimitStatus,
|
|
68
|
+
idle: ctx.isIdle() && !ctx.hasPendingMessages(),
|
|
69
|
+
lastInteractionAt,
|
|
70
|
+
});
|
|
71
|
+
if (refusal !== null) return;
|
|
72
|
+
ctx.compact({
|
|
73
|
+
onComplete: () => pi.appendEntry(ENTRY_TYPE),
|
|
74
|
+
// Claude Code logs a failed idle compaction and shows nothing; pi's TUI
|
|
75
|
+
// still reports an error or an Esc cancel, as for any compaction.
|
|
76
|
+
onError: () => {},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
pi.events.on(LAST_REQUEST_CHANNEL, (data) => {
|
|
81
|
+
const capture = data as RequestCapture | undefined;
|
|
82
|
+
const ctx = liveUiCtx(sessionCtx);
|
|
83
|
+
warm = false;
|
|
84
|
+
if (!capture || ctx?.mode !== "tui" || !hasOneHourCache(capture.payload)) {
|
|
85
|
+
timer.cancel();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
timer.arm({ at: Date.now(), model: modelSpec({ provider: capture.provider, id: capture.modelId }), thinking: pi.getThinkingLevel() });
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
pi.on("after_provider_response", (event) => {
|
|
92
|
+
if (!timer.pending) return;
|
|
93
|
+
const entry = Object.entries(event.headers ?? {}).find(([name]) => name.toLowerCase() === RATE_LIMIT_HEADER);
|
|
94
|
+
rateLimitStatus = entry?.[1];
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
pi.on("message_end", (event) => {
|
|
98
|
+
const message = event.message as { role?: string; usage?: { cacheRead?: number; cacheWrite?: number } };
|
|
99
|
+
if (message.role === "assistant") warm = (message.usage?.cacheRead ?? 0) + (message.usage?.cacheWrite ?? 0) > 0;
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
pi.on("session_start", (_event, ctx) => {
|
|
103
|
+
sessionCtx = ctx;
|
|
104
|
+
rateLimitStatus = undefined;
|
|
105
|
+
lastInteractionAt = undefined;
|
|
106
|
+
if (!ctx.hasUI || inputHookRegistered) return;
|
|
107
|
+
// Registered once: session_start fires again on every /clear and switch.
|
|
108
|
+
inputHookRegistered = true;
|
|
109
|
+
ctx.ui.onTerminalInput(() => {
|
|
110
|
+
lastInteractionAt = Date.now();
|
|
111
|
+
return undefined; // observe only
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
pi.on("session_shutdown", () => {
|
|
116
|
+
timer.cancel();
|
|
117
|
+
sessionCtx = undefined;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle compaction's rules, pure (no pi imports; index.ts is the wiring).
|
|
3
|
+
*
|
|
4
|
+
* Claude Code 2.1.289 compacts an idle main session shortly before its
|
|
5
|
+
* one-hour prompt cache expires: the compaction reads the warm cache at the
|
|
6
|
+
* cached rate, where the user's next prompt after expiry would write the whole
|
|
7
|
+
* context to the cache again. Its rules, matched here:
|
|
8
|
+
* - a timer is armed after every main-session request whose cache carries a
|
|
9
|
+
* one-hour TTL, for 90% of the hour (`fireAtFraction`), so 54 minutes;
|
|
10
|
+
* - when it fires, the first refusal below that applies wins, in Claude
|
|
11
|
+
* Code's order: off, compaction off, prefix changed, lapsed (fired more
|
|
12
|
+
* than 60 s late, or the cache is no longer warm), small (under 200,000
|
|
13
|
+
* context tokens), near the rate limit; then the host's: busy (a turn
|
|
14
|
+
* running or messages queued), present (a keystroke in the last 60 s);
|
|
15
|
+
* - otherwise it compacts with no instructions and shows
|
|
16
|
+
* "Compacted while idle, before the prompt cache expired".
|
|
17
|
+
* Claude Code's `not_one_hour` and `newer_request` refusals need no check
|
|
18
|
+
* here: every request replaces the timer, and only a one-hour request arms one.
|
|
19
|
+
*
|
|
20
|
+
* Claude Code ships it behind a server flag that defaults off; One Code turns
|
|
21
|
+
* it on, with `CC_IDLE_COMPACT=0` to opt out (working-docs/decisions/caching.md,
|
|
22
|
+
* "Idle compaction before the cache expires").
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import type { TimerOps } from "../lib/timer-ops.ts";
|
|
26
|
+
|
|
27
|
+
/** The notice Claude Code appends after an idle compaction. */
|
|
28
|
+
export const IDLE_COMPACT_NOTICE = "Compacted while idle, before the prompt cache expired";
|
|
29
|
+
|
|
30
|
+
/** The one-hour cache lifetime idle compaction is armed for. */
|
|
31
|
+
export const ONE_HOUR_MS = 60 * 60_000;
|
|
32
|
+
/** Claude Code's defaults (`minTokens` 200,000, `fireAtFraction` 0.9). */
|
|
33
|
+
export const DEFAULT_MIN_TOKENS = 200_000;
|
|
34
|
+
export const DEFAULT_FIRE_AT_FRACTION = 0.9;
|
|
35
|
+
/** A timer that fires later than this may find the cache gone (the machine slept). */
|
|
36
|
+
export const LATE_MS = 60_000;
|
|
37
|
+
/** A keystroke this recent means the user is present: no compaction under them. */
|
|
38
|
+
export const PRESENCE_MS = 60_000;
|
|
39
|
+
|
|
40
|
+
export interface IdleCompactConfig {
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
minTokens: number;
|
|
43
|
+
/** Delay from the request to the fire; undefined means `fireAtFraction` of the hour. */
|
|
44
|
+
delayMs?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The config from the environment: `CC_IDLE_COMPACT=0` turns it off;
|
|
49
|
+
* `CC_IDLE_COMPACT_MIN_TOKENS` and `CC_IDLE_COMPACT_DELAY_MS` override the
|
|
50
|
+
* threshold and the fire delay, for live testing (the delay stays under the
|
|
51
|
+
* hour, at least one second).
|
|
52
|
+
*/
|
|
53
|
+
export function idleCompactConfig(env: Record<string, string | undefined>): IdleCompactConfig {
|
|
54
|
+
const minTokens = Number(env.CC_IDLE_COMPACT_MIN_TOKENS);
|
|
55
|
+
const delayMs = Number(env.CC_IDLE_COMPACT_DELAY_MS);
|
|
56
|
+
return {
|
|
57
|
+
enabled: env.CC_IDLE_COMPACT !== "0",
|
|
58
|
+
minTokens: Number.isFinite(minTokens) && minTokens >= 1 ? minTokens : DEFAULT_MIN_TOKENS,
|
|
59
|
+
...(Number.isFinite(delayMs) && delayMs >= 1000 && delayMs < ONE_HOUR_MS ? { delayMs } : {}),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Why a fire did not compact. */
|
|
64
|
+
export type IdleRefusal = "disabled" | "compaction_off" | "prefix_changed" | "lapsed" | "small" | "near_limit" | "busy" | "present";
|
|
65
|
+
|
|
66
|
+
/** The one-hour main-session request a timer was armed for. */
|
|
67
|
+
export interface ArmedRequest {
|
|
68
|
+
/** When it went out (ms). */
|
|
69
|
+
at: number;
|
|
70
|
+
/** `provider/id` and thinking level it ran with: the cache's identity. */
|
|
71
|
+
model: string | undefined;
|
|
72
|
+
thinking: string | undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** What the session looks like when the timer fires. */
|
|
76
|
+
export interface FireProbe {
|
|
77
|
+
now: number;
|
|
78
|
+
/** When the timer was due. */
|
|
79
|
+
dueAt: number;
|
|
80
|
+
config: IdleCompactConfig;
|
|
81
|
+
/** pi's auto-compaction setting, and One Code's cache-reading compaction (CC_COMPACTION). */
|
|
82
|
+
compactionOn: boolean;
|
|
83
|
+
model: string | undefined;
|
|
84
|
+
thinking: string | undefined;
|
|
85
|
+
/** The armed request's reply reported cache reads or writes. */
|
|
86
|
+
warm: boolean;
|
|
87
|
+
contextTokens: number;
|
|
88
|
+
/** Anthropic's `anthropic-ratelimit-unified-status` from the latest response, if any. */
|
|
89
|
+
rateLimitStatus: string | undefined;
|
|
90
|
+
idle: boolean;
|
|
91
|
+
lastInteractionAt: number | undefined;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The first refusal that applies, in Claude Code's order, or null to compact. */
|
|
95
|
+
export function idleRefusal(armed: ArmedRequest, probe: FireProbe): IdleRefusal | null {
|
|
96
|
+
if (!probe.config.enabled) return "disabled";
|
|
97
|
+
if (!probe.compactionOn) return "compaction_off";
|
|
98
|
+
if (probe.model !== armed.model || probe.thinking !== armed.thinking) return "prefix_changed";
|
|
99
|
+
// The expiry half only trips under a CC_IDLE_COMPACT_DELAY_MS near the hour:
|
|
100
|
+
// at the default 54 minutes the lateness check trips first.
|
|
101
|
+
if (probe.now - probe.dueAt > LATE_MS || !probe.warm || probe.now >= armed.at + ONE_HOUR_MS) return "lapsed";
|
|
102
|
+
if (probe.contextTokens < probe.config.minTokens) return "small";
|
|
103
|
+
if (probe.rateLimitStatus !== undefined && probe.rateLimitStatus !== "allowed") return "near_limit";
|
|
104
|
+
if (!probe.idle) return "busy";
|
|
105
|
+
if (probe.lastInteractionAt !== undefined && probe.now - probe.lastInteractionAt < PRESENCE_MS) return "present";
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* One pending timer for the latest one-hour request. Each `arm` replaces it;
|
|
111
|
+
* `cancel()` drops it when the session's last request stops being a one-hour
|
|
112
|
+
* one, or there is none (a new session, a compaction, a branch switch).
|
|
113
|
+
*/
|
|
114
|
+
export class IdleCompactTimer {
|
|
115
|
+
private handle: unknown;
|
|
116
|
+
|
|
117
|
+
constructor(
|
|
118
|
+
private readonly timer: TimerOps,
|
|
119
|
+
private readonly now: () => number,
|
|
120
|
+
private readonly config: () => IdleCompactConfig,
|
|
121
|
+
private readonly onFire: (armed: ArmedRequest, dueAt: number, config: IdleCompactConfig) => void,
|
|
122
|
+
) {}
|
|
123
|
+
|
|
124
|
+
arm(request: ArmedRequest): void {
|
|
125
|
+
this.cancel();
|
|
126
|
+
const config = this.config();
|
|
127
|
+
if (!config.enabled) return;
|
|
128
|
+
const dueAt = request.at + (config.delayMs ?? ONE_HOUR_MS * DEFAULT_FIRE_AT_FRACTION);
|
|
129
|
+
this.handle = this.timer.set(() => {
|
|
130
|
+
this.handle = undefined;
|
|
131
|
+
this.onFire(request, dueAt, config);
|
|
132
|
+
}, Math.max(0, dueAt - this.now()));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
cancel(): void {
|
|
136
|
+
if (this.handle !== undefined) this.timer.clear(this.handle);
|
|
137
|
+
this.handle = undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
get pending(): boolean {
|
|
141
|
+
return this.handle !== undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
package/extensions/init/index.ts
CHANGED
|
@@ -1,26 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* init extension — Claude Code's `/init`.
|
|
3
3
|
*
|
|
4
|
-
* `/init` scans the repo and writes a
|
|
5
|
-
*
|
|
6
|
-
* a user turn (pi.sendUserMessage) and the
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* `/init` scans the repo and writes a CLAUDE.md for future sessions. It is a
|
|
5
|
+
* prompt template, not procedural code: the handler submits INIT_PROMPT
|
|
6
|
+
* (Claude Code's classic prompt) as a user turn (pi.sendUserMessage) and the
|
|
7
|
+
* model does the work with the ordinary tool set, as Claude Code's `/init`
|
|
8
|
+
* does.
|
|
9
9
|
*
|
|
10
10
|
* pi has no built-in `/init`, so a Claude Code user typing it would otherwise
|
|
11
11
|
* have the word go to the model as a chat message.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
import { createUserMessageSender } from "../lib/notifications.ts";
|
|
16
|
+
import { promptCommandText } from "../skill/invoke.ts";
|
|
15
17
|
import { INIT_PROMPT } from "./prompt.ts";
|
|
16
18
|
|
|
17
19
|
export default function initExtension(pi: ExtensionAPI) {
|
|
20
|
+
const sendUserMessage = createUserMessageSender(pi);
|
|
18
21
|
pi.registerCommand("init", {
|
|
19
|
-
description: "
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
handler: async () => {
|
|
23
|
-
pi.sendUserMessage(INIT_PROMPT, { deliverAs: "followUp" });
|
|
22
|
+
description: "Initialize a new CLAUDE.md file with codebase documentation",
|
|
23
|
+
handler: async (_args, ctx) => {
|
|
24
|
+
await sendUserMessage(ctx, promptCommandText("init", "", INIT_PROMPT));
|
|
24
25
|
},
|
|
25
26
|
});
|
|
26
27
|
}
|