@fyeeme/pi-peon-ping 1.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ ## [1.0.0] - 2025-07-25
4
+
5
+ ### Added
6
+
7
+ - Initial release of pi-peon-ping
8
+ - Route pi lifecycle events through peon.sh:
9
+ - `session_start` → `SessionStart`
10
+ - `turn_start` → `UserPromptSubmit`
11
+ - `turn_end` → `Stop`
12
+ - `tool_result` (isError) → `PostToolUseFailure`
13
+ - `session_shutdown` → `SessionEnd`
14
+ - Terminal tab title updates with visual status indicators
15
+ - Auto-discovery of peon.sh at standard install paths
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 fyeeme
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,110 @@
1
+ # pi-peon-ping
2
+
3
+ peon-ping adapter for [pi](https://github.com/earendil-works/pi-coding-agent). Routes pi lifecycle events through `peon.sh` for sound packs, desktop notifications, and trainer reminders.
4
+
5
+ ## Features
6
+
7
+ - **Sound packs** — Warcraft, StarCraft, Portal, Red Alert, and 160+ game character voice packs
8
+ - **Desktop notifications** — overlay banners when the terminal is not focused
9
+ - **Trainer reminders** — Pavel-style daily exercise nagging
10
+ - **SSH/devcontainer relay** — sounds play on your local machine, not the remote
11
+ - **Mobile notifications** — push to your phone via ntfy, Pushover, or Telegram
12
+ - **Tab title updates** — `● project: working...` / `✓ project: done` / `✗ project: error`
13
+
14
+ ## Prerequisites
15
+
16
+ Install [peon-ping](https://github.com/PeonPing/peon-ping) first:
17
+
18
+ **Option 1: Homebrew**
19
+
20
+ ```bash
21
+ brew install PeonPing/tap/peon-ping
22
+ ```
23
+
24
+ **Option 2: Installer script (macOS, Linux, WSL2)**
25
+
26
+ ```bash
27
+ curl -fsSL https://raw.githubusercontent.com/PeonPing/peon-ping/main/install.sh | bash
28
+ ```
29
+
30
+ ## Installation
31
+
32
+ ### As a pi package
33
+
34
+ ```bash
35
+ pi install @fyeeme/pi-peon-ping
36
+ ```
37
+
38
+ ### Manual (development)
39
+
40
+ Copy `index.ts` to `~/.pi/agent/extensions/pi-peon-ping/` (global) or `.pi/extensions/pi-peon-ping/` (project-local), then restart pi.
41
+
42
+ ### From the monorepo
43
+
44
+ This package lives at `packages/extensions/pi-peon-ping/`. To use it locally, symlink or copy the `index.ts` to your extensions directory:
45
+
46
+ ```bash
47
+ ln -s $(pwd)/packages/extensions/pi-peon-ping ~/.pi/agent/extensions/pi-peon-ping
48
+ ```
49
+
50
+ ## Event Mapping
51
+
52
+ | pi event | peon-ping event | Occurs when |
53
+ |------------------------------|------------------------|---------------------------------|
54
+ | `session_start` | `SessionStart` | Pi starts or resumes a session |
55
+ | `turn_start` | `UserPromptSubmit` | You submit a prompt |
56
+ | `turn_end` | `Stop` | Agent finishes responding |
57
+ | `tool_result` (isError) | `PostToolUseFailure` | A tool call fails |
58
+ | `session_shutdown` | `SessionEnd` | Session ends |
59
+
60
+ ## Configuration
61
+
62
+ ### Environment variable
63
+
64
+ | Variable | Description |
65
+ |----------|-------------|
66
+ | `PEON_SH` | Absolute path to `peon.sh` / `peon.ps1`. Overrides all automatic discovery. |
67
+
68
+ ### peon.sh Discovery
69
+
70
+ pi-peon-ping 自动查找已安装的 `peon.sh`,优先级如下(高 → 低):
71
+
72
+ | 优先级 | 来源 | 路径 |
73
+ |--------|------|------|
74
+ | 1 | `$PEON_SH` 环境变量 | 用户指定 |
75
+ | 2 | Homebrew (macOS) | `$(brew --prefix peon-ping)/libexec/peon.sh` |
76
+ | 3 | curl 安装 (Linux/macOS) | `~/.claude/hooks/peon-ping/peon.sh` |
77
+ | 4 | `--openpeon` 模式 | `~/.openpeon/hooks/peon-ping/peon.sh` |
78
+ | 5 | OpenClaw 安装 | `~/.openclaw/hooks/peon-ping/peon.sh` |
79
+ | 6 | Windows/Git Bash WSL2 回退 | `~/.claude/hooks/peon-ping/peon.ps1` |
80
+
81
+ 当找到 `.ps1` 文件时,自动使用 `powershell` 而非 `bash` 执行。
82
+
83
+ ### peon-ping 配置
84
+
85
+ 所有 peon-ping 的功能配置通过 `peon` CLI 进行:
86
+
87
+ ```bash
88
+ peon packs use glados # Switch sound pack
89
+ peon packs install --all # Install all packs
90
+ peon trainer on # Enable trainer mode
91
+ peon notifications off # Mute popups but keep sounds
92
+ peon mobile ntfy my-topic # Push notifications to phone
93
+ peon ssh-audio relay # Route audio for remote sessions
94
+ ```
95
+
96
+ ## Uninstall
97
+
98
+ ```bash
99
+ rm -rf ~/.pi/agent/extensions/pi-peon-ping
100
+ ```
101
+
102
+ ## How It Works
103
+
104
+ This is a thin adapter — it does not play sounds or send notifications directly. Instead, it finds `peon.sh` (from the peon-ping install) and spawns it with a JSON payload on each lifecycle event. `peon.sh` handles all sound selection, playback, notifications, and trainer logic.
105
+
106
+ The tab title is updated to reflect current agent state: `● working...` during a turn, `✓ done` on completion, `✗ error` on tool failures.
107
+
108
+ ## Credits
109
+
110
+ Based on the [oh-my-pi (omp) adapter](https://github.com/PeonPing/peon-ping/tree/main/adapters/omp) from the peon-ping project. Sound packs by [OpenPeon](https://openpeon.com/).
package/index.ts ADDED
@@ -0,0 +1,234 @@
1
+ /**
2
+ * pi-peon-ping — peon-ping adapter for pi
3
+ *
4
+ * Routes pi ExtensionAPI lifecycle events through peon.sh, giving pi users
5
+ * access to ALL peon-ping features:
6
+ * - Sound packs & rotation (Warcraft, StarCraft, Portal, etc.)
7
+ * - Desktop notifications
8
+ * - Trainer reminders (pushups, squats, etc.)
9
+ * - Spam detection
10
+ * - SSH/devcontainer relay
11
+ * - All config options via `peon` CLI
12
+ * - Tab title updates
13
+ *
14
+ * Event mapping (pi ExtensionAPI → peon.sh hook_event_name):
15
+ * session_start → SessionStart
16
+ * turn_start → UserPromptSubmit
17
+ * turn_end → Stop
18
+ * tool_result (event.isError === true) → PostToolUseFailure
19
+ * session_shutdown → SessionEnd
20
+ *
21
+ * peon.sh discovery (first match wins):
22
+ * 1. $PEON_SH env var (explicit override)
23
+ * 2. brew --prefix peon-ping (macOS Homebrew)
24
+ * 3. ~/.claude/hooks/peon-ping/peon.sh (curl installer, Linux/macOS)
25
+ * 4. ~/.openpeon/hooks/peon-ping/peon.sh (tool-agnostic install)
26
+ * 5. ~/.openclaw/hooks/peon-ping/peon.sh (OpenClaw install)
27
+ * 6. ~/.claude/hooks/peon-ping/peon.ps1 (Windows/Git Bash WSL2 fallback)
28
+ *
29
+ * Requires peon-ping installed:
30
+ * brew install PeonPing/tap/peon-ping
31
+ * # or: curl -fsSL peonping.com/install | bash
32
+ *
33
+ * Based on the oh-my-pi (omp) adapter from:
34
+ * https://github.com/PeonPing/peon-ping/tree/main/adapters/omp
35
+ */
36
+
37
+ import { spawn, execFileSync } from "node:child_process";
38
+ import { existsSync } from "node:fs";
39
+ import { homedir, platform } from "node:os";
40
+ import { basename, join } from "node:path";
41
+ import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
42
+
43
+ // ============================================================================
44
+ // Constants
45
+ // ============================================================================
46
+
47
+ /** Template paths for peon.sh discovery. $HOME is resolved at runtime. */
48
+ export const PEON_SH_TEMPLATES = [
49
+ // Homebrew (macOS)
50
+ "$(brew --prefix peon-ping)/libexec/peon.sh",
51
+ // curl installer default (Linux/macOS)
52
+ "$HOME/.claude/hooks/peon-ping/peon.sh",
53
+ // --openpeon mode (tool-agnostic root)
54
+ "$HOME/.openpeon/hooks/peon-ping/peon.sh",
55
+ // OpenClaw install
56
+ "$HOME/.openclaw/hooks/peon-ping/peon.sh",
57
+ // Windows/Git Bash WSL2 fallback
58
+ "$HOME/.claude/hooks/peon-ping/peon.ps1",
59
+ ];
60
+
61
+ // ============================================================================
62
+ // peon.sh discovery
63
+ // ============================================================================
64
+
65
+ export function resolvePeonShPaths(): string[] {
66
+ const home = homedir();
67
+ return PEON_SH_TEMPLATES.map((t) => t.replace("$HOME", home));
68
+ }
69
+
70
+ /**
71
+ * Try to resolve the Homebrew peon-ping prefix.
72
+ * Returns the libexec/peon.sh path, or undefined if brew is unavailable.
73
+ */
74
+ export function tryBrewPrefix(): string | undefined {
75
+ if (platform() !== "darwin") return undefined;
76
+ try {
77
+ const prefix = execFileSync("brew", ["--prefix", "peon-ping"], {
78
+ encoding: "utf-8",
79
+ timeout: 3000,
80
+ stdio: ["ignore", "pipe", "ignore"],
81
+ }).trim();
82
+ if (!prefix) return undefined;
83
+ const shPath = join(prefix, "libexec", "peon.sh");
84
+ return existsSync(shPath) ? shPath : undefined;
85
+ } catch {
86
+ return undefined;
87
+ }
88
+ }
89
+
90
+ export function findPeonSh(): string | null {
91
+ // 1. PEON_SH env var override
92
+ const envPath = process.env.PEON_SH;
93
+ if (envPath) {
94
+ try {
95
+ if (existsSync(envPath)) return envPath;
96
+ } catch {
97
+ // permission denied
98
+ }
99
+ }
100
+
101
+ // 2. Homebrew prefix (macOS only)
102
+ const brewPath = tryBrewPrefix();
103
+ if (brewPath) return brewPath;
104
+
105
+ // 3-6. Static paths
106
+ for (const p of resolvePeonShPaths()) {
107
+ try {
108
+ if (existsSync(p)) return p;
109
+ } catch {
110
+ // permission denied or other fs error
111
+ }
112
+ }
113
+ return null;
114
+ }
115
+
116
+ // ============================================================================
117
+ // Shell detection
118
+ // ============================================================================
119
+
120
+ /**
121
+ * Returns the shell command to use for spawning peon.
122
+ * On Windows (Git Bash / WSL2), peon.sh may be peon.ps1 and needs powershell.
123
+ */
124
+ export function resolveShellAndScript(
125
+ peonPath: string,
126
+ ): { shell: string; script: string } {
127
+ if (peonPath.endsWith(".ps1")) {
128
+ return { shell: "powershell", script: peonPath };
129
+ }
130
+ return { shell: "bash", script: peonPath };
131
+ }
132
+
133
+ // ============================================================================
134
+ // Terminal tab title helpers
135
+ // ============================================================================
136
+
137
+ export function setTabTitle(title: string): void {
138
+ process.stdout.write(`\x1b]0;${title}\x07`);
139
+ }
140
+
141
+ // ============================================================================
142
+ // Session ID helpers
143
+ // ============================================================================
144
+
145
+ export function getSessionId(ctx: ExtensionContext): string {
146
+ try {
147
+ const sm = ctx.sessionManager as { getSessionFile?: () => string | undefined };
148
+ const file = sm.getSessionFile?.();
149
+ if (file) return file;
150
+ } catch {
151
+ // session manager not available
152
+ }
153
+ return `pi-${Date.now()}`;
154
+ }
155
+
156
+ // ============================================================================
157
+ // peon.sh invocation
158
+ // ============================================================================
159
+
160
+ export function firePeon(
161
+ peonPath: string,
162
+ event: string,
163
+ cwd: string,
164
+ sessionId: string,
165
+ ): void {
166
+ const payload = JSON.stringify({
167
+ hook_event_name: event,
168
+ notification_type: "",
169
+ cwd,
170
+ session_id: sessionId,
171
+ permission_mode: "",
172
+ source: "pi",
173
+ });
174
+
175
+ try {
176
+ const { shell, script } = resolveShellAndScript(peonPath);
177
+ const proc = spawn(shell, [script], {
178
+ stdio: ["pipe", "ignore", "ignore"],
179
+ });
180
+ proc.stdin.write(payload);
181
+ proc.stdin.end();
182
+ proc.unref();
183
+ } catch {
184
+ // best-effort, silently ignore
185
+ }
186
+ }
187
+
188
+ // ============================================================================
189
+ // Extension
190
+ // ============================================================================
191
+
192
+ export default function peonPingExtension(pi: ExtensionAPI): void {
193
+ const peonPath = findPeonSh();
194
+ if (!peonPath) {
195
+ console.warn("[pi-peon-ping] peon.sh not found. Install peon-ping first:");
196
+ console.warn(" brew install PeonPing/tap/peon-ping");
197
+ console.warn(" # or: curl -fsSL peonping.com/install | bash");
198
+ console.warn(" # or: set PEON_SH=/path/to/peon.sh");
199
+ return;
200
+ }
201
+
202
+ const cwd = process.cwd();
203
+ const projectName = basename(cwd) || "pi";
204
+
205
+ pi.on("session_start", async (_event, ctx) => {
206
+ const sessionId = getSessionId(ctx);
207
+ if (ctx.hasUI) setTabTitle(`● ${projectName}: ready`);
208
+ firePeon(peonPath, "SessionStart", cwd, sessionId);
209
+ });
210
+
211
+ pi.on("turn_start", async (_event, ctx) => {
212
+ const sessionId = getSessionId(ctx);
213
+ if (ctx.hasUI) setTabTitle(`● ${projectName}: working...`);
214
+ firePeon(peonPath, "UserPromptSubmit", cwd, sessionId);
215
+ });
216
+
217
+ pi.on("turn_end", async (_event, ctx) => {
218
+ const sessionId = getSessionId(ctx);
219
+ if (ctx.hasUI) setTabTitle(`✓ ${projectName}: done`);
220
+ firePeon(peonPath, "Stop", cwd, sessionId);
221
+ });
222
+
223
+ pi.on("tool_result", async (event, ctx) => {
224
+ if (!event.isError) return;
225
+ const sessionId = getSessionId(ctx);
226
+ if (ctx.hasUI) setTabTitle(`✗ ${projectName}: error`);
227
+ firePeon(peonPath, "PostToolUseFailure", cwd, sessionId);
228
+ });
229
+
230
+ pi.on("session_shutdown", async (_event, ctx) => {
231
+ const sessionId = getSessionId(ctx);
232
+ firePeon(peonPath, "SessionEnd", cwd, sessionId);
233
+ });
234
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@fyeeme/pi-peon-ping",
3
+ "version": "1.0.0",
4
+ "description": "peon-ping adapter for pi — routes lifecycle events through peon.sh for sound packs, notifications, and trainer reminders.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "fyeeme",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/fyeeme/pi-packages"
14
+ },
15
+ "bugs": {
16
+ "url": "https://github.com/fyeeme/pi-packages/issues"
17
+ },
18
+ "homepage": "https://github.com/fyeeme/pi-packages#readme",
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "keywords": [
23
+ "pi-package",
24
+ "pi",
25
+ "peon-ping",
26
+ "sounds",
27
+ "notifications",
28
+ "lifecycle",
29
+ "omp"
30
+ ],
31
+ "files": [
32
+ "index.ts",
33
+ "README.md",
34
+ "LICENSE",
35
+ "CHANGELOG.md"
36
+ ],
37
+ "pi": {
38
+ "extensions": [
39
+ "./index.ts"
40
+ ]
41
+ },
42
+ "scripts": {
43
+ "test": "vitest --run",
44
+ "typecheck": "tsc"
45
+ },
46
+ "peerDependencies": {
47
+ "@earendil-works/pi-coding-agent": ">=0.77.0"
48
+ },
49
+ "devDependencies": {
50
+ "@earendil-works/pi-coding-agent": "0.77.0",
51
+ "@types/node": "22.19.19",
52
+ "typescript": "5.9.3"
53
+ }
54
+ }