claude-code-achievements 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +7 -0
- package/README.md +201 -0
- package/bin/config.js +285 -0
- package/bin/install.js +221 -0
- package/commands/achievements-settings.md +63 -0
- package/commands/achievements.md +93 -0
- package/data/achievements.json +168 -0
- package/data/i18n/en.json +94 -0
- package/data/i18n/ko.json +94 -0
- package/hooks/hooks.json +28 -0
- package/hooks/track-achievement.sh +178 -0
- package/hooks/track-stop.sh +44 -0
- package/package.json +38 -0
- package/scripts/init-state.sh +44 -0
- package/scripts/show-achievements.sh +152 -0
- package/scripts/show-notification.sh +145 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ui": {
|
|
3
|
+
"title": "CLAUDE CODE ACHIEVEMENTS",
|
|
4
|
+
"unlocked": "업적 달성!",
|
|
5
|
+
"locked": "잠김",
|
|
6
|
+
"progress": "진행 상황",
|
|
7
|
+
"rarity": "희귀도",
|
|
8
|
+
"tip": "팁",
|
|
9
|
+
"hint": "힌트"
|
|
10
|
+
},
|
|
11
|
+
"categories": {
|
|
12
|
+
"basics": { "name": "시작하기", "description": "Claude Code 첫걸음" },
|
|
13
|
+
"workflow": { "name": "워크플로우", "description": "개발 워크플로우 마스터" },
|
|
14
|
+
"tools": { "name": "파워 툴", "description": "고급 기능 활용" },
|
|
15
|
+
"mastery": { "name": "마스터리", "description": "전문가 수준" }
|
|
16
|
+
},
|
|
17
|
+
"achievements": {
|
|
18
|
+
"first_edit": {
|
|
19
|
+
"name": "첫 터치",
|
|
20
|
+
"description": "Claude와 함께 파일 편집하기",
|
|
21
|
+
"tip": "구체적으로 말하세요: '버그 고쳐줘' 대신 'login.js 42번 줄의 TypeError 수정해줘'"
|
|
22
|
+
},
|
|
23
|
+
"first_write": {
|
|
24
|
+
"name": "창조자",
|
|
25
|
+
"description": "새 파일 생성하기",
|
|
26
|
+
"tip": "Claude는 설명만으로 전체 파일을 만들 수 있어요. '로그인 폼 React 컴포넌트 만들어줘'"
|
|
27
|
+
},
|
|
28
|
+
"claude_md_creator": {
|
|
29
|
+
"name": "프로젝트 큐레이터",
|
|
30
|
+
"description": "CLAUDE.md로 프로젝트 컨텍스트 설정",
|
|
31
|
+
"tip": "CLAUDE.md는 프로젝트 이해를 돕습니다. 기술 스택, 코딩 스타일, 주요 파일, 자주 쓰는 명령어를 포함하세요."
|
|
32
|
+
},
|
|
33
|
+
"plan_mode_user": {
|
|
34
|
+
"name": "전략적 사고가",
|
|
35
|
+
"description": "복잡한 작업에 Plan 모드 사용",
|
|
36
|
+
"tip": "Shift+Tab 두 번으로 Plan 모드 진입. 아키텍처 결정이나 다단계 작업에 좋아요."
|
|
37
|
+
},
|
|
38
|
+
"git_commit": {
|
|
39
|
+
"name": "버전 관리자",
|
|
40
|
+
"description": "Claude와 함께 커밋하기",
|
|
41
|
+
"tip": "Claude가 커밋 메시지를 작성하게 하세요! conventional commits 형식으로 작성합니다."
|
|
42
|
+
},
|
|
43
|
+
"git_push": {
|
|
44
|
+
"name": "배포하기!",
|
|
45
|
+
"description": "원격 저장소에 푸시",
|
|
46
|
+
"tip": "푸시 전에 항상 변경 사항을 검토하세요. Claude에게 요약을 요청하세요."
|
|
47
|
+
},
|
|
48
|
+
"run_tests": {
|
|
49
|
+
"name": "품질 수호자",
|
|
50
|
+
"description": "Claude와 함께 테스트 실행",
|
|
51
|
+
"tip": "'테스트 실행하고 실패하면 고쳐줘'. 이게 바이브코딩 검증 루프입니다!"
|
|
52
|
+
},
|
|
53
|
+
"multi_agent": {
|
|
54
|
+
"name": "위임의 달인",
|
|
55
|
+
"description": "Task 도구로 서브 에이전트 생성",
|
|
56
|
+
"tip": "Task 도구는 복잡한 작업을 위한 전문 에이전트를 만듭니다."
|
|
57
|
+
},
|
|
58
|
+
"first_mcp": {
|
|
59
|
+
"name": "MCP 개척자",
|
|
60
|
+
"description": "MCP 도구 사용하기",
|
|
61
|
+
"tip": "MCP는 Claude 능력을 확장합니다. DB, API, 커스텀 도구에 연결하세요."
|
|
62
|
+
},
|
|
63
|
+
"web_searcher": {
|
|
64
|
+
"name": "웹 탐험가",
|
|
65
|
+
"description": "웹 검색하기",
|
|
66
|
+
"tip": "Claude가 최신 문서, 솔루션, API를 검색할 수 있어요."
|
|
67
|
+
},
|
|
68
|
+
"skill_invoker": {
|
|
69
|
+
"name": "스킬 마스터",
|
|
70
|
+
"description": "슬래시 명령 스킬 사용",
|
|
71
|
+
"tip": "/commit, /pr, /init 해보세요. ~/.claude/commands/에서 커스텀도 가능!"
|
|
72
|
+
},
|
|
73
|
+
"notebook_editor": {
|
|
74
|
+
"name": "데이터 과학자",
|
|
75
|
+
"description": "Jupyter 노트북 편집",
|
|
76
|
+
"tip": "Claude는 노트북을 이해해요! 데이터 분석, 시각화, ML 실험을 요청하세요."
|
|
77
|
+
},
|
|
78
|
+
"config_modifier": {
|
|
79
|
+
"name": "커스터마이저",
|
|
80
|
+
"description": "Claude Code 설정 수정",
|
|
81
|
+
"tip": "/config 또는 ~/.claude/settings.json을 편집하세요."
|
|
82
|
+
},
|
|
83
|
+
"hooks_user": {
|
|
84
|
+
"name": "자동화 아키텍트",
|
|
85
|
+
"description": "Claude Code 훅 설정",
|
|
86
|
+
"tip": "훅은 이벤트에 스크립트를 실행해요. 린팅, 테스트, 커스텀 워크플로우에 활용!"
|
|
87
|
+
},
|
|
88
|
+
"ralph_starter": {
|
|
89
|
+
"name": "루프 마스터",
|
|
90
|
+
"description": "자율 코딩 루프 시작",
|
|
91
|
+
"tip": "Ralph Loop는 목표 달성까지 Claude가 자율적으로 작업합니다."
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
package/hooks/hooks.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"matcher": ".*",
|
|
6
|
+
"hooks": [
|
|
7
|
+
{
|
|
8
|
+
"type": "command",
|
|
9
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/track-achievement.sh",
|
|
10
|
+
"timeout": 5
|
|
11
|
+
}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"Stop": [
|
|
16
|
+
{
|
|
17
|
+
"matcher": ".*",
|
|
18
|
+
"hooks": [
|
|
19
|
+
{
|
|
20
|
+
"type": "command",
|
|
21
|
+
"command": "${CLAUDE_PLUGIN_ROOT}/hooks/track-stop.sh",
|
|
22
|
+
"timeout": 5
|
|
23
|
+
}
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# track-achievement.sh - Main achievement tracking logic for PostToolUse hook
|
|
3
|
+
# Receives JSON via stdin with tool_name, tool_input, and permission_mode
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
|
|
8
|
+
STATE_DIR="${HOME}/.claude/achievements"
|
|
9
|
+
STATE_FILE="${STATE_DIR}/state.json"
|
|
10
|
+
ACHIEVEMENTS_FILE="${PLUGIN_ROOT}/data/achievements.json"
|
|
11
|
+
|
|
12
|
+
# Initialize state file if it doesn't exist
|
|
13
|
+
init_state() {
|
|
14
|
+
mkdir -p "${STATE_DIR}"
|
|
15
|
+
if [[ ! -f "${STATE_FILE}" ]]; then
|
|
16
|
+
cat > "${STATE_FILE}" << 'EOF'
|
|
17
|
+
{
|
|
18
|
+
"settings": { "language": "en", "notifications": true, "notification_style": "system" },
|
|
19
|
+
"achievements": {},
|
|
20
|
+
"counters": { "ralph_iterations": 0 },
|
|
21
|
+
"session": { "files_read_set": [] }
|
|
22
|
+
}
|
|
23
|
+
EOF
|
|
24
|
+
fi
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Read input from stdin
|
|
28
|
+
read_input() {
|
|
29
|
+
INPUT=$(cat)
|
|
30
|
+
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
|
|
31
|
+
TOOL_INPUT=$(echo "$INPUT" | jq -r '.tool_input // {}')
|
|
32
|
+
PERMISSION_MODE=$(echo "$INPUT" | jq -r '.permission_mode // "default"')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
# Check if achievement is already unlocked
|
|
36
|
+
is_unlocked() {
|
|
37
|
+
local achievement_id="$1"
|
|
38
|
+
jq -e ".achievements[\"${achievement_id}\"].unlocked == true" "${STATE_FILE}" > /dev/null 2>&1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Unlock an achievement
|
|
42
|
+
unlock_achievement() {
|
|
43
|
+
local achievement_id="$1"
|
|
44
|
+
local timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
45
|
+
local temp_file=$(mktemp)
|
|
46
|
+
jq ".achievements[\"${achievement_id}\"] = {\"unlocked\": true, \"unlockedAt\": \"${timestamp}\"}" "${STATE_FILE}" > "${temp_file}"
|
|
47
|
+
mv "${temp_file}" "${STATE_FILE}"
|
|
48
|
+
"${PLUGIN_ROOT}/scripts/show-notification.sh" "${achievement_id}"
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
# Check and unlock achievements based on tool use
|
|
52
|
+
check_achievements() {
|
|
53
|
+
# Plan mode achievement
|
|
54
|
+
if [[ "${PERMISSION_MODE}" == "plan" ]]; then
|
|
55
|
+
if ! is_unlocked "plan_mode_user"; then
|
|
56
|
+
unlock_achievement "plan_mode_user"
|
|
57
|
+
fi
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
case "${TOOL_NAME}" in
|
|
61
|
+
Write)
|
|
62
|
+
# first_write
|
|
63
|
+
if ! is_unlocked "first_write"; then
|
|
64
|
+
unlock_achievement "first_write"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
FILE_PATH=$(echo "$TOOL_INPUT" | jq -r '.file_path // empty')
|
|
68
|
+
|
|
69
|
+
# claude_md_creator
|
|
70
|
+
if [[ "${FILE_PATH}" =~ CLAUDE\.md$ ]]; then
|
|
71
|
+
if ! is_unlocked "claude_md_creator"; then
|
|
72
|
+
unlock_achievement "claude_md_creator"
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# config_modifier
|
|
77
|
+
if [[ "${FILE_PATH}" =~ \.claude/settings.*\.json$ ]]; then
|
|
78
|
+
if ! is_unlocked "config_modifier"; then
|
|
79
|
+
unlock_achievement "config_modifier"
|
|
80
|
+
fi
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
# hooks_user (hooks in settings or hooks.json)
|
|
84
|
+
if [[ "${FILE_PATH}" =~ hooks\.json$ ]] || [[ "${FILE_PATH}" =~ \.claude/settings.*\.json$ ]]; then
|
|
85
|
+
# Check if file contains hooks configuration
|
|
86
|
+
if [[ -f "${FILE_PATH}" ]]; then
|
|
87
|
+
if grep -q '"hooks"' "${FILE_PATH}" 2>/dev/null || grep -q '"PostToolUse"' "${FILE_PATH}" 2>/dev/null; then
|
|
88
|
+
if ! is_unlocked "hooks_user"; then
|
|
89
|
+
unlock_achievement "hooks_user"
|
|
90
|
+
fi
|
|
91
|
+
fi
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
;;
|
|
95
|
+
|
|
96
|
+
Edit)
|
|
97
|
+
# first_edit
|
|
98
|
+
if ! is_unlocked "first_edit"; then
|
|
99
|
+
unlock_achievement "first_edit"
|
|
100
|
+
fi
|
|
101
|
+
;;
|
|
102
|
+
|
|
103
|
+
Bash)
|
|
104
|
+
COMMAND=$(echo "$TOOL_INPUT" | jq -r '.command // empty')
|
|
105
|
+
|
|
106
|
+
# git_commit (git add/commit)
|
|
107
|
+
if [[ "${COMMAND}" =~ git[[:space:]]+(add|commit) ]]; then
|
|
108
|
+
if ! is_unlocked "git_commit"; then
|
|
109
|
+
unlock_achievement "git_commit"
|
|
110
|
+
fi
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# git_push
|
|
114
|
+
if [[ "${COMMAND}" =~ git[[:space:]]+push ]]; then
|
|
115
|
+
if ! is_unlocked "git_push"; then
|
|
116
|
+
unlock_achievement "git_push"
|
|
117
|
+
fi
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# run_tests
|
|
121
|
+
if [[ "${COMMAND}" =~ (npm|yarn|pnpm)[[:space:]]+(test|vitest|jest) ]] || \
|
|
122
|
+
[[ "${COMMAND}" =~ pytest ]] || \
|
|
123
|
+
[[ "${COMMAND}" =~ go[[:space:]]+test ]] || \
|
|
124
|
+
[[ "${COMMAND}" =~ cargo[[:space:]]+test ]] || \
|
|
125
|
+
[[ "${COMMAND}" =~ bun[[:space:]]+test ]]; then
|
|
126
|
+
if ! is_unlocked "run_tests"; then
|
|
127
|
+
unlock_achievement "run_tests"
|
|
128
|
+
fi
|
|
129
|
+
fi
|
|
130
|
+
;;
|
|
131
|
+
|
|
132
|
+
Task)
|
|
133
|
+
if ! is_unlocked "multi_agent"; then
|
|
134
|
+
unlock_achievement "multi_agent"
|
|
135
|
+
fi
|
|
136
|
+
;;
|
|
137
|
+
|
|
138
|
+
Skill)
|
|
139
|
+
if ! is_unlocked "skill_invoker"; then
|
|
140
|
+
unlock_achievement "skill_invoker"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
# ralph_starter
|
|
144
|
+
SKILL_NAME=$(echo "$TOOL_INPUT" | jq -r '.skill // empty')
|
|
145
|
+
if [[ "${SKILL_NAME}" =~ ralph-loop|ralph ]]; then
|
|
146
|
+
if ! is_unlocked "ralph_starter"; then
|
|
147
|
+
unlock_achievement "ralph_starter"
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
;;
|
|
151
|
+
|
|
152
|
+
WebSearch)
|
|
153
|
+
if ! is_unlocked "web_searcher"; then
|
|
154
|
+
unlock_achievement "web_searcher"
|
|
155
|
+
fi
|
|
156
|
+
;;
|
|
157
|
+
|
|
158
|
+
NotebookEdit)
|
|
159
|
+
if ! is_unlocked "notebook_editor"; then
|
|
160
|
+
unlock_achievement "notebook_editor"
|
|
161
|
+
fi
|
|
162
|
+
;;
|
|
163
|
+
|
|
164
|
+
mcp__*)
|
|
165
|
+
# Any MCP tool
|
|
166
|
+
if ! is_unlocked "first_mcp"; then
|
|
167
|
+
unlock_achievement "first_mcp"
|
|
168
|
+
fi
|
|
169
|
+
;;
|
|
170
|
+
esac
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
# Main
|
|
174
|
+
init_state
|
|
175
|
+
read_input
|
|
176
|
+
check_achievements
|
|
177
|
+
|
|
178
|
+
exit 0
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# track-stop.sh - Track Stop hook for ralph-loop iterations
|
|
3
|
+
# Called when a conversation ends or is interrupted
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
|
|
8
|
+
STATE_DIR="${HOME}/.claude/achievements"
|
|
9
|
+
STATE_FILE="${STATE_DIR}/state.json"
|
|
10
|
+
|
|
11
|
+
# Check if state file exists
|
|
12
|
+
if [[ ! -f "${STATE_FILE}" ]]; then
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Read input from stdin
|
|
17
|
+
INPUT=$(cat)
|
|
18
|
+
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
|
|
19
|
+
|
|
20
|
+
# Increment ralph iterations if ralph-loop is active
|
|
21
|
+
# This is detected by checking if the session has ralph_loop_active flag
|
|
22
|
+
RALPH_ACTIVE=$(jq -r '.session.ralph_loop_active // false' "${STATE_FILE}")
|
|
23
|
+
|
|
24
|
+
if [[ "${RALPH_ACTIVE}" == "true" ]]; then
|
|
25
|
+
# Increment counter
|
|
26
|
+
temp_file=$(mktemp)
|
|
27
|
+
jq '.counters.ralph_iterations = (.counters.ralph_iterations // 0) + 1' "${STATE_FILE}" > "${temp_file}"
|
|
28
|
+
mv "${temp_file}" "${STATE_FILE}"
|
|
29
|
+
|
|
30
|
+
# Check if threshold reached
|
|
31
|
+
RALPH_COUNT=$(jq -r '.counters.ralph_iterations' "${STATE_FILE}")
|
|
32
|
+
if [[ "${RALPH_COUNT}" -ge 100 ]]; then
|
|
33
|
+
# Check if already unlocked
|
|
34
|
+
if ! jq -e '.achievements["ralph_master"].unlocked == true' "${STATE_FILE}" > /dev/null 2>&1; then
|
|
35
|
+
"${PLUGIN_ROOT}/scripts/show-notification.sh" "ralph_master"
|
|
36
|
+
temp_file=$(mktemp)
|
|
37
|
+
timestamp=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
38
|
+
jq ".achievements[\"ralph_master\"] = {\"unlocked\": true, \"unlockedAt\": \"${timestamp}\"}" "${STATE_FILE}" > "${temp_file}"
|
|
39
|
+
mv "${temp_file}" "${STATE_FILE}"
|
|
40
|
+
fi
|
|
41
|
+
fi
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
exit 0
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-code-achievements",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Steam-style achievement system for Claude Code - gamify your coding journey!",
|
|
5
|
+
"author": "subinium",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/subinium/claude-code-achievements.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/subinium/claude-code-achievements#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/subinium/claude-code-achievements/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"claude",
|
|
17
|
+
"claude-code",
|
|
18
|
+
"achievements",
|
|
19
|
+
"gamification",
|
|
20
|
+
"vibecoding",
|
|
21
|
+
"plugin",
|
|
22
|
+
"cli"
|
|
23
|
+
],
|
|
24
|
+
"bin": {
|
|
25
|
+
"claude-code-achievements": "bin/install.js"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
".claude-plugin",
|
|
29
|
+
"commands/",
|
|
30
|
+
"hooks/",
|
|
31
|
+
"scripts/",
|
|
32
|
+
"data/",
|
|
33
|
+
"bin/"
|
|
34
|
+
],
|
|
35
|
+
"engines": {
|
|
36
|
+
"node": ">=14.0.0"
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# init-state.sh - Initialize or reset the achievements state file
|
|
3
|
+
# Usage: init-state.sh [--reset]
|
|
4
|
+
|
|
5
|
+
set -e
|
|
6
|
+
|
|
7
|
+
STATE_DIR="${HOME}/.claude/achievements"
|
|
8
|
+
STATE_FILE="${STATE_DIR}/state.json"
|
|
9
|
+
|
|
10
|
+
# Check for reset flag
|
|
11
|
+
if [[ "$1" == "--reset" ]]; then
|
|
12
|
+
rm -f "${STATE_FILE}"
|
|
13
|
+
echo "State file reset."
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
# Create directory if needed
|
|
17
|
+
mkdir -p "${STATE_DIR}"
|
|
18
|
+
|
|
19
|
+
# Create initial state if doesn't exist
|
|
20
|
+
if [[ ! -f "${STATE_FILE}" ]]; then
|
|
21
|
+
cat > "${STATE_FILE}" << 'EOF'
|
|
22
|
+
{
|
|
23
|
+
"settings": {
|
|
24
|
+
"language": "en",
|
|
25
|
+
"notifications": true
|
|
26
|
+
},
|
|
27
|
+
"achievements": {},
|
|
28
|
+
"counters": {
|
|
29
|
+
"ralph_iterations": 0,
|
|
30
|
+
"files_read": 0,
|
|
31
|
+
"edits_made": 0
|
|
32
|
+
},
|
|
33
|
+
"session": {
|
|
34
|
+
"files_read_set": [],
|
|
35
|
+
"ralph_loop_active": false
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
EOF
|
|
39
|
+
echo "State file initialized at: ${STATE_FILE}"
|
|
40
|
+
else
|
|
41
|
+
echo "State file already exists at: ${STATE_FILE}"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
exit 0
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# show-achievements.sh - Display achievements in a clean TUI-style view
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(dirname "$(dirname "$0")")}"
|
|
7
|
+
STATE_FILE="${HOME}/.claude/achievements/state.json"
|
|
8
|
+
ACHIEVEMENTS_FILE="${PLUGIN_ROOT}/data/achievements.json"
|
|
9
|
+
|
|
10
|
+
# Initialize state if needed
|
|
11
|
+
if [[ ! -f "${STATE_FILE}" ]]; then
|
|
12
|
+
mkdir -p "$(dirname "${STATE_FILE}")"
|
|
13
|
+
cat > "${STATE_FILE}" << 'EOF'
|
|
14
|
+
{
|
|
15
|
+
"settings": { "language": "en", "notifications": true, "notification_style": "system" },
|
|
16
|
+
"achievements": {},
|
|
17
|
+
"counters": { "ralph_iterations": 0 },
|
|
18
|
+
"session": { "files_read_set": [] }
|
|
19
|
+
}
|
|
20
|
+
EOF
|
|
21
|
+
fi
|
|
22
|
+
|
|
23
|
+
# Get user's language preference
|
|
24
|
+
LANG=$(jq -r '.settings.language // "en"' "${STATE_FILE}")
|
|
25
|
+
I18N_FILE="${PLUGIN_ROOT}/data/i18n/${LANG}.json"
|
|
26
|
+
|
|
27
|
+
# Function to get localized text
|
|
28
|
+
get_localized() {
|
|
29
|
+
local key="$1"
|
|
30
|
+
local fallback="$2"
|
|
31
|
+
local result=""
|
|
32
|
+
if [[ -f "${I18N_FILE}" ]]; then
|
|
33
|
+
result=$(jq -r "${key} // empty" "${I18N_FILE}" 2>/dev/null)
|
|
34
|
+
fi
|
|
35
|
+
if [[ -z "${result}" || "${result}" == "null" ]]; then
|
|
36
|
+
echo "${fallback}"
|
|
37
|
+
else
|
|
38
|
+
echo "${result}"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get_achievement_name() {
|
|
43
|
+
local id="$1"
|
|
44
|
+
get_localized ".achievements[\"${id}\"].name" "$(jq -r ".achievements[\"${id}\"].name" "${ACHIEVEMENTS_FILE}")"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
get_achievement_desc() {
|
|
48
|
+
local id="$1"
|
|
49
|
+
get_localized ".achievements[\"${id}\"].description" "$(jq -r ".achievements[\"${id}\"].description" "${ACHIEVEMENTS_FILE}")"
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Get rarity emoji
|
|
53
|
+
get_rarity_emoji() {
|
|
54
|
+
case "$1" in
|
|
55
|
+
common) echo "⬜" ;;
|
|
56
|
+
uncommon) echo "🟩" ;;
|
|
57
|
+
rare) echo "🟦" ;;
|
|
58
|
+
epic) echo "🟪" ;;
|
|
59
|
+
legendary) echo "🟨" ;;
|
|
60
|
+
*) echo "⬜" ;;
|
|
61
|
+
esac
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Calculate stats
|
|
65
|
+
print_header() {
|
|
66
|
+
local total=$(jq '.achievements | length' "${ACHIEVEMENTS_FILE}")
|
|
67
|
+
local unlocked=$(jq '.achievements | to_entries | map(select(.value.unlocked == true)) | length' "${STATE_FILE}")
|
|
68
|
+
local percent=0
|
|
69
|
+
[[ ${total} -gt 0 ]] && percent=$((unlocked * 100 / total))
|
|
70
|
+
|
|
71
|
+
echo ""
|
|
72
|
+
echo "🎮 CLAUDE CODE ACHIEVEMENTS"
|
|
73
|
+
echo "┌────────────────────────────────────────────────────────┐"
|
|
74
|
+
echo "│"
|
|
75
|
+
|
|
76
|
+
# Progress bar
|
|
77
|
+
local filled=$((percent / 5))
|
|
78
|
+
local empty=$((20 - filled))
|
|
79
|
+
printf "│ "
|
|
80
|
+
for ((i=0; i<filled; i++)); do printf "█"; done
|
|
81
|
+
for ((i=0; i<empty; i++)); do printf "░"; done
|
|
82
|
+
printf " %d/%d unlocked (%d%%)\n" "${unlocked}" "${total}" "${percent}"
|
|
83
|
+
echo "│"
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Show achievements for a category
|
|
87
|
+
show_category() {
|
|
88
|
+
local category="$1"
|
|
89
|
+
local category_name=$(jq -r ".categories[\"${category}\"].name // \"${category}\"" "${ACHIEVEMENTS_FILE}")
|
|
90
|
+
local localized_name=$(get_localized ".categories[\"${category}\"].name" "${category_name}")
|
|
91
|
+
|
|
92
|
+
echo "├── ${localized_name} ─────────────────────────────────────"
|
|
93
|
+
echo "│"
|
|
94
|
+
|
|
95
|
+
# Get achievements in this category
|
|
96
|
+
local ids=$(jq -r ".achievements | to_entries | map(select(.value.category == \"${category}\")) | .[].key" "${ACHIEVEMENTS_FILE}")
|
|
97
|
+
|
|
98
|
+
for id in ${ids}; do
|
|
99
|
+
local icon=$(jq -r ".achievements[\"${id}\"].icon // \"🏆\"" "${ACHIEVEMENTS_FILE}")
|
|
100
|
+
local rarity=$(jq -r ".achievements[\"${id}\"].rarity // \"common\"" "${ACHIEVEMENTS_FILE}")
|
|
101
|
+
local rarity_emoji=$(get_rarity_emoji "${rarity}")
|
|
102
|
+
local name=$(get_achievement_name "${id}")
|
|
103
|
+
local desc=$(get_achievement_desc "${id}")
|
|
104
|
+
|
|
105
|
+
local unlocked="false"
|
|
106
|
+
if jq -e ".achievements[\"${id}\"].unlocked == true" "${STATE_FILE}" > /dev/null 2>&1; then
|
|
107
|
+
unlocked="true"
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
if [[ "${unlocked}" == "true" ]]; then
|
|
111
|
+
echo "│ ✅ ${icon} ${name} ${rarity_emoji}"
|
|
112
|
+
else
|
|
113
|
+
echo "│ ⬛ ${icon} ${name} ${rarity_emoji}"
|
|
114
|
+
echo "│ ↳ ${desc}"
|
|
115
|
+
fi
|
|
116
|
+
done
|
|
117
|
+
echo "│"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# Show rarity legend
|
|
121
|
+
show_legend() {
|
|
122
|
+
echo "├────────────────────────────────────────────────────────┤"
|
|
123
|
+
echo "│ ⬜ Common 🟩 Uncommon 🟦 Rare 🟪 Epic 🟨 Legendary"
|
|
124
|
+
echo "└────────────────────────────────────────────────────────┘"
|
|
125
|
+
echo ""
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
# Main
|
|
129
|
+
ARG="${1:-all}"
|
|
130
|
+
|
|
131
|
+
print_header
|
|
132
|
+
|
|
133
|
+
case "${ARG}" in
|
|
134
|
+
stats)
|
|
135
|
+
show_legend
|
|
136
|
+
;;
|
|
137
|
+
all)
|
|
138
|
+
for category in $(jq -r '.categories | to_entries | sort_by(.value.order) | .[].key' "${ACHIEVEMENTS_FILE}"); do
|
|
139
|
+
show_category "${category}"
|
|
140
|
+
done
|
|
141
|
+
show_legend
|
|
142
|
+
;;
|
|
143
|
+
*)
|
|
144
|
+
if jq -e ".categories[\"${ARG}\"]" "${ACHIEVEMENTS_FILE}" > /dev/null 2>&1; then
|
|
145
|
+
show_category "${ARG}"
|
|
146
|
+
show_legend
|
|
147
|
+
else
|
|
148
|
+
echo "Unknown: ${ARG}"
|
|
149
|
+
echo "Available: basics, workflow, tools, mastery"
|
|
150
|
+
fi
|
|
151
|
+
;;
|
|
152
|
+
esac
|