콘텐츠로 이동

슬래시 명령어 내부 구조

이 문서는 coding-agent에서 슬래시 명령어가 어떻게 탐지되고, 중복 제거되며, 인터랙티브 모드에서 노출되고, 프롬프트 시점에 확장되는지를 설명합니다.

슬래시 명령어는 명령어 이름을 키(key: cmd => cmd.name)로 사용하는 기능(id: "slash-commands")입니다.

기능 레지스트리는 등록된 모든 프로바이더를 로드하고, 프로바이더 우선순위 내림차순으로 정렬한 후 첫 번째 항목 우선 방식으로 키 기반 중복 제거를 수행합니다.

현재 슬래시 명령어 프로바이더와 우선순위:

  1. native (OMP) — 우선순위 100
  2. claude — 우선순위 80
  3. claude-plugins — 우선순위 70
  4. codex — 우선순위 70

동점 처리: 동일 우선순위의 프로바이더는 등록 순서를 유지합니다. 현재 import 순서상 claude-pluginscodex보다 먼저 등록되므로, 이름이 충돌할 경우 플러그인 명령어가 codex 명령어보다 우선합니다.

slash-commands의 경우, 충돌은 기능 중복 제거를 통해 엄격하게 처리됩니다:

  • 가장 높은 우선순위의 항목이 result.items에 유지됩니다.
  • 낮은 우선순위의 중복 항목은 result.all에만 남고 _shadowed = true로 표시됩니다.

이는 프로바이더 간뿐만 아니라, 단일 프로바이더가 중복 이름을 반환하는 경우에도 적용됩니다.

프로바이더는 대부분 loadFilesFromDir(...)를 사용하며, 현재 다음과 같이 동작합니다:

  • 기본값은 비재귀 매칭(*.md)
  • gitignore: true, hidden: false로 네이티브 glob 사용
  • 매칭된 각 파일을 읽고 SlashCommand로 변환

따라서 숨김 파일/디렉터리는 로드되지 않으며, 무시된 경로는 건너뜁니다.

2) 프로바이더별 소스 경로 및 로컬 우선순위

섹션 제목: “2) 프로바이더별 소스 경로 및 로컬 우선순위”

검색 루트는 .xcsh 디렉터리에서 가져옵니다:

  • 프로젝트: <cwd>/.xcsh/commands/*.md
  • 사용자: ~/.xcsh/agent/commands/*.md

getConfigDirs()는 프로젝트를 먼저, 그다음 사용자 순으로 반환하므로, 이름이 충돌할 경우 프로젝트 네이티브 명령어가 사용자 네이티브 명령어보다 우선합니다.

로드 대상:

  • 사용자: ~/.claude/commands/*.md
  • 프로젝트: <cwd>/.claude/commands/*.md

프로바이더는 사용자 항목을 프로젝트 항목보다 먼저 추가하므로, 이 프로바이더 내에서 이름이 충돌할 경우 사용자 Claude 명령어가 프로젝트 Claude 명령어보다 우선합니다.

로드 대상:

  • 사용자: ~/.codex/commands/*.md
  • 프로젝트: <cwd>/.codex/commands/*.md

양쪽을 로드한 후 사용자 우선 순서로 평탄화하므로, 충돌 시 사용자 Codex 명령어가 프로젝트 Codex 명령어보다 우선합니다.

Codex 명령어 내용은 프론트매터 제거(parseFrontmatter)로 파싱되며, 명령어 이름은 프론트매터의 name 필드로 재정의할 수 있습니다. 지정하지 않으면 파일명이 사용됩니다.

claude-plugins 프로바이더 (claude-plugins.ts)

섹션 제목: “claude-plugins 프로바이더 (claude-plugins.ts)”

~/.claude/plugins/installed_plugins.json에서 플러그인 명령어 루트를 로드한 후, <pluginRoot>/commands/*.md를 스캔합니다.

순서는 레지스트리 반복 순서와 해당 JSON 데이터의 플러그인별 항목 순서를 따릅니다. 추가적인 정렬 단계는 없습니다.

3) 런타임 FileSlashCommand로의 구체화

섹션 제목: “3) 런타임 FileSlashCommand로의 구체화”

src/extensibility/slash-commands.tsloadSlashCommands()는 기능 항목을 프롬프트 시점에 사용되는 FileSlashCommand 객체로 변환합니다.

각 명령어에 대해:

  1. 프론트매터/본문 파싱(parseFrontmatter)
  2. 설명 소스:
    • frontmatter.description이 있으면 해당 값 사용
    • 없으면 본문의 첫 번째 비어있지 않은 줄(트림 후, 최대 60자, 초과 시 ...)
  3. 파싱된 본문을 실행 가능한 템플릿 콘텐츠로 유지
  4. via Claude Code Project와 같은 표시용 소스 문자열 계산

프론트매터 파싱 심각도는 소스에 따라 다릅니다:

  • native 레벨 → 파싱 오류는 fatal
  • user/project 레벨 → 파싱 오류는 warn이며 폴백 파싱 사용

파일시스템/프로바이더 명령어 이후, 내장 명령어 템플릿(EMBEDDED_COMMAND_TEMPLATES)이 이름이 아직 존재하지 않는 경우 추가됩니다.

현재 내장 세트는 src/task/commands.ts에서 가져오며 폴백으로 사용됩니다(source: "bundled").

4) 인터랙티브 모드: 명령어 목록의 출처

섹션 제목: “4) 인터랙티브 모드: 명령어 목록의 출처”

인터랙티브 모드는 자동완성과 명령어 라우팅을 위해 여러 명령어 소스를 결합합니다.

생성 시 다음으로부터 대기 중인 명령어 목록을 구성합니다:

  • 빌트인 명령어(BUILTIN_SLASH_COMMANDS, 선택된 명령어에 대한 인자 완성 및 인라인 힌트 포함)
  • 확장 등록된 슬래시 명령어(extensionRunner.getRegisteredCommands(...))
  • TypeScript 커스텀 명령어(session.customCommands), 슬래시 명령어 레이블로 매핑
  • skills.enableSkillCommands가 활성화된 경우 선택적 스킬 명령어(/skill:<name>)

이후 init()refreshSlashCommandState(...)를 호출하여 파일 기반 명령어를 로드하고 다음을 포함하는 CombinedAutocompleteProvider 하나를 설치합니다:

  • 위의 대기 중인 명령어
  • 탐지된 파일 기반 명령어

refreshSlashCommandState(...)는 또한 session.setSlashCommands(...)를 업데이트하여 프롬프트 확장이 동일한 탐지된 파일 명령어 세트를 사용하도록 합니다.

슬래시 명령어 상태는 다음 시점에 갱신됩니다:

  • 인터랙티브 초기화 중
  • /move로 작업 디렉터리 변경 후(handleMoveCommandresetCapabilities()를 호출한 다음 refreshSlashCommandState(newCwd) 호출)

명령어 디렉터리에 대한 지속적인 파일 감시자는 없습니다.

확장 대시보드도 slash-commands 기능을 로드하여 _shadowed 중복 항목을 포함한 활성/섀도잉된 명령어 항목을 표시합니다.

AgentSession.prompt(...)의 슬래시 처리 순서(expandPromptTemplates !== false인 경우):

  1. 확장 명령어 (#tryExecuteExtensionCommand)
    /name이 확장 등록 명령어와 일치하면 핸들러가 즉시 실행되고 프롬프트가 반환됩니다.
  2. TypeScript 커스텀 명령어 (#tryExecuteCustomCommand)
    경계 처리만: 일치하면 실행되며 다음을 반환할 수 있습니다:
    • string → 해당 문자열로 프롬프트 텍스트 교체
    • void/undefined → 처리된 것으로 간주; LLM 프롬프트 없음
  3. 파일 기반 슬래시 명령어 (expandSlashCommand)
    텍스트가 여전히 /로 시작하면 마크다운 명령어 확장을 시도합니다.
  4. 프롬프트 템플릿 (expandPromptTemplate)
    슬래시/커스텀 처리 이후 적용됩니다.
  5. 전달
    • 유휴 상태: 프롬프트가 즉시 에이전트로 전송됨
    • 스트리밍 중: streamingBehavior에 따라 steer/follow-up으로 큐에 추가됨

이것이 슬래시 명령어 확장이 프롬프트 템플릿 확장보다 먼저 수행되는 이유이며, 커스텀 명령어가 파일 명령어 매칭 전에 선행 슬래시를 변환할 수 있는 이유입니다.

6) 파일 기반 슬래시 명령어의 확장 시맨틱

섹션 제목: “6) 파일 기반 슬래시 명령어의 확장 시맨틱”

expandSlashCommand(text, fileCommands) 동작:

  • 텍스트가 /로 시작할 때만 실행
  • / 이후 첫 번째 토큰에서 명령어 이름 파싱
  • parseCommandArgs를 통해 나머지 텍스트에서 인자 파싱
  • 로드된 fileCommands에서 정확한 이름 매칭 검색
  • 일치하면 다음을 적용:
    • 위치 기반 교체: $1, $2, …
    • 집계 교체: $ARGUMENTS$@
    • 이후 { args, ARGUMENTS, arguments }prompt.render를 통한 템플릿 렌더링
  • 일치하지 않으면 원본 텍스트를 그대로 반환

파서는 단순한 따옴표 인식 분리 방식입니다:

  • 공백 유지를 위한 '단일'"이중" 따옴표 지원
  • 따옴표 구분자 제거
  • 백슬래시 이스케이프 규칙 미구현
  • 짝이 맞지 않는 따옴표는 오류가 아님; 파서가 끝까지 소비

알 수 없는 슬래시 입력은 핵심 슬래시 로직에 의해 거부되지 않습니다.

명령어가 확장/커스텀/파일 레이어에서 처리되지 않으면, expandSlashCommand는 원본 텍스트를 반환하고 리터럴 /... 프롬프트는 일반 프롬프트 템플릿 확장 및 LLM 전달을 통해 진행됩니다.

인터랙티브 모드는 InputController에서 많은 빌트인 명령어를 별도로 직접 처리합니다(예: /settings, /model, /mcp, /move, /exit). 이들은 session.prompt(...)보다 먼저 소비되므로, 해당 경로에서는 파일 명령어 확장에 도달하지 않습니다.

8) 스트리밍 중과 유휴 시의 차이점

섹션 제목: “8) 스트리밍 중과 유휴 시의 차이점”
  • session.prompt("/x ...")는 명령어 파이프라인을 실행하고 명령어를 즉시 실행하거나 확장된 텍스트를 직접 전송합니다.

스트리밍 경로 (session.isStreaming === true)

섹션 제목: “스트리밍 경로 (session.isStreaming === true)”
  • prompt(...)는 여전히 먼저 확장/커스텀/파일/템플릿 변환을 실행합니다.
  • 이후 streamingBehavior가 필요합니다:
    • "steer" → 인터럽트 메시지 큐에 추가(agent.steer)
    • "followUp" → 턴 후 메시지 큐에 추가(agent.followUp)
  • streamingBehavior가 생략되면 프롬프트에서 오류가 발생합니다.

명령어별 중요한 스트리밍 동작

섹션 제목: “명령어별 중요한 스트리밍 동작”
  • 확장 명령어는 스트리밍 중에도 즉시 실행됩니다(텍스트로 큐에 추가되지 않음).
  • steer(...)/followUp(...) 헬퍼 메서드는 확장 명령어를 거부합니다(#throwIfExtensionCommand). 동기적으로 실행해야 하는 핸들러를 위해 명령어 텍스트를 큐에 추가하지 않기 위함입니다.
  • 압축 큐 재실행은 isKnownSlashCommand(...)를 사용하여 큐에 추가된 항목을 session.prompt(...)로 재실행할지(알려진 슬래시 명령어의 경우), 아니면 raw steer/follow-up 메서드로 처리할지를 결정합니다.
  • 프로바이더 로드 실패는 격리됩니다. 레지스트리는 경고를 수집하고 다른 프로바이더로 계속 진행합니다.
  • 유효하지 않은 슬래시 명령어 항목(이름/경로/콘텐츠 누락 또는 유효하지 않은 레벨)은 기능 유효성 검사에서 제거됩니다.
  • 프론트매터 파싱 실패:
    • 네이티브 명령어: 치명적 파싱 오류가 전파됨
    • 비네이티브 명령어: 경고 + 폴백 키/값 파싱
  • 확장/커스텀 명령어 핸들러 예외는 포착되어 확장 오류 채널(또는 확장 러너가 없는 커스텀 명령어의 경우 로거 폴백)을 통해 보고되며, 처리된 것으로 간주됩니다(의도치 않은 폴백 실행 없음).