@juho0719/cckit 0.2.3 → 0.2.4
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/assets/hooks/agent-track.sh +53 -0
- package/assets/hooks/auto-commit-push.sh +123 -0
- package/assets/hooks/on-prompt-start.sh +6 -0
- package/assets/hooks/skill-track.sh +26 -0
- package/assets/hooks/subagent-notify.sh +20 -0
- package/assets/statusline/statusline.sh +186 -0
- package/dist/{chunk-OLLOS3GG.js → chunk-E3INXQNO.js} +38 -8
- package/dist/{chunk-SLVASXTF.js → chunk-W7RWPDBH.js} +13 -3
- package/dist/{cli-6WQMAFNA.js → cli-KZYBSIXO.js} +43 -14
- package/dist/{hooks-S73JTX2I.js → hooks-A2WQ2LGG.js} +1 -1
- package/dist/index.js +10 -10
- package/dist/{registry-BU55RMHU.js → registry-KRLOB4TH.js} +1 -1
- package/dist/{uninstall-cli-7XGNDIUC.js → uninstall-cli-GLYJG5V2.js} +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Agent 툴 실행 추적 — 여러 에이전트 동시 지원
|
|
3
|
+
# @hook-event PreToolUse|Agent|pre
|
|
4
|
+
# @hook-event PostToolUse|Agent|post
|
|
5
|
+
|
|
6
|
+
AGENT_STATE_DIR="/tmp/claude-agents"
|
|
7
|
+
mkdir -p "$AGENT_STATE_DIR"
|
|
8
|
+
|
|
9
|
+
EVENT="${1:-post}"
|
|
10
|
+
INPUT=$(cat)
|
|
11
|
+
|
|
12
|
+
# subagent_type + description으로 고유 파일명 생성
|
|
13
|
+
AGENT_ID=$(echo "$INPUT" | python3 -c "
|
|
14
|
+
import sys, json, hashlib
|
|
15
|
+
try:
|
|
16
|
+
d = json.load(sys.stdin)
|
|
17
|
+
ti = d.get('tool_input', {})
|
|
18
|
+
key = (ti.get('subagent_type', '') or '') + '|' + (ti.get('description', '') or '')
|
|
19
|
+
print(hashlib.md5(key.encode()).hexdigest()[:12])
|
|
20
|
+
except:
|
|
21
|
+
print('')
|
|
22
|
+
" 2>/dev/null)
|
|
23
|
+
|
|
24
|
+
if [ -z "$AGENT_ID" ]; then
|
|
25
|
+
exit 0
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
if [ "$EVENT" = "pre" ]; then
|
|
29
|
+
SUBAGENT_TYPE=$(echo "$INPUT" | python3 -c "
|
|
30
|
+
import sys, json
|
|
31
|
+
try:
|
|
32
|
+
d = json.load(sys.stdin)
|
|
33
|
+
print(d.get('tool_input', {}).get('subagent_type', '') or '')
|
|
34
|
+
except:
|
|
35
|
+
print('')
|
|
36
|
+
" 2>/dev/null)
|
|
37
|
+
|
|
38
|
+
DESCRIPTION=$(echo "$INPUT" | python3 -c "
|
|
39
|
+
import sys, json
|
|
40
|
+
try:
|
|
41
|
+
d = json.load(sys.stdin)
|
|
42
|
+
desc = d.get('tool_input', {}).get('description', '') or ''
|
|
43
|
+
print(desc[:50] if len(desc) > 50 else desc)
|
|
44
|
+
except:
|
|
45
|
+
print('')
|
|
46
|
+
" 2>/dev/null)
|
|
47
|
+
|
|
48
|
+
if [ -n "$SUBAGENT_TYPE" ]; then
|
|
49
|
+
echo "${SUBAGENT_TYPE}|${DESCRIPTION}" > "${AGENT_STATE_DIR}/${AGENT_ID}"
|
|
50
|
+
fi
|
|
51
|
+
else
|
|
52
|
+
rm -f "${AGENT_STATE_DIR}/${AGENT_ID}"
|
|
53
|
+
fi
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Auto commit & push hook - runs when Claude Code finishes a response
|
|
3
|
+
# @hook-event Stop||
|
|
4
|
+
# Compatible with bash 3.2 (macOS default)
|
|
5
|
+
|
|
6
|
+
# Get the working directory from the hook input
|
|
7
|
+
WORK_DIR=$(echo "$CLAUDE_HOOK_INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
8
|
+
if [ -z "$WORK_DIR" ]; then
|
|
9
|
+
WORK_DIR="$(pwd)"
|
|
10
|
+
fi
|
|
11
|
+
|
|
12
|
+
cd "$WORK_DIR" 2>/dev/null || exit 0
|
|
13
|
+
|
|
14
|
+
# Check if we're in a git repo
|
|
15
|
+
if ! git rev-parse --is-inside-work-tree &>/dev/null; then
|
|
16
|
+
exit 0
|
|
17
|
+
fi
|
|
18
|
+
|
|
19
|
+
# Check if there are any changes
|
|
20
|
+
if git diff --quiet && git diff --cached --quiet && [ -z "$(git ls-files --others --exclude-standard)" ]; then
|
|
21
|
+
exit 0
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null)
|
|
25
|
+
if [ -z "$BRANCH" ]; then
|
|
26
|
+
exit 0
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
# Stage all changes
|
|
30
|
+
git add -A
|
|
31
|
+
|
|
32
|
+
# ── 변경 파일 목록 수집 ───────────────────────────────
|
|
33
|
+
ADDED_FILES=$(git diff --cached --diff-filter=A --name-only)
|
|
34
|
+
MODIFIED_FILES=$(git diff --cached --diff-filter=M --name-only)
|
|
35
|
+
DELETED_FILES=$(git diff --cached --diff-filter=D --name-only)
|
|
36
|
+
|
|
37
|
+
ALL_FILES=$(git diff --cached --name-only)
|
|
38
|
+
ADDED_COUNT=$(echo "$ADDED_FILES" | grep -c . || true)
|
|
39
|
+
MODIFIED_COUNT=$(echo "$MODIFIED_FILES" | grep -c . || true)
|
|
40
|
+
DELETED_COUNT=$(echo "$DELETED_FILES" | grep -c . || true)
|
|
41
|
+
|
|
42
|
+
# ── 커밋 타입 감지 ────────────────────────────────────
|
|
43
|
+
# test: 파일이 모두 테스트/설정 관련이면 test/chore
|
|
44
|
+
HAS_TEST=$(echo "$ALL_FILES" | grep -E "(\.test\.|\.spec\.|__tests__|e2e/)" | head -1)
|
|
45
|
+
HAS_CONFIG=$(echo "$ALL_FILES" | grep -E "\.(config|json|yaml|yml|env|toml)$" | head -1)
|
|
46
|
+
HAS_ROADMAP=$(echo "$ALL_FILES" | grep -E "roadmap|ROADMAP|\.md$" | head -1)
|
|
47
|
+
HAS_SRC=$(echo "$ALL_FILES" | grep -E "^src/" | head -1)
|
|
48
|
+
|
|
49
|
+
if [ -n "$HAS_TEST" ] && [ -z "$HAS_SRC" ]; then
|
|
50
|
+
COMMIT_TYPE="test"
|
|
51
|
+
elif [ -n "$HAS_CONFIG" ] && [ -z "$HAS_SRC" ]; then
|
|
52
|
+
COMMIT_TYPE="chore"
|
|
53
|
+
elif [ -n "$HAS_ROADMAP" ] && [ -z "$HAS_SRC" ]; then
|
|
54
|
+
COMMIT_TYPE="docs"
|
|
55
|
+
else
|
|
56
|
+
COMMIT_TYPE="feat"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# ── 스코프: 가장 많이 변경된 2단계 디렉토리 ────────────
|
|
60
|
+
SCOPE=$(echo "$ALL_FILES" \
|
|
61
|
+
| awk -F'/' 'NF>=2 {print $1"/"$2} NF==1 {print $1}' \
|
|
62
|
+
| sort | uniq -c | sort -rn \
|
|
63
|
+
| awk 'NR==1{print $2}')
|
|
64
|
+
[ -z "$SCOPE" ] && SCOPE="root"
|
|
65
|
+
|
|
66
|
+
# ── Subject: 주요 파일명 나열 (확장자 제거, 최대 3개) ───
|
|
67
|
+
key_files() {
|
|
68
|
+
echo "$1" | while IFS= read -r f; do
|
|
69
|
+
[ -n "$f" ] && basename "$f" | sed 's/\.[^.]*$//'
|
|
70
|
+
done | head -3 | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
ADDED_NAMES=$(key_files "$ADDED_FILES")
|
|
74
|
+
MODIFIED_NAMES=$(key_files "$MODIFIED_FILES")
|
|
75
|
+
DELETED_NAMES=$(key_files "$DELETED_FILES")
|
|
76
|
+
|
|
77
|
+
SUBJECT_PARTS=""
|
|
78
|
+
[ -n "$ADDED_NAMES" ] && SUBJECT_PARTS="${SUBJECT_PARTS}add ${ADDED_NAMES}"
|
|
79
|
+
[ -n "$MODIFIED_NAMES" ] && SUBJECT_PARTS="${SUBJECT_PARTS:+$SUBJECT_PARTS; }update ${MODIFIED_NAMES}"
|
|
80
|
+
[ -n "$DELETED_NAMES" ] && SUBJECT_PARTS="${SUBJECT_PARTS:+$SUBJECT_PARTS; }remove ${DELETED_NAMES}"
|
|
81
|
+
|
|
82
|
+
# 파일이 4개 이상이면 개수도 병기
|
|
83
|
+
TOTAL=$((ADDED_COUNT + MODIFIED_COUNT + DELETED_COUNT))
|
|
84
|
+
if [ "$TOTAL" -gt 3 ]; then
|
|
85
|
+
SUBJECT_PARTS="${SUBJECT_PARTS} (+$((TOTAL - 3)) more)"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
SUBJECT="${COMMIT_TYPE}(${SCOPE}): ${SUBJECT_PARTS}"
|
|
89
|
+
|
|
90
|
+
# ── Body: 전체 파일 목록 ─────────────────────────────
|
|
91
|
+
BODY=""
|
|
92
|
+
if [ -n "$ADDED_FILES" ]; then
|
|
93
|
+
BODY="${BODY}New files:\n"
|
|
94
|
+
while IFS= read -r f; do
|
|
95
|
+
[ -n "$f" ] && BODY="${BODY} + ${f}\n"
|
|
96
|
+
done <<< "$ADDED_FILES"
|
|
97
|
+
fi
|
|
98
|
+
if [ -n "$MODIFIED_FILES" ]; then
|
|
99
|
+
[ -n "$BODY" ] && BODY="${BODY}\n"
|
|
100
|
+
BODY="${BODY}Modified:\n"
|
|
101
|
+
while IFS= read -r f; do
|
|
102
|
+
[ -n "$f" ] && BODY="${BODY} ~ ${f}\n"
|
|
103
|
+
done <<< "$MODIFIED_FILES"
|
|
104
|
+
fi
|
|
105
|
+
if [ -n "$DELETED_FILES" ]; then
|
|
106
|
+
[ -n "$BODY" ] && BODY="${BODY}\n"
|
|
107
|
+
BODY="${BODY}Deleted:\n"
|
|
108
|
+
while IFS= read -r f; do
|
|
109
|
+
[ -n "$f" ] && BODY="${BODY} - ${f}\n"
|
|
110
|
+
done <<< "$DELETED_FILES"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
FULL_MSG="${SUBJECT}\n\n${BODY}"
|
|
114
|
+
|
|
115
|
+
git commit -m "$(printf "%b" "$FULL_MSG")" --no-verify 2>/dev/null
|
|
116
|
+
|
|
117
|
+
# Push to remote if available
|
|
118
|
+
REMOTE=$(git remote 2>/dev/null | head -1)
|
|
119
|
+
if [ -n "$REMOTE" ]; then
|
|
120
|
+
git push "$REMOTE" "$BRANCH" --no-verify 2>/dev/null
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
exit 0
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Skill 툴 실행 추적
|
|
3
|
+
# @hook-event PreToolUse|Skill|pre
|
|
4
|
+
# @hook-event PostToolUse|Skill|post
|
|
5
|
+
|
|
6
|
+
SKILL_STATE_FILE="/tmp/claude-active-skill.txt"
|
|
7
|
+
|
|
8
|
+
EVENT="${1:-post}"
|
|
9
|
+
INPUT=$(cat)
|
|
10
|
+
|
|
11
|
+
if [ "$EVENT" = "pre" ]; then
|
|
12
|
+
SKILL_NAME=$(echo "$INPUT" | python3 -c "
|
|
13
|
+
import sys, json
|
|
14
|
+
try:
|
|
15
|
+
d = json.load(sys.stdin)
|
|
16
|
+
print(d.get('tool_input', {}).get('skill', '') or '')
|
|
17
|
+
except:
|
|
18
|
+
print('')
|
|
19
|
+
" 2>/dev/null)
|
|
20
|
+
|
|
21
|
+
if [ -n "$SKILL_NAME" ]; then
|
|
22
|
+
echo "$SKILL_NAME" > "$SKILL_STATE_FILE"
|
|
23
|
+
fi
|
|
24
|
+
else
|
|
25
|
+
rm -f "$SKILL_STATE_FILE"
|
|
26
|
+
fi
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# 응답 완료 시 macOS 알림 발송 — 마지막 커밋 메시지를 요약으로 표시
|
|
3
|
+
# @hook-event Stop||
|
|
4
|
+
|
|
5
|
+
WORK_DIR=$(echo "$CLAUDE_HOOK_INPUT" | jq -r '.cwd // empty' 2>/dev/null)
|
|
6
|
+
if [ -z "$WORK_DIR" ]; then
|
|
7
|
+
WORK_DIR="$(pwd)"
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
# 마지막 git 커밋 메시지를 알림 본문으로 사용
|
|
11
|
+
MSG=""
|
|
12
|
+
if git -C "$WORK_DIR" rev-parse --is-inside-work-tree &>/dev/null; then
|
|
13
|
+
MSG=$(git -C "$WORK_DIR" log --oneline -1 --format="%s" 2>/dev/null)
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
if [ -z "$MSG" ]; then
|
|
17
|
+
MSG="응답이 완료됐습니다."
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
osascript -e "display notification \"${MSG}\" with title \"Claude Code\" subtitle \"응답 완료\" sound name \"Glass\""
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Docs:
|
|
3
|
+
# - https://code.claude.com/docs/en/statusline
|
|
4
|
+
# - https://code.claude.com/docs/en/settings
|
|
5
|
+
#
|
|
6
|
+
# Notes:
|
|
7
|
+
# - Claude Code reruns the statusline when conversation state changes, debounced at about 300ms.
|
|
8
|
+
# - `agent.name` is officially provided.
|
|
9
|
+
# - Active skills are not officially exposed in the statusline JSON, so `skills` below is a best-effort
|
|
10
|
+
# inference from recent `/skill-name` or `$skill-name` mentions in the transcript.
|
|
11
|
+
|
|
12
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
13
|
+
printf '[--] | 📁 -- | default\n'
|
|
14
|
+
printf '◉ CTX unavailable | 🤖 main | 🧩 --\n'
|
|
15
|
+
exit 0
|
|
16
|
+
fi
|
|
17
|
+
|
|
18
|
+
input=$(cat 2>/dev/null)
|
|
19
|
+
[ -n "$input" ] || exit 0
|
|
20
|
+
|
|
21
|
+
repeat_char() {
|
|
22
|
+
local count="$1"
|
|
23
|
+
local char="$2"
|
|
24
|
+
local out=""
|
|
25
|
+
while [ "$count" -gt 0 ]; do
|
|
26
|
+
out="${out}${char}"
|
|
27
|
+
count=$((count - 1))
|
|
28
|
+
done
|
|
29
|
+
printf '%s' "$out"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
shorten() {
|
|
33
|
+
local text="$1"
|
|
34
|
+
local max="${2:-24}"
|
|
35
|
+
if [ "${#text}" -le "$max" ]; then
|
|
36
|
+
printf '%s' "$text"
|
|
37
|
+
else
|
|
38
|
+
printf '%s…' "${text:0:max-1}"
|
|
39
|
+
fi
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
current_dir_name() {
|
|
43
|
+
local path="$1"
|
|
44
|
+
path="${path%/}"
|
|
45
|
+
|
|
46
|
+
if [ -z "$path" ]; then
|
|
47
|
+
printf '/'
|
|
48
|
+
elif [ "$path" = "$HOME" ]; then
|
|
49
|
+
printf '~'
|
|
50
|
+
else
|
|
51
|
+
printf '%s' "${path##*/}"
|
|
52
|
+
fi
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
current_effort() {
|
|
56
|
+
if [ "${MAX_THINKING_TOKENS:-}" = "0" ]; then
|
|
57
|
+
printf 'off'
|
|
58
|
+
elif [ -n "${CLAUDE_CODE_EFFORT_LEVEL:-}" ]; then
|
|
59
|
+
printf '%s' "$CLAUDE_CODE_EFFORT_LEVEL"
|
|
60
|
+
elif [ "${CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING:-}" = "1" ] && [ -n "${MAX_THINKING_TOKENS:-}" ]; then
|
|
61
|
+
printf 'think:%s' "$MAX_THINKING_TOKENS"
|
|
62
|
+
elif [ "${CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING:-}" = "1" ]; then
|
|
63
|
+
printf 'fixed'
|
|
64
|
+
else
|
|
65
|
+
printf 'default'
|
|
66
|
+
fi
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
infer_skills() {
|
|
70
|
+
local transcript="$1"
|
|
71
|
+
local skills
|
|
72
|
+
|
|
73
|
+
if [ -z "$transcript" ] || [ ! -f "$transcript" ]; then
|
|
74
|
+
printf '%s' '--'
|
|
75
|
+
return
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
skills=$(
|
|
79
|
+
tail -n 200 "$transcript" 2>/dev/null |
|
|
80
|
+
jq -r '.. | .text? // empty' 2>/dev/null |
|
|
81
|
+
grep -Eo '(^|[[:space:]])(\$|/)[A-Za-z][A-Za-z0-9_-]+' |
|
|
82
|
+
sed -E 's/^[[:space:]]+//; s#^[/$]##' |
|
|
83
|
+
grep -Evi '^(add-dir|agents|bug|clear|compact|config|cost|doctor|help|init|login|logout|mcp|memory|model|permissions|plugin|plugins|pr-comments|review|status|statusline|terminal-setup|vim)$' |
|
|
84
|
+
awk '!seen[$0]++' |
|
|
85
|
+
head -n 3 |
|
|
86
|
+
awk 'BEGIN{ORS=""} {if (NR>1) printf ", "; printf $0}'
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
printf '%s' "${skills:---}"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
ctx_color() {
|
|
93
|
+
local pct="$1"
|
|
94
|
+
if [ "$pct" -ge 85 ]; then
|
|
95
|
+
printf '\033[38;2;255;107;107m'
|
|
96
|
+
elif [ "$pct" -ge 60 ]; then
|
|
97
|
+
printf '\033[38;2;255;184;77m'
|
|
98
|
+
else
|
|
99
|
+
printf '\033[38;2;92;224;163m'
|
|
100
|
+
fi
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
IFS=$'\t' read -r model dir agent transcript ctx_size cur_in cur_cache_create cur_cache_read used_fallback worktree_branch < <(
|
|
104
|
+
printf '%s' "$input" | jq -r '[
|
|
105
|
+
.model.display_name // .model.id // "--",
|
|
106
|
+
.workspace.current_dir // .cwd // "",
|
|
107
|
+
.agent.name // "main",
|
|
108
|
+
.transcript_path // "",
|
|
109
|
+
(.context_window.context_window_size // 0),
|
|
110
|
+
(.context_window.current_usage.input_tokens? // 0),
|
|
111
|
+
(.context_window.current_usage.cache_creation_input_tokens? // 0),
|
|
112
|
+
(.context_window.current_usage.cache_read_input_tokens? // 0),
|
|
113
|
+
(.context_window.used_percentage // 0 | floor),
|
|
114
|
+
.worktree.branch // ""
|
|
115
|
+
] | @tsv'
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
dir="${dir:-$PWD}"
|
|
119
|
+
model="${model:---}"
|
|
120
|
+
agent="${agent:-main}"
|
|
121
|
+
|
|
122
|
+
used_tokens=$((cur_in + cur_cache_create + cur_cache_read))
|
|
123
|
+
if [ "$ctx_size" -gt 0 ] && [ "$used_tokens" -gt 0 ]; then
|
|
124
|
+
used_pct=$((used_tokens * 100 / ctx_size))
|
|
125
|
+
else
|
|
126
|
+
used_pct="${used_fallback:-0}"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
case "$used_pct" in
|
|
130
|
+
''|*[!0-9]*)
|
|
131
|
+
used_pct=0
|
|
132
|
+
;;
|
|
133
|
+
esac
|
|
134
|
+
|
|
135
|
+
[ "$used_pct" -lt 0 ] && used_pct=0
|
|
136
|
+
[ "$used_pct" -gt 100 ] && used_pct=100
|
|
137
|
+
|
|
138
|
+
branch="$worktree_branch"
|
|
139
|
+
if [ -z "$branch" ] && [ -n "$dir" ] && git -C "$dir" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
140
|
+
branch="$(git -C "$dir" symbolic-ref --quiet --short HEAD 2>/dev/null || git -C "$dir" rev-parse --short HEAD 2>/dev/null)"
|
|
141
|
+
fi
|
|
142
|
+
|
|
143
|
+
skills="$(infer_skills "$transcript")"
|
|
144
|
+
effort="$(current_effort)"
|
|
145
|
+
|
|
146
|
+
dir_label="$(shorten "$(current_dir_name "$dir")" 24)"
|
|
147
|
+
branch_label="$(shorten "$branch" 26)"
|
|
148
|
+
agent_label="$(shorten "$agent" 18)"
|
|
149
|
+
skills_label="$(shorten "$skills" 32)"
|
|
150
|
+
|
|
151
|
+
bar_width=20
|
|
152
|
+
filled=$((used_pct * bar_width / 100))
|
|
153
|
+
empty=$((bar_width - filled))
|
|
154
|
+
|
|
155
|
+
RESET=$'\033[0m'
|
|
156
|
+
BOLD=$'\033[1m'
|
|
157
|
+
DIM=$'\033[2m'
|
|
158
|
+
|
|
159
|
+
MODEL_C=$'\033[1;38;2;110;200;255m'
|
|
160
|
+
DIR_C=$'\033[38;2;142;180;255m'
|
|
161
|
+
BRANCH_C=$'\033[38;2;120;230;160m'
|
|
162
|
+
EFFORT_C=$'\033[38;2;255;196;107m'
|
|
163
|
+
AGENT_C=$'\033[38;2;94;234;212m'
|
|
164
|
+
SKILL_C=$'\033[38;2;255;121;198m'
|
|
165
|
+
SEP_C=$'\033[38;2;108;114;134m'
|
|
166
|
+
FRAME_C=$'\033[38;2;96;103;122m'
|
|
167
|
+
TRACK_C=$'\033[38;2;62;68;84m'
|
|
168
|
+
LABEL_C=$'\033[38;2;145;215;255m'
|
|
169
|
+
PCT_C="$(ctx_color "$used_pct")"
|
|
170
|
+
|
|
171
|
+
filled_bar="$(repeat_char "$filled" '█')"
|
|
172
|
+
empty_bar="$(repeat_char "$empty" '░')"
|
|
173
|
+
|
|
174
|
+
line1="${MODEL_C}[${model}]${RESET} ${SEP_C}|${RESET} ${DIR_C}📁 ${dir_label}${RESET}"
|
|
175
|
+
if [ -n "$branch_label" ]; then
|
|
176
|
+
line1="${line1} ${SEP_C}|${RESET} ${BRANCH_C}⎇ ${branch_label}${RESET}"
|
|
177
|
+
fi
|
|
178
|
+
line1="${line1} ${SEP_C}|${RESET} ${EFFORT_C}${effort}${RESET}"
|
|
179
|
+
|
|
180
|
+
line2="${LABEL_C}◉ CTX ${FRAME_C}▕${PCT_C}${filled_bar}${TRACK_C}${empty_bar}${FRAME_C}▏ ${PCT_C}${used_pct}%${RESET}"
|
|
181
|
+
line2="${line2} ${SEP_C}|${RESET} ${AGENT_C}🤖 ${agent_label}${RESET}"
|
|
182
|
+
line2="${line2} ${SEP_C}|${RESET} ${SKILL_C}🧩 ${skills_label}${RESET}"
|
|
183
|
+
|
|
184
|
+
printf '%b\n' "$line1"
|
|
185
|
+
printf '%b\n' "$line2"
|
|
186
|
+
|
|
@@ -72,17 +72,47 @@ async function getCommands() {
|
|
|
72
72
|
}
|
|
73
73
|
async function getHooks() {
|
|
74
74
|
const dir = join(getAssetsDir(), "hooks");
|
|
75
|
-
const
|
|
75
|
+
const allFiles = await readdir(dir);
|
|
76
|
+
const files = allFiles.filter((f) => f.endsWith(".js") || f.endsWith(".sh"));
|
|
76
77
|
const items = [];
|
|
77
78
|
for (const file of files) {
|
|
78
79
|
const content = await readFile(join(dir, file), "utf-8");
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
80
|
+
if (file.endsWith(".js")) {
|
|
81
|
+
const descMatch = content.match(/\*\s+PostToolUse Hook:\s*(.+)/);
|
|
82
|
+
const description = descMatch ? descMatch[1].trim() : file;
|
|
83
|
+
const hookType = "PostToolUse";
|
|
84
|
+
let matcher = "Edit";
|
|
85
|
+
if (file.includes("typecheck")) matcher = "Edit|Write";
|
|
86
|
+
if (file.includes("format")) matcher = "Edit|Write";
|
|
87
|
+
items.push({ name: file, description, file, hookType, matcher });
|
|
88
|
+
} else {
|
|
89
|
+
const lines = content.split("\n");
|
|
90
|
+
const descLine = lines.find(
|
|
91
|
+
(l) => l.startsWith("#") && !l.startsWith("#!") && !l.includes("@hook-event")
|
|
92
|
+
);
|
|
93
|
+
const description = descLine ? descLine.replace(/^#\s*/, "") : file;
|
|
94
|
+
const eventRegex = /^# @hook-event ([^|]+)\|([^|]*)\|?(.*)$/gm;
|
|
95
|
+
const registrations = [];
|
|
96
|
+
let match;
|
|
97
|
+
while ((match = eventRegex.exec(content)) !== null) {
|
|
98
|
+
registrations.push({
|
|
99
|
+
hookType: match[1].trim(),
|
|
100
|
+
matcher: match[2].trim(),
|
|
101
|
+
args: match[3].trim() || void 0
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
if (registrations.length === 0) continue;
|
|
105
|
+
const [first] = registrations;
|
|
106
|
+
items.push({
|
|
107
|
+
name: file,
|
|
108
|
+
description,
|
|
109
|
+
file,
|
|
110
|
+
hookType: first.hookType,
|
|
111
|
+
matcher: first.matcher,
|
|
112
|
+
args: first.args,
|
|
113
|
+
registrations: registrations.length > 1 ? registrations : void 0
|
|
114
|
+
});
|
|
115
|
+
}
|
|
86
116
|
}
|
|
87
117
|
return items;
|
|
88
118
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
|
|
12
12
|
// src/installers/hooks.ts
|
|
13
13
|
import { join } from "path";
|
|
14
|
-
import { readFile, writeFile, access } from "fs/promises";
|
|
14
|
+
import { readFile, writeFile, access, chmod } from "fs/promises";
|
|
15
15
|
import { constants } from "fs";
|
|
16
16
|
async function mergeHookSettings(settingsPath, hookType, matcher, command) {
|
|
17
17
|
let settings = {};
|
|
@@ -44,8 +44,18 @@ async function installHooks(hooks, targetDir) {
|
|
|
44
44
|
const dest = join(hooksDir, hook.file);
|
|
45
45
|
await backupIfExists(dest);
|
|
46
46
|
await copyFileUtil(join(srcDir, hook.file), dest);
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
if (hook.file.endsWith(".sh")) {
|
|
48
|
+
await chmod(dest, 493);
|
|
49
|
+
}
|
|
50
|
+
const runtime = hook.file.endsWith(".sh") ? "bash" : "node";
|
|
51
|
+
const regs = hook.registrations ?? [
|
|
52
|
+
{ hookType: hook.hookType, matcher: hook.matcher, args: hook.args }
|
|
53
|
+
];
|
|
54
|
+
for (const reg of regs) {
|
|
55
|
+
const argsStr = reg.args ? ` ${reg.args}` : "";
|
|
56
|
+
const hookCommand = `${runtime} "$CLAUDE_PROJECT_DIR"/.claude/hooks/${hook.file}${argsStr}`;
|
|
57
|
+
await mergeHookSettings(settingsPath, reg.hookType, reg.matcher, hookCommand);
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
}
|
|
51
61
|
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
getMcpServers,
|
|
13
13
|
getRuleCategories,
|
|
14
14
|
getSkills
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-E3INXQNO.js";
|
|
16
16
|
import {
|
|
17
17
|
installAgents
|
|
18
18
|
} from "./chunk-EYY2IZ7N.js";
|
|
@@ -24,13 +24,18 @@ import {
|
|
|
24
24
|
} from "./chunk-3Y26YU4R.js";
|
|
25
25
|
import {
|
|
26
26
|
installHooks
|
|
27
|
-
} from "./chunk-
|
|
27
|
+
} from "./chunk-W7RWPDBH.js";
|
|
28
28
|
import {
|
|
29
29
|
installRules
|
|
30
30
|
} from "./chunk-ID643AV4.js";
|
|
31
|
-
import "./chunk-K25UZZVG.js";
|
|
32
|
-
import "./chunk-3GUKEMND.js";
|
|
33
31
|
import {
|
|
32
|
+
backupIfExists
|
|
33
|
+
} from "./chunk-K25UZZVG.js";
|
|
34
|
+
import {
|
|
35
|
+
copyFileUtil
|
|
36
|
+
} from "./chunk-3GUKEMND.js";
|
|
37
|
+
import {
|
|
38
|
+
getAssetsDir,
|
|
34
39
|
getGlobalDir,
|
|
35
40
|
getProjectDir
|
|
36
41
|
} from "./chunk-5XOKKPAA.js";
|
|
@@ -39,7 +44,20 @@ import {
|
|
|
39
44
|
import { checkbox, select, input, confirm } from "@inquirer/prompts";
|
|
40
45
|
import chalk from "chalk";
|
|
41
46
|
import ora from "ora";
|
|
47
|
+
import { join as join2 } from "path";
|
|
48
|
+
|
|
49
|
+
// src/installers/statusline.ts
|
|
42
50
|
import { join } from "path";
|
|
51
|
+
import { chmod } from "fs/promises";
|
|
52
|
+
async function installStatusline() {
|
|
53
|
+
const src = join(getAssetsDir(), "statusline", "statusline.sh");
|
|
54
|
+
const dest = join(getGlobalDir(), "statusline.sh");
|
|
55
|
+
await backupIfExists(dest);
|
|
56
|
+
await copyFileUtil(src, dest);
|
|
57
|
+
await chmod(dest, 493);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// src/cli.ts
|
|
43
61
|
function printBanner() {
|
|
44
62
|
console.log(
|
|
45
63
|
chalk.cyan.bold(`
|
|
@@ -79,7 +97,8 @@ async function runCli() {
|
|
|
79
97
|
{ name: "Hooks - Post-tool hooks", value: "hooks" },
|
|
80
98
|
{ name: "Rules - CLAUDE.md rules", value: "rules" },
|
|
81
99
|
{ name: "MCPs - MCP server configs", value: "mcps" },
|
|
82
|
-
{ name: "ClaudeMd
|
|
100
|
+
{ name: "ClaudeMd - Behavioral guidelines", value: "claudemd" },
|
|
101
|
+
{ name: "Statusline - Terminal statusline script", value: "statusline" }
|
|
83
102
|
]
|
|
84
103
|
});
|
|
85
104
|
if (selectedCategories.length === 0) {
|
|
@@ -95,7 +114,8 @@ async function runCli() {
|
|
|
95
114
|
ruleCategories: [],
|
|
96
115
|
mcps: [],
|
|
97
116
|
mcpEnvValues: /* @__PURE__ */ new Map(),
|
|
98
|
-
claudemds: []
|
|
117
|
+
claudemds: [],
|
|
118
|
+
statusline: false
|
|
99
119
|
};
|
|
100
120
|
const skippedMcps = /* @__PURE__ */ new Map();
|
|
101
121
|
if (selectedCategories.includes("agents")) {
|
|
@@ -164,6 +184,9 @@ async function runCli() {
|
|
|
164
184
|
}))
|
|
165
185
|
});
|
|
166
186
|
}
|
|
187
|
+
if (selectedCategories.includes("statusline")) {
|
|
188
|
+
plan.statusline = true;
|
|
189
|
+
}
|
|
167
190
|
if (selectedCategories.includes("mcps")) {
|
|
168
191
|
const allMcps = await getMcpServers();
|
|
169
192
|
plan.mcps = await checkbox({
|
|
@@ -213,9 +236,10 @@ async function runCli() {
|
|
|
213
236
|
if (plan.hooks.length) console.log(` Hooks : ${plan.hooks.map((h) => h.name).join(", ")}`);
|
|
214
237
|
if (plan.ruleCategories.length) console.log(` Rules : ${plan.ruleCategories.join(", ")}`);
|
|
215
238
|
if (plan.mcps.length) console.log(` MCPs : ${plan.mcps.map((m) => m.name).join(", ")}`);
|
|
216
|
-
if (plan.claudemds.length) console.log(` ClaudeMd: ${plan.claudemds.map((c) => c.name).join(", ")}`);
|
|
239
|
+
if (plan.claudemds.length) console.log(` ClaudeMd : ${plan.claudemds.map((c) => c.name).join(", ")}`);
|
|
240
|
+
if (plan.statusline) console.log(` Statusline: statusline.sh \u2192 ~/.claude/statusline.sh`);
|
|
217
241
|
console.log("");
|
|
218
|
-
const totalItems = plan.agents.length + plan.skills.length + plan.commands.length + plan.hooks.length + plan.ruleCategories.length + plan.mcps.length + plan.claudemds.length;
|
|
242
|
+
const totalItems = plan.agents.length + plan.skills.length + plan.commands.length + plan.hooks.length + plan.ruleCategories.length + plan.mcps.length + plan.claudemds.length + (plan.statusline ? 1 : 0);
|
|
219
243
|
if (totalItems === 0) {
|
|
220
244
|
console.log(chalk.yellow(" Nothing selected. Exiting."));
|
|
221
245
|
return;
|
|
@@ -256,15 +280,20 @@ async function runCli() {
|
|
|
256
280
|
spinner.text = "Installing claudemd...";
|
|
257
281
|
await installClaudemd(plan.claudemds, targetDir);
|
|
258
282
|
}
|
|
283
|
+
if (plan.statusline) {
|
|
284
|
+
spinner.text = "Installing statusline...";
|
|
285
|
+
await installStatusline();
|
|
286
|
+
}
|
|
259
287
|
spinner.succeed(chalk.green("Installation complete!"));
|
|
260
288
|
console.log("");
|
|
261
|
-
if (plan.agents.length) console.log(` ${chalk.green("\u2713")} ${plan.agents.length} agent(s) \u2192 ${
|
|
262
|
-
if (plan.skills.length) console.log(` ${chalk.green("\u2713")} ${plan.skills.length} skill(s) \u2192 ${
|
|
263
|
-
if (plan.commands.length) console.log(` ${chalk.green("\u2713")} ${plan.commands.length} command(s) \u2192 ${
|
|
264
|
-
if (plan.hooks.length) console.log(` ${chalk.green("\u2713")} ${plan.hooks.length} hook(s) \u2192 ${
|
|
265
|
-
if (plan.ruleCategories.length) console.log(` ${chalk.green("\u2713")} Rules copied \u2192 ${
|
|
289
|
+
if (plan.agents.length) console.log(` ${chalk.green("\u2713")} ${plan.agents.length} agent(s) \u2192 ${join2(targetDir, "agents")}`);
|
|
290
|
+
if (plan.skills.length) console.log(` ${chalk.green("\u2713")} ${plan.skills.length} skill(s) \u2192 ${join2(targetDir, "skills")}`);
|
|
291
|
+
if (plan.commands.length) console.log(` ${chalk.green("\u2713")} ${plan.commands.length} command(s) \u2192 ${join2(targetDir, "commands")}`);
|
|
292
|
+
if (plan.hooks.length) console.log(` ${chalk.green("\u2713")} ${plan.hooks.length} hook(s) \u2192 ${join2(targetDir, "hooks")}`);
|
|
293
|
+
if (plan.ruleCategories.length) console.log(` ${chalk.green("\u2713")} Rules copied \u2192 ${join2(targetDir, "rules")}`);
|
|
266
294
|
if (plan.mcps.length) console.log(` ${chalk.green("\u2713")} ${plan.mcps.length} MCP server(s) \u2192 ${scope === "global" ? "~/.claude.json" : "./.claude.json"}`);
|
|
267
|
-
if (plan.claudemds.length) console.log(` ${chalk.green("\u2713")} ClaudeMd appended \u2192 ${
|
|
295
|
+
if (plan.claudemds.length) console.log(` ${chalk.green("\u2713")} ClaudeMd appended \u2192 ${join2(targetDir, "CLAUDE.md")}`);
|
|
296
|
+
if (plan.statusline) console.log(` ${chalk.green("\u2713")} Statusline installed \u2192 ~/.claude/statusline.sh`);
|
|
268
297
|
console.log("");
|
|
269
298
|
if (skippedMcps.size > 0) {
|
|
270
299
|
console.log(chalk.yellow(" \u26A0 The following MCP servers have placeholder env vars:"));
|
package/dist/index.js
CHANGED
|
@@ -60,17 +60,17 @@ Installing all to ${targetDir}...
|
|
|
60
60
|
{ installMcps },
|
|
61
61
|
{ installClaudemd }
|
|
62
62
|
] = await Promise.all([
|
|
63
|
-
import("./registry-
|
|
64
|
-
import("./registry-
|
|
65
|
-
import("./registry-
|
|
66
|
-
import("./registry-
|
|
67
|
-
import("./registry-
|
|
68
|
-
import("./registry-
|
|
69
|
-
import("./registry-
|
|
63
|
+
import("./registry-KRLOB4TH.js"),
|
|
64
|
+
import("./registry-KRLOB4TH.js"),
|
|
65
|
+
import("./registry-KRLOB4TH.js"),
|
|
66
|
+
import("./registry-KRLOB4TH.js"),
|
|
67
|
+
import("./registry-KRLOB4TH.js"),
|
|
68
|
+
import("./registry-KRLOB4TH.js"),
|
|
69
|
+
import("./registry-KRLOB4TH.js"),
|
|
70
70
|
import("./agents-AEKT67A6.js"),
|
|
71
71
|
import("./skills-ULMW3UCM.js"),
|
|
72
72
|
import("./commands-P5LILVZ5.js"),
|
|
73
|
-
import("./hooks-
|
|
73
|
+
import("./hooks-A2WQ2LGG.js"),
|
|
74
74
|
import("./rules-EFSJ3L3A.js"),
|
|
75
75
|
import("./mcps-67Q7TBGW.js"),
|
|
76
76
|
import("./claudemd-KKQ2DL7P.js")
|
|
@@ -124,7 +124,7 @@ async function main() {
|
|
|
124
124
|
return;
|
|
125
125
|
}
|
|
126
126
|
if (args.includes("--uninstall") || args.includes("-u")) {
|
|
127
|
-
const { runUninstallCli } = await import("./uninstall-cli-
|
|
127
|
+
const { runUninstallCli } = await import("./uninstall-cli-GLYJG5V2.js");
|
|
128
128
|
await runUninstallCli();
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
@@ -134,7 +134,7 @@ async function main() {
|
|
|
134
134
|
await installAll(scope);
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
|
-
const { runCli } = await import("./cli-
|
|
137
|
+
const { runCli } = await import("./cli-KZYBSIXO.js");
|
|
138
138
|
await runCli();
|
|
139
139
|
}
|
|
140
140
|
main().catch((err) => {
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
getMcpServers,
|
|
7
7
|
getRuleCategories,
|
|
8
8
|
getSkills
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-E3INXQNO.js";
|
|
10
10
|
import {
|
|
11
11
|
getAssetsDir,
|
|
12
12
|
getGlobalDir,
|
|
@@ -199,7 +199,7 @@ async function removeHookFromSettings(settingsPath, hookFileName) {
|
|
|
199
199
|
const matchers = settings.hooks[hookType];
|
|
200
200
|
for (let i = matchers.length - 1; i >= 0; i--) {
|
|
201
201
|
const matcher = matchers[i];
|
|
202
|
-
matcher.hooks = matcher.hooks.filter((h) => !h.command.
|
|
202
|
+
matcher.hooks = matcher.hooks.filter((h) => !h.command.includes(suffix));
|
|
203
203
|
if (matcher.hooks.length === 0) {
|
|
204
204
|
matchers.splice(i, 1);
|
|
205
205
|
}
|
package/package.json
CHANGED