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 +1 -0
- package/LICENSE +21 -0
- package/README.md +64 -0
- package/SECURITY.md +13 -0
- package/index.ts +54 -0
- package/package.json +75 -0
- package/src/boop.ts +65 -0
- package/src/player.ts +48 -0
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
|
+
}
|