@diegopetrucci/pi-extensions 0.1.45 → 0.1.47
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/extensions/brrr/README.md +2 -0
- package/extensions/brrr/index.ts +17 -5
- package/extensions/brrr/package.json +1 -1
- package/extensions/claude-fast/README.md +1 -1
- package/extensions/claude-fast/index.ts +17 -6
- package/extensions/claude-fast/package.json +1 -1
- package/extensions/librarian/index.ts +67 -13
- package/extensions/minimal-footer/README.md +2 -0
- package/extensions/minimal-footer/index.ts +16 -5
- package/extensions/minimal-footer/package.json +1 -1
- package/extensions/notify/README.md +2 -0
- package/extensions/notify/index.ts +17 -4
- package/extensions/notify/package.json +1 -1
- package/extensions/openai-fast/README.md +1 -1
- package/extensions/openai-fast/index.ts +17 -6
- package/extensions/openai-fast/package.json +1 -1
- package/extensions/oracle/index.ts +41 -0
- package/extensions/oracle/package.json +1 -1
- package/extensions/review/README.md +1 -1
- package/extensions/review/index.ts +15 -4
- package/extensions/review/package.json +1 -1
- package/package.json +6 -1
|
@@ -35,6 +35,8 @@ Config files are merged, with project config overriding global config:
|
|
|
35
35
|
- `~/.pi/agent/extensions/brrr.json`
|
|
36
36
|
- `<project>/.pi/brrr.json`
|
|
37
37
|
|
|
38
|
+
Project config is only read after Pi reports that the project is trusted.
|
|
39
|
+
|
|
38
40
|
The default config expects your webhook in `BRRR_WEBHOOK_URL`:
|
|
39
41
|
|
|
40
42
|
```bash
|
package/extensions/brrr/index.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Config files (project overrides global):
|
|
8
8
|
* - ~/.pi/agent/extensions/brrr.json
|
|
9
|
-
* - <cwd>/.pi/brrr.json
|
|
9
|
+
* - <cwd>/.pi/brrr.json, when the project is trusted
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { execFile } from "node:child_process";
|
|
@@ -16,6 +16,7 @@ import { promisify } from "node:util";
|
|
|
16
16
|
import { getAgentDir, type ExtensionAPI, type ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
17
17
|
|
|
18
18
|
const execFileAsync = promisify(execFile);
|
|
19
|
+
const CONFIG_DIR_NAME = ".pi";
|
|
19
20
|
|
|
20
21
|
interface BrrrConfig {
|
|
21
22
|
enabled: boolean;
|
|
@@ -40,6 +41,11 @@ interface BrrrPayload {
|
|
|
40
41
|
image_url?: string;
|
|
41
42
|
}
|
|
42
43
|
|
|
44
|
+
type ProjectConfigContext = {
|
|
45
|
+
cwd: string;
|
|
46
|
+
isProjectTrusted?: () => boolean;
|
|
47
|
+
};
|
|
48
|
+
|
|
43
49
|
const DEFAULT_CONFIG: BrrrConfig = {
|
|
44
50
|
enabled: true,
|
|
45
51
|
onlyWhenInteractive: true,
|
|
@@ -71,9 +77,15 @@ function mergeConfig(base: BrrrConfig, overrides: Partial<BrrrConfig>): BrrrConf
|
|
|
71
77
|
};
|
|
72
78
|
}
|
|
73
79
|
|
|
74
|
-
function
|
|
80
|
+
function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
|
|
81
|
+
return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadConfig(ctx: ProjectConfigContext): BrrrConfig {
|
|
75
85
|
const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "brrr.json"));
|
|
76
|
-
const projectConfig =
|
|
86
|
+
const projectConfig = canReadProjectConfig(ctx)
|
|
87
|
+
? readConfigFile(join(ctx.cwd, CONFIG_DIR_NAME, "brrr.json"))
|
|
88
|
+
: {};
|
|
77
89
|
return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
|
|
78
90
|
}
|
|
79
91
|
|
|
@@ -219,13 +231,13 @@ export default function brrrExtension(pi: ExtensionAPI) {
|
|
|
219
231
|
pi.registerCommand("brrr", {
|
|
220
232
|
description: "Show brrr notification status",
|
|
221
233
|
handler: async (_args, ctx) => {
|
|
222
|
-
const config = loadConfig(ctx
|
|
234
|
+
const config = loadConfig(ctx);
|
|
223
235
|
notify(ctx, describeConfig(config, resolveWebhook(config.webhook)));
|
|
224
236
|
},
|
|
225
237
|
});
|
|
226
238
|
|
|
227
239
|
pi.on("agent_end", async (event, ctx) => {
|
|
228
|
-
const config = loadConfig(ctx
|
|
240
|
+
const config = loadConfig(ctx);
|
|
229
241
|
if (!config.enabled) return;
|
|
230
242
|
if (config.onlyWhenInteractive && !ctx.hasUI) return;
|
|
231
243
|
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
type ExtensionContext,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
+
const CONFIG_DIR_NAME = ".pi";
|
|
10
|
+
|
|
9
11
|
const EXTENSION_ID = "claude-fast";
|
|
10
12
|
const PROVIDER_ID = "anthropic";
|
|
11
13
|
const API_ID = "anthropic-messages";
|
|
@@ -35,6 +37,11 @@ type SessionState = {
|
|
|
35
37
|
lastInjectedModel?: string;
|
|
36
38
|
};
|
|
37
39
|
|
|
40
|
+
type ProjectConfigContext = {
|
|
41
|
+
cwd: string;
|
|
42
|
+
isProjectTrusted?: () => boolean;
|
|
43
|
+
};
|
|
44
|
+
|
|
38
45
|
type RecursivePartial<T> = {
|
|
39
46
|
[P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
|
|
40
47
|
};
|
|
@@ -77,21 +84,25 @@ function mergeConfig(
|
|
|
77
84
|
};
|
|
78
85
|
}
|
|
79
86
|
|
|
87
|
+
function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
|
|
88
|
+
return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
|
|
89
|
+
}
|
|
90
|
+
|
|
80
91
|
function findProjectConfigPath(cwd: string): string {
|
|
81
92
|
let current = cwd;
|
|
82
93
|
while (true) {
|
|
83
|
-
const candidate = join(current,
|
|
94
|
+
const candidate = join(current, CONFIG_DIR_NAME, "claude-fast.json");
|
|
84
95
|
if (existsSync(candidate)) return candidate;
|
|
85
96
|
|
|
86
97
|
const parent = dirname(current);
|
|
87
|
-
if (parent === current) return join(cwd,
|
|
98
|
+
if (parent === current) return join(cwd, CONFIG_DIR_NAME, "claude-fast.json");
|
|
88
99
|
current = parent;
|
|
89
100
|
}
|
|
90
101
|
}
|
|
91
102
|
|
|
92
|
-
function loadConfig(
|
|
103
|
+
function loadConfig(ctx: ProjectConfigContext): ClaudeFastConfig {
|
|
93
104
|
const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "claude-fast.json"));
|
|
94
|
-
const projectConfig = readConfigFile(findProjectConfigPath(cwd));
|
|
105
|
+
const projectConfig = canReadProjectConfig(ctx) ? readConfigFile(findProjectConfigPath(ctx.cwd)) : {};
|
|
95
106
|
return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
|
|
96
107
|
}
|
|
97
108
|
|
|
@@ -235,7 +246,7 @@ export default function claudeFastExtension(pi: ExtensionAPI) {
|
|
|
235
246
|
let state = states.get(ctx.sessionManager);
|
|
236
247
|
if (!state) {
|
|
237
248
|
state = {
|
|
238
|
-
config: loadConfig(ctx
|
|
249
|
+
config: loadConfig(ctx),
|
|
239
250
|
override: "auto",
|
|
240
251
|
};
|
|
241
252
|
states.set(ctx.sessionManager, state);
|
|
@@ -245,7 +256,7 @@ export default function claudeFastExtension(pi: ExtensionAPI) {
|
|
|
245
256
|
|
|
246
257
|
pi.on("session_start", (_event, ctx) => {
|
|
247
258
|
const state: SessionState = {
|
|
248
|
-
config: loadConfig(ctx
|
|
259
|
+
config: loadConfig(ctx),
|
|
249
260
|
override: "auto",
|
|
250
261
|
};
|
|
251
262
|
states.set(ctx.sessionManager, state);
|
|
@@ -629,20 +629,56 @@ function buildUserPrompt(query: string, repos: string[], owners: string[], maxSe
|
|
|
629
629
|
return `Task: locate and cite exact GitHub code locations that answer the query.\n\nQuery: ${query}\nRepository filters: ${repos.length ? repos.join(", ") : "(none)"}\nOwner filters: ${owners.length ? owners.join(", ") : "(none)"}\nMax search results per gh search call: ${maxSearchResults}\nLocal checkout cache: ${cache.mode === "enabled" ? `enabled at ${cache.root}` : "disabled"}\nCache decision: ${cache.decisionReason}\n\nRespond directly with concise, citation-heavy findings. Always pass --limit ${maxSearchResults} to gh search code unless a narrower command is clearly better.`;
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
-
|
|
632
|
+
type AssistantLikeMessage = {
|
|
633
|
+
role?: string;
|
|
634
|
+
content?: unknown;
|
|
635
|
+
stopReason?: unknown;
|
|
636
|
+
errorMessage?: unknown;
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
function getLastAssistantMessage(messages: unknown[]): AssistantLikeMessage | undefined {
|
|
633
640
|
for (let i = messages.length - 1; i >= 0; i--) {
|
|
634
|
-
const message = messages[i] as
|
|
635
|
-
if (message?.role
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
641
|
+
const message = messages[i] as AssistantLikeMessage;
|
|
642
|
+
if (message?.role === "assistant") return message;
|
|
643
|
+
}
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
function extractLastAssistantText(messages: unknown[]): string {
|
|
648
|
+
const message = getLastAssistantMessage(messages);
|
|
649
|
+
if (!message || !Array.isArray(message.content)) return "";
|
|
650
|
+
const parts: string[] = [];
|
|
651
|
+
for (const part of message.content) {
|
|
652
|
+
if (part && typeof part === "object" && (part as { type?: string }).type === "text") {
|
|
653
|
+
const text = (part as { text?: unknown }).text;
|
|
654
|
+
if (typeof text === "string") parts.push(text);
|
|
642
655
|
}
|
|
643
|
-
if (parts.length) return parts.join("").trim();
|
|
644
656
|
}
|
|
645
|
-
return "";
|
|
657
|
+
return parts.join("").trim();
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function describeNoAnswerReason(message: AssistantLikeMessage | undefined, turns: number): string {
|
|
661
|
+
if (!message) return "the internal subagent produced no assistant message";
|
|
662
|
+
if (message.stopReason === "error") {
|
|
663
|
+
const errorMessage = typeof message.errorMessage === "string" && message.errorMessage.trim()
|
|
664
|
+
? message.errorMessage.trim()
|
|
665
|
+
: "provider/model error";
|
|
666
|
+
return `the internal subagent stopped with an error: ${errorMessage}`;
|
|
667
|
+
}
|
|
668
|
+
if (message.stopReason === "aborted") return "the internal subagent was aborted before producing an answer";
|
|
669
|
+
if (turns >= MAX_TURNS) return `the internal subagent reached the ${MAX_TURNS}-turn budget without producing a final answer`;
|
|
670
|
+
const stopReason = typeof message.stopReason === "string" && message.stopReason.trim()
|
|
671
|
+
? ` (stopReason: ${message.stopReason.trim()})`
|
|
672
|
+
: "";
|
|
673
|
+
return `the internal subagent completed without final assistant text${stopReason}`;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
function formatInternalFailure(details: LibrarianDetails, reason: string): string {
|
|
677
|
+
return [
|
|
678
|
+
`Internal librarian run failed: ${reason}.`,
|
|
679
|
+
`Diagnostics: status=${details.status}; model=${details.model.modelRef}; thinking=${details.model.thinkingLevel}; modelSelection=${details.model.autoSelected ? "auto" : "configured"}; turns=${details.turns}; toolCalls=${details.toolCalls.length}.`,
|
|
680
|
+
"No answer was produced. This is not a reliable \"no results found\" signal; retry later or configure a different Librarian model with /librarian-config.",
|
|
681
|
+
].join("\n");
|
|
646
682
|
}
|
|
647
683
|
|
|
648
684
|
function formatToolCall(call: ToolCall): string {
|
|
@@ -847,6 +883,13 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
847
883
|
},
|
|
848
884
|
});
|
|
849
885
|
|
|
886
|
+
pi.on("tool_result", async (event) => {
|
|
887
|
+
if (event.toolName !== "librarian") return undefined;
|
|
888
|
+
const details = event.details as LibrarianDetails | undefined;
|
|
889
|
+
if (details?.status !== "error") return undefined;
|
|
890
|
+
return { isError: true };
|
|
891
|
+
});
|
|
892
|
+
|
|
850
893
|
pi.registerTool({
|
|
851
894
|
name: "librarian",
|
|
852
895
|
label: "Librarian",
|
|
@@ -1034,9 +1077,20 @@ export default function librarianExtension(pi: ExtensionAPI) {
|
|
|
1034
1077
|
await Promise.race([promptPromise, timeoutPromise]);
|
|
1035
1078
|
}
|
|
1036
1079
|
|
|
1080
|
+
const lastAssistant = session ? getLastAssistantMessage(session.state.messages) : undefined;
|
|
1037
1081
|
const answer = session ? extractLastAssistantText(session.state.messages) : "";
|
|
1038
|
-
|
|
1039
|
-
|
|
1082
|
+
if (answer) {
|
|
1083
|
+
lastContent = answer;
|
|
1084
|
+
details.status = "done";
|
|
1085
|
+
} else if (aborted) {
|
|
1086
|
+
lastContent = "Aborted";
|
|
1087
|
+
details.status = "aborted";
|
|
1088
|
+
} else {
|
|
1089
|
+
details.status = "error";
|
|
1090
|
+
const reason = describeNoAnswerReason(lastAssistant, details.turns);
|
|
1091
|
+
lastContent = formatInternalFailure(details, reason);
|
|
1092
|
+
details.error = reason;
|
|
1093
|
+
}
|
|
1040
1094
|
details.endedAt = Date.now();
|
|
1041
1095
|
emit(true);
|
|
1042
1096
|
|
|
@@ -76,6 +76,8 @@ Config files are merged, with project config overriding global config:
|
|
|
76
76
|
- `~/.pi/agent/extensions/minimal-footer.json`
|
|
77
77
|
- `<project>/.pi/minimal-footer.json`
|
|
78
78
|
|
|
79
|
+
Project config is only read after Pi reports that the project is trusted.
|
|
80
|
+
|
|
79
81
|
A ready-to-copy sample file is included at [`minimal-footer.example.json`](./minimal-footer.example.json).
|
|
80
82
|
|
|
81
83
|
Example:
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
type UsageSnapshot,
|
|
15
15
|
} from "./openai-usage";
|
|
16
16
|
|
|
17
|
+
const CONFIG_DIR_NAME = ".pi";
|
|
18
|
+
|
|
17
19
|
const DEFAULT_CONFIG: MinimalFooterConfig = {
|
|
18
20
|
context: {
|
|
19
21
|
showPercent: true,
|
|
@@ -93,6 +95,11 @@ type UsageSessionState = {
|
|
|
93
95
|
requestRender?: () => void;
|
|
94
96
|
};
|
|
95
97
|
|
|
98
|
+
type ProjectConfigContext = {
|
|
99
|
+
cwd: string;
|
|
100
|
+
isProjectTrusted?: () => boolean;
|
|
101
|
+
};
|
|
102
|
+
|
|
96
103
|
function readConfigFile(path: string): RecursivePartial<MinimalFooterConfig> {
|
|
97
104
|
if (!existsSync(path)) return {};
|
|
98
105
|
|
|
@@ -177,21 +184,25 @@ function normalizeDumbZoneColor(value: unknown, fallback: DumbZoneColor): DumbZo
|
|
|
177
184
|
return DUMB_ZONE_COLORS.has(value as DumbZoneColor) ? (value as DumbZoneColor) : fallback;
|
|
178
185
|
}
|
|
179
186
|
|
|
187
|
+
function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
|
|
188
|
+
return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
|
|
189
|
+
}
|
|
190
|
+
|
|
180
191
|
function findProjectConfigPath(cwd: string): string {
|
|
181
192
|
let current = cwd;
|
|
182
193
|
while (true) {
|
|
183
|
-
const candidate = join(current,
|
|
194
|
+
const candidate = join(current, CONFIG_DIR_NAME, "minimal-footer.json");
|
|
184
195
|
if (existsSync(candidate)) return candidate;
|
|
185
196
|
|
|
186
197
|
const parent = dirname(current);
|
|
187
|
-
if (parent === current) return join(cwd,
|
|
198
|
+
if (parent === current) return join(cwd, CONFIG_DIR_NAME, "minimal-footer.json");
|
|
188
199
|
current = parent;
|
|
189
200
|
}
|
|
190
201
|
}
|
|
191
202
|
|
|
192
|
-
function loadConfig(
|
|
203
|
+
function loadConfig(ctx: ProjectConfigContext): MinimalFooterConfig {
|
|
193
204
|
const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "minimal-footer.json"));
|
|
194
|
-
const projectConfig = readConfigFile(findProjectConfigPath(cwd));
|
|
205
|
+
const projectConfig = canReadProjectConfig(ctx) ? readConfigFile(findProjectConfigPath(ctx.cwd)) : {};
|
|
195
206
|
return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
|
|
196
207
|
}
|
|
197
208
|
|
|
@@ -268,7 +279,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
268
279
|
pi.on("session_start", (_event, ctx) => {
|
|
269
280
|
const state: UsageSessionState = {
|
|
270
281
|
authStorage: AuthStorage.create(),
|
|
271
|
-
config: loadConfig(ctx
|
|
282
|
+
config: loadConfig(ctx),
|
|
272
283
|
loading: false,
|
|
273
284
|
};
|
|
274
285
|
states.set(ctx.sessionManager, state);
|
|
@@ -67,6 +67,8 @@ Config files are merged, with project config overriding global config:
|
|
|
67
67
|
- `~/.pi/agent/extensions/notify.json`
|
|
68
68
|
- `<project>/.pi/notify.json`
|
|
69
69
|
|
|
70
|
+
Project config is only read after Pi reports that the project is trusted.
|
|
71
|
+
|
|
70
72
|
A ready-to-copy sample file is included at [`notify.example.json`](./notify.example.json).
|
|
71
73
|
|
|
72
74
|
Example:
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* Config files (project overrides global):
|
|
12
12
|
* - ~/.pi/agent/extensions/notify.json
|
|
13
|
-
* - <cwd>/.pi/notify.json
|
|
13
|
+
* - <cwd>/.pi/notify.json, when the project is trusted
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import { execFile } from "node:child_process";
|
|
@@ -19,10 +19,17 @@ import { join } from "node:path";
|
|
|
19
19
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
20
20
|
import { getAgentDir } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
|
|
22
|
+
const CONFIG_DIR_NAME = ".pi";
|
|
23
|
+
|
|
22
24
|
type TerminalBackend = "auto" | "osc777" | "osc99" | "none";
|
|
23
25
|
type DesktopBackend = "auto" | "macos" | "linux" | "windows-toast" | "none";
|
|
24
26
|
type SoundBackend = "auto" | "macos" | "linux" | "windows-beep" | "command" | "none";
|
|
25
27
|
|
|
28
|
+
type ProjectConfigContext = {
|
|
29
|
+
cwd: string;
|
|
30
|
+
isProjectTrusted?: () => boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
26
33
|
interface NotifyConfig {
|
|
27
34
|
enabled: boolean;
|
|
28
35
|
onlyWhenInteractive: boolean;
|
|
@@ -111,9 +118,15 @@ function mergeConfig(base: NotifyConfig, overrides: Partial<NotifyConfig>): Noti
|
|
|
111
118
|
};
|
|
112
119
|
}
|
|
113
120
|
|
|
114
|
-
function
|
|
121
|
+
function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
|
|
122
|
+
return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function loadConfig(ctx: ProjectConfigContext): NotifyConfig {
|
|
115
126
|
const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "notify.json"));
|
|
116
|
-
const projectConfig =
|
|
127
|
+
const projectConfig = canReadProjectConfig(ctx)
|
|
128
|
+
? readConfigFile(join(ctx.cwd, CONFIG_DIR_NAME, "notify.json"))
|
|
129
|
+
: {};
|
|
117
130
|
return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
|
|
118
131
|
}
|
|
119
132
|
|
|
@@ -245,7 +258,7 @@ async function playSound(config: NotifyConfig, backend: Exclude<SoundBackend, "a
|
|
|
245
258
|
|
|
246
259
|
export default function notifyExtension(pi: ExtensionAPI) {
|
|
247
260
|
pi.on("agent_end", async (_event, ctx) => {
|
|
248
|
-
const config = loadConfig(ctx
|
|
261
|
+
const config = loadConfig(ctx);
|
|
249
262
|
if (!config.enabled) return;
|
|
250
263
|
if (config.onlyWhenInteractive && !ctx.hasUI) return;
|
|
251
264
|
|
|
@@ -6,6 +6,8 @@ import {
|
|
|
6
6
|
type ExtensionContext,
|
|
7
7
|
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
|
|
9
|
+
const CONFIG_DIR_NAME = ".pi";
|
|
10
|
+
|
|
9
11
|
const EXTENSION_ID = "openai-fast";
|
|
10
12
|
const PROVIDER_ID = "openai-codex";
|
|
11
13
|
const API_ID = "openai-codex-responses";
|
|
@@ -33,6 +35,11 @@ type SessionState = {
|
|
|
33
35
|
lastInjectedModel?: string;
|
|
34
36
|
};
|
|
35
37
|
|
|
38
|
+
type ProjectConfigContext = {
|
|
39
|
+
cwd: string;
|
|
40
|
+
isProjectTrusted?: () => boolean;
|
|
41
|
+
};
|
|
42
|
+
|
|
36
43
|
type RecursivePartial<T> = {
|
|
37
44
|
[P in keyof T]?: T[P] extends object ? RecursivePartial<T[P]> : T[P];
|
|
38
45
|
};
|
|
@@ -71,21 +78,25 @@ function mergeConfig(
|
|
|
71
78
|
};
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
|
|
82
|
+
return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
|
|
83
|
+
}
|
|
84
|
+
|
|
74
85
|
function findProjectConfigPath(cwd: string): string {
|
|
75
86
|
let current = cwd;
|
|
76
87
|
while (true) {
|
|
77
|
-
const candidate = join(current,
|
|
88
|
+
const candidate = join(current, CONFIG_DIR_NAME, "openai-fast.json");
|
|
78
89
|
if (existsSync(candidate)) return candidate;
|
|
79
90
|
|
|
80
91
|
const parent = dirname(current);
|
|
81
|
-
if (parent === current) return join(cwd,
|
|
92
|
+
if (parent === current) return join(cwd, CONFIG_DIR_NAME, "openai-fast.json");
|
|
82
93
|
current = parent;
|
|
83
94
|
}
|
|
84
95
|
}
|
|
85
96
|
|
|
86
|
-
function loadConfig(
|
|
97
|
+
function loadConfig(ctx: ProjectConfigContext): OpenAIFastConfig {
|
|
87
98
|
const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "openai-fast.json"));
|
|
88
|
-
const projectConfig = readConfigFile(findProjectConfigPath(cwd));
|
|
99
|
+
const projectConfig = canReadProjectConfig(ctx) ? readConfigFile(findProjectConfigPath(ctx.cwd)) : {};
|
|
89
100
|
return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
|
|
90
101
|
}
|
|
91
102
|
|
|
@@ -211,7 +222,7 @@ export default function openAIFastExtension(pi: ExtensionAPI) {
|
|
|
211
222
|
let state = states.get(ctx.sessionManager);
|
|
212
223
|
if (!state) {
|
|
213
224
|
state = {
|
|
214
|
-
config: loadConfig(ctx
|
|
225
|
+
config: loadConfig(ctx),
|
|
215
226
|
override: "auto",
|
|
216
227
|
};
|
|
217
228
|
states.set(ctx.sessionManager, state);
|
|
@@ -221,7 +232,7 @@ export default function openAIFastExtension(pi: ExtensionAPI) {
|
|
|
221
232
|
|
|
222
233
|
pi.on("session_start", (_event, ctx) => {
|
|
223
234
|
const state: SessionState = {
|
|
224
|
-
config: loadConfig(ctx
|
|
235
|
+
config: loadConfig(ctx),
|
|
225
236
|
override: "auto",
|
|
226
237
|
};
|
|
227
238
|
states.set(ctx.sessionManager, state);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-openai-fast",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "A pi extension that enables OpenAI Codex Fast mode for ChatGPT-auth GPT-5.4 and GPT-5.5 by injecting the priority service tier.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -557,6 +557,26 @@ function extractTextFromContent(content: unknown): string {
|
|
|
557
557
|
return parts.join("\n\n").trim();
|
|
558
558
|
}
|
|
559
559
|
|
|
560
|
+
// Errors that typically resolve on retry: provider overload, rate limiting,
|
|
561
|
+
// transient 5xx, gateway/network failures, and request timeouts. Keep this list
|
|
562
|
+
// pattern-based so we recognize variants across providers without enumerating them.
|
|
563
|
+
const TRANSIENT_ERROR_PATTERN =
|
|
564
|
+
/\b(overload(?:ed)?|rate[ _-]?limit(?:ed)?|too many requests|429|500|502|503|504|bad gateway|service unavailable|gateway timeout|temporarily unavailable|timed?[ _-]?out|timeout|econnreset|econnrefused|etimedout|enetunreach|socket hang up|fetch failed)\b/i;
|
|
565
|
+
|
|
566
|
+
function isTransientErrorMessage(message: string): boolean {
|
|
567
|
+
return TRANSIENT_ERROR_PATTERN.test(message);
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function formatOracleModelError(stopReason: "error" | "aborted", errorMessage: string | undefined): string {
|
|
571
|
+
const trimmed = errorMessage?.trim();
|
|
572
|
+
if (stopReason === "aborted") {
|
|
573
|
+
return trimmed ? `Oracle model turn aborted: ${trimmed}` : "Oracle model turn aborted.";
|
|
574
|
+
}
|
|
575
|
+
if (!trimmed) return "Oracle model error (no detail provided by provider).";
|
|
576
|
+
const base = `Oracle model error: ${trimmed}`;
|
|
577
|
+
return isTransientErrorMessage(trimmed) ? `${base} (transient; retry may succeed)` : base;
|
|
578
|
+
}
|
|
579
|
+
|
|
560
580
|
function parseVersionScore(text: string): number {
|
|
561
581
|
const matches = text.match(/\d+(?:\.\d+){0,2}/g) ?? [];
|
|
562
582
|
let best = 0;
|
|
@@ -817,6 +837,9 @@ async function runOracle(
|
|
|
817
837
|
let finalOutput = "";
|
|
818
838
|
let stderr = "";
|
|
819
839
|
|
|
840
|
+
let lastStopReason: string | undefined;
|
|
841
|
+
let lastErrorMessage: string | undefined;
|
|
842
|
+
|
|
820
843
|
const details: OracleDetails = {
|
|
821
844
|
...selection,
|
|
822
845
|
includeBash,
|
|
@@ -888,6 +911,11 @@ async function runOracle(
|
|
|
888
911
|
if (text) finalOutput = text;
|
|
889
912
|
currentText = "";
|
|
890
913
|
|
|
914
|
+
const stopReason = event.message.stopReason;
|
|
915
|
+
lastStopReason = typeof stopReason === "string" ? stopReason : undefined;
|
|
916
|
+
const errorMessageField = event.message.errorMessage;
|
|
917
|
+
lastErrorMessage = typeof errorMessageField === "string" ? errorMessageField : undefined;
|
|
918
|
+
|
|
891
919
|
const messageUsage = event.message.usage;
|
|
892
920
|
if (messageUsage) {
|
|
893
921
|
usage.turns += 1;
|
|
@@ -946,6 +974,19 @@ async function runOracle(
|
|
|
946
974
|
return { ok: false, error: "Oracle was aborted.", details };
|
|
947
975
|
}
|
|
948
976
|
|
|
977
|
+
// The pi subprocess in `--mode json` does not promote an errored assistant
|
|
978
|
+
// turn to a non-zero exit code or stderr; the error is only carried on the
|
|
979
|
+
// streamed assistant message via stopReason/errorMessage. Surface that here
|
|
980
|
+
// so callers can distinguish transient provider errors from a genuinely
|
|
981
|
+
// empty response.
|
|
982
|
+
if (lastStopReason === "error" || lastStopReason === "aborted") {
|
|
983
|
+
return {
|
|
984
|
+
ok: false,
|
|
985
|
+
error: formatOracleModelError(lastStopReason, lastErrorMessage),
|
|
986
|
+
details,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
949
990
|
if (exitCode !== 0) {
|
|
950
991
|
return {
|
|
951
992
|
ok: false,
|
|
@@ -31,7 +31,7 @@ Then reload pi:
|
|
|
31
31
|
|
|
32
32
|
- Behavior is intentionally kept equivalent to the upstream source, with only packaging/attribution changes for this repository.
|
|
33
33
|
- PR review requires `gh` access and a clean working tree for tracked files.
|
|
34
|
-
- If a `REVIEW_GUIDELINES.md` file exists next to the repo's `.pi` directory, its contents are appended to the review prompt.
|
|
34
|
+
- If the project is trusted and a `REVIEW_GUIDELINES.md` file exists next to the repo's `.pi` directory, its contents are appended to the review prompt.
|
|
35
35
|
|
|
36
36
|
## License and attribution
|
|
37
37
|
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
* - `/review --extra "focus on performance regressions"` - add extra review instruction (works with any mode)
|
|
30
30
|
*
|
|
31
31
|
* Project-specific review guidelines:
|
|
32
|
-
* - If a REVIEW_GUIDELINES.md file exists in the
|
|
33
|
-
* its contents are appended to the review prompt.
|
|
32
|
+
* - If the project is trusted and a REVIEW_GUIDELINES.md file exists in the
|
|
33
|
+
* same directory as .pi, its contents are appended to the review prompt.
|
|
34
34
|
*
|
|
35
35
|
* Note: PR review requires a clean working tree (no uncommitted changes to tracked files).
|
|
36
36
|
*/
|
|
@@ -49,6 +49,8 @@ import {
|
|
|
49
49
|
import path from "node:path";
|
|
50
50
|
import { promises as fs } from "node:fs";
|
|
51
51
|
|
|
52
|
+
const CONFIG_DIR_NAME = ".pi";
|
|
53
|
+
|
|
52
54
|
// State to track fresh session review (where we branched from).
|
|
53
55
|
// Module-level state means only one review can be active at a time.
|
|
54
56
|
// This is intentional - the UI and /end-review command assume a single active review.
|
|
@@ -75,6 +77,15 @@ type ReviewSettingsState = {
|
|
|
75
77
|
customInstructions?: string;
|
|
76
78
|
};
|
|
77
79
|
|
|
80
|
+
type ProjectConfigContext = {
|
|
81
|
+
cwd: string;
|
|
82
|
+
isProjectTrusted?: () => boolean;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
|
|
86
|
+
return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
function setReviewWidget(ctx: ExtensionContext, active: boolean) {
|
|
79
90
|
if (!ctx.hasUI) return;
|
|
80
91
|
if (!active) {
|
|
@@ -501,7 +512,7 @@ async function loadProjectReviewGuidelines(cwd: string): Promise<string | null>
|
|
|
501
512
|
let currentDir = path.resolve(cwd);
|
|
502
513
|
|
|
503
514
|
while (true) {
|
|
504
|
-
const piDir = path.join(currentDir,
|
|
515
|
+
const piDir = path.join(currentDir, CONFIG_DIR_NAME);
|
|
505
516
|
const guidelinesPath = path.join(currentDir, "REVIEW_GUIDELINES.md");
|
|
506
517
|
|
|
507
518
|
const piStats = await fs.stat(piDir).catch(() => null);
|
|
@@ -1420,7 +1431,7 @@ export default function reviewExtension(pi: ExtensionAPI) {
|
|
|
1420
1431
|
includeLocalChanges: options?.includeLocalChanges === true,
|
|
1421
1432
|
});
|
|
1422
1433
|
const hint = getUserFacingHint(target);
|
|
1423
|
-
const projectGuidelines = await loadProjectReviewGuidelines(ctx.cwd);
|
|
1434
|
+
const projectGuidelines = canReadProjectConfig(ctx) ? await loadProjectReviewGuidelines(ctx.cwd) : null;
|
|
1424
1435
|
|
|
1425
1436
|
// Combine the review rubric with the specific prompt
|
|
1426
1437
|
let fullPrompt = `${REVIEW_RUBRIC}\n\n---\n\nPlease perform a code review with the following focus:\n\n${prompt}`;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@diegopetrucci/pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.47",
|
|
4
4
|
"description": "A collection of pi extensions and skills for annotation UIs, context management, workflow audits, review-comment triage, notifications, brrr push alerts, safety guards, GitHub research, repo-local knowledge, todos, tool rendering, model/provider helpers, and Xiaohei-style article illustrations.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
"type": "git",
|
|
14
14
|
"url": "git+https://github.com/diegopetrucci/pi-extensions.git"
|
|
15
15
|
},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"ci": "npm run preflight:install-state && npm run typecheck",
|
|
18
|
+
"preflight:install-state": "node scripts/check-install-state.mjs",
|
|
19
|
+
"typecheck": "npx tsc --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 --skipLibCheck $(git ls-files '*.ts')"
|
|
20
|
+
},
|
|
16
21
|
"files": [
|
|
17
22
|
"extensions",
|
|
18
23
|
"assets",
|