@dmsdc-ai/aigentry-deliberation 0.0.30 → 0.0.32
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 +327 -125
- 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;
|
|
@@ -1423,10 +1362,16 @@ function mapParticipantProfiles(speakers, candidates, typeOverrides) {
|
|
|
1423
1362
|
// Check for explicit type override
|
|
1424
1363
|
const overrideType = overrides[speaker] || overrides[raw];
|
|
1425
1364
|
if (overrideType) {
|
|
1365
|
+
const candidate = bySpeaker.get(speaker);
|
|
1426
1366
|
profiles.push({
|
|
1427
1367
|
speaker,
|
|
1428
1368
|
type: overrideType,
|
|
1429
|
-
...(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
|
+
} : {}),
|
|
1430
1375
|
});
|
|
1431
1376
|
continue;
|
|
1432
1377
|
}
|
|
@@ -1508,7 +1453,7 @@ function resolveTransportForSpeaker(state, speaker) {
|
|
|
1508
1453
|
// CLI-specific invocation flags for non-interactive execution
|
|
1509
1454
|
const CLI_INVOCATION_HINTS = {
|
|
1510
1455
|
claude: { cmd: "claude", flags: '-p --output-format text', example: 'CLAUDECODE= claude -p --output-format text "prompt"', envPrefix: 'CLAUDECODE=', modelFlag: '--model', provider: 'claude' },
|
|
1511
|
-
codex: { cmd: "codex", flags: 'exec
|
|
1456
|
+
codex: { cmd: "codex", flags: 'exec -', example: 'echo "prompt" | codex exec -', stdinMode: true, modelFlag: '--model', defaultModel: 'default', provider: 'chatgpt' },
|
|
1512
1457
|
gemini: { cmd: "gemini", flags: '', example: 'gemini "prompt"', modelFlag: '--model', provider: 'gemini' },
|
|
1513
1458
|
aider: { cmd: "aider", flags: '--message', example: 'aider --message "prompt"', modelFlag: '--model', provider: 'chatgpt' },
|
|
1514
1459
|
cursor: { cmd: "cursor", flags: '', example: 'cursor "prompt"', modelFlag: null, provider: 'chatgpt' },
|
|
@@ -1534,12 +1479,18 @@ function formatTransportGuidance(transport, state, speaker) {
|
|
|
1534
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.`;
|
|
1535
1480
|
}
|
|
1536
1481
|
case "clipboard":
|
|
1537
|
-
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.`;
|
|
1538
1486
|
case "browser_auto":
|
|
1539
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.`;
|
|
1540
1488
|
case "manual":
|
|
1541
1489
|
default:
|
|
1542
|
-
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.`;
|
|
1543
1494
|
}
|
|
1544
1495
|
}
|
|
1545
1496
|
|
|
@@ -1780,7 +1731,15 @@ tags: [deliberation]
|
|
|
1780
1731
|
if (entry.fallback_reason) parts.push(`fallback: ${entry.fallback_reason}`);
|
|
1781
1732
|
md += `> _${parts.join(" | ")}_\n\n`;
|
|
1782
1733
|
}
|
|
1783
|
-
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`;
|
|
1784
1743
|
}
|
|
1785
1744
|
return md;
|
|
1786
1745
|
}
|
|
@@ -1873,6 +1832,22 @@ function tmuxHasAttachedClients(sessionName) {
|
|
|
1873
1832
|
}
|
|
1874
1833
|
}
|
|
1875
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
|
+
|
|
1876
1851
|
function tmuxWindowCount(name) {
|
|
1877
1852
|
try {
|
|
1878
1853
|
const output = execFileSync("tmux", ["list-windows", "-t", name], {
|
|
@@ -1938,9 +1913,20 @@ function openPhysicalTerminal(sessionId) {
|
|
|
1938
1913
|
// Use grouped session (new-session -t) for independent active window per client
|
|
1939
1914
|
const attachCmd = `tmux new-session -t "${TMUX_SESSION}" \\; select-window -t "${TMUX_SESSION}:${winName}"`;
|
|
1940
1915
|
|
|
1941
|
-
//
|
|
1942
|
-
//
|
|
1943
|
-
|
|
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).
|
|
1944
1930
|
if (tmuxHasAttachedClients(TMUX_SESSION)) {
|
|
1945
1931
|
if (process.platform === "darwin") {
|
|
1946
1932
|
const groupAttachCmd = `tmux new-session -t "${TMUX_SESSION}" \\; select-window -t "${TMUX_SESSION}:${winName}"`;
|
|
@@ -2072,17 +2058,20 @@ function spawnMonitorTerminal(sessionId) {
|
|
|
2072
2058
|
if (hasTmuxSession(TMUX_SESSION)) {
|
|
2073
2059
|
// Skip if a window with the same name already exists (prevents duplicates)
|
|
2074
2060
|
if (hasTmuxWindow(TMUX_SESSION, winName)) {
|
|
2061
|
+
appendRuntimeLog("INFO", `TMUX_WINDOW_EXISTS: ${winName} in ${TMUX_SESSION}`);
|
|
2075
2062
|
return true;
|
|
2076
2063
|
}
|
|
2077
2064
|
execFileSync("tmux", ["new-window", "-t", TMUX_SESSION, "-n", winName, cmd], {
|
|
2078
2065
|
stdio: "ignore",
|
|
2079
2066
|
windowsHide: true,
|
|
2080
2067
|
});
|
|
2068
|
+
appendRuntimeLog("INFO", `TMUX_WINDOW_CREATED: ${winName} in existing ${TMUX_SESSION}`);
|
|
2081
2069
|
} else {
|
|
2082
2070
|
execFileSync("tmux", ["new-session", "-d", "-s", TMUX_SESSION, "-n", winName, cmd], {
|
|
2083
2071
|
stdio: "ignore",
|
|
2084
2072
|
windowsHide: true,
|
|
2085
2073
|
});
|
|
2074
|
+
appendRuntimeLog("INFO", `TMUX_SESSION_CREATED: ${TMUX_SESSION} with window ${winName}`);
|
|
2086
2075
|
}
|
|
2087
2076
|
return true;
|
|
2088
2077
|
} catch {
|
|
@@ -2319,7 +2308,7 @@ ${recent}
|
|
|
2319
2308
|
`;
|
|
2320
2309
|
}
|
|
2321
2310
|
|
|
2322
|
-
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 }) {
|
|
2323
2312
|
const resolved = resolveSessionId(session_id);
|
|
2324
2313
|
if (!resolved) {
|
|
2325
2314
|
return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
|
|
@@ -2384,8 +2373,9 @@ function submitDeliberationTurn({ session_id, speaker, content, turn_id, channel
|
|
|
2384
2373
|
votes: votes.length > 0 ? votes : undefined,
|
|
2385
2374
|
suggested_next_role: suggestedRole !== "free" ? suggestedRole : undefined,
|
|
2386
2375
|
role_drift: roleDrift || undefined,
|
|
2376
|
+
attachments: attachments || undefined,
|
|
2387
2377
|
});
|
|
2388
|
-
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}`);
|
|
2389
2379
|
|
|
2390
2380
|
state.current_speaker = selectNextSpeaker(state);
|
|
2391
2381
|
|
|
@@ -2459,6 +2449,7 @@ server.tool(
|
|
|
2459
2449
|
"Start a new deliberation. Multiple deliberations can run simultaneously.",
|
|
2460
2450
|
{
|
|
2461
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."),
|
|
2462
2453
|
rounds: z.coerce.number().optional().describe("Number of rounds (defaults to config setting, default 3)"),
|
|
2463
2454
|
first_speaker: z.string().trim().min(1).max(64).optional().describe("First speaker name (defaults to first item in speakers)"),
|
|
2464
2455
|
speakers: z.preprocess(
|
|
@@ -2493,9 +2484,9 @@ server.tool(
|
|
|
2493
2484
|
z.record(z.string(), z.enum(["critic", "implementer", "mediator", "researcher", "free"])).optional()
|
|
2494
2485
|
).describe("Per-speaker role assignment (e.g., {\"claude\": \"critic\", \"codex\": \"implementer\"})"),
|
|
2495
2486
|
role_preset: z.enum(["balanced", "debate", "research", "brainstorm", "review", "consensus"]).optional()
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
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 }) => {
|
|
2499
2490
|
// ── First-time onboarding guard ──
|
|
2500
2491
|
const config = loadDeliberationConfig();
|
|
2501
2492
|
if (!config.setup_complete) {
|
|
@@ -2509,7 +2500,13 @@ server.tool(
|
|
|
2509
2500
|
};
|
|
2510
2501
|
}
|
|
2511
2502
|
|
|
2512
|
-
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
|
+
}
|
|
2513
2510
|
const candidateSnapshot = await collectSpeakerCandidates({ include_cli: true, include_browser: true });
|
|
2514
2511
|
|
|
2515
2512
|
// Resolve effective settings from config
|
|
@@ -2821,7 +2818,7 @@ server.tool(
|
|
|
2821
2818
|
}
|
|
2822
2819
|
|
|
2823
2820
|
const speaker = state.current_speaker;
|
|
2824
|
-
|
|
2821
|
+
let { transport, profile, reason } = resolveTransportForSpeaker(state, speaker);
|
|
2825
2822
|
const turnId = state.pending_turn_id || null;
|
|
2826
2823
|
|
|
2827
2824
|
// ── Self-speaker detection ──
|
|
@@ -2833,14 +2830,25 @@ server.tool(
|
|
|
2833
2830
|
|
|
2834
2831
|
let guidance;
|
|
2835
2832
|
if (isSelfSpeaker) {
|
|
2836
|
-
guidance =
|
|
2833
|
+
guidance = t(
|
|
2834
|
+
`🟢 **It's your turn.** You (${speaker}) are the current speaker.\n\n` +
|
|
2837
2835
|
`Write your response and submit via \`deliberation_respond(session_id: "${state.id}", speaker: "${speaker}", content: "...")\`.\n\n` +
|
|
2838
|
-
`⚠️ **
|
|
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
|
+
);
|
|
2839
2846
|
} else {
|
|
2840
2847
|
guidance = formatTransportGuidance(transport, state, speaker);
|
|
2841
2848
|
}
|
|
2842
2849
|
|
|
2843
2850
|
let extra = "";
|
|
2851
|
+
let turnPrompt = "";
|
|
2844
2852
|
|
|
2845
2853
|
if (transport === "browser_auto") {
|
|
2846
2854
|
// Auto-execute browser_auto_turn
|
|
@@ -2854,7 +2862,7 @@ server.tool(
|
|
|
2854
2862
|
const modelSelection = getModelSelectionForTurn(state, turnSpeaker, turnProvider);
|
|
2855
2863
|
|
|
2856
2864
|
// Build prompt
|
|
2857
|
-
|
|
2865
|
+
turnPrompt = buildClipboardTurnPrompt(state, turnSpeaker, prompt, include_history_entries);
|
|
2858
2866
|
|
|
2859
2867
|
// Attach
|
|
2860
2868
|
const attachResult = await port.attach(sessionId, { provider: turnProvider, url: profile?.url });
|
|
@@ -2893,9 +2901,30 @@ server.tool(
|
|
|
2893
2901
|
} catch (autoErr) {
|
|
2894
2902
|
const errMsg = autoErr instanceof Error ? autoErr.message : String(autoErr);
|
|
2895
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
|
+
}
|
|
2896
2910
|
}
|
|
2897
2911
|
}
|
|
2898
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
|
+
}
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
extra += `\n\n### [turn_prompt]\n\`\`\`markdown\n${turnPrompt}\n\`\`\``;
|
|
2926
|
+
}
|
|
2927
|
+
|
|
2899
2928
|
const profileInfo = profile
|
|
2900
2929
|
? `\n**Profile:** ${profile.type}${profile.url ? ` | ${profile.url}` : ""}${profile.command ? ` | command: ${profile.command}` : ""}`
|
|
2901
2930
|
: "";
|
|
@@ -3061,13 +3090,19 @@ server.tool(
|
|
|
3061
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) }] };
|
|
3062
3091
|
}
|
|
3063
3092
|
|
|
3064
|
-
// Block recursive self-spawn: if the speaker is the same CLI as the caller,
|
|
3065
|
-
// spawning it would create infinite recursion and timeout.
|
|
3066
3093
|
const callerSpeaker = detectCallerSpeaker();
|
|
3067
3094
|
if (callerSpeaker && speaker === callerSpeaker) {
|
|
3068
3095
|
return { content: [{ type: "text", text: t(
|
|
3069
|
-
|
|
3070
|
-
|
|
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\`를 직접 호출해 주세요.`,
|
|
3071
3106
|
state?.lang)
|
|
3072
3107
|
}] };
|
|
3073
3108
|
}
|
|
@@ -3111,7 +3146,9 @@ server.tool(
|
|
|
3111
3146
|
child.stdin.end();
|
|
3112
3147
|
break;
|
|
3113
3148
|
case "codex":
|
|
3114
|
-
child = spawn("codex", ["exec", "
|
|
3149
|
+
child = spawn("codex", ["exec", "-"], { env, windowsHide: true });
|
|
3150
|
+
child.stdin.write(turnPrompt);
|
|
3151
|
+
child.stdin.end();
|
|
3115
3152
|
break;
|
|
3116
3153
|
case "gemini":
|
|
3117
3154
|
child = spawn("gemini", ["-p", turnPrompt], { env, windowsHide: true });
|
|
@@ -3140,12 +3177,23 @@ server.tool(
|
|
|
3140
3177
|
// Clean up output noise
|
|
3141
3178
|
let cleaned = stdout;
|
|
3142
3179
|
if (speaker === "codex") {
|
|
3143
|
-
|
|
3144
|
-
|
|
3145
|
-
|
|
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
|
+
}
|
|
3146
3194
|
} else if (speaker === "gemini") {
|
|
3147
3195
|
cleaned = stdout.split("\n")
|
|
3148
|
-
.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))
|
|
3149
3197
|
.join("\n");
|
|
3150
3198
|
}
|
|
3151
3199
|
resolve(cleaned.trim());
|
|
@@ -3201,9 +3249,11 @@ server.tool(
|
|
|
3201
3249
|
speaker: z.string().trim().min(1).max(64).describe("Responder name"),
|
|
3202
3250
|
content: z.string().optional().describe("Response content (markdown). Either content or content_file is required."),
|
|
3203
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"),
|
|
3204
3254
|
turn_id: z.string().optional().describe("Turn verification ID (value received from deliberation_route_turn)"),
|
|
3205
3255
|
},
|
|
3206
|
-
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 }) => {
|
|
3207
3257
|
// Guard: prevent orchestrator from fabricating responses for CLI/browser speakers
|
|
3208
3258
|
const resolved = resolveSessionId(session_id);
|
|
3209
3259
|
if (resolved && resolved !== "MULTIPLE") {
|
|
@@ -3229,22 +3279,148 @@ server.tool(
|
|
|
3229
3279
|
}
|
|
3230
3280
|
}
|
|
3231
3281
|
|
|
3232
|
-
// Support reading content from file to avoid JSON escaping issues
|
|
3282
|
+
// Support reading content from file or clipboard to avoid JSON escaping issues
|
|
3233
3283
|
let finalContent = content;
|
|
3234
|
-
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) {
|
|
3235
3291
|
try {
|
|
3236
3292
|
finalContent = fs.readFileSync(content_file, "utf-8").trim();
|
|
3237
3293
|
} catch (e) {
|
|
3238
3294
|
return { content: [{ type: "text", text: t(`❌ Failed to read content_file: ${e.message}`, `❌ content_file 읽기 실패: ${e.message}`, state?.lang) }] };
|
|
3239
3295
|
}
|
|
3240
3296
|
}
|
|
3241
|
-
if (!finalContent) {
|
|
3242
|
-
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
|
+
}
|
|
3243
3321
|
}
|
|
3244
|
-
|
|
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
|
+
remote_url: z.string().optional().describe("Optional Tailscale IP/Host and port (e.g., '100.100.100.5:3847') of the remote machine running the session. If provided, context is injected remotely."),
|
|
3335
|
+
},
|
|
3336
|
+
safeToolHandler("deliberation_inject_context", async ({ session_id, context, speaker, remote_url }) => {
|
|
3337
|
+
if (remote_url) {
|
|
3338
|
+
try {
|
|
3339
|
+
const baseUrl = remote_url.startsWith("http") ? remote_url : `http://${remote_url}`;
|
|
3340
|
+
// Ensure trailing slash is removed
|
|
3341
|
+
const cleanBaseUrl = baseUrl.replace(/\/$/, "");
|
|
3342
|
+
const response = await fetch(`${cleanBaseUrl}/api/sessions/${encodeURIComponent(session_id)}/context`, {
|
|
3343
|
+
method: "POST",
|
|
3344
|
+
headers: { "Content-Type": "application/json" },
|
|
3345
|
+
body: JSON.stringify({ context, speaker: speaker || "system" })
|
|
3346
|
+
});
|
|
3347
|
+
|
|
3348
|
+
if (!response.ok) {
|
|
3349
|
+
let errText = await response.text();
|
|
3350
|
+
try { errText = JSON.parse(errText).error || errText; } catch { /* ignore */ }
|
|
3351
|
+
return { content: [{ type: "text", text: `❌ Remote context injection failed (${response.status}): ${errText}` }] };
|
|
3352
|
+
}
|
|
3353
|
+
return { content: [{ type: "text", text: `✅ Context successfully injected remotely into session "${session_id}" at ${remote_url}.` }] };
|
|
3354
|
+
} catch (e) {
|
|
3355
|
+
return { content: [{ type: "text", text: `❌ Error connecting to remote observer at ${remote_url}: ${e.message}` }] };
|
|
3356
|
+
}
|
|
3357
|
+
}
|
|
3358
|
+
|
|
3359
|
+
const resolved = resolveSessionId(session_id);
|
|
3360
|
+
if (!resolved) {
|
|
3361
|
+
return { content: [{ type: "text", text: t("No active deliberation.", "활성 deliberation이 없습니다.", "en") }] };
|
|
3362
|
+
}
|
|
3363
|
+
if (resolved === "MULTIPLE") {
|
|
3364
|
+
return { content: [{ type: "text", text: multipleSessionsError() }] };
|
|
3365
|
+
}
|
|
3366
|
+
|
|
3367
|
+
return withSessionLock(resolved, () => {
|
|
3368
|
+
const state = loadSession(resolved);
|
|
3369
|
+
if (!state || state.status !== "active") {
|
|
3370
|
+
return { content: [{ type: "text", text: t(`Session "${resolved}" is not active.`, `세션 "${resolved}"이 활성 상태가 아닙니다.`, "en") }] };
|
|
3371
|
+
}
|
|
3372
|
+
|
|
3373
|
+
state.log.push({
|
|
3374
|
+
round: state.current_round,
|
|
3375
|
+
speaker: speaker || "system",
|
|
3376
|
+
content: `[Context Injection]\n${context}`,
|
|
3377
|
+
timestamp: new Date().toISOString(),
|
|
3378
|
+
event: "context_injection",
|
|
3379
|
+
});
|
|
3380
|
+
|
|
3381
|
+
appendRuntimeLog("INFO", `CONTEXT_INJECTION: ${state.id} | speaker: ${speaker || "system"} | length: ${context.length}`);
|
|
3382
|
+
saveSession(state);
|
|
3383
|
+
|
|
3384
|
+
return {
|
|
3385
|
+
content: [{
|
|
3386
|
+
type: "text",
|
|
3387
|
+
text: `✅ Context successfully injected into session "${state.id}".`,
|
|
3388
|
+
}],
|
|
3389
|
+
};
|
|
3390
|
+
});
|
|
3245
3391
|
})
|
|
3246
3392
|
);
|
|
3247
3393
|
|
|
3394
|
+
server.tool(
|
|
3395
|
+
"deliberation_copy_last_turn",
|
|
3396
|
+
"Copy the last turn's response to the system clipboard.",
|
|
3397
|
+
{
|
|
3398
|
+
session_id: z.string().optional().describe("Session ID (required if multiple sessions are active)"),
|
|
3399
|
+
},
|
|
3400
|
+
async ({ session_id }) => {
|
|
3401
|
+
const resolved = resolveSessionId(session_id);
|
|
3402
|
+
if (!resolved || resolved === "MULTIPLE") {
|
|
3403
|
+
return { content: [{ type: "text", text: t("No unique active deliberation found.", "고유한 활성 deliberation을 찾을 수 없습니다.", "en") }] };
|
|
3404
|
+
}
|
|
3405
|
+
const state = loadSession(resolved);
|
|
3406
|
+
if (!state || state.log.length === 0) {
|
|
3407
|
+
return { content: [{ type: "text", text: t("No responses yet.", "아직 응답이 없습니다.", "en") }] };
|
|
3408
|
+
}
|
|
3409
|
+
const last = state.log[state.log.length - 1];
|
|
3410
|
+
try {
|
|
3411
|
+
writeClipboardText(last.content);
|
|
3412
|
+
let imgMsg = "";
|
|
3413
|
+
if (last.attachments && last.attachments.length > 0) {
|
|
3414
|
+
const hasImg = last.attachments.some(a => a.type === "image");
|
|
3415
|
+
if (hasImg) imgMsg = "\n\n⚠️ Note: This response included images, but only text was copied to the clipboard.";
|
|
3416
|
+
}
|
|
3417
|
+
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 (ㅍ).` }] };
|
|
3418
|
+
} catch (e) {
|
|
3419
|
+
return { content: [{ type: "text", text: `❌ Failed to copy to clipboard: ${e.message}` }] };
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
);
|
|
3423
|
+
|
|
3248
3424
|
server.tool(
|
|
3249
3425
|
"deliberation_history",
|
|
3250
3426
|
"Return the deliberation history.",
|
|
@@ -3312,6 +3488,9 @@ server.tool(
|
|
|
3312
3488
|
saveSession(loaded);
|
|
3313
3489
|
archivePath = archiveState(loaded);
|
|
3314
3490
|
cleanupSyncMarkdown(loaded);
|
|
3491
|
+
// Clean up the active session JSON file upon completion
|
|
3492
|
+
const sessionFile = getSessionFile(loaded.id);
|
|
3493
|
+
try { if (fs.existsSync(sessionFile)) fs.unlinkSync(sessionFile); } catch { /* ignore */ }
|
|
3315
3494
|
state = loaded;
|
|
3316
3495
|
return null;
|
|
3317
3496
|
});
|
|
@@ -3545,7 +3724,7 @@ server.tool(
|
|
|
3545
3724
|
function invokeCliReviewer(command, prompt, timeoutMs) {
|
|
3546
3725
|
const hint = CLI_INVOCATION_HINTS[command];
|
|
3547
3726
|
let args;
|
|
3548
|
-
let opts = { encoding: "utf-8", timeout: timeoutMs, stdio: ["pipe", "pipe", "pipe"], maxBuffer:
|
|
3727
|
+
let opts = { encoding: "utf-8", timeout: timeoutMs, stdio: ["pipe", "pipe", "pipe"], maxBuffer: 10 * 1024 * 1024, windowsHide: true };
|
|
3549
3728
|
const env = { ...process.env };
|
|
3550
3729
|
|
|
3551
3730
|
switch (command) {
|
|
@@ -3553,11 +3732,10 @@ function invokeCliReviewer(command, prompt, timeoutMs) {
|
|
|
3553
3732
|
if (hint?.envPrefix?.includes("CLAUDECODE=")) delete env.CLAUDECODE;
|
|
3554
3733
|
args = ["-p", "--output-format", "text", "--no-input"];
|
|
3555
3734
|
opts.input = prompt;
|
|
3556
|
-
opts.stdio = ["pipe", "pipe", "pipe"];
|
|
3557
3735
|
break;
|
|
3558
3736
|
case "codex":
|
|
3559
|
-
args = ["exec",
|
|
3560
|
-
opts.
|
|
3737
|
+
args = ["exec", "-"];
|
|
3738
|
+
opts.input = prompt;
|
|
3561
3739
|
break;
|
|
3562
3740
|
case "gemini":
|
|
3563
3741
|
args = ["-p", prompt];
|
|
@@ -3573,7 +3751,17 @@ function invokeCliReviewer(command, prompt, timeoutMs) {
|
|
|
3573
3751
|
|
|
3574
3752
|
try {
|
|
3575
3753
|
const result = execFileSync(command, args, { ...opts, env });
|
|
3576
|
-
|
|
3754
|
+
let cleaned = result;
|
|
3755
|
+
if (command === "codex") {
|
|
3756
|
+
const lines = result.split("\n");
|
|
3757
|
+
const codexLineIdx = lines.findIndex(l => l.trim() === "codex");
|
|
3758
|
+
if (codexLineIdx !== -1) {
|
|
3759
|
+
cleaned = lines.slice(codexLineIdx + 1)
|
|
3760
|
+
.filter(line => !/^(tokens used$|^[0-9,]*$)/.test(line))
|
|
3761
|
+
.join("\n");
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
return { ok: true, response: cleaned.trim() };
|
|
3577
3765
|
} catch (error) {
|
|
3578
3766
|
if (error && error.killed) {
|
|
3579
3767
|
return { ok: false, error: "timeout" };
|
|
@@ -3913,24 +4101,24 @@ server.tool(
|
|
|
3913
4101
|
let stdout = "";
|
|
3914
4102
|
let stderr = "";
|
|
3915
4103
|
|
|
3916
|
-
|
|
4104
|
+
let proc;
|
|
4105
|
+
const env = { ...process.env, NO_COLOR: "1" };
|
|
4106
|
+
|
|
3917
4107
|
if (speaker === "claude") {
|
|
3918
|
-
|
|
4108
|
+
proc = spawn("claude", ["-p", "--output-format", "text", "--no-input"], { env, windowsHide: true });
|
|
4109
|
+
proc.stdin.write(opinionPrompt);
|
|
4110
|
+
proc.stdin.end();
|
|
3919
4111
|
} else if (speaker === "codex") {
|
|
3920
|
-
|
|
4112
|
+
proc = spawn("codex", ["exec", "-"], { env, windowsHide: true });
|
|
4113
|
+
proc.stdin.write(opinionPrompt);
|
|
4114
|
+
proc.stdin.end();
|
|
3921
4115
|
} else if (speaker === "gemini") {
|
|
3922
|
-
|
|
4116
|
+
proc = spawn("gemini", ["-p", opinionPrompt], { env, windowsHide: true });
|
|
3923
4117
|
} else {
|
|
3924
|
-
const flags = hint?.flags
|
|
3925
|
-
|
|
4118
|
+
const flags = hint?.flags ? (Array.isArray(hint.flags) ? hint.flags : hint.flags.split(/\s+/)) : [];
|
|
4119
|
+
proc = spawn(cmd, [...flags, opinionPrompt], { env, windowsHide: true });
|
|
3926
4120
|
}
|
|
3927
4121
|
|
|
3928
|
-
const proc = spawn(cmd, args, {
|
|
3929
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
3930
|
-
env: { ...process.env, NO_COLOR: "1" },
|
|
3931
|
-
timeout: 180000,
|
|
3932
|
-
});
|
|
3933
|
-
|
|
3934
4122
|
proc.stdout?.on("data", (d) => { stdout += d.toString(); });
|
|
3935
4123
|
proc.stderr?.on("data", (d) => { stderr += d.toString(); });
|
|
3936
4124
|
|
|
@@ -3941,7 +4129,21 @@ server.tool(
|
|
|
3941
4129
|
|
|
3942
4130
|
proc.on("close", (code) => {
|
|
3943
4131
|
clearTimeout(timer);
|
|
3944
|
-
|
|
4132
|
+
let cleaned = stdout.trim() || stderr.trim();
|
|
4133
|
+
if (speaker === "codex") {
|
|
4134
|
+
const lines = cleaned.split("\n");
|
|
4135
|
+
const codexLineIdx = lines.findIndex(l => l.trim() === "codex");
|
|
4136
|
+
if (codexLineIdx !== -1) {
|
|
4137
|
+
cleaned = lines.slice(codexLineIdx + 1)
|
|
4138
|
+
.filter(line => !/^(tokens used$|^[0-9,]*$)/.test(line))
|
|
4139
|
+
.join("\n").trim();
|
|
4140
|
+
}
|
|
4141
|
+
} else if (speaker === "gemini") {
|
|
4142
|
+
cleaned = cleaned.split("\n")
|
|
4143
|
+
.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))
|
|
4144
|
+
.join("\n").trim();
|
|
4145
|
+
}
|
|
4146
|
+
resolve(cleaned);
|
|
3945
4147
|
});
|
|
3946
4148
|
|
|
3947
4149
|
proc.on("error", (err) => {
|