@ekkos/cli 1.0.34 → 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/dist/capture/jsonl-rewriter.js +72 -7
- package/dist/commands/dashboard.js +186 -557
- package/dist/commands/init.js +3 -15
- package/dist/commands/run.js +222 -256
- package/dist/commands/setup.js +0 -47
- package/dist/commands/swarm-dashboard.js +4 -13
- package/dist/deploy/instructions.d.ts +2 -5
- package/dist/deploy/instructions.js +8 -11
- package/dist/deploy/settings.js +21 -15
- package/dist/deploy/skills.d.ts +0 -8
- package/dist/deploy/skills.js +0 -26
- package/dist/index.js +2 -2
- package/dist/lib/usage-parser.js +1 -2
- package/dist/utils/platform.d.ts +0 -3
- package/dist/utils/platform.js +1 -4
- package/dist/utils/session-binding.d.ts +1 -1
- package/dist/utils/session-binding.js +2 -3
- package/package.json +1 -1
- package/templates/agents/README.md +182 -0
- package/templates/agents/code-reviewer.md +166 -0
- package/templates/agents/debug-detective.md +169 -0
- package/templates/agents/ekkOS_Vercel.md +99 -0
- package/templates/agents/extension-manager.md +229 -0
- package/templates/agents/git-companion.md +185 -0
- package/templates/agents/github-test-agent.md +321 -0
- package/templates/agents/railway-manager.md +179 -0
- package/templates/hooks/assistant-response.ps1 +26 -94
- package/templates/hooks/lib/count-tokens.cjs +0 -0
- package/templates/hooks/lib/ekkos-reminders.sh +0 -0
- package/templates/hooks/session-start.ps1 +224 -61
- package/templates/hooks/session-start.sh +1 -1
- package/templates/hooks/stop.ps1 +249 -103
- package/templates/hooks/stop.sh +1 -1
- package/templates/hooks/user-prompt-submit.ps1 +519 -129
- 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/before-submit-prompt.sh +238 -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/windsurf-skills/ekkos-memory/SKILL.md +219 -0
- package/README.md +0 -57
|
@@ -3,7 +3,12 @@
|
|
|
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=
|
|
6
|
+
# EKKOS_TEMPLATE_VERSION=2.0.0
|
|
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
|
|
7
12
|
#
|
|
8
13
|
# Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
|
|
9
14
|
# - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
@@ -12,18 +17,27 @@
|
|
|
12
17
|
|
|
13
18
|
$ErrorActionPreference = "SilentlyContinue"
|
|
14
19
|
|
|
20
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
21
|
+
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
|
22
|
+
|
|
15
23
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
24
|
# CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
|
|
17
25
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
18
26
|
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
|
|
19
27
|
$SessionWordsJson = "$EkkosConfigDir\session-words.json"
|
|
20
28
|
$SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
|
|
29
|
+
$JsonParseHelper = "$EkkosConfigDir\.helpers\json-parse.cjs"
|
|
21
30
|
|
|
22
31
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
23
32
|
# INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
|
|
24
33
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
25
34
|
$EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
|
|
26
35
|
|
|
36
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
+
# API URL
|
|
38
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
+
$MemoryApiUrl = "https://api.ekkos.dev"
|
|
40
|
+
|
|
27
41
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
28
42
|
# Load session words from JSON file - NO HARDCODED ARRAYS
|
|
29
43
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -48,7 +62,9 @@ function Load-SessionWords {
|
|
|
48
62
|
}
|
|
49
63
|
}
|
|
50
64
|
|
|
65
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
51
66
|
# Read input from stdin
|
|
67
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
52
68
|
$inputJson = [Console]::In.ReadToEnd()
|
|
53
69
|
if (-not $inputJson) { exit 0 }
|
|
54
70
|
|
|
@@ -65,15 +81,24 @@ if (-not $userQuery) { exit 0 }
|
|
|
65
81
|
|
|
66
82
|
$rawSessionId = $input.session_id
|
|
67
83
|
if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
|
|
84
|
+
$transcriptPath = $input.transcript_path
|
|
68
85
|
|
|
69
86
|
# Fallback: read session_id from saved state
|
|
70
|
-
if ($rawSessionId -eq "unknown") {
|
|
87
|
+
if ($rawSessionId -eq "unknown" -or $rawSessionId -eq "null" -or [string]::IsNullOrEmpty($rawSessionId)) {
|
|
71
88
|
$stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
|
|
72
|
-
if (Test-Path $stateFile) {
|
|
89
|
+
if ((Test-Path $stateFile) -and (Test-Path $JsonParseHelper)) {
|
|
73
90
|
try {
|
|
74
|
-
$
|
|
75
|
-
$rawSessionId =
|
|
76
|
-
} catch {
|
|
91
|
+
$rawSessionId = node $JsonParseHelper $stateFile '.session_id' 2>$null
|
|
92
|
+
if (-not $rawSessionId) { $rawSessionId = "unknown" }
|
|
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)
|
|
77
102
|
}
|
|
78
103
|
}
|
|
79
104
|
|
|
@@ -108,6 +133,20 @@ if ($queryLower -match '(sql|query|supabase|prisma|database|table|column|select
|
|
|
108
133
|
$skillReminders += "SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names"
|
|
109
134
|
}
|
|
110
135
|
|
|
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
|
+
|
|
111
150
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
112
151
|
# SESSION NAME - Resolve early so it's available for all downstream use
|
|
113
152
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -137,158 +176,282 @@ function Convert-UuidToWords {
|
|
|
137
176
|
}
|
|
138
177
|
}
|
|
139
178
|
|
|
140
|
-
$
|
|
179
|
+
$sessionId = $rawSessionId
|
|
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
|
+
}
|
|
141
193
|
|
|
142
194
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
143
|
-
#
|
|
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.
|
|
195
|
+
# STATE DIRECTORIES
|
|
146
196
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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 {}
|
|
173
|
-
}
|
|
197
|
+
$stateDir = Join-Path $ProjectRoot ".claude\state"
|
|
198
|
+
if (-not (Test-Path $stateDir)) {
|
|
199
|
+
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
|
|
174
200
|
}
|
|
201
|
+
$sessionFile = Join-Path $stateDir "current-session.json"
|
|
202
|
+
|
|
203
|
+
$projectSessionDir = Join-Path $stateDir "sessions"
|
|
204
|
+
if (-not (Test-Path $projectSessionDir)) {
|
|
205
|
+
New-Item -ItemType Directory -Path $projectSessionDir -Force | Out-Null
|
|
206
|
+
}
|
|
207
|
+
$turnCounterFile = Join-Path $projectSessionDir "$sessionId.turn"
|
|
208
|
+
$contextSizeFile = Join-Path $projectSessionDir "$sessionId.context"
|
|
175
209
|
|
|
176
210
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
177
|
-
#
|
|
211
|
+
# Turn counter - TRANSCRIPT-BASED (source of truth)
|
|
212
|
+
# Count "type":"user" entries in transcript JSONL
|
|
178
213
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
}
|
|
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
|
|
197
240
|
}
|
|
198
241
|
}
|
|
199
242
|
|
|
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
|
+
|
|
200
252
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
201
|
-
#
|
|
253
|
+
# Context size tracking - Uses tokenizer script (single source)
|
|
202
254
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
203
|
-
$
|
|
204
|
-
if (
|
|
205
|
-
|
|
255
|
+
$prevContextPercent = 0
|
|
256
|
+
if (Test-Path $contextSizeFile) {
|
|
257
|
+
try {
|
|
258
|
+
$prevContextPercent = [int](Get-Content $contextSizeFile -Raw).Trim()
|
|
259
|
+
} catch {
|
|
260
|
+
$prevContextPercent = 0
|
|
261
|
+
}
|
|
206
262
|
}
|
|
207
263
|
|
|
208
|
-
$
|
|
209
|
-
$
|
|
210
|
-
$
|
|
264
|
+
$tokenPercent = 0
|
|
265
|
+
$ipcPercent = 0
|
|
266
|
+
$maxTokens = 200000
|
|
267
|
+
$tokenizerScript = Join-Path $ScriptDir "lib\count-tokens.cjs"
|
|
211
268
|
|
|
212
|
-
if (Test-Path $
|
|
269
|
+
if ($transcriptPath -and (Test-Path $transcriptPath) -and (Test-Path $tokenizerScript)) {
|
|
213
270
|
try {
|
|
214
|
-
$
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
$
|
|
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 }
|
|
221
278
|
}
|
|
222
|
-
} catch {
|
|
223
|
-
$turn = 0
|
|
224
|
-
}
|
|
279
|
+
} catch {}
|
|
225
280
|
}
|
|
226
281
|
|
|
227
|
-
|
|
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
|
|
282
|
+
Set-Content -Path $contextSizeFile -Value $tokenPercent -Force
|
|
234
283
|
|
|
235
|
-
|
|
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
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
298
|
+
# SINGLE SOURCE OF TRUTH: Update ALL session tracking systems
|
|
299
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
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
|
|
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});
|
|
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
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
# 6. CRITICAL: Bind session name to proxy for R2 eviction paths
|
|
382
|
+
$ekkosUserId = ""
|
|
383
|
+
if ((Test-Path $EkkosConfig) -and (Test-Path $JsonParseHelper)) {
|
|
384
|
+
try {
|
|
385
|
+
$ekkosUserId = node $JsonParseHelper $EkkosConfig '.userId' 2>$null
|
|
386
|
+
} catch {}
|
|
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
|
+
}
|
|
408
|
+
}
|
|
236
409
|
|
|
237
410
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
238
411
|
# LOCAL CACHE: Tier 0 capture (async, non-blocking)
|
|
239
412
|
# Per v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
240
413
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
241
414
|
$captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
|
|
242
|
-
if ($captureCmd -and $
|
|
415
|
+
if ($captureCmd -and $sessionId -ne "unknown") {
|
|
243
416
|
try {
|
|
244
|
-
# NEW format: ekkos-capture user <instance_id> <session_id> <session_name> <turn_id> <query> [project_path]
|
|
245
417
|
$queryBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userQuery))
|
|
246
|
-
$
|
|
418
|
+
$captureProjectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
|
|
247
419
|
|
|
248
420
|
Start-Job -ScriptBlock {
|
|
249
|
-
param($instanceId, $
|
|
421
|
+
param($instanceId, $sid, $sname, $turnNum, $queryB64, $projPath)
|
|
250
422
|
try {
|
|
251
423
|
$decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($queryB64))
|
|
252
|
-
& ekkos-capture user $instanceId $
|
|
424
|
+
& ekkos-capture user $instanceId $sid $sname $turnNum $decoded $projPath 2>&1 | Out-Null
|
|
253
425
|
} catch {}
|
|
254
|
-
} -ArgumentList $EkkosInstanceId, $
|
|
426
|
+
} -ArgumentList $EkkosInstanceId, $sessionId, $sessionName, $turnNumber, $queryBase64, $captureProjectRoot | Out-Null
|
|
255
427
|
} catch {}
|
|
256
428
|
}
|
|
257
429
|
|
|
258
430
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
259
431
|
# WORKING MEMORY: Fast capture to API (async, non-blocking)
|
|
260
432
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
261
|
-
|
|
262
|
-
if (Test-Path $configFile) {
|
|
433
|
+
if ($authToken) {
|
|
263
434
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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" `
|
|
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" `
|
|
281
446
|
-Method POST `
|
|
282
|
-
-Headers @{ Authorization = "Bearer $token" } `
|
|
283
|
-
-
|
|
284
|
-
-
|
|
285
|
-
}
|
|
286
|
-
}
|
|
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
|
|
287
452
|
} catch {}
|
|
288
453
|
}
|
|
289
454
|
|
|
290
|
-
$timestamp = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt") + " EST"
|
|
291
|
-
|
|
292
455
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
293
456
|
# DASHBOARD HINT FILE: write session info for ekkos dashboard --wait-for-new
|
|
294
457
|
# On Windows, active-sessions.json is never populated (hook PIDs are dead).
|
|
@@ -296,12 +459,12 @@ $timestamp = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt") + " EST"
|
|
|
296
459
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
297
460
|
if ($sessionName -ne "unknown-session") {
|
|
298
461
|
try {
|
|
299
|
-
$
|
|
462
|
+
$hintProjectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
|
|
300
463
|
$hintFile = Join-Path $EkkosConfigDir "hook-session-hint.json"
|
|
301
464
|
$hint = @{
|
|
302
465
|
sessionName = $sessionName
|
|
303
|
-
sessionId = $
|
|
304
|
-
projectPath = $
|
|
466
|
+
sessionId = $sessionId
|
|
467
|
+
projectPath = $hintProjectPath
|
|
305
468
|
ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
306
469
|
} | ConvertTo-Json -Depth 10 -Compress
|
|
307
470
|
Set-Content -Path $hintFile -Value $hint -Force
|
|
@@ -309,24 +472,251 @@ if ($sessionName -ne "unknown-session") {
|
|
|
309
472
|
}
|
|
310
473
|
|
|
311
474
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
312
|
-
#
|
|
475
|
+
# "/continue" COMMAND: Run AFTER /clear to restore last 5 turns
|
|
313
476
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
314
|
-
$
|
|
315
|
-
|
|
477
|
+
$queryLowerTrimmed = $queryLower.Trim()
|
|
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
|
|
316
539
|
|
|
317
|
-
|
|
318
|
-
$
|
|
540
|
+
## Your Last Response (truncated):
|
|
541
|
+
$lastResponse
|
|
319
542
|
|
|
320
543
|
"@
|
|
321
544
|
|
|
322
|
-
if ($
|
|
323
|
-
|
|
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 + '...');
|
|
554
|
+
});
|
|
555
|
+
" $restoreJson 2>$null
|
|
556
|
+
$continueOutput += "$recentContext`n"
|
|
557
|
+
} catch {}
|
|
558
|
+
$continueOutput += "`n"
|
|
559
|
+
}
|
|
560
|
+
|
|
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
|
|
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}"
|
|
324
705
|
}
|
|
325
706
|
|
|
326
|
-
|
|
707
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
708
|
+
# Output skill reminders if detected
|
|
709
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
710
|
+
if ($skillReminders.Count -gt 0) {
|
|
711
|
+
Write-Output ""
|
|
712
|
+
Write-Output "${MAGENTA}${BOLD}$($skillReminders -join "`n")${RESET}"
|
|
713
|
+
}
|
|
327
714
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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>"
|
|
331
721
|
|
|
332
|
-
|
|
722
|
+
exit 0
|