one-code-extension 0.1.3 → 0.3.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/LICENSE +20 -0
- package/README.md +200 -172
- package/agents/explore.md +9 -6
- package/agents/general-purpose.md +1 -1
- package/agents/plan.md +6 -4
- package/docs/guide/README.md +52 -0
- package/docs/guide/bring-your-claude-code-setup.md +126 -0
- package/docs/guide/configuration.md +171 -0
- package/docs/guide/differences-from-claude-code.md +150 -0
- package/docs/guide/doctor.md +86 -0
- package/docs/guide/hooks.md +109 -0
- package/docs/guide/installation.md +124 -0
- package/docs/guide/permissions-modes-and-auto-mode.md +450 -0
- package/docs/guide/providers-and-models.md +194 -0
- package/docs/guide/reference.md +224 -0
- package/docs/guide/sessions-and-context.md +190 -0
- package/docs/guide/skills-plugins-and-mcp.md +176 -0
- package/docs/guide/subagents-and-workflows.md +193 -0
- package/docs/guide/tasks-and-background-work.md +111 -0
- package/docs/guide/terminal-interface.md +155 -0
- package/docs/guide/tools.md +175 -0
- package/docs/guide/troubleshooting.md +175 -0
- package/extensions/ask-user/index.ts +65 -71
- package/extensions/ask-user/questions.ts +15 -28
- package/extensions/ask-user/widget.ts +533 -0
- package/extensions/auto-mode/actions.ts +9 -26
- package/extensions/auto-mode/classifier-prompt.ts +127 -0
- package/extensions/auto-mode/classifier.ts +305 -227
- package/extensions/auto-mode/config.ts +279 -77
- package/extensions/auto-mode/defaults.ts +17 -60
- package/extensions/auto-mode/instructions.ts +2 -1
- package/extensions/auto-mode/model-picker.ts +53 -31
- package/extensions/auto-mode/model-select.ts +153 -82
- package/extensions/auto-mode/paths.ts +13 -1
- package/extensions/auto-mode/pause.ts +8 -0
- package/extensions/auto-mode/prompt.ts +205 -238
- package/extensions/auto-mode/recoverability.ts +229 -0
- package/extensions/auto-mode/rules.ts +92 -75
- package/extensions/auto-mode/safety-floor.ts +43 -12
- package/extensions/auto-mode/sensitive.ts +11 -0
- package/extensions/auto-mode/setup-run.ts +245 -0
- package/extensions/auto-mode/setup.ts +235 -0
- package/extensions/auto-mode/shell-analysis.ts +332 -26
- package/extensions/auto-mode/transcript.ts +102 -0
- package/extensions/background/index.ts +256 -43
- package/extensions/background/monitor-batch.ts +58 -0
- package/extensions/background/registry.ts +9 -0
- package/extensions/background/wakeup.ts +45 -0
- package/extensions/bash/background.ts +47 -22
- package/extensions/bash/guards.ts +229 -0
- package/extensions/bash/index.ts +81 -19
- package/extensions/branding/index.ts +44 -18
- package/extensions/branding/startup.ts +23 -8
- package/extensions/claude-compat/index.ts +17 -2
- package/extensions/claude-context/index.ts +41 -12
- package/extensions/clear/index.ts +27 -0
- package/extensions/compaction/fit.ts +121 -0
- package/extensions/compaction/index.ts +176 -71
- package/extensions/compaction/prompt.ts +21 -1
- package/extensions/context-budget/budget.ts +61 -0
- package/extensions/context-budget/index.ts +66 -0
- package/extensions/context-management/index.ts +40 -16
- package/extensions/doctor/build.ts +226 -0
- package/extensions/doctor/cli.ts +99 -0
- package/extensions/doctor/compat.ts +532 -0
- package/extensions/doctor/dependencies.ts +186 -0
- package/extensions/doctor/fix-prompt.ts +125 -0
- package/extensions/doctor/index.ts +308 -0
- package/extensions/doctor/models.ts +231 -0
- package/extensions/doctor/presets.ts +208 -0
- package/extensions/doctor/report.ts +189 -0
- package/extensions/doctor/update-lookup.ts +46 -0
- package/extensions/doctor/viewer.ts +124 -0
- package/extensions/effort/index.ts +41 -4
- package/extensions/effort/slider.ts +33 -2
- package/extensions/file-tracker/index.ts +143 -20
- package/extensions/file-tracker/replay.ts +46 -0
- package/extensions/file-tracker/tracker.ts +51 -4
- package/extensions/footer/footer-line.ts +207 -0
- package/extensions/footer/index.ts +152 -0
- package/extensions/footer/pr.ts +53 -0
- package/extensions/hooks/executor.ts +57 -20
- package/extensions/hooks/formatter-notice.ts +76 -0
- package/extensions/hooks/index.ts +314 -73
- package/extensions/hooks/matcher.ts +35 -1
- package/extensions/hooks/plugin-hooks.ts +3 -3
- package/extensions/hooks/protocol.ts +58 -0
- package/extensions/hooks/subagent-bridge.ts +77 -0
- package/extensions/hooks/trust.ts +15 -4
- package/extensions/init/index.ts +26 -0
- package/extensions/init/prompt.ts +40 -0
- package/extensions/interrupted/index.ts +35 -0
- package/extensions/interrupted/line.ts +9 -0
- package/extensions/lib/abort.ts +15 -0
- package/extensions/lib/agent-loader.ts +183 -0
- package/extensions/lib/anthropic-payload.ts +15 -0
- package/extensions/lib/atomic-write.ts +56 -0
- package/extensions/lib/bridge-watch.ts +20 -0
- package/extensions/lib/cache-retention.ts +60 -0
- package/extensions/lib/capability-index.ts +466 -0
- package/extensions/lib/claude-context.ts +318 -27
- package/extensions/lib/claude-settings.ts +64 -0
- package/extensions/lib/consent-preview.ts +30 -0
- package/extensions/lib/deferred.ts +144 -13
- package/extensions/lib/favorites.ts +43 -0
- package/extensions/lib/fetch-timeout.ts +15 -0
- package/extensions/lib/frontmatter.ts +31 -0
- package/extensions/lib/git.ts +44 -2
- package/extensions/lib/hook-gate.ts +90 -0
- package/extensions/lib/interrupt.ts +94 -0
- package/extensions/lib/mcp-overrides.ts +61 -0
- package/extensions/lib/mcp-share.ts +25 -0
- package/extensions/lib/mcp-status.ts +34 -0
- package/extensions/lib/memory.ts +41 -6
- package/extensions/lib/model-facts.generated.json +2480 -0
- package/extensions/lib/model-facts.ts +162 -0
- package/extensions/lib/model-policy.ts +145 -189
- package/extensions/lib/model-tier.ts +480 -64
- package/extensions/lib/model-unusable.ts +37 -0
- package/extensions/lib/notifications.ts +320 -1
- package/extensions/lib/one-code-settings.ts +63 -0
- package/extensions/lib/original-command.ts +71 -0
- package/extensions/lib/package-version.ts +31 -0
- package/extensions/lib/paths.ts +39 -4
- package/extensions/lib/permission-gate.ts +192 -0
- package/extensions/lib/persisted-output.ts +18 -0
- package/extensions/lib/pi-ai-estimate.ts +160 -0
- package/extensions/lib/pi-version.ts +1 -1
- package/extensions/lib/plan-mode-channels.ts +11 -0
- package/extensions/lib/plugin-overrides.ts +24 -0
- package/extensions/lib/plugin-root.ts +44 -0
- package/extensions/lib/plugins.ts +233 -54
- package/extensions/lib/prefix-warm-gate.ts +157 -0
- package/extensions/lib/process-tree.ts +56 -0
- package/extensions/lib/reminders.ts +321 -68
- package/extensions/lib/scratchpad.ts +11 -8
- package/extensions/lib/session-lifecycle.ts +28 -0
- package/extensions/lib/settings-channels.ts +14 -0
- package/extensions/lib/shell-tasks.ts +54 -0
- package/extensions/lib/skill-overrides.ts +86 -0
- package/extensions/lib/skill-scan.ts +110 -0
- package/extensions/lib/text-width.ts +267 -0
- package/extensions/lib/tui-render.ts +281 -54
- package/extensions/lib/usage-bus.ts +75 -0
- package/extensions/lib/usage-tracker.ts +67 -0
- package/extensions/lib/worktree-isolation.ts +170 -0
- package/extensions/lsp/client.ts +69 -12
- package/extensions/lsp/format.ts +12 -2
- package/extensions/lsp/index.ts +273 -84
- package/extensions/lsp/install-hints.ts +43 -0
- package/extensions/lsp/keep-alive.ts +19 -0
- package/extensions/lsp/plugin-servers.ts +331 -0
- package/extensions/lsp/watcher.ts +157 -0
- package/extensions/mcp/announce.ts +99 -0
- package/extensions/mcp/client.ts +181 -22
- package/extensions/mcp/config.ts +55 -24
- package/extensions/mcp/index.ts +586 -86
- package/extensions/mcp/oauth/browser.ts +26 -0
- package/extensions/mcp/oauth/callback.ts +104 -0
- package/extensions/mcp/oauth/flow.ts +68 -0
- package/extensions/mcp/oauth/provider.ts +147 -0
- package/extensions/mcp/oauth/store.ts +86 -0
- package/extensions/mcp/panel/keys.ts +34 -0
- package/extensions/mcp/panel/model.ts +62 -0
- package/extensions/mcp/panel/render.ts +141 -0
- package/extensions/mcp/panel/state.ts +108 -0
- package/extensions/mcp/schema.ts +83 -6
- package/extensions/mcp/trust.ts +340 -0
- package/extensions/memory/entries.ts +163 -0
- package/extensions/memory/index.ts +119 -2
- package/extensions/memory/open-external.ts +97 -0
- package/extensions/memory/panel.ts +124 -0
- package/extensions/model-default/index.ts +74 -0
- package/extensions/notebook/index.ts +36 -18
- package/extensions/notebook/notebook.ts +110 -17
- package/extensions/permissions/index.ts +1012 -247
- package/extensions/permissions/matcher.ts +488 -57
- package/extensions/permissions/modes.ts +55 -7
- package/extensions/permissions/project-trust.ts +81 -0
- package/extensions/permissions/protected-paths.ts +16 -3
- package/extensions/permissions/rule-prose.ts +79 -0
- package/extensions/permissions/session-grant.ts +91 -0
- package/extensions/permissions/settings.ts +112 -33
- package/extensions/permissions/subagent-gate.ts +57 -0
- package/extensions/plan-mode/index.ts +143 -33
- package/extensions/plan-mode/reminder.ts +16 -19
- package/extensions/plan-mode/viewer.ts +40 -31
- package/extensions/plugins/counts.ts +68 -0
- package/extensions/plugins/index.ts +496 -32
- package/extensions/plugins/install/install.ts +167 -0
- package/extensions/plugins/install/paths.ts +23 -0
- package/extensions/plugins/install/registry.ts +72 -0
- package/extensions/plugins/marketplace/git.ts +103 -0
- package/extensions/plugins/marketplace/official.ts +36 -0
- package/extensions/plugins/marketplace/parse.ts +94 -0
- package/extensions/plugins/marketplace/registry.ts +60 -0
- package/extensions/plugins/marketplace/sync.ts +146 -0
- package/extensions/plugins/marketplace/types.ts +128 -0
- package/extensions/plugins/panel/keys.ts +52 -0
- package/extensions/plugins/panel/render.ts +315 -0
- package/extensions/plugins/panel/rows.ts +230 -0
- package/extensions/plugins/panel/state.ts +333 -0
- package/extensions/recap/index.ts +215 -0
- package/extensions/recap/prompt.ts +44 -0
- package/extensions/recap/scheduler.ts +87 -0
- package/extensions/search-tools/index.ts +0 -0
- package/extensions/search-tools/tools.ts +7 -5
- package/extensions/skill/index.ts +360 -34
- package/extensions/skill/invoke.ts +185 -0
- package/extensions/skill/listing.ts +21 -0
- package/extensions/skill/panel/keys.ts +41 -0
- package/extensions/skill/panel/render.ts +91 -0
- package/extensions/skill/panel/state.ts +115 -0
- package/extensions/spinner/index.ts +110 -0
- package/extensions/spinner/line.ts +51 -0
- package/extensions/spinner/verbs.ts +201 -0
- package/extensions/subagents/agents.ts +88 -44
- package/extensions/subagents/default-model.ts +62 -32
- package/extensions/subagents/delegation-steer.ts +23 -0
- package/extensions/subagents/hand-back-review.ts +67 -0
- package/extensions/subagents/index.ts +1545 -346
- package/extensions/subagents/live-runs.ts +293 -0
- package/extensions/subagents/model-select.ts +252 -82
- package/extensions/subagents/outcome.ts +90 -0
- package/extensions/subagents/panel-keys.ts +41 -0
- package/extensions/subagents/panel-render.ts +357 -0
- package/extensions/subagents/panel-widget.ts +330 -0
- package/extensions/subagents/pending-claim.ts +45 -0
- package/extensions/subagents/prose.ts +105 -0
- package/extensions/subagents/runner.ts +674 -0
- package/extensions/subagents/runs.ts +68 -1
- package/extensions/subagents/send-to-main-tool.ts +55 -0
- package/extensions/subagents/session-turns.ts +138 -0
- package/extensions/subagents/shell-panel.ts +198 -0
- package/extensions/subagents/usage.ts +16 -4
- package/extensions/subagents/worktree.ts +13 -0
- package/extensions/system-prompt/environment.ts +5 -3
- package/extensions/system-prompt/git-status.ts +93 -0
- package/extensions/system-prompt/index.ts +23 -1
- package/extensions/system-prompt/template.ts +34 -4
- package/extensions/system-prompt/tiers/common.ts +31 -6
- package/extensions/system-prompt/tiers/low.ts +13 -3
- package/extensions/system-prompt/tiers/mid.ts +21 -8
- package/extensions/system-reminder/index.ts +53 -9
- package/extensions/tasks/index.ts +65 -16
- package/extensions/tasks/store.ts +76 -3
- package/extensions/tool-search/announce.ts +42 -0
- package/extensions/tool-search/index.ts +136 -13
- package/extensions/turn-duration/index.ts +60 -0
- package/extensions/turn-duration/line.ts +22 -0
- package/extensions/turn-duration/span.ts +41 -0
- package/extensions/turn-duration/verbs.ts +24 -0
- package/extensions/web/backends.ts +421 -0
- package/extensions/web/index.ts +178 -21
- package/extensions/web-fetch/index.ts +74 -18
- package/extensions/web-fetch/summarize.ts +1 -1
- package/extensions/workflow/agent-session.ts +182 -112
- package/extensions/workflow/globals.ts +129 -106
- package/extensions/workflow/index.ts +343 -28
- package/extensions/workflow/records.ts +118 -0
- package/extensions/workflow/run-manager.ts +80 -31
- package/extensions/workflow/saved-workflows.ts +2 -1
- package/extensions/workflow/script-worker.mjs +110 -0
- package/extensions/workflow/types.ts +74 -8
- package/extensions/workflow/viewer.ts +562 -0
- package/extensions/workflow/vm-runtime.ts +118 -22
- package/extensions/workflow/widget.ts +126 -18
- package/extensions/worktree/guards.ts +240 -0
- package/extensions/worktree/index.ts +30 -5
- package/extensions/worktree/rewrite.ts +32 -16
- package/package.json +20 -10
- package/skills/code-review/SKILL.md +165 -0
- package/skills/fewer-permission-prompts/SKILL.md +61 -0
- package/skills/security-review/SKILL.md +178 -0
- package/skills/simplify/SKILL.md +70 -0
- package/themes/{one-code-light.json → onecode-light.json} +6 -1
- package/themes/{one-code.json → onecode.json} +6 -1
- package/types/app.d.ts +8 -1
- package/types/community-packages.d.ts +45 -2
- package/docs/decisions/auto-mode.md +0 -526
- package/docs/decisions/branding.md +0 -127
- package/docs/decisions/code-review-remediation.md +0 -106
- package/docs/decisions/compaction.md +0 -140
- package/docs/decisions/distribution.md +0 -168
- package/docs/decisions/hooks.md +0 -64
- package/docs/decisions/lsp.md +0 -58
- package/docs/decisions/mcp.md +0 -104
- package/docs/decisions/memory-state.md +0 -170
- package/docs/decisions/model-policy.md +0 -126
- package/docs/decisions/modes.md +0 -178
- package/docs/decisions/skills-plugins.md +0 -56
- package/docs/decisions/subagents-workflows.md +0 -225
- package/docs/decisions/system-prompt.md +0 -98
- package/docs/decisions/tools.md +0 -407
- package/docs/decisions/tui.md +0 -202
- package/docs/decisions.md +0 -148
- package/extensions/subagents/child.ts +0 -414
- package/extensions/subagents/rpc-turns.ts +0 -123
- package/extensions/todo/index.ts +0 -133
- package/extensions/workflow/permission-gate.ts +0 -71
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
* assume (see model-select.ts).
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
+
import { truncateLine } from "../lib/tui-render.ts";
|
|
19
|
+
|
|
18
20
|
export interface PickerEntry {
|
|
19
21
|
provider: string;
|
|
20
22
|
id: string;
|
|
@@ -143,7 +145,7 @@ export function modelPickerComponent(
|
|
|
143
145
|
tui: { requestRender(): void },
|
|
144
146
|
theme: unknown,
|
|
145
147
|
done: (choice: PickerEntry | null) => void,
|
|
146
|
-
): { render(): string[]; handleInput(data: string): void; invalidate(): void } {
|
|
148
|
+
): { render(width: number): string[]; handleInput(data: string): void; invalidate(): void } {
|
|
147
149
|
const paint: Paint = (color, text) => {
|
|
148
150
|
const themed = theme as { fg?(c: string, t: string): string } | undefined;
|
|
149
151
|
try {
|
|
@@ -155,23 +157,34 @@ export function modelPickerComponent(
|
|
|
155
157
|
let query = "";
|
|
156
158
|
let index = 0;
|
|
157
159
|
let filtered = options.entries;
|
|
160
|
+
// pi calls render(width) every frame and the picker repaints per keystroke;
|
|
161
|
+
// memoize by width and clear on any state change so a wide default subtitle
|
|
162
|
+
// (115 columns) never reaches the wire uncut — an overwide line crashes pi
|
|
163
|
+
// in regular TUI mode (TUI-REVIEW H2).
|
|
164
|
+
let cache: { width: number; lines: string[] } | undefined;
|
|
158
165
|
return {
|
|
159
|
-
render: () =>
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
166
|
+
render: (width: number) => {
|
|
167
|
+
if (cache && cache.width === width) return cache.lines;
|
|
168
|
+
const lines = [
|
|
169
|
+
"",
|
|
170
|
+
...renderModelPicker(
|
|
171
|
+
{
|
|
172
|
+
entries: filtered,
|
|
173
|
+
index,
|
|
174
|
+
query,
|
|
175
|
+
total: options.entries.length,
|
|
176
|
+
current: options.current,
|
|
177
|
+
title: options.title,
|
|
178
|
+
subtitle: options.subtitle,
|
|
179
|
+
},
|
|
180
|
+
paint,
|
|
181
|
+
width,
|
|
182
|
+
),
|
|
183
|
+
"",
|
|
184
|
+
];
|
|
185
|
+
cache = { width, lines };
|
|
186
|
+
return lines;
|
|
187
|
+
},
|
|
175
188
|
handleInput: (data: string) => {
|
|
176
189
|
const key = decodePickerKey(data);
|
|
177
190
|
if (!key) return;
|
|
@@ -184,27 +197,36 @@ export function modelPickerComponent(
|
|
|
184
197
|
filtered = filterEntries(options.entries, query);
|
|
185
198
|
index = 0;
|
|
186
199
|
}
|
|
200
|
+
cache = undefined;
|
|
187
201
|
tui.requestRender();
|
|
188
202
|
},
|
|
189
|
-
invalidate: () => {
|
|
203
|
+
invalidate: () => {
|
|
204
|
+
cache = undefined;
|
|
205
|
+
},
|
|
190
206
|
};
|
|
191
207
|
}
|
|
192
208
|
|
|
193
|
-
|
|
209
|
+
/** Key hint, kept on its own line so it survives at narrow widths. */
|
|
210
|
+
const PICKER_HINT = "type to filter · ↑/↓ · enter · esc";
|
|
211
|
+
/** Default privacy note for the classifier picker (the key hint rides its own line below). */
|
|
212
|
+
const CLASSIFIER_SUBTITLE = "It reads your prompts and CLAUDE.md — picking another provider sends them there.";
|
|
213
|
+
|
|
214
|
+
export function renderModelPicker(view: PickerView, paint: Paint, width = Infinity): string[] {
|
|
194
215
|
const maxVisible = view.maxVisible ?? 10;
|
|
216
|
+
// Every line is cut to `width` — pi-tui crashes the whole app on a rendered
|
|
217
|
+
// line wider than the terminal (TUI-REVIEW H2). The default Infinity leaves
|
|
218
|
+
// lines uncut (truncateLine returns them unchanged) for unit tests.
|
|
219
|
+
const cut = (line: string): string => truncateLine(line, width);
|
|
195
220
|
const lines: string[] = [];
|
|
196
|
-
lines.push(paint("accent", view.title ?? "Select the auto-mode classifier model"));
|
|
197
|
-
lines.push(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
),
|
|
203
|
-
);
|
|
204
|
-
lines.push(` filter: ${view.query}${paint("dim", "▏")}`);
|
|
221
|
+
lines.push(cut(paint("accent", view.title ?? "Select the auto-mode classifier model")));
|
|
222
|
+
lines.push(cut(paint("dim", view.subtitle ?? CLASSIFIER_SUBTITLE)));
|
|
223
|
+
// The key hint is always its own line, so a narrow terminal never clips the
|
|
224
|
+
// only place the keys are documented.
|
|
225
|
+
lines.push(cut(paint("dim", ` ${PICKER_HINT}`)));
|
|
226
|
+
lines.push(cut(` filter: ${view.query}${paint("dim", "▏")}`));
|
|
205
227
|
|
|
206
228
|
if (view.entries.length === 0) {
|
|
207
|
-
lines.push(paint("warning", " (no available model matches)"));
|
|
229
|
+
lines.push(cut(paint("warning", " (no available model matches)")));
|
|
208
230
|
return lines;
|
|
209
231
|
}
|
|
210
232
|
|
|
@@ -216,10 +238,10 @@ export function renderModelPicker(view: PickerView, paint: Paint): string[] {
|
|
|
216
238
|
.filter(Boolean)
|
|
217
239
|
.join(" ");
|
|
218
240
|
const suffix = annotations ? ` ${paint("dim", annotations)}` : "";
|
|
219
|
-
lines.push(at === view.index ? `${paint("accent", `❯ ${spec}`)}${suffix}` : ` ${spec}${suffix}`);
|
|
241
|
+
lines.push(cut(at === view.index ? `${paint("accent", `❯ ${spec}`)}${suffix}` : ` ${spec}${suffix}`));
|
|
220
242
|
}
|
|
221
243
|
if (view.entries.length > maxVisible || view.entries.length < view.total) {
|
|
222
|
-
lines.push(paint("dim", ` ${view.entries.length} of ${view.total} models${view.query ? " match" : ""}`));
|
|
244
|
+
lines.push(cut(paint("dim", ` ${view.entries.length} of ${view.total} models${view.query ? " match" : ""}`)));
|
|
223
245
|
}
|
|
224
246
|
return lines;
|
|
225
247
|
}
|
|
@@ -26,22 +26,22 @@
|
|
|
26
26
|
* match" is arbitrary. Cost is the one signal every provider carries.
|
|
27
27
|
*/
|
|
28
28
|
|
|
29
|
-
import type { Api, Model } from "@earendil-works/pi-ai";
|
|
29
|
+
import type { Api, AssistantMessage, Model } from "@earendil-works/pi-ai";
|
|
30
30
|
import {
|
|
31
|
+
crossesProvider,
|
|
31
32
|
findConfigured,
|
|
32
|
-
|
|
33
|
-
hintRank,
|
|
33
|
+
isStaleContainmentStamp,
|
|
34
34
|
modelIdentity,
|
|
35
|
-
|
|
36
|
-
pricedInput,
|
|
35
|
+
modelSpec as spec,
|
|
37
36
|
} from "../lib/model-policy.ts";
|
|
37
|
+
import { capableContainedCandidates } from "../lib/model-tier.ts";
|
|
38
38
|
|
|
39
39
|
export { findConfigured } from "../lib/model-policy.ts";
|
|
40
40
|
|
|
41
41
|
export interface Candidate {
|
|
42
42
|
model: Model<Api>;
|
|
43
43
|
/** How this candidate was arrived at, for the notice shown to the user. */
|
|
44
|
-
source: "configured" | "
|
|
44
|
+
source: "configured" | "economical" | "session";
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export interface SelectInput {
|
|
@@ -50,92 +50,141 @@ export interface SelectInput {
|
|
|
50
50
|
sessionModel: Model<Api> | undefined;
|
|
51
51
|
/** `autoMode.classifierModel`, if set. May name any provider. */
|
|
52
52
|
configured?: string;
|
|
53
|
+
/**
|
|
54
|
+
* The containment identity (`modelIdentity().containment`) the `classifierModel`
|
|
55
|
+
* setting was stamped for when set via `/auto-mode model`. When it differs from
|
|
56
|
+
* the current session's containment, a cross-provider setting is treated as
|
|
57
|
+
* stale (set for a session since left) and overridden with a warning — the same
|
|
58
|
+
* shape as the subagent `subagentModelSetFor` stamp. Undefined for a hand-edited
|
|
59
|
+
* setting.
|
|
60
|
+
*/
|
|
61
|
+
configuredSetForContainment?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A user-facing selection notice, tagged so an informational one is not shown as a warning. */
|
|
65
|
+
export interface ClassifierNotice {
|
|
66
|
+
level: "info" | "warning";
|
|
67
|
+
text: string;
|
|
53
68
|
}
|
|
54
69
|
|
|
55
70
|
/**
|
|
56
|
-
* The ordered candidate chain. More than one
|
|
57
|
-
* turns out to be unusable at call time (not
|
|
58
|
-
* by the provider) can be stepped over
|
|
59
|
-
* same shape as pi's subagent
|
|
71
|
+
* The ordered candidate chain plus any user-facing notices. More than one
|
|
72
|
+
* candidate is returned so one that turns out to be unusable at call time (not
|
|
73
|
+
* entitled on this account, withdrawn by the provider) can be stepped over
|
|
74
|
+
* instead of failing every tool call — the same shape as pi's subagent
|
|
75
|
+
* `fallbackModels`.
|
|
60
76
|
*
|
|
61
77
|
* The chain always ends at the session model when there is one, so auto mode
|
|
62
78
|
* degrades to "correct but not cheap" rather than to "broken".
|
|
79
|
+
*
|
|
80
|
+
* The automatic pick is the SAME floor-gated selector the subagent default uses
|
|
81
|
+
* (`capableContainedCandidates`): the cheapest same-provider model (cheap →
|
|
82
|
+
* workhorse → frontier, never `tiny`) at or above `automaticTierFloor` — a
|
|
83
|
+
* session on a workhorse-or-better model is screened by a workhorse-or-better
|
|
84
|
+
* model, Claude Code's `min(main, sonnet)`. `docs/decisions/auto-mode.md`
|
|
85
|
+
* measured the same `rm -rf` grading stage-1 62 on Sonnet and ~22 on Haiku —
|
|
86
|
+
* "a weak classifier is a weak boundary" — yet until 2026-09-05 the selector
|
|
87
|
+
* picked Haiku even when the session itself was Sonnet, so the gate's threshold
|
|
88
|
+
* behaviour depended on which model happened to be cheapest
|
|
89
|
+
* (PERMISSIONS-REVIEW-2026-09-05 M6). A cheap session keeps a cheap screener
|
|
90
|
+
* (nothing cheaper and capable exists), and `autoMode.classifierModel`
|
|
91
|
+
* overrides either way. The `tiny` exclusion is the capability floor
|
|
92
|
+
* `auto-mode.md` recorded as still-missing. Since 2026-09-11 subagents share
|
|
93
|
+
* the floor, so a session screens and delegates on one model.
|
|
94
|
+
*
|
|
95
|
+
* The floor governs the AUTOMATIC pick only; the terminal fallback is never
|
|
96
|
+
* refused, so the chain is empty only when there is no session model and nothing
|
|
97
|
+
* available at all.
|
|
63
98
|
*/
|
|
64
|
-
export function classifierCandidates({
|
|
99
|
+
export function classifierCandidates({
|
|
100
|
+
available,
|
|
101
|
+
sessionModel,
|
|
102
|
+
configured,
|
|
103
|
+
configuredSetForContainment,
|
|
104
|
+
}: SelectInput): { candidates: Candidate[]; notices: ClassifierNotice[] } {
|
|
65
105
|
const candidates: Candidate[] = [];
|
|
106
|
+
const notices: ClassifierNotice[] = [];
|
|
66
107
|
const push = (model: Model<Api> | undefined, source: Candidate["source"]) => {
|
|
67
108
|
if (!model) return;
|
|
68
109
|
if (candidates.some((entry) => entry.model.provider === model.provider && entry.model.id === model.id)) return;
|
|
69
110
|
candidates.push({ model, source });
|
|
70
111
|
};
|
|
71
112
|
|
|
72
|
-
// 1. What the user asked for
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
113
|
+
// 1. What the user asked for. Naming a provider is choosing it — but a setting
|
|
114
|
+
// stamped for a provider this session has since left is stale and overridden
|
|
115
|
+
// with a warning (parity with the subagent setting); a genuine cross-provider
|
|
116
|
+
// choice made for THIS session is honored with an announcement, because the
|
|
117
|
+
// classifier reads the user's prompts and CLAUDE.md.
|
|
118
|
+
const resolved = configured ? findConfigured(available, configured) : undefined;
|
|
119
|
+
if (resolved) {
|
|
120
|
+
if (sessionModel && crossesProvider(resolved, sessionModel)) {
|
|
121
|
+
if (isStaleContainmentStamp(configuredSetForContainment, sessionModel)) {
|
|
122
|
+
notices.push({
|
|
123
|
+
level: "warning",
|
|
124
|
+
text:
|
|
125
|
+
`autoMode.classifierModel ${spec(resolved)} was set for a different provider than this session (${spec(sessionModel)}); ` +
|
|
126
|
+
"a same-provider model screens calls instead. Re-set it with /auto-mode model on this session to use it here.",
|
|
127
|
+
});
|
|
128
|
+
} else {
|
|
129
|
+
// Informational, not a warning: the user deliberately set this for this
|
|
130
|
+
// session, so it is doing exactly what they asked.
|
|
131
|
+
notices.push({
|
|
132
|
+
level: "info",
|
|
133
|
+
text:
|
|
134
|
+
`autoMode.classifierModel ${spec(resolved)} is a different provider than this session (${spec(sessionModel)}) — ` +
|
|
135
|
+
"it reads your prompts and CLAUDE.md, so those go to that provider. It was set for this session, so it is honored.",
|
|
136
|
+
});
|
|
137
|
+
push(resolved, "configured");
|
|
138
|
+
}
|
|
139
|
+
} else {
|
|
140
|
+
push(resolved, "configured");
|
|
141
|
+
}
|
|
142
|
+
}
|
|
95
143
|
|
|
96
|
-
// 2.
|
|
97
|
-
//
|
|
98
|
-
//
|
|
144
|
+
// 2. The cheapest capable same-provider model, no dearer than the work being
|
|
145
|
+
// screened — screening a call more expensively than making it is
|
|
146
|
+
// indefensible. The shared gate excludes `tiny`, unpriced/opaque rows, and
|
|
147
|
+
// the session model itself, so an unpriced provider yields nothing here and
|
|
148
|
+
// the session model (step 3) screens the calls.
|
|
149
|
+
// With an Artificial Analysis snapshot (an optional user key —
|
|
150
|
+
// lib/capability-index.ts) the floor is MEASURED: a candidate whose coding
|
|
151
|
+
// index reaches min(session, Sonnet 5), on the thinking-off variant the
|
|
152
|
+
// classifier actually runs, screens — Claude Code's min(main, Sonnet) taken
|
|
153
|
+
// literally, so a flash-class model that measurably matches its vendor's
|
|
154
|
+
// flagship may screen it. A measured failure is never admitted; an
|
|
155
|
+
// unscored candidate (no key, no confirmed match) is judged by the
|
|
156
|
+
// name-class tier floor below, exactly as before the snapshot existed.
|
|
99
157
|
if (sessionModel) {
|
|
100
|
-
|
|
101
|
-
push(findRoleProfileModel(available, sessionModel, "classifier", withinBudget, inProvider), "role-profile");
|
|
158
|
+
for (const model of capableContainedCandidates(available, sessionModel, "classifier")) push(model, "economical");
|
|
102
159
|
}
|
|
103
160
|
|
|
104
|
-
// 3. The
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* 4. The session's own model, ahead of the cost-ranked pick.
|
|
114
|
-
*
|
|
115
|
-
* Price is not evidence of suitability, and a name is barely better. On
|
|
116
|
-
* OpenRouter the cheapest model in the catalog is `inclusionai/ling-2.6-flash`
|
|
117
|
-
* at $0.01/M, which would otherwise become the security boundary for being
|
|
118
|
-
* cheap. An attempt to gate that on the name carrying a known small-model word
|
|
119
|
-
* failed on the same example — "flash" is in the name, because any vendor may
|
|
120
|
-
* put it there; the word means "someone called this small", not "this family is
|
|
121
|
-
* known good".
|
|
122
|
-
*
|
|
123
|
-
* So nothing chosen on price alone leads. Providers that *are* vetted get their
|
|
124
|
-
* saving from the table above (which covers OpenRouter and every mainstream
|
|
125
|
-
* provider); anywhere else the model the user already trusted for the real work
|
|
126
|
-
* screens the calls — correct, merely not cheap — and `classifierModel` is
|
|
127
|
-
* there for someone who wants otherwise.
|
|
128
|
-
*/
|
|
161
|
+
// 3. The session's own model: always correct, just not cheap. Terminal
|
|
162
|
+
// fallback, so the gate degrades to screening on it rather than to broken.
|
|
163
|
+
// No tier floor here: a floor that refused a `tiny` session model would
|
|
164
|
+
// disarm auto mode on exactly the sessions least able to do without it,
|
|
165
|
+
// on a price-and-name heuristic rather than measured competence. The
|
|
166
|
+
// circumvention of a permission rule that motivated one is handled by
|
|
167
|
+
// giving the classifier the user's own deny rules to judge by effect
|
|
168
|
+
// (permissions/rule-prose.ts), not by picking a different screener.
|
|
129
169
|
push(sessionModel, "session");
|
|
130
170
|
|
|
131
|
-
// 5. Last resort, for when even the session model cannot serve.
|
|
132
|
-
push(cheapest, "cheapest-in-provider");
|
|
133
|
-
|
|
134
171
|
// Nothing configured and no session model (a headless run with a bare
|
|
135
172
|
// registry) — take anything available rather than refusing outright.
|
|
136
173
|
if (candidates.length === 0) push(available[0], "session");
|
|
137
174
|
|
|
138
|
-
|
|
175
|
+
// A configured spec that matched nothing available would otherwise fall through
|
|
176
|
+
// in silence, leaving the user believing their setting is in force.
|
|
177
|
+
if (configured && !resolved) {
|
|
178
|
+
const instead = candidates[0];
|
|
179
|
+
notices.push({
|
|
180
|
+
level: "warning",
|
|
181
|
+
text:
|
|
182
|
+
`autoMode.classifierModel is set to "${configured}", which is not an available model — check the name and that its provider is authenticated.` +
|
|
183
|
+
(instead ? ` Auto mode is using ${describeCandidate(instead)} instead.` : ""),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { candidates, notices };
|
|
139
188
|
}
|
|
140
189
|
|
|
141
190
|
/**
|
|
@@ -149,30 +198,52 @@ export function classifierCandidates({ available, sessionModel, configured }: Se
|
|
|
149
198
|
* model for the whole session over a blip. Misreading billing as transient
|
|
150
199
|
* merely retries and blocks noisily; misreading a blip as permanent bricks the
|
|
151
200
|
* candidate — so uncertainty goes to transient.
|
|
201
|
+
*
|
|
202
|
+
* "not supported" is here because a plan/entitlement refusal is often phrased
|
|
203
|
+
* that way rather than as a 403: Codex answers a classifier call on
|
|
204
|
+
* `gpt-5.3-codex-spark` with "The 'gpt-5.3-codex-spark' model is not supported
|
|
205
|
+
* when using Codex with a ChatGPT account." Without the match that read as a
|
|
206
|
+
* substantive error, so the chain never stepped past the cheapest-capable pick
|
|
207
|
+
* and auto mode blocked every call of the session (findings §10.19). It is
|
|
208
|
+
* anchored to the word "model" ahead of it, because plenty of REQUEST-shape
|
|
209
|
+
* complaints are phrased the same way ("streaming is not supported",
|
|
210
|
+
* "response_format is not supported for this model") and those are fixable
|
|
211
|
+
* quirks of one call, not a model this account can never use.
|
|
152
212
|
*/
|
|
153
213
|
export function isModelUnavailableError(message: string): boolean {
|
|
154
|
-
return /\b(401|403|404)\b|not_found|not found|does not exist|(no|have|lacks?)\s+access|unauthoriz|forbidden|invalid[_ -]?model|model[_ -]?not|unsupported[_ -]?model|no such model|entitl|insufficient[_ -]?quota|exceeded your current quota|billing/i.test(
|
|
214
|
+
return /\b(401|403|404)\b|not_found|not found|does not exist|(no|have|lacks?)\s+access|unauthoriz|forbidden|invalid[_ -]?model|model[_ -]?not|unsupported[_ -]?model|\bmodel\b[^.\n]{0,40}not supported|no such model|entitl|insufficient[_ -]?quota|exceeded your current quota|billing/i.test(
|
|
155
215
|
message,
|
|
156
216
|
);
|
|
157
217
|
}
|
|
158
218
|
|
|
159
219
|
/** One-line description for the notice and `/auto-mode config`. */
|
|
160
220
|
export function describeCandidate(candidate: Candidate): string {
|
|
161
|
-
const name =
|
|
221
|
+
const name = spec(candidate.model);
|
|
222
|
+
const where = modelIdentity(candidate.model).profile ?? candidate.model.provider;
|
|
162
223
|
switch (candidate.source) {
|
|
163
224
|
case "configured":
|
|
164
225
|
return `${name} (from autoMode.classifierModel)`;
|
|
165
|
-
case "
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
case "cheapest-in-provider": {
|
|
170
|
-
const where = modelIdentity(candidate.model).profile ?? candidate.model.provider;
|
|
171
|
-
return `${name} (cheapest available within ${where})`;
|
|
172
|
-
}
|
|
173
|
-
case "session": {
|
|
174
|
-
const where = modelIdentity(candidate.model).profile ?? candidate.model.provider;
|
|
175
|
-
return `${name} (this session's model — no vetted smaller model within ${where})`;
|
|
176
|
-
}
|
|
226
|
+
case "economical":
|
|
227
|
+
return `${name} (cheapest model within ${where} that meets the capability floor — measured coding index when an Artificial Analysis snapshot is cached, else the session's tier)`;
|
|
228
|
+
case "session":
|
|
229
|
+
return `${name} (this session's model — nothing cheaper within ${where} meets the capability floor)`;
|
|
177
230
|
}
|
|
178
231
|
}
|
|
232
|
+
|
|
233
|
+
/** Concatenated text blocks of a one-shot reply (classifier verdicts, setup drafts). */
|
|
234
|
+
export function replyText(reply: AssistantMessage): string {
|
|
235
|
+
return reply.content
|
|
236
|
+
.filter((block): block is { type: "text"; text: string } => block.type === "text")
|
|
237
|
+
.map((block) => block.text)
|
|
238
|
+
.join("");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Some pi builds resolve a per-provider baseUrl alongside the API key; it is
|
|
243
|
+
* not in every published version of the auth type, so it is read defensively
|
|
244
|
+
* and, when present, carried onto the model for the call.
|
|
245
|
+
*/
|
|
246
|
+
export function withAuthBaseUrl(model: Model<Api>, auth: unknown): Model<Api> {
|
|
247
|
+
const baseUrl = (auth as { baseUrl?: string }).baseUrl;
|
|
248
|
+
return baseUrl ? ({ ...model, baseUrl } as Model<Api>) : model;
|
|
249
|
+
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { lstatSync, readlinkSync, realpathSync } from "node:fs";
|
|
19
19
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
20
|
+
import { expandTilde } from "../lib/paths.ts";
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Tools whose calls write to a path. Lives here — the lowest shared layer both
|
|
@@ -29,6 +30,17 @@ export function isWritingTool(toolName: string): boolean {
|
|
|
29
30
|
return WRITING_TOOLS.has(toolName);
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The path a file tool call targets (`path` for pi's built-ins, `file_path`
|
|
35
|
+
* for Claude Code-shaped calls, `notebook_path` for notebook_edit), or
|
|
36
|
+
* undefined. The one place that knows the field names, shared by every
|
|
37
|
+
* path-based gate.
|
|
38
|
+
*/
|
|
39
|
+
export function pathArgument(input: Record<string, unknown> | undefined): string | undefined {
|
|
40
|
+
const path = input?.path ?? input?.file_path ?? input?.notebook_path;
|
|
41
|
+
return typeof path === "string" ? path : undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
32
44
|
/** Case-folded on darwin/win32, where the filesystem is case-insensitive. */
|
|
33
45
|
function normalize(target: string): string {
|
|
34
46
|
const normalized = resolve(target).replace(/\\/g, "/").replace(/\/+$/, "");
|
|
@@ -103,7 +115,7 @@ export function isWithin(base: string, target: string): boolean {
|
|
|
103
115
|
*/
|
|
104
116
|
export function toAbsolute(cwd: string, token: string, home: string): string {
|
|
105
117
|
if (token === "~") return home;
|
|
106
|
-
if (token.startsWith("~/")) return resolve(
|
|
118
|
+
if (token === "~" || token.startsWith("~/")) return resolve(expandTilde(token, home));
|
|
107
119
|
if (isAbsolute(token)) return resolve(token);
|
|
108
120
|
return resolve(cwd, token);
|
|
109
121
|
}
|
|
@@ -76,6 +76,14 @@ export class PauseTracker {
|
|
|
76
76
|
return this.paused;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/** Forget everything — a new session (`/clear`, `/resume`) starts with a clean record. */
|
|
80
|
+
reset(): void {
|
|
81
|
+
this.consecutive = 0;
|
|
82
|
+
this.total = 0;
|
|
83
|
+
this.paused = false;
|
|
84
|
+
this.denials.length = 0;
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
/** Most recent first, for the `/permissions` "recently denied" listing. */
|
|
80
88
|
recentDenials(limit = 10): Denial[] {
|
|
81
89
|
return this.denials.slice(-limit).reverse();
|