@settinghead/voxlert 0.3.8 → 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)
@@ -62,8 +62,7 @@ The setup wizard auto-detects running TTS backends. If none are running yet, set
62
62
  ### 2. Install and run setup
63
63
 
64
64
  ```bash
65
- npm install -g @settinghead/voxlert
66
- voxlert setup
65
+ npx voxlert --onboard
67
66
  ```
68
67
 
69
68
  The setup wizard configures:
@@ -72,7 +71,7 @@ The setup wizard configures:
72
71
  - Voice pack downloads
73
72
  - Active voice pack
74
73
  - TTS backend
75
- - Platform hooks for Claude Code, Cursor, and Codex
74
+ - Platform hooks for Claude Code, Cursor, Codex, and pi
76
75
 
77
76
  For OpenClaw, install the separate [OpenClaw plugin](docs/openclaw.md).
78
77
 
@@ -189,6 +188,27 @@ voxlert codex-notify
189
188
 
190
189
  `voxlert setup` can install or update the `notify` entry in `~/.codex/config.toml`. See [Codex integration](docs/codex.md).
191
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
+
192
212
  ### OpenClaw
193
213
 
194
214
  OpenClaw uses a separate plugin. See [OpenClaw integration](docs/openclaw.md) for installation, config, and troubleshooting.
@@ -219,6 +239,7 @@ flowchart TD
219
239
  A2[OpenClaw Plugin] --> B
220
240
  A3[Cursor Hook] --> B
221
241
  A4[Codex notify] --> B
242
+ A5[pi Extension] --> B
222
243
  B --> C[src/voxlert.js]
223
244
  C --> D{Event type?}
224
245
  D -- "Contextual (e.g. Stop)" --> E[LLM<br><i>generate in-character phrase</i>]
@@ -234,7 +255,7 @@ flowchart TD
234
255
  J --> K[afplay / ffplay]
235
256
  ```
236
257
 
237
- 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.
238
259
  2. Voxlert maps it to an event category and loads the active voice pack.
239
260
  3. Contextual events such as task completion or tool failure can use the configured LLM to generate a short in-character phrase.
240
261
  4. Other events use predefined fallback phrases from the pack.
@@ -279,7 +300,7 @@ Run `voxlert config path` to find `config.json`. You can edit it directly or use
279
300
 
280
301
  ### Event categories
281
302
 
282
- 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.
283
304
 
284
305
  | Category | Hook Event | Description | Default |
285
306
  |---|---|---|---|
@@ -322,9 +343,9 @@ You can also manage configuration interactively with the `/voxlert-config` slash
322
343
 
323
344
  ### Integration behavior
324
345
 
325
- - `voxlert setup` installs hooks for Claude Code, Cursor, and Codex.
346
+ - `voxlert setup` installs hooks for Claude Code, Cursor, Codex, and pi.
326
347
  - Re-run setup anytime to add a platform you skipped earlier.
327
- - `voxlert uninstall` removes Claude Code, Cursor, and Codex integration.
348
+ - `voxlert uninstall` removes Claude Code, Cursor, Codex, and pi integration.
328
349
  - OpenClaw is managed separately through its plugin.
329
350
  - The global `enabled` flag disables processing everywhere; there is no separate per-integration toggle in `config.json`.
330
351
 
@@ -354,7 +375,7 @@ voxlert notification # Choose notification style (popup / system / off
354
375
  voxlert test "<text>" # Run full pipeline: LLM -> TTS -> audio playback
355
376
  voxlert cost # Show accumulated token usage and estimated cost
356
377
  voxlert cost reset # Clear the usage log
357
- 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
358
379
  voxlert help # Show help
359
380
  voxlert --version # Show version
360
381
  ```
@@ -372,7 +393,7 @@ voxlert uninstall
372
393
  npm uninstall -g @settinghead/voxlert
373
394
  ```
374
395
 
375
- 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`.
376
397
 
377
398
  ## Advanced
378
399
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@settinghead/voxlert",
3
- "version": "0.3.8",
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
+ }
package/src/cli.js CHANGED
@@ -53,6 +53,14 @@ async function maybeRunSetup(command) {
53
53
 
54
54
  (async () => {
55
55
  const args = process.argv.slice(2);
56
+
57
+ // --onboard flag: run setup wizard directly (supports `npx voxlert --onboard`)
58
+ if (args.includes("--onboard")) {
59
+ const { runSetup } = await import("./setup.js");
60
+ await runSetup();
61
+ return;
62
+ }
63
+
56
64
  const requested = args[0] || "help";
57
65
  const command = resolveCommand(requested);
58
66
 
@@ -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;