@quecue/dev-notify 0.0.0-stage → 0.1.0-alpha.3

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.
@@ -0,0 +1,189 @@
1
+ import { readFile, mkdir, open, rename, unlink } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomBytes, createHash } from 'node:crypto';
4
+ import { setTimeout as delay } from 'node:timers/promises';
5
+ import { ConfigError, decodeUtf8, loadConfig, ready } from './config.js';
6
+ import { replyHooksReady } from './claude-code.js';
7
+ export const TTL = 24 * 60 * 60 * 1000;
8
+ export const digest = (text) => createHash('sha256').update(text).digest('hex');
9
+ export const botKey = (config) => digest(config.telegram.token);
10
+ export const targetEpoch = (target) => digest(JSON.stringify([target.session, target.pid, target.birth, target.tty]));
11
+ export const policyPath = (config) => config + '.reply.json';
12
+ export const replyDirectory = (config) => config + '.reply-data';
13
+ export async function readJson(file) {
14
+ try {
15
+ const bytes = await readFile(file);
16
+ if (bytes.length > 16 * 1024 * 1024)
17
+ throw new Error();
18
+ return JSON.parse(decodeUtf8(bytes));
19
+ }
20
+ catch (error) {
21
+ if (error.code === 'ENOENT')
22
+ return null;
23
+ throw new ConfigError('无法读取回复配置或状态;未覆盖、未启用远程输入');
24
+ }
25
+ }
26
+ export async function atomicJson(file, value) {
27
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
28
+ const temporary = path.join(path.dirname(file), `.reply-${randomBytes(12).toString('hex')}.tmp`);
29
+ const handle = await open(temporary, 'wx', 0o600);
30
+ try {
31
+ try {
32
+ await handle.writeFile(JSON.stringify(value) + '\n', 'utf8');
33
+ await handle.sync();
34
+ }
35
+ finally {
36
+ await handle.close();
37
+ }
38
+ await rename(temporary, file);
39
+ }
40
+ finally {
41
+ await unlink(temporary).catch(error => { if (error.code !== 'ENOENT')
42
+ throw error; });
43
+ }
44
+ }
45
+ export async function locked(file, work) {
46
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
47
+ let handle;
48
+ for (let attempt = 0; attempt < 100; attempt++) {
49
+ try {
50
+ handle = await open(file, 'wx', 0o600);
51
+ break;
52
+ }
53
+ catch (error) {
54
+ if (error.code !== 'EEXIST')
55
+ throw new ConfigError('无法获取回复状态写入锁');
56
+ if (attempt === 99)
57
+ throw new ConfigError('回复状态锁繁忙;确认进程已退出后才能处理残留锁');
58
+ await delay(40);
59
+ }
60
+ }
61
+ try {
62
+ return await work();
63
+ }
64
+ finally {
65
+ await handle?.close();
66
+ await unlink(file);
67
+ }
68
+ }
69
+ export async function accessReplies(file, env = process.env) {
70
+ const policy = await readJson(policyPath(file));
71
+ if (policy === null || policy.enabled === false)
72
+ return null;
73
+ if (policy.version !== 1 || policy.enabled !== true || typeof policy.bot !== 'string' || !/^[a-f0-9]{64}$/.test(policy.bot)
74
+ || typeof policy.generation !== 'string' || !/^[a-f0-9]{32}$/.test(policy.generation)
75
+ || !Array.isArray(policy.users) || !policy.users.length || policy.users.some((id) => typeof id !== 'string' || !/^[1-9][0-9]*$/.test(id))) {
76
+ throw new ConfigError('回复授权配置无效,拒绝远程输入');
77
+ }
78
+ if (!await replyHooksReady(file, { env }))
79
+ throw new ConfigError('回复所需 hooks 缺失、异步时序不安全或已禁用;请先重新运行 integration claude-code install 并在 /hooks 确认加载');
80
+ const config = await loadConfig(file, true, env);
81
+ if (!ready(config, 'telegram') || botKey(config) !== policy.bot)
82
+ throw new ConfigError('Telegram Bot 已变更或不可用,请重新运行 reply setup 授权');
83
+ const users = policy.users.filter((id) => config.telegram.chatIds.includes(id));
84
+ if (!users.length)
85
+ throw new ConfigError('回复授权用户不在当前 Telegram 接收名单中,拒绝远程输入');
86
+ const effective = { ...policy, users };
87
+ return { file, config, policy: effective, identity: digest(JSON.stringify([policy.bot, policy.generation, users, policy.permissions === true])) };
88
+ }
89
+ export async function stateTransaction(access, work) {
90
+ const directory = replyDirectory(access.file);
91
+ return locked(path.join(directory, 'state.lock'), async () => {
92
+ const policy = await readJson(policyPath(access.file));
93
+ if (!policy?.enabled || policy.bot !== access.policy.bot || policy.generation !== access.policy.generation
94
+ || (policy.permissions === true) !== (access.policy.permissions === true))
95
+ throw new ConfigError('回复授权已撤销或变化,拒绝写入旧状态');
96
+ let state = await readJson(path.join(directory, 'state.json'));
97
+ if (!state || state.identity !== access.identity)
98
+ state = { version: 1, identity: access.identity, offset: 0, sessions: {}, entries: {}, pending: {}, requests: {}, calls: {} };
99
+ const record = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
100
+ state.requests ??= {};
101
+ state.calls ??= {};
102
+ if (state.version !== 1 || !Number.isSafeInteger(state.offset) || state.offset < 0
103
+ || ![state.sessions, state.entries, state.pending, state.requests, state.calls].every(record))
104
+ throw new ConfigError('回复状态结构无效,拒绝远程输入');
105
+ const now = Date.now();
106
+ for (const section of [state.sessions, state.entries, state.pending, state.requests, state.calls])
107
+ for (const [key, value] of Object.entries(section)) {
108
+ if (!record(value) || typeof value.updated !== 'number' || !Number.isFinite(value.updated)
109
+ || value.updated > now + 60000 || now - value.updated > TTL)
110
+ delete section[key];
111
+ }
112
+ for (const [key, value] of Object.entries(state.sessions)) {
113
+ if (value.session !== key || !Number.isSafeInteger(value.pid) || value.pid <= 0 || typeof value.birth !== 'string'
114
+ || typeof value.tty !== 'string' || typeof value.working !== 'boolean')
115
+ delete state.sessions[key];
116
+ }
117
+ for (const [key, value] of Object.entries(state.entries)) {
118
+ if (typeof value.session !== 'string' || typeof value.epoch !== 'string')
119
+ delete state.entries[key];
120
+ }
121
+ for (const [key, value] of Object.entries(state.pending)) {
122
+ if (typeof value.epoch !== 'string' || !/^[1-9][0-9]*$/.test(value.chat) || typeof value.id !== 'string'
123
+ || !['queued', 'submitted', 'unknown'].includes(value.phase))
124
+ delete state.pending[key];
125
+ }
126
+ for (const [key, value] of Object.entries(state.requests)) {
127
+ if (value.id !== key || !['question', 'permission'].includes(value.kind) || !record(value.input)
128
+ || !Array.isArray(value.questions) || !record(value.answers) || !Array.isArray(value.selected)
129
+ || !Number.isFinite(value.expires))
130
+ delete state.requests[key];
131
+ }
132
+ const result = await work(state);
133
+ await atomicJson(path.join(directory, 'state.json'), state);
134
+ return result;
135
+ });
136
+ }
137
+ export async function configureReplies(file, prompts, env = process.env) {
138
+ if (!await replyHooksReady(file, { env }))
139
+ throw new ConfigError('请先运行 integration claude-code install 安装绑定此通知配置的本版完整 hooks,再配置回复');
140
+ const config = await loadConfig(file, true, env);
141
+ if (!ready(config, 'telegram'))
142
+ throw new ConfigError('回复 Claude 需要先配置 Telegram Bot 和私聊接收人');
143
+ const available = config.telegram.chatIds.filter(id => /^[1-9][0-9]*$/.test(id));
144
+ if (!available.length)
145
+ throw new ConfigError('远程回复只允许 Telegram 私聊用户,不支持群聊');
146
+ const before = await readJson(policyPath(file));
147
+ prompts.out('远程回复等同向本机 Claude Code 终端输入并提交内容;不会自动批准工具权限,但可提交 Claude 命令。只授权可信用户。');
148
+ prompts.out('飞书按钮跳转 Telegram;不直接接收飞书消息。需要独占 Bot 的 getUpdates:不得与旧 Python 桥接或另一台机器共用同一个 Bot。');
149
+ prompts.out('Windows 使用系统 PowerShell / 控制台 API;macOS 使用 Ghostty 或 tmux,需要相应系统权限。不依赖 Python。');
150
+ prompts.out('支持范围不含 VS Code 集成终端、WSL、SSH;不确定目标或会话已切换时拒绝输入。');
151
+ prompts.out('通知接收人不会自动获得输入权限。当前可选择的私聊 chat_id:' + available.join('、'));
152
+ const text = await prompts.text('输入本次明确授权的私聊 chat_id(逗号分隔,留空取消):');
153
+ if (!text.trim())
154
+ return false;
155
+ const users = [...new Set(text.split(',').map(id => id.trim()))];
156
+ if (users.some(id => !available.includes(id)))
157
+ throw new ConfigError('只能授权当前通知名单中的明确私聊用户');
158
+ prompts.out('将授权以下用户远程输入:' + users.join('、'));
159
+ prompts.out('结构化问题将展示真实选项并允许自定义回答;普通回复只在本轮结束后输入。问题/选项及可选的工具请求参数可能含敏感信息,将发送到授权私聊和已配置的飞书渠道。');
160
+ const permissions = /^(y|yes)$/i.test(await prompts.text('是否另外允许这些用户逐次点击批准/拒绝工具权限?不修改永久规则。[y/N] '));
161
+ if (!/^(y|yes)$/i.test(await prompts.text('确认启用远程回复并接受上述权限范围?[y/N] ')))
162
+ return false;
163
+ await locked(policyPath(file) + '.lock', async () => {
164
+ if (JSON.stringify(before) !== JSON.stringify(await readJson(policyPath(file))))
165
+ throw new ConfigError('回复授权已被其他进程修改,请重试');
166
+ const effective = await loadConfig(file, true, env);
167
+ if (botKey(effective) !== botKey(config) || users.some(id => !effective.telegram.chatIds.includes(id)))
168
+ throw new ConfigError('通知配置在授权期间已变化,请重试');
169
+ await atomicJson(policyPath(file), { version: 1, enabled: true, bot: botKey(config), users, permissions, generation: randomBytes(16).toString('hex') });
170
+ });
171
+ prompts.out('回复授权已保存;旧消息映射不继承。新回复需从启用后的任务完成通知进入。');
172
+ return true;
173
+ }
174
+ export async function disableReplies(file, prompts) {
175
+ const before = await readJson(policyPath(file));
176
+ if (!before?.enabled) {
177
+ prompts.out('远程回复未启用。');
178
+ return true;
179
+ }
180
+ if (!/^(y|yes)$/i.test(await prompts.text('确认禁用远程回复?通知和凭据保持不变。[y/N] ')))
181
+ return false;
182
+ await locked(policyPath(file) + '.lock', async () => {
183
+ if (JSON.stringify(before) !== JSON.stringify(await readJson(policyPath(file))))
184
+ throw new ConfigError('回复授权已变化,请重试');
185
+ await atomicJson(policyPath(file), { ...before, enabled: false });
186
+ });
187
+ prompts.out('远程回复已禁用;正在进行中的注入不能撤回,桥接下次检查授权时退出。');
188
+ return true;
189
+ }
@@ -0,0 +1,19 @@
1
+ import { requestJson } from './channels.js';
2
+ import { ConfigError } from './config.js';
3
+ export function telegramApi(token, request = requestJson) {
4
+ return async (method, payload) => {
5
+ if (!['sendMessage', 'editMessageReplyMarkup', 'answerCallbackQuery', 'getMe', 'getUpdates', 'getWebhookInfo'].includes(method))
6
+ throw new ConfigError('不支持的 Telegram 桥接方法');
7
+ const response = await request(`https://api.telegram.org/bot${token}/${method}`, payload);
8
+ if (response.ok !== true) {
9
+ if (response.error_code === 409)
10
+ throw new ConfigError('Telegram 轮询冲突;检查旧 Python 桥接、其他机器或 webhook,桥接不会抢占或删除 webhook');
11
+ throw new ConfigError('Telegram 桥接请求失败;不输出服务端正文或秘密');
12
+ }
13
+ return response.result;
14
+ };
15
+ }
16
+ export async function replyHint(api, chat, title, content, replyTo, markup) {
17
+ return api('sendMessage', { chat_id: chat, text: title + '\n\n' + content,
18
+ ...(replyTo ? { reply_parameters: { message_id: replyTo } } : {}), ...(markup ? { reply_markup: markup } : {}) });
19
+ }
package/dist/setup.js ADDED
@@ -0,0 +1,250 @@
1
+ import { readFile, mkdir, open, rename, unlink } from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { randomBytes } from 'node:crypto';
4
+ import { ConfigError, decodeUtf8, example, parseConfig } from './config.js';
5
+ import { encodeBundle, nativeStore } from './credentials.js';
6
+ import { terminalPrompts } from './prompts.js';
7
+ const yes = (text) => /^(y|yes)$/i.test(text.trim());
8
+ export async function snapshot(file) {
9
+ try {
10
+ return await readFile(file);
11
+ }
12
+ catch (error) {
13
+ if (error.code === 'ENOENT')
14
+ return null;
15
+ throw new ConfigError('无法读取现有配置,未修改');
16
+ }
17
+ }
18
+ function same(left, right) { return left === null ? right === null : right !== null && left.equals(right); }
19
+ export function readRaw(raw) {
20
+ if (raw === null)
21
+ return structuredClone(example);
22
+ let data;
23
+ try {
24
+ data = JSON.parse(decodeUtf8(raw));
25
+ }
26
+ catch {
27
+ throw new ConfigError('现有配置损坏,未覆盖;请先修复 JSON');
28
+ }
29
+ parseConfig(data, {}, () => ({}));
30
+ return data;
31
+ }
32
+ async function removeFile(file) {
33
+ try {
34
+ await unlink(file);
35
+ }
36
+ catch (error) {
37
+ if (error.code !== 'ENOENT')
38
+ throw error;
39
+ }
40
+ }
41
+ export async function commitConfig(file, data, expected, store, values, options = {}) {
42
+ const warn = options.warn ?? (text => console.error(text));
43
+ const target = structuredClone(data);
44
+ const old = target.credentials;
45
+ const id = randomBytes(16).toString('hex');
46
+ if (values !== null) {
47
+ encodeBundle(values);
48
+ target.credentials = { backend: 'keyring', id };
49
+ parseConfig(target, {}, () => values);
50
+ }
51
+ else {
52
+ delete target.credentials;
53
+ parseConfig(target, {});
54
+ }
55
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
56
+ const lockPath = file + '.setup.lock';
57
+ let lock;
58
+ try {
59
+ lock = await open(lockPath, 'wx', 0o600);
60
+ }
61
+ catch {
62
+ throw new ConfigError('配置写入锁不可用;请检查权限,或确认没有运行中的向导后处理残留 .setup.lock');
63
+ }
64
+ const temporary = path.join(path.dirname(file), `.config-${id}.tmp`);
65
+ let created = false, committed = false, tempCreated = false;
66
+ try {
67
+ await lock.close();
68
+ if (!same(await snapshot(file), expected))
69
+ throw new ConfigError('配置在向导期间已变化,未覆盖,请重试');
70
+ if (values !== null) {
71
+ store.create(id, values);
72
+ created = true;
73
+ const check = store.read(id);
74
+ if (check === null || JSON.stringify(check) !== JSON.stringify(values))
75
+ throw new ConfigError('凭据写入校验失败,未切换配置');
76
+ }
77
+ const stream = await open(temporary, 'wx', 0o600);
78
+ tempCreated = true;
79
+ try {
80
+ await stream.writeFile(JSON.stringify(target, null, 2) + '\n', 'utf8');
81
+ await stream.sync();
82
+ }
83
+ finally {
84
+ await stream.close();
85
+ }
86
+ if (!same(await snapshot(file), expected))
87
+ throw new ConfigError('配置在保存期间已变化,未覆盖,请重试');
88
+ await (options.replace ?? rename)(temporary, file);
89
+ committed = true;
90
+ }
91
+ finally {
92
+ if (tempCreated) {
93
+ try {
94
+ await removeFile(temporary);
95
+ }
96
+ catch {
97
+ warn(`临时配置清理失败:${path.basename(temporary)}`);
98
+ }
99
+ }
100
+ if (created && !committed) {
101
+ try {
102
+ store.remove(id);
103
+ }
104
+ catch {
105
+ warn(`配置未切换,但新凭据清理失败;引用 ID:${id}`);
106
+ }
107
+ }
108
+ try {
109
+ await removeFile(lockPath);
110
+ }
111
+ catch {
112
+ warn('配置写入锁清理失败,请确认没有向导运行后处理 .setup.lock');
113
+ }
114
+ }
115
+ if (old?.backend === 'keyring') {
116
+ try {
117
+ store.remove(old.id);
118
+ }
119
+ catch {
120
+ warn(`配置已保存,但旧凭据清理失败;引用 ID:${old.id}`);
121
+ }
122
+ }
123
+ else if (old?.backend === 'system') {
124
+ warn('Python 原型旧凭据未删除;仍由原型管理,避免跨实现误删');
125
+ }
126
+ }
127
+ export async function wizard(file, options = {}) {
128
+ const prompts = options.prompts ?? terminalPrompts();
129
+ const before = await snapshot(file), data = readRaw(before);
130
+ const { out } = prompts;
131
+ out(`配置路径:${file}`);
132
+ out('密钥隐藏输入并存入 Windows Credential Manager / macOS Keychain;JSON 只保存引用。');
133
+ out('本步骤仅配置渠道;保存后可另行确认启用 Claude Code 通知,不启用远程控制。只有额外同意测试时才发送测试消息。');
134
+ if (before && !yes(await prompts.text('已有配置,继续编辑?[y/N] ')))
135
+ return false;
136
+ const selection = await prompts.text('选择渠道:1 Telegram,2 飞书,3 两者;其他输入取消:');
137
+ if (!['1', '2', '3'].includes(selection))
138
+ return false;
139
+ const store = options.store ?? nativeStore();
140
+ let previous = {};
141
+ const reference = data.credentials;
142
+ if (reference?.backend === 'keyring') {
143
+ try {
144
+ const existing = store.read(reference.id);
145
+ if (!existing)
146
+ throw new Error();
147
+ previous = existing;
148
+ }
149
+ catch {
150
+ out('原凭据无法读取,可能缺失、锁定或权限不足。');
151
+ if (!yes(await prompts.text('放弃旧值,重新输入所选渠道凭据?[y/N] ')))
152
+ return false;
153
+ }
154
+ }
155
+ else if (reference?.backend === 'system')
156
+ out('检测到 Python 原型凭据;本次重新输入保存,旧凭据不读取、不删除。');
157
+ data.telegram ??= {};
158
+ data.feishu ??= {};
159
+ data.telegram.enabled = selection !== '2';
160
+ data.feishu.enabled = selection !== '1';
161
+ const values = {};
162
+ const readValue = async (label, key, required) => {
163
+ while (true) {
164
+ const hint = previous[key] ? '留空保留已存值' : '不从环境变量自动复制';
165
+ const answer = await prompts.secret(`${label}(${hint}${required ? '' : ';输入 - 清除'}):`);
166
+ const value = !required && answer === '-' ? '' : answer || previous[key] || '';
167
+ if (required && !value) {
168
+ out('此项不能为空,请重试,或 Ctrl+C 取消。');
169
+ continue;
170
+ }
171
+ try {
172
+ const check = { telegram: { enabled: key === 'telegram_token', chat_ids: ['1'] },
173
+ feishu: { enabled: key !== 'telegram_token' }, credentials: { backend: 'keyring', id: '0'.repeat(32) } };
174
+ parseConfig(check, {}, () => ({ [key]: value }));
175
+ return value;
176
+ }
177
+ catch {
178
+ out('格式不正确,请检查后重新输入;不会显示秘密。');
179
+ }
180
+ }
181
+ };
182
+ if (data.telegram.enabled) {
183
+ values.telegram_token = await readValue('Telegram Bot Token', 'telegram_token', true);
184
+ out('接收人需要先启动 Bot;本轮手动填写数字 chat_id,不启动 getUpdates 或自动配对。');
185
+ while (true) {
186
+ const entered = await prompts.text('接收人 chat_id,多个用逗号分隔(留空保留原名单):');
187
+ const ids = entered ? entered.split(',').map(item => item.trim()) : data.telegram.chat_ids ?? [];
188
+ try {
189
+ if (!ids.length)
190
+ throw new Error();
191
+ const check = parseConfig({ telegram: { chat_ids: ids } }, {});
192
+ data.telegram.chat_ids = check.telegram.chatIds;
193
+ break;
194
+ }
195
+ catch {
196
+ out('请填写至少一个有效数字 chat_id,或 Ctrl+C 取消。');
197
+ }
198
+ }
199
+ }
200
+ if (data.feishu.enabled) {
201
+ values.feishu_webhook = await readValue('飞书 webhook', 'feishu_webhook', true);
202
+ values.feishu_secret = await readValue('飞书签名密钥(可选)', 'feishu_secret', false);
203
+ }
204
+ encodeBundle(values);
205
+ const preview = { ...data, credentials: { backend: 'keyring', id: '0'.repeat(32) } };
206
+ parseConfig(preview, {}, () => values);
207
+ const env = options.env ?? process.env;
208
+ out(`将启用:${data.telegram.enabled ? `Telegram(${data.telegram.chat_ids.length} 个接收人)` : ''}${data.feishu.enabled ? ' 飞书' : ''}`);
209
+ out('环境变量仍优先(含显式空值);测试通知会使用最终生效的配置,不一定是刚输入的值。');
210
+ const overrides = [data.telegram.token_env ?? 'TELEGRAM_BOT_TOKEN', data.feishu.webhook_env ?? 'FEISHU_WEBHOOK',
211
+ data.feishu.secret_env ?? 'FEISHU_SECRET', 'TELEGRAM_CHAT_ID', 'TELEGRAM_CHAT_IDS'].filter(name => Object.hasOwn(env, name));
212
+ if (overrides.length)
213
+ out('当前存在环境覆盖:' + overrides.join('、'));
214
+ if (reference?.backend === 'keyring')
215
+ out('保存成功后删除旧凭据;共享旧引用 ID 的配置副本也需更新。');
216
+ out('未选中的渠道会禁用;新条目只保存所选渠道凭据。');
217
+ if (!yes(await prompts.text('确认保存配置和系统凭据?[y/N] ')))
218
+ return false;
219
+ await commitConfig(file, data, before, store, values, { warn: out });
220
+ out('配置和系统凭据已保存。');
221
+ if (options.testSend && yes(await prompts.text('发送固定内容的测试通知到生效的接收目标?[y/N] '))) {
222
+ try {
223
+ const effective = parseConfig(preview, env, () => values);
224
+ const ok = await options.testSend(effective);
225
+ out(ok ? '测试通知发送成功。' : '配置已保存,但测试通知失败或没有可用渠道;请运行 doctor 检查。');
226
+ }
227
+ catch {
228
+ out('配置已保存,但环境覆盖或网络导致测试失败;请运行 doctor 检查。');
229
+ }
230
+ }
231
+ else
232
+ out('未发送测试通知。');
233
+ return true;
234
+ }
235
+ export async function clearCredentials(file, prompts, store) {
236
+ const before = await snapshot(file), data = readRaw(before);
237
+ const reference = data.credentials;
238
+ if (!reference) {
239
+ prompts.out('此配置没有系统凭据引用,未修改。');
240
+ return true;
241
+ }
242
+ if (reference.backend !== 'keyring')
243
+ throw new ConfigError('这是 Python 原型的凭据,Node 不执行跨实现删除');
244
+ prompts.out('将移除当前配置的系统凭据引用并删除其条目;环境变量不变,共享引用的配置副本将失效。');
245
+ if (!yes(await prompts.text('确认清除?[y/N] ')))
246
+ return false;
247
+ await commitConfig(file, data, before, store ?? nativeStore(), null, { warn: prompts.out });
248
+ prompts.out('凭据引用已移除;如删除条目失败,上方会显示警告。');
249
+ return true;
250
+ }
package/package.json CHANGED
@@ -1,6 +1,45 @@
1
1
  {
2
2
  "name": "@quecue/dev-notify",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0-alpha.3",
4
+ "description": "跨平台开发任务通知工具,支持 Telegram 与飞书",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/QueCue/dev-notify.git"
9
+ },
10
+ "homepage": "https://github.com/QueCue/dev-notify#readme",
11
+ "bugs": {
12
+ "url": "https://github.com/QueCue/dev-notify/issues"
13
+ },
14
+ "type": "module",
15
+ "engines": {
16
+ "node": ">=22.0.0"
17
+ },
18
+ "bin": {
19
+ "dev-notify": "dist/cli.js"
20
+ },
21
+ "files": [
22
+ "dist/*.js",
23
+ "README.md",
24
+ "SECURITY.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
28
+ "scripts": {
29
+ "build": "tsc -p tsconfig.json",
30
+ "test": "npm run build && node --test test-js/*.test.mjs",
31
+ "start": "node dist/cli.js",
32
+ "prepack": "npm run build"
33
+ },
34
+ "devDependencies": {
35
+ "@types/node": "^22.0.0",
36
+ "typescript": "^5.9.0"
37
+ },
38
+ "dependencies": {
39
+ "@napi-rs/keyring": "2.1.2"
40
+ },
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "registry": "https://registry.npmjs.org/"
44
+ }
45
+ }