Skip to content

Commit 46b644b

Browse files
committed
feat: v0.2.0 - env var config, comprehensive tests, README rewrite
Implementation: - OPENCODE_BELL_EVENTS: filter which events trigger the bell - OPENCODE_BELL_DEBOUNCE: customize debounce window - Unified debounce key format (event.type:sessionId) - ring(key, now) with injectable timestamp for testing Tests (11 cases): - Env var config filtering, custom debounce, invalid fallback - Debounce window suppression, expiry, key isolation - TTY guard for true/false/undefined Docs: - README with features list, config section, events table, GIF placeholder - CLAUDE.md updated with env var config reference
1 parent dc94ce0 commit 46b644b

5 files changed

Lines changed: 424 additions & 43 deletions

File tree

CLAUDE.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project
6+
7+
opencode-bell 是 OpenCode CLI 的终端铃声通知插件。监听 4 种事件(`permission.asked``question.asked``session.idle``session.error`),通过输出 BEL 字符(`\x07`)实现通知。零依赖、单文件实现。
8+
9+
隐私设计:不读取消息内容、不执行外部命令、无网络调用,仅在 TTY 终端输出铃声。
10+
11+
## Commands
12+
13+
```bash
14+
npm test # 运行测试(node:test 原生框架)
15+
node -e "import('./index.js')" # 烟雾测试:验证模块可导入
16+
```
17+
18+
无构建步骤,纯 JS ES Module 直接发布。
19+
20+
## Architecture
21+
22+
`index.js` 导出 `OpencodeBellPlugin` 异步工厂函数,返回 `{ event(e) }` 插件对象。核心机制:
23+
24+
- **会话级防抖**:以 `eventType:sessionId` 为 key,默认 1200ms 内同 key 最多响铃一次
25+
- **TTY 守卫**`process.stdout.isTTY` 为 false 时静默跳过
26+
27+
### 环境变量配置
28+
29+
| 变量 | 说明 | 默认值 |
30+
|------|------|--------|
31+
| `OPENCODE_BELL_EVENTS` | 逗号分隔的监听事件列表 | `permission.asked,question.asked,session.idle,session.error` |
32+
| `OPENCODE_BELL_DEBOUNCE` | 防抖窗口,单位毫秒 | `1200` |
33+
34+
### 测试辅助
35+
36+
插件对象同时暴露 `_ring(key, now)` 方法,供测试直接注入时间戳验证防抖逻辑,生产使用中忽略即可。
37+
38+
## Publishing
39+
40+
`package.json``files` 字段限制发布为 3 个文件:`index.js``README.md``LICENSE`。用户通过在 `~/.config/opencode/opencode.json` 添加 `"plugin": ["opencode-bell@版本"]` 安装。

README.md

Lines changed: 43 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,68 @@
11
# opencode-bell
22

3-
OpenCode plugin that triggers a terminal bell on key events.
3+
<!-- TODO: replace with actual recording -->
4+
<p align="center"><img src="./docs/demo.gif" alt="opencode-bell demo" width="600"></p>
45

5-
Privacy-friendly by design: no system notifications, no message content, no external commands, no network calls. It only writes the BEL control character (`\x07`) to stdout.
6+
Never miss an OpenCode prompt again.
67

7-
## Install (recommended: npm)
8+
- Zero dependencies — single-file plugin
9+
- Privacy-friendly — no message content, no external commands, no network calls
10+
- Configurable — choose which events trigger the bell
11+
- Smart debounce — avoids bell spam for the same event type per session
812

9-
1. Add the plugin to `~/.config/opencode/opencode.json`:
13+
## Install
14+
15+
### npm (recommended)
16+
17+
Add the plugin to `~/.config/opencode/opencode.json`:
1018

1119
```json
1220
{
1321
"$schema": "https://opencode.ai/config.json",
14-
"plugin": ["opencode-bell@0.1.1"]
22+
"plugin": ["opencode-bell@0.2.0"]
1523
}
1624
```
1725

18-
2. Restart OpenCode CLI.
19-
20-
OpenCode will auto-install the package and cache it under `~/.cache/opencode/node_modules/`.
26+
Restart OpenCode. It will auto-install the package and cache it under `~/.cache/opencode/node_modules/`.
2127

22-
## Install (local file)
28+
### Local file
2329

2430
Copy `index.js` into `~/.config/opencode/plugins/` and restart OpenCode.
2531

26-
## What it does
32+
## Configuration
33+
34+
Two environment variables control behavior:
35+
36+
| Variable | Default | Description |
37+
|---|---|---|
38+
| `OPENCODE_BELL_EVENTS` | `permission.asked,question.asked,session.idle,session.error` | Comma-separated list of events that trigger the bell |
39+
| `OPENCODE_BELL_DEBOUNCE` | `1200` | Minimum ms between repeated bells for the same event type and session. Must be >= 1; 0 or negative falls back to default |
40+
41+
Example — bell only on permission requests, with a 5-second debounce:
42+
43+
```sh
44+
export OPENCODE_BELL_EVENTS="permission.asked"
45+
export OPENCODE_BELL_DEBOUNCE="5000"
46+
```
47+
48+
## Supported Events
2749

28-
The plugin triggers a terminal bell for these events:
50+
| Event | Fires when |
51+
|---|---|
52+
| `permission.asked` | OpenCode needs approval to run a tool |
53+
| `question.asked` | OpenCode asks the user a question |
54+
| `session.idle` | A session completes and is waiting for input |
55+
| `session.error` | A session encounters an error |
2956

30-
- `permission.asked`
31-
- `question.asked`
32-
- `session.idle`
33-
- `session.error`
57+
## How it works
3458

35-
## Verify
59+
The plugin writes the BEL character (`\x07`) to stdout. Your terminal emulator interprets it — typically as an audio beep, a visual flash, or a dock badge, depending on your terminal settings.
3660

37-
- Trigger a permission request (e.g., any tool that requires approval).
38-
- Or wait for a session to complete (`session.idle`).
61+
## Troubleshooting
3962

40-
If you do not hear/see anything, check Terminal.app settings:
63+
No sound or flash? Check your terminal bell settings:
4164

42-
- Terminal.app -> Settings -> Profiles -> Bell
65+
- **Terminal.app**: Settings -> Profiles -> Bell
4366

4467
## Uninstall
4568

index.js

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,32 @@
1+
// Event types that trigger the bell, comma-separated. Defaults to all four.
2+
const DEFAULT_EVENTS = "permission.asked,question.asked,session.idle,session.error"
3+
// Empty string or whitespace-only also falls back to defaults
4+
const rawEvents = process.env.OPENCODE_BELL_EVENTS || DEFAULT_EVENTS
5+
const parsed = rawEvents.split(",").map((s) => s.trim()).filter(Boolean)
6+
const ENABLED_EVENTS = new Set(parsed.length > 0 ? parsed : DEFAULT_EVENTS.split(","))
7+
8+
// Debounce window in ms. Falls back to 1200 if NaN, negative, or zero.
9+
const parsedDebounce = parseInt(process.env.OPENCODE_BELL_DEBOUNCE, 10)
10+
const DEBOUNCE_MS = parsedDebounce > 0 ? parsedDebounce : 1200
11+
12+
/**
13+
* OpencodeBellPlugin — OpenCode plugin factory.
14+
* Listens for configured events and writes ASCII BEL to the terminal.
15+
* Same key only rings once within the debounce window.
16+
*/
117
export const OpencodeBellPlugin = async () => {
218
const bell = "\x07"
3-
const debounceMs = 1200
19+
// Map<key, lastRingTimestamp> — tracks last ring time per key
420
const last = new Map()
521

6-
const ring = (key) => {
7-
const now = Date.now()
22+
/**
23+
* ring — debounced bell for a given key.
24+
* @param {string} key - debounce key, format "event.type:sessionId" or "event.type"
25+
* @param {number} now - current timestamp in ms, injectable for testing
26+
*/
27+
const ring = (key, now = Date.now()) => {
828
const prev = last.get(key) || 0
9-
if (now - prev < debounceMs) return
29+
if (now - prev < DEBOUNCE_MS) return
1030
last.set(key, now)
1131
if (process.stdout && process.stdout.isTTY) {
1232
process.stdout.write(bell)
@@ -15,22 +35,14 @@ export const OpencodeBellPlugin = async () => {
1535

1636
return {
1737
event: async ({ event }) => {
18-
if (event?.type === "permission.asked") {
19-
const sessionId = event?.properties?.sessionID
20-
ring(sessionId ? `permission:${sessionId}` : "permission")
21-
}
22-
if (event?.type === "question.asked") {
23-
const sessionId = event?.properties?.sessionID
24-
ring(sessionId ? `question:${sessionId}` : "question")
25-
}
26-
if (event?.type === "session.idle") {
27-
const sessionId = event?.properties?.sessionID
28-
ring(sessionId ? `idle:${sessionId}` : "idle")
29-
}
30-
if (event?.type === "session.error") {
31-
const sessionId = event?.properties?.sessionID
32-
ring(sessionId ? `error:${sessionId}` : "error")
33-
}
34-
}
38+
// Only handle events in the configured set, silently skip the rest
39+
if (!ENABLED_EVENTS.has(event?.type)) return
40+
const sessionId = event?.properties?.sessionID
41+
// Use "type:id" when sessionId exists, plain "type" when missing (no trailing colon)
42+
const key = sessionId ? `${event.type}:${sessionId}` : event.type
43+
ring(key)
44+
},
45+
// Exposed for testing
46+
_ring: ring,
3547
}
3648
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "opencode-bell",
3-
"version": "0.1.1",
3+
"version": "0.2.0",
44
"description": "OpenCode plugin: privacy-friendly terminal bell notifications",
55
"license": "MIT",
66
"type": "module",

0 commit comments

Comments
 (0)