콘텐츠로 이동

AbstractProvider

AbstractProvider는 모든 CLI의 정체성이 상속하는 기반 클래스입니다. 의도적으로 작습니다. 채워야 할 추상 멤버 둘, 그리고 공짜로 얻는 메서드 하나입니다.

export abstract class AbstractProvider {
/** The CLI's command name, as a user would type it. */
abstract get name(): string;
/** A fresh adapter for this CLI. */
abstract createAdapter(): AbstractAdapter;
async ask(question: string, options?: CommandOptions): Promise<Answer>;
toString(): string;
}
멤버 누가 제공하는가 무엇을 위한 것인가
name 하위 클래스 사용자가 타이핑하는 그대로의 CLI 명령 이름 — claude, codex
createAdapter() 하위 클래스 command가 마침내 필요로 할 때 만들어지는 새 어댑터
ask() 기반 클래스 질문 하나, 답변 하나
toString() 기반 클래스 이름. 프로바이더가 자기 자신으로 출력되도록

추상 멤버 둘을 채운 하위 클래스는 나머지 전부를 얻습니다. 그것이 의무의 전부입니다.

export class ClaudeProvider extends AbstractProvider {
override get name(): string {
return 'claude';
}
override createAdapter(): ClaudeAdapter {
return new ClaudeAdapter();
}
}
/** The `claude` CLI. */
export const Claude = new ClaudeProvider();

name은 표시 용도만이 아닙니다. 일회성 서브커맨드들 — command.auth, command.mcp, command.version() — 이 이 이름으로 생성되므로, 프로바이더가 지목하는 CLI를 상대로 실행됩니다.

ask(), 그리고 권한 모드를 갖지 않는 이유

섹션 제목: “ask(), 그리고 권한 모드를 갖지 않는 이유”

ask()는 나눌 대화가 없을 때를 위한 지름길입니다. command를 열고, 보내고, 턴이 끝날 때까지 어시스턴트의 텍스트를 모으고, 닫습니다.

const answer = await Claude.ask('what does this do?', { workingDir: '/proj' });
console.log(answer.text);

결과는 Answer로 resolve됩니다. 조립된 텍스트와 함께 CLI 자신의 result 엔트리를 담고 있어, 이 클래스가 직접 모델링하지 않고도 호출자가 비용·소요 시간 등 CLI가 보고한 것에 접근할 수 있습니다.

의도적으로 빠진 것은 권한 모드입니다. ask()자신의 권한 모드를 갖지 않고 실행되므로, CLI가 사용자의 설정된 기본값을 적용합니다.

근거는 ClaudeArgv를 지배하는 것과 같습니다. 플래그를 생략하면 사용자의 설정을 따르고, 플래그를 넘기면 그것을 덮어씁니다. ask()가 “사용자 설정을 따르라”는 뜻으로 넘길 수 있는 값은 존재하지 않습니다 — --permission-mode default는 특정 모드를 지칭할 뿐 양보를 뜻하지 않습니다. 그래서 아무것도 넘기지 않습니다.

두 번째 턴, 권한 처리, 스트리밍이 필요한 모든 경우에는 Command를 열어 들고 있어야 합니다.

모든 프로바이더 아래에는 어댑터가 있고, 그 계약은 멤버 다섯 개입니다.

abstract class AbstractAdapter {
abstract get command(): string;
abstract buildArgv(options: SpawnOptions): string[];
abstract spawn(options: SpawnOptions): ChildProcess;
abstract parseLine(line: string): AbstractEvent | null;
abstract reportedMode(event: AbstractEvent): PermissionMode | null;
}

의도적으로 이만큼 작습니다. 단일 구현에서 뽑아낸 계약은 추측이며, 커밋할 가치가 있는 형태는 두 번째를 견뎌 낸 것입니다. 그래서 이 계약은 두 번째 어댑터를 붙일 때 자라날 것으로 예상되며, 그 전에 자라지 않습니다.

프로바이더 간 차이는 그 시점에야 드러납니다. Claude가 대화를 sessionId로 식별하는 자리에서 Codex는 threadId로 식별하고, 어떤 CLI의 권한 어휘는 다른 CLI보다 항목이 많습니다. 그 차이를 보기 전에 그것을 예상하는 계약을 쓰면, 추측에 맞춘 추상화가 나옵니다.

따라서 의도된 순서는 이렇습니다.

  1. ClaudeAdapter를 정확히 옮긴다.
  2. 두 번째 어댑터를 붙이고, 둘에서 공통 계약을 추출한다.
  3. 그 뒤에야 AbstractAdapter를 확정한다.

ActiveRecord 역시 MySQL 하나로 시작했습니다.

작다는 것이 느슨하다는 뜻은 아닙니다. 다섯 멤버 중 둘은 짚어 둘 만한 방식으로 하중을 받고 있습니다.

buildArgvspawn과 분리되어 있고, spawn은 그것으로 정의됩니다. 그래야 Command.toArgv()가 프로세스를 시작하지 않고도 정확한 커맨드 라인을 호출자에게 보여 줄 수 있습니다 — to_sql의 대응물입니다. argv를 spawn 안에서 조립하면 둘이 서로 다른 명령을 기술할 수 있게 됩니다.

reportedMode가 어댑터에 있는 것은 그 어휘가 CLI의 것이기 때문입니다. 어떤 CLI는 한 모드를 acceptEdits라 쓰고, 다른 CLI는 같은 개념을 달리 씁니다. 번역은 그 차이가 드러나야 할 유일한 지점이며, 그래서 Session.modeSession.requiresRestartFor()가 특정 CLI의 어휘가 아니라 공유 어휘로 말할 수 있습니다.

어댑터가 소유하지 않는 것도 마찬가지로 고정되어 있습니다. 실행 파일 찾기, PATH 보강, 프로세스 트리 종료는 모든 CLI에 동일한 문제이므로 Process/에 살며, 프로바이더마다 다시 구현하는 대신 어댑터에 건네집니다.