@ekkos/cli 1.0.35 → 1.0.36

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.
@@ -1,160 +1,188 @@
1
- # ═══════════════════════════════════════════════════════════════════════════
2
- # ekkOS_ Hook: AssistantResponse - Validates and enforces footer format
3
- # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
- # EKKOS_MANAGED=1
5
- # ═══════════════════════════════════════════════════════════════════════════
6
- # Runs AFTER assistant response, checks footer compliance
7
- # Per spec v1.2 Addendum: NO jq, NO hardcoded arrays
8
- # ═══════════════════════════════════════════════════════════════════════════
9
-
10
- $ErrorActionPreference = "SilentlyContinue"
11
-
12
- $RESPONSE_FILE = $args[0]
13
- $HOOK_ENV = $args[1]
14
-
15
- # Exit if no response file
16
- if (-not $RESPONSE_FILE -or -not (Test-Path $RESPONSE_FILE)) {
17
- exit 0
18
- }
19
-
20
- # ═══════════════════════════════════════════════════════════════════════════
21
- # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
22
- # ═══════════════════════════════════════════════════════════════════════════
23
- $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { Join-Path $env:USERPROFILE ".ekkos" }
24
- $SessionWordsJson = Join-Path $EkkosConfigDir "session-words.json"
25
- $SessionWordsDefault = Join-Path $EkkosConfigDir ".defaults\session-words.json"
26
- $JsonParseHelper = Join-Path $EkkosConfigDir ".helpers\json-parse.cjs"
27
-
28
- # ═══════════════════════════════════════════════════════════════════════════
29
- # Parse metadata from hook environment using Node (no jq)
30
- # ═══════════════════════════════════════════════════════════════════════════
31
- function Parse-HookEnv {
32
- param(
33
- [string]$Json,
34
- [string]$Path
35
- )
36
- if (-not $Json) { return "" }
37
- try {
38
- $result = $Json | node -e "
39
- const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
40
- const path = '$Path'.replace(/^\./,'').split('.').filter(Boolean);
41
- let result = data;
42
- for (const p of path) {
43
- if (result === undefined || result === null) { result = undefined; break; }
44
- result = result[p];
45
- }
46
- if (result !== undefined && result !== null) console.log(result);
47
- " 2>$null
48
- if ($result) { return $result.Trim() } else { return "" }
49
- } catch {
50
- return ""
51
- }
52
- }
53
-
54
- $SESSION_ID = Parse-HookEnv $HOOK_ENV ".sessionId"
55
- if (-not $SESSION_ID) { $SESSION_ID = "unknown" }
56
-
57
- $MODEL = Parse-HookEnv $HOOK_ENV ".model"
58
- if (-not $MODEL) { $MODEL = "Claude Code (Opus 4.5)" }
59
-
60
- # ═══════════════════════════════════════════════════════════════════════════
61
- # Session name conversion - Uses external session-words.json (NO hardcoded arrays)
62
- # ═══════════════════════════════════════════════════════════════════════════
63
- $script:ADJECTIVES = @()
64
- $script:NOUNS = @()
65
- $script:VERBS = @()
66
- $script:SESSION_WORDS_LOADED = $false
67
-
68
- function Load-SessionWords {
69
- if ($script:SESSION_WORDS_LOADED) { return }
70
-
71
- $wordsFile = $SessionWordsJson
72
- if (-not (Test-Path $wordsFile)) {
73
- $wordsFile = $SessionWordsDefault
74
- }
75
-
76
- if ((-not (Test-Path $wordsFile)) -or (-not (Test-Path $JsonParseHelper))) {
77
- return
78
- }
79
-
80
- try {
81
- $adjRaw = & node $JsonParseHelper $wordsFile ".adjectives" 2>$null
82
- $nounRaw = & node $JsonParseHelper $wordsFile ".nouns" 2>$null
83
- $verbRaw = & node $JsonParseHelper $wordsFile ".verbs" 2>$null
84
-
85
- $script:ADJECTIVES = @()
86
- $script:NOUNS = @()
87
- $script:VERBS = @()
88
-
89
- if ($adjRaw) { $script:ADJECTIVES = @($adjRaw -split "`n" | Where-Object { $_ }) }
90
- if ($nounRaw) { $script:NOUNS = @($nounRaw -split "`n" | Where-Object { $_ }) }
91
- if ($verbRaw) { $script:VERBS = @($verbRaw -split "`n" | Where-Object { $_ }) }
92
-
93
- if ($script:ADJECTIVES.Count -gt 0 -and $script:NOUNS.Count -gt 0 -and $script:VERBS.Count -gt 0) {
94
- $script:SESSION_WORDS_LOADED = $true
95
- }
96
- } catch {}
97
- }
98
-
99
- function Convert-UuidToName {
100
- param([string]$uuid)
101
-
102
- Load-SessionWords
103
-
104
- if (-not $script:SESSION_WORDS_LOADED) {
105
- return "unknown-session"
106
- }
107
-
108
- $hex = $uuid -replace "-", ""
109
- $hex = $hex.Substring(0, [Math]::Min(12, $hex.Length))
110
-
111
- if ($hex -notmatch '^[0-9a-fA-F]+$') {
112
- return "unknown-session"
113
- }
114
-
115
- try {
116
- $adjSeed = [Convert]::ToInt32($hex.Substring(0, 4), 16)
117
- $nounSeed = [Convert]::ToInt32($hex.Substring(4, 4), 16)
118
- $verbSeed = [Convert]::ToInt32($hex.Substring(8, 4), 16)
119
-
120
- $adjIdx = $adjSeed % $script:ADJECTIVES.Count
121
- $nounIdx = $nounSeed % $script:NOUNS.Count
122
- $verbIdx = $verbSeed % $script:VERBS.Count
123
-
124
- return "$($script:ADJECTIVES[$adjIdx])-$($script:NOUNS[$nounIdx])-$($script:VERBS[$verbIdx])"
125
- } catch {
126
- return "unknown-session"
127
- }
128
- }
129
-
130
- $SESSION_NAME = Convert-UuidToName $SESSION_ID
131
- $TIMESTAMP = Get-Date -Format "yyyy-MM-dd hh:mm:ss tt K"
132
-
133
- # Required footer format
134
- $REQUIRED_FOOTER = "---`n$MODEL · ekkOS_ · $SESSION_NAME · $TIMESTAMP"
135
-
136
- # Check if response has correct footer
137
- $RESPONSE_CONTENT = Get-Content $RESPONSE_FILE -Raw
138
- $lines = $RESPONSE_CONTENT -split "`n"
139
- $LAST_LINE = ""
140
- if ($lines.Count -gt 0) {
141
- $LAST_LINE = $lines[$lines.Count - 1].Trim()
142
- }
143
-
144
- # Check if footer exists and is correct
145
- if ($LAST_LINE -match "ekkOS" -and $LAST_LINE -match [regex]::Escape($SESSION_NAME)) {
146
- # Footer exists - validate format
147
- if ($LAST_LINE -match [regex]::Escape($SESSION_NAME) -and $LAST_LINE -match "📅") {
148
- # Footer is correct
149
- exit 0
150
- } else {
151
- # Footer exists but is malformed - replace it
152
- $withoutFooter = ($lines | Select-Object -SkipLast 2) -join "`n"
153
- Set-Content -Path $RESPONSE_FILE -Value "$withoutFooter`n`n$REQUIRED_FOOTER" -NoNewline -Force
154
- }
155
- } else {
156
- # Footer missing - append it
157
- Add-Content -Path $RESPONSE_FILE -Value "`n`n$REQUIRED_FOOTER"
158
- }
159
-
160
- exit 0
1
+ # ═══════════════════════════════════════════════════════════════════════════
2
+ # ekkOS_ Hook: AssistantResponse - Process Claude's response (Windows)
3
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
+ # EKKOS_MANAGED=1
5
+ # ═══════════════════════════════════════════════════════════════════════════
6
+
7
+ $ErrorActionPreference = "SilentlyContinue"
8
+
9
+ # ═══════════════════════════════════════════════════════════════════════════
10
+ # CONFIG PATHS
11
+ # ═══════════════════════════════════════════════════════════════════════════
12
+ $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
13
+ $HooksEnabledJson = "$EkkosConfigDir\hooks-enabled.json"
14
+ $HooksEnabledDefault = "$EkkosConfigDir\.defaults\hooks-enabled.json"
15
+ $SessionWordsJson = "$EkkosConfigDir\session-words.json"
16
+ $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
17
+ $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
18
+
19
+ $MemoryApiUrl = "https://api.ekkos.dev"
20
+
21
+ # ═══════════════════════════════════════════════════════════════════════════
22
+ # CHECK ENABLEMENT
23
+ # ═══════════════════════════════════════════════════════════════════════════
24
+ function Test-HookEnabled {
25
+ param([string]$HookName)
26
+ $enabledFile = $HooksEnabledJson
27
+ if (-not (Test-Path $enabledFile)) { $enabledFile = $HooksEnabledDefault }
28
+ if (-not (Test-Path $enabledFile)) { return $true }
29
+ try {
30
+ $config = Get-Content $enabledFile -Raw | ConvertFrom-Json
31
+ if ($config.claude -and $config.claude.enabled) {
32
+ $enabledHooks = $config.claude.enabled
33
+ if ($enabledHooks -contains $HookName -or $enabledHooks -contains "assistant-response") { return $true }
34
+ return $false
35
+ }
36
+ return $true
37
+ } catch { return $true }
38
+ }
39
+
40
+ if (-not (Test-HookEnabled "assistant-response")) { exit 0 }
41
+
42
+ # ═══════════════════════════════════════════════════════════════════════════
43
+ # Load session words - NO HARDCODED ARRAYS
44
+ # ═══════════════════════════════════════════════════════════════════════════
45
+ $script:SessionWords = $null
46
+
47
+ function Load-SessionWords {
48
+ $wordsFile = $SessionWordsJson
49
+ if (-not (Test-Path $wordsFile)) { $wordsFile = $SessionWordsDefault }
50
+ if (-not (Test-Path $wordsFile)) { return $null }
51
+ try {
52
+ $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
53
+ } catch { return $null }
54
+ }
55
+
56
+ function Convert-UuidToWords {
57
+ param([string]$uuid)
58
+ if (-not $script:SessionWords) { Load-SessionWords }
59
+ if (-not $script:SessionWords) { return "unknown-session" }
60
+
61
+ $adjectives = $script:SessionWords.adjectives
62
+ $nouns = $script:SessionWords.nouns
63
+ $verbs = $script:SessionWords.verbs
64
+ if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
65
+ if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
66
+
67
+ $clean = $uuid -replace "-", ""
68
+ if ($clean.Length -lt 12) { return "unknown-session" }
69
+
70
+ try {
71
+ $a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
72
+ $n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
73
+ $an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
74
+ return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
75
+ } catch { return "unknown-session" }
76
+ }
77
+
78
+ # ═══════════════════════════════════════════════════════════════════════════
79
+ # READ INPUT
80
+ # ═══════════════════════════════════════════════════════════════════════════
81
+ $inputJson = [Console]::In.ReadToEnd()
82
+ if (-not $inputJson) { exit 0 }
83
+
84
+ try { $input = $inputJson | ConvertFrom-Json } catch { exit 0 }
85
+
86
+ $assistantResponse = $input.response
87
+ if (-not $assistantResponse) { $assistantResponse = $input.message }
88
+ if (-not $assistantResponse) { $assistantResponse = $input.content }
89
+ if (-not $assistantResponse) { exit 0 }
90
+
91
+ $rawSessionId = $input.session_id
92
+ if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
93
+
94
+ if ($rawSessionId -eq "unknown") {
95
+ $stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
96
+ if (Test-Path $stateFile) {
97
+ try {
98
+ $state = Get-Content $stateFile -Raw | ConvertFrom-Json
99
+ $rawSessionId = $state.session_id
100
+ } catch {}
101
+ }
102
+ }
103
+
104
+ $sessionName = Convert-UuidToWords $rawSessionId
105
+
106
+ # ═══════════════════════════════════════════════════════════════════════════
107
+ # READ TURN STATE
108
+ # ═══════════════════════════════════════════════════════════════════════════
109
+ $hookStateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
110
+ $turn = 0
111
+ if (Test-Path $hookStateFile) {
112
+ try {
113
+ $hookState = Get-Content $hookStateFile -Raw | ConvertFrom-Json
114
+ $turn = [int]$hookState.turn
115
+ } catch { $turn = 0 }
116
+ }
117
+
118
+ # ═══════════════════════════════════════════════════════════════════════════
119
+ # PATTERN TRACKING (detect [ekkOS_SELECT] blocks)
120
+ # ═══════════════════════════════════════════════════════════════════════════
121
+ $patternIds = @()
122
+ if ($assistantResponse -match '\[ekkOS_SELECT\]') {
123
+ $selectMatches = [regex]::Matches($assistantResponse, 'id:\s*([a-zA-Z0-9\-_]+)')
124
+ foreach ($match in $selectMatches) {
125
+ $patternIds += $match.Groups[1].Value
126
+ }
127
+ }
128
+
129
+ # ═══════════════════════════════════════════════════════════════════════════
130
+ # LOCAL CACHE: Tier 0 capture (async)
131
+ # ═══════════════════════════════════════════════════════════════════════════
132
+ $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
133
+ if ($captureCmd -and $rawSessionId -ne "unknown") {
134
+ try {
135
+ $responseBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($assistantResponse))
136
+ $toolsJson = "[]"
137
+ $filesJson = "[]"
138
+
139
+ $toolMatches = [regex]::Matches($assistantResponse, '\[TOOL:\s*([^\]]+)\]')
140
+ if ($toolMatches.Count -gt 0) {
141
+ $tools = $toolMatches | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique
142
+ $toolsJson = $tools | ConvertTo-Json -Depth 10 -Compress
143
+ }
144
+
145
+ Start-Job -ScriptBlock {
146
+ param($instanceId, $sessionId, $turnNum, $responseB64, $tools, $files)
147
+ try {
148
+ $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($responseB64))
149
+ & ekkos-capture response $instanceId $sessionId $turnNum $decoded $tools $files 2>&1 | Out-Null
150
+ } catch {}
151
+ } -ArgumentList $EkkosInstanceId, $rawSessionId, $turn, $responseBase64, $toolsJson, $filesJson | Out-Null
152
+ } catch {}
153
+ }
154
+
155
+ # ═══════════════════════════════════════════════════════════════════════════
156
+ # WORKING MEMORY: Fast capture to API (async)
157
+ # ═══════════════════════════════════════════════════════════════════════════
158
+ $configFile = Join-Path $EkkosConfigDir "config.json"
159
+ if (Test-Path $configFile) {
160
+ try {
161
+ $config = Get-Content $configFile -Raw | ConvertFrom-Json
162
+ $captureToken = $config.hookApiKey
163
+ if (-not $captureToken) { $captureToken = $config.apiKey }
164
+
165
+ if ($captureToken) {
166
+ Start-Job -ScriptBlock {
167
+ param($token, $instanceId, $sessionId, $sessionName, $turnNum, $response, $patterns)
168
+ $body = @{
169
+ session_id = $sessionId
170
+ session_name = $sessionName
171
+ instance_id = $instanceId
172
+ turn = $turnNum
173
+ response = $response.Substring(0, [Math]::Min(5000, $response.Length))
174
+ pattern_ids = $patterns
175
+ } | ConvertTo-Json -Depth 10
176
+
177
+ Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/working/turn" `
178
+ -Method POST `
179
+ -Headers @{ Authorization = "Bearer $token" } `
180
+ -ContentType "application/json" `
181
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue
182
+ } -ArgumentList $captureToken, $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $assistantResponse, $patternIds | Out-Null
183
+ }
184
+ } catch {}
185
+ }
186
+
187
+ # Silent exit - assistant-response hook should not produce output
188
+ exit 0
@@ -1,6 +1,14 @@
1
1
  #!/bin/bash
2
- # Post-response hook: Validates and enforces ekkOS footer format
2
+ # ═══════════════════════════════════════════════════════════════════════════
3
+ # ekkOS_ Hook: AssistantResponse - Validates and enforces footer format
4
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
5
+ # EKKOS_MANAGED=1
6
+ # ═══════════════════════════════════════════════════════════════════════════
3
7
  # Runs AFTER assistant response, checks footer compliance
8
+ # Per spec v1.2 Addendum: NO jq, NO hardcoded arrays
9
+ # ═══════════════════════════════════════════════════════════════════════════
10
+
11
+ set +e
4
12
 
5
13
  RESPONSE_FILE="$1"
6
14
  HOOK_ENV="$2"
@@ -10,87 +18,143 @@ if [[ ! -f "$RESPONSE_FILE" ]]; then
10
18
  exit 0
11
19
  fi
12
20
 
13
- # Parse metadata from hook environment
14
- SESSION_ID=$(echo "$HOOK_ENV" | jq -r '.sessionId // "unknown"')
15
- TURN=$(echo "$HOOK_ENV" | jq -r '.turn // 0')
16
- CONTEXT_PERCENT=$(echo "$HOOK_ENV" | jq -r '.contextUsagePercent // 0')
17
- MODEL=$(echo "$HOOK_ENV" | jq -r '.model // "Claude Code (Opus 4.5)"')
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
+ }
18
103
 
19
- # Convert session UUID to word-based name
20
104
  convert_uuid_to_name() {
21
- local uuid="$1"
22
-
23
- # Word lists (same as MCP)
24
- local ADJECTIVES=("cosmic" "turbo" "mega" "hyper" "quantum" "atomic" "stellar" "epic"
25
- "mighty" "groovy" "zippy" "snappy" "jazzy" "funky" "zesty" "peppy"
26
- "spicy" "crispy" "fluffy" "sparkly" "chunky" "bouncy" "bubbly" "sassy"
27
- "slick" "sleek" "bold" "nifty" "perky" "plucky" "witty" "nimble"
28
- "dapper" "fancy" "quirky" "punchy" "swift" "brave" "clever" "dandy"
29
- "eager" "fiery" "golden" "hasty" "icy" "jolly" "keen" "lively"
30
- "merry" "noble" "odd" "plush" "quick" "royal" "silly" "tidy"
31
- "ultra" "vivid" "wacky" "zany" "alpha" "beta" "cyber" "delta"
32
- "electric" "foggy" "giga" "hazy" "ionic" "jumpy" "kinky" "lunar"
33
- "magic" "nerdy" "omega" "pixel" "quaint" "retro" "solar" "techno"
34
- "unified" "viral" "wonky" "xerox" "yappy" "zen" "agile" "binary"
35
- "chrome" "disco" "elastic" "fizzy" "glossy" "humble" "itchy" "jiffy"
36
- "kooky" "loopy" "moody" "noisy")
37
-
38
- local NOUNS=("penguin" "panda" "otter" "narwhal" "alpaca" "llama" "badger" "walrus"
39
- "waffle" "pickle" "noodle" "pretzel" "muffin" "taco" "nugget" "biscuit"
40
- "rocket" "comet" "nebula" "quasar" "meteor" "photon" "pulsar" "nova"
41
- "ninja" "pirate" "wizard" "robot" "yeti" "phoenix" "sphinx" "kraken"
42
- "thunder" "blizzard" "tornado" "avalanche" "mango" "kiwi" "banana" "coconut"
43
- "donut" "espresso" "falafel" "gyro" "hummus" "icecream" "jambon" "kebab"
44
- "latte" "mocha" "nachos" "olive" "pasta" "quinoa" "ramen" "sushi"
45
- "tamale" "udon" "velvet" "wasabi" "xmas" "yogurt" "ziti" "anchor"
46
- "beacon" "canyon" "drifter" "echo" "falcon" "glacier" "harbor" "island"
47
- "jetpack" "kayak" "lagoon" "meadow" "orbit" "parrot" "quest"
48
- "rapids" "summit" "tunnel" "umbrella" "volcano" "whisper" "xylophone" "yacht"
49
- "zephyr" "acorn" "bobcat" "cactus" "dolphin" "eagle" "ferret" "gopher"
50
- "hedgehog" "iguana" "jackal" "koala")
51
-
52
- # Extract first 8 hex chars
53
- local hex="${uuid:0:8}"
54
- hex="${hex//-/}"
55
-
56
- # Convert to number
57
- local num=$((16#$hex))
58
-
59
- # Calculate indices
60
- local adj_idx=$((num % 100))
61
- local noun_idx=$(((num / 100) % 100))
62
-
63
- echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}"
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]}"
64
128
  }
65
129
 
66
130
  SESSION_NAME=$(convert_uuid_to_name "$SESSION_ID")
67
- TIMESTAMP=$(date "+%Y-%m-%d %I:%M %p %Z")
131
+ TIMESTAMP=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
68
132
 
69
133
  # Required footer format
70
134
  REQUIRED_FOOTER="---
71
- $MODEL · $SESSION_NAME · Turn $TURN · ${CONTEXT_PERCENT}% · 🧠 **ekkOS_™** · 📅 $TIMESTAMP"
135
+ $MODEL · 🧠 ekkOS_™ · $SESSION_NAME · 📅 $TIMESTAMP"
72
136
 
73
137
  # Check if response has correct footer
74
138
  RESPONSE_CONTENT=$(cat "$RESPONSE_FILE")
75
139
  LAST_LINE=$(echo "$RESPONSE_CONTENT" | tail -1)
76
140
 
77
141
  # Check if footer exists and is correct
78
- if [[ "$LAST_LINE" == *"ekkOS"* ]] && [[ "$LAST_LINE" == *"Turn"* ]]; then
79
- # Footer exists - validate format
80
- if [[ "$LAST_LINE" == *"Turn $TURN"* ]] && [[ "$LAST_LINE" == *"${CONTEXT_PERCENT}%"* ]] && [[ "$LAST_LINE" == *"$SESSION_NAME"* ]]; then
81
- # Footer is correct
82
- exit 0
83
- else
84
- # Footer exists but is malformed - replace it
85
- RESPONSE_WITHOUT_FOOTER=$(echo "$RESPONSE_CONTENT" | head -n -2) # Remove last 2 lines (--- and footer)
86
- echo "$RESPONSE_WITHOUT_FOOTER" > "$RESPONSE_FILE"
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
87
156
  echo "" >> "$RESPONSE_FILE"
88
157
  echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
89
- fi
90
- else
91
- # Footer missing - append it
92
- echo "" >> "$RESPONSE_FILE"
93
- echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
94
158
  fi
95
159
 
96
160
  exit 0
@@ -17,6 +17,12 @@
17
17
  "type": "command",
18
18
  "command": "bash .claude/hooks/stop.sh"
19
19
  }
20
+ ],
21
+ "AssistantResponse": [
22
+ {
23
+ "type": "command",
24
+ "command": "bash .claude/hooks/assistant-response.sh"
25
+ }
20
26
  ]
21
27
  }
22
28
  }