diff --git a/HOOKS.md b/HOOKS.md new file mode 100644 index 0000000..73f6edc --- /dev/null +++ b/HOOKS.md @@ -0,0 +1,346 @@ +# Aptabase 훅 시스템 문서 + +> 작성: 2026-04-08 +> 대상: `workhistory` 저장소의 `.claude/hooks/` 기반 Aptabase 토큰 기록 파이프라인 + +--- + +## 1. 개요 + +Claude Code 세션에서 발생한 응답 토큰을 **git 커밋 단위로** 묶어 자체 호스팅 Aptabase(`https://aptabase.hmac.kr`)로 자동 전송하는 시스템. 토큰은 세션 동안 로컬에 누적되다가 git 커밋이 감지되면 `claude_commit` 이벤트로 flush된다. + +**핵심 아이디어** +- 커밋을 "작업 단위"의 자연스러운 경계로 사용 +- 외부(IDE/터미널/GUI)에서 일어난 커밋도 포착 (`.git/hooks/post-commit` 설치) +- 전송 실패 시 재시도, 이중 전송 방지(`last_sent_commit` 해시 비교) + +--- + +## 2. 구성 파일 + +``` +.claude/ +├── settings.json # 훅 등록 (Stop / PostToolUse(Bash)) +├── hooks/ +│ ├── aptabase.json # app_key, host, user_name +│ ├── aptabase-accumulate.ps1 # Stop 훅 진입점 (Windows) +│ ├── aptabase-accumulate.sh # Stop 훅 진입점 (Unix) +│ ├── aptabase-commit.ps1 # 커밋 flush (Windows) +│ ├── aptabase-commit.sh # 커밋 flush (Unix) +│ ├── install-git-hook.sh # .git/hooks/post-commit 설치 +│ └── usage.md # 상세 사용 설명 +├── skills/aptabase/SKILL.md # /aptabase 슬래시 스킬 정의 +└── state/ + ├── .gitignore + └── aptabase-accum.json # 누적 상태 (자동 생성) +``` + +--- + +## 3. 동작 흐름 + +| 시점 | 훅 이벤트 | 실행 스크립트 | 역할 | +|---|---|---|---| +| Claude 응답 완료 | `Stop` | `aptabase-accumulate.ps1` | 트랜스크립트 JSONL 증분 파싱 → `state.accum`에 토큰 누적. 네트워크 호출 없음. | +| Claude `Bash` 툴 호출 후 | `PostToolUse(Bash)` | `aptabase-commit.ps1` | HEAD 해시 변화 감지 → 변했으면 Aptabase POST → 누적 리셋 | +| 모든 git 커밋 (외부 포함) | `.git/hooks/post-commit` | `aptabase-commit.ps1` | 동일 로직. Claude 밖 커밋도 포착 | + +``` +Claude 응답 완료 + │ + ▼ +[Stop: accumulate.ps1] + ├─ 트랜스크립트 새 byte 구간만 읽어 assistant usage 누적 + └─ state.accum 저장 (네트워크 없음) + +git commit (Claude Bash 또는 외부) + │ + ├─ PostToolUse(Bash) ┐ + │ │ + └─ post-commit 훅 ▼ + [commit.ps1] + ├─ HEAD 가 last_sent_commit 과 같으면 exit + ├─ 다르면: + │ ├─ 남은 트랜스크립트 flush + │ ├─ consumed_tokens 계산 (0이면 전송 생략) + │ ├─ POST /api/v0/event (event: claude_commit) + │ ├─ 성공: 누적 리셋, last_sent_commit = HEAD + │ └─ 실패: state 유지 → 다음 호출에서 재시도 + └─ exit 0 (실패해도 git/Claude 흐름 비차단) +``` + +--- + +## 4. 설정 — `aptabase.json` + +현재값 ([.claude/hooks/aptabase.json](.claude/hooks/aptabase.json)): + +```json +{ + "enabled": true, + "app_key": "A-SH-7756143445", + "aptabase_host": "https://aptabase.hmac.kr", + "user_name": "김민성(b16213)", + "git_repositories": ["D:/MYCLAUDE_PROJECT/workhistory"] +} +``` + +| 키 | 의미 | +|---|---| +| `enabled` | `false` 면 훅이 즉시 종료 (일시 비활성) | +| `app_key` | Aptabase App Key (self-hosted 는 `A-SH-` 접두사) | +| `aptabase_host` | Aptabase 인스턴스 base URL | +| `user_name` | `props.user_name` 으로 전송할 사용자 식별자 | +| `git_repositories` | (참고용) 대상 저장소 목록 | + +> ⚠️ `app_key`가 평문으로 커밋되어 있다. 공개 저장소로 전환할 경우 반드시 gitignore 또는 `.example` 템플릿으로 분리할 것. + +--- + +## 5. 훅 등록 — `settings.json` + +[.claude/settings.json](.claude/settings.json)은 인라인 PowerShell 래퍼로 구성되어 있다: + +```json +{ + "hooks": { + "Stop": [{ + "hooks": [{ + "type": "command", + "command": "powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command \"$j=[Console]::In.ReadToEnd();$c=try{($j|ConvertFrom-Json).cwd}catch{$null};if(-not $c){exit 0};$f=Join-Path $c '.claude\\hooks\\aptabase-accumulate.ps1';if(Test-Path $f){$t=[IO.Path]::GetTempFileName();[IO.File]::WriteAllText($t,$j,[Text.Encoding]::UTF8);try{&powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File $f $t}finally{Remove-Item $t -EA 0}}\"" + }] + }], + "PostToolUse": [{ + "matcher": "Bash", + "hooks": [{ + "type": "command", + "command": "powershell ... (같은 패턴, commit.ps1 호출)" + }] + }] + } +} +``` + +**래퍼의 동작** +1. stdin으로 들어온 훅 JSON을 읽는다 +2. `cwd` 필드를 추출해 프로젝트의 `.ps1` 경로를 찾는다 +3. 훅 JSON 전체를 UTF-8 임시 파일에 쓴다 +4. `.ps1`을 `-File` 인자로 실행하며 임시 파일 경로를 넘긴다 +5. 스크립트는 `$args[0]` 또는 stdin 양쪽에서 JSON을 읽을 수 있도록 작성되어 있음 + +--- + +## 6. 전송 이벤트 — `claude_commit` + +**엔드포인트**: `POST {aptabase_host}/api/v0/event` +**헤더**: `App-Key: `, `Content-Type: application/json` + +### systemProps +`osName`, `osVersion`, `locale`, `appVersion`, `sdkVersion`, `isDebug` + +### props + +| 필드 | 출처 | +|---|---| +| `claude_oauth_id` | `~/.claude.json` 의 `oauthAccount.emailAddress` (없으면 `anonymous`) | +| `plan` | `subscriptionType` / `plan` / `billingType` (API 키는 `apikey`) | +| `user_name` | `aptabase.json` 의 `user_name` | +| `local_ip` | UDP connect 트릭 (패킷 미전송) | +| `public_ip` | `api.ipify.org` → `ifconfig.me` fallback (2초 타임아웃) | +| `commit_hash` | `git rev-parse HEAD` | +| `commit_message` | `git log -1 --pretty=%B` | +| `issue_number` | 커밋 메시지 regex 추출 (`#123`, `fixes #45` 등). 없으면 `null` | +| `repository` | remote URL 또는 git root 디렉터리 | +| `consumed_tokens` | 가중 합계 (아래 참조) | +| `total_tokens` | 단순 합계 | +| `input_tokens`, `output_tokens`, `cache_creation_tokens`, `cache_read_tokens` | 세부 | + +### consumed_tokens 가중치 + +``` +consumed = input * 1.0 + + cache_creation * 1.25 + + cache_read * 0.10 + + output * 5.0 +``` + +Anthropic 청구 단가 비율과 맞춘 "실질 비용 단위". + +### 이슈 번호 추출 regex + +``` +(?:fix(?:e[sd])?|close[sd]?|resolve[sd]?)?[\s]*#(\d+) +``` + +예: `fixes #45`, `closes#12`, `#123` 모두 매칭. + +--- + +## 7. 누적 상태 파일 — `aptabase-accum.json` + +경로: `/.claude/state/aptabase-accum.json` + +```json +{ + "input_tokens": 1234, + "cache_creation_tokens": 5678, + "cache_read_tokens": 9012, + "output_tokens": 345, + "last_sent_commit": "abc123def...", + "last_transcript": "C:\\Users\\...\\session-uuid.jsonl", + "last_transcript_offset": 45678 +} +``` + +| 필드 | 의미 | +|---|---| +| `*_tokens` | 현재 누적 중인 토큰 (flush 시 0으로 리셋) | +| `last_sent_commit` | 마지막 성공 전송한 HEAD 해시 (중복 전송 방지) | +| `last_transcript` | 마지막으로 읽은 트랜스크립트 JSONL 경로 | +| `last_transcript_offset` | 해당 파일에서 이미 읽은 byte 위치 (JSONL append-only 특성 활용) | + +**수동 리셋**: `rm .claude/state/aptabase-accum.json` +**전송 기준점만 초기화**: `last_sent_commit` 필드를 `""` 로 편집 + +--- + +## 8. 첫 실행 / 엣지 케이스 + +| 상황 | 동작 | +|---|---| +| 첫 설치 후 첫 커밋 | `last_sent_commit` 이 비어 있으면 현재 HEAD 를 기준점으로 기록만 하고 전송 안 함 | +| `consumed_tokens == 0` | 전송 생략, `last_sent_commit` 만 갱신 | +| 네트워크 실패 | state 유지 → 다음 Bash 호출에서 재시도 (누적 + 새 커밋 병합) | +| Claude가 Bash로 커밋 | PostToolUse(Bash) + post-commit 둘 다 발동. `last_sent_commit` 비교로 한 번만 전송 | +| 외부(IDE)에서 커밋 | post-commit 만 발동 | +| git 저장소 밖 실행 | `git rev-parse HEAD` 실패 → 조용히 종료 | +| OAuth 미로그인 (API 키) | `claude_oauth_id = "anonymous"`, `plan = "apikey"` | +| `amend` / `rebase` / `cherry-pick` | HEAD 해시가 바뀌므로 모두 포착 | +| `last_sent_commit` 이 삭제된 커밋 | 탐지 후 현재 HEAD 를 새 기준점으로 재설정 (누적 리셋) | + +--- + +## 9. 검증 절차 + +### 9.1 Aptabase 도달성 (curl) + +```bash +APP_KEY=$(jq -r .app_key .claude/hooks/aptabase.json) +HOST=$(jq -r .aptabase_host .claude/hooks/aptabase.json) +curl -i -X POST "$HOST/api/v0/event" \ + -H "Content-Type: application/json" \ + -H "App-Key: $APP_KEY" \ + -H "User-Agent: ClaudeCodeHook/test" \ + -d '{ + "timestamp":"2026-04-08T00:00:00.000Z", + "sessionId":"manual-test", + "eventName":"claude_commit", + "systemProps":{"osName":"Test","sdkVersion":"manual"}, + "props":{"commit_message":"manual test","total_tokens":1} + }' +``` + +`HTTP/1.1 200` + `{}` 이면 수신 OK. + +### 9.2 accumulate.ps1 직접 실행 + +```bash +echo '{"transcript_path":"","cwd":"D:/MYCLAUDE_PROJECT/workhistory","hook_event_name":"Stop"}' \ + | powershell -NoProfile -ExecutionPolicy Bypass -File .claude/hooks/aptabase-accumulate.ps1 +cat .claude/state/aptabase-accum.json +``` + +`last_transcript`, `last_transcript_offset`, 각 `*_tokens` 가 채워지면 스크립트는 정상. + +### 9.3 E2E + +1. Claude 와 대화 → 토큰 누적 +2. `cat .claude/state/aptabase-accum.json` — accum 이 0 이 아님을 확인 +3. `git commit --allow-empty -m "test: aptabase #999"` +4. Aptabase 대시보드에서 `claude_commit` 이벤트 확인 +5. state 가 0 으로 리셋되고 `last_sent_commit` 이 새 HEAD 로 갱신됐는지 확인 + +--- + +## 10. 트러블슈팅 + +### 증상 A — `accum` 이 계속 0 (현재 이 저장소의 증상) + +**확인 순서** +1. `.claude/hooks/aptabase.json` 의 `enabled: true` 여부 +2. 훅을 수동 실행했을 때 (9.2) 정상 누적되는지 → YES면 settings.json 래퍼 문제, NO면 스크립트/경로 문제 +3. `last_transcript` 가 비어 있으면 **Stop 훅이 실제로는 스크립트까지 도달하지 않은 것** +4. settings.json 의 인라인 PowerShell 래퍼를 단순화해서 재시도 (§11 참조) + +### 증상 B — 커밋했는데 대시보드에 안 뜸 + +1. curl 직접 테스트 (9.1)로 네트워크/인증 분리 검증 +2. `last_sent_commit` 이 이미 HEAD 와 같은지 (이미 전송됨) +3. `consumed_tokens == 0` 이라 전송이 생략됐는지 (accum 확인) +4. `.git/hooks/post-commit` 이 실제로 존재하는지: `cat .git/hooks/post-commit` +5. 커밋이 git 저장소 내부에서 이루어졌는지 (`git rev-parse HEAD` 성공) + +### 증상 C — 첫 커밋이 무시됨 + +**의도된 동작**. `last_sent_commit` 가 빈 상태에서는 현재 HEAD 를 기준점으로 기록만 하고 전송하지 않는다. 두 번째 커밋부터 전송된다. + +### 일시 비활성 + +`aptabase.json` 의 `enabled: false`. 훅 등록은 유지해도 된다. + +### 전송 기준점 초기화 (디버깅용) + +```bash +jq '.last_sent_commit = ""' .claude/state/aptabase-accum.json > /tmp/_s \ + && mv /tmp/_s .claude/state/aptabase-accum.json +``` + +--- + +## 11. 알려진 이슈 — Stop 훅 비활성 + +현재 이 저장소의 `aptabase-accum.json` 은 `last_transcript` 가 빈 문자열인 채로 남아 있다. 이는 **Stop 훅 래퍼가 한 번도 `.ps1` 까지 도달하지 못했음**을 의미한다 (스크립트 자체는 수동 실행 시 정상 동작 확인됨). + +### 추정 원인 + +`settings.json` 의 인라인 PowerShell `-Command` 가 Claude Code 훅 실행 쉘(git-bash 기반일 가능성)을 거치며 `$j`, `$c`, `$f`, `$t` 같은 달러 변수가 **bash에 의해 빈 문자열로 치환**되어 PowerShell 에 도달하기 전에 손상되는 것으로 보인다. + +### 권장 수정 + +인라인 래퍼 대신 단순 파일 호출로 대체: + +```json +{ + "hooks": { + "Stop": [{ + "hooks": [{ + "type": "command", + "command": "powershell -NoProfile -ExecutionPolicy Bypass -File .claude/hooks/aptabase-accumulate.ps1" + }] + }], + "PostToolUse": [{ + "matcher": "Bash", + "hooks": [{ + "type": "command", + "command": "powershell -NoProfile -ExecutionPolicy Bypass -File .claude/hooks/aptabase-commit.ps1" + }] + }] + } +} +``` + +`.ps1` 들은 이미 stdin 에서 훅 JSON 을 읽도록 작성되어 있으므로 래퍼에서 `cwd` 추출·임시파일 생성 과정은 불필요하다. + +--- + +## 12. 제거 + +```bash +# 1. git post-commit 훅 제거 +bash .claude/hooks/install-git-hook.sh --uninstall + +# 2. settings.json 에서 Stop / PostToolUse(Bash) 훅 엔트리 제거 + +# 3. 파일 삭제 (선택) +rm -rf .claude/hooks .claude/state/aptabase-accum.json +``` diff --git a/PROJECT_ANALYSIS.md b/PROJECT_ANALYSIS.md new file mode 100644 index 0000000..06fd03e --- /dev/null +++ b/PROJECT_ANALYSIS.md @@ -0,0 +1,102 @@ +# workhistory 프로젝트 분석 + +생성일: 2026-04-08 + +## 1. 개요 + +`workhistory`는 개인 작업 이력·메모 저장소이자, **Claude Code 훅(Aptabase 토큰 기록) 테스트베드**로 운영되는 git 저장소다. 실제 "프로덕션 코드"는 없고, 저장소 자체가 `.claude/` 하위 훅/스킬 구성을 검증하기 위한 샌드박스다. + +- 위치: [d:/MYCLAUDE_PROJECT/workhistory](.) +- 원격 저장소: (로컬 전용 추정) +- 현재 브랜치: `main` +- 최근 커밋: `e09c019 test: empty test commit` + +## 2. 디렉터리 구조 + +``` +workhistory/ +├── README.md # 저장소 목적 요약 +├── test-aptabase.txt # 훅 테스트용 더미 +├── claude-common-installer(3).exe # 공통 설정 설치 바이너리 (12MB) +├── .claude/ +│ ├── settings.json # Stop / PostToolUse(Bash) 훅 등록 +│ ├── hooks/ +│ │ ├── aptabase.json # app_key, host, user_name +│ │ ├── aptabase-accumulate.{sh,ps1} # 응답 종료 시 토큰 누적 +│ │ ├── aptabase-commit.{sh,ps1} # 커밋 감지 → Aptabase flush +│ │ ├── install-git-hook.sh # .git/hooks/post-commit 설치 +│ │ └── usage.md # 상세 사용 문서 +│ ├── skills/aptabase/SKILL.md # 슬래시 스킬 정의 +│ └── state/ +│ ├── .gitignore +│ └── aptabase-accum.json # 누적 상태 (자동 생성) +└── memory/ # 장기 메모리 저장소 (README 기준) +``` + +> 참고: [usage.md](.claude/hooks/usage.md)에 언급된 `aptabase_common.py`, `aptabase-*.py` 파이썬 구현은 실제 디렉터리에는 없고, `.sh` / `.ps1` 진입점만 존재한다. Windows 환경이라 PowerShell 경로가 실제 실행된다. + +## 3. 핵심 기능 — Aptabase 토큰 기록 훅 + +### 3.1 목적 +Claude Code 세션에서 발생한 응답 토큰을 **git 커밋 단위로** 묶어 자체 호스팅 Aptabase(`https://aptabase.hmac.kr`)로 전송. 커밋 메시지·이슈 번호·사용자 식별자와 함께 누적 토큰(input/output/cache)을 이벤트(`claude_commit`)로 기록한다. + +### 3.2 훅 파이프라인 + +| 트리거 | 훅 이벤트 | 스크립트 | 역할 | +|---|---|---|---| +| Claude 응답 완료 | `Stop` | `aptabase-accumulate.ps1` | 트랜스크립트 JSONL 증분 파싱 → `state.accum` 누적 (네트워크 없음) | +| Claude의 `Bash` 툴 호출 후 | `PostToolUse(Bash)` | `aptabase-commit.ps1` | HEAD 해시 변화 감지 시 Aptabase POST 후 리셋 | +| 모든 git 커밋 (외부 포함) | `.git/hooks/post-commit` | `aptabase-commit.sh` | Claude 밖 커밋도 포착 (clone 시 재설치 필요) | + +중복 전송은 `last_sent_commit` 해시 비교로 방지. + +### 3.3 settings.json 등록 방식 ([settings.json](.claude/settings.json)) +PowerShell 인라인 래퍼로 stdin JSON을 받아 `cwd` 추출 → 프로젝트의 `.ps1`을 실행하는 구조. 이 방식 덕에 전역 설정 한 줄로 프로젝트별 훅이 동작한다. + +### 3.4 현재 설정값 ([aptabase.json](.claude/hooks/aptabase.json)) +- `enabled: true` +- `app_key: A-SH-7756143445` ⚠️ (self-hosted 키지만 평문 커밋 주의) +- `aptabase_host: https://aptabase.hmac.kr` +- `user_name: 김민성(b16213)` +- `git_repositories: ["D:/MYCLAUDE_PROJECT/workhistory"]` + +### 3.5 현재 상태 ([aptabase-accum.json](.claude/state/aptabase-accum.json)) +- 누적 토큰 전부 0 (마지막 커밋에서 flush된 직후 상태) +- `last_sent_commit: e09c019…` — HEAD와 일치 → 정상 + +## 4. 전송 이벤트 스키마 (`claude_commit`) + +`props` 주요 필드: +- **식별**: `claude_oauth_id`, `plan`, `user_name`, `local_ip`, `public_ip` +- **커밋**: `commit_hash`, `commit_message`, `issue_number`, `repository`, `repository_url` +- **토큰**: `total_tokens`, `input_tokens`, `cache_creation_tokens`, `cache_read_tokens`, `output_tokens` + +이슈 번호 regex: `FEAT-123`, `BUG-45`, `closes #45`, `GH-8`, `#123` 등. + +## 5. 슬래시 스킬 + +[.claude/skills/aptabase/SKILL.md](.claude/skills/aptabase/SKILL.md) — "aptabase 설정/테스트/토큰 기록 확인/누적 토큰" 질의 시 자동 발동. 설치·검증·트러블슈팅 절차를 담은 사용자-facing 문서 역할. + +## 6. 관찰 및 리스크 + +1. **`app_key` 평문 커밋** — self-hosted 라 영향은 제한적이나, 외부 공개 저장소로 전환 시 반드시 gitignore 또는 환경변수로 분리 필요. +2. **`usage.md` ↔ 실제 파일 불일치** — 문서는 `.py` 기반 구현을 전제로 하나, 실제는 `.sh` / `.ps1` 만 존재. 문서 갱신 또는 `.py` 복구 중 하나가 필요. +3. **12MB 바이너리 커밋** — `claude-common-installer(3).exe`가 저장소에 포함됨. Git LFS 또는 별도 배포 채널 권장. +4. **`.git/hooks/post-commit` 재설치** — clone 시마다 `install-git-hook.sh` 수동 실행 필요. 자동화 스크립트가 없음. +5. **`memory/` 디렉터리** — README에 명시되어 있으나 현재는 비어있음(또는 미생성). 전역 메모리는 `C:\Users\nbright\.claude\projects\...\memory\` 쪽에 저장되는 것으로 보임. + +## 7. 커밋 히스토리 + +``` +e09c019 test: empty test commit +a6e9e49 docs: flesh out README with repo purpose and structure +9357d85 test: aptabase flush dry-run (FEAT-999) +07660b9 test: empty test commit +e44877d Initial commit +``` + +대부분의 커밋이 훅 파이프라인 E2E 검증용 empty/dry-run 커밋이다. `9357d85`에서 이슈 번호 추출 로직까지 실제 flush 검증을 마친 것으로 보인다. + +## 8. 요약 + +이 저장소는 **"Claude Code 사용량을 커밋 단위로 Aptabase에 자동 기록하는 훅 시스템"의 개인 실험장**이다. 핵심 산출물은 `.claude/hooks/`의 Aptabase 연동 훅 묶음이며, 나머지(README, test 파일, empty 커밋)는 모두 그 훅의 동작을 검증하기 위한 스캐폴딩이다.