@diegopetrucci/pi-extensions 0.1.74 → 0.1.78

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 (108) hide show
  1. package/LICENSE +2 -0
  2. package/README.md +17 -12
  3. package/extensions/agent-workflow-audit/README.md +1 -1
  4. package/extensions/agent-workflow-audit/index.ts +1631 -1009
  5. package/extensions/agent-workflow-audit/package.json +1 -1
  6. package/extensions/annotate-git-diff/README.md +3 -1
  7. package/extensions/annotate-git-diff/clipboard.ts +109 -100
  8. package/extensions/annotate-git-diff/git.ts +846 -768
  9. package/extensions/annotate-git-diff/glimpseui.d.ts +87 -78
  10. package/extensions/annotate-git-diff/index.ts +712 -671
  11. package/extensions/annotate-git-diff/package.json +1 -1
  12. package/extensions/annotate-git-diff/prompt.ts +45 -45
  13. package/extensions/annotate-git-diff/quiet-glimpse.ts +161 -155
  14. package/extensions/annotate-git-diff/review-server.ts +337 -311
  15. package/extensions/annotate-git-diff/types.ts +132 -132
  16. package/extensions/annotate-git-diff/ui.ts +27 -27
  17. package/extensions/annotate-git-diff/watch.ts +74 -74
  18. package/extensions/annotate-git-diff/web/app.js +1947 -1884
  19. package/extensions/annotate-git-diff/web/index.html +5 -6
  20. package/extensions/annotate-last-message/README.md +1 -1
  21. package/extensions/annotate-last-message/glimpseui.d.ts +87 -78
  22. package/extensions/annotate-last-message/index.ts +210 -206
  23. package/extensions/annotate-last-message/package.json +1 -1
  24. package/extensions/annotate-last-message/prompt.ts +87 -66
  25. package/extensions/annotate-last-message/quiet-glimpse.ts +161 -155
  26. package/extensions/annotate-last-message/session.ts +94 -89
  27. package/extensions/annotate-last-message/theme.ts +199 -193
  28. package/extensions/annotate-last-message/types.ts +23 -23
  29. package/extensions/annotate-last-message/ui.ts +27 -25
  30. package/extensions/annotate-last-message/web/app.js +4 -7
  31. package/extensions/annotate-last-message/web/index.html +3 -3
  32. package/extensions/annotate-last-message/web/md-renderer.js +1 -1
  33. package/extensions/brrr/README.md +1 -1
  34. package/extensions/brrr/index.ts +213 -191
  35. package/extensions/brrr/package.json +1 -1
  36. package/extensions/claude-fast/README.md +5 -79
  37. package/extensions/claude-fast/index.ts +31 -289
  38. package/extensions/claude-fast/package.json +2 -3
  39. package/extensions/code-reviewer/README.md +6 -6
  40. package/extensions/code-reviewer/index.ts +1825 -1373
  41. package/extensions/code-reviewer/package.json +1 -1
  42. package/extensions/confirm-destructive/README.md +1 -1
  43. package/extensions/confirm-destructive/index.ts +50 -52
  44. package/extensions/confirm-destructive/package.json +1 -1
  45. package/extensions/context-cap/README.md +9 -9
  46. package/extensions/context-cap/index.ts +313 -305
  47. package/extensions/context-cap/package.json +2 -2
  48. package/extensions/context-inspector/index.ts +1211 -1064
  49. package/extensions/context-inspector/package.json +1 -1
  50. package/extensions/contrarian/README.md +4 -3
  51. package/extensions/contrarian/index.ts +1863 -1737
  52. package/extensions/contrarian/package.json +1 -1
  53. package/extensions/dirty-repo-guard/index.ts +37 -41
  54. package/extensions/dirty-repo-guard/package.json +1 -1
  55. package/extensions/dynamic-context-pruning/README.md +11 -43
  56. package/extensions/dynamic-context-pruning/index.ts +2131 -2105
  57. package/extensions/dynamic-context-pruning/package.json +1 -1
  58. package/extensions/fast/README.md +52 -14
  59. package/extensions/fast/index.ts +513 -317
  60. package/extensions/fast/package.json +1 -1
  61. package/extensions/git-footer/README.md +1 -1
  62. package/extensions/git-footer/index.ts +458 -411
  63. package/extensions/git-footer/package.json +2 -3
  64. package/extensions/gnosis/index.ts +180 -130
  65. package/extensions/gnosis/package.json +1 -1
  66. package/extensions/inline-bash/index.ts +87 -103
  67. package/extensions/inline-bash/package.json +1 -1
  68. package/extensions/librarian/README.md +1 -1
  69. package/extensions/librarian/index.ts +1877 -1419
  70. package/extensions/librarian/package.json +1 -1
  71. package/extensions/minimal-footer/README.md +5 -5
  72. package/extensions/minimal-footer/index.ts +987 -1012
  73. package/extensions/minimal-footer/minimal-footer.example.json +1 -1
  74. package/extensions/minimal-footer/openai-usage.ts +195 -190
  75. package/extensions/minimal-footer/package.json +1 -1
  76. package/extensions/notify/index.ts +222 -230
  77. package/extensions/notify/package.json +1 -1
  78. package/extensions/openai-fast/README.md +5 -73
  79. package/extensions/openai-fast/index.ts +32 -272
  80. package/extensions/openai-fast/package.json +2 -3
  81. package/extensions/oracle/README.md +4 -3
  82. package/extensions/oracle/index.ts +1740 -1627
  83. package/extensions/oracle/package.json +1 -1
  84. package/extensions/permission-gate/README.md +3 -0
  85. package/extensions/permission-gate/index.ts +785 -612
  86. package/extensions/permission-gate/package.json +1 -1
  87. package/extensions/permission-gate/powershell-safety.ts +840 -793
  88. package/extensions/project-mcp-json/.pi-fleet-tested-version +1 -0
  89. package/extensions/project-mcp-json/README.md +199 -0
  90. package/extensions/project-mcp-json/config.ts +564 -0
  91. package/extensions/project-mcp-json/index.ts +119 -0
  92. package/extensions/project-mcp-json/package.json +38 -0
  93. package/extensions/quiet-tools/.pi-fleet-tested-version +1 -1
  94. package/extensions/quiet-tools/README.md +30 -19
  95. package/extensions/quiet-tools/index.ts +376 -291
  96. package/extensions/quiet-tools/package.json +2 -2
  97. package/extensions/review/README.md +2 -2
  98. package/extensions/review/index.ts +1745 -1872
  99. package/extensions/review/package.json +1 -1
  100. package/extensions/todo/index.ts +280 -280
  101. package/extensions/todo/package.json +1 -1
  102. package/extensions/triage-comments/README.md +6 -4
  103. package/extensions/triage-comments/index.ts +2620 -1888
  104. package/extensions/triage-comments/package.json +1 -1
  105. package/package.json +9 -6
  106. package/assets/social-preview.png +0 -0
  107. package/extensions/claude-fast/claude-fast.example.json +0 -4
  108. package/extensions/openai-fast/openai-fast.example.json +0 -4
@@ -13,271 +13,293 @@ import { execFile } from "node:child_process";
13
13
  import { existsSync, readFileSync } from "node:fs";
14
14
  import { basename, join } from "node:path";
15
15
  import { promisify } from "node:util";
16
- import { CONFIG_DIR_NAME, getAgentDir, type ExtensionAPI, type ExtensionCommandContext, type SessionEntry } from "@earendil-works/pi-coding-agent";
16
+ import {
17
+ CONFIG_DIR_NAME,
18
+ getAgentDir,
19
+ type ExtensionAPI,
20
+ type ExtensionCommandContext,
21
+ type SessionEntry,
22
+ } from "@earendil-works/pi-coding-agent";
17
23
 
18
24
  const execFileAsync = promisify(execFile);
19
25
 
20
26
  interface BrrrConfig {
21
- enabled: boolean;
22
- onlyWhenInteractive: boolean;
23
- webhook: string;
24
- idleSeconds: number | null;
25
- title: string;
26
- message: string;
27
- includeLastAssistantMessage: boolean;
28
- sound: string;
29
- openUrl: string;
30
- imageUrl: string;
27
+ enabled: boolean;
28
+ onlyWhenInteractive: boolean;
29
+ webhook: string;
30
+ idleSeconds: number | null;
31
+ title: string;
32
+ message: string;
33
+ includeLastAssistantMessage: boolean;
34
+ sound: string;
35
+ openUrl: string;
36
+ imageUrl: string;
31
37
  }
32
38
 
33
39
  interface BrrrPayload {
34
- message: string;
35
- title?: string;
36
- subtitle?: string;
37
- expiration_date?: string;
38
- sound?: string;
39
- open_url?: string;
40
- image_url?: string;
40
+ message: string;
41
+ title?: string;
42
+ sound?: string;
43
+ open_url?: string;
44
+ image_url?: string;
41
45
  }
42
46
 
43
47
  type ProjectConfigContext = {
44
- cwd: string;
45
- isProjectTrusted?: () => boolean;
48
+ cwd: string;
49
+ isProjectTrusted?: () => boolean;
46
50
  };
47
51
 
48
52
  const DEFAULT_CONFIG: BrrrConfig = {
49
- enabled: true,
50
- onlyWhenInteractive: true,
51
- webhook: "$BRRR_WEBHOOK_URL",
52
- idleSeconds: 20,
53
- title: "Pi finished",
54
- message: "Pi finished working in '{project}'.",
55
- includeLastAssistantMessage: true,
56
- sound: "",
57
- openUrl: "",
58
- imageUrl: "",
53
+ enabled: true,
54
+ onlyWhenInteractive: true,
55
+ webhook: "$BRRR_WEBHOOK_URL",
56
+ idleSeconds: 20,
57
+ title: "Pi finished",
58
+ message: "Pi finished working in '{project}'.",
59
+ includeLastAssistantMessage: true,
60
+ sound: "",
61
+ openUrl: "",
62
+ imageUrl: "",
59
63
  };
60
64
 
61
65
  function readConfigFile(path: string): Partial<BrrrConfig> {
62
- if (!existsSync(path)) return {};
63
-
64
- try {
65
- return JSON.parse(readFileSync(path, "utf-8")) as Partial<BrrrConfig>;
66
- } catch (error) {
67
- console.error(`Warning: Could not parse ${path}: ${error}`);
68
- return {};
69
- }
66
+ if (!existsSync(path)) return {};
67
+
68
+ try {
69
+ return JSON.parse(readFileSync(path, "utf-8")) as Partial<BrrrConfig>;
70
+ } catch (error) {
71
+ console.error(`Warning: Could not parse ${path}: ${error}`);
72
+ return {};
73
+ }
70
74
  }
71
75
 
72
76
  function mergeConfig(base: BrrrConfig, overrides: Partial<BrrrConfig>): BrrrConfig {
73
- return {
74
- ...base,
75
- ...overrides,
76
- };
77
+ return {
78
+ ...base,
79
+ ...overrides,
80
+ };
77
81
  }
78
82
 
79
83
  function canReadProjectConfig(ctx: ProjectConfigContext): boolean {
80
- return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
84
+ return typeof ctx.isProjectTrusted === "function" && ctx.isProjectTrusted();
81
85
  }
82
86
 
83
87
  function loadConfig(ctx: ProjectConfigContext): BrrrConfig {
84
- const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "brrr.json"));
85
- const projectConfig = canReadProjectConfig(ctx)
86
- ? readConfigFile(join(ctx.cwd, CONFIG_DIR_NAME, "brrr.json"))
87
- : {};
88
- return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
88
+ const globalConfig = readConfigFile(join(getAgentDir(), "extensions", "brrr.json"));
89
+ const projectConfig = canReadProjectConfig(ctx)
90
+ ? readConfigFile(join(ctx.cwd, CONFIG_DIR_NAME, "brrr.json"))
91
+ : {};
92
+ return mergeConfig(mergeConfig(DEFAULT_CONFIG, globalConfig), projectConfig);
89
93
  }
90
94
 
91
95
  function resolveWebhook(raw: string): string | undefined {
92
- const value = raw.trim();
93
- if (!value) return undefined;
96
+ const value = raw.trim();
97
+ if (!value) return undefined;
94
98
 
95
- const envMatch = value.match(/^\$([A-Z0-9_]+)$/) ?? value.match(/^\$\{([A-Z0-9_]+)\}$/);
96
- if (envMatch) return process.env[envMatch[1]]?.trim() || undefined;
99
+ const envMatch = value.match(/^\$([A-Z0-9_]+)$/) ?? value.match(/^\$\{([A-Z0-9_]+)\}$/);
100
+ if (envMatch) return process.env[envMatch[1]]?.trim() || undefined;
97
101
 
98
- return value;
102
+ return value;
99
103
  }
100
104
 
101
105
  function isBrrrWebhookUrl(value: string): boolean {
102
- try {
103
- const url = new URL(value);
104
- return (
105
- url.protocol === "https:" &&
106
- (url.hostname === "api.brrr.now" || url.hostname === "dev.api.brrr.now") &&
107
- /^\/v1\/br_[A-Za-z0-9_]+$/.test(url.pathname)
108
- );
109
- } catch {
110
- return false;
111
- }
106
+ try {
107
+ const url = new URL(value);
108
+ return (
109
+ url.protocol === "https:" &&
110
+ (url.hostname === "api.brrr.now" || url.hostname === "dev.api.brrr.now") &&
111
+ /^\/v1\/br_[A-Za-z0-9_]+$/.test(url.pathname)
112
+ );
113
+ } catch {
114
+ return false;
115
+ }
112
116
  }
113
117
 
114
118
  async function getMacOsIdleSeconds(): Promise<number | null> {
115
- if (process.platform !== "darwin") return null;
116
-
117
- try {
118
- const { stdout } = await execFileAsync("ioreg", ["-c", "IOHIDSystem"], {
119
- timeout: 1000,
120
- maxBuffer: 1024 * 1024,
121
- });
122
- const match = stdout.match(/HIDIdleTime"\s*=\s*(\d+)/) ?? stdout.match(/HIDIdleTime\s+=\s+(\d+)/);
123
- if (!match) return null;
124
-
125
- const idleNanoseconds = Number(match[1]);
126
- if (!Number.isFinite(idleNanoseconds)) return null;
127
-
128
- return Math.floor(idleNanoseconds / 1_000_000_000);
129
- } catch {
130
- return null;
131
- }
119
+ if (process.platform !== "darwin") return null;
120
+
121
+ try {
122
+ const { stdout } = await execFileAsync("ioreg", ["-c", "IOHIDSystem"], {
123
+ timeout: 1000,
124
+ maxBuffer: 1024 * 1024,
125
+ });
126
+ const match =
127
+ stdout.match(/HIDIdleTime"\s*=\s*(\d+)/) ?? stdout.match(/HIDIdleTime\s+=\s+(\d+)/);
128
+ if (!match) return null;
129
+
130
+ const idleNanoseconds = Number(match[1]);
131
+ if (!Number.isFinite(idleNanoseconds)) return null;
132
+
133
+ return Math.floor(idleNanoseconds / 1_000_000_000);
134
+ } catch {
135
+ return null;
136
+ }
132
137
  }
133
138
 
134
139
  async function shouldSkipForIdleThreshold(idleSeconds: number | null): Promise<boolean> {
135
- if (idleSeconds === null) return false;
136
- if (!Number.isFinite(idleSeconds) || idleSeconds < 0) return false;
140
+ if (idleSeconds === null) return false;
141
+ if (!Number.isFinite(idleSeconds) || idleSeconds < 0) return false;
137
142
 
138
- const currentIdleSeconds = await getMacOsIdleSeconds();
139
- if (currentIdleSeconds === null) return false;
143
+ const currentIdleSeconds = await getMacOsIdleSeconds();
144
+ if (currentIdleSeconds === null) return false;
140
145
 
141
- return currentIdleSeconds < idleSeconds;
146
+ return currentIdleSeconds < idleSeconds;
142
147
  }
143
148
 
144
149
  function formatTemplate(template: string, cwd: string): string {
145
- const project = basename(cwd || process.cwd());
146
- return template.replaceAll("{project}", project).replaceAll("{cwd}", cwd);
150
+ const project = basename(cwd || process.cwd());
151
+ return template.replaceAll("{project}", project).replaceAll("{cwd}", cwd);
147
152
  }
148
153
 
149
154
  function extractTextContent(content: unknown): string {
150
- if (typeof content === "string") return content;
151
- if (!Array.isArray(content)) return "";
152
-
153
- const parts: string[] = [];
154
- for (const part of content) {
155
- if (typeof part === "string") {
156
- parts.push(part);
157
- continue;
158
- }
159
- if (typeof part !== "object" || part === null) continue;
160
-
161
- const typedPart = part as { type?: unknown; text?: unknown };
162
- if (typedPart.type !== undefined && typedPart.type !== "text") continue;
163
- if (typeof typedPart.text === "string") parts.push(typedPart.text);
164
- }
165
-
166
- return parts.join("\n");
155
+ if (typeof content === "string") return content;
156
+ if (!Array.isArray(content)) return "";
157
+
158
+ const parts: string[] = [];
159
+ for (const part of content) {
160
+ if (typeof part === "string") {
161
+ parts.push(part);
162
+ continue;
163
+ }
164
+ if (typeof part !== "object" || part === null) continue;
165
+
166
+ const typedPart = part as { type?: unknown; text?: unknown };
167
+ if (typedPart.type !== undefined && typedPart.type !== "text") continue;
168
+ if (typeof typedPart.text === "string") parts.push(typedPart.text);
169
+ }
170
+
171
+ return parts.join("\n");
167
172
  }
168
173
 
169
174
  function lastAssistantMessage(messages: readonly unknown[]): string | undefined {
170
- for (let index = messages.length - 1; index >= 0; index -= 1) {
171
- const message = messages[index];
172
- if (typeof message !== "object" || message === null) continue;
173
- if ((message as { role?: unknown }).role !== "assistant") continue;
175
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
176
+ const message = messages[index];
177
+ if (typeof message !== "object" || message === null) continue;
178
+ if ((message as { role?: unknown }).role !== "assistant") continue;
174
179
 
175
- const text = extractTextContent((message as { content?: unknown }).content).trim();
176
- return text || undefined;
177
- }
180
+ const text = extractTextContent((message as { content?: unknown }).content).trim();
181
+ return text || undefined;
182
+ }
178
183
 
179
- return undefined;
184
+ return undefined;
180
185
  }
181
186
 
182
187
  type SessionMessageSource = {
183
- buildContextEntries(): SessionEntry[];
188
+ buildContextEntries(): SessionEntry[];
184
189
  };
185
190
 
186
191
  function sessionMessages(sessionManager: SessionMessageSource | undefined): readonly unknown[] {
187
- if (!sessionManager) return [];
192
+ if (!sessionManager) return [];
188
193
 
189
- return sessionManager
190
- .buildContextEntries()
191
- .flatMap((entry: SessionEntry) => (entry.type === "message" ? [entry.message] : []));
194
+ return sessionManager
195
+ .buildContextEntries()
196
+ .flatMap((entry: SessionEntry) => (entry.type === "message" ? [entry.message] : []));
192
197
  }
193
198
 
194
199
  function resolveAssistantMessage(
195
- sessionManager: SessionMessageSource | undefined,
196
- fallbackMessages: readonly unknown[],
200
+ sessionManager: SessionMessageSource | undefined,
201
+ fallbackMessages: readonly unknown[],
197
202
  ): string | undefined {
198
- return lastAssistantMessage(sessionMessages(sessionManager)) ?? lastAssistantMessage(fallbackMessages);
203
+ return (
204
+ lastAssistantMessage(sessionMessages(sessionManager)) ?? lastAssistantMessage(fallbackMessages)
205
+ );
199
206
  }
200
207
 
201
208
  function truncateMessage(value: string): string {
202
- const trimmed = value.trim();
203
- if (trimmed.length <= 800) return trimmed;
204
- return `${trimmed.slice(0, 797).trimEnd()}...`;
209
+ const trimmed = value.trim();
210
+ if (trimmed.length <= 800) return trimmed;
211
+ return `${trimmed.slice(0, 797).trimEnd()}...`;
205
212
  }
206
213
 
207
- async function sendBrrr(webhook: string, payload: BrrrPayload): Promise<{ ok: boolean; error?: string }> {
208
- const controller = new AbortController();
209
- const timeout = setTimeout(() => controller.abort(), 2_000);
210
-
211
- try {
212
- const body: Record<string, string> = { message: payload.message };
213
- if (payload.title) body.title = payload.title;
214
- if (payload.subtitle) body.subtitle = payload.subtitle;
215
- if (payload.expiration_date) body.expiration_date = payload.expiration_date;
216
- if (payload.sound) body.sound = payload.sound;
217
- if (payload.open_url) body.open_url = payload.open_url;
218
- if (payload.image_url) body.image_url = payload.image_url;
219
-
220
- const response = await fetch(webhook, {
221
- method: "POST",
222
- headers: {
223
- "content-type": "application/json",
224
- accept: "application/json",
225
- },
226
- body: JSON.stringify(body),
227
- signal: controller.signal,
228
- });
229
-
230
- if (response.status === 202) return { ok: true };
231
- return { ok: false, error: `Unexpected response status ${response.status}.` };
232
- } catch (error) {
233
- return { ok: false, error: error instanceof Error ? error.message : "Unknown webhook failure." };
234
- } finally {
235
- clearTimeout(timeout);
236
- }
214
+ async function sendBrrr(
215
+ webhook: string,
216
+ payload: BrrrPayload,
217
+ ): Promise<{ ok: boolean; error?: string }> {
218
+ const controller = new AbortController();
219
+ const timeout = setTimeout(() => controller.abort(), 2_000);
220
+
221
+ try {
222
+ const body: Record<string, string> = { message: payload.message };
223
+ if (payload.title) body.title = payload.title;
224
+ if (payload.sound) body.sound = payload.sound;
225
+ if (payload.open_url) body.open_url = payload.open_url;
226
+ if (payload.image_url) body.image_url = payload.image_url;
227
+
228
+ const response = await fetch(webhook, {
229
+ method: "POST",
230
+ headers: {
231
+ "content-type": "application/json",
232
+ accept: "application/json",
233
+ },
234
+ body: JSON.stringify(body),
235
+ signal: controller.signal,
236
+ });
237
+
238
+ if (response.status === 202) return { ok: true };
239
+ return { ok: false, error: `Unexpected response status ${response.status}.` };
240
+ } catch (error) {
241
+ return {
242
+ ok: false,
243
+ error: error instanceof Error ? error.message : "Unknown webhook failure.",
244
+ };
245
+ } finally {
246
+ clearTimeout(timeout);
247
+ }
237
248
  }
238
249
 
239
- function notify(ctx: ExtensionCommandContext, message: string, type: "info" | "warning" | "error" = "info"): void {
240
- if (ctx.hasUI) ctx.ui.notify(message, type);
250
+ function notify(
251
+ ctx: ExtensionCommandContext,
252
+ message: string,
253
+ type: "info" | "warning" | "error" = "info",
254
+ ): void {
255
+ if (ctx.hasUI) ctx.ui.notify(message, type);
241
256
  }
242
257
 
243
258
  function describeConfig(config: BrrrConfig, webhook: string | undefined): string {
244
- const webhookStatus = webhook ? (isBrrrWebhookUrl(webhook) ? "configured" : "invalid") : "missing";
245
- const idle = config.idleSeconds === null ? "off" : `${config.idleSeconds}s`;
246
- return `brrr is ${config.enabled ? "enabled" : "disabled"}; webhook ${webhookStatus}; idle threshold ${idle}.`;
259
+ const webhookStatus = webhook
260
+ ? isBrrrWebhookUrl(webhook)
261
+ ? "configured"
262
+ : "invalid"
263
+ : "missing";
264
+ const idle = config.idleSeconds === null ? "off" : `${config.idleSeconds}s`;
265
+ return `brrr is ${config.enabled ? "enabled" : "disabled"}; webhook ${webhookStatus}; idle threshold ${idle}.`;
247
266
  }
248
267
 
249
268
  export default function brrrExtension(pi: ExtensionAPI) {
250
- pi.registerCommand("brrr", {
251
- description: "Show brrr notification status",
252
- handler: async (_args, ctx) => {
253
- const config = loadConfig(ctx);
254
- notify(ctx, describeConfig(config, resolveWebhook(config.webhook)));
255
- },
256
- });
257
-
258
- pi.on("agent_settled", async (event, ctx) => {
259
- const config = loadConfig(ctx);
260
- if (!config.enabled) return;
261
- if (config.onlyWhenInteractive && !ctx.hasUI) return;
262
-
263
- const webhook = resolveWebhook(config.webhook);
264
- if (!webhook || !isBrrrWebhookUrl(webhook)) return;
265
- if (await shouldSkipForIdleThreshold(config.idleSeconds)) return;
266
-
267
- const assistantMessage = config.includeLastAssistantMessage
268
- ? resolveAssistantMessage(ctx.sessionManager, (event as { messages?: readonly unknown[] }).messages ?? [])
269
- : undefined;
270
- const message = truncateMessage(assistantMessage || formatTemplate(config.message, ctx.cwd));
271
- const result = await sendBrrr(webhook, {
272
- title: formatTemplate(config.title, ctx.cwd),
273
- message,
274
- sound: config.sound.trim() || undefined,
275
- open_url: config.openUrl.trim() || undefined,
276
- image_url: config.imageUrl.trim() || undefined,
277
- });
278
-
279
- if (!result.ok && result.error) {
280
- console.error(`brrr notification failed: ${result.error}`);
281
- }
282
- });
269
+ pi.registerCommand("brrr", {
270
+ description: "Show brrr notification status",
271
+ handler: async (_args, ctx) => {
272
+ const config = loadConfig(ctx);
273
+ notify(ctx, describeConfig(config, resolveWebhook(config.webhook)));
274
+ },
275
+ });
276
+
277
+ pi.on("agent_settled", async (event, ctx) => {
278
+ const config = loadConfig(ctx);
279
+ if (!config.enabled) return;
280
+ if (config.onlyWhenInteractive && !ctx.hasUI) return;
281
+
282
+ const webhook = resolveWebhook(config.webhook);
283
+ if (!webhook || !isBrrrWebhookUrl(webhook)) return;
284
+ if (await shouldSkipForIdleThreshold(config.idleSeconds)) return;
285
+
286
+ const assistantMessage = config.includeLastAssistantMessage
287
+ ? resolveAssistantMessage(
288
+ ctx.sessionManager,
289
+ (event as { messages?: readonly unknown[] }).messages ?? [],
290
+ )
291
+ : undefined;
292
+ const message = truncateMessage(assistantMessage || formatTemplate(config.message, ctx.cwd));
293
+ const result = await sendBrrr(webhook, {
294
+ title: formatTemplate(config.title, ctx.cwd),
295
+ message,
296
+ sound: config.sound.trim() || undefined,
297
+ open_url: config.openUrl.trim() || undefined,
298
+ image_url: config.imageUrl.trim() || undefined,
299
+ });
300
+
301
+ if (!result.ok && result.error) {
302
+ console.error(`brrr notification failed: ${result.error}`);
303
+ }
304
+ });
283
305
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@diegopetrucci/pi-brrr",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "A pi extension that sends brrr push notifications when pi is ready for input.",
5
5
  "keywords": [
6
6
  "pi-package",
@@ -1,88 +1,21 @@
1
1
  # claude-fast
2
2
 
3
- A pi extension that enables Anthropic Claude Fast mode for supported Claude Opus models.
3
+ Deprecated standalone leftover. This package no longer enables Claude Fast mode, registers `/claude-fast`, or reads `claude-fast.json`. On startup it clears a leftover `claude-fast` footer status and removes `fast-mode-2026-02-01` from the current model header. It does not inject Fast fields into requests.
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.
5
+ Fast mode lives in the unified [`fast`](../fast) extension. Install `@diegopetrucci/pi-fast`, or install the collection, which loads `extensions/fast` only.
6
6
 
7
- When active, the extension injects this into eligible Anthropic request payloads:
8
-
9
- ```json
10
- {
11
- "speed": "fast"
12
- }
13
- ```
14
-
15
- It also adds the required Anthropic beta header value to the current model before requests:
16
-
17
- ```text
18
- anthropic-beta: fast-mode-2026-02-01
19
- ```
20
-
21
- ## Eligibility
22
-
23
- Fast mode is only injected when all of these are true:
24
-
25
- - The current provider is `anthropic`.
26
- - The current API is `anthropic-messages`.
27
- - The current model is `claude-opus-4-8` or `claude-opus-5`.
28
- - The request payload does not already include `speed`.
29
-
30
- Claude Fast mode is available for API-key access and Claude Code subscription/OAuth access when the account has access to Anthropic's Fast mode research preview. For Claude Code subscription users, Anthropic documents this as extra usage credits, not included subscription usage.
31
-
32
- ## Commands
33
-
34
- ```text
35
- /claude-fast
36
- ```
37
-
38
- Run `/claude-fast` to toggle Fast mode on or off for the current session/runtime. The command reports the new state in chat, and the footer shows `fast` while Fast mode is active for an eligible model.
39
-
40
- The extension defaults to off so installing the full collection does not accidentally spend Fast-mode credits.
41
-
42
- ## Config
43
-
44
- Optional global config:
45
-
46
- ```text
47
- ~/<pi-config-dir>/agent/extensions/claude-fast.json
48
- ```
49
-
50
- Optional project config:
51
-
52
- ```text
53
- <project>/<pi-config-dir>/claude-fast.json
54
- ```
55
-
56
- Here `<pi-config-dir>` is Pi's runtime config directory name (`CONFIG_DIR_NAME`; `.pi` by default). Project config overrides global config after Pi reports that the project is trusted.
57
-
58
- ```json
59
- {
60
- "enabled": false,
61
- "showStatus": true
62
- }
63
- ```
64
-
65
- - `enabled`: default Fast-mode state when there is no session override.
66
- - `showStatus`: show a compact `fast` status when Fast mode is active for the current model.
7
+ This package is standalone-only and is not auto-loaded by the `@diegopetrucci/pi-extensions` collection package.
67
8
 
68
9
  ## Install
69
10
 
70
- ### Standalone npm package
71
-
72
11
  ```bash
73
12
  pi install npm:@diegopetrucci/pi-claude-fast
74
13
  ```
75
14
 
76
- ### Collection package
15
+ Installing this package does not enable Fast mode. For the working extension:
77
16
 
78
17
  ```bash
79
- pi install npm:@diegopetrucci/pi-extensions
80
- ```
81
-
82
- ### GitHub package
83
-
84
- ```bash
85
- pi install git:github.com/diegopetrucci/pi-extensions
18
+ pi install npm:@diegopetrucci/pi-fast
86
19
  ```
87
20
 
88
21
  Then reload pi:
@@ -90,10 +23,3 @@ Then reload pi:
90
23
  ```text
91
24
  /reload
92
25
  ```
93
-
94
- ## Notes
95
-
96
- - Anthropic Fast mode has separate rate limits and costs more than standard speed.
97
- - Requests at different speeds do not share prompt-cache prefixes.
98
- - Anthropic documents Fast mode as unavailable with Priority Tier.
99
- - If pi adds first-class speed support later, this extension skips payloads that already contain `speed`.