@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
@@ -1,5 +1,6 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
- # ekkOS_ Hook: Stop - Session cleanup and capture finalization (Windows)
2
+ # ekkOS_ Hook: Stop - Captures turns to BOTH Working (Redis) and Episodic (Supabase)
3
+ # NO jq dependency - uses Node.js for all JSON parsing
3
4
  # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
5
  # EKKOS_MANAGED=1
5
6
  # EKKOS_MANIFEST_SHA256=<computed-at-build>
@@ -9,15 +10,32 @@
9
10
  # - All persisted records MUST include: instanceId, sessionId, sessionName
10
11
  # - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
11
12
  # ═══════════════════════════════════════════════════════════════════════════
13
+ # This hook captures every turn to:
14
+ # 1. Working Sessions (Redis) - Fast hot cache for /continue
15
+ # 2. Episodic Memory (Supabase) - Permanent cold storage
16
+ #
17
+ # NO compliance checking - skills handle that
18
+ # NO PatternGuard validation - skills handle that
19
+ # NO verbose output - just capture silently
20
+ # ═══════════════════════════════════════════════════════════════════════════
12
21
 
13
22
  $ErrorActionPreference = "SilentlyContinue"
14
23
 
15
24
  # ═══════════════════════════════════════════════════════════════════════════
16
25
  # CONFIG PATHS
17
26
  # ═══════════════════════════════════════════════════════════════════════════
27
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
28
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
29
+ $StateDir = Join-Path $ProjectRoot ".claude\state"
30
+
31
+ if (-not (Test-Path $StateDir)) {
32
+ New-Item -ItemType Directory -Path $StateDir -Force | Out-Null
33
+ }
34
+
18
35
  $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
19
36
  $SessionWordsJson = "$EkkosConfigDir\session-words.json"
20
37
  $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
38
+ $JsonParseHelper = "$EkkosConfigDir\.helpers\json-parse.cjs"
21
39
 
22
40
  # ═══════════════════════════════════════════════════════════════════════════
23
41
  # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
@@ -87,65 +105,131 @@ function Convert-UuidToWords {
87
105
  # ═══════════════════════════════════════════════════════════════════════════
88
106
  $inputJson = [Console]::In.ReadToEnd()
89
107
 
90
- # Get session ID from state
91
- $stateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
92
- $sessionFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
108
+ # ═══════════════════════════════════════════════════════════════════════════
109
+ # Parse input JSON for session_id, transcript_path, model
110
+ # ═══════════════════════════════════════════════════════════════════════════
93
111
  $rawSessionId = "unknown"
94
- $turn = 0
112
+ $transcriptPath = ""
113
+ $modelUsed = "claude-sonnet-4-5"
95
114
 
96
- if (Test-Path $stateFile) {
115
+ if ($inputJson) {
97
116
  try {
98
- $hookState = Get-Content $stateFile -Raw | ConvertFrom-Json
99
- $rawSessionId = $hookState.session_id
100
- $turn = [int]$hookState.turn
117
+ $stopInput = $inputJson | ConvertFrom-Json
118
+ if ($stopInput.session_id) { $rawSessionId = $stopInput.session_id }
119
+ if ($stopInput.transcript_path) { $transcriptPath = $stopInput.transcript_path }
120
+ if ($stopInput.model) { $modelUsed = $stopInput.model }
101
121
  } catch {}
102
122
  }
103
123
 
104
- if ($rawSessionId -eq "unknown" -and (Test-Path $sessionFile)) {
124
+ # ═══════════════════════════════════════════════════════════════════════════
125
+ # Session ID validation
126
+ # ═══════════════════════════════════════════════════════════════════════════
127
+ if (-not $rawSessionId -or $rawSessionId -eq "unknown" -or $rawSessionId -eq "null") {
128
+ exit 0
129
+ }
130
+
131
+ $isUuid = $rawSessionId -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
132
+
133
+ if ($isUuid) {
134
+ $sessionName = Convert-UuidToWords $rawSessionId
135
+ } else {
136
+ $sessionName = $rawSessionId
137
+ }
138
+
139
+ # ═══════════════════════════════════════════════════════════════════════════
140
+ # Turn Number - read from turn file written by user-prompt-submit.ps1
141
+ # ═══════════════════════════════════════════════════════════════════════════
142
+ $sessionsDir = Join-Path $StateDir "sessions"
143
+ if (-not (Test-Path $sessionsDir)) {
144
+ New-Item -ItemType Directory -Path $sessionsDir -Force | Out-Null
145
+ }
146
+
147
+ $turnFile = Join-Path $sessionsDir "$rawSessionId.turn"
148
+ $turn = 1
149
+ if (Test-Path $turnFile) {
150
+ try {
151
+ $turnContent = Get-Content $turnFile -Raw
152
+ $turn = [int]$turnContent.Trim()
153
+ } catch {
154
+ $turn = 1
155
+ }
156
+ }
157
+
158
+ # ═══════════════════════════════════════════════════════════════════════════
159
+ # Load auth - No jq
160
+ # ═══════════════════════════════════════════════════════════════════════════
161
+ $configFile = Join-Path $EkkosConfigDir "config.json"
162
+ $authToken = ""
163
+ $userId = ""
164
+
165
+ if ((Test-Path $configFile) -and (Test-Path $JsonParseHelper)) {
166
+ try {
167
+ $authToken = & node $JsonParseHelper $configFile '.hookApiKey' 2>$null
168
+ if (-not $authToken) {
169
+ $authToken = & node $JsonParseHelper $configFile '.apiKey' 2>$null
170
+ }
171
+ $userId = & node $JsonParseHelper $configFile '.userId' 2>$null
172
+ } catch {}
173
+ } elseif (Test-Path $configFile) {
105
174
  try {
106
- $sessionData = Get-Content $sessionFile -Raw | ConvertFrom-Json
107
- $rawSessionId = $sessionData.session_id
175
+ $config = Get-Content $configFile -Raw | ConvertFrom-Json
176
+ $authToken = if ($config.hookApiKey) { $config.hookApiKey } else { $config.apiKey }
177
+ $userId = $config.userId
108
178
  } catch {}
109
179
  }
110
180
 
111
- $sessionName = Convert-UuidToWords $rawSessionId
181
+ if (-not $authToken) {
182
+ $envLocal = Join-Path $ProjectRoot ".env.local"
183
+ if (Test-Path $envLocal) {
184
+ try {
185
+ $envContent = Get-Content $envLocal -Raw
186
+ if ($envContent -match 'SUPABASE_SECRET_KEY=["'']?([^"''\r\n]+)') {
187
+ $authToken = $Matches[1]
188
+ }
189
+ } catch {}
190
+ }
191
+ }
192
+
193
+ if (-not $authToken) { exit 0 }
194
+
195
+ $memoryApiUrl = "https://api.ekkos.dev"
112
196
 
113
197
  # ═══════════════════════════════════════════════════════════════════════════
114
198
  # SESSION BINDING: Bridge _pending → real session name for proxy eviction
115
199
  # Windows has no PTY so run.ts can't detect the session name. The stop hook
116
200
  # is the first place we have a confirmed session name, so we bind here.
117
- # Mac does this in stop.sh (lines 171-179). Logic is identical.
118
201
  # ═══════════════════════════════════════════════════════════════════════════
119
- $configFile = Join-Path $EkkosConfigDir "config.json"
120
- if ((Test-Path $configFile) -and $sessionName -ne "unknown-session") {
202
+ if ($sessionName -and $sessionName -ne "unknown-session" -and $userId) {
121
203
  try {
122
- $config = Get-Content $configFile -Raw | ConvertFrom-Json
123
- $userId = $config.userId
124
- $authToken = if ($config.hookApiKey) { $config.hookApiKey } else { $config.apiKey }
204
+ $projectPath = ((Get-Location).Path) -replace '\\', '/'
205
+ $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
125
206
 
126
- if ($userId -and $authToken) {
127
- $projectPath = (Get-Location).Path
128
- $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
129
-
130
- $projectPath = $projectPath -replace '\\', '/'
131
- $bindBody = @{
132
- userId = $userId
133
- realSession = $sessionName
134
- projectPath = $projectPath
135
- pendingSession = $pendingSession
136
- } | ConvertTo-Json -Depth 10
137
-
138
- Start-Job -ScriptBlock {
139
- param($body, $token)
140
- Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
207
+ $bindBody = @{
208
+ userId = $userId
209
+ realSession = $sessionName
210
+ projectPath = $projectPath
211
+ pendingSession = $pendingSession
212
+ } | ConvertTo-Json -Depth 10 -Compress
213
+
214
+ Start-Job -ScriptBlock {
215
+ param($body, $apiUrl)
216
+ try {
217
+ Invoke-RestMethod -Uri "$apiUrl/proxy/session/bind" `
141
218
  -Method POST `
142
219
  -Headers @{ "Content-Type" = "application/json" } `
143
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
144
- } -ArgumentList $bindBody, $authToken | Out-Null
145
- }
220
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
221
+ -TimeoutSec 2 `
222
+ -ErrorAction SilentlyContinue | Out-Null
223
+ } catch {}
224
+ } -ArgumentList $bindBody, $memoryApiUrl | Out-Null
146
225
  } catch {}
147
226
  }
148
227
 
228
+ # ═══════════════════════════════════════════════════════════════════════════
229
+ # EVICTION: Handled by IPC (In-Place Progressive Compression) in the proxy.
230
+ # No hook-side eviction needed - passthrough is default for cache stability.
231
+ # ═══════════════════════════════════════════════════════════════════════════
232
+
149
233
  # ═══════════════════════════════════════════════════════════════════════════
150
234
  # LOCAL CACHE: ACK turn to mark as synced
151
235
  # Per v1.2 ADDENDUM: Pass instanceId for namespacing
@@ -169,15 +253,13 @@ $isInterrupted = $false
169
253
  $stopReason = ""
170
254
  if ($inputJson) {
171
255
  try {
172
- $stopInput = $inputJson | ConvertFrom-Json
173
- $isInterrupted = $stopInput.interrupted -eq $true
174
- $stopReason = $stopInput.stop_reason
256
+ $stopParsed = $inputJson | ConvertFrom-Json
257
+ $isInterrupted = $stopParsed.interrupted -eq $true
258
+ $stopReason = $stopParsed.stop_reason
175
259
  } catch {}
176
260
  }
177
261
 
178
262
  if ($isInterrupted -or $stopReason -eq "user_cancelled" -or $stopReason -eq "interrupted") {
179
- if (Test-Path $stateFile) { Remove-Item $stateFile -Force }
180
- Write-Output "ekkOS session ended (interrupted)"
181
263
  exit 0
182
264
  }
183
265
 
@@ -185,14 +267,6 @@ if ($isInterrupted -or $stopReason -eq "user_cancelled" -or $stopReason -eq "int
185
267
  # EXTRACT CONVERSATION FROM TRANSCRIPT
186
268
  # Mirrors stop.sh: Extract last user query, assistant response, file changes
187
269
  # ═══════════════════════════════════════════════════════════════════════════
188
- $transcriptPath = ""
189
- if ($inputJson) {
190
- try {
191
- $stopInput2 = $inputJson | ConvertFrom-Json
192
- $transcriptPath = $stopInput2.transcript_path
193
- } catch {}
194
- }
195
-
196
270
  $lastUser = ""
197
271
  $lastAssistant = ""
198
272
  $fileChangesJson = "[]"
@@ -228,6 +302,7 @@ for (let i = entries.length - 1; i >= 0; i--) {
228
302
  const parts = content.map(c => {
229
303
  if (c.type === 'text') return c.text;
230
304
  if (c.type === 'tool_use') return '[TOOL: ' + c.name + ']';
305
+ if (c.type === 'thinking') return '[THINKING]' + (c.thinking || c.text || '') + '[/THINKING]';
231
306
  return '';
232
307
  }).filter(Boolean);
233
308
  lastAssistant = parts.join('\n'); break;
@@ -257,7 +332,49 @@ console.log(JSON.stringify({
257
332
  $lastUser = $parsed.user
258
333
  $lastAssistant = $parsed.assistant
259
334
  $fileChangesJson = ($parsed.fileChanges | ConvertTo-Json -Depth 10 -Compress)
260
- if (-not $fileChangesJson) { $fileChangesJson = "[]" }
335
+ if (-not $fileChangesJson -or $fileChangesJson -eq "null") { $fileChangesJson = "[]" }
336
+ }
337
+ } catch {}
338
+ }
339
+
340
+ if (-not $lastUser -or $lastUser -match '\[Request interrupted') {
341
+ exit 0
342
+ }
343
+
344
+ # ═══════════════════════════════════════════════════════════════════════════
345
+ # Extract tools_used and files_referenced from file changes
346
+ # ═══════════════════════════════════════════════════════════════════════════
347
+ $toolsUsed = @()
348
+ $filesReferenced = @()
349
+
350
+ if ($parsed -and $parsed.fileChanges) {
351
+ try {
352
+ $toolsUsed = @($parsed.fileChanges | ForEach-Object { $_.tool } | Where-Object { $_ } | Select-Object -Unique)
353
+ $filesReferenced = @($parsed.fileChanges | ForEach-Object { $_.path } | Where-Object { $_ } | Select-Object -Unique)
354
+ } catch {
355
+ $toolsUsed = @()
356
+ $filesReferenced = @()
357
+ }
358
+ }
359
+
360
+ # ═══════════════════════════════════════════════════════════════════════════
361
+ # Token breakdown from tokenizer script
362
+ # ═══════════════════════════════════════════════════════════════════════════
363
+ $totalTokens = 0
364
+ $inputTokens = 0
365
+ $cacheReadTokens = 0
366
+ $cacheCreationTokens = 0
367
+
368
+ $tokenizerScript = Join-Path $ScriptDir "lib\count-tokens.cjs"
369
+ if ($transcriptPath -and (Test-Path $transcriptPath) -and (Test-Path $tokenizerScript)) {
370
+ try {
371
+ $tokenJson = & node $tokenizerScript $transcriptPath --json 2>$null
372
+ if ($tokenJson) {
373
+ $tokenData = $tokenJson | ConvertFrom-Json
374
+ if ($tokenData.total_tokens) { $totalTokens = [int]$tokenData.total_tokens }
375
+ if ($tokenData.input_tokens) { $inputTokens = [int]$tokenData.input_tokens }
376
+ if ($tokenData.cache_read_tokens) { $cacheReadTokens = [int]$tokenData.cache_read_tokens }
377
+ if ($tokenData.cache_creation_tokens) { $cacheCreationTokens = [int]$tokenData.cache_creation_tokens }
261
378
  }
262
379
  } catch {}
263
380
  }
@@ -267,69 +384,98 @@ console.log(JSON.stringify({
267
384
  # Mirrors stop.sh dual-write at lines 271-356
268
385
  # ═══════════════════════════════════════════════════════════════════════════
269
386
  if ($lastUser -and $lastAssistant -and $authToken) {
270
- $modelUsed = "claude-sonnet-4-5"
271
- if ($inputJson) {
272
- try {
273
- $stopInput3 = $inputJson | ConvertFrom-Json
274
- if ($stopInput3.model) { $modelUsed = $stopInput3.model }
275
- } catch {}
276
- }
277
-
278
387
  $timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
279
388
  $projectPath = ((Get-Location).Path) -replace '\\', '/'
280
389
 
390
+ # Prepare tools_used and files_referenced as JSON strings for passing into jobs
391
+ $toolsUsedJson = ($toolsUsed | ConvertTo-Json -Depth 5 -Compress)
392
+ if (-not $toolsUsedJson -or $toolsUsedJson -eq "null") { $toolsUsedJson = "[]" }
393
+ # Single-element arrays lose their array wrapper in PowerShell ConvertTo-Json
394
+ if ($toolsUsed.Count -eq 1) { $toolsUsedJson = "[$toolsUsedJson]" }
395
+
396
+ $filesRefJson = ($filesReferenced | ConvertTo-Json -Depth 5 -Compress)
397
+ if (-not $filesRefJson -or $filesRefJson -eq "null") { $filesRefJson = "[]" }
398
+ if ($filesReferenced.Count -eq 1) { $filesRefJson = "[$filesRefJson]" }
399
+
281
400
  # 1. WORKING SESSIONS (Redis)
282
401
  Start-Job -ScriptBlock {
283
- param($token, $sessionName, $turnNum, $userQuery, $agentResponse, $model)
284
- $body = @{
285
- session_name = $sessionName
286
- turn_number = $turnNum
287
- user_query = $userQuery
288
- agent_response = $agentResponse.Substring(0, [Math]::Min(50000, $agentResponse.Length))
289
- model = $model
290
- tools_used = @()
291
- files_referenced = @()
292
- } | ConvertTo-Json -Depth 10
293
-
294
- Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/turn" `
295
- -Method POST `
296
- -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
297
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
298
- -ErrorAction SilentlyContinue | Out-Null
299
- } -ArgumentList $authToken, $sessionName, $turn, $lastUser, $lastAssistant, $modelUsed | Out-Null
402
+ param($token, $apiUrl, $sessionName, $turnNum, $userQuery, $agentResponse, $model, $toolsJson, $filesJson, $totalTok, $inputTok, $cacheReadTok, $cacheCreateTok)
403
+ try {
404
+ $truncated = $agentResponse
405
+ if ($agentResponse.Length -gt 50000) {
406
+ $truncated = $agentResponse.Substring(0, 50000)
407
+ }
408
+ $body = @{
409
+ session_name = $sessionName
410
+ turn_number = [int]$turnNum
411
+ user_query = $userQuery
412
+ agent_response = $truncated
413
+ model = $model
414
+ tools_used = @(($toolsJson | ConvertFrom-Json))
415
+ files_referenced = @(($filesJson | ConvertFrom-Json))
416
+ total_context_tokens = [int]$totalTok
417
+ token_breakdown = @{
418
+ input_tokens = [int]$inputTok
419
+ cache_read_tokens = [int]$cacheReadTok
420
+ cache_creation_tokens = [int]$cacheCreateTok
421
+ }
422
+ } | ConvertTo-Json -Depth 10 -Compress
423
+
424
+ Invoke-RestMethod -Uri "$apiUrl/api/v1/working/turn" `
425
+ -Method POST `
426
+ -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
427
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
428
+ -TimeoutSec 5 `
429
+ -ErrorAction SilentlyContinue | Out-Null
430
+ } catch {}
431
+ } -ArgumentList $authToken, $memoryApiUrl, $sessionName, $turn, $lastUser, $lastAssistant, $modelUsed, $toolsUsedJson, $filesRefJson, $totalTokens, $inputTokens, $cacheReadTokens, $cacheCreationTokens | Out-Null
300
432
 
301
433
  # 2. EPISODIC MEMORY (Supabase)
302
434
  Start-Job -ScriptBlock {
303
- param($token, $userQuery, $agentResponse, $sessionId, $userId, $fileChanges, $model, $ts, $turnNum, $sessionName)
304
- $body = @{
305
- user_query = $userQuery
306
- assistant_response = $agentResponse
307
- session_id = $sessionId
308
- user_id = if ($userId) { $userId } else { "system" }
309
- file_changes = @()
310
- metadata = @{
311
- source = "claude-code"
312
- model_used = $model
313
- captured_at = $ts
314
- turn_number = $turnNum
315
- session_name = $sessionName
316
- minimal_hook = $true
317
- }
318
- } | ConvertTo-Json -Depth 10
319
-
320
- Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/memory/capture" `
321
- -Method POST `
322
- -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
323
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
324
- -ErrorAction SilentlyContinue | Out-Null
325
- } -ArgumentList $authToken, $lastUser, $lastAssistant, $rawSessionId, $userId, $fileChangesJson, $modelUsed, $timestamp, $turn, $sessionName | Out-Null
435
+ param($token, $apiUrl, $userQuery, $agentResponse, $sessionId, $uid, $fileChanges, $model, $ts, $turnNum, $sessionName)
436
+ try {
437
+ $fcArray = @()
438
+ try { $fcArray = @(($fileChanges | ConvertFrom-Json)) } catch { $fcArray = @() }
439
+
440
+ $body = @{
441
+ user_query = $userQuery
442
+ assistant_response = $agentResponse
443
+ session_id = $sessionId
444
+ user_id = if ($uid) { $uid } else { "system" }
445
+ file_changes = $fcArray
446
+ metadata = @{
447
+ source = "claude-code"
448
+ model_used = $model
449
+ captured_at = $ts
450
+ turn_number = [int]$turnNum
451
+ session_name = $sessionName
452
+ minimal_hook = $true
453
+ }
454
+ } | ConvertTo-Json -Depth 10 -Compress
455
+
456
+ Invoke-RestMethod -Uri "$apiUrl/api/v1/memory/capture" `
457
+ -Method POST `
458
+ -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
459
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
460
+ -TimeoutSec 5 `
461
+ -ErrorAction SilentlyContinue | Out-Null
462
+ } catch {}
463
+ } -ArgumentList $authToken, $memoryApiUrl, $lastUser, $lastAssistant, $rawSessionId, $userId, $fileChangesJson, $modelUsed, $timestamp, $turn, $sessionName | Out-Null
326
464
  }
327
465
 
328
466
  # ═══════════════════════════════════════════════════════════════════════════
329
- # CLEAN UP STATE FILES
467
+ # Update local .ekkos/current-focus.md (if exists) - SILENT
330
468
  # ═══════════════════════════════════════════════════════════════════════════
331
- if (Test-Path $stateFile) {
332
- Remove-Item $stateFile -Force
469
+ $ekkosDir = Join-Path $ProjectRoot ".ekkos"
470
+ if ((Test-Path $ekkosDir) -and $lastUser) {
471
+ try {
472
+ $focusFile = Join-Path $ekkosDir "current-focus.md"
473
+ $taskSummary = $lastUser.Substring(0, [Math]::Min(100, $lastUser.Length))
474
+ if ($lastUser.Length -gt 100) { $taskSummary += "..." }
475
+ $focusTimestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
476
+ $focusContent = "---`nlast_updated: $focusTimestamp`nsession_id: $rawSessionId`n---`n`n# Current Focus`n$taskSummary"
477
+ Set-Content -Path $focusFile -Value $focusContent -Force
478
+ } catch {}
333
479
  }
334
480
 
335
- Write-Output "ekkOS session ended"
481
+ exit 0
@@ -161,7 +161,7 @@ fi
161
161
 
162
162
  [ -z "$AUTH_TOKEN" ] && exit 0
163
163
 
164
- MEMORY_API_URL="https://mcp.ekkos.dev"
164
+ MEMORY_API_URL="https://api.ekkos.dev"
165
165
 
166
166
  # ═══════════════════════════════════════════════════════════════════════════
167
167
  # SESSION BINDING: Bridge _pending → real session name for proxy eviction