@ekkos/cli 1.0.34 → 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.
Files changed (44) hide show
  1. package/dist/capture/jsonl-rewriter.js +72 -7
  2. package/dist/commands/dashboard.js +186 -557
  3. package/dist/commands/init.js +3 -15
  4. package/dist/commands/run.js +222 -256
  5. package/dist/commands/setup.js +0 -47
  6. package/dist/commands/swarm-dashboard.js +4 -13
  7. package/dist/deploy/instructions.d.ts +2 -5
  8. package/dist/deploy/instructions.js +8 -11
  9. package/dist/deploy/settings.js +21 -15
  10. package/dist/deploy/skills.d.ts +0 -8
  11. package/dist/deploy/skills.js +0 -26
  12. package/dist/index.js +2 -2
  13. package/dist/lib/usage-parser.js +1 -2
  14. package/dist/utils/platform.d.ts +0 -3
  15. package/dist/utils/platform.js +1 -4
  16. package/dist/utils/session-binding.d.ts +1 -1
  17. package/dist/utils/session-binding.js +2 -3
  18. package/package.json +1 -1
  19. package/templates/agents/README.md +182 -0
  20. package/templates/agents/code-reviewer.md +166 -0
  21. package/templates/agents/debug-detective.md +169 -0
  22. package/templates/agents/ekkOS_Vercel.md +99 -0
  23. package/templates/agents/extension-manager.md +229 -0
  24. package/templates/agents/git-companion.md +185 -0
  25. package/templates/agents/github-test-agent.md +321 -0
  26. package/templates/agents/railway-manager.md +179 -0
  27. package/templates/hooks/assistant-response.ps1 +26 -94
  28. package/templates/hooks/lib/count-tokens.cjs +0 -0
  29. package/templates/hooks/lib/ekkos-reminders.sh +0 -0
  30. package/templates/hooks/session-start.ps1 +224 -61
  31. package/templates/hooks/session-start.sh +1 -1
  32. package/templates/hooks/stop.ps1 +249 -103
  33. package/templates/hooks/stop.sh +1 -1
  34. package/templates/hooks/user-prompt-submit.ps1 +519 -129
  35. package/templates/hooks/user-prompt-submit.sh +2 -2
  36. package/templates/plan-template.md +0 -0
  37. package/templates/spec-template.md +0 -0
  38. package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
  39. package/templates/windsurf-hooks/install.sh +0 -0
  40. package/templates/windsurf-hooks/lib/contract.sh +0 -0
  41. package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
  42. package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
  43. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  44. package/README.md +0 -57
@@ -2,117 +2,66 @@
2
2
  # ekkOS_ Hook: AssistantResponse - Process Claude's response (Windows)
3
3
  # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
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
5
  # ═══════════════════════════════════════════════════════════════════════════
12
6
 
13
7
  $ErrorActionPreference = "SilentlyContinue"
14
8
 
15
9
  # ═══════════════════════════════════════════════════════════════════════════
16
- # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
10
+ # CONFIG PATHS
17
11
  # ═══════════════════════════════════════════════════════════════════════════
18
12
  $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
19
13
  $HooksEnabledJson = "$EkkosConfigDir\hooks-enabled.json"
20
14
  $HooksEnabledDefault = "$EkkosConfigDir\.defaults\hooks-enabled.json"
21
15
  $SessionWordsJson = "$EkkosConfigDir\session-words.json"
22
16
  $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
23
-
24
- # ═══════════════════════════════════════════════════════════════════════════
25
- # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
26
- # ═══════════════════════════════════════════════════════════════════════════
27
17
  $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
28
18
 
19
+ $MemoryApiUrl = "https://api.ekkos.dev"
20
+
29
21
  # ═══════════════════════════════════════════════════════════════════════════
30
- # CHECK ENABLEMENT - Respect hooks-enabled.json
22
+ # CHECK ENABLEMENT
31
23
  # ═══════════════════════════════════════════════════════════════════════════
32
24
  function Test-HookEnabled {
33
25
  param([string]$HookName)
34
-
35
26
  $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
-
27
+ if (-not (Test-Path $enabledFile)) { $enabledFile = $HooksEnabledDefault }
28
+ if (-not (Test-Path $enabledFile)) { return $true }
45
29
  try {
46
30
  $config = Get-Content $enabledFile -Raw | ConvertFrom-Json
47
-
48
- # Check claude.enabled array
49
31
  if ($config.claude -and $config.claude.enabled) {
50
32
  $enabledHooks = $config.claude.enabled
51
- if ($enabledHooks -contains $HookName -or $enabledHooks -contains "assistant-response") {
52
- return $true
53
- }
33
+ if ($enabledHooks -contains $HookName -or $enabledHooks -contains "assistant-response") { return $true }
54
34
  return $false
55
35
  }
56
-
57
- return $true # Default to enabled if no config
58
- } catch {
59
- return $true # Default to enabled on error
60
- }
36
+ return $true
37
+ } catch { return $true }
61
38
  }
62
39
 
63
- # Check if this hook is enabled
64
- if (-not (Test-HookEnabled "assistant-response")) {
65
- exit 0
66
- }
40
+ if (-not (Test-HookEnabled "assistant-response")) { exit 0 }
67
41
 
68
42
  # ═══════════════════════════════════════════════════════════════════════════
69
- # Load session words from JSON file - NO HARDCODED ARRAYS
43
+ # Load session words - NO HARDCODED ARRAYS
70
44
  # ═══════════════════════════════════════════════════════════════════════════
71
45
  $script:SessionWords = $null
72
46
 
73
47
  function Load-SessionWords {
74
48
  $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
-
49
+ if (-not (Test-Path $wordsFile)) { $wordsFile = $SessionWordsDefault }
50
+ if (-not (Test-Path $wordsFile)) { return $null }
85
51
  try {
86
52
  $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
87
- } catch {
88
- return $null
89
- }
53
+ } catch { return $null }
90
54
  }
91
55
 
92
- # ═══════════════════════════════════════════════════════════════════════════
93
- # SESSION NAME (UUID to words) - Uses external session-words.json
94
- # ═══════════════════════════════════════════════════════════════════════════
95
56
  function Convert-UuidToWords {
96
57
  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
- }
58
+ if (-not $script:SessionWords) { Load-SessionWords }
59
+ if (-not $script:SessionWords) { return "unknown-session" }
107
60
 
108
61
  $adjectives = $script:SessionWords.adjectives
109
62
  $nouns = $script:SessionWords.nouns
110
63
  $verbs = $script:SessionWords.verbs
111
-
112
- if (-not $adjectives -or -not $nouns -or -not $verbs) {
113
- return "unknown-session"
114
- }
115
-
64
+ if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
116
65
  if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
117
66
 
118
67
  $clean = $uuid -replace "-", ""
@@ -122,11 +71,8 @@ function Convert-UuidToWords {
122
71
  $a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
123
72
  $n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
124
73
  $an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
125
-
126
74
  return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
127
- } catch {
128
- return "unknown-session"
129
- }
75
+ } catch { return "unknown-session" }
130
76
  }
131
77
 
132
78
  # ═══════════════════════════════════════════════════════════════════════════
@@ -135,23 +81,16 @@ function Convert-UuidToWords {
135
81
  $inputJson = [Console]::In.ReadToEnd()
136
82
  if (-not $inputJson) { exit 0 }
137
83
 
138
- try {
139
- $input = $inputJson | ConvertFrom-Json
140
- } catch {
141
- exit 0
142
- }
84
+ try { $input = $inputJson | ConvertFrom-Json } catch { exit 0 }
143
85
 
144
- # Extract response content
145
86
  $assistantResponse = $input.response
146
87
  if (-not $assistantResponse) { $assistantResponse = $input.message }
147
88
  if (-not $assistantResponse) { $assistantResponse = $input.content }
148
89
  if (-not $assistantResponse) { exit 0 }
149
90
 
150
- # Get session ID
151
91
  $rawSessionId = $input.session_id
152
92
  if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
153
93
 
154
- # Fallback: read session_id from saved state
155
94
  if ($rawSessionId -eq "unknown") {
156
95
  $stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
157
96
  if (Test-Path $stateFile) {
@@ -167,16 +106,13 @@ $sessionName = Convert-UuidToWords $rawSessionId
167
106
  # ═══════════════════════════════════════════════════════════════════════════
168
107
  # READ TURN STATE
169
108
  # ═══════════════════════════════════════════════════════════════════════════
170
- $stateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
109
+ $hookStateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
171
110
  $turn = 0
172
-
173
- if (Test-Path $stateFile) {
111
+ if (Test-Path $hookStateFile) {
174
112
  try {
175
- $hookState = Get-Content $stateFile -Raw | ConvertFrom-Json
113
+ $hookState = Get-Content $hookStateFile -Raw | ConvertFrom-Json
176
114
  $turn = [int]$hookState.turn
177
- } catch {
178
- $turn = 0
179
- }
115
+ } catch { $turn = 0 }
180
116
  }
181
117
 
182
118
  # ═══════════════════════════════════════════════════════════════════════════
@@ -184,7 +120,6 @@ if (Test-Path $stateFile) {
184
120
  # ═══════════════════════════════════════════════════════════════════════════
185
121
  $patternIds = @()
186
122
  if ($assistantResponse -match '\[ekkOS_SELECT\]') {
187
- # Extract pattern IDs from SELECT blocks
188
123
  $selectMatches = [regex]::Matches($assistantResponse, 'id:\s*([a-zA-Z0-9\-_]+)')
189
124
  foreach ($match in $selectMatches) {
190
125
  $patternIds += $match.Groups[1].Value
@@ -192,18 +127,15 @@ if ($assistantResponse -match '\[ekkOS_SELECT\]') {
192
127
  }
193
128
 
194
129
  # ═══════════════════════════════════════════════════════════════════════════
195
- # LOCAL CACHE: Tier 0 capture (async, non-blocking)
196
- # Per v1.2 ADDENDUM: Pass instanceId for namespacing
130
+ # LOCAL CACHE: Tier 0 capture (async)
197
131
  # ═══════════════════════════════════════════════════════════════════════════
198
132
  $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
199
133
  if ($captureCmd -and $rawSessionId -ne "unknown") {
200
134
  try {
201
- # NEW format: ekkos-capture response <instance_id> <session_id> <turn_id> <response> [tools] [files]
202
135
  $responseBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($assistantResponse))
203
136
  $toolsJson = "[]"
204
137
  $filesJson = "[]"
205
138
 
206
- # Extract tools used from response
207
139
  $toolMatches = [regex]::Matches($assistantResponse, '\[TOOL:\s*([^\]]+)\]')
208
140
  if ($toolMatches.Count -gt 0) {
209
141
  $tools = $toolMatches | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique
@@ -221,7 +153,7 @@ if ($captureCmd -and $rawSessionId -ne "unknown") {
221
153
  }
222
154
 
223
155
  # ═══════════════════════════════════════════════════════════════════════════
224
- # WORKING MEMORY: Fast capture to API (async, non-blocking)
156
+ # WORKING MEMORY: Fast capture to API (async)
225
157
  # ═══════════════════════════════════════════════════════════════════════════
226
158
  $configFile = Join-Path $EkkosConfigDir "config.json"
227
159
  if (Test-Path $configFile) {
@@ -242,7 +174,7 @@ if (Test-Path $configFile) {
242
174
  pattern_ids = $patterns
243
175
  } | ConvertTo-Json -Depth 10
244
176
 
245
- Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/turn" `
177
+ Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/working/turn" `
246
178
  -Method POST `
247
179
  -Headers @{ Authorization = "Bearer $token" } `
248
180
  -ContentType "application/json" `
File without changes
File without changes
@@ -1,29 +1,28 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
- # ekkOS_ Hook: SessionStart - Initialize session (Windows)
2
+ # ekkOS_ Hook: SessionStart - MINIMAL + TIME MACHINE + DIRECTIVES (Windows)
3
3
  # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
4
  # EKKOS_MANAGED=1
5
- # EKKOS_MANIFEST_SHA256=<computed-at-build>
6
- # EKKOS_TEMPLATE_VERSION=1.0.0
5
+ # ═══════════════════════════════════════════════════════════════════════════
6
+ # This hook does THREE things:
7
+ # 1. Check for pending Time Machine "Continue from here" requests
8
+ # 2. Initialize session tracking + Golden Loop
9
+ # 3. Fetch and inject user directives (MUST/NEVER/PREFER/AVOID)
7
10
  #
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
+ # Per spec v1.2 Addendum: NO hardcoded arrays, uses session-words.json
11
12
  # ═══════════════════════════════════════════════════════════════════════════
12
13
 
13
14
  $ErrorActionPreference = "SilentlyContinue"
14
15
 
15
16
  # ═══════════════════════════════════════════════════════════════════════════
16
- # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
17
+ # CONFIG PATHS
17
18
  # ═══════════════════════════════════════════════════════════════════════════
18
19
  $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
19
20
  $SessionWordsJson = "$EkkosConfigDir\session-words.json"
20
21
  $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
21
-
22
- # ═══════════════════════════════════════════════════════════════════════════
23
- # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
24
- # ═══════════════════════════════════════════════════════════════════════════
25
22
  $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
26
23
 
24
+ $MemoryApiUrl = "https://api.ekkos.dev"
25
+
27
26
  # ═══════════════════════════════════════════════════════════════════════════
28
27
  # Load session words from JSON file - NO HARDCODED ARRAYS
29
28
  # ═══════════════════════════════════════════════════════════════════════════
@@ -31,41 +30,22 @@ $script:SessionWords = $null
31
30
 
32
31
  function Load-SessionWords {
33
32
  $wordsFile = $SessionWordsJson
34
-
35
- if (-not (Test-Path $wordsFile)) {
36
- $wordsFile = $SessionWordsDefault
37
- }
38
-
39
- if (-not (Test-Path $wordsFile)) {
40
- return $null
41
- }
42
-
33
+ if (-not (Test-Path $wordsFile)) { $wordsFile = $SessionWordsDefault }
34
+ if (-not (Test-Path $wordsFile)) { return $null }
43
35
  try {
44
36
  $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
45
- } catch {
46
- return $null
47
- }
37
+ } catch { return $null }
48
38
  }
49
39
 
50
40
  function Convert-UuidToWords {
51
41
  param([string]$uuid)
52
-
53
- if (-not $script:SessionWords) {
54
- Load-SessionWords
55
- }
56
-
57
- if (-not $script:SessionWords) {
58
- return "unknown-session"
59
- }
42
+ if (-not $script:SessionWords) { Load-SessionWords }
43
+ if (-not $script:SessionWords) { return "unknown-session" }
60
44
 
61
45
  $adjectives = $script:SessionWords.adjectives
62
46
  $nouns = $script:SessionWords.nouns
63
47
  $verbs = $script:SessionWords.verbs
64
-
65
- if (-not $adjectives -or -not $nouns -or -not $verbs) {
66
- return "unknown-session"
67
- }
68
-
48
+ if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
69
49
  if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
70
50
 
71
51
  $clean = $uuid -replace "-", ""
@@ -75,36 +55,116 @@ function Convert-UuidToWords {
75
55
  $a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
76
56
  $n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
77
57
  $an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
78
-
79
58
  return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
80
- } catch {
81
- return "unknown-session"
82
- }
59
+ } catch { return "unknown-session" }
83
60
  }
84
61
 
85
62
  # ═══════════════════════════════════════════════════════════════════════════
86
63
  # READ INPUT
87
64
  # ═══════════════════════════════════════════════════════════════════════════
88
65
  $inputJson = [Console]::In.ReadToEnd()
89
-
90
66
  try {
91
67
  $input = $inputJson | ConvertFrom-Json
92
68
  $sessionId = $input.session_id
93
69
  } catch {
94
70
  $sessionId = "unknown"
95
71
  }
72
+ if (-not $sessionId) { $sessionId = "unknown" }
96
73
 
97
74
  $sessionName = Convert-UuidToWords $sessionId
98
75
 
99
76
  # ═══════════════════════════════════════════════════════════════════════════
100
- # INITIALIZE STATE DIRECTORY
77
+ # LOAD AUTH
78
+ # ═══════════════════════════════════════════════════════════════════════════
79
+ $authToken = ""
80
+ $userId = ""
81
+ $configFile = Join-Path $EkkosConfigDir "config.json"
82
+ if (Test-Path $configFile) {
83
+ try {
84
+ $config = Get-Content $configFile -Raw | ConvertFrom-Json
85
+ $authToken = $config.hookApiKey
86
+ if (-not $authToken) { $authToken = $config.apiKey }
87
+ $userId = $config.userId
88
+ } catch {}
89
+ }
90
+ if (-not $authToken) { exit 0 }
91
+
92
+ # ═══════════════════════════════════════════════════════════════════════════
93
+ # TIME MACHINE: Check for pending "Continue from here" requests
101
94
  # ═══════════════════════════════════════════════════════════════════════════
95
+ $timeMachineSession = ""
96
+ $timeMachineFromTurn = ""
97
+ $timeMachineToTurn = ""
98
+ $restoreRequestId = $env:EKKOS_RESTORE
99
+
100
+ if (-not $timeMachineSession -and $userId) {
101
+ try {
102
+ $headers = @{ "Authorization" = "Bearer $authToken" }
103
+ $pendingResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/context/restore-request/pending?user_id=$userId" `
104
+ -Method GET -Headers $headers -TimeoutSec 3
105
+
106
+ if ($pendingResponse.pending -eq $true -and $pendingResponse.request) {
107
+ $timeMachineSession = $pendingResponse.request.session_id
108
+ $timeMachineFromTurn = $pendingResponse.request.from_turn
109
+ $timeMachineToTurn = $pendingResponse.request.to_turn
110
+ $restoreRequestId = $pendingResponse.request.request_id
111
+
112
+ if ($timeMachineSession) {
113
+ $esc = [char]27
114
+ [Console]::Error.WriteLine("")
115
+ [Console]::Error.WriteLine("${esc}[0;35m------------------------------------------------------------------------${esc}[0m")
116
+ [Console]::Error.WriteLine("${esc}[0;35m${esc}[1m TIME MACHINE${esc}[0m ${esc}[2m| Restoring session from web request...${esc}[0m")
117
+ [Console]::Error.WriteLine("${esc}[0;35m------------------------------------------------------------------------${esc}[0m")
118
+
119
+ $consumeBody = @{ request_id = $restoreRequestId } | ConvertTo-Json -Depth 10
120
+ try {
121
+ Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/context/restore-request/consume" `
122
+ -Method POST -Headers @{ "Authorization" = "Bearer $authToken"; "Content-Type" = "application/json" } `
123
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($consumeBody)) -TimeoutSec 3 | Out-Null
124
+ } catch {}
125
+ }
126
+ }
127
+ } catch {}
128
+ }
129
+
130
+ # ═══════════════════════════════════════════════════════════════════════════
131
+ # SESSION PERSISTENCE - PROJECT-LOCAL for isolation
132
+ # ═══════════════════════════════════════════════════════════════════════════
133
+ $projectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
102
134
  $stateDir = Join-Path $env:USERPROFILE ".claude\state"
103
- if (-not (Test-Path $stateDir)) {
104
- New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
135
+ $projectSessionDir = Join-Path $stateDir "sessions"
136
+
137
+ if (-not (Test-Path $stateDir)) { New-Item -ItemType Directory -Path $stateDir -Force | Out-Null }
138
+ if (-not (Test-Path $projectSessionDir)) { New-Item -ItemType Directory -Path $projectSessionDir -Force | Out-Null }
139
+
140
+ $savedTurnCount = 0
141
+ $mostRecentSession = ""
142
+
143
+ if ($sessionId -ne "unknown") {
144
+ $turnFile = Join-Path $projectSessionDir "$sessionId.turn"
145
+ if (Test-Path $turnFile) {
146
+ try { $savedTurnCount = [int](Get-Content $turnFile -Raw).Trim() } catch { $savedTurnCount = 0 }
147
+ $mostRecentSession = $sessionId
148
+ } else {
149
+ $mostRecentFile = Get-ChildItem "$projectSessionDir\*.turn" -ErrorAction SilentlyContinue |
150
+ Sort-Object LastWriteTime -Descending | Select-Object -First 1
151
+ if ($mostRecentFile) {
152
+ $mostRecentSession = $mostRecentFile.BaseName
153
+ try { $savedTurnCount = [int](Get-Content $mostRecentFile.FullName -Raw).Trim() } catch { $savedTurnCount = 0 }
154
+ }
155
+ }
105
156
  }
106
157
 
107
- # Reset turn counter for new session
158
+ $sessionFile = Join-Path $stateDir "current-session.json"
159
+ $sessionData = @{
160
+ session_id = $sessionId
161
+ session_name = $sessionName
162
+ instance_id = $EkkosInstanceId
163
+ timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
164
+ project_root = $projectRoot
165
+ } | ConvertTo-Json -Depth 10
166
+ Set-Content -Path $sessionFile -Value $sessionData -Force
167
+
108
168
  $stateFile = Join-Path $stateDir "hook-state.json"
109
169
  $state = @{
110
170
  turn = 0
@@ -113,34 +173,137 @@ $state = @{
113
173
  instance_id = $EkkosInstanceId
114
174
  started_at = (Get-Date).ToString("o")
115
175
  } | ConvertTo-Json -Depth 10
116
-
117
176
  Set-Content -Path $stateFile -Value $state -Force
118
177
 
119
- # Save session ID for other hooks
120
- $sessionFile = Join-Path $stateDir "current-session.json"
121
- $sessionData = @{
122
- session_id = $sessionId
123
- session_name = $sessionName
124
- instance_id = $EkkosInstanceId
125
- } | ConvertTo-Json -Depth 10
178
+ # ═══════════════════════════════════════════════════════════════════════════
179
+ # GOLDEN LOOP: Initialize session tracking file
180
+ # ═══════════════════════════════════════════════════════════════════════════
181
+ $ekkosDir = Join-Path $projectRoot ".ekkos"
182
+ if (-not (Test-Path $ekkosDir)) { New-Item -ItemType Directory -Path $ekkosDir -Force | Out-Null }
126
183
 
127
- Set-Content -Path $sessionFile -Value $sessionData -Force
184
+ $goldenLoopFile = Join-Path $ekkosDir "golden-loop-current.json"
185
+ $goldenLoop = @{
186
+ phase = "idle"
187
+ turn = 0
188
+ session = $sessionId
189
+ timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
190
+ stats = @{ retrieved = 0; applied = 0; forged = 0 }
191
+ } | ConvertTo-Json -Depth 10
192
+ Set-Content -Path $goldenLoopFile -Value $goldenLoop -Force
128
193
 
129
194
  # ═══════════════════════════════════════════════════════════════════════════
130
- # LOCAL CACHE: Initialize session in Tier 0 cache
131
- # Per v1.2 ADDENDUM: Pass instanceId for namespacing
195
+ # LOCAL CACHE: Initialize session
132
196
  # ═══════════════════════════════════════════════════════════════════════════
133
197
  $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
134
198
  if ($captureCmd -and $sessionId -ne "unknown") {
135
199
  try {
136
- # NEW format: ekkos-capture init <instance_id> <session_id> <session_name>
137
200
  Start-Job -ScriptBlock {
138
201
  param($instanceId, $sessId, $sessName)
139
- try {
140
- & ekkos-capture init $instanceId $sessId $sessName 2>&1 | Out-Null
141
- } catch {}
202
+ try { & ekkos-capture init $instanceId $sessId $sessName 2>&1 | Out-Null } catch {}
142
203
  } -ArgumentList $EkkosInstanceId, $sessionId, $sessionName | Out-Null
143
204
  } catch {}
144
205
  }
145
206
 
146
- Write-Output "ekkOS session initialized"
207
+ # ═══════════════════════════════════════════════════════════════════════════
208
+ # COLORS
209
+ # ═══════════════════════════════════════════════════════════════════════════
210
+ $esc = [char]27
211
+ $CYAN = "${esc}[0;36m"; $GREEN = "${esc}[0;32m"; $MAGENTA = "${esc}[0;35m"
212
+ $DIM = "${esc}[2m"; $BOLD = "${esc}[1m"; $RESET = "${esc}[0m"
213
+
214
+ # ═══════════════════════════════════════════════════════════════════════════
215
+ # TIME MACHINE RESTORATION
216
+ # ═══════════════════════════════════════════════════════════════════════════
217
+ if ($timeMachineSession) {
218
+ [Console]::Error.WriteLine("")
219
+ $tmPreview = $timeMachineSession.Substring(0, [Math]::Min(12, $timeMachineSession.Length))
220
+ [Console]::Error.WriteLine("${MAGENTA}${BOLD} TIME MACHINE${RESET} ${DIM}| Restoring past session: ${tmPreview}...${RESET}")
221
+
222
+ $recallBody = @{ session_id = $timeMachineSession; last_n = 15; format = "summary" }
223
+ if ($timeMachineFromTurn -and $timeMachineToTurn) {
224
+ $recallBody = @{ session_id = $timeMachineSession; from_turn = [int]$timeMachineFromTurn; to_turn = [int]$timeMachineToTurn; format = "summary" }
225
+ } elseif ($timeMachineFromTurn) {
226
+ $recallBody = @{ session_id = $timeMachineSession; from_turn = [int]$timeMachineFromTurn; format = "summary" }
227
+ }
228
+
229
+ $recallJson = $recallBody | ConvertTo-Json -Depth 10
230
+ try {
231
+ $restoreResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/turns/recall" `
232
+ -Method POST `
233
+ -Headers @{ "Authorization" = "Bearer $authToken"; "Content-Type" = "application/json" } `
234
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($recallJson)) -TimeoutSec 5
235
+
236
+ $restoredCount = ($restoreResponse.turns | Measure-Object).Count
237
+ if ($restoredCount -gt 0) {
238
+ [Console]::Error.WriteLine("${MAGENTA} ✓${RESET} Restored $restoredCount turns from past session")
239
+ [Console]::Error.WriteLine("")
240
+
241
+ $turnsOutput = ""
242
+ foreach ($t in $restoreResponse.turns) {
243
+ $q = if ($t.user_query) { $t.user_query.Substring(0, [Math]::Min(100, $t.user_query.Length)) } else { "..." }
244
+ $r = if ($t.assistant_response) { $t.assistant_response.Substring(0, [Math]::Min(200, $t.assistant_response.Length)) } else { "..." }
245
+ $turnLine = "**Turn $($t.turn_number)**: $q..."
246
+ [Console]::Error.WriteLine($turnLine)
247
+ [Console]::Error.WriteLine("> $r...")
248
+ [Console]::Error.WriteLine("")
249
+ $turnsOutput += "$turnLine`n> $r...`n`n"
250
+ }
251
+ Write-Output $turnsOutput
252
+
253
+ [Console]::Error.WriteLine("${DIM}You've traveled to a past session. Continue from here!${RESET}")
254
+ [Console]::Error.WriteLine("")
255
+ }
256
+ } catch {}
257
+ }
258
+
259
+ # ═══════════════════════════════════════════════════════════════════════════
260
+ # DIRECTIVE RETRIEVAL: Fetch user's MUST/NEVER/PREFER/AVOID rules
261
+ # ═══════════════════════════════════════════════════════════════════════════
262
+ if ($authToken) {
263
+ try {
264
+ $directivesResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/memory/directives?limit=20" `
265
+ -Method GET -Headers @{ "Authorization" = "Bearer $authToken" } -TimeoutSec 3
266
+
267
+ $directiveCount = $directivesResponse.count
268
+ if ($directiveCount -gt 0) {
269
+ Write-Output "<system-reminder>"
270
+ Write-Output "USER DIRECTIVES (FOLLOW THESE):"
271
+ Write-Output ""
272
+
273
+ foreach ($type in @("MUST", "NEVER", "PREFER", "AVOID")) {
274
+ $rules = $directivesResponse.$type
275
+ if ($rules -and ($rules | Measure-Object).Count -gt 0) {
276
+ Write-Output "${type}:"
277
+ $rules | Select-Object -First 5 | ForEach-Object { Write-Output " - $($_.rule)" }
278
+ }
279
+ }
280
+
281
+ Write-Output "</system-reminder>"
282
+ [Console]::Error.WriteLine("${GREEN} $directiveCount directives loaded${RESET}")
283
+ }
284
+ } catch {}
285
+ }
286
+
287
+ # ═══════════════════════════════════════════════════════════════════════════
288
+ # STATUS DISPLAY
289
+ # ═══════════════════════════════════════════════════════════════════════════
290
+ if ($savedTurnCount -gt 0) {
291
+ [Console]::Error.WriteLine("")
292
+ [Console]::Error.WriteLine("${CYAN}${BOLD} ekkOS${RESET} ${DIM}|${RESET} Session: $sessionId ${DIM}|${RESET} ${GREEN}$savedTurnCount turns${RESET}")
293
+ }
294
+
295
+ if ($timeMachineSession) {
296
+ $tmP = $timeMachineSession.Substring(0, [Math]::Min(12, $timeMachineSession.Length))
297
+ [Console]::Error.WriteLine("${MAGENTA}------------------------------------------------------------------------${RESET}")
298
+ [Console]::Error.WriteLine("${MAGENTA} ${RESET} Time Machine active - Restored from session ${tmP}...")
299
+ [Console]::Error.WriteLine("${MAGENTA}------------------------------------------------------------------------${RESET}")
300
+ } elseif ($savedTurnCount -gt 0) {
301
+ [Console]::Error.WriteLine("${GREEN}------------------------------------------------------------------------${RESET}")
302
+ [Console]::Error.WriteLine("${GREEN}✓${RESET} Session continued - $savedTurnCount turns preserved - Ready to resume")
303
+ [Console]::Error.WriteLine("${GREEN}------------------------------------------------------------------------${RESET}")
304
+ } else {
305
+ [Console]::Error.WriteLine("${CYAN}✓${RESET} New session started")
306
+ }
307
+ [Console]::Error.WriteLine("")
308
+
309
+ exit 0
@@ -78,7 +78,7 @@ fi
78
78
 
79
79
  [ -z "$AUTH_TOKEN" ] && exit 0
80
80
 
81
- MEMORY_API_URL="https://mcp.ekkos.dev"
81
+ MEMORY_API_URL="https://api.ekkos.dev"
82
82
 
83
83
  # ═══════════════════════════════════════════════════════════════════════════
84
84
  # TIME MACHINE: Check for pending "Continue from here" requests