claude-notify-ding 0.0.1

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,84 @@
1
+ # claude-notify-ding
2
+
3
+ Claude Code 触发 hook 时给钉钉推送一条**文本**提醒——只做提示,不做注入。
4
+
5
+ ## 它能做什么
6
+
7
+ - Claude 一轮回答结束 → 钉钉收到一条带项目名 + 回复预览的提醒
8
+ - Claude 请求工具授权 → 钉钉收到一条「🔐 工具授权请求」提醒(你自己走到终端按 1 / 2 / Esc)
9
+ - Claude 长时间等输入 → 可选,默认关
10
+
11
+ 消息格式是钉钉群机器人原生的 markdown 消息(关键词同时放在 `title` 和正文里以过安全校验),不带按钮、不开端口、不连外网隧道。
12
+
13
+ ## 安装
14
+
15
+ 需要 Node ≥ 18 和一个开了「自定义关键词」的钉钉群机器人。
16
+
17
+ ```bash
18
+ npm install -g claude-notify-ding
19
+ claude-notify init
20
+ ```
21
+
22
+ `init` 会问两件事:
23
+
24
+ - 钉钉机器人 webhook URL
25
+ - 关键词(须与机器人安全设置里的关键词完全一致)
26
+
27
+ 然后向你的钉钉发一条「配置成功 ✅」测试消息;测试通过后才会写配置和注册 hook。webhook 不通就拒绝写入,避免留一份永远发不出的配置。
28
+
29
+ ## 命令
30
+
31
+ ```
32
+ claude-notify init 交互式配置 + 注册 hook
33
+ claude-notify status 查看配置与 hook 注册状态
34
+ claude-notify uninstall 撤销 hook 注册 + 删除配置(日志保留)
35
+ ```
36
+
37
+ `hook on-stop` 和 `hook on-notification` 是 Claude Code 内部调用的子命令,不需要手动跑。
38
+
39
+ ## 推送时机
40
+
41
+ 所有推送都是 markdown,标题统一是 `【关键词】<项目名> 提醒`,正文是用 `>` 引起来的预览块:
42
+
43
+ | 触发 | 预览正文 |
44
+ | ---------------------------------------- | ---------------------------------------------- |
45
+ | Claude 一轮回复结束(Stop hook) | 最后一条回复的预览 |
46
+ | Claude 请求工具授权(Notification hook) | `🔐 工具授权请求` + 原始 hook 消息(含工具名) |
47
+ | Claude 长时间等输入(默认关闭) | `⏳ ` + 原始 hook 消息 |
48
+ | 其他未知 Notification | 原文透传 |
49
+
50
+ ## 配置文件
51
+
52
+ 位于 `~/.claude-notify/config.json`:
53
+
54
+ ```json
55
+ {
56
+ "schemaVersion": 3,
57
+ "webhook": "https://oapi.dingtalk.com/robot/send?access_token=...",
58
+ "keyword": "Claude",
59
+ "preview": { "maxLength": 100, "includeCwd": true },
60
+ "notifications": { "idleReminder": false },
61
+ "logs": { "retentionDays": 7 }
62
+ }
63
+ ```
64
+
65
+ - `preview.maxLength`:Stop 推送中回复预览的字符上限,超出截断加 `…`
66
+ - `notifications.idleReminder`:是否在 Claude 长闲置时也推送(默认关;Stop 已经为同一轮发过一次提醒)
67
+ - `logs.retentionDays`:事件日志保留天数,超过这个天数的 `YYYY-MM-DD.log` 自动删除;设为 `0` 或负数则永久保留
68
+
69
+ 直接编辑这个文件即可生效,无需重启任何东西。
70
+
71
+ ## 它会改你机器上的什么
72
+
73
+ - `~/.claude-notify/config.json`:配置本体
74
+ - `~/.claude-notify/logs/<YYYY-MM-DD>.log`:每天一份的事件日志(hook 触发、推送结果、失败原因);默认保留 7 天,到期文件在下次 hook 触发时自动清理(每 24h 至多扫一次,见 `logs.retentionDays`)
75
+ - `~/.claude/settings.json`:注册两条 hook 命令到 `hooks.Stop` 和 `hooks.Notification`,已有的其他工具和顶层字段不动
76
+
77
+ `uninstall` 会撤销 hook 注册并删 config,**保留日志**。
78
+
79
+ ## 卸载
80
+
81
+ ```bash
82
+ claude-notify uninstall
83
+ npm uninstall -g claude-notify-ding
84
+ ```
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env node
2
+ import { main } from '../dist/cli.js';
3
+ main(process.argv).catch((err) => {
4
+ console.error(err);
5
+ process.exit(1);
6
+ });
package/dist/cli.js ADDED
@@ -0,0 +1,67 @@
1
+ import { Command } from 'commander';
2
+ import { promptInit, runInit } from './commands/init.js';
3
+ import { collectStatus, renderStatus } from './commands/status.js';
4
+ import { runUninstall } from './commands/uninstall.js';
5
+ import { runOnStop, readStdinPayload } from './commands/hook/on-stop.js';
6
+ import { runOnNotification, readStdinPayload as readNotificationPayload, } from './commands/hook/on-notification.js';
7
+ export async function main(argv) {
8
+ const program = new Command();
9
+ program
10
+ .name('claude-notify')
11
+ .description('Claude Code 触发 hook 时推送钉钉文本提醒')
12
+ .version('0.0.1');
13
+ program
14
+ .command('init')
15
+ .description('交互式配置 webhook 和关键词')
16
+ .action(async () => {
17
+ try {
18
+ const answers = await promptInit();
19
+ const result = await runInit(answers);
20
+ if (!result.ok) {
21
+ console.error(`✗ 测试消息发送失败:${result.error}`);
22
+ console.error(' 请检查 webhook URL 和关键词是否正确,未写入配置。');
23
+ process.exit(1);
24
+ }
25
+ console.log('✅ 配置成功,已注册 Stop / Notification hook。');
26
+ console.log(' 下次 Claude Code 停下或请求工具授权时,你的钉钉会收到提醒。');
27
+ }
28
+ catch (err) {
29
+ console.error(`init 失败:${err instanceof Error ? err.message : String(err)}`);
30
+ process.exit(1);
31
+ }
32
+ });
33
+ program
34
+ .command('status')
35
+ .description('显示当前配置与 hook 注册情况')
36
+ .action(async () => {
37
+ const s = await collectStatus();
38
+ console.log(renderStatus(s));
39
+ });
40
+ program
41
+ .command('uninstall')
42
+ .description('撤销 hook 注册并删除配置(保留日志)')
43
+ .action(async () => {
44
+ await runUninstall();
45
+ console.log('✅ 已卸载。日志保留在 ~/.claude-notify/logs/');
46
+ });
47
+ const hook = program.command('hook').description('内部命令,被 Claude Code hook 调用');
48
+ hook
49
+ .command('on-stop')
50
+ .description('Stop hook 入口(从 stdin 读 payload)')
51
+ .action(async () => {
52
+ const payload = await readStdinPayload();
53
+ const code = await runOnStop(payload);
54
+ process.exit(code);
55
+ });
56
+ hook
57
+ .command('on-notification')
58
+ .description('Notification hook 入口(工具授权 / 长闲置提醒)')
59
+ .action(async () => {
60
+ const payload = await readNotificationPayload();
61
+ const code = await runOnNotification(payload);
62
+ process.exit(code);
63
+ });
64
+ await program.parseAsync(argv);
65
+ return 0;
66
+ }
67
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,128 @@
1
+ import { loadConfig } from '../../config/config-store.js';
2
+ import { buildMarkdownMessage } from '../../dingtalk/message-builder.js';
3
+ import { sendToWebhook } from '../../dingtalk/client.js';
4
+ import { logEvent, maybeCleanupOldLogs } from '../../util/logger.js';
5
+ export async function runOnNotification(payload) {
6
+ let retentionDays = 0;
7
+ try {
8
+ const config = await loadConfig();
9
+ if (!config)
10
+ return 0;
11
+ retentionDays = config.logs.retentionDays;
12
+ const kind = classifyNotification(payload.message);
13
+ await logEvent('info', 'notification_received', {
14
+ session_id: payload.session_id,
15
+ message: payload.message,
16
+ kind,
17
+ }).catch(() => { });
18
+ switch (kind) {
19
+ case 'permission':
20
+ await sendPermissionText(payload, config);
21
+ break;
22
+ case 'idle':
23
+ if (config.notifications.idleReminder) {
24
+ await sendIdleReminder(payload, config);
25
+ }
26
+ else {
27
+ await logEvent('info', 'notification_idle_suppressed', {
28
+ session_id: payload.session_id,
29
+ }).catch(() => { });
30
+ }
31
+ break;
32
+ case 'unknown':
33
+ // Don't drop on the floor — log and fall through to a plain text
34
+ // message so future Claude Code message wordings still notify.
35
+ await sendUnknownNotification(payload, config);
36
+ break;
37
+ }
38
+ }
39
+ catch (err) {
40
+ await logEvent('error', 'notification_hook_failed', {
41
+ session_id: payload.session_id,
42
+ message: err instanceof Error ? err.message : String(err),
43
+ }).catch(() => { });
44
+ }
45
+ // Throttled cleanup — see on-stop.ts for the same call. Must never break
46
+ // the hook contract, so errors are swallowed.
47
+ await maybeCleanupOldLogs(retentionDays).catch(() => { });
48
+ return 0;
49
+ }
50
+ export function classifyNotification(message) {
51
+ const lower = message.toLowerCase();
52
+ // Match permission asks across Claude Code wording variants:
53
+ // "Claude needs your permission" (current Claude Code)
54
+ // "Claude needs your permission to use Bash" (older / longer form)
55
+ // "Claude is requesting permission to use Edit" (some builds)
56
+ if (lower.includes('needs your permission') ||
57
+ lower.includes('requesting permission') ||
58
+ lower.includes('permission to use')) {
59
+ return 'permission';
60
+ }
61
+ if (lower.includes('waiting for your input') || lower.includes('waiting for input')) {
62
+ return 'idle';
63
+ }
64
+ return 'unknown';
65
+ }
66
+ async function sendPermissionText(payload, config) {
67
+ // Pass the hook's own message through — Claude Code already includes the
68
+ // tool name in current wordings ("needs your permission to use Bash"),
69
+ // and we no longer parse the transcript here.
70
+ await sendToWebhook(config.webhook, buildMarkdownMessage({
71
+ keyword: config.keyword,
72
+ cwd: payload.cwd,
73
+ preview: `🔐 工具授权请求\n${payload.message}`,
74
+ }));
75
+ await logEvent('info', 'notification_permission_pushed', {
76
+ session_id: payload.session_id,
77
+ }).catch(() => { });
78
+ }
79
+ async function sendIdleReminder(payload, config) {
80
+ await sendToWebhook(config.webhook, buildMarkdownMessage({
81
+ keyword: config.keyword,
82
+ cwd: payload.cwd,
83
+ preview: `⏳ ${payload.message}`,
84
+ }));
85
+ await logEvent('info', 'notification_idle_pushed', {
86
+ session_id: payload.session_id,
87
+ }).catch(() => { });
88
+ }
89
+ async function sendUnknownNotification(payload, config) {
90
+ await sendToWebhook(config.webhook, buildMarkdownMessage({
91
+ keyword: config.keyword,
92
+ cwd: payload.cwd,
93
+ preview: payload.message,
94
+ }));
95
+ await logEvent('info', 'notification_unknown_pushed', {
96
+ session_id: payload.session_id,
97
+ message: payload.message,
98
+ }).catch(() => { });
99
+ }
100
+ export async function readStdinPayload() {
101
+ const chunks = [];
102
+ for await (const chunk of process.stdin) {
103
+ chunks.push(Buffer.from(chunk));
104
+ }
105
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
106
+ const empty = {
107
+ session_id: '',
108
+ transcript_path: '',
109
+ cwd: process.cwd(),
110
+ message: '',
111
+ };
112
+ if (!raw)
113
+ return empty;
114
+ let parsed;
115
+ try {
116
+ parsed = JSON.parse(raw);
117
+ }
118
+ catch {
119
+ return empty;
120
+ }
121
+ return {
122
+ session_id: parsed.session_id ?? '',
123
+ transcript_path: parsed.transcript_path ?? '',
124
+ cwd: parsed.cwd ?? process.cwd(),
125
+ message: typeof parsed.message === 'string' ? parsed.message : '',
126
+ };
127
+ }
128
+ //# sourceMappingURL=on-notification.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"on-notification.js","sourceRoot":"","sources":["../../../src/commands/hook/on-notification.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,81 @@
1
+ import { loadConfig } from '../../config/config-store.js';
2
+ import { buildMarkdownMessage } from '../../dingtalk/message-builder.js';
3
+ import { sendToWebhook } from '../../dingtalk/client.js';
4
+ import { detectStopState } from '../../transcript/state-detector.js';
5
+ import { logEvent, maybeCleanupOldLogs } from '../../util/logger.js';
6
+ const DEFAULT_PREVIEW_MAX = 100;
7
+ /**
8
+ * Stop hook entry.
9
+ *
10
+ * Stop fires only when the assistant turn is fully complete — by then every
11
+ * tool_use has its matching tool_result. We push a plain text reminder with
12
+ * the tail of Claude's last reply so the user knows the turn finished.
13
+ *
14
+ * Tool-approval and idle-waiting notifications are owned by the Notification
15
+ * hook (`on-notification.ts`) instead.
16
+ */
17
+ export async function runOnStop(payload) {
18
+ let retentionDays = 0;
19
+ try {
20
+ const config = await loadConfig();
21
+ if (!config)
22
+ return 0;
23
+ retentionDays = config.logs.retentionDays;
24
+ const maxLen = config.preview?.maxLength ?? DEFAULT_PREVIEW_MAX;
25
+ const state = await detectStopState(payload.transcript_path, {
26
+ maxPreviewLength: maxLen,
27
+ });
28
+ await sendText(payload, state.preview, config);
29
+ }
30
+ catch (err) {
31
+ await logEvent('error', 'stop_hook_failed', {
32
+ session_id: payload.session_id,
33
+ message: err instanceof Error ? err.message : String(err),
34
+ }).catch(() => { });
35
+ }
36
+ // Throttled — at most once per 24h per install. Awaited so failures
37
+ // surface in the same log, but errors are swallowed: cleanup must never
38
+ // break the hook contract.
39
+ await maybeCleanupOldLogs(retentionDays).catch(() => { });
40
+ return 0;
41
+ }
42
+ async function sendText(payload, preview, config) {
43
+ const msg = buildMarkdownMessage({
44
+ keyword: config.keyword,
45
+ cwd: payload.cwd,
46
+ preview,
47
+ });
48
+ await sendToWebhook(config.webhook, msg);
49
+ await logEvent('info', 'stop_notified', {
50
+ session_id: payload.session_id,
51
+ cwd: payload.cwd,
52
+ mode: 'markdown',
53
+ }).catch(() => { });
54
+ }
55
+ export async function readStdinPayload() {
56
+ const chunks = [];
57
+ for await (const chunk of process.stdin) {
58
+ chunks.push(Buffer.from(chunk));
59
+ }
60
+ const raw = Buffer.concat(chunks).toString('utf8').trim();
61
+ const empty = {
62
+ session_id: '',
63
+ transcript_path: '',
64
+ cwd: process.cwd(),
65
+ };
66
+ if (!raw)
67
+ return empty;
68
+ let parsed;
69
+ try {
70
+ parsed = JSON.parse(raw);
71
+ }
72
+ catch {
73
+ return empty;
74
+ }
75
+ return {
76
+ session_id: parsed.session_id ?? '',
77
+ transcript_path: parsed.transcript_path ?? '',
78
+ cwd: parsed.cwd ?? process.cwd(),
79
+ };
80
+ }
81
+ //# sourceMappingURL=on-stop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"on-stop.js","sourceRoot":"","sources":["../../../src/commands/hook/on-stop.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,54 @@
1
+ import prompts from 'prompts';
2
+ import { saveConfig } from '../config/config-store.js';
3
+ import { registerAllHooks } from '../config/hooks.js';
4
+ import { sendToWebhook } from '../dingtalk/client.js';
5
+ import { buildMarkdownMessage } from '../dingtalk/message-builder.js';
6
+ export async function runInit(answers) {
7
+ // 1. Send a test message FIRST so we don't write config for a broken webhook
8
+ try {
9
+ await sendToWebhook(answers.webhook, buildMarkdownMessage({
10
+ keyword: answers.keyword,
11
+ cwd: 'claude-notify',
12
+ preview: 'claude-notify 配置成功 ✅',
13
+ }));
14
+ }
15
+ catch (err) {
16
+ return {
17
+ ok: false,
18
+ error: err instanceof Error ? err.message : String(err),
19
+ };
20
+ }
21
+ // 2. Persist config (v3 — text-only schema, no bridge knobs)
22
+ await saveConfig({
23
+ webhook: answers.webhook,
24
+ keyword: answers.keyword,
25
+ });
26
+ // 3. Register hooks — always Stop + Notification, and wipe any legacy
27
+ // bridge-era hooks that might still be in settings.json from a pre-0.2
28
+ // install. See config/hooks.ts.
29
+ await registerAllHooks();
30
+ return { ok: true };
31
+ }
32
+ export async function promptInit() {
33
+ const answers = await prompts([
34
+ {
35
+ type: 'text',
36
+ name: 'webhook',
37
+ message: '钉钉机器人 webhook URL:',
38
+ validate: (v) => v.startsWith('https://oapi.dingtalk.com/robot/send') ||
39
+ '需以 https://oapi.dingtalk.com/robot/send 开头',
40
+ },
41
+ {
42
+ type: 'text',
43
+ name: 'keyword',
44
+ message: '关键词 (须与机器人安全设置一致):',
45
+ initial: 'Claude',
46
+ },
47
+ ], {
48
+ onCancel: () => {
49
+ throw new Error('init cancelled by user');
50
+ },
51
+ });
52
+ return answers;
53
+ }
54
+ //# sourceMappingURL=init.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAAA,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AACtD,OAAO,EAAE,oBAAoB,EAAE,MAAM,gCAAgC,CAAC;AAYtE,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,OAAoB;IAChD,6EAA6E;IAC7E,IAAI,CAAC;QACH,MAAM,aAAa,CACjB,OAAO,CAAC,OAAO,EACf,oBAAoB,CAAC;YACnB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,GAAG,EAAE,eAAe;YACpB,OAAO,EAAE,sBAAsB;SAChC,CAAC,CACH,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO;YACL,EAAE,EAAE,KAAK;YACT,KAAK,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC;SACxD,CAAC;IACJ,CAAC;IAED,6DAA6D;IAC7D,MAAM,UAAU,CAAC;QACf,OAAO,EAAE,OAAO,CAAC,OAAO;QACxB,OAAO,EAAE,OAAO,CAAC,OAAO;KACzB,CAAC,CAAC;IAEH,sEAAsE;IACtE,uEAAuE;IACvE,gCAAgC;IAChC,MAAM,gBAAgB,EAAE,CAAC;IAEzB,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC;AACtB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU;IAC9B,MAAM,OAAO,GAAG,MAAM,OAAO,CAC3B;QACE;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,oBAAoB;YAC7B,QAAQ,EAAE,CAAC,CAAS,EAAE,EAAE,CACtB,CAAC,CAAC,UAAU,CAAC,sCAAsC,CAAC;gBACpD,4CAA4C;SAC/C;QACD;YACE,IAAI,EAAE,MAAM;YACZ,IAAI,EAAE,SAAS;YACf,OAAO,EAAE,oBAAoB;YAC7B,OAAO,EAAE,QAAQ;SAClB;KACF,EACD;QACE,QAAQ,EAAE,GAAG,EAAE;YACb,MAAM,IAAI,KAAK,CAAC,wBAAwB,CAAC,CAAC;QAC5C,CAAC;KACF,CACF,CAAC;IACF,OAAO,OAAsB,CAAC;AAChC,CAAC"}
@@ -0,0 +1,37 @@
1
+ import { loadConfig } from '../config/config-store.js';
2
+ import { getRegisteredHookStatus, } from '../config/hooks.js';
3
+ function maskWebhook(url) {
4
+ return url.replace(/access_token=([^&]+)/, (_m, t) => {
5
+ const last = t.slice(-1);
6
+ return `access_token=${'*'.repeat(Math.max(t.length - 1, 1))}${last}`;
7
+ });
8
+ }
9
+ export async function collectStatus() {
10
+ const config = await loadConfig();
11
+ const hooks = await getRegisteredHookStatus();
12
+ if (!config) {
13
+ return { configured: false, hooks };
14
+ }
15
+ return {
16
+ configured: true,
17
+ keyword: config.keyword,
18
+ webhookMasked: maskWebhook(config.webhook),
19
+ hooks,
20
+ };
21
+ }
22
+ export function renderStatus(s) {
23
+ const lines = [];
24
+ lines.push(`配置文件: ${s.configured ? '✓ 已存在' : '✗ 未配置 (运行 claude-notify init)'}`);
25
+ if (s.configured) {
26
+ lines.push(`关键词: ${s.keyword}`);
27
+ lines.push(`Webhook: ${s.webhookMasked}`);
28
+ }
29
+ lines.push('');
30
+ lines.push('Hooks:');
31
+ for (const h of s.hooks) {
32
+ const mark = h.registered ? '✓' : '✗';
33
+ lines.push(` ${mark} ${h.event.padEnd(18)} ${h.command}`);
34
+ }
35
+ return lines.join('\n');
36
+ }
37
+ //# sourceMappingURL=status.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AACvD,OAAO,EACL,uBAAuB,GAExB,MAAM,oBAAoB,CAAC;AAS5B,SAAS,WAAW,CAAC,GAAW;IAC9B,OAAO,GAAG,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE;QACnD,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACzB,OAAO,gBAAgB,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC;IACxE,CAAC,CAAC,CAAC;AACL,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa;IACjC,MAAM,MAAM,GAAG,MAAM,UAAU,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,MAAM,uBAAuB,EAAE,CAAC;IAE9C,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;IACtC,CAAC;IAED,OAAO;QACL,UAAU,EAAE,IAAI;QAChB,OAAO,EAAE,MAAM,CAAC,OAAO;QACvB,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,OAAO,CAAC;QAC1C,KAAK;KACN,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,CAAa;IACxC,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,+BAA+B,EAAE,CAAC,CAAC;IAChF,IAAI,CAAC,CAAC,UAAU,EAAE,CAAC;QACjB,KAAK,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;QAClC,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,aAAa,EAAE,CAAC,CAAC;IAC7C,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACf,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IACrB,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,CAAC;QACxB,MAAM,IAAI,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC;QACtC,KAAK,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAC1B,CAAC"}
@@ -0,0 +1,20 @@
1
+ import fs from 'fs-extra';
2
+ import { paths } from '../config/paths.js';
3
+ import { unregisterAllHooks } from '../config/hooks.js';
4
+ /**
5
+ * Tear down the claude-notify installation:
6
+ * 1. Unregister every hook claude-notify could have written to
7
+ * ~/.claude/settings.json (current + legacy bridge-era ones)
8
+ * 2. Delete config.json
9
+ * 3. Keep logs/ for forensics
10
+ */
11
+ export async function runUninstall() {
12
+ // 1. Unregister every hook we could have written.
13
+ await unregisterAllHooks();
14
+ // 2. Remove config.
15
+ if (await fs.pathExists(paths.configFile())) {
16
+ await fs.remove(paths.configFile());
17
+ }
18
+ // logs/ deliberately preserved.
19
+ }
20
+ //# sourceMappingURL=uninstall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uninstall.js","sourceRoot":"","sources":["../../src/commands/uninstall.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,UAAU,CAAC;AAC1B,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC3C,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAExD;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,kDAAkD;IAClD,MAAM,kBAAkB,EAAE,CAAC;IAE3B,oBAAoB;IACpB,IAAI,MAAM,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,EAAE,CAAC;QAC5C,MAAM,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;IACtC,CAAC;IACD,gCAAgC;AAClC,CAAC"}
@@ -0,0 +1,86 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { paths } from './paths.js';
4
+ async function readSettings() {
5
+ const file = paths.claudeSettingsFile();
6
+ if (!(await fs.pathExists(file)))
7
+ return {};
8
+ return (await fs.readJson(file));
9
+ }
10
+ async function writeSettings(s) {
11
+ const file = paths.claudeSettingsFile();
12
+ await fs.ensureDir(path.dirname(file));
13
+ await fs.writeJson(file, s, { spaces: 2 });
14
+ }
15
+ /**
16
+ * Returns true if `entry` is a properly-shaped matcher group whose inner hooks
17
+ * include the given command. Tolerates the legacy buggy shape `{ command }`.
18
+ */
19
+ function entryHasCommand(entry, command) {
20
+ // Legacy buggy shape: { command: '...' } at the top level.
21
+ if (entry.command === command && !entry.hooks)
22
+ return true;
23
+ // Current correct shape: { matcher?, hooks: [{ type, command }] }
24
+ if (Array.isArray(entry.hooks)) {
25
+ return entry.hooks.some((h) => h && h.type === 'command' && h.command === command);
26
+ }
27
+ return false;
28
+ }
29
+ export async function registerHook(event, command) {
30
+ const settings = await readSettings();
31
+ settings.hooks ??= {};
32
+ settings.hooks[event] ??= [];
33
+ const arr = settings.hooks[event];
34
+ // Idempotent: skip if any existing entry already contains this command (in either shape).
35
+ if (arr.some((e) => entryHasCommand(e, command))) {
36
+ await writeSettings(settings);
37
+ return;
38
+ }
39
+ const newGroup = {
40
+ matcher: '',
41
+ hooks: [{ type: 'command', command }],
42
+ };
43
+ arr.push(newGroup);
44
+ await writeSettings(settings);
45
+ }
46
+ export async function unregisterHook(event, command) {
47
+ const settings = await readSettings();
48
+ const evtArr = settings.hooks?.[event];
49
+ if (!evtArr)
50
+ return;
51
+ // Strip our command from each entry, accommodating both legacy and current shapes.
52
+ const remaining = [];
53
+ for (const entry of evtArr) {
54
+ // Legacy shape: drop the whole entry if it matches.
55
+ if (entry.command === command && !entry.hooks)
56
+ continue;
57
+ // Current shape: filter inner hooks.
58
+ if (Array.isArray(entry.hooks)) {
59
+ const filteredInner = entry.hooks.filter((h) => !(h && h.type === 'command' && h.command === command));
60
+ if (filteredInner.length === 0)
61
+ continue; // drop emptied matcher group
62
+ remaining.push({ ...entry, hooks: filteredInner });
63
+ continue;
64
+ }
65
+ // Unknown shape: leave it alone.
66
+ remaining.push(entry);
67
+ }
68
+ if (remaining.length === 0) {
69
+ delete settings.hooks[event];
70
+ }
71
+ else {
72
+ settings.hooks[event] = remaining;
73
+ }
74
+ if (settings.hooks && Object.keys(settings.hooks).length === 0) {
75
+ delete settings.hooks;
76
+ }
77
+ await writeSettings(settings);
78
+ }
79
+ export async function isHookRegistered(event, command) {
80
+ const settings = await readSettings();
81
+ const evtArr = settings.hooks?.[event];
82
+ if (!evtArr)
83
+ return false;
84
+ return evtArr.some((e) => entryHasCommand(e, command));
85
+ }
86
+ //# sourceMappingURL=claude-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-settings.js","sourceRoot":"","sources":["../../src/config/claude-settings.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ import fs from 'fs-extra';
2
+ import { paths } from './paths.js';
3
+ /**
4
+ * v3 = text-only schema. v1/v2 had `bridge`, `delay`, `actions` fields for
5
+ * the now-removed remote-injection path; we silently drop them on read.
6
+ */
7
+ export const CONFIG_SCHEMA_VERSION = 3;
8
+ export function defaultNotificationsConfig() {
9
+ return { idleReminder: false };
10
+ }
11
+ export function defaultPreviewConfig() {
12
+ return { maxLength: 100, includeCwd: true };
13
+ }
14
+ export function defaultLogsConfig() {
15
+ return { retentionDays: 7 };
16
+ }
17
+ function isPlainObject(v) {
18
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
19
+ }
20
+ function mergePreview(input) {
21
+ const def = defaultPreviewConfig();
22
+ if (!isPlainObject(input))
23
+ return def;
24
+ return {
25
+ maxLength: typeof input.maxLength === 'number' ? input.maxLength : def.maxLength,
26
+ includeCwd: typeof input.includeCwd === 'boolean' ? input.includeCwd : def.includeCwd,
27
+ };
28
+ }
29
+ function mergeNotifications(input) {
30
+ const def = defaultNotificationsConfig();
31
+ if (!isPlainObject(input))
32
+ return def;
33
+ return {
34
+ idleReminder: typeof input.idleReminder === 'boolean' ? input.idleReminder : def.idleReminder,
35
+ };
36
+ }
37
+ function mergeLogs(input) {
38
+ const def = defaultLogsConfig();
39
+ if (!isPlainObject(input))
40
+ return def;
41
+ return {
42
+ retentionDays: typeof input.retentionDays === 'number' && Number.isFinite(input.retentionDays)
43
+ ? input.retentionDays
44
+ : def.retentionDays,
45
+ };
46
+ }
47
+ /**
48
+ * Normalise a parsed JSON blob (or a partial input from a caller) into a fully
49
+ * populated v3 config object. Legacy `bridge` / `delay` / `actions` fields from
50
+ * v1/v2 configs are silently dropped — we no longer support remote injection.
51
+ */
52
+ function normalize(raw) {
53
+ const obj = isPlainObject(raw) ? raw : {};
54
+ return {
55
+ schemaVersion: CONFIG_SCHEMA_VERSION,
56
+ webhook: typeof obj.webhook === 'string' ? obj.webhook : '',
57
+ keyword: typeof obj.keyword === 'string' ? obj.keyword : 'Claude',
58
+ preview: mergePreview(obj.preview),
59
+ notifications: mergeNotifications(obj.notifications),
60
+ logs: mergeLogs(obj.logs),
61
+ };
62
+ }
63
+ export async function loadConfig() {
64
+ const file = paths.configFile();
65
+ if (!(await fs.pathExists(file)))
66
+ return null;
67
+ const raw = (await fs.readJson(file));
68
+ // In-memory migration only — reads must not produce writes.
69
+ return normalize(raw);
70
+ }
71
+ export async function saveConfig(cfg) {
72
+ const merged = normalize(cfg);
73
+ const file = paths.configFile();
74
+ await fs.ensureDir(paths.root());
75
+ await fs.writeJson(file, merged, { spaces: 2 });
76
+ }
77
+ export async function configExists() {
78
+ return fs.pathExists(paths.configFile());
79
+ }
80
+ //# sourceMappingURL=config-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config-store.js","sourceRoot":"","sources":["../../src/config/config-store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import { registerHook, unregisterHook, isHookRegistered, } from './claude-settings.js';
2
+ /**
3
+ * The set of hooks claude-notify registers today. Stop → text recap when a
4
+ * turn finishes; Notification → text recap when Claude needs permission or
5
+ * is idle waiting on input.
6
+ */
7
+ export const HOOK_COMMANDS = [
8
+ { event: 'Stop', command: 'claude-notify hook on-stop' },
9
+ { event: 'Notification', command: 'claude-notify hook on-notification' },
10
+ ];
11
+ /**
12
+ * Hooks earlier versions of claude-notify (pre-0.2) used to register but no
13
+ * longer ship a command for. `uninstall` and `init` both wipe these so a
14
+ * user upgrading from a bridge-era install doesn't end up with stale entries
15
+ * in `~/.claude/settings.json` that point at non-existent commands.
16
+ */
17
+ export const LEGACY_HOOK_COMMANDS = [
18
+ { event: 'SessionStart', command: 'claude-notify hook on-session-start' },
19
+ { event: 'UserPromptSubmit', command: 'claude-notify hook on-user-prompt-submit' },
20
+ ];
21
+ /**
22
+ * Idempotently register every hook in `HOOK_COMMANDS`, after first wiping
23
+ * any legacy hook entries left over from a pre-0.2 install. Returns the list
24
+ * that's now present.
25
+ */
26
+ export async function registerAllHooks() {
27
+ // Clean up legacy hooks first so settings.json doesn't keep pointing at
28
+ // commands that no longer exist.
29
+ for (const h of LEGACY_HOOK_COMMANDS) {
30
+ await unregisterHook(h.event, h.command);
31
+ }
32
+ for (const h of HOOK_COMMANDS) {
33
+ await registerHook(h.event, h.command);
34
+ }
35
+ return [...HOOK_COMMANDS];
36
+ }
37
+ /**
38
+ * Remove every hook claude-notify could ever have registered — current and
39
+ * legacy — regardless of which version originally wrote it. Idempotent.
40
+ */
41
+ export async function unregisterAllHooks() {
42
+ const all = [...HOOK_COMMANDS, ...LEGACY_HOOK_COMMANDS];
43
+ for (const h of all) {
44
+ await unregisterHook(h.event, h.command);
45
+ }
46
+ return all;
47
+ }
48
+ /**
49
+ * Snapshot of which claude-notify hooks are currently present in
50
+ * `~/.claude/settings.json`. Reports the current (post-0.2) hook set only;
51
+ * legacy hooks are intentionally excluded from status output.
52
+ */
53
+ export async function getRegisteredHookStatus() {
54
+ const out = [];
55
+ for (const h of HOOK_COMMANDS) {
56
+ out.push({ ...h, registered: await isHookRegistered(h.event, h.command) });
57
+ }
58
+ return out;
59
+ }
60
+ //# sourceMappingURL=hooks.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../src/config/hooks.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,YAAY,EACZ,cAAc,EACd,gBAAgB,GACjB,MAAM,sBAAsB,CAAC;AAiB9B;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAA2B;IACnD,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,4BAA4B,EAAE;IACxD,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,oCAAoC,EAAE;CACzE,CAAC;AAEF;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAA2B;IAC1D,EAAE,KAAK,EAAE,cAAc,EAAE,OAAO,EAAE,qCAAqC,EAAE;IACzE,EAAE,KAAK,EAAE,kBAAkB,EAAE,OAAO,EAAE,0CAA0C,EAAE;CACnF,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB;IACpC,wEAAwE;IACxE,iCAAiC;IACjC,KAAK,MAAM,CAAC,IAAI,oBAAoB,EAAE,CAAC;QACrC,MAAM,cAAc,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,aAAa,EAAE,CAAC;QAC9B,MAAM,YAAY,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IACzC,CAAC;IACD,OAAO,CAAC,GAAG,aAAa,CAAC,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,GAAG,GAAG,CAAC,GAAG,aAAa,EAAE,GAAG,oBAAoB,CAAC,CAAC;IACxD,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;QACpB,MAAM,cAAc,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAMD;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB;IAC3C,MAAM,GAAG,GAA6B,EAAE,CAAC;IACzC,KAAK,MAAM,CAAC,IAAI,aAAa,EAAE,CAAC;QAC9B,GAAG,CAAC,IAAI,CAAC,EAAE,GAAG,CAAC,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7E,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,9 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+ export const paths = {
4
+ root: () => path.join(os.homedir(), '.claude-notify'),
5
+ configFile: () => path.join(os.homedir(), '.claude-notify', 'config.json'),
6
+ logsDir: () => path.join(os.homedir(), '.claude-notify', 'logs'),
7
+ claudeSettingsFile: () => path.join(os.homedir(), '.claude', 'settings.json'),
8
+ };
9
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/config/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,SAAS,CAAC;AACzB,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,MAAM,CAAC,MAAM,KAAK,GAAG;IACnB,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,gBAAgB,CAAC;IACrD,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,gBAAgB,EAAE,aAAa,CAAC;IAC1E,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,gBAAgB,EAAE,MAAM,CAAC;IAChE,kBAAkB,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,eAAe,CAAC;CAC9E,CAAC"}
@@ -0,0 +1,24 @@
1
+ export class DingTalkError extends Error {
2
+ code;
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = 'DingTalkError';
7
+ }
8
+ }
9
+ export async function sendToWebhook(url, payload) {
10
+ // Node 18+ ships a stable global `fetch` — no undici dep needed.
11
+ const res = await fetch(url, {
12
+ method: 'POST',
13
+ headers: { 'Content-Type': 'application/json' },
14
+ body: JSON.stringify(payload),
15
+ });
16
+ if (!res.ok) {
17
+ throw new DingTalkError(`HTTP ${res.status} from DingTalk webhook`);
18
+ }
19
+ const data = (await res.json());
20
+ if (data.errcode && data.errcode !== 0) {
21
+ throw new DingTalkError(`DingTalk error ${data.errcode}: ${data.errmsg ?? 'unknown'}`, data.errcode);
22
+ }
23
+ }
24
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/dingtalk/client.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,aAAc,SAAQ,KAAK;IACO;IAA7C,YAAY,OAAe,EAAkB,IAAa;QACxD,KAAK,CAAC,OAAO,CAAC,CAAC;QAD4B,SAAI,GAAJ,IAAI,CAAS;QAExD,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;IAC9B,CAAC;CACF;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,GAAW,EAAE,OAAe;IAC9D,iEAAiE;IACjE,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC3B,MAAM,EAAE,MAAM;QACd,OAAO,EAAE,EAAE,cAAc,EAAE,kBAAkB,EAAE;QAC/C,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC;KAC9B,CAAC,CAAC;IACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;QACZ,MAAM,IAAI,aAAa,CAAC,QAAQ,GAAG,CAAC,MAAM,wBAAwB,CAAC,CAAC;IACtE,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA0C,CAAC;IACzE,IAAI,IAAI,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;QACvC,MAAM,IAAI,aAAa,CACrB,kBAAkB,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,MAAM,IAAI,SAAS,EAAE,EAC7D,IAAI,CAAC,OAAO,CACb,CAAC;IACJ,CAAC;AACH,CAAC"}
@@ -0,0 +1,33 @@
1
+ function basenameCrossPlatform(p) {
2
+ // Handle both / and \ separators regardless of host platform
3
+ const normalized = p.replace(/\\/g, '/').replace(/\/+$/, '');
4
+ const parts = normalized.split('/');
5
+ return parts[parts.length - 1] || p;
6
+ }
7
+ /**
8
+ * Build a DingTalk markdown message. The keyword is placed in BOTH `title`
9
+ * and `text` because DingTalk's custom-keyword security check scans both
10
+ * fields — putting it only in one risks silent rejection if the user's
11
+ * robot is configured strictly.
12
+ *
13
+ * The preview body is wrapped in a `>` blockquote for readability; Claude's
14
+ * replies often already contain markdown (lists, code fences), which the
15
+ * DingTalk client renders inline.
16
+ */
17
+ export function buildMarkdownMessage(input) {
18
+ const project = basenameCrossPlatform(input.cwd);
19
+ const preview = input.preview.trim() || '(无预览内容)';
20
+ const title = `【${input.keyword}】${project} 提醒`;
21
+ // Prefix every preview line with `> ` so multi-line replies stay inside
22
+ // the same blockquote — a bare `>` line would terminate it.
23
+ const quoted = preview
24
+ .split('\n')
25
+ .map((line) => `> ${line}`)
26
+ .join('\n');
27
+ const text = `### ${title}\n\n${quoted}`;
28
+ return {
29
+ msgtype: 'markdown',
30
+ markdown: { title, text },
31
+ };
32
+ }
33
+ //# sourceMappingURL=message-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"message-builder.js","sourceRoot":"","sources":["../../src/dingtalk/message-builder.ts"],"names":[],"mappings":"AAWA,SAAS,qBAAqB,CAAC,CAAS;IACtC,6DAA6D;IAC7D,MAAM,UAAU,GAAG,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IAC7D,MAAM,KAAK,GAAG,UAAU,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACpC,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,oBAAoB,CAAC,KAA2B;IAC9D,MAAM,OAAO,GAAG,qBAAqB,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,SAAS,CAAC;IAClD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,IAAI,OAAO,KAAK,CAAC;IAChD,wEAAwE;IACxE,4DAA4D;IAC5D,MAAM,MAAM,GAAG,OAAO;SACnB,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC;SAC1B,IAAI,CAAC,IAAI,CAAC,CAAC;IACd,MAAM,IAAI,GAAG,OAAO,KAAK,OAAO,MAAM,EAAE,CAAC;IACzC,OAAO;QACL,OAAO,EAAE,UAAU;QACnB,QAAQ,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE;KAC1B,CAAC;AACJ,CAAC"}
@@ -0,0 +1,114 @@
1
+ import fs from 'fs-extra';
2
+ function normaliseContent(c) {
3
+ if (Array.isArray(c))
4
+ return c;
5
+ if (typeof c === 'string')
6
+ return [{ type: 'text', text: c }];
7
+ return [];
8
+ }
9
+ function extractTurn(obj) {
10
+ // Claude Code (current) shape
11
+ if (obj.type === 'assistant' && obj.message?.role === 'assistant') {
12
+ return {
13
+ content: normaliseContent(obj.message.content),
14
+ timestamp: typeof obj.timestamp === 'string' ? obj.timestamp : null,
15
+ };
16
+ }
17
+ // Legacy SDK dump shape
18
+ if (obj.role === 'assistant') {
19
+ return {
20
+ content: normaliseContent(obj.content),
21
+ timestamp: typeof obj.timestamp === 'string' ? obj.timestamp : null,
22
+ };
23
+ }
24
+ return null;
25
+ }
26
+ /**
27
+ * Stream-read `path` and return the last assistant turn. Returns null when
28
+ * no assistant turn is present (empty transcript, only user messages, etc.).
29
+ *
30
+ * `lastN` (default 1) lets callers pull a small recent window when context
31
+ * matters — e.g. state detection wants to know whether the most recent
32
+ * assistant turn ended with a tool_use that has no matching tool_result yet.
33
+ */
34
+ export async function loadLastAssistantTurn(path) {
35
+ if (!(await fs.pathExists(path)))
36
+ return null;
37
+ const raw = await fs.readFile(path, 'utf8');
38
+ const lines = raw.split(/\r?\n/);
39
+ let last = null;
40
+ for (const line of lines) {
41
+ if (!line.trim())
42
+ continue;
43
+ let obj;
44
+ try {
45
+ obj = JSON.parse(line);
46
+ }
47
+ catch {
48
+ continue;
49
+ }
50
+ const t = extractTurn(obj);
51
+ if (t)
52
+ last = t;
53
+ }
54
+ return last;
55
+ }
56
+ export async function loadAllEntries(path) {
57
+ if (!(await fs.pathExists(path)))
58
+ return [];
59
+ const raw = await fs.readFile(path, 'utf8');
60
+ const out = [];
61
+ for (const line of raw.split(/\r?\n/)) {
62
+ if (!line.trim())
63
+ continue;
64
+ let obj;
65
+ try {
66
+ obj = JSON.parse(line);
67
+ }
68
+ catch {
69
+ continue;
70
+ }
71
+ let role;
72
+ let content;
73
+ if (obj.message && (obj.type === 'assistant' || obj.type === 'user')) {
74
+ role = obj.message.role;
75
+ content = normaliseContent(obj.message.content);
76
+ }
77
+ else if (obj.role === 'assistant' || obj.role === 'user') {
78
+ role = obj.role;
79
+ content = normaliseContent(obj.content);
80
+ }
81
+ out.push({
82
+ type: typeof obj.type === 'string' ? obj.type : (role ?? 'unknown'),
83
+ message: role && content ? { role, content } : null,
84
+ timestamp: typeof obj.timestamp === 'string' ? obj.timestamp : null,
85
+ });
86
+ }
87
+ return out;
88
+ }
89
+ function truncateWithEllipsis(text, maxLength) {
90
+ if (maxLength <= 0)
91
+ return '';
92
+ if (text.length <= maxLength)
93
+ return text;
94
+ let cut = text.slice(0, maxLength);
95
+ if (/[\uD800-\uDBFF]$/.test(cut))
96
+ cut = cut.slice(0, -1);
97
+ return cut + '…';
98
+ }
99
+ /**
100
+ * Return the joined text of all `text` blocks in the last assistant turn,
101
+ * truncated. Tool use blocks are summarised in a separate state-detector
102
+ * function — this one stays focused on "what did Claude say".
103
+ */
104
+ export async function extractLastAssistantText(transcriptPath, maxLength) {
105
+ const turn = await loadLastAssistantTurn(transcriptPath);
106
+ if (!turn)
107
+ return '';
108
+ const text = turn.content
109
+ .filter((b) => b.type === 'text' && typeof b.text === 'string')
110
+ .map((b) => b.text)
111
+ .join('\n');
112
+ return truncateWithEllipsis(text, maxLength);
113
+ }
114
+ //# sourceMappingURL=parser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parser.js","sourceRoot":"","sources":["../../src/transcript/parser.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ import { loadAllEntries, } from './parser.js';
2
+ const DEFAULT_PREVIEW_MAX = 100;
3
+ /**
4
+ * Extract the text of the last assistant turn for inclusion in the push.
5
+ * Returns `awaiting_input` with empty preview when the transcript is empty
6
+ * or unreadable — never throws.
7
+ */
8
+ export async function detectStopState(transcriptPath, opts = {}) {
9
+ const maxLen = opts.maxPreviewLength ?? DEFAULT_PREVIEW_MAX;
10
+ const entries = await loadAllEntries(transcriptPath);
11
+ if (entries.length === 0)
12
+ return { kind: 'awaiting_input', preview: '' };
13
+ const lastAssistantIdx = findLastAssistantIndex(entries);
14
+ if (lastAssistantIdx === -1)
15
+ return { kind: 'awaiting_input', preview: '' };
16
+ const blocks = entries[lastAssistantIdx].message.content;
17
+ return {
18
+ kind: 'awaiting_input',
19
+ preview: truncate(extractText(blocks), maxLen),
20
+ };
21
+ }
22
+ function findLastAssistantIndex(entries) {
23
+ for (let i = entries.length - 1; i >= 0; i--) {
24
+ if (entries[i].message?.role === 'assistant')
25
+ return i;
26
+ }
27
+ return -1;
28
+ }
29
+ function extractText(blocks) {
30
+ return blocks
31
+ .filter((b) => b.type === 'text' && typeof b.text === 'string')
32
+ .map((b) => b.text)
33
+ .join('\n')
34
+ .trim();
35
+ }
36
+ function truncate(s, max) {
37
+ if (max <= 0)
38
+ return '';
39
+ if (s.length <= max)
40
+ return s;
41
+ let cut = s.slice(0, max);
42
+ if (/[\uD800-\uDBFF]$/.test(cut))
43
+ cut = cut.slice(0, -1);
44
+ return cut + '…';
45
+ }
46
+ //# sourceMappingURL=state-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"state-detector.js","sourceRoot":"","sources":["../../src/transcript/state-detector.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,cAAc,GAGf,MAAM,aAAa,CAAC;AAwBrB,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAEhC;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,cAAsB,EACtB,OAA+B,EAAE;IAEjC,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,IAAI,mBAAmB,CAAC;IAC5D,MAAM,OAAO,GAAG,MAAM,cAAc,CAAC,cAAc,CAAC,CAAC;IACrD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAEzE,MAAM,gBAAgB,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACzD,IAAI,gBAAgB,KAAK,CAAC,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,gBAAgB,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;IAE5E,MAAM,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAE,CAAC,OAAQ,CAAC,OAAO,CAAC;IAC3D,OAAO;QACL,IAAI,EAAE,gBAAgB;QACtB,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC;KAC/C,CAAC;AACJ,CAAC;AAED,SAAS,sBAAsB,CAAC,OAA0B;IACxD,KAAK,IAAI,CAAC,GAAG,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;QAC7C,IAAI,OAAO,CAAC,CAAC,CAAE,CAAC,OAAO,EAAE,IAAI,KAAK,WAAW;YAAE,OAAO,CAAC,CAAC;IAC1D,CAAC;IACD,OAAO,CAAC,CAAC,CAAC;AACZ,CAAC;AAED,SAAS,WAAW,CAAC,MAAsB;IACzC,OAAO,MAAM;SACV,MAAM,CAAC,CAAC,CAAC,EAAuC,EAAE,CACjD,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,OAAQ,CAAS,CAAC,IAAI,KAAK,QAAQ,CACzD;SACA,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;SAClB,IAAI,CAAC,IAAI,CAAC;SACV,IAAI,EAAE,CAAC;AACZ,CAAC;AAED,SAAS,QAAQ,CAAC,CAAS,EAAE,GAAW;IACtC,IAAI,GAAG,IAAI,CAAC;QAAE,OAAO,EAAE,CAAC;IACxB,IAAI,CAAC,CAAC,MAAM,IAAI,GAAG;QAAE,OAAO,CAAC,CAAC;IAC9B,IAAI,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;IAC1B,IAAI,kBAAkB,CAAC,IAAI,CAAC,GAAG,CAAC;QAAE,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACzD,OAAO,GAAG,GAAG,GAAG,CAAC;AACnB,CAAC"}
@@ -0,0 +1,7 @@
1
+ export class ConfigMissingError extends Error {
2
+ constructor() {
3
+ super('claude-notify is not configured. Run `claude-notify init` first.');
4
+ this.name = 'ConfigMissingError';
5
+ }
6
+ }
7
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/util/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3C;QACE,KAAK,CAAC,kEAAkE,CAAC,CAAC;QAC1E,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF"}
@@ -0,0 +1,92 @@
1
+ import fs from 'fs-extra';
2
+ import path from 'node:path';
3
+ import { paths } from '../config/paths.js';
4
+ function todayFile() {
5
+ const d = new Date();
6
+ const yyyy = d.getUTCFullYear();
7
+ const mm = String(d.getUTCMonth() + 1).padStart(2, '0');
8
+ const dd = String(d.getUTCDate()).padStart(2, '0');
9
+ return path.join(paths.logsDir(), `${yyyy}-${mm}-${dd}.log`);
10
+ }
11
+ export async function logEvent(level, event, data) {
12
+ await fs.ensureDir(paths.logsDir());
13
+ const line = JSON.stringify({
14
+ ...(data ?? {}),
15
+ ts: new Date().toISOString(),
16
+ level,
17
+ event,
18
+ });
19
+ await fs.appendFile(todayFile(), line + '\n');
20
+ }
21
+ /**
22
+ * Log retention helpers — keep ~/.claude-notify/logs/ bounded so a long-running
23
+ * install doesn't accumulate years of `.log` files. Only files matching the
24
+ * canonical `YYYY-MM-DD.log` pattern are touched — anything else the user (or
25
+ * a future feature) drops in the dir is left alone.
26
+ */
27
+ const CLEANUP_STAMP_FILE = '.last-cleanup';
28
+ const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000;
29
+ const LOG_FILE_PATTERN = /^\d{4}-\d{2}-\d{2}\.log$/;
30
+ /**
31
+ * Delete `YYYY-MM-DD.log` files whose mtime is older than `retentionDays` days.
32
+ * Returns the number of files removed. `retentionDays <= 0` is treated as
33
+ * "keep forever" and is a no-op.
34
+ */
35
+ export async function cleanupOldLogs(retentionDays) {
36
+ if (!Number.isFinite(retentionDays) || retentionDays <= 0)
37
+ return 0;
38
+ const dir = paths.logsDir();
39
+ if (!(await fs.pathExists(dir)))
40
+ return 0;
41
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1000;
42
+ const entries = await fs.readdir(dir);
43
+ let removed = 0;
44
+ for (const name of entries) {
45
+ if (!LOG_FILE_PATTERN.test(name))
46
+ continue;
47
+ const full = path.join(dir, name);
48
+ try {
49
+ const stat = await fs.stat(full);
50
+ if (stat.mtimeMs < cutoff) {
51
+ await fs.remove(full);
52
+ removed++;
53
+ }
54
+ }
55
+ catch {
56
+ // best-effort — if a file vanishes mid-scan or perms block stat, skip it
57
+ }
58
+ }
59
+ return removed;
60
+ }
61
+ /**
62
+ * Throttled wrapper around `cleanupOldLogs` — at most one scan per 24h per
63
+ * install, tracked by mtime on `~/.claude-notify/logs/.last-cleanup`. Safe to
64
+ * call on every hook invocation; meant for fire-and-forget use.
65
+ */
66
+ export async function maybeCleanupOldLogs(retentionDays) {
67
+ if (!Number.isFinite(retentionDays) || retentionDays <= 0)
68
+ return 0;
69
+ const dir = paths.logsDir();
70
+ const stamp = path.join(dir, CLEANUP_STAMP_FILE);
71
+ try {
72
+ if (await fs.pathExists(stamp)) {
73
+ const st = await fs.stat(stamp);
74
+ if (Date.now() - st.mtimeMs < CLEANUP_INTERVAL_MS)
75
+ return 0;
76
+ }
77
+ }
78
+ catch {
79
+ // fall through and attempt cleanup anyway
80
+ }
81
+ // Refresh the stamp BEFORE scanning so concurrent hook invocations don't
82
+ // double-scan. Worst case on a write failure: we scan again next call.
83
+ await fs.ensureDir(dir);
84
+ try {
85
+ await fs.writeFile(stamp, new Date().toISOString());
86
+ }
87
+ catch {
88
+ // ignore — stamp is an optimisation, not load-bearing
89
+ }
90
+ return cleanupOldLogs(retentionDays);
91
+ }
92
+ //# sourceMappingURL=logger.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/util/logger.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "claude-notify-ding",
3
+ "version": "0.0.1",
4
+ "description": "Claude Code 触发 hook 时推送钉钉文本提醒",
5
+ "type": "module",
6
+ "bin": {
7
+ "claude-notify": "./bin/claude-notify.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "dist",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "scripts": {
18
+ "build": "tsc",
19
+ "test": "vitest run --passWithNoTests",
20
+ "test:watch": "vitest",
21
+ "typecheck": "tsc --noEmit",
22
+ "clean": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\""
23
+ },
24
+ "dependencies": {
25
+ "commander": "^12.1.0",
26
+ "fs-extra": "^11.2.0",
27
+ "prompts": "^2.4.2"
28
+ },
29
+ "devDependencies": {
30
+ "@types/fs-extra": "^11.0.4",
31
+ "@types/node": "^20.14.0",
32
+ "@types/prompts": "^2.4.9",
33
+ "typescript": "^5.5.0",
34
+ "vitest": "^2.0.0"
35
+ }
36
+ }