@settinghead/voxlert 0.3.9 → 0.3.10

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 @@
12
12
 
13
13
  # Voxlert
14
14
 
15
- LLM-generated voice notifications for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor](https://cursor.com/docs/agent/hooks), [OpenAI Codex](https://developers.openai.com/codex/), and [OpenClaw](https://openclaw.dev), spoken by game characters like the StarCraft Adjutant, Kerrigan, C&C EVA, SHODAN, and more.
15
+ LLM-generated voice notifications for [Claude Code](https://docs.anthropic.com/en/docs/claude-code), [Cursor](https://cursor.com/docs/agent/hooks), [OpenAI Codex](https://developers.openai.com/codex/), [pi](https://github.com/badlogic/pi-mono), and [OpenClaw](https://openclaw.dev), spoken by game characters like the StarCraft Adjutant, Kerrigan, C&C EVA, SHODAN, and more.
16
16
 
17
17
  ## Why Voxlert?
18
18
 
@@ -24,7 +24,7 @@ Voxlert makes each session speak in a distinct character voice with its own tone
24
24
 
25
25
  Voxlert is for users who:
26
26
 
27
- - Run two or more AI coding agent sessions concurrently (Claude Code, Cursor, Codex, OpenClaw)
27
+ - Run two or more AI coding agent sessions concurrently (Claude Code, Cursor, Codex, pi, OpenClaw)
28
28
  - Get interrupted by notification chimes but can't tell which window needs attention
29
29
  - Want ambient audio feedback that doesn't require looking at a screen
30
30
  - Are comfortable installing local tooling (Node.js, optionally Python for TTS)
@@ -71,7 +71,7 @@ The setup wizard configures:
71
71
  - Voice pack downloads
72
72
  - Active voice pack
73
73
  - TTS backend
74
- - Platform hooks for Claude Code, Cursor, and Codex
74
+ - Platform hooks for Claude Code, Cursor, Codex, and pi
75
75
 
76
76
  For OpenClaw, install the separate [OpenClaw plugin](docs/openclaw.md).
77
77
 
@@ -188,6 +188,27 @@ voxlert codex-notify
188
188
 
189
189
  `voxlert setup` can install or update the `notify` entry in `~/.codex/config.toml`. See [Codex integration](docs/codex.md).
190
190
 
191
+ ### pi
192
+
193
+ Installed through `voxlert setup`, which copies a TypeScript extension to `~/.pi/agent/extensions/voxlert.ts`. Alternatively, install the pi package directly:
194
+
195
+ ```bash
196
+ pi install npm:@settinghead/pi-voxlert
197
+ ```
198
+
199
+ The extension hooks into pi's lifecycle events and pipes them through `voxlert hook`:
200
+
201
+ | pi Event | Voxlert Event | Category |
202
+ |---|---|---|
203
+ | `agent_end` | Stop | `task.complete` |
204
+ | `tool_result` (error) | PostToolUseFailure | `task.error` |
205
+ | `session_shutdown` | SessionEnd | `session.end` |
206
+ | `session_before_compact` | PreCompact | `resource.limit` |
207
+
208
+ The extension also registers a `/voxlert` command (test, status) and a `voxlert_speak` tool that lets the LLM speak phrases on demand.
209
+
210
+ Run `/reload` in pi or start a new session after installing. See the [pi-voxlert README](pi-package/README.md) for details.
211
+
191
212
  ### OpenClaw
192
213
 
193
214
  OpenClaw uses a separate plugin. See [OpenClaw integration](docs/openclaw.md) for installation, config, and troubleshooting.
@@ -218,6 +239,7 @@ flowchart TD
218
239
  A2[OpenClaw Plugin] --> B
219
240
  A3[Cursor Hook] --> B
220
241
  A4[Codex notify] --> B
242
+ A5[pi Extension] --> B
221
243
  B --> C[src/voxlert.js]
222
244
  C --> D{Event type?}
223
245
  D -- "Contextual (e.g. Stop)" --> E[LLM<br><i>generate in-character phrase</i>]
@@ -233,7 +255,7 @@ flowchart TD
233
255
  J --> K[afplay / ffplay]
234
256
  ```
235
257
 
236
- 1. A hook or notify event fires from Claude Code, Cursor, Codex, or OpenClaw.
258
+ 1. A hook or notify event fires from Claude Code, Cursor, Codex, pi, or OpenClaw.
237
259
  2. Voxlert maps it to an event category and loads the active voice pack.
238
260
  3. Contextual events such as task completion or tool failure can use the configured LLM to generate a short in-character phrase.
239
261
  4. Other events use predefined fallback phrases from the pack.
@@ -278,7 +300,7 @@ Run `voxlert config path` to find `config.json`. You can edit it directly or use
278
300
 
279
301
  ### Event categories
280
302
 
281
- Event categories apply across Claude Code, Cursor, Codex, and OpenClaw where the corresponding event exists.
303
+ Event categories apply across Claude Code, Cursor, Codex, pi, and OpenClaw where the corresponding event exists.
282
304
 
283
305
  | Category | Hook Event | Description | Default |
284
306
  |---|---|---|---|
@@ -321,9 +343,9 @@ You can also manage configuration interactively with the `/voxlert-config` slash
321
343
 
322
344
  ### Integration behavior
323
345
 
324
- - `voxlert setup` installs hooks for Claude Code, Cursor, and Codex.
346
+ - `voxlert setup` installs hooks for Claude Code, Cursor, Codex, and pi.
325
347
  - Re-run setup anytime to add a platform you skipped earlier.
326
- - `voxlert uninstall` removes Claude Code, Cursor, and Codex integration.
348
+ - `voxlert uninstall` removes Claude Code, Cursor, Codex, and pi integration.
327
349
  - OpenClaw is managed separately through its plugin.
328
350
  - The global `enabled` flag disables processing everywhere; there is no separate per-integration toggle in `config.json`.
329
351
 
@@ -353,7 +375,7 @@ voxlert notification # Choose notification style (popup / system / off
353
375
  voxlert test "<text>" # Run full pipeline: LLM -> TTS -> audio playback
354
376
  voxlert cost # Show accumulated token usage and estimated cost
355
377
  voxlert cost reset # Clear the usage log
356
- voxlert uninstall # Remove hooks from Claude Code, Cursor, and Codex, optionally config/cache
378
+ voxlert uninstall # Remove hooks from Claude Code, Cursor, Codex, and pi, optionally config/cache
357
379
  voxlert help # Show help
358
380
  voxlert --version # Show version
359
381
  ```
@@ -371,7 +393,7 @@ voxlert uninstall
371
393
  npm uninstall -g @settinghead/voxlert
372
394
  ```
373
395
 
374
- This removes Voxlert hooks from Claude Code, Cursor, and Codex, the `voxlert-config` skill, and optionally your local config and cache in `~/.voxlert`.
396
+ This removes Voxlert hooks from Claude Code, Cursor, Codex, and pi, the `voxlert-config` skill, and optionally your local config and cache in `~/.voxlert`.
375
397
 
376
398
  ## Advanced
377
399
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@settinghead/voxlert",
3
- "version": "0.3.9",
4
- "description": "LLM-generated voice notifications for Claude Code, Cursor, OpenAI Codex, and OpenClaw, spoken by game characters like the StarCraft Adjutant, Kerrigan, C&C EVA, SHODAN, and more.",
3
+ "version": "0.3.10",
4
+ "description": "LLM-generated voice notifications for Claude Code, Cursor, OpenAI Codex, pi, and OpenClaw, spoken by game characters like the StarCraft Adjutant, Kerrigan, C&C EVA, SHODAN, and more.",
5
5
  "repository": {
6
6
  "type": "git",
7
7
  "url": "git+https://github.com/settinghead/voxlert.git"
@@ -16,6 +16,7 @@
16
16
  "packs/*/pack.json",
17
17
  "packs/*/voice.wav",
18
18
  "skills/",
19
+ "pi-package/extensions/",
19
20
  "assets/*.png",
20
21
  "assets/*.jpg",
21
22
  "assets/*.gif",
@@ -33,8 +34,7 @@
33
34
  "test": "node --test ./test/*.test.js"
34
35
  },
35
36
  "publishConfig": {
36
- "access": "public",
37
- "provenance": true
37
+ "access": "public"
38
38
  },
39
39
  "license": "MIT",
40
40
  "dependencies": {
@@ -0,0 +1,231 @@
1
+ /**
2
+ * pi-voxlert — Voice notifications for pi coding sessions.
3
+ *
4
+ * Hooks into pi agent lifecycle events and pipes them through the Voxlert CLI
5
+ * to generate contextual, in-character voice notifications spoken by game
6
+ * characters (SHODAN, StarCraft Adjutant, C&C EVA, HEV Suit, etc.).
7
+ *
8
+ * Prerequisites:
9
+ * npm install -g @settinghead/voxlert
10
+ * voxlert setup
11
+ *
12
+ * The extension calls `voxlert hook` with the event data on stdin, so whatever
13
+ * TTS backend + voice pack + LLM backend you configured via `voxlert config`
14
+ * is used automatically.
15
+ */
16
+
17
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
18
+ import { Type } from "@sinclair/typebox";
19
+ import { execSync, spawn } from "node:child_process";
20
+ import { basename } from "node:path";
21
+
22
+ // ---------------------------------------------------------------------------
23
+ // Helpers
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Resolve the voxlert binary path, or null if not installed. */
27
+ function findVoxlert(): string | null {
28
+ try {
29
+ return execSync("which voxlert", { encoding: "utf-8" }).trim();
30
+ } catch {
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /** Pipe an event through `voxlert hook` (fire-and-forget, async). */
36
+ function fireVoxlert(
37
+ eventName: string,
38
+ cwd: string,
39
+ extra: Record<string, unknown> = {},
40
+ ): void {
41
+ const payload = JSON.stringify({
42
+ hook_event_name: eventName,
43
+ cwd,
44
+ source: "pi",
45
+ ...extra,
46
+ });
47
+
48
+ const child = spawn("voxlert", ["hook"], {
49
+ stdio: ["pipe", "ignore", "ignore"],
50
+ detached: true,
51
+ });
52
+
53
+ child.stdin.write(payload);
54
+ child.stdin.end();
55
+ child.unref(); // don't block pi on audio playback
56
+ }
57
+
58
+ /** Check if Voxlert CLI is installed and available. */
59
+ function isVoxlertAvailable(): boolean {
60
+ return findVoxlert() !== null;
61
+ }
62
+
63
+ /**
64
+ * Extract the last assistant message text from pi's event messages array.
65
+ * Handles both direct message format and session entry format.
66
+ */
67
+ function extractLastAssistantText(messages: unknown[]): string {
68
+ if (!Array.isArray(messages)) return "";
69
+
70
+ for (let i = messages.length - 1; i >= 0; i--) {
71
+ const msg = messages[i] as any;
72
+ // Handle session entry format: { type: "message", message: { role, content } }
73
+ const actualMsg = msg?.message || msg;
74
+
75
+ if (actualMsg?.role === "assistant") {
76
+ const content = actualMsg.content;
77
+ if (typeof content === "string") return content.slice(0, 500);
78
+ if (Array.isArray(content)) {
79
+ return content
80
+ .filter((c: any) => c.type === "text")
81
+ .map((c: any) => c.text)
82
+ .join("\n")
83
+ .slice(0, 500);
84
+ }
85
+ }
86
+ }
87
+ return "";
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // Extension
92
+ // ---------------------------------------------------------------------------
93
+
94
+ export default function (pi: ExtensionAPI) {
95
+ let available = isVoxlertAvailable();
96
+
97
+ // ------------------------------------------------------------------
98
+ // Session start: verify Voxlert is installed, fire SessionStart
99
+ // ------------------------------------------------------------------
100
+ pi.on("session_start", async (_event, ctx) => {
101
+ available = isVoxlertAvailable();
102
+ if (!available) {
103
+ ctx.ui.notify(
104
+ "Voxlert not found. Install with: npm install -g @settinghead/voxlert && voxlert setup",
105
+ "warning",
106
+ );
107
+ } else {
108
+ ctx.ui.setStatus("voxlert", "🔊 Voxlert");
109
+ }
110
+ });
111
+
112
+ // ------------------------------------------------------------------
113
+ // Agent end → "Stop" hook (task finished, waiting for input)
114
+ // Passes last_assistant_message so the LLM can generate a
115
+ // contextual phrase about what just happened.
116
+ // ------------------------------------------------------------------
117
+ pi.on("agent_end", async (event, ctx) => {
118
+ if (!available) return;
119
+
120
+ const messages = (event as any).messages || [];
121
+ const lastAssistantMessage = extractLastAssistantText(messages);
122
+
123
+ fireVoxlert("Stop", ctx.cwd, {
124
+ last_assistant_message: lastAssistantMessage,
125
+ });
126
+ });
127
+
128
+ // ------------------------------------------------------------------
129
+ // Tool errors → "PostToolUseFailure" hook (contextual event)
130
+ // ------------------------------------------------------------------
131
+ pi.on("tool_result", async (event, ctx) => {
132
+ if (!available) return;
133
+ if (event.isError) {
134
+ const text =
135
+ event.content
136
+ ?.filter((c: any) => c.type === "text")
137
+ .map((c: any) => c.text)
138
+ .join("\n")
139
+ .slice(0, 500) || "";
140
+
141
+ fireVoxlert("PostToolUseFailure", ctx.cwd, {
142
+ error_message: `${event.toolName}: ${text}`,
143
+ });
144
+ }
145
+ });
146
+
147
+ // ------------------------------------------------------------------
148
+ // Session shutdown → "SessionEnd" hook
149
+ // ------------------------------------------------------------------
150
+ pi.on("session_shutdown", async (_event, ctx) => {
151
+ if (!available) return;
152
+ fireVoxlert("SessionEnd", ctx.cwd);
153
+ });
154
+
155
+ // ------------------------------------------------------------------
156
+ // Context compaction → "PreCompact" hook
157
+ // ------------------------------------------------------------------
158
+ pi.on("session_before_compact", async (_event, ctx) => {
159
+ if (!available) return;
160
+ fireVoxlert("PreCompact", ctx.cwd);
161
+ });
162
+
163
+ // ------------------------------------------------------------------
164
+ // /voxlert command — quick controls
165
+ // ------------------------------------------------------------------
166
+ pi.registerCommand("voxlert", {
167
+ description: "Voxlert voice notifications: test, status, or configure",
168
+ handler: async (args, ctx) => {
169
+ const sub = (args || "").trim().split(/\s+/)[0];
170
+
171
+ if (sub === "test") {
172
+ if (!available) {
173
+ ctx.ui.notify("Voxlert CLI not installed.", "error");
174
+ return;
175
+ }
176
+ fireVoxlert("Stop", ctx.cwd);
177
+ ctx.ui.notify("Sent test notification.", "info");
178
+ return;
179
+ }
180
+
181
+ if (sub === "status") {
182
+ ctx.ui.notify(
183
+ available
184
+ ? "Voxlert is active. Voice notifications will play on agent_end, tool errors, compaction, and session end."
185
+ : "Voxlert CLI not found. Run: npm install -g @settinghead/voxlert && voxlert setup",
186
+ available ? "info" : "warning",
187
+ );
188
+ return;
189
+ }
190
+
191
+ // Default: show help
192
+ ctx.ui.notify(
193
+ "Usage: /voxlert [test|status]\n" +
194
+ " test — fire a test voice notification\n" +
195
+ " status — check if Voxlert CLI is available\n" +
196
+ "\nConfigure voice packs, TTS backend, etc. via: voxlert config",
197
+ "info",
198
+ );
199
+ },
200
+ });
201
+
202
+ // ------------------------------------------------------------------
203
+ // voxlert_speak tool — let the LLM speak through Voxlert
204
+ // ------------------------------------------------------------------
205
+ pi.registerTool({
206
+ name: "voxlert_speak",
207
+ label: "Voxlert Speak",
208
+ description:
209
+ "Speak a phrase aloud through Voxlert using the user's configured voice pack and TTS backend. " +
210
+ "Use this when the user asks you to say something out loud, announce something, or test voice notifications.",
211
+ parameters: Type.Object({
212
+ phrase: Type.String({ description: "The phrase to speak aloud (2-12 words work best)" }),
213
+ }),
214
+ async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
215
+ if (!available) {
216
+ throw new Error(
217
+ "Voxlert CLI not installed. Install with: npm install -g @settinghead/voxlert && voxlert setup",
218
+ );
219
+ }
220
+
221
+ fireVoxlert("Stop", ctx.cwd, {
222
+ phrase_override: params.phrase,
223
+ });
224
+
225
+ return {
226
+ content: [{ type: "text", text: `Speaking: "${params.phrase}"` }],
227
+ details: { phrase: params.phrase },
228
+ };
229
+ },
230
+ });
231
+ }
@@ -4,7 +4,7 @@ export function formatHelp(commands, pkg) {
4
4
  .map((command) => command.help.join("\n"));
5
5
 
6
6
  return [
7
- `voxlert v${pkg.version} — Game character voice notifications for Claude Code, Cursor, Codex, and OpenClaw`,
7
+ `voxlert v${pkg.version} — Game character voice notifications for Claude Code, Cursor, Codex, pi, and OpenClaw`,
8
8
  "",
9
9
  "Usage:",
10
10
  ...sections,
@@ -3,10 +3,11 @@ import confirm from "@inquirer/confirm";
3
3
  import { unregisterHooks, removeSkill } from "../hooks.js";
4
4
  import { unregisterCursorHooks } from "../cursor-hooks.js";
5
5
  import { unregisterCodexNotify } from "../codex-config.js";
6
+ import { removePiExtension } from "../pi-hooks.js";
6
7
  import { STATE_DIR } from "../paths.js";
7
8
 
8
9
  async function runUninstall() {
9
- console.log("Removing Voxlert hooks and skill...\n");
10
+ console.log("Removing Voxlert hooks, extensions, and skill...\n");
10
11
 
11
12
  const claudeRemoved = unregisterHooks();
12
13
  if (claudeRemoved > 0) {
@@ -23,13 +24,18 @@ async function runUninstall() {
23
24
  console.log(" Removed notify from ~/.codex/config.toml");
24
25
  }
25
26
 
27
+ const piRemoved = removePiExtension();
28
+ if (piRemoved) {
29
+ console.log(" Removed Voxlert extension from ~/.pi/agent/extensions/");
30
+ }
31
+
26
32
  const skillRemoved = removeSkill();
27
33
  if (skillRemoved) {
28
34
  console.log(" Removed voxlert-config skill");
29
35
  }
30
36
 
31
- if (claudeRemoved === 0 && cursorRemoved === 0 && !codexRemoved && !skillRemoved) {
32
- console.log(" No Voxlert hooks or skill were found.");
37
+ if (claudeRemoved === 0 && cursorRemoved === 0 && !codexRemoved && !piRemoved && !skillRemoved) {
38
+ console.log(" No Voxlert hooks, extensions, or skill were found.");
33
39
  }
34
40
 
35
41
  if (existsSync(STATE_DIR)) {
@@ -43,14 +49,14 @@ async function runUninstall() {
43
49
  }
44
50
  }
45
51
 
46
- console.log("\nUninstall complete. You can still run 'voxlert' if installed via npm; run 'npm uninstall -g @settinghead/voxlert' to remove the CLI.");
52
+ console.log("\nUninstall complete. You can still run 'voxlert' if installed via npm; run 'npm uninstall -g @settinghead/voxlert' to remove the CLI.\nFor pi, you can also run: pi remove npm:@settinghead/pi-voxlert");
47
53
  }
48
54
 
49
55
  export const uninstallCommand = {
50
56
  name: "uninstall",
51
57
  aliases: [],
52
58
  help: [
53
- " voxlert uninstall Remove hooks from Claude Code, Cursor, and Codex, optionally config/cache",
59
+ " voxlert uninstall Remove hooks from Claude Code, Cursor, Codex, and pi, optionally config/cache",
54
60
  ],
55
61
  skipSetupWizard: true,
56
62
  skipUpgradeCheck: false,
@@ -0,0 +1,45 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "fs";
2
+ import { join, dirname } from "path";
3
+ import { homedir } from "os";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+
8
+ const PI_EXTENSIONS_DIR = join(homedir(), ".pi", "agent", "extensions");
9
+ const PI_EXTENSION_FILE = join(PI_EXTENSIONS_DIR, "voxlert.ts");
10
+
11
+ /**
12
+ * The extension source is bundled at ../pi-package/extensions/voxlert.ts
13
+ * relative to this file (cli/src/).
14
+ */
15
+ const EXTENSION_SRC = join(__dirname, "..", "pi-package", "extensions", "voxlert.ts");
16
+
17
+ /**
18
+ * Check if the Voxlert extension is installed in pi's extensions directory.
19
+ */
20
+ export function hasPiExtension() {
21
+ return existsSync(PI_EXTENSION_FILE);
22
+ }
23
+
24
+ /**
25
+ * Install the Voxlert extension to ~/.pi/agent/extensions/voxlert.ts.
26
+ * Copies the bundled extension source file.
27
+ * @returns {boolean} true if installed successfully
28
+ */
29
+ export function installPiExtension() {
30
+ if (!existsSync(EXTENSION_SRC)) return false;
31
+ mkdirSync(PI_EXTENSIONS_DIR, { recursive: true });
32
+ const content = readFileSync(EXTENSION_SRC, "utf-8");
33
+ writeFileSync(PI_EXTENSION_FILE, content);
34
+ return true;
35
+ }
36
+
37
+ /**
38
+ * Remove the Voxlert extension from ~/.pi/agent/extensions/.
39
+ * @returns {boolean} true if a file was removed
40
+ */
41
+ export function removePiExtension() {
42
+ if (!existsSync(PI_EXTENSION_FILE)) return false;
43
+ rmSync(PI_EXTENSION_FILE);
44
+ return true;
45
+ }
package/src/setup.js CHANGED
@@ -21,6 +21,7 @@ import { LLM_PROVIDERS, getProvider } from "./providers.js";
21
21
  import { registerHooks, installSkill, unregisterHooks, hasVoxlertHooks, hasInstalledSkill, removeSkill } from "./hooks.js";
22
22
  import { registerCursorHooks, unregisterCursorHooks, hasCursorHooks } from "./cursor-hooks.js";
23
23
  import { registerCodexNotify, getCodexConfigPath, unregisterCodexNotify, hasCodexNotify } from "./codex-config.js";
24
+ import { installPiExtension, removePiExtension, hasPiExtension } from "./pi-hooks.js";
24
25
  import { printSetupHeader, printStep, printStatus, printSuccess, printWarning, highlight } from "./setup-ui.js";
25
26
  import {
26
27
  probeTtsBackend,
@@ -238,6 +239,7 @@ export async function runSetup({ nonInteractive = false } = {}) {
238
239
  if (hasVoxlertHooks() || hasInstalledSkill()) installedPlatforms.push("Claude");
239
240
  if (hasCursorHooks()) installedPlatforms.push("Cursor");
240
241
  if (hasCodexNotify()) installedPlatforms.push("Codex");
242
+ if (hasPiExtension()) installedPlatforms.push("pi");
241
243
  await printSetupHeader(config, installedPlatforms);
242
244
 
243
245
  // --- Step 1: LLM Provider ---
@@ -502,6 +504,12 @@ export async function runSetup({ nonInteractive = false } = {}) {
502
504
  description: "Install/update notify in ~/.codex/config.toml",
503
505
  checked: hasCodexNotify(),
504
506
  },
507
+ {
508
+ name: "pi",
509
+ value: "pi",
510
+ description: "Install extension to ~/.pi/agent/extensions/",
511
+ checked: hasPiExtension(),
512
+ },
505
513
  ];
506
514
 
507
515
  const selectedPlatforms = await checkbox({
@@ -554,6 +562,20 @@ export async function runSetup({ nonInteractive = false } = {}) {
554
562
  }
555
563
  }
556
564
 
565
+ if (selectedPlatforms.includes("pi")) {
566
+ if (installPiExtension()) {
567
+ printSuccess("Installed Voxlert extension to ~/.pi/agent/extensions/voxlert.ts");
568
+ printStatus("Next", "Run /reload in pi or start a new session to activate.");
569
+ } else {
570
+ printWarning("Could not install pi extension (source file not found).");
571
+ }
572
+ } else {
573
+ const piRemoved = removePiExtension();
574
+ if (piRemoved) {
575
+ printWarning("Removed Voxlert extension from ~/.pi/agent/extensions/");
576
+ }
577
+ }
578
+
557
579
  if (selectedPlatforms.length === 0) {
558
580
  printWarning("No platforms selected. Run 'voxlert setup' again to install hooks later.");
559
581
  }
@@ -681,6 +703,9 @@ function printSetupSummary(config, chosenProvider, selectedPlatforms) {
681
703
  if (selectedPlatforms.includes("codex")) {
682
704
  printStatus("Codex", "Start a new Codex session to pick up the notify config.");
683
705
  }
706
+ if (selectedPlatforms.includes("pi")) {
707
+ printStatus("pi", "Run /reload in pi or start a new session.");
708
+ }
684
709
  printStatus("Reconfigure", "voxlert setup");
685
710
  console.log("");
686
711
  }
package/src/voxlert.js CHANGED
@@ -93,6 +93,57 @@ export async function processHookEvent(eventData) {
93
93
  const pack = loadPack(config);
94
94
  const projectName = cwd ? basename(cwd) : "";
95
95
 
96
+ // Allow callers to bypass LLM generation entirely with a pre-built phrase
97
+ if (eventData.phrase_override && typeof eventData.phrase_override === "string") {
98
+ const overridePhrase = eventData.phrase_override.trim();
99
+ if (overridePhrase) {
100
+ debugLog("processHookEvent using phrase_override", { source, phrase: overridePhrase.slice(0, 120) });
101
+
102
+ // Prepend prefix
103
+ const prefixTemplate = config.prefix !== undefined ? config.prefix : "${dirname}";
104
+ let resolvedPrefix = "";
105
+ if (prefixTemplate !== "") {
106
+ resolvedPrefix = prefixTemplate.replace(/\$\{dirname\}/g, projectName);
107
+ if (resolvedPrefix) {
108
+ const finalPhrase = `${resolvedPrefix}; ${overridePhrase}`;
109
+ const packId = config.active_pack || "sc1-kerrigan-infested";
110
+ appendLog(
111
+ `[${new Date().toISOString()}] source=${source} event=${eventName} category=${category} phrase=${finalPhrase.replace(/\s+/g, " ").slice(0, 120)}`,
112
+ config,
113
+ );
114
+ showOverlay(finalPhrase, {
115
+ category,
116
+ packName: pack.name,
117
+ packId: pack.id || packId,
118
+ prefix: resolvedPrefix,
119
+ config,
120
+ overlayColors: pack.overlay_colors,
121
+ });
122
+ await speakPhrase(finalPhrase, config, pack);
123
+ debugLog("processHookEvent done (phrase_override)", { source });
124
+ return;
125
+ }
126
+ }
127
+
128
+ const packId = config.active_pack || "sc1-kerrigan-infested";
129
+ appendLog(
130
+ `[${new Date().toISOString()}] source=${source} event=${eventName} category=${category} phrase=${overridePhrase.replace(/\s+/g, " ").slice(0, 120)}`,
131
+ config,
132
+ );
133
+ showOverlay(overridePhrase, {
134
+ category,
135
+ packName: pack.name,
136
+ packId: pack.id || packId,
137
+ prefix: "",
138
+ config,
139
+ overlayColors: pack.overlay_colors,
140
+ });
141
+ await speakPhrase(overridePhrase, config, pack);
142
+ debugLog("processHookEvent done (phrase_override)", { source });
143
+ return;
144
+ }
145
+ }
146
+
96
147
  // For contextual events, try LLM phrase generation
97
148
  let phrase = null;
98
149
  let fallbackReason = null;