claude-hook-notify 1.0.0 → 1.2.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 +4 -4
- package/package.json +1 -1
- package/src/cli.js +2 -2
- package/src/notify.js +44 -15
- package/src/setup.js +71 -31
package/README.md
CHANGED
|
@@ -73,14 +73,13 @@ npx claude-hook-notify notify --title "构建完成" --message "所有测试通
|
|
|
73
73
|
| `Stop` | Claude Code 完成一次响应 | Glass |
|
|
74
74
|
| `TaskCompleted` | 子任务被标记为完成 | Hero |
|
|
75
75
|
| `Notification` | Claude Code 需要你注意(等输入) | Ping |
|
|
76
|
-
| `StopFailure` | API 错误导致中断(限流/认证/服务器错误等) | Sosumi |
|
|
77
76
|
|
|
78
77
|
> **注意**: `Stop` 事件在响应因 token 限制被截断时会显示特殊提示。
|
|
79
78
|
|
|
80
|
-
|
|
79
|
+
也可以添加额外事件(注意:`StopFailure` 不是 Claude Code 官方支持的 hook 事件,使用会导致配置报错):
|
|
81
80
|
|
|
82
81
|
```bash
|
|
83
|
-
npx claude-hook-notify setup --events Stop,TaskCompleted,Notification,
|
|
82
|
+
npx claude-hook-notify setup --events Stop,TaskCompleted,Notification,PostToolUseFailure,SubagentStop
|
|
84
83
|
```
|
|
85
84
|
|
|
86
85
|
## 平台支持
|
|
@@ -113,7 +112,8 @@ await sendNotification({
|
|
|
113
112
|
## 已知限制
|
|
114
113
|
|
|
115
114
|
- **Ctrl+C 用户中断**: 用户手动按 Ctrl+C 取消时不会触发任何 hook 事件,因此无法发送通知。
|
|
116
|
-
- **网络完全断开**: 如果网络完全断开导致 Claude Code 进程本身退出,hook 可能无法执行。
|
|
115
|
+
- **网络完全断开**: 如果网络完全断开导致 Claude Code 进程本身退出,hook 可能无法执行。
|
|
116
|
+
- **StopFailure 事件**: `StopFailure` 不是 Claude Code 官方支持的 hook 事件名,默认不再注册。如通过 `--events` 手动指定,会导致 settings.json 校验报错,整个配置文件被跳过。
|
|
117
117
|
|
|
118
118
|
## License
|
|
119
119
|
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -17,7 +17,7 @@ const HELP = `
|
|
|
17
17
|
--global 安装到全局配置 ~/.claude/settings.json(默认)
|
|
18
18
|
--local 安装到当前项目 .claude/settings.json
|
|
19
19
|
--events <事件列表> 要监听的事件,逗号分隔
|
|
20
|
-
默认: Stop,TaskCompleted,Notification
|
|
20
|
+
默认: Stop,TaskCompleted,Notification
|
|
21
21
|
|
|
22
22
|
notify 选项:
|
|
23
23
|
--event <事件名> 事件类型 (Stop/TaskCompleted/Notification/...)
|
|
@@ -69,7 +69,7 @@ async function main() {
|
|
|
69
69
|
const scope = args.local ? "local" : "global";
|
|
70
70
|
const events = args.events
|
|
71
71
|
? args.events.split(",").map((e) => e.trim())
|
|
72
|
-
: ["Stop", "TaskCompleted", "Notification"
|
|
72
|
+
: ["Stop", "TaskCompleted", "Notification"];
|
|
73
73
|
await setup({ scope, events });
|
|
74
74
|
return;
|
|
75
75
|
}
|
package/src/notify.js
CHANGED
|
@@ -234,20 +234,38 @@ async function sendNotification(options = {}) {
|
|
|
234
234
|
return result;
|
|
235
235
|
}
|
|
236
236
|
} else if (platform === "win32") {
|
|
237
|
-
method = "powershell";
|
|
237
|
+
method = "powershell-toast";
|
|
238
238
|
command = "powershell.exe";
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
$
|
|
245
|
-
$
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
239
|
+
const t = title.replace(/'/g, "''");
|
|
240
|
+
const m = message.replace(/'/g, "''");
|
|
241
|
+
const appId =
|
|
242
|
+
"{1AC14E77-02E7-4E5D-B744-2EB1AE5198B7}\\WindowsPowerShell\\v1.0\\powershell.exe";
|
|
243
|
+
const psScript = [
|
|
244
|
+
"$t='" + t + "'",
|
|
245
|
+
"$m='" + m + "'",
|
|
246
|
+
"try{",
|
|
247
|
+
" [void][Windows.UI.Notifications.ToastNotificationManager,Windows.UI.Notifications,ContentType=WindowsRuntime]",
|
|
248
|
+
" [void][Windows.Data.Xml.Dom.XmlDocument,Windows.Data.Xml.Dom,ContentType=WindowsRuntime]",
|
|
249
|
+
" $x=New-Object Windows.Data.Xml.Dom.XmlDocument",
|
|
250
|
+
" $te=[System.Security.SecurityElement]::Escape($t)",
|
|
251
|
+
" $me=[System.Security.SecurityElement]::Escape($m)",
|
|
252
|
+
' $x.LoadXml("<toast><visual><binding template=\'ToastGeneric\'><text>$te</text><text>$me</text></binding></visual></toast>")',
|
|
253
|
+
" $toast=[Windows.UI.Notifications.ToastNotification]::new($x)",
|
|
254
|
+
" [Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('" + appId + "').Show($toast)",
|
|
255
|
+
"}catch{",
|
|
256
|
+
" [void][System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms')",
|
|
257
|
+
" $n=New-Object System.Windows.Forms.NotifyIcon",
|
|
258
|
+
" $n.Icon=[System.Drawing.SystemIcons]::Information",
|
|
259
|
+
" $n.BalloonTipTitle=$t",
|
|
260
|
+
" $n.BalloonTipText=$m",
|
|
261
|
+
" $n.Visible=$true",
|
|
262
|
+
" $n.ShowBalloonTip(5000)",
|
|
263
|
+
" Start-Sleep -Seconds 6",
|
|
264
|
+
" $n.Dispose()",
|
|
265
|
+
"}",
|
|
266
|
+
].join("\n");
|
|
267
|
+
const encoded = Buffer.from(psScript, "utf16le").toString("base64");
|
|
268
|
+
args = ["-NoProfile", "-EncodedCommand", encoded];
|
|
251
269
|
}
|
|
252
270
|
|
|
253
271
|
const result = { sent: !dryRun, method, command, args };
|
|
@@ -260,8 +278,19 @@ async function sendNotification(options = {}) {
|
|
|
260
278
|
|
|
261
279
|
try {
|
|
262
280
|
if (command) {
|
|
263
|
-
|
|
264
|
-
|
|
281
|
+
if (platform === "win32") {
|
|
282
|
+
// Windows: spawn detached so hook exits immediately, notification lives independently
|
|
283
|
+
const { spawn } = require("child_process");
|
|
284
|
+
const child = spawn(command, args, {
|
|
285
|
+
detached: true,
|
|
286
|
+
stdio: "ignore",
|
|
287
|
+
windowsHide: true,
|
|
288
|
+
});
|
|
289
|
+
child.unref();
|
|
290
|
+
} else {
|
|
291
|
+
const { execFileSync } = require("child_process");
|
|
292
|
+
execFileSync(command, args, { stdio: "ignore", timeout: 5000 });
|
|
293
|
+
}
|
|
265
294
|
}
|
|
266
295
|
} catch (err) {
|
|
267
296
|
result.sent = false;
|
package/src/setup.js
CHANGED
|
@@ -111,7 +111,7 @@ function checkDependencies() {
|
|
|
111
111
|
/**
|
|
112
112
|
* 安装 hooks 配置
|
|
113
113
|
*/
|
|
114
|
-
async function setup({ scope = "global", events = ["Stop", "TaskCompleted", "Notification"
|
|
114
|
+
async function setup({ scope = "global", events = ["Stop", "TaskCompleted", "Notification"] }) {
|
|
115
115
|
const settingsPath = getSettingsPath(scope);
|
|
116
116
|
const scopeLabel = scope === "global" ? "全局" : "项目";
|
|
117
117
|
|
|
@@ -196,6 +196,44 @@ async function setup({ scope = "global", events = ["Stop", "TaskCompleted", "Not
|
|
|
196
196
|
console.log();
|
|
197
197
|
}
|
|
198
198
|
|
|
199
|
+
/**
|
|
200
|
+
* 清理 npx 缓存中的 claude-hook-notify
|
|
201
|
+
*/
|
|
202
|
+
function cleanNpxCache() {
|
|
203
|
+
let cacheDir;
|
|
204
|
+
try {
|
|
205
|
+
cacheDir = require("child_process")
|
|
206
|
+
.execSync("npm config get cache", { encoding: "utf-8" })
|
|
207
|
+
.trim();
|
|
208
|
+
} catch {
|
|
209
|
+
return 0;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const npxDir = path.join(cacheDir, "_npx");
|
|
213
|
+
if (!fs.existsSync(npxDir)) return 0;
|
|
214
|
+
|
|
215
|
+
let cleaned = 0;
|
|
216
|
+
try {
|
|
217
|
+
const entries = fs.readdirSync(npxDir);
|
|
218
|
+
for (const entry of entries) {
|
|
219
|
+
const pkgPath = path.join(npxDir, entry, "package.json");
|
|
220
|
+
try {
|
|
221
|
+
if (!fs.existsSync(pkgPath)) continue;
|
|
222
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
223
|
+
if (pkg.dependencies && pkg.dependencies[PKG_NAME]) {
|
|
224
|
+
fs.rmSync(path.join(npxDir, entry), { recursive: true, force: true });
|
|
225
|
+
cleaned++;
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
// 跳过无法读取的目录
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
} catch {
|
|
232
|
+
// 无法读取 npx 缓存目录
|
|
233
|
+
}
|
|
234
|
+
return cleaned;
|
|
235
|
+
}
|
|
236
|
+
|
|
199
237
|
/**
|
|
200
238
|
* 卸载 hooks 配置
|
|
201
239
|
*/
|
|
@@ -207,41 +245,34 @@ async function uninstall({ scope = "global" }) {
|
|
|
207
245
|
console.log(` ${c.bold("🔔 Claude Code Notify — 卸载")}`);
|
|
208
246
|
console.log();
|
|
209
247
|
|
|
210
|
-
|
|
211
|
-
console.log(` ${c.yellow("⚠")} 未找到配置文件: ${settingsPath}`);
|
|
212
|
-
console.log();
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const settings = readJSON(settingsPath);
|
|
217
|
-
if (!settings.hooks) {
|
|
218
|
-
console.log(` ${c.yellow("⚠")} 配置中没有 hooks`);
|
|
219
|
-
console.log();
|
|
220
|
-
return;
|
|
221
|
-
}
|
|
222
|
-
|
|
248
|
+
// 清理 hooks 配置
|
|
223
249
|
let removed = 0;
|
|
224
250
|
|
|
225
|
-
|
|
226
|
-
const
|
|
227
|
-
settings.hooks
|
|
228
|
-
(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
251
|
+
if (fs.existsSync(settingsPath)) {
|
|
252
|
+
const settings = readJSON(settingsPath);
|
|
253
|
+
if (settings.hooks) {
|
|
254
|
+
for (const [event, hookConfigs] of Object.entries(settings.hooks)) {
|
|
255
|
+
const before = hookConfigs.length;
|
|
256
|
+
settings.hooks[event] = hookConfigs.filter(
|
|
257
|
+
(h) => !h.hooks?.some((hh) => hh.command?.includes(PKG_NAME))
|
|
258
|
+
);
|
|
259
|
+
removed += before - settings.hooks[event].length;
|
|
260
|
+
|
|
261
|
+
// 清理空数组
|
|
262
|
+
if (settings.hooks[event].length === 0) {
|
|
263
|
+
delete settings.hooks[event];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 清理空 hooks 对象
|
|
268
|
+
if (Object.keys(settings.hooks).length === 0) {
|
|
269
|
+
delete settings.hooks;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
writeJSON(settingsPath, settings);
|
|
235
273
|
}
|
|
236
274
|
}
|
|
237
275
|
|
|
238
|
-
// 清理空 hooks 对象
|
|
239
|
-
if (Object.keys(settings.hooks).length === 0) {
|
|
240
|
-
delete settings.hooks;
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
writeJSON(settingsPath, settings);
|
|
244
|
-
|
|
245
276
|
if (removed > 0) {
|
|
246
277
|
console.log(
|
|
247
278
|
` ${c.green("✓")} 已从${scopeLabel}配置中移除 ${removed} 个通知 hook`
|
|
@@ -254,6 +285,15 @@ async function uninstall({ scope = "global" }) {
|
|
|
254
285
|
console.log(
|
|
255
286
|
` ${c.dim("配置文件: " + settingsPath)}`
|
|
256
287
|
);
|
|
288
|
+
|
|
289
|
+
// 清理 npx 缓存
|
|
290
|
+
const cleaned = cleanNpxCache();
|
|
291
|
+
if (cleaned > 0) {
|
|
292
|
+
console.log(
|
|
293
|
+
` ${c.green("✓")} 已清理 ${cleaned} 个 npx 缓存目录`
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
257
297
|
console.log();
|
|
258
298
|
}
|
|
259
299
|
|