@dmsdc-ai/aigentry-deliberation 0.0.29 → 0.0.31
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/browser-control-port.js +273 -113
- package/index.js +329 -133
- package/install.js +1 -0
- package/observer.js +76 -11
- package/package.json +1 -1
- package/selectors/chatgpt.json +3 -1
- package/selectors/copilot.json +3 -2
- package/selectors/deepseek.json +2 -1
- package/selectors/grok.json +4 -2
- package/selectors/mistral.json +7 -6
- package/selectors/qwen.json +4 -3
package/index.js
CHANGED
|
@@ -74,6 +74,7 @@ import { fileURLToPath } from "url";
|
|
|
74
74
|
import os from "os";
|
|
75
75
|
import { OrchestratedBrowserPort } from "./browser-control-port.js";
|
|
76
76
|
import { getModelSelectionForTurn } from "./model-router.js";
|
|
77
|
+
import { readClipboardText, writeClipboardText, hasClipboardImage, captureClipboardImage } from "./clipboard.js";
|
|
77
78
|
import {
|
|
78
79
|
DECISION_STAGES, STAGE_TRANSITIONS,
|
|
79
80
|
createDecisionSession, advanceStage, buildConflictMap,
|
|
@@ -383,6 +384,18 @@ function formatRuntimeError(error) {
|
|
|
383
384
|
function appendRuntimeLog(level, message) {
|
|
384
385
|
try {
|
|
385
386
|
fs.mkdirSync(path.dirname(GLOBAL_RUNTIME_LOG), { recursive: true });
|
|
387
|
+
|
|
388
|
+
// Simple rotation: if log > 1MB, truncate it
|
|
389
|
+
try {
|
|
390
|
+
if (fs.existsSync(GLOBAL_RUNTIME_LOG)) {
|
|
391
|
+
const stats = fs.statSync(GLOBAL_RUNTIME_LOG);
|
|
392
|
+
if (stats.size > 1024 * 1024) { // 1MB
|
|
393
|
+
const oldLog = GLOBAL_RUNTIME_LOG + ".old";
|
|
394
|
+
fs.renameSync(GLOBAL_RUNTIME_LOG, oldLog);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
} catch { /* ignore rotation failures */ }
|
|
398
|
+
|
|
386
399
|
const line = `${new Date().toISOString()} [${level}] ${message}\n`;
|
|
387
400
|
fs.appendFileSync(GLOBAL_RUNTIME_LOG, line, "utf-8");
|
|
388
401
|
} catch {
|
|
@@ -624,80 +637,6 @@ function detectCallerSpeaker() {
|
|
|
624
637
|
return null;
|
|
625
638
|
}
|
|
626
639
|
|
|
627
|
-
function resolveClipboardReader() {
|
|
628
|
-
if (process.platform === "darwin" && commandExistsInPath("pbpaste")) {
|
|
629
|
-
return { cmd: "pbpaste", args: [] };
|
|
630
|
-
}
|
|
631
|
-
if (process.platform === "win32") {
|
|
632
|
-
const windowsShell = ["powershell.exe", "powershell", "pwsh.exe", "pwsh"]
|
|
633
|
-
.find(cmd => commandExistsInPath(cmd));
|
|
634
|
-
if (windowsShell) {
|
|
635
|
-
return { cmd: windowsShell, args: ["-NoProfile", "-Command", "Get-Clipboard -Raw"] };
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
if (commandExistsInPath("wl-paste")) {
|
|
639
|
-
return { cmd: "wl-paste", args: ["-n"] };
|
|
640
|
-
}
|
|
641
|
-
if (commandExistsInPath("xclip")) {
|
|
642
|
-
return { cmd: "xclip", args: ["-selection", "clipboard", "-o"] };
|
|
643
|
-
}
|
|
644
|
-
if (commandExistsInPath("xsel")) {
|
|
645
|
-
return { cmd: "xsel", args: ["--clipboard", "--output"] };
|
|
646
|
-
}
|
|
647
|
-
return null;
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
function resolveClipboardWriter() {
|
|
651
|
-
if (process.platform === "darwin" && commandExistsInPath("pbcopy")) {
|
|
652
|
-
return { cmd: "pbcopy", args: [] };
|
|
653
|
-
}
|
|
654
|
-
if (process.platform === "win32") {
|
|
655
|
-
if (commandExistsInPath("clip.exe") || commandExistsInPath("clip")) {
|
|
656
|
-
return { cmd: "clip", args: [] };
|
|
657
|
-
}
|
|
658
|
-
const windowsShell = ["powershell.exe", "powershell", "pwsh.exe", "pwsh"]
|
|
659
|
-
.find(cmd => commandExistsInPath(cmd));
|
|
660
|
-
if (windowsShell) {
|
|
661
|
-
return { cmd: windowsShell, args: ["-NoProfile", "-Command", "[Console]::In.ReadToEnd() | Set-Clipboard"] };
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
if (commandExistsInPath("wl-copy")) {
|
|
665
|
-
return { cmd: "wl-copy", args: [] };
|
|
666
|
-
}
|
|
667
|
-
if (commandExistsInPath("xclip")) {
|
|
668
|
-
return { cmd: "xclip", args: ["-selection", "clipboard"] };
|
|
669
|
-
}
|
|
670
|
-
if (commandExistsInPath("xsel")) {
|
|
671
|
-
return { cmd: "xsel", args: ["--clipboard", "--input"] };
|
|
672
|
-
}
|
|
673
|
-
return null;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function readClipboardText() {
|
|
677
|
-
const tool = resolveClipboardReader();
|
|
678
|
-
if (!tool) {
|
|
679
|
-
throw new Error("No supported clipboard read command found (pbpaste/wl-paste/xclip/xsel etc).");
|
|
680
|
-
}
|
|
681
|
-
return execFileSync(tool.cmd, tool.args, {
|
|
682
|
-
encoding: "utf-8",
|
|
683
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
684
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
685
|
-
});
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function writeClipboardText(text) {
|
|
689
|
-
const tool = resolveClipboardWriter();
|
|
690
|
-
if (!tool) {
|
|
691
|
-
throw new Error("No supported clipboard write command found (pbcopy/wl-copy/xclip/xsel etc).");
|
|
692
|
-
}
|
|
693
|
-
execFileSync(tool.cmd, tool.args, {
|
|
694
|
-
input: text,
|
|
695
|
-
encoding: "utf-8",
|
|
696
|
-
stdio: ["pipe", "ignore", "pipe"],
|
|
697
|
-
maxBuffer: 5 * 1024 * 1024,
|
|
698
|
-
});
|
|
699
|
-
}
|
|
700
|
-
|
|
701
640
|
function isLlmUrl(url = "") {
|
|
702
641
|
const value = String(url || "").trim();
|
|
703
642
|
if (!value) return false;
|
|
@@ -719,7 +658,10 @@ function dedupeBrowserTabs(tabs = []) {
|
|
|
719
658
|
const title = String(tab?.title || "").trim();
|
|
720
659
|
const url = String(tab?.url || "").trim();
|
|
721
660
|
if (!url || (!isLlmUrl(url) && !isExtensionLlmTab(url, title))) continue;
|
|
722
|
-
|
|
661
|
+
// Dedup by title+url (ignore browser name) so that the same tab detected
|
|
662
|
+
// via both AppleScript and CDP is not duplicated. The first occurrence wins,
|
|
663
|
+
// so callers should add preferred sources first (e.g., CDP before AppleScript).
|
|
664
|
+
const key = `${title}\t${url}`;
|
|
723
665
|
if (seen.has(key)) continue;
|
|
724
666
|
seen.add(key);
|
|
725
667
|
out.push({
|
|
@@ -1145,6 +1087,16 @@ async function collectBrowserLlmTabs() {
|
|
|
1145
1087
|
};
|
|
1146
1088
|
}
|
|
1147
1089
|
|
|
1090
|
+
// CDP first: CDP-detected tabs are preferred over AppleScript-detected ones
|
|
1091
|
+
// because they carry CDP metadata (tab ID, WebSocket URL) for browser_auto transport.
|
|
1092
|
+
// Since dedupeBrowserTabs keeps the first occurrence, CDP entries win the dedup.
|
|
1093
|
+
const shouldUseCdp = mode === "auto" || mode === "cdp";
|
|
1094
|
+
if (shouldUseCdp) {
|
|
1095
|
+
const cdp = await collectBrowserLlmTabsViaCdp();
|
|
1096
|
+
tabs.push(...cdp.tabs);
|
|
1097
|
+
if (cdp.note) notes.push(cdp.note);
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1148
1100
|
const shouldUseAppleScript = mode === "auto" || mode === "applescript";
|
|
1149
1101
|
if (shouldUseAppleScript && process.platform === "darwin") {
|
|
1150
1102
|
const mac = collectBrowserLlmTabsViaAppleScript();
|
|
@@ -1154,13 +1106,6 @@ async function collectBrowserLlmTabs() {
|
|
|
1154
1106
|
notes.push("AppleScript scanning is macOS only. Switch to CDP scanning.");
|
|
1155
1107
|
}
|
|
1156
1108
|
|
|
1157
|
-
const shouldUseCdp = mode === "auto" || mode === "cdp";
|
|
1158
|
-
if (shouldUseCdp) {
|
|
1159
|
-
const cdp = await collectBrowserLlmTabsViaCdp();
|
|
1160
|
-
tabs.push(...cdp.tabs);
|
|
1161
|
-
if (cdp.note) notes.push(cdp.note);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
1109
|
const uniqTabs = dedupeBrowserTabs(tabs);
|
|
1165
1110
|
return {
|
|
1166
1111
|
tabs: uniqTabs,
|
|
@@ -1345,6 +1290,17 @@ async function collectSpeakerCandidates({ include_cli = true, include_browser =
|
|
|
1345
1290
|
}
|
|
1346
1291
|
}
|
|
1347
1292
|
}
|
|
1293
|
+
|
|
1294
|
+
// Third pass: upgrade browser-detected candidates that missed the first hostname match.
|
|
1295
|
+
// When CDP is reachable, AppleScript-detected speakers should also get browser_auto
|
|
1296
|
+
// transport. The OrchestratedBrowserPort will create/navigate tabs on demand if needed.
|
|
1297
|
+
if (cdpReachable) {
|
|
1298
|
+
for (const candidate of candidates) {
|
|
1299
|
+
if (candidate.type !== "browser" || candidate.auto_registered) continue;
|
|
1300
|
+
if (candidate.cdp_available) continue; // already matched
|
|
1301
|
+
candidate.cdp_available = true;
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1348
1304
|
}
|
|
1349
1305
|
|
|
1350
1306
|
return { candidates, browserNote };
|
|
@@ -1406,10 +1362,16 @@ function mapParticipantProfiles(speakers, candidates, typeOverrides) {
|
|
|
1406
1362
|
// Check for explicit type override
|
|
1407
1363
|
const overrideType = overrides[speaker] || overrides[raw];
|
|
1408
1364
|
if (overrideType) {
|
|
1365
|
+
const candidate = bySpeaker.get(speaker);
|
|
1409
1366
|
profiles.push({
|
|
1410
1367
|
speaker,
|
|
1411
1368
|
type: overrideType,
|
|
1412
|
-
...(overrideType === "browser_auto"
|
|
1369
|
+
...(overrideType === "browser_auto" || overrideType === "browser" ? {
|
|
1370
|
+
provider: candidate?.provider || null,
|
|
1371
|
+
browser: candidate?.browser || null,
|
|
1372
|
+
title: candidate?.title || null,
|
|
1373
|
+
url: candidate?.url || null,
|
|
1374
|
+
} : {}),
|
|
1413
1375
|
});
|
|
1414
1376
|
continue;
|
|
1415
1377
|
}
|
|
@@ -1491,7 +1453,7 @@ function resolveTransportForSpeaker(state, speaker) {
|
|
|
1491
1453
|
// CLI-specific invocation flags for non-interactive execution
|
|
1492
1454
|
const CLI_INVOCATION_HINTS = {
|
|
1493
1455
|
claude: { cmd: "claude", flags: '-p --output-format text', example: 'CLAUDECODE= claude -p --output-format text "prompt"', envPrefix: 'CLAUDECODE=', modelFlag: '--model', provider: 'claude' },
|
|
1494
|
-
codex: { cmd: "codex", flags: 'exec
|
|
1456
|
+
codex: { cmd: "codex", flags: 'exec -', example: 'echo "prompt" | codex exec -', stdinMode: true, modelFlag: '--model', defaultModel: 'default', provider: 'chatgpt' },
|
|
1495
1457
|
gemini: { cmd: "gemini", flags: '', example: 'gemini "prompt"', modelFlag: '--model', provider: 'gemini' },
|
|
1496
1458
|
aider: { cmd: "aider", flags: '--message', example: 'aider --message "prompt"', modelFlag: '--model', provider: 'chatgpt' },
|
|
1497
1459
|
cursor: { cmd: "cursor", flags: '', example: 'cursor "prompt"', modelFlag: null, provider: 'chatgpt' },
|
|
@@ -1517,12 +1479,18 @@ function formatTransportGuidance(transport, state, speaker) {
|
|
|
1517
1479
|
return `CLI speaker. Respond directly via \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`.${invocationGuide}${modelGuide}\n\n⛔ **No API calls**: Do not call LLM APIs directly via REST API, HTTP requests, urllib, requests, etc. Only use the CLI tools above.`;
|
|
1518
1480
|
}
|
|
1519
1481
|
case "clipboard":
|
|
1520
|
-
return `Browser LLM speaker.
|
|
1482
|
+
return `Browser LLM speaker. Copy the prompt below and paste it into the browser LLM using **Cmd+V (ㅍ)**, then submit the response via \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", use_clipboard: true)\` after copying the LLM's response with **Cmd+C (ㅊ)**.\n\n` +
|
|
1483
|
+
`📋 **Prompt has been copied to your clipboard.** (If not, copy the [turn_prompt] section below manually).\n` +
|
|
1484
|
+
`🖼️ **To include an image:** Copy the image to your clipboard and use \`include_clipboard_image: true\` in \`deliberation_respond\`.\n\n` +
|
|
1485
|
+
`⛔ **No API calls**: This speaker responds only via web browser. Do not call LLMs via REST API or HTTP requests.`;
|
|
1521
1486
|
case "browser_auto":
|
|
1522
1487
|
return `Auto browser speaker. Proceed automatically with \`deliberation_browser_auto_turn(session_id: "${sid}")\`. Inputs directly to browser LLM via CDP and reads responses.\n\n⛔ **No API calls**: Proceeds only via CDP automation. No REST API or HTTP requests.`;
|
|
1523
1488
|
case "manual":
|
|
1524
1489
|
default:
|
|
1525
|
-
return `Manual speaker. Get a response from the LLM's **web UI or CLI tool** and submit via \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`.\n\n
|
|
1490
|
+
return `Manual speaker. Get a response from the LLM's **web UI or CLI tool** and submit via \`deliberation_respond(session_id: "${sid}", speaker: "${speaker}", content: "...")\`.\n\n` +
|
|
1491
|
+
`📋 **Copy the [turn_prompt] section below** to the web UI.\n` +
|
|
1492
|
+
`🖼️ **To include an image:** Copy the image to your clipboard and use \`include_clipboard_image: true\` in \`deliberation_respond\`.\n\n` +
|
|
1493
|
+
`⛔ **Absolutely no API calls**: Calling LLM APIs directly via REST API, HTTP requests (urllib, requests, fetch, etc.) is forbidden. Only use web browser UI or CLI tools. Direct API key calls will result in deliberation participation being rejected.`;
|
|
1526
1494
|
}
|
|
1527
1495
|
}
|
|
1528
1496
|
|
|
@@ -1763,7 +1731,15 @@ tags: [deliberation]
|
|
|
1763
1731
|
if (entry.fallback_reason) parts.push(`fallback: ${entry.fallback_reason}`);
|
|
1764
1732
|
md += `> _${parts.join(" | ")}_\n\n`;
|
|
1765
1733
|
}
|
|
1766
|
-
md += `${entry.content}\n\n
|
|
1734
|
+
md += `${entry.content}\n\n`;
|
|
1735
|
+
if (entry.attachments && entry.attachments.length > 0) {
|
|
1736
|
+
for (const att of entry.attachments) {
|
|
1737
|
+
if (att.type === "image") {
|
|
1738
|
+
md += `\n\n`;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
md += `---\n\n`;
|
|
1767
1743
|
}
|
|
1768
1744
|
return md;
|
|
1769
1745
|
}
|
|
@@ -1856,6 +1832,22 @@ function tmuxHasAttachedClients(sessionName) {
|
|
|
1856
1832
|
}
|
|
1857
1833
|
}
|
|
1858
1834
|
|
|
1835
|
+
function isTmuxWindowViewed(sessionName, windowName) {
|
|
1836
|
+
try {
|
|
1837
|
+
// List all clients and check for matching window name.
|
|
1838
|
+
// Grouped sessions (created via 'new-session -t') share the same windows,
|
|
1839
|
+
// so checking for the window name anywhere in the client list is sufficient.
|
|
1840
|
+
const output = execFileSync("tmux", ["list-clients", "-F", "#{window_name}"], {
|
|
1841
|
+
encoding: "utf-8",
|
|
1842
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1843
|
+
windowsHide: true,
|
|
1844
|
+
});
|
|
1845
|
+
return String(output).split("\n").map(s => s.trim()).filter(Boolean).includes(windowName);
|
|
1846
|
+
} catch {
|
|
1847
|
+
return false;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1859
1851
|
function tmuxWindowCount(name) {
|
|
1860
1852
|
try {
|
|
1861
1853
|
const output = execFileSync("tmux", ["list-windows", "-t", name], {
|
|
@@ -1921,9 +1913,20 @@ function openPhysicalTerminal(sessionId) {
|
|
|
1921
1913
|
// Use grouped session (new-session -t) for independent active window per client
|
|
1922
1914
|
const attachCmd = `tmux new-session -t "${TMUX_SESSION}" \\; select-window -t "${TMUX_SESSION}:${winName}"`;
|
|
1923
1915
|
|
|
1924
|
-
//
|
|
1925
|
-
//
|
|
1926
|
-
|
|
1916
|
+
// Prevent duplicate windows for the SAME session:
|
|
1917
|
+
// If a client is already viewing this specific window, just activate Terminal.app
|
|
1918
|
+
if (isTmuxWindowViewed(TMUX_SESSION, winName)) {
|
|
1919
|
+
appendRuntimeLog("INFO", `TMUX_WINDOW_ALREADY_VIEWED: ${winName}. Activating existing Terminal.`);
|
|
1920
|
+
if (process.platform === "darwin") {
|
|
1921
|
+
try {
|
|
1922
|
+
execFileSync("osascript", ["-e", 'tell application "Terminal" to activate'], { stdio: "ignore" });
|
|
1923
|
+
} catch { /* ignore */ }
|
|
1924
|
+
}
|
|
1925
|
+
return { opened: true, windowIds: [] };
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
// If a terminal is already attached to OTHER windows, open a NEW grouped session
|
|
1929
|
+
// instead of select-window (which would hijack all attached clients' views).
|
|
1927
1930
|
if (tmuxHasAttachedClients(TMUX_SESSION)) {
|
|
1928
1931
|
if (process.platform === "darwin") {
|
|
1929
1932
|
const groupAttachCmd = `tmux new-session -t "${TMUX_SESSION}" \\; select-window -t "${TMUX_SESSION}:${winName}"`;
|
|
@@ -2055,17 +2058,20 @@ function spawnMonitorTerminal(sessionId) {
|
|
|
2055
2058
|
if (hasTmuxSession(TMUX_SESSION)) {
|
|
2056
2059
|
// Skip if a window with the same name already exists (prevents duplicates)
|
|
2057
2060
|
if (hasTmuxWindow(TMUX_SESSION, winName)) {
|
|
2061
|
+
appendRuntimeLog("INFO", `TMUX_WINDOW_EXISTS: ${winName} in ${TMUX_SESSION}`);
|
|
2058
2062
|
return true;
|
|
2059
2063
|
}
|
|
2060
2064
|
execFileSync("tmux", ["new-window", "-t", TMUX_SESSION, "-n", winName, cmd], {
|
|
2061
2065
|
stdio: "ignore",
|
|
2062
2066
|
windowsHide: true,
|
|
2063
2067
|
});
|
|
2068
|
+
appendRuntimeLog("INFO", `TMUX_WINDOW_CREATED: ${winName} in existing ${TMUX_SESSION}`);
|
|
2064
2069
|
} else {
|
|
2065
2070
|
execFileSync("tmux", ["new-session", "-d", "-s", TMUX_SESSION, "-n", winName, cmd], {
|
|
2066
2071
|
stdio: "ignore",
|
|
2067
2072
|
windowsHide: true,
|
|
2068
2073
|
});
|
|
2074
|
+
appendRuntimeLog("INFO", `TMUX_SESSION_CREATED: ${TMUX_SESSION} with window ${winName}`);
|
|
2069
2075
|
}
|
|
2070
2076
|
return true;
|
|
2071
2077
|
} catch {
|
|
@@ -2302,7 +2308,7 @@ ${recent}
|
|
|
2302
2308
|
`;
|
|
2303
2309
|
}
|
|
2304
2310
|
|
|
2305
|
-
function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel_used, fallback_reason }) {
|
|
2311
|
+
function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel_used, fallback_reason, attachments }) {
|
|
2306
2312
|
const resolved = resolveSessionId(session_id);
|
|
2307
2313
|
if (!resolved) {
|
|
2308
2314
|
return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
|
|
@@ -2367,8 +2373,9 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
|
|
|
2367
2373
|
votes: votes.length > 0 ? votes : undefined,
|
|
2368
2374
|
suggested_next_role: suggestedRole !== "free" ? suggestedRole : undefined,
|
|
2369
2375
|
role_drift: roleDrift || undefined,
|
|
2376
|
+
attachments: attachments || undefined,
|
|
2370
2377
|
});
|
|
2371
|
-
appendRuntimeLog("INFO", `TURN: ${state.id} | R${state.current_round} | speaker: ${normalizedSpeaker} | votes: ${votes.length > 0 ? votes.map(v => v.vote).join(",") : "none"} | channel: ${channel_used || "respond"} |
|
|
2378
|
+
appendRuntimeLog("INFO", `TURN: ${state.id} | R${state.current_round} | speaker: ${normalizedSpeaker} | votes: ${votes.length > 0 ? votes.map(v => v.vote).join(",") : "none"} | channel: ${channel_used || "respond"} | attachments: ${attachments ? attachments.length : 0}`);
|
|
2372
2379
|
|
|
2373
2380
|
state.current_speaker = selectNextSpeaker(state);
|
|
2374
2381
|
|
|
@@ -2442,6 +2449,7 @@ server.tool(
|
|
|
2442
2449
|
"Start a new deliberation. Multiple deliberations can run simultaneously.",
|
|
2443
2450
|
{
|
|
2444
2451
|
topic: z.string().describe("Discussion topic"),
|
|
2452
|
+
session_id: z.string().trim().min(1).max(64).optional().describe("Explicit session ID to use. If omitted, one is generated from topic."),
|
|
2445
2453
|
rounds: z.coerce.number().optional().describe("Number of rounds (defaults to config setting, default 3)"),
|
|
2446
2454
|
first_speaker: z.string().trim().min(1).max(64).optional().describe("First speaker name (defaults to first item in speakers)"),
|
|
2447
2455
|
speakers: z.preprocess(
|
|
@@ -2476,9 +2484,9 @@ server.tool(
|
|
|
2476
2484
|
z.record(z.string(), z.enum(["critic", "implementer", "mediator", "researcher", "free"])).optional()
|
|
2477
2485
|
).describe("Per-speaker role assignment (e.g., {\"claude\": \"critic\", \"codex\": \"implementer\"})"),
|
|
2478
2486
|
role_preset: z.enum(["balanced", "debate", "research", "brainstorm", "review", "consensus"]).optional()
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
|
|
2487
|
+
.describe("Role preset (balanced/debate/research/brainstorm/review/consensus). Ignored if speaker_roles is specified"),
|
|
2488
|
+
},
|
|
2489
|
+
safeToolHandler("deliberation_start", async ({ topic, session_id, rounds, first_speaker, speakers, speaker_instructions, require_manual_speakers, auto_discover_speakers, participant_types, ordering_strategy, speaker_roles, role_preset }) => {
|
|
2482
2490
|
// ── First-time onboarding guard ──
|
|
2483
2491
|
const config = loadDeliberationConfig();
|
|
2484
2492
|
if (!config.setup_complete) {
|
|
@@ -2492,7 +2500,13 @@ server.tool(
|
|
|
2492
2500
|
};
|
|
2493
2501
|
}
|
|
2494
2502
|
|
|
2495
|
-
const sessionId = generateSessionId(topic);
|
|
2503
|
+
const sessionId = session_id || generateSessionId(topic);
|
|
2504
|
+
if (session_id) {
|
|
2505
|
+
const existing = loadSession(session_id);
|
|
2506
|
+
if (existing && existing.status === "active") {
|
|
2507
|
+
return { content: [{ type: "text", text: `❌ Session "${session_id}" is already active. Please use a different ID or reset it first.` }] };
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2496
2510
|
const candidateSnapshot = await collectSpeakerCandidates({ include_cli: true, include_browser: true });
|
|
2497
2511
|
|
|
2498
2512
|
// Resolve effective settings from config
|
|
@@ -2804,7 +2818,7 @@ server.tool(
|
|
|
2804
2818
|
}
|
|
2805
2819
|
|
|
2806
2820
|
const speaker = state.current_speaker;
|
|
2807
|
-
|
|
2821
|
+
let { transport, profile, reason } = resolveTransportForSpeaker(state, speaker);
|
|
2808
2822
|
const turnId = state.pending_turn_id || null;
|
|
2809
2823
|
|
|
2810
2824
|
// ── Self-speaker detection ──
|
|
@@ -2816,14 +2830,25 @@ server.tool(
|
|
|
2816
2830
|
|
|
2817
2831
|
let guidance;
|
|
2818
2832
|
if (isSelfSpeaker) {
|
|
2819
|
-
guidance =
|
|
2833
|
+
guidance = t(
|
|
2834
|
+
`🟢 **It's your turn.** You (${speaker}) are the current speaker.\n\n` +
|
|
2820
2835
|
`Write your response and submit via \`deliberation_respond(session_id: "${state.id}", speaker: "${speaker}", content: "...")\`.\n\n` +
|
|
2821
|
-
`⚠️ **
|
|
2836
|
+
`⚠️ **Wait! Why can't I use cli_auto_turn?**\n` +
|
|
2837
|
+
`You are currently the **orchestrator** (the AI running this tool). If you try to spawn yourself automatically, it would create an infinite loop (you calling yourself calling yourself...) and timeout.\n\n` +
|
|
2838
|
+
`Please analyze the topic and history above, formulate your response, and call \`deliberation_respond\` directly.`,
|
|
2839
|
+
`🟢 **당신의 차례입니다.** 당신(${speaker})이 현재 발언자입니다.\n\n` +
|
|
2840
|
+
`응답을 작성하고 \`deliberation_respond(session_id: "${state.id}", speaker: "${speaker}", content: "...")\`를 통해 제출하세요.\n\n` +
|
|
2841
|
+
`⚠️ **잠깐! 왜 cli_auto_turn을 쓸 수 없나요?**\n` +
|
|
2842
|
+
`당신은 현재 **오케스트레이터**(이 도구를 실행 중인 AI)입니다. 자기 자신을 자동으로 spawn하려고 하면 무한 루프(자신이 자신을 호출하고 다시 호출하는...)가 발생하여 타임아웃이 됩니다.\n\n` +
|
|
2843
|
+
`위의 주제와 이력을 분석하여 응답을 작성한 뒤, \`deliberation_respond\`를 직접 호출해 주세요.`,
|
|
2844
|
+
state?.lang
|
|
2845
|
+
);
|
|
2822
2846
|
} else {
|
|
2823
2847
|
guidance = formatTransportGuidance(transport, state, speaker);
|
|
2824
2848
|
}
|
|
2825
2849
|
|
|
2826
2850
|
let extra = "";
|
|
2851
|
+
let turnPrompt = "";
|
|
2827
2852
|
|
|
2828
2853
|
if (transport === "browser_auto") {
|
|
2829
2854
|
// Auto-execute browser_auto_turn
|
|
@@ -2837,7 +2862,7 @@ server.tool(
|
|
|
2837
2862
|
const modelSelection = getModelSelectionForTurn(state, turnSpeaker, turnProvider);
|
|
2838
2863
|
|
|
2839
2864
|
// Build prompt
|
|
2840
|
-
|
|
2865
|
+
turnPrompt = buildClipboardTurnPrompt(state, turnSpeaker, prompt, include_history_entries);
|
|
2841
2866
|
|
|
2842
2867
|
// Attach
|
|
2843
2868
|
const attachResult = await port.attach(sessionId, { provider: turnProvider, url: profile?.url });
|
|
@@ -2876,7 +2901,28 @@ server.tool(
|
|
|
2876
2901
|
} catch (autoErr) {
|
|
2877
2902
|
const errMsg = autoErr instanceof Error ? autoErr.message : String(autoErr);
|
|
2878
2903
|
extra = `\n\n⚠️ Auto-execution failed (${errMsg}). Restart Chrome with --remote-debugging-port=9222.`;
|
|
2904
|
+
// Fallback to clipboard preparation
|
|
2905
|
+
transport = "clipboard";
|
|
2906
|
+
// Re-generate guidance for the new transport
|
|
2907
|
+
if (!isSelfSpeaker) {
|
|
2908
|
+
guidance = formatTransportGuidance(transport, state, speaker);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
|
|
2913
|
+
if (transport === "clipboard" || transport === "manual") {
|
|
2914
|
+
// Prepare prompt for manual/clipboard transport
|
|
2915
|
+
turnPrompt = buildClipboardTurnPrompt(state, speaker, prompt, include_history_entries);
|
|
2916
|
+
|
|
2917
|
+
if (auto_prepare_clipboard) {
|
|
2918
|
+
try {
|
|
2919
|
+
writeClipboardText(turnPrompt);
|
|
2920
|
+
} catch (clipErr) {
|
|
2921
|
+
extra += `\n\n⚠️ Failed to copy to clipboard: ${clipErr.message}`;
|
|
2922
|
+
}
|
|
2879
2923
|
}
|
|
2924
|
+
|
|
2925
|
+
extra += `\n\n### [turn_prompt]\n\`\`\`markdown\n${turnPrompt}\n\`\`\``;
|
|
2880
2926
|
}
|
|
2881
2927
|
|
|
2882
2928
|
const profileInfo = profile
|
|
@@ -3044,13 +3090,19 @@ server.tool(
|
|
|
3044
3090
|
return { content: [{ type: "text", text: t(`Speaker "${speaker}" is not a CLI type (transport: ${transport}). Browser speakers should use deliberation_browser_auto_turn.`, `speaker "${speaker}"는 CLI 타입이 아닙니다 (transport: ${transport}). 브라우저 speaker는 deliberation_browser_auto_turn을 사용하세요.`, state?.lang) }] };
|
|
3045
3091
|
}
|
|
3046
3092
|
|
|
3047
|
-
// Block recursive self-spawn: if the speaker is the same CLI as the caller,
|
|
3048
|
-
// spawning it would create infinite recursion and timeout.
|
|
3049
3093
|
const callerSpeaker = detectCallerSpeaker();
|
|
3050
3094
|
if (callerSpeaker && speaker === callerSpeaker) {
|
|
3051
3095
|
return { content: [{ type: "text", text: t(
|
|
3052
|
-
|
|
3053
|
-
|
|
3096
|
+
`🟢 **It's your turn.** You (${speaker}) are the current speaker.\n\n` +
|
|
3097
|
+
`Write your response and submit via \`deliberation_respond(session_id: "${resolved}", speaker: "${speaker}", content: "...")\`.\n\n` +
|
|
3098
|
+
`⚠️ **Wait! Why can't I use cli_auto_turn?**\n` +
|
|
3099
|
+
`You are currently the **orchestrator** (the AI running this tool). If you try to spawn yourself automatically, it would create an infinite loop (you calling yourself calling yourself...) and timeout.\n\n` +
|
|
3100
|
+
`Please analyze the topic and history above, formulate your response, and call \`deliberation_respond\` directly.`,
|
|
3101
|
+
`🟢 **당신의 차례입니다.** 당신(${speaker})이 현재 발언자입니다.\n\n` +
|
|
3102
|
+
`응답을 작성하고 \`deliberation_respond(session_id: "${resolved}", speaker: "${speaker}", content: "...")\`를 통해 제출하세요.\n\n` +
|
|
3103
|
+
`⚠️ **잠깐! 왜 cli_auto_turn을 쓸 수 없나요?**\n` +
|
|
3104
|
+
`당신은 현재 **오케스트레이터**(이 도구를 실행 중인 AI)입니다. 자기 자신을 자동으로 spawn하려고 하면 무한 루프(자신이 자신을 호출하고 다시 호출하는...)가 발생하여 타임아웃이 됩니다.\n\n` +
|
|
3105
|
+
`위의 주제와 이력을 분석하여 응답을 작성한 뒤, \`deliberation_respond\`를 직접 호출해 주세요.`,
|
|
3054
3106
|
state?.lang)
|
|
3055
3107
|
}] };
|
|
3056
3108
|
}
|
|
@@ -3094,7 +3146,9 @@ server.tool(
|
|
|
3094
3146
|
child.stdin.end();
|
|
3095
3147
|
break;
|
|
3096
3148
|
case "codex":
|
|
3097
|
-
child = spawn("codex", ["exec", "
|
|
3149
|
+
child = spawn("codex", ["exec", "-"], { env, windowsHide: true });
|
|
3150
|
+
child.stdin.write(turnPrompt);
|
|
3151
|
+
child.stdin.end();
|
|
3098
3152
|
break;
|
|
3099
3153
|
case "gemini":
|
|
3100
3154
|
child = spawn("gemini", ["-p", turnPrompt], { env, windowsHide: true });
|
|
@@ -3123,12 +3177,23 @@ server.tool(
|
|
|
3123
3177
|
// Clean up output noise
|
|
3124
3178
|
let cleaned = stdout;
|
|
3125
3179
|
if (speaker === "codex") {
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3180
|
+
// Codex output includes the prompt and metadata.
|
|
3181
|
+
// Find the line starting with "codex" and take everything after it.
|
|
3182
|
+
const lines = stdout.split("\n");
|
|
3183
|
+
const codexLineIdx = lines.findIndex(l => l.trim() === "codex");
|
|
3184
|
+
if (codexLineIdx !== -1) {
|
|
3185
|
+
cleaned = lines.slice(codexLineIdx + 1)
|
|
3186
|
+
.filter(line => !/^(tokens used$|^[0-9,]*$)/.test(line))
|
|
3187
|
+
.join("\n");
|
|
3188
|
+
} else {
|
|
3189
|
+
// Fallback regex cleaning
|
|
3190
|
+
cleaned = stdout.split("\n")
|
|
3191
|
+
.filter(line => !/^(OpenAI Codex|--------|workdir:|model:|provider:|approval:|sandbox:|reasoning|session id:|user$|mcp:|thinking$|tokens used$|^[0-9,]*$)/.test(line))
|
|
3192
|
+
.join("\n");
|
|
3193
|
+
}
|
|
3129
3194
|
} else if (speaker === "gemini") {
|
|
3130
3195
|
cleaned = stdout.split("\n")
|
|
3131
|
-
.filter(line => !/^(Loaded cached|Error during discovery)/.test(line))
|
|
3196
|
+
.filter(line => !/^(Loaded cached|Error during discovery|\[MCP error\]| {4}at| {2}errno:| {2}code:| {2}syscall:| {2}path:| {2}spawnargs:|MCP issues detected|Server .* supports tool updates)/.test(line))
|
|
3132
3197
|
.join("\n");
|
|
3133
3198
|
}
|
|
3134
3199
|
resolve(cleaned.trim());
|
|
@@ -3184,9 +3249,11 @@ server.tool(
|
|
|
3184
3249
|
speaker: z.string().trim().min(1).max(64).describe("Responder name"),
|
|
3185
3250
|
content: z.string().optional().describe("Response content (markdown). Either content or content_file is required."),
|
|
3186
3251
|
content_file: z.string().optional().describe("File path containing response content. For avoiding JSON escape issues. File content is used as-is for content."),
|
|
3252
|
+
use_clipboard: z.boolean().optional().describe("Read content from system clipboard (alternative to content/content_file)"),
|
|
3253
|
+
include_clipboard_image: z.boolean().optional().describe("Capture and include image from system clipboard"),
|
|
3187
3254
|
turn_id: z.string().optional().describe("Turn verification ID (value received from deliberation_route_turn)"),
|
|
3188
3255
|
},
|
|
3189
|
-
safeToolHandler("deliberation_respond", async ({ session_id, speaker, content, content_file, turn_id }) => {
|
|
3256
|
+
safeToolHandler("deliberation_respond", async ({ session_id, speaker, content, content_file, use_clipboard, include_clipboard_image, turn_id }) => {
|
|
3190
3257
|
// Guard: prevent orchestrator from fabricating responses for CLI/browser speakers
|
|
3191
3258
|
const resolved = resolveSessionId(session_id);
|
|
3192
3259
|
if (resolved && resolved !== "MULTIPLE") {
|
|
@@ -3212,22 +3279,125 @@ server.tool(
|
|
|
3212
3279
|
}
|
|
3213
3280
|
}
|
|
3214
3281
|
|
|
3215
|
-
// Support reading content from file to avoid JSON escaping issues
|
|
3282
|
+
// Support reading content from file or clipboard to avoid JSON escaping issues
|
|
3216
3283
|
let finalContent = content;
|
|
3217
|
-
if (
|
|
3284
|
+
if (use_clipboard && !content) {
|
|
3285
|
+
try {
|
|
3286
|
+
finalContent = readClipboardText();
|
|
3287
|
+
} catch (e) {
|
|
3288
|
+
return { content: [{ type: "text", text: t(`❌ Failed to read from clipboard: ${e.message}`, `❌ 클립보드 읽기 실패: ${e.message}`, state?.lang) }] };
|
|
3289
|
+
}
|
|
3290
|
+
} else if (content_file && !content) {
|
|
3218
3291
|
try {
|
|
3219
3292
|
finalContent = fs.readFileSync(content_file, "utf-8").trim();
|
|
3220
3293
|
} catch (e) {
|
|
3221
3294
|
return { content: [{ type: "text", text: t(`❌ Failed to read content_file: ${e.message}`, `❌ content_file 읽기 실패: ${e.message}`, state?.lang) }] };
|
|
3222
3295
|
}
|
|
3223
3296
|
}
|
|
3224
|
-
if (!finalContent) {
|
|
3225
|
-
return { content: [{ type: "text", text: t("❌ Either content or
|
|
3297
|
+
if (!finalContent && !include_clipboard_image) {
|
|
3298
|
+
return { content: [{ type: "text", text: t("❌ Either content, content_file, or include_clipboard_image must be provided.", "❌ content, content_file 또는 include_clipboard_image 중 하나를 제공해야 합니다.", "en") }] };
|
|
3299
|
+
}
|
|
3300
|
+
|
|
3301
|
+
const attachments = [];
|
|
3302
|
+
if (include_clipboard_image) {
|
|
3303
|
+
if (process.platform !== "darwin") {
|
|
3304
|
+
return { content: [{ type: "text", text: "❌ Clipboard image capture is currently only supported on macOS." }] };
|
|
3305
|
+
}
|
|
3306
|
+
if (!hasClipboardImage()) {
|
|
3307
|
+
return { content: [{ type: "text", text: t("❌ No image found on clipboard.", "❌ 클립보드에서 이미지를 찾을 수 없습니다.", state?.lang) }] };
|
|
3308
|
+
}
|
|
3309
|
+
|
|
3310
|
+
const attachmentsDir = path.join(getSessionsDir(), `${resolved}_attachments`);
|
|
3311
|
+
if (!fs.existsSync(attachmentsDir)) fs.mkdirSync(attachmentsDir, { recursive: true });
|
|
3312
|
+
|
|
3313
|
+
const imgName = `img_${Date.now()}.png`;
|
|
3314
|
+
const imgPath = path.join(attachmentsDir, imgName);
|
|
3315
|
+
|
|
3316
|
+
if (captureClipboardImage(imgPath)) {
|
|
3317
|
+
attachments.push({ type: "image", path: `attachments/${imgName}`, localPath: imgPath });
|
|
3318
|
+
} else {
|
|
3319
|
+
return { content: [{ type: "text", text: "❌ Failed to capture image from clipboard." }] };
|
|
3320
|
+
}
|
|
3321
|
+
}
|
|
3322
|
+
|
|
3323
|
+
return submitDeliberationTurn({ session_id, speaker, content: finalContent || "(Image response)", turn_id, channel_used: "cli_respond", attachments });
|
|
3324
|
+
})
|
|
3325
|
+
);
|
|
3326
|
+
|
|
3327
|
+
server.tool(
|
|
3328
|
+
"deliberation_inject_context",
|
|
3329
|
+
"Inject additional context or instructions into a specific active session. (Useful for local or remote context injection via Tailscale)",
|
|
3330
|
+
{
|
|
3331
|
+
session_id: z.string().describe("Session ID to inject context into"),
|
|
3332
|
+
context: z.string().describe("The context text to inject"),
|
|
3333
|
+
speaker: z.string().default("system").describe("Optional label for who injected the context (default: 'system')"),
|
|
3334
|
+
},
|
|
3335
|
+
safeToolHandler("deliberation_inject_context", async ({ session_id, context, speaker }) => {
|
|
3336
|
+
const resolved = resolveSessionId(session_id);
|
|
3337
|
+
if (!resolved) {
|
|
3338
|
+
return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
|
|
3226
3339
|
}
|
|
3227
|
-
|
|
3340
|
+
if (resolved === "MULTIPLE") {
|
|
3341
|
+
return { content: [{ type: "text", text: multipleSessionsError() }] };
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
return withSessionLock(resolved, () => {
|
|
3345
|
+
const state = loadSession(resolved);
|
|
3346
|
+
if (!state || state.status !== "active") {
|
|
3347
|
+
return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
|
|
3348
|
+
}
|
|
3349
|
+
|
|
3350
|
+
state.log.push({
|
|
3351
|
+
round: state.current_round,
|
|
3352
|
+
speaker: speaker || "system",
|
|
3353
|
+
content: `[Context Injection]\n${context}`,
|
|
3354
|
+
timestamp: new Date().toISOString(),
|
|
3355
|
+
event: "context_injection",
|
|
3356
|
+
});
|
|
3357
|
+
|
|
3358
|
+
appendRuntimeLog("INFO", `CONTEXT_INJECTION: ${state.id} | speaker: ${speaker || "system"} | length: ${context.length}`);
|
|
3359
|
+
saveSession(state);
|
|
3360
|
+
|
|
3361
|
+
return {
|
|
3362
|
+
content: [{
|
|
3363
|
+
type: "text",
|
|
3364
|
+
text: `✅ Context successfully injected into session "${state.id}".`,
|
|
3365
|
+
}],
|
|
3366
|
+
};
|
|
3367
|
+
});
|
|
3228
3368
|
})
|
|
3229
3369
|
);
|
|
3230
3370
|
|
|
3371
|
+
server.tool(
|
|
3372
|
+
"deliberation_copy_last_turn",
|
|
3373
|
+
"Copy the last turn's response to the system clipboard.",
|
|
3374
|
+
{
|
|
3375
|
+
session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
|
|
3376
|
+
},
|
|
3377
|
+
async ({ session_id }) => {
|
|
3378
|
+
const resolved = resolveSessionId(session_id);
|
|
3379
|
+
if (!resolved || resolved === "MULTIPLE") {
|
|
3380
|
+
return { content: [{ type: "text", text: t("No unique active deliberation found.", "고유한 활성 deliberation을 찾을 수 없습니다.", "en") }] };
|
|
3381
|
+
}
|
|
3382
|
+
const state = loadSession(resolved);
|
|
3383
|
+
if (!state || state.log.length === 0) {
|
|
3384
|
+
return { content: [{ type: "text", text: t("No responses yet.", "아직 응답이 없습니다.", "en") }] };
|
|
3385
|
+
}
|
|
3386
|
+
const last = state.log[state.log.length - 1];
|
|
3387
|
+
try {
|
|
3388
|
+
writeClipboardText(last.content);
|
|
3389
|
+
let imgMsg = "";
|
|
3390
|
+
if (last.attachments && last.attachments.length > 0) {
|
|
3391
|
+
const hasImg = last.attachments.some(a => a.type === "image");
|
|
3392
|
+
if (hasImg) imgMsg = "\n\n⚠️ Note: This response included images, but only text was copied to the clipboard.";
|
|
3393
|
+
}
|
|
3394
|
+
return { content: [{ type: "text", text: `📋 **[${last.speaker}]'s response copied to clipboard.** (Round ${last.round})${imgMsg}\n\nYou can now paste it into other tools using Cmd+V (ㅍ).` }] };
|
|
3395
|
+
} catch (e) {
|
|
3396
|
+
return { content: [{ type: "text", text: `❌ Failed to copy to clipboard: ${e.message}` }] };
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
);
|
|
3400
|
+
|
|
3231
3401
|
server.tool(
|
|
3232
3402
|
"deliberation_history",
|
|
3233
3403
|
"Return the deliberation history.",
|
|
@@ -3295,6 +3465,9 @@ server.tool(
|
|
|
3295
3465
|
saveSession(loaded);
|
|
3296
3466
|
archivePath = archiveState(loaded);
|
|
3297
3467
|
cleanupSyncMarkdown(loaded);
|
|
3468
|
+
// Clean up the active session JSON file upon completion
|
|
3469
|
+
const sessionFile = getSessionFile(loaded.id);
|
|
3470
|
+
try { if (fs.existsSync(sessionFile)) fs.unlinkSync(sessionFile); } catch { /* ignore */ }
|
|
3298
3471
|
state = loaded;
|
|
3299
3472
|
return null;
|
|
3300
3473
|
});
|
|
@@ -3528,7 +3701,7 @@ server.tool(
|
|
|
3528
3701
|
function invokeCliReviewer(command, prompt, timeoutMs) {
|
|
3529
3702
|
const hint = CLI_INVOCATION_HINTS[command];
|
|
3530
3703
|
let args;
|
|
3531
|
-
let opts = { encoding: "utf-8", timeout: timeoutMs, stdio: ["pipe", "pipe", "pipe"], maxBuffer:
|
|
3704
|
+
let opts = { encoding: "utf-8", timeout: timeoutMs, stdio: ["pipe", "pipe", "pipe"], maxBuffer: 10 * 1024 * 1024, windowsHide: true };
|
|
3532
3705
|
const env = { ...process.env };
|
|
3533
3706
|
|
|
3534
3707
|
switch (command) {
|
|
@@ -3536,11 +3709,10 @@ function invokeCliReviewer(command, prompt, timeoutMs) {
|
|
|
3536
3709
|
if (hint?.envPrefix?.includes("CLAUDECODE=")) delete env.CLAUDECODE;
|
|
3537
3710
|
args = ["-p", "--output-format", "text", "--no-input"];
|
|
3538
3711
|
opts.input = prompt;
|
|
3539
|
-
opts.stdio = ["pipe", "pipe", "pipe"];
|
|
3540
3712
|
break;
|
|
3541
3713
|
case "codex":
|
|
3542
|
-
args = ["exec",
|
|
3543
|
-
opts.
|
|
3714
|
+
args = ["exec", "-"];
|
|
3715
|
+
opts.input = prompt;
|
|
3544
3716
|
break;
|
|
3545
3717
|
case "gemini":
|
|
3546
3718
|
args = ["-p", prompt];
|
|
@@ -3556,7 +3728,17 @@ function invokeCliReviewer(command, prompt, timeoutMs) {
|
|
|
3556
3728
|
|
|
3557
3729
|
try {
|
|
3558
3730
|
const result = execFileSync(command, args, { ...opts, env });
|
|
3559
|
-
|
|
3731
|
+
let cleaned = result;
|
|
3732
|
+
if (command === "codex") {
|
|
3733
|
+
const lines = result.split("\n");
|
|
3734
|
+
const codexLineIdx = lines.findIndex(l => l.trim() === "codex");
|
|
3735
|
+
if (codexLineIdx !== -1) {
|
|
3736
|
+
cleaned = lines.slice(codexLineIdx + 1)
|
|
3737
|
+
.filter(line => !/^(tokens used$|^[0-9,]*$)/.test(line))
|
|
3738
|
+
.join("\n");
|
|
3739
|
+
}
|
|
3740
|
+
}
|
|
3741
|
+
return { ok: true, response: cleaned.trim() };
|
|
3560
3742
|
} catch (error) {
|
|
3561
3743
|
if (error && error.killed) {
|
|
3562
3744
|
return { ok: false, error: "timeout" };
|
|
@@ -3896,24 +4078,24 @@ server.tool(
|
|
|
3896
4078
|
let stdout = "";
|
|
3897
4079
|
let stderr = "";
|
|
3898
4080
|
|
|
3899
|
-
|
|
4081
|
+
let proc;
|
|
4082
|
+
const env = { ...process.env, NO_COLOR: "1" };
|
|
4083
|
+
|
|
3900
4084
|
if (speaker === "claude") {
|
|
3901
|
-
|
|
4085
|
+
proc = spawn("claude", ["-p", "--output-format", "text", "--no-input"], { env, windowsHide: true });
|
|
4086
|
+
proc.stdin.write(opinionPrompt);
|
|
4087
|
+
proc.stdin.end();
|
|
3902
4088
|
} else if (speaker === "codex") {
|
|
3903
|
-
|
|
4089
|
+
proc = spawn("codex", ["exec", "-"], { env, windowsHide: true });
|
|
4090
|
+
proc.stdin.write(opinionPrompt);
|
|
4091
|
+
proc.stdin.end();
|
|
3904
4092
|
} else if (speaker === "gemini") {
|
|
3905
|
-
|
|
4093
|
+
proc = spawn("gemini", ["-p", opinionPrompt], { env, windowsHide: true });
|
|
3906
4094
|
} else {
|
|
3907
|
-
const flags = hint?.flags
|
|
3908
|
-
|
|
4095
|
+
const flags = hint?.flags ? (Array.isArray(hint.flags) ? hint.flags : hint.flags.split(/\s+/)) : [];
|
|
4096
|
+
proc = spawn(cmd, [...flags, opinionPrompt], { env, windowsHide: true });
|
|
3909
4097
|
}
|
|
3910
4098
|
|
|
3911
|
-
const proc = spawn(cmd, args, {
|
|
3912
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
3913
|
-
env: { ...process.env, NO_COLOR: "1" },
|
|
3914
|
-
timeout: 180000,
|
|
3915
|
-
});
|
|
3916
|
-
|
|
3917
4099
|
proc.stdout?.on("data", (d) => { stdout += d.toString(); });
|
|
3918
4100
|
proc.stderr?.on("data", (d) => { stderr += d.toString(); });
|
|
3919
4101
|
|
|
@@ -3924,7 +4106,21 @@ server.tool(
|
|
|
3924
4106
|
|
|
3925
4107
|
proc.on("close", (code) => {
|
|
3926
4108
|
clearTimeout(timer);
|
|
3927
|
-
|
|
4109
|
+
let cleaned = stdout.trim() || stderr.trim();
|
|
4110
|
+
if (speaker === "codex") {
|
|
4111
|
+
const lines = cleaned.split("\n");
|
|
4112
|
+
const codexLineIdx = lines.findIndex(l => l.trim() === "codex");
|
|
4113
|
+
if (codexLineIdx !== -1) {
|
|
4114
|
+
cleaned = lines.slice(codexLineIdx + 1)
|
|
4115
|
+
.filter(line => !/^(tokens used$|^[0-9,]*$)/.test(line))
|
|
4116
|
+
.join("\n").trim();
|
|
4117
|
+
}
|
|
4118
|
+
} else if (speaker === "gemini") {
|
|
4119
|
+
cleaned = cleaned.split("\n")
|
|
4120
|
+
.filter(line => !/^(Loaded cached|Error during discovery|\[MCP error\]| {4}at| {2}errno:| {2}code:| {2}syscall:| {2}path:| {2}spawnargs:|MCP issues detected|Server .* supports tool updates)/.test(line))
|
|
4121
|
+
.join("\n").trim();
|
|
4122
|
+
}
|
|
4123
|
+
resolve(cleaned);
|
|
3928
4124
|
});
|
|
3929
4125
|
|
|
3930
4126
|
proc.on("error", (err) => {
|