콘텐츠로 이동

왜 ActiveCLI인가

CLI를 감싸는 일은 배포하기 전까지만 쉽습니다. 배포하고 나면 알게 됩니다. 실행 파일이 내 프로세스가 물려받은 PATH에 없다는 것, Windows는 서로 다른 네 개의 환경이라는 것, 자식을 죽여도 그 자식들은 계속 살아 있다는 것, 그리고 내 프로세스가 강제 종료되면 CLI는 살아남아 계속 과금된다는 것을 말입니다.

이 문제들은 모든 CLI에 동일하며, 이 라이브러리의 대부분이 바로 그것입니다.

문제 실제로 겪는 모습
GUI 프로세스는 최소한의 PATH만 물려받는다 사용자가 brew나 nvm으로 설치한 CLI가 보이지 않는다. spawn이 ENOENT로 실패한다
where claude가 엉뚱한 파일을 지목한다 npm이 claude.cmd 옆에 확장자 없는 래퍼를 함께 설치하는데, cmd.exe는 후자를 실행한다
WSL UNC 경로는 cwd가 될 수 없다 \\wsl.localhost\...는 Windows에서 실패하고, 배포판 안에는 존재하지도 않는다
win32에서 shell: true는 공백에서 쪼갠다 C:\Program Files\ 아래의 런처가 반으로 찢어지고, 인자는 주입 가능해진다
cmd.exe는 따옴표 안에서도 %VAR%를 전개한다 CLI가 보기도 전에 인자가 조용히 다시 쓰인다
cmd.exe는 &에서 명령을 끝낸다 JSON 설정이 잘린 채로 도착한다. CI가 첫 실행에서 이것을 잡았다
유능한 터미널은 출력을 ANSI 이스케이프로 장식한다 \x1b[1 q가 프로토콜과 같은 스트림에 실려 와 JSON 한 줄의 파싱이 실패한다
자식을 죽이는 것은 트리를 죽이는 것이 아니다 서브에이전트 셸과 백그라운드 작업이 계속 돌아간다
git-bash 워커는 트리에서 떨어져 나간다 taskkill /T가 그것들을 보지 못하고, 고아로 살아남는다
강제 SIGKILL은 정리 코드를 하나도 실행하지 않는다 CLI가 호스트보다 오래 살아남고, 이후의 어떤 호스트에도 보이지 않는다

macOS에서는 결코 드러나지 않았을, CI가 잡아낸 것

섹션 제목: “macOS에서는 결코 드러나지 않았을, CI가 잡아낸 것”

Windows CI 첫 실행에서 macOS 머신으로는 드러날 수 없었던 결함 하나가 드러났습니다.

cmd.exe를 통과하는 인자에서 a&b|c<d>e가 a에서 잘렸고, cmd는 'b' is not recognized as an internal or external command를 뱉었습니다. Node의 CommandLineToArgvW 인용이 &를 literal로 만든다는 통념은 틀렸으며, 코드의 원본 주석에도 그렇게 적혀 있었습니다.

mcp add-json이 넘기는 JSON은 정확히 이 문자들로 가득합니다. Windows 사용자의 MCP 설정이 디스크에 저장되는 길에 조용히 잘려 나갈 뻔했습니다.

해법은 & | < >만 캐럿으로 이스케이프하고 그 외에는 건드리지 않는 것입니다. 괄호까지 이스케이프한 첫 시도는 write("hi") 같은 평범한 인자를 망가뜨렸습니다. 캐럿은 인용되지 않은 문맥에서만 의미를 가지며, 괄호는 블록 문법 안에서만 특별하기 때문입니다.

사람에게 건네는 것과 프로그램에게 건네는 것은 다른 일입니다

섹션 제목: “사람에게 건네는 것과 프로그램에게 건네는 것은 다른 일입니다”

이 영역의 작업 대부분은 CLI를 감싸 그 결과를 사람에게 건넵니다. 터미널 UI, 데스크톱 애플리케이션, 에디터 패널이 그렇습니다. 그것은 CLI를 감싸 결과를 프로그램에게 건네는 일과는 다른 일입니다.

사람은 감싼 쪽이 하지 못한 것을 대신 채워 줍니다. CLI가 아예 시작되지 않았음을 알아채고 설치하고, 프롬프트가 뜨면 답하고, 활성 상태 보기에서 남은 프로세스를 발견해 죽입니다. 라이브러리 소비자에게는 그런 것이 하나도 없습니다. 감싼 쪽이 처리하지 못한 것은 그대로 처리되지 않은 promise rejection이 되거나, 멈춰 선 await가 되거나, 배후에서 조용히 과금되는 프로세스가 됩니다.

그래서 여기서 중요한 표면은 사람이 바라보는 표면이 아닙니다. 프로그램이 의지할 수 있는 표면 — 동작하는 세션을 내놓거나 대응 가능한 오류를 던지거나 둘 중 하나인 spawn, 형태가 안정적인 이벤트 스트림, 그리고 호스트가 강제 종료되어도 버티는 정리 — 입니다.

CLI마다 래퍼를 하나씩 두지 않고, 추상 어댑터를 두는 이유

섹션 제목: “CLI마다 래퍼를 하나씩 두지 않고, 추상 어댑터를 두는 이유”

당연해 보이는 접근은 CLI마다 작은 래퍼를 쓰고 각자가 자기 CLI가 받는 것을 그대로 드러내게 하는 것입니다. 첫 번째 CLI에서는 동작하고, 정확히 두 번째에서 동작을 멈춥니다.

CLI들이 단어 자체에 대해 합의하지 않기 때문입니다. 어떤 CLI는 대화를 세션 id로 식별하고 다른 CLI는 스레드 id로 식별합니다. 어떤 CLI는 모델 이름을 받고, 다른 CLI는 모델 이름에 더해 엔드포인트와 서비스 티어를 받습니다. 그대로 두면 각 래퍼의 send는 자기 CLI가 원한 파라미터를 계속 불려 가고, 결국 호출자가 페이로드를 만들기 위해 지금 상대하는 CLI가 누구인지 알아야 하는 상태가 됩니다.

그 지점에서 CLI들은 통합된 것이 아니라 한 지붕 아래에 분리 수납된 것입니다.

추상 어댑터가 그 대안입니다. Command는 의도로 말하고 — 메시지, 모델, 권한 모드 — 그 의도를 특정 CLI의 플래그로 옮기는 것은 어댑터의 몫이지 호출자의 몫이 아닙니다. 어휘가 라이브러리에 속하므로, 호출자는 프로바이더를 한 단어로 지목하고 거기서 멈춥니다.

Zed의 Agent Client Protocol은 2025-12부터 JetBrains IDE에 내장되었고 25종 이상의 에이전트를 지원합니다. ActiveCLI는 두 가지 이유로 ACP와 경쟁하지 않습니다.

  • ACP는 사람이 에디터 앞에 앉아 승인한다를 전제합니다. 프로그램이 사람 없이 CLI를 구동하는 상황에서는 그 전제가 성립하지 않습니다.
  • ACP는 프로세스 실행 문제 — 실행 파일 탐색, Windows 인용, WSL 경로 — 를 표준화하지 않습니다. 채택자들은 그 문제를 그대로 안고 갑니다. Zed #56176은 WSL에서 ACP 서버가 실행되지 않는다고 보고하고, Kiro 문서는 “절대 경로를 하드코딩하라”를 공식 해법으로 제시하며, JetBrains AI Assistant는 WSL을 아예 지원하지 않습니다.

따라서 ACP는 경쟁자가 아니라 어댑터 후보입니다. 같은 계약으로 구동할 CLI 모양의 대상이 하나 더 있는 것입니다.