@ekkos/cli 1.0.35 → 1.1.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 (36) hide show
  1. package/README.md +57 -0
  2. package/dist/commands/dashboard.js +561 -186
  3. package/dist/deploy/settings.js +13 -26
  4. package/package.json +2 -4
  5. package/templates/CLAUDE.md +135 -23
  6. package/templates/ekkos-manifest.json +8 -8
  7. package/templates/hooks/assistant-response.ps1 +256 -160
  8. package/templates/hooks/assistant-response.sh +130 -66
  9. package/templates/hooks/hooks.json +24 -6
  10. package/templates/hooks/lib/contract.sh +43 -31
  11. package/templates/hooks/lib/count-tokens.cjs +0 -0
  12. package/templates/hooks/lib/ekkos-reminders.sh +0 -0
  13. package/templates/hooks/lib/state.sh +53 -1
  14. package/templates/hooks/session-start.ps1 +91 -391
  15. package/templates/hooks/session-start.sh +201 -166
  16. package/templates/hooks/stop.ps1 +202 -341
  17. package/templates/hooks/stop.sh +275 -948
  18. package/templates/hooks/user-prompt-submit.ps1 +224 -548
  19. package/templates/hooks/user-prompt-submit.sh +382 -456
  20. package/templates/plan-template.md +0 -0
  21. package/templates/spec-template.md +0 -0
  22. package/templates/windsurf-hooks/hooks.json +9 -2
  23. package/templates/windsurf-hooks/install.sh +0 -0
  24. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  25. package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
  26. package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
  27. package/templates/agents/README.md +0 -182
  28. package/templates/agents/code-reviewer.md +0 -166
  29. package/templates/agents/debug-detective.md +0 -169
  30. package/templates/agents/ekkOS_Vercel.md +0 -99
  31. package/templates/agents/extension-manager.md +0 -229
  32. package/templates/agents/git-companion.md +0 -185
  33. package/templates/agents/github-test-agent.md +0 -321
  34. package/templates/agents/railway-manager.md +0 -179
  35. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
  36. package/templates/windsurf-skills/ekkos-memory/SKILL.md +0 -219
@@ -1,160 +1,256 @@
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
+ # 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 12) { return "unknown-session" }
120
+
121
+ try {
122
+ $a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
123
+ $n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
124
+ $an = [Convert]::ToInt32($clean.Substring(8,4), 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 -Depth 10 -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 -Depth 10
244
+
245
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/turn" `
246
+ -Method POST `
247
+ -Headers @{ Authorization = "Bearer $token" } `
248
+ -ContentType "application/json" `
249
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($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