@settinghead/pi-voxlert 0.1.0

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 ADDED
@@ -0,0 +1,61 @@
1
+ # pi-voxlert
2
+
3
+ **SHODAN, the StarCraft Adjutant, and GLaDOS narrate your pi coding sessions.**
4
+
5
+ Voice notifications for [pi](https://github.com/badlogic/pi) powered by [Voxlert](https://github.com/settinghead/voxlert). When your agent finishes a task or hits an error, you hear a contextual phrase in a game character's voice — instead of silence or a generic chime.
6
+
7
+ > "Awaiting further orders, Commander. Build process complete."
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ # 1. Install the Voxlert CLI (one-time)
13
+ npm install -g @settinghead/voxlert
14
+ voxlert setup
15
+
16
+ # 2. Install the pi package
17
+ pi install npm:@settinghead/pi-voxlert
18
+ ```
19
+
20
+ ## What it does
21
+
22
+ | pi event | Voxlert action |
23
+ |----------|---------------|
24
+ | **Agent finishes** (`agent_end`) | Speaks a contextual in-character phrase |
25
+ | **Tool error** (`tool_result` with error) | Announces the error in character |
26
+
27
+ Phrases are generated per-event by an LLM, so you hear things like *"Pathetic authentication corrected"* (SHODAN) or *"Warning, Commander. Test suite failure detected"* (Adjutant) — not canned sounds.
28
+
29
+ ## Commands
30
+
31
+ | Command | Description |
32
+ |---------|-------------|
33
+ | `/voxlert test` | Fire a test voice notification |
34
+ | `/voxlert status` | Check if Voxlert CLI is available |
35
+ | `/voxlert` | Show help |
36
+
37
+ The LLM can also call the `voxlert_speak` tool to say something aloud on demand.
38
+
39
+ ## Configuration
40
+
41
+ All voice pack, TTS backend, and LLM settings are managed through the Voxlert CLI:
42
+
43
+ ```bash
44
+ voxlert config # interactive configuration
45
+ voxlert packs # list available voice packs
46
+ voxlert test "Hello" # test your setup
47
+ ```
48
+
49
+ Supports local TTS (Qwen3-TTS on Apple Silicon, Chatterbox on CUDA) and multiple LLM backends (OpenRouter, OpenAI, Anthropic, Gemini).
50
+
51
+ ## Requirements
52
+
53
+ - [pi](https://github.com/badlogic/pi) coding agent
54
+ - [Voxlert CLI](https://github.com/settinghead/voxlert) installed and configured (`npm install -g @settinghead/voxlert && voxlert setup`)
55
+ - A TTS backend running (or Voxlert falls back to text notifications)
56
+
57
+ ## Links
58
+
59
+ - [Voxlert repo](https://github.com/settinghead/voxlert)
60
+ - [Demo video](https://youtu.be/5xFXGijwJuk)
61
+ - [Available voice packs](https://github.com/settinghead/voxlert#voice-packs)
@@ -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/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@settinghead/pi-voxlert",
3
+ "version": "0.1.0",
4
+ "description": "SHODAN, the StarCraft Adjutant, and GLaDOS narrate your pi coding sessions. LLM-generated voice notifications spoken by game characters — know which agent needs you, by ear.",
5
+ "keywords": ["pi-package", "voxlert", "voice-notifications", "tts", "coding-agent", "notifications", "audio"],
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/settinghead/voxlert.git",
9
+ "directory": "pi-package"
10
+ },
11
+ "bugs": {
12
+ "url": "https://github.com/settinghead/voxlert/issues"
13
+ },
14
+ "homepage": "https://github.com/settinghead/voxlert",
15
+ "license": "MIT",
16
+ "author": "settinghead",
17
+ "type": "module",
18
+ "files": [
19
+ "extensions/",
20
+ "README.md"
21
+ ],
22
+ "pi": {
23
+ "extensions": ["./extensions"],
24
+ "video": "https://youtu.be/5xFXGijwJuk"
25
+ },
26
+ "peerDependencies": {
27
+ "@mariozechner/pi-coding-agent": "*",
28
+ "@sinclair/typebox": "*"
29
+ }
30
+ }