@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.
- package/dist/cache/LocalSessionStore.d.ts +34 -21
- package/dist/cache/LocalSessionStore.js +169 -53
- package/dist/cache/capture.d.ts +19 -11
- package/dist/cache/capture.js +243 -76
- package/dist/cache/types.d.ts +14 -1
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +148 -73
- package/dist/commands/hooks.d.ts +109 -0
- package/dist/commands/hooks.js +668 -0
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +69 -21
- package/dist/index.js +42 -1
- package/dist/restore/RestoreOrchestrator.d.ts +17 -3
- package/dist/restore/RestoreOrchestrator.js +64 -22
- package/dist/utils/paths.d.ts +125 -0
- package/dist/utils/paths.js +283 -0
- package/package.json +1 -1
- package/templates/ekkos-manifest.json +223 -0
- package/templates/helpers/json-parse.cjs +101 -0
- package/templates/hooks/assistant-response.ps1 +256 -0
- package/templates/hooks/assistant-response.sh +124 -64
- package/templates/hooks/session-start.ps1 +107 -2
- package/templates/hooks/session-start.sh +201 -166
- package/templates/hooks/stop.ps1 +124 -3
- package/templates/hooks/stop.sh +470 -843
- package/templates/hooks/user-prompt-submit.ps1 +107 -22
- package/templates/hooks/user-prompt-submit.sh +403 -393
- package/templates/project-stubs/session-start.ps1 +63 -0
- package/templates/project-stubs/session-start.sh +55 -0
- package/templates/project-stubs/stop.ps1 +63 -0
- package/templates/project-stubs/stop.sh +55 -0
- package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
- package/templates/project-stubs/user-prompt-submit.sh +55 -0
- package/templates/shared/hooks-enabled.json +22 -0
- 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
|
-
#
|
|
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
|
-
#
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
"
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|