@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,11 +1,18 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
2
  # ekkOS_ Hook: UserPromptSubmit - SEAMLESS CONTEXT CONTINUITY (Windows)
3
- # ═══════════════════════════════════════════════════════════════════════════
3
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
+ # EKKOS_MANAGED=1
5
+ # EKKOS_MANIFEST_SHA256=<computed-at-build>
6
+ # EKKOS_TEMPLATE_VERSION=2.0.0
7
+ #
4
8
  # ZERO USER ACTION NEEDED:
5
9
  # 1. Tracks turn number and context size
6
10
  # 2. Detects when compaction happened (context dropped from high to low)
7
11
  # 3. AUTO-INJECTS restored context - user just keeps working
8
- # Per spec v1.2 Addendum: NO jq dependency, uses Node.js for JSON
12
+ #
13
+ # Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
14
+ # - All persisted records MUST include: instanceId, sessionId, sessionName
15
+ # - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
9
16
  # ═══════════════════════════════════════════════════════════════════════════
10
17
 
11
18
  $ErrorActionPreference = "SilentlyContinue"
@@ -14,643 +21,702 @@ $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
14
21
  $ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
15
22
 
16
23
  # ═══════════════════════════════════════════════════════════════════════════
17
- # CONFIG PATHS - Per spec v1.2 Addendum
24
+ # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
18
25
  # ═══════════════════════════════════════════════════════════════════════════
19
- $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { Join-Path $env:USERPROFILE ".ekkos" }
20
- $SessionWordsJson = Join-Path $EkkosConfigDir "session-words.json"
21
- $SessionWordsDefault = Join-Path $EkkosConfigDir ".defaults\session-words.json"
22
- $JsonParseHelper = Join-Path $EkkosConfigDir ".helpers\json-parse.cjs"
26
+ $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
27
+ $SessionWordsJson = "$EkkosConfigDir\session-words.json"
28
+ $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
29
+ $JsonParseHelper = "$EkkosConfigDir\.helpers\json-parse.cjs"
23
30
 
24
31
  # ═══════════════════════════════════════════════════════════════════════════
25
- # WORD-BASED SESSION NAMES - Uses external session-words.json
32
+ # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
26
33
  # ═══════════════════════════════════════════════════════════════════════════
27
- $script:ADJECTIVES = @()
28
- $script:NOUNS = @()
29
- $script:VERBS = @()
30
- $script:SESSION_WORDS_LOADED = $false
34
+ $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
31
35
 
32
- function Load-SessionWords {
33
- if ($script:SESSION_WORDS_LOADED) { return }
36
+ # ═══════════════════════════════════════════════════════════════════════════
37
+ # API URL
38
+ # ═══════════════════════════════════════════════════════════════════════════
39
+ $MemoryApiUrl = "https://api.ekkos.dev"
40
+
41
+ # ═══════════════════════════════════════════════════════════════════════════
42
+ # Load session words from JSON file - NO HARDCODED ARRAYS
43
+ # ═══════════════════════════════════════════════════════════════════════════
44
+ $script:SessionWords = $null
34
45
 
46
+ function Load-SessionWords {
35
47
  $wordsFile = $SessionWordsJson
48
+
49
+ # Fallback to managed defaults if user file missing/invalid
36
50
  if (-not (Test-Path $wordsFile)) {
37
51
  $wordsFile = $SessionWordsDefault
38
52
  }
39
53
 
40
- if ((-not (Test-Path $wordsFile)) -or (-not (Test-Path $JsonParseHelper))) {
41
- $script:ADJECTIVES = @("unknown")
42
- $script:NOUNS = @("session")
43
- $script:VERBS = @("starts")
44
- return
45
- }
46
-
47
- try {
48
- $adjRaw = & node $JsonParseHelper $wordsFile ".adjectives" 2>$null
49
- $nounRaw = & node $JsonParseHelper $wordsFile ".nouns" 2>$null
50
- $verbRaw = & node $JsonParseHelper $wordsFile ".verbs" 2>$null
51
-
52
- $script:ADJECTIVES = @()
53
- $script:NOUNS = @()
54
- $script:VERBS = @()
55
-
56
- if ($adjRaw) { $script:ADJECTIVES = @($adjRaw -split "`n" | Where-Object { $_ }) }
57
- if ($nounRaw) { $script:NOUNS = @($nounRaw -split "`n" | Where-Object { $_ }) }
58
- if ($verbRaw) { $script:VERBS = @($verbRaw -split "`n" | Where-Object { $_ }) }
59
-
60
- if ($script:ADJECTIVES.Count -eq 0) { $script:ADJECTIVES = @("unknown") }
61
- if ($script:NOUNS.Count -eq 0) { $script:NOUNS = @("session") }
62
- if ($script:VERBS.Count -eq 0) { $script:VERBS = @("starts") }
63
-
64
- $script:SESSION_WORDS_LOADED = $true
65
- } catch {
66
- $script:ADJECTIVES = @("unknown")
67
- $script:NOUNS = @("session")
68
- $script:VERBS = @("starts")
69
- }
70
- }
71
-
72
- function Convert-UuidToWords {
73
- param([string]$uuid)
74
-
75
- Load-SessionWords
76
-
77
- $hex = $uuid -replace "-", ""
78
- $hex = $hex.Substring(0, [Math]::Min(12, $hex.Length))
79
- if ($hex -notmatch '^[0-9a-fA-F]+$') { return "unknown-session-starts" }
80
-
81
- try {
82
- $adjSeed = [Convert]::ToInt32($hex.Substring(0, 4), 16)
83
- $nounSeed = [Convert]::ToInt32($hex.Substring(4, 4), 16)
84
- $verbSeed = [Convert]::ToInt32($hex.Substring(8, 4), 16)
85
-
86
- $adjIdx = $adjSeed % $script:ADJECTIVES.Count
87
- $nounIdx = $nounSeed % $script:NOUNS.Count
88
- $verbIdx = $verbSeed % $script:VERBS.Count
89
-
90
- return "$($script:ADJECTIVES[$adjIdx])-$($script:NOUNS[$nounIdx])-$($script:VERBS[$verbIdx])"
91
- } catch {
92
- return "unknown-session-starts"
54
+ if (-not (Test-Path $wordsFile)) {
55
+ return $null
93
56
  }
94
- }
95
57
 
96
- # ═══════════════════════════════════════════════════════════════════════════
97
- # JSON parsing helper (no jq) - pipes JSON to node, extracts dot-path value
98
- # ═══════════════════════════════════════════════════════════════════════════
99
- function Parse-JsonValue {
100
- param(
101
- [string]$Json,
102
- [string]$Path
103
- )
104
- if (-not $Json) { return "" }
105
58
  try {
106
- $result = $Json | node -e "
107
- const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
108
- const path = '$Path'.replace(/^\./,'').split('.').filter(Boolean);
109
- let result = data;
110
- for (const p of path) {
111
- if (result === undefined || result === null) { result = undefined; break; }
112
- result = result[p];
113
- }
114
- if (result !== undefined && result !== null) console.log(result);
115
- " 2>$null
116
- if ($result) { return $result.Trim() } else { return "" }
59
+ $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
117
60
  } catch {
118
- return ""
61
+ return $null
119
62
  }
120
63
  }
121
64
 
122
65
  # ═══════════════════════════════════════════════════════════════════════════
123
66
  # Read input from stdin
124
67
  # ═══════════════════════════════════════════════════════════════════════════
125
- $INPUT = [Console]::In.ReadToEnd()
126
- if (-not $INPUT) { exit 0 }
68
+ $inputJson = [Console]::In.ReadToEnd()
69
+ if (-not $inputJson) { exit 0 }
127
70
 
128
- $USER_QUERY = Parse-JsonValue $INPUT ".query"
129
- if (-not $USER_QUERY) { $USER_QUERY = Parse-JsonValue $INPUT ".message" }
130
- if (-not $USER_QUERY) { $USER_QUERY = Parse-JsonValue $INPUT ".prompt" }
131
- if (-not $USER_QUERY -or $USER_QUERY -eq "null") { exit 0 }
71
+ try {
72
+ $input = $inputJson | ConvertFrom-Json
73
+ } catch {
74
+ exit 0
75
+ }
76
+
77
+ $userQuery = $input.query
78
+ if (-not $userQuery) { $userQuery = $input.message }
79
+ if (-not $userQuery) { $userQuery = $input.prompt }
80
+ if (-not $userQuery) { exit 0 }
132
81
 
133
- $RAW_SESSION_ID = Parse-JsonValue $INPUT ".session_id"
134
- if (-not $RAW_SESSION_ID -or $RAW_SESSION_ID -eq "null") { $RAW_SESSION_ID = "unknown" }
135
- $TRANSCRIPT_PATH = Parse-JsonValue $INPUT ".transcript_path"
82
+ $rawSessionId = $input.session_id
83
+ if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
84
+ $transcriptPath = $input.transcript_path
136
85
 
137
- # Fallback: read session_id from saved state if not in INPUT
138
- if ($RAW_SESSION_ID -eq "unknown") {
86
+ # Fallback: read session_id from saved state
87
+ if ($rawSessionId -eq "unknown" -or $rawSessionId -eq "null" -or [string]::IsNullOrEmpty($rawSessionId)) {
139
88
  $stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
140
- if (Test-Path $stateFile) {
141
- $savedState = Get-Content $stateFile -Raw
142
- $savedId = Parse-JsonValue $savedState ".session_id"
143
- if ($savedId -and $savedId -ne "unknown" -and $savedId -ne "null") {
144
- $RAW_SESSION_ID = $savedId
89
+ if ((Test-Path $stateFile) -and (Test-Path $JsonParseHelper)) {
90
+ try {
91
+ $rawSessionId = node $JsonParseHelper $stateFile '.session_id' 2>$null
92
+ if (-not $rawSessionId) { $rawSessionId = "unknown" }
93
+ } catch {
94
+ $rawSessionId = "unknown"
145
95
  }
146
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)
102
+ }
147
103
  }
148
104
 
149
105
  # ═══════════════════════════════════════════════════════════════════════════
150
106
  # INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
151
- # Detects ALL applicable skills/tools and injects as system reminder
152
107
  # ═══════════════════════════════════════════════════════════════════════════
153
- $SKILL_REMINDERS = @()
154
- $QUERY_LOWER = $USER_QUERY.ToLower()
108
+ $skillReminders = @()
109
+ $queryLower = $userQuery.ToLower()
155
110
 
156
111
  # Memory First - Debug/Error/Problem solving
157
- if ($QUERY_LOWER -match '(how do i|debug|error|bug|fix|not working|broken|fails|issue|problem|wrong|crash)') {
158
- $SKILL_REMINDERS += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Memory_First`") FIRST before debugging"
112
+ if ($queryLower -match '(how do i|debug|error|bug|fix|not working|broken|fails|issue|problem|wrong|crash)') {
113
+ $skillReminders += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Memory_First`") FIRST before debugging"
159
114
  }
160
115
 
161
- # Recall Triggers (Time-based memory)
162
- if ($QUERY_LOWER -match '(yesterday|last week|last month|remember when|what did we|where did we leave|before|earlier|previous|ago)') {
163
- $SKILL_REMINDERS += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Deep_Recall`") for time-based memory"
116
+ # Recall Triggers - Time-based memory
117
+ if ($queryLower -match '(yesterday|last week|last month|remember when|what did we|where did we leave|before|earlier|previous|ago)') {
118
+ $skillReminders += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Deep_Recall`") for time-based memory"
164
119
  }
165
120
 
166
- # Directive Triggers (User preferences)
167
- if ($QUERY_LOWER -match '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on)') {
168
- $SKILL_REMINDERS += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Preferences`") to capture directive"
121
+ # Directive Triggers - User preferences
122
+ if ($queryLower -match '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on)') {
123
+ $skillReminders += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Preferences`") to capture directive"
169
124
  }
170
125
 
171
- # Safety Triggers (Destructive actions)
172
- if ($QUERY_LOWER -match '(delete|drop |rm -rf|deploy|push.*main|push.*master|production|migrate|rollback)') {
173
- $SKILL_REMINDERS += "SAFETY REQUIRED: Call ekkOS_Conflict before this destructive action"
126
+ # Safety Triggers - Destructive actions
127
+ if ($queryLower -match '(delete|drop |rm -rf|deploy|push.*main|push.*master|production|migrate|rollback)') {
128
+ $skillReminders += "SAFETY REQUIRED: Call ekkOS_Conflict before this destructive action"
174
129
  }
175
130
 
176
- # Schema Triggers (Database operations)
177
- if ($QUERY_LOWER -match '(sql|query|supabase|prisma|database|table|column|select |insert |update |where )') {
178
- $SKILL_REMINDERS += "SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names"
131
+ # Schema Triggers - Database operations
132
+ if ($queryLower -match '(sql|query|supabase|prisma|database|table|column|select |insert |update |where )') {
133
+ $skillReminders += "SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names"
179
134
  }
180
135
 
181
- # Secret Triggers (API keys, credentials)
182
- if ($QUERY_LOWER -match '(api key|token|password|credential|secret|my.*key|store.*key)') {
183
- $SKILL_REMINDERS += "SECRETS: Use ekkOS_StoreSecret to securely save credentials"
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 {}
184
148
  }
185
149
 
186
- # Plan Triggers (Complex multi-step tasks)
187
- if ($QUERY_LOWER -match '(implement|build|create.*feature|refactor|migrate|set up|architecture)') {
188
- $SKILL_REMINDERS += "PLAN REQUIRED: Call ekkOS_Plan for complex multi-step tasks"
189
- }
150
+ # ═══════════════════════════════════════════════════════════════════════════
151
+ # SESSION NAME - Resolve early so it's available for all downstream use
152
+ # ═══════════════════════════════════════════════════════════════════════════
153
+ function Convert-UuidToWords {
154
+ param([string]$uuid)
190
155
 
191
- # Learn Triggers (User expressing success/failure)
192
- if ($QUERY_LOWER -match '(that worked|thanks|perfect|great|awesome|nailed it|solved|fixed it)') {
193
- $SKILL_REMINDERS += "LEARN: Consider calling ekkOS_Forge to capture this solution as a pattern"
194
- }
156
+ if (-not $script:SessionWords) { Load-SessionWords }
157
+ if (-not $script:SessionWords) { return "unknown-session" }
195
158
 
196
- # Codebase Triggers (Project-specific code search)
197
- if ($QUERY_LOWER -match '(where is|find.*file|search.*code|in this project|in the codebase)') {
198
- $SKILL_REMINDERS += "CODEBASE: Use ekkOS_Codebase for project-specific code search"
199
- }
159
+ $adjectives = $script:SessionWords.adjectives
160
+ $nouns = $script:SessionWords.nouns
161
+ $verbs = $script:SessionWords.verbs
200
162
 
201
- # Combine skill reminders (only take first 3 to avoid noise)
202
- $SKILL_REMINDER = ""
203
- $reminderCount = $SKILL_REMINDERS.Count
204
- if ($reminderCount -gt 0) {
205
- $maxReminders = [Math]::Min(3, $reminderCount)
206
- $SKILL_REMINDER = ($SKILL_REMINDERS | Select-Object -First $maxReminders) -join "`n"
207
- }
163
+ if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
164
+ if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
208
165
 
209
- # ═══════════════════════════════════════════════════════════════════════════
210
- # Load auth
211
- # ═══════════════════════════════════════════════════════════════════════════
212
- $EKKOS_CONFIG = Join-Path $env:USERPROFILE ".ekkos\config.json"
213
- $AUTH_TOKEN = ""
166
+ $clean = $uuid -replace "-", ""
167
+ if ($clean.Length -lt 12) { return "unknown-session" }
214
168
 
215
- if ((Test-Path $EKKOS_CONFIG) -and (Test-Path $JsonParseHelper)) {
216
- $AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
217
- if (-not $AUTH_TOKEN) {
218
- $AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".apiKey" 2>$null
169
+ try {
170
+ $a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
171
+ $n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
172
+ $an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
173
+ return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
174
+ } catch {
175
+ return "unknown-session"
219
176
  }
220
177
  }
221
178
 
222
- if (-not $AUTH_TOKEN) {
223
- $envLocalFile = Join-Path $ProjectRoot ".env.local"
224
- if (Test-Path $envLocalFile) {
225
- $envLines = Get-Content $envLocalFile
226
- foreach ($line in $envLines) {
227
- if ($line -match '^SUPABASE_SECRET_KEY=(.+)$') {
228
- $AUTH_TOKEN = $Matches[1].Trim('"', "'", ' ', "`r")
229
- break
230
- }
231
- }
232
- }
179
+ $sessionId = $rawSessionId
180
+ if (-not $sessionId -or $sessionId -eq "unknown" -or $sessionId -eq "null") {
181
+ exit 0
233
182
  }
234
183
 
235
- $MEMORY_API_URL = "https://mcp.ekkos.dev"
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
+ }
236
193
 
237
194
  # ═══════════════════════════════════════════════════════════════════════════
238
- # Session ID - NEW ID per conversation (not persisted 24h anymore)
195
+ # STATE DIRECTORIES
239
196
  # ═══════════════════════════════════════════════════════════════════════════
240
- $StateDir = Join-Path $ProjectRoot ".claude\state"
241
- if (-not (Test-Path $StateDir)) {
242
- New-Item -ItemType Directory -Path $StateDir -Force | Out-Null
197
+ $stateDir = Join-Path $ProjectRoot ".claude\state"
198
+ if (-not (Test-Path $stateDir)) {
199
+ New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
243
200
  }
244
- $SESSION_FILE = Join-Path $StateDir "current-session.json"
201
+ $sessionFile = Join-Path $stateDir "current-session.json"
245
202
 
246
- $SESSION_ID = $RAW_SESSION_ID
247
-
248
- # Skip if no valid session ID from Claude
249
- if (-not $SESSION_ID -or $SESSION_ID -eq "unknown" -or $SESSION_ID -eq "null") {
250
- exit 0
203
+ $projectSessionDir = Join-Path $stateDir "sessions"
204
+ if (-not (Test-Path $projectSessionDir)) {
205
+ New-Item -ItemType Directory -Path $projectSessionDir -Force | Out-Null
251
206
  }
252
-
253
- # Save for other hooks to reference
254
- $timestampUtc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
255
- Set-Content -Path $SESSION_FILE -Value "{`"session_id`": `"$SESSION_ID`", `"timestamp`": `"$timestampUtc`"}" -Force
207
+ $turnCounterFile = Join-Path $projectSessionDir "$sessionId.turn"
208
+ $contextSizeFile = Join-Path $projectSessionDir "$sessionId.context"
256
209
 
257
210
  # ═══════════════════════════════════════════════════════════════════════════
258
- # Turn counter - PROJECT-LOCAL storage
211
+ # Turn counter - TRANSCRIPT-BASED (source of truth)
212
+ # Count "type":"user" entries in transcript JSONL
259
213
  # ═══════════════════════════════════════════════════════════════════════════
260
- $ProjectSessionDir = Join-Path $StateDir "sessions"
261
- if (-not (Test-Path $ProjectSessionDir)) {
262
- New-Item -ItemType Directory -Path $ProjectSessionDir -Force | Out-Null
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
+ }
263
231
  }
264
- $TURN_COUNTER_FILE = Join-Path $ProjectSessionDir "$SESSION_ID.turn"
265
232
 
266
- # Count actual user messages in transcript for accurate turn number
267
- $TURN_NUMBER = 1
268
- if ($TRANSCRIPT_PATH -and (Test-Path $TRANSCRIPT_PATH)) {
233
+ # Detect post-clear: saved count higher than transcript means /clear happened
234
+ $savedTurnCount = 0
235
+ if (Test-Path $turnCounterFile) {
269
236
  try {
270
- $transcriptContent = Get-Content $TRANSCRIPT_PATH -Raw
271
- $TURN_NUMBER = ([regex]::Matches($transcriptContent, '"type":"user"')).Count
272
- if ($TURN_NUMBER -eq 0) { $TURN_NUMBER = 1 }
237
+ $savedTurnCount = [int](Get-Content $turnCounterFile -Raw).Trim()
273
238
  } catch {
274
- $TURN_NUMBER = 1
239
+ $savedTurnCount = 0
275
240
  }
276
241
  }
277
242
 
278
- # PRESERVE HISTORY: Don't overwrite if saved count is higher (after /clear)
279
- $SAVED_TURN_COUNT = 0
280
- $TRANSCRIPT_TURN_COUNT = $TURN_NUMBER
281
- $POST_CLEAR_DETECTED = $false
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
282
251
 
283
- if (Test-Path $TURN_COUNTER_FILE) {
252
+ # ═══════════════════════════════════════════════════════════════════════════
253
+ # Context size tracking - Uses tokenizer script (single source)
254
+ # ═══════════════════════════════════════════════════════════════════════════
255
+ $prevContextPercent = 0
256
+ if (Test-Path $contextSizeFile) {
284
257
  try {
285
- $SAVED_TURN_COUNT = [int](Get-Content $TURN_COUNTER_FILE -Raw).Trim()
258
+ $prevContextPercent = [int](Get-Content $contextSizeFile -Raw).Trim()
286
259
  } catch {
287
- $SAVED_TURN_COUNT = 0
260
+ $prevContextPercent = 0
288
261
  }
289
262
  }
290
263
 
291
- if ($SAVED_TURN_COUNT -gt $TURN_NUMBER) {
292
- # Post-clear: INCREMENT from saved count
293
- $TURN_NUMBER = $SAVED_TURN_COUNT + 1
294
- $POST_CLEAR_DETECTED = $true
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 {}
295
280
  }
296
- Set-Content -Path $TURN_COUNTER_FILE -Value "$TURN_NUMBER" -Force
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"
297
296
 
298
297
  # ═══════════════════════════════════════════════════════════════════════════
299
- # WORKING MEMORY: Fast capture each turn (async, non-blocking)
298
+ # SINGLE SOURCE OF TRUTH: Update ALL session tracking systems
300
299
  # ═══════════════════════════════════════════════════════════════════════════
301
- if (Test-Path $EKKOS_CONFIG) {
302
- $CAPTURE_TOKEN = ""
303
- if (Test-Path $JsonParseHelper) {
304
- $CAPTURE_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
305
- if (-not $CAPTURE_TOKEN) {
306
- $CAPTURE_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".apiKey" 2>$null
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 '\\', '/'
304
+
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 {}
314
+
315
+ # 2. Global ekkOS state (for extension LOCAL-FIRST read)
316
+ $ekkosGlobalState = Join-Path $EkkosConfigDir "current-session.json"
317
+ try {
318
+ if (-not (Test-Path $EkkosConfigDir)) {
319
+ New-Item -ItemType Directory -Path $EkkosConfigDir -Force | Out-Null
307
320
  }
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});
308
360
  }
361
+ fs.writeFileSync(filePath, JSON.stringify(sessions, null, 2));
362
+ } catch(e) {}
363
+ " $sessionId $sessionName $utcTimestamp $ProjectRoot $activeSessionsFile 2>$null
364
+ } catch {}
309
365
 
310
- if ($CAPTURE_TOKEN -and $CAPTURE_TOKEN -ne "null") {
311
- # Async capture to Redis/Supabase - doesn't block hook execution
312
- $queryEscaped = $USER_QUERY -replace '"', '\"' -replace '\\', '\\\\'
313
- $captureBody = "{`"session_id`":`"$RAW_SESSION_ID`",`"turn`":$TURN_NUMBER,`"query`":`"$queryEscaped`"}"
366
+ # 5. Update Redis via API (async)
367
+ if ($authToken) {
368
+ $sessionBody = @{ session_name = $sessionName } | ConvertTo-Json -Depth 10 -Compress
314
369
  Start-Job -ScriptBlock {
315
- param($url, $token, $body)
370
+ param($body, $token, $apiUrl)
316
371
  try {
317
- Invoke-RestMethod -Uri "$url/api/v1/working/fast-capture" `
372
+ Invoke-RestMethod -Uri "$apiUrl/api/v1/working/session/current" `
318
373
  -Method POST `
319
374
  -Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
320
375
  -Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
321
- -TimeoutSec 3 `
322
- -ErrorAction SilentlyContinue | Out-Null
376
+ -TimeoutSec 3 -ErrorAction SilentlyContinue | Out-Null
323
377
  } catch {}
324
- } -ArgumentList $MEMORY_API_URL, $CAPTURE_TOKEN, $captureBody | Out-Null
378
+ } -ArgumentList $sessionBody, $authToken, $MemoryApiUrl | Out-Null
325
379
  }
326
- }
327
380
 
328
- # ═══════════════════════════════════════════════════════════════════════════
329
- # LOCAL CACHE: Tier 0 capture for instant /continue (async, non-blocking)
330
- # ═══════════════════════════════════════════════════════════════════════════
331
- $ekkosCapturePath = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
332
- if ($ekkosCapturePath) {
333
- $SESSION_NAME_FOR_CAPTURE = Convert-UuidToWords $RAW_SESSION_ID
334
- $projectRootNorm = $ProjectRoot -replace '\\', '/'
335
- Start-Job -ScriptBlock {
336
- param($sessionId, $sessionName, $turnNum, $query, $projRoot)
381
+ # 6. CRITICAL: Bind session name to proxy for R2 eviction paths
382
+ $ekkosUserId = ""
383
+ if ((Test-Path $EkkosConfig) -and (Test-Path $JsonParseHelper)) {
337
384
  try {
338
- & ekkos-capture user $sessionId $sessionName $turnNum $query $projRoot 2>$null | Out-Null
385
+ $ekkosUserId = node $JsonParseHelper $EkkosConfig '.userId' 2>$null
339
386
  } catch {}
340
- } -ArgumentList $RAW_SESSION_ID, $SESSION_NAME_FOR_CAPTURE, $TURN_NUMBER, $USER_QUERY, $projectRootNorm | Out-Null
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
396
+
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
+ }
341
408
  }
342
409
 
343
410
  # ═══════════════════════════════════════════════════════════════════════════
344
- # GOLDEN LOOP: CAPTURE PHASE - Track turn start
411
+ # LOCAL CACHE: Tier 0 capture (async, non-blocking)
412
+ # Per v1.2 ADDENDUM: Pass instanceId for namespacing
345
413
  # ═══════════════════════════════════════════════════════════════════════════
346
- $EkkosDir = Join-Path $ProjectRoot ".ekkos"
347
- if (-not (Test-Path $EkkosDir)) {
348
- New-Item -ItemType Directory -Path $EkkosDir -Force | Out-Null
349
- }
350
- $GOLDEN_LOOP_FILE = Join-Path $EkkosDir "golden-loop-current.json"
414
+ $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
415
+ if ($captureCmd -and $sessionId -ne "unknown") {
416
+ try {
417
+ $queryBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userQuery))
418
+ $captureProjectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
351
419
 
352
- # Write current phase to file (extension watches this for real-time updates)
353
- $glPathEscaped = $GOLDEN_LOOP_FILE -replace '\\', '\\\\'
354
- try {
355
- node -e "
356
- const fs = require('fs');
357
- const data = {
358
- phase: 'capture',
359
- turn: $TURN_NUMBER,
360
- session: '$SESSION_ID',
361
- timestamp: new Date().toISOString(),
362
- stats: { retrieved: 0, applied: 0, forged: 0 }
363
- };
364
- fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
365
- " 2>$null
366
- } catch {}
367
-
368
- # ═══════════════════════════════════════════════════════════════════════════
369
- # GOLDEN LOOP: RETRIEVE PHASE - Auto-retrieve patterns from ekkOS
370
- # ═══════════════════════════════════════════════════════════════════════════
371
- $EKKOS_API_KEY = ""
372
- $hookApiKeyFile = Join-Path $env:USERPROFILE ".ekkos\.hookApiKey"
373
- if (Test-Path $hookApiKeyFile) {
374
- $EKKOS_API_KEY = (Get-Content $hookApiKeyFile -Raw).Trim()
375
- } elseif ((Test-Path $EKKOS_CONFIG) -and (Test-Path $JsonParseHelper)) {
376
- $EKKOS_API_KEY = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
420
+ Start-Job -ScriptBlock {
421
+ param($instanceId, $sid, $sname, $turnNum, $queryB64, $projPath)
422
+ try {
423
+ $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($queryB64))
424
+ & ekkos-capture user $instanceId $sid $sname $turnNum $decoded $projPath 2>&1 | Out-Null
425
+ } catch {}
426
+ } -ArgumentList $EkkosInstanceId, $sessionId, $sessionName, $turnNumber, $queryBase64, $captureProjectRoot | Out-Null
427
+ } catch {}
377
428
  }
378
429
 
379
- $RETRIEVED_PATTERNS = ""
380
- $PATTERN_COUNT = 0
381
- $RETRIEVED_DIRECTIVES = ""
382
- $DIRECTIVE_COUNT = 0
383
-
384
430
  # ═══════════════════════════════════════════════════════════════════════════
385
- # DIRECTIVE CACHE: Local cache to avoid API calls every turn
431
+ # WORKING MEMORY: Fast capture to API (async, non-blocking)
386
432
  # ═══════════════════════════════════════════════════════════════════════════
387
- $DirectiveCacheDir = Join-Path $env:USERPROFILE ".ekkos\cache"
388
- $DirectiveCacheFile = Join-Path $DirectiveCacheDir "directives.json"
389
- $DIRECTIVE_CACHE_TTL = 3600 # 1 hour in seconds
390
- if (-not (Test-Path $DirectiveCacheDir)) {
391
- New-Item -ItemType Directory -Path $DirectiveCacheDir -Force | Out-Null
433
+ if ($authToken) {
434
+ 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" `
446
+ -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
452
+ } catch {}
392
453
  }
393
454
 
394
- # Check if we need to refresh directive cache
395
- $DIRECTIVE_CACHE_VALID = $false
396
- $DIRECTIVE_TRIGGER_DETECTED = $false
397
-
398
- if ($QUERY_LOWER -match '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on|directive|preference)') {
399
- $DIRECTIVE_TRIGGER_DETECTED = $true
455
+ # ═══════════════════════════════════════════════════════════════════════════
456
+ # DASHBOARD HINT FILE: write session info for ekkos dashboard --wait-for-new
457
+ # On Windows, active-sessions.json is never populated (hook PIDs are dead).
458
+ # The dashboard reads this file instead to locate the JSONL path.
459
+ # ═══════════════════════════════════════════════════════════════════════════
460
+ if ($sessionName -ne "unknown-session") {
461
+ try {
462
+ $hintProjectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
463
+ $hintFile = Join-Path $EkkosConfigDir "hook-session-hint.json"
464
+ $hint = @{
465
+ sessionName = $sessionName
466
+ sessionId = $sessionId
467
+ projectPath = $hintProjectPath
468
+ ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
469
+ } | ConvertTo-Json -Depth 10 -Compress
470
+ Set-Content -Path $hintFile -Value $hint -Force
471
+ } catch {}
400
472
  }
401
473
 
402
- if (Test-Path $DirectiveCacheFile) {
403
- $cacheContent = Get-Content $DirectiveCacheFile -Raw
404
- $cacheTimestampStr = Parse-JsonValue $cacheContent ".cached_at"
405
- if ($cacheTimestampStr -match '^\d+$') {
406
- $cacheTimestamp = [int64]$cacheTimestampStr
407
- $currentTimestamp = [int64]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())
408
- $cacheAge = $currentTimestamp - $cacheTimestamp
474
+ # ═══════════════════════════════════════════════════════════════════════════
475
+ # "/continue" COMMAND: Run AFTER /clear to restore last 5 turns
476
+ # ═══════════════════════════════════════════════════════════════════════════
477
+ $queryLowerTrimmed = $queryLower.Trim()
409
478
 
410
- if ($cacheAge -lt $DIRECTIVE_CACHE_TTL -and -not $DIRECTIVE_TRIGGER_DETECTED) {
411
- $DIRECTIVE_CACHE_VALID = $true
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 = @() }
412
496
  }
413
- }
414
- }
415
497
 
416
- # Decide whether to inject directives this turn
417
- $SHOULD_INJECT_DIRECTIVES = $false
418
- if ($TURN_NUMBER -eq 1 -or $POST_CLEAR_DETECTED -or $DIRECTIVE_TRIGGER_DETECTED) {
419
- $SHOULD_INJECT_DIRECTIVES = $true
420
- }
498
+ $restoreJson = $restoreResponse | ConvertTo-Json -Depth 20 -Compress
499
+ $restoredCount = 0
500
+ $lastTask = "unknown task"
501
+ $lastResponse = ""
421
502
 
422
- if ($EKKOS_API_KEY -and $USER_QUERY) {
423
- # Update phase to RETRIEVE
424
- try {
425
- node -e "
426
- const fs = require('fs');
427
- const data = {
428
- phase: 'retrieve',
429
- turn: $TURN_NUMBER,
430
- session: '$SESSION_ID',
431
- timestamp: new Date().toISOString(),
432
- stats: { retrieved: 0, applied: 0, forged: 0 }
433
- };
434
- fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
435
- " 2>$null
436
- } catch {}
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 }
437
510
 
438
- # Build sources array - always include patterns, conditionally include directives
439
- if ($DIRECTIVE_CACHE_VALID) {
440
- $searchSources = '["patterns"]'
441
- } else {
442
- $searchSources = '["patterns", "directives"]'
443
- }
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" }
444
518
 
445
- # Escape user query for JSON
446
- $queryJsonEscaped = $USER_QUERY -replace '\\', '\\\\' -replace '"', '\"' -replace "`n", '\n' -replace "`r", '' -replace "`t", '\t'
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 = "" }
447
526
 
448
- # Call ekkOS MCP gateway
449
- $SEARCH_RESPONSE_RAW = $null
450
- try {
451
- $searchBody = "{`"tool`": `"ekkOS_Search`", `"arguments`": {`"query`": `"$queryJsonEscaped`", `"limit`": 5, `"sources`": $searchSources}}"
452
- $SEARCH_RESPONSE_RAW = Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/mcp/call" `
453
- -Method POST `
454
- -Headers @{ Authorization = "Bearer $EKKOS_API_KEY"; "Content-Type" = "application/json" } `
455
- -Body ([System.Text.Encoding]::UTF8.GetBytes($searchBody)) `
456
- -TimeoutSec 2 `
457
- -ErrorAction Stop
458
- } catch {
459
- $SEARCH_RESPONSE_RAW = $null
460
- }
527
+ Write-Output ""
528
+ Write-Output "${GREEN}${BOLD}Session continued${RESET} ${DIM}(${restoredCount} turns restored)${RESET}"
529
+ Write-Output ""
461
530
 
462
- if ($SEARCH_RESPONSE_RAW) {
463
- # Convert to JSON string for Node parsing
464
- $SEARCH_JSON = ""
465
- if ($SEARCH_RESPONSE_RAW -is [string]) {
466
- $SEARCH_JSON = $SEARCH_RESPONSE_RAW
467
- } else {
468
- try {
469
- $SEARCH_JSON = $SEARCH_RESPONSE_RAW | ConvertTo-Json -Depth 20 -Compress
470
- } catch {
471
- $SEARCH_JSON = '{}'
472
- }
473
- }
531
+ $continueOutput = @"
532
+ <system-reminder>
533
+ $("=" * 75)
534
+ CONTEXT RESTORED - Resume seamlessly. DO NOT ask 'what were we doing?'
535
+ $("=" * 75)
474
536
 
475
- # Count patterns retrieved
476
- $patternCountStr = $SEARCH_JSON | node -e "
477
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
478
- const patterns = (d.result && d.result.results && d.result.results.patterns) || [];
479
- console.log(patterns.length);
480
- " 2>$null
481
- if ($patternCountStr -match '^\d+$') {
482
- $PATTERN_COUNT = [int]$patternCountStr
483
- }
537
+ ## Last User Request:
538
+ $lastTask
484
539
 
485
- # Update golden loop with retrieved count
486
- if ($PATTERN_COUNT -gt 0) {
487
- try {
488
- node -e "
489
- const fs = require('fs');
490
- const data = {
491
- phase: 'inject',
492
- turn: $TURN_NUMBER,
493
- session: '$SESSION_ID',
494
- timestamp: new Date().toISOString(),
495
- stats: { retrieved: $PATTERN_COUNT, applied: 0, forged: 0 }
496
- };
497
- fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
498
- " 2>$null
499
- } catch {}
540
+ ## Your Last Response (truncated):
541
+ $lastResponse
500
542
 
501
- # Format patterns for injection
502
- $RETRIEVED_PATTERNS = $SEARCH_JSON | node -e "
503
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
504
- const patterns = (d.result && d.result.results && d.result.results.patterns) || [];
505
- patterns.forEach(p => {
506
- console.log('**' + (p.title || '') + '**');
507
- console.log(p.problem || p.guidance || '');
508
- console.log('');
509
- console.log('## Solution');
510
- console.log(p.solution || p.content || '');
511
- console.log('');
512
- console.log('Success Rate: ' + ((p.success_rate || 0) * 100) + '%');
513
- console.log('Applied: ' + (p.applied_count || 0) + ' times');
514
- console.log('');
515
- });
516
- " 2>$null
517
- }
543
+ "@
518
544
 
519
- # ═══════════════════════════════════════════════════════════════════
520
- # DIRECTIVE HANDLING: Use cache if valid, otherwise process response
521
- # ═══════════════════════════════════════════════════════════════════
522
- if ($DIRECTIVE_CACHE_VALID) {
523
- # Load directives from cache
524
- $cacheContent = Get-Content $DirectiveCacheFile -Raw
525
- $dcStr = Parse-JsonValue $cacheContent ".count"
526
- if ($dcStr -match '^\d+$') { $DIRECTIVE_COUNT = [int]$dcStr }
527
- $RETRIEVED_DIRECTIVES = Parse-JsonValue $cacheContent ".formatted"
528
- } else {
529
- # Extract and format DIRECTIVES from API response
530
- $directiveResult = $SEARCH_JSON | node -e "
531
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
532
- const directives = (d.result && d.result.results && d.result.results.directives) || [];
533
- const count = directives.length;
534
- if (count === 0) { console.log(JSON.stringify({count: 0, formatted: ''})); process.exit(0); }
535
-
536
- const grouped = {MUST: [], NEVER: [], PREFER: [], AVOID: []};
537
- directives.forEach(d => {
538
- if (grouped[d.type]) grouped[d.type].push(d.rule);
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 + '...');
539
554
  });
555
+ " $restoreJson 2>$null
556
+ $continueOutput += "$recentContext`n"
557
+ } catch {}
558
+ $continueOutput += "`n"
559
+ }
540
560
 
541
- let formatted = 'USER DIRECTIVES (FOLLOW THESE):';
542
- for (const [type, rules] of Object.entries(grouped)) {
543
- if (rules.length > 0) {
544
- formatted += '\n\n' + type + ':';
545
- rules.forEach(r => { formatted += '\n - ' + r; });
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
+ "@
568
+
569
+ Write-Output $continueOutput
570
+ Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| ${timestamp}${RESET}"
571
+ exit 0
546
572
  }
547
573
  }
548
574
 
549
- console.log(JSON.stringify({count, formatted}));
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}"
550
582
 
551
- // Save to cache
552
- const cacheData = {count, formatted, cached_at: Math.floor(Date.now() / 1000)};
553
- try {
554
- const fs = require('fs');
555
- fs.mkdirSync(process.argv[1], {recursive: true});
556
- fs.writeFileSync(process.argv[2], JSON.stringify(cacheData));
557
- } catch(e) {}
558
- " -- "$DirectiveCacheDir" "$DirectiveCacheFile" 2>$null
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
+ }
559
599
 
560
- if ($directiveResult) {
561
- $dcStr = $directiveResult | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.count||0)" 2>$null
562
- if ($dcStr -match '^\d+$') { $DIRECTIVE_COUNT = [int]$dcStr }
563
- $RETRIEVED_DIRECTIVES = $directiveResult | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.formatted||'')" 2>$null
564
- }
565
- }
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}"
566
630
  }
567
- }
568
631
 
569
- # ═══════════════════════════════════════════════════════════════════════════
570
- # COLORS (ANSI escape sequences via [char]27)
571
- # ═══════════════════════════════════════════════════════════════════════════
572
- $ESC = [char]27
573
- $CYAN = "$ESC[0;36m"
574
- $GREEN = "$ESC[0;32m"
575
- $YELLOW = "$ESC[1;33m"
576
- $MAGENTA = "$ESC[0;35m"
577
- $DIM = "$ESC[2m"
578
- $BOLD = "$ESC[1m"
579
- $RESET = "$ESC[0m"
632
+ Write-Output ""
633
+ Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| ${sessionName} | ${timestamp}${RESET}"
580
634
 
581
- $CURRENT_TIME = Get-Date -Format "yyyy-MM-dd hh:mm:ss tt K"
635
+ } elseif ($postClearDetected -and $authToken) {
636
+ # /clear detected - show visible restoration banner
637
+ $separator = "${GREEN}" + ("=" * 78) + "${RESET}"
582
638
 
583
- # Generate session name
584
- $SESSION_NAME = ""
585
- if ($SESSION_ID -and $SESSION_ID -ne "unknown" -and $SESSION_ID -ne "null") {
586
- $SESSION_NAME = Convert-UuidToWords $SESSION_ID
587
- }
639
+ Write-Output $separator
640
+ Write-Output "${GREEN}${BOLD}SESSION CONTINUED${RESET} ${DIM}| ${turnNumber} turns preserved | Restoring context...${RESET}"
641
+ Write-Output $separator
588
642
 
589
- # ═══════════════════════════════════════════════════════════════════════════
590
- # "/continue" COMMAND: Delegated to Skill system (DO NOT INTERCEPT)
591
- # ═══════════════════════════════════════════════════════════════════════════
592
- # REMOVED: Hook used to intercept /continue and do simple restoration
593
- # NOW: Let /continue Skill handle it - supports session names + intelligent narrative
643
+ # Also write to stderr for visibility
644
+ [Console]::Error.WriteLine("${GREEN}${BOLD}SESSION CONTINUED${RESET} ${DIM}| ${turnNumber} turns preserved | Context restored${RESET}")
594
645
 
595
- # ═══════════════════════════════════════════════════════════════════════════
596
- # AUTO-RESTORE REMOVED: Manual /continue only (saves 79% token burn!)
597
- # ═══════════════════════════════════════════════════════════════════════════
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
+ }
598
662
 
599
- # Simple status line - no context warnings, Claude handles its own context
600
- Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
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 }
601
672
 
602
- # Output skill reminder if detected
603
- if ($SKILL_REMINDER) {
604
- Write-Output ""
605
- Write-Output "${MAGENTA}${BOLD}${SKILL_REMINDER}${RESET}"
606
- }
673
+ if ($clearRestoredCount -gt 0) {
674
+ Write-Output "${GREEN} Restored ${clearRestoredCount} recent turns${RESET}"
675
+ Write-Output ""
607
676
 
608
- # GOLDEN LOOP: INJECT PHASE - Inject directives FIRST (highest priority)
609
- # SMART INJECTION: Only on Turn 1, post-clear, or directive trigger
610
- if ($SHOULD_INJECT_DIRECTIVES -and $RETRIEVED_DIRECTIVES -and $DIRECTIVE_COUNT -gt 0) {
611
- Write-Output ""
612
- Write-Output "<system-reminder>"
613
- Write-Output "$RETRIEVED_DIRECTIVES"
614
- Write-Output "</system-reminder>"
615
- }
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
+ }
616
691
 
617
- # GOLDEN LOOP: INJECT PHASE - Inject retrieved patterns into context
618
- if ($RETRIEVED_PATTERNS -and $PATTERN_COUNT -gt 0) {
619
692
  Write-Output ""
620
- Write-Output "<system-reminder>"
621
- Write-Output "RETRIEVED PATTERNS FROM ekkOS MEMORY ($PATTERN_COUNT patterns found)"
693
+ Write-Output "${DIM}Full history: `"recall`" or `"turns 1-${turnNumber}`"${RESET}"
694
+ Write-Output $separator
622
695
  Write-Output ""
623
- Write-Output "$RETRIEVED_PATTERNS"
624
- Write-Output ""
625
- Write-Output "These patterns were automatically retrieved based on your query. Review and apply relevant solutions."
626
- Write-Output "</system-reminder>"
627
- Write-Output ""
628
- Write-Output "MANDATORY PATTERN ACKNOWLEDGMENT (PatternGuard):"
629
- Write-Output "You MUST acknowledge ALL $PATTERN_COUNT patterns above - either SELECT or SKIP each one."
630
- Write-Output ""
631
- Write-Output "For patterns you USE:"
632
- Write-Output "[ekkOS_SELECT]"
633
- Write-Output "- id: <pattern_id>"
634
- Write-Output " reason: <1-line why using>"
635
- Write-Output " confidence: <0.0-1.0>"
636
- Write-Output "[/ekkOS_SELECT]"
637
- Write-Output ""
638
- Write-Output "For patterns NOT relevant:"
639
- Write-Output "[ekkOS_SKIP]"
640
- Write-Output "- id: <pattern_id>"
641
- Write-Output " reason: <1-line why not relevant>"
642
- Write-Output "[/ekkOS_SKIP]"
643
- Write-Output ""
644
- Write-Output "AUTOMATIC APPLICATION: When pattern matches your task, APPLY IT immediately without asking."
645
- Write-Output "Track outcome after: ekkOS_Outcome({success: true/false})"
646
- Write-Output ""
647
- Write-Output "100% coverage required. This is how the system learns what works."
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}"
648
705
  }
649
706
 
650
- # Inject footer format reminder (helps Claude remember session name)
651
- if ($SESSION_NAME -and $SESSION_NAME -ne "unknown-session") {
707
+ # ═══════════════════════════════════════════════════════════════════════════
708
+ # Output skill reminders if detected
709
+ # ═══════════════════════════════════════════════════════════════════════════
710
+ if ($skillReminders.Count -gt 0) {
652
711
  Write-Output ""
653
- Write-Output "<footer-format>End responses with: Claude Code ({Model}) · ekkOS_ · Turn ${TURN_NUMBER} · ${SESSION_NAME} · ${CURRENT_TIME}</footer-format>"
712
+ Write-Output "${MAGENTA}${BOLD}$($skillReminders -join "`n")${RESET}"
654
713
  }
655
714
 
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>"
721
+
656
722
  exit 0