@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 +31 -9
- package/package.json +4 -4
- package/pi-package/extensions/voxlert.ts +231 -0
- package/src/commands/help.js +1 -1
- package/src/commands/uninstall.js +11 -5
- package/src/pi-hooks.js +45 -0
- package/src/setup.js +25 -0
- package/src/voxlert.js +51 -0
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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/commands/help.js
CHANGED
|
@@ -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
|
|
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,
|
package/src/pi-hooks.js
ADDED
|
@@ -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;
|