@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 +30 -4
- package/assets/minimal-footer-preview.png +0 -0
- package/assets/oracle-preview.svg +46 -0
- package/assets/social-preview.png +0 -0
- package/extensions/minimal-footer/README.md +2 -2
- package/extensions/minimal-footer/index.ts +1 -1
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/oracle/README.md +97 -0
- package/extensions/oracle/index.ts +885 -0
- package/extensions/oracle/package.json +29 -0
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# pi-extensions
|
|
2
2
|
|
|
3
|
-

|
|
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.
|
|
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">> 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>
|
|
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
|
|
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}
|
|
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);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# oracle
|
|
2
|
+
|
|
3
|
+

|
|
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.
|
|
4
|
-
"description": "A collection of pi extensions,
|
|
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/
|
|
32
|
+
"image": "https://raw.githubusercontent.com/diegopetrucci/pi-extensions/main/assets/oracle-preview.svg"
|
|
32
33
|
}
|
|
33
34
|
}
|