@diegopetrucci/pi-extensions 0.1.2 → 0.1.5

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 CHANGED
@@ -1,7 +1,5 @@
1
1
  # pi-extensions
2
2
 
3
- ![pi-extensions banner](https://raw.githubusercontent.com/diegopetrucci/pi-extensions/main/assets/social-preview.png)
4
-
5
3
  A collection of [pi](https://github.com/badlogic/pi-mono) agent extensions I made.
6
4
 
7
5
  ## Included extensions
@@ -9,6 +7,7 @@ A collection of [pi](https://github.com/badlogic/pi-mono) agent extensions I mad
9
7
  | Extension | Description |
10
8
  |---|---|
11
9
  | [`minimal-footer`](./extensions/minimal-footer) | Replaces pi's built-in footer with a minimal two-line layout: branch/repo on the first line, context/model on the second. |
10
+ | [`oracle`](./extensions/oracle) | Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, covers pi’s built-in providers with hardcoded rankings, sets reasoning to xhigh by default, and shows live status while running. |
12
11
 
13
12
  ## Install
14
13
 
@@ -23,7 +22,7 @@ pi install git:github.com/diegopetrucci/pi-extensions
23
22
  Or pin to a tagged version:
24
23
 
25
24
  ```bash
26
- pi install git:github.com/diegopetrucci/pi-extensions@v0.1.2
25
+ pi install git:github.com/diegopetrucci/pi-extensions@v0.1.5
27
26
  ```
28
27
 
29
28
  ### npm
@@ -50,9 +49,15 @@ If you only want one extension, you have two options.
50
49
  pi install npm:@diegopetrucci/pi-minimal-footer
51
50
  ```
52
51
 
52
+ ```bash
53
+ pi install npm:@diegopetrucci/pi-oracle
54
+ ```
55
+
53
56
  ### Option 2: filter the repo package
54
57
 
55
- If you prefer the collection package, you can filter it in your pi settings:
58
+ If you prefer the collection package, you can filter it in your pi settings.
59
+
60
+ Minimal footer only:
56
61
 
57
62
  ```json
58
63
  {
@@ -65,6 +70,19 @@ If you prefer the collection package, you can filter it in your pi settings:
65
70
  }
66
71
  ```
67
72
 
73
+ Oracle only:
74
+
75
+ ```json
76
+ {
77
+ "packages": [
78
+ {
79
+ "source": "npm:@diegopetrucci/pi-extensions",
80
+ "extensions": ["extensions/oracle/index.ts"]
81
+ }
82
+ ]
83
+ }
84
+ ```
85
+
68
86
  ## npm publishing
69
87
 
70
88
  The repo is set up to support both:
@@ -76,6 +94,14 @@ The repo is set up to support both:
76
94
 
77
95
  Each extension lives in its own subdirectory under [`extensions/`](./extensions). This keeps the repo easy to grow while still letting each extension carry its own package metadata and documentation.
78
96
 
97
+ ## Oracle docs
98
+
99
+ - [Oracle provider matrix](./docs/oracle-provider-matrix.md)
100
+ - [Release notes for v0.1.5](./docs/release-notes-v0.1.5.md)
101
+ - [GitHub release body for v0.1.5](./docs/github-release-v0.1.5.md)
102
+ - [Publish checklist for v0.1.5](./docs/publish-checklist-v0.1.5.md)
103
+ - [Announcement copy for v0.1.5](./docs/announcement-v0.1.5.md)
104
+
79
105
  ## License
80
106
 
81
107
  [MIT](./LICENSE)
Binary file
@@ -0,0 +1,46 @@
1
+ <svg width="1600" height="900" viewBox="0 0 1600 900" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <rect width="1600" height="900" fill="#071019"/>
3
+ <defs>
4
+ <linearGradient id="bg" x1="160" y1="120" x2="1400" y2="780" gradientUnits="userSpaceOnUse">
5
+ <stop stop-color="#0F1F2F"/>
6
+ <stop offset="1" stop-color="#09131E"/>
7
+ </linearGradient>
8
+ <linearGradient id="accent" x1="312" y1="210" x2="1220" y2="680" gradientUnits="userSpaceOnUse">
9
+ <stop stop-color="#7C4DFF"/>
10
+ <stop offset="0.45" stop-color="#4EA8DE"/>
11
+ <stop offset="1" stop-color="#7AE582"/>
12
+ </linearGradient>
13
+ <filter id="glow" x="0" y="0" width="1600" height="900" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
14
+ <feGaussianBlur stdDeviation="40" result="blur"/>
15
+ <feBlend in="SourceGraphic" in2="blur" mode="screen"/>
16
+ </filter>
17
+ </defs>
18
+
19
+ <circle cx="1310" cy="180" r="210" fill="#7C4DFF" fill-opacity="0.12" filter="url(#glow)"/>
20
+ <circle cx="340" cy="740" r="240" fill="#4EA8DE" fill-opacity="0.1" filter="url(#glow)"/>
21
+
22
+ <rect x="150" y="110" width="1300" height="680" rx="28" fill="url(#bg)" stroke="#203245" stroke-width="2"/>
23
+ <rect x="150" y="110" width="1300" height="74" rx="28" fill="#0C1621"/>
24
+ <circle cx="197" cy="147" r="8" fill="#FF5F57"/>
25
+ <circle cx="225" cy="147" r="8" fill="#FEBC2E"/>
26
+ <circle cx="253" cy="147" r="8" fill="#28C840"/>
27
+ <text x="300" y="154" fill="#8AA1B5" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="24">pi oracle</text>
28
+
29
+ <text x="230" y="278" fill="#E6EDF3" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="30">&gt; use the oracle to review this refactor</text>
30
+
31
+ <rect x="230" y="330" width="1090" height="104" rx="18" fill="#0B1520" stroke="#22384D"/>
32
+ <text x="266" y="374" fill="#7AE582" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="28">oracle</text>
33
+ <text x="390" y="374" fill="#8AA1B5" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="28">read-only · gpt-5.4 · xhigh</text>
34
+ <text x="266" y="416" fill="#A8C1D6" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="24">selected via provider-specific ranking on the current subscription</text>
35
+
36
+ <rect x="230" y="470" width="1090" height="210" rx="20" fill="#08111A" stroke="#22384D"/>
37
+ <text x="266" y="522" fill="#B7C9D8" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="26">Bottom line:</text>
38
+ <text x="266" y="568" fill="#E6EDF3" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="28">The change looks safe. No write-path regressions found.</text>
39
+ <text x="266" y="614" fill="#8AA1B5" font-family="ui-monospace, SFMono-Regular, Menlo, monospace" font-size="24">Checked callsites, branching logic, and notification conditions.</text>
40
+
41
+ <rect x="230" y="718" width="1090" height="28" rx="14" fill="#101B27"/>
42
+ <rect x="230" y="718" width="690" height="28" rx="14" fill="url(#accent)"/>
43
+
44
+ <text x="230" y="835" fill="#E6EDF3" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="72" font-weight="700">Oracle for pi</text>
45
+ <text x="230" y="874" fill="#8AA1B5" font-family="Inter, ui-sans-serif, system-ui, sans-serif" font-size="28">Amp-style high-reasoning read-only subagent</text>
46
+ </svg>
Binary file
@@ -17,14 +17,14 @@ On wide terminals it renders two lines:
17
17
 
18
18
  ```text
19
19
  <git-branch> <repo-name>
20
- <context-%> <model> <thinking>
20
+ <context-%> <model> <thinking>
21
21
  ```
22
22
 
23
23
  Example:
24
24
 
25
25
  ```text
26
26
  fix/remove-detached-image-tasks SendItToMy
27
- 44.1% gpt-5.4 high
27
+ 44.1% gpt-5.4 high
28
28
  ```
29
29
 
30
30
  On narrow terminals it falls back to one item per line.
@@ -19,7 +19,7 @@ export default function (pi: ExtensionAPI) {
19
19
 
20
20
  const model = ctx.model?.id ?? "no-model";
21
21
  const thinking = pi.getThinkingLevel();
22
- const modelText = thinking === "off" ? model : `${model} ${thinking}`;
22
+ const modelText = thinking === "off" ? model : `${model} ${thinking}`;
23
23
 
24
24
  const branchStyled = theme.fg("dim", branch);
25
25
  const repoStyled = theme.fg("dim", repo);
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-minimal-footer",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "A minimal custom footer for pi.",
5
5
  "keywords": ["pi-package", "pi", "terminal", "footer"],
6
6
  "license": "MIT",
@@ -0,0 +1,97 @@
1
+ # oracle
2
+
3
+ ![oracle preview](https://raw.githubusercontent.com/diegopetrucci/pi-extensions/main/assets/oracle-preview.svg)
4
+
5
+ An Amp-style oracle for [pi](https://github.com/badlogic/pi-mono).
6
+
7
+ It adds an `oracle` tool that spins up a separate read-only pi subprocess and sends it to the strongest reasoning model available on the **same provider/subscription** the user is currently using.
8
+
9
+ ## What it does
10
+
11
+ - creates an isolated read-only subprocess
12
+ - auto-picks the strongest reasoning model on the current provider
13
+ - uses provider-specific hardcoded rankings first, then a heuristic fallback
14
+ - sets reasoning/thinking to `xhigh` by default for reasoning models
15
+ - defaults to `read,grep,find,ls`
16
+ - can optionally allow non-mutating `bash` inspection
17
+ - shows a live oracle status line and widget while the subprocess is running
18
+ - renders the oracle response with model, thinking level, timing, and usage info
19
+
20
+ ## Model selection
21
+
22
+ By default, the extension:
23
+
24
+ 1. looks at the current model's provider
25
+ 2. lists authenticated models on that provider
26
+ 3. prefers reasoning-capable models
27
+ 4. tries a provider-specific hardcoded priority list first
28
+ 5. falls back to a heuristic that favors stronger tiers like `opus`, `pro`, newer versions, and penalizes `mini`, `flash`, `haiku`, `spark`, etc.
29
+
30
+ The hardcoded rankings now cover pi's built-in provider set, including OpenAI/Codex, Anthropic, Google variants, GitHub Copilot, Bedrock, Azure OpenAI Responses, Groq, Hugging Face, Kimi, MiniMax, Mistral, OpenCode, OpenRouter, Vercel AI Gateway, xAI, ZAI, and Cerebras.
31
+
32
+ If no reasoning model exists on the current provider, it falls back to the best available model on that provider.
33
+
34
+ ## Reasoning level
35
+
36
+ Yes — the extension explicitly sets the oracle reasoning level.
37
+
38
+ - reasoning models default to `xhigh`
39
+ - non-reasoning models default to `off`
40
+ - you can override it with the tool's optional `thinkingLevel` parameter
41
+
42
+ Use `/oracle-model` inside pi to see what it would pick right now.
43
+
44
+ See also:
45
+ - [Oracle provider matrix](../../docs/oracle-provider-matrix.md)
46
+ - [v0.1.5 release notes](../../docs/release-notes-v0.1.5.md)
47
+
48
+ ## Install
49
+
50
+ ### Standalone npm package
51
+
52
+ ```bash
53
+ pi install npm:@diegopetrucci/pi-oracle
54
+ ```
55
+
56
+ ### Collection package
57
+
58
+ ```bash
59
+ pi install npm:@diegopetrucci/pi-extensions
60
+ ```
61
+
62
+ ### GitHub package
63
+
64
+ ```bash
65
+ pi install git:github.com/diegopetrucci/pi-extensions
66
+ ```
67
+
68
+ Then reload pi:
69
+
70
+ ```text
71
+ /reload
72
+ ```
73
+
74
+ ## Usage
75
+
76
+ Ask pi normally, for example:
77
+
78
+ - `Use the oracle to review the last commit for regressions.`
79
+ - `Use the oracle for a second opinion on this refactor plan.`
80
+ - `Debug this issue and lean on the oracle heavily.`
81
+
82
+ The main agent can call the tool directly.
83
+
84
+ ## Tool parameters
85
+
86
+ - `task` - required prompt for the oracle
87
+ - `includeBash` - optional, adds `bash` for non-mutating inspection
88
+ - `model` - optional explicit model override
89
+ - `thinkingLevel` - optional reasoning/thinking override
90
+ - `cwd` - optional working directory override
91
+
92
+ ## Notes
93
+
94
+ - The oracle is intentionally **read-only by default**.
95
+ - It is best for review, analysis, planning, debugging, and second opinions.
96
+ - It is slower than using the main model directly, so it should be used selectively.
97
+ - While it runs in interactive mode, it adds a footer status line and a widget below the editor.
@@ -0,0 +1,885 @@
1
+ import { spawn } from "node:child_process";
2
+ import { existsSync } from "node:fs";
3
+ import { basename } from "node:path";
4
+ import { StringEnum } from "@mariozechner/pi-ai";
5
+ import { getMarkdownTheme, type ExtensionAPI } from "@mariozechner/pi-coding-agent";
6
+ import { Container, Markdown, Spacer, Text } from "@mariozechner/pi-tui";
7
+ import { Type } from "@sinclair/typebox";
8
+
9
+ type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh";
10
+
11
+ type PiModel = {
12
+ provider: string;
13
+ id: string;
14
+ name?: string;
15
+ reasoning?: boolean;
16
+ contextWindow?: number;
17
+ maxTokens?: number;
18
+ };
19
+
20
+ interface UsageStats {
21
+ input: number;
22
+ output: number;
23
+ cacheRead: number;
24
+ cacheWrite: number;
25
+ cost: number;
26
+ turns: number;
27
+ contextTokens: number;
28
+ }
29
+
30
+ interface OracleSelection {
31
+ modelRef: string;
32
+ provider: string;
33
+ modelId: string;
34
+ modelName?: string;
35
+ thinkingLevel: ThinkingLevel;
36
+ autoSelected: boolean;
37
+ selectionReason: string;
38
+ }
39
+
40
+ interface OracleDetails extends OracleSelection {
41
+ includeBash: boolean;
42
+ usage: UsageStats;
43
+ stderr: string;
44
+ exitCode: number;
45
+ durationMs: number;
46
+ cwd: string;
47
+ }
48
+
49
+ interface OracleUiRun {
50
+ task: string;
51
+ includeBash: boolean;
52
+ startedAt: number;
53
+ selection?: OracleSelection;
54
+ preview?: string;
55
+ }
56
+
57
+ const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
58
+ const READ_ONLY_PLUS_BASH_TOOLS = [...READ_ONLY_TOOLS, "bash"];
59
+ const DEFAULT_THINKING_LEVEL: ThinkingLevel = "xhigh";
60
+ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as const;
61
+ const COLLAPSED_LINE_LIMIT = 8;
62
+ const ORACLE_STATUS_ID = "oracle";
63
+ const ORACLE_WIDGET_ID = "oracle";
64
+
65
+ const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
66
+ "amazon-bedrock": [
67
+ "claude-opus-4-7",
68
+ "claude-opus-4-6",
69
+ "claude-opus-4-5",
70
+ "claude-opus-4-1",
71
+ "claude-opus-4",
72
+ "claude-sonnet-4-6",
73
+ "claude-sonnet-4-5",
74
+ "claude-sonnet-4",
75
+ "deepseek.r1",
76
+ "kimi-k2.5",
77
+ "minimax-m2.1",
78
+ ],
79
+ anthropic: [
80
+ "claude-opus-4-6",
81
+ "claude-opus-4.6",
82
+ "claude-opus-4-5",
83
+ "claude-opus-4.5",
84
+ "claude-opus-4-1",
85
+ "claude-opus-4.1",
86
+ "claude-opus-4-0",
87
+ "claude-opus-4",
88
+ "claude-sonnet-4-6",
89
+ "claude-sonnet-4.6",
90
+ "claude-sonnet-4-5",
91
+ "claude-sonnet-4.5",
92
+ "claude-sonnet-4-0",
93
+ "claude-sonnet-4",
94
+ "claude-3-7-sonnet",
95
+ ],
96
+ "azure-openai-responses": [
97
+ "gpt-5.4-pro",
98
+ "gpt-5-pro",
99
+ "gpt-5.2",
100
+ "gpt-5.2-codex",
101
+ "gpt-5.1-codex-max",
102
+ "gpt-5.1-chat-latest",
103
+ "o3-pro",
104
+ "o3-deep-research",
105
+ "o1-pro",
106
+ "gpt-5.4-mini",
107
+ "gpt-5-mini",
108
+ ],
109
+ cerebras: ["zai-glm-4.7", "llama3.1-8b"],
110
+ "github-copilot": [
111
+ "claude-opus-4.7",
112
+ "claude-opus-4.5",
113
+ "gpt-5.2",
114
+ "gpt-5.1-codex-max",
115
+ "gpt-5.1",
116
+ "gpt-5",
117
+ "gpt-5.3-codex",
118
+ "gemini-3-pro-preview",
119
+ "claude-sonnet-4.5",
120
+ "gemini-2.5-pro",
121
+ ],
122
+ google: [
123
+ "gemini-3.1-pro-preview",
124
+ "gemini-3-pro-preview",
125
+ "gemini-2.5-pro-preview",
126
+ "gemini-2.5-pro",
127
+ "gemini-2.5-flash-preview",
128
+ "gemini-2.5-flash-lite-preview",
129
+ "gemini-2.5-flash-lite",
130
+ ],
131
+ "google-antigravity": [
132
+ "claude-opus-4-6-thinking",
133
+ "claude-sonnet-4-5-thinking",
134
+ "gemini-3.1-pro-low",
135
+ "gemini-3-flash",
136
+ "gemini-2.0-flash",
137
+ ],
138
+ "google-gemini-cli": ["gemini-3-pro-preview", "gemini-2.5-pro", "gemini-1.5-flash"],
139
+ "google-vertex": [
140
+ "gemini-3.1-pro-preview-customtools",
141
+ "gemini-3-pro-preview",
142
+ "gemini-2.5-pro",
143
+ "gemini-2.5-flash-lite",
144
+ "gemini-2.0-flash-lite",
145
+ ],
146
+ groq: [
147
+ "openai/gpt-oss-120b",
148
+ "groq/compound-mini",
149
+ "qwen/qwen3-32b",
150
+ "moonshotai/kimi-k2-instruct",
151
+ "meta-llama/llama-4-scout",
152
+ ],
153
+ huggingface: [
154
+ "zai-org/GLM-5.1",
155
+ "Qwen/Qwen3-235B-A22B-Thinking-2507",
156
+ "moonshotai/Kimi-K2.5",
157
+ "deepseek-ai/DeepSeek-V3.2",
158
+ "MiniMaxAI/MiniMax-M2.5",
159
+ "Qwen/Qwen3-Coder-Next",
160
+ ],
161
+ "kimi-coding": ["kimi-k2-thinking"],
162
+ minimax: ["MiniMax-M2.7-highspeed"],
163
+ "minimax-cn": ["MiniMax-M2.7-highspeed"],
164
+ mistral: [
165
+ "magistral-medium-latest",
166
+ "devstral-medium-latest",
167
+ "mistral-large-latest",
168
+ "mistral-large-2411",
169
+ "mistral-medium-2508",
170
+ "mistral-small-2603",
171
+ "devstral-2512",
172
+ ],
173
+ openai: [
174
+ "gpt-5.4-pro",
175
+ "gpt-5-pro",
176
+ "gpt-5.2",
177
+ "gpt-5.2-codex",
178
+ "gpt-5.1-codex-max",
179
+ "gpt-5.1-chat-latest",
180
+ "o3-pro",
181
+ "o3-deep-research",
182
+ "o1-pro",
183
+ "gpt-5.4-mini",
184
+ "gpt-5-mini",
185
+ ],
186
+ "openai-codex": [
187
+ "gpt-5.4",
188
+ "gpt-5.3-codex",
189
+ "gpt-5.2",
190
+ "gpt-5.1-codex-max",
191
+ "gpt-5.4-mini",
192
+ "gpt-5.1-codex-mini",
193
+ "big-pickle",
194
+ ],
195
+ opencode: [
196
+ "gpt-5.4",
197
+ "claude-opus-4-7",
198
+ "claude-opus-4-5",
199
+ "gpt-5.2-codex",
200
+ "gpt-5.1-codex",
201
+ "glm-5",
202
+ "kimi-k2.5",
203
+ "qwen3.5-plus",
204
+ "minimax-m2.5-free",
205
+ ],
206
+ "opencode-go": ["qwen3.6-plus", "minimax-m2.7", "mimo-v2-pro", "kimi-k2.5"],
207
+ openrouter: [
208
+ "anthropic/claude-opus-4.6-fast",
209
+ "anthropic/claude-opus-4.5",
210
+ "anthropic/claude-opus-4",
211
+ "google/gemini-3.1-pro-preview-customtools",
212
+ "google/gemini-2.5-pro",
213
+ "moonshotai/kimi-k2-thinking",
214
+ "deepseek/deepseek-r1",
215
+ "deepseek/deepseek-v3.2",
216
+ "minimax/minimax-m2.1",
217
+ ],
218
+ "vercel-ai-gateway": [
219
+ "anthropic/claude-opus-4.6",
220
+ "anthropic/claude-opus-4.1",
221
+ "anthropic/claude-sonnet-4.6",
222
+ "openai/gpt-5.1-codex",
223
+ "openai/gpt-5-codex",
224
+ "moonshotai/kimi-k2-thinking",
225
+ "deepseek/deepseek-v3.2-thinking",
226
+ "alibaba/qwen3-max-thinking",
227
+ "google/gemini-3-flash",
228
+ ],
229
+ xai: ["grok-4-1-fast", "grok-4-fast", "grok-3-mini-latest", "grok-3-mini-fast", "grok-3-latest"],
230
+ zai: ["glm-5.1", "glm-5", "glm-4.7-flash", "glm-4.6v", "glm-4.5v", "glm-4.5-air"],
231
+ "gemini-cli": ["gemini-3-pro-preview", "gemini-2.5-pro", "gemini-1.5-flash"],
232
+ };
233
+
234
+ const ORACLE_SYSTEM_PROMPT = [
235
+ "You are Oracle, a read-only high-reasoning coding assistant.",
236
+ "Your job is analysis, debugging, planning, review, and second opinions.",
237
+ "Use the available tools to inspect the repository and gather evidence.",
238
+ "Never claim to have changed files or run mutating actions.",
239
+ "If bash is available, use it only for non-mutating inspection commands.",
240
+ "Be concrete. Reference file paths, symbols, commands, and risks when helpful.",
241
+ "Prefer short sections and finish with a concise 'Bottom line' summary.",
242
+ ].join("\n");
243
+
244
+ const OracleParams = Type.Object({
245
+ task: Type.String({
246
+ description: "Question or task for the oracle. Include enough context for a stand-alone review or analysis.",
247
+ }),
248
+ includeBash: Type.Optional(
249
+ Type.Boolean({
250
+ description: "Allow non-mutating bash inspection in addition to read/grep/find/ls. Default: false.",
251
+ default: false,
252
+ }),
253
+ ),
254
+ model: Type.Optional(
255
+ Type.String({
256
+ description:
257
+ "Optional exact model or model pattern override. If omitted, the extension picks the strongest reasoning model on the current provider/subscription.",
258
+ }),
259
+ ),
260
+ thinkingLevel: Type.Optional(
261
+ StringEnum(THINKING_LEVELS, {
262
+ description:
263
+ "Optional reasoning level override for the oracle subprocess. Default: xhigh for reasoning models, off for non-reasoning models.",
264
+ }),
265
+ ),
266
+ cwd: Type.Optional(Type.String({ description: "Optional working directory for the oracle subprocess." })),
267
+ });
268
+
269
+ function createEmptyUsage(): UsageStats {
270
+ return {
271
+ input: 0,
272
+ output: 0,
273
+ cacheRead: 0,
274
+ cacheWrite: 0,
275
+ cost: 0,
276
+ turns: 0,
277
+ contextTokens: 0,
278
+ };
279
+ }
280
+
281
+ function formatTokens(count: number): string {
282
+ if (count < 1000) return count.toString();
283
+ if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
284
+ if (count < 1000000) return `${Math.round(count / 1000)}k`;
285
+ return `${(count / 1000000).toFixed(1)}M`;
286
+ }
287
+
288
+ function formatDuration(durationMs: number): string {
289
+ if (durationMs < 1000) return `${durationMs}ms`;
290
+ if (durationMs < 60_000) return `${(durationMs / 1000).toFixed(1)}s`;
291
+ return `${(durationMs / 60_000).toFixed(1)}m`;
292
+ }
293
+
294
+ function formatUsage(stats: UsageStats): string {
295
+ const parts: string[] = [];
296
+ if (stats.turns) parts.push(`${stats.turns} turn${stats.turns === 1 ? "" : "s"}`);
297
+ if (stats.input) parts.push(`↑${formatTokens(stats.input)}`);
298
+ if (stats.output) parts.push(`↓${formatTokens(stats.output)}`);
299
+ if (stats.cacheRead) parts.push(`R${formatTokens(stats.cacheRead)}`);
300
+ if (stats.cacheWrite) parts.push(`W${formatTokens(stats.cacheWrite)}`);
301
+ if (stats.cost) parts.push(`$${stats.cost.toFixed(4)}`);
302
+ if (stats.contextTokens) parts.push(`ctx:${formatTokens(stats.contextTokens)}`);
303
+ return parts.join(" ");
304
+ }
305
+
306
+ function extractTextFromContent(content: unknown): string {
307
+ if (!Array.isArray(content)) return "";
308
+ const parts: string[] = [];
309
+ for (const part of content) {
310
+ if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
311
+ const text = (part as { text?: string }).text;
312
+ if (typeof text === "string" && text.trim()) parts.push(text);
313
+ }
314
+ }
315
+ return parts.join("\n\n").trim();
316
+ }
317
+
318
+ function parseVersionScore(text: string): number {
319
+ const matches = text.match(/\d+(?:\.\d+){0,2}/g) ?? [];
320
+ let best = 0;
321
+ for (const match of matches) {
322
+ const [major = "0", minor = "0", patch = "0"] = match.split(".");
323
+ const score = Number(major) * 1_000_000 + Number(minor) * 1_000 + Number(patch);
324
+ if (score > best) best = score;
325
+ }
326
+ return best;
327
+ }
328
+
329
+ function rankModel(model: PiModel): number {
330
+ const text = `${model.id} ${model.name ?? ""}`.toLowerCase();
331
+ const has = (regex: RegExp): boolean => regex.test(text);
332
+ let score = 0;
333
+
334
+ if (model.reasoning) score += 10_000_000;
335
+ score += parseVersionScore(text) * 1_000;
336
+ score += Math.min(model.maxTokens ?? 0, 200_000);
337
+ score += Math.floor(Math.min(model.contextWindow ?? 0, 1_000_000) / 100);
338
+
339
+ if (has(/\bopus\b/)) score += 350_000;
340
+ if (has(/\bpro\b/)) score += 180_000;
341
+ if (has(/\bmax\b/)) score += 60_000;
342
+ if (has(/\bultra\b/)) score += 150_000;
343
+ if (has(/\bsonnet\b/)) score += 120_000;
344
+ if (has(/\bcodex\b|\bcoder\b|\bcode\b/)) score += 40_000;
345
+ if (has(/\breasoning\b|\bthink(?:ing)?\b/)) score += 60_000;
346
+
347
+ if (has(/\bhaiku\b/)) score -= 420_000;
348
+ if (has(/\bmini\b/)) score -= 520_000;
349
+ if (has(/\bnano\b/)) score -= 700_000;
350
+ if (has(/\bflash\b/)) score -= 520_000;
351
+ if (has(/\bspark\b/)) score -= 650_000;
352
+ if (has(/\blite\b|\bsmall\b|\bfast\b|\binstant\b/)) score -= 300_000;
353
+
354
+ return score;
355
+ }
356
+
357
+ function shorten(text: string, max: number): string {
358
+ const singleLine = text.replace(/\s+/g, " ").trim();
359
+ if (singleLine.length <= max) return singleLine;
360
+ return `${singleLine.slice(0, Math.max(1, max - 3))}...`;
361
+ }
362
+
363
+ function getProviderPreferenceList(provider: string | undefined): string[] | undefined {
364
+ if (!provider) return undefined;
365
+ return PROVIDER_MODEL_PREFERENCES[provider.toLowerCase()];
366
+ }
367
+
368
+ function selectPreferredModel(models: PiModel[], provider: string | undefined): PiModel | undefined {
369
+ const preferences = getProviderPreferenceList(provider);
370
+ if (!preferences || preferences.length === 0) return undefined;
371
+ const lowered = models.map((model) => ({ model, haystack: `${model.id} ${model.name ?? ""}`.toLowerCase() }));
372
+ for (const pattern of preferences) {
373
+ const match = lowered.find((entry) => entry.haystack.includes(pattern.toLowerCase()));
374
+ if (match) return match.model;
375
+ }
376
+ return undefined;
377
+ }
378
+
379
+ function getPiInvocation(args: string[]): { command: string; args: string[] } {
380
+ const currentScript = process.argv[1];
381
+ const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/");
382
+ if (currentScript && !isBunVirtualScript && existsSync(currentScript)) {
383
+ return { command: process.execPath, args: [currentScript, ...args] };
384
+ }
385
+
386
+ const execName = basename(process.execPath).toLowerCase();
387
+ const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
388
+ if (!isGenericRuntime) {
389
+ return { command: process.execPath, args };
390
+ }
391
+
392
+ return { command: "pi", args };
393
+ }
394
+
395
+ function withThinking(modelRef: string, thinkingLevel: ThinkingLevel): string {
396
+ if (/(?:^|\/)[^:]+:(off|minimal|low|medium|high|xhigh)$/i.test(modelRef)) return modelRef;
397
+ return `${modelRef}:${thinkingLevel}`;
398
+ }
399
+
400
+ function resolveThinkingLevel(model: PiModel | undefined, override: ThinkingLevel | undefined): ThinkingLevel {
401
+ if (override) return override;
402
+ return model?.reasoning ? DEFAULT_THINKING_LEVEL : "off";
403
+ }
404
+
405
+ async function findAvailableModel(
406
+ ctx: { model?: PiModel; modelRegistry: { getAvailable(): Promise<PiModel[]> } },
407
+ modelRef: string,
408
+ ): Promise<PiModel | undefined> {
409
+ const available = await ctx.modelRegistry.getAvailable();
410
+ const trimmed = modelRef.trim();
411
+ const provider = trimmed.includes("/") ? trimmed.split("/")[0].toLowerCase() : ctx.model?.provider?.toLowerCase();
412
+ const id = trimmed.includes("/") ? trimmed.split("/").slice(1).join("/").toLowerCase() : trimmed.toLowerCase();
413
+
414
+ const exact = available.find(
415
+ (model) => model.id.toLowerCase() === id && (!provider || model.provider.toLowerCase() === provider),
416
+ );
417
+ if (exact) return exact;
418
+
419
+ const partial = available.find(
420
+ (model) =>
421
+ model.id.toLowerCase().includes(id) && (!provider || model.provider.toLowerCase() === provider),
422
+ );
423
+ if (partial) return partial;
424
+
425
+ if (!provider) {
426
+ const uniqueById = available.filter((model) => model.id.toLowerCase() === id);
427
+ if (uniqueById.length === 1) return uniqueById[0];
428
+ }
429
+
430
+ return undefined;
431
+ }
432
+
433
+ async function selectOracleModel(
434
+ ctx: { model?: PiModel; modelRegistry: { getAvailable(): Promise<PiModel[]> } },
435
+ thinkingLevelOverride?: ThinkingLevel,
436
+ ): Promise<{ ok: true; selection: OracleSelection } | { ok: false; error: string }> {
437
+ const available = await ctx.modelRegistry.getAvailable();
438
+ if (available.length === 0) {
439
+ return {
440
+ ok: false,
441
+ error: "No authenticated models are available. Log in or configure an API key first.",
442
+ };
443
+ }
444
+
445
+ const currentProvider = ctx.model?.provider;
446
+ const sameProvider = currentProvider ? available.filter((model) => model.provider === currentProvider) : [];
447
+ const sameProviderReasoning = sameProvider.filter((model) => model.reasoning);
448
+ const allReasoning = available.filter((model) => model.reasoning);
449
+
450
+ let candidates = sameProviderReasoning;
451
+ let reason = "Selected the top-ranked reasoning model on the current provider.";
452
+ let providerForPreferences = currentProvider;
453
+
454
+ if (candidates.length === 0 && sameProvider.length > 0) {
455
+ candidates = sameProvider;
456
+ reason = "The current provider has no reasoning models available, so the top-ranked model on that provider was used.";
457
+ } else if (candidates.length === 0 && allReasoning.length > 0) {
458
+ candidates = allReasoning;
459
+ providerForPreferences = undefined;
460
+ reason = "No current model/provider was active, so the top-ranked reasoning model across all available providers was used.";
461
+ } else if (candidates.length === 0) {
462
+ candidates = available;
463
+ providerForPreferences = undefined;
464
+ reason = "No reasoning models were available, so the top-ranked model across all available providers was used.";
465
+ }
466
+
467
+ const preferred = selectPreferredModel(candidates, providerForPreferences);
468
+ const winner = preferred ?? [...candidates].sort((a, b) => rankModel(b) - rankModel(a))[0];
469
+ const selectionReason = preferred
470
+ ? `Selected ${winner.id} via the hardcoded preference list for ${winner.provider}.`
471
+ : reason;
472
+
473
+ return {
474
+ ok: true,
475
+ selection: {
476
+ modelRef: `${winner.provider}/${winner.id}`,
477
+ provider: winner.provider,
478
+ modelId: winner.id,
479
+ modelName: winner.name,
480
+ thinkingLevel: resolveThinkingLevel(winner, thinkingLevelOverride),
481
+ autoSelected: true,
482
+ selectionReason,
483
+ },
484
+ };
485
+ }
486
+
487
+ function updateOracleUi(ctx: Parameters<ExtensionAPI["registerCommand"]>[1]["handler"] extends (
488
+ args: any,
489
+ ctx: infer T,
490
+ ) => any
491
+ ? T
492
+ : never,
493
+ activeRuns: Map<string, OracleUiRun>,
494
+ ): void {
495
+ if (!ctx.hasUI) return;
496
+ const theme = ctx.ui.theme;
497
+ if (activeRuns.size === 0) {
498
+ ctx.ui.setStatus(ORACLE_STATUS_ID, undefined);
499
+ ctx.ui.setWidget(ORACLE_WIDGET_ID, undefined);
500
+ return;
501
+ }
502
+
503
+ const runs = [...activeRuns.values()].sort((a, b) => b.startedAt - a.startedAt);
504
+ const primary = runs[0];
505
+ const activeCount = runs.length;
506
+ const modelText = primary.selection?.modelId ?? "selecting…";
507
+ const thinkingText = primary.selection?.thinkingLevel && primary.selection.thinkingLevel !== "off"
508
+ ? ` ${primary.selection.thinkingLevel}`
509
+ : "";
510
+ const elapsed = formatDuration(Date.now() - primary.startedAt);
511
+ const mode = primary.includeBash ? "read-only+bash" : "read-only";
512
+ const status =
513
+ theme.fg("accent", "🔮 oracle") +
514
+ theme.fg("dim", ` ${modelText}${thinkingText} · ${mode} · ${elapsed}`) +
515
+ (activeCount > 1 ? theme.fg("warning", ` · ${activeCount} active`) : "");
516
+ ctx.ui.setStatus(ORACLE_STATUS_ID, status);
517
+
518
+ const lines = [
519
+ `🔮 Oracle ${activeCount > 1 ? `(${activeCount} active)` : ""}`.trim(),
520
+ `${primary.selection?.modelRef ?? "selecting model…"} · ${mode} · ${elapsed}`,
521
+ `task: ${shorten(primary.task, 110)}`,
522
+ ];
523
+ if (primary.preview && primary.preview.trim()) lines.push(`preview: ${shorten(primary.preview, 110)}`);
524
+ ctx.ui.setWidget(ORACLE_WIDGET_ID, lines, { placement: "belowEditor" });
525
+ }
526
+
527
+ async function runOracle(
528
+ selection: OracleSelection,
529
+ params: { task: string; includeBash?: boolean; cwd?: string },
530
+ signal: AbortSignal | undefined,
531
+ onUpdate: ((result: { content: Array<{ type: "text"; text: string }>; details: OracleDetails }) => void) | undefined,
532
+ defaultCwd: string,
533
+ ): Promise<{ ok: true; output: string; details: OracleDetails } | { ok: false; error: string; details: OracleDetails }> {
534
+ const cwd = params.cwd ?? defaultCwd;
535
+ const includeBash = params.includeBash ?? false;
536
+ const tools = includeBash ? READ_ONLY_PLUS_BASH_TOOLS : READ_ONLY_TOOLS;
537
+ const startedAt = Date.now();
538
+ const usage = createEmptyUsage();
539
+ let currentText = "";
540
+ let finalOutput = "";
541
+ let stderr = "";
542
+
543
+ const details: OracleDetails = {
544
+ ...selection,
545
+ includeBash,
546
+ usage,
547
+ stderr,
548
+ exitCode: 0,
549
+ durationMs: 0,
550
+ cwd,
551
+ };
552
+
553
+ const emit = () => {
554
+ details.stderr = stderr;
555
+ details.durationMs = Date.now() - startedAt;
556
+ onUpdate?.({
557
+ content: [{ type: "text", text: currentText || finalOutput || "Consulting oracle..." }],
558
+ details,
559
+ });
560
+ };
561
+
562
+ const modelArg = withThinking(selection.modelRef, selection.thinkingLevel);
563
+ const args = [
564
+ "--mode",
565
+ "json",
566
+ "-p",
567
+ "--no-session",
568
+ "--model",
569
+ modelArg,
570
+ "--tools",
571
+ tools.join(","),
572
+ "--append-system-prompt",
573
+ ORACLE_SYSTEM_PROMPT,
574
+ params.task,
575
+ ];
576
+
577
+ const invocation = getPiInvocation(args);
578
+ let wasAborted = false;
579
+
580
+ const exitCode = await new Promise<number>((resolve) => {
581
+ const proc = spawn(invocation.command, invocation.args, {
582
+ cwd,
583
+ shell: false,
584
+ stdio: ["ignore", "pipe", "pipe"],
585
+ });
586
+ let buffer = "";
587
+
588
+ const processLine = (line: string) => {
589
+ if (!line.trim()) return;
590
+ let event: any;
591
+ try {
592
+ event = JSON.parse(line);
593
+ } catch {
594
+ return;
595
+ }
596
+
597
+ if (event.type === "message_start" && event.message?.role === "assistant") {
598
+ currentText = "";
599
+ emit();
600
+ return;
601
+ }
602
+
603
+ if (event.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
604
+ currentText += event.assistantMessageEvent.delta ?? "";
605
+ emit();
606
+ return;
607
+ }
608
+
609
+ if (event.type === "message_end" && event.message?.role === "assistant") {
610
+ const text = extractTextFromContent(event.message.content) || currentText;
611
+ if (text) finalOutput = text;
612
+ currentText = "";
613
+
614
+ const messageUsage = event.message.usage;
615
+ if (messageUsage) {
616
+ usage.turns += 1;
617
+ usage.input += messageUsage.input || 0;
618
+ usage.output += messageUsage.output || 0;
619
+ usage.cacheRead += messageUsage.cacheRead || 0;
620
+ usage.cacheWrite += messageUsage.cacheWrite || 0;
621
+ usage.cost += messageUsage.cost?.total || 0;
622
+ usage.contextTokens = messageUsage.totalTokens || usage.contextTokens;
623
+ }
624
+
625
+ emit();
626
+ }
627
+ };
628
+
629
+ proc.stdout.on("data", (data) => {
630
+ buffer += data.toString();
631
+ const lines = buffer.split("\n");
632
+ buffer = lines.pop() || "";
633
+ for (const line of lines) processLine(line);
634
+ });
635
+
636
+ proc.stderr.on("data", (data) => {
637
+ stderr += data.toString();
638
+ emit();
639
+ });
640
+
641
+ proc.on("close", (code) => {
642
+ if (buffer.trim()) processLine(buffer);
643
+ resolve(code ?? 0);
644
+ });
645
+
646
+ proc.on("error", (error) => {
647
+ stderr += `${error instanceof Error ? error.message : String(error)}\n`;
648
+ resolve(1);
649
+ });
650
+
651
+ if (signal) {
652
+ const killProc = () => {
653
+ wasAborted = true;
654
+ proc.kill("SIGTERM");
655
+ setTimeout(() => {
656
+ if (!proc.killed) proc.kill("SIGKILL");
657
+ }, 5000);
658
+ };
659
+ if (signal.aborted) killProc();
660
+ else signal.addEventListener("abort", killProc, { once: true });
661
+ }
662
+ });
663
+
664
+ details.stderr = stderr.trim();
665
+ details.exitCode = exitCode;
666
+ details.durationMs = Date.now() - startedAt;
667
+
668
+ if (wasAborted) {
669
+ return { ok: false, error: "Oracle was aborted.", details };
670
+ }
671
+
672
+ if (exitCode !== 0) {
673
+ return {
674
+ ok: false,
675
+ error: details.stderr || finalOutput || "Oracle subprocess failed.",
676
+ details,
677
+ };
678
+ }
679
+
680
+ if (!finalOutput.trim()) {
681
+ return {
682
+ ok: false,
683
+ error: details.stderr || "Oracle finished without returning any text.",
684
+ details,
685
+ };
686
+ }
687
+
688
+ return {
689
+ ok: true,
690
+ output: finalOutput.trim(),
691
+ details,
692
+ };
693
+ }
694
+
695
+ function renderCollapsedText(text: string, lineLimit = COLLAPSED_LINE_LIMIT): string {
696
+ const lines = text.trim().split("\n");
697
+ if (lines.length <= lineLimit) return lines.join("\n");
698
+ return [...lines.slice(0, lineLimit), `... (${lines.length - lineLimit} more lines)`].join("\n");
699
+ }
700
+
701
+ export default function oracleExtension(pi: ExtensionAPI) {
702
+ const activeRuns = new Map<string, OracleUiRun>();
703
+
704
+ pi.on("session_start", async (_event, ctx) => {
705
+ activeRuns.clear();
706
+ updateOracleUi(ctx, activeRuns);
707
+ });
708
+
709
+ pi.registerCommand("oracle-model", {
710
+ description: "Show which model the oracle would use right now",
711
+ handler: async (_args, ctx) => {
712
+ const selectionResult = await selectOracleModel(ctx);
713
+ if (!selectionResult.ok) {
714
+ if (ctx.hasUI) ctx.ui.notify(selectionResult.error, "error");
715
+ else console.log(selectionResult.error);
716
+ return;
717
+ }
718
+
719
+ const { selection } = selectionResult;
720
+ const message = `Oracle: ${selection.modelRef} (${selection.thinkingLevel}) — ${selection.selectionReason}`;
721
+ if (ctx.hasUI) ctx.ui.notify(message, "info");
722
+ else console.log(message);
723
+ },
724
+ });
725
+
726
+ pi.registerTool({
727
+ name: "oracle",
728
+ label: "Oracle",
729
+ description:
730
+ "Consult a separate read-only oracle subprocess for deep analysis, code review, debugging, planning, and second opinions.",
731
+ promptSnippet:
732
+ "Consult a read-only oracle that auto-selects the strongest reasoning model on the current provider/subscription.",
733
+ promptGuidelines: [
734
+ "Use this tool sparingly when you want a second opinion, deeper analysis, code review, debugging help, or a higher-reasoning pass.",
735
+ "Do not use it for routine low-value work; it is slower than the main agent.",
736
+ "The oracle is read-only by default. Set includeBash only when shell-based inspection is genuinely useful.",
737
+ "The oracle sets thinking to xhigh by default for reasoning models, unless the tool call explicitly overrides thinkingLevel.",
738
+ ],
739
+ parameters: OracleParams,
740
+
741
+ async execute(toolCallId, params, signal, onUpdate, ctx) {
742
+ const uiRun: OracleUiRun = {
743
+ task: params.task,
744
+ includeBash: params.includeBash ?? false,
745
+ startedAt: Date.now(),
746
+ };
747
+ activeRuns.set(toolCallId, uiRun);
748
+ updateOracleUi(ctx, activeRuns);
749
+
750
+ try {
751
+ let selection: OracleSelection;
752
+ if (params.model?.trim()) {
753
+ const modelRef = params.model.trim();
754
+ const matched = await findAvailableModel(ctx, modelRef);
755
+ const provider =
756
+ matched?.provider ?? (modelRef.includes("/") ? modelRef.split("/")[0] : ctx.model?.provider ?? "unknown");
757
+ const modelId = matched?.id ?? (modelRef.includes("/") ? modelRef.split("/").slice(1).join("/") : modelRef);
758
+ selection = {
759
+ modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
760
+ provider,
761
+ modelId,
762
+ modelName: matched?.name,
763
+ thinkingLevel: resolveThinkingLevel(matched, params.thinkingLevel),
764
+ autoSelected: false,
765
+ selectionReason: matched
766
+ ? "Used the explicit model override provided in the tool call."
767
+ : "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.",
768
+ };
769
+ } else {
770
+ const selectionResult = await selectOracleModel(ctx, params.thinkingLevel);
771
+ if (!selectionResult.ok) {
772
+ return {
773
+ content: [{ type: "text", text: selectionResult.error }],
774
+ details: {
775
+ modelRef: "",
776
+ provider: ctx.model?.provider ?? "unknown",
777
+ modelId: "",
778
+ modelName: undefined,
779
+ thinkingLevel: params.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
780
+ autoSelected: true,
781
+ selectionReason: selectionResult.error,
782
+ includeBash: params.includeBash ?? false,
783
+ usage: createEmptyUsage(),
784
+ stderr: "",
785
+ exitCode: 1,
786
+ durationMs: 0,
787
+ cwd: params.cwd ?? ctx.cwd,
788
+ },
789
+ isError: true,
790
+ };
791
+ }
792
+ selection = selectionResult.selection;
793
+ }
794
+
795
+ uiRun.selection = selection;
796
+ updateOracleUi(ctx, activeRuns);
797
+
798
+ const handleUpdate = (partial: { content: Array<{ type: "text"; text: string }>; details: OracleDetails }) => {
799
+ uiRun.preview = partial.content[0]?.text ?? uiRun.preview;
800
+ updateOracleUi(ctx, activeRuns);
801
+ onUpdate?.(partial);
802
+ };
803
+
804
+ const result = await runOracle(selection, params, signal, handleUpdate, ctx.cwd);
805
+ if (!result.ok) {
806
+ return {
807
+ content: [{ type: "text", text: result.error }],
808
+ details: result.details,
809
+ isError: true,
810
+ };
811
+ }
812
+
813
+ return {
814
+ content: [{ type: "text", text: result.output }],
815
+ details: result.details,
816
+ };
817
+ } finally {
818
+ activeRuns.delete(toolCallId);
819
+ updateOracleUi(ctx, activeRuns);
820
+ }
821
+ },
822
+
823
+ renderCall(args, theme) {
824
+ const task = args.task.length > 90 ? `${args.task.slice(0, 90)}...` : args.task;
825
+ const mode = args.includeBash ? "read-only+bash" : "read-only";
826
+ const override = args.model ? ` ${theme.fg("muted", `[${args.model}]`)}` : "";
827
+ const thinking = args.thinkingLevel ? ` ${theme.fg("warning", `(${args.thinkingLevel})`)}` : "";
828
+ return new Text(
829
+ `${theme.fg("toolTitle", theme.bold("oracle "))}${theme.fg("accent", mode)}${override}${thinking}\n ${theme.fg("dim", task)}`,
830
+ 0,
831
+ 0,
832
+ );
833
+ },
834
+
835
+ renderResult(result, { expanded }, theme) {
836
+ const details = result.details as OracleDetails | undefined;
837
+ const body = result.content[0]?.type === "text" ? result.content[0].text : "(no output)";
838
+ if (!details) return new Text(body, 0, 0);
839
+
840
+ const icon = result.isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
841
+ const header = `${icon} ${theme.fg("toolTitle", theme.bold("oracle "))}${theme.fg("accent", details.modelRef || "(auto)")}`;
842
+ const subheader = [
843
+ details.thinkingLevel !== "off" ? details.thinkingLevel : undefined,
844
+ details.includeBash ? "read-only+bash" : "read-only",
845
+ formatDuration(details.durationMs),
846
+ ]
847
+ .filter(Boolean)
848
+ .join(" · ");
849
+ const usage = formatUsage(details.usage);
850
+
851
+ if (!expanded) {
852
+ let text = header;
853
+ if (subheader) text += `\n${theme.fg("dim", subheader)}`;
854
+ text += `\n\n${theme.fg("toolOutput", renderCollapsedText(body))}`;
855
+ if (usage) text += `\n\n${theme.fg("dim", usage)}`;
856
+ if (result.isError && details.stderr) text += `\n${theme.fg("error", renderCollapsedText(details.stderr, 4))}`;
857
+ text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
858
+ return new Text(text, 0, 0);
859
+ }
860
+
861
+ const container = new Container();
862
+ container.addChild(new Text(header, 0, 0));
863
+ if (subheader) container.addChild(new Text(theme.fg("dim", subheader), 0, 0));
864
+ container.addChild(new Spacer(1));
865
+ container.addChild(new Text(theme.fg("muted", "Selection"), 0, 0));
866
+ container.addChild(new Text(theme.fg("dim", details.selectionReason), 0, 0));
867
+ container.addChild(new Spacer(1));
868
+ container.addChild(new Text(theme.fg("muted", "Output"), 0, 0));
869
+ container.addChild(new Markdown(body.trim(), 0, 0, getMarkdownTheme()));
870
+ if (usage) {
871
+ container.addChild(new Spacer(1));
872
+ container.addChild(new Text(theme.fg("muted", "Usage"), 0, 0));
873
+ container.addChild(new Text(theme.fg("dim", usage), 0, 0));
874
+ }
875
+ if (details.stderr) {
876
+ container.addChild(new Spacer(1));
877
+ container.addChild(new Text(theme.fg("muted", "stderr"), 0, 0));
878
+ container.addChild(
879
+ new Text(result.isError ? theme.fg("error", details.stderr) : theme.fg("dim", details.stderr), 0, 0),
880
+ );
881
+ }
882
+ return container;
883
+ },
884
+ });
885
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "@diegopetrucci/pi-oracle",
3
+ "version": "0.1.5",
4
+ "description": "An Amp-style oracle extension for pi that consults the strongest reasoning model on your current provider.",
5
+ "keywords": ["pi-package", "pi", "oracle", "reasoning", "subagent"],
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/diegopetrucci/pi-extensions.git",
10
+ "directory": "extensions/oracle"
11
+ },
12
+ "files": [
13
+ "index.ts",
14
+ "README.md"
15
+ ],
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "pi": {
20
+ "extensions": [
21
+ "index.ts"
22
+ ],
23
+ "image": "https://raw.githubusercontent.com/diegopetrucci/pi-extensions/main/assets/oracle-preview.svg"
24
+ },
25
+ "peerDependencies": {
26
+ "@mariozechner/pi-coding-agent": "*",
27
+ "@mariozechner/pi-tui": "*"
28
+ }
29
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.2",
4
- "description": "A collection of pi extensions, starting with a minimal custom footer.",
3
+ "version": "0.1.5",
4
+ "description": "A collection of pi extensions, including a minimal custom footer and an Amp-style oracle.",
5
5
  "keywords": ["pi-package", "pi", "terminal", "agent"],
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -26,8 +26,9 @@
26
26
  },
27
27
  "pi": {
28
28
  "extensions": [
29
- "./extensions/minimal-footer/index.ts"
29
+ "./extensions/minimal-footer/index.ts",
30
+ "./extensions/oracle/index.ts"
30
31
  ],
31
- "image": "https://raw.githubusercontent.com/diegopetrucci/pi-extensions/main/assets/minimal-footer-preview.png"
32
+ "image": "https://raw.githubusercontent.com/diegopetrucci/pi-extensions/main/assets/oracle-preview.svg"
32
33
  }
33
34
  }