@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.
Files changed (47) hide show
  1. package/dist/cache/capture.js +0 -0
  2. package/dist/commands/dashboard.js +57 -49
  3. package/dist/commands/hooks.d.ts +25 -36
  4. package/dist/commands/hooks.js +43 -615
  5. package/dist/commands/init.js +7 -23
  6. package/dist/commands/run.js +97 -11
  7. package/dist/commands/setup.js +10 -352
  8. package/dist/deploy/hooks.d.ts +8 -5
  9. package/dist/deploy/hooks.js +12 -105
  10. package/dist/deploy/settings.d.ts +8 -2
  11. package/dist/deploy/settings.js +22 -51
  12. package/dist/index.js +17 -39
  13. package/dist/utils/state.js +7 -2
  14. package/package.json +1 -1
  15. package/templates/CLAUDE.md +82 -292
  16. package/templates/cursor-rules/ekkos-memory.md +48 -108
  17. package/templates/windsurf-rules/ekkos-memory.md +62 -64
  18. package/templates/cursor-hooks/after-agent-response.sh +0 -117
  19. package/templates/cursor-hooks/before-submit-prompt.sh +0 -419
  20. package/templates/cursor-hooks/hooks.json +0 -20
  21. package/templates/cursor-hooks/lib/contract.sh +0 -320
  22. package/templates/cursor-hooks/stop.sh +0 -75
  23. package/templates/hooks/assistant-response.ps1 +0 -256
  24. package/templates/hooks/assistant-response.sh +0 -160
  25. package/templates/hooks/hooks.json +0 -40
  26. package/templates/hooks/lib/contract.sh +0 -332
  27. package/templates/hooks/lib/count-tokens.cjs +0 -86
  28. package/templates/hooks/lib/ekkos-reminders.sh +0 -98
  29. package/templates/hooks/lib/state.sh +0 -210
  30. package/templates/hooks/session-start.ps1 +0 -146
  31. package/templates/hooks/session-start.sh +0 -353
  32. package/templates/hooks/stop.ps1 +0 -349
  33. package/templates/hooks/stop.sh +0 -382
  34. package/templates/hooks/user-prompt-submit.ps1 +0 -419
  35. package/templates/hooks/user-prompt-submit.sh +0 -516
  36. package/templates/project-stubs/session-start.ps1 +0 -63
  37. package/templates/project-stubs/session-start.sh +0 -55
  38. package/templates/project-stubs/stop.ps1 +0 -63
  39. package/templates/project-stubs/stop.sh +0 -55
  40. package/templates/project-stubs/user-prompt-submit.ps1 +0 -63
  41. package/templates/project-stubs/user-prompt-submit.sh +0 -55
  42. package/templates/windsurf-hooks/README.md +0 -212
  43. package/templates/windsurf-hooks/hooks.json +0 -17
  44. package/templates/windsurf-hooks/install.sh +0 -148
  45. package/templates/windsurf-hooks/lib/contract.sh +0 -322
  46. package/templates/windsurf-hooks/post-cascade-response.sh +0 -251
  47. 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
- }