개인 프로젝트

turnbell - CLI 코딩 에이전트를 위한 음성·데스크톱·웹훅 알림

코딩 에이전트가 작업을 마치거나 입력이 필요한 순간, 응답을 음성으로 읽어주고 데스크톱 알림을 띄우거나 휴대폰으로 알려주는 로컬 우선 CLI, 텔레메트리 없음.

turnbell은 CLI 코딩 에이전트가 작업을 마치거나 입력을 기다리며 멈춘 순간을 알려준다. Claude Code, Codex, Gemini CLI로 긴 작업을 실행해 두고 다른 일을 하다가, 터미널을 계속 지켜보는 대신 turnbell이 응답을 음성으로 읽어주고, 데스크톱 알림을 띄우고, 소리를 재생하거나 웹훅으로 알려준다. 에이전트가 나를 필요로 하는 바로 그 순간에, 1분 먼저도 10분 늦게도 아니게.

turnbell - CLI 코딩 에이전트를 위한 음성·데스크톱·웹훅 알림

내가 만든 것

  • 8개 에이전트 1급 통합: Claude Code, Codex, Gemini CLI, Copilot CLI, Cursor, opencode, Antigravity, Amp까지, 각 어댑터는 해당 에이전트의 훅 시스템이 실제로 제공하는 done/blocked/read-aloud 신호에 맞춰 연결되며, 그 외 모든 명령을 위한 범용 turnbell run 래퍼도 제공한다.
  • 원장(ledger) 기반 설정 기록: turnbell이 에이전트 설정 파일에 추가하는 모든 훅은 원자적이고 append-only인 원장에 줄 단위 diff와 함께 기록되어, turnbell uninstall이 turnbell이 작성한 부분만 정확히 제거하고 그 이후 사용자가 직접 수정한 내용은 그대로 보존할 수 있다.
  • 읽어주기 "말할 수 있는 모드": 에이전트 자체의 메모리 파일(CLAUDE.md, AGENTS.md, GEMINI.md)에 옵트인 방식으로 짧은 지시문을 추가해, 응답을 말하기 좋은 형태로 유지하고 코드나 상세 내용은 임시 파일로 대신 보내도록 유도한다. 되돌릴 수 있고, 24시간 후 자동으로 정리된다.
  • 백그라운드를 인식하는 완료 감지: Claude Code의 done 알림은 서브 에이전트, 워크플로, 팀메이트가 아직 작업 중이면 대기하다가, 실제로 턴이 끝난 시점에 딱 한 번만 알려준다.
  • 로컬 우선 웹훅 싱크: 단 하나의 직렬화 함수가 정확히 여섯 개의 메타데이터 필드(타임스탬프, 에이전트, 이벤트, 프로젝트, 요약)만 ntfy, Pushover, Slack 또는 임의의 엔드포인트로 보낸다. 작업 디렉터리도, 코드도, 에이전트의 실제 응답도 절대 보내지 않는다.

왜 중요했나

긴 에이전트 실행은 늘 터미널을 계속 지켜보거나, 권한 요청에 멈춰 있는 순간을 놓치거나 둘 중 하나였다. turnbell은 백그라운드 데몬도, SDK도, 어떤 텔레메트리도 추가하지 않고 이 공백을 메운다: 각 에이전트 자체의 훅을 연결할 뿐이며, 추가한 모든 연결은 추적하고 되돌릴 수 있다.

이 프로젝트가 존재하는 이유

긴 Claude Code나 Codex 실행은 요란하게 실패하지 않는다. 권한 요청에 조용히 멈춰 있거나, 내가 전혀 다른 탭 세 개 너머에 가 있는 동안 조용히 끝나 있을 뿐이다. 해법은 더 큰 터미널 창이 아니라, 턴이 정말로 나를 필요로 하는 그 순간에 알려주는 것이다. 나는 이 프로젝트의 유일한 기여자이고, 지금도 계속 관리하고 있으며, 내 워크플로를 먼저 해결하고 그다음에야 공개 npm 패키지로 출시했다. 이전 이름은 hollr였다. turnbell로의 개명은 꽤 신중하게 이루어져서, 패키지는 여전히 이전 이름을 위한 호환 bin 별칭과 자동 설정 마이그레이션을 함께 배포한다.

제약 조건: SDK도, 데몬도, 침습적인 것도 없다

turnbell의 모든 에이전트 통합은 단 하나의 규칙을 따른다: 에이전트 자체의 훅 설정을 수정해 turnbell emit을 호출하게 만드는 것, 그 이상은 없다. 백그라운드 프로세스도, 에이전트가 임포트해야 하는 SDK도, 폴링도 없다. 이 제약 때문에 제거(uninstall) 문제도 알려진 한계로 문서화하고 넘어가는 대신 제대로 풀어야 했다.

"최선을 다한" 제거 대신 원장 기반 기록

turnbell init을 만드는 뻔한 방법은 에이전트가 쓰는 설정 파일에 훅 항목을 그냥 추가하고 끝내는 것이다. 그 위에 turnbell uninstall을 만드는 뻔한 방법은 turnbell 자신의 마커를 grep으로 찾아 파일 안의 다른 내용과 충돌하지 않기를 바라는 것이다. 이것이 "최선을 다한(best effort)" 제거의 실체이고, 수정 대상 파일이 사용자의 다른 도구들이 의존하는 공유 설정 파일일 때는 이 정도로는 부족하다.

src/adapters/diffwire.ts는 다른 접근을 택한다: 모든 기록은 JSON이나 텍스트 파일에 대한 WireOp로 표현되고, 원자적 쓰기(임시 파일 후 rename, 크래시 시에도 부분 쓰기가 없음)로 적용되며, turnbell이 어떤 키나 마커를 건드렸는지와 이전 값이 무엇이었는지를 정확히 기록하는 원장(listWiredKeys, appendLedgerEntry)에 추가된다. turnbell init은 실제로 아무것도 쓰기 전에 생성된 줄 단위 diff로 모든 변경을 미리 보여준다. turnbell uninstall은 파일의 현재 상태에서 의도를 다시 추론하는 대신 이 원장을 그대로 재생하는데, 바로 이 점이 "turnbell이 추가한 것만 정교하게 제거하고, 그 이후 사용자가 직접 한 수정은 보존된다"는 문구를 README의 최선을 다한 주장이 아니라 실제 보장으로 만들어 준다.

Codex의 특수 케이스

Codex는 설정에서 단 하나의 최상위 notify 명령만 허용하며, 훅 목록 같은 건 없다. 사용자가 이미 다른 용도로 notify 명령을 연결해 두었다면, 그것을 순진하게 덮어쓰는 순간 기존 설정이 조용히 깨진다. 이 어댑터는 교체하기 전에 원래의 notify 값을 보관해 두고, turnbell uninstall은 키를 그냥 삭제하는 대신 이 보관본에서 값을 복원한다. 유일하게 복구할 수 없는 경우는 설치와 제거 사이에 config.toml 자체가 삭제된 경우로, 이때는 복원할 대상이 아예 남아 있지 않다. 이는 감추지 않고 실제 한계로 명확히 문서화되어 있다.

위임 작업 중에는 알리지 않는다

Claude Code 자체의 done 훅은 최상위 턴마다 발생하지만, 하나의 턴이 서브 에이전트, 워크플로, 팀메이트를 만들어 부모 턴이 완료를 보고한 뒤에도 계속 작업이 이어질 수 있다. turnbell의 초기 버전은 그 시점에 곧바로 알림을 보냈고, 그 결과 백그라운드 작업이 아직 진행 중인데도 터미널로 불려 오는 상황이 벌어졌다. 이 문제를 고치기 위해, Claude Code가 더 이상 진행 중인 위임 작업이 없다고 보고할 때까지 done 알림을 보류하며, 오래 지속되는 shell/워처와 monitor 작업은 명시적으로 무시한다. 이런 작업까지 "진행 중"으로 취급하면 백그라운드 프로세스가 하나라도 있는 세션에서는 모든 알림이 침묵해 버리기 때문이다.

페이로드는 설계상 메타데이터로만 구성된다

turnbell은 로컬 우선이다: 음성, 알림, 소리는 절대 기기 밖으로 나가지 않는다. 네트워크를 타는 유일한 것은 사용자가 직접 설정한 웹훅뿐이며, 그 경로는 타임스탬프, 에이전트, 이벤트, 프로젝트 이름(basename), 짧은 요약 문자열까지 정확히 여섯 개 필드만 내보내는 단 하나의 직렬화 함수를 거친다. 작업 디렉터리나 에이전트의 실제 응답이 실수로 새어 나갈 수 있는 두 번째 코드 경로는 존재하지 않는다. 전체 코드베이스에서 네트워크에 접근하는 것이 이 직렬화 함수 하나뿐이기 때문이다. turnbell test --show-payload는 실제로 전송하지 않고 정확한 페이로드를 출력해 주므로, 이 주장은 단언이 아니라 직접 검증 가능하다.

기술 스택

TypeScript, 번들링은 tsup, 테스트는 80% 커버리지 게이트를 둔 Vitest, 플랫폼별 네이티브 OS 프리미티브로 음성/알림/소리 처리(macOS는 안정 버전, Linux와 Windows는 베타), 그리고 공유 SDK 없이 에이전트별 훅/설정 형식(Claude Code, Codex, Gemini CLI, Copilot CLI, Cursor, opencode, Antigravity, Amp)을 각각 지원한다. MIT 라이선스.