dingtalk-ask-mcp-server 0.1.4 → 0.1.6

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
@@ -60,15 +60,19 @@ dingtalk-ask install --client-id <AppKey> --client-secret <AppSecret> \
60
60
  [--timeout 1440000] [--host cc|codex|both]
61
61
  ```
62
62
 
63
+ > **不想记参数?** 在终端直接跑 `dingtalk-ask install`(不带凭证)会进入**交互式向导**,一步步问你 AppKey / AppSecret / robotCode / userId / 宿主(有默认值回车即用)。
64
+
63
65
  它会自动改这些(合并保留既有内容、幂等):
64
66
 
65
67
  | 宿主 | 改动的文件 | 写入内容 |
66
68
  |---|---|---|
67
- | **Claude Code** | `<当前目录>/.mcp.json` | `mcpServers.dingtalk-ask`(服务 + 凭证 env) |
68
- | | `<当前目录>/.claude/settings.json`(`--global` 则 `~/.claude/settings.json`) | `PermissionRequest` 钩子 + `env.MCP_TOOL_TIMEOUT` |
69
+ | **Claude Code**(项目级) | `<当前目录>/.mcp.json` | `mcpServers.dingtalk-ask`(服务 + 凭证 env) |
70
+ | | `<当前目录>/.claude/settings.json` | `PermissionRequest` 钩子 + `env.MCP_TOOL_TIMEOUT` |
71
+ | **Claude Code**(`--global`·所有项目) | `~/.claude.json`(先备份 `.bak`) | `mcpServers.dingtalk-ask` |
72
+ | | `~/.claude/settings.json` | 钩子 + `MCP_TOOL_TIMEOUT` |
69
73
  | **Codex** | `~/.codex/config.toml` | `[mcp_servers.dingtalk-ask]`(含 `tool_timeout_sec=1500` + 凭证,先备份 `.bak`) |
70
74
 
71
- 默认 **CC 必装,检测到 `~/.codex` 时 Codex 一并装**。装完**重启 Claude Code / 重开 Codex 会话**生效。
75
+ 默认 **CC 必装,检测到 `~/.codex` 时 Codex 一并装**。CC 默认写**项目级**(只对当前项目生效);加 **`--global`** 写**用户级**,免每个项目各装一次(合并保留你其它 MCP server)。装完**重启 Claude Code / 重开 Codex 会话**生效。
72
76
 
73
77
  ### 方式 B:手动接入(想自己控制时)
74
78
 
@@ -14,6 +14,7 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync, unlin
14
14
  import { homedir, tmpdir } from 'node:os';
15
15
  import { join, dirname } from 'node:path';
16
16
  import { fileURLToPath } from 'node:url';
17
+ import { createInterface } from 'node:readline/promises';
17
18
  import { resolveDingtalkConfig } from './dingtalk/config.js';
18
19
  import { mergeArgsIntoEnv } from './cliArgs.js';
19
20
  import { packageVersion } from './version.js';
@@ -54,17 +55,22 @@ function writeJson(path, obj) {
54
55
  mkdirSync(dirname(path), { recursive: true });
55
56
  writeFileSync(path, `${JSON.stringify(obj, null, 2)}\n`, 'utf8');
56
57
  }
57
- /** 装 Claude Code:.mcp.json(MCP server)+ .claude/settings.json(钩子 + MCP_TOOL_TIMEOUT)。 */
58
+ /** 装 Claude Code:MCP server(项目 .mcp.json --global 用户级 ~/.claude.json)+ 钩子 + MCP_TOOL_TIMEOUT */
58
59
  function installClaudeCode(config, argv, nodePath, indexPath) {
59
60
  const cwd = process.cwd();
60
- // 1. .mcp.json
61
- const mcpPath = join(cwd, '.mcp.json');
61
+ const global = argv.includes('--global');
62
+ // 1. MCP server:--global 写 ~/.claude.json(用户级,所有项目可见),否则项目 .mcp.json
63
+ const mcpPath = global ? join(homedir(), '.claude.json') : join(cwd, '.mcp.json');
62
64
  const entry = buildMcpEntry(config, nodePath, indexPath);
63
- const { settings: mcpMerged, existed } = mergeMcpServers(readJson(mcpPath), MCP_SERVER_NAME, entry);
65
+ const before = readJson(mcpPath); // 解析失败会抛错中止,绝不覆盖(保护 ~/.claude.json)。
66
+ const { settings: mcpMerged, existed } = mergeMcpServers(before, MCP_SERVER_NAME, entry);
67
+ if (global && existsSync(mcpPath)) {
68
+ copyFileSync(mcpPath, `${mcpPath}.bak`);
69
+ }
64
70
  writeJson(mcpPath, mcpMerged);
65
- console.log(`✅ [CC] ${existed ? '更新' : '写入'} MCP server 到 ${mcpPath}`);
66
- // 2. .claude/settings.json:钩子 + MCP_TOOL_TIMEOUT(略大于 ask 超时)
67
- const settingsPath = resolveSettingsPath(argv.includes('--global') ? ['--global'] : [], cwd, homedir());
71
+ console.log(`✅ [CC] ${existed ? '更新' : '写入'} MCP server 到 ${mcpPath}${global ? '(用户级·所有项目可见,已备份 .bak)' : ''}`);
72
+ // 2. .claude/settings.json:钩子 + MCP_TOOL_TIMEOUT(略大于 ask 超时)。
73
+ const settingsPath = resolveSettingsPath(global ? ['--global'] : [], cwd, homedir());
68
74
  const command = `node "${permissionHookPath()}"`;
69
75
  const hookMerged = mergeHook(readJson(settingsPath), command);
70
76
  const tmMerged = ensureMcpToolTimeout(hookMerged.settings, config.timeoutMs + 60_000);
@@ -160,6 +166,8 @@ async function runInfo() {
160
166
  console.log('\n【配置 · Claude Code】');
161
167
  const mcp = inspectMcpJson(tolerantJson(join(cwd, '.mcp.json')));
162
168
  console.log(` .mcp.json (${cwd}):${mcp.configured ? `✅ 已配(command=${mcp.command ?? '?'}, userId=${mask(mcp.userId)})` : '❌ 未配'}`);
169
+ const userMcp = inspectMcpJson(tolerantJson(join(homedir(), '.claude.json')));
170
+ console.log(` 用户级 ~/.claude.json:${userMcp.configured ? `✅ 已配(所有项目可见,userId=${mask(userMcp.userId)})` : '❌ 未配'}`);
163
171
  const projSettings = inspectSettingsHook(tolerantJson(join(cwd, '.claude', 'settings.json')));
164
172
  // MCP_TOOL_TIMEOUT 可能在 settings.json 或 settings.local.json,两处都看。
165
173
  const localTimeout = inspectSettingsHook(tolerantJson(join(cwd, '.claude', 'settings.local.json'))).mcpToolTimeout;
@@ -200,7 +208,7 @@ function main() {
200
208
  usage();
201
209
  }
202
210
  if (cmd === 'install') {
203
- runInstall(rest);
211
+ void runInstall(rest);
204
212
  }
205
213
  else if (cmd === 'uninstall') {
206
214
  runUninstall(rest);
@@ -287,16 +295,20 @@ function runUninstall(rest) {
287
295
  const doCc = host === undefined || host === 'cc' || host === 'both';
288
296
  const doCodex = host === 'codex' || host === 'both' || (host === undefined && existsSync(join(homedir(), '.codex')));
289
297
  if (doCc) {
290
- const mcpPath = join(cwd, '.mcp.json');
298
+ const global = rest.includes('--global');
299
+ const mcpPath = global ? join(homedir(), '.claude.json') : join(cwd, '.mcp.json');
291
300
  const mcp = removeMcpServer(tolerantJson(mcpPath), MCP_SERVER_NAME);
292
301
  if (mcp.removed) {
302
+ if (global && existsSync(mcpPath)) {
303
+ copyFileSync(mcpPath, `${mcpPath}.bak`);
304
+ }
293
305
  writeJson(mcpPath, mcp.settings);
294
- console.log(`· [CC] 从 ${mcpPath} 移除 MCP server`);
306
+ console.log(`· [CC] 从 ${mcpPath} 移除 MCP server${global ? '(已备份 .bak)' : ''}`);
295
307
  }
296
308
  else {
297
309
  console.log(`· [CC] ${mcpPath} 无 dingtalk-ask,跳过`);
298
310
  }
299
- const settingsPath = resolveSettingsPath(rest.includes('--global') ? ['--global'] : [], cwd, homedir());
311
+ const settingsPath = resolveSettingsPath(global ? ['--global'] : [], cwd, homedir());
300
312
  const hookRes = removeHook(tolerantJson(settingsPath));
301
313
  if (hookRes.removed) {
302
314
  writeJson(settingsPath, hookRes.settings);
@@ -328,23 +340,75 @@ function runUninstall(rest) {
328
340
  cleanRuntime();
329
341
  console.log('\n✅ 已卸载。重启 Claude Code / 重开 Codex 会话后彻底生效。');
330
342
  }
331
- /** install 子命令:装 MCP + 钩子。 */
332
- function runInstall(rest) {
343
+ /** 交互式逐步收集凭证(缺配置时的向导)。返回解析后的 config 与可选 host。 */
344
+ async function promptConfig(env, cwd) {
345
+ const existing = inspectMcpJson(tolerantJson(join(cwd, '.mcp.json')));
346
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
347
+ try {
348
+ console.log('未检测到完整凭证,进入交互式配置(直接回车用【中括号】内默认值):\n');
349
+ const ask = async (label, def) => {
350
+ const a = (await rl.question(` ${label}${def ? ` [${def}]` : ''}: `)).trim();
351
+ return a || def || '';
352
+ };
353
+ const askSecret = async (label, realDef) => {
354
+ const a = (await rl.question(` ${label}${realDef ? ` [${mask(realDef)}]` : ''}: `)).trim();
355
+ return a || realDef || '';
356
+ };
357
+ const clientId = await ask('AppKey (client-id)', env.DINGTALK_CLIENT_ID);
358
+ const clientSecret = await askSecret('AppSecret (client-secret)', env.DINGTALK_CLIENT_SECRET);
359
+ const robotCode = await ask('机器人 robotCode(一般同 AppKey)', env.DINGTALK_ROBOT_CODE ?? clientId);
360
+ const userId = await ask('你的钉钉 userId', env.DINGTALK_USER_ID ?? existing.userId);
361
+ const defaultHost = existsSync(join(homedir(), '.codex')) ? 'both' : 'cc';
362
+ const hostRaw = await ask('装到哪个宿主 cc/codex/both', defaultHost);
363
+ const host = hostRaw === 'cc' || hostRaw === 'codex' || hostRaw === 'both' ? hostRaw : undefined;
364
+ const merged = {
365
+ ...env,
366
+ DINGTALK_CLIENT_ID: clientId,
367
+ DINGTALK_CLIENT_SECRET: clientSecret,
368
+ DINGTALK_ROBOT_CODE: robotCode || clientId,
369
+ DINGTALK_USER_ID: userId,
370
+ };
371
+ let config;
372
+ try {
373
+ config = resolveDingtalkConfig(merged);
374
+ }
375
+ catch (error) {
376
+ console.error(`\n凭证不完整:${error instanceof Error ? error.message : String(error)}`);
377
+ process.exit(1);
378
+ }
379
+ return { config, host };
380
+ }
381
+ finally {
382
+ rl.close();
383
+ }
384
+ }
385
+ /** install 子命令:装 MCP + 钩子(缺凭证时进交互式向导)。 */
386
+ async function runInstall(rest) {
387
+ const cwd = process.cwd();
388
+ const env = mergeArgsIntoEnv(process.env, rest);
333
389
  let config;
390
+ let interactiveHost;
334
391
  try {
335
- config = resolveDingtalkConfig(mergeArgsIntoEnv(process.env, rest));
392
+ config = resolveDingtalkConfig(env);
336
393
  }
337
- catch (error) {
338
- console.error(`配置错误:${error instanceof Error ? error.message : String(error)}`);
339
- console.error('请用 --client-id / --client-secret / --robot-code / --user-id 提供凭证。');
340
- process.exit(1);
394
+ catch {
395
+ // 缺凭证:交互式终端 逐步引导输入;非交互(管道/CI)→ 报错提示用参数。
396
+ if (process.stdin.isTTY !== true) {
397
+ console.error('缺少钉钉凭证。请用 --client-id / --client-secret / --robot-code / --user-id 提供,');
398
+ console.error('或在交互式终端直接运行 `dingtalk-ask install` 逐步输入。');
399
+ process.exit(1);
400
+ }
401
+ const r = await promptConfig(env, cwd);
402
+ config = r.config;
403
+ interactiveHost = r.host;
341
404
  }
342
405
  const nodePath = process.execPath;
343
406
  const indexPath = fileURLToPath(new URL('./index.js', import.meta.url));
344
- // 目标宿主:显式 --host 优先;否则 CC 必装,Codex 存在则一并装。
345
- const host = parseHost(rest);
407
+ // 目标宿主:显式 --host 优先,其次交互式选择;否则 CC 必装、Codex 存在则一并装。
408
+ const host = parseHost(rest) ?? interactiveHost;
346
409
  const doCc = host === undefined || host === 'cc' || host === 'both';
347
410
  const doCodex = host === 'codex' || host === 'both' || (host === undefined && existsSync(join(homedir(), '.codex')));
411
+ console.log('');
348
412
  if (doCc) {
349
413
  installClaudeCode(config, rest, nodePath, indexPath);
350
414
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dingtalk-ask-mcp-server",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Human-in-the-loop MCP server:把 code agent(Claude Code / Codex)的决策与命令审批推送到钉钉,阻塞等你在手机回复。离开电脑也能远程拍板。",
5
5
  "keywords": [
6
6
  "mcp",