Skip to content

Commit 2088edc

Browse files
duyetduyetbot
andcommitted
feat(sag-notify): genericize for sharing, add language presets and sag install reference
- Remove personal/environment-specific info: no absolute home paths, no ~/.secret default, no real project names, no account-bound (professional) voice id - Default config is now generic + English; key resolved from ELEVENLABS_API_KEY env (optional user key_file) - Add multi-language presets (languages.<lang>); language setting selects preset, templates.* overrides win, en fallback. Ship en + vi - Add references/sag-cli.md: install (brew steipete/tap/sag), auth, env vars, models, voices - lib.sh sag_template() resolves template by precedence; sag_resolve_key no longer assumes ~/.secret - Update skill, README, and setup/config/test commands to be generic and language-aware - Add 'No Personal / Environment-Specific Information' rule to repo CLAUDE.md - Bump plugin to 1.1.0 Co-Authored-By: duyetbot <duyetbot@users.noreply.github.com>
1 parent 44e9513 commit 2088edc

12 files changed

Lines changed: 224 additions & 53 deletions

File tree

CLAUDE.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,30 @@ plugin-name/
5353

5454
When changing plugin metadata, keep Claude and Codex manifests in sync and run `bash scripts/validate-plugins.sh`.
5555

56+
## No Personal / Environment-Specific Information
57+
58+
Plugins in this repo are **published and shared**. Never bake personal or
59+
machine-specific information into any plugin file (manifests, hooks, scripts,
60+
skills, commands, configs, docs, examples).
61+
62+
**Never hardcode:**
63+
- Absolute home paths (`/Users/<you>/…`) — use `$HOME`, `~`, `${CLAUDE_PLUGIN_ROOT}`, or `$PWD`.
64+
- Personal secret-file conventions (e.g. `~/.secret`) — read secrets from documented env vars; allow an optional, user-set key-file path.
65+
- Real project, client, host, or repo names — use neutral placeholders (`demo-project`, `/path/to/project`).
66+
- Account-bound resources (private/professional voice IDs, API keys, tokens, account ids, emails).
67+
68+
**Defaults must be generic.** Ship neutral, English-first defaults that work for
69+
any user. Put personal preferences (language, voice, key location, custom
70+
wording) in the user's own config under `~/.config/<plugin>/` — outside the repo,
71+
never committed.
72+
73+
Author/owner metadata (`author`, marketplace `owner`) is allowed — that is
74+
legitimate attribution, not environment-specific leakage. Shipping additional
75+
**language presets** (e.g. a `vi` template set) is a feature, not personal info.
76+
77+
Before committing a plugin, scan for leaks:
78+
`grep -rniE "/Users/|~/\.secret|<your-name>|<real-project-names>" <plugin>/`
79+
5680
## Commit Convention
5781

5882
Use semantic commits with plugin scope:

sag-notify/.claude-plugin/plugin.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "sag-notify",
3-
"version": "1.0.0",
4-
"description": "Spoken voice notifications via sag (ElevenLabs TTS). Speaks a short alert when Claude needs you, and a per-turn summary when work finishes — naming the project. Auto-injected hooks; configurable voice, language, and message templates.",
3+
"version": "1.1.0",
4+
"description": "Spoken voice notifications via sag (ElevenLabs TTS). Speaks a short alert when Claude needs you, and a per-turn summary when work finishes — naming the project. Auto-injected hooks; configurable voice, multi-language presets, and message templates.",
55
"author": {
66
"name": "duyet"
77
}

sag-notify/README.md

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,49 @@
11
# sag-notify
22

3-
Spoken voice notifications for Claude Code via [`sag`](https://github.com/) (ElevenLabs TTS).
3+
Spoken voice notifications for Claude Code via [`sag`](https://github.com/steipete/sag) (ElevenLabs TTS).
44

55
- **Needs you** → when Claude waits for permission/input, it speaks a short alert naming the project.
66
- **Turn done** → when Claude finishes substantive work, it speaks a one-line summary you author.
77

88
Both are driven by **auto-injected hooks** — once the plugin is enabled, every session gets them, no per-session setup.
99

10+
## Requirements
11+
12+
The `sag` CLI must be installed and authenticated. Install with `brew install steipete/tap/sag` and set `ELEVENLABS_API_KEY`. Full guide: [skills/sag-voice/references/sag-cli.md](skills/sag-voice/references/sag-cli.md).
13+
1014
## How it works
1115

1216
| Event | Hook | Behavior |
1317
|-------|------|----------|
14-
| `Notification` | `hooks/notify.sh` | Speaks `templates.notification` with the project name. The harness message is English, so it's replaced by your (Vietnamese by default) template. |
15-
| `Stop` | `hooks/summary.sh` | Reads the body Claude wrote to `summary_file`, speaks it via `templates.summary`, deletes the file. **Silent when no file exists**, so trivial turns are quiet. |
18+
| `Notification` | `hooks/notify.sh` | Speaks the notification template, naming the project, in the configured language. (The harness message is English, so it is replaced by the template.) |
19+
| `Stop` | `hooks/summary.sh` | Reads the body Claude wrote to `summary_file`, speaks it via the summary template, deletes the file. **Silent when no file exists**, so trivial turns are quiet. |
1620

17-
To make Claude speak a summary, it writes a short body to `~/.claude/.sag-summary` during a substantive turn. The hook adds the `Đây là Claude, project <name>.` prefix.
21+
To make Claude speak a summary, it writes a short body to `~/.claude/.sag-summary` during a substantive turn. The hook adds the `This is <name>, reporting from project <name>:` framing automatically.
1822

1923
## Setup
2024

21-
1. Enable the plugin (it ships `hooks/hooks.json`).
22-
2. Run `/sag-notify:setup` — checks `sag`/`jq`, resolves the API key, picks a voice, writes config, and tests audio.
23-
3. Or just rely on defaults: Brian voice, Vietnamese, key from `~/.secret`.
25+
1. Install + authenticate `sag` (see Requirements).
26+
2. Enable the plugin (it ships `hooks/hooks.json`).
27+
3. Run `/sag-notify:setup` — checks `sag`/`jq`, resolves the API key, picks a voice, writes config, and tests audio.
28+
4. Or just rely on defaults: Brian voice, English templates, key from the `ELEVENLABS_API_KEY` environment variable.
2429

25-
`ELEVENLABS_API_KEY` must be resolvable — from the env or the configured `key_file` (default `~/.secret`).
30+
`ELEVENLABS_API_KEY` must be resolvable — from the environment, or from an optional key file you point `key_file` at in your user config.
2631

2732
## Configuration
2833

29-
User config at `~/.config/sag-notify/config.json` overrides [`config.default.json`](config.default.json). See the **sag-voice** skill or run `/sag-notify:config`. Key settings: `enabled`, `events.{notification,summary}`, `voice_id`, `model_id`, `self_name`, `language`, `key_file`, `summary_file`, `error_log`, `max_chars`, `templates.{notification,summary}`.
34+
User config at `~/.config/sag-notify/config.json` overrides [`config.default.json`](config.default.json). See the **sag-voice** skill or run `/sag-notify:config`. Key settings: `enabled`, `events.{notification,summary}`, `voice_id`, `model_id`, `self_name`, `language`, `key_file`, `summary_file`, `error_log`, `max_chars`, `templates.{notification,summary}`, `languages.<lang>.{notification,summary}`.
35+
36+
### Languages
37+
38+
Set `language` to pick a built-in preset. `en` and `vi` ship by default; add more under `languages.<lang>` (placeholders `{name}`, `{project}`, `{body}`). Resolution: explicit `templates.<kind>` override → `languages.<language>.<kind>``languages.en.<kind>`.
39+
40+
```bash
41+
jq '.language = "vi"' ~/.config/sag-notify/config.json > /tmp/c && mv /tmp/c ~/.config/sag-notify/config.json
42+
```
3043

3144
### Voice tiers (important)
3245

33-
`premade` voices work on the **free** ElevenLabs tier. `professional`/library voices (e.g. the native Vietnamese "Minh Trung" `FTYCiQT21H9XQvhRu0ch`) return **402 Payment Required** unless you upgrade. Default is **Brian** (`nPczCjzI2devNBz1zQrb`, premade) speaking Vietnamese via the multilingual `eleven_flash_v2_5` model.
46+
`premade` voices work on the **free** ElevenLabs tier. `professional`/library voices return **402 Payment Required** unless you upgrade your plan. The default is **Brian** (`nPczCjzI2devNBz1zQrb`, premade); `eleven_flash_v2_5` is multilingual so a premade voice can speak any language (with an accent). Run `sag voices` to list what your key can use.
3447

3548
## Commands
3649

sag-notify/commands/config.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,16 +15,18 @@ jq '<filter>' ~/.config/sag-notify/config.json > /tmp/sag-cfg.json && mv /tmp/sa
1515
```
1616

1717
Common changes:
18+
- **Language**: `.language = "vi"` (built-in: `en`, `vi`). Add a new one: `.languages.fr = {"notification":"…{name}…{project}…","summary":"…{name}…{project}…{body}…"}`.
1819
- **Voice**: `.voice_id = "<ID>"` (run `sag voices`; premade = free, professional = paid/402).
20+
- **Custom wording** (overrides the language preset): `.templates.notification = "…"` / `.templates.summary = "…"`. Set to `""` to fall back to the language preset.
1921
- **Disable a sound**: `.events.notification = false` or `.events.summary = false`.
2022
- **Turn everything off**: `.enabled = false`.
21-
- **Language → English**: set both templates, e.g. `.templates.notification = "This is {name}, project {project} needs your input."` and `.templates.summary = "This is {name} on project {project}. {body}"`.
2223
- **Name**: `.self_name = "..."`.
2324
- **Length cap**: `.max_chars = 200`.
25+
- **Key file**: `.key_file = "~/path/to/keyfile"` (sourced if `ELEVENLABS_API_KEY` is unset).
2426

2527
After changing the voice or templates, verify in the FOREGROUND (hooks background the call):
2628
```
27-
! source ~/.secret 2>/dev/null; sag speak --model-id "$(jq -r .model_id ~/.config/sag-notify/config.json)" --voice-id "$(jq -r .voice_id ~/.config/sag-notify/config.json)" "$(jq -r .templates.summary ~/.config/sag-notify/config.json | sed 's/{name}/Claude/;s/{project}/test/;s/{body}/kiểm tra/')" 2>&1 | grep -iE "failed|402" || echo OK
29+
! V=$(jq -r .voice_id ~/.config/sag-notify/config.json); M=$(jq -r .model_id ~/.config/sag-notify/config.json); sag speak --model-id "$M" --voice-id "$V" "test" 2>&1 | grep -iE "failed|402" || echo OK
2830
```
2931

3032
Always validate the result: `jq . ~/.config/sag-notify/config.json`.

sag-notify/commands/setup.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,24 @@ One-time setup for spoken voice notifications via `sag` (ElevenLabs TTS).
66

77
Walk the user through setup, doing the work for them where possible:
88

9-
1. **Check `sag` and `jq`** are installed: `command -v sag jq`. If `sag` is missing, tell the user to install it (it's the ElevenLabs TTS CLI) and stop.
9+
1. **Check `sag` and `jq`** are installed: `command -v sag jq`. If `sag` is missing, point them at the install guide [skills/sag-voice/references/sag-cli.md](../skills/sag-voice/references/sag-cli.md) (`brew install steipete/tap/sag`) and stop.
1010

11-
2. **Resolve the API key.** Check `ELEVENLABS_API_KEY` in the env and in the key file (`~/.secret` by default). If absent, instruct the user to add it themselves (do NOT ask them to paste the secret in chat):
11+
2. **Resolve the API key.** Check `ELEVENLABS_API_KEY` in the environment. If absent, instruct the user to add it themselves (do NOT ask them to paste the secret in chat). Recommend their shell env file so non-interactive hook shells inherit it:
1212
```
13-
! echo "export ELEVENLABS_API_KEY='your-key'" >> ~/.secret
13+
! echo "export ELEVENLABS_API_KEY='your-key'" >> ~/.zshenv
1414
```
15+
Or, if they keep secrets in a separate file, set `key_file` in their user config to that path.
1516

16-
3. **Pick a voice.** Run `sag voices` and show the `premade` voices (free tier). Use AskUserQuestion to let them choose, or default to Brian (`nPczCjzI2devNBz1zQrb`). Warn that `professional`/library voices need a paid plan (402 error).
17+
3. **Choose a language.** Ask which built-in preset (`en`, `vi`, or another they want added). Set `language` in the user config; add a `languages.<lang>` entry if it's a new one.
1718

18-
4. **Choose language + templates.** Default is Vietnamese (`Đây là {name}, project {project} …`). Offer English as an alternative.
19+
4. **Pick a voice.** Run `sag voices` and show the `premade` voices (free tier). Use AskUserQuestion to let them choose, or default to Brian (`nPczCjzI2devNBz1zQrb`). Warn that `professional`/library voices need a paid plan (402 error).
1920

2021
5. **Write user config** to `~/.config/sag-notify/config.json` (create the dir). Start from the plugin's `config.default.json` and apply their choices with `jq`.
2122

2223
6. **Verify audio in the FOREGROUND** (the hooks background the call, so exit codes lie):
2324
```
24-
! source ~/.secret 2>/dev/null; sag speak --model-id eleven_flash_v2_5 --voice-id <ID> "Đây là Claude. Thiết lập hoàn tất." 2>&1 | grep -iE "failed|402|payment" || echo OK
25+
! sag speak --model-id eleven_flash_v2_5 --voice-id <ID> "<a short test line in their language>" 2>&1 | grep -iE "failed|402|payment" || echo OK
2526
```
26-
Empty/`OK` = success. A 402 means the voice needs a paid plan — go back to step 3.
27+
Empty/`OK` = success. A 402 means the voice needs a paid plan — go back to step 4.
2728

2829
7. Confirm the hooks are active and explain that summaries only speak when Claude writes a body to the configured `summary_file`.

sag-notify/commands/test.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,14 @@ Run both hook paths the same way Claude Code would, then check the error log.
1111
! CFG=~/.config/sag-notify/config.json; [ -f "$CFG" ] || CFG="${CLAUDE_PLUGIN_ROOT}/config.default.json"; jq . "$CFG"
1212
```
1313

14-
2. **Notification path** — fire the hook with a fake cwd so it names a project:
14+
2. **Notification path** — fire the hook with a sample cwd so it names a project:
1515
```
16-
! echo '{"cwd":"/Users/duet/project/anyrouter"}' | "${CLAUDE_PLUGIN_ROOT}/hooks/notify.sh"; echo exit=$?
16+
! echo '{"cwd":"/path/to/demo-project"}' | "${CLAUDE_PLUGIN_ROOT}/hooks/notify.sh"; echo exit=$?
1717
```
1818

19-
3. **Summary path** — prime a body, then fire the Stop hook:
19+
3. **Summary path** — prime a body, then fire the Stop hook (use the configured language for the body):
2020
```
21-
! printf 'đây là một bài kiểm tra âm thanh.' > ~/.claude/.sag-summary; echo '{"cwd":"/Users/duet/project/clickhouse-monitor"}' | "${CLAUDE_PLUGIN_ROOT}/hooks/summary.sh"; echo exit=$?
21+
! printf 'this is an audio test.' > ~/.claude/.sag-summary; echo '{"cwd":"/path/to/demo-project"}' | "${CLAUDE_PLUGIN_ROOT}/hooks/summary.sh"; echo exit=$?
2222
```
2323

2424
4. **Check for silent failures** (the hooks background the call, so a clean exit ≠ sound):

sag-notify/config.default.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,23 @@
77
"voice_id": "nPczCjzI2devNBz1zQrb",
88
"model_id": "eleven_flash_v2_5",
99
"self_name": "Claude",
10-
"language": "vi",
11-
"key_file": "~/.secret",
10+
"language": "en",
11+
"key_file": "",
1212
"summary_file": "~/.claude/.sag-summary",
1313
"error_log": "~/.claude/.sag-error.log",
1414
"max_chars": 280,
1515
"templates": {
16-
"notification": "Đây là {name}. Mình đang làm ở project {project}, cần bạn xem qua một chút.",
17-
"summary": "Đây là {name}, báo cáo từ project {project}: {body}"
16+
"notification": "",
17+
"summary": ""
18+
},
19+
"languages": {
20+
"en": {
21+
"notification": "This is {name}. I'm working on project {project} and could use your input.",
22+
"summary": "This is {name}, reporting from project {project}: {body}"
23+
},
24+
"vi": {
25+
"notification": "Đây là {name}. Mình đang làm ở project {project}, cần bạn xem qua một chút.",
26+
"summary": "Đây là {name}, báo cáo từ project {project}: {body}"
27+
}
1828
}
1929
}

sag-notify/hooks/lib.sh

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,12 @@ cfg() {
2727
printf '%s' "${2:-}"
2828
}
2929

30-
# Ensure ELEVENLABS_API_KEY is available, sourcing the configured key file if needed.
30+
# Ensure ELEVENLABS_API_KEY is available. Prefer the environment; optionally source
31+
# a user-configured key file (config `key_file`). No key file is assumed by default.
3132
sag_resolve_key() {
3233
[ -n "${ELEVENLABS_API_KEY:-}" ] && return 0
33-
local kf; kf=$(sag_expand "$(cfg '.key_file' "$HOME/.secret")")
34-
[ -f "$kf" ] && . "$kf" 2>/dev/null || true
34+
local kf; kf=$(sag_expand "$(cfg '.key_file' '')")
35+
[ -n "$kf" ] && [ -f "$kf" ] && . "$kf" 2>/dev/null || true
3536
[ -n "${ELEVENLABS_API_KEY:-}" ]
3637
}
3738

@@ -40,6 +41,20 @@ sag_project() {
4041
local p; p=$(basename "$1"); printf '%s' "${p//[-_]/ }"
4142
}
4243

44+
# sag_template <notification|summary> — resolve the message template.
45+
# Precedence: explicit `templates.<kind>` override → `languages.<language>.<kind>`
46+
# preset → `languages.en.<kind>` fallback. Lets users pick a language (vi, en, …)
47+
# via the `language` setting, or fully override with their own `templates`.
48+
sag_template() {
49+
local kind="$1" lang t
50+
t=$(cfg ".templates.$kind" '')
51+
[ -n "$t" ] && { printf '%s' "$t"; return; }
52+
lang=$(cfg '.language' 'en')
53+
t=$(cfg ".languages.${lang}.${kind}" '')
54+
[ -n "$t" ] && { printf '%s' "$t"; return; }
55+
cfg ".languages.en.${kind}" ''
56+
}
57+
4358
# sag_speak <text> — fire-and-forget TTS; errors go to the configured log, never /dev/null.
4459
sag_speak() {
4560
local voice model log

sag-notify/hooks/notify.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ cwd=$(printf '%s' "$input" | jq -r '.cwd // empty' 2>/dev/null || true)
1919

2020
name=$(cfg '.self_name' 'Claude')
2121
project=$(sag_project "$cwd")
22-
tmpl=$(cfg '.templates.notification' 'Đây là {name}, project {project} cần bạn xem qua.')
22+
tmpl=$(sag_template notification)
2323

2424
sag_speak "$(sag_render "$tmpl" "$name" "$project")"
2525
exit 0

sag-notify/hooks/summary.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ maxlen=$(cfg '.max_chars' '280')
2828
body=${body:0:$maxlen}
2929
name=$(cfg '.self_name' 'Claude')
3030
project=$(sag_project "$cwd")
31-
tmpl=$(cfg '.templates.summary' 'Đây là {name}, project {project}. {body}')
31+
tmpl=$(sag_template summary)
3232

3333
sag_speak "$(sag_render "$tmpl" "$name" "$project" "$body")"
3434
exit 0

0 commit comments

Comments
 (0)