@ekkos/cli 1.0.34 → 1.0.35
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 +4 -2
- package/templates/CLAUDE.md +23 -135
- 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/ekkos-manifest.json +8 -8
- package/templates/hooks/assistant-response.ps1 +160 -256
- package/templates/hooks/assistant-response.sh +66 -130
- package/templates/hooks/hooks.json +0 -6
- package/templates/hooks/lib/contract.sh +31 -43
- package/templates/hooks/lib/count-tokens.cjs +0 -0
- package/templates/hooks/lib/ekkos-reminders.sh +0 -0
- package/templates/hooks/lib/state.sh +1 -53
- package/templates/hooks/session-start.ps1 +391 -91
- package/templates/hooks/session-start.sh +166 -201
- package/templates/hooks/stop.ps1 +341 -202
- package/templates/hooks/stop.sh +948 -275
- package/templates/hooks/user-prompt-submit.ps1 +548 -224
- package/templates/hooks/user-prompt-submit.sh +456 -382
- 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/hooks.json +2 -9
- package/templates/windsurf-hooks/install.sh +0 -0
- package/templates/windsurf-hooks/lib/contract.sh +0 -2
- 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
|
@@ -1,332 +1,656 @@
|
|
|
1
1
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
2
2
|
# ekkOS_ Hook: UserPromptSubmit - SEAMLESS CONTEXT CONTINUITY (Windows)
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
# Per
|
|
9
|
-
# - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
10
|
-
# - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
|
|
3
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
4
|
+
# ZERO USER ACTION NEEDED:
|
|
5
|
+
# 1. Tracks turn number and context size
|
|
6
|
+
# 2. Detects when compaction happened (context dropped from high to low)
|
|
7
|
+
# 3. AUTO-INJECTS restored context - user just keeps working
|
|
8
|
+
# Per spec v1.2 Addendum: NO jq dependency, uses Node.js for JSON
|
|
11
9
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
12
10
|
|
|
13
11
|
$ErrorActionPreference = "SilentlyContinue"
|
|
14
12
|
|
|
15
|
-
|
|
16
|
-
|
|
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"
|
|
13
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
14
|
+
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
|
21
15
|
|
|
22
16
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
-
#
|
|
17
|
+
# CONFIG PATHS - Per spec v1.2 Addendum
|
|
24
18
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
-
$
|
|
19
|
+
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { Join-Path $env:USERPROFILE ".ekkos" }
|
|
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"
|
|
26
23
|
|
|
27
24
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
-
#
|
|
25
|
+
# WORD-BASED SESSION NAMES - Uses external session-words.json
|
|
29
26
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
-
$script:
|
|
27
|
+
$script:ADJECTIVES = @()
|
|
28
|
+
$script:NOUNS = @()
|
|
29
|
+
$script:VERBS = @()
|
|
30
|
+
$script:SESSION_WORDS_LOADED = $false
|
|
31
31
|
|
|
32
32
|
function Load-SessionWords {
|
|
33
|
-
$
|
|
33
|
+
if ($script:SESSION_WORDS_LOADED) { return }
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
$wordsFile = $SessionWordsJson
|
|
36
36
|
if (-not (Test-Path $wordsFile)) {
|
|
37
37
|
$wordsFile = $SessionWordsDefault
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
if (-not (Test-Path $wordsFile)) {
|
|
41
|
-
|
|
40
|
+
if ((-not (Test-Path $wordsFile)) -or (-not (Test-Path $JsonParseHelper))) {
|
|
41
|
+
$script:ADJECTIVES = @("unknown")
|
|
42
|
+
$script:NOUNS = @("session")
|
|
43
|
+
$script:VERBS = @("starts")
|
|
44
|
+
return
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
try {
|
|
45
|
-
$
|
|
48
|
+
$adjRaw = & node $JsonParseHelper $wordsFile ".adjectives" 2>$null
|
|
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
|
|
46
65
|
} catch {
|
|
47
|
-
|
|
66
|
+
$script:ADJECTIVES = @("unknown")
|
|
67
|
+
$script:NOUNS = @("session")
|
|
68
|
+
$script:VERBS = @("starts")
|
|
48
69
|
}
|
|
49
70
|
}
|
|
50
71
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (-not $inputJson) { exit 0 }
|
|
72
|
+
function Convert-UuidToWords {
|
|
73
|
+
param([string]$uuid)
|
|
54
74
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
}
|
|
95
|
+
|
|
96
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
97
|
+
# JSON parsing helper (no jq) - pipes JSON to node, extracts dot-path value
|
|
98
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
99
|
+
function Parse-JsonValue {
|
|
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
|
+
}
|
|
59
120
|
}
|
|
60
121
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
122
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
123
|
+
# Read input from stdin
|
|
124
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
125
|
+
$INPUT = [Console]::In.ReadToEnd()
|
|
126
|
+
if (-not $INPUT) { exit 0 }
|
|
65
127
|
|
|
66
|
-
$
|
|
67
|
-
if (-not $
|
|
128
|
+
$USER_QUERY = Parse-JsonValue $INPUT ".query"
|
|
129
|
+
if (-not $USER_QUERY) { $USER_QUERY = Parse-JsonValue $INPUT ".message" }
|
|
130
|
+
if (-not $USER_QUERY) { $USER_QUERY = Parse-JsonValue $INPUT ".prompt" }
|
|
131
|
+
if (-not $USER_QUERY -or $USER_QUERY -eq "null") { exit 0 }
|
|
68
132
|
|
|
69
|
-
|
|
70
|
-
if ($
|
|
133
|
+
$RAW_SESSION_ID = Parse-JsonValue $INPUT ".session_id"
|
|
134
|
+
if (-not $RAW_SESSION_ID -or $RAW_SESSION_ID -eq "null") { $RAW_SESSION_ID = "unknown" }
|
|
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") {
|
|
71
139
|
$stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
|
|
72
140
|
if (Test-Path $stateFile) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
141
|
+
$savedState = Get-Content $stateFile -Raw
|
|
142
|
+
$savedId = Parse-JsonValue $savedState ".session_id"
|
|
143
|
+
if ($savedId -and $savedId -ne "unknown" -and $savedId -ne "null") {
|
|
144
|
+
$RAW_SESSION_ID = $savedId
|
|
145
|
+
}
|
|
77
146
|
}
|
|
78
147
|
}
|
|
79
148
|
|
|
80
149
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
81
150
|
# INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
|
|
151
|
+
# Detects ALL applicable skills/tools and injects as system reminder
|
|
82
152
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
83
|
-
$
|
|
84
|
-
$
|
|
153
|
+
$SKILL_REMINDERS = @()
|
|
154
|
+
$QUERY_LOWER = $USER_QUERY.ToLower()
|
|
85
155
|
|
|
86
156
|
# Memory First - Debug/Error/Problem solving
|
|
87
|
-
if ($
|
|
88
|
-
$
|
|
157
|
+
if ($QUERY_LOWER -match '(how do i|debug|error|bug|fix|not working|broken|fails|issue|problem|wrong|crash)') {
|
|
158
|
+
$SKILL_REMINDERS += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Memory_First`") FIRST before debugging"
|
|
89
159
|
}
|
|
90
160
|
|
|
91
|
-
# Recall Triggers
|
|
92
|
-
if ($
|
|
93
|
-
$
|
|
161
|
+
# Recall Triggers (Time-based memory)
|
|
162
|
+
if ($QUERY_LOWER -match '(yesterday|last week|last month|remember when|what did we|where did we leave|before|earlier|previous|ago)') {
|
|
163
|
+
$SKILL_REMINDERS += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Deep_Recall`") for time-based memory"
|
|
94
164
|
}
|
|
95
165
|
|
|
96
|
-
# Directive Triggers
|
|
97
|
-
if ($
|
|
98
|
-
$
|
|
166
|
+
# Directive Triggers (User preferences)
|
|
167
|
+
if ($QUERY_LOWER -match '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on)') {
|
|
168
|
+
$SKILL_REMINDERS += "SKILL REQUIRED: Call Skill(skill: `"ekkOS_Preferences`") to capture directive"
|
|
99
169
|
}
|
|
100
170
|
|
|
101
|
-
# Safety Triggers
|
|
102
|
-
if ($
|
|
103
|
-
$
|
|
171
|
+
# Safety Triggers (Destructive actions)
|
|
172
|
+
if ($QUERY_LOWER -match '(delete|drop |rm -rf|deploy|push.*main|push.*master|production|migrate|rollback)') {
|
|
173
|
+
$SKILL_REMINDERS += "SAFETY REQUIRED: Call ekkOS_Conflict before this destructive action"
|
|
104
174
|
}
|
|
105
175
|
|
|
106
|
-
# Schema Triggers
|
|
107
|
-
if ($
|
|
108
|
-
$
|
|
176
|
+
# Schema Triggers (Database operations)
|
|
177
|
+
if ($QUERY_LOWER -match '(sql|query|supabase|prisma|database|table|column|select |insert |update |where )') {
|
|
178
|
+
$SKILL_REMINDERS += "SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names"
|
|
179
|
+
}
|
|
180
|
+
|
|
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"
|
|
109
207
|
}
|
|
110
208
|
|
|
111
209
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
112
|
-
#
|
|
210
|
+
# Load auth
|
|
113
211
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
-
|
|
115
|
-
|
|
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
|
+
}
|
|
116
234
|
|
|
117
|
-
|
|
118
|
-
if (-not $script:SessionWords) { return "unknown-session" }
|
|
235
|
+
$MEMORY_API_URL = "https://mcp.ekkos.dev"
|
|
119
236
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
237
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
238
|
+
# Session ID - NEW ID per conversation (not persisted 24h anymore)
|
|
239
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
240
|
+
$StateDir = Join-Path $ProjectRoot ".claude\state"
|
|
241
|
+
if (-not (Test-Path $StateDir)) {
|
|
242
|
+
New-Item -ItemType Directory -Path $StateDir -Force | Out-Null
|
|
243
|
+
}
|
|
244
|
+
$SESSION_FILE = Join-Path $StateDir "current-session.json"
|
|
245
|
+
|
|
246
|
+
$SESSION_ID = $RAW_SESSION_ID
|
|
247
|
+
|
|
248
|
+
# Skip if no valid session ID from Claude
|
|
249
|
+
if (-not $SESSION_ID -or $SESSION_ID -eq "unknown" -or $SESSION_ID -eq "null") {
|
|
250
|
+
exit 0
|
|
251
|
+
}
|
|
123
252
|
|
|
124
|
-
|
|
125
|
-
|
|
253
|
+
# Save for other hooks to reference
|
|
254
|
+
$timestampUtc = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
255
|
+
Set-Content -Path $SESSION_FILE -Value "{`"session_id`": `"$SESSION_ID`", `"timestamp`": `"$timestampUtc`"}" -Force
|
|
126
256
|
|
|
127
|
-
|
|
128
|
-
|
|
257
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
258
|
+
# Turn counter - PROJECT-LOCAL storage
|
|
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"
|
|
129
265
|
|
|
266
|
+
# Count actual user messages in transcript for accurate turn number
|
|
267
|
+
$TURN_NUMBER = 1
|
|
268
|
+
if ($TRANSCRIPT_PATH -and (Test-Path $TRANSCRIPT_PATH)) {
|
|
130
269
|
try {
|
|
131
|
-
$
|
|
132
|
-
$
|
|
133
|
-
|
|
134
|
-
return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
|
|
270
|
+
$transcriptContent = Get-Content $TRANSCRIPT_PATH -Raw
|
|
271
|
+
$TURN_NUMBER = ([regex]::Matches($transcriptContent, '"type":"user"')).Count
|
|
272
|
+
if ($TURN_NUMBER -eq 0) { $TURN_NUMBER = 1 }
|
|
135
273
|
} catch {
|
|
136
|
-
|
|
274
|
+
$TURN_NUMBER = 1
|
|
137
275
|
}
|
|
138
276
|
}
|
|
139
277
|
|
|
140
|
-
|
|
278
|
+
# PRESERVE HISTORY: Don't overwrite if saved count is higher (after /clear)
|
|
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
|
|
141
297
|
|
|
142
298
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
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.
|
|
299
|
+
# WORKING MEMORY: Fast capture each turn (async, non-blocking)
|
|
146
300
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
147
|
-
if (
|
|
148
|
-
$
|
|
149
|
-
if (Test-Path $
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
$
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
172
|
-
} catch {}
|
|
301
|
+
if (Test-Path $EKKOS_CONFIG) {
|
|
302
|
+
$CAPTURE_TOKEN = ""
|
|
303
|
+
if (Test-Path $JsonParseHelper) {
|
|
304
|
+
$CAPTURE_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
|
|
305
|
+
if (-not $CAPTURE_TOKEN) {
|
|
306
|
+
$CAPTURE_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".apiKey" 2>$null
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if ($CAPTURE_TOKEN -and $CAPTURE_TOKEN -ne "null") {
|
|
311
|
+
# Async capture to Redis/Supabase - doesn't block hook execution
|
|
312
|
+
$queryEscaped = $USER_QUERY -replace '"', '\"' -replace '\\', '\\\\'
|
|
313
|
+
$captureBody = "{`"session_id`":`"$RAW_SESSION_ID`",`"turn`":$TURN_NUMBER,`"query`":`"$queryEscaped`"}"
|
|
314
|
+
Start-Job -ScriptBlock {
|
|
315
|
+
param($url, $token, $body)
|
|
316
|
+
try {
|
|
317
|
+
Invoke-RestMethod -Uri "$url/api/v1/working/fast-capture" `
|
|
318
|
+
-Method POST `
|
|
319
|
+
-Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
|
|
320
|
+
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
|
|
321
|
+
-TimeoutSec 3 `
|
|
322
|
+
-ErrorAction SilentlyContinue | Out-Null
|
|
323
|
+
} catch {}
|
|
324
|
+
} -ArgumentList $MEMORY_API_URL, $CAPTURE_TOKEN, $captureBody | Out-Null
|
|
173
325
|
}
|
|
174
326
|
}
|
|
175
327
|
|
|
176
328
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
177
|
-
#
|
|
329
|
+
# LOCAL CACHE: Tier 0 capture for instant /continue (async, non-blocking)
|
|
178
330
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
331
|
+
$ekkosCapturePath = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
|
|
332
|
+
if ($ekkosCapturePath) {
|
|
333
|
+
$SESSION_NAME_FOR_CAPTURE = Convert-UuidToWords $RAW_SESSION_ID
|
|
334
|
+
$projectRootNorm = $ProjectRoot -replace '\\', '/'
|
|
335
|
+
Start-Job -ScriptBlock {
|
|
336
|
+
param($sessionId, $sessionName, $turnNum, $query, $projRoot)
|
|
182
337
|
try {
|
|
183
|
-
$
|
|
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
|
-
}
|
|
338
|
+
& ekkos-capture user $sessionId $sessionName $turnNum $query $projRoot 2>$null | Out-Null
|
|
196
339
|
} catch {}
|
|
197
|
-
}
|
|
340
|
+
} -ArgumentList $RAW_SESSION_ID, $SESSION_NAME_FOR_CAPTURE, $TURN_NUMBER, $USER_QUERY, $projectRootNorm | Out-Null
|
|
198
341
|
}
|
|
199
342
|
|
|
200
343
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
201
|
-
#
|
|
344
|
+
# GOLDEN LOOP: CAPTURE PHASE - Track turn start
|
|
202
345
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
203
|
-
$
|
|
204
|
-
if (-not (Test-Path $
|
|
205
|
-
New-Item -ItemType Directory -Path $
|
|
346
|
+
$EkkosDir = Join-Path $ProjectRoot ".ekkos"
|
|
347
|
+
if (-not (Test-Path $EkkosDir)) {
|
|
348
|
+
New-Item -ItemType Directory -Path $EkkosDir -Force | Out-Null
|
|
206
349
|
}
|
|
350
|
+
$GOLDEN_LOOP_FILE = Join-Path $EkkosDir "golden-loop-current.json"
|
|
207
351
|
|
|
208
|
-
|
|
209
|
-
$
|
|
210
|
-
|
|
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 {}
|
|
211
367
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
$RETRIEVED_PATTERNS = ""
|
|
380
|
+
$PATTERN_COUNT = 0
|
|
381
|
+
$RETRIEVED_DIRECTIVES = ""
|
|
382
|
+
$DIRECTIVE_COUNT = 0
|
|
383
|
+
|
|
384
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
385
|
+
# DIRECTIVE CACHE: Local cache to avoid API calls every turn
|
|
386
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
387
|
+
$DirectiveCacheDir = Join-Path $env:USERPROFILE ".ekkos\cache"
|
|
388
|
+
$DirectiveCacheFile = Join-Path $DirectiveCacheDir "directives.json"
|
|
389
|
+
$DIRECTIVE_CACHE_TTL = 3600 # 1 hour in seconds
|
|
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
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (Test-Path $DirectiveCacheFile) {
|
|
403
|
+
$cacheContent = Get-Content $DirectiveCacheFile -Raw
|
|
404
|
+
$cacheTimestampStr = Parse-JsonValue $cacheContent ".cached_at"
|
|
405
|
+
if ($cacheTimestampStr -match '^\d+$') {
|
|
406
|
+
$cacheTimestamp = [int64]$cacheTimestampStr
|
|
407
|
+
$currentTimestamp = [int64]([DateTimeOffset]::UtcNow.ToUnixTimeSeconds())
|
|
408
|
+
$cacheAge = $currentTimestamp - $cacheTimestamp
|
|
409
|
+
|
|
410
|
+
if ($cacheAge -lt $DIRECTIVE_CACHE_TTL -and -not $DIRECTIVE_TRIGGER_DETECTED) {
|
|
411
|
+
$DIRECTIVE_CACHE_VALID = $true
|
|
221
412
|
}
|
|
222
|
-
} catch {
|
|
223
|
-
$turn = 0
|
|
224
413
|
}
|
|
225
414
|
}
|
|
226
415
|
|
|
227
|
-
#
|
|
228
|
-
$
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
timestamp = (Get-Date).ToString("o")
|
|
233
|
-
} | ConvertTo-Json -Depth 10
|
|
416
|
+
# Decide whether to inject directives this turn
|
|
417
|
+
$SHOULD_INJECT_DIRECTIVES = $false
|
|
418
|
+
if ($TURN_NUMBER -eq 1 -or $POST_CLEAR_DETECTED -or $DIRECTIVE_TRIGGER_DETECTED) {
|
|
419
|
+
$SHOULD_INJECT_DIRECTIVES = $true
|
|
420
|
+
}
|
|
234
421
|
|
|
235
|
-
|
|
422
|
+
if ($EKKOS_API_KEY -and $USER_QUERY) {
|
|
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 {}
|
|
236
437
|
|
|
237
|
-
#
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
$
|
|
242
|
-
|
|
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
|
+
}
|
|
444
|
+
|
|
445
|
+
# Escape user query for JSON
|
|
446
|
+
$queryJsonEscaped = $USER_QUERY -replace '\\', '\\\\' -replace '"', '\"' -replace "`n", '\n' -replace "`r", '' -replace "`t", '\t'
|
|
447
|
+
|
|
448
|
+
# Call ekkOS MCP gateway
|
|
449
|
+
$SEARCH_RESPONSE_RAW = $null
|
|
243
450
|
try {
|
|
244
|
-
|
|
245
|
-
$
|
|
246
|
-
|
|
451
|
+
$searchBody = "{`"tool`": `"ekkOS_Search`", `"arguments`": {`"query`": `"$queryJsonEscaped`", `"limit`": 5, `"sources`": $searchSources}}"
|
|
452
|
+
$SEARCH_RESPONSE_RAW = Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/mcp/call" `
|
|
453
|
+
-Method POST `
|
|
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
|
+
}
|
|
247
461
|
|
|
248
|
-
|
|
249
|
-
|
|
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
|
+
}
|
|
474
|
+
|
|
475
|
+
# Count patterns retrieved
|
|
476
|
+
$patternCountStr = $SEARCH_JSON | node -e "
|
|
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) {
|
|
250
487
|
try {
|
|
251
|
-
|
|
252
|
-
|
|
488
|
+
node -e "
|
|
489
|
+
const fs = require('fs');
|
|
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
|
|
253
499
|
} catch {}
|
|
254
|
-
} -ArgumentList $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $queryBase64, $projectRoot | Out-Null
|
|
255
|
-
} catch {}
|
|
256
|
-
}
|
|
257
500
|
|
|
258
|
-
#
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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
|
|
501
|
+
# Format patterns for injection
|
|
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
|
|
286
517
|
}
|
|
287
|
-
|
|
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
|
+
}
|
|
288
547
|
}
|
|
289
548
|
|
|
290
|
-
|
|
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
|
+
}
|
|
567
|
+
}
|
|
291
568
|
|
|
292
569
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
293
|
-
#
|
|
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.
|
|
570
|
+
# COLORS (ANSI escape sequences via [char]27)
|
|
296
571
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
572
|
+
$ESC = [char]27
|
|
573
|
+
$CYAN = "$ESC[0;36m"
|
|
574
|
+
$GREEN = "$ESC[0;32m"
|
|
575
|
+
$YELLOW = "$ESC[1;33m"
|
|
576
|
+
$MAGENTA = "$ESC[0;35m"
|
|
577
|
+
$DIM = "$ESC[2m"
|
|
578
|
+
$BOLD = "$ESC[1m"
|
|
579
|
+
$RESET = "$ESC[0m"
|
|
580
|
+
|
|
581
|
+
$CURRENT_TIME = Get-Date -Format "yyyy-MM-dd hh:mm:ss tt K"
|
|
582
|
+
|
|
583
|
+
# Generate session name
|
|
584
|
+
$SESSION_NAME = ""
|
|
585
|
+
if ($SESSION_ID -and $SESSION_ID -ne "unknown" -and $SESSION_ID -ne "null") {
|
|
586
|
+
$SESSION_NAME = Convert-UuidToWords $SESSION_ID
|
|
309
587
|
}
|
|
310
588
|
|
|
311
589
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
312
|
-
#
|
|
590
|
+
# "/continue" COMMAND: Delegated to Skill system (DO NOT INTERCEPT)
|
|
313
591
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
314
|
-
|
|
315
|
-
|
|
592
|
+
# REMOVED: Hook used to intercept /continue and do simple restoration
|
|
593
|
+
# NOW: Let /continue Skill handle it - supports session names + intelligent narrative
|
|
316
594
|
|
|
317
|
-
|
|
318
|
-
|
|
595
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
596
|
+
# AUTO-RESTORE REMOVED: Manual /continue only (saves 79% token burn!)
|
|
597
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
319
598
|
|
|
320
|
-
|
|
599
|
+
# Simple status line - no context warnings, Claude handles its own context
|
|
600
|
+
Write-Output "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
321
601
|
|
|
322
|
-
|
|
323
|
-
|
|
602
|
+
# Output skill reminder if detected
|
|
603
|
+
if ($SKILL_REMINDER) {
|
|
604
|
+
Write-Output ""
|
|
605
|
+
Write-Output "${MAGENTA}${BOLD}${SKILL_REMINDER}${RESET}"
|
|
324
606
|
}
|
|
325
607
|
|
|
326
|
-
|
|
608
|
+
# GOLDEN LOOP: INJECT PHASE - Inject directives FIRST (highest priority)
|
|
609
|
+
# SMART INJECTION: Only on Turn 1, post-clear, or directive trigger
|
|
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>"
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
# GOLDEN LOOP: INJECT PHASE - Inject retrieved patterns into context
|
|
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
|
+
}
|
|
327
649
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
"
|
|
650
|
+
# Inject footer format reminder (helps Claude remember session name)
|
|
651
|
+
if ($SESSION_NAME -and $SESSION_NAME -ne "unknown-session") {
|
|
652
|
+
Write-Output ""
|
|
653
|
+
Write-Output "<footer-format>End responses with: Claude Code ({Model}) · ekkOS_ · Turn ${TURN_NUMBER} · ${SESSION_NAME} · ${CURRENT_TIME}</footer-format>"
|
|
654
|
+
}
|
|
331
655
|
|
|
332
|
-
|
|
656
|
+
exit 0
|