@ekkos/cli 1.0.35 → 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 +2 -4
- package/templates/CLAUDE.md +135 -23
- package/templates/ekkos-manifest.json +8 -8
- package/templates/hooks/assistant-response.ps1 +256 -160
- package/templates/hooks/assistant-response.sh +130 -66
- package/templates/hooks/hooks.json +24 -6
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/count-tokens.cjs +0 -0
- package/templates/hooks/lib/ekkos-reminders.sh +0 -0
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/session-start.ps1 +91 -391
- package/templates/hooks/session-start.sh +201 -166
- package/templates/hooks/stop.ps1 +202 -341
- package/templates/hooks/stop.sh +275 -948
- package/templates/hooks/user-prompt-submit.ps1 +224 -548
- package/templates/hooks/user-prompt-submit.sh +382 -456
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/install.sh +0 -0
- package/templates/windsurf-hooks/lib/contract.sh +2 -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
|
@@ -1,656 +1,332 @@
|
|
|
1
1
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
2
2
|
# ekkOS_ Hook: UserPromptSubmit - SEAMLESS CONTEXT CONTINUITY (Windows)
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# Per
|
|
3
|
+
# MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
|
|
4
|
+
# EKKOS_MANAGED=1
|
|
5
|
+
# EKKOS_MANIFEST_SHA256=<computed-at-build>
|
|
6
|
+
# EKKOS_TEMPLATE_VERSION=1.0.0
|
|
7
|
+
#
|
|
8
|
+
# Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
|
|
9
|
+
# - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
10
|
+
# - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
|
|
9
11
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
10
12
|
|
|
11
13
|
$ErrorActionPreference = "SilentlyContinue"
|
|
12
14
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
+
# CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
|
|
17
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
+
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
|
|
19
|
+
$SessionWordsJson = "$EkkosConfigDir\session-words.json"
|
|
20
|
+
$SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
|
|
15
21
|
|
|
16
22
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
-
#
|
|
23
|
+
# INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
|
|
18
24
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
-
$
|
|
20
|
-
$SessionWordsJson = Join-Path $EkkosConfigDir "session-words.json"
|
|
21
|
-
$SessionWordsDefault = Join-Path $EkkosConfigDir ".defaults\session-words.json"
|
|
22
|
-
$JsonParseHelper = Join-Path $EkkosConfigDir ".helpers\json-parse.cjs"
|
|
25
|
+
$EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
|
|
23
26
|
|
|
24
27
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
-
#
|
|
28
|
+
# Load session words from JSON file - NO HARDCODED ARRAYS
|
|
26
29
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
-
$script:
|
|
28
|
-
$script:NOUNS = @()
|
|
29
|
-
$script:VERBS = @()
|
|
30
|
-
$script:SESSION_WORDS_LOADED = $false
|
|
30
|
+
$script:SessionWords = $null
|
|
31
31
|
|
|
32
32
|
function Load-SessionWords {
|
|
33
|
-
if ($script:SESSION_WORDS_LOADED) { return }
|
|
34
|
-
|
|
35
33
|
$wordsFile = $SessionWordsJson
|
|
34
|
+
|
|
35
|
+
# Fallback to managed defaults if user file missing/invalid
|
|
36
36
|
if (-not (Test-Path $wordsFile)) {
|
|
37
37
|
$wordsFile = $SessionWordsDefault
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
if (
|
|
41
|
-
$
|
|
42
|
-
$script:NOUNS = @("session")
|
|
43
|
-
$script:VERBS = @("starts")
|
|
44
|
-
return
|
|
40
|
+
if (-not (Test-Path $wordsFile)) {
|
|
41
|
+
return $null
|
|
45
42
|
}
|
|
46
43
|
|
|
47
44
|
try {
|
|
48
|
-
$
|
|
49
|
-
$nounRaw = & node $JsonParseHelper $wordsFile ".nouns" 2>$null
|
|
50
|
-
$verbRaw = & node $JsonParseHelper $wordsFile ".verbs" 2>$null
|
|
51
|
-
|
|
52
|
-
$script:ADJECTIVES = @()
|
|
53
|
-
$script:NOUNS = @()
|
|
54
|
-
$script:VERBS = @()
|
|
55
|
-
|
|
56
|
-
if ($adjRaw) { $script:ADJECTIVES = @($adjRaw -split "`n" | Where-Object { $_ }) }
|
|
57
|
-
if ($nounRaw) { $script:NOUNS = @($nounRaw -split "`n" | Where-Object { $_ }) }
|
|
58
|
-
if ($verbRaw) { $script:VERBS = @($verbRaw -split "`n" | Where-Object { $_ }) }
|
|
59
|
-
|
|
60
|
-
if ($script:ADJECTIVES.Count -eq 0) { $script:ADJECTIVES = @("unknown") }
|
|
61
|
-
if ($script:NOUNS.Count -eq 0) { $script:NOUNS = @("session") }
|
|
62
|
-
if ($script:VERBS.Count -eq 0) { $script:VERBS = @("starts") }
|
|
63
|
-
|
|
64
|
-
$script:SESSION_WORDS_LOADED = $true
|
|
45
|
+
$script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
|
|
65
46
|
} catch {
|
|
66
|
-
$
|
|
67
|
-
$script:NOUNS = @("session")
|
|
68
|
-
$script:VERBS = @("starts")
|
|
47
|
+
return $null
|
|
69
48
|
}
|
|
70
49
|
}
|
|
71
50
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
Load-SessionWords
|
|
76
|
-
|
|
77
|
-
$hex = $uuid -replace "-", ""
|
|
78
|
-
$hex = $hex.Substring(0, [Math]::Min(12, $hex.Length))
|
|
79
|
-
if ($hex -notmatch '^[0-9a-fA-F]+$') { return "unknown-session-starts" }
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
$adjSeed = [Convert]::ToInt32($hex.Substring(0, 4), 16)
|
|
83
|
-
$nounSeed = [Convert]::ToInt32($hex.Substring(4, 4), 16)
|
|
84
|
-
$verbSeed = [Convert]::ToInt32($hex.Substring(8, 4), 16)
|
|
85
|
-
|
|
86
|
-
$adjIdx = $adjSeed % $script:ADJECTIVES.Count
|
|
87
|
-
$nounIdx = $nounSeed % $script:NOUNS.Count
|
|
88
|
-
$verbIdx = $verbSeed % $script:VERBS.Count
|
|
89
|
-
|
|
90
|
-
return "$($script:ADJECTIVES[$adjIdx])-$($script:NOUNS[$nounIdx])-$($script:VERBS[$verbIdx])"
|
|
91
|
-
} catch {
|
|
92
|
-
return "unknown-session-starts"
|
|
93
|
-
}
|
|
94
|
-
}
|
|
51
|
+
# Read input from stdin
|
|
52
|
+
$inputJson = [Console]::In.ReadToEnd()
|
|
53
|
+
if (-not $inputJson) { exit 0 }
|
|
95
54
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
param(
|
|
101
|
-
[string]$Json,
|
|
102
|
-
[string]$Path
|
|
103
|
-
)
|
|
104
|
-
if (-not $Json) { return "" }
|
|
105
|
-
try {
|
|
106
|
-
$result = $Json | node -e "
|
|
107
|
-
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
108
|
-
const path = '$Path'.replace(/^\./,'').split('.').filter(Boolean);
|
|
109
|
-
let result = data;
|
|
110
|
-
for (const p of path) {
|
|
111
|
-
if (result === undefined || result === null) { result = undefined; break; }
|
|
112
|
-
result = result[p];
|
|
113
|
-
}
|
|
114
|
-
if (result !== undefined && result !== null) console.log(result);
|
|
115
|
-
" 2>$null
|
|
116
|
-
if ($result) { return $result.Trim() } else { return "" }
|
|
117
|
-
} catch {
|
|
118
|
-
return ""
|
|
119
|
-
}
|
|
55
|
+
try {
|
|
56
|
+
$input = $inputJson | ConvertFrom-Json
|
|
57
|
+
} catch {
|
|
58
|
+
exit 0
|
|
120
59
|
}
|
|
121
60
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
$
|
|
126
|
-
if (-not $INPUT) { exit 0 }
|
|
61
|
+
$userQuery = $input.query
|
|
62
|
+
if (-not $userQuery) { $userQuery = $input.message }
|
|
63
|
+
if (-not $userQuery) { $userQuery = $input.prompt }
|
|
64
|
+
if (-not $userQuery) { exit 0 }
|
|
127
65
|
|
|
128
|
-
$
|
|
129
|
-
if (-not $
|
|
130
|
-
if (-not $USER_QUERY) { $USER_QUERY = Parse-JsonValue $INPUT ".prompt" }
|
|
131
|
-
if (-not $USER_QUERY -or $USER_QUERY -eq "null") { exit 0 }
|
|
66
|
+
$rawSessionId = $input.session_id
|
|
67
|
+
if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
|
|
132
68
|
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
$TRANSCRIPT_PATH = Parse-JsonValue $INPUT ".transcript_path"
|
|
136
|
-
|
|
137
|
-
# Fallback: read session_id from saved state if not in INPUT
|
|
138
|
-
if ($RAW_SESSION_ID -eq "unknown") {
|
|
69
|
+
# Fallback: read session_id from saved state
|
|
70
|
+
if ($rawSessionId -eq "unknown") {
|
|
139
71
|
$stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
|
|
140
72
|
if (Test-Path $stateFile) {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
73
|
+
try {
|
|
74
|
+
$state = Get-Content $stateFile -Raw | ConvertFrom-Json
|
|
75
|
+
$rawSessionId = $state.session_id
|
|
76
|
+
} catch {}
|
|
146
77
|
}
|
|
147
78
|
}
|
|
148
79
|
|
|
149
80
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
150
81
|
# INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
|
|
151
|
-
# Detects ALL applicable skills/tools and injects as system reminder
|
|
152
82
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
153
|
-
$
|
|
154
|
-
$
|
|
83
|
+
$skillReminders = @()
|
|
84
|
+
$queryLower = $userQuery.ToLower()
|
|
155
85
|
|
|
156
86
|
# Memory First - Debug/Error/Problem solving
|
|
157
|
-
if ($
|
|
158
|
-
$
|
|
87
|
+
if ($queryLower -match '(how do i|debug|error|bug|fix|not working|broken|fails|issue|problem|wrong|crash)') {
|
|
88
|
+
$skillReminders += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Memory_First`") FIRST before debugging"
|
|
159
89
|
}
|
|
160
90
|
|
|
161
|
-
# Recall Triggers
|
|
162
|
-
if ($
|
|
163
|
-
$
|
|
91
|
+
# Recall Triggers - Time-based memory
|
|
92
|
+
if ($queryLower -match '(yesterday|last week|last month|remember when|what did we|where did we leave|before|earlier|previous|ago)') {
|
|
93
|
+
$skillReminders += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Deep_Recall`") for time-based memory"
|
|
164
94
|
}
|
|
165
95
|
|
|
166
|
-
# Directive Triggers
|
|
167
|
-
if ($
|
|
168
|
-
$
|
|
96
|
+
# Directive Triggers - User preferences
|
|
97
|
+
if ($queryLower -match '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on)') {
|
|
98
|
+
$skillReminders += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Preferences`") to capture directive"
|
|
169
99
|
}
|
|
170
100
|
|
|
171
|
-
# Safety Triggers
|
|
172
|
-
if ($
|
|
173
|
-
$
|
|
101
|
+
# Safety Triggers - Destructive actions
|
|
102
|
+
if ($queryLower -match '(delete|drop |rm -rf|deploy|push.*main|push.*master|production|migrate|rollback)') {
|
|
103
|
+
$skillReminders += "SAFETY REQUIRED: Call ekkOS_Conflict before this destructive action"
|
|
174
104
|
}
|
|
175
105
|
|
|
176
|
-
# Schema Triggers
|
|
177
|
-
if ($
|
|
178
|
-
$
|
|
106
|
+
# Schema Triggers - Database operations
|
|
107
|
+
if ($queryLower -match '(sql|query|supabase|prisma|database|table|column|select |insert |update |where )') {
|
|
108
|
+
$skillReminders += "SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names"
|
|
179
109
|
}
|
|
180
110
|
|
|
181
|
-
# Secret Triggers (API keys, credentials)
|
|
182
|
-
if ($QUERY_LOWER -match '(api key|token|password|credential|secret|my.*key|store.*key)') {
|
|
183
|
-
$SKILL_REMINDERS += "SECRETS: Use ekkOS_StoreSecret to securely save credentials"
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
# Plan Triggers (Complex multi-step tasks)
|
|
187
|
-
if ($QUERY_LOWER -match '(implement|build|create.*feature|refactor|migrate|set up|architecture)') {
|
|
188
|
-
$SKILL_REMINDERS += "PLAN REQUIRED: Call ekkOS_Plan for complex multi-step tasks"
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
# Learn Triggers (User expressing success/failure)
|
|
192
|
-
if ($QUERY_LOWER -match '(that worked|thanks|perfect|great|awesome|nailed it|solved|fixed it)') {
|
|
193
|
-
$SKILL_REMINDERS += "LEARN: Consider calling ekkOS_Forge to capture this solution as a pattern"
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
# Codebase Triggers (Project-specific code search)
|
|
197
|
-
if ($QUERY_LOWER -match '(where is|find.*file|search.*code|in this project|in the codebase)') {
|
|
198
|
-
$SKILL_REMINDERS += "CODEBASE: Use ekkOS_Codebase for project-specific code search"
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
# Combine skill reminders (only take first 3 to avoid noise)
|
|
202
|
-
$SKILL_REMINDER = ""
|
|
203
|
-
$reminderCount = $SKILL_REMINDERS.Count
|
|
204
|
-
if ($reminderCount -gt 0) {
|
|
205
|
-
$maxReminders = [Math]::Min(3, $reminderCount)
|
|
206
|
-
$SKILL_REMINDER = ($SKILL_REMINDERS | Select-Object -First $maxReminders) -join "`n"
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
210
|
-
# Load auth
|
|
211
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
212
|
-
$EKKOS_CONFIG = Join-Path $env:USERPROFILE ".ekkos\config.json"
|
|
213
|
-
$AUTH_TOKEN = ""
|
|
214
|
-
|
|
215
|
-
if ((Test-Path $EKKOS_CONFIG) -and (Test-Path $JsonParseHelper)) {
|
|
216
|
-
$AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
|
|
217
|
-
if (-not $AUTH_TOKEN) {
|
|
218
|
-
$AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".apiKey" 2>$null
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
if (-not $AUTH_TOKEN) {
|
|
223
|
-
$envLocalFile = Join-Path $ProjectRoot ".env.local"
|
|
224
|
-
if (Test-Path $envLocalFile) {
|
|
225
|
-
$envLines = Get-Content $envLocalFile
|
|
226
|
-
foreach ($line in $envLines) {
|
|
227
|
-
if ($line -match '^SUPABASE_SECRET_KEY=(.+)$') {
|
|
228
|
-
$AUTH_TOKEN = $Matches[1].Trim('"', "'", ' ', "`r")
|
|
229
|
-
break
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
$MEMORY_API_URL = "https://mcp.ekkos.dev"
|
|
236
|
-
|
|
237
111
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
238
|
-
#
|
|
112
|
+
# SESSION NAME - Resolve early so it's available for all downstream use
|
|
239
113
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
New-Item -ItemType Directory -Path $StateDir -Force | Out-Null
|
|
243
|
-
}
|
|
244
|
-
$SESSION_FILE = Join-Path $StateDir "current-session.json"
|
|
114
|
+
function Convert-UuidToWords {
|
|
115
|
+
param([string]$uuid)
|
|
245
116
|
|
|
246
|
-
$
|
|
117
|
+
if (-not $script:SessionWords) { Load-SessionWords }
|
|
118
|
+
if (-not $script:SessionWords) { return "unknown-session" }
|
|
247
119
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
120
|
+
$adjectives = $script:SessionWords.adjectives
|
|
121
|
+
$nouns = $script:SessionWords.nouns
|
|
122
|
+
$verbs = $script:SessionWords.verbs
|
|
252
123
|
|
|
253
|
-
|
|
254
|
-
$
|
|
255
|
-
Set-Content -Path $SESSION_FILE -Value "{`"session_id`": `"$SESSION_ID`", `"timestamp`": `"$timestampUtc`"}" -Force
|
|
124
|
+
if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
|
|
125
|
+
if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
|
|
256
126
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
260
|
-
$ProjectSessionDir = Join-Path $StateDir "sessions"
|
|
261
|
-
if (-not (Test-Path $ProjectSessionDir)) {
|
|
262
|
-
New-Item -ItemType Directory -Path $ProjectSessionDir -Force | Out-Null
|
|
263
|
-
}
|
|
264
|
-
$TURN_COUNTER_FILE = Join-Path $ProjectSessionDir "$SESSION_ID.turn"
|
|
127
|
+
$clean = $uuid -replace "-", ""
|
|
128
|
+
if ($clean.Length -lt 12) { return "unknown-session" }
|
|
265
129
|
|
|
266
|
-
# Count actual user messages in transcript for accurate turn number
|
|
267
|
-
$TURN_NUMBER = 1
|
|
268
|
-
if ($TRANSCRIPT_PATH -and (Test-Path $TRANSCRIPT_PATH)) {
|
|
269
130
|
try {
|
|
270
|
-
$
|
|
271
|
-
$
|
|
272
|
-
|
|
131
|
+
$a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
|
|
132
|
+
$n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
|
|
133
|
+
$an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
|
|
134
|
+
return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
|
|
273
135
|
} catch {
|
|
274
|
-
|
|
136
|
+
return "unknown-session"
|
|
275
137
|
}
|
|
276
138
|
}
|
|
277
139
|
|
|
278
|
-
|
|
279
|
-
$SAVED_TURN_COUNT = 0
|
|
280
|
-
$TRANSCRIPT_TURN_COUNT = $TURN_NUMBER
|
|
281
|
-
$POST_CLEAR_DETECTED = $false
|
|
282
|
-
|
|
283
|
-
if (Test-Path $TURN_COUNTER_FILE) {
|
|
284
|
-
try {
|
|
285
|
-
$SAVED_TURN_COUNT = [int](Get-Content $TURN_COUNTER_FILE -Raw).Trim()
|
|
286
|
-
} catch {
|
|
287
|
-
$SAVED_TURN_COUNT = 0
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
if ($SAVED_TURN_COUNT -gt $TURN_NUMBER) {
|
|
292
|
-
# Post-clear: INCREMENT from saved count
|
|
293
|
-
$TURN_NUMBER = $SAVED_TURN_COUNT + 1
|
|
294
|
-
$POST_CLEAR_DETECTED = $true
|
|
295
|
-
}
|
|
296
|
-
Set-Content -Path $TURN_COUNTER_FILE -Value "$TURN_NUMBER" -Force
|
|
140
|
+
$sessionName = Convert-UuidToWords $rawSessionId
|
|
297
141
|
|
|
298
142
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
299
|
-
#
|
|
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.
|
|
300
146
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
301
|
-
if (
|
|
302
|
-
$
|
|
303
|
-
if (Test-Path $
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
$
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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 {}
|
|
325
173
|
}
|
|
326
174
|
}
|
|
327
175
|
|
|
328
176
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
329
|
-
#
|
|
177
|
+
# SESSION CURRENT: Update Redis with current session name
|
|
330
178
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
331
|
-
$
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
$projectRootNorm = $ProjectRoot -replace '\\', '/'
|
|
335
|
-
Start-Job -ScriptBlock {
|
|
336
|
-
param($sessionId, $sessionName, $turnNum, $query, $projRoot)
|
|
179
|
+
if ($sessionName -ne "unknown-session" -and $rawSessionId -ne "unknown") {
|
|
180
|
+
$configFile2 = Join-Path $EkkosConfigDir "config.json"
|
|
181
|
+
if (Test-Path $configFile2) {
|
|
337
182
|
try {
|
|
338
|
-
|
|
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
|
+
}
|
|
339
196
|
} catch {}
|
|
340
|
-
}
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
344
|
-
# GOLDEN LOOP: CAPTURE PHASE - Track turn start
|
|
345
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
346
|
-
$EkkosDir = Join-Path $ProjectRoot ".ekkos"
|
|
347
|
-
if (-not (Test-Path $EkkosDir)) {
|
|
348
|
-
New-Item -ItemType Directory -Path $EkkosDir -Force | Out-Null
|
|
349
|
-
}
|
|
350
|
-
$GOLDEN_LOOP_FILE = Join-Path $EkkosDir "golden-loop-current.json"
|
|
351
|
-
|
|
352
|
-
# Write current phase to file (extension watches this for real-time updates)
|
|
353
|
-
$glPathEscaped = $GOLDEN_LOOP_FILE -replace '\\', '\\\\'
|
|
354
|
-
try {
|
|
355
|
-
node -e "
|
|
356
|
-
const fs = require('fs');
|
|
357
|
-
const data = {
|
|
358
|
-
phase: 'capture',
|
|
359
|
-
turn: $TURN_NUMBER,
|
|
360
|
-
session: '$SESSION_ID',
|
|
361
|
-
timestamp: new Date().toISOString(),
|
|
362
|
-
stats: { retrieved: 0, applied: 0, forged: 0 }
|
|
363
|
-
};
|
|
364
|
-
fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
|
|
365
|
-
" 2>$null
|
|
366
|
-
} catch {}
|
|
367
|
-
|
|
368
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
369
|
-
# GOLDEN LOOP: RETRIEVE PHASE - Auto-retrieve patterns from ekkOS
|
|
370
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
371
|
-
$EKKOS_API_KEY = ""
|
|
372
|
-
$hookApiKeyFile = Join-Path $env:USERPROFILE ".ekkos\.hookApiKey"
|
|
373
|
-
if (Test-Path $hookApiKeyFile) {
|
|
374
|
-
$EKKOS_API_KEY = (Get-Content $hookApiKeyFile -Raw).Trim()
|
|
375
|
-
} elseif ((Test-Path $EKKOS_CONFIG) -and (Test-Path $JsonParseHelper)) {
|
|
376
|
-
$EKKOS_API_KEY = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
|
|
197
|
+
}
|
|
377
198
|
}
|
|
378
199
|
|
|
379
|
-
$RETRIEVED_PATTERNS = ""
|
|
380
|
-
$PATTERN_COUNT = 0
|
|
381
|
-
$RETRIEVED_DIRECTIVES = ""
|
|
382
|
-
$DIRECTIVE_COUNT = 0
|
|
383
|
-
|
|
384
200
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
385
|
-
#
|
|
201
|
+
# TURN TRACKING & STATE MANAGEMENT
|
|
386
202
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
387
|
-
$
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (-not (Test-Path $DirectiveCacheDir)) {
|
|
391
|
-
New-Item -ItemType Directory -Path $DirectiveCacheDir -Force | Out-Null
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
# Check if we need to refresh directive cache
|
|
395
|
-
$DIRECTIVE_CACHE_VALID = $false
|
|
396
|
-
$DIRECTIVE_TRIGGER_DETECTED = $false
|
|
397
|
-
|
|
398
|
-
if ($QUERY_LOWER -match '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on|directive|preference)') {
|
|
399
|
-
$DIRECTIVE_TRIGGER_DETECTED = $true
|
|
203
|
+
$stateDir = Join-Path $env:USERPROFILE ".claude\state"
|
|
204
|
+
if (-not (Test-Path $stateDir)) {
|
|
205
|
+
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
|
|
400
206
|
}
|
|
401
207
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if ($cacheTimestampStr -match '^\d+$') {
|
|
406
|
-
$cacheTimestamp = [int64]$cacheTimestampStr
|
|
407
|
-
$currentTimestamp = [int64]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())
|
|
408
|
-
$cacheAge = $currentTimestamp - $cacheTimestamp
|
|
208
|
+
$stateFile = Join-Path $stateDir "hook-state.json"
|
|
209
|
+
$turn = 0
|
|
210
|
+
$contextPercent = ""
|
|
409
211
|
|
|
410
|
-
|
|
411
|
-
|
|
212
|
+
if (Test-Path $stateFile) {
|
|
213
|
+
try {
|
|
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
|
|
412
221
|
}
|
|
222
|
+
} catch {
|
|
223
|
+
$turn = 0
|
|
413
224
|
}
|
|
414
225
|
}
|
|
415
226
|
|
|
416
|
-
#
|
|
417
|
-
$
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
# Update phase to RETRIEVE
|
|
424
|
-
try {
|
|
425
|
-
node -e "
|
|
426
|
-
const fs = require('fs');
|
|
427
|
-
const data = {
|
|
428
|
-
phase: 'retrieve',
|
|
429
|
-
turn: $TURN_NUMBER,
|
|
430
|
-
session: '$SESSION_ID',
|
|
431
|
-
timestamp: new Date().toISOString(),
|
|
432
|
-
stats: { retrieved: 0, applied: 0, forged: 0 }
|
|
433
|
-
};
|
|
434
|
-
fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
|
|
435
|
-
" 2>$null
|
|
436
|
-
} catch {}
|
|
437
|
-
|
|
438
|
-
# Build sources array - always include patterns, conditionally include directives
|
|
439
|
-
if ($DIRECTIVE_CACHE_VALID) {
|
|
440
|
-
$searchSources = '["patterns"]'
|
|
441
|
-
} else {
|
|
442
|
-
$searchSources = '["patterns", "directives"]'
|
|
443
|
-
}
|
|
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
|
|
444
234
|
|
|
445
|
-
|
|
446
|
-
$queryJsonEscaped = $USER_QUERY -replace '\\', '\\\\' -replace '"', '\"' -replace "`n", '\n' -replace "`r", '' -replace "`t", '\t'
|
|
235
|
+
Set-Content -Path $stateFile -Value $newState -Force
|
|
447
236
|
|
|
448
|
-
|
|
449
|
-
|
|
237
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
238
|
+
# LOCAL CACHE: Tier 0 capture (async, non-blocking)
|
|
239
|
+
# Per v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
240
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
241
|
+
$captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
|
|
242
|
+
if ($captureCmd -and $rawSessionId -ne "unknown") {
|
|
450
243
|
try {
|
|
451
|
-
|
|
452
|
-
$
|
|
453
|
-
|
|
454
|
-
-Headers @{ Authorization = "Bearer $EKKOS_API_KEY"; "Content-Type" = "application/json" } `
|
|
455
|
-
-Body ([System.Text.Encoding]::UTF8.GetBytes($searchBody)) `
|
|
456
|
-
-TimeoutSec 2 `
|
|
457
|
-
-ErrorAction Stop
|
|
458
|
-
} catch {
|
|
459
|
-
$SEARCH_RESPONSE_RAW = $null
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
if ($SEARCH_RESPONSE_RAW) {
|
|
463
|
-
# Convert to JSON string for Node parsing
|
|
464
|
-
$SEARCH_JSON = ""
|
|
465
|
-
if ($SEARCH_RESPONSE_RAW -is [string]) {
|
|
466
|
-
$SEARCH_JSON = $SEARCH_RESPONSE_RAW
|
|
467
|
-
} else {
|
|
468
|
-
try {
|
|
469
|
-
$SEARCH_JSON = $SEARCH_RESPONSE_RAW | ConvertTo-Json -Depth 20 -Compress
|
|
470
|
-
} catch {
|
|
471
|
-
$SEARCH_JSON = '{}'
|
|
472
|
-
}
|
|
473
|
-
}
|
|
244
|
+
# NEW format: ekkos-capture user <instance_id> <session_id> <session_name> <turn_id> <query> [project_path]
|
|
245
|
+
$queryBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($userQuery))
|
|
246
|
+
$projectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
|
|
474
247
|
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
478
|
-
const patterns = (d.result && d.result.results && d.result.results.patterns) || [];
|
|
479
|
-
console.log(patterns.length);
|
|
480
|
-
" 2>$null
|
|
481
|
-
if ($patternCountStr -match '^\d+$') {
|
|
482
|
-
$PATTERN_COUNT = [int]$patternCountStr
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
# Update golden loop with retrieved count
|
|
486
|
-
if ($PATTERN_COUNT -gt 0) {
|
|
248
|
+
Start-Job -ScriptBlock {
|
|
249
|
+
param($instanceId, $sessionId, $sessionName, $turnNum, $queryB64, $projectPath)
|
|
487
250
|
try {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
const data = {
|
|
491
|
-
phase: 'inject',
|
|
492
|
-
turn: $TURN_NUMBER,
|
|
493
|
-
session: '$SESSION_ID',
|
|
494
|
-
timestamp: new Date().toISOString(),
|
|
495
|
-
stats: { retrieved: $PATTERN_COUNT, applied: 0, forged: 0 }
|
|
496
|
-
};
|
|
497
|
-
fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
|
|
498
|
-
" 2>$null
|
|
251
|
+
$decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($queryB64))
|
|
252
|
+
& ekkos-capture user $instanceId $sessionId $sessionName $turnNum $decoded $projectPath 2>&1 | Out-Null
|
|
499
253
|
} catch {}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
$RETRIEVED_PATTERNS = $SEARCH_JSON | node -e "
|
|
503
|
-
const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
504
|
-
const patterns = (d.result && d.result.results && d.result.results.patterns) || [];
|
|
505
|
-
patterns.forEach(p => {
|
|
506
|
-
console.log('**' + (p.title || '') + '**');
|
|
507
|
-
console.log(p.problem || p.guidance || '');
|
|
508
|
-
console.log('');
|
|
509
|
-
console.log('## Solution');
|
|
510
|
-
console.log(p.solution || p.content || '');
|
|
511
|
-
console.log('');
|
|
512
|
-
console.log('Success Rate: ' + ((p.success_rate || 0) * 100) + '%');
|
|
513
|
-
console.log('Applied: ' + (p.applied_count || 0) + ' times');
|
|
514
|
-
console.log('');
|
|
515
|
-
});
|
|
516
|
-
" 2>$null
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
# ═══════════════════════════════════════════════════════════════════
|
|
520
|
-
# DIRECTIVE HANDLING: Use cache if valid, otherwise process response
|
|
521
|
-
# ═══════════════════════════════════════════════════════════════════
|
|
522
|
-
if ($DIRECTIVE_CACHE_VALID) {
|
|
523
|
-
# Load directives from cache
|
|
524
|
-
$cacheContent = Get-Content $DirectiveCacheFile -Raw
|
|
525
|
-
$dcStr = Parse-JsonValue $cacheContent ".count"
|
|
526
|
-
if ($dcStr -match '^\d+$') { $DIRECTIVE_COUNT = [int]$dcStr }
|
|
527
|
-
$RETRIEVED_DIRECTIVES = Parse-JsonValue $cacheContent ".formatted"
|
|
528
|
-
} else {
|
|
529
|
-
# Extract and format DIRECTIVES from API response
|
|
530
|
-
$directiveResult = $SEARCH_JSON | node -e "
|
|
531
|
-
const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
532
|
-
const directives = (d.result && d.result.results && d.result.results.directives) || [];
|
|
533
|
-
const count = directives.length;
|
|
534
|
-
if (count === 0) { console.log(JSON.stringify({count: 0, formatted: ''})); process.exit(0); }
|
|
535
|
-
|
|
536
|
-
const grouped = {MUST: [], NEVER: [], PREFER: [], AVOID: []};
|
|
537
|
-
directives.forEach(d => {
|
|
538
|
-
if (grouped[d.type]) grouped[d.type].push(d.rule);
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
let formatted = 'USER DIRECTIVES (FOLLOW THESE):';
|
|
542
|
-
for (const [type, rules] of Object.entries(grouped)) {
|
|
543
|
-
if (rules.length > 0) {
|
|
544
|
-
formatted += '\n\n' + type + ':';
|
|
545
|
-
rules.forEach(r => { formatted += '\n - ' + r; });
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
console.log(JSON.stringify({count, formatted}));
|
|
550
|
-
|
|
551
|
-
// Save to cache
|
|
552
|
-
const cacheData = {count, formatted, cached_at: Math.floor(Date.now() / 1000)};
|
|
553
|
-
try {
|
|
554
|
-
const fs = require('fs');
|
|
555
|
-
fs.mkdirSync(process.argv[1], {recursive: true});
|
|
556
|
-
fs.writeFileSync(process.argv[2], JSON.stringify(cacheData));
|
|
557
|
-
} catch(e) {}
|
|
558
|
-
" -- "$DirectiveCacheDir" "$DirectiveCacheFile" 2>$null
|
|
559
|
-
|
|
560
|
-
if ($directiveResult) {
|
|
561
|
-
$dcStr = $directiveResult | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.count||0)" 2>$null
|
|
562
|
-
if ($dcStr -match '^\d+$') { $DIRECTIVE_COUNT = [int]$dcStr }
|
|
563
|
-
$RETRIEVED_DIRECTIVES = $directiveResult | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.formatted||'')" 2>$null
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
}
|
|
254
|
+
} -ArgumentList $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $queryBase64, $projectRoot | Out-Null
|
|
255
|
+
} catch {}
|
|
567
256
|
}
|
|
568
257
|
|
|
569
258
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
570
|
-
#
|
|
259
|
+
# WORKING MEMORY: Fast capture to API (async, non-blocking)
|
|
571
260
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
572
|
-
$
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
$
|
|
576
|
-
$
|
|
577
|
-
$
|
|
578
|
-
|
|
579
|
-
$
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
261
|
+
$configFile = Join-Path $EkkosConfigDir "config.json"
|
|
262
|
+
if (Test-Path $configFile) {
|
|
263
|
+
try {
|
|
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" `
|
|
281
|
+
-Method POST `
|
|
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
|
+
}
|
|
287
|
+
} catch {}
|
|
587
288
|
}
|
|
588
289
|
|
|
290
|
+
$timestamp = (Get-Date).ToString("yyyy-MM-dd hh:mm:ss tt") + " EST"
|
|
291
|
+
|
|
589
292
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
590
|
-
#
|
|
293
|
+
# DASHBOARD HINT FILE: write session info for ekkos dashboard --wait-for-new
|
|
294
|
+
# On Windows, active-sessions.json is never populated (hook PIDs are dead).
|
|
295
|
+
# The dashboard reads this file instead to locate the JSONL path.
|
|
591
296
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
592
|
-
|
|
593
|
-
|
|
297
|
+
if ($sessionName -ne "unknown-session") {
|
|
298
|
+
try {
|
|
299
|
+
$projectPath = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
|
|
300
|
+
$hintFile = Join-Path $EkkosConfigDir "hook-session-hint.json"
|
|
301
|
+
$hint = @{
|
|
302
|
+
sessionName = $sessionName
|
|
303
|
+
sessionId = $rawSessionId
|
|
304
|
+
projectPath = $projectPath
|
|
305
|
+
ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
306
|
+
} | ConvertTo-Json -Depth 10 -Compress
|
|
307
|
+
Set-Content -Path $hintFile -Value $hint -Force
|
|
308
|
+
} catch {}
|
|
309
|
+
}
|
|
594
310
|
|
|
595
311
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
596
|
-
#
|
|
312
|
+
# OUTPUT SYSTEM REMINDER
|
|
597
313
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
314
|
+
$esc = [char]27
|
|
315
|
+
$header = "${esc}[0;36m${esc}[1m🧠 ekkOS Memory${esc}[0m ${esc}[2m| $sessionName | $timestamp${esc}[0m"
|
|
598
316
|
|
|
599
|
-
|
|
600
|
-
|
|
317
|
+
$output = @"
|
|
318
|
+
$header
|
|
601
319
|
|
|
602
|
-
|
|
603
|
-
if ($SKILL_REMINDER) {
|
|
604
|
-
Write-Output ""
|
|
605
|
-
Write-Output "${MAGENTA}${BOLD}${SKILL_REMINDER}${RESET}"
|
|
606
|
-
}
|
|
320
|
+
"@
|
|
607
321
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
if ($SHOULD_INJECT_DIRECTIVES -and $RETRIEVED_DIRECTIVES -and $DIRECTIVE_COUNT -gt 0) {
|
|
611
|
-
Write-Output ""
|
|
612
|
-
Write-Output "<system-reminder>"
|
|
613
|
-
Write-Output "$RETRIEVED_DIRECTIVES"
|
|
614
|
-
Write-Output "</system-reminder>"
|
|
322
|
+
if ($skillReminders.Count -gt 0) {
|
|
323
|
+
$output += "${esc}[0;35m${esc}[1m" + ($skillReminders -join "`n") + "${esc}[0m`n"
|
|
615
324
|
}
|
|
616
325
|
|
|
617
|
-
|
|
618
|
-
if ($RETRIEVED_PATTERNS -and $PATTERN_COUNT -gt 0) {
|
|
619
|
-
Write-Output ""
|
|
620
|
-
Write-Output "<system-reminder>"
|
|
621
|
-
Write-Output "RETRIEVED PATTERNS FROM ekkOS MEMORY ($PATTERN_COUNT patterns found)"
|
|
622
|
-
Write-Output ""
|
|
623
|
-
Write-Output "$RETRIEVED_PATTERNS"
|
|
624
|
-
Write-Output ""
|
|
625
|
-
Write-Output "These patterns were automatically retrieved based on your query. Review and apply relevant solutions."
|
|
626
|
-
Write-Output "</system-reminder>"
|
|
627
|
-
Write-Output ""
|
|
628
|
-
Write-Output "MANDATORY PATTERN ACKNOWLEDGMENT (PatternGuard):"
|
|
629
|
-
Write-Output "You MUST acknowledge ALL $PATTERN_COUNT patterns above - either SELECT or SKIP each one."
|
|
630
|
-
Write-Output ""
|
|
631
|
-
Write-Output "For patterns you USE:"
|
|
632
|
-
Write-Output "[ekkOS_SELECT]"
|
|
633
|
-
Write-Output "- id: <pattern_id>"
|
|
634
|
-
Write-Output " reason: <1-line why using>"
|
|
635
|
-
Write-Output " confidence: <0.0-1.0>"
|
|
636
|
-
Write-Output "[/ekkOS_SELECT]"
|
|
637
|
-
Write-Output ""
|
|
638
|
-
Write-Output "For patterns NOT relevant:"
|
|
639
|
-
Write-Output "[ekkOS_SKIP]"
|
|
640
|
-
Write-Output "- id: <pattern_id>"
|
|
641
|
-
Write-Output " reason: <1-line why not relevant>"
|
|
642
|
-
Write-Output "[/ekkOS_SKIP]"
|
|
643
|
-
Write-Output ""
|
|
644
|
-
Write-Output "AUTOMATIC APPLICATION: When pattern matches your task, APPLY IT immediately without asking."
|
|
645
|
-
Write-Output "Track outcome after: ekkOS_Outcome({success: true/false})"
|
|
646
|
-
Write-Output ""
|
|
647
|
-
Write-Output "100% coverage required. This is how the system learns what works."
|
|
648
|
-
}
|
|
326
|
+
$output += @"
|
|
649
327
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
Write-Output "<footer-format>End responses with: Claude Code ({Model}) · ekkOS_ · Turn ${TURN_NUMBER} · ${SESSION_NAME} · ${CURRENT_TIME}</footer-format>"
|
|
654
|
-
}
|
|
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
|
+
"@
|
|
655
331
|
|
|
656
|
-
|
|
332
|
+
Write-Output $output
|