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