@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.
@@ -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,6 @@
1
+ #!/usr/bin/env bash
2
+ # 새 요청 시작 시 agent 상태 파일 초기화
3
+ # @hook-event UserPromptSubmit||
4
+
5
+ rm -f /tmp/claude-agents/* 2>/dev/null
6
+ 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 files = (await readdir(dir)).filter((f) => f.endsWith(".js"));
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
- const descMatch = content.match(/\*\s+PostToolUse Hook:\s*(.+)/);
80
- const description = descMatch ? descMatch[1].trim() : file;
81
- const hookType = "PostToolUse";
82
- let matcher = "Edit";
83
- if (file.includes("typecheck")) matcher = "Edit|Write";
84
- if (file.includes("format")) matcher = "Edit|Write";
85
- items.push({ name: file, description, file, hookType, matcher });
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
- const hookCommand = `node "$CLAUDE_PROJECT_DIR"/.claude/hooks/${hook.file}`;
48
- await mergeHookSettings(settingsPath, hook.hookType, hook.matcher, hookCommand);
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-OLLOS3GG.js";
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-SLVASXTF.js";
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 - Behavioral guidelines", value: "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 ${join(targetDir, "agents")}`);
262
- if (plan.skills.length) console.log(` ${chalk.green("\u2713")} ${plan.skills.length} skill(s) \u2192 ${join(targetDir, "skills")}`);
263
- if (plan.commands.length) console.log(` ${chalk.green("\u2713")} ${plan.commands.length} command(s) \u2192 ${join(targetDir, "commands")}`);
264
- if (plan.hooks.length) console.log(` ${chalk.green("\u2713")} ${plan.hooks.length} hook(s) \u2192 ${join(targetDir, "hooks")}`);
265
- if (plan.ruleCategories.length) console.log(` ${chalk.green("\u2713")} Rules copied \u2192 ${join(targetDir, "rules")}`);
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 ${join(targetDir, "CLAUDE.md")}`);
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:"));
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  installHooks
3
- } from "./chunk-SLVASXTF.js";
3
+ } from "./chunk-W7RWPDBH.js";
4
4
  import "./chunk-K25UZZVG.js";
5
5
  import "./chunk-3GUKEMND.js";
6
6
  import "./chunk-5XOKKPAA.js";
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-BU55RMHU.js"),
64
- import("./registry-BU55RMHU.js"),
65
- import("./registry-BU55RMHU.js"),
66
- import("./registry-BU55RMHU.js"),
67
- import("./registry-BU55RMHU.js"),
68
- import("./registry-BU55RMHU.js"),
69
- import("./registry-BU55RMHU.js"),
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-S73JTX2I.js"),
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-7XGNDIUC.js");
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-6WQMAFNA.js");
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-OLLOS3GG.js";
9
+ } from "./chunk-E3INXQNO.js";
10
10
  import "./chunk-5XOKKPAA.js";
11
11
  export {
12
12
  getAgents,
@@ -6,7 +6,7 @@ import {
6
6
  getMcpServers,
7
7
  getRuleCategories,
8
8
  getSkills
9
- } from "./chunk-OLLOS3GG.js";
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.endsWith(suffix));
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juho0719/cckit",
3
- "version": "0.2.3",
3
+ "version": "0.2.4",
4
4
  "description": "Claude Code Harness Installer - Interactive CLI for installing Claude Code agents, skills, commands, hooks, rules, and MCP servers",
5
5
  "type": "module",
6
6
  "bin": {