@ekkos/cli 1.0.35 → 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.
@@ -1,445 +1,308 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
- # ekkOS_ Hook: SessionStart - MINIMAL + AUTO-RESTORE + TIME MACHINE CONTINUE
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
5
  # ═══════════════════════════════════════════════════════════════════════════
6
6
  # This hook does THREE things:
7
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)
8
+ # 2. Initialize session tracking + Golden Loop
9
+ # 3. Fetch and inject user directives (MUST/NEVER/PREFER/AVOID)
10
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
19
- #
20
- # Per spec v1.2 Addendum: NO jq dependency
11
+ # Per spec v1.2 Addendum: NO hardcoded arrays, uses session-words.json
21
12
  # ═══════════════════════════════════════════════════════════════════════════
22
13
 
23
14
  $ErrorActionPreference = "SilentlyContinue"
24
15
 
25
- $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
26
- $ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
27
-
28
16
  # ═══════════════════════════════════════════════════════════════════════════
29
- # CONFIG PATHS - Per spec v1.2 Addendum
17
+ # CONFIG PATHS
30
18
  # ═══════════════════════════════════════════════════════════════════════════
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"
19
+ $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
20
+ $SessionWordsJson = "$EkkosConfigDir\session-words.json"
21
+ $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
22
+ $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
33
23
 
34
- $INPUT = [Console]::In.ReadToEnd()
24
+ $MemoryApiUrl = "https://api.ekkos.dev"
35
25
 
36
26
  # ═══════════════════════════════════════════════════════════════════════════
37
- # JSON parsing helper (no jq) - pipes JSON to node, extracts dot-path value
27
+ # Load session words from JSON file - NO HARDCODED ARRAYS
38
28
  # ═══════════════════════════════════════════════════════════════════════════
39
- function Parse-JsonValue {
40
- param(
41
- [string]$Json,
42
- [string]$Path
43
- )
44
- if (-not $Json) { return "" }
29
+ $script:SessionWords = $null
30
+
31
+ function Load-SessionWords {
32
+ $wordsFile = $SessionWordsJson
33
+ if (-not (Test-Path $wordsFile)) { $wordsFile = $SessionWordsDefault }
34
+ if (-not (Test-Path $wordsFile)) { return $null }
45
35
  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
- }
36
+ $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
37
+ } catch { return $null }
60
38
  }
61
39
 
62
- $SESSION_ID = Parse-JsonValue $INPUT ".session_id"
63
- if (-not $SESSION_ID) { $SESSION_ID = "unknown" }
40
+ function Convert-UuidToWords {
41
+ param([string]$uuid)
42
+ if (-not $script:SessionWords) { Load-SessionWords }
43
+ if (-not $script:SessionWords) { return "unknown-session" }
64
44
 
65
- $TRANSCRIPT_PATH = Parse-JsonValue $INPUT ".transcript_path"
66
- $SOURCE = Parse-JsonValue $INPUT ".source"
67
- if (-not $SOURCE) { $SOURCE = "unknown" }
45
+ $adjectives = $script:SessionWords.adjectives
46
+ $nouns = $script:SessionWords.nouns
47
+ $verbs = $script:SessionWords.verbs
48
+ if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
49
+ if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
68
50
 
69
- # ═══════════════════════════════════════════════════════════════════════════
70
- # Load auth
71
- # ═══════════════════════════════════════════════════════════════════════════
72
- $EKKOS_CONFIG = Join-Path $env:USERPROFILE ".ekkos\config.json"
73
- $AUTH_TOKEN = ""
74
- $USER_ID = ""
75
-
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
80
- }
81
- $USER_ID = & node $JsonParseHelper $EKKOS_CONFIG ".userId" 2>$null
82
- }
51
+ $clean = $uuid -replace "-", ""
52
+ if ($clean.Length -lt 12) { return "unknown-session" }
83
53
 
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
- }
94
- }
54
+ try {
55
+ $a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
56
+ $n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
57
+ $an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
58
+ return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
59
+ } catch { return "unknown-session" }
95
60
  }
96
61
 
97
- if (-not $AUTH_TOKEN) { exit 0 }
62
+ # ═══════════════════════════════════════════════════════════════════════════
63
+ # READ INPUT
64
+ # ═══════════════════════════════════════════════════════════════════════════
65
+ $inputJson = [Console]::In.ReadToEnd()
66
+ try {
67
+ $input = $inputJson | ConvertFrom-Json
68
+ $sessionId = $input.session_id
69
+ } catch {
70
+ $sessionId = "unknown"
71
+ }
72
+ if (-not $sessionId) { $sessionId = "unknown" }
98
73
 
99
- $MEMORY_API_URL = "https://mcp.ekkos.dev"
74
+ $sessionName = Convert-UuidToWords $sessionId
100
75
 
101
76
  # ═══════════════════════════════════════════════════════════════════════════
102
- # COLORS (ANSI escape sequences via [char]27)
77
+ # LOAD AUTH
103
78
  # ═══════════════════════════════════════════════════════════════════════════
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"
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 }
112
91
 
113
92
  # ═══════════════════════════════════════════════════════════════════════════
114
93
  # TIME MACHINE: Check for pending "Continue from here" requests
115
94
  # ═══════════════════════════════════════════════════════════════════════════
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
- }
95
+ $timeMachineSession = ""
96
+ $timeMachineFromTurn = ""
97
+ $timeMachineToTurn = ""
98
+ $restoreRequestId = $env:EKKOS_RESTORE
125
99
 
126
- # Check API for pending requests (if we have user_id)
127
- if ((-not $TIME_MACHINE_SESSION) -and $USER_ID) {
128
- $PENDING_RESPONSE_RAW = ""
100
+ if (-not $timeMachineSession -and $userId) {
129
101
  try {
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
135
- } catch {
136
- $PENDING_RESPONSE_RAW = ""
137
- }
138
-
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
- }
151
-
152
- $IS_PENDING = Parse-JsonValue $PENDING_JSON ".pending"
153
-
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"
159
-
160
- if ($TIME_MACHINE_SESSION) {
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
161
114
  [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")
165
-
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
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 {}
179
125
  }
180
126
  }
181
- }
127
+ } catch {}
182
128
  }
183
129
 
184
130
  # ═══════════════════════════════════════════════════════════════════════════
185
- # Session ID persistence - PROJECT-LOCAL for isolation
131
+ # SESSION PERSISTENCE - PROJECT-LOCAL for isolation
186
132
  # ═══════════════════════════════════════════════════════════════════════════
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"
133
+ $projectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
134
+ $stateDir = Join-Path $env:USERPROFILE ".claude\state"
135
+ $projectSessionDir = Join-Path $stateDir "sessions"
192
136
 
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
- }
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 }
198
139
 
199
- # Use Claude's RAW_SESSION_ID directly (from session_id field)
200
- $CURRENT_SESSION_ID = $SESSION_ID
201
-
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
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
216
148
  } 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
- }
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 }
228
154
  }
229
155
  }
230
156
  }
231
157
 
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
- }
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
+
168
+ $stateFile = Join-Path $stateDir "hook-state.json"
169
+ $state = @{
170
+ turn = 0
171
+ session_id = $sessionId
172
+ session_name = $sessionName
173
+ instance_id = $EkkosInstanceId
174
+ started_at = (Get-Date).ToString("o")
175
+ } | ConvertTo-Json -Depth 10
176
+ Set-Content -Path $stateFile -Value $state -Force
239
177
 
240
178
  # ═══════════════════════════════════════════════════════════════════════════
241
179
  # GOLDEN LOOP: Initialize session tracking file
242
180
  # ═══════════════════════════════════════════════════════════════════════════
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
246
- }
247
- $GOLDEN_LOOP_FILE = Join-Path $EKKOS_DIR "golden-loop-current.json"
248
-
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 {}
181
+ $ekkosDir = Join-Path $projectRoot ".ekkos"
182
+ if (-not (Test-Path $ekkosDir)) { New-Item -ItemType Directory -Path $ekkosDir -Force | Out-Null }
183
+
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
264
193
 
265
194
  # ═══════════════════════════════════════════════════════════════════════════
266
- # AUTO-RESTORE REMOVED: Manual /continue only (saves 79% token burn!)
267
- # ═══════════════════════════════════════════════════════════════════════════
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)
195
+ # LOCAL CACHE: Initialize session
274
196
  # ═══════════════════════════════════════════════════════════════════════════
197
+ $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
198
+ if ($captureCmd -and $sessionId -ne "unknown") {
199
+ try {
200
+ Start-Job -ScriptBlock {
201
+ param($instanceId, $sessId, $sessName)
202
+ try { & ekkos-capture init $instanceId $sessId $sessName 2>&1 | Out-Null } catch {}
203
+ } -ArgumentList $EkkosInstanceId, $sessionId, $sessionName | Out-Null
204
+ } catch {}
205
+ }
275
206
 
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
- }
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"
282
213
 
214
+ # ═══════════════════════════════════════════════════════════════════════════
215
+ # TIME MACHINE RESTORATION
216
+ # ═══════════════════════════════════════════════════════════════════════════
217
+ if ($timeMachineSession) {
283
218
  [Console]::Error.WriteLine("")
284
- [Console]::Error.WriteLine("$MAGENTA${BOLD} TIME MACHINE$RESET $DIM| Restoring past session: ${tmPreview}...$RESET")
285
-
286
- # Build recall request with turn range
287
- $RECALL_BODY = "{`"session_id`": `"$TIME_MACHINE_SESSION`", `"last_n`": 15, `"format`": `"summary`"}"
288
-
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`"}"
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" }
293
227
  }
294
228
 
295
- # Fetch turns from L2
296
- $RESTORE_RESPONSE = $null
229
+ $recallJson = $recallBody | ConvertTo-Json -Depth 10
297
230
  try {
298
- $RESTORE_RESPONSE = Invoke-RestMethod -Uri "$MEMORY_API_URL/api/v1/turns/recall" `
231
+ $restoreResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/turns/recall" `
299
232
  -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
- }
307
-
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) }
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"
334
250
  }
335
- $tn = if ($t.turn_number) { $t.turn_number } else { "?" }
251
+ Write-Output $turnsOutput
336
252
 
337
- $turnLine = "**Turn $tn**: $q..."
338
- $responseLine = "> $r..."
339
- $turnsOutput += "$turnLine`n$responseLine`n`n"
253
+ [Console]::Error.WriteLine("${DIM}You've traveled to a past session. Continue from here!${RESET}")
254
+ [Console]::Error.WriteLine("")
340
255
  }
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
- }
256
+ } catch {}
351
257
  }
352
258
 
353
259
  # ═══════════════════════════════════════════════════════════════════════════
354
260
  # DIRECTIVE RETRIEVAL: Fetch user's MUST/NEVER/PREFER/AVOID rules
355
261
  # ═══════════════════════════════════════════════════════════════════════════
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
262
+ if ($authToken) {
363
263
  try {
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 {
379
- try {
380
- $DIRECTIVES_JSON = $DIRECTIVES_RESPONSE_RAW | ConvertTo-Json -Depth 10 -Compress
381
- } catch {
382
- $DIRECTIVES_JSON = "{}"
383
- }
384
- }
264
+ $directivesResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/memory/directives?limit=20" `
265
+ -Method GET -Headers @{ "Authorization" = "Bearer $authToken" } -TimeoutSec 3
385
266
 
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
267
+ $directiveCount = $directivesResponse.count
268
+ if ($directiveCount -gt 0) {
395
269
  Write-Output "<system-reminder>"
396
270
  Write-Output "USER DIRECTIVES (FOLLOW THESE):"
397
271
  Write-Output ""
398
272
 
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
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
+ }
413
279
  }
414
280
 
415
281
  Write-Output "</system-reminder>"
416
- [Console]::Error.WriteLine("$GREEN $DIRECTIVE_COUNT directives loaded$RESET")
282
+ [Console]::Error.WriteLine("${GREEN} $directiveCount directives loaded${RESET}")
417
283
  }
418
- }
284
+ } catch {}
419
285
  }
420
286
 
421
- # Simple status display (no auto-restore)
422
- if ($SAVED_TURN_COUNT -gt 0) {
287
+ # ═══════════════════════════════════════════════════════════════════════════
288
+ # STATUS DISPLAY
289
+ # ═══════════════════════════════════════════════════════════════════════════
290
+ if ($savedTurnCount -gt 0) {
423
291
  [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")
292
+ [Console]::Error.WriteLine("${CYAN}${BOLD} ekkOS${RESET} ${DIM}|${RESET} Session: $sessionId ${DIM}|${RESET} ${GREEN}$savedTurnCount turns${RESET}")
426
293
  }
427
294
 
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")
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}")
441
304
  } else {
442
- [Console]::Error.WriteLine("${CYAN}$RESET New session started")
305
+ [Console]::Error.WriteLine("${CYAN}✓${RESET} New session started")
443
306
  }
444
307
  [Console]::Error.WriteLine("")
445
308