@diegopetrucci/pi-extensions 0.1.61 → 0.1.63

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.
Files changed (72) hide show
  1. package/.pi-fleet-tested-version +1 -1
  2. package/README.md +3 -2
  3. package/extensions/agent-workflow-audit/.pi-fleet-tested-version +1 -1
  4. package/extensions/agent-workflow-audit/package.json +1 -1
  5. package/extensions/annotate-git-diff/.pi-fleet-tested-version +1 -1
  6. package/extensions/annotate-git-diff/package.json +1 -1
  7. package/extensions/annotate-last-message/.pi-fleet-tested-version +1 -1
  8. package/extensions/annotate-last-message/package.json +1 -1
  9. package/extensions/brrr/.pi-fleet-tested-version +1 -1
  10. package/extensions/brrr/package.json +1 -1
  11. package/extensions/claude-fast/.pi-fleet-tested-version +1 -1
  12. package/extensions/claude-fast/README.md +2 -0
  13. package/extensions/claude-fast/package.json +1 -1
  14. package/extensions/code-reviewer/.pi-fleet-tested-version +1 -1
  15. package/extensions/code-reviewer/README.md +1 -0
  16. package/extensions/code-reviewer/index.ts +24 -2
  17. package/extensions/code-reviewer/package.json +1 -1
  18. package/extensions/confirm-destructive/.pi-fleet-tested-version +1 -1
  19. package/extensions/confirm-destructive/package.json +1 -1
  20. package/extensions/context-cap/.pi-fleet-tested-version +1 -1
  21. package/extensions/context-cap/package.json +1 -1
  22. package/extensions/context-inspector/.pi-fleet-tested-version +1 -1
  23. package/extensions/context-inspector/package.json +1 -1
  24. package/extensions/contrarian/.pi-fleet-tested-version +1 -1
  25. package/extensions/contrarian/README.md +2 -0
  26. package/extensions/contrarian/index.ts +34 -8
  27. package/extensions/contrarian/package.json +1 -1
  28. package/extensions/dirty-repo-guard/.pi-fleet-tested-version +1 -1
  29. package/extensions/dirty-repo-guard/package.json +1 -1
  30. package/extensions/dynamic-context-pruning/.pi-fleet-tested-version +1 -1
  31. package/extensions/dynamic-context-pruning/README.md +78 -11
  32. package/extensions/dynamic-context-pruning/index.ts +171 -21
  33. package/extensions/dynamic-context-pruning/package.json +1 -1
  34. package/extensions/fast/.pi-fleet-tested-version +1 -0
  35. package/extensions/fast/README.md +119 -0
  36. package/extensions/fast/fast.example.json +4 -0
  37. package/extensions/fast/index.ts +383 -0
  38. package/extensions/fast/package.json +39 -0
  39. package/extensions/git-footer/.pi-fleet-tested-version +1 -1
  40. package/extensions/git-footer/package.json +1 -1
  41. package/extensions/gnosis/.pi-fleet-tested-version +1 -1
  42. package/extensions/gnosis/package.json +1 -1
  43. package/extensions/illustrations-to-explain-things/.pi-fleet-tested-version +1 -1
  44. package/extensions/illustrations-to-explain-things/package.json +1 -1
  45. package/extensions/inline-bash/.pi-fleet-tested-version +1 -1
  46. package/extensions/inline-bash/package.json +1 -1
  47. package/extensions/librarian/.pi-fleet-tested-version +1 -1
  48. package/extensions/librarian/README.md +1 -1
  49. package/extensions/librarian/index.ts +34 -5
  50. package/extensions/librarian/package.json +1 -1
  51. package/extensions/minimal-footer/.pi-fleet-tested-version +1 -1
  52. package/extensions/minimal-footer/package.json +1 -1
  53. package/extensions/notify/.pi-fleet-tested-version +1 -1
  54. package/extensions/notify/package.json +1 -1
  55. package/extensions/openai-fast/.pi-fleet-tested-version +1 -1
  56. package/extensions/openai-fast/README.md +2 -0
  57. package/extensions/openai-fast/package.json +1 -1
  58. package/extensions/oracle/.pi-fleet-tested-version +1 -1
  59. package/extensions/oracle/README.md +2 -0
  60. package/extensions/oracle/index.ts +34 -8
  61. package/extensions/oracle/package.json +1 -1
  62. package/extensions/permission-gate/.pi-fleet-tested-version +1 -1
  63. package/extensions/permission-gate/package.json +1 -1
  64. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  65. package/extensions/quiet-tools/package.json +1 -1
  66. package/extensions/review/.pi-fleet-tested-version +1 -1
  67. package/extensions/review/package.json +1 -1
  68. package/extensions/todo/.pi-fleet-tested-version +1 -1
  69. package/extensions/todo/package.json +1 -1
  70. package/extensions/triage-comments/.pi-fleet-tested-version +1 -1
  71. package/extensions/triage-comments/package.json +1 -1
  72. package/package.json +5 -6
@@ -0,0 +1,383 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import {
4
+ CONFIG_DIR_NAME,
5
+ getAgentDir,
6
+ type ExtensionAPI,
7
+ type ExtensionContext,
8
+ } from "@earendil-works/pi-coding-agent";
9
+
10
+ const EXTENSION_ID = "fast";
11
+ const ANTHROPIC_PROVIDER_ID = "anthropic";
12
+ const ANTHROPIC_API_ID = "anthropic-messages";
13
+ const ANTHROPIC_FAST_SPEED = "fast";
14
+ const ANTHROPIC_FAST_BETA = "fast-mode-2026-02-01";
15
+ const CLAUDE_CODE_OAUTH_BETAS = ["claude-code-20250219", "oauth-2025-04-20"];
16
+ const FINE_GRAINED_TOOL_STREAMING_BETA = "fine-grained-tool-streaming-2025-05-14";
17
+ const INTERLEAVED_THINKING_BETA = "interleaved-thinking-2025-05-14";
18
+ const ANTHROPIC_SUPPORTED_MODELS = new Set(["claude-opus-4-6", "claude-opus-4-7", "claude-opus-4-8"]);
19
+
20
+ const OPENAI_PROVIDER_ID = "openai-codex";
21
+ const OPENAI_API_ID = "openai-codex-responses";
22
+ const OPENAI_FAST_SERVICE_TIER = "priority";
23
+ const OPENAI_SUPPORTED_MODELS = new Set([
24
+ "gpt-5.4",
25
+ "gpt-5.5",
26
+ "gpt-5.6-sol",
27
+ "gpt-5.6-terra",
28
+ "gpt-5.6-luna",
29
+ ]);
30
+
31
+ const DEFAULT_CONFIG: FastConfig = {
32
+ enabled: false,
33
+ showStatus: true,
34
+ };
35
+
36
+ type FastOverride = "auto" | "on" | "off";
37
+ type FastProvider = "anthropic" | "openai";
38
+
39
+ type FastConfig = {
40
+ /** Default Fast-mode state when there is no session override. */
41
+ enabled: boolean;
42
+ /** Show a compact `fast` status when Fast mode is active for the current model. */
43
+ showStatus: boolean;
44
+ };
45
+
46
+ type SessionState = {
47
+ config: FastConfig;
48
+ override: FastOverride;
49
+ lastInjectedAt?: number;
50
+ lastInjectedModel?: string;
51
+ };
52
+
53
+ type ProjectConfigContext = {
54
+ cwd: string;
55
+ isProjectTrusted?: () => boolean;
56
+ };
57
+
58
+ type RecursivePartial<T> = {
59
+ [P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
60
+ };
61
+
62
+ type PayloadRecord = Record<string, unknown>;
63
+
64
+ type AnthropicHeaderCompat = {
65
+ forceAdaptiveThinking?: boolean;
66
+ supportsEagerToolInputStreaming?: boolean;
67
+ };
68
+
69
+ type Eligibility = {
70
+ eligible: boolean;
71
+ modelKey: string;
72
+ provider?: FastProvider;
73
+ reason?: string;
74
+ };
75
+
76
+ function readConfigFile(path: string): RecursivePartial<FastConfig> {
77
+ if (!existsSync(path)) return {};
78
+
79
+ try {
80
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
81
+ return isPayloadRecord(parsed) ? (parsed as RecursivePartial<FastConfig>) : {};
82
+ } catch (error) {
83
+ console.error(`Warning: Could not parse ${path}: ${error}`);
84
+ return {};
85
+ }
86
+ }
87
+
88
+ function normalizeBoolean(value: unknown, fallback: boolean): boolean {
89
+ return typeof value === "boolean" ? value : fallback;
90
+ }
91
+
92
+ function mergeConfig(base: FastConfig, overrides: RecursivePartial<FastConfig>): FastConfig {
93
+ return {
94
+ enabled: normalizeBoolean(overrides.enabled, base.enabled),
95
+ showStatus: normalizeBoolean(overrides.showStatus, base.showStatus),
96
+ };
97
+ }
98
+
99
+ function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
100
+ return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
101
+ }
102
+
103
+ function findProjectConfigPath(cwd: string): string {
104
+ let current = cwd;
105
+ while (true) {
106
+ const candidate = join(current, CONFIG_DIR_NAME, "fast.json");
107
+ if (existsSync(candidate)) return candidate;
108
+
109
+ const parent = dirname(current);
110
+ if (parent === current) return join(cwd, CONFIG_DIR_NAME, "fast.json");
111
+ current = parent;
112
+ }
113
+ }
114
+
115
+ function loadConfig(ctx: ProjectConfigContext): FastConfig {
116
+ const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "fast.json"));
117
+ const projectConfig = canReadProjectConfig(ctx) ? readConfigFile(findProjectConfigPath(ctx.cwd)) : {};
118
+ return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
119
+ }
120
+
121
+ function isPayloadRecord(payload: unknown): payload is PayloadRecord {
122
+ return typeof payload === "object" && payload !== null && !Array.isArray(payload);
123
+ }
124
+
125
+ function modelKey(ctx: ExtensionContext): string {
126
+ const model = ctx.model;
127
+ return model ? `${model.provider}/${model.id}` : "no-model";
128
+ }
129
+
130
+ function isFastEnabled(state: SessionState): boolean {
131
+ if (state.override === "on") return true;
132
+ if (state.override === "off") return false;
133
+ return state.config.enabled;
134
+ }
135
+
136
+ function describeMode(state: SessionState): string {
137
+ if (state.override === "on") return "on (session override)";
138
+ if (state.override === "off") return "off (session override)";
139
+ return state.config.enabled ? "on (config default)" : "off (config default)";
140
+ }
141
+
142
+ function getEligibility(ctx: ExtensionContext): Eligibility {
143
+ const model = ctx.model;
144
+ if (!model) {
145
+ return { eligible: false, modelKey: "no-model", reason: "no model is selected" };
146
+ }
147
+
148
+ const key = `${model.provider}/${model.id}`;
149
+ if (model.provider === ANTHROPIC_PROVIDER_ID) {
150
+ if (model.api !== ANTHROPIC_API_ID) {
151
+ return {
152
+ eligible: false,
153
+ modelKey: key,
154
+ reason: `current API is ${model.api}, not ${ANTHROPIC_API_ID}`,
155
+ };
156
+ }
157
+
158
+ if (!ANTHROPIC_SUPPORTED_MODELS.has(model.id)) {
159
+ return {
160
+ eligible: false,
161
+ modelKey: key,
162
+ reason: "Fast mode is only enabled for Claude Opus 4.6, 4.7, and 4.8",
163
+ };
164
+ }
165
+
166
+ return { eligible: true, modelKey: key, provider: "anthropic" };
167
+ }
168
+
169
+ if (model.provider === OPENAI_PROVIDER_ID) {
170
+ if (model.api !== OPENAI_API_ID) {
171
+ return {
172
+ eligible: false,
173
+ modelKey: key,
174
+ reason: `current API is ${model.api}, not ${OPENAI_API_ID}`,
175
+ };
176
+ }
177
+
178
+ if (!OPENAI_SUPPORTED_MODELS.has(model.id)) {
179
+ return {
180
+ eligible: false,
181
+ modelKey: key,
182
+ reason: "Fast mode is not enabled for this OpenAI Codex model",
183
+ };
184
+ }
185
+
186
+ if (!ctx.modelRegistry.isUsingOAuth(model)) {
187
+ return {
188
+ eligible: false,
189
+ modelKey: key,
190
+ reason: "ChatGPT OAuth auth is required; API-key auth is intentionally not used",
191
+ };
192
+ }
193
+
194
+ return { eligible: true, modelKey: key, provider: "openai" };
195
+ }
196
+
197
+ return {
198
+ eligible: false,
199
+ modelKey: key,
200
+ reason: `current provider ${model.provider} does not support this extension's Fast mode`,
201
+ };
202
+ }
203
+
204
+ function updateStatus(ctx: ExtensionContext, state: SessionState): void {
205
+ if (!ctx.hasUI) return;
206
+ if (!state.config.showStatus) {
207
+ ctx.ui.setStatus(EXTENSION_ID, undefined);
208
+ return;
209
+ }
210
+
211
+ const eligibility = getEligibility(ctx);
212
+ ctx.ui.setStatus(
213
+ EXTENSION_ID,
214
+ isFastEnabled(state) && eligibility.eligible ? "fast" : undefined,
215
+ );
216
+ }
217
+
218
+ function getStatusMessage(ctx: ExtensionContext, state: SessionState): string {
219
+ const enabled = isFastEnabled(state);
220
+ const eligibility = getEligibility(ctx);
221
+ const active = enabled && eligibility.eligible;
222
+ const injected = state.lastInjectedAt
223
+ ? ` Last injected for ${state.lastInjectedModel ?? "unknown model"} ${Math.max(0, Math.round((Date.now() - state.lastInjectedAt) / 1000))}s ago.`
224
+ : "";
225
+
226
+ if (active) {
227
+ const wireSetting =
228
+ eligibility.provider === "anthropic"
229
+ ? `speed=${ANTHROPIC_FAST_SPEED}`
230
+ : `service_tier=${OPENAI_FAST_SERVICE_TIER}`;
231
+ return `Fast mode is ${describeMode(state)} and active for ${eligibility.modelKey}; requests will use ${wireSetting}.${injected}`;
232
+ }
233
+
234
+ if (enabled) {
235
+ return `Fast mode is ${describeMode(state)}, but inactive for ${eligibility.modelKey}: ${eligibility.reason}.${injected}`;
236
+ }
237
+
238
+ return `Fast mode is ${describeMode(state)}. Current model: ${eligibility.modelKey}.${injected}`;
239
+ }
240
+
241
+ function splitBetaHeader(value: string | null | undefined): string[] {
242
+ return (value ?? "")
243
+ .split(",")
244
+ .map((part) => part.trim())
245
+ .filter(Boolean);
246
+ }
247
+
248
+ async function isAnthropicOAuth(ctx: ExtensionContext): Promise<boolean> {
249
+ try {
250
+ const resolution = await ctx.modelRegistry.getProviderAuth(ANTHROPIC_PROVIDER_ID);
251
+ const apiKey = resolution?.auth.apiKey;
252
+ if (typeof apiKey === "string") return apiKey.includes("sk-ant-oat");
253
+ } catch {
254
+ // Fall back to the synchronous registry snapshot below.
255
+ }
256
+
257
+ return ctx.model ? ctx.modelRegistry.isUsingOAuth(ctx.model) : false;
258
+ }
259
+
260
+ function getAnthropicProviderBetas(ctx: ExtensionContext, hasActiveTools: boolean): string[] {
261
+ const model = ctx.model;
262
+ if (!model) return [];
263
+ const compat = model.compat as AnthropicHeaderCompat | undefined;
264
+
265
+ const betas: string[] = [];
266
+ if (hasActiveTools && compat?.supportsEagerToolInputStreaming === false) {
267
+ betas.push(FINE_GRAINED_TOOL_STREAMING_BETA);
268
+ }
269
+ if (compat?.forceAdaptiveThinking !== true) {
270
+ betas.push(INTERLEAVED_THINKING_BETA);
271
+ }
272
+ return betas;
273
+ }
274
+
275
+ async function injectAnthropicFastHeader(
276
+ headers: Record<string, string | null>,
277
+ ctx: ExtensionContext,
278
+ state: SessionState,
279
+ hasActiveTools: boolean,
280
+ ): Promise<void> {
281
+ if (!isFastEnabled(state)) return;
282
+ const eligibility = getEligibility(ctx);
283
+ if (!eligibility.eligible || eligibility.provider !== "anthropic") return;
284
+
285
+ const matchingKeys = Object.keys(headers).filter((name) => name.toLowerCase() === "anthropic-beta");
286
+ const existing = matchingKeys.flatMap((name) => splitBetaHeader(headers[name]));
287
+ const matchingModelEntries = Object.entries(ctx.model?.headers ?? {}).filter(
288
+ ([name]) => name.toLowerCase() === "anthropic-beta",
289
+ );
290
+ const modelBetas = matchingModelEntries.flatMap(([, value]) => splitBetaHeader(value));
291
+ const providerBetas = getAnthropicProviderBetas(ctx, hasActiveTools);
292
+ const oauthBetas = (await isAnthropicOAuth(ctx)) ? CLAUDE_CODE_OAUTH_BETAS : [];
293
+ const next = Array.from(
294
+ new Set([...modelBetas, ...existing, ...oauthBetas, ...providerBetas, ANTHROPIC_FAST_BETA]),
295
+ );
296
+
297
+ for (const key of matchingKeys) delete headers[key];
298
+ const outputKey = matchingModelEntries.at(-1)?.[0] ?? "anthropic-beta";
299
+ headers[outputKey] = next.join(",");
300
+ }
301
+
302
+ function injectFastPayload(
303
+ payload: unknown,
304
+ ctx: ExtensionContext,
305
+ state: SessionState,
306
+ ): PayloadRecord | undefined {
307
+ if (!isFastEnabled(state)) return undefined;
308
+ const eligibility = getEligibility(ctx);
309
+ if (!eligibility.eligible || !eligibility.provider) return undefined;
310
+ if (!isPayloadRecord(payload)) return undefined;
311
+ if (payload.model !== ctx.model?.id) return undefined;
312
+
313
+ if (eligibility.provider === "anthropic") {
314
+ if ("speed" in payload) return undefined;
315
+ state.lastInjectedAt = Date.now();
316
+ state.lastInjectedModel = eligibility.modelKey;
317
+ return { ...payload, speed: ANTHROPIC_FAST_SPEED };
318
+ }
319
+
320
+ if ("service_tier" in payload) return undefined;
321
+ state.lastInjectedAt = Date.now();
322
+ state.lastInjectedModel = eligibility.modelKey;
323
+ return { ...payload, service_tier: OPENAI_FAST_SERVICE_TIER };
324
+ }
325
+
326
+ export default function fastExtension(pi: ExtensionAPI) {
327
+ const states = new WeakMap<object, SessionState>();
328
+
329
+ function getState(ctx: ExtensionContext): SessionState {
330
+ let state = states.get(ctx.sessionManager);
331
+ if (!state) {
332
+ state = {
333
+ config: loadConfig(ctx),
334
+ override: "auto",
335
+ };
336
+ states.set(ctx.sessionManager, state);
337
+ }
338
+ return state;
339
+ }
340
+
341
+ pi.on("session_start", (_event, ctx) => {
342
+ const state: SessionState = {
343
+ config: loadConfig(ctx),
344
+ override: "auto",
345
+ };
346
+ states.set(ctx.sessionManager, state);
347
+ updateStatus(ctx, state);
348
+ });
349
+
350
+ pi.on("model_select", (_event, ctx) => {
351
+ updateStatus(ctx, getState(ctx));
352
+ });
353
+
354
+ pi.on("before_provider_headers", async (event, ctx) => {
355
+ const hasActiveTools = typeof pi.getActiveTools === "function" && pi.getActiveTools().length > 0;
356
+ await injectAnthropicFastHeader(event.headers, ctx, getState(ctx), hasActiveTools);
357
+ });
358
+
359
+ pi.on("before_provider_request", (event, ctx) => {
360
+ const state = getState(ctx);
361
+ const nextPayload = injectFastPayload(event.payload, ctx, state);
362
+ updateStatus(ctx, state);
363
+ return nextPayload;
364
+ });
365
+
366
+ pi.registerCommand("fast", {
367
+ description: "Toggle Fast mode for supported OpenAI Codex and Anthropic Claude models",
368
+ getArgumentCompletions: () => null,
369
+ handler: async (args, ctx) => {
370
+ const state = getState(ctx);
371
+ const action = args.trim();
372
+
373
+ if (!action) {
374
+ state.override = isFastEnabled(state) ? "off" : "on";
375
+ updateStatus(ctx, state);
376
+ ctx.ui.notify(getStatusMessage(ctx, state), "info");
377
+ return;
378
+ }
379
+
380
+ ctx.ui.notify("Usage: /fast", "warning");
381
+ },
382
+ });
383
+ }
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@diegopetrucci/pi-fast",
3
+ "version": "0.1.0",
4
+ "description": "A pi extension that enables Fast mode for supported OpenAI Codex and Anthropic Claude models based on the currently selected model.",
5
+ "keywords": [
6
+ "pi-package",
7
+ "pi",
8
+ "openai",
9
+ "anthropic",
10
+ "fast"
11
+ ],
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/diegopetrucci/pi-extensions.git",
16
+ "directory": "extensions/fast"
17
+ },
18
+ "files": [
19
+ "index.ts",
20
+ "fast.example.json",
21
+ "README.md",
22
+ ".pi-fleet-tested-version"
23
+ ],
24
+ "publishConfig": {
25
+ "access": "public"
26
+ },
27
+ "pi": {
28
+ "extensions": [
29
+ "index.ts"
30
+ ]
31
+ },
32
+ "peerDependencies": {
33
+ "@earendil-works/pi-coding-agent": "*"
34
+ },
35
+ "type": "module",
36
+ "engines": {
37
+ "node": ">=22.19.0"
38
+ }
39
+ }
@@ -1 +1 @@
1
- 0.82.1
1
+ 0.83.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-git-footer",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "A TLH-style git status add-on for pi's built-in footer.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1 +1 @@
1
- 0.82.1
1
+ 0.83.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-gnosis",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "A pi extension that exposes the gnosis repo-local knowledge base CLI as an agent tool.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1 +1 @@
1
- 0.82.1
1
+ 0.83.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-illustrations-to-explain-things",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "description": "A pi skill for generating clean, absurd Xiaohei-style article illustrations, shot lists, and visual metaphors.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1 +1 @@
1
- 0.82.1
1
+ 0.83.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-inline-bash",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "A pi extension that expands inline bash commands in user prompts.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1 +1 @@
1
- 0.82.1
1
+ 0.83.0
@@ -4,7 +4,7 @@ A pi GitHub research scout inspired by `pi-librarian`, with a local checkout cac
4
4
 
5
5
  When the `librarian` tool runs, it can cache/reuse repository checkouts locally. Use `/librarian-cache off` to force GitHub API/search and temporary fetched files only, or `/librarian-cache on` to re-enable cached local checkouts.
6
6
 
7
- The internal librarian subagent uses a fast auto-selected model by default, requests `low` thinking, and prompts its scout to batch independent GitHub probes in parallel. Use `/librarian-config` to set a persistent internal model or thinking-level preference.
7
+ The internal librarian subagent uses a fast auto-selected model by default, requests `low` thinking, and prompts its scout to batch independent GitHub probes in parallel. Automatic candidates and fallbacks stay within the current Pi session's model scope when that scope is non-empty; an explicit tool or `/librarian-config` model remains authoritative. Use `/librarian-config` to set a persistent internal model or thinking-level preference.
8
8
 
9
9
  ## Install
10
10
 
@@ -46,6 +46,31 @@ type PiModel = {
46
46
  thinkingLevelMap?: ThinkingLevelMap;
47
47
  };
48
48
 
49
+ type ModelSelectionContext = {
50
+ model?: any;
51
+ modelRegistry: { getAvailable(): any[] | Promise<any[]> };
52
+ scopedModels?: ReadonlyArray<{ model: any; thinkingLevel?: ThinkingLevel }>;
53
+ };
54
+
55
+ function modelKey(model: any): string {
56
+ return `${model.provider}/${model.id}`.toLowerCase();
57
+ }
58
+
59
+ function hasModelScope(ctx: ModelSelectionContext): boolean {
60
+ return Array.isArray(ctx.scopedModels) && ctx.scopedModels.length > 0;
61
+ }
62
+
63
+ function isModelInScope(ctx: ModelSelectionContext, model: any): boolean {
64
+ if (!hasModelScope(ctx)) return true;
65
+ const key = modelKey(model);
66
+ return ctx.scopedModels!.some((entry) => modelKey(entry.model) === key);
67
+ }
68
+
69
+ async function getAutoSelectableModels(ctx: ModelSelectionContext): Promise<any[]> {
70
+ const available = await ctx.modelRegistry.getAvailable();
71
+ return hasModelScope(ctx) ? available.filter((model) => isModelInScope(ctx, model)) : available;
72
+ }
73
+
49
74
  type CreateAgentSessionOptions = NonNullable<Parameters<typeof createAgentSession>[0]>;
50
75
 
51
76
  function getModelRuntimeOption(ctx: { modelRegistry?: unknown }): Pick<CreateAgentSessionOptions, "modelRuntime"> {
@@ -677,7 +702,7 @@ function rankPreferredFastLibrarianModel(model: any): number {
677
702
  }
678
703
 
679
704
  async function findAvailableModel(
680
- ctx: { model?: any; modelRegistry: { getAvailable(): any[] | Promise<any[]> } },
705
+ ctx: ModelSelectionContext,
681
706
  modelPreference: string,
682
707
  ): Promise<any | undefined> {
683
708
  const available = await ctx.modelRegistry.getAvailable();
@@ -732,7 +757,7 @@ function withLibrarianFallbackReason(candidate: LibrarianCandidate, previous: Li
732
757
  // these in order and skip ones that fail with a model-availability error,
733
758
  // ultimately falling back to the known-good current session model.
734
759
  async function buildLibrarianCandidates(
735
- ctx: { model?: any; modelRegistry: { getAvailable(): any[] | Promise<any[]> } },
760
+ ctx: ModelSelectionContext,
736
761
  modelPreference: string | undefined,
737
762
  thinkingLevel: ThinkingLevel,
738
763
  ): Promise<LibrarianCandidate[]> {
@@ -762,7 +787,7 @@ async function buildLibrarianCandidates(
762
787
  }
763
788
  }
764
789
 
765
- const available = await ctx.modelRegistry.getAvailable();
790
+ const available = await getAutoSelectableModels(ctx);
766
791
  const preferred = available.filter(isPreferredFastLibrarianModel);
767
792
  const ranked = preferred.length > 0
768
793
  ? [...preferred].sort((a, b) => rankPreferredFastLibrarianModel(a) - rankPreferredFastLibrarianModel(b))
@@ -785,7 +810,7 @@ async function buildLibrarianCandidates(
785
810
  });
786
811
 
787
812
  // Final fallback: the active session model is known to be servable.
788
- if (ctx.model) {
813
+ if (ctx.model && isModelInScope(ctx, ctx.model)) {
789
814
  push(ctx.model, {
790
815
  modelRef: modelRef(ctx.model),
791
816
  provider: ctx.model.provider,
@@ -797,7 +822,11 @@ async function buildLibrarianCandidates(
797
822
  }
798
823
 
799
824
  if (candidates.length === 0) {
800
- throw new Error("No authenticated models are available for Librarian. Log in or configure an API key first.");
825
+ throw new Error(
826
+ hasModelScope(ctx)
827
+ ? "No authenticated models are available for Librarian in the current session model scope. Adjust the scope, log in, or configure an API key."
828
+ : "No authenticated models are available for Librarian. Log in or configure an API key first.",
829
+ );
801
830
  }
802
831
 
803
832
  return candidates;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-librarian",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "A pi GitHub research scout with a toggleable local repo checkout cache under the user's OS cache directory.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1 +1 @@
1
- 0.82.1
1
+ 0.83.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-minimal-footer",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "A minimal custom footer for pi.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1 +1 @@
1
- 0.82.1
1
+ 0.83.0
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-notify",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "A pi extension that sends a notification when the agent is ready for input.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1 +1 @@
1
- 0.82.1
1
+ 0.83.0
@@ -2,6 +2,8 @@
2
2
 
3
3
  A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4, GPT-5.5, and GPT-5.6 Codex variants (`gpt-5.6-sol`, `gpt-5.6-terra`, `gpt-5.6-luna`).
4
4
 
5
+ This package is standalone-only and is not auto-loaded by the `@diegopetrucci/pi-extensions` collection package. The collection uses the unified [`fast`](../fast) extension instead.
6
+
5
7
  When active, the extension injects this into eligible OpenAI Codex request payloads:
6
8
 
7
9
  ```json
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-openai-fast",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
4
4
  "description": "A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4, GPT-5.5, and GPT-5.6 Codex variants by injecting the priority service tier.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1 +1 @@
1
- 0.82.1
1
+ 0.83.0
@@ -106,6 +106,8 @@ Tool-call parameters still win over these defaults. `auto` clears the configured
106
106
  - `thinkingLevel` - optional reasoning/thinking override; falls back to the `/oracle thinking` default, then built-in defaults
107
107
  - `cwd` - optional working directory override
108
108
 
109
+ When the current Pi session has a non-empty model scope, Oracle limits automatic model selection and automatic fallbacks to that scope. An explicit tool or `/oracle model` override remains authoritative even when it is outside the session scope.
110
+
109
111
  ## Notes
110
112
 
111
113
  - The oracle is intentionally **read-only by default**.