@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
package/templates/hooks/stop.ps1
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
# ekkOS_ Hook: Stop -
|
|
2
|
+
# ekkOS_ Hook: Stop - Captures turns to BOTH Working (Redis) and Episodic (Supabase)
|
|
3
|
+
# NO jq dependency - uses Node.js for all JSON parsing
|
|
3
4
|
# MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
|
|
4
5
|
# EKKOS_MANAGED=1
|
|
5
6
|
# EKKOS_MANIFEST_SHA256=<computed-at-build>
|
|
@@ -9,15 +10,32 @@
|
|
|
9
10
|
# - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
10
11
|
# - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
|
|
11
12
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
# This hook captures every turn to:
|
|
14
|
+
# 1. Working Sessions (Redis) - Fast hot cache for /continue
|
|
15
|
+
# 2. Episodic Memory (Supabase) - Permanent cold storage
|
|
16
|
+
#
|
|
17
|
+
# NO compliance checking - skills handle that
|
|
18
|
+
# NO PatternGuard validation - skills handle that
|
|
19
|
+
# NO verbose output - just capture silently
|
|
20
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
12
21
|
|
|
13
22
|
$ErrorActionPreference = "SilentlyContinue"
|
|
14
23
|
|
|
15
24
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
25
|
# CONFIG PATHS
|
|
17
26
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
28
|
+
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
|
29
|
+
$StateDir = Join-Path $ProjectRoot ".claude\state"
|
|
30
|
+
|
|
31
|
+
if (-not (Test-Path $StateDir)) {
|
|
32
|
+
New-Item -ItemType Directory -Path $StateDir -Force | Out-Null
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
|
|
19
36
|
$SessionWordsJson = "$EkkosConfigDir\session-words.json"
|
|
20
37
|
$SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
|
|
38
|
+
$JsonParseHelper = "$EkkosConfigDir\.helpers\json-parse.cjs"
|
|
21
39
|
|
|
22
40
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
23
41
|
# INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
|
|
@@ -87,65 +105,131 @@ function Convert-UuidToWords {
|
|
|
87
105
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
88
106
|
$inputJson = [Console]::In.ReadToEnd()
|
|
89
107
|
|
|
90
|
-
#
|
|
91
|
-
|
|
92
|
-
|
|
108
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
109
|
+
# Parse input JSON for session_id, transcript_path, model
|
|
110
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
93
111
|
$rawSessionId = "unknown"
|
|
94
|
-
$
|
|
112
|
+
$transcriptPath = ""
|
|
113
|
+
$modelUsed = "claude-sonnet-4-5"
|
|
95
114
|
|
|
96
|
-
if (
|
|
115
|
+
if ($inputJson) {
|
|
97
116
|
try {
|
|
98
|
-
$
|
|
99
|
-
$rawSessionId = $
|
|
100
|
-
$
|
|
117
|
+
$stopInput = $inputJson | ConvertFrom-Json
|
|
118
|
+
if ($stopInput.session_id) { $rawSessionId = $stopInput.session_id }
|
|
119
|
+
if ($stopInput.transcript_path) { $transcriptPath = $stopInput.transcript_path }
|
|
120
|
+
if ($stopInput.model) { $modelUsed = $stopInput.model }
|
|
101
121
|
} catch {}
|
|
102
122
|
}
|
|
103
123
|
|
|
104
|
-
|
|
124
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
125
|
+
# Session ID validation
|
|
126
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
127
|
+
if (-not $rawSessionId -or $rawSessionId -eq "unknown" -or $rawSessionId -eq "null") {
|
|
128
|
+
exit 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
$isUuid = $rawSessionId -match '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
132
|
+
|
|
133
|
+
if ($isUuid) {
|
|
134
|
+
$sessionName = Convert-UuidToWords $rawSessionId
|
|
135
|
+
} else {
|
|
136
|
+
$sessionName = $rawSessionId
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
140
|
+
# Turn Number - read from turn file written by user-prompt-submit.ps1
|
|
141
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
142
|
+
$sessionsDir = Join-Path $StateDir "sessions"
|
|
143
|
+
if (-not (Test-Path $sessionsDir)) {
|
|
144
|
+
New-Item -ItemType Directory -Path $sessionsDir -Force | Out-Null
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
$turnFile = Join-Path $sessionsDir "$rawSessionId.turn"
|
|
148
|
+
$turn = 1
|
|
149
|
+
if (Test-Path $turnFile) {
|
|
150
|
+
try {
|
|
151
|
+
$turnContent = Get-Content $turnFile -Raw
|
|
152
|
+
$turn = [int]$turnContent.Trim()
|
|
153
|
+
} catch {
|
|
154
|
+
$turn = 1
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
159
|
+
# Load auth - No jq
|
|
160
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
161
|
+
$configFile = Join-Path $EkkosConfigDir "config.json"
|
|
162
|
+
$authToken = ""
|
|
163
|
+
$userId = ""
|
|
164
|
+
|
|
165
|
+
if ((Test-Path $configFile) -and (Test-Path $JsonParseHelper)) {
|
|
166
|
+
try {
|
|
167
|
+
$authToken = & node $JsonParseHelper $configFile '.hookApiKey' 2>$null
|
|
168
|
+
if (-not $authToken) {
|
|
169
|
+
$authToken = & node $JsonParseHelper $configFile '.apiKey' 2>$null
|
|
170
|
+
}
|
|
171
|
+
$userId = & node $JsonParseHelper $configFile '.userId' 2>$null
|
|
172
|
+
} catch {}
|
|
173
|
+
} elseif (Test-Path $configFile) {
|
|
105
174
|
try {
|
|
106
|
-
$
|
|
107
|
-
$
|
|
175
|
+
$config = Get-Content $configFile -Raw | ConvertFrom-Json
|
|
176
|
+
$authToken = if ($config.hookApiKey) { $config.hookApiKey } else { $config.apiKey }
|
|
177
|
+
$userId = $config.userId
|
|
108
178
|
} catch {}
|
|
109
179
|
}
|
|
110
180
|
|
|
111
|
-
|
|
181
|
+
if (-not $authToken) {
|
|
182
|
+
$envLocal = Join-Path $ProjectRoot ".env.local"
|
|
183
|
+
if (Test-Path $envLocal) {
|
|
184
|
+
try {
|
|
185
|
+
$envContent = Get-Content $envLocal -Raw
|
|
186
|
+
if ($envContent -match 'SUPABASE_SECRET_KEY=["'']?([^"''\r\n]+)') {
|
|
187
|
+
$authToken = $Matches[1]
|
|
188
|
+
}
|
|
189
|
+
} catch {}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (-not $authToken) { exit 0 }
|
|
194
|
+
|
|
195
|
+
$memoryApiUrl = "https://api.ekkos.dev"
|
|
112
196
|
|
|
113
197
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
114
198
|
# SESSION BINDING: Bridge _pending → real session name for proxy eviction
|
|
115
199
|
# Windows has no PTY so run.ts can't detect the session name. The stop hook
|
|
116
200
|
# is the first place we have a confirmed session name, so we bind here.
|
|
117
|
-
# Mac does this in stop.sh (lines 171-179). Logic is identical.
|
|
118
201
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
119
|
-
$
|
|
120
|
-
if ((Test-Path $configFile) -and $sessionName -ne "unknown-session") {
|
|
202
|
+
if ($sessionName -and $sessionName -ne "unknown-session" -and $userId) {
|
|
121
203
|
try {
|
|
122
|
-
$
|
|
123
|
-
$
|
|
124
|
-
$authToken = if ($config.hookApiKey) { $config.hookApiKey } else { $config.apiKey }
|
|
204
|
+
$projectPath = ((Get-Location).Path) -replace '\\', '/'
|
|
205
|
+
$pendingSession = if ($env:EKKOS_PENDING_SESSION) { $env:EKKOS_PENDING_SESSION } else { "_pending" }
|
|
125
206
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Start-Job -ScriptBlock {
|
|
139
|
-
param($body, $token)
|
|
140
|
-
Invoke-RestMethod -Uri "https://mcp.ekkos.dev/proxy/session/bind" `
|
|
207
|
+
$bindBody = @{
|
|
208
|
+
userId = $userId
|
|
209
|
+
realSession = $sessionName
|
|
210
|
+
projectPath = $projectPath
|
|
211
|
+
pendingSession = $pendingSession
|
|
212
|
+
} | ConvertTo-Json -Depth 10 -Compress
|
|
213
|
+
|
|
214
|
+
Start-Job -ScriptBlock {
|
|
215
|
+
param($body, $apiUrl)
|
|
216
|
+
try {
|
|
217
|
+
Invoke-RestMethod -Uri "$apiUrl/proxy/session/bind" `
|
|
141
218
|
-Method POST `
|
|
142
219
|
-Headers @{ "Content-Type" = "application/json" } `
|
|
143
|
-
-Body ([System.Text.Encoding]::UTF8.GetBytes($body))
|
|
144
|
-
|
|
145
|
-
|
|
220
|
+
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
|
|
221
|
+
-TimeoutSec 2 `
|
|
222
|
+
-ErrorAction SilentlyContinue | Out-Null
|
|
223
|
+
} catch {}
|
|
224
|
+
} -ArgumentList $bindBody, $memoryApiUrl | Out-Null
|
|
146
225
|
} catch {}
|
|
147
226
|
}
|
|
148
227
|
|
|
228
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
229
|
+
# EVICTION: Handled by IPC (In-Place Progressive Compression) in the proxy.
|
|
230
|
+
# No hook-side eviction needed - passthrough is default for cache stability.
|
|
231
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
232
|
+
|
|
149
233
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
150
234
|
# LOCAL CACHE: ACK turn to mark as synced
|
|
151
235
|
# Per v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
@@ -169,15 +253,13 @@ $isInterrupted = $false
|
|
|
169
253
|
$stopReason = ""
|
|
170
254
|
if ($inputJson) {
|
|
171
255
|
try {
|
|
172
|
-
$
|
|
173
|
-
$isInterrupted = $
|
|
174
|
-
$stopReason = $
|
|
256
|
+
$stopParsed = $inputJson | ConvertFrom-Json
|
|
257
|
+
$isInterrupted = $stopParsed.interrupted -eq $true
|
|
258
|
+
$stopReason = $stopParsed.stop_reason
|
|
175
259
|
} catch {}
|
|
176
260
|
}
|
|
177
261
|
|
|
178
262
|
if ($isInterrupted -or $stopReason -eq "user_cancelled" -or $stopReason -eq "interrupted") {
|
|
179
|
-
if (Test-Path $stateFile) { Remove-Item $stateFile -Force }
|
|
180
|
-
Write-Output "ekkOS session ended (interrupted)"
|
|
181
263
|
exit 0
|
|
182
264
|
}
|
|
183
265
|
|
|
@@ -185,14 +267,6 @@ if ($isInterrupted -or $stopReason -eq "user_cancelled" -or $stopReason -eq "int
|
|
|
185
267
|
# EXTRACT CONVERSATION FROM TRANSCRIPT
|
|
186
268
|
# Mirrors stop.sh: Extract last user query, assistant response, file changes
|
|
187
269
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
188
|
-
$transcriptPath = ""
|
|
189
|
-
if ($inputJson) {
|
|
190
|
-
try {
|
|
191
|
-
$stopInput2 = $inputJson | ConvertFrom-Json
|
|
192
|
-
$transcriptPath = $stopInput2.transcript_path
|
|
193
|
-
} catch {}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
270
|
$lastUser = ""
|
|
197
271
|
$lastAssistant = ""
|
|
198
272
|
$fileChangesJson = "[]"
|
|
@@ -228,6 +302,7 @@ for (let i = entries.length - 1; i >= 0; i--) {
|
|
|
228
302
|
const parts = content.map(c => {
|
|
229
303
|
if (c.type === 'text') return c.text;
|
|
230
304
|
if (c.type === 'tool_use') return '[TOOL: ' + c.name + ']';
|
|
305
|
+
if (c.type === 'thinking') return '[THINKING]' + (c.thinking || c.text || '') + '[/THINKING]';
|
|
231
306
|
return '';
|
|
232
307
|
}).filter(Boolean);
|
|
233
308
|
lastAssistant = parts.join('\n'); break;
|
|
@@ -257,7 +332,49 @@ console.log(JSON.stringify({
|
|
|
257
332
|
$lastUser = $parsed.user
|
|
258
333
|
$lastAssistant = $parsed.assistant
|
|
259
334
|
$fileChangesJson = ($parsed.fileChanges | ConvertTo-Json -Depth 10 -Compress)
|
|
260
|
-
if (-not $fileChangesJson) { $fileChangesJson = "[]" }
|
|
335
|
+
if (-not $fileChangesJson -or $fileChangesJson -eq "null") { $fileChangesJson = "[]" }
|
|
336
|
+
}
|
|
337
|
+
} catch {}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (-not $lastUser -or $lastUser -match '\[Request interrupted') {
|
|
341
|
+
exit 0
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
345
|
+
# Extract tools_used and files_referenced from file changes
|
|
346
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
347
|
+
$toolsUsed = @()
|
|
348
|
+
$filesReferenced = @()
|
|
349
|
+
|
|
350
|
+
if ($parsed -and $parsed.fileChanges) {
|
|
351
|
+
try {
|
|
352
|
+
$toolsUsed = @($parsed.fileChanges | ForEach-Object { $_.tool } | Where-Object { $_ } | Select-Object -Unique)
|
|
353
|
+
$filesReferenced = @($parsed.fileChanges | ForEach-Object { $_.path } | Where-Object { $_ } | Select-Object -Unique)
|
|
354
|
+
} catch {
|
|
355
|
+
$toolsUsed = @()
|
|
356
|
+
$filesReferenced = @()
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
361
|
+
# Token breakdown from tokenizer script
|
|
362
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
363
|
+
$totalTokens = 0
|
|
364
|
+
$inputTokens = 0
|
|
365
|
+
$cacheReadTokens = 0
|
|
366
|
+
$cacheCreationTokens = 0
|
|
367
|
+
|
|
368
|
+
$tokenizerScript = Join-Path $ScriptDir "lib\count-tokens.cjs"
|
|
369
|
+
if ($transcriptPath -and (Test-Path $transcriptPath) -and (Test-Path $tokenizerScript)) {
|
|
370
|
+
try {
|
|
371
|
+
$tokenJson = & node $tokenizerScript $transcriptPath --json 2>$null
|
|
372
|
+
if ($tokenJson) {
|
|
373
|
+
$tokenData = $tokenJson | ConvertFrom-Json
|
|
374
|
+
if ($tokenData.total_tokens) { $totalTokens = [int]$tokenData.total_tokens }
|
|
375
|
+
if ($tokenData.input_tokens) { $inputTokens = [int]$tokenData.input_tokens }
|
|
376
|
+
if ($tokenData.cache_read_tokens) { $cacheReadTokens = [int]$tokenData.cache_read_tokens }
|
|
377
|
+
if ($tokenData.cache_creation_tokens) { $cacheCreationTokens = [int]$tokenData.cache_creation_tokens }
|
|
261
378
|
}
|
|
262
379
|
} catch {}
|
|
263
380
|
}
|
|
@@ -267,69 +384,98 @@ console.log(JSON.stringify({
|
|
|
267
384
|
# Mirrors stop.sh dual-write at lines 271-356
|
|
268
385
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
269
386
|
if ($lastUser -and $lastAssistant -and $authToken) {
|
|
270
|
-
$modelUsed = "claude-sonnet-4-5"
|
|
271
|
-
if ($inputJson) {
|
|
272
|
-
try {
|
|
273
|
-
$stopInput3 = $inputJson | ConvertFrom-Json
|
|
274
|
-
if ($stopInput3.model) { $modelUsed = $stopInput3.model }
|
|
275
|
-
} catch {}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
387
|
$timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
279
388
|
$projectPath = ((Get-Location).Path) -replace '\\', '/'
|
|
280
389
|
|
|
390
|
+
# Prepare tools_used and files_referenced as JSON strings for passing into jobs
|
|
391
|
+
$toolsUsedJson = ($toolsUsed | ConvertTo-Json -Depth 5 -Compress)
|
|
392
|
+
if (-not $toolsUsedJson -or $toolsUsedJson -eq "null") { $toolsUsedJson = "[]" }
|
|
393
|
+
# Single-element arrays lose their array wrapper in PowerShell ConvertTo-Json
|
|
394
|
+
if ($toolsUsed.Count -eq 1) { $toolsUsedJson = "[$toolsUsedJson]" }
|
|
395
|
+
|
|
396
|
+
$filesRefJson = ($filesReferenced | ConvertTo-Json -Depth 5 -Compress)
|
|
397
|
+
if (-not $filesRefJson -or $filesRefJson -eq "null") { $filesRefJson = "[]" }
|
|
398
|
+
if ($filesReferenced.Count -eq 1) { $filesRefJson = "[$filesRefJson]" }
|
|
399
|
+
|
|
281
400
|
# 1. WORKING SESSIONS (Redis)
|
|
282
401
|
Start-Job -ScriptBlock {
|
|
283
|
-
param($token, $sessionName, $turnNum, $userQuery, $agentResponse, $model)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
402
|
+
param($token, $apiUrl, $sessionName, $turnNum, $userQuery, $agentResponse, $model, $toolsJson, $filesJson, $totalTok, $inputTok, $cacheReadTok, $cacheCreateTok)
|
|
403
|
+
try {
|
|
404
|
+
$truncated = $agentResponse
|
|
405
|
+
if ($agentResponse.Length -gt 50000) {
|
|
406
|
+
$truncated = $agentResponse.Substring(0, 50000)
|
|
407
|
+
}
|
|
408
|
+
$body = @{
|
|
409
|
+
session_name = $sessionName
|
|
410
|
+
turn_number = [int]$turnNum
|
|
411
|
+
user_query = $userQuery
|
|
412
|
+
agent_response = $truncated
|
|
413
|
+
model = $model
|
|
414
|
+
tools_used = @(($toolsJson | ConvertFrom-Json))
|
|
415
|
+
files_referenced = @(($filesJson | ConvertFrom-Json))
|
|
416
|
+
total_context_tokens = [int]$totalTok
|
|
417
|
+
token_breakdown = @{
|
|
418
|
+
input_tokens = [int]$inputTok
|
|
419
|
+
cache_read_tokens = [int]$cacheReadTok
|
|
420
|
+
cache_creation_tokens = [int]$cacheCreateTok
|
|
421
|
+
}
|
|
422
|
+
} | ConvertTo-Json -Depth 10 -Compress
|
|
423
|
+
|
|
424
|
+
Invoke-RestMethod -Uri "$apiUrl/api/v1/working/turn" `
|
|
425
|
+
-Method POST `
|
|
426
|
+
-Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
|
|
427
|
+
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
|
|
428
|
+
-TimeoutSec 5 `
|
|
429
|
+
-ErrorAction SilentlyContinue | Out-Null
|
|
430
|
+
} catch {}
|
|
431
|
+
} -ArgumentList $authToken, $memoryApiUrl, $sessionName, $turn, $lastUser, $lastAssistant, $modelUsed, $toolsUsedJson, $filesRefJson, $totalTokens, $inputTokens, $cacheReadTokens, $cacheCreationTokens | Out-Null
|
|
300
432
|
|
|
301
433
|
# 2. EPISODIC MEMORY (Supabase)
|
|
302
434
|
Start-Job -ScriptBlock {
|
|
303
|
-
param($token, $userQuery, $agentResponse, $sessionId, $
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
-
|
|
325
|
-
|
|
435
|
+
param($token, $apiUrl, $userQuery, $agentResponse, $sessionId, $uid, $fileChanges, $model, $ts, $turnNum, $sessionName)
|
|
436
|
+
try {
|
|
437
|
+
$fcArray = @()
|
|
438
|
+
try { $fcArray = @(($fileChanges | ConvertFrom-Json)) } catch { $fcArray = @() }
|
|
439
|
+
|
|
440
|
+
$body = @{
|
|
441
|
+
user_query = $userQuery
|
|
442
|
+
assistant_response = $agentResponse
|
|
443
|
+
session_id = $sessionId
|
|
444
|
+
user_id = if ($uid) { $uid } else { "system" }
|
|
445
|
+
file_changes = $fcArray
|
|
446
|
+
metadata = @{
|
|
447
|
+
source = "claude-code"
|
|
448
|
+
model_used = $model
|
|
449
|
+
captured_at = $ts
|
|
450
|
+
turn_number = [int]$turnNum
|
|
451
|
+
session_name = $sessionName
|
|
452
|
+
minimal_hook = $true
|
|
453
|
+
}
|
|
454
|
+
} | ConvertTo-Json -Depth 10 -Compress
|
|
455
|
+
|
|
456
|
+
Invoke-RestMethod -Uri "$apiUrl/api/v1/memory/capture" `
|
|
457
|
+
-Method POST `
|
|
458
|
+
-Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
|
|
459
|
+
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
|
|
460
|
+
-TimeoutSec 5 `
|
|
461
|
+
-ErrorAction SilentlyContinue | Out-Null
|
|
462
|
+
} catch {}
|
|
463
|
+
} -ArgumentList $authToken, $memoryApiUrl, $lastUser, $lastAssistant, $rawSessionId, $userId, $fileChangesJson, $modelUsed, $timestamp, $turn, $sessionName | Out-Null
|
|
326
464
|
}
|
|
327
465
|
|
|
328
466
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
329
|
-
#
|
|
467
|
+
# Update local .ekkos/current-focus.md (if exists) - SILENT
|
|
330
468
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
331
|
-
|
|
332
|
-
|
|
469
|
+
$ekkosDir = Join-Path $ProjectRoot ".ekkos"
|
|
470
|
+
if ((Test-Path $ekkosDir) -and $lastUser) {
|
|
471
|
+
try {
|
|
472
|
+
$focusFile = Join-Path $ekkosDir "current-focus.md"
|
|
473
|
+
$taskSummary = $lastUser.Substring(0, [Math]::Min(100, $lastUser.Length))
|
|
474
|
+
if ($lastUser.Length -gt 100) { $taskSummary += "..." }
|
|
475
|
+
$focusTimestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
476
|
+
$focusContent = "---`nlast_updated: $focusTimestamp`nsession_id: $rawSessionId`n---`n`n# Current Focus`n$taskSummary"
|
|
477
|
+
Set-Content -Path $focusFile -Value $focusContent -Force
|
|
478
|
+
} catch {}
|
|
333
479
|
}
|
|
334
480
|
|
|
335
|
-
|
|
481
|
+
exit 0
|
package/templates/hooks/stop.sh
CHANGED
|
@@ -161,7 +161,7 @@ fi
|
|
|
161
161
|
|
|
162
162
|
[ -z "$AUTH_TOKEN" ] && exit 0
|
|
163
163
|
|
|
164
|
-
MEMORY_API_URL="https://
|
|
164
|
+
MEMORY_API_URL="https://api.ekkos.dev"
|
|
165
165
|
|
|
166
166
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
167
167
|
# SESSION BINDING: Bridge _pending → real session name for proxy eviction
|