@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
|
@@ -2,117 +2,66 @@
|
|
|
2
2
|
# ekkOS_ Hook: AssistantResponse - Process Claude's response (Windows)
|
|
3
3
|
# MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
|
|
4
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
|
|
11
5
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
12
6
|
|
|
13
7
|
$ErrorActionPreference = "SilentlyContinue"
|
|
14
8
|
|
|
15
9
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
# CONFIG PATHS
|
|
10
|
+
# CONFIG PATHS
|
|
17
11
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
18
12
|
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
|
|
19
13
|
$HooksEnabledJson = "$EkkosConfigDir\hooks-enabled.json"
|
|
20
14
|
$HooksEnabledDefault = "$EkkosConfigDir\.defaults\hooks-enabled.json"
|
|
21
15
|
$SessionWordsJson = "$EkkosConfigDir\session-words.json"
|
|
22
16
|
$SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
|
|
23
|
-
|
|
24
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
-
# INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
|
|
26
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
27
17
|
$EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
|
|
28
18
|
|
|
19
|
+
$MemoryApiUrl = "https://api.ekkos.dev"
|
|
20
|
+
|
|
29
21
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
-
# CHECK ENABLEMENT
|
|
22
|
+
# CHECK ENABLEMENT
|
|
31
23
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
32
24
|
function Test-HookEnabled {
|
|
33
25
|
param([string]$HookName)
|
|
34
|
-
|
|
35
26
|
$enabledFile = $HooksEnabledJson
|
|
36
|
-
if (-not (Test-Path $enabledFile)) {
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (-not (Test-Path $enabledFile)) {
|
|
41
|
-
# No enablement file = all enabled by default
|
|
42
|
-
return $true
|
|
43
|
-
}
|
|
44
|
-
|
|
27
|
+
if (-not (Test-Path $enabledFile)) { $enabledFile = $HooksEnabledDefault }
|
|
28
|
+
if (-not (Test-Path $enabledFile)) { return $true }
|
|
45
29
|
try {
|
|
46
30
|
$config = Get-Content $enabledFile -Raw | ConvertFrom-Json
|
|
47
|
-
|
|
48
|
-
# Check claude.enabled array
|
|
49
31
|
if ($config.claude -and $config.claude.enabled) {
|
|
50
32
|
$enabledHooks = $config.claude.enabled
|
|
51
|
-
if ($enabledHooks -contains $HookName -or $enabledHooks -contains "assistant-response") {
|
|
52
|
-
return $true
|
|
53
|
-
}
|
|
33
|
+
if ($enabledHooks -contains $HookName -or $enabledHooks -contains "assistant-response") { return $true }
|
|
54
34
|
return $false
|
|
55
35
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
} catch {
|
|
59
|
-
return $true # Default to enabled on error
|
|
60
|
-
}
|
|
36
|
+
return $true
|
|
37
|
+
} catch { return $true }
|
|
61
38
|
}
|
|
62
39
|
|
|
63
|
-
|
|
64
|
-
if (-not (Test-HookEnabled "assistant-response")) {
|
|
65
|
-
exit 0
|
|
66
|
-
}
|
|
40
|
+
if (-not (Test-HookEnabled "assistant-response")) { exit 0 }
|
|
67
41
|
|
|
68
42
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
69
|
-
# Load session words
|
|
43
|
+
# Load session words - NO HARDCODED ARRAYS
|
|
70
44
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
71
45
|
$script:SessionWords = $null
|
|
72
46
|
|
|
73
47
|
function Load-SessionWords {
|
|
74
48
|
$wordsFile = $SessionWordsJson
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if (-not (Test-Path $wordsFile)) {
|
|
78
|
-
$wordsFile = $SessionWordsDefault
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
if (-not (Test-Path $wordsFile)) {
|
|
82
|
-
return $null
|
|
83
|
-
}
|
|
84
|
-
|
|
49
|
+
if (-not (Test-Path $wordsFile)) { $wordsFile = $SessionWordsDefault }
|
|
50
|
+
if (-not (Test-Path $wordsFile)) { return $null }
|
|
85
51
|
try {
|
|
86
52
|
$script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
|
|
87
|
-
} catch {
|
|
88
|
-
return $null
|
|
89
|
-
}
|
|
53
|
+
} catch { return $null }
|
|
90
54
|
}
|
|
91
55
|
|
|
92
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
93
|
-
# SESSION NAME (UUID to words) - Uses external session-words.json
|
|
94
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
95
56
|
function Convert-UuidToWords {
|
|
96
57
|
param([string]$uuid)
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (-not $script:SessionWords) {
|
|
100
|
-
Load-SessionWords
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
# Handle missing session words gracefully
|
|
104
|
-
if (-not $script:SessionWords) {
|
|
105
|
-
return "unknown-session"
|
|
106
|
-
}
|
|
58
|
+
if (-not $script:SessionWords) { Load-SessionWords }
|
|
59
|
+
if (-not $script:SessionWords) { return "unknown-session" }
|
|
107
60
|
|
|
108
61
|
$adjectives = $script:SessionWords.adjectives
|
|
109
62
|
$nouns = $script:SessionWords.nouns
|
|
110
63
|
$verbs = $script:SessionWords.verbs
|
|
111
|
-
|
|
112
|
-
if (-not $adjectives -or -not $nouns -or -not $verbs) {
|
|
113
|
-
return "unknown-session"
|
|
114
|
-
}
|
|
115
|
-
|
|
64
|
+
if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
|
|
116
65
|
if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
|
|
117
66
|
|
|
118
67
|
$clean = $uuid -replace "-", ""
|
|
@@ -122,11 +71,8 @@ function Convert-UuidToWords {
|
|
|
122
71
|
$a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
|
|
123
72
|
$n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
|
|
124
73
|
$an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
|
|
125
|
-
|
|
126
74
|
return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
|
|
127
|
-
} catch {
|
|
128
|
-
return "unknown-session"
|
|
129
|
-
}
|
|
75
|
+
} catch { return "unknown-session" }
|
|
130
76
|
}
|
|
131
77
|
|
|
132
78
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -135,23 +81,16 @@ function Convert-UuidToWords {
|
|
|
135
81
|
$inputJson = [Console]::In.ReadToEnd()
|
|
136
82
|
if (-not $inputJson) { exit 0 }
|
|
137
83
|
|
|
138
|
-
try {
|
|
139
|
-
$input = $inputJson | ConvertFrom-Json
|
|
140
|
-
} catch {
|
|
141
|
-
exit 0
|
|
142
|
-
}
|
|
84
|
+
try { $input = $inputJson | ConvertFrom-Json } catch { exit 0 }
|
|
143
85
|
|
|
144
|
-
# Extract response content
|
|
145
86
|
$assistantResponse = $input.response
|
|
146
87
|
if (-not $assistantResponse) { $assistantResponse = $input.message }
|
|
147
88
|
if (-not $assistantResponse) { $assistantResponse = $input.content }
|
|
148
89
|
if (-not $assistantResponse) { exit 0 }
|
|
149
90
|
|
|
150
|
-
# Get session ID
|
|
151
91
|
$rawSessionId = $input.session_id
|
|
152
92
|
if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
|
|
153
93
|
|
|
154
|
-
# Fallback: read session_id from saved state
|
|
155
94
|
if ($rawSessionId -eq "unknown") {
|
|
156
95
|
$stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
|
|
157
96
|
if (Test-Path $stateFile) {
|
|
@@ -167,16 +106,13 @@ $sessionName = Convert-UuidToWords $rawSessionId
|
|
|
167
106
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
168
107
|
# READ TURN STATE
|
|
169
108
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
170
|
-
$
|
|
109
|
+
$hookStateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
|
|
171
110
|
$turn = 0
|
|
172
|
-
|
|
173
|
-
if (Test-Path $stateFile) {
|
|
111
|
+
if (Test-Path $hookStateFile) {
|
|
174
112
|
try {
|
|
175
|
-
$hookState = Get-Content $
|
|
113
|
+
$hookState = Get-Content $hookStateFile -Raw | ConvertFrom-Json
|
|
176
114
|
$turn = [int]$hookState.turn
|
|
177
|
-
} catch {
|
|
178
|
-
$turn = 0
|
|
179
|
-
}
|
|
115
|
+
} catch { $turn = 0 }
|
|
180
116
|
}
|
|
181
117
|
|
|
182
118
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -184,7 +120,6 @@ if (Test-Path $stateFile) {
|
|
|
184
120
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
185
121
|
$patternIds = @()
|
|
186
122
|
if ($assistantResponse -match '\[ekkOS_SELECT\]') {
|
|
187
|
-
# Extract pattern IDs from SELECT blocks
|
|
188
123
|
$selectMatches = [regex]::Matches($assistantResponse, 'id:\s*([a-zA-Z0-9\-_]+)')
|
|
189
124
|
foreach ($match in $selectMatches) {
|
|
190
125
|
$patternIds += $match.Groups[1].Value
|
|
@@ -192,18 +127,15 @@ if ($assistantResponse -match '\[ekkOS_SELECT\]') {
|
|
|
192
127
|
}
|
|
193
128
|
|
|
194
129
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
195
|
-
# LOCAL CACHE: Tier 0 capture (async
|
|
196
|
-
# Per v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
130
|
+
# LOCAL CACHE: Tier 0 capture (async)
|
|
197
131
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
198
132
|
$captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
|
|
199
133
|
if ($captureCmd -and $rawSessionId -ne "unknown") {
|
|
200
134
|
try {
|
|
201
|
-
# NEW format: ekkos-capture response <instance_id> <session_id> <turn_id> <response> [tools] [files]
|
|
202
135
|
$responseBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($assistantResponse))
|
|
203
136
|
$toolsJson = "[]"
|
|
204
137
|
$filesJson = "[]"
|
|
205
138
|
|
|
206
|
-
# Extract tools used from response
|
|
207
139
|
$toolMatches = [regex]::Matches($assistantResponse, '\[TOOL:\s*([^\]]+)\]')
|
|
208
140
|
if ($toolMatches.Count -gt 0) {
|
|
209
141
|
$tools = $toolMatches | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique
|
|
@@ -221,7 +153,7 @@ if ($captureCmd -and $rawSessionId -ne "unknown") {
|
|
|
221
153
|
}
|
|
222
154
|
|
|
223
155
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
224
|
-
# WORKING MEMORY: Fast capture to API (async
|
|
156
|
+
# WORKING MEMORY: Fast capture to API (async)
|
|
225
157
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
226
158
|
$configFile = Join-Path $EkkosConfigDir "config.json"
|
|
227
159
|
if (Test-Path $configFile) {
|
|
@@ -242,7 +174,7 @@ if (Test-Path $configFile) {
|
|
|
242
174
|
pattern_ids = $patterns
|
|
243
175
|
} | ConvertTo-Json -Depth 10
|
|
244
176
|
|
|
245
|
-
Invoke-RestMethod -Uri "https://
|
|
177
|
+
Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/working/turn" `
|
|
246
178
|
-Method POST `
|
|
247
179
|
-Headers @{ Authorization = "Bearer $token" } `
|
|
248
180
|
-ContentType "application/json" `
|
|
File without changes
|
|
File without changes
|
|
@@ -1,29 +1,28 @@
|
|
|
1
1
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
# ekkOS_ Hook: SessionStart -
|
|
2
|
+
# ekkOS_ Hook: SessionStart - MINIMAL + TIME MACHINE + DIRECTIVES (Windows)
|
|
3
3
|
# MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
|
|
4
4
|
# EKKOS_MANAGED=1
|
|
5
|
-
#
|
|
6
|
-
#
|
|
5
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
6
|
+
# This hook does THREE things:
|
|
7
|
+
# 1. Check for pending Time Machine "Continue from here" requests
|
|
8
|
+
# 2. Initialize session tracking + Golden Loop
|
|
9
|
+
# 3. Fetch and inject user directives (MUST/NEVER/PREFER/AVOID)
|
|
7
10
|
#
|
|
8
|
-
# Per
|
|
9
|
-
# - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
10
|
-
# - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
|
|
11
|
+
# Per spec v1.2 Addendum: NO hardcoded arrays, uses session-words.json
|
|
11
12
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
12
13
|
|
|
13
14
|
$ErrorActionPreference = "SilentlyContinue"
|
|
14
15
|
|
|
15
16
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
# CONFIG PATHS
|
|
17
|
+
# CONFIG PATHS
|
|
17
18
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
18
19
|
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
|
|
19
20
|
$SessionWordsJson = "$EkkosConfigDir\session-words.json"
|
|
20
21
|
$SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
|
|
21
|
-
|
|
22
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
-
# INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
|
|
24
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
25
22
|
$EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
|
|
26
23
|
|
|
24
|
+
$MemoryApiUrl = "https://api.ekkos.dev"
|
|
25
|
+
|
|
27
26
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
28
27
|
# Load session words from JSON file - NO HARDCODED ARRAYS
|
|
29
28
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -31,41 +30,22 @@ $script:SessionWords = $null
|
|
|
31
30
|
|
|
32
31
|
function Load-SessionWords {
|
|
33
32
|
$wordsFile = $SessionWordsJson
|
|
34
|
-
|
|
35
|
-
if (-not (Test-Path $wordsFile)) {
|
|
36
|
-
$wordsFile = $SessionWordsDefault
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (-not (Test-Path $wordsFile)) {
|
|
40
|
-
return $null
|
|
41
|
-
}
|
|
42
|
-
|
|
33
|
+
if (-not (Test-Path $wordsFile)) { $wordsFile = $SessionWordsDefault }
|
|
34
|
+
if (-not (Test-Path $wordsFile)) { return $null }
|
|
43
35
|
try {
|
|
44
36
|
$script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
|
|
45
|
-
} catch {
|
|
46
|
-
return $null
|
|
47
|
-
}
|
|
37
|
+
} catch { return $null }
|
|
48
38
|
}
|
|
49
39
|
|
|
50
40
|
function Convert-UuidToWords {
|
|
51
41
|
param([string]$uuid)
|
|
52
|
-
|
|
53
|
-
if (-not $script:SessionWords) {
|
|
54
|
-
Load-SessionWords
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (-not $script:SessionWords) {
|
|
58
|
-
return "unknown-session"
|
|
59
|
-
}
|
|
42
|
+
if (-not $script:SessionWords) { Load-SessionWords }
|
|
43
|
+
if (-not $script:SessionWords) { return "unknown-session" }
|
|
60
44
|
|
|
61
45
|
$adjectives = $script:SessionWords.adjectives
|
|
62
46
|
$nouns = $script:SessionWords.nouns
|
|
63
47
|
$verbs = $script:SessionWords.verbs
|
|
64
|
-
|
|
65
|
-
if (-not $adjectives -or -not $nouns -or -not $verbs) {
|
|
66
|
-
return "unknown-session"
|
|
67
|
-
}
|
|
68
|
-
|
|
48
|
+
if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
|
|
69
49
|
if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
|
|
70
50
|
|
|
71
51
|
$clean = $uuid -replace "-", ""
|
|
@@ -75,36 +55,116 @@ function Convert-UuidToWords {
|
|
|
75
55
|
$a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
|
|
76
56
|
$n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
|
|
77
57
|
$an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
|
|
78
|
-
|
|
79
58
|
return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
|
|
80
|
-
} catch {
|
|
81
|
-
return "unknown-session"
|
|
82
|
-
}
|
|
59
|
+
} catch { return "unknown-session" }
|
|
83
60
|
}
|
|
84
61
|
|
|
85
62
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
86
63
|
# READ INPUT
|
|
87
64
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
88
65
|
$inputJson = [Console]::In.ReadToEnd()
|
|
89
|
-
|
|
90
66
|
try {
|
|
91
67
|
$input = $inputJson | ConvertFrom-Json
|
|
92
68
|
$sessionId = $input.session_id
|
|
93
69
|
} catch {
|
|
94
70
|
$sessionId = "unknown"
|
|
95
71
|
}
|
|
72
|
+
if (-not $sessionId) { $sessionId = "unknown" }
|
|
96
73
|
|
|
97
74
|
$sessionName = Convert-UuidToWords $sessionId
|
|
98
75
|
|
|
99
76
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
-
#
|
|
77
|
+
# LOAD AUTH
|
|
78
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
79
|
+
$authToken = ""
|
|
80
|
+
$userId = ""
|
|
81
|
+
$configFile = Join-Path $EkkosConfigDir "config.json"
|
|
82
|
+
if (Test-Path $configFile) {
|
|
83
|
+
try {
|
|
84
|
+
$config = Get-Content $configFile -Raw | ConvertFrom-Json
|
|
85
|
+
$authToken = $config.hookApiKey
|
|
86
|
+
if (-not $authToken) { $authToken = $config.apiKey }
|
|
87
|
+
$userId = $config.userId
|
|
88
|
+
} catch {}
|
|
89
|
+
}
|
|
90
|
+
if (-not $authToken) { exit 0 }
|
|
91
|
+
|
|
92
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
93
|
+
# TIME MACHINE: Check for pending "Continue from here" requests
|
|
101
94
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
95
|
+
$timeMachineSession = ""
|
|
96
|
+
$timeMachineFromTurn = ""
|
|
97
|
+
$timeMachineToTurn = ""
|
|
98
|
+
$restoreRequestId = $env:EKKOS_RESTORE
|
|
99
|
+
|
|
100
|
+
if (-not $timeMachineSession -and $userId) {
|
|
101
|
+
try {
|
|
102
|
+
$headers = @{ "Authorization" = "Bearer $authToken" }
|
|
103
|
+
$pendingResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/context/restore-request/pending?user_id=$userId" `
|
|
104
|
+
-Method GET -Headers $headers -TimeoutSec 3
|
|
105
|
+
|
|
106
|
+
if ($pendingResponse.pending -eq $true -and $pendingResponse.request) {
|
|
107
|
+
$timeMachineSession = $pendingResponse.request.session_id
|
|
108
|
+
$timeMachineFromTurn = $pendingResponse.request.from_turn
|
|
109
|
+
$timeMachineToTurn = $pendingResponse.request.to_turn
|
|
110
|
+
$restoreRequestId = $pendingResponse.request.request_id
|
|
111
|
+
|
|
112
|
+
if ($timeMachineSession) {
|
|
113
|
+
$esc = [char]27
|
|
114
|
+
[Console]::Error.WriteLine("")
|
|
115
|
+
[Console]::Error.WriteLine("${esc}[0;35m------------------------------------------------------------------------${esc}[0m")
|
|
116
|
+
[Console]::Error.WriteLine("${esc}[0;35m${esc}[1m TIME MACHINE${esc}[0m ${esc}[2m| Restoring session from web request...${esc}[0m")
|
|
117
|
+
[Console]::Error.WriteLine("${esc}[0;35m------------------------------------------------------------------------${esc}[0m")
|
|
118
|
+
|
|
119
|
+
$consumeBody = @{ request_id = $restoreRequestId } | ConvertTo-Json -Depth 10
|
|
120
|
+
try {
|
|
121
|
+
Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/context/restore-request/consume" `
|
|
122
|
+
-Method POST -Headers @{ "Authorization" = "Bearer $authToken"; "Content-Type" = "application/json" } `
|
|
123
|
+
-Body ([System.Text.Encoding]::UTF8.GetBytes($consumeBody)) -TimeoutSec 3 | Out-Null
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} catch {}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
131
|
+
# SESSION PERSISTENCE - PROJECT-LOCAL for isolation
|
|
132
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
133
|
+
$projectRoot = if ($env:PWD) { $env:PWD } else { (Get-Location).Path }
|
|
102
134
|
$stateDir = Join-Path $env:USERPROFILE ".claude\state"
|
|
103
|
-
|
|
104
|
-
|
|
135
|
+
$projectSessionDir = Join-Path $stateDir "sessions"
|
|
136
|
+
|
|
137
|
+
if (-not (Test-Path $stateDir)) { New-Item -ItemType Directory -Path $stateDir -Force | Out-Null }
|
|
138
|
+
if (-not (Test-Path $projectSessionDir)) { New-Item -ItemType Directory -Path $projectSessionDir -Force | Out-Null }
|
|
139
|
+
|
|
140
|
+
$savedTurnCount = 0
|
|
141
|
+
$mostRecentSession = ""
|
|
142
|
+
|
|
143
|
+
if ($sessionId -ne "unknown") {
|
|
144
|
+
$turnFile = Join-Path $projectSessionDir "$sessionId.turn"
|
|
145
|
+
if (Test-Path $turnFile) {
|
|
146
|
+
try { $savedTurnCount = [int](Get-Content $turnFile -Raw).Trim() } catch { $savedTurnCount = 0 }
|
|
147
|
+
$mostRecentSession = $sessionId
|
|
148
|
+
} else {
|
|
149
|
+
$mostRecentFile = Get-ChildItem "$projectSessionDir\*.turn" -ErrorAction SilentlyContinue |
|
|
150
|
+
Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
|
151
|
+
if ($mostRecentFile) {
|
|
152
|
+
$mostRecentSession = $mostRecentFile.BaseName
|
|
153
|
+
try { $savedTurnCount = [int](Get-Content $mostRecentFile.FullName -Raw).Trim() } catch { $savedTurnCount = 0 }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
105
156
|
}
|
|
106
157
|
|
|
107
|
-
|
|
158
|
+
$sessionFile = Join-Path $stateDir "current-session.json"
|
|
159
|
+
$sessionData = @{
|
|
160
|
+
session_id = $sessionId
|
|
161
|
+
session_name = $sessionName
|
|
162
|
+
instance_id = $EkkosInstanceId
|
|
163
|
+
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
164
|
+
project_root = $projectRoot
|
|
165
|
+
} | ConvertTo-Json -Depth 10
|
|
166
|
+
Set-Content -Path $sessionFile -Value $sessionData -Force
|
|
167
|
+
|
|
108
168
|
$stateFile = Join-Path $stateDir "hook-state.json"
|
|
109
169
|
$state = @{
|
|
110
170
|
turn = 0
|
|
@@ -113,34 +173,137 @@ $state = @{
|
|
|
113
173
|
instance_id = $EkkosInstanceId
|
|
114
174
|
started_at = (Get-Date).ToString("o")
|
|
115
175
|
} | ConvertTo-Json -Depth 10
|
|
116
|
-
|
|
117
176
|
Set-Content -Path $stateFile -Value $state -Force
|
|
118
177
|
|
|
119
|
-
#
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
instance_id = $EkkosInstanceId
|
|
125
|
-
} | ConvertTo-Json -Depth 10
|
|
178
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
179
|
+
# GOLDEN LOOP: Initialize session tracking file
|
|
180
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
181
|
+
$ekkosDir = Join-Path $projectRoot ".ekkos"
|
|
182
|
+
if (-not (Test-Path $ekkosDir)) { New-Item -ItemType Directory -Path $ekkosDir -Force | Out-Null }
|
|
126
183
|
|
|
127
|
-
|
|
184
|
+
$goldenLoopFile = Join-Path $ekkosDir "golden-loop-current.json"
|
|
185
|
+
$goldenLoop = @{
|
|
186
|
+
phase = "idle"
|
|
187
|
+
turn = 0
|
|
188
|
+
session = $sessionId
|
|
189
|
+
timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
190
|
+
stats = @{ retrieved = 0; applied = 0; forged = 0 }
|
|
191
|
+
} | ConvertTo-Json -Depth 10
|
|
192
|
+
Set-Content -Path $goldenLoopFile -Value $goldenLoop -Force
|
|
128
193
|
|
|
129
194
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
130
|
-
# LOCAL CACHE: Initialize session
|
|
131
|
-
# Per v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
195
|
+
# LOCAL CACHE: Initialize session
|
|
132
196
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
133
197
|
$captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
|
|
134
198
|
if ($captureCmd -and $sessionId -ne "unknown") {
|
|
135
199
|
try {
|
|
136
|
-
# NEW format: ekkos-capture init <instance_id> <session_id> <session_name>
|
|
137
200
|
Start-Job -ScriptBlock {
|
|
138
201
|
param($instanceId, $sessId, $sessName)
|
|
139
|
-
try {
|
|
140
|
-
& ekkos-capture init $instanceId $sessId $sessName 2>&1 | Out-Null
|
|
141
|
-
} catch {}
|
|
202
|
+
try { & ekkos-capture init $instanceId $sessId $sessName 2>&1 | Out-Null } catch {}
|
|
142
203
|
} -ArgumentList $EkkosInstanceId, $sessionId, $sessionName | Out-Null
|
|
143
204
|
} catch {}
|
|
144
205
|
}
|
|
145
206
|
|
|
146
|
-
|
|
207
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
208
|
+
# COLORS
|
|
209
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
210
|
+
$esc = [char]27
|
|
211
|
+
$CYAN = "${esc}[0;36m"; $GREEN = "${esc}[0;32m"; $MAGENTA = "${esc}[0;35m"
|
|
212
|
+
$DIM = "${esc}[2m"; $BOLD = "${esc}[1m"; $RESET = "${esc}[0m"
|
|
213
|
+
|
|
214
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
215
|
+
# TIME MACHINE RESTORATION
|
|
216
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
217
|
+
if ($timeMachineSession) {
|
|
218
|
+
[Console]::Error.WriteLine("")
|
|
219
|
+
$tmPreview = $timeMachineSession.Substring(0, [Math]::Min(12, $timeMachineSession.Length))
|
|
220
|
+
[Console]::Error.WriteLine("${MAGENTA}${BOLD} TIME MACHINE${RESET} ${DIM}| Restoring past session: ${tmPreview}...${RESET}")
|
|
221
|
+
|
|
222
|
+
$recallBody = @{ session_id = $timeMachineSession; last_n = 15; format = "summary" }
|
|
223
|
+
if ($timeMachineFromTurn -and $timeMachineToTurn) {
|
|
224
|
+
$recallBody = @{ session_id = $timeMachineSession; from_turn = [int]$timeMachineFromTurn; to_turn = [int]$timeMachineToTurn; format = "summary" }
|
|
225
|
+
} elseif ($timeMachineFromTurn) {
|
|
226
|
+
$recallBody = @{ session_id = $timeMachineSession; from_turn = [int]$timeMachineFromTurn; format = "summary" }
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
$recallJson = $recallBody | ConvertTo-Json -Depth 10
|
|
230
|
+
try {
|
|
231
|
+
$restoreResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/turns/recall" `
|
|
232
|
+
-Method POST `
|
|
233
|
+
-Headers @{ "Authorization" = "Bearer $authToken"; "Content-Type" = "application/json" } `
|
|
234
|
+
-Body ([System.Text.Encoding]::UTF8.GetBytes($recallJson)) -TimeoutSec 5
|
|
235
|
+
|
|
236
|
+
$restoredCount = ($restoreResponse.turns | Measure-Object).Count
|
|
237
|
+
if ($restoredCount -gt 0) {
|
|
238
|
+
[Console]::Error.WriteLine("${MAGENTA} ✓${RESET} Restored $restoredCount turns from past session")
|
|
239
|
+
[Console]::Error.WriteLine("")
|
|
240
|
+
|
|
241
|
+
$turnsOutput = ""
|
|
242
|
+
foreach ($t in $restoreResponse.turns) {
|
|
243
|
+
$q = if ($t.user_query) { $t.user_query.Substring(0, [Math]::Min(100, $t.user_query.Length)) } else { "..." }
|
|
244
|
+
$r = if ($t.assistant_response) { $t.assistant_response.Substring(0, [Math]::Min(200, $t.assistant_response.Length)) } else { "..." }
|
|
245
|
+
$turnLine = "**Turn $($t.turn_number)**: $q..."
|
|
246
|
+
[Console]::Error.WriteLine($turnLine)
|
|
247
|
+
[Console]::Error.WriteLine("> $r...")
|
|
248
|
+
[Console]::Error.WriteLine("")
|
|
249
|
+
$turnsOutput += "$turnLine`n> $r...`n`n"
|
|
250
|
+
}
|
|
251
|
+
Write-Output $turnsOutput
|
|
252
|
+
|
|
253
|
+
[Console]::Error.WriteLine("${DIM}You've traveled to a past session. Continue from here!${RESET}")
|
|
254
|
+
[Console]::Error.WriteLine("")
|
|
255
|
+
}
|
|
256
|
+
} catch {}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
260
|
+
# DIRECTIVE RETRIEVAL: Fetch user's MUST/NEVER/PREFER/AVOID rules
|
|
261
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
262
|
+
if ($authToken) {
|
|
263
|
+
try {
|
|
264
|
+
$directivesResponse = Invoke-RestMethod -Uri "$MemoryApiUrl/api/v1/memory/directives?limit=20" `
|
|
265
|
+
-Method GET -Headers @{ "Authorization" = "Bearer $authToken" } -TimeoutSec 3
|
|
266
|
+
|
|
267
|
+
$directiveCount = $directivesResponse.count
|
|
268
|
+
if ($directiveCount -gt 0) {
|
|
269
|
+
Write-Output "<system-reminder>"
|
|
270
|
+
Write-Output "USER DIRECTIVES (FOLLOW THESE):"
|
|
271
|
+
Write-Output ""
|
|
272
|
+
|
|
273
|
+
foreach ($type in @("MUST", "NEVER", "PREFER", "AVOID")) {
|
|
274
|
+
$rules = $directivesResponse.$type
|
|
275
|
+
if ($rules -and ($rules | Measure-Object).Count -gt 0) {
|
|
276
|
+
Write-Output "${type}:"
|
|
277
|
+
$rules | Select-Object -First 5 | ForEach-Object { Write-Output " - $($_.rule)" }
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
Write-Output "</system-reminder>"
|
|
282
|
+
[Console]::Error.WriteLine("${GREEN} $directiveCount directives loaded${RESET}")
|
|
283
|
+
}
|
|
284
|
+
} catch {}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
288
|
+
# STATUS DISPLAY
|
|
289
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
290
|
+
if ($savedTurnCount -gt 0) {
|
|
291
|
+
[Console]::Error.WriteLine("")
|
|
292
|
+
[Console]::Error.WriteLine("${CYAN}${BOLD} ekkOS${RESET} ${DIM}|${RESET} Session: $sessionId ${DIM}|${RESET} ${GREEN}$savedTurnCount turns${RESET}")
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if ($timeMachineSession) {
|
|
296
|
+
$tmP = $timeMachineSession.Substring(0, [Math]::Min(12, $timeMachineSession.Length))
|
|
297
|
+
[Console]::Error.WriteLine("${MAGENTA}------------------------------------------------------------------------${RESET}")
|
|
298
|
+
[Console]::Error.WriteLine("${MAGENTA} ${RESET} Time Machine active - Restored from session ${tmP}...")
|
|
299
|
+
[Console]::Error.WriteLine("${MAGENTA}------------------------------------------------------------------------${RESET}")
|
|
300
|
+
} elseif ($savedTurnCount -gt 0) {
|
|
301
|
+
[Console]::Error.WriteLine("${GREEN}------------------------------------------------------------------------${RESET}")
|
|
302
|
+
[Console]::Error.WriteLine("${GREEN}✓${RESET} Session continued - $savedTurnCount turns preserved - Ready to resume")
|
|
303
|
+
[Console]::Error.WriteLine("${GREEN}------------------------------------------------------------------------${RESET}")
|
|
304
|
+
} else {
|
|
305
|
+
[Console]::Error.WriteLine("${CYAN}✓${RESET} New session started")
|
|
306
|
+
}
|
|
307
|
+
[Console]::Error.WriteLine("")
|
|
308
|
+
|
|
309
|
+
exit 0
|
|
@@ -78,7 +78,7 @@ fi
|
|
|
78
78
|
|
|
79
79
|
[ -z "$AUTH_TOKEN" ] && exit 0
|
|
80
80
|
|
|
81
|
-
MEMORY_API_URL="https://
|
|
81
|
+
MEMORY_API_URL="https://api.ekkos.dev"
|
|
82
82
|
|
|
83
83
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
84
84
|
# TIME MACHINE: Check for pending "Continue from here" requests
|