@diegopetrucci/pi-extensions 0.1.29 → 0.1.30

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
@@ -12,7 +12,7 @@ A collection of [pi](https://github.com/earendil-works/pi-mono) agent extensions
12
12
  - [`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
13
  - [`notify`](./extensions/notify): Sends configurable terminal, desktop, bell, and sound notifications when pi finishes and is ready for input.
14
14
  - [`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, covers pi’s built-in providers (including Together) with hardcoded rankings, requests xhigh reasoning by default and clamps to model capabilities, and shows live status while running.
15
+ - [`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
16
  - [`permission-gate`](./extensions/permission-gate): Prompts for confirmation before dangerous bash commands like `rm -rf`, `sudo`, and `chmod 777`.
17
17
  - [`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
18
  - [`todo`](./extensions/todo): Adds a branch-aware `todo` tool for the agent and a `/todos` viewer for users.
@@ -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 { basename } from "node:path";
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 selectionResult = await selectOracleModel(ctx);
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
- if (ctx.hasUI) ctx.ui.notify(selectionResult.error, "error");
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
- if (ctx.hasUI) ctx.ui.notify(message, "info");
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 (params.model?.trim()) {
946
- const modelRef = params.model.trim();
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, params.thinkingLevel);
1152
+ const thinking = resolveThinkingLevel(matched, configuredThinkingLevel);
1153
+ const usedToolOverride = !!explicitModel.model;
952
1154
  const selectionReason = matched
953
- ? appendThinkingLevelClampReason("Used the explicit model override provided in the tool call.", thinking)
954
- : "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.";
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, params.thinkingLevel);
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: params.thinkingLevel ?? DEFAULT_THINKING_LEVEL,
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.8",
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-extensions",
3
- "version": "0.1.29",
3
+ "version": "0.1.30",
4
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
5
  "keywords": ["pi-package", "pi", "terminal", "agent"],
6
6
  "license": "MIT",