@ekkos/cli 1.0.36 → 1.1.0
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/README.md +57 -0
- package/dist/commands/dashboard.js +561 -186
- package/dist/deploy/settings.js +13 -26
- package/package.json +1 -1
- package/templates/hooks/assistant-response.ps1 +94 -26
- package/templates/hooks/hooks.json +24 -12
- package/templates/hooks/lib/count-tokens.cjs +0 -0
- package/templates/hooks/lib/ekkos-reminders.sh +0 -0
- package/templates/hooks/session-start.ps1 +61 -224
- package/templates/hooks/session-start.sh +1 -1
- package/templates/hooks/stop.ps1 +103 -249
- package/templates/hooks/stop.sh +1 -1
- package/templates/hooks/user-prompt-submit.ps1 +129 -519
- package/templates/hooks/user-prompt-submit.sh +2 -2
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/windsurf-hooks/install.sh +0 -0
- package/templates/windsurf-hooks/lib/contract.sh +0 -0
- package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
- package/templates/agents/README.md +0 -182
- package/templates/agents/code-reviewer.md +0 -166
- package/templates/agents/debug-detective.md +0 -169
- package/templates/agents/ekkOS_Vercel.md +0 -99
- package/templates/agents/extension-manager.md +0 -229
- package/templates/agents/git-companion.md +0 -185
- package/templates/agents/github-test-agent.md +0 -321
- package/templates/agents/railway-manager.md +0 -179
- package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +0 -219
|
@@ -3,12 +3,7 @@
|
|
|
3
3
|
# MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
|
|
4
4
|
# EKKOS_MANAGED=1
|
|
5
5
|
# EKKOS_MANIFEST_SHA256=<computed-at-build>
|
|
6
|
-
# EKKOS_TEMPLATE_VERSION=
|
|
7
|
-
#
|
|
8
|
-
# ZERO USER ACTION NEEDED:
|
|
9
|
-
# 1. Tracks turn number and context size
|
|
10
|
-
# 2. Detects when compaction happened (context dropped from high to low)
|
|
11
|
-
# 3. AUTO-INJECTS restored context - user just keeps working
|
|
6
|
+
# EKKOS_TEMPLATE_VERSION=1.0.0
|
|
12
7
|
#
|
|
13
8
|
# Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
|
|
14
9
|
# - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
@@ -17,27 +12,18 @@
|
|
|
17
12
|
|
|
18
13
|
$ErrorActionPreference = "SilentlyContinue"
|
|
19
14
|
|
|
20
|
-
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
21
|
-
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
|
22
|
-
|
|
23
15
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
24
16
|
# CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
|
|
25
17
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
26
18
|
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
|
|
27
19
|
$SessionWordsJson = "$EkkosConfigDir\session-words.json"
|
|
28
20
|
$SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
|
|
29
|
-
$JsonParseHelper = "$EkkosConfigDir\.helpers\json-parse.cjs"
|
|
30
21
|
|
|
31
22
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
32
23
|
# INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
|
|
33
24
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
34
25
|
$EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
|
|
35
26
|
|
|
36
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
-
# API URL
|
|
38
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
-
$MemoryApiUrl = "https://api.ekkos.dev"
|
|
40
|
-
|
|
41
27
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
42
28
|
# Load session words from JSON file - NO HARDCODED ARRAYS
|
|
43
29
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -62,9 +48,7 @@ function Load-SessionWords {
|
|
|
62
48
|
}
|
|
63
49
|
}
|
|
64
50
|
|
|
65
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
66
51
|
# Read input from stdin
|
|
67
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
68
52
|
$inputJson = [Console]::In.ReadToEnd()
|
|
69
53
|
if (-not $inputJson) { exit 0 }
|
|
70
54
|
|
|
@@ -81,24 +65,15 @@ if (-not $userQuery) { exit 0 }
|
|
|
81
65
|
|
|
82
66
|
$rawSessionId = $input.session_id
|
|
83
67
|
if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
|
|
84
|
-
$transcriptPath = $input.transcript_path
|
|
85
68
|
|
|
86
69
|
# Fallback: read session_id from saved state
|
|
87
|
-
if ($rawSessionId -eq "unknown"
|
|
70
|
+
if ($rawSessionId -eq "unknown") {
|
|
88
71
|
$stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
|
|
89
|
-
if (
|
|
72
|
+
if (Test-Path $stateFile) {
|
|
90
73
|
try {
|
|
91
|
-
$
|
|
92
|
-
|
|
93
|
-
} catch {
|
|
94
|
-
$rawSessionId = "unknown"
|
|
95
|
-
}
|
|
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)
|
|
74
|
+
$state = Get-Content $stateFile -Raw | ConvertFrom-Json
|
|
75
|
+
$rawSessionId = $state.session_id
|
|
76
|
+
} catch {}
|
|
102
77
|
}
|
|
103
78
|
}
|
|
104
79
|
|
|
@@ -133,20 +108,6 @@ if ($queryLower -match '(sql|query|supabase|prisma|database|table|column|select
|
|
|
133
108
|
$skillReminders += "SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names"
|
|
134
109
|
}
|
|
135
110
|
|
|
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 {}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
111
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
151
112
|
# SESSION NAME - Resolve early so it's available for all downstream use
|
|
152
113
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -176,282 +137,158 @@ function Convert-UuidToWords {
|
|
|
176
137
|
}
|
|
177
138
|
}
|
|
178
139
|
|
|
179
|
-
$
|
|
180
|
-
if (-not $sessionId -or $sessionId -eq "unknown" -or $sessionId -eq "null") {
|
|
181
|
-
exit 0
|
|
182
|
-
}
|
|
183
|
-
|
|
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
|
-
}
|
|
140
|
+
$sessionName = Convert-UuidToWords $rawSessionId
|
|
193
141
|
|
|
194
142
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
195
|
-
#
|
|
143
|
+
# PROXY SESSION BIND: _pending → real session name (fires every turn)
|
|
144
|
+
# Mirrors bash user-prompt-submit.sh lines 319-338.
|
|
145
|
+
# No PTY on Windows so run.ts can't detect session name — hook must bind it.
|
|
196
146
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
197
|
-
$
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
$
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
$
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
}
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
# Detect post-clear: saved count higher than transcript means /clear happened
|
|
234
|
-
$savedTurnCount = 0
|
|
235
|
-
if (Test-Path $turnCounterFile) {
|
|
236
|
-
try {
|
|
237
|
-
$savedTurnCount = [int](Get-Content $turnCounterFile -Raw).Trim()
|
|
238
|
-
} catch {
|
|
239
|
-
$savedTurnCount = 0
|
|
147
|
+
if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
|
|
148
|
+
$configFile = Join-Path $EkkosConfigDir "config.json"
|
|
149
|
+
if (Test-Path $configFile) {
|
|
150
|
+
try {
|
|
151
|
+
$config = Get-Content $configFile -Raw | ConvertFrom-Json
|
|
152
|
+
$userId = $config.userId
|
|
153
|
+
if ($userId) {
|
|
154
|
+
$projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
|
|
155
|
+
$pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
|
|
156
|
+
$projectPath = $projectPath -replace '\\', '/'
|
|
157
|
+
$bindBody = @{
|
|
158
|
+
userId = $userId
|
|
159
|
+
realSession = $sessionName
|
|
160
|
+
projectPath = $projectPath
|
|
161
|
+
pendingSession = $pendingSession
|
|
162
|
+
} | ConvertTo-Json -Depth 10 -Compress
|
|
163
|
+
|
|
164
|
+
Start-Job -ScriptBlock {
|
|
165
|
+
param($body)
|
|
166
|
+
Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
|
|
167
|
+
-Method POST `
|
|
168
|
+
-Headers @{ "Content-Type" = "application/json" } `
|
|
169
|
+
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
|
|
170
|
+
} -ArgumentList $bindBody | Out-Null
|
|
171
|
+
}
|
|
172
|
+
} catch {}
|
|
240
173
|
}
|
|
241
174
|
}
|
|
242
175
|
|
|
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
|
|
251
|
-
|
|
252
176
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
253
|
-
#
|
|
177
|
+
# SESSION CURRENT: Update Redis with current session name
|
|
254
178
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
255
|
-
$
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
179
|
+
if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
|
|
180
|
+
$configFile2 = Join-Path $EkkosConfigDir "config.json"
|
|
181
|
+
if (Test-Path $configFile2) {
|
|
182
|
+
try {
|
|
183
|
+
$config2 = Get-Content $configFile2 -Raw | ConvertFrom-Json
|
|
184
|
+
$sessionToken = $config2.hookApiKey
|
|
185
|
+
if (-not $sessionToken) { $sessionToken = $config2.apiKey }
|
|
186
|
+
if ($sessionToken) {
|
|
187
|
+
$sessionBody = @{ session_name = $sessionName } | ConvertTo-Json -Depth 10
|
|
188
|
+
Start-Job -ScriptBlock {
|
|
189
|
+
param($body, $token)
|
|
190
|
+
Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/session/current" `
|
|
191
|
+
-Method POST `
|
|
192
|
+
-Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
|
|
193
|
+
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue | Out-Null
|
|
194
|
+
} -ArgumentList $sessionBody, $sessionToken | Out-Null
|
|
195
|
+
}
|
|
196
|
+
} catch {}
|
|
261
197
|
}
|
|
262
198
|
}
|
|
263
199
|
|
|
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 {}
|
|
280
|
-
}
|
|
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"
|
|
296
|
-
|
|
297
200
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
298
|
-
#
|
|
201
|
+
# TURN TRACKING & STATE MANAGEMENT
|
|
299
202
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
203
|
+
$stateDir = Join-Path $env:USERPROFILE ".claude\state"
|
|
204
|
+
if (-not (Test-Path $stateDir)) {
|
|
205
|
+
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
|
|
206
|
+
}
|
|
304
207
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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 {}
|
|
208
|
+
$stateFile = Join-Path $stateDir "hook-state.json"
|
|
209
|
+
$turn = 0
|
|
210
|
+
$contextPercent = ""
|
|
314
211
|
|
|
315
|
-
|
|
316
|
-
$ekkosGlobalState = Join-Path $EkkosConfigDir "current-session.json"
|
|
212
|
+
if (Test-Path $stateFile) {
|
|
317
213
|
try {
|
|
318
|
-
|
|
319
|
-
|
|
214
|
+
$hookState = Get-Content $stateFile -Raw | ConvertFrom-Json
|
|
215
|
+
# Only continue incrementing if this state belongs to the SAME session.
|
|
216
|
+
# If session changed, reset turn counter to 0.
|
|
217
|
+
if ($hookState.session_id -eq $rawSessionId) {
|
|
218
|
+
$turn = [int]$hookState.turn + 1
|
|
219
|
+
} else {
|
|
220
|
+
$turn = 0
|
|
320
221
|
}
|
|
321
|
-
|
|
322
|
-
|
|
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});
|
|
360
|
-
}
|
|
361
|
-
fs.writeFileSync(filePath, JSON.stringify(sessions, null, 2));
|
|
362
|
-
} catch(e) {}
|
|
363
|
-
" $sessionId $sessionName $utcTimestamp $ProjectRoot $activeSessionsFile 2>$null
|
|
364
|
-
} catch {}
|
|
365
|
-
|
|
366
|
-
# 5. Update Redis via API (async)
|
|
367
|
-
if ($authToken) {
|
|
368
|
-
$sessionBody = @{ session_name = $sessionName } | ConvertTo-Json -Depth 10 -Compress
|
|
369
|
-
Start-Job -ScriptBlock {
|
|
370
|
-
param($body, $token, $apiUrl)
|
|
371
|
-
try {
|
|
372
|
-
Invoke-RestMethod -Uri "$apiUrl/api/v1/working/session/current" `
|
|
373
|
-
-Method POST `
|
|
374
|
-
-Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
|
|
375
|
-
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
|
|
376
|
-
-TimeoutSec 3 -ErrorAction SilentlyContinue | Out-Null
|
|
377
|
-
} catch {}
|
|
378
|
-
} -ArgumentList $sessionBody, $authToken, $MemoryApiUrl | Out-Null
|
|
222
|
+
} catch {
|
|
223
|
+
$turn = 0
|
|
379
224
|
}
|
|
225
|
+
}
|
|
380
226
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
|
227
|
+
# Save updated state
|
|
228
|
+
$newState = @{
|
|
229
|
+
turn = $turn
|
|
230
|
+
session_id = $rawSessionId
|
|
231
|
+
last_query = $userQuery.Substring(0, [Math]::Min(100, $userQuery.Length))
|
|
232
|
+
timestamp = (Get-Date).ToString("o")
|
|
233
|
+
} | ConvertTo-Json -Depth 10
|
|
396
234
|
|
|
397
|
-
|
|
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
|
-
}
|
|
408
|
-
}
|
|
235
|
+
Set-Content -Path $stateFile -Value $newState -Force
|
|
409
236
|
|
|
410
237
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
411
238
|
# LOCAL CACHE: Tier 0 capture (async, non-blocking)
|
|
412
239
|
# Per v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
413
240
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
414
241
|
$captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
|
|
415
|
-
if ($captureCmd -and $
|
|
242
|
+
if ($captureCmd -and $rawSessionId -ne "unknown") {
|
|
416
243
|
try {
|
|
244
|
+
# NEW format: ekkos-capture user <instance_id> <session_id> <session_name> <turn_id> <query> [project_path]
|
|
417
245
|
$queryBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userQuery))
|
|
418
|
-
$
|
|
246
|
+
$projectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
|
|
419
247
|
|
|
420
248
|
Start-Job -ScriptBlock {
|
|
421
|
-
param($instanceId, $
|
|
249
|
+
param($instanceId, $sessionId, $sessionName, $turnNum, $queryB64, $projectPath)
|
|
422
250
|
try {
|
|
423
251
|
$decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($queryB64))
|
|
424
|
-
& ekkos-capture user $instanceId $
|
|
252
|
+
& ekkos-capture user $instanceId $sessionId $sessionName $turnNum $decoded $projectPath 2>&1 | Out-Null
|
|
425
253
|
} catch {}
|
|
426
|
-
} -ArgumentList $EkkosInstanceId, $
|
|
254
|
+
} -ArgumentList $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $queryBase64, $projectRoot | Out-Null
|
|
427
255
|
} catch {}
|
|
428
256
|
}
|
|
429
257
|
|
|
430
258
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
431
259
|
# WORKING MEMORY: Fast capture to API (async, non-blocking)
|
|
432
260
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
433
|
-
|
|
261
|
+
$configFile = Join-Path $EkkosConfigDir "config.json"
|
|
262
|
+
if (Test-Path $configFile) {
|
|
434
263
|
try {
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
264
|
+
$config = Get-Content $configFile -Raw | ConvertFrom-Json
|
|
265
|
+
$captureToken = $config.hookApiKey
|
|
266
|
+
if (-not $captureToken) { $captureToken = $config.apiKey }
|
|
267
|
+
|
|
268
|
+
if ($captureToken) {
|
|
269
|
+
# Async capture using Start-Job (non-blocking)
|
|
270
|
+
Start-Job -ScriptBlock {
|
|
271
|
+
param($token, $instanceId, $sessionId, $sessionName, $turnNum, $query)
|
|
272
|
+
$body = @{
|
|
273
|
+
session_id = $sessionId
|
|
274
|
+
session_name = $sessionName
|
|
275
|
+
instance_id = $instanceId
|
|
276
|
+
turn = $turnNum
|
|
277
|
+
query = $query
|
|
278
|
+
} | ConvertTo-Json -Depth 10
|
|
279
|
+
|
|
280
|
+
Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/fast-capture" `
|
|
446
281
|
-Method POST `
|
|
447
|
-
-Headers @{ Authorization = "Bearer $token"
|
|
448
|
-
-
|
|
449
|
-
-
|
|
450
|
-
}
|
|
451
|
-
}
|
|
282
|
+
-Headers @{ Authorization = "Bearer $token" } `
|
|
283
|
+
-ContentType "application/json" `
|
|
284
|
+
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) -ErrorAction SilentlyContinue
|
|
285
|
+
} -ArgumentList $captureToken, $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $userQuery | Out-Null
|
|
286
|
+
}
|
|
452
287
|
} catch {}
|
|
453
288
|
}
|
|
454
289
|
|
|
290
|
+
$timestamp = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt") + " EST"
|
|
291
|
+
|
|
455
292
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
456
293
|
# DASHBOARD HINT FILE: write session info for ekkos dashboard --wait-for-new
|
|
457
294
|
# On Windows, active-sessions.json is never populated (hook PIDs are dead).
|
|
@@ -459,12 +296,12 @@ if ($authToken) {
|
|
|
459
296
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
460
297
|
if ($sessionName -ne "unknown-session") {
|
|
461
298
|
try {
|
|
462
|
-
$
|
|
299
|
+
$projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
|
|
463
300
|
$hintFile = Join-Path $EkkosConfigDir "hook-session-hint.json"
|
|
464
301
|
$hint = @{
|
|
465
302
|
sessionName = $sessionName
|
|
466
|
-
sessionId = $
|
|
467
|
-
projectPath = $
|
|
303
|
+
sessionId = $rawSessionId
|
|
304
|
+
projectPath = $projectPath
|
|
468
305
|
ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
469
306
|
} | ConvertTo-Json -Depth 10 -Compress
|
|
470
307
|
Set-Content -Path $hintFile -Value $hint -Force
|
|
@@ -472,251 +309,24 @@ if ($sessionName -ne "unknown-session") {
|
|
|
472
309
|
}
|
|
473
310
|
|
|
474
311
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
475
|
-
#
|
|
312
|
+
# OUTPUT SYSTEM REMINDER
|
|
476
313
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
477
|
-
$
|
|
478
|
-
|
|
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 = @() }
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
$restoreJson = $restoreResponse | ConvertTo-Json -Depth 20 -Compress
|
|
499
|
-
$restoredCount = 0
|
|
500
|
-
$lastTask = "unknown task"
|
|
501
|
-
$lastResponse = ""
|
|
502
|
-
|
|
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 }
|
|
510
|
-
|
|
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" }
|
|
518
|
-
|
|
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 = "" }
|
|
526
|
-
|
|
527
|
-
Write-Output ""
|
|
528
|
-
Write-Output "${GREEN}${BOLD}Session continued${RESET} ${DIM}(${restoredCount} turns restored)${RESET}"
|
|
529
|
-
Write-Output ""
|
|
530
|
-
|
|
531
|
-
$continueOutput = @"
|
|
532
|
-
<system-reminder>
|
|
533
|
-
$("=" * 75)
|
|
534
|
-
CONTEXT RESTORED - Resume seamlessly. DO NOT ask 'what were we doing?'
|
|
535
|
-
$("=" * 75)
|
|
536
|
-
|
|
537
|
-
## Last User Request:
|
|
538
|
-
$lastTask
|
|
539
|
-
|
|
540
|
-
## Your Last Response (truncated):
|
|
541
|
-
$lastResponse
|
|
542
|
-
|
|
543
|
-
"@
|
|
314
|
+
$esc = [char]27
|
|
315
|
+
$header = "${esc}[0;36m${esc}[1m🧠 ekkOS Memory${esc}[0m ${esc}[2m| $sessionName | $timestamp${esc}[0m"
|
|
544
316
|
|
|
545
|
-
|
|
546
|
-
|
|
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 + '...');
|
|
554
|
-
});
|
|
555
|
-
" $restoreJson 2>$null
|
|
556
|
-
$continueOutput += "$recentContext`n"
|
|
557
|
-
} catch {}
|
|
558
|
-
$continueOutput += "`n"
|
|
559
|
-
}
|
|
317
|
+
$output = @"
|
|
318
|
+
$header
|
|
560
319
|
|
|
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
320
|
"@
|
|
568
321
|
|
|
569
|
-
Write-Output $continueOutput
|
|
570
|
-
Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| ${timestamp}${RESET}"
|
|
571
|
-
exit 0
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
|
|
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}"
|
|
582
|
-
|
|
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
|
-
}
|
|
599
|
-
|
|
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}"
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
Write-Output ""
|
|
633
|
-
Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| ${sessionName} | ${timestamp}${RESET}"
|
|
634
|
-
|
|
635
|
-
} elseif ($postClearDetected -and $authToken) {
|
|
636
|
-
# /clear detected - show visible restoration banner
|
|
637
|
-
$separator = "${GREEN}" + ("=" * 78) + "${RESET}"
|
|
638
|
-
|
|
639
|
-
Write-Output $separator
|
|
640
|
-
Write-Output "${GREEN}${BOLD}SESSION CONTINUED${RESET} ${DIM}| ${turnNumber} turns preserved | Restoring context...${RESET}"
|
|
641
|
-
Write-Output $separator
|
|
642
|
-
|
|
643
|
-
# Also write to stderr for visibility
|
|
644
|
-
[Console]::Error.WriteLine("${GREEN}${BOLD}SESSION CONTINUED${RESET} ${DIM}| ${turnNumber} turns preserved | Context restored${RESET}")
|
|
645
|
-
|
|
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
|
-
}
|
|
662
|
-
|
|
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 }
|
|
672
|
-
|
|
673
|
-
if ($clearRestoredCount -gt 0) {
|
|
674
|
-
Write-Output "${GREEN} Restored ${clearRestoredCount} recent turns${RESET}"
|
|
675
|
-
Write-Output ""
|
|
676
|
-
|
|
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
|
-
}
|
|
691
|
-
|
|
692
|
-
Write-Output ""
|
|
693
|
-
Write-Output "${DIM}Full history: `"recall`" or `"turns 1-${turnNumber}`"${RESET}"
|
|
694
|
-
Write-Output $separator
|
|
695
|
-
Write-Output ""
|
|
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}"
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
708
|
-
# Output skill reminders if detected
|
|
709
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
710
322
|
if ($skillReminders.Count -gt 0) {
|
|
711
|
-
|
|
712
|
-
Write-Output "${MAGENTA}${BOLD}$($skillReminders -join "`n")${RESET}"
|
|
323
|
+
$output += "${esc}[0;35m${esc}[1m" + ($skillReminders -join "`n") + "${esc}[0m`n"
|
|
713
324
|
}
|
|
714
325
|
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
Write-Output "<footer-note>Do not include a turn counter in the footer.</footer-note>"
|
|
326
|
+
$output += @"
|
|
327
|
+
|
|
328
|
+
<footer-format>End responses with: Claude Code ({Model}) · 🧠 ekkOS_™ · $sessionName · $timestamp</footer-format>
|
|
329
|
+
<footer-note>Do not include a turn counter in the footer.</footer-note>
|
|
330
|
+
"@
|
|
721
331
|
|
|
722
|
-
|
|
332
|
+
Write-Output $output
|