@dmsdc-ai/aigentry-devkit 0.1.3 → 0.1.5
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/install.ps1 +9 -6
- package/install.sh +66 -10
- package/mcp-servers/deliberation/index.js +490 -155
- package/package.json +1 -1
- package/skills/deliberation/SKILL.md +35 -13
package/install.ps1
CHANGED
|
@@ -116,17 +116,20 @@ Write-Header "5. MCP Registration"
|
|
|
116
116
|
$mcpConfig = Join-Path $ClaudeDir ".mcp.json"
|
|
117
117
|
New-Item -ItemType Directory -Path $ClaudeDir -Force | Out-Null
|
|
118
118
|
|
|
119
|
+
$cfg = $null
|
|
119
120
|
if (Test-Path $mcpConfig) {
|
|
120
121
|
try {
|
|
121
|
-
$
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
122
|
+
$raw = Get-Content $mcpConfig -Raw
|
|
123
|
+
if ($raw) {
|
|
124
|
+
$cfg = $raw | ConvertFrom-Json
|
|
125
|
+
}
|
|
126
|
+
} catch {}
|
|
127
|
+
}
|
|
128
|
+
if (-not $cfg) {
|
|
126
129
|
$cfg = [pscustomobject]@{}
|
|
127
130
|
}
|
|
128
131
|
|
|
129
|
-
if (-not $cfg.PSObject.Properties.
|
|
132
|
+
if (-not ($cfg.PSObject.Properties.Match("mcpServers"))) {
|
|
130
133
|
$cfg | Add-Member -MemberType NoteProperty -Name mcpServers -Value ([pscustomobject]@{})
|
|
131
134
|
}
|
|
132
135
|
|
package/install.sh
CHANGED
|
@@ -220,9 +220,65 @@ else
|
|
|
220
220
|
warn "direnv not found. Skipping .envrc setup."
|
|
221
221
|
fi
|
|
222
222
|
|
|
223
|
-
# ──
|
|
224
|
-
header "7.
|
|
223
|
+
# ── 참가자 CLI 선택 ──
|
|
224
|
+
header "7. Participant CLI Selection"
|
|
225
|
+
|
|
226
|
+
# Detect available CLIs
|
|
227
|
+
AVAILABLE_CLIS=""
|
|
228
|
+
for cli in claude codex gemini qwen chatgpt aider llm opencode cursor; do
|
|
229
|
+
if command -v "$cli" >/dev/null 2>&1; then
|
|
230
|
+
AVAILABLE_CLIS="$AVAILABLE_CLIS $cli"
|
|
231
|
+
fi
|
|
232
|
+
done
|
|
233
|
+
AVAILABLE_CLIS=$(echo "$AVAILABLE_CLIS" | xargs) # trim
|
|
234
|
+
|
|
235
|
+
if [ -z "$AVAILABLE_CLIS" ]; then
|
|
236
|
+
warn "No participant CLIs detected. Install claude, codex, gemini, or other AI CLIs."
|
|
237
|
+
else
|
|
238
|
+
info "Detected CLIs: $AVAILABLE_CLIS"
|
|
239
|
+
echo ""
|
|
240
|
+
|
|
241
|
+
# Interactive selection (skip if --force flag or non-interactive)
|
|
242
|
+
DELIBERATION_CONFIG="$MCP_DEST/config.json"
|
|
243
|
+
if [ -t 0 ] && [ "${FORCE_INSTALL:-}" != "true" ]; then
|
|
244
|
+
echo -e " ${BOLD}Select CLIs to enable for deliberation:${NC}"
|
|
245
|
+
echo ""
|
|
246
|
+
|
|
247
|
+
SELECTED_CLIS=""
|
|
248
|
+
for cli in $AVAILABLE_CLIS; do
|
|
249
|
+
printf " Enable ${CYAN}%-12s${NC} for deliberation? [Y/n] " "$cli"
|
|
250
|
+
read -r answer </dev/tty
|
|
251
|
+
case "$answer" in
|
|
252
|
+
[nN]*) ;;
|
|
253
|
+
*) SELECTED_CLIS="$SELECTED_CLIS $cli" ;;
|
|
254
|
+
esac
|
|
255
|
+
done
|
|
256
|
+
SELECTED_CLIS=$(echo "$SELECTED_CLIS" | xargs)
|
|
257
|
+
|
|
258
|
+
if [ -z "$SELECTED_CLIS" ]; then
|
|
259
|
+
warn "No CLIs selected. All detected CLIs will be available by default."
|
|
260
|
+
SELECTED_CLIS="$AVAILABLE_CLIS"
|
|
261
|
+
fi
|
|
262
|
+
else
|
|
263
|
+
# Non-interactive: enable all detected
|
|
264
|
+
SELECTED_CLIS="$AVAILABLE_CLIS"
|
|
265
|
+
fi
|
|
225
266
|
|
|
267
|
+
info "Enabled CLIs: $SELECTED_CLIS"
|
|
268
|
+
|
|
269
|
+
# Save config
|
|
270
|
+
node -e "
|
|
271
|
+
const fs = require('fs');
|
|
272
|
+
const configPath = '$DELIBERATION_CONFIG';
|
|
273
|
+
let config = {};
|
|
274
|
+
try { config = JSON.parse(fs.readFileSync(configPath, 'utf-8')); } catch {}
|
|
275
|
+
config.enabled_clis = '${SELECTED_CLIS}'.split(/\s+/).filter(Boolean);
|
|
276
|
+
config.updated = new Date().toISOString();
|
|
277
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
278
|
+
" && info "Saved CLI config to $DELIBERATION_CONFIG" || warn "Failed to save CLI config"
|
|
279
|
+
fi
|
|
280
|
+
|
|
281
|
+
# Codex MCP registration (only CLI with native MCP support)
|
|
226
282
|
if command -v codex >/dev/null 2>&1; then
|
|
227
283
|
codex mcp add deliberation -- node "$MCP_DEST/index.js" 2>/dev/null && \
|
|
228
284
|
info "Registered deliberation MCP in Codex" || \
|
|
@@ -233,14 +289,13 @@ if command -v codex >/dev/null 2>&1; then
|
|
|
233
289
|
else
|
|
234
290
|
warn "Codex MCP verification failed. Run manually: codex mcp add deliberation -- node $MCP_DEST/index.js"
|
|
235
291
|
fi
|
|
236
|
-
else
|
|
237
|
-
warn "Codex CLI not found. Skipping Codex integration."
|
|
238
292
|
fi
|
|
239
293
|
|
|
240
294
|
header "8. Cross-platform Notes"
|
|
241
|
-
info "
|
|
295
|
+
info "Supported participant CLIs: claude, codex, gemini, qwen, chatgpt, aider, llm, opencode, cursor"
|
|
296
|
+
info "Manage enabled CLIs anytime: deliberation_cli_config MCP tool"
|
|
242
297
|
info "Browser LLM tab detection: macOS automation + CDP scan (Linux/Windows need browser remote-debugging port)."
|
|
243
|
-
info "
|
|
298
|
+
info "CDP auto-detect upgrades browser speakers to browser_auto for hands-free operation."
|
|
244
299
|
|
|
245
300
|
# ── 완료 ──
|
|
246
301
|
header "Installation Complete!"
|
|
@@ -252,9 +307,10 @@ echo -e " MCP Server: $MCP_DEST"
|
|
|
252
307
|
echo -e " Config: $CLAUDE_DIR"
|
|
253
308
|
echo ""
|
|
254
309
|
echo -e " ${BOLD}Next steps:${NC}"
|
|
255
|
-
echo -e " 1. Restart
|
|
256
|
-
echo -e " 2.
|
|
257
|
-
echo -e " 3.
|
|
258
|
-
echo -e " 4.
|
|
310
|
+
echo -e " 1. Restart CLI processes for MCP changes to take effect"
|
|
311
|
+
echo -e " 2. Modify enabled CLIs anytime via deliberation_cli_config MCP tool"
|
|
312
|
+
echo -e " 3. Add other MCP servers to $MCP_CONFIG as needed"
|
|
313
|
+
echo -e " 4. Configure your HUD in settings.json if not already done"
|
|
314
|
+
echo -e " 5. For Linux/Windows browser scan, launch browser with --remote-debugging-port=9222"
|
|
259
315
|
echo ""
|
|
260
316
|
echo -e " ${CYAN}Enjoy your AI development environment!${NC}"
|
|
@@ -19,9 +19,8 @@
|
|
|
19
19
|
* deliberation_reset 세션 초기화 (session_id 선택적, 없으면 전체)
|
|
20
20
|
* deliberation_speaker_candidates 선택 가능한 스피커 후보(로컬 CLI + 브라우저 LLM 탭) 조회
|
|
21
21
|
* deliberation_browser_llm_tabs 브라우저 LLM 탭 목록 조회
|
|
22
|
-
* deliberation_clipboard_prepare_turn 브라우저 LLM용 턴 프롬프트를 클립보드로 복사
|
|
23
|
-
* deliberation_clipboard_submit_turn 클립보드 텍스트를 현재 턴 응답으로 제출
|
|
24
22
|
* deliberation_browser_auto_turn 브라우저 LLM에 자동으로 턴을 전송하고 응답을 수집 (CDP 기반)
|
|
23
|
+
* deliberation_request_review 코드 리뷰 요청 (CLI 리뷰어 자동 호출, sync/async 모드)
|
|
25
24
|
*/
|
|
26
25
|
|
|
27
26
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
@@ -55,6 +54,22 @@ const DEFAULT_CLI_CANDIDATES = [
|
|
|
55
54
|
"continue",
|
|
56
55
|
];
|
|
57
56
|
const MAX_AUTO_DISCOVERED_SPEAKERS = 12;
|
|
57
|
+
|
|
58
|
+
function loadDeliberationConfig() {
|
|
59
|
+
const configPath = path.join(HOME, ".local", "lib", "mcp-deliberation", "config.json");
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
62
|
+
} catch {
|
|
63
|
+
return {};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function saveDeliberationConfig(config) {
|
|
68
|
+
const configPath = path.join(HOME, ".local", "lib", "mcp-deliberation", "config.json");
|
|
69
|
+
config.updated = new Date().toISOString();
|
|
70
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
71
|
+
}
|
|
72
|
+
|
|
58
73
|
const DEFAULT_BROWSER_APPS = ["Google Chrome", "Brave Browser", "Arc", "Microsoft Edge", "Safari"];
|
|
59
74
|
const DEFAULT_LLM_DOMAINS = [
|
|
60
75
|
"chatgpt.com",
|
|
@@ -241,6 +256,13 @@ function resolveCliCandidates() {
|
|
|
241
256
|
.split(/[,\s]+/)
|
|
242
257
|
.map(v => v.trim())
|
|
243
258
|
.filter(Boolean);
|
|
259
|
+
|
|
260
|
+
// If config has enabled_clis, use that as the primary filter
|
|
261
|
+
const config = loadDeliberationConfig();
|
|
262
|
+
if (Array.isArray(config.enabled_clis) && config.enabled_clis.length > 0) {
|
|
263
|
+
return dedupeSpeakers([...fromEnv, ...config.enabled_clis]);
|
|
264
|
+
}
|
|
265
|
+
|
|
244
266
|
return dedupeSpeakers([...fromEnv, ...DEFAULT_CLI_CANDIDATES]);
|
|
245
267
|
}
|
|
246
268
|
|
|
@@ -578,6 +600,59 @@ async function collectBrowserLlmTabsViaCdp() {
|
|
|
578
600
|
};
|
|
579
601
|
}
|
|
580
602
|
|
|
603
|
+
async function ensureCdpAvailable() {
|
|
604
|
+
const endpoints = resolveCdpEndpoints();
|
|
605
|
+
|
|
606
|
+
// First attempt: try existing CDP endpoints
|
|
607
|
+
for (const endpoint of endpoints) {
|
|
608
|
+
try {
|
|
609
|
+
const payload = await fetchJson(endpoint, 1500);
|
|
610
|
+
if (Array.isArray(payload)) {
|
|
611
|
+
return { available: true, endpoint };
|
|
612
|
+
}
|
|
613
|
+
} catch { /* not reachable */ }
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// If none respond and platform is macOS, try auto-launching Chrome with CDP
|
|
617
|
+
if (process.platform === "darwin") {
|
|
618
|
+
try {
|
|
619
|
+
execFileSync("open", ["-a", "Google Chrome", "--args", "--remote-debugging-port=9222"], {
|
|
620
|
+
timeout: 5000,
|
|
621
|
+
stdio: "ignore",
|
|
622
|
+
});
|
|
623
|
+
} catch {
|
|
624
|
+
return {
|
|
625
|
+
available: false,
|
|
626
|
+
reason: "Chrome 자동 실행에 실패했습니다. Chrome을 수동으로 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// Wait for Chrome to initialize CDP
|
|
631
|
+
sleepMs(2000);
|
|
632
|
+
|
|
633
|
+
// Retry CDP connection after launch
|
|
634
|
+
for (const endpoint of endpoints) {
|
|
635
|
+
try {
|
|
636
|
+
const payload = await fetchJson(endpoint, 2000);
|
|
637
|
+
if (Array.isArray(payload)) {
|
|
638
|
+
return { available: true, endpoint, launched: true };
|
|
639
|
+
}
|
|
640
|
+
} catch { /* still not reachable */ }
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return {
|
|
644
|
+
available: false,
|
|
645
|
+
reason: "Chrome을 실행했지만 CDP에 연결할 수 없습니다. Chrome을 완전히 종료한 후 다시 시도해주세요. (이미 실행 중인 Chrome이 CDP 없이 시작된 경우 재시작 필요)",
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Non-macOS: cannot auto-launch
|
|
650
|
+
return {
|
|
651
|
+
available: false,
|
|
652
|
+
reason: "Chrome CDP를 활성화할 수 없습니다. Chrome을 --remote-debugging-port=9222 옵션과 함께 실행해주세요.",
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
581
656
|
function collectBrowserLlmTabsViaAppleScript() {
|
|
582
657
|
if (process.platform !== "darwin") {
|
|
583
658
|
return { tabs: [], note: "AppleScript 탭 스캔은 macOS에서만 지원됩니다." };
|
|
@@ -588,56 +663,67 @@ function collectBrowserLlmTabsViaAppleScript() {
|
|
|
588
663
|
const domainList = `{${escapedDomains.map(d => `"${d}"`).join(", ")}}`;
|
|
589
664
|
const appList = `{${escapedApps.map(a => `"${a}"`).join(", ")}}`;
|
|
590
665
|
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
666
|
+
// NOTE: Use stdin pipe (`osascript -`) instead of multiple `-e` flags
|
|
667
|
+
// because osascript's `-e` mode silently breaks with nested try/on error blocks.
|
|
668
|
+
// Also wrap dynamic `tell application` with `using terms from` so that
|
|
669
|
+
// Chrome-specific properties like `tabs` resolve via the scripting dictionary.
|
|
670
|
+
// Use ASCII character 9 for tab delimiter because `using terms from`
|
|
671
|
+
// shadows the built-in `tab` constant, turning it into the literal string "tab".
|
|
672
|
+
const scriptText = `set llmDomains to ${domainList}
|
|
673
|
+
set browserApps to ${appList}
|
|
674
|
+
set outText to ""
|
|
675
|
+
set tabChar to ASCII character 9
|
|
676
|
+
tell application "System Events"
|
|
677
|
+
set runningApps to name of every application process
|
|
678
|
+
end tell
|
|
679
|
+
repeat with appName in browserApps
|
|
680
|
+
if runningApps contains (appName as string) then
|
|
681
|
+
try
|
|
682
|
+
if (appName as string) is "Safari" then
|
|
683
|
+
using terms from application "Safari"
|
|
684
|
+
tell application (appName as string)
|
|
685
|
+
repeat with w in windows
|
|
686
|
+
try
|
|
687
|
+
repeat with t in tabs of w
|
|
688
|
+
set u to URL of t as string
|
|
689
|
+
set matched to false
|
|
690
|
+
repeat with d in llmDomains
|
|
691
|
+
if u contains (d as string) then set matched to true
|
|
692
|
+
end repeat
|
|
693
|
+
if matched then set outText to outText & (appName as string) & tabChar & (name of t as string) & tabChar & u & linefeed
|
|
694
|
+
end repeat
|
|
695
|
+
end try
|
|
696
|
+
end repeat
|
|
697
|
+
end tell
|
|
698
|
+
end using terms from
|
|
699
|
+
else
|
|
700
|
+
using terms from application "Google Chrome"
|
|
701
|
+
tell application (appName as string)
|
|
702
|
+
repeat with w in windows
|
|
703
|
+
try
|
|
704
|
+
repeat with t in tabs of w
|
|
705
|
+
set u to URL of t as string
|
|
706
|
+
set matched to false
|
|
707
|
+
repeat with d in llmDomains
|
|
708
|
+
if u contains (d as string) then set matched to true
|
|
709
|
+
end repeat
|
|
710
|
+
if matched then set outText to outText & (appName as string) & tabChar & (title of t as string) & tabChar & u & linefeed
|
|
711
|
+
end repeat
|
|
712
|
+
end try
|
|
713
|
+
end repeat
|
|
714
|
+
end tell
|
|
715
|
+
end using terms from
|
|
716
|
+
end if
|
|
717
|
+
on error errMsg
|
|
718
|
+
set outText to outText & (appName as string) & tabChar & "ERROR" & tabChar & errMsg & linefeed
|
|
719
|
+
end try
|
|
720
|
+
end if
|
|
721
|
+
end repeat
|
|
722
|
+
return outText`;
|
|
638
723
|
|
|
639
724
|
try {
|
|
640
|
-
const raw = execFileSync("osascript",
|
|
725
|
+
const raw = execFileSync("osascript", ["-"], {
|
|
726
|
+
input: scriptText,
|
|
641
727
|
encoding: "utf-8",
|
|
642
728
|
timeout: 8000,
|
|
643
729
|
maxBuffer: 2 * 1024 * 1024,
|
|
@@ -745,8 +831,14 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
|
|
|
745
831
|
|
|
746
832
|
let browserNote = null;
|
|
747
833
|
if (include_browser) {
|
|
834
|
+
// Ensure CDP is available before probing browser tabs
|
|
835
|
+
const cdpStatus = await ensureCdpAvailable();
|
|
836
|
+
if (cdpStatus.launched) {
|
|
837
|
+
browserNote = "Chrome CDP 자동 실행됨 (--remote-debugging-port=9222)";
|
|
838
|
+
}
|
|
839
|
+
|
|
748
840
|
const { tabs, note } = await collectBrowserLlmTabs();
|
|
749
|
-
browserNote = note || null;
|
|
841
|
+
browserNote = browserNote ? `${browserNote} | ${note || ""}`.replace(/ \| $/, "") : (note || null);
|
|
750
842
|
const providerCounts = new Map();
|
|
751
843
|
for (const tab of tabs) {
|
|
752
844
|
const provider = inferLlmProvider(tab.url);
|
|
@@ -924,7 +1016,7 @@ function formatTransportGuidance(transport, state, speaker) {
|
|
|
924
1016
|
case "cli_respond":
|
|
925
1017
|
return `CLI speaker입니다. \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`로 직접 응답하세요.`;
|
|
926
1018
|
case "clipboard":
|
|
927
|
-
return `브라우저 LLM speaker입니다.
|
|
1019
|
+
return `브라우저 LLM speaker입니다. CDP 자동 연결 시도 중... Chrome이 이미 CDP 없이 실행 중이면 재시작이 필요할 수 있습니다.`;
|
|
928
1020
|
case "browser_auto":
|
|
929
1021
|
return `자동 브라우저 speaker입니다. \`deliberation_browser_auto_turn(session_id: "${sid}")\`으로 자동 진행됩니다. CDP를 통해 브라우저 LLM에 직접 입력하고 응답을 읽습니다.`;
|
|
930
1022
|
case "manual":
|
|
@@ -1803,6 +1895,23 @@ server.tool(
|
|
|
1803
1895
|
created: new Date().toISOString(),
|
|
1804
1896
|
updated: new Date().toISOString(),
|
|
1805
1897
|
};
|
|
1898
|
+
|
|
1899
|
+
// Ensure CDP is ready if any speaker requires browser transport
|
|
1900
|
+
const hasBrowserSpeaker = state.participant_profiles.some(
|
|
1901
|
+
p => p.type === "browser" || p.type === "browser_auto"
|
|
1902
|
+
);
|
|
1903
|
+
if (hasBrowserSpeaker) {
|
|
1904
|
+
const cdpReady = await ensureCdpAvailable();
|
|
1905
|
+
if (!cdpReady.available) {
|
|
1906
|
+
return {
|
|
1907
|
+
content: [{
|
|
1908
|
+
type: "text",
|
|
1909
|
+
text: `❌ 브라우저 LLM speaker가 포함되어 있지만 CDP에 연결할 수 없습니다.\n\n${cdpReady.reason}\n\nCDP 연결 후 다시 deliberation_start를 호출하세요.`,
|
|
1910
|
+
}],
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1806
1915
|
withSessionLock(sessionId, () => {
|
|
1807
1916
|
saveSession(state);
|
|
1808
1917
|
});
|
|
@@ -1945,88 +2054,6 @@ server.tool(
|
|
|
1945
2054
|
}
|
|
1946
2055
|
);
|
|
1947
2056
|
|
|
1948
|
-
server.tool(
|
|
1949
|
-
"deliberation_clipboard_prepare_turn",
|
|
1950
|
-
"현재 턴 요청 프롬프트를 생성해 클립보드에 복사합니다. 브라우저 LLM에 붙여넣어 사용하세요.",
|
|
1951
|
-
{
|
|
1952
|
-
session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
|
|
1953
|
-
speaker: z.string().trim().min(1).max(64).optional().describe("대상 speaker (미지정 시 현재 차례)"),
|
|
1954
|
-
prompt: z.string().optional().describe("브라우저 LLM에 추가로 전달할 지시"),
|
|
1955
|
-
include_history_entries: z.number().int().min(0).max(12).default(4).describe("프롬프트에 포함할 최근 로그 개수"),
|
|
1956
|
-
},
|
|
1957
|
-
async ({ session_id, speaker, prompt, include_history_entries }) => {
|
|
1958
|
-
const resolved = resolveSessionId(session_id);
|
|
1959
|
-
if (!resolved) {
|
|
1960
|
-
return { content: [{ type: "text", text: "활성 deliberation이 없습니다." }] };
|
|
1961
|
-
}
|
|
1962
|
-
if (resolved === "MULTIPLE") {
|
|
1963
|
-
return { content: [{ type: "text", text: multipleSessionsError() }] };
|
|
1964
|
-
}
|
|
1965
|
-
|
|
1966
|
-
const state = loadSession(resolved);
|
|
1967
|
-
if (!state || state.status !== "active") {
|
|
1968
|
-
return { content: [{ type: "text", text: `세션 "${resolved}"이 활성 상태가 아닙니다.` }] };
|
|
1969
|
-
}
|
|
1970
|
-
|
|
1971
|
-
const targetSpeaker = normalizeSpeaker(speaker) || normalizeSpeaker(state.current_speaker) || state.speakers[0];
|
|
1972
|
-
if (targetSpeaker !== state.current_speaker) {
|
|
1973
|
-
return {
|
|
1974
|
-
content: [{
|
|
1975
|
-
type: "text",
|
|
1976
|
-
text: `[${state.id}] 지금은 **${state.current_speaker}** 차례입니다. prepare 대상 speaker는 현재 차례와 같아야 합니다.`,
|
|
1977
|
-
}],
|
|
1978
|
-
};
|
|
1979
|
-
}
|
|
1980
|
-
|
|
1981
|
-
const payload = buildClipboardTurnPrompt(state, targetSpeaker, prompt, include_history_entries);
|
|
1982
|
-
try {
|
|
1983
|
-
writeClipboardText(payload);
|
|
1984
|
-
} catch (error) {
|
|
1985
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
1986
|
-
return { content: [{ type: "text", text: `클립보드 복사 실패: ${message}` }] };
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
return {
|
|
1990
|
-
content: [{
|
|
1991
|
-
type: "text",
|
|
1992
|
-
text: `✅ [${state.id}] 턴 프롬프트를 클립보드에 복사했습니다.\n\n**대상 speaker:** ${targetSpeaker}\n**라운드:** ${state.current_round}/${state.max_rounds}\n\n다음 단계:\n1. 브라우저 LLM에 붙여넣고 응답 생성\n2. 응답 본문을 복사\n3. deliberation_clipboard_submit_turn(session_id: "${state.id}", speaker: "${targetSpeaker}") 호출\n\n${PRODUCT_DISCLAIMER}`,
|
|
1993
|
-
}],
|
|
1994
|
-
};
|
|
1995
|
-
}
|
|
1996
|
-
);
|
|
1997
|
-
|
|
1998
|
-
server.tool(
|
|
1999
|
-
"deliberation_clipboard_submit_turn",
|
|
2000
|
-
"클립보드 텍스트(또는 content)를 현재 턴 응답으로 제출합니다.",
|
|
2001
|
-
{
|
|
2002
|
-
session_id: z.string().optional().describe("세션 ID (여러 세션 진행 중이면 필수)"),
|
|
2003
|
-
speaker: z.string().trim().min(1).max(64).describe("응답자 이름"),
|
|
2004
|
-
content: z.string().optional().describe("응답 내용 (미지정 시 클립보드 텍스트 사용)"),
|
|
2005
|
-
trim_content: z.boolean().default(false).describe("응답 앞뒤 공백 제거 여부"),
|
|
2006
|
-
turn_id: z.string().optional().describe("턴 검증 ID"),
|
|
2007
|
-
},
|
|
2008
|
-
safeToolHandler("deliberation_clipboard_submit_turn", async ({ session_id, speaker, content, trim_content, turn_id }) => {
|
|
2009
|
-
let body = content;
|
|
2010
|
-
if (typeof body !== "string") {
|
|
2011
|
-
try {
|
|
2012
|
-
body = readClipboardText();
|
|
2013
|
-
} catch (error) {
|
|
2014
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
2015
|
-
return { content: [{ type: "text", text: `클립보드 읽기 실패: ${message}` }] };
|
|
2016
|
-
}
|
|
2017
|
-
}
|
|
2018
|
-
|
|
2019
|
-
if (trim_content) {
|
|
2020
|
-
body = body.trim();
|
|
2021
|
-
}
|
|
2022
|
-
if (!body || body.trim().length === 0) {
|
|
2023
|
-
return { content: [{ type: "text", text: "제출할 응답이 비어 있습니다. 클립보드 또는 content를 확인하세요." }] };
|
|
2024
|
-
}
|
|
2025
|
-
|
|
2026
|
-
return submitDeliberationTurn({ session_id, speaker, content: body, turn_id, channel_used: "clipboard" });
|
|
2027
|
-
})
|
|
2028
|
-
);
|
|
2029
|
-
|
|
2030
2057
|
server.tool(
|
|
2031
2058
|
"deliberation_route_turn",
|
|
2032
2059
|
"현재 턴의 speaker에 맞는 transport를 자동 결정하고 안내합니다. CLI speaker는 자동 응답 경로, 브라우저 speaker는 클립보드 경로로 라우팅합니다.",
|
|
@@ -2057,18 +2084,6 @@ server.tool(
|
|
|
2057
2084
|
|
|
2058
2085
|
let extra = "";
|
|
2059
2086
|
|
|
2060
|
-
if (transport === "clipboard" && auto_prepare_clipboard) {
|
|
2061
|
-
// 자동으로 클립보드 prepare 실행
|
|
2062
|
-
const payload = buildClipboardTurnPrompt(state, speaker, prompt, include_history_entries);
|
|
2063
|
-
try {
|
|
2064
|
-
writeClipboardText(payload);
|
|
2065
|
-
extra = `\n\n✅ 클립보드에 턴 프롬프트가 자동 복사되었습니다.`;
|
|
2066
|
-
} catch (error) {
|
|
2067
|
-
const message = error instanceof Error ? error.message : "unknown error";
|
|
2068
|
-
extra = `\n\n⚠️ 클립보드 자동 복사 실패: ${message}\n수동으로 deliberation_clipboard_prepare_turn을 호출하세요.`;
|
|
2069
|
-
}
|
|
2070
|
-
}
|
|
2071
|
-
|
|
2072
2087
|
if (transport === "browser_auto") {
|
|
2073
2088
|
// Auto-execute browser_auto_turn
|
|
2074
2089
|
try {
|
|
@@ -2109,15 +2124,8 @@ server.tool(
|
|
|
2109
2124
|
throw new Error(waitResult.error?.message || "no response received");
|
|
2110
2125
|
}
|
|
2111
2126
|
} catch (autoErr) {
|
|
2112
|
-
// Fallback to clipboard
|
|
2113
2127
|
const errMsg = autoErr instanceof Error ? autoErr.message : String(autoErr);
|
|
2114
|
-
|
|
2115
|
-
try {
|
|
2116
|
-
writeClipboardText(payload);
|
|
2117
|
-
extra = `\n\n⚠️ 자동 실행 실패 (${errMsg}). 클립보드 모드로 폴백했습니다.\n✅ 클립보드에 턴 프롬프트가 복사되었습니다.`;
|
|
2118
|
-
} catch (clipErr) {
|
|
2119
|
-
extra = `\n\n⚠️ 자동 실행 실패 (${errMsg}). 클립보드 복사도 실패했습니다.\n수동으로 deliberation_clipboard_prepare_turn을 호출하세요.`;
|
|
2120
|
-
}
|
|
2128
|
+
extra = `\n\n⚠️ 자동 실행 실패 (${errMsg}). Chrome을 --remote-debugging-port=9222로 재시작하세요.`;
|
|
2121
2129
|
}
|
|
2122
2130
|
}
|
|
2123
2131
|
|
|
@@ -2195,7 +2203,7 @@ server.tool(
|
|
|
2195
2203
|
// Step 4: Wait for response
|
|
2196
2204
|
const waitResult = await port.waitTurnResult(resolved, turnId, timeout_sec);
|
|
2197
2205
|
if (!waitResult.ok) {
|
|
2198
|
-
return { content: [{ type: "text", text: `⏱️ 브라우저 LLM 응답 대기 타임아웃 (${timeout_sec}초)\n\n**에러:** ${waitResult.error.message}\n\
|
|
2206
|
+
return { content: [{ type: "text", text: `⏱️ 브라우저 LLM 응답 대기 타임아웃 (${timeout_sec}초)\n\n**에러:** ${waitResult.error.message}\n\n자동 실행이 타임아웃되었습니다. Chrome이 --remote-debugging-port=9222로 실행 중인지 확인하세요.\n\n${PRODUCT_DISCLAIMER}` }] };
|
|
2199
2207
|
}
|
|
2200
2208
|
|
|
2201
2209
|
// Step 5: Submit the response
|
|
@@ -2436,6 +2444,333 @@ server.tool(
|
|
|
2436
2444
|
})
|
|
2437
2445
|
);
|
|
2438
2446
|
|
|
2447
|
+
server.tool(
|
|
2448
|
+
"deliberation_cli_config",
|
|
2449
|
+
"딜리버레이션 참가자 CLI 설정을 조회하거나 변경합니다. enabled_clis를 지정하면 저장합니다.",
|
|
2450
|
+
{
|
|
2451
|
+
enabled_clis: z.array(z.string()).optional().describe("활성화할 CLI 목록 (예: [\"claude\", \"codex\", \"gemini\"]). 미지정 시 현재 설정 조회"),
|
|
2452
|
+
},
|
|
2453
|
+
safeToolHandler("deliberation_cli_config", async ({ enabled_clis }) => {
|
|
2454
|
+
const config = loadDeliberationConfig();
|
|
2455
|
+
|
|
2456
|
+
if (!enabled_clis) {
|
|
2457
|
+
// Read mode: show current config + detected CLIs
|
|
2458
|
+
const detected = discoverLocalCliSpeakers();
|
|
2459
|
+
const configured = Array.isArray(config.enabled_clis) ? config.enabled_clis : [];
|
|
2460
|
+
const mode = configured.length > 0 ? "config" : "auto-detect";
|
|
2461
|
+
|
|
2462
|
+
return {
|
|
2463
|
+
content: [{
|
|
2464
|
+
type: "text",
|
|
2465
|
+
text: `## Deliberation CLI 설정\n\n**모드:** ${mode}\n**설정된 CLI:** ${configured.length > 0 ? configured.join(", ") : "(없음 — 전체 자동 감지)"}\n**현재 감지된 CLI:** ${detected.join(", ") || "(없음)"}\n**지원 CLI 전체:** ${DEFAULT_CLI_CANDIDATES.join(", ")}\n\n변경하려면:\n\`deliberation_cli_config(enabled_clis: ["claude", "codex"])\`\n\n전체 자동 감지로 되돌리려면:\n\`deliberation_cli_config(enabled_clis: [])\``,
|
|
2466
|
+
}],
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
|
|
2470
|
+
// Write mode: save new config
|
|
2471
|
+
if (enabled_clis.length === 0) {
|
|
2472
|
+
// Empty array = reset to auto-detect all
|
|
2473
|
+
delete config.enabled_clis;
|
|
2474
|
+
saveDeliberationConfig(config);
|
|
2475
|
+
return {
|
|
2476
|
+
content: [{
|
|
2477
|
+
type: "text",
|
|
2478
|
+
text: `✅ CLI 설정 초기화 완료. 전체 자동 감지 모드로 전환되었습니다.\n감지 대상: ${DEFAULT_CLI_CANDIDATES.join(", ")}`,
|
|
2479
|
+
}],
|
|
2480
|
+
};
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
// Validate CLIs
|
|
2484
|
+
const valid = [];
|
|
2485
|
+
const invalid = [];
|
|
2486
|
+
for (const cli of enabled_clis) {
|
|
2487
|
+
const normalized = cli.trim().toLowerCase();
|
|
2488
|
+
if (normalized) valid.push(normalized);
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
config.enabled_clis = valid;
|
|
2492
|
+
saveDeliberationConfig(config);
|
|
2493
|
+
|
|
2494
|
+
// Check which are actually installed
|
|
2495
|
+
const installed = valid.filter(cli => {
|
|
2496
|
+
try {
|
|
2497
|
+
execFileSync(process.platform === "win32" ? "where" : "which", [cli], { stdio: "ignore" });
|
|
2498
|
+
return true;
|
|
2499
|
+
} catch { return false; }
|
|
2500
|
+
});
|
|
2501
|
+
const notInstalled = valid.filter(cli => !installed.includes(cli));
|
|
2502
|
+
|
|
2503
|
+
let result = `✅ CLI 설정 저장 완료!\n\n**활성화된 CLI:** ${valid.join(", ")}`;
|
|
2504
|
+
if (installed.length > 0) result += `\n**설치 확인됨:** ${installed.join(", ")}`;
|
|
2505
|
+
if (notInstalled.length > 0) result += `\n**⚠️ 미설치:** ${notInstalled.join(", ")} (PATH에서 찾을 수 없음)`;
|
|
2506
|
+
|
|
2507
|
+
return { content: [{ type: "text", text: result }] };
|
|
2508
|
+
})
|
|
2509
|
+
);
|
|
2510
|
+
|
|
2511
|
+
// ── Request Review (auto-review) ───────────────────────────────
|
|
2512
|
+
|
|
2513
|
+
function invokeCliReviewer(command, prompt, timeoutMs) {
|
|
2514
|
+
const args = ["-p", prompt, "--no-input"];
|
|
2515
|
+
try {
|
|
2516
|
+
const result = execFileSync(command, args, {
|
|
2517
|
+
encoding: "utf-8",
|
|
2518
|
+
timeout: timeoutMs,
|
|
2519
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
2520
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
2521
|
+
windowsHide: true,
|
|
2522
|
+
});
|
|
2523
|
+
return { ok: true, response: result.trim() };
|
|
2524
|
+
} catch (error) {
|
|
2525
|
+
if (error && error.killed) {
|
|
2526
|
+
return { ok: false, error: "timeout" };
|
|
2527
|
+
}
|
|
2528
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
2529
|
+
return { ok: false, error: msg };
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2533
|
+
function buildReviewPrompt(context, question, priorReviews) {
|
|
2534
|
+
let prompt = `You are a code reviewer. Provide a concise, structured review.\n\n`;
|
|
2535
|
+
prompt += `## Context\n${context}\n\n`;
|
|
2536
|
+
prompt += `## Review Question\n${question}\n\n`;
|
|
2537
|
+
if (priorReviews.length > 0) {
|
|
2538
|
+
prompt += `## Prior Reviews\n`;
|
|
2539
|
+
for (const r of priorReviews) {
|
|
2540
|
+
prompt += `### ${r.reviewer}\n${r.response}\n\n`;
|
|
2541
|
+
}
|
|
2542
|
+
}
|
|
2543
|
+
prompt += `Respond with your review. Be specific about issues, risks, and suggestions.`;
|
|
2544
|
+
return prompt;
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
function synthesizeReviews(context, question, reviews) {
|
|
2548
|
+
if (reviews.length === 0) return "(No reviews completed)";
|
|
2549
|
+
|
|
2550
|
+
let synthesis = `## Review Synthesis\n\n`;
|
|
2551
|
+
synthesis += `**Question:** ${question}\n`;
|
|
2552
|
+
synthesis += `**Reviews:** ${reviews.length}\n\n`;
|
|
2553
|
+
|
|
2554
|
+
synthesis += `### Individual Reviews\n\n`;
|
|
2555
|
+
for (const r of reviews) {
|
|
2556
|
+
synthesis += `#### ${r.reviewer}\n${r.response}\n\n`;
|
|
2557
|
+
}
|
|
2558
|
+
|
|
2559
|
+
if (reviews.length > 1) {
|
|
2560
|
+
synthesis += `### Summary\n`;
|
|
2561
|
+
synthesis += `${reviews.length} reviewer(s) provided feedback on: ${question}\n`;
|
|
2562
|
+
synthesis += `Reviewers: ${reviews.map(r => r.reviewer).join(", ")}\n`;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
return synthesis;
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
server.tool(
|
|
2569
|
+
"deliberation_request_review",
|
|
2570
|
+
"코드 리뷰를 요청합니다. 여러 CLI 리뷰어에게 동시에 리뷰를 요청하고 결과를 종합합니다.",
|
|
2571
|
+
{
|
|
2572
|
+
context: z.string().describe("리뷰할 변경사항 설명 (코드, diff, 설계 등)"),
|
|
2573
|
+
question: z.string().describe("리뷰 질문 (예: 'Is this error handling sufficient?')"),
|
|
2574
|
+
reviewers: z.array(z.string().trim().min(1).max(64)).min(1).describe("리뷰어 CLI 목록 (예: [\"claude\", \"codex\"])"),
|
|
2575
|
+
mode: z.enum(["sync", "async"]).default("sync").describe("sync: 결과 대기 후 반환, async: session_id 즉시 반환"),
|
|
2576
|
+
deadline_ms: z.number().int().min(5000).max(600000).default(60000).describe("전체 타임아웃 (밀리초, 기본 60초)"),
|
|
2577
|
+
min_reviews: z.number().int().min(1).default(1).describe("최소 필요 리뷰 수 (기본 1)"),
|
|
2578
|
+
on_timeout: z.enum(["partial", "fail"]).default("partial").describe("타임아웃 시 동작: partial=부분 결과 반환, fail=에러"),
|
|
2579
|
+
},
|
|
2580
|
+
safeToolHandler("deliberation_request_review", async ({ context, question, reviewers, mode, deadline_ms, min_reviews, on_timeout }) => {
|
|
2581
|
+
// Validate reviewers exist in PATH
|
|
2582
|
+
const validReviewers = [];
|
|
2583
|
+
const invalidReviewers = [];
|
|
2584
|
+
for (const r of reviewers) {
|
|
2585
|
+
const normalized = normalizeSpeaker(r);
|
|
2586
|
+
if (!normalized) continue;
|
|
2587
|
+
if (commandExistsInPath(normalized)) {
|
|
2588
|
+
validReviewers.push(normalized);
|
|
2589
|
+
} else {
|
|
2590
|
+
invalidReviewers.push(normalized);
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
|
|
2594
|
+
if (validReviewers.length === 0) {
|
|
2595
|
+
return {
|
|
2596
|
+
content: [{
|
|
2597
|
+
type: "text",
|
|
2598
|
+
text: `❌ 유효한 리뷰어가 없습니다. PATH에서 찾을 수 없는 CLI: ${invalidReviewers.join(", ")}\n\n사용 가능한 CLI를 확인하려면 deliberation_speaker_candidates를 호출하세요.`,
|
|
2599
|
+
}],
|
|
2600
|
+
};
|
|
2601
|
+
}
|
|
2602
|
+
|
|
2603
|
+
// Create mini-session
|
|
2604
|
+
const sessionId = generateSessionId("review");
|
|
2605
|
+
const callerSpeaker = detectCallerSpeaker() || "requester";
|
|
2606
|
+
const now = new Date().toISOString();
|
|
2607
|
+
|
|
2608
|
+
const state = {
|
|
2609
|
+
id: sessionId,
|
|
2610
|
+
project: getProjectSlug(),
|
|
2611
|
+
topic: question.slice(0, 80),
|
|
2612
|
+
type: "auto_review",
|
|
2613
|
+
status: "active",
|
|
2614
|
+
max_rounds: 1,
|
|
2615
|
+
current_round: 1,
|
|
2616
|
+
current_speaker: validReviewers[0],
|
|
2617
|
+
speakers: validReviewers,
|
|
2618
|
+
participant_profiles: validReviewers.map(r => ({ speaker: r, type: "cli", command: r })),
|
|
2619
|
+
log: [],
|
|
2620
|
+
synthesis: null,
|
|
2621
|
+
requester: callerSpeaker,
|
|
2622
|
+
review_context: context,
|
|
2623
|
+
review_question: question,
|
|
2624
|
+
review_mode: mode,
|
|
2625
|
+
review_deadline_ms: deadline_ms,
|
|
2626
|
+
review_min_reviews: min_reviews,
|
|
2627
|
+
review_on_timeout: on_timeout,
|
|
2628
|
+
pending_turn_id: generateTurnId(),
|
|
2629
|
+
monitor_terminal_window_ids: [],
|
|
2630
|
+
created: now,
|
|
2631
|
+
updated: now,
|
|
2632
|
+
};
|
|
2633
|
+
|
|
2634
|
+
withSessionLock(sessionId, () => {
|
|
2635
|
+
ensureDirs();
|
|
2636
|
+
saveSession(state);
|
|
2637
|
+
});
|
|
2638
|
+
|
|
2639
|
+
// Async mode: return immediately
|
|
2640
|
+
if (mode === "async") {
|
|
2641
|
+
const warn = invalidReviewers.length > 0
|
|
2642
|
+
? `\n⚠️ PATH에서 찾을 수 없는 리뷰어 (제외됨): ${invalidReviewers.join(", ")}`
|
|
2643
|
+
: "";
|
|
2644
|
+
return {
|
|
2645
|
+
content: [{
|
|
2646
|
+
type: "text",
|
|
2647
|
+
text: `✅ 비동기 리뷰 세션 생성됨\n\n**Session ID:** ${sessionId}\n**리뷰어:** ${validReviewers.join(", ")}\n**모드:** async${warn}\n\n진행 상태는 \`deliberation_status(session_id: "${sessionId}")\`로 확인하세요.`,
|
|
2648
|
+
}],
|
|
2649
|
+
};
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
// Sync mode: invoke each reviewer sequentially with deadline enforcement
|
|
2653
|
+
const globalStart = Date.now();
|
|
2654
|
+
const softBudgetPerReviewer = Math.floor(deadline_ms / validReviewers.length);
|
|
2655
|
+
const completedReviews = [];
|
|
2656
|
+
const timedOutReviewers = [];
|
|
2657
|
+
const failedReviewers = [];
|
|
2658
|
+
|
|
2659
|
+
for (const reviewer of validReviewers) {
|
|
2660
|
+
const elapsed = Date.now() - globalStart;
|
|
2661
|
+
const remaining = deadline_ms - elapsed;
|
|
2662
|
+
|
|
2663
|
+
// Global deadline check
|
|
2664
|
+
if (remaining <= 1000) {
|
|
2665
|
+
timedOutReviewers.push(reviewer);
|
|
2666
|
+
continue;
|
|
2667
|
+
}
|
|
2668
|
+
|
|
2669
|
+
// Per-reviewer timeout: min of soft budget and remaining global time
|
|
2670
|
+
const reviewerTimeout = Math.min(softBudgetPerReviewer, remaining);
|
|
2671
|
+
|
|
2672
|
+
const prompt = buildReviewPrompt(context, question, completedReviews);
|
|
2673
|
+
const result = invokeCliReviewer(reviewer, prompt, reviewerTimeout);
|
|
2674
|
+
|
|
2675
|
+
if (result.ok) {
|
|
2676
|
+
const entry = { reviewer, response: result.response };
|
|
2677
|
+
completedReviews.push(entry);
|
|
2678
|
+
|
|
2679
|
+
// Add to session log
|
|
2680
|
+
withSessionLock(sessionId, () => {
|
|
2681
|
+
const latest = loadSession(sessionId);
|
|
2682
|
+
if (!latest) return;
|
|
2683
|
+
latest.log.push({
|
|
2684
|
+
round: 1,
|
|
2685
|
+
speaker: reviewer,
|
|
2686
|
+
content: result.response,
|
|
2687
|
+
timestamp: new Date().toISOString(),
|
|
2688
|
+
turn_id: generateTurnId(),
|
|
2689
|
+
channel_used: "cli_auto_review",
|
|
2690
|
+
fallback_reason: null,
|
|
2691
|
+
});
|
|
2692
|
+
latest.updated = new Date().toISOString();
|
|
2693
|
+
saveSession(latest);
|
|
2694
|
+
});
|
|
2695
|
+
} else if (result.error === "timeout") {
|
|
2696
|
+
timedOutReviewers.push(reviewer);
|
|
2697
|
+
} else {
|
|
2698
|
+
failedReviewers.push({ reviewer, error: result.error });
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
|
|
2702
|
+
// Check min_reviews threshold
|
|
2703
|
+
if (completedReviews.length < min_reviews) {
|
|
2704
|
+
if (on_timeout === "fail") {
|
|
2705
|
+
// Mark session as failed
|
|
2706
|
+
withSessionLock(sessionId, () => {
|
|
2707
|
+
const latest = loadSession(sessionId);
|
|
2708
|
+
if (!latest) return;
|
|
2709
|
+
latest.status = "completed";
|
|
2710
|
+
latest.synthesis = `Review failed: only ${completedReviews.length}/${min_reviews} required reviews completed.`;
|
|
2711
|
+
saveSession(latest);
|
|
2712
|
+
archiveState(latest);
|
|
2713
|
+
cleanupSyncMarkdown(latest);
|
|
2714
|
+
});
|
|
2715
|
+
|
|
2716
|
+
return {
|
|
2717
|
+
content: [{
|
|
2718
|
+
type: "text",
|
|
2719
|
+
text: `❌ 리뷰 실패: 최소 ${min_reviews}개 리뷰 필요, ${completedReviews.length}개만 완료\n\n**Session:** ${sessionId}\n**완료:** ${completedReviews.map(r => r.reviewer).join(", ") || "(없음)"}\n**타임아웃:** ${timedOutReviewers.join(", ") || "(없음)"}\n**실패:** ${failedReviewers.map(r => `${r.reviewer}: ${r.error}`).join(", ") || "(없음)"}`,
|
|
2720
|
+
}],
|
|
2721
|
+
};
|
|
2722
|
+
}
|
|
2723
|
+
// on_timeout === "partial": fall through to return partial results
|
|
2724
|
+
}
|
|
2725
|
+
|
|
2726
|
+
// Synthesize
|
|
2727
|
+
const synthesis = synthesizeReviews(context, question, completedReviews);
|
|
2728
|
+
|
|
2729
|
+
// Complete session
|
|
2730
|
+
let archivePath = null;
|
|
2731
|
+
withSessionLock(sessionId, () => {
|
|
2732
|
+
const latest = loadSession(sessionId);
|
|
2733
|
+
if (!latest) return;
|
|
2734
|
+
latest.status = "completed";
|
|
2735
|
+
latest.synthesis = synthesis;
|
|
2736
|
+
latest.current_speaker = "none";
|
|
2737
|
+
saveSession(latest);
|
|
2738
|
+
archivePath = archiveState(latest);
|
|
2739
|
+
cleanupSyncMarkdown(latest);
|
|
2740
|
+
});
|
|
2741
|
+
|
|
2742
|
+
const totalMs = Date.now() - globalStart;
|
|
2743
|
+
const coverage = `${completedReviews.length}/${validReviewers.length}`;
|
|
2744
|
+
const warn = invalidReviewers.length > 0
|
|
2745
|
+
? `\n**제외된 리뷰어 (미설치):** ${invalidReviewers.join(", ")}`
|
|
2746
|
+
: "";
|
|
2747
|
+
const timeoutInfo = timedOutReviewers.length > 0
|
|
2748
|
+
? `\n**타임아웃 리뷰어:** ${timedOutReviewers.join(", ")}`
|
|
2749
|
+
: "";
|
|
2750
|
+
const failInfo = failedReviewers.length > 0
|
|
2751
|
+
? `\n**실패 리뷰어:** ${failedReviewers.map(r => `${r.reviewer}: ${r.error}`).join(", ")}`
|
|
2752
|
+
: "";
|
|
2753
|
+
|
|
2754
|
+
const resultPayload = {
|
|
2755
|
+
synthesis,
|
|
2756
|
+
completed_reviewers: completedReviews.map(r => r.reviewer),
|
|
2757
|
+
timed_out_reviewers: timedOutReviewers,
|
|
2758
|
+
failed_reviewers: failedReviewers.map(r => r.reviewer),
|
|
2759
|
+
coverage,
|
|
2760
|
+
mode: "sync",
|
|
2761
|
+
session_id: sessionId,
|
|
2762
|
+
elapsed_ms: totalMs,
|
|
2763
|
+
};
|
|
2764
|
+
|
|
2765
|
+
return {
|
|
2766
|
+
content: [{
|
|
2767
|
+
type: "text",
|
|
2768
|
+
text: `## Review 완료\n\n**Session:** ${sessionId}\n**Coverage:** ${coverage}\n**소요 시간:** ${totalMs}ms\n**완료 리뷰어:** ${completedReviews.map(r => r.reviewer).join(", ") || "(없음)"}${timeoutInfo}${failInfo}${warn}\n\n${synthesis}\n\n---\n\n\`\`\`json\n${JSON.stringify(resultPayload, null, 2)}\n\`\`\``,
|
|
2769
|
+
}],
|
|
2770
|
+
};
|
|
2771
|
+
})
|
|
2772
|
+
);
|
|
2773
|
+
|
|
2439
2774
|
// ── Start ──────────────────────────────────────────────────────
|
|
2440
2775
|
|
|
2441
2776
|
const transport = new StdioServerTransport();
|
package/package.json
CHANGED
|
@@ -29,9 +29,7 @@ Claude/Codex를 포함해 MCP를 지원하는 임의 CLI들이 구조화된 토
|
|
|
29
29
|
| `deliberation_status` | 토론 상태 조회 | 선택적* |
|
|
30
30
|
| `deliberation_context` | 프로젝트 컨텍스트 로드 | 불필요 |
|
|
31
31
|
| `deliberation_browser_llm_tabs` | 브라우저 LLM 탭 목록 (웹 기반 LLM 참여용) | 불필요 |
|
|
32
|
-
| `
|
|
33
|
-
| `deliberation_clipboard_submit_turn` | 클립보드 기반 턴 제출 (응답 붙여넣기) | 선택적* |
|
|
34
|
-
| `deliberation_route_turn` | 현재 차례 speaker의 transport(CLI/clipboard/manual)를 자동 라우팅 | 선택적* |
|
|
32
|
+
| `deliberation_route_turn` | 현재 차례 speaker의 transport(CLI/browser_auto/manual)를 자동 라우팅 | 선택적* |
|
|
35
33
|
| `deliberation_respond` | 현재 차례의 응답 제출 | 선택적* |
|
|
36
34
|
| `deliberation_history` | 전체 토론 기록 조회 | 선택적* |
|
|
37
35
|
| `deliberation_synthesize` | 합성 보고서 생성 및 토론 완료 | 선택적* |
|
|
@@ -52,21 +50,38 @@ Claude/Codex를 포함해 MCP를 지원하는 임의 CLI들이 구조화된 토
|
|
|
52
50
|
- "deliberation", "deliberate", "토론", "debate"
|
|
53
51
|
- "deliberation 시작", "토론 시작", "토론해", "토론하자"
|
|
54
52
|
- "deliberation_start", "deliberation_respond", "deliberation_route_turn"
|
|
55
|
-
- "speaker candidates", "브라우저 LLM"
|
|
53
|
+
- "speaker candidates", "브라우저 LLM"
|
|
54
|
+
- "크롬", "브라우저", "웹 LLM", "chrome", "browser LLM"
|
|
56
55
|
- "{주제} 토론", "{주제} deliberation"
|
|
57
56
|
|
|
58
57
|
## 워크플로우
|
|
59
58
|
|
|
60
59
|
### A. 사용자 선택형 진행 (권장)
|
|
61
60
|
1. `deliberation_speaker_candidates` → 참가 가능한 CLI/브라우저 speaker 확인
|
|
62
|
-
2.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
61
|
+
2. **AskUserQuestion으로 참가자 선택** — 감지된 CLI/브라우저 speaker 목록을 `multiSelect: true`로 제시하여 사용자가 원하는 참가자만 체크. 예:
|
|
62
|
+
```
|
|
63
|
+
AskUserQuestion({
|
|
64
|
+
questions: [{
|
|
65
|
+
question: "토론에 참여할 speaker를 선택하세요",
|
|
66
|
+
header: "참가자",
|
|
67
|
+
multiSelect: true,
|
|
68
|
+
options: [
|
|
69
|
+
{ label: "claude", description: "CLI (자동 응답)" },
|
|
70
|
+
{ label: "codex", description: "CLI (자동 응답)" },
|
|
71
|
+
{ label: "gemini", description: "CLI (자동 응답)" },
|
|
72
|
+
{ label: "web-chatgpt-1", description: "⚡자동 (CDP 자동 연결)" },
|
|
73
|
+
{ label: "web-claude-1", description: "⚡자동 (CDP 자동 연결)" },
|
|
74
|
+
{ label: "web-gemini-1", description: "⚡자동 (CDP 자동 연결)" }
|
|
75
|
+
]
|
|
76
|
+
}]
|
|
77
|
+
})
|
|
78
|
+
```
|
|
79
|
+
3. `deliberation_start` (선택된 speakers 전달) → session_id 획득
|
|
80
|
+
4. `deliberation_route_turn` → 현재 차례 speaker transport 자동 결정
|
|
81
|
+
- CLI speaker → 자동 응답
|
|
82
|
+
- browser_auto → CDP로 자동 전송/수집
|
|
83
|
+
5. 반복 후 `deliberation_synthesize(session_id)` → 합성 완료
|
|
84
|
+
6. 구현이 필요하면 `deliberation-executor` 스킬로 handoff
|
|
70
85
|
예: "session_id {id} 합의안 구현해줘"
|
|
71
86
|
|
|
72
87
|
### B. 병렬 세션 운영
|
|
@@ -100,6 +115,13 @@ bash deliberation-monitor.sh <session_id>
|
|
|
100
115
|
bash deliberation-monitor.sh --tmux
|
|
101
116
|
```
|
|
102
117
|
|
|
118
|
+
### E. 브라우저 LLM 자동 연결 (CDP Auto-Activation)
|
|
119
|
+
- 브라우저 LLM speaker가 선택되면 CDP(Chrome DevTools Protocol)가 자동으로 활성화됩니다.
|
|
120
|
+
- macOS에서는 Chrome이 실행되지 않은 경우 `--remote-debugging-port=9222`로 자동 실행을 시도합니다.
|
|
121
|
+
- **Chrome이 이미 CDP 없이 실행 중인 경우**: Chrome을 완전히 종료한 후 다시 시도해야 합니다. (최초 1회만 필요)
|
|
122
|
+
- CDP 연결 성공 시 모든 브라우저 speaker는 ⚡자동 모드로 동작합니다.
|
|
123
|
+
- Windows/Linux에서는 사용자가 직접 Chrome을 `--remote-debugging-port=9222`로 실행해야 합니다.
|
|
124
|
+
|
|
103
125
|
## 역할 규칙
|
|
104
126
|
|
|
105
127
|
### 역할 예시 A: 비판적 분석가
|
|
@@ -130,6 +152,6 @@ bash deliberation-monitor.sh --tmux
|
|
|
130
152
|
1. 여러 deliberation을 동시에 병렬 진행 가능
|
|
131
153
|
2. session_id는 `deliberation_start` 응답에서 확인
|
|
132
154
|
3. 토론 결과는 Obsidian vault에 자동 아카이브 (프로젝트 폴더 존재 시)
|
|
133
|
-
4.
|
|
155
|
+
4. 실시간 sync 파일은 state 디렉토리에 저장되며 완료 시 자동 삭제됨 (프로젝트 루트 오염 없음)
|
|
134
156
|
5. `Transport closed` 발생 시 현재 CLI 세션 재시작 후 재시도 (stdio 연결은 세션 바인딩)
|
|
135
157
|
6. 멀티 세션 운영 중 `pkill -f mcp-deliberation` 사용 금지 (다른 세션 연결까지 끊길 수 있음)
|