@ekkos/cli 1.0.36 → 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.
@@ -3,12 +3,7 @@
3
3
  # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
4
  # EKKOS_MANAGED=1
5
5
  # EKKOS_MANIFEST_SHA256=<computed-at-build>
6
- # EKKOS_TEMPLATE_VERSION=2.0.0
7
- #
8
- # ZERO USER ACTION NEEDED:
9
- # 1. Tracks turn number and context size
10
- # 2. Detects when compaction happened (context dropped from high to low)
11
- # 3. AUTO-INJECTS restored context - user just keeps working
6
+ # EKKOS_TEMPLATE_VERSION=1.0.0
12
7
  #
13
8
  # Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
14
9
  # - All persisted records MUST include: instanceId, sessionId, sessionName
@@ -17,27 +12,18 @@
17
12
 
18
13
  $ErrorActionPreference = "SilentlyContinue"
19
14
 
20
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
21
- $ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
22
-
23
15
  # ═══════════════════════════════════════════════════════════════════════════
24
16
  # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
25
17
  # ═══════════════════════════════════════════════════════════════════════════
26
18
  $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
27
19
  $SessionWordsJson = "$EkkosConfigDir\session-words.json"
28
20
  $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
29
- $JsonParseHelper = "$EkkosConfigDir\.helpers\json-parse.cjs"
30
21
 
31
22
  # ═══════════════════════════════════════════════════════════════════════════
32
23
  # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
33
24
  # ═══════════════════════════════════════════════════════════════════════════
34
25
  $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
35
26
 
36
- # ═══════════════════════════════════════════════════════════════════════════
37
- # API URL
38
- # ═══════════════════════════════════════════════════════════════════════════
39
- $MemoryApiUrl = "https://api.ekkos.dev"
40
-
41
27
  # ═══════════════════════════════════════════════════════════════════════════
42
28
  # Load session words from JSON file - NO HARDCODED ARRAYS
43
29
  # ═══════════════════════════════════════════════════════════════════════════
@@ -62,9 +48,7 @@ function Load-SessionWords {
62
48
  }
63
49
  }
64
50
 
65
- # ═══════════════════════════════════════════════════════════════════════════
66
51
  # Read input from stdin
67
- # ═══════════════════════════════════════════════════════════════════════════
68
52
  $inputJson = [Console]::In.ReadToEnd()
69
53
  if (-not $inputJson) { exit 0 }
70
54
 
@@ -81,24 +65,15 @@ if (-not $userQuery) { exit 0 }
81
65
 
82
66
  $rawSessionId = $input.session_id
83
67
  if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
84
- $transcriptPath = $input.transcript_path
85
68
 
86
69
  # Fallback: read session_id from saved state
87
- if ($rawSessionId -eq "unknown" -or $rawSessionId -eq "null" -or [string]::IsNullOrEmpty($rawSessionId)) {
70
+ if ($rawSessionId -eq "unknown") {
88
71
  $stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
89
- if ((Test-Path $stateFile) -and (Test-Path $JsonParseHelper)) {
72
+ if (Test-Path $stateFile) {
90
73
  try {
91
- $rawSessionId = node $JsonParseHelper $stateFile '.session_id' 2>$null
92
- if (-not $rawSessionId) { $rawSessionId = "unknown" }
93
- } catch {
94
- $rawSessionId = "unknown"
95
- }
96
- }
97
-
98
- # VSCode extension fallback: Extract session ID from transcript path
99
- # Path format: ~/.claude/projects/<project>/<session-uuid>.jsonl
100
- if ($rawSessionId -eq "unknown" -and $transcriptPath -and (Test-Path $transcriptPath)) {
101
- $rawSessionId = [System.IO.Path]::GetFileNameWithoutExtension($transcriptPath)
74
+ $state = Get-Content $stateFile -Raw | ConvertFrom-Json
75
+ $rawSessionId = $state.session_id
76
+ } catch {}
102
77
  }
103
78
  }
104
79
 
@@ -133,20 +108,6 @@ if ($queryLower -match '(sql|query|supabase|prisma|database|table|column|select
133
108
  $skillReminders += "SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names"
134
109
  }
135
110
 
136
- # ═══════════════════════════════════════════════════════════════════════════
137
- # Load auth - Read from config.json
138
- # ═══════════════════════════════════════════════════════════════════════════
139
- $EkkosConfig = Join-Path $EkkosConfigDir "config.json"
140
- $authToken = ""
141
- if ((Test-Path $EkkosConfig) -and (Test-Path $JsonParseHelper)) {
142
- try {
143
- $authToken = node $JsonParseHelper $EkkosConfig '.hookApiKey' 2>$null
144
- if (-not $authToken) {
145
- $authToken = node $JsonParseHelper $EkkosConfig '.apiKey' 2>$null
146
- }
147
- } catch {}
148
- }
149
-
150
111
  # ═══════════════════════════════════════════════════════════════════════════
151
112
  # SESSION NAME - Resolve early so it's available for all downstream use
152
113
  # ═══════════════════════════════════════════════════════════════════════════
@@ -176,282 +137,158 @@ function Convert-UuidToWords {
176
137
  }
177
138
  }
178
139
 
179
- $sessionId = $rawSessionId
180
- if (-not $sessionId -or $sessionId -eq "unknown" -or $sessionId -eq "null") {
181
- exit 0
182
- }
183
-
184
- # Check if SESSION_ID is a UUID (8-4-4-4-12 format)
185
- $isUuid = $sessionId -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
186
-
187
- $sessionName = ""
188
- if ($isUuid) {
189
- $sessionName = Convert-UuidToWords $sessionId
190
- } else {
191
- $sessionName = $sessionId
192
- }
140
+ $sessionName = Convert-UuidToWords $rawSessionId
193
141
 
194
142
  # ═══════════════════════════════════════════════════════════════════════════
195
- # STATE DIRECTORIES
143
+ # PROXY SESSION BIND: _pending → real session name (fires every turn)
144
+ # Mirrors bash user-prompt-submit.sh lines 319-338.
145
+ # No PTY on Windows so run.ts can't detect session name — hook must bind it.
196
146
  # ═══════════════════════════════════════════════════════════════════════════
197
- $stateDir = Join-Path $ProjectRoot ".claude\state"
198
- if (-not (Test-Path $stateDir)) {
199
- New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
200
- }
201
- $sessionFile = Join-Path $stateDir "current-session.json"
202
-
203
- $projectSessionDir = Join-Path $stateDir "sessions"
204
- if (-not (Test-Path $projectSessionDir)) {
205
- New-Item -ItemType Directory -Path $projectSessionDir -Force | Out-Null
206
- }
207
- $turnCounterFile = Join-Path $projectSessionDir "$sessionId.turn"
208
- $contextSizeFile = Join-Path $projectSessionDir "$sessionId.context"
209
-
210
- # ═══════════════════════════════════════════════════════════════════════════
211
- # Turn counter - TRANSCRIPT-BASED (source of truth)
212
- # Count "type":"user" entries in transcript JSONL
213
- # ═══════════════════════════════════════════════════════════════════════════
214
- $turnNumber = 1
215
- if ($transcriptPath -and (Test-Path $transcriptPath)) {
216
- try {
217
- $turnNumber = node -e "
218
- const fs = require('fs');
219
- const lines = fs.readFileSync(process.argv[1], 'utf8').split('\n').filter(Boolean);
220
- let count = 0;
221
- for (const line of lines) {
222
- try { if (JSON.parse(line).type === 'user') count++; } catch(e) {}
223
- }
224
- console.log(count || 1);
225
- " $transcriptPath 2>$null
226
- $turnNumber = [int]$turnNumber
227
- if ($turnNumber -eq 0) { $turnNumber = 1 }
228
- } catch {
229
- $turnNumber = 1
230
- }
231
- }
232
-
233
- # Detect post-clear: saved count higher than transcript means /clear happened
234
- $savedTurnCount = 0
235
- if (Test-Path $turnCounterFile) {
236
- try {
237
- $savedTurnCount = [int](Get-Content $turnCounterFile -Raw).Trim()
238
- } catch {
239
- $savedTurnCount = 0
147
+ if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
148
+ $configFile = Join-Path $EkkosConfigDir "config.json"
149
+ if (Test-Path $configFile) {
150
+ try {
151
+ $config = Get-Content $configFile -Raw | ConvertFrom-Json
152
+ $userId = $config.userId
153
+ if ($userId) {
154
+ $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
155
+ $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
156
+ $projectPath = $projectPath -replace '\\', '/'
157
+ $bindBody = @{
158
+ userId = $userId
159
+ realSession = $sessionName
160
+ projectPath = $projectPath
161
+ pendingSession = $pendingSession
162
+ } | ConvertTo-Json -Depth 10 -Compress
163
+
164
+ Start-Job -ScriptBlock {
165
+ param($body)
166
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
167
+ -Method POST `
168
+ -Headers @{ "Content-Type" = "application/json" } `
169
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
170
+ } -ArgumentList $bindBody | Out-Null
171
+ }
172
+ } catch {}
240
173
  }
241
174
  }
242
175
 
243
- $postClearDetected = $false
244
- if ($savedTurnCount -gt $turnNumber) {
245
- $postClearDetected = $true
246
- $turnNumber = $savedTurnCount + 1
247
- }
248
-
249
- # Save current turn count
250
- Set-Content -Path $turnCounterFile -Value $turnNumber -Force
251
-
252
176
  # ═══════════════════════════════════════════════════════════════════════════
253
- # Context size tracking - Uses tokenizer script (single source)
177
+ # SESSION CURRENT: Update Redis with current session name
254
178
  # ═══════════════════════════════════════════════════════════════════════════
255
- $prevContextPercent = 0
256
- if (Test-Path $contextSizeFile) {
257
- try {
258
- $prevContextPercent = [int](Get-Content $contextSizeFile -Raw).Trim()
259
- } catch {
260
- $prevContextPercent = 0
179
+ if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
180
+ $configFile2 = Join-Path $EkkosConfigDir "config.json"
181
+ if (Test-Path $configFile2) {
182
+ try {
183
+ $config2 = Get-Content $configFile2 -Raw | ConvertFrom-Json
184
+ $sessionToken = $config2.hookApiKey
185
+ if (-not $sessionToken) { $sessionToken = $config2.apiKey }
186
+ if ($sessionToken) {
187
+ $sessionBody = @{ session_name = $sessionName } | ConvertTo-Json -Depth 10
188
+ Start-Job -ScriptBlock {
189
+ param($body, $token)
190
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/session/current" `
191
+ -Method POST `
192
+ -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
193
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
194
+ } -ArgumentList $sessionBody, $sessionToken | Out-Null
195
+ }
196
+ } catch {}
261
197
  }
262
198
  }
263
199
 
264
- $tokenPercent = 0
265
- $ipcPercent = 0
266
- $maxTokens = 200000
267
- $tokenizerScript = Join-Path $ScriptDir "lib\count-tokens.cjs"
268
-
269
- if ($transcriptPath -and (Test-Path $transcriptPath) -and (Test-Path $tokenizerScript)) {
270
- try {
271
- $tokenCount = node $tokenizerScript $transcriptPath 2>$null
272
- if ($tokenCount -match '^\d+$' -and [int]$tokenCount -gt 0) {
273
- $tokenPercent = [int]([int]$tokenCount * 100 / $maxTokens)
274
- if ($tokenPercent -gt 100) { $tokenPercent = 100 }
275
- # In proxy mode, IPC compresses ~65-70% - show estimated post-compression %
276
- $ipcPercent = [int]($tokenPercent * 30 / 100)
277
- if ($ipcPercent -lt 1) { $ipcPercent = 1 }
278
- }
279
- } catch {}
280
- }
281
-
282
- Set-Content -Path $contextSizeFile -Value $tokenPercent -Force
283
-
284
- # ═══════════════════════════════════════════════════════════════════════════
285
- # COLORS
286
- # ═══════════════════════════════════════════════════════════════════════════
287
- $esc = [char]27
288
- $CYAN = "${esc}[0;36m"
289
- $GREEN = "${esc}[0;32m"
290
- $MAGENTA = "${esc}[0;35m"
291
- $DIM = "${esc}[2m"
292
- $BOLD = "${esc}[1m"
293
- $RESET = "${esc}[0m"
294
-
295
- $timestamp = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt") + " EST"
296
-
297
200
  # ═══════════════════════════════════════════════════════════════════════════
298
- # SINGLE SOURCE OF TRUTH: Update ALL session tracking systems
201
+ # TURN TRACKING & STATE MANAGEMENT
299
202
  # ═══════════════════════════════════════════════════════════════════════════
300
- if ($sessionName -and $sessionName -ne "unknown-session") {
301
- $utcTimestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
302
- $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
303
- $projectPathUnix = $projectPath -replace '\\', '/'
203
+ $stateDir = Join-Path $env:USERPROFILE ".claude\state"
204
+ if (-not (Test-Path $stateDir)) {
205
+ New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
206
+ }
304
207
 
305
- # 1. Project-level state file
306
- try {
307
- $stateJson = @{
308
- session_id = $sessionId
309
- session_name = $sessionName
310
- timestamp = $utcTimestamp
311
- } | ConvertTo-Json -Depth 10 -Compress
312
- Set-Content -Path $sessionFile -Value $stateJson -Force
313
- } catch {}
208
+ $stateFile = Join-Path $stateDir "hook-state.json"
209
+ $turn = 0
210
+ $contextPercent = ""
314
211
 
315
- # 2. Global ekkOS state (for extension LOCAL-FIRST read)
316
- $ekkosGlobalState = Join-Path $EkkosConfigDir "current-session.json"
212
+ if (Test-Path $stateFile) {
317
213
  try {
318
- if (-not (Test-Path $EkkosConfigDir)) {
319
- New-Item -ItemType Directory -Path $EkkosConfigDir -Force | Out-Null
214
+ $hookState = Get-Content $stateFile -Raw | ConvertFrom-Json
215
+ # Only continue incrementing if this state belongs to the SAME session.
216
+ # If session changed, reset turn counter to 0.
217
+ if ($hookState.session_id -eq $rawSessionId) {
218
+ $turn = [int]$hookState.turn + 1
219
+ } else {
220
+ $turn = 0
320
221
  }
321
- $globalJson = @{
322
- session_id = $sessionId
323
- session_name = $sessionName
324
- project = $ProjectRoot
325
- timestamp = $utcTimestamp
326
- } | ConvertTo-Json -Depth 10 -Compress
327
- Set-Content -Path $ekkosGlobalState -Value $globalJson -Force
328
- } catch {}
329
-
330
- # 3. CLI state file
331
- $cliStateFile = Join-Path $EkkosConfigDir "state.json"
332
- try {
333
- $cliJson = @{
334
- sessionId = $sessionId
335
- sessionName = $sessionName
336
- turnNumber = $turnNumber
337
- lastUpdated = $utcTimestamp
338
- projectPath = $ProjectRoot
339
- } | ConvertTo-Json -Depth 10 -Compress
340
- Set-Content -Path $cliStateFile -Value $cliJson -Force
341
- } catch {}
342
-
343
- # 4. Multi-session tracking - upsert into active-sessions.json
344
- $activeSessionsFile = Join-Path $EkkosConfigDir "active-sessions.json"
345
- try {
346
- node -e "
347
- const fs = require('fs');
348
- const sid = process.argv[1], sname = process.argv[2], ts = process.argv[3], proj = process.argv[4];
349
- try {
350
- let sessions = [];
351
- const filePath = process.argv[5];
352
- if (fs.existsSync(filePath)) {
353
- sessions = JSON.parse(fs.readFileSync(filePath, 'utf8') || '[]');
354
- }
355
- const idx = sessions.findIndex(s => s.sessionId === sid);
356
- if (idx >= 0) {
357
- sessions[idx] = {...sessions[idx], sessionName: sname, lastHeartbeat: ts, projectPath: proj};
358
- } else {
359
- sessions.push({sessionId: sid, sessionName: sname, pid: 0, startedAt: ts, projectPath: proj, lastHeartbeat: ts});
360
- }
361
- fs.writeFileSync(filePath, JSON.stringify(sessions, null, 2));
362
- } catch(e) {}
363
- " $sessionId $sessionName $utcTimestamp $ProjectRoot $activeSessionsFile 2>$null
364
- } catch {}
365
-
366
- # 5. Update Redis via API (async)
367
- if ($authToken) {
368
- $sessionBody = @{ session_name = $sessionName } | ConvertTo-Json -Depth 10 -Compress
369
- Start-Job -ScriptBlock {
370
- param($body, $token, $apiUrl)
371
- try {
372
- Invoke-RestMethod -Uri "$apiUrl/api/v1/working/session/current" `
373
- -Method POST `
374
- -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
375
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
376
- -TimeoutSec 3 -ErrorAction SilentlyContinue | Out-Null
377
- } catch {}
378
- } -ArgumentList $sessionBody, $authToken, $MemoryApiUrl | Out-Null
222
+ } catch {
223
+ $turn = 0
379
224
  }
225
+ }
380
226
 
381
- # 6. CRITICAL: Bind session name to proxy for R2 eviction paths
382
- $ekkosUserId = ""
383
- if ((Test-Path $EkkosConfig) -and (Test-Path $JsonParseHelper)) {
384
- try {
385
- $ekkosUserId = node $JsonParseHelper $EkkosConfig '.userId' 2>$null
386
- } catch {}
387
- }
388
- if ($ekkosUserId -and $sessionName) {
389
- $pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
390
- $bindBody = @{
391
- userId = $ekkosUserId
392
- realSession = $sessionName
393
- projectPath = $projectPathUnix
394
- pendingSession = $pendingSession
395
- } | ConvertTo-Json -Depth 10 -Compress
227
+ # Save updated state
228
+ $newState = @{
229
+ turn = $turn
230
+ session_id = $rawSessionId
231
+ last_query = $userQuery.Substring(0, [Math]::Min(100, $userQuery.Length))
232
+ timestamp = (Get-Date).ToString("o")
233
+ } | ConvertTo-Json -Depth 10
396
234
 
397
- Start-Job -ScriptBlock {
398
- param($body, $apiUrl)
399
- try {
400
- Invoke-RestMethod -Uri "$apiUrl/proxy/session/bind" `
401
- -Method POST `
402
- -Headers @{ "Content-Type" = "application/json" } `
403
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
404
- -TimeoutSec 3 -ErrorAction SilentlyContinue | Out-Null
405
- } catch {}
406
- } -ArgumentList $bindBody, $MemoryApiUrl | Out-Null
407
- }
408
- }
235
+ Set-Content -Path $stateFile -Value $newState -Force
409
236
 
410
237
  # ═══════════════════════════════════════════════════════════════════════════
411
238
  # LOCAL CACHE: Tier 0 capture (async, non-blocking)
412
239
  # Per v1.2 ADDENDUM: Pass instanceId for namespacing
413
240
  # ═══════════════════════════════════════════════════════════════════════════
414
241
  $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
415
- if ($captureCmd -and $sessionId -ne "unknown") {
242
+ if ($captureCmd -and $rawSessionId -ne "unknown") {
416
243
  try {
244
+ # NEW format: ekkos-capture user <instance_id> <session_id> <session_name> <turn_id> <query> [project_path]
417
245
  $queryBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userQuery))
418
- $captureProjectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
246
+ $projectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
419
247
 
420
248
  Start-Job -ScriptBlock {
421
- param($instanceId, $sid, $sname, $turnNum, $queryB64, $projPath)
249
+ param($instanceId, $sessionId, $sessionName, $turnNum, $queryB64, $projectPath)
422
250
  try {
423
251
  $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($queryB64))
424
- & ekkos-capture user $instanceId $sid $sname $turnNum $decoded $projPath 2>&1 | Out-Null
252
+ & ekkos-capture user $instanceId $sessionId $sessionName $turnNum $decoded $projectPath 2>&1 | Out-Null
425
253
  } catch {}
426
- } -ArgumentList $EkkosInstanceId, $sessionId, $sessionName, $turnNumber, $queryBase64, $captureProjectRoot | Out-Null
254
+ } -ArgumentList $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $queryBase64, $projectRoot | Out-Null
427
255
  } catch {}
428
256
  }
429
257
 
430
258
  # ═══════════════════════════════════════════════════════════════════════════
431
259
  # WORKING MEMORY: Fast capture to API (async, non-blocking)
432
260
  # ═══════════════════════════════════════════════════════════════════════════
433
- if ($authToken) {
261
+ $configFile = Join-Path $EkkosConfigDir "config.json"
262
+ if (Test-Path $configFile) {
434
263
  try {
435
- Start-Job -ScriptBlock {
436
- param($token, $instanceId, $sid, $sname, $turnNum, $query, $apiUrl)
437
- $body = @{
438
- session_id = $sid
439
- session_name = $sname
440
- instance_id = $instanceId
441
- turn = $turnNum
442
- query = $query
443
- } | ConvertTo-Json -Depth 10
444
- try {
445
- Invoke-RestMethod -Uri "$apiUrl/api/v1/working/fast-capture" `
264
+ $config = Get-Content $configFile -Raw | ConvertFrom-Json
265
+ $captureToken = $config.hookApiKey
266
+ if (-not $captureToken) { $captureToken = $config.apiKey }
267
+
268
+ if ($captureToken) {
269
+ # Async capture using Start-Job (non-blocking)
270
+ Start-Job -ScriptBlock {
271
+ param($token, $instanceId, $sessionId, $sessionName, $turnNum, $query)
272
+ $body = @{
273
+ session_id = $sessionId
274
+ session_name = $sessionName
275
+ instance_id = $instanceId
276
+ turn = $turnNum
277
+ query = $query
278
+ } | ConvertTo-Json -Depth 10
279
+
280
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/fast-capture" `
446
281
  -Method POST `
447
- -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
448
- -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
449
- -TimeoutSec 3 -ErrorAction SilentlyContinue | Out-Null
450
- } catch {}
451
- } -ArgumentList $authToken, $EkkosInstanceId, $sessionId, $sessionName, $turnNumber, $userQuery, $MemoryApiUrl | Out-Null
282
+ -Headers @{ Authorization = "Bearer $token" } `
283
+ -ContentType "application/json" `
284
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue
285
+ } -ArgumentList $captureToken, $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $userQuery | Out-Null
286
+ }
452
287
  } catch {}
453
288
  }
454
289
 
290
+ $timestamp = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt") + " EST"
291
+
455
292
  # ═══════════════════════════════════════════════════════════════════════════
456
293
  # DASHBOARD HINT FILE: write session info for ekkos dashboard --wait-for-new
457
294
  # On Windows, active-sessions.json is never populated (hook PIDs are dead).
@@ -459,12 +296,12 @@ if ($authToken) {
459
296
  # ═══════════════════════════════════════════════════════════════════════════
460
297
  if ($sessionName -ne "unknown-session") {
461
298
  try {
462
- $hintProjectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
299
+ $projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
463
300
  $hintFile = Join-Path $EkkosConfigDir "hook-session-hint.json"
464
301
  $hint = @{
465
302
  sessionName = $sessionName
466
- sessionId = $sessionId
467
- projectPath = $hintProjectPath
303
+ sessionId = $rawSessionId
304
+ projectPath = $projectPath
468
305
  ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
469
306
  } | ConvertTo-Json -Depth 10 -Compress
470
307
  Set-Content -Path $hintFile -Value $hint -Force
@@ -472,251 +309,24 @@ if ($sessionName -ne "unknown-session") {
472
309
  }
473
310
 
474
311
  # ═══════════════════════════════════════════════════════════════════════════
475
- # "/continue" COMMAND: Run AFTER /clear to restore last 5 turns
312
+ # OUTPUT SYSTEM REMINDER
476
313
  # ═══════════════════════════════════════════════════════════════════════════
477
- $queryLowerTrimmed = $queryLower.Trim()
478
-
479
- if ($queryLowerTrimmed -eq "/continue" -or $queryLowerTrimmed -match '^/continue\s' -or $queryLowerTrimmed -eq "continue" -or $queryLowerTrimmed -eq "continue.") {
480
- if ($authToken) {
481
- try {
482
- $restoreBody = @{
483
- session_id = "current"
484
- last_n = 5
485
- format = "detailed"
486
- } | ConvertTo-Json -Depth 10 -Compress
487
- $restoreBodyBytes = [System.Text.Encoding]::UTF8.GetBytes($restoreBody)
488
-
489
- $restoreResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/turns/recall" `
490
- -Method POST `
491
- -Headers @{ Authorization = "Bearer $authToken"; "Content-Type" = "application/json" } `
492
- -Body $restoreBodyBytes `
493
- -TimeoutSec 5 -ErrorAction Stop
494
- } catch {
495
- $restoreResponse = @{ turns = @() }
496
- }
497
-
498
- $restoreJson = $restoreResponse | ConvertTo-Json -Depth 20 -Compress
499
- $restoredCount = 0
500
- $lastTask = "unknown task"
501
- $lastResponse = ""
502
-
503
- try {
504
- $restoredCount = node -e "
505
- const d = JSON.parse(process.argv[1] || '{}');
506
- console.log((d.turns || []).length);
507
- " $restoreJson 2>$null
508
- $restoredCount = [int]$restoredCount
509
- } catch { $restoredCount = 0 }
510
-
511
- try {
512
- $lastTask = node -e "
513
- const d = JSON.parse(process.argv[1] || '{}');
514
- const turns = d.turns || [];
515
- console.log((turns[turns.length-1]?.user_query || 'unknown task').substring(0, 200));
516
- " $restoreJson 2>$null
517
- } catch { $lastTask = "unknown task" }
518
-
519
- try {
520
- $lastResponse = node -e "
521
- const d = JSON.parse(process.argv[1] || '{}');
522
- const turns = d.turns || [];
523
- console.log((turns[turns.length-1]?.assistant_response || '').substring(0, 500));
524
- " $restoreJson 2>$null
525
- } catch { $lastResponse = "" }
526
-
527
- Write-Output ""
528
- Write-Output "${GREEN}${BOLD}Session continued${RESET} ${DIM}(${restoredCount} turns restored)${RESET}"
529
- Write-Output ""
530
-
531
- $continueOutput = @"
532
- <system-reminder>
533
- $("=" * 75)
534
- CONTEXT RESTORED - Resume seamlessly. DO NOT ask 'what were we doing?'
535
- $("=" * 75)
536
-
537
- ## Last User Request:
538
- $lastTask
539
-
540
- ## Your Last Response (truncated):
541
- $lastResponse
542
-
543
- "@
314
+ $esc = [char]27
315
+ $header = "${esc}[0;36m${esc}[1m🧠 ekkOS Memory${esc}[0m ${esc}[2m| $sessionName | $timestamp${esc}[0m"
544
316
 
545
- if ($restoredCount -gt 1) {
546
- $continueOutput += "## Recent Context (older -> newer):`n"
547
- try {
548
- $recentContext = node -e "
549
- const d = JSON.parse(process.argv[1] || '{}');
550
- const turns = d.turns || [];
551
- turns.slice(0, -1).forEach(t => {
552
- const q = (t.user_query || '...').substring(0, 100);
553
- console.log('- Turn ' + (t.turn_number || '?') + ': ' + q + '...');
554
- });
555
- " $restoreJson 2>$null
556
- $continueOutput += "$recentContext`n"
557
- } catch {}
558
- $continueOutput += "`n"
559
- }
317
+ $output = @"
318
+ $header
560
319
 
561
- $continueOutput += @"
562
- $("=" * 75)
563
- INSTRUCTION: Start your response with 'Continuing -' then pick up
564
- exactly where you left off. If mid-task, continue it. If done, ask what's next.
565
- $("=" * 75)
566
- </system-reminder>
567
320
  "@
568
321
 
569
- Write-Output $continueOutput
570
- Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| ${timestamp}${RESET}"
571
- exit 0
572
- }
573
- }
574
-
575
- # ═══════════════════════════════════════════════════════════════════════════
576
- # COMPACTION DETECTION: If context dropped dramatically, auto-restore
577
- # Was >50% last turn, now <15% = compaction happened
578
- # ═══════════════════════════════════════════════════════════════════════════
579
- if ($prevContextPercent -gt 50 -and $tokenPercent -lt 15 -and $authToken) {
580
- Write-Output ""
581
- Write-Output "${GREEN}${BOLD}CONTEXT RESTORED${RESET} ${DIM}| Compaction detected | Auto-loading recent turns...${RESET}"
582
-
583
- try {
584
- $compactBody = @{
585
- session_id = $sessionId
586
- last_n = 10
587
- format = "summary"
588
- } | ConvertTo-Json -Depth 10 -Compress
589
- $compactBodyBytes = [System.Text.Encoding]::UTF8.GetBytes($compactBody)
590
-
591
- $compactResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/turns/recall" `
592
- -Method POST `
593
- -Headers @{ Authorization = "Bearer $authToken"; "Content-Type" = "application/json" } `
594
- -Body $compactBodyBytes `
595
- -TimeoutSec 5 -ErrorAction Stop
596
- } catch {
597
- $compactResponse = @{ turns = @() }
598
- }
599
-
600
- $compactJson = $compactResponse | ConvertTo-Json -Depth 20 -Compress
601
- $compactRestoredCount = 0
602
- try {
603
- $compactRestoredCount = node -e "
604
- const d = JSON.parse(process.argv[1] || '{}');
605
- console.log((d.turns || []).length);
606
- " $compactJson 2>$null
607
- $compactRestoredCount = [int]$compactRestoredCount
608
- } catch { $compactRestoredCount = 0 }
609
-
610
- if ($compactRestoredCount -gt 0) {
611
- Write-Output "${GREEN} Restored ${compactRestoredCount} turns from Layer 2${RESET}"
612
- Write-Output ""
613
- Write-Output "${MAGENTA}${BOLD}## Recent Context (auto-restored)${RESET}"
614
- Write-Output ""
615
-
616
- try {
617
- $compactTurns = node -e "
618
- const d = JSON.parse(process.argv[1] || '{}');
619
- (d.turns || []).forEach(t => {
620
- const q = (t.user_query || '...').substring(0, 120);
621
- const a = (t.assistant_response || '...').substring(0, 250);
622
- console.log('**Turn ' + (t.turn_number || '?') + '**: ' + q + '...\n> ' + a + '...\n');
623
- });
624
- " $compactJson 2>$null
625
- Write-Output $compactTurns
626
- } catch {}
627
-
628
- Write-Output ""
629
- Write-Output "${DIM}Full history: `"turns 1-${turnNumber}`" or `"recall yesterday`"${RESET}"
630
- }
631
-
632
- Write-Output ""
633
- Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| ${sessionName} | ${timestamp}${RESET}"
634
-
635
- } elseif ($postClearDetected -and $authToken) {
636
- # /clear detected - show visible restoration banner
637
- $separator = "${GREEN}" + ("=" * 78) + "${RESET}"
638
-
639
- Write-Output $separator
640
- Write-Output "${GREEN}${BOLD}SESSION CONTINUED${RESET} ${DIM}| ${turnNumber} turns preserved | Restoring context...${RESET}"
641
- Write-Output $separator
642
-
643
- # Also write to stderr for visibility
644
- [Console]::Error.WriteLine("${GREEN}${BOLD}SESSION CONTINUED${RESET} ${DIM}| ${turnNumber} turns preserved | Context restored${RESET}")
645
-
646
- try {
647
- $clearBody = @{
648
- session_id = $sessionId
649
- last_n = 10
650
- format = "summary"
651
- } | ConvertTo-Json -Depth 10 -Compress
652
- $clearBodyBytes = [System.Text.Encoding]::UTF8.GetBytes($clearBody)
653
-
654
- $clearResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/turns/recall" `
655
- -Method POST `
656
- -Headers @{ Authorization = "Bearer $authToken"; "Content-Type" = "application/json" } `
657
- -Body $clearBodyBytes `
658
- -TimeoutSec 5 -ErrorAction Stop
659
- } catch {
660
- $clearResponse = @{ turns = @() }
661
- }
662
-
663
- $clearJson = $clearResponse | ConvertTo-Json -Depth 20 -Compress
664
- $clearRestoredCount = 0
665
- try {
666
- $clearRestoredCount = node -e "
667
- const d = JSON.parse(process.argv[1] || '{}');
668
- console.log((d.turns || []).length);
669
- " $clearJson 2>$null
670
- $clearRestoredCount = [int]$clearRestoredCount
671
- } catch { $clearRestoredCount = 0 }
672
-
673
- if ($clearRestoredCount -gt 0) {
674
- Write-Output "${GREEN} Restored ${clearRestoredCount} recent turns${RESET}"
675
- Write-Output ""
676
-
677
- try {
678
- $clearTurns = node -e "
679
- const d = JSON.parse(process.argv[1] || '{}');
680
- (d.turns || []).forEach(t => {
681
- const q = (t.query_preview || t.user_query || '...').substring(0, 80);
682
- const a = (t.response_preview || t.assistant_response || '...').substring(0, 150);
683
- console.log('**Turn ' + (t.turn_number || '?') + '**: ' + q + '...\n> ' + a + '...\n');
684
- });
685
- " $clearJson 2>$null
686
- Write-Output $clearTurns
687
- } catch {}
688
- } else {
689
- Write-Output "${GREEN} History preserved (${turnNumber} turns)${RESET}"
690
- }
691
-
692
- Write-Output ""
693
- Write-Output "${DIM}Full history: `"recall`" or `"turns 1-${turnNumber}`"${RESET}"
694
- Write-Output $separator
695
- Write-Output ""
696
- Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| ${sessionName} | ${timestamp}${RESET}"
697
-
698
- } elseif ($tokenPercent -ge 50) {
699
- # High context - show IPC percent
700
- Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| ~${ipcPercent}% IPC | ${sessionName} | ${timestamp}${RESET}"
701
-
702
- } else {
703
- # Normal output - session name + timestamp
704
- Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| ${sessionName} | ${timestamp}${RESET}"
705
- }
706
-
707
- # ═══════════════════════════════════════════════════════════════════════════
708
- # Output skill reminders if detected
709
- # ═══════════════════════════════════════════════════════════════════════════
710
322
  if ($skillReminders.Count -gt 0) {
711
- Write-Output ""
712
- Write-Output "${MAGENTA}${BOLD}$($skillReminders -join "`n")${RESET}"
323
+ $output += "${esc}[0;35m${esc}[1m" + ($skillReminders -join "`n") + "${esc}[0m`n"
713
324
  }
714
325
 
715
- # ═══════════════════════════════════════════════════════════════════════════
716
- # FOOTER FORMAT HINT
717
- # ═══════════════════════════════════════════════════════════════════════════
718
- Write-Output ""
719
- Write-Output "<footer-format>End responses with: Claude Code ({Model}) · ekkOS_ · $sessionName · $timestamp</footer-format>"
720
- Write-Output "<footer-note>Do not include a turn counter in the footer.</footer-note>"
326
+ $output += @"
327
+
328
+ <footer-format>End responses with: Claude Code ({Model}) · 🧠 ekkOS_™ · $sessionName · $timestamp</footer-format>
329
+ <footer-note>Do not include a turn counter in the footer.</footer-note>
330
+ "@
721
331
 
722
- exit 0
332
+ Write-Output $output