@ekkos/cli 1.2.17 → 1.3.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/dist/cache/capture.js +0 -0
- package/dist/commands/dashboard.js +57 -49
- package/dist/commands/hooks.d.ts +25 -36
- package/dist/commands/hooks.js +43 -615
- package/dist/commands/init.js +7 -23
- package/dist/commands/run.js +97 -11
- package/dist/commands/setup.js +10 -352
- package/dist/deploy/hooks.d.ts +8 -5
- package/dist/deploy/hooks.js +12 -105
- package/dist/deploy/settings.d.ts +8 -2
- package/dist/deploy/settings.js +22 -51
- package/dist/index.js +17 -39
- package/dist/utils/state.js +7 -2
- package/package.json +1 -1
- package/templates/CLAUDE.md +82 -292
- package/templates/cursor-rules/ekkos-memory.md +48 -108
- package/templates/windsurf-rules/ekkos-memory.md +62 -64
- package/templates/cursor-hooks/after-agent-response.sh +0 -117
- package/templates/cursor-hooks/before-submit-prompt.sh +0 -419
- package/templates/cursor-hooks/hooks.json +0 -20
- package/templates/cursor-hooks/lib/contract.sh +0 -320
- package/templates/cursor-hooks/stop.sh +0 -75
- package/templates/hooks/assistant-response.ps1 +0 -256
- package/templates/hooks/assistant-response.sh +0 -160
- package/templates/hooks/hooks.json +0 -40
- package/templates/hooks/lib/contract.sh +0 -332
- package/templates/hooks/lib/count-tokens.cjs +0 -86
- package/templates/hooks/lib/ekkos-reminders.sh +0 -98
- package/templates/hooks/lib/state.sh +0 -210
- package/templates/hooks/session-start.ps1 +0 -146
- package/templates/hooks/session-start.sh +0 -353
- package/templates/hooks/stop.ps1 +0 -349
- package/templates/hooks/stop.sh +0 -382
- package/templates/hooks/user-prompt-submit.ps1 +0 -419
- package/templates/hooks/user-prompt-submit.sh +0 -516
- package/templates/project-stubs/session-start.ps1 +0 -63
- package/templates/project-stubs/session-start.sh +0 -55
- package/templates/project-stubs/stop.ps1 +0 -63
- package/templates/project-stubs/stop.sh +0 -55
- package/templates/project-stubs/user-prompt-submit.ps1 +0 -63
- package/templates/project-stubs/user-prompt-submit.sh +0 -55
- package/templates/windsurf-hooks/README.md +0 -212
- package/templates/windsurf-hooks/hooks.json +0 -17
- package/templates/windsurf-hooks/install.sh +0 -148
- package/templates/windsurf-hooks/lib/contract.sh +0 -322
- package/templates/windsurf-hooks/post-cascade-response.sh +0 -251
- package/templates/windsurf-hooks/pre-user-prompt.sh +0 -435
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
-
# ekkOS_ Hook: AssistantResponse - Validates and enforces footer format
|
|
4
|
-
# MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
|
|
5
|
-
# EKKOS_MANAGED=1
|
|
6
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
7
|
-
# Runs AFTER assistant response, checks footer compliance
|
|
8
|
-
# Per spec v1.2 Addendum: NO jq, NO hardcoded arrays
|
|
9
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
10
|
-
|
|
11
|
-
set +e
|
|
12
|
-
|
|
13
|
-
RESPONSE_FILE="$1"
|
|
14
|
-
HOOK_ENV="$2"
|
|
15
|
-
|
|
16
|
-
# Exit if no response file
|
|
17
|
-
if [[ ! -f "$RESPONSE_FILE" ]]; then
|
|
18
|
-
exit 0
|
|
19
|
-
fi
|
|
20
|
-
|
|
21
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
22
|
-
# CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
|
|
23
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
-
EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
|
|
25
|
-
SESSION_WORDS_JSON="$EKKOS_CONFIG_DIR/session-words.json"
|
|
26
|
-
SESSION_WORDS_DEFAULT="$EKKOS_CONFIG_DIR/.defaults/session-words.json"
|
|
27
|
-
JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
|
|
28
|
-
|
|
29
|
-
# Parse metadata from hook environment using Node (no jq)
|
|
30
|
-
parse_hook_env() {
|
|
31
|
-
local json="$1"
|
|
32
|
-
local path="$2"
|
|
33
|
-
echo "$json" | node -e "
|
|
34
|
-
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
35
|
-
const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
|
|
36
|
-
let result = data;
|
|
37
|
-
for (const p of path) {
|
|
38
|
-
if (result === undefined || result === null) { result = undefined; break; }
|
|
39
|
-
result = result[p];
|
|
40
|
-
}
|
|
41
|
-
if (result !== undefined && result !== null) console.log(result);
|
|
42
|
-
" 2>/dev/null || echo ""
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
SESSION_ID=$(parse_hook_env "$HOOK_ENV" '.sessionId')
|
|
46
|
-
[ -z "$SESSION_ID" ] && SESSION_ID="unknown"
|
|
47
|
-
|
|
48
|
-
MODEL=$(parse_hook_env "$HOOK_ENV" '.model')
|
|
49
|
-
[ -z "$MODEL" ] && MODEL="Claude Code (Opus 4.5)"
|
|
50
|
-
|
|
51
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
52
|
-
# Session name conversion - Uses external session-words.json (NO hardcoded arrays)
|
|
53
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
54
|
-
declare -a ADJECTIVES
|
|
55
|
-
declare -a NOUNS
|
|
56
|
-
declare -a VERBS
|
|
57
|
-
SESSION_WORDS_LOADED=false
|
|
58
|
-
|
|
59
|
-
load_session_words() {
|
|
60
|
-
if [ "$SESSION_WORDS_LOADED" = "true" ]; then
|
|
61
|
-
return 0
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
local words_file="$SESSION_WORDS_JSON"
|
|
65
|
-
if [ ! -f "$words_file" ]; then
|
|
66
|
-
words_file="$SESSION_WORDS_DEFAULT"
|
|
67
|
-
fi
|
|
68
|
-
|
|
69
|
-
if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
|
|
70
|
-
return 1
|
|
71
|
-
fi
|
|
72
|
-
|
|
73
|
-
if command -v node &>/dev/null; then
|
|
74
|
-
if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
|
|
75
|
-
readarray -t ADJECTIVES < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
|
|
76
|
-
readarray -t NOUNS < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
|
|
77
|
-
readarray -t VERBS < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
|
|
78
|
-
else
|
|
79
|
-
local i=0
|
|
80
|
-
while IFS= read -r line; do
|
|
81
|
-
ADJECTIVES[i]="$line"
|
|
82
|
-
((i++))
|
|
83
|
-
done < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
|
|
84
|
-
i=0
|
|
85
|
-
while IFS= read -r line; do
|
|
86
|
-
NOUNS[i]="$line"
|
|
87
|
-
((i++))
|
|
88
|
-
done < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
|
|
89
|
-
i=0
|
|
90
|
-
while IFS= read -r line; do
|
|
91
|
-
VERBS[i]="$line"
|
|
92
|
-
((i++))
|
|
93
|
-
done < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
|
|
94
|
-
fi
|
|
95
|
-
|
|
96
|
-
if [ ${#ADJECTIVES[@]} -gt 0 ] && [ ${#NOUNS[@]} -gt 0 ] && [ ${#VERBS[@]} -gt 0 ]; then
|
|
97
|
-
SESSION_WORDS_LOADED=true
|
|
98
|
-
return 0
|
|
99
|
-
fi
|
|
100
|
-
fi
|
|
101
|
-
return 1
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
convert_uuid_to_name() {
|
|
105
|
-
local uuid="$1"
|
|
106
|
-
|
|
107
|
-
load_session_words || {
|
|
108
|
-
echo "unknown-session"
|
|
109
|
-
return
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
local hex="${uuid//-/}"
|
|
113
|
-
hex="${hex:0:12}"
|
|
114
|
-
|
|
115
|
-
if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
|
|
116
|
-
echo "unknown-session"
|
|
117
|
-
return
|
|
118
|
-
fi
|
|
119
|
-
|
|
120
|
-
local adj_seed=$((16#${hex:0:4}))
|
|
121
|
-
local noun_seed=$((16#${hex:4:4}))
|
|
122
|
-
local verb_seed=$((16#${hex:8:4}))
|
|
123
|
-
local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
|
|
124
|
-
local noun_idx=$((noun_seed % ${#NOUNS[@]}))
|
|
125
|
-
local verb_idx=$((verb_seed % ${#VERBS[@]}))
|
|
126
|
-
|
|
127
|
-
echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
SESSION_NAME=$(convert_uuid_to_name "$SESSION_ID")
|
|
131
|
-
TIMESTAMP=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
|
|
132
|
-
|
|
133
|
-
# Required footer format
|
|
134
|
-
REQUIRED_FOOTER="---
|
|
135
|
-
$MODEL · 🧠 ekkOS_™ · $SESSION_NAME · 📅 $TIMESTAMP"
|
|
136
|
-
|
|
137
|
-
# Check if response has correct footer
|
|
138
|
-
RESPONSE_CONTENT=$(cat "$RESPONSE_FILE")
|
|
139
|
-
LAST_LINE=$(echo "$RESPONSE_CONTENT" | tail -1)
|
|
140
|
-
|
|
141
|
-
# Check if footer exists and is correct
|
|
142
|
-
if [[ "$LAST_LINE" == *"ekkOS"* ]] && [[ "$LAST_LINE" == *"$SESSION_NAME"* ]]; then
|
|
143
|
-
# Footer exists - validate format
|
|
144
|
-
if [[ "$LAST_LINE" == *"$SESSION_NAME"* ]] && [[ "$LAST_LINE" == *"📅"* ]]; then
|
|
145
|
-
# Footer is correct
|
|
146
|
-
exit 0
|
|
147
|
-
else
|
|
148
|
-
# Footer exists but is malformed - replace it
|
|
149
|
-
RESPONSE_WITHOUT_FOOTER=$(echo "$RESPONSE_CONTENT" | head -n -2)
|
|
150
|
-
echo "$RESPONSE_WITHOUT_FOOTER" > "$RESPONSE_FILE"
|
|
151
|
-
echo "" >> "$RESPONSE_FILE"
|
|
152
|
-
echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
|
|
153
|
-
fi
|
|
154
|
-
else
|
|
155
|
-
# Footer missing - append it
|
|
156
|
-
echo "" >> "$RESPONSE_FILE"
|
|
157
|
-
echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
|
|
158
|
-
fi
|
|
159
|
-
|
|
160
|
-
exit 0
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"hooks": {
|
|
3
|
-
"SessionStart": [
|
|
4
|
-
{
|
|
5
|
-
"matcher": "*",
|
|
6
|
-
"hooks": [
|
|
7
|
-
{
|
|
8
|
-
"type": "command",
|
|
9
|
-
"command": "bash .claude/hooks/session-start.sh",
|
|
10
|
-
"timeout": 5000
|
|
11
|
-
}
|
|
12
|
-
]
|
|
13
|
-
}
|
|
14
|
-
],
|
|
15
|
-
"UserPromptSubmit": [
|
|
16
|
-
{
|
|
17
|
-
"matcher": "*",
|
|
18
|
-
"hooks": [
|
|
19
|
-
{
|
|
20
|
-
"type": "command",
|
|
21
|
-
"command": "bash .claude/hooks/user-prompt-submit.sh",
|
|
22
|
-
"timeout": 5000
|
|
23
|
-
}
|
|
24
|
-
]
|
|
25
|
-
}
|
|
26
|
-
],
|
|
27
|
-
"Stop": [
|
|
28
|
-
{
|
|
29
|
-
"matcher": "*",
|
|
30
|
-
"hooks": [
|
|
31
|
-
{
|
|
32
|
-
"type": "command",
|
|
33
|
-
"command": "bash .claude/hooks/stop.sh",
|
|
34
|
-
"timeout": 5000
|
|
35
|
-
}
|
|
36
|
-
]
|
|
37
|
-
}
|
|
38
|
-
]
|
|
39
|
-
}
|
|
40
|
-
}
|
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
-
# ekkOS_ Turn Contract Library
|
|
4
|
-
#
|
|
5
|
-
# Shared functions for Golden Loop compliance enforcement.
|
|
6
|
-
# Used by BOTH Claude Code (.claude/) and Cursor (.cursor/) hooks.
|
|
7
|
-
#
|
|
8
|
-
# TURN CONTRACT: Evidence that retrieval occurred before answering.
|
|
9
|
-
# This is the SINGLE SOURCE OF TRUTH for compliance auditing.
|
|
10
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
-
|
|
12
|
-
# Get contract directory based on environment
|
|
13
|
-
get_contract_dir() {
|
|
14
|
-
local source="${1:-claude-code}"
|
|
15
|
-
local project_root="${2:-$PROJECT_ROOT}"
|
|
16
|
-
|
|
17
|
-
if [ "$source" = "cursor" ]; then
|
|
18
|
-
echo "$project_root/.cursor/state/ekkos"
|
|
19
|
-
else
|
|
20
|
-
echo "$project_root/.claude/state/ekkos"
|
|
21
|
-
fi
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
# Generate stable hash of user prompt (for deduplication)
|
|
25
|
-
generate_query_hash() {
|
|
26
|
-
local query="$1"
|
|
27
|
-
# Use md5 on macOS, md5sum on Linux
|
|
28
|
-
if command -v md5 >/dev/null 2>&1; then
|
|
29
|
-
echo -n "$query" | md5 | cut -c1-16
|
|
30
|
-
elif command -v md5sum >/dev/null 2>&1; then
|
|
31
|
-
echo -n "$query" | md5sum | cut -c1-16
|
|
32
|
-
else
|
|
33
|
-
# Fallback: simple hash using cksum
|
|
34
|
-
echo -n "$query" | cksum | cut -d' ' -f1
|
|
35
|
-
fi
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
# Write turn contract at RETRIEVAL time
|
|
39
|
-
# This is the EVIDENCE that retrieval happened before answering
|
|
40
|
-
write_turn_contract() {
|
|
41
|
-
local session_id="$1"
|
|
42
|
-
local retrieval_ok="$2"
|
|
43
|
-
local retrieval_source="$3"
|
|
44
|
-
local pattern_ids="$4" # Comma-separated list
|
|
45
|
-
local directive_ids="$5" # Comma-separated list
|
|
46
|
-
local query_hash="$6"
|
|
47
|
-
local project_root="${7:-$PROJECT_ROOT}"
|
|
48
|
-
|
|
49
|
-
local contract_dir
|
|
50
|
-
contract_dir=$(get_contract_dir "$retrieval_source" "$project_root")
|
|
51
|
-
mkdir -p "$contract_dir" 2>/dev/null || return 1
|
|
52
|
-
|
|
53
|
-
local contract_file="$contract_dir/turn-contract-${session_id}.json"
|
|
54
|
-
local timestamp
|
|
55
|
-
timestamp=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
56
|
-
|
|
57
|
-
# Convert comma-separated IDs to JSON array
|
|
58
|
-
local pattern_array
|
|
59
|
-
local directive_array
|
|
60
|
-
if [ -n "$pattern_ids" ]; then
|
|
61
|
-
pattern_array=$(echo "$pattern_ids" | tr ',' '\n' | grep -v '^$' | jq -R . | jq -s .)
|
|
62
|
-
else
|
|
63
|
-
pattern_array="[]"
|
|
64
|
-
fi
|
|
65
|
-
if [ -n "$directive_ids" ]; then
|
|
66
|
-
directive_array=$(echo "$directive_ids" | tr ',' '\n' | grep -v '^$' | jq -R . | jq -s .)
|
|
67
|
-
else
|
|
68
|
-
directive_array="[]"
|
|
69
|
-
fi
|
|
70
|
-
|
|
71
|
-
# Write contract
|
|
72
|
-
cat > "$contract_file" << EOF
|
|
73
|
-
{
|
|
74
|
-
"session_id": "$session_id",
|
|
75
|
-
"retrieval_ok": $retrieval_ok,
|
|
76
|
-
"retrieval_source": "$retrieval_source",
|
|
77
|
-
"retrieved_pattern_ids": $pattern_array,
|
|
78
|
-
"retrieved_directive_ids": $directive_array,
|
|
79
|
-
"timestamp": "$timestamp",
|
|
80
|
-
"query_hash": "$query_hash",
|
|
81
|
-
"ekkos_strict": ${EKKOS_STRICT:-1}
|
|
82
|
-
}
|
|
83
|
-
EOF
|
|
84
|
-
|
|
85
|
-
return 0
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
# Read turn contract
|
|
89
|
-
read_turn_contract() {
|
|
90
|
-
local session_id="$1"
|
|
91
|
-
local retrieval_source="$2"
|
|
92
|
-
local project_root="${3:-$PROJECT_ROOT}"
|
|
93
|
-
|
|
94
|
-
local contract_dir
|
|
95
|
-
contract_dir=$(get_contract_dir "$retrieval_source" "$project_root")
|
|
96
|
-
local contract_file="$contract_dir/turn-contract-${session_id}.json"
|
|
97
|
-
|
|
98
|
-
if [ -f "$contract_file" ]; then
|
|
99
|
-
cat "$contract_file"
|
|
100
|
-
return 0
|
|
101
|
-
else
|
|
102
|
-
return 1
|
|
103
|
-
fi
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
# Extract field from contract JSON
|
|
107
|
-
get_contract_field() {
|
|
108
|
-
local contract_json="$1"
|
|
109
|
-
local field="$2"
|
|
110
|
-
|
|
111
|
-
echo "$contract_json" | jq -r ".$field // \"\"" 2>/dev/null
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
# Get array field as comma-separated string
|
|
115
|
-
get_contract_array() {
|
|
116
|
-
local contract_json="$1"
|
|
117
|
-
local field="$2"
|
|
118
|
-
|
|
119
|
-
echo "$contract_json" | jq -r ".$field // [] | join(\",\")" 2>/dev/null
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
# Cleanup turn contract (only after successful capture)
|
|
123
|
-
cleanup_turn_contract() {
|
|
124
|
-
local session_id="$1"
|
|
125
|
-
local retrieval_source="$2"
|
|
126
|
-
local project_root="${3:-$PROJECT_ROOT}"
|
|
127
|
-
|
|
128
|
-
local contract_dir
|
|
129
|
-
contract_dir=$(get_contract_dir "$retrieval_source" "$project_root")
|
|
130
|
-
local contract_file="$contract_dir/turn-contract-${session_id}.json"
|
|
131
|
-
|
|
132
|
-
rm -f "$contract_file" 2>/dev/null
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
# Check if strict mode is enabled
|
|
136
|
-
is_strict_mode() {
|
|
137
|
-
[ "${EKKOS_STRICT:-1}" = "1" ] # DEFAULT: ON
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
# Generate strict mode blocker message for Claude Code
|
|
141
|
-
get_strict_blocker_message() {
|
|
142
|
-
cat << 'EOF'
|
|
143
|
-
════════════════════════════════════════════════════════════════════════════
|
|
144
|
-
⛔ EKKOS_STRICT MODE: RETRIEVAL FAILED — DO NOT ANSWER
|
|
145
|
-
|
|
146
|
-
ekkOS memory retrieval failed or was not authenticated.
|
|
147
|
-
In STRICT mode, you MUST NOT answer technical questions without memory context.
|
|
148
|
-
|
|
149
|
-
ACTION REQUIRED:
|
|
150
|
-
1. Check if ekkOS is connected: Run `ekkOS: Connect` in VS Code
|
|
151
|
-
2. Verify auth token exists: ~/.ekkos/config.json
|
|
152
|
-
3. Retry the request after connecting
|
|
153
|
-
|
|
154
|
-
DO NOT proceed with answering. The Golden Loop requires retrieval FIRST.
|
|
155
|
-
════════════════════════════════════════════════════════════════════════════
|
|
156
|
-
EOF
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
# Validate PatternGuard coverage (returns 0-100)
|
|
160
|
-
# PORTABLE: Works on macOS without Perl regex (grep -P)
|
|
161
|
-
calculate_pattern_guard_coverage() {
|
|
162
|
-
local assistant_response="$1"
|
|
163
|
-
local pattern_ids="$2" # Comma-separated
|
|
164
|
-
|
|
165
|
-
# Count total patterns - more robust counting
|
|
166
|
-
local total_count=0
|
|
167
|
-
if [ -n "$pattern_ids" ]; then
|
|
168
|
-
total_count=$(echo "$pattern_ids" | tr ',' '\n' | grep -v '^$' | wc -l | tr -d ' ')
|
|
169
|
-
fi
|
|
170
|
-
|
|
171
|
-
if [ "$total_count" -eq 0 ]; then
|
|
172
|
-
echo "100" # No patterns = 100% coverage by definition
|
|
173
|
-
return 0
|
|
174
|
-
fi
|
|
175
|
-
|
|
176
|
-
# Extract acknowledged IDs using portable awk (works on macOS and Linux)
|
|
177
|
-
local acknowledged_count=0
|
|
178
|
-
|
|
179
|
-
# Use awk to extract SELECT block and count IDs - PORTABLE approach
|
|
180
|
-
local select_count=0
|
|
181
|
-
select_count=$(echo "$assistant_response" | awk '
|
|
182
|
-
/\[ekkOS_SELECT\]/{in_block=1; next}
|
|
183
|
-
/\[\/ekkOS_SELECT\]/{in_block=0}
|
|
184
|
-
in_block && /id:/{count++}
|
|
185
|
-
END{print count+0}
|
|
186
|
-
')
|
|
187
|
-
acknowledged_count=$((acknowledged_count + select_count))
|
|
188
|
-
|
|
189
|
-
# Use awk to extract SKIP block and count IDs
|
|
190
|
-
local skip_count=0
|
|
191
|
-
skip_count=$(echo "$assistant_response" | awk '
|
|
192
|
-
/\[ekkOS_SKIP\]/{in_block=1; next}
|
|
193
|
-
/\[\/ekkOS_SKIP\]/{in_block=0}
|
|
194
|
-
in_block && /id:/{count++}
|
|
195
|
-
END{print count+0}
|
|
196
|
-
')
|
|
197
|
-
acknowledged_count=$((acknowledged_count + skip_count))
|
|
198
|
-
|
|
199
|
-
# Legacy: Check for [ekkOS_APPLY] markers (fallback)
|
|
200
|
-
if [ "$acknowledged_count" -eq 0 ]; then
|
|
201
|
-
local apply_count
|
|
202
|
-
apply_count=$(echo "$assistant_response" | grep -c '\[ekkOS_APPLY\]' 2>/dev/null || echo 0)
|
|
203
|
-
acknowledged_count=$((acknowledged_count + apply_count))
|
|
204
|
-
fi
|
|
205
|
-
|
|
206
|
-
# Calculate coverage percentage
|
|
207
|
-
local coverage=0
|
|
208
|
-
if [ "$total_count" -gt 0 ]; then
|
|
209
|
-
coverage=$((acknowledged_count * 100 / total_count))
|
|
210
|
-
fi
|
|
211
|
-
|
|
212
|
-
# Cap at 100%
|
|
213
|
-
if [ "$coverage" -gt 100 ]; then
|
|
214
|
-
coverage=100
|
|
215
|
-
fi
|
|
216
|
-
|
|
217
|
-
echo "$coverage"
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
# Check for ekkOS footer presence
|
|
221
|
-
check_footer_present() {
|
|
222
|
-
local assistant_response="$1"
|
|
223
|
-
|
|
224
|
-
# Look for the mandatory footer format:
|
|
225
|
-
# 🧠 **ekkOS_™** · 📅 YYYY-MM-DD
|
|
226
|
-
# OR
|
|
227
|
-
# {IDE} ({Model}) · 🧠 **ekkOS_™** · 📅 {Timestamp}
|
|
228
|
-
|
|
229
|
-
if echo "$assistant_response" | grep -qE '🧠.*ekkOS.*📅.*[0-9]{4}-[0-9]{2}-[0-9]{2}'; then
|
|
230
|
-
echo "true"
|
|
231
|
-
return 0
|
|
232
|
-
else
|
|
233
|
-
echo "false"
|
|
234
|
-
return 1
|
|
235
|
-
fi
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
# Build compliance metadata for capture
|
|
239
|
-
build_compliance_metadata() {
|
|
240
|
-
local retrieval_ok="$1"
|
|
241
|
-
local pattern_guard_coverage="$2"
|
|
242
|
-
local footer_present="$3"
|
|
243
|
-
local ekkos_strict="$4"
|
|
244
|
-
local retrieved_count="$5"
|
|
245
|
-
|
|
246
|
-
local pattern_guard_required="false"
|
|
247
|
-
if [ "$retrieved_count" -gt 0 ]; then
|
|
248
|
-
pattern_guard_required="true"
|
|
249
|
-
fi
|
|
250
|
-
|
|
251
|
-
cat << EOF
|
|
252
|
-
{
|
|
253
|
-
"retrieval_ok": $retrieval_ok,
|
|
254
|
-
"pattern_guard_required": $pattern_guard_required,
|
|
255
|
-
"pattern_guard_coverage_pct": $pattern_guard_coverage,
|
|
256
|
-
"footer_present": $footer_present,
|
|
257
|
-
"ekkos_strict": $ekkos_strict,
|
|
258
|
-
"retrieved_count": $retrieved_count,
|
|
259
|
-
"compliance_version": "1.0"
|
|
260
|
-
}
|
|
261
|
-
EOF
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
# Determine if turn is compliant
|
|
265
|
-
# ROBUST: Handles edge cases with non-numeric or empty values
|
|
266
|
-
is_turn_compliant() {
|
|
267
|
-
local retrieval_ok="${1:-true}"
|
|
268
|
-
local pattern_guard_coverage="${2:-100}"
|
|
269
|
-
local footer_present="${3:-true}"
|
|
270
|
-
local pattern_count="${4:-0}"
|
|
271
|
-
|
|
272
|
-
# Sanitize numeric values (default to safe values)
|
|
273
|
-
pattern_guard_coverage=$(echo "$pattern_guard_coverage" | grep -oE '^[0-9]+$' || echo "100")
|
|
274
|
-
pattern_count=$(echo "$pattern_count" | grep -oE '^[0-9]+$' || echo "0")
|
|
275
|
-
|
|
276
|
-
# Retrieval must have succeeded
|
|
277
|
-
if [ "$retrieval_ok" != "true" ]; then
|
|
278
|
-
echo "false"
|
|
279
|
-
return 1
|
|
280
|
-
fi
|
|
281
|
-
|
|
282
|
-
# If patterns were retrieved, PatternGuard must be 100%
|
|
283
|
-
if [ "$pattern_count" -gt 0 ] && [ "$pattern_guard_coverage" -lt 100 ]; then
|
|
284
|
-
echo "false"
|
|
285
|
-
return 1
|
|
286
|
-
fi
|
|
287
|
-
|
|
288
|
-
# Footer must be present
|
|
289
|
-
if [ "$footer_present" != "true" ]; then
|
|
290
|
-
echo "false"
|
|
291
|
-
return 1
|
|
292
|
-
fi
|
|
293
|
-
|
|
294
|
-
echo "true"
|
|
295
|
-
return 0
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
# Generate violation reason
|
|
299
|
-
get_violation_reason() {
|
|
300
|
-
local retrieval_ok="$1"
|
|
301
|
-
local pattern_guard_coverage="$2"
|
|
302
|
-
local footer_present="$3"
|
|
303
|
-
local pattern_count="$4"
|
|
304
|
-
|
|
305
|
-
local reasons=""
|
|
306
|
-
|
|
307
|
-
if [ "$retrieval_ok" != "true" ]; then
|
|
308
|
-
reasons="retrieval_failed"
|
|
309
|
-
fi
|
|
310
|
-
|
|
311
|
-
if [ "$pattern_count" -gt 0 ] && [ "$pattern_guard_coverage" -lt 100 ]; then
|
|
312
|
-
if [ -n "$reasons" ]; then
|
|
313
|
-
reasons="$reasons,pattern_guard_incomplete"
|
|
314
|
-
else
|
|
315
|
-
reasons="pattern_guard_incomplete"
|
|
316
|
-
fi
|
|
317
|
-
fi
|
|
318
|
-
|
|
319
|
-
if [ "$footer_present" != "true" ]; then
|
|
320
|
-
if [ -n "$reasons" ]; then
|
|
321
|
-
reasons="$reasons,footer_missing"
|
|
322
|
-
else
|
|
323
|
-
reasons="footer_missing"
|
|
324
|
-
fi
|
|
325
|
-
fi
|
|
326
|
-
|
|
327
|
-
if [ -z "$reasons" ]; then
|
|
328
|
-
reasons="none"
|
|
329
|
-
fi
|
|
330
|
-
|
|
331
|
-
echo "$reasons"
|
|
332
|
-
}
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Token counter for Claude Code hooks
|
|
4
|
-
* Extracts ACTUAL token data from Anthropic API usage field
|
|
5
|
-
*
|
|
6
|
-
* The transcript contains the real usage data from each API response:
|
|
7
|
-
* - input_tokens: new tokens in this request
|
|
8
|
-
* - cache_read_input_tokens: cached tokens from previous turns
|
|
9
|
-
* - cache_creation_input_tokens: tokens added to cache
|
|
10
|
-
*
|
|
11
|
-
* Usage:
|
|
12
|
-
* node count-tokens.cjs <transcript.jsonl> -> outputs total token count
|
|
13
|
-
* node count-tokens.cjs <transcript.jsonl> --json -> outputs full breakdown as JSON
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
const fs = require('fs');
|
|
17
|
-
|
|
18
|
-
const filePath = process.argv[2];
|
|
19
|
-
const outputJson = process.argv.includes('--json');
|
|
20
|
-
|
|
21
|
-
if (!filePath) {
|
|
22
|
-
console.error('Usage: count-tokens.cjs <transcript.jsonl> [--json]');
|
|
23
|
-
process.exit(1);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (!fs.existsSync(filePath)) {
|
|
27
|
-
console.error(`[count-tokens] ERROR: File not found: ${filePath}`);
|
|
28
|
-
process.exit(1);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Extract token breakdown from the most recent assistant message's usage field
|
|
33
|
-
* This is the authoritative data from Anthropic's API
|
|
34
|
-
*/
|
|
35
|
-
function getTokenBreakdown(filePath) {
|
|
36
|
-
const content = fs.readFileSync(filePath, 'utf-8');
|
|
37
|
-
const lines = content.trim().split('\n').filter(Boolean);
|
|
38
|
-
|
|
39
|
-
// Find the most recent assistant message with usage data
|
|
40
|
-
let latestUsage = null;
|
|
41
|
-
|
|
42
|
-
for (const line of lines) {
|
|
43
|
-
try {
|
|
44
|
-
const entry = JSON.parse(line);
|
|
45
|
-
|
|
46
|
-
if (entry.type === 'assistant' && entry.message?.usage) {
|
|
47
|
-
latestUsage = entry.message.usage;
|
|
48
|
-
}
|
|
49
|
-
} catch (e) {
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
if (!latestUsage) {
|
|
55
|
-
return {
|
|
56
|
-
input_tokens: 0,
|
|
57
|
-
cache_read_tokens: 0,
|
|
58
|
-
cache_creation_tokens: 0,
|
|
59
|
-
total_tokens: 0,
|
|
60
|
-
output_tokens: 0
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
const inputTokens = latestUsage.input_tokens || 0;
|
|
65
|
-
const cacheRead = latestUsage.cache_read_input_tokens || 0;
|
|
66
|
-
const cacheCreation = latestUsage.cache_creation_input_tokens || 0;
|
|
67
|
-
const outputTokens = latestUsage.output_tokens || 0;
|
|
68
|
-
|
|
69
|
-
return {
|
|
70
|
-
input_tokens: inputTokens,
|
|
71
|
-
cache_read_tokens: cacheRead,
|
|
72
|
-
cache_creation_tokens: cacheCreation,
|
|
73
|
-
total_tokens: inputTokens + cacheRead + cacheCreation,
|
|
74
|
-
output_tokens: outputTokens
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const breakdown = getTokenBreakdown(filePath);
|
|
79
|
-
|
|
80
|
-
if (outputJson) {
|
|
81
|
-
// Output full breakdown as JSON (for API)
|
|
82
|
-
console.log(JSON.stringify(breakdown));
|
|
83
|
-
} else {
|
|
84
|
-
// Output just the total (backward compatible)
|
|
85
|
-
console.log(breakdown.total_tokens);
|
|
86
|
-
}
|