pi-boop 0.0.1

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/CHANGELOG.md ADDED
@@ -0,0 +1 @@
1
+ # pi-boop
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Braden Lamb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,64 @@
1
+ # pi-boop
2
+
3
+ A [Pi](https://pi.dev) extension that plays the terminal bell or a custom sound when Pi finishes a turn and is waiting for your input.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ # from npm
9
+ pi install npm:pi-boop
10
+
11
+ # or from git
12
+ pi install git:github.com/bradennss/pi-boop
13
+
14
+ # try it for a single run without installing
15
+ pi -e npm:pi-boop
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ Once installed, pi-boop rings the terminal bell every time Pi settles and waits for you.
21
+
22
+ Run `/boop` at any time to play the sound and check your setup.
23
+
24
+ ### Play a custom sound
25
+
26
+ Point pi-boop at an audio file to play it instead of the bell:
27
+
28
+ ```bash
29
+ export PI_BOOP_SOUND=~/sounds/ready.wav
30
+ ```
31
+
32
+ Or set it for a single run:
33
+
34
+ ```bash
35
+ pi --boop-sound ~/sounds/ready.wav
36
+ ```
37
+
38
+ pi-boop plays the file with the platform's player (`afplay` on macOS, `paplay` on Linux, PowerShell on Windows). If the file cannot be played, it falls back to the terminal bell.
39
+
40
+ ### Turn it off
41
+
42
+ ```bash
43
+ export PI_BOOP_DISABLE=1 # every run
44
+ pi --boop-off # a single run
45
+ ```
46
+
47
+ ## Requirements
48
+
49
+ - Node.js >= 20.
50
+
51
+ ## Development
52
+
53
+ ```bash
54
+ pnpm install
55
+ pnpm run check
56
+ ```
57
+
58
+ ## Contributing
59
+
60
+ Every change that affects the published package needs a [changeset](https://github.com/changesets/changesets):
61
+
62
+ ```bash
63
+ pnpm changeset
64
+ ```
package/SECURITY.md ADDED
@@ -0,0 +1,13 @@
1
+ # Security Policy
2
+
3
+ ## Scope and behavior
4
+
5
+ `pi-boop` is a Pi extension. Like all Pi extensions it runs with your full user permissions. This extension:
6
+
7
+ - Writes the terminal bell character to standard error when Pi settles and waits for input.
8
+ - When `PI_BOOP_SOUND` or `--boop-sound` is set, spawns the platform audio player (`afplay`, `paplay`, or PowerShell) with the path you provide to play that file.
9
+ - Has no network calls, no telemetry, and no credential access. It has no runtime dependencies.
10
+
11
+ ## Reporting a vulnerability
12
+
13
+ Please report suspected vulnerabilities privately via a [GitHub security advisory](https://github.com/bradennss/pi-boop/security/advisories/new). You will receive an acknowledgement within a few days.
package/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * pi-boop plays the terminal bell or a custom sound file when Pi finishes a
3
+ * turn and is waiting for user input.
4
+ */
5
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
6
+ import {
7
+ resolveConfig,
8
+ settingsFromEnv,
9
+ type BoopConfig,
10
+ type RawBoopSettings,
11
+ } from "./src/boop.ts";
12
+ import { boop } from "./src/player.ts";
13
+
14
+ const SOUND_FLAG = "boop-sound";
15
+ const OFF_FLAG = "boop-off";
16
+
17
+ export default function piBoop(pi: ExtensionAPI): void {
18
+ pi.registerFlag(SOUND_FLAG, {
19
+ type: "string",
20
+ description: "Path to a custom sound file to play when Pi waits for input.",
21
+ });
22
+ pi.registerFlag(OFF_FLAG, {
23
+ type: "boolean",
24
+ default: false,
25
+ description: "Disable pi-boop for this run.",
26
+ });
27
+
28
+ function currentConfig(): BoopConfig {
29
+ const env = settingsFromEnv(process.env);
30
+ const soundFlag = pi.getFlag(SOUND_FLAG);
31
+ const offFlag = pi.getFlag(OFF_FLAG);
32
+ const raw: RawBoopSettings = {
33
+ soundFile:
34
+ typeof soundFlag === "string" && soundFlag ? soundFlag : env.soundFile,
35
+ disabled: offFlag === true || env.disabled,
36
+ };
37
+ return resolveConfig(raw);
38
+ }
39
+
40
+ pi.on("agent_settled", async (_event, ctx) => {
41
+ if (!ctx.isIdle()) {
42
+ return;
43
+ }
44
+ boop(currentConfig());
45
+ });
46
+
47
+ pi.registerCommand("boop", {
48
+ description: "Play the pi-boop sound now to test your configuration.",
49
+ handler: async (_args, ctx) => {
50
+ boop(currentConfig());
51
+ ctx.ui.notify("Played pi-boop.", "info");
52
+ },
53
+ });
54
+ }
package/package.json ADDED
@@ -0,0 +1,75 @@
1
+ {
2
+ "name": "pi-boop",
3
+ "version": "0.0.1",
4
+ "description": "Pi extension that plays the terminal bell or a custom sound when Pi waits for input.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Braden Lamb <hi@braden.lol>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/bradennss/pi-boop.git"
11
+ },
12
+ "homepage": "https://github.com/bradennss/pi-boop#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/bradennss/pi-boop/issues"
15
+ },
16
+ "keywords": [
17
+ "pi-package",
18
+ "pi",
19
+ "pi-extension",
20
+ "pi-coding-agent",
21
+ "sound",
22
+ "bell",
23
+ "notify"
24
+ ],
25
+ "exports": {
26
+ ".": {
27
+ "types": "./index.ts",
28
+ "import": "./index.ts",
29
+ "default": "./index.ts"
30
+ }
31
+ },
32
+ "files": [
33
+ "index.ts",
34
+ "src",
35
+ "README.md",
36
+ "LICENSE",
37
+ "SECURITY.md",
38
+ "CHANGELOG.md"
39
+ ],
40
+ "engines": {
41
+ "node": ">=20"
42
+ },
43
+ "packageManager": "pnpm@10.32.1",
44
+ "scripts": {
45
+ "typecheck": "tsc --noEmit",
46
+ "lint": "eslint .",
47
+ "format": "prettier --write .",
48
+ "format:check": "prettier --check .",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest",
51
+ "check": "pnpm run typecheck && pnpm run lint && pnpm run format:check && pnpm test",
52
+ "prepack": "pnpm run check"
53
+ },
54
+ "pi": {
55
+ "extensions": [
56
+ "./index.ts"
57
+ ]
58
+ },
59
+ "peerDependencies": {
60
+ "@earendil-works/pi-coding-agent": "*"
61
+ },
62
+ "devDependencies": {
63
+ "@changesets/changelog-github": "^1.0.0",
64
+ "@changesets/cli": "^3.0.1",
65
+ "@earendil-works/pi-coding-agent": "^0.84.2",
66
+ "@eslint/js": "^10.0.1",
67
+ "@types/node": "^22.0.0",
68
+ "eslint": "^10.8.1",
69
+ "eslint-config-prettier": "^10.1.8",
70
+ "prettier": "^3.9.6",
71
+ "typescript": "^5.6.0",
72
+ "typescript-eslint": "^8.67.0",
73
+ "vitest": "^4.1.11"
74
+ }
75
+ }
package/src/boop.ts ADDED
@@ -0,0 +1,65 @@
1
+ /** The ASCII BEL character. Writing it to a terminal rings the terminal bell. */
2
+ export const BELL = "\u0007";
3
+
4
+ /** Resolved runtime behavior for a single boop. */
5
+ export interface BoopConfig {
6
+ enabled: boolean;
7
+ soundFile: string | undefined;
8
+ }
9
+
10
+ /** Unnormalized settings gathered from the environment or CLI flags. */
11
+ export interface RawBoopSettings {
12
+ soundFile?: string;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ /** Normalize raw settings into a config the player can act on. */
17
+ export function resolveConfig(raw: RawBoopSettings): BoopConfig {
18
+ const soundFile = raw.soundFile?.trim() ?? "";
19
+ return {
20
+ enabled: raw.disabled !== true,
21
+ soundFile: soundFile.length > 0 ? soundFile : undefined,
22
+ };
23
+ }
24
+
25
+ const TRUTHY = new Set(["1", "true", "yes", "on"]);
26
+
27
+ /** Read raw settings from process environment variables. */
28
+ export function settingsFromEnv(
29
+ env: Record<string, string | undefined>,
30
+ ): RawBoopSettings {
31
+ return {
32
+ soundFile: env.PI_BOOP_SOUND,
33
+ disabled: TRUTHY.has((env.PI_BOOP_DISABLE ?? "").trim().toLowerCase()),
34
+ };
35
+ }
36
+
37
+ /** A spawnable command that plays an audio file. */
38
+ export interface PlayerCommand {
39
+ command: string;
40
+ args: string[];
41
+ }
42
+
43
+ /** Pick the command that plays a sound file on the given platform. */
44
+ export function soundPlayerCommand(
45
+ platform: NodeJS.Platform,
46
+ soundFile: string,
47
+ ): PlayerCommand | undefined {
48
+ switch (platform) {
49
+ case "darwin":
50
+ return { command: "afplay", args: [soundFile] };
51
+ case "linux":
52
+ return { command: "paplay", args: [soundFile] };
53
+ case "win32":
54
+ return {
55
+ command: "powershell",
56
+ args: [
57
+ "-NoProfile",
58
+ "-Command",
59
+ `(New-Object Media.SoundPlayer '${soundFile}').PlaySync()`,
60
+ ],
61
+ };
62
+ default:
63
+ return undefined;
64
+ }
65
+ }
package/src/player.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { spawn } from "node:child_process";
2
+ import { BELL, soundPlayerCommand, type BoopConfig } from "./boop.ts";
3
+
4
+ /** Injectable side effects so the player can be tested without real audio. */
5
+ export interface PlayerDeps {
6
+ platform: NodeJS.Platform;
7
+ ringBell: () => void;
8
+ spawn: typeof spawn;
9
+ }
10
+
11
+ /** Real dependencies wired to the current process. */
12
+ export function defaultDeps(): PlayerDeps {
13
+ return {
14
+ platform: process.platform,
15
+ ringBell: () => {
16
+ process.stderr.write(BELL);
17
+ },
18
+ spawn,
19
+ };
20
+ }
21
+
22
+ /**
23
+ * Play a single boop. Plays the configured sound file when one is set and the
24
+ * platform has a player, otherwise rings the terminal bell. A failed sound
25
+ * player also falls back to the bell.
26
+ */
27
+ export function boop(
28
+ config: BoopConfig,
29
+ deps: PlayerDeps = defaultDeps(),
30
+ ): void {
31
+ if (!config.enabled) {
32
+ return;
33
+ }
34
+
35
+ if (config.soundFile) {
36
+ const command = soundPlayerCommand(deps.platform, config.soundFile);
37
+ if (command) {
38
+ const child = deps.spawn(command.command, command.args, {
39
+ stdio: "ignore",
40
+ });
41
+ child.on("error", deps.ringBell);
42
+ child.unref();
43
+ return;
44
+ }
45
+ }
46
+
47
+ deps.ringBell();
48
+ }