dingtalk-ask-mcp-server 0.1.1 → 0.1.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.
package/README.md CHANGED
@@ -138,6 +138,8 @@ Codex 若 `approval_policy="never"` 本就不弹命令审批,故只提供 `ask_h
138
138
 
139
139
  ```bash
140
140
  dingtalk-ask install … # 一键接入(见上)
141
+ dingtalk-ask uninstall [--host …] # 一键卸载:删 MCP+钩子(CC/Codex) + 清运行时(config.toml 先备份 .bak)
142
+ dingtalk-ask info # 查看配置与进程状态(守护进程/离开模式/各宿主是否配好)
141
143
  dingtalk-ask daemon status|stop # 查看/停止共享守护进程
142
144
  dingtalk-ask away on|off|status # 本地切换离开模式
143
145
  dingtalk-ask clean # 清空运行时状态(守护进程/锁/flag/就绪标记/日志),不动配置文件
@@ -10,7 +10,7 @@
10
10
  * 写入均为宿主本地配置(.mcp.json / .claude/settings.json / ~/.codex/config.toml,皆不入库),
11
11
  * 合并保留既有内容、幂等。纯逻辑在 installConfig.ts / installHookCore.ts。
12
12
  */
13
- import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, unlinkSync, readdirSync } from 'node:fs';
13
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, unlinkSync, readdirSync, statSync } from 'node:fs';
14
14
  import { homedir, tmpdir } from 'node:os';
15
15
  import { join, dirname } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
@@ -21,7 +21,7 @@ import { mergeHook, permissionHookPath, resolveSettingsPath } from './installHoo
21
21
  import { daemonLockPath, readDaemonLock, removeDaemonLock } from './daemon/lock.js';
22
22
  import { probeHealth } from './daemonClient.js';
23
23
  import { isAway, setAway, awayFlagPath } from './awayState.js';
24
- import { buildMcpEntry, mergeMcpServers, ensureMcpToolTimeout, codexHasDingtalk, buildCodexBlock, MCP_SERVER_NAME, } from './installConfig.js';
24
+ import { buildMcpEntry, mergeMcpServers, ensureMcpToolTimeout, codexHasDingtalk, buildCodexBlock, inspectMcpJson, inspectSettingsHook, removeMcpServer, removeHook, removeCodexBlock, MCP_SERVER_NAME, } from './installConfig.js';
25
25
  /** 解析目标宿主。默认 CC;Codex 见 resolveCodexTarget。 */
26
26
  function parseHost(argv) {
27
27
  const i = argv.indexOf('--host');
@@ -99,15 +99,131 @@ function usage() {
99
99
  console.log('用法:');
100
100
  console.log(' dingtalk-ask install --client-id … --client-secret … --robot-code … --user-id … \\');
101
101
  console.log(' [--timeout <ms>] [--host cc|codex|both] # 一键装 MCP+钩子');
102
+ console.log(' dingtalk-ask uninstall [--host cc|codex|both] # 一键删 MCP+钩子 + 清运行时');
102
103
  console.log(' dingtalk-ask daemon <status|stop> # 查看/停止共享守护进程');
103
104
  console.log(' dingtalk-ask away <on|off|status> # 本地切换离开模式');
105
+ console.log(' dingtalk-ask info # 查看配置与进程状态');
104
106
  console.log(' dingtalk-ask clean # 清空运行时状态(守护进程/锁/flag/标记/日志)');
105
107
  console.log(' dingtalk-ask -v | --version # 版本');
106
108
  console.log(' dingtalk-ask -h | --help # 帮助');
107
109
  process.exit(1);
108
110
  }
109
- /** clean 子命令:清空运行时临时状态(不动配置文件)。 */
110
- function runClean() {
111
+ /** 宽松读 JSON:文件缺失/损坏都返回空对象(info 用,不抛错)。 */
112
+ function tolerantJson(path) {
113
+ try {
114
+ return JSON.parse(readFileSync(path, 'utf8'));
115
+ }
116
+ catch {
117
+ return {};
118
+ }
119
+ }
120
+ /** 轻度掩码(展示 userId 等,不完全暴露)。 */
121
+ function mask(v) {
122
+ if (v === undefined || v === '') {
123
+ return '(未知)';
124
+ }
125
+ return v.length <= 4 ? '***' : `${v.slice(0, 3)}***${v.slice(-2)}`;
126
+ }
127
+ /** info 子命令:一屏看配置与进程状态。 */
128
+ async function runInfo() {
129
+ const tmp = tmpdir();
130
+ const cwd = process.cwd();
131
+ console.log(`dingtalk-ask v${packageVersion()}`);
132
+ console.log('\n【进程】');
133
+ const lock = readDaemonLock(daemonLockPath());
134
+ if (lock === undefined) {
135
+ console.log(' 守护进程:未运行');
136
+ }
137
+ else {
138
+ const probe = await probeHealth(lock);
139
+ console.log(` 守护进程:${probe.ok ? '在跑' : '锁存在但无应答'} port=${lock.port} pid=${lock.pid} 就绪=${probe.ready}`);
140
+ }
141
+ console.log(` 离开模式:${isAway() ? '开(推钉钉)' : '关(只在终端)'}`);
142
+ let greeted = [];
143
+ try {
144
+ greeted = readdirSync(tmp)
145
+ .filter((f) => f.startsWith('dingtalk-greeted-'))
146
+ .map((f) => f.replace(/^dingtalk-greeted-/, '').replace(/\.flag$/, ''));
147
+ }
148
+ catch {
149
+ /* 忽略 */
150
+ }
151
+ console.log(` 已发就绪提示的机器人:${greeted.length > 0 ? greeted.join('、') : '无'}`);
152
+ const logPath = join(tmp, 'dingtalk-daemon.log');
153
+ try {
154
+ const st = statSync(logPath);
155
+ console.log(` 守护进程日志:${logPath}(${Math.round(st.size / 1024)}KB)`);
156
+ }
157
+ catch {
158
+ console.log(' 守护进程日志:无');
159
+ }
160
+ console.log('\n【配置 · Claude Code】');
161
+ const mcp = inspectMcpJson(tolerantJson(join(cwd, '.mcp.json')));
162
+ console.log(` .mcp.json (${cwd}):${mcp.configured ? `✅ 已配(command=${mcp.command ?? '?'}, userId=${mask(mcp.userId)})` : '❌ 未配'}`);
163
+ const projSettings = inspectSettingsHook(tolerantJson(join(cwd, '.claude', 'settings.json')));
164
+ // MCP_TOOL_TIMEOUT 可能在 settings.json 或 settings.local.json,两处都看。
165
+ const localTimeout = inspectSettingsHook(tolerantJson(join(cwd, '.claude', 'settings.local.json'))).mcpToolTimeout;
166
+ const mcpToolTimeout = projSettings.mcpToolTimeout ?? localTimeout;
167
+ console.log(` 项目 settings.json:命令审批钩子 ${projSettings.hook ? '✅' : '❌'}` +
168
+ `,MCP_TOOL_TIMEOUT=${mcpToolTimeout ?? '未设'}`);
169
+ const userSettings = inspectSettingsHook(tolerantJson(join(homedir(), '.claude', 'settings.json')));
170
+ console.log(` 用户 settings.json (~):命令审批钩子 ${userSettings.hook ? '✅' : '❌'}`);
171
+ console.log('\n【配置 · Codex】');
172
+ const codexPath = join(homedir(), '.codex', 'config.toml');
173
+ let codexContent = '';
174
+ try {
175
+ codexContent = readFileSync(codexPath, 'utf8');
176
+ }
177
+ catch {
178
+ codexContent = '';
179
+ }
180
+ if (codexContent === '') {
181
+ console.log(` ${codexPath}:无文件(未用 Codex?)`);
182
+ }
183
+ else if (codexHasDingtalk(codexContent)) {
184
+ const m = /\[mcp_servers\.dingtalk-ask\][\s\S]*?tool_timeout_sec\s*=\s*(\d+)/.exec(codexContent);
185
+ console.log(` ${codexPath}:✅ 已配(tool_timeout_sec=${m ? m[1] : '?'})`);
186
+ }
187
+ else {
188
+ console.log(` ${codexPath}:❌ 未配 dingtalk-ask`);
189
+ }
190
+ }
191
+ function main() {
192
+ const args = process.argv.slice(2);
193
+ const cmd = args[0];
194
+ const rest = args.slice(1);
195
+ if (cmd === '-v' || cmd === '--version') {
196
+ console.log(packageVersion());
197
+ return;
198
+ }
199
+ if (cmd === '-h' || cmd === '--help') {
200
+ usage();
201
+ }
202
+ if (cmd === 'install') {
203
+ runInstall(rest);
204
+ }
205
+ else if (cmd === 'uninstall') {
206
+ runUninstall(rest);
207
+ }
208
+ else if (cmd === 'daemon') {
209
+ void runDaemon(rest);
210
+ }
211
+ else if (cmd === 'away') {
212
+ runAway(rest);
213
+ }
214
+ else if (cmd === 'info') {
215
+ void runInfo();
216
+ }
217
+ else if (cmd === 'clean') {
218
+ runClean();
219
+ }
220
+ else {
221
+ usage();
222
+ }
223
+ }
224
+ main();
225
+ /** 清理运行时状态(停守护进程 + 删锁/离开flag/就绪标记/日志)。clean 与 uninstall 共用。 */
226
+ function cleanRuntime() {
111
227
  const tmp = tmpdir();
112
228
  // 1. 停守护进程 + 删锁。
113
229
  const lock = readDaemonLock(daemonLockPath());
@@ -158,36 +274,60 @@ function runClean() {
158
274
  catch {
159
275
  /* 忽略 */
160
276
  }
277
+ }
278
+ /** clean 子命令:只清运行时状态,不动配置文件。 */
279
+ function runClean() {
280
+ cleanRuntime();
161
281
  console.log('✅ 已清空运行时状态。配置文件(.mcp.json / settings.json / config.toml)未改动。');
162
282
  }
163
- function main() {
164
- const args = process.argv.slice(2);
165
- const cmd = args[0];
166
- const rest = args.slice(1);
167
- if (cmd === '-v' || cmd === '--version') {
168
- console.log(packageVersion());
169
- return;
170
- }
171
- if (cmd === '-h' || cmd === '--help') {
172
- usage();
173
- }
174
- if (cmd === 'install') {
175
- runInstall(rest);
176
- }
177
- else if (cmd === 'daemon') {
178
- void runDaemon(rest);
179
- }
180
- else if (cmd === 'away') {
181
- runAway(rest);
182
- }
183
- else if (cmd === 'clean') {
184
- runClean();
283
+ /** uninstall 子命令:删 MCP + 钩子(CC/Codex) + 清运行时状态(install 的逆操作)。 */
284
+ function runUninstall(rest) {
285
+ const cwd = process.cwd();
286
+ const host = parseHost(rest);
287
+ const doCc = host === undefined || host === 'cc' || host === 'both';
288
+ const doCodex = host === 'codex' || host === 'both' || (host === undefined && existsSync(join(homedir(), '.codex')));
289
+ if (doCc) {
290
+ const mcpPath = join(cwd, '.mcp.json');
291
+ const mcp = removeMcpServer(tolerantJson(mcpPath), MCP_SERVER_NAME);
292
+ if (mcp.removed) {
293
+ writeJson(mcpPath, mcp.settings);
294
+ console.log( [CC] ${mcpPath} 移除 MCP server`);
295
+ }
296
+ else {
297
+ console.log(`· [CC] ${mcpPath} dingtalk-ask,跳过`);
298
+ }
299
+ const settingsPath = resolveSettingsPath(rest.includes('--global') ? ['--global'] : [], cwd, homedir());
300
+ const hookRes = removeHook(tolerantJson(settingsPath));
301
+ if (hookRes.removed) {
302
+ writeJson(settingsPath, hookRes.settings);
303
+ console.log(`· [CC] ${settingsPath} 移除命令审批钩子(MCP_TOOL_TIMEOUT 保留,可能被其它 MCP 用)`);
304
+ }
305
+ else {
306
+ console.log(`· [CC] ${settingsPath} 无本工具钩子,跳过`);
307
+ }
185
308
  }
186
- else {
187
- usage();
309
+ if (doCodex) {
310
+ const cfgPath = join(homedir(), '.codex', 'config.toml');
311
+ let content = '';
312
+ try {
313
+ content = readFileSync(cfgPath, 'utf8');
314
+ }
315
+ catch {
316
+ content = '';
317
+ }
318
+ const res = removeCodexBlock(content);
319
+ if (res.removed) {
320
+ copyFileSync(cfgPath, `${cfgPath}.bak`);
321
+ writeFileSync(cfgPath, res.content, 'utf8');
322
+ console.log(`· [Codex] 从 ${cfgPath} 移除 [mcp_servers.dingtalk-ask](已备份 .bak)`);
323
+ }
324
+ else {
325
+ console.log(`· [Codex] ${cfgPath} 无 dingtalk-ask,跳过`);
326
+ }
188
327
  }
328
+ cleanRuntime();
329
+ console.log('\n✅ 已卸载。重启 Claude Code / 重开 Codex 会话后彻底生效。');
189
330
  }
190
- main();
191
331
  /** install 子命令:装 MCP + 钩子。 */
192
332
  function runInstall(rest) {
193
333
  let config;
@@ -54,6 +54,91 @@ export function ensureMcpToolTimeout(settings, ms) {
54
54
  export function codexHasDingtalk(content) {
55
55
  return content.includes(`[mcp_servers.${MCP_SERVER_NAME}]`);
56
56
  }
57
+ /** 检查 .mcp.json 是否配了 dingtalk-ask,并抽取 command / userId 供展示(纯函数)。 */
58
+ export function inspectMcpJson(obj) {
59
+ const servers = obj.mcpServers;
60
+ const s = servers?.[MCP_SERVER_NAME];
61
+ if (s === undefined) {
62
+ return { configured: false };
63
+ }
64
+ let userId = s.env?.DINGTALK_USER_ID;
65
+ if (userId === undefined && Array.isArray(s.args)) {
66
+ const i = s.args.indexOf('--user-id');
67
+ if (i >= 0) {
68
+ userId = s.args[i + 1];
69
+ }
70
+ }
71
+ return { configured: true, command: s.command, userId };
72
+ }
73
+ /** 检查 settings.json 是否装了命令审批钩子、以及 MCP_TOOL_TIMEOUT(纯函数)。 */
74
+ export function inspectSettingsHook(obj) {
75
+ const hooks = obj.hooks;
76
+ const list = hooks?.PermissionRequest;
77
+ const hook = Array.isArray(list) &&
78
+ list.some((e) => Array.isArray(e.hooks) && e.hooks.some((h) => typeof h.command === 'string' && h.command.includes('permissionHook.js')));
79
+ const env = obj.env;
80
+ const t = env?.MCP_TOOL_TIMEOUT;
81
+ return { hook, mcpToolTimeout: typeof t === 'string' ? t : undefined };
82
+ }
83
+ /** 从 .mcp.json 对象移除某 server(纯函数)。 */
84
+ export function removeMcpServer(obj, name) {
85
+ const servers = obj.mcpServers;
86
+ if (servers === undefined || !(name in servers)) {
87
+ return { settings: obj, removed: false };
88
+ }
89
+ const copy = { ...servers };
90
+ delete copy[name];
91
+ return { settings: { ...obj, mcpServers: copy }, removed: true };
92
+ }
93
+ /** 从 settings.json 对象移除本工具的命令审批钩子(纯函数,不动 MCP_TOOL_TIMEOUT)。 */
94
+ export function removeHook(obj) {
95
+ const hooks = obj.hooks;
96
+ const list = hooks?.PermissionRequest;
97
+ if (!Array.isArray(list)) {
98
+ return { settings: obj, removed: false };
99
+ }
100
+ const kept = list.filter((e) => !(Array.isArray(e.hooks) && e.hooks.some((h) => typeof h.command === 'string' && h.command.includes('permissionHook.js'))));
101
+ if (kept.length === list.length) {
102
+ return { settings: obj, removed: false };
103
+ }
104
+ const newHooks = { ...hooks };
105
+ if (kept.length === 0) {
106
+ delete newHooks.PermissionRequest;
107
+ }
108
+ else {
109
+ newHooks.PermissionRequest = kept;
110
+ }
111
+ return { settings: { ...obj, hooks: newHooks }, removed: true };
112
+ }
113
+ /**
114
+ * 从 Codex config.toml 文本移除 dingtalk-ask 的两个 section(纯函数)。
115
+ *
116
+ * 逐行扫描:遇到 `[mcp_servers.dingtalk-ask]` 或 `[...dingtalk-ask.env]` 段头即进入跳过,
117
+ * 直到下一个 `[` 段头或文件末尾。
118
+ *
119
+ * @param content 原 config.toml 文本。
120
+ * @returns { content: 移除后文本, removed: 是否有删除 }。
121
+ */
122
+ export function removeCodexBlock(content) {
123
+ const lines = content.split('\n');
124
+ const out = [];
125
+ let skipping = false;
126
+ let removed = false;
127
+ for (const line of lines) {
128
+ const trimmed = line.trim();
129
+ if (trimmed.startsWith('[')) {
130
+ skipping =
131
+ trimmed.startsWith(`[mcp_servers.${MCP_SERVER_NAME}]`) || trimmed.startsWith(`[mcp_servers.${MCP_SERVER_NAME}.env]`);
132
+ if (skipping) {
133
+ removed = true;
134
+ }
135
+ }
136
+ if (!skipping) {
137
+ out.push(line);
138
+ }
139
+ }
140
+ return { content: out.join('\n'), removed };
141
+ }
57
142
  /**
58
143
  * 构造 Codex config.toml 追加块(TOML 基本字符串转义,无需 TOML 库)。
59
144
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dingtalk-ask-mcp-server",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Human-in-the-loop MCP server:把 code agent(Claude Code / Codex)的决策与命令审批推送到钉钉,阻塞等你在手机回复。离开电脑也能远程拍板。",
5
5
  "keywords": [
6
6
  "mcp",