@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 +61 -0
- package/extensions/voxlert.ts +231 -0
- package/package.json +30 -0
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
|
+
}
|