@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.
- package/package.json +2 -4
- package/templates/CLAUDE.md +135 -23
- package/templates/ekkos-manifest.json +8 -8
- package/templates/hooks/assistant-response.ps1 +188 -160
- package/templates/hooks/assistant-response.sh +130 -66
- package/templates/hooks/hooks.json +6 -0
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/session-start.ps1 +218 -355
- package/templates/hooks/session-start.sh +202 -167
- package/templates/hooks/stop.ps1 +305 -298
- package/templates/hooks/stop.sh +275 -948
- package/templates/hooks/user-prompt-submit.ps1 +563 -497
- package/templates/hooks/user-prompt-submit.sh +383 -457
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/lib/contract.sh +2 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -219
|
@@ -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
|
-
#
|
|
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 -
|
|
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 {
|
|
20
|
-
$SessionWordsJson =
|
|
21
|
-
$SessionWordsDefault =
|
|
22
|
-
$JsonParseHelper =
|
|
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
|
-
#
|
|
32
|
+
# INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
|
|
26
33
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
-
$
|
|
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
|
-
|
|
33
|
-
|
|
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 (
|
|
41
|
-
$
|
|
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
|
-
$
|
|
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
|
-
$
|
|
126
|
-
if (-not $
|
|
68
|
+
$inputJson = [Console]::In.ReadToEnd()
|
|
69
|
+
if (-not $inputJson) { exit 0 }
|
|
127
70
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
-
$
|
|
134
|
-
if (-not $
|
|
135
|
-
$
|
|
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
|
|
138
|
-
if ($
|
|
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
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
$
|
|
154
|
-
$
|
|
108
|
+
$skillReminders = @()
|
|
109
|
+
$queryLower = $userQuery.ToLower()
|
|
155
110
|
|
|
156
111
|
# Memory First - Debug/Error/Problem solving
|
|
157
|
-
if ($
|
|
158
|
-
$
|
|
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
|
|
162
|
-
if ($
|
|
163
|
-
$
|
|
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
|
|
167
|
-
if ($
|
|
168
|
-
$
|
|
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
|
|
172
|
-
if ($
|
|
173
|
-
$
|
|
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
|
|
177
|
-
if ($
|
|
178
|
-
$
|
|
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
|
-
#
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
192
|
-
if (
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
$
|
|
199
|
-
}
|
|
159
|
+
$adjectives = $script:SessionWords.adjectives
|
|
160
|
+
$nouns = $script:SessionWords.nouns
|
|
161
|
+
$verbs = $script:SessionWords.verbs
|
|
200
162
|
|
|
201
|
-
|
|
202
|
-
$
|
|
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
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
$
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
195
|
+
# STATE DIRECTORIES
|
|
239
196
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
240
|
-
$
|
|
241
|
-
if (-not (Test-Path $
|
|
242
|
-
New-Item -ItemType Directory -Path $
|
|
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
|
-
$
|
|
201
|
+
$sessionFile = Join-Path $stateDir "current-session.json"
|
|
245
202
|
|
|
246
|
-
$
|
|
247
|
-
|
|
248
|
-
|
|
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
|
-
|
|
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 -
|
|
211
|
+
# Turn counter - TRANSCRIPT-BASED (source of truth)
|
|
212
|
+
# Count "type":"user" entries in transcript JSONL
|
|
259
213
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
260
|
-
$
|
|
261
|
-
if (-
|
|
262
|
-
|
|
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
|
-
#
|
|
267
|
-
$
|
|
268
|
-
if (
|
|
233
|
+
# Detect post-clear: saved count higher than transcript means /clear happened
|
|
234
|
+
$savedTurnCount = 0
|
|
235
|
+
if (Test-Path $turnCounterFile) {
|
|
269
236
|
try {
|
|
270
|
-
$
|
|
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
|
-
$
|
|
239
|
+
$savedTurnCount = 0
|
|
275
240
|
}
|
|
276
241
|
}
|
|
277
242
|
|
|
278
|
-
|
|
279
|
-
$
|
|
280
|
-
$
|
|
281
|
-
$
|
|
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
|
-
|
|
252
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
253
|
+
# Context size tracking - Uses tokenizer script (single source)
|
|
254
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
255
|
+
$prevContextPercent = 0
|
|
256
|
+
if (Test-Path $contextSizeFile) {
|
|
284
257
|
try {
|
|
285
|
-
$
|
|
258
|
+
$prevContextPercent = [int](Get-Content $contextSizeFile -Raw).Trim()
|
|
286
259
|
} catch {
|
|
287
|
-
$
|
|
260
|
+
$prevContextPercent = 0
|
|
288
261
|
}
|
|
289
262
|
}
|
|
290
263
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
298
|
+
# SINGLE SOURCE OF TRUTH: Update ALL session tracking systems
|
|
300
299
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
301
|
-
if (
|
|
302
|
-
$
|
|
303
|
-
if (
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
$
|
|
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($
|
|
370
|
+
param($body, $token, $apiUrl)
|
|
316
371
|
try {
|
|
317
|
-
Invoke-RestMethod -Uri "$
|
|
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 $
|
|
378
|
+
} -ArgumentList $sessionBody, $authToken, $MemoryApiUrl | Out-Null
|
|
325
379
|
}
|
|
326
|
-
}
|
|
327
380
|
|
|
328
|
-
#
|
|
329
|
-
|
|
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
|
-
|
|
385
|
+
$ekkosUserId = node $JsonParseHelper $EkkosConfig '.userId' 2>$null
|
|
339
386
|
} catch {}
|
|
340
|
-
}
|
|
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
|
-
#
|
|
411
|
+
# LOCAL CACHE: Tier 0 capture (async, non-blocking)
|
|
412
|
+
# Per v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
345
413
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
346
|
-
$
|
|
347
|
-
if (-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
$
|
|
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
|
-
|
|
353
|
-
$
|
|
354
|
-
try {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
#
|
|
431
|
+
# WORKING MEMORY: Fast capture to API (async, non-blocking)
|
|
386
432
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
#
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
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
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
|
|
411
|
-
|
|
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
|
-
|
|
417
|
-
$
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
}
|
|
498
|
+
$restoreJson = $restoreResponse | ConvertTo-Json -Depth 20 -Compress
|
|
499
|
+
$restoredCount = 0
|
|
500
|
+
$lastTask = "unknown task"
|
|
501
|
+
$lastResponse = ""
|
|
421
502
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
476
|
-
|
|
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
|
-
|
|
486
|
-
|
|
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
|
-
|
|
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
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
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
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
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
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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
|
-
|
|
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
|
-
|
|
635
|
+
} elseif ($postClearDetected -and $authToken) {
|
|
636
|
+
# /clear detected - show visible restoration banner
|
|
637
|
+
$separator = "${GREEN}" + ("=" * 78) + "${RESET}"
|
|
582
638
|
|
|
583
|
-
|
|
584
|
-
$
|
|
585
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
600
|
-
|
|
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
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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 "
|
|
621
|
-
Write-Output
|
|
693
|
+
Write-Output "${DIM}Full history: `"recall`" or `"turns 1-${turnNumber}`"${RESET}"
|
|
694
|
+
Write-Output $separator
|
|
622
695
|
Write-Output ""
|
|
623
|
-
Write-Output "$
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
Write-Output ""
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
Write-Output "
|
|
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
|
-
#
|
|
651
|
-
|
|
707
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
708
|
+
# Output skill reminders if detected
|
|
709
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
710
|
+
if ($skillReminders.Count -gt 0) {
|
|
652
711
|
Write-Output ""
|
|
653
|
-
Write-Output "
|
|
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
|