@ekkos/cli 1.0.33 → 1.0.35

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 (51) 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 +221 -259
  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 +4 -2
  19. package/templates/CLAUDE.md +23 -135
  20. package/templates/agents/README.md +182 -0
  21. package/templates/agents/code-reviewer.md +166 -0
  22. package/templates/agents/debug-detective.md +169 -0
  23. package/templates/agents/ekkOS_Vercel.md +99 -0
  24. package/templates/agents/extension-manager.md +229 -0
  25. package/templates/agents/git-companion.md +185 -0
  26. package/templates/agents/github-test-agent.md +321 -0
  27. package/templates/agents/railway-manager.md +179 -0
  28. package/templates/ekkos-manifest.json +8 -8
  29. package/templates/hooks/assistant-response.ps1 +160 -256
  30. package/templates/hooks/assistant-response.sh +66 -130
  31. package/templates/hooks/hooks.json +0 -6
  32. package/templates/hooks/lib/contract.sh +31 -43
  33. package/templates/hooks/lib/count-tokens.cjs +0 -0
  34. package/templates/hooks/lib/ekkos-reminders.sh +0 -0
  35. package/templates/hooks/lib/state.sh +1 -53
  36. package/templates/hooks/session-start.ps1 +391 -91
  37. package/templates/hooks/session-start.sh +166 -201
  38. package/templates/hooks/stop.ps1 +341 -202
  39. package/templates/hooks/stop.sh +948 -275
  40. package/templates/hooks/user-prompt-submit.ps1 +548 -224
  41. package/templates/hooks/user-prompt-submit.sh +456 -382
  42. package/templates/plan-template.md +0 -0
  43. package/templates/spec-template.md +0 -0
  44. package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
  45. package/templates/windsurf-hooks/hooks.json +2 -9
  46. package/templates/windsurf-hooks/install.sh +0 -0
  47. package/templates/windsurf-hooks/lib/contract.sh +0 -2
  48. package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
  49. package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
  50. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  51. package/README.md +0 -57
@@ -1,146 +1,446 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
- # ekkOS_ Hook: SessionStart - Initialize session (Windows)
2
+ # ekkOS_ Hook: SessionStart - MINIMAL + AUTO-RESTORE + TIME MACHINE CONTINUE
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
9
+ # 3. Auto-restore from L2 if recent turns exist (FAST TRIM support)
10
+ #
11
+ # TIME MACHINE FLOW:
12
+ # User clicks "Continue from here" on web -> API queues request ->
13
+ # User runs `claude` -> This hook detects pending request ->
14
+ # Restores THAT session's context -> Seamless time travel!
15
+ #
16
+ # FAST TRIM FLOW:
17
+ # User runs /clear -> session-start detects fresh session ->
18
+ # Checks L2 for recent turns -> Auto-injects last 15 turns -> Seamless continuity
7
19
  #
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
20
+ # Per spec v1.2 Addendum: NO jq dependency
11
21
  # ═══════════════════════════════════════════════════════════════════════════
12
22
 
13
23
  $ErrorActionPreference = "SilentlyContinue"
14
24
 
25
+ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
26
+ $ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
27
+
15
28
  # ═══════════════════════════════════════════════════════════════════════════
16
- # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
29
+ # CONFIG PATHS - Per spec v1.2 Addendum
17
30
  # ═══════════════════════════════════════════════════════════════════════════
18
- $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
19
- $SessionWordsJson = "$EkkosConfigDir\session-words.json"
20
- $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
31
+ $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { Join-Path $env:USERPROFILE ".ekkos" }
32
+ $JsonParseHelper = Join-Path $EkkosConfigDir ".helpers\json-parse.cjs"
33
+
34
+ $INPUT = [Console]::In.ReadToEnd()
21
35
 
22
36
  # ═══════════════════════════════════════════════════════════════════════════
23
- # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
37
+ # JSON parsing helper (no jq) - pipes JSON to node, extracts dot-path value
24
38
  # ═══════════════════════════════════════════════════════════════════════════
25
- $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
39
+ function Parse-JsonValue {
40
+ param(
41
+ [string]$Json,
42
+ [string]$Path
43
+ )
44
+ if (-not $Json) { return "" }
45
+ try {
46
+ $result = $Json | node -e "
47
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
48
+ const path = '$Path'.replace(/^\./,'').split('.').filter(Boolean);
49
+ let result = data;
50
+ for (const p of path) {
51
+ if (result === undefined || result === null) { result = undefined; break; }
52
+ result = result[p];
53
+ }
54
+ if (result !== undefined && result !== null) console.log(result);
55
+ " 2>$null
56
+ if ($result) { return $result.Trim() } else { return "" }
57
+ } catch {
58
+ return ""
59
+ }
60
+ }
61
+
62
+ $SESSION_ID = Parse-JsonValue $INPUT ".session_id"
63
+ if (-not $SESSION_ID) { $SESSION_ID = "unknown" }
64
+
65
+ $TRANSCRIPT_PATH = Parse-JsonValue $INPUT ".transcript_path"
66
+ $SOURCE = Parse-JsonValue $INPUT ".source"
67
+ if (-not $SOURCE) { $SOURCE = "unknown" }
26
68
 
27
69
  # ═══════════════════════════════════════════════════════════════════════════
28
- # Load session words from JSON file - NO HARDCODED ARRAYS
70
+ # Load auth
29
71
  # ═══════════════════════════════════════════════════════════════════════════
30
- $script:SessionWords = $null
31
-
32
- function Load-SessionWords {
33
- $wordsFile = $SessionWordsJson
72
+ $EKKOS_CONFIG = Join-Path $env:USERPROFILE ".ekkos\config.json"
73
+ $AUTH_TOKEN = ""
74
+ $USER_ID = ""
34
75
 
35
- if (-not (Test-Path $wordsFile)) {
36
- $wordsFile = $SessionWordsDefault
76
+ if ((Test-Path $EKKOS_CONFIG) -and (Test-Path $JsonParseHelper)) {
77
+ $AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
78
+ if (-not $AUTH_TOKEN) {
79
+ $AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".apiKey" 2>$null
37
80
  }
81
+ $USER_ID = & node $JsonParseHelper $EKKOS_CONFIG ".userId" 2>$null
82
+ }
38
83
 
39
- if (-not (Test-Path $wordsFile)) {
40
- return $null
84
+ if (-not $AUTH_TOKEN) {
85
+ $envLocalFile = Join-Path $ProjectRoot ".env.local"
86
+ if (Test-Path $envLocalFile) {
87
+ $envLines = Get-Content $envLocalFile
88
+ foreach ($line in $envLines) {
89
+ if ($line -match '^SUPABASE_SECRET_KEY=(.+)$') {
90
+ $AUTH_TOKEN = $Matches[1].Trim('"', "'", ' ', "`r")
91
+ break
92
+ }
93
+ }
41
94
  }
95
+ }
96
+
97
+ if (-not $AUTH_TOKEN) { exit 0 }
98
+
99
+ $MEMORY_API_URL = "https://mcp.ekkos.dev"
100
+
101
+ # ═══════════════════════════════════════════════════════════════════════════
102
+ # COLORS (ANSI escape sequences via [char]27)
103
+ # ═══════════════════════════════════════════════════════════════════════════
104
+ $ESC = [char]27
105
+ $CYAN = "$ESC[0;36m"
106
+ $GREEN = "$ESC[0;32m"
107
+ $YELLOW = "$ESC[1;33m"
108
+ $MAGENTA = "$ESC[0;35m"
109
+ $DIM = "$ESC[2m"
110
+ $BOLD = "$ESC[1m"
111
+ $RESET = "$ESC[0m"
42
112
 
113
+ # ═══════════════════════════════════════════════════════════════════════════
114
+ # TIME MACHINE: Check for pending "Continue from here" requests
115
+ # ═══════════════════════════════════════════════════════════════════════════
116
+ $RESTORE_REQUEST_ID = $env:EKKOS_RESTORE
117
+ $TIME_MACHINE_SESSION = ""
118
+ $TIME_MACHINE_FROM_TURN = ""
119
+ $TIME_MACHINE_TO_TURN = ""
120
+
121
+ # Check via env var first, then API
122
+ if ($RESTORE_REQUEST_ID) {
123
+ [Console]::Error.WriteLine("$MAGENTA Time Machine request detected: $RESTORE_REQUEST_ID$RESET")
124
+ }
125
+
126
+ # Check API for pending requests (if we have user_id)
127
+ if ((-not $TIME_MACHINE_SESSION) -and $USER_ID) {
128
+ $PENDING_RESPONSE_RAW = ""
43
129
  try {
44
- $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
130
+ $PENDING_RESPONSE_RAW = Invoke-RestMethod -Uri "$MEMORY_API_URL/api/v1/context/restore-request/pending?user_id=$USER_ID" `
131
+ -Method GET `
132
+ -Headers @{ Authorization = "Bearer $AUTH_TOKEN" } `
133
+ -TimeoutSec 3 `
134
+ -ErrorAction Stop
45
135
  } catch {
46
- return $null
136
+ $PENDING_RESPONSE_RAW = ""
47
137
  }
48
- }
49
138
 
50
- function Convert-UuidToWords {
51
- param([string]$uuid)
139
+ if ($PENDING_RESPONSE_RAW) {
140
+ # Convert response object to JSON string for Parse-JsonValue
141
+ $PENDING_JSON = ""
142
+ if ($PENDING_RESPONSE_RAW -is [string]) {
143
+ $PENDING_JSON = $PENDING_RESPONSE_RAW
144
+ } else {
145
+ try {
146
+ $PENDING_JSON = $PENDING_RESPONSE_RAW | ConvertTo-Json -Depth 10 -Compress
147
+ } catch {
148
+ $PENDING_JSON = "{}"
149
+ }
150
+ }
52
151
 
53
- if (-not $script:SessionWords) {
54
- Load-SessionWords
55
- }
152
+ $IS_PENDING = Parse-JsonValue $PENDING_JSON ".pending"
56
153
 
57
- if (-not $script:SessionWords) {
58
- return "unknown-session"
59
- }
154
+ if ($IS_PENDING -eq "true" -or $IS_PENDING -eq "True") {
155
+ $TIME_MACHINE_SESSION = Parse-JsonValue $PENDING_JSON ".request.session_id"
156
+ $TIME_MACHINE_FROM_TURN = Parse-JsonValue $PENDING_JSON ".request.from_turn"
157
+ $TIME_MACHINE_TO_TURN = Parse-JsonValue $PENDING_JSON ".request.to_turn"
158
+ $RESTORE_REQUEST_ID = Parse-JsonValue $PENDING_JSON ".request.request_id"
60
159
 
61
- $adjectives = $script:SessionWords.adjectives
62
- $nouns = $script:SessionWords.nouns
63
- $verbs = $script:SessionWords.verbs
160
+ if ($TIME_MACHINE_SESSION) {
161
+ [Console]::Error.WriteLine("")
162
+ [Console]::Error.WriteLine("$MAGENTA------------------------------------------------------------------------$RESET")
163
+ [Console]::Error.WriteLine("$MAGENTA${BOLD} TIME MACHINE$RESET $DIM| Restoring session from web request...$RESET")
164
+ [Console]::Error.WriteLine("$MAGENTA------------------------------------------------------------------------$RESET")
64
165
 
65
- if (-not $adjectives -or -not $nouns -or -not $verbs) {
66
- return "unknown-session"
166
+ # Mark request as consumed (background, non-blocking)
167
+ $consumeBody = "{`"request_id`": `"$RESTORE_REQUEST_ID`"}"
168
+ Start-Job -ScriptBlock {
169
+ param($url, $token, $body)
170
+ try {
171
+ Invoke-RestMethod -Uri "$url/api/v1/context/restore-request/consume" `
172
+ -Method POST `
173
+ -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
174
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
175
+ -TimeoutSec 3 `
176
+ -ErrorAction SilentlyContinue | Out-Null
177
+ } catch {}
178
+ } -ArgumentList $MEMORY_API_URL, $AUTH_TOKEN, $consumeBody | Out-Null
179
+ }
180
+ }
67
181
  }
182
+ }
68
183
 
69
- if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
184
+ # ═══════════════════════════════════════════════════════════════════════════
185
+ # Session ID persistence - PROJECT-LOCAL for isolation
186
+ # ═══════════════════════════════════════════════════════════════════════════
187
+ $STATE_DIR = Join-Path $ProjectRoot ".claude\state"
188
+ if (-not (Test-Path $STATE_DIR)) {
189
+ New-Item -ItemType Directory -Path $STATE_DIR -Force | Out-Null
190
+ }
191
+ $SESSION_FILE = Join-Path $STATE_DIR "current-session.json"
70
192
 
71
- $clean = $uuid -replace "-", ""
72
- if ($clean.Length -lt 12) { return "unknown-session" }
193
+ # Project-local session storage (isolated per project)
194
+ $PROJECT_SESSION_DIR = Join-Path $STATE_DIR "sessions"
195
+ if (-not (Test-Path $PROJECT_SESSION_DIR)) {
196
+ New-Item -ItemType Directory -Path $PROJECT_SESSION_DIR -Force | Out-Null
197
+ }
73
198
 
74
- try {
75
- $a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
76
- $n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
77
- $an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
199
+ # Use Claude's RAW_SESSION_ID directly (from session_id field)
200
+ $CURRENT_SESSION_ID = $SESSION_ID
78
201
 
79
- return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
80
- } catch {
81
- return "unknown-session"
202
+ # Find most recent session in THIS PROJECT for auto-restore
203
+ $MOST_RECENT_SESSION = ""
204
+ $SAVED_TURN_COUNT = 0
205
+
206
+ if ($CURRENT_SESSION_ID -and $CURRENT_SESSION_ID -ne "unknown") {
207
+ # Check if THIS session has saved turns (for /clear continuity)
208
+ $TURN_COUNTER_FILE = Join-Path $PROJECT_SESSION_DIR "$CURRENT_SESSION_ID.turn"
209
+ if (Test-Path $TURN_COUNTER_FILE) {
210
+ try {
211
+ $SAVED_TURN_COUNT = [int](Get-Content $TURN_COUNTER_FILE -Raw).Trim()
212
+ } catch {
213
+ $SAVED_TURN_COUNT = 0
214
+ }
215
+ $MOST_RECENT_SESSION = $CURRENT_SESSION_ID
216
+ } else {
217
+ # Fresh start: find most recent session in project
218
+ $turnFiles = Get-ChildItem -Path $PROJECT_SESSION_DIR -Filter "*.turn" -ErrorAction SilentlyContinue |
219
+ Sort-Object LastWriteTime -Descending |
220
+ Select-Object -First 1
221
+ if ($turnFiles) {
222
+ $MOST_RECENT_SESSION = [System.IO.Path]::GetFileNameWithoutExtension($turnFiles.Name)
223
+ try {
224
+ $SAVED_TURN_COUNT = [int](Get-Content $turnFiles.FullName -Raw).Trim()
225
+ } catch {
226
+ $SAVED_TURN_COUNT = 0
227
+ }
228
+ }
82
229
  }
83
230
  }
84
231
 
232
+ # Save current session info
233
+ if ($CURRENT_SESSION_ID) {
234
+ $TIMESTAMP_UTC = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
235
+ $projectRootNorm = $ProjectRoot -replace '\\', '/'
236
+ $sessionInfoJson = "{`"session_id`": `"$CURRENT_SESSION_ID`", `"timestamp`": `"$TIMESTAMP_UTC`", `"project_root`": `"$projectRootNorm`"}"
237
+ Set-Content -Path $SESSION_FILE -Value $sessionInfoJson -Force
238
+ }
239
+
85
240
  # ═══════════════════════════════════════════════════════════════════════════
86
- # READ INPUT
241
+ # GOLDEN LOOP: Initialize session tracking file
87
242
  # ═══════════════════════════════════════════════════════════════════════════
88
- $inputJson = [Console]::In.ReadToEnd()
89
-
90
- try {
91
- $input = $inputJson | ConvertFrom-Json
92
- $sessionId = $input.session_id
93
- } catch {
94
- $sessionId = "unknown"
243
+ $EKKOS_DIR = Join-Path $ProjectRoot ".ekkos"
244
+ if (-not (Test-Path $EKKOS_DIR)) {
245
+ New-Item -ItemType Directory -Path $EKKOS_DIR -Force | Out-Null
95
246
  }
247
+ $GOLDEN_LOOP_FILE = Join-Path $EKKOS_DIR "golden-loop-current.json"
96
248
 
97
- $sessionName = Convert-UuidToWords $sessionId
249
+ # Initialize with session start state using Node (no jq)
250
+ $glPathEscaped = $GOLDEN_LOOP_FILE -replace '\\', '\\\\'
251
+ try {
252
+ node -e "
253
+ const fs = require('fs');
254
+ const data = {
255
+ phase: 'idle',
256
+ turn: 0,
257
+ session: '$CURRENT_SESSION_ID',
258
+ timestamp: new Date().toISOString(),
259
+ stats: { retrieved: 0, applied: 0, forged: 0 }
260
+ };
261
+ fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
262
+ " 2>$null
263
+ } catch {}
98
264
 
99
265
  # ═══════════════════════════════════════════════════════════════════════════
100
- # INITIALIZE STATE DIRECTORY
266
+ # AUTO-RESTORE REMOVED: Manual /continue only (saves 79% token burn!)
101
267
  # ═══════════════════════════════════════════════════════════════════════════
102
- $stateDir = Join-Path $env:USERPROFILE ".claude\state"
103
- if (-not (Test-Path $stateDir)) {
104
- New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
105
- }
268
+ # WHY REMOVED:
269
+ # - Auto-restore burned 5,000 tokens per turn on session start
270
+ # - Manual /continue: one-time cost + clean slate (79% token savings!)
271
+ # - Manual /continue is 10x more powerful (Bash + multi-source + narrative)
272
+ #
273
+ # KEPT: Time Machine feature (explicit user request)
274
+ # ═══════════════════════════════════════════════════════════════════════════
275
+
276
+ # Handle Time Machine requests (explicit user action)
277
+ if ($TIME_MACHINE_SESSION) {
278
+ $tmPreview = $TIME_MACHINE_SESSION
279
+ if ($tmPreview.Length -gt 12) {
280
+ $tmPreview = $tmPreview.Substring(0, 12)
281
+ }
282
+
283
+ [Console]::Error.WriteLine("")
284
+ [Console]::Error.WriteLine("$MAGENTA${BOLD} TIME MACHINE$RESET $DIM| Restoring past session: ${tmPreview}...$RESET")
106
285
 
107
- # Reset turn counter for new session
108
- $stateFile = Join-Path $stateDir "hook-state.json"
109
- $state = @{
110
- turn = 0
111
- session_id = $sessionId
112
- session_name = $sessionName
113
- instance_id = $EkkosInstanceId
114
- started_at = (Get-Date).ToString("o")
115
- } | ConvertTo-Json -Depth 10
286
+ # Build recall request with turn range
287
+ $RECALL_BODY = "{`"session_id`": `"$TIME_MACHINE_SESSION`", `"last_n`": 15, `"format`": `"summary`"}"
116
288
 
117
- Set-Content -Path $stateFile -Value $state -Force
289
+ if ($TIME_MACHINE_FROM_TURN -and $TIME_MACHINE_TO_TURN) {
290
+ $RECALL_BODY = "{`"session_id`": `"$TIME_MACHINE_SESSION`", `"from_turn`": $TIME_MACHINE_FROM_TURN, `"to_turn`": $TIME_MACHINE_TO_TURN, `"format`": `"summary`"}"
291
+ } elseif ($TIME_MACHINE_FROM_TURN) {
292
+ $RECALL_BODY = "{`"session_id`": `"$TIME_MACHINE_SESSION`", `"from_turn`": $TIME_MACHINE_FROM_TURN, `"format`": `"summary`"}"
293
+ }
294
+
295
+ # Fetch turns from L2
296
+ $RESTORE_RESPONSE = $null
297
+ try {
298
+ $RESTORE_RESPONSE = Invoke-RestMethod -Uri "$MEMORY_API_URL/api/v1/turns/recall" `
299
+ -Method POST `
300
+ -Headers @{ Authorization = "Bearer $AUTH_TOKEN"; "Content-Type" = "application/json" } `
301
+ -Body ([System.Text.Encoding]::UTF8.GetBytes($RECALL_BODY)) `
302
+ -TimeoutSec 5 `
303
+ -ErrorAction Stop
304
+ } catch {
305
+ $RESTORE_RESPONSE = $null
306
+ }
118
307
 
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
308
+ # Check if we got turns back
309
+ $RESTORED_COUNT = 0
310
+ $RESTORED_TURNS = @()
311
+ if ($RESTORE_RESPONSE -and $RESTORE_RESPONSE.turns) {
312
+ $RESTORED_TURNS = @($RESTORE_RESPONSE.turns)
313
+ $RESTORED_COUNT = $RESTORED_TURNS.Count
314
+ }
315
+
316
+ if ($RESTORED_COUNT -gt 0) {
317
+ [Console]::Error.WriteLine("$MAGENTA $RESET Restored $RESTORED_COUNT turns from past session")
318
+ [Console]::Error.WriteLine("")
319
+ [Console]::Error.WriteLine("$MAGENTA${BOLD}## Time Machine Context$RESET")
320
+ [Console]::Error.WriteLine("")
321
+
322
+ # Build turns output for both stderr (display) and stdout (context injection)
323
+ $turnsOutput = ""
324
+ foreach ($t in $RESTORED_TURNS) {
325
+ $q = ""
326
+ if ($t.user_query) {
327
+ $q = $t.user_query
328
+ if ($q.Length -gt 100) { $q = $q.Substring(0, 100) }
329
+ }
330
+ $r = ""
331
+ if ($t.assistant_response) {
332
+ $r = $t.assistant_response
333
+ if ($r.Length -gt 200) { $r = $r.Substring(0, 200) }
334
+ }
335
+ $tn = if ($t.turn_number) { $t.turn_number } else { "?" }
126
336
 
127
- Set-Content -Path $sessionFile -Value $sessionData -Force
337
+ $turnLine = "**Turn $tn**: $q..."
338
+ $responseLine = "> $r..."
339
+ $turnsOutput += "$turnLine`n$responseLine`n`n"
340
+ }
341
+
342
+ # Output to stderr (visible in terminal)
343
+ [Console]::Error.WriteLine($turnsOutput)
344
+ # Output to stdout (injected into context)
345
+ Write-Output $turnsOutput
346
+
347
+ [Console]::Error.WriteLine("")
348
+ [Console]::Error.WriteLine("${DIM}You've traveled to a past session. Continue from here!$RESET")
349
+ [Console]::Error.WriteLine("")
350
+ }
351
+ }
128
352
 
129
353
  # ═══════════════════════════════════════════════════════════════════════════
130
- # LOCAL CACHE: Initialize session in Tier 0 cache
131
- # Per v1.2 ADDENDUM: Pass instanceId for namespacing
354
+ # DIRECTIVE RETRIEVAL: Fetch user's MUST/NEVER/PREFER/AVOID rules
132
355
  # ═══════════════════════════════════════════════════════════════════════════
133
- $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
134
- if ($captureCmd -and $sessionId -ne "unknown") {
356
+ $DIRECTIVES_INJECTED = $false
357
+ $DIRECTIVE_COUNT = 0
358
+
359
+ # Only fetch if we have auth
360
+ if ($AUTH_TOKEN) {
361
+ # Fetch directives (top 20 by priority to avoid token bloat)
362
+ $DIRECTIVES_RESPONSE_RAW = $null
135
363
  try {
136
- # NEW format: ekkos-capture init <instance_id> <session_id> <session_name>
137
- Start-Job -ScriptBlock {
138
- param($instanceId, $sessId, $sessName)
364
+ $DIRECTIVES_RESPONSE_RAW = Invoke-RestMethod -Uri "$MEMORY_API_URL/api/v1/memory/directives?limit=20" `
365
+ -Method GET `
366
+ -Headers @{ Authorization = "Bearer $AUTH_TOKEN" } `
367
+ -TimeoutSec 3 `
368
+ -ErrorAction Stop
369
+ } catch {
370
+ $DIRECTIVES_RESPONSE_RAW = $null
371
+ }
372
+
373
+ if ($DIRECTIVES_RESPONSE_RAW) {
374
+ # Convert response object to JSON string for Parse-JsonValue / Node
375
+ $DIRECTIVES_JSON = ""
376
+ if ($DIRECTIVES_RESPONSE_RAW -is [string]) {
377
+ $DIRECTIVES_JSON = $DIRECTIVES_RESPONSE_RAW
378
+ } else {
139
379
  try {
140
- & ekkos-capture init $instanceId $sessId $sessName 2>&1 | Out-Null
141
- } catch {}
142
- } -ArgumentList $EkkosInstanceId, $sessionId, $sessionName | Out-Null
143
- } catch {}
380
+ $DIRECTIVES_JSON = $DIRECTIVES_RESPONSE_RAW | ConvertTo-Json -Depth 10 -Compress
381
+ } catch {
382
+ $DIRECTIVES_JSON = "{}"
383
+ }
384
+ }
385
+
386
+ $DIRECTIVE_COUNT_STR = Parse-JsonValue $DIRECTIVES_JSON ".count"
387
+ if ($DIRECTIVE_COUNT_STR -match '^\d+$') {
388
+ $DIRECTIVE_COUNT = [int]$DIRECTIVE_COUNT_STR
389
+ }
390
+
391
+ if ($DIRECTIVE_COUNT -gt 0) {
392
+ $DIRECTIVES_INJECTED = $true
393
+
394
+ # Extract MUST/NEVER/PREFER/AVOID arrays using Node
395
+ Write-Output "<system-reminder>"
396
+ Write-Output "USER DIRECTIVES (FOLLOW THESE):"
397
+ Write-Output ""
398
+
399
+ $directiveOutput = $DIRECTIVES_JSON | node -e "
400
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
401
+ const types = ['MUST', 'NEVER', 'PREFER', 'AVOID'];
402
+ types.forEach(type => {
403
+ const rules = (d[type] || []).slice(0, 5);
404
+ if (rules.length > 0) {
405
+ console.log(type + ':');
406
+ rules.forEach(r => console.log(' - ' + (r.rule || '')));
407
+ }
408
+ });
409
+ " 2>$null
410
+
411
+ if ($directiveOutput) {
412
+ Write-Output $directiveOutput
413
+ }
414
+
415
+ Write-Output "</system-reminder>"
416
+ [Console]::Error.WriteLine("$GREEN $DIRECTIVE_COUNT directives loaded$RESET")
417
+ }
418
+ }
419
+ }
420
+
421
+ # Simple status display (no auto-restore)
422
+ if ($SAVED_TURN_COUNT -gt 0) {
423
+ [Console]::Error.WriteLine("")
424
+ $displaySessionId = if ($CURRENT_SESSION_ID) { $CURRENT_SESSION_ID } else { $SESSION_ID }
425
+ [Console]::Error.WriteLine("$CYAN${BOLD} ekkOS$RESET $DIM|$RESET Session: $displaySessionId $DIM|$RESET $GREEN$SAVED_TURN_COUNT turns$RESET")
426
+ }
427
+
428
+ # Final confirmation that's always visible
429
+ if ($TIME_MACHINE_SESSION) {
430
+ $tmFinalPreview = $TIME_MACHINE_SESSION
431
+ if ($tmFinalPreview.Length -gt 12) {
432
+ $tmFinalPreview = $tmFinalPreview.Substring(0, 12)
433
+ }
434
+ [Console]::Error.WriteLine("$MAGENTA------------------------------------------------------------------------$RESET")
435
+ [Console]::Error.WriteLine("$MAGENTA $RESET Time Machine active - Restored from session ${tmFinalPreview}...")
436
+ [Console]::Error.WriteLine("$MAGENTA------------------------------------------------------------------------$RESET")
437
+ } elseif ($SAVED_TURN_COUNT -gt 0) {
438
+ [Console]::Error.WriteLine("$GREEN------------------------------------------------------------------------$RESET")
439
+ [Console]::Error.WriteLine("${GREEN}$RESET Session continued - $SAVED_TURN_COUNT turns preserved - Ready to resume")
440
+ [Console]::Error.WriteLine("$GREEN------------------------------------------------------------------------$RESET")
441
+ } else {
442
+ [Console]::Error.WriteLine("${CYAN}$RESET New session started")
144
443
  }
444
+ [Console]::Error.WriteLine("")
145
445
 
146
- Write-Output "ekkOS session initialized"
446
+ exit 0