jskim-node #388

Merged
kjs merged 58 commits from jskim-node into main 2026-02-13 09:59:55 +09:00
4 changed files with 5612 additions and 47 deletions
Showing only changes of commit 08dde416b1 - Show all commits

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,15 +16,12 @@ import {
CallToolRequestSchema, CallToolRequestSchema,
ListToolsRequestSchema, ListToolsRequestSchema,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { exec } from "child_process"; import { spawn } from "child_process";
import { promisify } from "util";
import { platform } from "os"; import { platform } from "os";
import { AGENT_CONFIGS } from "./agents/prompts.js"; import { AGENT_CONFIGS } from "./agents/prompts.js";
import { AgentType, ParallelResult } from "./agents/types.js"; import { AgentType, ParallelResult } from "./agents/types.js";
import { logger } from "./utils/logger.js"; import { logger } from "./utils/logger.js";
const execAsync = promisify(exec);
// OS 감지 // OS 감지
const isWindows = platform() === "win32"; const isWindows = platform() === "win32";
logger.info(`Platform detected: ${platform()} (isWindows: ${isWindows})`); logger.info(`Platform detected: ${platform()} (isWindows: ${isWindows})`);
@ -46,9 +43,11 @@ const server = new Server(
* Cursor Agent CLI를 * Cursor Agent CLI를
* Cursor Team Plan - API ! * Cursor Team Plan - API !
* *
* spawn + stdin
*
* : * :
* - Windows: cmd /c "echo. | agent ..." (stdin ) * - Windows: agent (PATH에서 )
* - Mac/Linux: ~/.local/bin/agent * - Mac/Linux: ~/.local/bin/agent
*/ */
async function callAgentCLI( async function callAgentCLI(
agentType: AgentType, agentType: AgentType,
@ -60,56 +59,90 @@ async function callAgentCLI(
// 모델 선택: PM은 opus, 나머지는 sonnet // 모델 선택: PM은 opus, 나머지는 sonnet
const model = agentType === 'pm' ? 'opus-4.5' : 'sonnet-4.5'; const model = agentType === 'pm' ? 'opus-4.5' : 'sonnet-4.5';
logger.info(`Calling ${agentType} agent via CLI`, { model, task: task.substring(0, 100) }); logger.info(`Calling ${agentType} agent via CLI (spawn)`, { model, task: task.substring(0, 100) });
try {
const userMessage = context const userMessage = context
? `${task}\n\n배경 정보:\n${context}` ? `${task}\n\n배경 정보:\n${context}`
: task; : task;
// 프롬프트를 임시 파일에 저장하여 쉘 이스케이프 문제 회피
const fullPrompt = `${config.systemPrompt}\n\n---\n\n${userMessage}`; const fullPrompt = `${config.systemPrompt}\n\n---\n\n${userMessage}`;
// Base64 인코딩으로 특수문자 문제 해결
const encodedPrompt = Buffer.from(fullPrompt).toString('base64');
let cmd: string;
let shell: string;
const agentPath = isWindows ? 'agent' : `${process.env.HOME}/.local/bin/agent`; const agentPath = isWindows ? 'agent' : `${process.env.HOME}/.local/bin/agent`;
if (isWindows) { return new Promise<string>((resolve, reject) => {
// Windows: PowerShell을 통해 Base64 디코딩 후 실행 let stdout = '';
cmd = `powershell -Command "$prompt = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('${encodedPrompt}')); echo $prompt | ${agentPath} --model ${model} --print"`; let stderr = '';
shell = 'powershell.exe'; let settled = false;
} else {
// Mac/Linux: echo로 base64 디코딩 후 파이프
cmd = `echo "${encodedPrompt}" | base64 -d | ${agentPath} --model ${model} --print`;
shell = '/bin/bash';
}
logger.debug(`Executing: ${agentPath} --model ${model} --print`); const child = spawn(agentPath, ['--model', model, '--print'], {
const { stdout, stderr } = await execAsync(cmd, {
cwd: process.cwd(), cwd: process.cwd(),
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
timeout: 300000, // 5분 타임아웃
shell,
env: { env: {
...process.env, ...process.env,
PATH: `${process.env.HOME}/.local/bin:${process.env.PATH}`, PATH: `${process.env.HOME}/.local/bin:${process.env.PATH}`,
}, },
stdio: ['pipe', 'pipe', 'pipe'],
}); });
if (stderr && !stderr.includes('warning') && !stderr.includes('info')) { child.stdout.on('data', (data: Buffer) => {
logger.warn(`${agentType} agent stderr`, { stderr: stderr.substring(0, 500) }); stdout += data.toString();
});
child.stderr.on('data', (data: Buffer) => {
stderr += data.toString();
});
child.on('error', (err: Error) => {
if (!settled) {
settled = true;
logger.error(`${agentType} agent spawn error`, err);
reject(err);
}
});
child.on('close', (code: number | null) => {
if (settled) return;
settled = true;
if (stderr) {
// 경고/정보 레벨 stderr는 무시
const significantStderr = stderr
.split('\n')
.filter((line: string) => line && !line.includes('warning') && !line.includes('info') && !line.includes('debug'))
.join('\n');
if (significantStderr) {
logger.warn(`${agentType} agent stderr`, { stderr: significantStderr.substring(0, 500) });
}
} }
logger.info(`${agentType} agent completed via CLI`); if (code === 0 || stdout.trim().length > 0) {
return stdout.trim(); // 정상 종료이거나, 에러 코드여도 stdout에 결과가 있으면 성공 처리
} catch (error) { logger.info(`${agentType} agent completed via CLI (exit code: ${code})`);
logger.error(`${agentType} agent CLI error`, error); resolve(stdout.trim());
throw error; } else {
const errorMsg = `Agent exited with code ${code}. stderr: ${stderr.substring(0, 1000)}`;
logger.error(`${agentType} agent CLI error`, { code, stderr: stderr.substring(0, 1000) });
reject(new Error(errorMsg));
} }
});
// 타임아웃 (5분)
const timeout = setTimeout(() => {
if (!settled) {
settled = true;
child.kill('SIGTERM');
logger.error(`${agentType} agent timed out after 5 minutes`);
reject(new Error(`${agentType} agent timed out after 5 minutes`));
}
}, 300000);
// 프로세스 종료 시 타이머 클리어
child.on('close', () => clearTimeout(timeout));
// stdin으로 프롬프트 직접 전달 (쉘 이스케이프 문제 없음!)
child.stdin.write(fullPrompt);
child.stdin.end();
logger.debug(`Prompt sent to ${agentType} agent via stdin (${fullPrompt.length} chars)`);
});
} }
/** /**