@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,446 +1,146 @@
|
|
|
1
1
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
# ekkOS_ Hook: SessionStart -
|
|
2
|
+
# ekkOS_ Hook: SessionStart - Initialize session (Windows)
|
|
3
3
|
# MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
|
|
4
4
|
# EKKOS_MANAGED=1
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# 1. Check for pending Time Machine "Continue from here" requests
|
|
8
|
-
# 2. Initialize session tracking
|
|
9
|
-
# 3. Auto-restore from L2 if recent turns exist (FAST TRIM support)
|
|
10
|
-
#
|
|
11
|
-
# TIME MACHINE FLOW:
|
|
12
|
-
# User clicks "Continue from here" on web -> API queues request ->
|
|
13
|
-
# User runs `claude` -> This hook detects pending request ->
|
|
14
|
-
# Restores THAT session's context -> Seamless time travel!
|
|
15
|
-
#
|
|
16
|
-
# FAST TRIM FLOW:
|
|
17
|
-
# User runs /clear -> session-start detects fresh session ->
|
|
18
|
-
# Checks L2 for recent turns -> Auto-injects last 15 turns -> Seamless continuity
|
|
5
|
+
# EKKOS_MANIFEST_SHA256=<computed-at-build>
|
|
6
|
+
# EKKOS_TEMPLATE_VERSION=1.0.0
|
|
19
7
|
#
|
|
20
|
-
# Per
|
|
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
|
|
21
11
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
22
12
|
|
|
23
13
|
$ErrorActionPreference = "SilentlyContinue"
|
|
24
14
|
|
|
25
|
-
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
26
|
-
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
|
27
|
-
|
|
28
15
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
-
# CONFIG PATHS -
|
|
16
|
+
# CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
|
|
30
17
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
31
|
-
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else {
|
|
32
|
-
$
|
|
33
|
-
|
|
34
|
-
$INPUT = [Console]::In.ReadToEnd()
|
|
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"
|
|
35
21
|
|
|
36
22
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
37
|
-
#
|
|
23
|
+
# INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
|
|
38
24
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
39
|
-
|
|
40
|
-
param(
|
|
41
|
-
[string]$Json,
|
|
42
|
-
[string]$Path
|
|
43
|
-
)
|
|
44
|
-
if (-not $Json) { return "" }
|
|
45
|
-
try {
|
|
46
|
-
$result = $Json | node -e "
|
|
47
|
-
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
48
|
-
const path = '$Path'.replace(/^\./,'').split('.').filter(Boolean);
|
|
49
|
-
let result = data;
|
|
50
|
-
for (const p of path) {
|
|
51
|
-
if (result === undefined || result === null) { result = undefined; break; }
|
|
52
|
-
result = result[p];
|
|
53
|
-
}
|
|
54
|
-
if (result !== undefined && result !== null) console.log(result);
|
|
55
|
-
" 2>$null
|
|
56
|
-
if ($result) { return $result.Trim() } else { return "" }
|
|
57
|
-
} catch {
|
|
58
|
-
return ""
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
$SESSION_ID = Parse-JsonValue $INPUT ".session_id"
|
|
63
|
-
if (-not $SESSION_ID) { $SESSION_ID = "unknown" }
|
|
64
|
-
|
|
65
|
-
$TRANSCRIPT_PATH = Parse-JsonValue $INPUT ".transcript_path"
|
|
66
|
-
$SOURCE = Parse-JsonValue $INPUT ".source"
|
|
67
|
-
if (-not $SOURCE) { $SOURCE = "unknown" }
|
|
25
|
+
$EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
|
|
68
26
|
|
|
69
27
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
-
# Load
|
|
28
|
+
# Load session words from JSON file - NO HARDCODED ARRAYS
|
|
71
29
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
-
$
|
|
73
|
-
$AUTH_TOKEN = ""
|
|
74
|
-
$USER_ID = ""
|
|
30
|
+
$script:SessionWords = $null
|
|
75
31
|
|
|
76
|
-
|
|
77
|
-
$
|
|
78
|
-
if (-not $AUTH_TOKEN) {
|
|
79
|
-
$AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".apiKey" 2>$null
|
|
80
|
-
}
|
|
81
|
-
$USER_ID = & node $JsonParseHelper $EKKOS_CONFIG ".userId" 2>$null
|
|
82
|
-
}
|
|
32
|
+
function Load-SessionWords {
|
|
33
|
+
$wordsFile = $SessionWordsJson
|
|
83
34
|
|
|
84
|
-
if (-not $
|
|
85
|
-
|
|
86
|
-
if (Test-Path $envLocalFile) {
|
|
87
|
-
$envLines = Get-Content $envLocalFile
|
|
88
|
-
foreach ($line in $envLines) {
|
|
89
|
-
if ($line -match '^SUPABASE_SECRET_KEY=(.+)$') {
|
|
90
|
-
$AUTH_TOKEN = $Matches[1].Trim('"', "'", ' ', "`r")
|
|
91
|
-
break
|
|
92
|
-
}
|
|
93
|
-
}
|
|
35
|
+
if (-not (Test-Path $wordsFile)) {
|
|
36
|
+
$wordsFile = $SessionWordsDefault
|
|
94
37
|
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
if (-not $AUTH_TOKEN) { exit 0 }
|
|
98
|
-
|
|
99
|
-
$MEMORY_API_URL = "https://mcp.ekkos.dev"
|
|
100
|
-
|
|
101
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
-
# COLORS (ANSI escape sequences via [char]27)
|
|
103
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
104
|
-
$ESC = [char]27
|
|
105
|
-
$CYAN = "$ESC[0;36m"
|
|
106
|
-
$GREEN = "$ESC[0;32m"
|
|
107
|
-
$YELLOW = "$ESC[1;33m"
|
|
108
|
-
$MAGENTA = "$ESC[0;35m"
|
|
109
|
-
$DIM = "$ESC[2m"
|
|
110
|
-
$BOLD = "$ESC[1m"
|
|
111
|
-
$RESET = "$ESC[0m"
|
|
112
38
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
$RESTORE_REQUEST_ID = $env:EKKOS_RESTORE
|
|
117
|
-
$TIME_MACHINE_SESSION = ""
|
|
118
|
-
$TIME_MACHINE_FROM_TURN = ""
|
|
119
|
-
$TIME_MACHINE_TO_TURN = ""
|
|
120
|
-
|
|
121
|
-
# Check via env var first, then API
|
|
122
|
-
if ($RESTORE_REQUEST_ID) {
|
|
123
|
-
[Console]::Error.WriteLine("$MAGENTA Time Machine request detected: $RESTORE_REQUEST_ID$RESET")
|
|
124
|
-
}
|
|
39
|
+
if (-not (Test-Path $wordsFile)) {
|
|
40
|
+
return $null
|
|
41
|
+
}
|
|
125
42
|
|
|
126
|
-
# Check API for pending requests (if we have user_id)
|
|
127
|
-
if ((-not $TIME_MACHINE_SESSION) -and $USER_ID) {
|
|
128
|
-
$PENDING_RESPONSE_RAW = ""
|
|
129
43
|
try {
|
|
130
|
-
$
|
|
131
|
-
-Method GET `
|
|
132
|
-
-Headers @{ Authorization = "Bearer $AUTH_TOKEN" } `
|
|
133
|
-
-TimeoutSec 3 `
|
|
134
|
-
-ErrorAction Stop
|
|
44
|
+
$script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
|
|
135
45
|
} catch {
|
|
136
|
-
$
|
|
46
|
+
return $null
|
|
137
47
|
}
|
|
48
|
+
}
|
|
138
49
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
$PENDING_JSON = ""
|
|
142
|
-
if ($PENDING_RESPONSE_RAW -is [string]) {
|
|
143
|
-
$PENDING_JSON = $PENDING_RESPONSE_RAW
|
|
144
|
-
} else {
|
|
145
|
-
try {
|
|
146
|
-
$PENDING_JSON = $PENDING_RESPONSE_RAW | ConvertTo-Json -Depth 10 -Compress
|
|
147
|
-
} catch {
|
|
148
|
-
$PENDING_JSON = "{}"
|
|
149
|
-
}
|
|
150
|
-
}
|
|
50
|
+
function Convert-UuidToWords {
|
|
51
|
+
param([string]$uuid)
|
|
151
52
|
|
|
152
|
-
|
|
53
|
+
if (-not $script:SessionWords) {
|
|
54
|
+
Load-SessionWords
|
|
55
|
+
}
|
|
153
56
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
$TIME_MACHINE_TO_TURN = Parse-JsonValue $PENDING_JSON ".request.to_turn"
|
|
158
|
-
$RESTORE_REQUEST_ID = Parse-JsonValue $PENDING_JSON ".request.request_id"
|
|
57
|
+
if (-not $script:SessionWords) {
|
|
58
|
+
return "unknown-session"
|
|
59
|
+
}
|
|
159
60
|
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
[Console]::Error.WriteLine("$MAGENTA${BOLD} TIME MACHINE$RESET $DIM| Restoring session from web request...$RESET")
|
|
164
|
-
[Console]::Error.WriteLine("$MAGENTA------------------------------------------------------------------------$RESET")
|
|
61
|
+
$adjectives = $script:SessionWords.adjectives
|
|
62
|
+
$nouns = $script:SessionWords.nouns
|
|
63
|
+
$verbs = $script:SessionWords.verbs
|
|
165
64
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
Start-Job -ScriptBlock {
|
|
169
|
-
param($url, $token, $body)
|
|
170
|
-
try {
|
|
171
|
-
Invoke-RestMethod -Uri "$url/api/v1/context/restore-request/consume" `
|
|
172
|
-
-Method POST `
|
|
173
|
-
-Headers @{ Authorization = "Bearer $token"; "Content-Type" = "application/json" } `
|
|
174
|
-
-Body ([System.Text.Encoding]::UTF8.GetBytes($body)) `
|
|
175
|
-
-TimeoutSec 3 `
|
|
176
|
-
-ErrorAction SilentlyContinue | Out-Null
|
|
177
|
-
} catch {}
|
|
178
|
-
} -ArgumentList $MEMORY_API_URL, $AUTH_TOKEN, $consumeBody | Out-Null
|
|
179
|
-
}
|
|
180
|
-
}
|
|
65
|
+
if (-not $adjectives -or -not $nouns -or -not $verbs) {
|
|
66
|
+
return "unknown-session"
|
|
181
67
|
}
|
|
182
|
-
}
|
|
183
68
|
|
|
184
|
-
|
|
185
|
-
# Session ID persistence - PROJECT-LOCAL for isolation
|
|
186
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
187
|
-
$STATE_DIR = Join-Path $ProjectRoot ".claude\state"
|
|
188
|
-
if (-not (Test-Path $STATE_DIR)) {
|
|
189
|
-
New-Item -ItemType Directory -Path $STATE_DIR -Force | Out-Null
|
|
190
|
-
}
|
|
191
|
-
$SESSION_FILE = Join-Path $STATE_DIR "current-session.json"
|
|
192
|
-
|
|
193
|
-
# Project-local session storage (isolated per project)
|
|
194
|
-
$PROJECT_SESSION_DIR = Join-Path $STATE_DIR "sessions"
|
|
195
|
-
if (-not (Test-Path $PROJECT_SESSION_DIR)) {
|
|
196
|
-
New-Item -ItemType Directory -Path $PROJECT_SESSION_DIR -Force | Out-Null
|
|
197
|
-
}
|
|
69
|
+
if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
|
|
198
70
|
|
|
199
|
-
|
|
200
|
-
$
|
|
71
|
+
$clean = $uuid -replace "-", ""
|
|
72
|
+
if ($clean.Length -lt 12) { return "unknown-session" }
|
|
201
73
|
|
|
202
|
-
|
|
203
|
-
$
|
|
204
|
-
$
|
|
74
|
+
try {
|
|
75
|
+
$a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
|
|
76
|
+
$n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
|
|
77
|
+
$an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
|
|
205
78
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
if (Test-Path $TURN_COUNTER_FILE) {
|
|
210
|
-
try {
|
|
211
|
-
$SAVED_TURN_COUNT = [int](Get-Content $TURN_COUNTER_FILE -Raw).Trim()
|
|
212
|
-
} catch {
|
|
213
|
-
$SAVED_TURN_COUNT = 0
|
|
214
|
-
}
|
|
215
|
-
$MOST_RECENT_SESSION = $CURRENT_SESSION_ID
|
|
216
|
-
} else {
|
|
217
|
-
# Fresh start: find most recent session in project
|
|
218
|
-
$turnFiles = Get-ChildItem -Path $PROJECT_SESSION_DIR -Filter "*.turn" -ErrorAction SilentlyContinue |
|
|
219
|
-
Sort-Object LastWriteTime -Descending |
|
|
220
|
-
Select-Object -First 1
|
|
221
|
-
if ($turnFiles) {
|
|
222
|
-
$MOST_RECENT_SESSION = [System.IO.Path]::GetFileNameWithoutExtension($turnFiles.Name)
|
|
223
|
-
try {
|
|
224
|
-
$SAVED_TURN_COUNT = [int](Get-Content $turnFiles.FullName -Raw).Trim()
|
|
225
|
-
} catch {
|
|
226
|
-
$SAVED_TURN_COUNT = 0
|
|
227
|
-
}
|
|
228
|
-
}
|
|
79
|
+
return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
|
|
80
|
+
} catch {
|
|
81
|
+
return "unknown-session"
|
|
229
82
|
}
|
|
230
83
|
}
|
|
231
84
|
|
|
232
|
-
# Save current session info
|
|
233
|
-
if ($CURRENT_SESSION_ID) {
|
|
234
|
-
$TIMESTAMP_UTC = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
|
235
|
-
$projectRootNorm = $ProjectRoot -replace '\\', '/'
|
|
236
|
-
$sessionInfoJson = "{`"session_id`": `"$CURRENT_SESSION_ID`", `"timestamp`": `"$TIMESTAMP_UTC`", `"project_root`": `"$projectRootNorm`"}"
|
|
237
|
-
Set-Content -Path $SESSION_FILE -Value $sessionInfoJson -Force
|
|
238
|
-
}
|
|
239
|
-
|
|
240
85
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
241
|
-
#
|
|
86
|
+
# READ INPUT
|
|
242
87
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
243
|
-
$
|
|
244
|
-
if (-not (Test-Path $EKKOS_DIR)) {
|
|
245
|
-
New-Item -ItemType Directory -Path $EKKOS_DIR -Force | Out-Null
|
|
246
|
-
}
|
|
247
|
-
$GOLDEN_LOOP_FILE = Join-Path $EKKOS_DIR "golden-loop-current.json"
|
|
88
|
+
$inputJson = [Console]::In.ReadToEnd()
|
|
248
89
|
|
|
249
|
-
# Initialize with session start state using Node (no jq)
|
|
250
|
-
$glPathEscaped = $GOLDEN_LOOP_FILE -replace '\\', '\\\\'
|
|
251
90
|
try {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
stats: { retrieved: 0, applied: 0, forged: 0 }
|
|
260
|
-
};
|
|
261
|
-
fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
|
|
262
|
-
" 2>$null
|
|
263
|
-
} catch {}
|
|
91
|
+
$input = $inputJson | ConvertFrom-Json
|
|
92
|
+
$sessionId = $input.session_id
|
|
93
|
+
} catch {
|
|
94
|
+
$sessionId = "unknown"
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
$sessionName = Convert-UuidToWords $sessionId
|
|
264
98
|
|
|
265
99
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
266
|
-
#
|
|
267
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
268
|
-
# WHY REMOVED:
|
|
269
|
-
# - Auto-restore burned 5,000 tokens per turn on session start
|
|
270
|
-
# - Manual /continue: one-time cost + clean slate (79% token savings!)
|
|
271
|
-
# - Manual /continue is 10x more powerful (Bash + multi-source + narrative)
|
|
272
|
-
#
|
|
273
|
-
# KEPT: Time Machine feature (explicit user request)
|
|
100
|
+
# INITIALIZE STATE DIRECTORY
|
|
274
101
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
+
$stateDir = Join-Path $env:USERPROFILE ".claude\state"
|
|
103
|
+
if (-not (Test-Path $stateDir)) {
|
|
104
|
+
New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
|
|
105
|
+
}
|
|
275
106
|
|
|
276
|
-
#
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
# Build recall request with turn range
|
|
287
|
-
$RECALL_BODY = "{`"session_id`": `"$TIME_MACHINE_SESSION`", `"last_n`": 15, `"format`": `"summary`"}"
|
|
288
|
-
|
|
289
|
-
if ($TIME_MACHINE_FROM_TURN -and $TIME_MACHINE_TO_TURN) {
|
|
290
|
-
$RECALL_BODY = "{`"session_id`": `"$TIME_MACHINE_SESSION`", `"from_turn`": $TIME_MACHINE_FROM_TURN, `"to_turn`": $TIME_MACHINE_TO_TURN, `"format`": `"summary`"}"
|
|
291
|
-
} elseif ($TIME_MACHINE_FROM_TURN) {
|
|
292
|
-
$RECALL_BODY = "{`"session_id`": `"$TIME_MACHINE_SESSION`", `"from_turn`": $TIME_MACHINE_FROM_TURN, `"format`": `"summary`"}"
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
# Fetch turns from L2
|
|
296
|
-
$RESTORE_RESPONSE = $null
|
|
297
|
-
try {
|
|
298
|
-
$RESTORE_RESPONSE = Invoke-RestMethod -Uri "$MEMORY_API_URL/api/v1/turns/recall" `
|
|
299
|
-
-Method POST `
|
|
300
|
-
-Headers @{ Authorization = "Bearer $AUTH_TOKEN"; "Content-Type" = "application/json" } `
|
|
301
|
-
-Body ([System.Text.Encoding]::UTF8.GetBytes($RECALL_BODY)) `
|
|
302
|
-
-TimeoutSec 5 `
|
|
303
|
-
-ErrorAction Stop
|
|
304
|
-
} catch {
|
|
305
|
-
$RESTORE_RESPONSE = $null
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
# Check if we got turns back
|
|
309
|
-
$RESTORED_COUNT = 0
|
|
310
|
-
$RESTORED_TURNS = @()
|
|
311
|
-
if ($RESTORE_RESPONSE -and $RESTORE_RESPONSE.turns) {
|
|
312
|
-
$RESTORED_TURNS = @($RESTORE_RESPONSE.turns)
|
|
313
|
-
$RESTORED_COUNT = $RESTORED_TURNS.Count
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if ($RESTORED_COUNT -gt 0) {
|
|
317
|
-
[Console]::Error.WriteLine("$MAGENTA $RESET Restored $RESTORED_COUNT turns from past session")
|
|
318
|
-
[Console]::Error.WriteLine("")
|
|
319
|
-
[Console]::Error.WriteLine("$MAGENTA${BOLD}## Time Machine Context$RESET")
|
|
320
|
-
[Console]::Error.WriteLine("")
|
|
107
|
+
# Reset turn counter for new session
|
|
108
|
+
$stateFile = Join-Path $stateDir "hook-state.json"
|
|
109
|
+
$state = @{
|
|
110
|
+
turn = 0
|
|
111
|
+
session_id = $sessionId
|
|
112
|
+
session_name = $sessionName
|
|
113
|
+
instance_id = $EkkosInstanceId
|
|
114
|
+
started_at = (Get-Date).ToString("o")
|
|
115
|
+
} | ConvertTo-Json -Depth 10
|
|
321
116
|
|
|
322
|
-
|
|
323
|
-
$turnsOutput = ""
|
|
324
|
-
foreach ($t in $RESTORED_TURNS) {
|
|
325
|
-
$q = ""
|
|
326
|
-
if ($t.user_query) {
|
|
327
|
-
$q = $t.user_query
|
|
328
|
-
if ($q.Length -gt 100) { $q = $q.Substring(0, 100) }
|
|
329
|
-
}
|
|
330
|
-
$r = ""
|
|
331
|
-
if ($t.assistant_response) {
|
|
332
|
-
$r = $t.assistant_response
|
|
333
|
-
if ($r.Length -gt 200) { $r = $r.Substring(0, 200) }
|
|
334
|
-
}
|
|
335
|
-
$tn = if ($t.turn_number) { $t.turn_number } else { "?" }
|
|
117
|
+
Set-Content -Path $stateFile -Value $state -Force
|
|
336
118
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
119
|
+
# Save session ID for other hooks
|
|
120
|
+
$sessionFile = Join-Path $stateDir "current-session.json"
|
|
121
|
+
$sessionData = @{
|
|
122
|
+
session_id = $sessionId
|
|
123
|
+
session_name = $sessionName
|
|
124
|
+
instance_id = $EkkosInstanceId
|
|
125
|
+
} | ConvertTo-Json -Depth 10
|
|
341
126
|
|
|
342
|
-
|
|
343
|
-
[Console]::Error.WriteLine($turnsOutput)
|
|
344
|
-
# Output to stdout (injected into context)
|
|
345
|
-
Write-Output $turnsOutput
|
|
346
|
-
|
|
347
|
-
[Console]::Error.WriteLine("")
|
|
348
|
-
[Console]::Error.WriteLine("${DIM}You've traveled to a past session. Continue from here!$RESET")
|
|
349
|
-
[Console]::Error.WriteLine("")
|
|
350
|
-
}
|
|
351
|
-
}
|
|
127
|
+
Set-Content -Path $sessionFile -Value $sessionData -Force
|
|
352
128
|
|
|
353
129
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
354
|
-
#
|
|
130
|
+
# LOCAL CACHE: Initialize session in Tier 0 cache
|
|
131
|
+
# Per v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
355
132
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
356
|
-
$
|
|
357
|
-
$
|
|
358
|
-
|
|
359
|
-
# Only fetch if we have auth
|
|
360
|
-
if ($AUTH_TOKEN) {
|
|
361
|
-
# Fetch directives (top 20 by priority to avoid token bloat)
|
|
362
|
-
$DIRECTIVES_RESPONSE_RAW = $null
|
|
133
|
+
$captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
|
|
134
|
+
if ($captureCmd -and $sessionId -ne "unknown") {
|
|
363
135
|
try {
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
-TimeoutSec 3 `
|
|
368
|
-
-ErrorAction Stop
|
|
369
|
-
} catch {
|
|
370
|
-
$DIRECTIVES_RESPONSE_RAW = $null
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
if ($DIRECTIVES_RESPONSE_RAW) {
|
|
374
|
-
# Convert response object to JSON string for Parse-JsonValue / Node
|
|
375
|
-
$DIRECTIVES_JSON = ""
|
|
376
|
-
if ($DIRECTIVES_RESPONSE_RAW -is [string]) {
|
|
377
|
-
$DIRECTIVES_JSON = $DIRECTIVES_RESPONSE_RAW
|
|
378
|
-
} else {
|
|
136
|
+
# NEW format: ekkos-capture init <instance_id> <session_id> <session_name>
|
|
137
|
+
Start-Job -ScriptBlock {
|
|
138
|
+
param($instanceId, $sessId, $sessName)
|
|
379
139
|
try {
|
|
380
|
-
|
|
381
|
-
} catch {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
$DIRECTIVE_COUNT_STR = Parse-JsonValue $DIRECTIVES_JSON ".count"
|
|
387
|
-
if ($DIRECTIVE_COUNT_STR -match '^\d+$') {
|
|
388
|
-
$DIRECTIVE_COUNT = [int]$DIRECTIVE_COUNT_STR
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
if ($DIRECTIVE_COUNT -gt 0) {
|
|
392
|
-
$DIRECTIVES_INJECTED = $true
|
|
393
|
-
|
|
394
|
-
# Extract MUST/NEVER/PREFER/AVOID arrays using Node
|
|
395
|
-
Write-Output "<system-reminder>"
|
|
396
|
-
Write-Output "USER DIRECTIVES (FOLLOW THESE):"
|
|
397
|
-
Write-Output ""
|
|
398
|
-
|
|
399
|
-
$directiveOutput = $DIRECTIVES_JSON | node -e "
|
|
400
|
-
const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
401
|
-
const types = ['MUST', 'NEVER', 'PREFER', 'AVOID'];
|
|
402
|
-
types.forEach(type => {
|
|
403
|
-
const rules = (d[type] || []).slice(0, 5);
|
|
404
|
-
if (rules.length > 0) {
|
|
405
|
-
console.log(type + ':');
|
|
406
|
-
rules.forEach(r => console.log(' - ' + (r.rule || '')));
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
" 2>$null
|
|
410
|
-
|
|
411
|
-
if ($directiveOutput) {
|
|
412
|
-
Write-Output $directiveOutput
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
Write-Output "</system-reminder>"
|
|
416
|
-
[Console]::Error.WriteLine("$GREEN $DIRECTIVE_COUNT directives loaded$RESET")
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
# Simple status display (no auto-restore)
|
|
422
|
-
if ($SAVED_TURN_COUNT -gt 0) {
|
|
423
|
-
[Console]::Error.WriteLine("")
|
|
424
|
-
$displaySessionId = if ($CURRENT_SESSION_ID) { $CURRENT_SESSION_ID } else { $SESSION_ID }
|
|
425
|
-
[Console]::Error.WriteLine("$CYAN${BOLD} ekkOS$RESET $DIM|$RESET Session: $displaySessionId $DIM|$RESET $GREEN$SAVED_TURN_COUNT turns$RESET")
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
# Final confirmation that's always visible
|
|
429
|
-
if ($TIME_MACHINE_SESSION) {
|
|
430
|
-
$tmFinalPreview = $TIME_MACHINE_SESSION
|
|
431
|
-
if ($tmFinalPreview.Length -gt 12) {
|
|
432
|
-
$tmFinalPreview = $tmFinalPreview.Substring(0, 12)
|
|
433
|
-
}
|
|
434
|
-
[Console]::Error.WriteLine("$MAGENTA------------------------------------------------------------------------$RESET")
|
|
435
|
-
[Console]::Error.WriteLine("$MAGENTA $RESET Time Machine active - Restored from session ${tmFinalPreview}...")
|
|
436
|
-
[Console]::Error.WriteLine("$MAGENTA------------------------------------------------------------------------$RESET")
|
|
437
|
-
} elseif ($SAVED_TURN_COUNT -gt 0) {
|
|
438
|
-
[Console]::Error.WriteLine("$GREEN------------------------------------------------------------------------$RESET")
|
|
439
|
-
[Console]::Error.WriteLine("${GREEN}$RESET Session continued - $SAVED_TURN_COUNT turns preserved - Ready to resume")
|
|
440
|
-
[Console]::Error.WriteLine("$GREEN------------------------------------------------------------------------$RESET")
|
|
441
|
-
} else {
|
|
442
|
-
[Console]::Error.WriteLine("${CYAN}$RESET New session started")
|
|
140
|
+
& ekkos-capture init $instanceId $sessId $sessName 2>&1 | Out-Null
|
|
141
|
+
} catch {}
|
|
142
|
+
} -ArgumentList $EkkosInstanceId, $sessionId, $sessionName | Out-Null
|
|
143
|
+
} catch {}
|
|
443
144
|
}
|
|
444
|
-
[Console]::Error.WriteLine("")
|
|
445
145
|
|
|
446
|
-
|
|
146
|
+
Write-Output "ekkOS session initialized"
|