cvox 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/README.md ADDED
@@ -0,0 +1,73 @@
1
+ # cvox
2
+
3
+ [![npm version](https://img.shields.io/npm/v/cvox.svg)](https://www.npmjs.com/package/cvox)
4
+
5
+ Voice notifications for [Claude Code](https://docs.anthropic.com/en/docs/claude-code) hooks. Get spoken alerts when Claude needs permission or finishes a task — so you can step away from the screen.
6
+
7
+ ## Features
8
+
9
+ - Cross-platform TTS: macOS (`say`), Linux (`espeak`), Windows (SAPI via PowerShell)
10
+ - Two hook events: permission prompt and task completion
11
+ - Three-layer config merging: defaults → `~/.cvox.json` → project `.cvox.json`
12
+ - Idempotent installation — safe to run multiple times
13
+ - `{project}` placeholder in messages, auto-detected from directory name
14
+
15
+ ## Quick Start
16
+
17
+ ```bash
18
+ # One-liner: install hooks into your project
19
+ npx cvox init
20
+
21
+ # Or install globally
22
+ npm install -g cvox
23
+ cvox init
24
+
25
+ # Install hooks globally (applies to all projects)
26
+ cvox init --global
27
+ ```
28
+
29
+ That's it. Claude Code will now speak to you when it needs attention.
30
+
31
+ ## Configuration
32
+
33
+ Create a `.cvox.json` in your project root or home directory (`~/.cvox.json`) to customize behavior:
34
+
35
+ ```json
36
+ {
37
+ "project": "my-app",
38
+ "hooks": {
39
+ "notification": {
40
+ "enabled": true,
41
+ "message": "{project} needs permission"
42
+ },
43
+ "stop": {
44
+ "enabled": true,
45
+ "message": "{project} task complete"
46
+ }
47
+ },
48
+ "tts": {
49
+ "enabled": true
50
+ }
51
+ }
52
+ ```
53
+
54
+ | Field | Type | Default | Description |
55
+ |-------|------|---------|-------------|
56
+ | `project` | string | directory name | Project name used in `{project}` placeholder |
57
+ | `hooks.notification.enabled` | boolean | `true` | Enable voice alert on permission prompts |
58
+ | `hooks.notification.message` | string | `"{project} needs permission"` | Message spoken on permission prompt |
59
+ | `hooks.stop.enabled` | boolean | `true` | Enable voice alert on task completion |
60
+ | `hooks.stop.message` | string | `"{project} task completed"` | Message spoken on task completion |
61
+ | `tts.enabled` | boolean | `true` | Enable/disable TTS globally |
62
+
63
+ Config files are merged with deep merge — you only need to specify the fields you want to override.
64
+
65
+ ## How It Works
66
+
67
+ 1. `cvox init` injects hooks into Claude Code's `settings.json`
68
+ 2. When Claude Code triggers a hook event (permission prompt or stop), it pipes a JSON payload via stdin to `cvox notify`
69
+ 3. `cvox notify` reads the event, loads your config, and calls the platform TTS engine to speak the message
70
+
71
+ ## License
72
+
73
+ MIT
@@ -0,0 +1,14 @@
1
+ import { generateHooksConfig } from "../hooks/config.js";
2
+ import { getSettingsPath, readSettings, writeSettings, mergeHooks, } from "../utils/settings.js";
3
+ export function initCommand(options) {
4
+ const isGlobal = options.global ?? false;
5
+ const settingsPath = getSettingsPath(isGlobal);
6
+ const settings = readSettings(settingsPath);
7
+ const cvoxHooks = generateHooksConfig();
8
+ const merged = mergeHooks(settings, cvoxHooks);
9
+ writeSettings(settingsPath, merged);
10
+ const target = isGlobal ? "全局" : "项目";
11
+ console.log(`cvox: 已配置 ${target} hooks → ${settingsPath}`);
12
+ console.log(" - Notification (permission_prompt)");
13
+ console.log(" - Stop");
14
+ }
@@ -0,0 +1,78 @@
1
+ import { execFile } from "child_process";
2
+ import { loadConfig } from "../utils/config.js";
3
+ function readStdin() {
4
+ return new Promise((resolve) => {
5
+ let data = "";
6
+ process.stdin.setEncoding("utf-8");
7
+ process.stdin.on("data", (chunk) => (data += chunk));
8
+ process.stdin.on("end", () => resolve(data));
9
+ if (process.stdin.isTTY) {
10
+ resolve("");
11
+ }
12
+ });
13
+ }
14
+ function mapEventName(hookEventName) {
15
+ const map = {
16
+ Notification: "notification",
17
+ Stop: "stop",
18
+ };
19
+ return map[hookEventName] ?? null;
20
+ }
21
+ function speak(message, config) {
22
+ const { tts } = config;
23
+ if (!tts.enabled)
24
+ return;
25
+ const engine = detectEngine();
26
+ switch (engine) {
27
+ case "say": {
28
+ execFile("say", [message], () => { });
29
+ break;
30
+ }
31
+ case "espeak": {
32
+ execFile("espeak", [message], () => { });
33
+ break;
34
+ }
35
+ case "sapi": {
36
+ const ps = `Add-Type -AssemblyName System.Speech; ` +
37
+ `$s = New-Object System.Speech.Synthesis.SpeechSynthesizer; ` +
38
+ `$s.Speak('${message.replace(/'/g, "''")}')`;
39
+ execFile("powershell", ["-Command", ps], () => { });
40
+ break;
41
+ }
42
+ }
43
+ }
44
+ function detectEngine() {
45
+ switch (process.platform) {
46
+ case "darwin":
47
+ return "say";
48
+ case "win32":
49
+ return "sapi";
50
+ default:
51
+ return "espeak";
52
+ }
53
+ }
54
+ export async function notifyCommand() {
55
+ const raw = await readStdin();
56
+ if (!raw.trim())
57
+ return;
58
+ let input;
59
+ try {
60
+ input = JSON.parse(raw);
61
+ }
62
+ catch {
63
+ return;
64
+ }
65
+ const eventName = input.hook_event_name;
66
+ if (!eventName)
67
+ return;
68
+ const eventKey = mapEventName(eventName);
69
+ if (!eventKey)
70
+ return;
71
+ const cwd = input.cwd || process.cwd();
72
+ const config = loadConfig(cwd);
73
+ const hookConfig = config.hooks[eventKey];
74
+ if (!hookConfig.enabled)
75
+ return;
76
+ const message = hookConfig.message.replace(/\{project\}/g, config.project);
77
+ speak(message, config);
78
+ }
@@ -0,0 +1,23 @@
1
+ export function generateHooksConfig() {
2
+ const notifyHook = {
3
+ type: "command",
4
+ command: "cvox notify",
5
+ async: true,
6
+ };
7
+ return {
8
+ hooks: {
9
+ Notification: [
10
+ {
11
+ matcher: "permission_prompt",
12
+ hooks: [notifyHook],
13
+ },
14
+ ],
15
+ Stop: [
16
+ {
17
+ matcher: "",
18
+ hooks: [{ ...notifyHook }],
19
+ },
20
+ ],
21
+ },
22
+ };
23
+ }
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import { initCommand } from "./commands/init.js";
4
+ import { notifyCommand } from "./commands/notify.js";
5
+ const program = new Command();
6
+ program
7
+ .name("cvox")
8
+ .description("Claude Voice Notifications")
9
+ .version("1.0.0");
10
+ program
11
+ .command("init")
12
+ .description("向 Claude Code settings 注入 hooks")
13
+ .option("--global", "写入全局 ~/.claude/settings.json")
14
+ .action(initCommand);
15
+ program
16
+ .command("notify")
17
+ .description("被 hooks 调用,读取 stdin 播放语音通知")
18
+ .action(notifyCommand);
19
+ program.parse();
@@ -0,0 +1,62 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ export const DEFAULT_CONFIG = {
5
+ project: "",
6
+ hooks: {
7
+ notification: {
8
+ enabled: true,
9
+ message: "{project} needs permission",
10
+ },
11
+ stop: {
12
+ enabled: true,
13
+ message: "{project} task completed",
14
+ },
15
+ },
16
+ tts: {
17
+ enabled: true,
18
+ },
19
+ desktop: {
20
+ enabled: false,
21
+ },
22
+ };
23
+ function deepMerge(target, source) {
24
+ const result = { ...target };
25
+ for (const key of Object.keys(source)) {
26
+ if (source[key] &&
27
+ typeof source[key] === "object" &&
28
+ !Array.isArray(source[key]) &&
29
+ target[key] &&
30
+ typeof target[key] === "object") {
31
+ result[key] = deepMerge(target[key], source[key]);
32
+ }
33
+ else {
34
+ result[key] = source[key];
35
+ }
36
+ }
37
+ return result;
38
+ }
39
+ function tryReadJson(filePath) {
40
+ try {
41
+ const content = fs.readFileSync(filePath, "utf-8");
42
+ return JSON.parse(content);
43
+ }
44
+ catch {
45
+ return null;
46
+ }
47
+ }
48
+ export function loadConfig(cwd) {
49
+ const projectConfig = tryReadJson(path.join(cwd, ".cvox.json"));
50
+ const globalConfig = tryReadJson(path.join(os.homedir(), ".cvox.json"));
51
+ let config = { ...DEFAULT_CONFIG };
52
+ if (globalConfig) {
53
+ config = deepMerge(config, globalConfig);
54
+ }
55
+ if (projectConfig) {
56
+ config = deepMerge(config, projectConfig);
57
+ }
58
+ if (!config.project) {
59
+ config.project = path.basename(cwd);
60
+ }
61
+ return config;
62
+ }
@@ -0,0 +1,46 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ const CVOX_MARKER = "cvox notify";
5
+ export function getSettingsPath(global, cwd) {
6
+ if (global) {
7
+ return path.join(os.homedir(), ".claude", "settings.json");
8
+ }
9
+ return path.join(cwd || process.cwd(), ".claude", "settings.local.json");
10
+ }
11
+ export function readSettings(filePath) {
12
+ try {
13
+ const content = fs.readFileSync(filePath, "utf-8");
14
+ return JSON.parse(content);
15
+ }
16
+ catch {
17
+ return {};
18
+ }
19
+ }
20
+ export function writeSettings(filePath, settings) {
21
+ const dir = path.dirname(filePath);
22
+ fs.mkdirSync(dir, { recursive: true });
23
+ fs.writeFileSync(filePath, JSON.stringify(settings, null, 2) + "\n");
24
+ }
25
+ function isCvoxHook(hook) {
26
+ return (hook &&
27
+ typeof hook.command === "string" &&
28
+ hook.command.includes(CVOX_MARKER));
29
+ }
30
+ function isCvoxMatcher(matcher) {
31
+ return (matcher &&
32
+ Array.isArray(matcher.hooks) &&
33
+ matcher.hooks.some(isCvoxHook));
34
+ }
35
+ export function mergeHooks(settings, cvoxHooks) {
36
+ const result = { ...settings };
37
+ const existingHooks = result.hooks || {};
38
+ const merged = { ...existingHooks };
39
+ for (const [eventName, cvoxMatchers] of Object.entries(cvoxHooks.hooks)) {
40
+ const existing = merged[eventName] || [];
41
+ const filtered = existing.filter((m) => !isCvoxMatcher(m));
42
+ merged[eventName] = [...filtered, ...cvoxMatchers];
43
+ }
44
+ result.hooks = merged;
45
+ return result;
46
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "cvox",
3
+ "version": "1.0.0",
4
+ "description": "Claude Voice Notifications - voice alerts for Claude Code hooks",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "bin": {
8
+ "cvox": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc"
15
+ },
16
+ "keywords": [
17
+ "claude",
18
+ "claude-code",
19
+ "hooks",
20
+ "tts",
21
+ "voice",
22
+ "notifications",
23
+ "cli"
24
+ ],
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "git+https://github.com/lmk123/cvox.git"
28
+ },
29
+ "homepage": "https://github.com/lmk123/cvox#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/lmk123/cvox/issues"
32
+ },
33
+ "engines": {
34
+ "node": ">=16"
35
+ },
36
+ "dependencies": {
37
+ "commander": "^12.1.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^20.14.0",
41
+ "typescript": "^5.5.3"
42
+ }
43
+ }