@ekkos/cli 0.2.9 → 0.2.10

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 (35) hide show
  1. package/dist/cache/LocalSessionStore.d.ts +34 -21
  2. package/dist/cache/LocalSessionStore.js +169 -53
  3. package/dist/cache/capture.d.ts +19 -11
  4. package/dist/cache/capture.js +243 -76
  5. package/dist/cache/types.d.ts +14 -1
  6. package/dist/commands/doctor.d.ts +10 -0
  7. package/dist/commands/doctor.js +148 -73
  8. package/dist/commands/hooks.d.ts +109 -0
  9. package/dist/commands/hooks.js +668 -0
  10. package/dist/commands/run.d.ts +1 -0
  11. package/dist/commands/run.js +69 -21
  12. package/dist/index.js +42 -1
  13. package/dist/restore/RestoreOrchestrator.d.ts +17 -3
  14. package/dist/restore/RestoreOrchestrator.js +64 -22
  15. package/dist/utils/paths.d.ts +125 -0
  16. package/dist/utils/paths.js +283 -0
  17. package/package.json +1 -1
  18. package/templates/ekkos-manifest.json +223 -0
  19. package/templates/helpers/json-parse.cjs +101 -0
  20. package/templates/hooks/assistant-response.ps1 +256 -0
  21. package/templates/hooks/assistant-response.sh +124 -64
  22. package/templates/hooks/session-start.ps1 +107 -2
  23. package/templates/hooks/session-start.sh +201 -166
  24. package/templates/hooks/stop.ps1 +124 -3
  25. package/templates/hooks/stop.sh +470 -843
  26. package/templates/hooks/user-prompt-submit.ps1 +107 -22
  27. package/templates/hooks/user-prompt-submit.sh +403 -393
  28. package/templates/project-stubs/session-start.ps1 +63 -0
  29. package/templates/project-stubs/session-start.sh +55 -0
  30. package/templates/project-stubs/stop.ps1 +63 -0
  31. package/templates/project-stubs/stop.sh +55 -0
  32. package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
  33. package/templates/project-stubs/user-prompt-submit.sh +55 -0
  34. package/templates/shared/hooks-enabled.json +22 -0
  35. package/templates/shared/session-words.json +45 -0
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * json-parse.cjs - Minimal JSON parser for ekkOS hooks
4
+ * Eliminates jq dependency for cross-platform compatibility
5
+ *
6
+ * Usage: json-parse.cjs <file> [path]
7
+ *
8
+ * Examples:
9
+ * json-parse.cjs config.json # Output entire file
10
+ * json-parse.cjs config.json .apiKey # Output single value
11
+ * json-parse.cjs words.json .adjectives # Output array (one per line)
12
+ * json-parse.cjs hooks.json .targets.claude.stop # Nested path
13
+ *
14
+ * Exit codes:
15
+ * 0 - Success (including empty/null result)
16
+ * 1 - Error (file not found, invalid JSON, etc.)
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const args = process.argv.slice(2);
23
+
24
+ if (args.length < 1) {
25
+ console.error('Usage: json-parse.cjs <file> [path]');
26
+ console.error('Example: json-parse.cjs config.json .apiKey');
27
+ process.exit(1);
28
+ }
29
+
30
+ const filePath = args[0];
31
+ const jsonPath = args[1] || null;
32
+
33
+ // Check file exists
34
+ if (!fs.existsSync(filePath)) {
35
+ console.error(`Error: File not found: ${filePath}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ let data;
40
+ try {
41
+ const content = fs.readFileSync(filePath, 'utf8');
42
+ data = JSON.parse(content);
43
+ } catch (err) {
44
+ console.error(`Error: Failed to parse JSON: ${err.message}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // If no path specified, output entire JSON
49
+ if (!jsonPath) {
50
+ console.log(JSON.stringify(data, null, 2));
51
+ process.exit(0);
52
+ }
53
+
54
+ // Parse path and extract value
55
+ // Supports: .foo.bar, .foo[0], .foo.bar[1].baz
56
+ function extractValue(obj, pathStr) {
57
+ if (!pathStr || pathStr === '.') return obj;
58
+
59
+ // Remove leading dot if present
60
+ const cleanPath = pathStr.startsWith('.') ? pathStr.slice(1) : pathStr;
61
+ if (!cleanPath) return obj;
62
+
63
+ // Split on . and [ but keep the brackets for array access
64
+ const parts = cleanPath.split(/\.|\[|\]/).filter(Boolean);
65
+
66
+ let result = obj;
67
+ for (const part of parts) {
68
+ if (result === undefined || result === null) {
69
+ return undefined;
70
+ }
71
+ result = result[part];
72
+ }
73
+ return result;
74
+ }
75
+
76
+ const result = extractValue(data, jsonPath);
77
+
78
+ // Handle different result types
79
+ if (result === undefined || result === null) {
80
+ // Empty output, exit 0 (not an error - just no value)
81
+ process.exit(0);
82
+ }
83
+
84
+ if (Array.isArray(result)) {
85
+ // Output array items one per line (like jq -r '.[]')
86
+ for (const item of result) {
87
+ if (typeof item === 'object') {
88
+ console.log(JSON.stringify(item));
89
+ } else {
90
+ console.log(item);
91
+ }
92
+ }
93
+ } else if (typeof result === 'object') {
94
+ console.log(JSON.stringify(result, null, 2));
95
+ } else if (typeof result === 'boolean') {
96
+ console.log(result ? 'true' : 'false');
97
+ } else {
98
+ console.log(result);
99
+ }
100
+
101
+ process.exit(0);
@@ -0,0 +1,256 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════
2
+ # ekkOS_ Hook: AssistantResponse - Process Claude's response (Windows)
3
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
+ # EKKOS_MANAGED=1
5
+ # EKKOS_MANIFEST_SHA256=<computed-at-build>
6
+ # EKKOS_TEMPLATE_VERSION=1.0.0
7
+ #
8
+ # Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
9
+ # - All persisted records MUST include: instanceId, sessionId, sessionName
10
+ # - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
11
+ # ═══════════════════════════════════════════════════════════════════════════
12
+
13
+ $ErrorActionPreference = "SilentlyContinue"
14
+
15
+ # ═══════════════════════════════════════════════════════════════════════════
16
+ # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
17
+ # ═══════════════════════════════════════════════════════════════════════════
18
+ $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
19
+ $HooksEnabledJson = "$EkkosConfigDir\hooks-enabled.json"
20
+ $HooksEnabledDefault = "$EkkosConfigDir\.defaults\hooks-enabled.json"
21
+ $SessionWordsJson = "$EkkosConfigDir\session-words.json"
22
+ $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
23
+
24
+ # ═══════════════════════════════════════════════════════════════════════════
25
+ # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
26
+ # ═══════════════════════════════════════════════════════════════════════════
27
+ $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
28
+
29
+ # ═══════════════════════════════════════════════════════════════════════════
30
+ # CHECK ENABLEMENT - Respect hooks-enabled.json
31
+ # ═══════════════════════════════════════════════════════════════════════════
32
+ function Test-HookEnabled {
33
+ param([string]$HookName)
34
+
35
+ $enabledFile = $HooksEnabledJson
36
+ if (-not (Test-Path $enabledFile)) {
37
+ $enabledFile = $HooksEnabledDefault
38
+ }
39
+
40
+ if (-not (Test-Path $enabledFile)) {
41
+ # No enablement file = all enabled by default
42
+ return $true
43
+ }
44
+
45
+ try {
46
+ $config = Get-Content $enabledFile -Raw | ConvertFrom-Json
47
+
48
+ # Check claude.enabled array
49
+ if ($config.claude -and $config.claude.enabled) {
50
+ $enabledHooks = $config.claude.enabled
51
+ if ($enabledHooks -contains $HookName -or $enabledHooks -contains "assistant-response") {
52
+ return $true
53
+ }
54
+ return $false
55
+ }
56
+
57
+ return $true # Default to enabled if no config
58
+ } catch {
59
+ return $true # Default to enabled on error
60
+ }
61
+ }
62
+
63
+ # Check if this hook is enabled
64
+ if (-not (Test-HookEnabled "assistant-response")) {
65
+ exit 0
66
+ }
67
+
68
+ # ═══════════════════════════════════════════════════════════════════════════
69
+ # Load session words from JSON file - NO HARDCODED ARRAYS
70
+ # ═══════════════════════════════════════════════════════════════════════════
71
+ $script:SessionWords = $null
72
+
73
+ function Load-SessionWords {
74
+ $wordsFile = $SessionWordsJson
75
+
76
+ # Fallback to managed defaults if user file missing/invalid
77
+ if (-not (Test-Path $wordsFile)) {
78
+ $wordsFile = $SessionWordsDefault
79
+ }
80
+
81
+ if (-not (Test-Path $wordsFile)) {
82
+ return $null
83
+ }
84
+
85
+ try {
86
+ $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
87
+ } catch {
88
+ return $null
89
+ }
90
+ }
91
+
92
+ # ═══════════════════════════════════════════════════════════════════════════
93
+ # SESSION NAME (UUID to words) - Uses external session-words.json
94
+ # ═══════════════════════════════════════════════════════════════════════════
95
+ function Convert-UuidToWords {
96
+ param([string]$uuid)
97
+
98
+ # Load session words if not already loaded
99
+ if (-not $script:SessionWords) {
100
+ Load-SessionWords
101
+ }
102
+
103
+ # Handle missing session words gracefully
104
+ if (-not $script:SessionWords) {
105
+ return "unknown-session"
106
+ }
107
+
108
+ $adjectives = $script:SessionWords.adjectives
109
+ $nouns = $script:SessionWords.nouns
110
+ $verbs = $script:SessionWords.verbs
111
+
112
+ if (-not $adjectives -or -not $nouns -or -not $verbs) {
113
+ return "unknown-session"
114
+ }
115
+
116
+ if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
117
+
118
+ $clean = $uuid -replace "-", ""
119
+ if ($clean.Length -lt 6) { return "unknown-session" }
120
+
121
+ try {
122
+ $a = [Convert]::ToInt32($clean.Substring(0,2), 16) % $adjectives.Length
123
+ $n = [Convert]::ToInt32($clean.Substring(2,2), 16) % $nouns.Length
124
+ $an = [Convert]::ToInt32($clean.Substring(4,2), 16) % $verbs.Length
125
+
126
+ return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
127
+ } catch {
128
+ return "unknown-session"
129
+ }
130
+ }
131
+
132
+ # ═══════════════════════════════════════════════════════════════════════════
133
+ # READ INPUT
134
+ # ═══════════════════════════════════════════════════════════════════════════
135
+ $inputJson = [Console]::In.ReadToEnd()
136
+ if (-not $inputJson) { exit 0 }
137
+
138
+ try {
139
+ $input = $inputJson | ConvertFrom-Json
140
+ } catch {
141
+ exit 0
142
+ }
143
+
144
+ # Extract response content
145
+ $assistantResponse = $input.response
146
+ if (-not $assistantResponse) { $assistantResponse = $input.message }
147
+ if (-not $assistantResponse) { $assistantResponse = $input.content }
148
+ if (-not $assistantResponse) { exit 0 }
149
+
150
+ # Get session ID
151
+ $rawSessionId = $input.session_id
152
+ if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
153
+
154
+ # Fallback: read session_id from saved state
155
+ if ($rawSessionId -eq "unknown") {
156
+ $stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
157
+ if (Test-Path $stateFile) {
158
+ try {
159
+ $state = Get-Content $stateFile -Raw | ConvertFrom-Json
160
+ $rawSessionId = $state.session_id
161
+ } catch {}
162
+ }
163
+ }
164
+
165
+ $sessionName = Convert-UuidToWords $rawSessionId
166
+
167
+ # ═══════════════════════════════════════════════════════════════════════════
168
+ # READ TURN STATE
169
+ # ═══════════════════════════════════════════════════════════════════════════
170
+ $stateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
171
+ $turn = 0
172
+
173
+ if (Test-Path $stateFile) {
174
+ try {
175
+ $hookState = Get-Content $stateFile -Raw | ConvertFrom-Json
176
+ $turn = [int]$hookState.turn
177
+ } catch {
178
+ $turn = 0
179
+ }
180
+ }
181
+
182
+ # ═══════════════════════════════════════════════════════════════════════════
183
+ # PATTERN TRACKING (detect [ekkOS_SELECT] blocks)
184
+ # ═══════════════════════════════════════════════════════════════════════════
185
+ $patternIds = @()
186
+ if ($assistantResponse -match '\[ekkOS_SELECT\]') {
187
+ # Extract pattern IDs from SELECT blocks
188
+ $selectMatches = [regex]::Matches($assistantResponse, 'id:\s*([a-zA-Z0-9\-_]+)')
189
+ foreach ($match in $selectMatches) {
190
+ $patternIds += $match.Groups[1].Value
191
+ }
192
+ }
193
+
194
+ # ═══════════════════════════════════════════════════════════════════════════
195
+ # LOCAL CACHE: Tier 0 capture (async, non-blocking)
196
+ # Per v1.2 ADDENDUM: Pass instanceId for namespacing
197
+ # ═══════════════════════════════════════════════════════════════════════════
198
+ $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
199
+ if ($captureCmd -and $rawSessionId -ne "unknown") {
200
+ try {
201
+ # NEW format: ekkos-capture response <instance_id> <session_id> <turn_id> <response> [tools] [files]
202
+ $responseBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($assistantResponse))
203
+ $toolsJson = "[]"
204
+ $filesJson = "[]"
205
+
206
+ # Extract tools used from response
207
+ $toolMatches = [regex]::Matches($assistantResponse, '\[TOOL:\s*([^\]]+)\]')
208
+ if ($toolMatches.Count -gt 0) {
209
+ $tools = $toolMatches | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique
210
+ $toolsJson = $tools | ConvertTo-Json -Compress
211
+ }
212
+
213
+ Start-Job -ScriptBlock {
214
+ param($instanceId, $sessionId, $turnNum, $responseB64, $tools, $files)
215
+ try {
216
+ $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($responseB64))
217
+ & ekkos-capture response $instanceId $sessionId $turnNum $decoded $tools $files 2>&1 | Out-Null
218
+ } catch {}
219
+ } -ArgumentList $EkkosInstanceId, $rawSessionId, $turn, $responseBase64, $toolsJson, $filesJson | Out-Null
220
+ } catch {}
221
+ }
222
+
223
+ # ═══════════════════════════════════════════════════════════════════════════
224
+ # WORKING MEMORY: Fast capture to API (async, non-blocking)
225
+ # ═══════════════════════════════════════════════════════════════════════════
226
+ $configFile = Join-Path $EkkosConfigDir "config.json"
227
+ if (Test-Path $configFile) {
228
+ try {
229
+ $config = Get-Content $configFile -Raw | ConvertFrom-Json
230
+ $captureToken = $config.hookApiKey
231
+ if (-not $captureToken) { $captureToken = $config.apiKey }
232
+
233
+ if ($captureToken) {
234
+ Start-Job -ScriptBlock {
235
+ param($token, $instanceId, $sessionId, $sessionName, $turnNum, $response, $patterns)
236
+ $body = @{
237
+ session_id = $sessionId
238
+ session_name = $sessionName
239
+ instance_id = $instanceId
240
+ turn = $turnNum
241
+ response = $response.Substring(0, [Math]::Min(5000, $response.Length))
242
+ pattern_ids = $patterns
243
+ } | ConvertTo-Json
244
+
245
+ Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/working/turn" `
246
+ -Method POST `
247
+ -Headers @{ Authorization = "Bearer $token" } `
248
+ -ContentType "application/json" `
249
+ -Body $body -ErrorAction SilentlyContinue
250
+ } -ArgumentList $captureToken, $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $assistantResponse, $patternIds | Out-Null
251
+ }
252
+ } catch {}
253
+ }
254
+
255
+ # Silent exit - assistant-response hook should not produce output
256
+ 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,57 +18,109 @@ 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
+ TURN=$(parse_hook_env "$HOOK_ENV" '.turn')
49
+ [ -z "$TURN" ] && TURN="0"
50
+
51
+ CONTEXT_PERCENT=$(parse_hook_env "$HOOK_ENV" '.contextUsagePercent')
52
+ [ -z "$CONTEXT_PERCENT" ] && CONTEXT_PERCENT="0"
53
+
54
+ MODEL=$(parse_hook_env "$HOOK_ENV" '.model')
55
+ [ -z "$MODEL" ] && MODEL="Claude Code (Opus 4.5)"
56
+
57
+ # ═══════════════════════════════════════════════════════════════════════════
58
+ # Session name conversion - Uses external session-words.json (NO hardcoded arrays)
59
+ # ═══════════════════════════════════════════════════════════════════════════
60
+ declare -a ADJECTIVES
61
+ declare -a NOUNS
62
+ SESSION_WORDS_LOADED=false
63
+
64
+ load_session_words() {
65
+ if [ "$SESSION_WORDS_LOADED" = "true" ]; then
66
+ return 0
67
+ fi
68
+
69
+ local words_file="$SESSION_WORDS_JSON"
70
+ if [ ! -f "$words_file" ]; then
71
+ words_file="$SESSION_WORDS_DEFAULT"
72
+ fi
73
+
74
+ if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
75
+ return 1
76
+ fi
77
+
78
+ if command -v node &>/dev/null; then
79
+ if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
80
+ readarray -t ADJECTIVES < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
81
+ readarray -t NOUNS < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
82
+ else
83
+ local i=0
84
+ while IFS= read -r line; do
85
+ ADJECTIVES[i]="$line"
86
+ ((i++))
87
+ done < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
88
+ i=0
89
+ while IFS= read -r line; do
90
+ NOUNS[i]="$line"
91
+ ((i++))
92
+ done < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
93
+ fi
94
+
95
+ if [ ${#ADJECTIVES[@]} -gt 0 ] && [ ${#NOUNS[@]} -gt 0 ]; then
96
+ SESSION_WORDS_LOADED=true
97
+ return 0
98
+ fi
99
+ fi
100
+ return 1
101
+ }
18
102
 
19
- # Convert session UUID to word-based name
20
103
  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]}"
104
+ local uuid="$1"
105
+
106
+ load_session_words || {
107
+ echo "unknown-session"
108
+ return
109
+ }
110
+
111
+ local hex="${uuid:0:8}"
112
+ hex="${hex//-/}"
113
+
114
+ if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
115
+ echo "unknown-session"
116
+ return
117
+ fi
118
+
119
+ local num=$((16#$hex))
120
+ local adj_idx=$((num % ${#ADJECTIVES[@]}))
121
+ local noun_idx=$(((num / ${#ADJECTIVES[@]}) % ${#NOUNS[@]}))
122
+
123
+ echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}"
64
124
  }
65
125
 
66
126
  SESSION_NAME=$(convert_uuid_to_name "$SESSION_ID")
@@ -68,7 +128,7 @@ TIMESTAMP=$(date "+%Y-%m-%d %I:%M %p %Z")
68
128
 
69
129
  # Required footer format
70
130
  REQUIRED_FOOTER="---
71
- $MODEL · $SESSION_NAME · Turn $TURN · ${CONTEXT_PERCENT}% · 🧠 **ekkOS_™** · 📅 $TIMESTAMP"
131
+ $MODEL - $SESSION_NAME - Turn $TURN - ${CONTEXT_PERCENT}% - ekkOS - $TIMESTAMP"
72
132
 
73
133
  # Check if response has correct footer
74
134
  RESPONSE_CONTENT=$(cat "$RESPONSE_FILE")
@@ -76,21 +136,21 @@ LAST_LINE=$(echo "$RESPONSE_CONTENT" | tail -1)
76
136
 
77
137
  # Check if footer exists and is correct
78
138
  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"
139
+ # Footer exists - validate format
140
+ if [[ "$LAST_LINE" == *"Turn $TURN"* ]] && [[ "$LAST_LINE" == *"${CONTEXT_PERCENT}%"* ]] && [[ "$LAST_LINE" == *"$SESSION_NAME"* ]]; then
141
+ # Footer is correct
142
+ exit 0
143
+ else
144
+ # Footer exists but is malformed - replace it
145
+ RESPONSE_WITHOUT_FOOTER=$(echo "$RESPONSE_CONTENT" | head -n -2)
146
+ echo "$RESPONSE_WITHOUT_FOOTER" > "$RESPONSE_FILE"
147
+ echo "" >> "$RESPONSE_FILE"
148
+ echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
149
+ fi
150
+ else
151
+ # Footer missing - append it
87
152
  echo "" >> "$RESPONSE_FILE"
88
153
  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
154
  fi
95
155
 
96
156
  exit 0