@dmsdc-ai/aigentry-deliberation 0.0.18 → 0.0.20

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/doctor.js ADDED
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * MCP Connection Doctor — aigentry-deliberation
5
+ *
6
+ * Triages MCP connection failures across all registered CLI environments:
7
+ * - ~/.codex/config.toml (Codex CLI)
8
+ * - ~/.claude/.mcp.json (Claude Code)
9
+ * - ~/.gemini/settings.json (Gemini CLI)
10
+ *
11
+ * Checks:
12
+ * 1. Path existence for every mcp_servers.* entry
13
+ * 2. Temp-path detection (/var/folders, /tmp, /_npx)
14
+ * 3. MODULE_NOT_FOUND traces in logs
15
+ *
16
+ * Usage:
17
+ * node doctor.js
18
+ * npx @dmsdc-ai/aigentry-deliberation doctor
19
+ */
20
+
21
+ import fs from "node:fs";
22
+ import path from "node:path";
23
+ import { execSync } from "node:child_process";
24
+
25
+ const HOME = process.env.HOME || process.env.USERPROFILE || "";
26
+ const IS_WIN = process.platform === "win32";
27
+
28
+ // ── Config file locations ──────────────────────────────────────
29
+
30
+ const CONFIGS = [
31
+ {
32
+ name: "Codex CLI",
33
+ path: path.join(HOME, ".codex", "config.toml"),
34
+ format: "toml",
35
+ },
36
+ {
37
+ name: "Claude Code",
38
+ path: path.join(HOME, ".claude", ".mcp.json"),
39
+ format: "json",
40
+ },
41
+ {
42
+ name: "Gemini CLI",
43
+ path: path.join(HOME, ".gemini", "settings.json"),
44
+ format: "json",
45
+ },
46
+ ];
47
+
48
+ // Temp path patterns that indicate npx/ephemeral installs
49
+ const TEMP_PATTERNS = [
50
+ /[/\\]_npx[/\\]/,
51
+ /[/\\]\.npm[/\\]_npx[/\\]/,
52
+ /^\/var\/folders\//,
53
+ /^\/tmp\//,
54
+ /^\/private\/var\/folders\//,
55
+ /[/\\]Temp[/\\]/i,
56
+ /^C:\\Users\\[^\\]+\\AppData\\Local\\Temp\\/i,
57
+ ];
58
+
59
+ // Log locations to scan for MODULE_NOT_FOUND
60
+ const LOG_LOCATIONS = [
61
+ path.join(HOME, ".codex", "log"),
62
+ path.join(HOME, ".local", "lib", "mcp-deliberation", "runtime.log"),
63
+ ];
64
+
65
+ // ── TOML parser (minimal, mcp_servers only) ────────────────────
66
+
67
+ function parseMcpServersFromToml(content) {
68
+ const servers = {};
69
+ const lines = content.split("\n");
70
+ let currentServer = null;
71
+
72
+ for (const raw of lines) {
73
+ const line = raw.trim();
74
+
75
+ // Match [mcp_servers.NAME] or [mcp_servers.NAME.env]
76
+ const sectionMatch = line.match(/^\[mcp_servers\.([^\].]+)\]$/);
77
+ if (sectionMatch) {
78
+ currentServer = sectionMatch[1];
79
+ if (!servers[currentServer]) {
80
+ servers[currentServer] = { command: null, args: [] };
81
+ }
82
+ continue;
83
+ }
84
+
85
+ // Skip sub-sections like [mcp_servers.NAME.env]
86
+ if (/^\[/.test(line)) {
87
+ if (!/^\[mcp_servers\./.test(line)) currentServer = null;
88
+ continue;
89
+ }
90
+
91
+ if (!currentServer) continue;
92
+
93
+ // command = "node"
94
+ const cmdMatch = line.match(/^command\s*=\s*"([^"]+)"/);
95
+ if (cmdMatch) {
96
+ servers[currentServer].command = cmdMatch[1];
97
+ continue;
98
+ }
99
+
100
+ // args = ["path1", "path2"]
101
+ const argsMatch = line.match(/^args\s*=\s*\[([^\]]*)\]/);
102
+ if (argsMatch) {
103
+ servers[currentServer].args = argsMatch[1]
104
+ .split(",")
105
+ .map((s) => s.trim().replace(/^"|"$/g, ""))
106
+ .filter(Boolean);
107
+ }
108
+ }
109
+
110
+ return servers;
111
+ }
112
+
113
+ // ── JSON parser ────────────────────────────────────────────────
114
+
115
+ function parseMcpServersFromJson(content) {
116
+ try {
117
+ const parsed = JSON.parse(content);
118
+ return parsed.mcpServers || {};
119
+ } catch {
120
+ return null; // parse error
121
+ }
122
+ }
123
+
124
+ // ── Path resolution ────────────────────────────────────────────
125
+
126
+ function resolveServerPaths(server) {
127
+ const paths = [];
128
+
129
+ // Extract paths from args
130
+ if (Array.isArray(server.args)) {
131
+ for (const arg of server.args) {
132
+ // Skip flags and short args
133
+ if (arg.startsWith("-") || arg.startsWith("@")) continue;
134
+ // Skip bare package names (no path separator)
135
+ if (!arg.includes("/") && !arg.includes("\\")) continue;
136
+ // Expand ~
137
+ const resolved = arg.startsWith("~")
138
+ ? path.join(HOME, arg.slice(1))
139
+ : arg;
140
+ paths.push(resolved);
141
+ }
142
+ }
143
+
144
+ return paths;
145
+ }
146
+
147
+ // ── Checks ─────────────────────────────────────────────────────
148
+
149
+ function checkPathExists(p) {
150
+ try {
151
+ return fs.existsSync(p);
152
+ } catch {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ function isTempPath(p) {
158
+ return TEMP_PATTERNS.some((re) => re.test(p));
159
+ }
160
+
161
+ function isNpxCommand(server) {
162
+ return server.command === "npx";
163
+ }
164
+
165
+ function scanLogsForModuleNotFound(logPaths, limit = 50) {
166
+ const findings = [];
167
+
168
+ for (const logPath of logPaths) {
169
+ if (!fs.existsSync(logPath)) continue;
170
+
171
+ // For directories, scan recent files
172
+ const stat = fs.statSync(logPath);
173
+ const files = stat.isDirectory()
174
+ ? fs
175
+ .readdirSync(logPath)
176
+ .filter((f) => f.endsWith(".log") || f.endsWith(".txt") || f.endsWith(".jsonl"))
177
+ .map((f) => path.join(logPath, f))
178
+ : [logPath];
179
+
180
+ for (const file of files) {
181
+ try {
182
+ const content = fs.readFileSync(file, "utf-8");
183
+ const lines = content.split("\n");
184
+ // Scan last N lines for "Cannot find module" (the actionable line)
185
+ // Skip bare "code: 'MODULE_NOT_FOUND'" lines — they duplicate the real error
186
+ const recent = lines.slice(-limit);
187
+ for (const line of recent) {
188
+ const cfm = line.match(/Cannot find module '([^']+)'/);
189
+ if (cfm) {
190
+ findings.push({
191
+ file: path.basename(file),
192
+ module: cfm[1],
193
+ line: line.trim().slice(0, 200),
194
+ });
195
+ }
196
+ }
197
+ } catch {
198
+ /* skip unreadable files */
199
+ }
200
+ }
201
+ }
202
+
203
+ return findings;
204
+ }
205
+
206
+ // ── Fix suggestions ────────────────────────────────────────────
207
+
208
+ function suggestFix(serverName, server, issue) {
209
+ switch (issue) {
210
+ case "path_missing":
211
+ if (serverName === "deliberation" || serverName === "mcp-deliberation") {
212
+ return `npx @dmsdc-ai/aigentry-deliberation install`;
213
+ }
214
+ if (serverName.includes("brain") || serverName.includes("aigentry-brain")) {
215
+ return `npx @dmsdc-ai/aigentry-brain install`;
216
+ }
217
+ if (isNpxCommand(server)) {
218
+ const pkg = (server.args || []).find((a) => a.startsWith("@") || !a.startsWith("-"));
219
+ return pkg ? `npx -y ${pkg} # npx 서버는 자동 설치됩니다` : null;
220
+ }
221
+ return `# ${serverName}: 서버 파일을 올바른 경로에 설치하세요`;
222
+
223
+ case "temp_path": {
224
+ const tempArg = (server.args || []).find((a) => isTempPath(a));
225
+ if (serverName === "deliberation" || serverName === "mcp-deliberation") {
226
+ return `npx @dmsdc-ai/aigentry-deliberation install # 영구 경로로 재설치`;
227
+ }
228
+ if (serverName.includes("brain") || serverName.includes("aigentry-brain")) {
229
+ return `npx @dmsdc-ai/aigentry-brain install # 영구 경로로 재설치`;
230
+ }
231
+ return `# ${serverName}: 임시 경로(${tempArg}) → 영구 경로로 변경 필요`;
232
+ }
233
+
234
+ case "module_not_found":
235
+ if (isNpxCommand(server)) {
236
+ const pkg = (server.args || []).find((a) => a.startsWith("@") || !a.startsWith("-"));
237
+ return pkg ? `npm install -g ${pkg}` : `# ${serverName}: 패키지를 전역 설치하세요`;
238
+ }
239
+ return `cd $(dirname "${(server.args || [])[0] || ""}") && npm install`;
240
+
241
+ default:
242
+ return null;
243
+ }
244
+ }
245
+
246
+ // ── Main diagnostic ────────────────────────────────────────────
247
+
248
+ function runDiagnostics() {
249
+ console.log("\n🩺 MCP Connection Doctor — aigentry-deliberation\n");
250
+ console.log("━".repeat(60));
251
+
252
+ let totalServers = 0;
253
+ let totalIssues = 0;
254
+ const allIssues = [];
255
+
256
+ // ── Phase 1: Config file scanning ──
257
+
258
+ for (const cfg of CONFIGS) {
259
+ console.log(`\n📋 ${cfg.name}: ${cfg.path}`);
260
+
261
+ if (!fs.existsSync(cfg.path)) {
262
+ console.log(" ⚠️ 설정 파일 없음 (스킵)");
263
+ continue;
264
+ }
265
+
266
+ const content = fs.readFileSync(cfg.path, "utf-8");
267
+ let servers;
268
+
269
+ if (cfg.format === "toml") {
270
+ servers = parseMcpServersFromToml(content);
271
+ } else {
272
+ servers = parseMcpServersFromJson(content);
273
+ if (servers === null) {
274
+ console.log(" ❌ JSON 파싱 실패");
275
+ totalIssues++;
276
+ allIssues.push({
277
+ config: cfg.name,
278
+ server: "(전체)",
279
+ issue: "JSON 파싱 실패",
280
+ fix: `# ${cfg.path} 파일의 JSON 문법을 확인하세요`,
281
+ });
282
+ continue;
283
+ }
284
+ }
285
+
286
+ const serverEntries = Object.entries(servers);
287
+ if (serverEntries.length === 0) {
288
+ console.log(" (등록된 MCP 서버 없음)");
289
+ continue;
290
+ }
291
+
292
+ for (const [name, server] of serverEntries) {
293
+ totalServers++;
294
+ const issues = [];
295
+ const filePaths = resolveServerPaths(server);
296
+
297
+ // Check 1: npx command (volatile but expected)
298
+ if (isNpxCommand(server)) {
299
+ console.log(` ✅ ${name}: npx (자동 설치)`);
300
+ continue;
301
+ }
302
+
303
+ // Check paths: temp path (root cause) takes priority over missing
304
+ for (const p of filePaths) {
305
+ if (isTempPath(p)) {
306
+ issues.push({ type: "temp_path", detail: p });
307
+ } else if (!checkPathExists(p)) {
308
+ issues.push({ type: "path_missing", detail: p });
309
+ }
310
+ }
311
+
312
+ if (issues.length === 0) {
313
+ console.log(` ✅ ${name}: 정상`);
314
+ } else {
315
+ for (const issue of issues) {
316
+ totalIssues++;
317
+ const fix = suggestFix(name, server, issue.type);
318
+ const label =
319
+ issue.type === "path_missing"
320
+ ? "❌ 경로 없음"
321
+ : "⚠️ 임시 경로";
322
+ console.log(` ${label}: ${name}`);
323
+ console.log(` 경로: ${issue.detail}`);
324
+ if (fix) console.log(` 복구: ${fix}`);
325
+ allIssues.push({
326
+ config: cfg.name,
327
+ server: name,
328
+ issue: label,
329
+ path: issue.detail,
330
+ fix,
331
+ });
332
+ }
333
+ }
334
+ }
335
+ }
336
+
337
+ // ── Phase 2: MODULE_NOT_FOUND log scan ──
338
+
339
+ console.log(`\n📜 로그 스캔 (MODULE_NOT_FOUND)`);
340
+ const moduleFindings = scanLogsForModuleNotFound(LOG_LOCATIONS);
341
+
342
+ if (moduleFindings.length === 0) {
343
+ console.log(" ✅ MODULE_NOT_FOUND 흔적 없음");
344
+ } else {
345
+ // Deduplicate by module path
346
+ const seen = new Set();
347
+ for (const f of moduleFindings) {
348
+ if (seen.has(f.module)) continue;
349
+ seen.add(f.module);
350
+ totalIssues++;
351
+ console.log(` ❌ ${f.file}: Cannot find module '${f.module}'`);
352
+
353
+ // Generate smart fix based on module path
354
+ let fix;
355
+ const mod = f.module;
356
+ if (isTempPath(mod)) {
357
+ if (mod.includes("aigentry-brain") || mod.includes("brain")) {
358
+ fix = `npx @dmsdc-ai/aigentry-brain install # 임시 경로 → 영구 설치`;
359
+ } else if (mod.includes("deliberation") || mod.includes("mcp-deliberation")) {
360
+ fix = `npx @dmsdc-ai/aigentry-deliberation install # 임시 경로 → 영구 설치`;
361
+ } else {
362
+ fix = `# 임시 경로(${mod}) — MCP 설정에서 영구 경로로 변경 필요`;
363
+ }
364
+ } else {
365
+ fix = `# ${mod} 파일이 존재하지 않음 — 해당 MCP 서버 재설치 필요`;
366
+ }
367
+
368
+ console.log(` 복구: ${fix}`);
369
+ allIssues.push({ config: "logs", server: mod, issue: "MODULE_NOT_FOUND", fix });
370
+ }
371
+ }
372
+
373
+ // ── Phase 3: deliberation self-check ──
374
+
375
+ console.log(`\n🔍 deliberation 자체 점검`);
376
+ const installDir = IS_WIN
377
+ ? path.join(
378
+ process.env.LOCALAPPDATA ||
379
+ path.join(HOME, "AppData", "Local"),
380
+ "mcp-deliberation"
381
+ )
382
+ : path.join(HOME, ".local", "lib", "mcp-deliberation");
383
+
384
+ const selfPath = path.join(installDir, "index.js");
385
+ if (checkPathExists(selfPath)) {
386
+ console.log(` ✅ 서버 파일: ${selfPath}`);
387
+ } else {
388
+ totalIssues++;
389
+ console.log(` ❌ 서버 파일 없음: ${selfPath}`);
390
+ console.log(` 복구: npx @dmsdc-ai/aigentry-deliberation install`);
391
+ }
392
+
393
+ // Check node_modules
394
+ const nodeModules = path.join(installDir, "node_modules");
395
+ if (checkPathExists(nodeModules)) {
396
+ console.log(` ✅ node_modules: 설치됨`);
397
+ } else {
398
+ totalIssues++;
399
+ console.log(` ❌ node_modules 없음`);
400
+ console.log(` 복구: cd ${installDir} && npm install`);
401
+ }
402
+
403
+ // Syntax check
404
+ try {
405
+ execSync(`node --check "${selfPath}"`, { stdio: "pipe", timeout: 5000 });
406
+ console.log(` ✅ 문법 검증: 통과`);
407
+ } catch {
408
+ totalIssues++;
409
+ console.log(` ❌ 문법 오류 감지`);
410
+ console.log(` 복구: npx @dmsdc-ai/aigentry-deliberation install`);
411
+ }
412
+
413
+ // ── Summary ──
414
+
415
+ console.log("\n" + "━".repeat(60));
416
+ if (totalIssues === 0) {
417
+ console.log(`\n✅ 전체 정상 — ${totalServers}개 MCP 서버 점검 완료\n`);
418
+ } else {
419
+ console.log(`\n❌ ${totalIssues}개 문제 발견 (${totalServers}개 서버 점검)\n`);
420
+
421
+ if (allIssues.length > 0) {
422
+ console.log("📌 즉시 복구 커맨드:\n");
423
+ const fixSet = new Set();
424
+ for (const issue of allIssues) {
425
+ if (issue.fix && !fixSet.has(issue.fix)) {
426
+ fixSet.add(issue.fix);
427
+ console.log(` ${issue.fix}`);
428
+ }
429
+ }
430
+ console.log();
431
+ }
432
+ }
433
+
434
+ return totalIssues === 0 ? 0 : 1;
435
+ }
436
+
437
+ // ── Entry point ────────────────────────────────────────────────
438
+
439
+ const exitCode = runDiagnostics();
440
+ process.exit(exitCode);
package/index.js CHANGED
@@ -2515,19 +2515,11 @@ server.tool(
2515
2515
  nonLiveCli.push(s);
2516
2516
  }
2517
2517
  }
2518
+ // Warn but proceed — user explicitly selected these speakers.
2519
+ // cli_auto_turn will handle runtime errors per-turn.
2520
+ let detectWarningLiveness = "";
2518
2521
  if (nonLiveCli.length > 0) {
2519
- const liveSpeakers = speakerOrder.filter(s => !nonLiveCli.includes(s));
2520
- const liveSnapshot = await collectSpeakerCandidates({ include_cli: true, include_browser: true });
2521
- const candidateText = formatSpeakerCandidatesReport(liveSnapshot);
2522
- const liveList = liveSpeakers.length > 0
2523
- ? `\n\n실행 가능한 스피커만으로 시작하려면:\ndeliberation_start(topic: "${topic.slice(0, 50)}...", speakers: ${JSON.stringify(liveSpeakers)})`
2524
- : "";
2525
- return {
2526
- content: [{
2527
- type: "text",
2528
- text: `⚠️ 일부 CLI 스피커가 현재 실행 불가합니다:\n${nonLiveCli.map(s => ` - \`${s}\` ❌`).join("\n")}\n\n실행 가능: ${liveSpeakers.length > 0 ? liveSpeakers.map(s => `\`${s}\``).join(", ") : "(없음)"}\n\n${candidateText}${liveList}\n\n실행 불가 원인: CLI가 설치되지 않았거나, 중첩 세션 제약, 또는 인증 만료일 수 있습니다.`,
2529
- }],
2530
- };
2522
+ detectWarningLiveness = `\n\n⚠️ 일부 CLI가 현재 실행 불가 상태이지만 사용자 선택을 존중하여 진행합니다:\n${nonLiveCli.map(s => ` - \`${s}\` ❌`).join("\n")}\n턴 진행 시 CLI 실행을 재시도합니다. 실패 시 해당 턴에서 오류가 보고됩니다.`;
2531
2523
  }
2532
2524
 
2533
2525
  const participantMode = hasManualSpeakers
@@ -2622,7 +2614,7 @@ server.tool(
2622
2614
  return {
2623
2615
  content: [{
2624
2616
  type: "text",
2625
- text: `✅ Deliberation 시작! Forum이 생성되었습니다.\n\n**세션:** ${sessionId}\n**프로젝트:** ${state.project}\n**주제:** ${topic}\n**라운드:** ${rounds}\n**발언 순서:** ${state.ordering_strategy || "cyclic"}\n**참가자 구성:** ${participantMode}\n**참가자:** ${speakerOrder.join(", ")}\n**첫 발언:** ${state.current_speaker}\n**동시 진행 세션:** ${active.length}개${terminalMsg}${detectWarning}\n\n**역할 배정:**${role_preset ? ` (프리셋: ${role_preset})` : ""}\n${speakerOrder.map(s => ` - \`${s}\`: ${(state.speaker_roles || {})[s] || "free"}`).join("\n")}\n\n**환경 상태:**\n${formatDegradationReport(state.degradation)}\n\n**Transport 라우팅:**\n${transportSummary}\n\n💡 이후 도구 호출 시 session_id: "${sessionId}" 를 사용하세요.\n📋 Forum 상태 조회: \`deliberation_status(session_id: "${sessionId}")\``,
2617
+ text: `✅ Deliberation 시작! Forum이 생성되었습니다.\n\n**세션:** ${sessionId}\n**프로젝트:** ${state.project}\n**주제:** ${topic}\n**라운드:** ${rounds}\n**발언 순서:** ${state.ordering_strategy || "cyclic"}\n**참가자 구성:** ${participantMode}\n**참가자:** ${speakerOrder.join(", ")}\n**첫 발언:** ${state.current_speaker}\n**동시 진행 세션:** ${active.length}개${terminalMsg}${detectWarning}${detectWarningLiveness}\n\n**역할 배정:**${role_preset ? ` (프리셋: ${role_preset})` : ""}\n${speakerOrder.map(s => ` - \`${s}\`: ${(state.speaker_roles || {})[s] || "free"}`).join("\n")}\n\n**환경 상태:**\n${formatDegradationReport(state.degradation)}\n\n**Transport 라우팅:**\n${transportSummary}\n\n💡 이후 도구 호출 시 session_id: "${sessionId}" 를 사용하세요.\n📋 Forum 상태 조회: \`deliberation_status(session_id: "${sessionId}")\``,
2626
2618
  }],
2627
2619
  };
2628
2620
  })
@@ -3697,6 +3689,23 @@ const __currentFile = new URL(import.meta.url).pathname.replace(/^\/([A-Z]:)/, "
3697
3689
  const __entryFile = process.argv[1] ? path.resolve(process.argv[1]) : null;
3698
3690
  if (__entryFile && path.resolve(__currentFile) === __entryFile) {
3699
3691
  const transport = new StdioServerTransport();
3692
+
3693
+ // ── Gemini CLI compatibility: strip $schema from tool inputSchemas ──
3694
+ // Gemini CLI strictly validates MCP tool schemas and rejects $schema metadata
3695
+ // that zod-to-json-schema adds. Intercept transport.send to patch tools/list responses.
3696
+ const _origSend = transport.send.bind(transport);
3697
+ transport.send = (message) => {
3698
+ if (message.result && Array.isArray(message.result.tools)) {
3699
+ for (const tool of message.result.tools) {
3700
+ if (tool.inputSchema) {
3701
+ delete tool.inputSchema["$schema"];
3702
+ if (!tool.inputSchema.type) tool.inputSchema.type = "object";
3703
+ }
3704
+ }
3705
+ }
3706
+ return _origSend(message);
3707
+ };
3708
+
3700
3709
  await server.connect(transport);
3701
3710
  }
3702
3711
 
package/install.js CHANGED
@@ -10,8 +10,9 @@
10
10
  * What it does:
11
11
  * 1. Copies server files to ~/.local/lib/mcp-deliberation/
12
12
  * 2. Installs npm dependencies
13
- * 3. Registers MCP server in ~/.claude/.mcp.json
14
- * 4. Ready to use next Claude Code session will auto-load
13
+ * 3. Registers MCP server in ~/.claude/.mcp.json (Claude Code)
14
+ * 4. Registers MCP server in ~/.gemini/settings.json (Gemini CLI)
15
+ * 5. Ready to use — next Claude Code or Gemini CLI session will auto-load
15
16
  */
16
17
 
17
18
  import { execSync } from "node:child_process";
@@ -26,6 +27,7 @@ const INSTALL_DIR = IS_WIN
26
27
  ? path.join(process.env.LOCALAPPDATA || path.join(HOME, "AppData", "Local"), "mcp-deliberation")
27
28
  : path.join(HOME, ".local", "lib", "mcp-deliberation");
28
29
  const MCP_CONFIG = path.join(HOME, ".claude", ".mcp.json");
30
+ const GEMINI_CONFIG = path.join(HOME, ".gemini", "settings.json");
29
31
 
30
32
  /** Normalize path to forward slashes for JSON config (Windows backslash → forward slash) */
31
33
  function toForwardSlash(p) {
@@ -38,6 +40,7 @@ const FILES_TO_COPY = [
38
40
  "browser-control-port.js",
39
41
  "degradation-state-machine.js",
40
42
  "model-router.js",
43
+ "doctor.js",
41
44
  "session-monitor.sh",
42
45
  "session-monitor-win.js",
43
46
  "package.json",
@@ -137,7 +140,34 @@ function install() {
137
140
  ? " → 기존 등록 업데이트 완료"
138
141
  : " → 새로 등록 완료");
139
142
 
140
- // Step 5: Make session-monitor.sh executable
143
+ // Step 5: Register Gemini CLI MCP server
144
+ log("🔧 Gemini CLI MCP 서버 등록 시도...");
145
+ const geminiDir = path.join(HOME, ".gemini");
146
+ if (!fs.existsSync(geminiDir)) fs.mkdirSync(geminiDir, { recursive: true });
147
+
148
+ let geminiConfig = {};
149
+ if (fs.existsSync(GEMINI_CONFIG)) {
150
+ try {
151
+ geminiConfig = JSON.parse(fs.readFileSync(GEMINI_CONFIG, "utf-8"));
152
+ } catch {
153
+ geminiConfig = {};
154
+ }
155
+ }
156
+
157
+ if (!geminiConfig.mcpServers) geminiConfig.mcpServers = {};
158
+
159
+ const geminiAlreadyRegistered = !!geminiConfig.mcpServers.deliberation;
160
+ geminiConfig.mcpServers.deliberation = {
161
+ command: "node",
162
+ args: [toForwardSlash(path.join(INSTALL_DIR, "index.js"))],
163
+ };
164
+
165
+ fs.writeFileSync(GEMINI_CONFIG, JSON.stringify(geminiConfig, null, 2));
166
+ log(geminiAlreadyRegistered
167
+ ? " → Gemini CLI 기존 등록 업데이트 완료"
168
+ : " → Gemini CLI 새로 등록 완료");
169
+
170
+ // Step 6: Make session-monitor.sh executable
141
171
  const monitorScript = path.join(INSTALL_DIR, "session-monitor.sh");
142
172
  if (fs.existsSync(monitorScript)) {
143
173
  try {
@@ -154,7 +184,7 @@ function install() {
154
184
  // Done
155
185
  console.log("\n✅ 설치 완료!\n");
156
186
  console.log(" 다음 단계:");
157
- console.log(" 1. Claude Code 세션을 재시작하세요");
187
+ console.log(" 1. Claude Code 또는 Gemini CLI 세션을 재시작하세요");
158
188
  console.log(" 2. \"토론 시작해\" 또는 deliberation_start(topic: \"...\") 호출");
159
189
  console.log(" 3. 첫 사용 시 온보딩 위저드가 기본 설정을 안내합니다\n");
160
190
  }
@@ -175,18 +205,31 @@ Options:
175
205
 
176
206
  설치 경로: ${INSTALL_DIR}
177
207
  MCP 설정: ${MCP_CONFIG}
208
+ Gemini: ${GEMINI_CONFIG}
178
209
  `);
179
210
  } else if (args.includes("--uninstall") || args.includes("uninstall")) {
180
211
  console.log("\n🗑️ Deliberation MCP Server 제거\n");
181
212
 
182
- // Remove from MCP config
213
+ // Remove from Claude MCP config
183
214
  if (fs.existsSync(MCP_CONFIG)) {
184
215
  try {
185
216
  const mcpConfig = JSON.parse(fs.readFileSync(MCP_CONFIG, "utf-8"));
186
217
  if (mcpConfig.mcpServers?.deliberation) {
187
218
  delete mcpConfig.mcpServers.deliberation;
188
219
  fs.writeFileSync(MCP_CONFIG, JSON.stringify(mcpConfig, null, 2));
189
- log("MCP 등록 해제 완료");
220
+ log("Claude Code MCP 등록 해제 완료");
221
+ }
222
+ } catch { /* ignore */ }
223
+ }
224
+
225
+ // Remove from Gemini CLI config
226
+ if (fs.existsSync(GEMINI_CONFIG)) {
227
+ try {
228
+ const geminiConfig = JSON.parse(fs.readFileSync(GEMINI_CONFIG, "utf-8"));
229
+ if (geminiConfig.mcpServers?.deliberation) {
230
+ delete geminiConfig.mcpServers.deliberation;
231
+ fs.writeFileSync(GEMINI_CONFIG, JSON.stringify(geminiConfig, null, 2));
232
+ log("Gemini CLI MCP 등록 해제 완료");
190
233
  }
191
234
  } catch { /* ignore */ }
192
235
  }
@@ -197,7 +240,7 @@ MCP 설정: ${MCP_CONFIG}
197
240
  log("설치 디렉토리 삭제 완료");
198
241
  }
199
242
 
200
- console.log("\n✅ 제거 완료. Claude Code를 재시작하세요.\n");
243
+ console.log("\n✅ 제거 완료. Claude Code / Gemini CLI를 재시작하세요.\n");
201
244
  } else {
202
245
  install();
203
246
  }
@@ -0,0 +1,224 @@
1
+ // model-router.js
2
+ // Dynamic model selection based on deliberation prompt context
3
+
4
+ const CATEGORY_KEYWORDS = {
5
+ reasoning: [
6
+ 'analyze', 'analysis', 'debug', 'debugging', 'complex', 'logic', 'reasoning',
7
+ 'problem', 'solve', 'evaluate', 'assess', 'critique', 'argument', 'inference',
8
+ 'contradiction', 'flaw', 'why', 'explain', 'cause', 'effect', 'implication',
9
+ 'consequence', 'tradeoff', 'compare', 'pros', 'cons', 'decision', 'strategy',
10
+ 'architecture', 'design pattern', 'refactor', 'optimize', 'performance',
11
+ ],
12
+ coding: [
13
+ 'code', 'implement', 'function', 'class', 'module', 'api', 'library',
14
+ 'algorithm', 'data structure', 'typescript', 'javascript', 'python', 'rust',
15
+ 'build', 'deploy', 'test', 'unit test', 'integration', 'bug', 'fix', 'error',
16
+ 'syntax', 'runtime', 'compile', 'script', 'program', 'software', 'feature',
17
+ 'endpoint', 'schema', 'database', 'query', 'sql', 'migration', 'lint',
18
+ ],
19
+ creative: [
20
+ 'write', 'writing', 'design', 'brainstorm', 'idea', 'creative', 'story',
21
+ 'narrative', 'style', 'tone', 'voice', 'draft', 'outline', 'concept',
22
+ 'vision', 'imagine', 'suggest', 'propose', 'generate', 'name', 'brand',
23
+ 'ui', 'ux', 'interface', 'experience', 'aesthetic', 'layout', 'visual',
24
+ ],
25
+ research: [
26
+ 'research', 'find', 'search', 'fact', 'data', 'statistic', 'source',
27
+ 'reference', 'study', 'survey', 'report', 'paper', 'documentation', 'docs',
28
+ 'compare', 'benchmark', 'review', 'overview', 'summary', 'what is', 'who is',
29
+ 'history', 'background', 'context', 'information', 'knowledge',
30
+ ],
31
+ simple: [
32
+ 'yes', 'no', 'confirm', 'ok', 'okay', 'agree', 'disagree', 'correct',
33
+ 'hello', 'hi', 'thanks', 'thank you', 'sure', 'got it', 'understood',
34
+ 'clarify', 'quick', 'brief', 'simple', 'basic',
35
+ ],
36
+ };
37
+
38
+ const COMPLEXITY_KEYWORDS = {
39
+ high: [
40
+ 'complex', 'complicated', 'difficult', 'challenging', 'sophisticated',
41
+ 'advanced', 'deep', 'thorough', 'comprehensive', 'exhaustive', 'detailed',
42
+ 'multi-step', 'multi-faceted', 'nuanced', 'ambiguous', 'architecture',
43
+ 'system', 'scalable', 'distributed', 'concurrent', 'critical', 'production',
44
+ ],
45
+ low: [
46
+ 'simple', 'basic', 'quick', 'brief', 'short', 'easy', 'trivial',
47
+ 'straightforward', 'obvious', 'clear', 'confirm', 'yes', 'no', 'agree',
48
+ 'ok', 'sure', 'hello', 'hi',
49
+ ],
50
+ };
51
+
52
+ const ROLE_KEYWORDS = {
53
+ critic: 'reasoning',
54
+ implementer: 'coding',
55
+ mediator: 'creative',
56
+ researcher: 'research',
57
+ };
58
+
59
+ /**
60
+ * Count keyword matches in text for a given keyword list.
61
+ */
62
+ function countMatches(text, keywords) {
63
+ const lower = text.toLowerCase();
64
+ return keywords.reduce((count, kw) => {
65
+ return count + (lower.includes(kw.toLowerCase()) ? 1 : 0);
66
+ }, 0);
67
+ }
68
+
69
+ /**
70
+ * Analyze deliberation context to determine category and complexity.
71
+ * @param {string} topic - The deliberation topic
72
+ * @param {string} recentLog - Recent log text from deliberation
73
+ * @param {string} role - The speaker's role
74
+ * @returns {{ category: string, complexity: string }}
75
+ */
76
+ export function analyzePromptContext(topic, recentLog, role) {
77
+ const text = `${topic} ${recentLog}`;
78
+
79
+ // Role-based category hint
80
+ let roleCategory = null;
81
+ if (role) {
82
+ const lowerRole = role.toLowerCase();
83
+ for (const [keyword, category] of Object.entries(ROLE_KEYWORDS)) {
84
+ if (lowerRole.includes(keyword)) {
85
+ roleCategory = category;
86
+ break;
87
+ }
88
+ }
89
+ }
90
+
91
+ // Keyword-based category scoring
92
+ const scores = {};
93
+ for (const [category, keywords] of Object.entries(CATEGORY_KEYWORDS)) {
94
+ scores[category] = countMatches(text, keywords);
95
+ }
96
+
97
+ // Find top category by keyword score
98
+ let topCategory = 'simple';
99
+ let topScore = 0;
100
+ for (const [category, score] of Object.entries(scores)) {
101
+ if (score > topScore) {
102
+ topScore = score;
103
+ topCategory = category;
104
+ }
105
+ }
106
+
107
+ // Role hint takes precedence if no strong keyword signal
108
+ const category = topScore >= 2 ? topCategory : (roleCategory || topCategory);
109
+
110
+ // Complexity detection
111
+ const highCount = countMatches(text, COMPLEXITY_KEYWORDS.high);
112
+ const lowCount = countMatches(text, COMPLEXITY_KEYWORDS.low);
113
+
114
+ let complexity;
115
+ if (highCount > lowCount && highCount >= 1) {
116
+ complexity = 'high';
117
+ } else if (lowCount > 0 && highCount === 0) {
118
+ complexity = 'low';
119
+ } else {
120
+ complexity = 'medium';
121
+ }
122
+
123
+ return { category, complexity };
124
+ }
125
+
126
+ /**
127
+ * Select the optimal model for a given provider based on category and complexity.
128
+ * @param {string} provider - The AI provider identifier
129
+ * @param {string} category - Prompt category
130
+ * @param {string} complexity - Prompt complexity
131
+ * @returns {{ model: string, reason: string }}
132
+ */
133
+ export function selectModelForProvider(provider, category, complexity) {
134
+ const isHighReasoning = category === 'reasoning' || complexity === 'high';
135
+ const isSimple = complexity === 'low' || category === 'simple';
136
+ const isCoding = category === 'coding';
137
+
138
+ switch (provider) {
139
+ case 'chatgpt': {
140
+ if (isHighReasoning) return { model: 'o3', reason: 'High-complexity reasoning task' };
141
+ if (isCoding) return { model: 'o4-mini', reason: 'Coding/implementation task' };
142
+ if (isSimple) return { model: 'gpt-4o-mini', reason: 'Simple task, cost-efficient model' };
143
+ return { model: 'gpt-4o', reason: 'Creative or medium-complexity task' };
144
+ }
145
+
146
+ case 'claude': {
147
+ if (isHighReasoning) return { model: 'opus', reason: 'High-complexity reasoning task' };
148
+ if (isSimple) return { model: 'haiku', reason: 'Simple task, fast and cost-efficient' };
149
+ return { model: 'sonnet', reason: 'Coding or medium-complexity task' };
150
+ }
151
+
152
+ case 'gemini': {
153
+ if (isHighReasoning || complexity === 'high') return { model: '2.5 Pro', reason: 'High-complexity or reasoning task' };
154
+ if (isSimple) return { model: '2.0 Flash', reason: 'Simple task, fast response' };
155
+ return { model: '2.5 Flash', reason: 'Medium-complexity task' };
156
+ }
157
+
158
+ case 'deepseek': {
159
+ if (category === 'reasoning') return { model: 'DeepSeek-R1', reason: 'Reasoning-focused task' };
160
+ return { model: 'DeepSeek-V3', reason: 'General task' };
161
+ }
162
+
163
+ case 'grok': {
164
+ if (complexity === 'high' || isHighReasoning) return { model: 'grok-3', reason: 'High-complexity task' };
165
+ return { model: 'grok-3-mini', reason: 'Simple task' };
166
+ }
167
+
168
+ case 'mistral': {
169
+ if (complexity === 'high' || isHighReasoning) return { model: 'Mistral Large', reason: 'High-complexity task' };
170
+ return { model: 'Mistral Small', reason: 'Simple task' };
171
+ }
172
+
173
+ case 'poe': {
174
+ if (complexity === 'high' || isHighReasoning) return { model: 'Claude-3.5-Sonnet', reason: 'High-complexity task' };
175
+ if (isSimple) return { model: 'Claude-3-Haiku', reason: 'Simple task' };
176
+ return { model: 'GPT-4o', reason: 'Medium-complexity task' };
177
+ }
178
+
179
+ case 'qwen': {
180
+ if (complexity === 'high' || isHighReasoning) return { model: 'Qwen-Max', reason: 'High-complexity task' };
181
+ return { model: 'Qwen-Plus', reason: 'Simple task' };
182
+ }
183
+
184
+ case 'huggingchat': {
185
+ if (complexity === 'high' || isHighReasoning) return { model: 'Qwen/QwQ-32B', reason: 'High-complexity task' };
186
+ return { model: 'meta-llama/Llama-3.3-70B', reason: 'Simple task' };
187
+ }
188
+
189
+ case 'copilot': {
190
+ return { model: 'GPT-4o', reason: 'Copilot uses GPT-4o (no model selection)' };
191
+ }
192
+
193
+ case 'perplexity': {
194
+ return { model: 'default', reason: 'Perplexity uses default model (no model selection)' };
195
+ }
196
+
197
+ default: {
198
+ return { model: 'default', reason: `Unknown provider: ${provider}` };
199
+ }
200
+ }
201
+ }
202
+
203
+ /**
204
+ * High-level function: analyze state and return optimal model selection for a turn.
205
+ * @param {object} state - Deliberation state with topic, log, speaker_roles
206
+ * @param {string} speaker - The current speaker identifier
207
+ * @param {string} provider - The AI provider to use
208
+ * @returns {{ model: string, category: string, complexity: string, reason: string }}
209
+ */
210
+ export function getModelSelectionForTurn(state, speaker, provider) {
211
+ const topic = state.topic || '';
212
+ const role = (state.speaker_roles && state.speaker_roles[speaker]) || '';
213
+
214
+ // Use recent log entries (last 5) for context analysis
215
+ const recentEntries = Array.isArray(state.log) ? state.log.slice(-5) : [];
216
+ const recentLog = recentEntries
217
+ .map(entry => (typeof entry === 'string' ? entry : JSON.stringify(entry)))
218
+ .join(' ');
219
+
220
+ const { category, complexity } = analyzePromptContext(topic, recentLog, role);
221
+ const { model, reason } = selectModelForProvider(provider, category, complexity);
222
+
223
+ return { model, category, complexity, reason };
224
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dmsdc-ai/aigentry-deliberation",
3
- "version": "0.0.18",
3
+ "version": "0.0.20",
4
4
  "description": "MCP Deliberation Server — Multi-session AI deliberation with smart speaker ordering and persona roles",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -21,14 +21,17 @@
21
21
  "aigentry-deliberation": "index.js",
22
22
  "mcp-deliberation": "index.js",
23
23
  "deliberation-observer": "observer.js",
24
- "deliberation-install": "install.js"
24
+ "deliberation-install": "install.js",
25
+ "deliberation-doctor": "doctor.js"
25
26
  },
26
27
  "engines": {
27
28
  "node": ">=18"
28
29
  },
29
30
  "files": [
30
31
  "index.js",
32
+ "model-router.js",
31
33
  "install.js",
34
+ "doctor.js",
32
35
  "observer.js",
33
36
  "browser-control-port.js",
34
37
  "degradation-state-machine.js",