@diegopetrucci/pi-extensions 0.1.52 → 0.1.54
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/.pi-fleet-tested-version +1 -1
- package/README.md +35 -15
- package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
- package/extensions/agent-workflow-audit/index.ts +12 -0
- package/extensions/agent-workflow-audit/package.json +1 -1
- package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-git-diff/git.ts +1 -0
- package/extensions/annotate-git-diff/index.ts +1 -1
- package/extensions/annotate-git-diff/package.json +1 -1
- package/extensions/annotate-git-diff/prompt.ts +2 -1
- package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
- package/extensions/annotate-last-message/index.ts +1 -1
- package/extensions/annotate-last-message/package.json +1 -1
- package/extensions/brrr/.pi-fleet-tested-version +1 -1
- package/extensions/brrr/index.ts +4 -3
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
- package/extensions/claude-fast/package.json +1 -1
- package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
- package/extensions/confirm-destructive/package.json +1 -1
- package/extensions/context-cap/.pi-fleet-tested-version +1 -1
- package/extensions/context-cap/package.json +1 -1
- package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
- package/extensions/context-inspector/index.ts +5 -0
- package/extensions/context-inspector/package.json +1 -1
- package/extensions/contrarian/.pi-fleet-tested-version +1 -1
- package/extensions/contrarian/index.ts +160 -35
- package/extensions/contrarian/package.json +1 -1
- package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
- package/extensions/dirty-repo-guard/package.json +1 -1
- package/extensions/git-footer/.pi-fleet-tested-version +1 -1
- package/extensions/git-footer/index.ts +42 -14
- package/extensions/git-footer/package.json +1 -1
- package/extensions/gnosis/.pi-fleet-tested-version +1 -1
- package/extensions/gnosis/package.json +1 -1
- package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
- package/extensions/illustrations-to-explain-things/package.json +1 -1
- package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
- package/extensions/inline-bash/package.json +1 -1
- package/extensions/librarian/.pi-fleet-tested-version +1 -1
- package/extensions/librarian/index.ts +206 -85
- package/extensions/librarian/package.json +1 -1
- package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
- package/extensions/minimal-footer/index.ts +132 -72
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/notify/.pi-fleet-tested-version +1 -1
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
- package/extensions/openai-fast/package.json +1 -1
- package/extensions/oracle/.pi-fleet-tested-version +1 -1
- package/extensions/oracle/index.ts +146 -34
- package/extensions/oracle/package.json +1 -1
- package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
- package/extensions/permission-gate/index.ts +5 -1
- package/extensions/permission-gate/package.json +1 -1
- package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
- package/extensions/quiet-tools/index.ts +6 -0
- package/extensions/quiet-tools/package.json +1 -1
- package/extensions/review/.pi-fleet-tested-version +1 -1
- package/extensions/review/index.ts +251 -197
- package/extensions/review/package.json +1 -1
- package/extensions/todo/.pi-fleet-tested-version +1 -1
- package/extensions/todo/index.ts +1 -1
- package/extensions/todo/package.json +1 -1
- package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
- package/extensions/triage-comments/index.ts +25 -3
- package/extensions/triage-comments/package.json +1 -1
- package/package.json +6 -5
package/.pi-fleet-tested-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
package/README.md
CHANGED
|
@@ -2,32 +2,52 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@diegopetrucci/pi-extensions)
|
|
4
4
|
|
|
5
|
-
A collection of [pi](https://
|
|
5
|
+
A collection of [pi](https://pi.dev) agent extensions I made.
|
|
6
|
+
|
|
7
|
+
### Subagents
|
|
8
|
+
|
|
9
|
+
- [`contrarian`](./extensions/contrarian): Adds a read-only contrarian tool that spins up an isolated subprocess to stress-test plans, designs, assumptions, bug hypotheses, and conclusions by steelmanning the strongest opposing case.
|
|
10
|
+
- [`librarian`](./extensions/librarian): Adds a GitHub research scout with a local repo checkout cache disabled by default under the OS user cache directory, toggleable with `/librarian-cache`, configurable subagent model/thinking defaults via `/librarian-config`, and cached repos expiring after 7 days of non-use.
|
|
11
|
+
- [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, supports persisted `/oracle` model/thinking defaults, requests xhigh reasoning by default and clamps to model capabilities, and shows live status while running.
|
|
12
|
+
|
|
13
|
+
### Providers
|
|
14
|
+
|
|
15
|
+
- [`claude-fast`](./extensions/claude-fast): Adds `/claude-fast` to enable Anthropic Claude Fast mode for supported Claude Opus models by injecting `speed: "fast"`.
|
|
16
|
+
- [`openai-fast`](./extensions/openai-fast): Adds `/fast` to enable OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.
|
|
17
|
+
|
|
18
|
+
### UI
|
|
19
|
+
|
|
20
|
+
- [`git-footer`](./extensions/git-footer): Standalone extension that adds TLH-style git dirty counts, ahead/behind, and optional PR number to pi's built-in footer status area.
|
|
21
|
+
- [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal configurable two-line layout: branch plus cached git dirty/ahead/PR status and repo on the first line, context/model on the second, optional `DUMB ZONE`, optional `xp` marker, plus OpenAI Codex 5-hour and 7-day usage when available.
|
|
22
|
+
- [`quiet-tools`](./extensions/quiet-tools): Renders collapsed built-in tool rows as a one-line invocation plus an expand hint without changing model-visible tool results; toggle temporarily with `/quiet-tools`.
|
|
23
|
+
|
|
24
|
+
### / Slash commands
|
|
6
25
|
|
|
7
26
|
- [`agent-workflow-audit`](./extensions/agent-workflow-audit): Adds `/agent-workflow-audit`, which runs an isolated repo workflow audit subagent and returns only the final distilled report to the main session.
|
|
8
27
|
- [`annotate-git-diff`](./extensions/annotate-git-diff): Adds `/annotate-git-diff`, a native Glimpse UI for annotating git diffs and appending structured feedback prompts to the editor.
|
|
9
28
|
- [`annotate-last-message`](./extensions/annotate-last-message): Adds `/annotate-last-message`, a native Glimpse UI for annotating the latest assistant reply and sending planning-oriented feedback back through the editor.
|
|
10
|
-
- [`
|
|
11
|
-
- [`
|
|
12
|
-
|
|
13
|
-
|
|
29
|
+
- [`review`](./extensions/review): Adds `/review` and `/end-review` for interactive code reviews of local changes, commits, PRs, and selected paths.
|
|
30
|
+
- [`triage-comments`](./extensions/triage-comments): Adds `/triage-comments` and a read-only `triage_comments` subagent tool that can auto-detect the current branch's PR, filter resolved/outdated inline comments, classify selected review comments with evidence, and suggest handling options without implementing changes
|
|
31
|
+
|
|
32
|
+
### Context management
|
|
33
|
+
|
|
14
34
|
- [`context-cap`](./extensions/context-cap): Caps effective model context windows at 200k tokens by default so pi avoids the `dumb zone`; toggle temporarily with `/context-cap`.
|
|
15
35
|
- [`context-inspector`](./extensions/context-inspector): Adds `/context`, a local self-contained HTML dashboard that breaks down where the current session context is going, with category overview, top offenders, and drilldown search.
|
|
36
|
+
|
|
37
|
+
### Safety
|
|
38
|
+
|
|
39
|
+
- [`confirm-destructive`](./extensions/confirm-destructive): Confirms before destructive session actions like clear, switch, and fork.
|
|
16
40
|
- [`dirty-repo-guard`](./extensions/dirty-repo-guard): Prompts before new sessions, session switches, or forks when the current git repo has uncommitted changes.
|
|
17
|
-
- [`
|
|
41
|
+
- [`permission-gate`](./extensions/permission-gate): Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`.
|
|
42
|
+
|
|
43
|
+
### Quality of life
|
|
44
|
+
|
|
45
|
+
- [`brrr`](./extensions/brrr): Sends brrr push notifications when pi finishes an agent turn and is ready for input, with optional macOS idle gating.
|
|
18
46
|
- [`gnosis`](./extensions/gnosis): Exposes the `gn` repo-local knowledge base CLI as an agent tool for searching and recording durable project decisions, constraints, and intent.
|
|
19
47
|
- [`illustrations-to-explain-things`](./extensions/illustrations-to-explain-things): Adds a skill for generating clean, absurd Xiaohei-style article illustrations, shot lists, image edits, and visual metaphors.
|
|
20
48
|
- [`inline-bash`](./extensions/inline-bash): Expands `!{command}` snippets in user prompts by running them through bash before the prompt reaches the agent.
|
|
21
|
-
- [`librarian`](./extensions/librarian): Adds a GitHub research scout with a local repo checkout cache disabled by default under the OS user cache directory, toggleable with `/librarian-cache`, configurable subagent model/thinking defaults via `/librarian-config`, and cached repos expiring after 7 days of non-use.
|
|
22
|
-
- [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal configurable two-line layout: branch plus cached git dirty/ahead/PR status and repo on the first line, context/model on the second, optional `DUMB ZONE`, optional `xp` marker, plus OpenAI Codex 5-hour and 7-day usage when available.
|
|
23
49
|
- [`notify`](./extensions/notify): Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input.
|
|
24
|
-
- [`
|
|
25
|
-
- [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, supports persisted `/oracle` model/thinking defaults, requests xhigh reasoning by default and clamps to model capabilities, and shows live status while running.
|
|
26
|
-
- [`permission-gate`](./extensions/permission-gate): Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`.
|
|
27
|
-
- [`quiet-tools`](./extensions/quiet-tools): Renders collapsed built-in tool rows as a one-line invocation plus an expand hint without changing model-visible tool results; toggle temporarily with `/quiet-tools`.
|
|
28
|
-
- [`review`](./extensions/review): Adds `/review` and `/end-review` for interactive code reviews of local changes, commits, PRs, and selected paths.
|
|
29
|
-
- [`todo`](./extensions/todo): Adds a branch-aware `todo` tool for the agent and a `/todos` viewer for users.
|
|
30
|
-
- [`triage-comments`](./extensions/triage-comments): Adds `/triage-comments` and a read-only `triage_comments` subagent tool that can auto-detect the current branch's PR, filter resolved/outdated inline comments, classify selected review comments with evidence, and suggest handling options without implementing changes.
|
|
50
|
+
- [`todo`](./extensions/todo): Adds a branch-aware `todo` tool for the agent and a `/todos` viewer for users..
|
|
31
51
|
|
|
32
52
|
(For the full list of pi extensions I use, [check out my dotfiles](https://github.com/diegopetrucci/dot/blob/main/.pi/agent/settings.json).)
|
|
33
53
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
|
@@ -1103,6 +1103,18 @@ async function runAudit(
|
|
|
1103
1103
|
}
|
|
1104
1104
|
}
|
|
1105
1105
|
|
|
1106
|
+
export const __test__ = {
|
|
1107
|
+
appendRunBoundary,
|
|
1108
|
+
assertToolPathInsideCwd,
|
|
1109
|
+
buildSystemPrompt,
|
|
1110
|
+
buildUserPrompt,
|
|
1111
|
+
createAuditRuntimeGuardExtension,
|
|
1112
|
+
formatToolCall,
|
|
1113
|
+
getBlockedBashReason,
|
|
1114
|
+
parseArgs,
|
|
1115
|
+
renderCollapsedReport,
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1106
1118
|
export default function agentWorkflowAuditExtension(pi: ExtensionAPI) {
|
|
1107
1119
|
pi.registerMessageRenderer(CUSTOM_TYPE, (message, { expanded }, theme) => {
|
|
1108
1120
|
const details = message.details as AuditDetails | undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
|
@@ -94,7 +94,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
94
94
|
}
|
|
95
95
|
|
|
96
96
|
async function reviewRepository(ctx: ExtensionCommandContext): Promise<void> {
|
|
97
|
-
if (
|
|
97
|
+
if (ctx.mode !== "tui") {
|
|
98
98
|
ctx.ui.notify("annotate-git-diff requires interactive mode.", "error");
|
|
99
99
|
return;
|
|
100
100
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-annotate-git-diff",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A standalone pi extension that adds /annotate-git-diff, a native Glimpse UI for annotating git diffs and sending feedback to the editor.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -43,6 +43,7 @@ function formatLocation(comment: DiffReviewComment, file: ReviewFile | undefined
|
|
|
43
43
|
|
|
44
44
|
export function composeReviewPrompt(files: ReviewFile[], payload: ReviewSubmitPayload): string {
|
|
45
45
|
const fileMap = new Map(files.map((file) => [file.id, file]));
|
|
46
|
+
const comments = payload.comments.filter((comment) => comment.body.trim().length > 0);
|
|
46
47
|
const lines: string[] = [];
|
|
47
48
|
|
|
48
49
|
lines.push("Please address the following feedback");
|
|
@@ -54,7 +55,7 @@ export function composeReviewPrompt(files: ReviewFile[], payload: ReviewSubmitPa
|
|
|
54
55
|
lines.push("");
|
|
55
56
|
}
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
comments.forEach((comment, index) => {
|
|
58
59
|
const file = fileMap.get(comment.fileId);
|
|
59
60
|
lines.push(`${index + 1}. ${formatLocation(comment, file)}`);
|
|
60
61
|
lines.push(` ${comment.body.trim()}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
|
@@ -38,7 +38,7 @@ export function registerAnnotateLastMessageCommand(pi: ExtensionAPI): void {
|
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
async function openAnnotationWindow(ctx: ExtensionCommandContext): Promise<void> {
|
|
41
|
-
if (
|
|
41
|
+
if (ctx.mode !== "tui") {
|
|
42
42
|
ctx.ui.notify("annotate-last-message requires interactive mode.", "error");
|
|
43
43
|
return;
|
|
44
44
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-annotate-last-message",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "A standalone pi extension that adds /annotate-last-message, a native Glimpse UI for annotating the latest assistant reply.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
package/extensions/brrr/index.ts
CHANGED
|
@@ -159,8 +159,9 @@ function extractTextContent(content: unknown): string {
|
|
|
159
159
|
}
|
|
160
160
|
if (typeof part !== "object" || part === null) continue;
|
|
161
161
|
|
|
162
|
-
const
|
|
163
|
-
if (
|
|
162
|
+
const typedPart = part as { type?: unknown; text?: unknown };
|
|
163
|
+
if (typedPart.type !== undefined && typedPart.type !== "text") continue;
|
|
164
|
+
if (typeof typedPart.text === "string") parts.push(typedPart.text);
|
|
164
165
|
}
|
|
165
166
|
|
|
166
167
|
return parts.join("\n");
|
|
@@ -173,7 +174,7 @@ function lastAssistantMessage(messages: readonly unknown[]): string | undefined
|
|
|
173
174
|
if ((message as { role?: unknown }).role !== "assistant") continue;
|
|
174
175
|
|
|
175
176
|
const text = extractTextContent((message as { content?: unknown }).content).trim();
|
|
176
|
-
|
|
177
|
+
return text || undefined;
|
|
177
178
|
}
|
|
178
179
|
|
|
179
180
|
return undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
|
@@ -1447,6 +1447,11 @@ pre { margin: 0; white-space: pre-wrap; word-break: break-word; color: #cbd5e1;
|
|
|
1447
1447
|
</html>`;
|
|
1448
1448
|
}
|
|
1449
1449
|
|
|
1450
|
+
export const __testing = {
|
|
1451
|
+
analyzeEntries,
|
|
1452
|
+
buildReportData,
|
|
1453
|
+
};
|
|
1454
|
+
|
|
1450
1455
|
export default function contextInspectorExtension(pi: ExtensionAPI) {
|
|
1451
1456
|
pi.registerCommand("context", {
|
|
1452
1457
|
description: "Open a local HTML breakdown of where this session's context is going",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
|
@@ -72,7 +72,20 @@ const COLLAPSED_LINE_LIMIT = 8;
|
|
|
72
72
|
const CONTRARIAN_STATUS_ID = "contrarian";
|
|
73
73
|
const CONTRARIAN_WIDGET_ID = "contrarian";
|
|
74
74
|
const CONTRARIAN_CONFIG_FILE = "contrarian.json";
|
|
75
|
-
const CONTRARIAN_MODEL_PREFERENCES = [
|
|
75
|
+
const CONTRARIAN_MODEL_PREFERENCES = [
|
|
76
|
+
"gpt-5.5",
|
|
77
|
+
"claude-opus-4-8",
|
|
78
|
+
"claude-opus-4.8",
|
|
79
|
+
"claude-sonnet-5-0",
|
|
80
|
+
"claude-sonnet-5.0",
|
|
81
|
+
"claude-sonnet-5",
|
|
82
|
+
"claude-sonnet-4-6",
|
|
83
|
+
"claude-sonnet-4.6",
|
|
84
|
+
"claude-sonnet-4-5",
|
|
85
|
+
"claude-sonnet-4.5",
|
|
86
|
+
"claude-sonnet-4-0",
|
|
87
|
+
"claude-sonnet-4",
|
|
88
|
+
];
|
|
76
89
|
|
|
77
90
|
const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
78
91
|
"amazon-bedrock": [
|
|
@@ -83,6 +96,7 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
83
96
|
"claude-opus-4-5",
|
|
84
97
|
"claude-opus-4-1",
|
|
85
98
|
"claude-opus-4",
|
|
99
|
+
"claude-sonnet-5",
|
|
86
100
|
"claude-sonnet-4-6",
|
|
87
101
|
"claude-sonnet-4-5",
|
|
88
102
|
"claude-sonnet-4",
|
|
@@ -107,6 +121,9 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
107
121
|
"claude-opus-4.1",
|
|
108
122
|
"claude-opus-4-0",
|
|
109
123
|
"claude-opus-4",
|
|
124
|
+
"claude-sonnet-5-0",
|
|
125
|
+
"claude-sonnet-5.0",
|
|
126
|
+
"claude-sonnet-5",
|
|
110
127
|
"claude-sonnet-4-6",
|
|
111
128
|
"claude-sonnet-4.6",
|
|
112
129
|
"claude-sonnet-4-5",
|
|
@@ -185,6 +202,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
185
202
|
"gpt-5",
|
|
186
203
|
"gemini-3.1-pro-preview",
|
|
187
204
|
"gemini-3-pro-preview",
|
|
205
|
+
"claude-sonnet-5.0",
|
|
206
|
+
"claude-sonnet-5",
|
|
188
207
|
"claude-sonnet-4.6",
|
|
189
208
|
"claude-sonnet-4.5",
|
|
190
209
|
"gemini-2.5-pro",
|
|
@@ -360,6 +379,8 @@ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
|
360
379
|
"anthropic/claude-opus-4.6",
|
|
361
380
|
"anthropic/claude-opus-4.5",
|
|
362
381
|
"anthropic/claude-opus-4.1",
|
|
382
|
+
"anthropic/claude-sonnet-5.0",
|
|
383
|
+
"anthropic/claude-sonnet-5",
|
|
363
384
|
"anthropic/claude-sonnet-4.6",
|
|
364
385
|
"openai/gpt-5.5-pro",
|
|
365
386
|
"openai/gpt-5.5",
|
|
@@ -619,6 +640,19 @@ function isTransientErrorMessage(message: string): boolean {
|
|
|
619
640
|
return TRANSIENT_ERROR_PATTERN.test(message);
|
|
620
641
|
}
|
|
621
642
|
|
|
643
|
+
// A model the catalog advertises but the active provider/subscription cannot
|
|
644
|
+
// serve surfaces as a not-found/404-style error (legacy snapshots or
|
|
645
|
+
// access-gated tiers). These are NOT transient: the same model keeps failing,
|
|
646
|
+
// so callers should fall back to a different model instead of retrying it.
|
|
647
|
+
const MODEL_AVAILABILITY_ERROR_PATTERN =
|
|
648
|
+
/\b(?:404|403|not[_ ]?found(?:[_ ]?error)?|model[_ ]?not[_ ]?found(?:[_ ]?error)?|no such model|unknown model|does not exist|is not available|not available|model[_ ]?not[_ ]?available|unsupported model|invalid model|forbidden|access[ _-]?denied|permission[ _-]?denied|not[ _-]?entitled|do(?:es)? not have access)\b/i;
|
|
649
|
+
|
|
650
|
+
function isModelAvailabilityError(message: string | undefined): boolean {
|
|
651
|
+
if (!message) return false;
|
|
652
|
+
if (isTransientErrorMessage(message)) return false;
|
|
653
|
+
return MODEL_AVAILABILITY_ERROR_PATTERN.test(message);
|
|
654
|
+
}
|
|
655
|
+
|
|
622
656
|
function formatContrarianModelError(stopReason: "error" | "aborted", errorMessage: string | undefined): string {
|
|
623
657
|
const trimmed = errorMessage?.trim();
|
|
624
658
|
if (stopReason === "aborted") {
|
|
@@ -810,10 +844,48 @@ async function findAvailableModel(
|
|
|
810
844
|
return undefined;
|
|
811
845
|
}
|
|
812
846
|
|
|
847
|
+
function toContrarianSelection(
|
|
848
|
+
model: PiModel,
|
|
849
|
+
thinkingLevelOverride: ThinkingLevel | undefined,
|
|
850
|
+
reason: string,
|
|
851
|
+
): ContrarianSelection {
|
|
852
|
+
const thinking = resolveThinkingLevel(model, thinkingLevelOverride);
|
|
853
|
+
return {
|
|
854
|
+
modelRef: `${model.provider}/${model.id}`,
|
|
855
|
+
provider: model.provider,
|
|
856
|
+
modelId: model.id,
|
|
857
|
+
modelName: model.name,
|
|
858
|
+
thinkingLevel: thinking.effective,
|
|
859
|
+
...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
|
|
860
|
+
autoSelected: true,
|
|
861
|
+
selectionReason: appendThinkingLevelClampReason(reason, thinking),
|
|
862
|
+
};
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
function buildSessionFallbackSelection(
|
|
866
|
+
ctx: { model?: PiModel },
|
|
867
|
+
thinkingLevelOverride: ThinkingLevel | undefined,
|
|
868
|
+
): ContrarianSelection | undefined {
|
|
869
|
+
const model = ctx.model;
|
|
870
|
+
if (!model) return undefined;
|
|
871
|
+
return toContrarianSelection(
|
|
872
|
+
model,
|
|
873
|
+
thinkingLevelOverride,
|
|
874
|
+
`Used the current session model ${model.provider}/${model.id} as a final fallback.`,
|
|
875
|
+
);
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function withFallbackReason(selection: ContrarianSelection, previous: ContrarianSelection): ContrarianSelection {
|
|
879
|
+
return {
|
|
880
|
+
...selection,
|
|
881
|
+
selectionReason: `${selection.selectionReason} (Fell back from ${previous.modelRef} after a model-availability error.)`,
|
|
882
|
+
};
|
|
883
|
+
}
|
|
884
|
+
|
|
813
885
|
async function selectContrarianModel(
|
|
814
886
|
ctx: { model?: PiModel; modelRegistry: { getAvailable(): PiModel[] | Promise<PiModel[]> } },
|
|
815
887
|
thinkingLevelOverride?: ThinkingLevel,
|
|
816
|
-
): Promise<{ ok: true; selection: ContrarianSelection } | { ok: false; error: string }> {
|
|
888
|
+
): Promise<{ ok: true; selection: ContrarianSelection; ordered: ContrarianSelection[] } | { ok: false; error: string }> {
|
|
817
889
|
const available = await ctx.modelRegistry.getAvailable();
|
|
818
890
|
if (available.length === 0) {
|
|
819
891
|
return {
|
|
@@ -884,29 +956,25 @@ async function selectContrarianModel(
|
|
|
884
956
|
reason = "No reasoning models were available, so the top-ranked model across all available providers was used.";
|
|
885
957
|
}
|
|
886
958
|
|
|
959
|
+
const sorted = [...candidates].sort((a, b) => rankModel(b) - rankModel(a));
|
|
887
960
|
const preferred = providerForPreferences
|
|
888
961
|
? selectPreferredModel(candidates, providerForPreferences)
|
|
889
962
|
: selectPreferredAcrossProviders(candidates);
|
|
890
|
-
const
|
|
891
|
-
|
|
892
|
-
const
|
|
893
|
-
|
|
894
|
-
|
|
963
|
+
const orderedModels = preferred ? [preferred, ...sorted.filter((model) => model !== preferred)] : sorted;
|
|
964
|
+
|
|
965
|
+
const ordered = orderedModels.map((model, index) =>
|
|
966
|
+
toContrarianSelection(
|
|
967
|
+
model,
|
|
968
|
+
thinkingLevelOverride,
|
|
969
|
+
index === 0
|
|
970
|
+
? preferred
|
|
971
|
+
? `Selected ${model.id} via the hardcoded preference lists while preferring an opposite provider/model family.`
|
|
972
|
+
: reason
|
|
973
|
+
: `Auto-selected ${model.provider}/${model.id} as a lower-ranked fallback.`,
|
|
974
|
+
),
|
|
895
975
|
);
|
|
896
976
|
|
|
897
|
-
return {
|
|
898
|
-
ok: true,
|
|
899
|
-
selection: {
|
|
900
|
-
modelRef: `${winner.provider}/${winner.id}`,
|
|
901
|
-
provider: winner.provider,
|
|
902
|
-
modelId: winner.id,
|
|
903
|
-
modelName: winner.name,
|
|
904
|
-
thinkingLevel: thinking.effective,
|
|
905
|
-
...(thinking.clamped ? { requestedThinkingLevel: thinking.requested, thinkingLevelClamped: true } : {}),
|
|
906
|
-
autoSelected: true,
|
|
907
|
-
selectionReason,
|
|
908
|
-
},
|
|
909
|
-
};
|
|
977
|
+
return { ok: true, selection: ordered[0], ordered };
|
|
910
978
|
}
|
|
911
979
|
|
|
912
980
|
function updateContrarianUi(ctx: ExtensionContext, activeRuns: Map<string, ContrarianUiRun>): void {
|
|
@@ -1137,6 +1205,13 @@ function renderCollapsedText(text: string, lineLimit = COLLAPSED_LINE_LIMIT): st
|
|
|
1137
1205
|
return [...lines.slice(0, lineLimit), `... (${lines.length - lineLimit} more lines)`].join("\n");
|
|
1138
1206
|
}
|
|
1139
1207
|
|
|
1208
|
+
export const __test__ = {
|
|
1209
|
+
findAvailableModel,
|
|
1210
|
+
parseModelPreference,
|
|
1211
|
+
resolveThinkingLevel,
|
|
1212
|
+
selectContrarianModel,
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1140
1215
|
export default function contrarianExtension(pi: ExtensionAPI) {
|
|
1141
1216
|
const activeRuns = new Map<string, ContrarianUiRun>();
|
|
1142
1217
|
let preferences: ContrarianPreferences = {};
|
|
@@ -1300,13 +1375,17 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1300
1375
|
parameters: ContrarianParams,
|
|
1301
1376
|
|
|
1302
1377
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1378
|
+
const rawTask = (params as { task?: unknown }).task;
|
|
1379
|
+
const task = typeof rawTask === "string" ? rawTask.trim() : "";
|
|
1380
|
+
if (!task) throw new Error("Invalid parameters: expected task to be a non-empty string.");
|
|
1381
|
+
|
|
1303
1382
|
const explicitModel = parseModelPreference(params.model);
|
|
1304
1383
|
const defaultModel = parseModelPreference(preferences.model);
|
|
1305
1384
|
const configuredModel = explicitModel.model ?? defaultModel.model;
|
|
1306
1385
|
const configuredThinkingLevel =
|
|
1307
1386
|
params.thinkingLevel ?? explicitModel.thinkingLevel ?? preferences.thinkingLevel ?? defaultModel.thinkingLevel;
|
|
1308
1387
|
const uiRun: ContrarianUiRun = {
|
|
1309
|
-
task
|
|
1388
|
+
task,
|
|
1310
1389
|
includeBash: params.includeBash ?? false,
|
|
1311
1390
|
startedAt: Date.now(),
|
|
1312
1391
|
};
|
|
@@ -1314,7 +1393,16 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1314
1393
|
updateContrarianUi(ctx, activeRuns);
|
|
1315
1394
|
|
|
1316
1395
|
try {
|
|
1317
|
-
|
|
1396
|
+
const attempts: ContrarianSelection[] = [];
|
|
1397
|
+
const seen = new Set<string>();
|
|
1398
|
+
const pushAttempt = (candidate: ContrarianSelection | undefined) => {
|
|
1399
|
+
if (!candidate) return;
|
|
1400
|
+
const key = `${candidate.provider}/${candidate.modelId}`.toLowerCase();
|
|
1401
|
+
if (seen.has(key)) return;
|
|
1402
|
+
seen.add(key);
|
|
1403
|
+
attempts.push(candidate);
|
|
1404
|
+
};
|
|
1405
|
+
|
|
1318
1406
|
if (configuredModel) {
|
|
1319
1407
|
const modelRef = configuredModel;
|
|
1320
1408
|
const matched = await findAvailableModel(ctx, modelRef);
|
|
@@ -1333,7 +1421,7 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1333
1421
|
: usedToolOverride
|
|
1334
1422
|
? "Used the explicit model override provided in the tool call. The model was not matched against the authenticated model list, so the reasoning level fallback was applied."
|
|
1335
1423
|
: "Used the configured default contrarian model. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
|
|
1336
|
-
|
|
1424
|
+
pushAttempt({
|
|
1337
1425
|
modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
|
|
1338
1426
|
provider,
|
|
1339
1427
|
modelId,
|
|
@@ -1344,7 +1432,7 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1344
1432
|
: {}),
|
|
1345
1433
|
autoSelected: false,
|
|
1346
1434
|
selectionReason,
|
|
1347
|
-
};
|
|
1435
|
+
});
|
|
1348
1436
|
} else {
|
|
1349
1437
|
const selectionResult = await selectContrarianModel(ctx, configuredThinkingLevel);
|
|
1350
1438
|
if (!selectionResult.ok) {
|
|
@@ -1367,11 +1455,14 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1367
1455
|
},
|
|
1368
1456
|
};
|
|
1369
1457
|
}
|
|
1370
|
-
|
|
1458
|
+
for (const candidate of selectionResult.ordered) pushAttempt(candidate);
|
|
1371
1459
|
}
|
|
1372
1460
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1461
|
+
// Keep the known-good session model as a final fallback: the catalog can
|
|
1462
|
+
// advertise models the active provider/subscription cannot actually serve
|
|
1463
|
+
// (legacy snapshots, access-gated tiers), which fail with a not-found/404
|
|
1464
|
+
// error. Falling back lets the run degrade gracefully instead of hard-failing.
|
|
1465
|
+
pushAttempt(buildSessionFallbackSelection(ctx, configuredThinkingLevel));
|
|
1375
1466
|
|
|
1376
1467
|
const handleUpdate = (partial: { content: Array<{ type: "text"; text: string }>; details: ContrarianDetails }) => {
|
|
1377
1468
|
uiRun.preview = partial.content[0]?.text ?? uiRun.preview;
|
|
@@ -1379,17 +1470,51 @@ export default function contrarianExtension(pi: ExtensionAPI) {
|
|
|
1379
1470
|
onUpdate?.(partial);
|
|
1380
1471
|
};
|
|
1381
1472
|
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1473
|
+
let lastErrorResult: { ok: false; error: string; details: ContrarianDetails } | undefined;
|
|
1474
|
+
for (let index = 0; index < attempts.length; index++) {
|
|
1475
|
+
const previous = index > 0 ? attempts[index - 1] : undefined;
|
|
1476
|
+
const attempt = previous ? withFallbackReason(attempts[index], previous) : attempts[index];
|
|
1477
|
+
uiRun.selection = attempt;
|
|
1478
|
+
updateContrarianUi(ctx, activeRuns);
|
|
1479
|
+
|
|
1480
|
+
const result = await runContrarian(attempt, { ...params, task }, signal, handleUpdate, ctx.cwd);
|
|
1481
|
+
if (result.ok) {
|
|
1482
|
+
return {
|
|
1483
|
+
content: [{ type: "text", text: result.output }],
|
|
1484
|
+
details: result.details,
|
|
1485
|
+
};
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
lastErrorResult = result;
|
|
1489
|
+
const canFallBack = index < attempts.length - 1 && isModelAvailabilityError(result.error);
|
|
1490
|
+
if (!canFallBack) {
|
|
1491
|
+
return {
|
|
1492
|
+
content: [{ type: "text", text: result.error }],
|
|
1493
|
+
details: result.details,
|
|
1494
|
+
};
|
|
1495
|
+
}
|
|
1388
1496
|
}
|
|
1389
1497
|
|
|
1390
1498
|
return {
|
|
1391
|
-
content: [
|
|
1392
|
-
|
|
1499
|
+
content: [
|
|
1500
|
+
{ type: "text", text: lastErrorResult?.error ?? "Contrarian could not select an available model." },
|
|
1501
|
+
],
|
|
1502
|
+
details:
|
|
1503
|
+
lastErrorResult?.details ?? {
|
|
1504
|
+
modelRef: "",
|
|
1505
|
+
provider: ctx.model?.provider ?? "unknown",
|
|
1506
|
+
modelId: "",
|
|
1507
|
+
modelName: undefined,
|
|
1508
|
+
thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
1509
|
+
autoSelected: true,
|
|
1510
|
+
selectionReason: "No authenticated models were available to run the contrarian.",
|
|
1511
|
+
includeBash: params.includeBash ?? false,
|
|
1512
|
+
usage: createEmptyUsage(),
|
|
1513
|
+
stderr: "",
|
|
1514
|
+
exitCode: 1,
|
|
1515
|
+
durationMs: 0,
|
|
1516
|
+
cwd: params.cwd ?? ctx.cwd,
|
|
1517
|
+
},
|
|
1393
1518
|
};
|
|
1394
1519
|
} finally {
|
|
1395
1520
|
activeRuns.delete(toolCallId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.80.3
|