@diegopetrucci/pi-extensions 0.1.29 → 0.1.31
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 +2 -1
- package/extensions/gnosis/README.md +59 -0
- package/extensions/gnosis/index.ts +159 -0
- package/extensions/gnosis/package.json +29 -0
- package/extensions/oracle/README.md +19 -2
- package/extensions/oracle/index.ts +224 -15
- package/extensions/oracle/package.json +1 -1
- package/package.json +9 -3
package/README.md
CHANGED
|
@@ -7,12 +7,13 @@ A collection of [pi](https://github.com/earendil-works/pi-mono) agent extensions
|
|
|
7
7
|
- [`context-cap`](./extensions/context-cap): Caps effective model context windows at 200k tokens by default so pi avoids the `dumb zone`; toggle temporarily with `/context-cap`.
|
|
8
8
|
- [`context-inspector`](./extensions/context-inspector): Adds `/context`, a local self-contained HTML dashboard that breaks down where the current session context is going, with category overview, top offenders, and drilldown search.
|
|
9
9
|
- [`dirty-repo-guard`](./extensions/dirty-repo-guard): Prompts before new sessions, session switches, or forks when the current git repo has uncommitted changes.
|
|
10
|
+
- [`gnosis`](./extensions/gnosis): Exposes the `gn` repo-local knowledge base CLI as an agent tool for searching and recording durable project decisions, constraints, and intent.
|
|
10
11
|
- [`inline-bash`](./extensions/inline-bash): Expands `!{command}` snippets in user prompts by running them through bash before the prompt reaches the agent.
|
|
11
12
|
- [`librarian`](./extensions/librarian): Adds a GitHub research scout with a local repo checkout cache enabled by default under the OS user cache directory, toggleable with `/librarian-cache`, with cached repos expiring after 30 days of non-use.
|
|
12
13
|
- [`minimal-footer`](./extensions/minimal-footer): Replaces pi's built-in footer with a minimal configurable two-line layout: branch/repo on the first line, context/model on the second, optional `DUMB ZONE`, plus OpenAI Codex 5-hour and 7-day usage when available.
|
|
13
14
|
- [`notify`](./extensions/notify): Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input.
|
|
14
15
|
- [`openai-fast`](./extensions/openai-fast): Adds `/fast` to enable OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.
|
|
15
|
-
- [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription,
|
|
16
|
+
- [`oracle`](./extensions/oracle): Adds an Amp-style read-only oracle tool that auto-selects the strongest reasoning model on the current provider/subscription, supports persisted `/oracle` model/thinking defaults, requests xhigh reasoning by default and clamps to model capabilities, and shows live status while running.
|
|
16
17
|
- [`permission-gate`](./extensions/permission-gate): Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`.
|
|
17
18
|
- [`quiet-tools`](./extensions/quiet-tools): Renders collapsed built-in tool rows as a one-line invocation plus an expand hint without changing model-visible tool results; toggle temporarily with `/quiet-tools`.
|
|
18
19
|
- [`todo`](./extensions/todo): Adds a branch-aware `todo` tool for the agent and a `/todos` viewer for users.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# gnosis
|
|
2
|
+
|
|
3
|
+
A pi extension that exposes [`gnosis`](https://github.com/skorokithakis/gnosis) as an agent tool.
|
|
4
|
+
|
|
5
|
+
`gnosis` is a repo-local knowledge base for durable project context: decisions, rejected alternatives, constraints, operational lessons, and intent that are not obvious from code or docs.
|
|
6
|
+
|
|
7
|
+
## What it adds
|
|
8
|
+
|
|
9
|
+
- a `gnosis` tool for the agent
|
|
10
|
+
- support for `plan`, `review`, `search`, `latest`, `show`, `topics`, `write`, and `reindex`
|
|
11
|
+
- prompt guidance encouraging the agent to search before implementation and record only durable, non-obvious knowledge
|
|
12
|
+
|
|
13
|
+
This extension intentionally does not add slash commands; it is an agent-facing wrapper around the `gn` CLI. It also intentionally omits `edit` and `rm` actions from the tool surface.
|
|
14
|
+
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
Install the `gn` CLI first:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
brew install --cask skorokithakis/tap/gnosis
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
Or with Go:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
go install github.com/skorokithakis/gnosis/cmd/gn@latest
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
### Standalone npm package
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pi install npm:@diegopetrucci/pi-gnosis
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Collection package
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pi install npm:@diegopetrucci/pi-extensions
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### GitHub package
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pi install git:github.com/diegopetrucci/pi-extensions
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then reload pi:
|
|
50
|
+
|
|
51
|
+
```text
|
|
52
|
+
/reload
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Notes
|
|
56
|
+
|
|
57
|
+
- The extension shells out to `gn` in the current Pi working directory.
|
|
58
|
+
- Gnosis stores entries in `.gnosis/entries.jsonl` at the repo root and uses a disposable SQLite FTS5 index for search.
|
|
59
|
+
- `write` mutates the repo-local gnosis knowledge base; `reindex` rebuilds the search cache.
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_MAX_BYTES,
|
|
4
|
+
DEFAULT_MAX_LINES,
|
|
5
|
+
formatSize,
|
|
6
|
+
truncateTail,
|
|
7
|
+
type ExtensionAPI,
|
|
8
|
+
type ToolExecutionMode,
|
|
9
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { Type, type Static } from "typebox";
|
|
11
|
+
|
|
12
|
+
const GNOSIS_TIMEOUT_MS = 30_000;
|
|
13
|
+
|
|
14
|
+
const GnosisParams = Type.Object({
|
|
15
|
+
action: StringEnum(["plan", "review", "search", "latest", "show", "topics", "write", "reindex"] as const, {
|
|
16
|
+
description: "The gnosis operation to run.",
|
|
17
|
+
}),
|
|
18
|
+
query: Type.Optional(Type.String({ minLength: 1, description: "Search query for action=search. Use uppercase OR/NOT for FTS operators." })),
|
|
19
|
+
target: Type.Optional(Type.String({ minLength: 1, description: "Entry ID prefix or topic for action=show." })),
|
|
20
|
+
topics: Type.Optional(
|
|
21
|
+
Type.Array(Type.String({ minLength: 1 }), {
|
|
22
|
+
minItems: 1,
|
|
23
|
+
description: "Topics for action=write. Each normalized topic must be at least 7 chars.",
|
|
24
|
+
}),
|
|
25
|
+
),
|
|
26
|
+
text: Type.Optional(Type.String({ minLength: 1, description: "Knowledge text for action=write." })),
|
|
27
|
+
related: Type.Optional(
|
|
28
|
+
Type.Array(Type.String({ minLength: 1 }), { description: "Related entry IDs or unique prefixes for action=write." }),
|
|
29
|
+
),
|
|
30
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, description: "Positive result limit for action=search or action=latest." })),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
type GnosisParams = Static<typeof GnosisParams>;
|
|
34
|
+
|
|
35
|
+
function positiveInteger(value: number | undefined, name: string): string | undefined {
|
|
36
|
+
if (value === undefined) return undefined;
|
|
37
|
+
if (!Number.isInteger(value) || value <= 0) throw new Error(`${name} must be a positive integer.`);
|
|
38
|
+
return String(value);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function requireString(value: string | undefined, name: string): string {
|
|
42
|
+
const trimmed = value?.trim();
|
|
43
|
+
if (!trimmed) throw new Error(`${name} is required.`);
|
|
44
|
+
return trimmed;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildGnosisArgs(params: GnosisParams): string[] {
|
|
48
|
+
switch (params.action) {
|
|
49
|
+
case "plan":
|
|
50
|
+
return ["help", "plan"];
|
|
51
|
+
case "review":
|
|
52
|
+
return ["help", "review"];
|
|
53
|
+
case "search": {
|
|
54
|
+
const args = ["search", requireString(params.query, "query")];
|
|
55
|
+
const limit = positiveInteger(params.limit, "limit");
|
|
56
|
+
if (limit) args.push("--limit", limit);
|
|
57
|
+
return args;
|
|
58
|
+
}
|
|
59
|
+
case "latest": {
|
|
60
|
+
const args = ["latest"];
|
|
61
|
+
const limit = positiveInteger(params.limit, "limit");
|
|
62
|
+
if (limit) args.push("--limit", limit);
|
|
63
|
+
return args;
|
|
64
|
+
}
|
|
65
|
+
case "show":
|
|
66
|
+
return ["show", requireString(params.target, "target")];
|
|
67
|
+
case "topics":
|
|
68
|
+
return ["topics"];
|
|
69
|
+
case "write": {
|
|
70
|
+
const topics = params.topics?.map((topic) => topic.trim()).filter(Boolean) ?? [];
|
|
71
|
+
if (topics.length === 0) throw new Error("topics is required for action=write.");
|
|
72
|
+
const args = ["write", topics.join(","), requireString(params.text, "text")];
|
|
73
|
+
const related = params.related?.map((id) => id.trim()).filter(Boolean) ?? [];
|
|
74
|
+
if (related.length > 0) args.push("--related", related.join(","));
|
|
75
|
+
return args;
|
|
76
|
+
}
|
|
77
|
+
case "reindex":
|
|
78
|
+
return ["reindex"];
|
|
79
|
+
default: {
|
|
80
|
+
const exhaustive: never = params.action;
|
|
81
|
+
throw new Error(`Unsupported gnosis action: ${exhaustive}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function installHint(): string {
|
|
87
|
+
return [
|
|
88
|
+
"The `gn` CLI is required for the gnosis extension.",
|
|
89
|
+
"Install it with one of:",
|
|
90
|
+
" brew install --cask skorokithakis/tap/gnosis",
|
|
91
|
+
" go install github.com/skorokithakis/gnosis/cmd/gn@latest",
|
|
92
|
+
].join("\n");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function formatOutput(stdout: string, stderr: string): { text: string; truncated: boolean } {
|
|
96
|
+
const raw = [stdout, stderr].filter(Boolean).join(stderr && stdout ? "\n" : "").trimEnd();
|
|
97
|
+
if (!raw) return { text: "(no output)", truncated: false };
|
|
98
|
+
|
|
99
|
+
const truncation = truncateTail(raw, {
|
|
100
|
+
maxBytes: DEFAULT_MAX_BYTES,
|
|
101
|
+
maxLines: DEFAULT_MAX_LINES,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
let text = truncation.content;
|
|
105
|
+
if (truncation.truncated) {
|
|
106
|
+
text += `\n\n[Output truncated: ${truncation.outputLines} of ${truncation.totalLines} lines`;
|
|
107
|
+
text += ` (${formatSize(truncation.outputBytes)} of ${formatSize(truncation.totalBytes)}).]`;
|
|
108
|
+
}
|
|
109
|
+
return { text, truncated: truncation.truncated };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default function gnosisExtension(pi: ExtensionAPI) {
|
|
113
|
+
pi.registerTool({
|
|
114
|
+
name: "gnosis",
|
|
115
|
+
label: "Gnosis",
|
|
116
|
+
description:
|
|
117
|
+
"Search and record repo-local project knowledge using the `gn` CLI. Supports plan/review doctrine, search, latest, show, topics, write, and reindex. Does not support edit/rm; use shell only with explicit user intent for destructive gnosis maintenance.",
|
|
118
|
+
executionMode: "sequential" as ToolExecutionMode,
|
|
119
|
+
promptSnippet: "Search and record repo-local project knowledge through gnosis (`gn`).",
|
|
120
|
+
promptGuidelines: [
|
|
121
|
+
"Use the gnosis tool before implementing changes that may touch prior architecture, project decisions, rejected alternatives, or human constraints.",
|
|
122
|
+
"Use gnosis with action=\"search\" and uppercase OR between likely topic keywords, for example `auth OR token OR session`.",
|
|
123
|
+
"Use gnosis with action=\"write\" only for durable, non-obvious project knowledge that is not already captured in code, comments, docs, or the commit message.",
|
|
124
|
+
"Prefer code comments over gnosis with action=\"write\" when the knowledge has an obvious specific code anchor.",
|
|
125
|
+
],
|
|
126
|
+
parameters: GnosisParams,
|
|
127
|
+
|
|
128
|
+
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
129
|
+
const args = buildGnosisArgs(params);
|
|
130
|
+
|
|
131
|
+
let result: Awaited<ReturnType<typeof pi.exec>>;
|
|
132
|
+
try {
|
|
133
|
+
result = await pi.exec("gn", args, {
|
|
134
|
+
cwd: ctx.cwd,
|
|
135
|
+
signal,
|
|
136
|
+
timeout: GNOSIS_TIMEOUT_MS,
|
|
137
|
+
});
|
|
138
|
+
} catch (error) {
|
|
139
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
140
|
+
throw new Error(`${installHint()}\n\nExecution error: ${message}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const output = formatOutput(result.stdout ?? "", result.stderr ?? "");
|
|
144
|
+
if (result.killed) throw new Error(`gnosis ${params.action} timed out or was cancelled.\n\n${output.text}`);
|
|
145
|
+
if (result.code !== 0) {
|
|
146
|
+
throw new Error(`gnosis ${params.action} failed with exit code ${result.code}.\n\n${output.text}\n\n${installHint()}`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
content: [{ type: "text", text: output.text }],
|
|
151
|
+
details: {
|
|
152
|
+
action: params.action,
|
|
153
|
+
args,
|
|
154
|
+
truncated: output.truncated,
|
|
155
|
+
},
|
|
156
|
+
};
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@diegopetrucci/pi-gnosis",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A pi extension that exposes the gnosis repo-local knowledge base CLI as an agent tool.",
|
|
5
|
+
"keywords": ["pi-package", "pi", "gnosis", "memory", "knowledge-base", "tool"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/diegopetrucci/pi-extensions.git",
|
|
10
|
+
"directory": "extensions/gnosis"
|
|
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
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@earendil-works/pi-ai": "*",
|
|
26
|
+
"@earendil-works/pi-coding-agent": "*",
|
|
27
|
+
"typebox": "*"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -38,6 +38,7 @@ Yes — the extension explicitly sets the oracle reasoning level.
|
|
|
38
38
|
- reasoning models request `xhigh` by default, then use the Pi-compatible effective thinking level supported by the matched model
|
|
39
39
|
- non-reasoning models default to `off`
|
|
40
40
|
- you can override it with the tool's optional `thinkingLevel` parameter; matched models still clamp unsupported overrides and report the effective level
|
|
41
|
+
- you can persist a default thinking level with `/oracle thinking <level>` so future automatic oracle tool calls use it when the agent does not pass a per-call override
|
|
41
42
|
|
|
42
43
|
Use `/oracle-model` inside pi to see what it would pick right now.
|
|
43
44
|
|
|
@@ -81,12 +82,28 @@ Ask pi normally, for example:
|
|
|
81
82
|
|
|
82
83
|
The main agent can call the tool directly.
|
|
83
84
|
|
|
85
|
+
## User defaults
|
|
86
|
+
|
|
87
|
+
Use `/oracle` to set persisted defaults that apply to future oracle tool calls, including calls the agent launches automatically without per-call overrides.
|
|
88
|
+
|
|
89
|
+
```text
|
|
90
|
+
/oracle status
|
|
91
|
+
/oracle model anthropic/claude-opus-4-5
|
|
92
|
+
/oracle thinking high
|
|
93
|
+
/oracle thinking auto
|
|
94
|
+
/oracle clear model
|
|
95
|
+
/oracle clear thinking
|
|
96
|
+
/oracle clear
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Tool-call parameters still win over these defaults. `auto` clears the configured default and restores the built-in selection behavior. Preferences are saved under pi's agent directory in `extensions/oracle.json`.
|
|
100
|
+
|
|
84
101
|
## Tool parameters
|
|
85
102
|
|
|
86
103
|
- `task` - required prompt for the oracle
|
|
87
104
|
- `includeBash` - optional, adds `bash` for non-mutating inspection
|
|
88
|
-
- `model` - optional explicit model override
|
|
89
|
-
- `thinkingLevel` - optional reasoning/thinking override
|
|
105
|
+
- `model` - optional explicit model override; falls back to the `/oracle model` default, then auto-selection
|
|
106
|
+
- `thinkingLevel` - optional reasoning/thinking override; falls back to the `/oracle thinking` default, then built-in defaults
|
|
90
107
|
- `cwd` - optional working directory override
|
|
91
108
|
|
|
92
109
|
## Notes
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as path from "node:path";
|
|
4
5
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
5
|
-
import { getMarkdownTheme, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import { getAgentDir, getMarkdownTheme, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
6
7
|
import { Container, Markdown, Spacer, Text } from "@earendil-works/pi-tui";
|
|
7
8
|
import { Type } from "typebox";
|
|
8
9
|
|
|
@@ -58,6 +59,11 @@ interface OracleUiRun {
|
|
|
58
59
|
preview?: string;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
interface OraclePreferences {
|
|
63
|
+
model?: string;
|
|
64
|
+
thinkingLevel?: ThinkingLevel;
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
const READ_ONLY_TOOLS = ["read", "grep", "find", "ls"];
|
|
62
68
|
const READ_ONLY_PLUS_BASH_TOOLS = [...READ_ONLY_TOOLS, "bash"];
|
|
63
69
|
const DEFAULT_THINKING_LEVEL: ThinkingLevel = "xhigh";
|
|
@@ -65,6 +71,7 @@ const THINKING_LEVELS = ["off", "minimal", "low", "medium", "high", "xhigh"] as
|
|
|
65
71
|
const COLLAPSED_LINE_LIMIT = 8;
|
|
66
72
|
const ORACLE_STATUS_ID = "oracle";
|
|
67
73
|
const ORACLE_WIDGET_ID = "oracle";
|
|
74
|
+
const ORACLE_CONFIG_FILE = "oracle.json";
|
|
68
75
|
|
|
69
76
|
const PROVIDER_MODEL_PREFERENCES: Record<string, string[]> = {
|
|
70
77
|
"amazon-bedrock": [
|
|
@@ -429,6 +436,74 @@ function createEmptyUsage(): UsageStats {
|
|
|
429
436
|
};
|
|
430
437
|
}
|
|
431
438
|
|
|
439
|
+
function getOracleConfigPath(): string {
|
|
440
|
+
return path.join(getAgentDir(), "extensions", ORACLE_CONFIG_FILE);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function parseThinkingLevel(value: unknown): ThinkingLevel | undefined {
|
|
444
|
+
if (typeof value !== "string") return undefined;
|
|
445
|
+
const normalized = value.trim().toLowerCase();
|
|
446
|
+
return (THINKING_LEVELS as readonly string[]).includes(normalized) ? (normalized as ThinkingLevel) : undefined;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function normalizeModelPreference(value: unknown): string | undefined {
|
|
450
|
+
if (typeof value !== "string") return undefined;
|
|
451
|
+
const trimmed = value.trim();
|
|
452
|
+
return trimmed ? trimmed : undefined;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function parseModelPreference(value: unknown): { model?: string; thinkingLevel?: ThinkingLevel } {
|
|
456
|
+
const model = normalizeModelPreference(value);
|
|
457
|
+
if (!model) return {};
|
|
458
|
+
const match = model.match(/^(.*):(off|minimal|low|medium|high|xhigh)$/i);
|
|
459
|
+
if (!match?.[1]) return { model };
|
|
460
|
+
return { model: match[1], thinkingLevel: parseThinkingLevel(match[2]) };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
async function readOraclePreferences(): Promise<OraclePreferences> {
|
|
464
|
+
try {
|
|
465
|
+
const raw = await fs.readFile(getOracleConfigPath(), "utf8");
|
|
466
|
+
const parsed = JSON.parse(raw) as {
|
|
467
|
+
model?: unknown;
|
|
468
|
+
defaultModel?: unknown;
|
|
469
|
+
thinkingLevel?: unknown;
|
|
470
|
+
defaultThinkingLevel?: unknown;
|
|
471
|
+
};
|
|
472
|
+
return {
|
|
473
|
+
model: parseModelPreference(parsed.model).model ?? parseModelPreference(parsed.defaultModel).model,
|
|
474
|
+
thinkingLevel:
|
|
475
|
+
parseThinkingLevel(parsed.thinkingLevel) ??
|
|
476
|
+
parseThinkingLevel(parsed.defaultThinkingLevel) ??
|
|
477
|
+
parseModelPreference(parsed.model).thinkingLevel ??
|
|
478
|
+
parseModelPreference(parsed.defaultModel).thinkingLevel,
|
|
479
|
+
};
|
|
480
|
+
} catch {
|
|
481
|
+
return {};
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
async function writeOraclePreferences(preferences: OraclePreferences): Promise<void> {
|
|
486
|
+
const configPath = getOracleConfigPath();
|
|
487
|
+
const config = {
|
|
488
|
+
...(preferences.model ? { model: preferences.model } : {}),
|
|
489
|
+
...(preferences.thinkingLevel ? { thinkingLevel: preferences.thinkingLevel } : {}),
|
|
490
|
+
updatedAt: new Date().toISOString(),
|
|
491
|
+
};
|
|
492
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
493
|
+
await fs.writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function formatOraclePreferences(preferences: OraclePreferences): string {
|
|
497
|
+
const model = preferences.model ?? "auto";
|
|
498
|
+
const thinkingLevel = preferences.thinkingLevel ?? "auto";
|
|
499
|
+
return `Oracle defaults: model=${model}, thinkingLevel=${thinkingLevel}. Config: ${getOracleConfigPath()}`;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function notifyCommand(ctx: any, message: string, kind = "info"): void {
|
|
503
|
+
if (ctx.hasUI && ctx.ui) ctx.ui.notify(message, kind);
|
|
504
|
+
else console.log(message);
|
|
505
|
+
}
|
|
506
|
+
|
|
432
507
|
function formatTokens(count: number): string {
|
|
433
508
|
if (count < 1000) return count.toString();
|
|
434
509
|
if (count < 10000) return `${(count / 1000).toFixed(1)}k`;
|
|
@@ -534,7 +609,7 @@ function getPiInvocation(args: string[]): { command: string; args: string[] } {
|
|
|
534
609
|
return { command: process.execPath, args: [currentScript, ...args] };
|
|
535
610
|
}
|
|
536
611
|
|
|
537
|
-
const execName = basename(process.execPath).toLowerCase();
|
|
612
|
+
const execName = path.basename(process.execPath).toLowerCase();
|
|
538
613
|
const isGenericRuntime = /^(node|bun)(\.exe)?$/.test(execName);
|
|
539
614
|
if (!isGenericRuntime) {
|
|
540
615
|
return { command: process.execPath, args };
|
|
@@ -893,26 +968,147 @@ function renderCollapsedText(text: string, lineLimit = COLLAPSED_LINE_LIMIT): st
|
|
|
893
968
|
|
|
894
969
|
export default function oracleExtension(pi: ExtensionAPI) {
|
|
895
970
|
const activeRuns = new Map<string, OracleUiRun>();
|
|
971
|
+
let preferences: OraclePreferences = {};
|
|
896
972
|
|
|
897
973
|
pi.on("session_start", async (_event, ctx) => {
|
|
974
|
+
preferences = await readOraclePreferences();
|
|
898
975
|
activeRuns.clear();
|
|
899
976
|
updateOracleUi(ctx, activeRuns);
|
|
900
977
|
});
|
|
901
978
|
|
|
979
|
+
pi.registerCommand("oracle", {
|
|
980
|
+
description: "Configure Oracle default model and thinking level for future oracle tool calls",
|
|
981
|
+
getArgumentCompletions: (prefix) => {
|
|
982
|
+
const parts = prefix.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
983
|
+
const first = parts[0] ?? "";
|
|
984
|
+
if (parts.length <= 1) {
|
|
985
|
+
const commands = ["status", "model", "thinking", "clear"];
|
|
986
|
+
const matches = commands.filter((command) => command.startsWith(first));
|
|
987
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
988
|
+
}
|
|
989
|
+
if (parts[0] === "thinking") {
|
|
990
|
+
const query = parts[1] ?? "";
|
|
991
|
+
const values = ["auto", ...THINKING_LEVELS];
|
|
992
|
+
const matches = values.filter((value) => value.startsWith(query));
|
|
993
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
994
|
+
}
|
|
995
|
+
if (parts[0] === "clear") {
|
|
996
|
+
const query = parts[1] ?? "";
|
|
997
|
+
const values = ["all", "model", "thinking"];
|
|
998
|
+
const matches = values.filter((value) => value.startsWith(query));
|
|
999
|
+
return matches.length > 0 ? matches.map((value) => ({ value, label: value })) : null;
|
|
1000
|
+
}
|
|
1001
|
+
return null;
|
|
1002
|
+
},
|
|
1003
|
+
handler: async (args, ctx) => {
|
|
1004
|
+
const raw = args.trim();
|
|
1005
|
+
const tokens = raw ? raw.split(/\s+/) : [];
|
|
1006
|
+
const [command = "status", ...rest] = tokens;
|
|
1007
|
+
const action = command.toLowerCase();
|
|
1008
|
+
const configPath = getOracleConfigPath();
|
|
1009
|
+
|
|
1010
|
+
const save = async (next: OraclePreferences): Promise<string | undefined> => {
|
|
1011
|
+
preferences = next;
|
|
1012
|
+
try {
|
|
1013
|
+
await writeOraclePreferences(preferences);
|
|
1014
|
+
return undefined;
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1017
|
+
return `Preference changed for this process, but could not save ${configPath}: ${message}`;
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
|
|
1021
|
+
if (action === "status" || action === "show") {
|
|
1022
|
+
notifyCommand(ctx, formatOraclePreferences(preferences));
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
if (action === "model") {
|
|
1027
|
+
const model = rest.join(" ").trim();
|
|
1028
|
+
const normalizedModelAction = model.toLowerCase();
|
|
1029
|
+
if (!model) {
|
|
1030
|
+
notifyCommand(ctx, "Usage: /oracle model <provider/model|auto>", "warning");
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
if (normalizedModelAction === "auto" || normalizedModelAction === "clear" || normalizedModelAction === "default") {
|
|
1034
|
+
const warning = await save({ ...preferences, model: undefined });
|
|
1035
|
+
notifyCommand(ctx, `Oracle default model cleared; future oracle calls will auto-select. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
const parsedModel = parseModelPreference(model);
|
|
1039
|
+
const next = {
|
|
1040
|
+
...preferences,
|
|
1041
|
+
model: parsedModel.model,
|
|
1042
|
+
thinkingLevel: parsedModel.thinkingLevel ?? preferences.thinkingLevel,
|
|
1043
|
+
};
|
|
1044
|
+
const warning = await save(next);
|
|
1045
|
+
notifyCommand(ctx, `Oracle default model set to ${parsedModel.model}. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
if (action === "thinking" || action === "think" || action === "thinking-level") {
|
|
1050
|
+
const value = rest[0]?.trim().toLowerCase();
|
|
1051
|
+
if (!value) {
|
|
1052
|
+
notifyCommand(ctx, `Usage: /oracle thinking ${THINKING_LEVELS.join(" | ")} | auto`, "warning");
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
if (value === "auto" || value === "clear" || value === "default") {
|
|
1056
|
+
const warning = await save({ ...preferences, thinkingLevel: undefined });
|
|
1057
|
+
notifyCommand(ctx, `Oracle default thinking level cleared; future oracle calls will use built-in defaults. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
const thinkingLevel = parseThinkingLevel(value);
|
|
1061
|
+
if (!thinkingLevel) {
|
|
1062
|
+
notifyCommand(ctx, `Usage: /oracle thinking ${THINKING_LEVELS.join(" | ")} | auto`, "warning");
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const warning = await save({ ...preferences, thinkingLevel });
|
|
1066
|
+
notifyCommand(ctx, `Oracle default thinking level set to ${thinkingLevel}. ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
|
|
1067
|
+
return;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
if (action === "clear" || action === "reset") {
|
|
1071
|
+
const target = rest[0]?.trim().toLowerCase() || "all";
|
|
1072
|
+
let next: OraclePreferences;
|
|
1073
|
+
if (target === "all") next = {};
|
|
1074
|
+
else if (target === "model") next = { ...preferences, model: undefined };
|
|
1075
|
+
else if (target === "thinking" || target === "thinking-level") next = { ...preferences, thinkingLevel: undefined };
|
|
1076
|
+
else {
|
|
1077
|
+
notifyCommand(ctx, "Usage: /oracle clear [all|model|thinking]", "warning");
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const warning = await save(next);
|
|
1081
|
+
notifyCommand(ctx, `Oracle defaults cleared (${target}). ${warning ?? formatOraclePreferences(preferences)}`, warning ? "warning" : "info");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
notifyCommand(ctx, "Usage: /oracle status | model <provider/model|auto> | thinking <off|minimal|low|medium|high|xhigh|auto> | clear [all|model|thinking]", "warning");
|
|
1086
|
+
},
|
|
1087
|
+
});
|
|
1088
|
+
|
|
902
1089
|
pi.registerCommand("oracle-model", {
|
|
903
1090
|
description: "Show which model the oracle would use right now",
|
|
904
1091
|
handler: async (_args, ctx) => {
|
|
905
|
-
const
|
|
1092
|
+
const defaultModel = parseModelPreference(preferences.model);
|
|
1093
|
+
if (defaultModel.model) {
|
|
1094
|
+
const matched = await findAvailableModel(ctx, defaultModel.model);
|
|
1095
|
+
const thinking = resolveThinkingLevel(matched, preferences.thinkingLevel ?? defaultModel.thinkingLevel);
|
|
1096
|
+
const modelRef = matched ? `${matched.provider}/${matched.id}` : defaultModel.model;
|
|
1097
|
+
const suffix = matched
|
|
1098
|
+
? appendThinkingLevelClampReason("Configured default oracle model is active.", thinking)
|
|
1099
|
+
: "Configured default oracle model is active, but it was not matched against the authenticated model list.";
|
|
1100
|
+
notifyCommand(ctx, `Oracle: ${modelRef} (${thinking.effective}) — ${suffix}`);
|
|
1101
|
+
return;
|
|
1102
|
+
}
|
|
1103
|
+
const selectionResult = await selectOracleModel(ctx, preferences.thinkingLevel);
|
|
906
1104
|
if (!selectionResult.ok) {
|
|
907
|
-
|
|
908
|
-
else console.log(selectionResult.error);
|
|
1105
|
+
notifyCommand(ctx, selectionResult.error, "error");
|
|
909
1106
|
return;
|
|
910
1107
|
}
|
|
911
1108
|
|
|
912
1109
|
const { selection } = selectionResult;
|
|
913
1110
|
const message = `Oracle: ${selection.modelRef} (${selection.thinkingLevel}) — ${selection.selectionReason}`;
|
|
914
|
-
|
|
915
|
-
else console.log(message);
|
|
1111
|
+
notifyCommand(ctx, message);
|
|
916
1112
|
},
|
|
917
1113
|
});
|
|
918
1114
|
|
|
@@ -932,6 +1128,11 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
932
1128
|
parameters: OracleParams,
|
|
933
1129
|
|
|
934
1130
|
async execute(toolCallId, params, signal, onUpdate, ctx) {
|
|
1131
|
+
const explicitModel = parseModelPreference(params.model);
|
|
1132
|
+
const defaultModel = parseModelPreference(preferences.model);
|
|
1133
|
+
const configuredModel = explicitModel.model ?? defaultModel.model;
|
|
1134
|
+
const configuredThinkingLevel =
|
|
1135
|
+
params.thinkingLevel ?? explicitModel.thinkingLevel ?? preferences.thinkingLevel ?? defaultModel.thinkingLevel;
|
|
935
1136
|
const uiRun: OracleUiRun = {
|
|
936
1137
|
task: params.task,
|
|
937
1138
|
includeBash: params.includeBash ?? false,
|
|
@@ -942,16 +1143,24 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
942
1143
|
|
|
943
1144
|
try {
|
|
944
1145
|
let selection: OracleSelection;
|
|
945
|
-
if (
|
|
946
|
-
const modelRef =
|
|
1146
|
+
if (configuredModel) {
|
|
1147
|
+
const modelRef = configuredModel;
|
|
947
1148
|
const matched = await findAvailableModel(ctx, modelRef);
|
|
948
1149
|
const provider =
|
|
949
1150
|
matched?.provider ?? (modelRef.includes("/") ? modelRef.split("/")[0] : ctx.model?.provider ?? "unknown");
|
|
950
1151
|
const modelId = matched?.id ?? (modelRef.includes("/") ? modelRef.split("/").slice(1).join("/") : modelRef);
|
|
951
|
-
const thinking = resolveThinkingLevel(matched,
|
|
1152
|
+
const thinking = resolveThinkingLevel(matched, configuredThinkingLevel);
|
|
1153
|
+
const usedToolOverride = !!explicitModel.model;
|
|
952
1154
|
const selectionReason = matched
|
|
953
|
-
? appendThinkingLevelClampReason(
|
|
954
|
-
|
|
1155
|
+
? appendThinkingLevelClampReason(
|
|
1156
|
+
usedToolOverride
|
|
1157
|
+
? "Used the explicit model override provided in the tool call."
|
|
1158
|
+
: "Used the configured default oracle model.",
|
|
1159
|
+
thinking,
|
|
1160
|
+
)
|
|
1161
|
+
: usedToolOverride
|
|
1162
|
+
? "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."
|
|
1163
|
+
: "Used the configured default oracle model. The model was not matched against the authenticated model list, so the reasoning level fallback was applied.";
|
|
955
1164
|
selection = {
|
|
956
1165
|
modelRef: matched ? `${matched.provider}/${matched.id}` : modelRef,
|
|
957
1166
|
provider,
|
|
@@ -965,7 +1174,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
965
1174
|
selectionReason,
|
|
966
1175
|
};
|
|
967
1176
|
} else {
|
|
968
|
-
const selectionResult = await selectOracleModel(ctx,
|
|
1177
|
+
const selectionResult = await selectOracleModel(ctx, configuredThinkingLevel);
|
|
969
1178
|
if (!selectionResult.ok) {
|
|
970
1179
|
return {
|
|
971
1180
|
content: [{ type: "text", text: selectionResult.error }],
|
|
@@ -974,7 +1183,7 @@ export default function oracleExtension(pi: ExtensionAPI) {
|
|
|
974
1183
|
provider: ctx.model?.provider ?? "unknown",
|
|
975
1184
|
modelId: "",
|
|
976
1185
|
modelName: undefined,
|
|
977
|
-
thinkingLevel:
|
|
1186
|
+
thinkingLevel: configuredThinkingLevel ?? DEFAULT_THINKING_LEVEL,
|
|
978
1187
|
autoSelected: true,
|
|
979
1188
|
selectionReason: selectionResult.error,
|
|
980
1189
|
includeBash: params.includeBash ?? false,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-oracle",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "An Amp-style oracle extension for pi that consults the strongest reasoning model on your current provider.",
|
|
5
5
|
"keywords": ["pi-package", "pi", "oracle", "reasoning", "subagent"],
|
|
6
6
|
"license": "MIT",
|
package/package.json
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "A collection of pi extensions for context management, workflow audits, review-comment triage, notifications, safety guards, GitHub research, todos, tool rendering, and model/provider helpers.",
|
|
5
|
-
"keywords": [
|
|
3
|
+
"version": "0.1.31",
|
|
4
|
+
"description": "A collection of pi extensions for context management, workflow audits, review-comment triage, notifications, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, and model/provider helpers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"pi-package",
|
|
7
|
+
"pi",
|
|
8
|
+
"terminal",
|
|
9
|
+
"agent"
|
|
10
|
+
],
|
|
6
11
|
"license": "MIT",
|
|
7
12
|
"repository": {
|
|
8
13
|
"type": "git",
|
|
@@ -34,6 +39,7 @@
|
|
|
34
39
|
"./extensions/context-cap/index.ts",
|
|
35
40
|
"./extensions/context-inspector/index.ts",
|
|
36
41
|
"./extensions/dirty-repo-guard/index.ts",
|
|
42
|
+
"./extensions/gnosis/index.ts",
|
|
37
43
|
"./extensions/inline-bash/index.ts",
|
|
38
44
|
"./extensions/librarian/index.ts",
|
|
39
45
|
"./extensions/quiet-tools/index.ts",
|