chore: initial commit — portable agent memory & skill scaffold
Extracted and cleaned up from a prior WSL/Windows agent-memory setup. Contains only project-agnostic content: - Claude_Memory/: feedback rules, user profile, reusable reference docs, session scratch space (claude_info/, claude_log/), mirrored skill defs - caveman/: ultra-compressed communication mode skill + subcommands - html-slide-deck/: self-contained HTML slide deck builder skill Project-specific knowledge (previously mixed in) was moved out to each project's own repository as part of this cleanup.
This commit is contained in:
commit
25256ec417
61 files changed
+4431
No files matched your search
Executable
+2
@@ -0,0 +1,2 @@
|
|||||||
|
cache
|
||||||
|
graphify-out/
|
||||||
Executable
+46
@@ -0,0 +1,46 @@
|
|||||||
|
# Memory Index
|
||||||
|
|
||||||
|
> 작업 시작 시 `claude_info/CHECKPOINT.md` 먼저 확인.
|
||||||
|
> 소스 코드 작업 전 `graphify-out/GRAPH_REPORT.md` 먼저 읽기.
|
||||||
|
|
||||||
|
## 세션 상태
|
||||||
|
- [CHECKPOINT](claude_info/CHECKPOINT.md) — 현재 작업 상태, 브랜치, 다음 시작 위치
|
||||||
|
- [NOTES](claude_info/NOTES.md) — 세션별 주요 발견사항·비직관적 동작
|
||||||
|
- [TIPS](claude_info/TIPS.md) — 재사용 패턴 & 해결법
|
||||||
|
- [TODO](claude_info/TODO.md) — 우선순위별 다음 작업 목록
|
||||||
|
|
||||||
|
## 작업 로그 (claude_log/)
|
||||||
|
- (현재 없음 — 프로젝트별 로그는 각 프로젝트 폴더에 기록. 포맷은 [README](claude_log/README.md) 참고)
|
||||||
|
|
||||||
|
## Feedback (행동 규칙)
|
||||||
|
- [Commit 전 Linter](feedback_linter_before_commit.md) — commit 전 변경 파일 linter 실행
|
||||||
|
- [Agent 자동 승인](feedback_agent_permission.md) — Agent(*) 자동 실행
|
||||||
|
- [Bash/Python3 자동 승인](feedback_bash_python_auto_approve.md) — Bash/Python3 즉시 실행
|
||||||
|
- [Truncated output 판단 금지](feedback_truncated_output.md) — "Output too large" 시 전체 파일 읽고 판단
|
||||||
|
- [Caveman 한국어](feedback_caveman_korean.md) — caveman 모드 활성 시 한국어 파편문
|
||||||
|
- [Caveman MD 스타일](feedback_caveman_md_style.md) — 기획/스펙 문서 caveman style 작성
|
||||||
|
- [Handoff 로드](feedback_handoff_load.md) — 핸드오프 파일 경유 세션 인계
|
||||||
|
- [Worktree 확인](feedback_worktree_check.md) — 코드 작업 전 git worktree list + .worktrees/ 확인
|
||||||
|
|
||||||
|
## User Profile
|
||||||
|
- [코드 주석 스타일](user_code_comment_style.md) — 주요 로직 영문 주석, Doxygen (모든 코드베이스 공통)
|
||||||
|
- [세션 시작 스킬 안내](user_session_start_skills.md) — 세션 시작 시 활성 스킬 목록 제시
|
||||||
|
- [Usage 리셋 알람](user_usage_alarm.md) — "usage 꽉 찼다" → 5시간 후 알람 예약
|
||||||
|
- [Developer Profile](user_developer_profile.md) — 소통/의사결정/디버깅 성향 프로필
|
||||||
|
|
||||||
|
## Reference
|
||||||
|
- [graphify 도구](reference_graphify.md) — Knowledge Graph 빌드, 출력물 구조, 명령어 (범용)
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
- `claude_skills/` — archive-session, graphify, grill-me, handoff
|
||||||
|
- `../caveman/` — caveman, cavecrew, caveman-commit/compress/help/review/stats, find-skills
|
||||||
|
- `../html-slide-deck/`, `../html-slide-deck.skill`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 프로젝트별 메모리는 각 프로젝트 폴더에서 관리
|
||||||
|
|
||||||
|
이 폴더(`Agent_Memories`)는 프로젝트에 무관하게 재사용 가능한 범용 지식만 보관한다.
|
||||||
|
특정 프로젝트(코드베이스·언어·툴체인에 종속된) 지식은 해당 프로젝트 폴더 안에서 관리한다.
|
||||||
|
예: Universal_AVI 프로젝트 관련 지식은 `4_Universal_AVI/.claude/claude_info/`, `claude_log/`에 있음
|
||||||
|
(2026-07-01 통합 정리 이전 자료는 `4_Universal_AVI/.claude/_imported_from_agent_memory_unification/`에 아카이브됨).
|
||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# CHECKPOINT — 세션 진행 상황
|
||||||
|
|
||||||
|
> 작업 시작 시 이 파일을 가장 먼저 확인할 것.
|
||||||
|
> 작업 종료 전 현재 상태로 업데이트할 것.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 현재 상태
|
||||||
|
|
||||||
|
- **마지막 업데이트**: (없음)
|
||||||
|
- **진행 중인 작업**: 없음
|
||||||
|
- **브랜치**: (없음)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- 업데이트 포맷:
|
||||||
|
## 현재 상태
|
||||||
|
- **마지막 업데이트**: 2026-MM-DD
|
||||||
|
- **진행 중인 작업**: 작업명 또는 없음
|
||||||
|
- **브랜치**: feature/xxx or HEAD
|
||||||
|
-->
|
||||||
Executable
+18
@@ -0,0 +1,18 @@
|
|||||||
|
# NOTES — 중요 발견사항
|
||||||
|
|
||||||
|
작업 중 발견한 설계 결정, 비직관적 동작, 주의사항을 기록.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 포맷
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### [YYYY-MM-DD] 제목
|
||||||
|
|
||||||
|
**관련 파일**: `경로`
|
||||||
|
**내용**: 발견한 사실 / 결정 이유 / 주의사항
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- 여기에 노트 추가 -->
|
||||||
Executable
+22
@@ -0,0 +1,22 @@
|
|||||||
|
# TIPS — 재사용 패턴 & 해결법
|
||||||
|
|
||||||
|
반복적으로 쓰이는 코드 패턴, 편집 방법, 이슈 해결법을 기록.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 포맷
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### [태그] 제목
|
||||||
|
|
||||||
|
**상황**: 어떤 상황에서 쓰는가
|
||||||
|
**해결책**: 방법 설명
|
||||||
|
**예시**:
|
||||||
|
\`\`\`cpp
|
||||||
|
// 코드
|
||||||
|
\`\`\`
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- 여기에 팁 추가 -->
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
# TODO — 다음 작업 목록
|
||||||
|
|
||||||
|
미완료 항목, 다음 세션에서 이어받을 작업을 우선순위 순으로 기록.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 우선순위 기준
|
||||||
|
|
||||||
|
- `P1` — 즉시 처리 필요 (블로커)
|
||||||
|
- `P2` — 현재 마일스톤 내 처리
|
||||||
|
- `P3` — 다음 마일스톤 이후 가능
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<!-- 여기에 TODO 추가 -->
|
||||||
Executable
+51
@@ -0,0 +1,51 @@
|
|||||||
|
# claude_md — 작업 지식 로그
|
||||||
|
|
||||||
|
작업 중 발견한 코드 구조, 패턴, 제약조건을 날짜/주제별로 기록.
|
||||||
|
에이전트가 동일 영역 재작업 시 중복 탐색을 줄이기 위한 지식 베이스.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 파일 네이밍
|
||||||
|
|
||||||
|
```
|
||||||
|
LOG_YYYYMMDD_<Topic>.md
|
||||||
|
```
|
||||||
|
|
||||||
|
예시:
|
||||||
|
- `LOG_20260410_Auth_Module_Structure.md`
|
||||||
|
- `LOG_20260411_Build_Pipeline_Encoding.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 파일 포맷
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
---
|
||||||
|
date: YYYY-MM-DD
|
||||||
|
topic: 주제 (한글 가능)
|
||||||
|
related_files: [파일경로1, 파일경로2]
|
||||||
|
tags: [태그1, 태그2]
|
||||||
|
---
|
||||||
|
|
||||||
|
# 제목
|
||||||
|
|
||||||
|
## 발견 내용
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
## 주의사항
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
## 관련 코드 위치
|
||||||
|
|
||||||
|
- `파일경로:라인번호` — 설명
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 저장 시점
|
||||||
|
|
||||||
|
- 새로운 모듈/클래스 구조를 파악한 후
|
||||||
|
- 예상과 다른 동작/패턴을 발견했을 때
|
||||||
|
- 향후 동일 영역 작업 시 참조 가치가 있는 정보를 얻었을 때
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: archive-session
|
||||||
|
description: 현재 세션의 작업 결과, 파악된 지식, 다음 단계(TODO)를 프로젝트의 공식 문서 체계(claude_log, claude_info)에 따라 자동으로 아카이브하고 요약합니다. 작업 종료 직전이나 주요 마일스톤 완료 시 사용합니다.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Archive Session
|
||||||
|
|
||||||
|
이 스킬은 현재 세션에서 발생한 지식과 진행 상황을 구조화된 문서로 저장하여 다음 세션에서 문맥을 완벽하게 복원할 수 있도록 돕습니다.
|
||||||
|
|
||||||
|
## 📁 문서 체계 (Storage Locations)
|
||||||
|
|
||||||
|
모든 문서는 프로젝트 루트의 `.claude/` 하위에 저장됩니다.
|
||||||
|
|
||||||
|
- **작업 로그 (`.claude/claude_log/`)**: 세션의 상세 기술적 내역 보관.
|
||||||
|
- 파일명: `LOG_YYYYMMDD_<Topic>.md`
|
||||||
|
- **세션 정보 (`.claude/claude_info/`)**: 지속적으로 업데이트되는 상태 정보.
|
||||||
|
- `CHECKPOINT.md`: 현재 위치 및 다음 시작 지점.
|
||||||
|
- `NOTES.md`: 파악된 주요 기술적 사실.
|
||||||
|
- `TIPS.md`: 재사용 가능한 패턴.
|
||||||
|
- `TODO.md`: 잔여 작업 목록.
|
||||||
|
|
||||||
|
## 🔄 워크플로우 (Workflow)
|
||||||
|
|
||||||
|
스킬 호출 시 다음 단계를 수행하세요:
|
||||||
|
|
||||||
|
### 1. 정보 수집 및 요약
|
||||||
|
- `git log` 및 `git diff`를 통해 이번 세션의 코드 변경 사항 확인.
|
||||||
|
- 파악된 새로운 아키텍처 규칙, 안티 패턴, 파일 구조 정리.
|
||||||
|
|
||||||
|
### 2. 상세 로그 작성 (`.claude/claude_log/`)
|
||||||
|
새로운 로그 파일을 생성합니다.
|
||||||
|
- **날짜**: YYYY-MM-DD
|
||||||
|
- **주제**: 핵심 작업 내용 (예: UI Mapping Enhancement)
|
||||||
|
- **내용**: 변경된 파일 목록, 구현된 주요 로직, 직면했던 문제와 해결 방법.
|
||||||
|
|
||||||
|
### 3. 상태 업데이트 (`.claude/claude_info/`)
|
||||||
|
기존 파일들을 **교체(Overwrite)** 하거나 **추가(Append)** 합니다.
|
||||||
|
- **CHECKPOINT**: 이전 내용을 지우고 현재의 최신 상태와 "Next Step"만 명확히 기술.
|
||||||
|
- **NOTES/TIPS**: 이번에 새로 배운 내용이 있다면 리스트에 추가.
|
||||||
|
- **TODO**: 완료된 항목은 제거하거나 체크(`[x]`)하고, 새로 발견된 과제 추가.
|
||||||
|
|
||||||
|
### 4. 프로젝트 루트 업데이트
|
||||||
|
- **CLAUDE.md**: 프로젝트 전반에 걸친 중요한 규칙(Rule) 변경이 있다면 반영.
|
||||||
|
|
||||||
|
### 5. Graphify 지식 그래프 갱신
|
||||||
|
세션 중 코드 변경이 있었다면 `graphify update .`를 실행하여 지식 그래프를 최신 상태로 유지.
|
||||||
|
```bash
|
||||||
|
if command -v graphify &> /dev/null; then
|
||||||
|
graphify update .
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
- graphify CLI가 없으면 skip.
|
||||||
|
- 완료 후 사용자에게 갱신 여부 보고.
|
||||||
|
|
||||||
|
## 💡 작성 가이드라인 (Writing Guidelines)
|
||||||
|
|
||||||
|
- **언어**: 모든 문서는 **한국어**로 작성하되, 기술 용어는 원문을 병기하거나 그대로 사용 (예: Multi-line support).
|
||||||
|
- **간결성**: Caveman 스타일을 유지하여 핵심 정보만 불릿 포인트로 기술.
|
||||||
|
- **검증**: 아카이브 완료 후 `ls` 명령으로 파일 생성을 확인하고 사용자에게 요약 보고.
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0.8.1
|
||||||
+1090
File diff suppressed because it is too large.
Load diff
+10
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: grill-me
|
||||||
|
description: Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
|
||||||
|
---
|
||||||
|
|
||||||
|
Interview me relentlessly about every aspect of this plan until we reach a shared understanding. Walk down each branch of the design tree, resolving dependencies between decisions one-by-one. For each question, provide your recommended answer.
|
||||||
|
|
||||||
|
Ask the questions one at a time.
|
||||||
|
|
||||||
|
If a question can be answered by exploring the codebase, explore the codebase instead.
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: handoff
|
||||||
|
description: Compact the current conversation into a handoff document for another agent to pick up.
|
||||||
|
argument-hint: "What will the next session be used for?"
|
||||||
|
---
|
||||||
|
|
||||||
|
Write a handoff document summarising the current conversation so a fresh agent can continue the work. Save it to a path produced by `mktemp -t handoff-XXXXXX.md` (read the file before you write to it).
|
||||||
|
|
||||||
|
Suggest the skills to be used, if any, by the next session.
|
||||||
|
|
||||||
|
Do not duplicate content already captured in other artifacts (PRDs, plans, ADRs, issues, commits, diffs). Reference them by path or URL instead.
|
||||||
|
|
||||||
|
If the user passed arguments, treat them as a description of what the next session will focus on and tailor the doc accordingly.
|
||||||
Executable
+13
@@ -0,0 +1,13 @@
|
|||||||
|
---
|
||||||
|
name: Agent 도구 자동 승인
|
||||||
|
description: Agent(서브에이전트) 도구는 settings.json allow 목록에 등록되어 있어 자동 실행됨
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
`Agent(*)` = `~/.claude/settings.json` `permissions.allow` 등록 → 승인없이 자동실행.
|
||||||
|
|
||||||
|
**Why:** `Bash(python3:*)` 동일. 코드리뷰·탐색 서브에이전트 매번 승인프롬프트 불필요.
|
||||||
|
|
||||||
|
**How to apply:**
|
||||||
|
- `Agent` 호출 시 확인없이 실행
|
||||||
|
- `Bash(python3:*)` 동일 "Don't Ask" 동작
|
||||||
+11
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: Bash/Python3 자동 승인
|
||||||
|
description: Bash 및 Python3 명령은 사용자 승인 없이 자동 실행
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
Bash/Python3 실행 시 승인 요청 금지.
|
||||||
|
|
||||||
|
**Why:** 반복 호출마다 승인 요청 → 작업 흐름 단절.
|
||||||
|
|
||||||
|
**How to apply:** Bash 호출 즉시 실행. 단, 파일 삭제·git push 등 파괴적 작업은 확인 필요.
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: caveman-korean
|
||||||
|
description: caveman 모드 활성 시 한국어로 응답
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: feedback
|
||||||
|
originSessionId: 8f69f046-87b8-492c-8f19-ca78a8512cfa
|
||||||
|
---
|
||||||
|
|
||||||
|
Caveman 모드 활성 여부 무관하게 모든 응답 한국어. Caveman 문체(간결·파편·조사 생략) 유지하되 언어는 항상 한국어.
|
||||||
|
|
||||||
|
**Why:** 사용자 명시 요청. 영어 caveman 출력 거부.
|
||||||
|
**How to apply:** `/caveman` 활성 시 한국어 파편문으로 응답. 예: "Bug in auth middleware" → "auth 미들웨어 버그".
|
||||||
|
Gemini/Antigravity 에이전트에서는 세션 기본값을 caveman mode(핵심만, 한국어)로 둘 것을 사용자가 선호.
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: feedback-caveman-md-style
|
||||||
|
description: "프로젝트 기획/스펙 MD 파일은 caveman style(파편문, 간결체)로 작성"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: feedback
|
||||||
|
originSessionId: 9eaa0ad7-0128-4d11-be9b-fa11a0bb5936
|
||||||
|
---
|
||||||
|
|
||||||
|
프로젝트 기획/스펙 문서(PROJECT.md, REQUIREMENTS.md, ROADMAP.md 류 — 특정 기획 도구 무관)는 caveman style로 작성.
|
||||||
|
|
||||||
|
**Why:** 불필요한 설명 없이 핵심 정보만. 가독성·유지보수 효율.
|
||||||
|
|
||||||
|
**How to apply:** 문장 대신 키워드/명사구 중심. 동사는 최소화. 설명 문단 지양.
|
||||||
Executable
+15
@@ -0,0 +1,15 @@
|
|||||||
|
---
|
||||||
|
name: ""
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
originSessionId: e5b7295d-190c-4567-9da4-b103fffee074
|
||||||
|
---
|
||||||
|
|
||||||
|
"handoff 불러들여", "handoff 파일 로드", "이전 handoff 불러와" 등 handoff 파일 로드 요청 시:
|
||||||
|
|
||||||
|
1. `~/.claude/last_handoff_path.txt` 읽기 → 경로 확인
|
||||||
|
2. 해당 경로의 파일 Read → 컨텍스트 복원
|
||||||
|
|
||||||
|
**Why:** handoff 스킬은 매번 `/tmp/handoff-XXXXXX.md` 형태의 임시 파일을 생성하므로 경로가 고정되지 않음. PostToolUse hook이 Write 시 자동으로 `~/.claude/last_handoff_path.txt`에 경로를 저장.
|
||||||
|
|
||||||
|
**How to apply:** handoff 로드 요청 시 파일 경로 추측 금지. 반드시 `~/.claude/last_handoff_path.txt` 먼저 읽을 것.
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
---
|
||||||
|
name: Commit 전 Linter 수행
|
||||||
|
description: git commit 전 변경 파일에 linter 실행, 통과 후 commit
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
`git commit` 전 변경 파일에 linter 실행.
|
||||||
|
|
||||||
|
**Why:** 린팅 오류 커밋 이력 방지.
|
||||||
|
|
||||||
|
**How to apply:**
|
||||||
|
- `git diff --name-only HEAD` 또는 `git status`로 변경 파일 확인.
|
||||||
|
- `.clang-format` 있으면 `clang-format --dry-run`으로 C++ 검사.
|
||||||
|
- `.clang-tidy` 있으면 `clang-tidy` 정적 분석.
|
||||||
|
- 기타 linter(eslint, pylint 등) 있으면 실행.
|
||||||
|
- linter 오류 → 수정 후 commit.
|
||||||
|
- linter 설정 없어도 사용자에게 수행 여부 확인.
|
||||||
Executable
+14
@@ -0,0 +1,14 @@
|
|||||||
|
---
|
||||||
|
name: feedback-truncated-output
|
||||||
|
description: "system-reminder에 \"Output too large / Preview\" 표시 시 전체 파일 읽기 필수"
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: feedback
|
||||||
|
originSessionId: 442b5c1a-bf31-4f49-be3a-abe955ac1516
|
||||||
|
---
|
||||||
|
|
||||||
|
system-reminder에 "Output too large... Full output saved to: <path>" 메시지가 있으면, 미리보기(Preview)만 보고 내용을 판단하지 말 것.
|
||||||
|
|
||||||
|
**Why:** 2KB 미리보기만 보고 "CHECKPOINT.md / TODO.md만 주입됐다"고 잘못 단정한 사례 발생. 실제로는 NOTES.md, TIPS.md, 로그 파일 2개까지 모두 주입됐음.
|
||||||
|
|
||||||
|
**How to apply:** "Output too large" 문구 확인 시 → 저장된 전체 파일 경로를 Read 또는 `cat`으로 읽은 후 판단.
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: 코드 작업 전 Worktree 확인
|
||||||
|
description: 코드 작업 전 `git worktree list` + `.worktrees/` 확인 → 기존 worktree 재사용 or 신규 생성 여부 사용자 확인
|
||||||
|
type: feedback
|
||||||
|
---
|
||||||
|
|
||||||
|
코드 작업 전 반드시 worktree 상태 확인.
|
||||||
|
|
||||||
|
**Why:** 기존 worktree 모르고 main 브랜치 작업 or 브랜치 중복 생성 방지.
|
||||||
|
|
||||||
|
**How to apply:**
|
||||||
|
1. `git worktree list`로 등록된 worktree 목록 확인.
|
||||||
|
2. `.worktrees/` 하위 폴더명 확인 (`ls .worktrees/` 또는 `Glob`).
|
||||||
|
3. 관련 worktree 존재 → 해당 worktree 사용.
|
||||||
|
4. 관련 worktree 없음 → 사용자에게 신규 생성 여부 확인.
|
||||||
|
5. 생성 시 `.worktrees/<작업명>` 경로 + 브랜치명 사용자 협의.
|
||||||
Executable
+88
@@ -0,0 +1,88 @@
|
|||||||
|
---
|
||||||
|
name: reference_graphify
|
||||||
|
description: graphify 도구 전체 개요 — 설치·실행 방법, 출력물 위치, 쿼리 방법
|
||||||
|
metadata:
|
||||||
|
type: reference
|
||||||
|
---
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
`graphify` (pip 패키지명: `graphifyy`)는 폴더의 코드·문서·이미지·영상을 Knowledge Graph로 변환하는 도구.
|
||||||
|
트리거: `/graphify` 슬래시 명령 → Skill tool `skill: "graphify"` 호출 필수.
|
||||||
|
|
||||||
|
> **공유 리소스 가능성**: graphify 출력물을 다른 AI Agent(예: Google Antigravity)와 공유하는 경우, 출력물 수정·재빌드 시 다른 Agent의 참조에 영향을 줄 수 있으므로 주의.
|
||||||
|
|
||||||
|
- Skill 위치: `Claude_Memory/claude_skills/graphify/SKILL.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 출력물 구조 (`graphify-out/`)
|
||||||
|
|
||||||
|
| 파일 | 설명 |
|
||||||
|
|------|------|
|
||||||
|
| `GRAPH_REPORT.md` | 코드베이스 질문 시 **먼저 읽어야 할 파일**. God Nodes·커뮤니티·Surprising Connections 포함 |
|
||||||
|
| `graph.json` | 원본 그래프 데이터 (GraphRAG-ready JSON) |
|
||||||
|
| `graph.html` | 브라우저에서 여는 인터랙티브 시각화 |
|
||||||
|
| `manifest.json` | 이전 실행 기록 (incremental update용) |
|
||||||
|
| `cache/` | 파일별 시멘틱 추출 캐시 |
|
||||||
|
| `.graphify_labels.json` | 커뮤니티 레이블 |
|
||||||
|
| `.graphify_python` | 사용 중인 Python 인터프리터 경로 |
|
||||||
|
| `.graphify_root` | 스캔 루트 경로 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 주요 명령
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 최초 빌드
|
||||||
|
/graphify <path>
|
||||||
|
|
||||||
|
# 변경 파일만 재추출 (코드만 변경 시 LLM 불필요)
|
||||||
|
/graphify <path> --update
|
||||||
|
|
||||||
|
# 그래프 질문 (BFS: 넓은 컨텍스트)
|
||||||
|
/graphify query "<질문>"
|
||||||
|
|
||||||
|
# 그래프 질문 (DFS: 특정 경로 추적)
|
||||||
|
/graphify query "<질문>" --dfs
|
||||||
|
|
||||||
|
# 두 개념 간 최단 경로
|
||||||
|
/graphify path "<A>" "<B>"
|
||||||
|
|
||||||
|
# 단일 노드 설명
|
||||||
|
/graphify explain "<노드명>"
|
||||||
|
|
||||||
|
# 커뮤니티 재클러스터링만 (추출 생략)
|
||||||
|
/graphify <path> --cluster-only
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 관리 정책
|
||||||
|
|
||||||
|
- graphify 출력물(`graphify-out/`)은 대상 프로젝트 루트가 아니라 **이 메모리 폴더(`Agent_Memories`) 하위**에서 관리하는 것을 기본 원칙으로 한다 (프로젝트 저장소를 캐시로 오염시키지 않기 위함).
|
||||||
|
- 프로젝트별로 스캔 제외 규칙(불필요한 대용량/생성물 폴더)이 필요할 수 있음 — 대상 프로젝트의 `.gitignore` 패턴을 기본으로 따르고, 프로젝트 전용 제외 규칙은 해당 프로젝트의 자체 메모리/문서에 기록한다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 사용 규칙 (CLAUDE.md에서)
|
||||||
|
|
||||||
|
1. **소스 파일 읽기·grep/glob 검색·코드베이스 질문 전 항상 `graphify-out/GRAPH_REPORT.md` 먼저 읽기**
|
||||||
|
2. `graphify-out/wiki/index.md` 존재 시 raw 파일 대신 탐색
|
||||||
|
3. 교차 모듈 관계 질문: grep 대신 `graphify query/path/explain` 우선
|
||||||
|
4. graphify 출력물은 대상 프로젝트 루트가 아닌 이 메모리 폴더 하위에서 관리
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 설치 확인
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# uv 기반 (권장)
|
||||||
|
uv tool install --upgrade graphifyy
|
||||||
|
|
||||||
|
# pip 기반
|
||||||
|
pip install graphifyy --break-system-packages
|
||||||
|
```
|
||||||
|
|
||||||
|
GEMINI_API_KEY 또는 GOOGLE_API_KEY 미설정 시 → Claude Code 서브에이전트로 시멘틱 추출 수행.
|
||||||
|
설정 시 → Gemini(`gemini-3-flash-preview`) 사용으로 토큰 절감.
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: 코드 주석 스타일 규칙
|
||||||
|
description: 구현 시 주요 Logic→영문 주석, 필요시 Doxygen
|
||||||
|
type: user
|
||||||
|
---
|
||||||
|
|
||||||
|
Impl: EN comment per major logic block. Doxygen for func/class/complex logic.
|
||||||
|
|
||||||
|
**Applies to:** 모든 코드베이스 (프로젝트 무관, 2026-04-08 global 규칙으로 격상됨)
|
||||||
|
|
||||||
|
**Why:** readability/doc quality 선호, all codebases.
|
||||||
|
|
||||||
|
**How to apply:**
|
||||||
|
- Every significant logic block → short English comment describing intent
|
||||||
|
- Function/class definitions → Doxygen `/** @brief ... @param ... @return ... */`
|
||||||
|
- Complex conditionals → inline comment: condition purpose
|
||||||
Executable
+27
@@ -0,0 +1,27 @@
|
|||||||
|
---
|
||||||
|
name: user_developer_profile
|
||||||
|
description: 사용자의 개발자 성향 프로필 — 소통/의사결정/디버깅/UX 철학 등
|
||||||
|
metadata:
|
||||||
|
type: user
|
||||||
|
---
|
||||||
|
|
||||||
|
| Dimension | Rating | Confidence |
|
||||||
|
|-----------|--------|------------|
|
||||||
|
| Communication | terse-direct | HIGH |
|
||||||
|
| Decisions | fast-intuitive | HIGH |
|
||||||
|
| Explanations | concise | MEDIUM |
|
||||||
|
| Debugging | diagnostic | MEDIUM |
|
||||||
|
| UX Philosophy | function-first | MEDIUM |
|
||||||
|
| Vendor Choices | opinionated | LOW |
|
||||||
|
| Frustrations | instruction-adherence | MEDIUM |
|
||||||
|
| Learning | self-directed | MEDIUM |
|
||||||
|
|
||||||
|
**Directives:**
|
||||||
|
- **Communication:** 간결·행동. 결과 먼저. 군더더기 없음.
|
||||||
|
- **Decisions:** 명확 추천, 빠르게. 목록 금지(요청 시 제외).
|
||||||
|
- **Explanations:** 설명+코드 간결. 한 문장 이유. 설명 불필요(요청 시 제외).
|
||||||
|
- **Debugging:** 원인 먼저, 수정.
|
||||||
|
- **UX Philosophy:** 기능 우선. 스타일 금지(요청 시). UX=사용성/데이터흐름.
|
||||||
|
- **Vendor Choices:** 기술 제약 준수. 대체 라이브러리 금지(요청 시). 최소 의존성.
|
||||||
|
- **Frustrations:** 지시 정확히. 반복 재독. 범위 확장 금지. 모호 시 질문.
|
||||||
|
- **Learning:** 맥락 파악 전제. 직접 답변. 기본 생략.
|
||||||
Executable
+11
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
name: 세션 시작 시 활성 스킬 안내
|
||||||
|
description: 세션 시작 시 활성 skill 목록 사용자에게 알릴 것
|
||||||
|
type: user
|
||||||
|
---
|
||||||
|
|
||||||
|
세션 시작 시 활성 Claude skill 목록 사용자에게 안내.
|
||||||
|
|
||||||
|
**Why:** 사용자가 `/skills` 없이 활성 스킬 파악 가능.
|
||||||
|
|
||||||
|
**How to apply:** 새 세션 시작 시 (context 요약 후 첫 응답 포함) system-reminder 의 skills 목록 확인, 표 형식으로 제시.
|
||||||
Executable
+16
@@ -0,0 +1,16 @@
|
|||||||
|
---
|
||||||
|
name: user-usage-alarm
|
||||||
|
description: Claude Code usage 리셋 알람 패턴 — 사용자가 usage 꽉 찼다고 하면 1시간 후 PushNotification 예약
|
||||||
|
metadata:
|
||||||
|
node_type: memory
|
||||||
|
type: feedback
|
||||||
|
originSessionId: 246c0ef0-1a76-42ef-bcc3-483d58b9fc21
|
||||||
|
---
|
||||||
|
|
||||||
|
사용자 "usage 꽉 찼다"/"usage full" 전달 시:
|
||||||
|
1. 즉시 `ScheduleWakeup(delaySeconds=18000)` 호출 — 5시간 후 예약
|
||||||
|
2. 5시간 후 `PushNotification` 발송 — "Claude Code usage가 리셋됐을 것 같습니다"
|
||||||
|
|
||||||
|
**Why:** Claude Code usage % 조회 API 없음 — 직접 감지 불가. 사용자 수동 알림 기준 5시간 타이머.
|
||||||
|
|
||||||
|
**How to apply:** 사용자 usage 한도 도달 메시지 수신 즉시 — 확인 없이 ScheduleWakeup 실행.
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 JunHo Kim
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Agent Memories
|
||||||
|
|
||||||
|
A portable, project-agnostic memory and skill scaffold for AI coding agents (Claude Code, Gemini/Antigravity, and similar).
|
||||||
|
|
||||||
|
The core idea: an agent that works across many different codebases accumulates two kinds of knowledge —
|
||||||
|
knowledge that is useful *everywhere* (how you like to communicate, how you like commits done, reusable
|
||||||
|
tool workflows) and knowledge that only makes sense for *one specific project* (its language, its build
|
||||||
|
system, its domain quirks). Mixing the two makes the "everywhere" knowledge unusable when you switch to a
|
||||||
|
different project. This repository keeps only the first kind. Project-specific knowledge lives inside each
|
||||||
|
project's own repository instead.
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
Agent_Memories/
|
||||||
|
├── Claude_Memory/ # Claude-specific working memory
|
||||||
|
│ ├── MEMORY.md # index — start here
|
||||||
|
│ ├── claude_info/ # CHECKPOINT / NOTES / TIPS / TODO (session-scoped scratch files)
|
||||||
|
│ ├── claude_log/ # dated work logs (LOG_YYYYMMDD_<topic>.md), format only — see README inside
|
||||||
|
│ ├── claude_skills/ # mirrored Skill definitions (archive-session, graphify, grill-me, handoff)
|
||||||
|
│ ├── feedback_*.md # behavioral rules learned from user corrections/confirmations
|
||||||
|
│ ├── user_*.md # stable facts about the user (preferences, profile)
|
||||||
|
│ └── reference_*.md # reusable tool/reference documentation (e.g. graphify usage)
|
||||||
|
├── caveman/ # ultra-compressed communication mode (skill + subcommands)
|
||||||
|
├── html-slide-deck/ # skill for building self-contained HTML slide decks
|
||||||
|
└── html-slide-deck.skill # packaged version of the above
|
||||||
|
```
|
||||||
|
|
||||||
|
### Memory file conventions (`Claude_Memory/`)
|
||||||
|
|
||||||
|
| Prefix | Meaning | Changes how often? |
|
||||||
|
|---|---|---|
|
||||||
|
| `feedback_*.md` | A behavioral rule learned from an explicit user correction or confirmation. Each file states the rule, **why** it exists, and **how** to apply it. | Rare — only when the user corrects or confirms an approach |
|
||||||
|
| `user_*.md` | Stable facts about the user: preferences, working style, profile. | Rare |
|
||||||
|
| `reference_*.md` | Documentation for a reusable tool or workflow that isn't tied to any one project. | Occasional |
|
||||||
|
| `claude_info/*.md` | Short-lived session scratch space (current status, discoveries, todos). | Every session |
|
||||||
|
| `claude_log/*.md` | Dated logs of non-obvious discoveries, kept for future reference. | Per notable discovery |
|
||||||
|
|
||||||
|
**Rule of thumb**: if a note only makes sense to someone who already knows the specific project you were
|
||||||
|
working in, it does not belong here — move it into that project's own memory/notes instead. This repository
|
||||||
|
was extracted from a working setup that originally mixed both kinds of content; project-specific material
|
||||||
|
was moved out during that cleanup.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
1. Clone or copy this repository's contents into wherever your agent looks for pers
|
||||||
Executable
+41
@@ -0,0 +1,41 @@
|
|||||||
|
# cavecrew
|
||||||
|
|
||||||
|
Decision guide. When to delegate to caveman subagents instead of doing the work inline.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Tells the main thread when to spawn a caveman-style subagent versus the vanilla equivalent. The win: subagent tool-results inject back into main context verbatim, and caveman output is roughly 1/3 the size of vanilla prose. Across 20 delegations in one session, that is the difference between context exhaustion and finishing the task.
|
||||||
|
|
||||||
|
Three subagents:
|
||||||
|
|
||||||
|
| Subagent | Job | Use when |
|
||||||
|
|----------|-----|----------|
|
||||||
|
| `cavecrew-investigator` | Locate code (read-only) | "Where is X defined / what calls Y / list uses of Z" |
|
||||||
|
| `cavecrew-builder` | Surgical edit, 1-2 files | Scope is obvious, ≤2 files. Refuses 3+ file scope. |
|
||||||
|
| `cavecrew-reviewer` | Diff/file review | One-line findings with severity emoji |
|
||||||
|
|
||||||
|
Use vanilla `Explore` or `Code Reviewer` when you want prose, architecture commentary, or rationale. Use main thread directly for one-line answers and 3+ file refactors.
|
||||||
|
|
||||||
|
This skill is a decision guide, not a slash command. It activates when the conversation mentions delegation.
|
||||||
|
|
||||||
|
## How to invoke
|
||||||
|
|
||||||
|
Triggers on phrases like "delegate to subagent", "use cavecrew", "spawn investigator", "save context", "compressed agent output".
|
||||||
|
|
||||||
|
## Example chaining
|
||||||
|
|
||||||
|
Locate → fix → verify (most common):
|
||||||
|
|
||||||
|
1. `cavecrew-investigator` returns site list (`path:line — symbol — note`)
|
||||||
|
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`
|
||||||
|
3. `cavecrew-reviewer` audits the resulting diff
|
||||||
|
|
||||||
|
Parallel scout: spawn 2-3 `cavecrew-investigator` calls in one message with different angles (defs, callers, tests). Aggregate in main.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`SKILL.md`](./SKILL.md) — full decision matrix and output contracts
|
||||||
|
- [`agents/cavecrew-investigator.md`](../../agents/cavecrew-investigator.md)
|
||||||
|
- [`agents/cavecrew-builder.md`](../../agents/cavecrew-builder.md)
|
||||||
|
- [`agents/cavecrew-reviewer.md`](../../agents/cavecrew-reviewer.md)
|
||||||
|
- [Caveman README](../../README.md) — repo overview
|
||||||
Executable
+82
@@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
name: cavecrew
|
||||||
|
description: >
|
||||||
|
Decision guide for delegating to caveman-style subagents. Tells the main
|
||||||
|
thread WHEN to spawn `cavecrew-investigator` (locate code), `cavecrew-builder`
|
||||||
|
(1-2 file edit), or `cavecrew-reviewer` (diff review) instead of doing the
|
||||||
|
work inline or using vanilla `Explore`. Subagent output is caveman-compressed
|
||||||
|
so the tool-result injected back into main context is ~60% smaller — main
|
||||||
|
context lasts longer across long sessions.
|
||||||
|
Trigger: "delegate to subagent", "use cavecrew", "spawn investigator/builder/reviewer",
|
||||||
|
"save context", "compressed agent output".
|
||||||
|
---
|
||||||
|
|
||||||
|
Cavecrew = three subagent presets that emit caveman output. Same job as Anthropic defaults (`Explore`, edit-style agents, reviewer); difference is the tool-result they return is compressed, so main context shrinks per delegation.
|
||||||
|
|
||||||
|
## When to use cavecrew vs alternatives
|
||||||
|
|
||||||
|
| Task | Use |
|
||||||
|
|---|---|
|
||||||
|
| "Where is X defined / what calls Y / list uses of Z" | `cavecrew-investigator` |
|
||||||
|
| Same but you also want suggestions/architecture commentary | `Explore` (vanilla) |
|
||||||
|
| Surgical edit, ≤2 files, scope obvious | `cavecrew-builder` |
|
||||||
|
| New feature / 3+ files / cross-cutting refactor | Main thread or `feature-dev:code-architect` |
|
||||||
|
| Review diff, branch, or file for bugs | `cavecrew-reviewer` |
|
||||||
|
| Deep code review with rationale + alternatives | `Code Reviewer` (vanilla) |
|
||||||
|
| One-line answer you already know | Main thread, no subagent |
|
||||||
|
|
||||||
|
Rule of thumb: **if you'd want the subagent's output in 1/3 the tokens, pick cavecrew. If you'd want prose, pick vanilla.**
|
||||||
|
|
||||||
|
## Why this exists (the real win)
|
||||||
|
|
||||||
|
Subagent tool results get injected into main context verbatim. A vanilla `Explore` that returns 2k tokens of prose costs 2k tokens of main-context budget every time. The same finding from `cavecrew-investigator` returns ~700 tokens. Across 20 delegations in one session that's the difference between context exhaustion and finishing the task.
|
||||||
|
|
||||||
|
## Output contracts
|
||||||
|
|
||||||
|
What main thread can rely on per agent:
|
||||||
|
|
||||||
|
**`cavecrew-investigator`**
|
||||||
|
```
|
||||||
|
<Header>:
|
||||||
|
- path:line — `symbol` — short note
|
||||||
|
totals: <counts>.
|
||||||
|
```
|
||||||
|
Or `No match.` Always file-path-first, line-number-attached, backticked symbols. Safe to grep with `path:\d+`.
|
||||||
|
|
||||||
|
**`cavecrew-builder`**
|
||||||
|
```
|
||||||
|
<path:line-range> — <change ≤10 words>.
|
||||||
|
verified: <re-read OK | mismatch @ path:line>.
|
||||||
|
```
|
||||||
|
Or one of: `too-big.` / `needs-confirm.` / `ambiguous.` / `regressed.` (terminal first token).
|
||||||
|
|
||||||
|
**`cavecrew-reviewer`**
|
||||||
|
```
|
||||||
|
path:line: <emoji> <severity>: <problem>. <fix>.
|
||||||
|
totals: N🔴 N🟡 N🔵 N❓
|
||||||
|
```
|
||||||
|
Or `No issues.` Findings sorted file → line ascending.
|
||||||
|
|
||||||
|
## Chaining patterns
|
||||||
|
|
||||||
|
**Locate → fix → verify** (most common):
|
||||||
|
1. `cavecrew-investigator` returns site list.
|
||||||
|
2. Main thread picks 1-2 sites, hands paths to `cavecrew-builder`.
|
||||||
|
3. `cavecrew-reviewer` audits the diff.
|
||||||
|
|
||||||
|
**Parallel scout** (when investigation is broad):
|
||||||
|
Spawn 2-3 `cavecrew-investigator` calls in one message (different angles: defs vs callers vs tests). Aggregate in main thread.
|
||||||
|
|
||||||
|
**Single-shot edit** (when site is already known):
|
||||||
|
Skip investigator. Hand exact path:line to `cavecrew-builder` directly.
|
||||||
|
|
||||||
|
## What NOT to do
|
||||||
|
|
||||||
|
- Don't use `cavecrew-builder` when you don't already know the file. Spawn investigator first or main thread will eat tokens passing context.
|
||||||
|
- Don't chain `cavecrew-investigator → cavecrew-builder` for a 5-file refactor. Builder will return `too-big.` and you'll have wasted a turn.
|
||||||
|
- Don't ask `cavecrew-reviewer` for "general feedback" — it returns findings only, no architecture opinions. Use `Code Reviewer` for that.
|
||||||
|
- Don't expect prose. Cavecrew output is structured, sometimes terse to the point of cryptic. If a human will read it directly, paraphrase.
|
||||||
|
|
||||||
|
## Auto-clarity (inherited)
|
||||||
|
|
||||||
|
Subagents drop caveman → normal English for security warnings, irreversible-action confirmations, and any output where fragment ambiguity could be misread. Resume caveman after.
|
||||||
Executable
+44
@@ -0,0 +1,44 @@
|
|||||||
|
# caveman-commit
|
||||||
|
|
||||||
|
Terse Conventional Commits. Why over what.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Generates commit messages in Conventional Commits format. Subject ≤50 chars, hard cap 72. Imperative mood. Body only when the *why* is non-obvious or there are breaking changes. No AI attribution, no "this commit does X", no emoji unless the project uses them. Body always required for breaking changes, security fixes, data migrations, and reverts — future debuggers need the context.
|
||||||
|
|
||||||
|
Outputs only the message. Does not stage, commit, or amend.
|
||||||
|
|
||||||
|
## How to invoke
|
||||||
|
|
||||||
|
```
|
||||||
|
/caveman-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
Also triggers on phrases like "write a commit", "commit message", "generate commit".
|
||||||
|
|
||||||
|
## Example output
|
||||||
|
|
||||||
|
Diff: new endpoint for user profile.
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(api): add GET /users/:id/profile
|
||||||
|
|
||||||
|
Mobile client needs profile data without the full user payload
|
||||||
|
to reduce LTE bandwidth on cold-launch screens.
|
||||||
|
|
||||||
|
Closes #128
|
||||||
|
```
|
||||||
|
|
||||||
|
Diff: breaking API rename.
|
||||||
|
|
||||||
|
```
|
||||||
|
feat(api)!: rename /v1/orders to /v1/checkout
|
||||||
|
|
||||||
|
BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
|
||||||
|
before 2026-06-01. Old route returns 410 after that date.
|
||||||
|
```
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
|
||||||
|
- [Caveman README](../../README.md) — repo overview
|
||||||
Executable
+65
@@ -0,0 +1,65 @@
|
|||||||
|
---
|
||||||
|
name: caveman-commit
|
||||||
|
description: >
|
||||||
|
Ultra-compressed commit message generator. Cuts noise from commit messages while preserving
|
||||||
|
intent and reasoning. Conventional Commits format. Subject ≤50 chars, body only when "why"
|
||||||
|
isn't obvious. Use when user says "write a commit", "commit message", "generate commit",
|
||||||
|
"/commit", or invokes /caveman-commit. Auto-triggers when staging changes.
|
||||||
|
---
|
||||||
|
|
||||||
|
Write commit messages terse and exact. Conventional Commits format. No fluff. Why over what.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
**Subject line:**
|
||||||
|
- `[<types>](<scope>): <imperative summary>` — `<scope>` optional
|
||||||
|
- Types: `feat`, `fix`, `refactor`, `perf`, `docs`, `test`, `chore`, `build`, `ci`, `style`, `revert`
|
||||||
|
- Imperative mood: "add", "fix", "remove" — not "added", "adds", "adding"
|
||||||
|
- ≤50 chars when possible, hard cap 72
|
||||||
|
- No trailing period
|
||||||
|
- Match project convention for capitalization after the colon
|
||||||
|
|
||||||
|
**Body (only if needed):**
|
||||||
|
- Skip entirely when subject is self-explanatory
|
||||||
|
- Add body only for: non-obvious *why*, breaking changes, migration notes, linked issues
|
||||||
|
- Wrap at 72 chars
|
||||||
|
- Bullets `-` not `*`
|
||||||
|
- Reference issues/PRs at end: `Closes #42`, `Refs #17`
|
||||||
|
|
||||||
|
**What NEVER goes in:**
|
||||||
|
- "This commit does X", "I", "we", "now", "currently" — the diff says what
|
||||||
|
- "As requested by..." — use Co-authored-by trailer
|
||||||
|
- "Generated with Claude Code" or any AI attribution
|
||||||
|
- Emoji (unless project convention requires)
|
||||||
|
- Restating the file name when scope already says it
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Diff: new endpoint for user profile with body explaining the why
|
||||||
|
- ❌ "feat: add a new endpoint to get user profile information from the database"
|
||||||
|
- ✅
|
||||||
|
```
|
||||||
|
[feat](api): add GET /users/:id/profile
|
||||||
|
|
||||||
|
Mobile client needs profile data without the full user payload
|
||||||
|
to reduce LTE bandwidth on cold-launch screens.
|
||||||
|
|
||||||
|
Closes #128
|
||||||
|
```
|
||||||
|
|
||||||
|
Diff: breaking API change
|
||||||
|
- ✅
|
||||||
|
```
|
||||||
|
[feat](api)!: rename /v1/orders to /v1/checkout
|
||||||
|
|
||||||
|
BREAKING CHANGE: clients on /v1/orders must migrate to /v1/checkout
|
||||||
|
before 2026-06-01. Old route returns 410 after that date.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Auto-Clarity
|
||||||
|
|
||||||
|
Always include body for: breaking changes, security fixes, data migrations, anything reverting a prior commit. Never compress these into subject-only — future debuggers need the context.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
Only generates the commit message. Does not run `git commit`, does not stage files, does not amend. Output the message as a code block ready to paste. "stop caveman-commit" or "normal mode": revert to verbose commit style.
|
||||||
Executable
+163
@@ -0,0 +1,163 @@
|
|||||||
|
<p align="center">
|
||||||
|
<img src="https://em-content.zobj.net/source/apple/391/rock_1faa8.png" width="80" />
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">caveman-compress</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<strong>shrink memory file. save token every session.</strong>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
A Claude Code skill that compresses your project memory files (`CLAUDE.md`, todos, preferences) into caveman format — so every session loads fewer tokens automatically.
|
||||||
|
|
||||||
|
Claude read `CLAUDE.md` on every session start. If file big, cost big. Caveman make file small. Cost go down forever.
|
||||||
|
|
||||||
|
## What It Do
|
||||||
|
|
||||||
|
```
|
||||||
|
/caveman-compress CLAUDE.md
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
CLAUDE.md ← compressed (Claude reads this — fewer tokens every session)
|
||||||
|
CLAUDE.original.md ← human-readable backup (you edit this)
|
||||||
|
```
|
||||||
|
|
||||||
|
Original never lost. You can read and edit `.original.md`. Run skill again to re-compress after edits.
|
||||||
|
|
||||||
|
## Benchmarks
|
||||||
|
|
||||||
|
Real results on real project files:
|
||||||
|
|
||||||
|
| File | Original | Compressed | Saved |
|
||||||
|
|------|----------:|----------:|------:|
|
||||||
|
| `claude-md-preferences.md` | 706 | 285 | **59.6%** |
|
||||||
|
| `project-notes.md` | 1145 | 535 | **53.3%** |
|
||||||
|
| `claude-md-project.md` | 1122 | 636 | **43.3%** |
|
||||||
|
| `todo-list.md` | 627 | 388 | **38.1%** |
|
||||||
|
| `mixed-with-code.md` | 888 | 560 | **36.9%** |
|
||||||
|
| **Average** | **898** | **481** | **46%** |
|
||||||
|
|
||||||
|
All validations passed ✅ — headings, code blocks, URLs, file paths preserved exactly.
|
||||||
|
|
||||||
|
## Before / After
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="50%">
|
||||||
|
|
||||||
|
### 📄 Original (706 tokens)
|
||||||
|
|
||||||
|
> "I strongly prefer TypeScript with strict mode enabled for all new code. Please don't use `any` type unless there's genuinely no way around it, and if you do, leave a comment explaining the reasoning. I find that taking the time to properly type things catches a lot of bugs before they ever make it to runtime."
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td width="50%">
|
||||||
|
|
||||||
|
### 🪨 Caveman (285 tokens)
|
||||||
|
|
||||||
|
> "Prefer TypeScript strict mode always. No `any` unless unavoidable — comment why if used. Proper types catch bugs early."
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
**Same instructions. 60% fewer tokens. Every. Single. Session.**
|
||||||
|
|
||||||
|
## Security
|
||||||
|
|
||||||
|
`caveman-compress` is flagged as Snyk High Risk due to subprocess and file I/O patterns detected by static analysis. This is a false positive — see [SECURITY.md](./SECURITY.md) for a full explanation of what the skill does and does not do.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
Compress is built in with the `caveman` plugin. Install `caveman` once, then use `/caveman-compress`.
|
||||||
|
|
||||||
|
If you need local files, the compress skill lives at:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
caveman-compress/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Requires:** Python 3.10+
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
/caveman-compress <filepath>
|
||||||
|
```
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
```
|
||||||
|
/caveman-compress CLAUDE.md
|
||||||
|
/caveman-compress docs/preferences.md
|
||||||
|
/caveman-compress todos.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### What files work
|
||||||
|
|
||||||
|
| Type | Compress? |
|
||||||
|
|------|-----------|
|
||||||
|
| `.md`, `.txt`, `.rst`, `.typ`, `.typst`, `.tex` | ✅ Yes |
|
||||||
|
| Extensionless natural language | ✅ Yes |
|
||||||
|
| `.py`, `.js`, `.ts`, `.json`, `.yaml` | ❌ Skip (code/config) |
|
||||||
|
| `*.original.md` | ❌ Skip (backup files) |
|
||||||
|
|
||||||
|
## How It Work
|
||||||
|
|
||||||
|
```
|
||||||
|
/caveman-compress CLAUDE.md
|
||||||
|
↓
|
||||||
|
detect file type (no tokens)
|
||||||
|
↓
|
||||||
|
Claude compresses (tokens — one call)
|
||||||
|
↓
|
||||||
|
validate output (no tokens)
|
||||||
|
checks: headings, code blocks, URLs, file paths, bullets
|
||||||
|
↓
|
||||||
|
if errors: Claude fixes cherry-picked issues only (tokens — targeted fix)
|
||||||
|
does NOT recompress — only patches broken parts
|
||||||
|
↓
|
||||||
|
retry up to 2 times
|
||||||
|
↓
|
||||||
|
write compressed → CLAUDE.md
|
||||||
|
write original → CLAUDE.original.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Only two things use tokens: initial compression + targeted fix if validation fails. Everything else is local Python.
|
||||||
|
|
||||||
|
## What Is Preserved
|
||||||
|
|
||||||
|
Caveman compress natural language. It never touch:
|
||||||
|
|
||||||
|
- Code blocks (` ``` ` fenced or indented)
|
||||||
|
- Inline code (`` `backtick content` ``)
|
||||||
|
- URLs and links
|
||||||
|
- File paths (`/src/components/...`)
|
||||||
|
- Commands (`npm install`, `git commit`)
|
||||||
|
- Technical terms, library names, API names
|
||||||
|
- Headings (exact text preserved)
|
||||||
|
- Tables (structure preserved, cell text compressed)
|
||||||
|
- Dates, version numbers, numeric values
|
||||||
|
|
||||||
|
## Why This Matter
|
||||||
|
|
||||||
|
`CLAUDE.md` loads on **every session start**. A 1000-token project memory file costs tokens every single time you open a project. Over 100 sessions that's 100,000 tokens of overhead — just for context you already wrote.
|
||||||
|
|
||||||
|
Caveman cut that by ~46% on average. Same instructions. Same accuracy. Less waste.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌────────────────────────────────────────────┐
|
||||||
|
│ TOKEN SAVINGS PER FILE █████ 46% │
|
||||||
|
│ SESSIONS THAT BENEFIT ██████████ 100% │
|
||||||
|
│ INFORMATION PRESERVED ██████████ 100% │
|
||||||
|
│ SETUP TIME █ 1x │
|
||||||
|
└────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
## Part of Caveman
|
||||||
|
|
||||||
|
This skill is part of the [caveman](https://github.com/JuliusBrussee/caveman) toolkit — making Claude use fewer tokens without losing accuracy.
|
||||||
|
|
||||||
|
- **caveman** — make Claude *speak* like caveman (cuts response tokens ~65%)
|
||||||
|
- **caveman-compress** — make Claude *read* less (cuts context tokens ~46%)
|
||||||
Executable
+31
@@ -0,0 +1,31 @@
|
|||||||
|
# Security
|
||||||
|
|
||||||
|
## Snyk High Risk Rating
|
||||||
|
|
||||||
|
`caveman-compress` receives a Snyk High Risk rating due to static analysis heuristics. This document explains what the skill does and does not do.
|
||||||
|
|
||||||
|
### What triggers the rating
|
||||||
|
|
||||||
|
1. **subprocess usage**: The skill calls the `claude` CLI via `subprocess.run()` as a fallback when `ANTHROPIC_API_KEY` is not set. The subprocess call uses a fixed argument list — no shell interpolation occurs. User file content is passed via stdin, not as a shell argument.
|
||||||
|
|
||||||
|
2. **File read/write**: The skill reads the file the user explicitly points it at, compresses it, and writes the result back to the same path. A `.original.md` backup is saved alongside it. No files outside the user-specified path are read or written.
|
||||||
|
|
||||||
|
### What the skill does NOT do
|
||||||
|
|
||||||
|
- Does not execute user file content as code
|
||||||
|
- Does not make network requests except to Anthropic's API (via SDK or CLI)
|
||||||
|
- Does not access files outside the path the user provides
|
||||||
|
- Does not use shell=True or string interpolation in subprocess calls
|
||||||
|
- Does not collect or transmit any data beyond the file being compressed
|
||||||
|
|
||||||
|
### Auth behavior
|
||||||
|
|
||||||
|
If `ANTHROPIC_API_KEY` is set, the skill uses the Anthropic Python SDK directly (no subprocess). If not set, it falls back to the `claude` CLI, which uses the user's existing Claude desktop authentication.
|
||||||
|
|
||||||
|
### File size limit
|
||||||
|
|
||||||
|
Files larger than 500KB are rejected before any API call is made.
|
||||||
|
|
||||||
|
### Reporting a vulnerability
|
||||||
|
|
||||||
|
If you believe you've found a genuine security issue, please open a GitHub issue with the label `security`.
|
||||||
Executable
+111
@@ -0,0 +1,111 @@
|
|||||||
|
---
|
||||||
|
name: caveman-compress
|
||||||
|
description: >
|
||||||
|
Compress natural language memory files (CLAUDE.md, todos, preferences) into caveman format
|
||||||
|
to save input tokens. Preserves all technical substance, code, URLs, and structure.
|
||||||
|
Compressed version overwrites the original file. Human-readable backup saved as FILE.original.md.
|
||||||
|
Trigger: /caveman-compress FILEPATH or "compress memory file"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Caveman Compress
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Compress natural language files (CLAUDE.md, todos, preferences) into caveman-speak to reduce input tokens. Compressed version overwrites original. Human-readable backup saved as `<filename>.original.md`.
|
||||||
|
|
||||||
|
## Trigger
|
||||||
|
|
||||||
|
`/caveman-compress <filepath>` or when user asks to compress a memory file.
|
||||||
|
|
||||||
|
## Process
|
||||||
|
|
||||||
|
1. The compression scripts live in `scripts/` (adjacent to this SKILL.md). If the path is not immediately available, search for `scripts/__main__.py` next to this SKILL.md.
|
||||||
|
|
||||||
|
2. From the directory containing this SKILL.md, run:
|
||||||
|
|
||||||
|
python3 -m scripts <absolute_filepath>
|
||||||
|
|
||||||
|
3. The CLI will:
|
||||||
|
- detect file type (no tokens)
|
||||||
|
- call Claude to compress
|
||||||
|
- validate output (no tokens)
|
||||||
|
- if errors: cherry-pick fix with Claude (targeted fixes only, no recompression)
|
||||||
|
- retry up to 2 times
|
||||||
|
- if still failing after 2 retries: report error to user, leave original file untouched
|
||||||
|
|
||||||
|
4. Return result to user
|
||||||
|
|
||||||
|
## Compression Rules
|
||||||
|
|
||||||
|
### Remove
|
||||||
|
- Articles: a, an, the
|
||||||
|
- Filler: just, really, basically, actually, simply, essentially, generally
|
||||||
|
- Pleasantries: "sure", "certainly", "of course", "happy to", "I'd recommend"
|
||||||
|
- Hedging: "it might be worth", "you could consider", "it would be good to"
|
||||||
|
- Redundant phrasing: "in order to" → "to", "make sure to" → "ensure", "the reason is because" → "because"
|
||||||
|
- Connective fluff: "however", "furthermore", "additionally", "in addition"
|
||||||
|
|
||||||
|
### Preserve EXACTLY (never modify)
|
||||||
|
- Code blocks (fenced ``` and indented)
|
||||||
|
- Inline code (`backtick content`)
|
||||||
|
- URLs and links (full URLs, markdown links)
|
||||||
|
- File paths (`/src/components/...`, `./config.yaml`)
|
||||||
|
- Commands (`npm install`, `git commit`, `docker build`)
|
||||||
|
- Technical terms (library names, API names, protocols, algorithms)
|
||||||
|
- Proper nouns (project names, people, companies)
|
||||||
|
- Dates, version numbers, numeric values
|
||||||
|
- Environment variables (`$HOME`, `NODE_ENV`)
|
||||||
|
|
||||||
|
### Preserve Structure
|
||||||
|
- All markdown headings (keep exact heading text, compress body below)
|
||||||
|
- Bullet point hierarchy (keep nesting level)
|
||||||
|
- Numbered lists (keep numbering)
|
||||||
|
- Tables (compress cell text, keep structure)
|
||||||
|
- Frontmatter/YAML headers in markdown files
|
||||||
|
|
||||||
|
### Compress
|
||||||
|
- Use short synonyms: "big" not "extensive", "fix" not "implement a solution for", "use" not "utilize"
|
||||||
|
- Fragments OK: "Run tests before commit" not "You should always run tests before committing"
|
||||||
|
- Drop "you should", "make sure to", "remember to" — just state the action
|
||||||
|
- Merge redundant bullets that say the same thing differently
|
||||||
|
- Keep one example where multiple examples show the same pattern
|
||||||
|
|
||||||
|
CRITICAL RULE:
|
||||||
|
Anything inside ``` ... ``` must be copied EXACTLY.
|
||||||
|
Do not:
|
||||||
|
- remove comments
|
||||||
|
- remove spacing
|
||||||
|
- reorder lines
|
||||||
|
- shorten commands
|
||||||
|
- simplify anything
|
||||||
|
|
||||||
|
Inline code (`...`) must be preserved EXACTLY.
|
||||||
|
Do not modify anything inside backticks.
|
||||||
|
|
||||||
|
If file contains code blocks:
|
||||||
|
- Treat code blocks as read-only regions
|
||||||
|
- Only compress text outside them
|
||||||
|
- Do not merge sections around code
|
||||||
|
|
||||||
|
## Pattern
|
||||||
|
|
||||||
|
Original:
|
||||||
|
> You should always make sure to run the test suite before pushing any changes to the main branch. This is important because it helps catch bugs early and prevents broken builds from being deployed to production.
|
||||||
|
|
||||||
|
Compressed:
|
||||||
|
> Run tests before push to main. Catch bugs early, prevent broken prod deploys.
|
||||||
|
|
||||||
|
Original:
|
||||||
|
> The application uses a microservices architecture with the following components. The API gateway handles all incoming requests and routes them to the appropriate service. The authentication service is responsible for managing user sessions and JWT tokens.
|
||||||
|
|
||||||
|
Compressed:
|
||||||
|
> Microservices architecture. API gateway route all requests to services. Auth service manage user sessions + JWT tokens.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
- ONLY compress natural language files (.md, .txt, .typ, .typst, .tex, extensionless)
|
||||||
|
- NEVER modify: .py, .js, .ts, .json, .yaml, .yml, .toml, .env, .lock, .css, .html, .xml, .sql, .sh
|
||||||
|
- If file has mixed content (prose + code), compress ONLY the prose sections
|
||||||
|
- If unsure whether something is code or prose, leave it unchanged
|
||||||
|
- Original file is backed up as FILE.original.md before overwriting
|
||||||
|
- Never compress FILE.original.md (skip it)
|
||||||
Executable
+9
@@ -0,0 +1,9 @@
|
|||||||
|
"""Caveman compress scripts.
|
||||||
|
|
||||||
|
This package provides tools to compress natural language markdown files
|
||||||
|
into caveman format to save input tokens.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = ["cli", "compress", "detect", "validate"]
|
||||||
|
|
||||||
|
__version__ = "1.0.0"
|
||||||
Executable
+3
@@ -0,0 +1,3 @@
|
|||||||
|
from .cli import main
|
||||||
|
|
||||||
|
main()
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+80
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Support both direct execution and module import
|
||||||
|
try:
|
||||||
|
from .validate import validate
|
||||||
|
except ImportError:
|
||||||
|
sys.path.insert(0, str(Path(__file__).parent))
|
||||||
|
from validate import validate
|
||||||
|
|
||||||
|
try:
|
||||||
|
import tiktoken
|
||||||
|
_enc = tiktoken.get_encoding("o200k_base")
|
||||||
|
except ImportError:
|
||||||
|
_enc = None
|
||||||
|
|
||||||
|
|
||||||
|
def count_tokens(text):
|
||||||
|
if _enc is None:
|
||||||
|
return len(text.split()) # fallback: word count
|
||||||
|
return len(_enc.encode(text))
|
||||||
|
|
||||||
|
|
||||||
|
def benchmark_pair(orig_path: Path, comp_path: Path):
|
||||||
|
orig_text = orig_path.read_text()
|
||||||
|
comp_text = comp_path.read_text()
|
||||||
|
|
||||||
|
orig_tokens = count_tokens(orig_text)
|
||||||
|
comp_tokens = count_tokens(comp_text)
|
||||||
|
saved = 100 * (orig_tokens - comp_tokens) / orig_tokens if orig_tokens > 0 else 0.0
|
||||||
|
result = validate(orig_path, comp_path)
|
||||||
|
|
||||||
|
return (comp_path.name, orig_tokens, comp_tokens, saved, result.is_valid)
|
||||||
|
|
||||||
|
|
||||||
|
def print_table(rows):
|
||||||
|
print("\n| File | Original | Compressed | Saved % | Valid |")
|
||||||
|
print("|------|----------|------------|---------|-------|")
|
||||||
|
for r in rows:
|
||||||
|
print(f"| {r[0]} | {r[1]} | {r[2]} | {r[3]:.1f}% | {'✅' if r[4] else '❌'} |")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Direct file pair: python3 benchmark.py original.md compressed.md
|
||||||
|
if len(sys.argv) == 3:
|
||||||
|
orig = Path(sys.argv[1]).resolve()
|
||||||
|
comp = Path(sys.argv[2]).resolve()
|
||||||
|
if not orig.exists():
|
||||||
|
print(f"❌ Not found: {orig}")
|
||||||
|
sys.exit(1)
|
||||||
|
if not comp.exists():
|
||||||
|
print(f"❌ Not found: {comp}")
|
||||||
|
sys.exit(1)
|
||||||
|
print_table([benchmark_pair(orig, comp)])
|
||||||
|
return
|
||||||
|
|
||||||
|
# Glob mode: repo_root/tests/caveman-compress/
|
||||||
|
# __file__ lives at <repo_root>/skills/caveman-compress/scripts/benchmark.py
|
||||||
|
# Walk up four dirs: scripts → caveman-compress → skills → repo_root.
|
||||||
|
tests_dir = Path(__file__).resolve().parents[3] / "tests" / "caveman-compress"
|
||||||
|
if not tests_dir.exists():
|
||||||
|
print(f"❌ Tests dir not found: {tests_dir}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
rows = []
|
||||||
|
for orig in sorted(tests_dir.glob("*.original.md")):
|
||||||
|
comp = orig.with_name(orig.stem.removesuffix(".original") + ".md")
|
||||||
|
if comp.exists():
|
||||||
|
rows.append(benchmark_pair(orig, comp))
|
||||||
|
|
||||||
|
if not rows:
|
||||||
|
print("No compressed file pairs found.")
|
||||||
|
return
|
||||||
|
|
||||||
|
print_table(rows)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+85
@@ -0,0 +1,85 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Caveman Compress CLI
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
caveman <filepath>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Force UTF-8 on stdout/stderr before any code can print. Windows consoles
|
||||||
|
# default to cp1252 and crash on the ❌ glyphs in error/validation branches,
|
||||||
|
# masking the real error and leaving the user with a half-compressed file.
|
||||||
|
for _stream in (sys.stdout, sys.stderr):
|
||||||
|
reconfigure = getattr(_stream, "reconfigure", None)
|
||||||
|
if callable(reconfigure):
|
||||||
|
try:
|
||||||
|
reconfigure(encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .compress import compress_file
|
||||||
|
from .detect import detect_file_type, should_compress
|
||||||
|
|
||||||
|
|
||||||
|
def print_usage():
|
||||||
|
print("Usage: caveman <filepath>")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) != 2:
|
||||||
|
print_usage()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
filepath = Path(sys.argv[1])
|
||||||
|
|
||||||
|
# Check file exists
|
||||||
|
if not filepath.exists():
|
||||||
|
print(f"❌ File not found: {filepath}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not filepath.is_file():
|
||||||
|
print(f"❌ Not a file: {filepath}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
filepath = filepath.resolve()
|
||||||
|
|
||||||
|
# Detect file type
|
||||||
|
file_type = detect_file_type(filepath)
|
||||||
|
|
||||||
|
print(f"Detected: {file_type}")
|
||||||
|
|
||||||
|
# Check if compressible
|
||||||
|
if not should_compress(filepath):
|
||||||
|
print("Skipping: file is not natural language (code/config)")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
print("Starting caveman compression...\n")
|
||||||
|
|
||||||
|
try:
|
||||||
|
success = compress_file(filepath)
|
||||||
|
|
||||||
|
if success:
|
||||||
|
print("\nCompression completed successfully")
|
||||||
|
backup_path = filepath.with_name(filepath.stem + ".original.md")
|
||||||
|
print(f"Compressed: {filepath}")
|
||||||
|
print(f"Original: {backup_path}")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("\n❌ Compression failed after retries")
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\nInterrupted by user")
|
||||||
|
sys.exit(130)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"\n❌ Error: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Executable
+254
@@ -0,0 +1,254 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Caveman Memory Compression Orchestrator
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python scripts/compress.py <filepath>
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
OUTER_FENCE_REGEX = re.compile(
|
||||||
|
r"\A\s*(`{3,}|~{3,})[^\n]*\n(.*)\n\1\s*\Z", re.DOTALL
|
||||||
|
)
|
||||||
|
|
||||||
|
# Filenames and paths that almost certainly hold secrets or PII. Compressing
|
||||||
|
# them ships raw bytes to the Anthropic API — a third-party data boundary that
|
||||||
|
# developers on sensitive codebases cannot cross. detect.py already skips .env
|
||||||
|
# by extension, but credentials.md / secrets.txt / ~/.aws/credentials would
|
||||||
|
# slip through the natural-language filter. This is a hard refuse before read.
|
||||||
|
SENSITIVE_BASENAME_REGEX = re.compile(
|
||||||
|
r"(?ix)^("
|
||||||
|
r"\.env(\..+)?"
|
||||||
|
r"|\.netrc"
|
||||||
|
r"|credentials(\..+)?"
|
||||||
|
r"|secrets?(\..+)?"
|
||||||
|
r"|passwords?(\..+)?"
|
||||||
|
r"|id_(rsa|dsa|ecdsa|ed25519)(\.pub)?"
|
||||||
|
r"|authorized_keys"
|
||||||
|
r"|known_hosts"
|
||||||
|
r"|.*\.(pem|key|p12|pfx|crt|cer|jks|keystore|asc|gpg)"
|
||||||
|
r")$"
|
||||||
|
)
|
||||||
|
|
||||||
|
SENSITIVE_PATH_COMPONENTS = frozenset({".ssh", ".aws", ".gnupg", ".kube", ".docker"})
|
||||||
|
|
||||||
|
SENSITIVE_NAME_TOKENS = (
|
||||||
|
"secret", "credential", "password", "passwd",
|
||||||
|
"apikey", "accesskey", "token", "privatekey",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_sensitive_path(filepath: Path) -> bool:
|
||||||
|
"""Heuristic denylist for files that must never be shipped to a third-party API."""
|
||||||
|
name = filepath.name
|
||||||
|
if SENSITIVE_BASENAME_REGEX.match(name):
|
||||||
|
return True
|
||||||
|
lowered_parts = {p.lower() for p in filepath.parts}
|
||||||
|
if lowered_parts & SENSITIVE_PATH_COMPONENTS:
|
||||||
|
return True
|
||||||
|
# Normalize separators so "api-key" and "api_key" both match "apikey".
|
||||||
|
lower = re.sub(r"[_\-\s.]", "", name.lower())
|
||||||
|
return any(tok in lower for tok in SENSITIVE_NAME_TOKENS)
|
||||||
|
|
||||||
|
|
||||||
|
def strip_llm_wrapper(text: str) -> str:
|
||||||
|
"""Strip outer ```markdown ... ``` fence when it wraps the entire output."""
|
||||||
|
m = OUTER_FENCE_REGEX.match(text)
|
||||||
|
if m:
|
||||||
|
return m.group(2)
|
||||||
|
return text
|
||||||
|
|
||||||
|
from .detect import should_compress
|
||||||
|
from .validate import validate
|
||||||
|
|
||||||
|
MAX_RETRIES = 2
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Claude Calls ----------
|
||||||
|
|
||||||
|
|
||||||
|
def call_claude(prompt: str) -> str:
|
||||||
|
api_key = os.environ.get("ANTHROPIC_API_KEY")
|
||||||
|
if api_key:
|
||||||
|
try:
|
||||||
|
import anthropic
|
||||||
|
|
||||||
|
client = anthropic.Anthropic(api_key=api_key)
|
||||||
|
msg = client.messages.create(
|
||||||
|
model=os.environ.get("CAVEMAN_MODEL", "claude-sonnet-4-5"),
|
||||||
|
max_tokens=8192,
|
||||||
|
messages=[{"role": "user", "content": prompt}],
|
||||||
|
)
|
||||||
|
return strip_llm_wrapper(msg.content[0].text.strip())
|
||||||
|
except ImportError:
|
||||||
|
pass # anthropic not installed, fall back to CLI
|
||||||
|
# Fallback: use claude CLI (handles desktop auth)
|
||||||
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
["claude", "--print"],
|
||||||
|
input=prompt,
|
||||||
|
text=True,
|
||||||
|
capture_output=True,
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
return strip_llm_wrapper(result.stdout.strip())
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
raise RuntimeError(f"Claude call failed:\n{e.stderr}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_compress_prompt(original: str) -> str:
|
||||||
|
return f"""
|
||||||
|
Compress this markdown into caveman format.
|
||||||
|
|
||||||
|
STRICT RULES:
|
||||||
|
- Do NOT modify anything inside ``` code blocks
|
||||||
|
- Do NOT modify anything inside inline backticks
|
||||||
|
- Preserve ALL URLs exactly
|
||||||
|
- Preserve ALL headings exactly
|
||||||
|
- Preserve file paths and commands
|
||||||
|
- Return ONLY the compressed markdown body — do NOT wrap the entire output in a ```markdown fence or any other fence. Inner code blocks from the original stay as-is; do not add a new outer fence around the whole file.
|
||||||
|
|
||||||
|
Only compress natural language.
|
||||||
|
|
||||||
|
TEXT:
|
||||||
|
{original}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def build_fix_prompt(original: str, compressed: str, errors: List[str]) -> str:
|
||||||
|
errors_str = "\n".join(f"- {e}" for e in errors)
|
||||||
|
return f"""You are fixing a caveman-compressed markdown file. Specific validation errors were found.
|
||||||
|
|
||||||
|
CRITICAL RULES:
|
||||||
|
- DO NOT recompress or rephrase the file
|
||||||
|
- ONLY fix the listed errors — leave everything else exactly as-is
|
||||||
|
- The ORIGINAL is provided as reference only (to restore missing content)
|
||||||
|
- Preserve caveman style in all untouched sections
|
||||||
|
|
||||||
|
ERRORS TO FIX:
|
||||||
|
{errors_str}
|
||||||
|
|
||||||
|
HOW TO FIX:
|
||||||
|
- Missing URL: find it in ORIGINAL, restore it exactly where it belongs in COMPRESSED
|
||||||
|
- Code block mismatch: find the exact code block in ORIGINAL, restore it in COMPRESSED
|
||||||
|
- Heading mismatch: restore the exact heading text from ORIGINAL into COMPRESSED
|
||||||
|
- Do not touch any section not mentioned in the errors
|
||||||
|
|
||||||
|
ORIGINAL (reference only):
|
||||||
|
{original}
|
||||||
|
|
||||||
|
COMPRESSED (fix this):
|
||||||
|
{compressed}
|
||||||
|
|
||||||
|
Return ONLY the fixed compressed file. No explanation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Core Logic ----------
|
||||||
|
|
||||||
|
|
||||||
|
def compress_file(filepath: Path) -> bool:
|
||||||
|
# Resolve and validate path
|
||||||
|
filepath = filepath.resolve()
|
||||||
|
MAX_FILE_SIZE = 500_000 # 500KB
|
||||||
|
if not filepath.exists():
|
||||||
|
raise FileNotFoundError(f"File not found: {filepath}")
|
||||||
|
if filepath.stat().st_size > MAX_FILE_SIZE:
|
||||||
|
raise ValueError(f"File too large to compress safely (max 500KB): {filepath}")
|
||||||
|
|
||||||
|
# Refuse files that look like they contain secrets or PII. Compressing ships
|
||||||
|
# the raw bytes to the Anthropic API — a third-party boundary — so we fail
|
||||||
|
# loudly rather than silently exfiltrate credentials or keys. Override is
|
||||||
|
# intentional: the user must rename the file if the heuristic is wrong.
|
||||||
|
if is_sensitive_path(filepath):
|
||||||
|
raise ValueError(
|
||||||
|
f"Refusing to compress {filepath}: filename looks sensitive "
|
||||||
|
"(credentials, keys, secrets, or known private paths). "
|
||||||
|
"Compression sends file contents to the Anthropic API. "
|
||||||
|
"Rename the file if this is a false positive."
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"Processing: {filepath}")
|
||||||
|
|
||||||
|
if not should_compress(filepath):
|
||||||
|
print("Skipping (not natural language)")
|
||||||
|
return False
|
||||||
|
|
||||||
|
original_text = filepath.read_text(errors="ignore")
|
||||||
|
backup_path = filepath.with_name(filepath.stem + ".original.md")
|
||||||
|
|
||||||
|
if not original_text.strip():
|
||||||
|
print("❌ Refusing to compress: file is empty or whitespace-only.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check if backup already exists to prevent accidental overwriting
|
||||||
|
if backup_path.exists():
|
||||||
|
print(f"⚠️ Backup file already exists: {backup_path}")
|
||||||
|
print("The original backup may contain important content.")
|
||||||
|
print("Aborting to prevent data loss. Please remove or rename the backup file if you want to proceed.")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Step 1: Compress
|
||||||
|
print("Compressing with Claude...")
|
||||||
|
compressed = call_claude(build_compress_prompt(original_text))
|
||||||
|
|
||||||
|
if compressed is None or not compressed.strip():
|
||||||
|
print("❌ Compression aborted: Claude returned an empty response.")
|
||||||
|
print(" Original file is untouched (no backup created).")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if compressed.strip() == original_text.strip():
|
||||||
|
print("❌ Compression aborted: output is identical to input.")
|
||||||
|
print(" Likely causes: Claude refused, returned the prompt verbatim, or the file is")
|
||||||
|
print(" already in caveman form. Original file is untouched (no backup created).")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Save original as backup, then verify the backup readback before
|
||||||
|
# touching the input file. If the filesystem dropped bytes (encoding,
|
||||||
|
# antivirus, disk full), unlink the bad backup and abort instead of
|
||||||
|
# leaving the user with a corrupt backup + compressed primary.
|
||||||
|
backup_path.write_text(original_text)
|
||||||
|
backup_readback = backup_path.read_text(errors="ignore")
|
||||||
|
if backup_readback != original_text:
|
||||||
|
print(f"❌ Backup write verification failed: {backup_path}")
|
||||||
|
print(" In-memory original differs from on-disk backup. Aborting before touching the input file.")
|
||||||
|
try:
|
||||||
|
backup_path.unlink()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
filepath.write_text(compressed)
|
||||||
|
|
||||||
|
# Step 2: Validate + Retry
|
||||||
|
for attempt in range(MAX_RETRIES):
|
||||||
|
print(f"\nValidation attempt {attempt + 1}")
|
||||||
|
|
||||||
|
result = validate(backup_path, filepath)
|
||||||
|
|
||||||
|
if result.is_valid:
|
||||||
|
print("Validation passed")
|
||||||
|
break
|
||||||
|
|
||||||
|
print("❌ Validation failed:")
|
||||||
|
for err in result.errors:
|
||||||
|
print(f" - {err}")
|
||||||
|
|
||||||
|
if attempt == MAX_RETRIES - 1:
|
||||||
|
# Restore original on failure
|
||||||
|
filepath.write_text(original_text)
|
||||||
|
backup_path.unlink(missing_ok=True)
|
||||||
|
print("❌ Failed after retries — original restored")
|
||||||
|
return False
|
||||||
|
|
||||||
|
print("Fixing with Claude...")
|
||||||
|
compressed = call_claude(
|
||||||
|
build_fix_prompt(original_text, compressed, result.errors)
|
||||||
|
)
|
||||||
|
filepath.write_text(compressed)
|
||||||
|
|
||||||
|
return True
|
||||||
Executable
+121
@@ -0,0 +1,121 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Detect whether a file is natural language (compressible) or code/config (skip)."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Extensions that are natural language and compressible
|
||||||
|
COMPRESSIBLE_EXTENSIONS = {".md", ".txt", ".markdown", ".rst", ".typ", ".typst", ".tex"}
|
||||||
|
|
||||||
|
# Extensions that are code/config and should be skipped
|
||||||
|
SKIP_EXTENSIONS = {
|
||||||
|
".py", ".js", ".ts", ".tsx", ".jsx", ".json", ".yaml", ".yml",
|
||||||
|
".toml", ".env", ".lock", ".css", ".scss", ".html", ".xml",
|
||||||
|
".sql", ".sh", ".bash", ".zsh", ".go", ".rs", ".java", ".c",
|
||||||
|
".cpp", ".h", ".hpp", ".rb", ".php", ".swift", ".kt", ".lua",
|
||||||
|
".dockerfile", ".makefile", ".csv", ".ini", ".cfg",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Patterns that indicate a line is code
|
||||||
|
CODE_PATTERNS = [
|
||||||
|
re.compile(r"^\s*(import |from .+ import |require\(|const |let |var )"),
|
||||||
|
re.compile(r"^\s*(def |class |function |async function |export )"),
|
||||||
|
re.compile(r"^\s*(if\s*\(|for\s*\(|while\s*\(|switch\s*\(|try\s*\{)"),
|
||||||
|
re.compile(r"^\s*[\}\]\);]+\s*$"), # closing braces/brackets
|
||||||
|
re.compile(r"^\s*@\w+"), # decorators/annotations
|
||||||
|
re.compile(r'^\s*"[^"]+"\s*:\s*'), # JSON-like key-value
|
||||||
|
re.compile(r"^\s*\w+\s*=\s*[{\[\(\"']"), # assignment with literal
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _is_code_line(line: str) -> bool:
|
||||||
|
"""Check if a line looks like code."""
|
||||||
|
return any(p.match(line) for p in CODE_PATTERNS)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_json_content(text: str) -> bool:
|
||||||
|
"""Check if content is valid JSON."""
|
||||||
|
try:
|
||||||
|
json.loads(text)
|
||||||
|
return True
|
||||||
|
except (json.JSONDecodeError, ValueError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _is_yaml_content(lines: list[str]) -> bool:
|
||||||
|
"""Heuristic: check if content looks like YAML."""
|
||||||
|
yaml_indicators = 0
|
||||||
|
for line in lines[:30]:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("---"):
|
||||||
|
yaml_indicators += 1
|
||||||
|
elif re.match(r"^\w[\w\s]*:\s", stripped):
|
||||||
|
yaml_indicators += 1
|
||||||
|
elif stripped.startswith("- ") and ":" in stripped:
|
||||||
|
yaml_indicators += 1
|
||||||
|
# If most non-empty lines look like YAML
|
||||||
|
non_empty = sum(1 for l in lines[:30] if l.strip())
|
||||||
|
return non_empty > 0 and yaml_indicators / non_empty > 0.6
|
||||||
|
|
||||||
|
|
||||||
|
def detect_file_type(filepath: Path) -> str:
|
||||||
|
"""Classify a file as 'natural_language', 'code', 'config', or 'unknown'.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
One of: 'natural_language', 'code', 'config', 'unknown'
|
||||||
|
"""
|
||||||
|
ext = filepath.suffix.lower()
|
||||||
|
|
||||||
|
# Extension-based classification
|
||||||
|
if ext in COMPRESSIBLE_EXTENSIONS:
|
||||||
|
return "natural_language"
|
||||||
|
if ext in SKIP_EXTENSIONS:
|
||||||
|
return "code" if ext not in {".json", ".yaml", ".yml", ".toml", ".ini", ".cfg", ".env"} else "config"
|
||||||
|
|
||||||
|
# Extensionless files (like CLAUDE.md, TODO) — check content
|
||||||
|
if not ext:
|
||||||
|
try:
|
||||||
|
text = filepath.read_text(errors="ignore")
|
||||||
|
except (OSError, PermissionError):
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
lines = text.splitlines()[:50]
|
||||||
|
|
||||||
|
if _is_json_content(text[:10000]):
|
||||||
|
return "config"
|
||||||
|
if _is_yaml_content(lines):
|
||||||
|
return "config"
|
||||||
|
|
||||||
|
code_lines = sum(1 for l in lines if l.strip() and _is_code_line(l))
|
||||||
|
non_empty = sum(1 for l in lines if l.strip())
|
||||||
|
if non_empty > 0 and code_lines / non_empty > 0.4:
|
||||||
|
return "code"
|
||||||
|
|
||||||
|
return "natural_language"
|
||||||
|
|
||||||
|
return "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
def should_compress(filepath: Path) -> bool:
|
||||||
|
"""Return True if the file is natural language and should be compressed."""
|
||||||
|
if not filepath.is_file():
|
||||||
|
return False
|
||||||
|
# Skip backup files
|
||||||
|
if filepath.name.endswith(".original.md"):
|
||||||
|
return False
|
||||||
|
return detect_file_type(filepath) == "natural_language"
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python detect.py <file1> [file2] ...")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
for path_str in sys.argv[1:]:
|
||||||
|
p = Path(path_str).resolve()
|
||||||
|
file_type = detect_file_type(p)
|
||||||
|
compress = should_compress(p)
|
||||||
|
print(f" {p.name:30s} type={file_type:20s} compress={compress}")
|
||||||
Executable
+213
@@ -0,0 +1,213 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import re
|
||||||
|
from collections import Counter
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
URL_REGEX = re.compile(r"https?://[^\s)]+")
|
||||||
|
FENCE_OPEN_REGEX = re.compile(r"^(\s{0,3})(`{3,}|~{3,})(.*)$")
|
||||||
|
HEADING_REGEX = re.compile(r"^(#{1,6})\s+(.*)", re.MULTILINE)
|
||||||
|
BULLET_REGEX = re.compile(r"^\s*[-*+]\s+", re.MULTILINE)
|
||||||
|
|
||||||
|
# crude but effective path detection
|
||||||
|
# Requires either a path prefix (./ ../ / or drive letter) or a slash/backslash within the match
|
||||||
|
PATH_REGEX = re.compile(r"(?:\./|\.\./|/|[A-Za-z]:\\)[\w\-/\\\.]+|[\w\-\.]+[/\\][\w\-/\\\.]+")
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationResult:
|
||||||
|
def __init__(self):
|
||||||
|
self.is_valid = True
|
||||||
|
self.errors = []
|
||||||
|
self.warnings = []
|
||||||
|
|
||||||
|
def add_error(self, msg):
|
||||||
|
self.is_valid = False
|
||||||
|
self.errors.append(msg)
|
||||||
|
|
||||||
|
def add_warning(self, msg):
|
||||||
|
self.warnings.append(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(path: Path) -> str:
|
||||||
|
return path.read_text(errors="ignore")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Extractors ----------
|
||||||
|
|
||||||
|
|
||||||
|
def extract_headings(text):
|
||||||
|
return [(level, title.strip()) for level, title in HEADING_REGEX.findall(text)]
|
||||||
|
|
||||||
|
|
||||||
|
def extract_code_blocks(text):
|
||||||
|
"""Line-based fenced code block extractor.
|
||||||
|
|
||||||
|
Handles ``` and ~~~ fences with variable length (CommonMark: closing
|
||||||
|
fence must use same char and be at least as long as opening). Supports
|
||||||
|
nested fences (e.g. an outer 4-backtick block wrapping inner 3-backtick
|
||||||
|
content).
|
||||||
|
"""
|
||||||
|
blocks = []
|
||||||
|
lines = text.split("\n")
|
||||||
|
i = 0
|
||||||
|
n = len(lines)
|
||||||
|
while i < n:
|
||||||
|
m = FENCE_OPEN_REGEX.match(lines[i])
|
||||||
|
if not m:
|
||||||
|
i += 1
|
||||||
|
continue
|
||||||
|
fence_char = m.group(2)[0]
|
||||||
|
fence_len = len(m.group(2))
|
||||||
|
open_line = lines[i]
|
||||||
|
block_lines = [open_line]
|
||||||
|
i += 1
|
||||||
|
closed = False
|
||||||
|
while i < n:
|
||||||
|
close_m = FENCE_OPEN_REGEX.match(lines[i])
|
||||||
|
if (
|
||||||
|
close_m
|
||||||
|
and close_m.group(2)[0] == fence_char
|
||||||
|
and len(close_m.group(2)) >= fence_len
|
||||||
|
and close_m.group(3).strip() == ""
|
||||||
|
):
|
||||||
|
block_lines.append(lines[i])
|
||||||
|
closed = True
|
||||||
|
i += 1
|
||||||
|
break
|
||||||
|
block_lines.append(lines[i])
|
||||||
|
i += 1
|
||||||
|
if closed:
|
||||||
|
blocks.append("\n".join(block_lines))
|
||||||
|
# Unclosed fences are silently skipped — they indicate malformed markdown
|
||||||
|
# and including them would cause false-positive validation failures.
|
||||||
|
return blocks
|
||||||
|
|
||||||
|
|
||||||
|
def extract_urls(text):
|
||||||
|
return set(URL_REGEX.findall(text))
|
||||||
|
|
||||||
|
|
||||||
|
def extract_paths(text):
|
||||||
|
return set(PATH_REGEX.findall(text))
|
||||||
|
|
||||||
|
|
||||||
|
def count_bullets(text):
|
||||||
|
return len(BULLET_REGEX.findall(text))
|
||||||
|
|
||||||
|
|
||||||
|
def extract_inline_codes(text):
|
||||||
|
text_without_fences = re.sub(r"^```[\s\S]*?^```", "", text, flags=re.MULTILINE)
|
||||||
|
text_without_fences = re.sub(r"^~~~[\s\S]*?^~~~", "", text_without_fences, flags=re.MULTILINE)
|
||||||
|
return re.findall(r"`([^`]+)`", text_without_fences)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Validators ----------
|
||||||
|
|
||||||
|
|
||||||
|
def validate_headings(orig, comp, result):
|
||||||
|
h1 = extract_headings(orig)
|
||||||
|
h2 = extract_headings(comp)
|
||||||
|
|
||||||
|
if len(h1) != len(h2):
|
||||||
|
result.add_error(f"Heading count mismatch: {len(h1)} vs {len(h2)}")
|
||||||
|
|
||||||
|
if h1 != h2:
|
||||||
|
result.add_warning("Heading text/order changed")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_code_blocks(orig, comp, result):
|
||||||
|
c1 = extract_code_blocks(orig)
|
||||||
|
c2 = extract_code_blocks(comp)
|
||||||
|
|
||||||
|
if c1 != c2:
|
||||||
|
result.add_error("Code blocks not preserved exactly")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_urls(orig, comp, result):
|
||||||
|
u1 = extract_urls(orig)
|
||||||
|
u2 = extract_urls(comp)
|
||||||
|
|
||||||
|
if u1 != u2:
|
||||||
|
result.add_error(f"URL mismatch: lost={u1 - u2}, added={u2 - u1}")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_paths(orig, comp, result):
|
||||||
|
p1 = extract_paths(orig)
|
||||||
|
p2 = extract_paths(comp)
|
||||||
|
|
||||||
|
if p1 != p2:
|
||||||
|
result.add_warning(f"Path mismatch: lost={p1 - p2}, added={p2 - p1}")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_bullets(orig, comp, result):
|
||||||
|
b1 = count_bullets(orig)
|
||||||
|
b2 = count_bullets(comp)
|
||||||
|
|
||||||
|
if b1 == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
diff = abs(b1 - b2) / b1
|
||||||
|
|
||||||
|
if diff > 0.15:
|
||||||
|
result.add_warning(f"Bullet count changed too much: {b1} -> {b2}")
|
||||||
|
|
||||||
|
|
||||||
|
def validate_inline_codes(orig, comp, result):
|
||||||
|
c1 = Counter(extract_inline_codes(orig))
|
||||||
|
c2 = Counter(extract_inline_codes(comp))
|
||||||
|
|
||||||
|
if c1 != c2:
|
||||||
|
lost = set(c1.keys()) - set(c2.keys())
|
||||||
|
added = set(c2.keys()) - set(c1.keys())
|
||||||
|
for code, count in c1.items():
|
||||||
|
if code in c2 and c2[code] < count:
|
||||||
|
lost.add(f"{code} (lost {count - c2[code]} of {count} occurrences)")
|
||||||
|
if lost:
|
||||||
|
result.add_error(f"Inline code lost: {lost}")
|
||||||
|
if added:
|
||||||
|
result.add_warning(f"Inline code added: {added}")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- Main ----------
|
||||||
|
|
||||||
|
|
||||||
|
def validate(original_path: Path, compressed_path: Path) -> ValidationResult:
|
||||||
|
result = ValidationResult()
|
||||||
|
|
||||||
|
orig = read_file(original_path)
|
||||||
|
comp = read_file(compressed_path)
|
||||||
|
|
||||||
|
validate_headings(orig, comp, result)
|
||||||
|
validate_code_blocks(orig, comp, result)
|
||||||
|
validate_urls(orig, comp, result)
|
||||||
|
validate_paths(orig, comp, result)
|
||||||
|
validate_bullets(orig, comp, result)
|
||||||
|
validate_inline_codes(orig, comp, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- CLI ----------
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
import sys
|
||||||
|
|
||||||
|
if len(sys.argv) != 3:
|
||||||
|
print("Usage: python validate.py <original> <compressed>")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
orig = Path(sys.argv[1]).resolve()
|
||||||
|
comp = Path(sys.argv[2]).resolve()
|
||||||
|
|
||||||
|
res = validate(orig, comp)
|
||||||
|
|
||||||
|
print(f"\nValid: {res.is_valid}")
|
||||||
|
|
||||||
|
if res.errors:
|
||||||
|
print("\nErrors:")
|
||||||
|
for e in res.errors:
|
||||||
|
print(f" - {e}")
|
||||||
|
|
||||||
|
if res.warnings:
|
||||||
|
print("\nWarnings:")
|
||||||
|
for w in res.warnings:
|
||||||
|
print(f" - {w}")
|
||||||
Executable
+38
@@ -0,0 +1,38 @@
|
|||||||
|
# caveman-help
|
||||||
|
|
||||||
|
Quick-reference card. One shot, no mode change.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Prints a cheat sheet of all caveman modes, sibling skills, deactivation triggers, and how to set the default mode via env var or config file. One-shot display — does not flip the active mode, write flag files, or persist anything. Use when you forget the slash commands.
|
||||||
|
|
||||||
|
## How to invoke
|
||||||
|
|
||||||
|
```
|
||||||
|
/caveman-help
|
||||||
|
```
|
||||||
|
|
||||||
|
Also triggers on "caveman help", "what caveman commands", "how do I use caveman".
|
||||||
|
|
||||||
|
## Example output
|
||||||
|
|
||||||
|
```
|
||||||
|
Modes:
|
||||||
|
/caveman full (default)
|
||||||
|
/caveman lite lighter
|
||||||
|
/caveman ultra extreme
|
||||||
|
/caveman wenyan classical Chinese
|
||||||
|
|
||||||
|
Skills:
|
||||||
|
/caveman-commit terse Conventional Commits
|
||||||
|
/caveman-review one-line PR comments
|
||||||
|
/caveman-stats session token savings
|
||||||
|
|
||||||
|
Deactivate:
|
||||||
|
"stop caveman" or "normal mode"
|
||||||
|
```
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`SKILL.md`](./SKILL.md) — full reference card
|
||||||
|
- [Caveman README](../../README.md) — repo overview
|
||||||
Executable
+59
@@ -0,0 +1,59 @@
|
|||||||
|
---
|
||||||
|
name: caveman-help
|
||||||
|
description: >
|
||||||
|
Quick-reference card for all caveman modes, skills, and commands.
|
||||||
|
One-shot display, not a persistent mode. Trigger: /caveman-help,
|
||||||
|
"caveman help", "what caveman commands", "how do I use caveman".
|
||||||
|
---
|
||||||
|
|
||||||
|
# Caveman Help
|
||||||
|
|
||||||
|
Display this reference card when invoked. One-shot — do NOT change mode, write flag files, or persist anything. Output in caveman style.
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
| Mode | Trigger | What change |
|
||||||
|
|------|---------|-------------|
|
||||||
|
| **Lite** | `/caveman lite` | Drop filler. Keep sentence structure. |
|
||||||
|
| **Full** | `/caveman` | Drop articles, filler, pleasantries, hedging. Fragments OK. Default. |
|
||||||
|
| **Ultra** | `/caveman ultra` | Extreme compression. Bare fragments. Tables over prose. |
|
||||||
|
| **Wenyan-Lite** | `/caveman wenyan-lite` | Classical Chinese style, light compression. |
|
||||||
|
| **Wenyan-Full** | `/caveman wenyan` | Full 文言文. Maximum classical terseness. |
|
||||||
|
| **Wenyan-Ultra** | `/caveman wenyan-ultra` | Extreme. Ancient scholar on a budget. |
|
||||||
|
|
||||||
|
Mode stick until changed or session end.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
| Skill | Trigger | What it do |
|
||||||
|
|-------|---------|-----------|
|
||||||
|
| **caveman-commit** | `/caveman-commit` | Terse commit messages. Conventional Commits. ≤50 char subject. |
|
||||||
|
| **caveman-review** | `/caveman-review` | One-line PR comments: `L42: bug: user null. Add guard.` |
|
||||||
|
| **caveman-compress** | `/caveman-compress <file>` | Compress .md files to caveman prose. Saves ~46% input tokens. |
|
||||||
|
| **caveman-help** | `/caveman-help` | This card. |
|
||||||
|
|
||||||
|
## Deactivate
|
||||||
|
|
||||||
|
Say "stop caveman" or "normal mode". Resume anytime with `/caveman`.
|
||||||
|
|
||||||
|
## Configure Default Mode
|
||||||
|
|
||||||
|
Default mode = `full`. Change it:
|
||||||
|
|
||||||
|
**Environment variable** (highest priority):
|
||||||
|
```bash
|
||||||
|
export CAVEMAN_DEFAULT_MODE=ultra
|
||||||
|
```
|
||||||
|
|
||||||
|
**Config file** (`~/.config/caveman/config.json`):
|
||||||
|
```json
|
||||||
|
{ "defaultMode": "lite" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `"off"` to disable auto-activation on session start. User can still activate manually with `/caveman`.
|
||||||
|
|
||||||
|
Resolution: env var > config file > `full`.
|
||||||
|
|
||||||
|
## More
|
||||||
|
|
||||||
|
Full docs: https://github.com/JuliusBrussee/caveman
|
||||||
Executable
+33
@@ -0,0 +1,33 @@
|
|||||||
|
# caveman-review
|
||||||
|
|
||||||
|
One-line PR comments. Location, problem, fix. No throat-clearing.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Generates code review comments in `L<line>: <severity> <problem>. <fix>.` format. One line per finding. Severity emoji: 🔴 bug, 🟡 risk, 🔵 nit, ❓ question. Drops "I noticed that...", hedging, and restating what the diff already shows. Keeps exact line numbers, backticked symbols, and concrete fixes.
|
||||||
|
|
||||||
|
Auto-clarity: drops terse mode for CVE-class security findings, architectural disagreements, and onboarding contexts where the author needs the *why*. Resumes terse for the rest.
|
||||||
|
|
||||||
|
Output only — does not approve, request changes, or run linters.
|
||||||
|
|
||||||
|
## How to invoke
|
||||||
|
|
||||||
|
```
|
||||||
|
/caveman-review
|
||||||
|
```
|
||||||
|
|
||||||
|
Also triggers on "review this PR", "code review", "review the diff".
|
||||||
|
|
||||||
|
## Example output
|
||||||
|
|
||||||
|
```
|
||||||
|
L42: 🔴 bug: user can be null after .find(). Add guard before .email.
|
||||||
|
L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.
|
||||||
|
L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).
|
||||||
|
L107: ❓ q: why drop the cache here? Reads on next request will miss.
|
||||||
|
```
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
|
||||||
|
- [Caveman README](../../README.md) — repo overview
|
||||||
Executable
+55
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
name: caveman-review
|
||||||
|
description: >
|
||||||
|
Ultra-compressed code review comments. Cuts noise from PR feedback while preserving
|
||||||
|
the actionable signal. Each comment is one line: location, problem, fix. Use when user
|
||||||
|
says "review this PR", "code review", "review the diff", "/review", or invokes
|
||||||
|
/caveman-review. Auto-triggers when reviewing pull requests.
|
||||||
|
---
|
||||||
|
|
||||||
|
Write code review comments terse and actionable. One line per finding. Location, problem, fix. No throat-clearing.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
**Format:** `L<line>: <problem>. <fix>.` — or `<file>:L<line>: ...` when reviewing multi-file diffs.
|
||||||
|
|
||||||
|
**Severity prefix (optional, when mixed):**
|
||||||
|
- `🔴 bug:` — broken behavior, will cause incident
|
||||||
|
- `🟡 risk:` — works but fragile (race, missing null check, swallowed error)
|
||||||
|
- `🔵 nit:` — style, naming, micro-optim. Author can ignore
|
||||||
|
- `❓ q:` — genuine question, not a suggestion
|
||||||
|
|
||||||
|
**Drop:**
|
||||||
|
- "I noticed that...", "It seems like...", "You might want to consider..."
|
||||||
|
- "This is just a suggestion but..." — use `nit:` instead
|
||||||
|
- "Great work!", "Looks good overall but..." — say it once at the top, not per comment
|
||||||
|
- Restating what the line does — the reviewer can read the diff
|
||||||
|
- Hedging ("perhaps", "maybe", "I think") — if unsure use `q:`
|
||||||
|
|
||||||
|
**Keep:**
|
||||||
|
- Exact line numbers
|
||||||
|
- Exact symbol/function/variable names in backticks
|
||||||
|
- Concrete fix, not "consider refactoring this"
|
||||||
|
- The *why* if the fix isn't obvious from the problem statement
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
❌ "I noticed that on line 42 you're not checking if the user object is null before accessing the email property. This could potentially cause a crash if the user is not found in the database. You might want to add a null check here."
|
||||||
|
|
||||||
|
✅ `L42: 🔴 bug: user can be null after .find(). Add guard before .email.`
|
||||||
|
|
||||||
|
❌ "It looks like this function is doing a lot of things and might benefit from being broken up into smaller functions for readability."
|
||||||
|
|
||||||
|
✅ `L88-140: 🔵 nit: 50-line fn does 4 things. Extract validate/normalize/persist.`
|
||||||
|
|
||||||
|
❌ "Have you considered what happens if the API returns a 429? I think we should probably handle that case."
|
||||||
|
|
||||||
|
✅ `L23: 🟡 risk: no retry on 429. Wrap in withBackoff(3).`
|
||||||
|
|
||||||
|
## Auto-Clarity
|
||||||
|
|
||||||
|
Drop terse mode for: security findings (CVE-class bugs need full explanation + reference), architectural disagreements (need rationale, not just a one-liner), and onboarding contexts where the author is new and needs the "why". In those cases write a normal paragraph, then resume terse for the rest.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
Reviews only — does not write the code fix, does not approve/request-changes, does not run linters. Output the comment(s) ready to paste into the PR. "stop caveman-review" or "normal mode": revert to verbose review style.
|
||||||
Executable
+30
@@ -0,0 +1,30 @@
|
|||||||
|
# caveman-stats
|
||||||
|
|
||||||
|
Real session token receipts. No AI estimation.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Reads the current Claude Code session log directly and reports actual input/output token usage plus estimated savings versus a non-caveman baseline. Numbers come from the JSONL session log on disk — the model itself does not compute or estimate them. Output is injected by the `caveman-mode-tracker` hook, which intercepts `/caveman-stats` and returns the formatted stats as a blocked-decision reason.
|
||||||
|
|
||||||
|
Each run also writes a lifetime-savings suffix file used by the statusline badge (`⛏ 12.4k`).
|
||||||
|
|
||||||
|
## How to invoke
|
||||||
|
|
||||||
|
```
|
||||||
|
/caveman-stats
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example output
|
||||||
|
|
||||||
|
```
|
||||||
|
Session: 47 turns
|
||||||
|
Input: 12,304 tokens
|
||||||
|
Output: 3,891 tokens (caveman)
|
||||||
|
Baseline: 11,247 tokens (estimated without caveman)
|
||||||
|
Saved: 7,356 tokens (~65%)
|
||||||
|
```
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`SKILL.md`](./SKILL.md) — hook contract and mechanics
|
||||||
|
- [Caveman README](../../README.md) — repo overview
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: caveman-stats
|
||||||
|
description: >
|
||||||
|
Show real token usage and estimated savings for the current session.
|
||||||
|
Reads directly from the Claude Code session log — no AI estimation.
|
||||||
|
Triggers on /caveman-stats. Output is injected by the mode-tracker hook;
|
||||||
|
the model itself does not compute the numbers.
|
||||||
|
---
|
||||||
|
|
||||||
|
This skill is delivered by `hooks/caveman-stats.js` (read by `hooks/caveman-mode-tracker.js` on `/caveman-stats`). The model does not need to do anything when this skill fires — the hook returns `decision: "block"` with the formatted stats as the reason. The user sees the numbers immediately.
|
||||||
Executable
+48
@@ -0,0 +1,48 @@
|
|||||||
|
# caveman
|
||||||
|
|
||||||
|
Talk like smart caveman. Same brain, fewer tokens.
|
||||||
|
|
||||||
|
## What it does
|
||||||
|
|
||||||
|
Compress every model response to caveman-style prose. Drops articles, filler, pleasantries, and hedging. Keeps every technical detail, code block, error string, and symbol exact. Cuts ~65-75% of output tokens with full accuracy preserved. Mode persists for the whole session until changed or stopped.
|
||||||
|
|
||||||
|
Six intensity levels:
|
||||||
|
|
||||||
|
| Level | What change |
|
||||||
|
|-------|-------------|
|
||||||
|
| `lite` | Drop filler/hedging. Sentences stay full. Professional but tight. |
|
||||||
|
| `full` | Default. Drop articles, fragments OK, short synonyms. |
|
||||||
|
| `ultra` | Bare fragments. Abbreviations (DB, auth, fn). Arrows for causality. |
|
||||||
|
| `wenyan-lite` | Classical Chinese register, light compression. |
|
||||||
|
| `wenyan-full` | Maximum 文言文. 80-90% character reduction. |
|
||||||
|
| `wenyan-ultra` | Extreme classical compression. |
|
||||||
|
|
||||||
|
Auto-clarity rule: caveman drops to normal prose for security warnings, irreversible-action confirmations, multi-step sequences where fragment ambiguity risks misread, and when user repeats a question. Resumes after the clear part.
|
||||||
|
|
||||||
|
## How to invoke
|
||||||
|
|
||||||
|
```
|
||||||
|
/caveman # full mode (default)
|
||||||
|
/caveman lite # lighter compression
|
||||||
|
/caveman ultra # extreme compression
|
||||||
|
/caveman wenyan # classical Chinese
|
||||||
|
stop caveman # back to normal prose
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example output
|
||||||
|
|
||||||
|
Question: "Why does my React component re-render?"
|
||||||
|
|
||||||
|
Normal prose:
|
||||||
|
> Your component re-renders because you create a new object reference each render. Wrapping it in `useMemo` will fix the issue.
|
||||||
|
|
||||||
|
Caveman (full):
|
||||||
|
> New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`.
|
||||||
|
|
||||||
|
Caveman (ultra):
|
||||||
|
> Inline obj prop → new ref → re-render. `useMemo`.
|
||||||
|
|
||||||
|
## See also
|
||||||
|
|
||||||
|
- [`SKILL.md`](./SKILL.md) — full LLM-facing instructions
|
||||||
|
- [Caveman README](../../README.md) — repo overview, install, benchmarks
|
||||||
Executable
+74
@@ -0,0 +1,74 @@
|
|||||||
|
---
|
||||||
|
name: caveman
|
||||||
|
description: >
|
||||||
|
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
|
||||||
|
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra,
|
||||||
|
wenyan-lite, wenyan-full, wenyan-ultra.
|
||||||
|
Use when user says "caveman mode", "talk like caveman", "use caveman", "less tokens",
|
||||||
|
"be brief", or invokes /caveman. Also auto-triggers when token efficiency is requested.
|
||||||
|
---
|
||||||
|
|
||||||
|
Respond terse like smart caveman. All technical substance stay. Only fluff die.
|
||||||
|
|
||||||
|
## Persistence
|
||||||
|
|
||||||
|
ACTIVE EVERY RESPONSE. No revert after many turns. No filler drift. Still active if unsure. Off only: "stop caveman" / "normal mode".
|
||||||
|
|
||||||
|
Default: **full**. Switch: `/caveman lite|full|ultra`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
Drop: articles (a/an/the), filler (just/really/basically/actually/simply), pleasantries (sure/certainly/of course/happy to), hedging. Fragments OK. Short synonyms (big not extensive, fix not "implement a solution for"). Technical terms exact. Code blocks unchanged. Errors quoted exact.
|
||||||
|
|
||||||
|
Pattern: `[thing] [action] [reason]. [next step].`
|
||||||
|
|
||||||
|
Not: "Sure! I'd be happy to help you with that. The issue you're experiencing is likely caused by..."
|
||||||
|
Yes: "Bug in auth middleware. Token expiry check use `<` not `<=`. Fix:"
|
||||||
|
|
||||||
|
## Intensity
|
||||||
|
|
||||||
|
| Level | What change |
|
||||||
|
|-------|------------|
|
||||||
|
| **lite** | No filler/hedging. Keep articles + full sentences. Professional but tight |
|
||||||
|
| **full** | Drop articles, fragments OK, short synonyms. Classic caveman |
|
||||||
|
| **ultra** | Abbreviate prose words (DB/auth/config/req/res/fn/impl), strip conjunctions, arrows for causality (X → Y), one word when one word enough. Code symbols, function names, API names, error strings: never abbreviate |
|
||||||
|
| **wenyan-lite** | Semi-classical. Drop filler/hedging but keep grammar structure, classical register |
|
||||||
|
| **wenyan-full** | Maximum classical terseness. Fully 文言文. 80-90% character reduction. Classical sentence patterns, verbs precede objects, subjects often omitted, classical particles (之/乃/為/其) |
|
||||||
|
| **wenyan-ultra** | Extreme abbreviation while keeping classical Chinese feel. Maximum compression, ultra terse |
|
||||||
|
|
||||||
|
Example — "Why React component re-render?"
|
||||||
|
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
||||||
|
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
||||||
|
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
||||||
|
- wenyan-lite: "組件頻重繪,以每繪新生對象參照故。以 useMemo 包之。"
|
||||||
|
- wenyan-full: "物出新參照,致重繪。useMemo .Wrap之。"
|
||||||
|
- wenyan-ultra: "新參照→重繪。useMemo Wrap。"
|
||||||
|
|
||||||
|
Example — "Explain database connection pooling."
|
||||||
|
- lite: "Connection pooling reuses open connections instead of creating new ones per request. Avoids repeated handshake overhead."
|
||||||
|
- full: "Pool reuse open DB connections. No new connection per request. Skip handshake overhead."
|
||||||
|
- ultra: "Pool = reuse DB conn. Skip handshake → fast under load."
|
||||||
|
- wenyan-full: "池reuse open connection。不每req新開。skip handshake overhead。"
|
||||||
|
- wenyan-ultra: "池reuse conn。skip handshake → fast。"
|
||||||
|
|
||||||
|
## Auto-Clarity
|
||||||
|
|
||||||
|
Drop caveman when:
|
||||||
|
- Security warnings
|
||||||
|
- Irreversible action confirmations
|
||||||
|
- Multi-step sequences where fragment order or omitted conjunctions risk misread
|
||||||
|
- Compression itself creates technical ambiguity (e.g., `"migrate table drop column backup first"` — order unclear without articles/conjunctions)
|
||||||
|
- User asks to clarify or repeats question
|
||||||
|
|
||||||
|
Resume caveman after clear part done.
|
||||||
|
|
||||||
|
Example — destructive op:
|
||||||
|
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
|
||||||
|
> ```sql
|
||||||
|
> DROP TABLE users;
|
||||||
|
> ```
|
||||||
|
> Caveman resume. Verify backup exist first.
|
||||||
|
|
||||||
|
## Boundaries
|
||||||
|
|
||||||
|
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end.
|
||||||
Executable
+142
@@ -0,0 +1,142 @@
|
|||||||
|
---
|
||||||
|
name: find-skills
|
||||||
|
description: Helps users discover and install agent skills when they ask questions like "how do I do X", "find a skill for X", "is there a skill that can...", or express interest in extending capabilities. This skill should be used when the user is looking for functionality that might exist as an installable skill.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Find Skills
|
||||||
|
|
||||||
|
This skill helps you discover and install skills from the open agent skills ecosystem.
|
||||||
|
|
||||||
|
## When to Use This Skill
|
||||||
|
|
||||||
|
Use this skill when the user:
|
||||||
|
|
||||||
|
- Asks "how do I do X" where X might be a common task with an existing skill
|
||||||
|
- Says "find a skill for X" or "is there a skill for X"
|
||||||
|
- Asks "can you do X" where X is a specialized capability
|
||||||
|
- Expresses interest in extending agent capabilities
|
||||||
|
- Wants to search for tools, templates, or workflows
|
||||||
|
- Mentions they wish they had help with a specific domain (design, testing, deployment, etc.)
|
||||||
|
|
||||||
|
## What is the Skills CLI?
|
||||||
|
|
||||||
|
The Skills CLI (`npx skills`) is the package manager for the open agent skills ecosystem. Skills are modular packages that extend agent capabilities with specialized knowledge, workflows, and tools.
|
||||||
|
|
||||||
|
**Key commands:**
|
||||||
|
|
||||||
|
- `npx skills find [query]` - Search for skills interactively or by keyword
|
||||||
|
- `npx skills add <package>` - Install a skill from GitHub or other sources
|
||||||
|
- `npx skills check` - Check for skill updates
|
||||||
|
- `npx skills update` - Update all installed skills
|
||||||
|
|
||||||
|
**Browse skills at:** https://skills.sh/
|
||||||
|
|
||||||
|
## How to Help Users Find Skills
|
||||||
|
|
||||||
|
### Step 1: Understand What They Need
|
||||||
|
|
||||||
|
When a user asks for help with something, identify:
|
||||||
|
|
||||||
|
1. The domain (e.g., React, testing, design, deployment)
|
||||||
|
2. The specific task (e.g., writing tests, creating animations, reviewing PRs)
|
||||||
|
3. Whether this is a common enough task that a skill likely exists
|
||||||
|
|
||||||
|
### Step 2: Check the Leaderboard First
|
||||||
|
|
||||||
|
Before running a CLI search, check the [skills.sh leaderboard](https://skills.sh/) to see if a well-known skill already exists for the domain. The leaderboard ranks skills by total installs, surfacing the most popular and battle-tested options.
|
||||||
|
|
||||||
|
For example, top skills for web development include:
|
||||||
|
- `vercel-labs/agent-skills` — React, Next.js, web design (100K+ installs each)
|
||||||
|
- `anthropics/skills` — Frontend design, document processing (100K+ installs)
|
||||||
|
|
||||||
|
### Step 3: Search for Skills
|
||||||
|
|
||||||
|
If the leaderboard doesn't cover the user's need, run the find command:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx skills find [query]
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
- User asks "how do I make my React app faster?" → `npx skills find react performance`
|
||||||
|
- User asks "can you help me with PR reviews?" → `npx skills find pr review`
|
||||||
|
- User asks "I need to create a changelog" → `npx skills find changelog`
|
||||||
|
|
||||||
|
### Step 4: Verify Quality Before Recommending
|
||||||
|
|
||||||
|
**Do not recommend a skill based solely on search results.** Always verify:
|
||||||
|
|
||||||
|
1. **Install count** — Prefer skills with 1K+ installs. Be cautious with anything under 100.
|
||||||
|
2. **Source reputation** — Official sources (`vercel-labs`, `anthropics`, `microsoft`) are more trustworthy than unknown authors.
|
||||||
|
3. **GitHub stars** — Check the source repository. A skill from a repo with <100 stars should be treated with skepticism.
|
||||||
|
|
||||||
|
### Step 5: Present Options to the User
|
||||||
|
|
||||||
|
When you find relevant skills, present them to the user with:
|
||||||
|
|
||||||
|
1. The skill name and what it does
|
||||||
|
2. The install count and source
|
||||||
|
3. The install command they can run
|
||||||
|
4. A link to learn more at skills.sh
|
||||||
|
|
||||||
|
Example response:
|
||||||
|
|
||||||
|
```
|
||||||
|
I found a skill that might help! The "react-best-practices" skill provides
|
||||||
|
React and Next.js performance optimization guidelines from Vercel Engineering.
|
||||||
|
(185K installs)
|
||||||
|
|
||||||
|
To install it:
|
||||||
|
npx skills add vercel-labs/agent-skills@react-best-practices
|
||||||
|
|
||||||
|
Learn more: https://skills.sh/vercel-labs/agent-skills/react-best-practices
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 6: Offer to Install
|
||||||
|
|
||||||
|
If the user wants to proceed, you can install the skill for them:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx skills add <owner/repo@skill> -g -y
|
||||||
|
```
|
||||||
|
|
||||||
|
The `-g` flag installs globally (user-level) and `-y` skips confirmation prompts.
|
||||||
|
|
||||||
|
## Common Skill Categories
|
||||||
|
|
||||||
|
When searching, consider these common categories:
|
||||||
|
|
||||||
|
| Category | Example Queries |
|
||||||
|
| --------------- | ---------------------------------------- |
|
||||||
|
| Web Development | react, nextjs, typescript, css, tailwind |
|
||||||
|
| Testing | testing, jest, playwright, e2e |
|
||||||
|
| DevOps | deploy, docker, kubernetes, ci-cd |
|
||||||
|
| Documentation | docs, readme, changelog, api-docs |
|
||||||
|
| Code Quality | review, lint, refactor, best-practices |
|
||||||
|
| Design | ui, ux, design-system, accessibility |
|
||||||
|
| Productivity | workflow, automation, git |
|
||||||
|
|
||||||
|
## Tips for Effective Searches
|
||||||
|
|
||||||
|
1. **Use specific keywords**: "react testing" is better than just "testing"
|
||||||
|
2. **Try alternative terms**: If "deploy" doesn't work, try "deployment" or "ci-cd"
|
||||||
|
3. **Check popular sources**: Many skills come from `vercel-labs/agent-skills` or `ComposioHQ/awesome-claude-skills`
|
||||||
|
|
||||||
|
## When No Skills Are Found
|
||||||
|
|
||||||
|
If no relevant skills exist:
|
||||||
|
|
||||||
|
1. Acknowledge that no existing skill was found
|
||||||
|
2. Offer to help with the task directly using your general capabilities
|
||||||
|
3. Suggest the user could create their own skill with `npx skills init`
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```
|
||||||
|
I searched for skills related to "xyz" but didn't find any matches.
|
||||||
|
I can still help you with this task directly! Would you like me to proceed?
|
||||||
|
|
||||||
|
If this is something you do often, you could create your own skill:
|
||||||
|
npx skills init my-xyz-skill
|
||||||
|
```
|
||||||
Executable
BIN
Binary file not shown.
Executable
+97
@@ -0,0 +1,97 @@
|
|||||||
|
---
|
||||||
|
name: html-slide-deck
|
||||||
|
description: |
|
||||||
|
PPT 스타일의 HTML 슬라이드 덱을 생성하거나 수정합니다. 기술 가이드, 업무 보고서, 교육 자료 등 다양한 용도의 프레젠테이션을 완전히 자체 포함된 단일 HTML 파일로 만듭니다.
|
||||||
|
|
||||||
|
다음 상황에서 이 스킬을 사용하세요: "슬라이드 만들어줘", "PPT 형식으로 정리해줘", "가이드 문서를 슬라이드로", "발표 자료 만들어줘", "단계별 절차를 슬라이드로", 기존 HTML 슬라이드에 페이지 추가/수정/삭제, 레이아웃 변경 요청 등. HTML이나 슬라이드를 명시하지 않아도 프레젠테이션 형태가 적합하다고 판단되면 이 스킬을 사용하세요.
|
||||||
|
---
|
||||||
|
|
||||||
|
# HTML 슬라이드 덱 생성/수정 스킬
|
||||||
|
|
||||||
|
## 개요
|
||||||
|
|
||||||
|
완전 자체 포함(self-contained) 단일 HTML 파일로 PPT 스타일 슬라이드 덱을 만드는 스킬입니다.
|
||||||
|
외부 서버 없이 브라우저에서 바로 열리며, 키보드/클릭 네비게이션, 사이드바 목차, 진행 표시줄을 포함합니다.
|
||||||
|
|
||||||
|
## Step 1: 요구사항 파악
|
||||||
|
|
||||||
|
작업 시작 전 다음을 확인하세요:
|
||||||
|
|
||||||
|
- **목적**: 기술 가이드? 업무 보고? 교육 자료?
|
||||||
|
- **내용 구조**: 슬라이드 수, 계층(Step/Task 구분 여부), 코드 포함 여부
|
||||||
|
- **신규 생성 vs 기존 수정**: 파일이 이미 있는지 확인
|
||||||
|
- **출력 경로**: 저장할 폴더
|
||||||
|
|
||||||
|
## Step 2: 템플릿 선택
|
||||||
|
|
||||||
|
### 🔷 기술 가이드 (Technical Guide) — 이번 세션 검증됨
|
||||||
|
절차적 기술 문서에 최적. 단계(Step) + 태스크(Task) 계층, 코드 블록, 다이어그램 지원.
|
||||||
|
- 다크 제목 슬라이드 + 라이트 콘텐츠 슬라이드
|
||||||
|
- Step 헤더 슬라이드 (강조색 배경) + Task 콘텐츠 슬라이드
|
||||||
|
- Highlight.js (C++/Python/JS 등 코드 강조) + Mermaid.js (플로우차트)
|
||||||
|
→ `references/engine.md`의 **Technical Guide 보일러플레이트** 사용
|
||||||
|
|
||||||
|
### 🔶 업무 보고서 (Business Report) — 기본 엔진 사용
|
||||||
|
데이터, 표, 불릿포인트 중심. 밝은 배경, 헤더/푸터 포함.
|
||||||
|
→ `references/engine.md`의 **기본 엔진**을 기반으로 커스텀 CSS 적용
|
||||||
|
|
||||||
|
### 🟢 교육 자료 (Training Material) — 기본 엔진 사용
|
||||||
|
퀴즈, 예시, 체크리스트 위주. 색상 풍부, 시각적 강조.
|
||||||
|
→ `references/engine.md`의 **기본 엔진**을 기반으로 커스텀 CSS 적용
|
||||||
|
|
||||||
|
## Step 3-A: 신규 생성 워크플로우
|
||||||
|
|
||||||
|
1. **콘텐츠 아웃라인 작성**: 슬라이드 목록과 각 슬라이드의 내용을 먼저 정리
|
||||||
|
2. **SLIDE_META 설계**: 사이드바에 표시될 슬라이드 메타데이터 배열 설계
|
||||||
|
```javascript
|
||||||
|
const SLIDE_META = [
|
||||||
|
{ idx: 0, label: '제목 · 목차', dot: '🏠', color: '#475569', step: false },
|
||||||
|
{ idx: 1, label: 'Step 1 · 개요', dot: '1', color: '#1D4ED8', step: true },
|
||||||
|
{ idx: 2, label: 'Task 1-1 · ...',dot: '1', color: '#1D4ED8', step: false },
|
||||||
|
// idx는 DOM 순서(0부터 시작)와 정확히 일치해야 함
|
||||||
|
];
|
||||||
|
```
|
||||||
|
3. **보일러플레이트 삽입**: `references/engine.md`에서 전체 HTML 구조 복사
|
||||||
|
4. **슬라이드 추가**: `references/slide-types.md`의 타입별 템플릿 참조
|
||||||
|
5. **레이아웃 적용**: `references/layout-patterns.md`의 패턴 참조
|
||||||
|
6. **제목 슬라이드 뱃지 업데이트**: "N 슬라이드" 카운트를 실제 슬라이드 수로
|
||||||
|
|
||||||
|
## Step 3-B: 기존 수정 워크플로우
|
||||||
|
|
||||||
|
수정 전 반드시:
|
||||||
|
1. **전체 SLIDE_META 파악**: 현재 슬라이드 목록과 idx 값 확인
|
||||||
|
2. **DOM 슬라이드 총 수 확인**: `class="slide ` 패턴 grep으로 총 개수 파악
|
||||||
|
3. **data-idx는 장식용**: JS 엔진은 DOM 순서(querySelectorAll 결과)를 사용함.
|
||||||
|
SLIDE_META의 `idx` 값이 DOM 순서와 일치해야 사이드바 네비게이션이 동작함.
|
||||||
|
|
||||||
|
**슬라이드 추가 시**:
|
||||||
|
- 원하는 위치에 HTML 삽입
|
||||||
|
- 해당 위치 이후의 모든 SLIDE_META idx 값을 +1
|
||||||
|
- "N 슬라이드" 뱃지 +1
|
||||||
|
|
||||||
|
**슬라이드 삭제/병합 시**:
|
||||||
|
- HTML에서 슬라이드 div 제거
|
||||||
|
- SLIDE_META에서 해당 항목 제거, 이후 idx -1
|
||||||
|
- "N 슬라이드" 뱃지 -1
|
||||||
|
|
||||||
|
**레이아웃 수정 시**:
|
||||||
|
- 변경 전 `grep`으로 현재 CSS 값 정확히 확인 후 수정
|
||||||
|
- 추측으로 old_string 작성하지 말 것
|
||||||
|
|
||||||
|
## Step 4: 품질 체크
|
||||||
|
|
||||||
|
완성 후 확인:
|
||||||
|
- [ ] SLIDE_META idx가 DOM 순서와 정확히 일치하는가
|
||||||
|
- [ ] The End 슬라이드가 SLIDE_META에 포함되지 않는가 (네비게이션 전용)
|
||||||
|
- [ ] 제목 슬라이드의 "N 슬라이드" 뱃지가 실제 수와 맞는가
|
||||||
|
- [ ] 모든 코드 블록이 `hljs.highlightAll()` 범위 내에 있는가
|
||||||
|
- [ ] Mermaid 다이어그램에 `class="mermaid"`가 있는가
|
||||||
|
- [ ] 모바일/좁은 화면에서 overflow가 적절히 처리되는가
|
||||||
|
|
||||||
|
## 레퍼런스 파일 안내
|
||||||
|
|
||||||
|
| 파일 | 내용 | 언제 읽기 |
|
||||||
|
|------|------|-----------|
|
||||||
|
| `references/engine.md` | HTML 보일러플레이트 (head, CSS, JS 엔진 전체) | 신규 생성 시, 엔진 동작 이해 필요 시 |
|
||||||
|
| `references/slide-types.md` | 슬라이드 타입별 HTML 템플릿 | 새 슬라이드 추가 시 |
|
||||||
|
| `references/layout-patterns.md` | 콘텐츠 레이아웃 패턴 (코드, 그리드, 테이블 등) | 슬라이드 내부 레이아웃 구성 시 |
|
||||||
Executable
+498
@@ -0,0 +1,498 @@
|
|||||||
|
# HTML 슬라이드 엔진 레퍼런스
|
||||||
|
|
||||||
|
이 파일에는 슬라이드 덱의 뼈대(boilerplate) 전체가 담겨 있습니다.
|
||||||
|
신규 파일을 만들 때 아래 보일러플레이트를 복사한 뒤 `<!-- SLIDES HERE -->` 영역에 슬라이드를 채워 넣으세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Technical Guide 보일러플레이트
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ko">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>{{TITLE}}</title>
|
||||||
|
|
||||||
|
<!-- 코드 하이라이팅 (Highlight.js) -->
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css" />
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/cpp.min.js"></script>
|
||||||
|
<!-- Mermaid 다이어그램 (필요 없으면 제거 가능) -->
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/10.6.1/mermaid.min.js"></script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
/* ══ Reset ══════════════════════════════════════════════════ */
|
||||||
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||||
|
html, body {
|
||||||
|
width: 100%; height: 100%; overflow: hidden;
|
||||||
|
font-family: 'Segoe UI', 'Apple SD Gothic Neo', sans-serif;
|
||||||
|
background: #0F172A;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ══ Sidebar ════════════════════════════════════════════════ */
|
||||||
|
#sidebar {
|
||||||
|
position: fixed; top: 0; left: 0; bottom: 0; width: 300px;
|
||||||
|
background: rgba(10,17,32,0.97); backdrop-filter: blur(16px);
|
||||||
|
border-right: 1px solid rgba(255,255,255,.08);
|
||||||
|
z-index: 600; transform: translateX(-300px);
|
||||||
|
transition: transform .28s cubic-bezier(.4,0,.2,1);
|
||||||
|
display: flex; flex-direction: column; overflow: hidden;
|
||||||
|
box-shadow: 4px 0 32px rgba(0,0,0,.5);
|
||||||
|
}
|
||||||
|
#sidebar.open { transform: translateX(0); }
|
||||||
|
|
||||||
|
.sb-head { padding: 18px 16px 14px; border-bottom: 1px solid rgba(255,255,255,.06); flex-shrink: 0; }
|
||||||
|
.sb-badge {
|
||||||
|
display: inline-block; background: #1D4ED8; color: #fff;
|
||||||
|
font-size: 9px; font-weight: 800; letter-spacing: .1em;
|
||||||
|
text-transform: uppercase; padding: 3px 8px; border-radius: 4px; margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.sb-title { color: #F8FAFC; font-size: 13px; font-weight: 700; line-height: 1.4; }
|
||||||
|
.sb-list { flex: 1; overflow-y: auto; padding: 10px 8px; }
|
||||||
|
.sb-list::-webkit-scrollbar { width: 4px; }
|
||||||
|
.sb-list::-webkit-scrollbar-thumb { background: rgba(255,255,255,.15); border-radius: 2px; }
|
||||||
|
|
||||||
|
.sb-item {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
padding: 8px 10px; border-radius: 8px; cursor: pointer;
|
||||||
|
transition: background .15s; margin-bottom: 2px;
|
||||||
|
}
|
||||||
|
.sb-item:hover { background: rgba(255,255,255,.07); }
|
||||||
|
.sb-item.active { background: rgba(255,255,255,.12); }
|
||||||
|
.sb-item.is-step { margin-top: 6px; }
|
||||||
|
.sb-dot {
|
||||||
|
width: 26px; height: 26px; border-radius: 6px;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 11px; font-weight: 800; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.sb-label { color: #CBD5E1; font-size: 12px; font-weight: 500; line-height: 1.3; }
|
||||||
|
.sb-item.active .sb-label { color: #F1F5F9; font-weight: 700; }
|
||||||
|
|
||||||
|
/* 사이드바 최하단 도움말 버튼 */
|
||||||
|
.sb-help-btn {
|
||||||
|
margin: 10px 12px 14px; padding: 8px 12px;
|
||||||
|
background: rgba(255,255,255,.06); border: 1px solid rgba(255,255,255,.1);
|
||||||
|
border-radius: 8px; color: #94A3B8; font-size: 11px;
|
||||||
|
cursor: pointer; text-align: center; flex-shrink: 0;
|
||||||
|
transition: background .15s;
|
||||||
|
}
|
||||||
|
.sb-help-btn:hover { background: rgba(255,255,255,.1); }
|
||||||
|
|
||||||
|
/* ══ Deck ═══════════════════════════════════════════════════ */
|
||||||
|
#deck {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; bottom: 48px;
|
||||||
|
transition: left .28s cubic-bezier(.4,0,.2,1),
|
||||||
|
right .28s cubic-bezier(.4,0,.2,1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
body.sb-open #deck { left: 300px; }
|
||||||
|
|
||||||
|
/* ══ Slides ═════════════════════════════════════════════════ */
|
||||||
|
.slide {
|
||||||
|
position: absolute; inset: 0;
|
||||||
|
transform: translateX(100%);
|
||||||
|
transition: transform .32s cubic-bezier(.4,0,.2,1);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.slide.active { transform: translateX(0); }
|
||||||
|
.slide.exit-left { transform: translateX(-100%); }
|
||||||
|
.slide.exit-right { transform: translateX(100%); }
|
||||||
|
|
||||||
|
/* ── 제목/목차 슬라이드 ─── */
|
||||||
|
.slide-title {
|
||||||
|
background: linear-gradient(135deg, #0F172A 0%, #1E1B4B 50%, #0F172A 100%);
|
||||||
|
display: flex; flex-direction: row;
|
||||||
|
justify-content: flex-start; align-items: stretch;
|
||||||
|
padding: 0; overflow: hidden;
|
||||||
|
}
|
||||||
|
.tc-left {
|
||||||
|
flex: 0 0 50%; display: flex; flex-direction: column; justify-content: center;
|
||||||
|
padding: 52px 44px 52px 72px;
|
||||||
|
border-right: 1px solid rgba(255,255,255,.1);
|
||||||
|
position: relative; z-index: 1;
|
||||||
|
}
|
||||||
|
.tc-right {
|
||||||
|
flex: 0 0 50%; display: flex; flex-direction: column;
|
||||||
|
padding: 32px 52px 24px 44px;
|
||||||
|
}
|
||||||
|
.t-eyebrow { font-size: 11px; font-weight: 700; letter-spacing: .15em; text-transform: uppercase; color: #818CF8; margin-bottom: 16px; }
|
||||||
|
.t-title { font-size: 36px; font-weight: 800; color: #F8FAFC; line-height: 1.2; margin-bottom: 16px; }
|
||||||
|
.t-subtitle { font-size: 15px; color: #94A3B8; line-height: 1.6; margin-bottom: 28px; }
|
||||||
|
.t-meta { display: flex; gap: 16px; flex-wrap: wrap; }
|
||||||
|
.t-meta-item { font-size: 12px; color: #64748B; }
|
||||||
|
.t-meta-item strong { color: #94A3B8; }
|
||||||
|
|
||||||
|
/* TOC 오른쪽 패널 */
|
||||||
|
.toc-label { font-size: 10px; font-weight: 700; letter-spacing: .12em; text-transform: uppercase; color: #6366F1; margin-bottom: 14px; flex-shrink: 0; }
|
||||||
|
.toc-grid {
|
||||||
|
display: grid; grid-template-columns: 1fr;
|
||||||
|
gap: 8px; flex: 1; align-content: stretch; overflow: hidden;
|
||||||
|
}
|
||||||
|
.toc-item {
|
||||||
|
display: flex; align-items: center; gap: 10px;
|
||||||
|
background: rgba(255,255,255,.05); border: 1px solid rgba(255,255,255,.08);
|
||||||
|
border-radius: 8px; padding: 0 14px; cursor: pointer;
|
||||||
|
transition: background .15s; min-height: 0;
|
||||||
|
}
|
||||||
|
.toc-item:hover { background: rgba(255,255,255,.1); }
|
||||||
|
.toc-num { font-size: 11px; font-weight: 800; color: #818CF8; min-width: 20px; }
|
||||||
|
.toc-text { font-size: 12px; color: #CBD5E1; font-weight: 500; }
|
||||||
|
|
||||||
|
/* ── Step 헤더 슬라이드 ─── */
|
||||||
|
.slide-step {
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
justify-content: center; align-items: flex-start;
|
||||||
|
padding: 64px 80px;
|
||||||
|
}
|
||||||
|
/* Step 배경색 변형 */
|
||||||
|
.bg-s1 { background: linear-gradient(135deg, #1e3a8a 0%, #1d4ed8 100%); }
|
||||||
|
.bg-s2 { background: linear-gradient(135deg, #3b0764 0%, #7c3aed 100%); }
|
||||||
|
.bg-s3 { background: linear-gradient(135deg, #831843 0%, #db2777 100%); }
|
||||||
|
.bg-s4 { background: linear-gradient(135deg, #064e3b 0%, #059669 100%); }
|
||||||
|
.bg-s5 { background: linear-gradient(135deg, #7c2d12 0%, #ea580c 100%); }
|
||||||
|
|
||||||
|
.sh-big-num {
|
||||||
|
font-size: 120px; font-weight: 900; color: rgba(255,255,255,.12);
|
||||||
|
line-height: 1; margin-bottom: -20px; user-select: none;
|
||||||
|
}
|
||||||
|
.sh-eyebrow { font-size: 11px; font-weight: 700; letter-spacing: .15em; text-transform: uppercase; color: rgba(255,255,255,.6); margin-bottom: 16px; }
|
||||||
|
.sh-title { font-size: 42px; font-weight: 800; color: #fff; line-height: 1.15; margin-bottom: 20px; }
|
||||||
|
.sh-desc { font-size: 15px; color: rgba(255,255,255,.75); line-height: 1.7; max-width: 560px; margin-bottom: 28px; }
|
||||||
|
.sh-pills { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
.sh-pill { background: rgba(255,255,255,.15); color: #fff; font-size: 11px; font-weight: 600; padding: 5px 12px; border-radius: 20px; }
|
||||||
|
|
||||||
|
/* ── 콘텐츠 슬라이드 ─── */
|
||||||
|
.slide-content { background: #F8FAFC; display: flex; flex-direction: column; }
|
||||||
|
.sc-header {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
padding: 20px 52px 16px; border-bottom: 2px solid #E2E8F0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.sc-header h2 { font-size: 20px; font-weight: 800; color: #0F172A; }
|
||||||
|
.sc-body { flex: 1; overflow-y: auto; padding: 22px 52px 36px; }
|
||||||
|
.sc-body-flex { display: flex; flex-direction: column; }
|
||||||
|
|
||||||
|
/* Task 뱃지 색상 */
|
||||||
|
.task-badge {
|
||||||
|
font-size: 10px; font-weight: 800; letter-spacing: .08em;
|
||||||
|
text-transform: uppercase; padding: 4px 10px; border-radius: 6px; flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.b1 { background: #DBEAFE; color: #1D4ED8; }
|
||||||
|
.b2 { background: #EDE9FE; color: #7C3AED; }
|
||||||
|
.b3 { background: #FCE7F3; color: #DB2777; }
|
||||||
|
.b4 { background: #D1FAE5; color: #059669; }
|
||||||
|
.b5 { background: #FFEDD5; color: #EA580C; }
|
||||||
|
|
||||||
|
/* ── 엔딩 슬라이드 ─── */
|
||||||
|
.slide-end {
|
||||||
|
background: linear-gradient(135deg, #0F172A 0%, #1E1B4B 100%);
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
justify-content: center; align-items: center; text-align: center;
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.end-deco1 {
|
||||||
|
position: absolute; width: 400px; height: 400px; border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(99,102,241,.2) 0%, transparent 70%);
|
||||||
|
top: -100px; right: -100px;
|
||||||
|
}
|
||||||
|
.end-deco2 {
|
||||||
|
position: absolute; width: 300px; height: 300px; border-radius: 50%;
|
||||||
|
background: radial-gradient(circle, rgba(139,92,246,.15) 0%, transparent 70%);
|
||||||
|
bottom: -80px; left: -80px;
|
||||||
|
}
|
||||||
|
.end-label { font-size: 11px; font-weight: 700; letter-spacing: .2em; text-transform: uppercase; color: #6366F1; margin-bottom: 20px; position: relative; z-index: 1; }
|
||||||
|
.end-title { font-size: 72px; font-weight: 900; color: #F8FAFC; margin-bottom: 24px; position: relative; z-index: 1; }
|
||||||
|
.end-divider { width: 60px; height: 3px; background: #6366F1; border-radius: 2px; margin-bottom: 24px; position: relative; z-index: 1; }
|
||||||
|
.end-subtitle { font-size: 18px; color: #94A3B8; margin-bottom: 12px; position: relative; z-index: 1; }
|
||||||
|
.end-meta { font-size: 13px; color: #64748B; position: relative; z-index: 1; }
|
||||||
|
|
||||||
|
/* ══ 공용 컴포넌트 ═══════════════════════════════════════════ */
|
||||||
|
.desc { font-size: 13.5px; color: #475569; line-height: 1.7; }
|
||||||
|
code { font-family: 'Consolas', 'Courier New', monospace; background: #EEF2FF; color: #4338CA; padding: 2px 6px; border-radius: 4px; font-size: .9em; }
|
||||||
|
|
||||||
|
/* 파일 경로 뱃지 */
|
||||||
|
.file-row { margin-bottom: 12px; }
|
||||||
|
.file-pill { display: inline-block; background: #1E293B; color: #94A3B8; font-family: 'Consolas', monospace; font-size: 11px; padding: 4px 10px; border-radius: 6px; }
|
||||||
|
|
||||||
|
/* 코드 블록 */
|
||||||
|
.code-wrap { border-radius: 10px; overflow: hidden; border: 1px solid #E2E8F0; }
|
||||||
|
.code-hdr { display: flex; justify-content: space-between; align-items: center; padding: 8px 14px; background: #1E293B; }
|
||||||
|
.code-fn { color: #94A3B8; font-size: 11px; font-family: 'Consolas', monospace; }
|
||||||
|
.code-lang { color: #64748B; font-size: 10px; font-weight: 700; letter-spacing: .08em; text-transform: uppercase; }
|
||||||
|
.code-wrap pre { margin: 0; max-height: 320px; overflow-y: auto; }
|
||||||
|
.code-wrap pre code { font-size: 12.5px; line-height: 1.6; }
|
||||||
|
|
||||||
|
/* Callout */
|
||||||
|
.callout { display: flex; gap: 12px; padding: 12px 16px; border-radius: 10px; margin-top: 14px; }
|
||||||
|
.callout.note { background: #EFF6FF; border-left: 3px solid #3B82F6; }
|
||||||
|
.callout.warn { background: #FFFBEB; border-left: 3px solid #F59E0B; }
|
||||||
|
.callout.tip { background: #F0FDF4; border-left: 3px solid #22C55E; }
|
||||||
|
.callout.danger { background: #FEF2F2; border-left: 3px solid #EF4444; }
|
||||||
|
.callout .ci { font-size: 16px; flex-shrink: 0; margin-top: 1px; }
|
||||||
|
.callout div { font-size: 12.5px; color: #334155; line-height: 1.6; }
|
||||||
|
.callout strong { display: block; margin-bottom: 4px; color: #0F172A; font-size: 13px; }
|
||||||
|
|
||||||
|
/* 체크리스트 */
|
||||||
|
.checklist { display: flex; flex-direction: column; gap: 14px; margin-top: 8px; }
|
||||||
|
.check-item { display: flex; gap: 14px; align-items: flex-start; }
|
||||||
|
.check-circle {
|
||||||
|
width: 32px; height: 32px; border-radius: 50%; flex-shrink: 0;
|
||||||
|
background: #EEF2FF; color: #4F46E5; font-size: 13px; font-weight: 800;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
.check-item div { font-size: 13px; color: #334155; line-height: 1.65; }
|
||||||
|
.check-item strong { display: block; color: #0F172A; margin-bottom: 2px; }
|
||||||
|
|
||||||
|
/* 테이블 */
|
||||||
|
.ini-table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
||||||
|
.ini-table th { background: #1E293B; color: #94A3B8; padding: 10px 14px; text-align: left; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; }
|
||||||
|
.ini-table td { padding: 10px 14px; border-bottom: 1px solid #E2E8F0; color: #334155; vertical-align: top; }
|
||||||
|
.ini-table tr:last-child td { border-bottom: none; }
|
||||||
|
.ini-table td:first-child { font-family: 'Consolas', monospace; color: #6D28D9; font-weight: 600; }
|
||||||
|
.ini-table.compact td, .ini-table.compact th { padding: 6px 10px; font-size: 12px; }
|
||||||
|
|
||||||
|
/* ══ 진행 표시줄 & 네비게이션 ════════════════════════════════ */
|
||||||
|
#progress-bar {
|
||||||
|
position: fixed; top: 0; left: 0; right: 0; height: 3px;
|
||||||
|
background: rgba(255,255,255,.08); z-index: 500;
|
||||||
|
transition: left .28s cubic-bezier(.4,0,.2,1), right .28s cubic-bezier(.4,0,.2,1);
|
||||||
|
}
|
||||||
|
body.sb-open #progress-bar { left: 300px; }
|
||||||
|
#progress-fill { height: 100%; background: #6366F1; transform: scaleX(0); transform-origin: left; transition: transform .32s ease; }
|
||||||
|
|
||||||
|
#nav {
|
||||||
|
position: fixed; bottom: 0; left: 0; right: 0; height: 48px;
|
||||||
|
background: rgba(15,23,42,.95); backdrop-filter: blur(12px);
|
||||||
|
border-top: 1px solid rgba(255,255,255,.06);
|
||||||
|
display: flex; align-items: center; justify-content: center; gap: 16px;
|
||||||
|
z-index: 500;
|
||||||
|
transition: left .28s cubic-bezier(.4,0,.2,1), right .28s cubic-bezier(.4,0,.2,1);
|
||||||
|
}
|
||||||
|
body.sb-open #nav { left: 300px; }
|
||||||
|
#nav button {
|
||||||
|
background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.1);
|
||||||
|
color: #94A3B8; width: 32px; height: 32px; border-radius: 8px;
|
||||||
|
font-size: 16px; cursor: pointer; transition: all .15s;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
}
|
||||||
|
#nav button:hover:not(:disabled) { background: rgba(255,255,255,.15); color: #F1F5F9; }
|
||||||
|
#nav button:disabled { opacity: 0.3; cursor: default; }
|
||||||
|
#slide-counter { font-size: 11px; color: #64748B; min-width: 44px; text-align: center; }
|
||||||
|
#dot-nav { display: flex; gap: 6px; align-items: center; }
|
||||||
|
.dot { width: 6px; height: 6px; border-radius: 50%; background: rgba(255,255,255,.2); cursor: pointer; transition: all .2s; }
|
||||||
|
.dot.active { background: #6366F1; transform: scale(1.4); }
|
||||||
|
|
||||||
|
/* 사이드바 토글 버튼 */
|
||||||
|
#sb-toggle {
|
||||||
|
position: fixed; top: 12px; left: 12px; z-index: 700;
|
||||||
|
width: 36px; height: 36px; border-radius: 10px;
|
||||||
|
background: rgba(15,23,42,.9); border: 1px solid rgba(255,255,255,.12);
|
||||||
|
color: #94A3B8; font-size: 18px; cursor: pointer;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
transition: all .15s;
|
||||||
|
}
|
||||||
|
#sb-toggle:hover { background: rgba(255,255,255,.15); color: #F1F5F9; }
|
||||||
|
|
||||||
|
/* 도움말 모달 */
|
||||||
|
#help-modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,.6); z-index: 900; align-items: center; justify-content: center; }
|
||||||
|
#help-modal.open { display: flex; }
|
||||||
|
.hm-box { background: #1E293B; border-radius: 16px; padding: 28px 32px; max-width: 480px; width: 90%; border: 1px solid rgba(255,255,255,.1); }
|
||||||
|
.hm-title { color: #F8FAFC; font-size: 16px; font-weight: 700; margin-bottom: 20px; }
|
||||||
|
.kb-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||||
|
.kb-item { display: flex; gap: 10px; align-items: center; }
|
||||||
|
.kb-key { background: #334155; color: #F1F5F9; font-size: 10px; font-family: monospace; padding: 3px 7px; border-radius: 4px; white-space: nowrap; flex-shrink: 0; }
|
||||||
|
.kb-desc { color: #94A3B8; font-size: 12px; }
|
||||||
|
.hm-close { margin-top: 20px; width: 100%; padding: 10px; background: #334155; border: none; color: #F1F5F9; border-radius: 8px; cursor: pointer; font-size: 13px; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<!-- 사이드바 토글 -->
|
||||||
|
<button id="sb-toggle" onclick="toggleSidebar()" title="목차 (T)">☰</button>
|
||||||
|
|
||||||
|
<!-- 사이드바 -->
|
||||||
|
<div id="sidebar">
|
||||||
|
<div class="sb-head">
|
||||||
|
<div class="sb-badge">{{SHORT_TITLE}}</div>
|
||||||
|
<div class="sb-title">{{FULL_TITLE}}</div>
|
||||||
|
</div>
|
||||||
|
<div class="sb-list" id="sb-list"></div>
|
||||||
|
<div class="sb-help-btn" onclick="document.getElementById('help-modal').classList.toggle('open')">⌨ 단축키 도움말</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 진행 표시줄 -->
|
||||||
|
<div id="progress-bar"><div id="progress-fill"></div></div>
|
||||||
|
|
||||||
|
<!-- 슬라이드 덱 -->
|
||||||
|
<div id="deck">
|
||||||
|
|
||||||
|
<!-- SLIDES HERE -->
|
||||||
|
|
||||||
|
</div><!-- #deck -->
|
||||||
|
|
||||||
|
<!-- 네비게이션 -->
|
||||||
|
<div id="nav">
|
||||||
|
<button id="btn-prev" onclick="prevSlide()" disabled title="이전 (← PgUp)">←</button>
|
||||||
|
<div id="dot-nav"></div>
|
||||||
|
<span id="slide-counter">1 / 1</span>
|
||||||
|
<button id="btn-next" onclick="nextSlide()" title="다음 (→ PgDn)">→</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 도움말 모달 -->
|
||||||
|
<div id="help-modal">
|
||||||
|
<div class="hm-box">
|
||||||
|
<div class="hm-title">⌨ 키보드 단축키</div>
|
||||||
|
<div class="kb-grid">
|
||||||
|
<div class="kb-item"><span class="kb-key">→ / PgDn</span><div class="kb-desc">다음 슬라이드</div></div>
|
||||||
|
<div class="kb-item"><span class="kb-key">← / PgUp</span><div class="kb-desc">이전 슬라이드</div></div>
|
||||||
|
<div class="kb-item"><span class="kb-key">Home</span><div class="kb-desc">첫 슬라이드로</div></div>
|
||||||
|
<div class="kb-item"><span class="kb-key">End</span><div class="kb-desc">마지막 슬라이드로</div></div>
|
||||||
|
<div class="kb-item"><span class="kb-key">T</span><div class="kb-desc">목차 사이드바 토글</div></div>
|
||||||
|
<div class="kb-item"><span class="kb-key">Esc</span><div class="kb-desc">모달/사이드바 닫기</div></div>
|
||||||
|
</div>
|
||||||
|
<button class="hm-close" onclick="document.getElementById('help-modal').classList.remove('open')">닫기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* ─── Mermaid (다이어그램 필요 시) ──────────────────────────── */
|
||||||
|
mermaid.initialize({
|
||||||
|
startOnLoad: true, securityLevel: 'loose', theme: 'base',
|
||||||
|
themeVariables: { fontFamily: "'Segoe UI','Apple SD Gothic Neo',sans-serif", fontSize: '14px' },
|
||||||
|
flowchart: { curve: 'basis', htmlLabels: true, nodeSpacing: 60, rankSpacing: 80 }
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ─── Slide Engine ──────────────────────────────────────────── */
|
||||||
|
const slides = Array.from(document.querySelectorAll('.slide'));
|
||||||
|
const TOTAL = slides.length;
|
||||||
|
let current = 0;
|
||||||
|
let animating = false;
|
||||||
|
const DURATION = 320; // CSS transition 길이와 일치해야 함
|
||||||
|
|
||||||
|
function goToSlide(idx) {
|
||||||
|
if (idx < 0 || idx >= TOTAL || idx === current || animating) return;
|
||||||
|
animating = true;
|
||||||
|
const isForward = idx > current;
|
||||||
|
const prevIdx = current;
|
||||||
|
current = idx;
|
||||||
|
const prevEl = slides[prevIdx];
|
||||||
|
const nextEl = slides[current];
|
||||||
|
|
||||||
|
// 다음 슬라이드를 트랜지션 없이 오프스크린에 배치
|
||||||
|
nextEl.style.transition = 'none';
|
||||||
|
nextEl.style.transform = isForward ? 'translateX(100%)' : 'translateX(-100%)';
|
||||||
|
void nextEl.offsetWidth; // reflow 강제
|
||||||
|
|
||||||
|
// 트랜지션 복원 후 동시 이동
|
||||||
|
nextEl.style.transition = '';
|
||||||
|
prevEl.style.transform = isForward ? 'translateX(-100%)' : 'translateX(100%)';
|
||||||
|
nextEl.style.transform = 'translateX(0)';
|
||||||
|
nextEl.classList.add('active');
|
||||||
|
prevEl.classList.remove('active');
|
||||||
|
|
||||||
|
setTimeout(() => { animating = false; }, DURATION);
|
||||||
|
updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextSlide() { goToSlide(current + 1); }
|
||||||
|
function prevSlide() { goToSlide(current - 1); }
|
||||||
|
|
||||||
|
/* ─── Sidebar ───────────────────────────────────────────────── */
|
||||||
|
const SB_WIDTH = 300;
|
||||||
|
function toggleSidebar() { document.body.classList.toggle('sb-open'); }
|
||||||
|
|
||||||
|
/* ─── SLIDE_META → 사이드바 + 도트 네비 생성 ──────────────── */
|
||||||
|
// ⚠️ idx 값이 DOM 순서와 반드시 일치해야 합니다 (0부터 시작)
|
||||||
|
// The End 슬라이드는 SLIDE_META에 포함하지 않습니다
|
||||||
|
const SLIDE_META = [
|
||||||
|
// { idx: 0, label: '제목 · 목차', dot: '🏠', color: '#475569', step: false },
|
||||||
|
// 여기에 슬라이드 메타데이터 추가
|
||||||
|
];
|
||||||
|
|
||||||
|
const sbList = document.getElementById('sb-list');
|
||||||
|
SLIDE_META.forEach(m => {
|
||||||
|
const el = document.createElement('div');
|
||||||
|
el.className = 'sb-item' + (m.step ? ' is-step' : '') + (m.idx === 0 ? ' active' : '');
|
||||||
|
el.dataset.idx = m.idx;
|
||||||
|
const dot = document.createElement('div');
|
||||||
|
dot.className = 'sb-dot';
|
||||||
|
dot.style.background = m.color + '22';
|
||||||
|
dot.style.color = m.color;
|
||||||
|
dot.textContent = m.dot;
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'sb-label';
|
||||||
|
label.textContent = m.label;
|
||||||
|
el.appendChild(dot);
|
||||||
|
el.appendChild(label);
|
||||||
|
el.addEventListener('click', () => goToSlide(+el.dataset.idx));
|
||||||
|
sbList.appendChild(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ─── 도트 네비 생성 ────────────────────────────────────────── */
|
||||||
|
const dotNav = document.getElementById('dot-nav');
|
||||||
|
slides.forEach((_, i) => {
|
||||||
|
const d = document.createElement('div');
|
||||||
|
d.className = 'dot' + (i === 0 ? ' active' : '');
|
||||||
|
d.addEventListener('click', () => goToSlide(i));
|
||||||
|
dotNav.appendChild(d);
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ─── UI 업데이트 ───────────────────────────────────────────── */
|
||||||
|
const counter = document.getElementById('slide-counter');
|
||||||
|
const progressFill = document.getElementById('progress-fill');
|
||||||
|
const btnPrev = document.getElementById('btn-prev');
|
||||||
|
const btnNext = document.getElementById('btn-next');
|
||||||
|
const dots = Array.from(dotNav.children);
|
||||||
|
|
||||||
|
function updateUI() {
|
||||||
|
counter.textContent = `${current + 1} / ${TOTAL}`;
|
||||||
|
progressFill.style.transform = `scaleX(${(current + 1) / TOTAL})`;
|
||||||
|
btnPrev.disabled = current === 0;
|
||||||
|
btnNext.disabled = current === TOTAL - 1;
|
||||||
|
dots.forEach((d, i) => d.classList.toggle('active', i === current));
|
||||||
|
document.querySelectorAll('.sb-item').forEach(el => {
|
||||||
|
el.classList.toggle('active', +el.dataset.idx === current);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── 키보드 단축키 ─────────────────────────────────────────── */
|
||||||
|
document.addEventListener('keydown', e => {
|
||||||
|
const modal = document.getElementById('help-modal');
|
||||||
|
const isOpen = modal.classList.contains('open');
|
||||||
|
switch (e.key) {
|
||||||
|
case 'ArrowRight': case 'PageDown': e.preventDefault(); nextSlide(); break;
|
||||||
|
case 'ArrowLeft': case 'PageUp': e.preventDefault(); prevSlide(); break;
|
||||||
|
case 'Home': e.preventDefault(); goToSlide(0); break;
|
||||||
|
case 'End': e.preventDefault(); goToSlide(TOTAL - 1); break;
|
||||||
|
case 't': case 'T': if (!isOpen) toggleSidebar(); break;
|
||||||
|
case 'Escape':
|
||||||
|
if (isOpen) modal.classList.remove('open');
|
||||||
|
else document.body.classList.remove('sb-open');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ─── 초기화 ────────────────────────────────────────────────── */
|
||||||
|
hljs.highlightAll();
|
||||||
|
updateUI();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 핵심 동작 원리
|
||||||
|
|
||||||
|
### 슬라이드 트랜지션
|
||||||
|
모든 슬라이드는 `position: absolute; inset: 0`으로 겹쳐 있고, `transform: translateX(±100%)`로 오프스크린에 숨겨집니다. `goToSlide()`는 `void el.offsetWidth`(reflow 강제)를 호출해 position 재설정 후 트랜지션을 시작하므로, 방향성 있는 슬라이드-인/아웃 애니메이션이 구현됩니다.
|
||||||
|
|
||||||
|
### 사이드바 Push
|
||||||
|
`body.sb-open` 클래스가 토글되면 `#deck`, `#nav`, `#progress-bar`가 CSS `left: 300px`으로 밀려납니다. 사이드바 자체는 `translateX(-300px) → translateX(0)` 전환입니다.
|
||||||
|
|
||||||
|
### SLIDE_META vs DOM 순서
|
||||||
|
- JS 엔진은 `querySelectorAll('.slide')` 결과의 **배열 인덱스**를 사용
|
||||||
|
- `SLIDE_META[i].idx`가 그 배열 인덱스와 일치해야 사이드바 클릭이 올바른 슬라이드로 이동
|
||||||
|
- 슬라이드 div의 `data-idx` 속성은 **시각적 레퍼런스용**일 뿐, JS가 읽지 않음
|
||||||
+224
@@ -0,0 +1,224 @@
|
|||||||
|
# 콘텐츠 레이아웃 패턴
|
||||||
|
|
||||||
|
`sc-body` 안에서 사용하는 레이아웃 패턴 모음입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 단일 코드 블록
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="file-row"><span class="file-pill">src/MyFile.cpp</span></div>
|
||||||
|
<p class="desc" style="margin-bottom:12px;">{{설명 텍스트}}</p>
|
||||||
|
<div class="code-wrap">
|
||||||
|
<div class="code-hdr">
|
||||||
|
<span class="code-fn">MyFile.cpp — functionName()</span>
|
||||||
|
<span class="code-lang">C++</span>
|
||||||
|
</div>
|
||||||
|
<pre><code class="language-cpp">// 코드 여기에
|
||||||
|
void MyFunction() {
|
||||||
|
// ...
|
||||||
|
}</code></pre>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**지원 언어**: `language-cpp`, `language-javascript`, `language-python`, `language-plaintext` 등 Highlight.js 지원 언어 모두 사용 가능.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 화면을 채우는 코드 블록 (sc-body-flex 필요)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- sc-body에 sc-body-flex 클래스 추가 필요 -->
|
||||||
|
<div class="file-row"><span class="file-pill">src/MyFile.cpp</span></div>
|
||||||
|
<div style="display:flex; flex-direction:column; flex:1; min-height:0; gap:10px; margin-top:8px;">
|
||||||
|
<div class="code-wrap" style="flex:1; display:flex; flex-direction:column; min-height:0;">
|
||||||
|
<div class="code-hdr">
|
||||||
|
<span class="code-fn">MyFile.cpp</span>
|
||||||
|
<span class="code-lang">C++</span>
|
||||||
|
</div>
|
||||||
|
<pre style="flex:1; overflow-y:auto; margin:0;">
|
||||||
|
<code class="language-cpp">// 긴 코드</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 2×2 스텝 그리드 (단계별 절차)
|
||||||
|
|
||||||
|
4단계 절차를 카드로 표현할 때 사용합니다. `sc-body-flex` 필요.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- sc-body에 sc-body-flex 클래스 추가 필요 -->
|
||||||
|
<p class="desc" style="margin-bottom:6px;">{{전체 설명}}</p>
|
||||||
|
<div style="display:grid; grid-template-columns:1fr 1fr; grid-template-rows:1fr 1fr; gap:12px; flex:1; min-height:0; margin-top:10px;">
|
||||||
|
|
||||||
|
<!-- 카드 ① -->
|
||||||
|
<div style="background:#F5F3FF; border-radius:10px; padding:14px 16px 12px; border-left:3px solid #7C3AED; display:flex; flex-direction:column; gap:8px; overflow:hidden;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; font-size:13px; font-weight:800; color:#3730A3; flex-shrink:0;">
|
||||||
|
<span style="background:#7C3AED; color:#fff; width:22px; height:22px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:11px;">①</span>
|
||||||
|
{{단계 제목}}
|
||||||
|
</div>
|
||||||
|
<pre style="background:rgba(67,56,202,.1); border-radius:6px; padding:8px 12px; flex:1; overflow:auto; font-family:'Consolas','Courier New',monospace; font-size:10.5px; color:#312E81; line-height:1.65; white-space:pre; margin:0;">{{코드 또는 내용}}</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 카드 ② ③ ④ — 동일 구조 반복 -->
|
||||||
|
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**카드 수 조정**: 3개는 `grid-template-columns: 1fr 1fr 1fr; grid-template-rows: 1fr;`, 6개는 `1fr 1fr 1fr / 1fr 1fr`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 2컬럼 레이아웃 (테이블 + 코드)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- sc-body에 sc-body-flex 클래스 추가 필요 -->
|
||||||
|
<div class="file-row"><span class="file-pill">config.ini</span></div>
|
||||||
|
<div style="display:flex; gap:18px; flex:1; min-height:0; margin-top:8px;">
|
||||||
|
|
||||||
|
<!-- 좌: 필드 설명 테이블 -->
|
||||||
|
<div style="flex:0 0 44%; display:flex; flex-direction:column; min-width:0;">
|
||||||
|
<div style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.1em; color:#EA580C; margin-bottom:7px;">📋 필드 구조</div>
|
||||||
|
<table class="ini-table compact">
|
||||||
|
<thead><tr><th>필드</th><th>설명</th><th>예시</th></tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr><td>Field1</td><td>설명</td><td>값</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 우: 코드 + callout -->
|
||||||
|
<div style="flex:1; display:flex; flex-direction:column; gap:10px; overflow-y:auto; min-width:0;">
|
||||||
|
<div style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.1em; color:#EA580C; margin-bottom:7px;">✏️ 등록 방법</div>
|
||||||
|
<div class="code-wrap" style="flex:1; min-height:0;">
|
||||||
|
<div class="code-hdr"><span class="code-fn">config.ini</span><span class="code-lang">INI</span></div>
|
||||||
|
<pre style="flex:1; overflow-y:auto;"><code class="language-plaintext">; 코드</code></pre>
|
||||||
|
</div>
|
||||||
|
<div class="callout note" style="flex-shrink:0;">
|
||||||
|
<span class="ci">💡</span>
|
||||||
|
<div><strong>팁 제목</strong>팁 내용</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Callout 변형
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- 정보 (파랑) -->
|
||||||
|
<div class="callout note">
|
||||||
|
<span class="ci">💡</span>
|
||||||
|
<div><strong>제목</strong>내용</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 주의 (노랑) -->
|
||||||
|
<div class="callout warn">
|
||||||
|
<span class="ci">⚠️</span>
|
||||||
|
<div><strong>제목</strong>내용</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 성공/팁 (초록) -->
|
||||||
|
<div class="callout tip">
|
||||||
|
<span class="ci">✅</span>
|
||||||
|
<div><strong>제목</strong>내용</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 위험 (빨강) -->
|
||||||
|
<div class="callout danger">
|
||||||
|
<span class="ci">🚨</span>
|
||||||
|
<div><strong>제목</strong>내용</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 체크리스트
|
||||||
|
|
||||||
|
검증 단계나 확인 사항 나열에 사용합니다.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p class="desc" style="margin-bottom:16px;">{{서두}}</p>
|
||||||
|
<div class="checklist">
|
||||||
|
<div class="check-item">
|
||||||
|
<div class="check-circle">①</div>
|
||||||
|
<div><strong>항목 제목</strong>상세 설명</div>
|
||||||
|
</div>
|
||||||
|
<div class="check-item">
|
||||||
|
<div class="check-circle">②</div>
|
||||||
|
<div><strong>항목 제목</strong>상세 설명</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Mermaid 다이어그램
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="mermaid">
|
||||||
|
flowchart LR
|
||||||
|
A[시작] --> B{조건}
|
||||||
|
B -->|Yes| C[처리]
|
||||||
|
B -->|No| D[종료]
|
||||||
|
C --> D
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**주의**: Mermaid는 `mermaid.initialize()`가 실행된 이후 DOM에 있어야 렌더링됩니다.
|
||||||
|
슬라이드가 처음부터 hidden 상태라면 첫 번째 슬라이드가 아닌 경우 렌더링이 지연될 수 있습니다.
|
||||||
|
해결: `mermaid.initialize({ startOnLoad: true })`가 보일러플레이트에 포함되어 있으므로 보통 정상 동작합니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 플로우 & 파일 목록 (2열 Overview)
|
||||||
|
|
||||||
|
전체 흐름과 관련 파일을 한 슬라이드에 나란히 보여줄 때 사용합니다.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!-- sc-body에 sc-body-flex 클래스 추가 필요 -->
|
||||||
|
<div style="display:flex; gap:28px; flex:1; min-height:0; margin-top:4px;">
|
||||||
|
|
||||||
|
<!-- 좌: 플로우 다이어그램 -->
|
||||||
|
<div style="flex:1; display:flex; flex-direction:column; min-width:0;">
|
||||||
|
<div style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.1em; color:#7C3AED; margin-bottom:12px;">🔄 전체 플로우</div>
|
||||||
|
<div class="mermaid" style="flex:1; overflow:auto;">
|
||||||
|
flowchart TD
|
||||||
|
A[단계 1] --> B[단계 2] --> C[단계 3]
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 우: 파일 목록 -->
|
||||||
|
<div style="flex:0 0 260px; display:flex; flex-direction:column; min-width:0;">
|
||||||
|
<div style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:.1em; color:#1D4ED8; margin-bottom:12px;">📁 수정 파일</div>
|
||||||
|
<!-- 파일 항목 반복 -->
|
||||||
|
<div style="display:flex; flex-direction:column; gap:8px; overflow-y:auto; flex:1;">
|
||||||
|
<div style="background:#F1F5F9; border-radius:8px; padding:10px 14px; border-left:3px solid #1D4ED8;">
|
||||||
|
<div style="font-family:monospace; font-size:11px; color:#1D4ED8; font-weight:700;">파일명.cpp</div>
|
||||||
|
<div style="font-size:12px; color:#475569; margin-top:3px;">파일 역할 설명</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 패턴 선택 가이드
|
||||||
|
|
||||||
|
| 콘텐츠 유형 | 권장 패턴 |
|
||||||
|
|------------|----------|
|
||||||
|
| 코드 설명 + 짧은 코드 | 패턴 1 (단일 코드 블록) |
|
||||||
|
| 긴 코드 전체 표시 | 패턴 2 (화면 채우는 코드) |
|
||||||
|
| 단계별 절차 (4단계) | 패턴 3 (2×2 스텝 그리드) |
|
||||||
|
| 설정 파일 필드 설명 | 패턴 4 (2컬럼 테이블+코드) |
|
||||||
|
| 주의사항/팁 강조 | 패턴 5 (Callout) |
|
||||||
|
| 체크리스트/검증 | 패턴 6 (체크리스트) |
|
||||||
|
| 전체 흐름 시각화 | 패턴 7 (Mermaid) |
|
||||||
|
| 흐름 + 파일 목록 | 패턴 8 (2열 Overview) |
|
||||||
Executable
+134
@@ -0,0 +1,134 @@
|
|||||||
|
# 슬라이드 타입별 HTML 템플릿
|
||||||
|
|
||||||
|
각 슬라이드 타입의 HTML 구조입니다. `<!-- SLIDES HERE -->` 위치에 삽입하세요.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 제목 + 목차 슬라이드 (slide-title)
|
||||||
|
|
||||||
|
프레젠테이션의 첫 슬라이드. 좌측 = 제목 정보, 우측 = 목차 그리드.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="slide slide-title active" data-idx="0">
|
||||||
|
<!-- 좌측: 제목 영역 -->
|
||||||
|
<div class="tc-left">
|
||||||
|
<div class="t-eyebrow">{{CATEGORY}} · {{SUBTITLE}}</div>
|
||||||
|
<div class="t-title">{{MAIN TITLE}}</div>
|
||||||
|
<div class="t-subtitle">{{DESCRIPTION}}</div>
|
||||||
|
<div class="t-meta">
|
||||||
|
<div class="t-meta-item">🖥 <strong>N 슬라이드</strong></div>
|
||||||
|
<div class="t-meta-item">⏱ <strong>약 X분</strong></div>
|
||||||
|
<div class="t-meta-item">📁 <strong>X개 파일</strong></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 우측: 목차 그리드 -->
|
||||||
|
<div class="tc-right">
|
||||||
|
<div class="toc-label">📋 목차</div>
|
||||||
|
<div class="toc-grid">
|
||||||
|
<div class="toc-item" onclick="goToSlide(N)">
|
||||||
|
<span class="toc-num">01</span>
|
||||||
|
<span class="toc-text">{{TOC ITEM 1}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="toc-item" onclick="goToSlide(N)">
|
||||||
|
<span class="toc-num">02</span>
|
||||||
|
<span class="toc-text">{{TOC ITEM 2}}</span>
|
||||||
|
</div>
|
||||||
|
<!-- 항목 추가 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**팁**: 목차 항목이 많을수록 각 행이 작아집니다. 5개 이하일 때 가장 보기 좋습니다. `goToSlide(N)`의 N은 해당 섹션의 첫 번째 슬라이드 DOM 인덱스입니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Step 헤더 슬라이드 (slide-step)
|
||||||
|
|
||||||
|
새로운 단계(Step)를 시작할 때 사용하는 전환 슬라이드. 강조색 그라디언트 배경.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="slide slide-step bg-s1" data-idx="N">
|
||||||
|
<!-- bg-s1~bg-s5 중 선택: s1=파랑, s2=보라, s3=핑크, s4=초록, s5=주황 -->
|
||||||
|
<div class="sh-big-num">1</div> <!-- 단계 번호 (장식용) -->
|
||||||
|
<span class="sh-eyebrow">Step 1 · {{CATEGORY}}</span>
|
||||||
|
<div class="sh-title">{{STEP TITLE}}</div>
|
||||||
|
<div class="sh-desc">{{STEP 설명 — 이 단계에서 무엇을 하는지 1~2문장}}</div>
|
||||||
|
<div class="sh-pills">
|
||||||
|
<span class="sh-pill">Task 1-1 · {{태스크명}}</span>
|
||||||
|
<span class="sh-pill">Task 1-2 · {{태스크명}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**배경색 선택 가이드**: 단계 순서에 따라 s1→s2→s3→s4→s5 순으로 사용하면 자연스러운 색상 흐름이 생깁니다.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 콘텐츠 슬라이드 (slide-content)
|
||||||
|
|
||||||
|
실제 내용이 담기는 슬라이드. 헤더 + 본문 영역으로 구성됩니다.
|
||||||
|
|
||||||
|
### 기본 구조
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="slide slide-content" data-idx="N">
|
||||||
|
<div class="sc-header">
|
||||||
|
<span class="task-badge b1">Task 1-1</span> <!-- b1~b5 중 해당 Step 색상과 일치시킬 것 -->
|
||||||
|
<h2>{{슬라이드 제목}}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="sc-body">
|
||||||
|
<!-- 여기에 레이아웃 패턴 삽입 (layout-patterns.md 참조) -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 콘텐츠가 많은 경우 (flex 확장)
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="slide slide-content" data-idx="N">
|
||||||
|
<div class="sc-header">
|
||||||
|
<span class="task-badge b2">Task 2-1</span>
|
||||||
|
<h2>{{슬라이드 제목}}</h2>
|
||||||
|
</div>
|
||||||
|
<div class="sc-body sc-body-flex" style="padding-top:16px;">
|
||||||
|
<!-- flex: 1로 높이를 최대한 활용하는 레이아웃 -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
**`sc-body-flex` 사용 시기**: 코드 블록, 다단 레이아웃, 테이블 등이 화면 높이를 최대한 채워야 할 때.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 엔딩 슬라이드 (slide-end)
|
||||||
|
|
||||||
|
프레젠테이션 마지막 슬라이드. SLIDE_META에 **포함하지 않습니다**.
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="slide slide-end" data-idx="N">
|
||||||
|
<div class="end-deco1"></div>
|
||||||
|
<div class="end-deco2"></div>
|
||||||
|
<div class="end-label">End of Presentation</div>
|
||||||
|
<div class="end-title">The End</div>
|
||||||
|
<div class="end-divider"></div>
|
||||||
|
<div class="end-subtitle">{{프레젠테이션 제목}}</div>
|
||||||
|
<div class="end-meta">{{부제 — 예: "X단계 · Y개 Task · Z개 파일 수정"}}</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 태스크 뱃지 색상 매핑
|
||||||
|
|
||||||
|
Step 헤더의 배경색과 Task 슬라이드의 뱃지 색상을 일치시키세요:
|
||||||
|
|
||||||
|
| Step 배경 | Task 뱃지 클래스 | 색상 |
|
||||||
|
|-----------|-----------------|------|
|
||||||
|
| `bg-s1` | `b1` | 파랑 (#1D4ED8) |
|
||||||
|
| `bg-s2` | `b2` | 보라 (#7C3AED) |
|
||||||
|
| `bg-s3` | `b3` | 핑크 (#DB2777) |
|
||||||
|
| `bg-s4` | `b4` | 초록 (#059669) |
|
||||||
|
| `bg-s5` | `b5` | 주황 (#EA580C) |
|
||||||
|
|
||||||
|
SLIDE_META의 `color`도 동일한 색상 코드를 사용하면 사이드바의 색상이 통일됩니다.
|
||||||
Reference in new issue
Block a user