@ekkos/cli 1.0.33 → 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 +221 -259
- 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,146 +1,446 @@
|
|
|
1
1
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
2
|
-
# ekkOS_ Hook: SessionStart -
|
|
2
|
+
# ekkOS_ Hook: SessionStart - MINIMAL + AUTO-RESTORE + TIME MACHINE CONTINUE
|
|
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
|
|
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
|
|
7
19
|
#
|
|
8
|
-
# Per
|
|
9
|
-
# - All persisted records MUST include: instanceId, sessionId, sessionName
|
|
10
|
-
# - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
|
|
20
|
+
# Per spec v1.2 Addendum: NO jq dependency
|
|
11
21
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
12
22
|
|
|
13
23
|
$ErrorActionPreference = "SilentlyContinue"
|
|
14
24
|
|
|
25
|
+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
|
26
|
+
$ProjectRoot = Split-Path -Parent (Split-Path -Parent $ScriptDir)
|
|
27
|
+
|
|
15
28
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
# CONFIG PATHS -
|
|
29
|
+
# CONFIG PATHS - Per spec v1.2 Addendum
|
|
17
30
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
18
|
-
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else {
|
|
19
|
-
$
|
|
20
|
-
|
|
31
|
+
$EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { Join-Path $env:USERPROFILE ".ekkos" }
|
|
32
|
+
$JsonParseHelper = Join-Path $EkkosConfigDir ".helpers\json-parse.cjs"
|
|
33
|
+
|
|
34
|
+
$INPUT = [Console]::In.ReadToEnd()
|
|
21
35
|
|
|
22
36
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
23
|
-
#
|
|
37
|
+
# JSON parsing helper (no jq) - pipes JSON to node, extracts dot-path value
|
|
24
38
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
25
|
-
|
|
39
|
+
function Parse-JsonValue {
|
|
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" }
|
|
26
68
|
|
|
27
69
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
28
|
-
# Load
|
|
70
|
+
# Load auth
|
|
29
71
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
30
|
-
$
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
$wordsFile = $SessionWordsJson
|
|
72
|
+
$EKKOS_CONFIG = Join-Path $env:USERPROFILE ".ekkos\config.json"
|
|
73
|
+
$AUTH_TOKEN = ""
|
|
74
|
+
$USER_ID = ""
|
|
34
75
|
|
|
35
|
-
|
|
36
|
-
|
|
76
|
+
if ((Test-Path $EKKOS_CONFIG) -and (Test-Path $JsonParseHelper)) {
|
|
77
|
+
$AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".hookApiKey" 2>$null
|
|
78
|
+
if (-not $AUTH_TOKEN) {
|
|
79
|
+
$AUTH_TOKEN = & node $JsonParseHelper $EKKOS_CONFIG ".apiKey" 2>$null
|
|
37
80
|
}
|
|
81
|
+
$USER_ID = & node $JsonParseHelper $EKKOS_CONFIG ".userId" 2>$null
|
|
82
|
+
}
|
|
38
83
|
|
|
39
|
-
|
|
40
|
-
|
|
84
|
+
if (-not $AUTH_TOKEN) {
|
|
85
|
+
$envLocalFile = Join-Path $ProjectRoot ".env.local"
|
|
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
|
+
}
|
|
41
94
|
}
|
|
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"
|
|
42
112
|
|
|
113
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
+
# TIME MACHINE: Check for pending "Continue from here" requests
|
|
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
|
+
}
|
|
125
|
+
|
|
126
|
+
# Check API for pending requests (if we have user_id)
|
|
127
|
+
if ((-not $TIME_MACHINE_SESSION) -and $USER_ID) {
|
|
128
|
+
$PENDING_RESPONSE_RAW = ""
|
|
43
129
|
try {
|
|
44
|
-
$
|
|
130
|
+
$PENDING_RESPONSE_RAW = Invoke-RestMethod -Uri "$MEMORY_API_URL/api/v1/context/restore-request/pending?user_id=$USER_ID" `
|
|
131
|
+
-Method GET `
|
|
132
|
+
-Headers @{ Authorization = "Bearer $AUTH_TOKEN" } `
|
|
133
|
+
-TimeoutSec 3 `
|
|
134
|
+
-ErrorAction Stop
|
|
45
135
|
} catch {
|
|
46
|
-
|
|
136
|
+
$PENDING_RESPONSE_RAW = ""
|
|
47
137
|
}
|
|
48
|
-
}
|
|
49
138
|
|
|
50
|
-
|
|
51
|
-
|
|
139
|
+
if ($PENDING_RESPONSE_RAW) {
|
|
140
|
+
# Convert response object to JSON string for Parse-JsonValue
|
|
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
|
+
}
|
|
52
151
|
|
|
53
|
-
|
|
54
|
-
Load-SessionWords
|
|
55
|
-
}
|
|
152
|
+
$IS_PENDING = Parse-JsonValue $PENDING_JSON ".pending"
|
|
56
153
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
154
|
+
if ($IS_PENDING -eq "true" -or $IS_PENDING -eq "True") {
|
|
155
|
+
$TIME_MACHINE_SESSION = Parse-JsonValue $PENDING_JSON ".request.session_id"
|
|
156
|
+
$TIME_MACHINE_FROM_TURN = Parse-JsonValue $PENDING_JSON ".request.from_turn"
|
|
157
|
+
$TIME_MACHINE_TO_TURN = Parse-JsonValue $PENDING_JSON ".request.to_turn"
|
|
158
|
+
$RESTORE_REQUEST_ID = Parse-JsonValue $PENDING_JSON ".request.request_id"
|
|
60
159
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
160
|
+
if ($TIME_MACHINE_SESSION) {
|
|
161
|
+
[Console]::Error.WriteLine("")
|
|
162
|
+
[Console]::Error.WriteLine("$MAGENTA------------------------------------------------------------------------$RESET")
|
|
163
|
+
[Console]::Error.WriteLine("$MAGENTA${BOLD} TIME MACHINE$RESET $DIM| Restoring session from web request...$RESET")
|
|
164
|
+
[Console]::Error.WriteLine("$MAGENTA------------------------------------------------------------------------$RESET")
|
|
64
165
|
|
|
65
|
-
|
|
66
|
-
|
|
166
|
+
# Mark request as consumed (background, non-blocking)
|
|
167
|
+
$consumeBody = "{`"request_id`": `"$RESTORE_REQUEST_ID`"}"
|
|
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
|
+
}
|
|
67
181
|
}
|
|
182
|
+
}
|
|
68
183
|
|
|
69
|
-
|
|
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"
|
|
70
192
|
|
|
71
|
-
|
|
72
|
-
|
|
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
|
+
}
|
|
73
198
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
$n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
|
|
77
|
-
$an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
|
|
199
|
+
# Use Claude's RAW_SESSION_ID directly (from session_id field)
|
|
200
|
+
$CURRENT_SESSION_ID = $SESSION_ID
|
|
78
201
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
202
|
+
# Find most recent session in THIS PROJECT for auto-restore
|
|
203
|
+
$MOST_RECENT_SESSION = ""
|
|
204
|
+
$SAVED_TURN_COUNT = 0
|
|
205
|
+
|
|
206
|
+
if ($CURRENT_SESSION_ID -and $CURRENT_SESSION_ID -ne "unknown") {
|
|
207
|
+
# Check if THIS session has saved turns (for /clear continuity)
|
|
208
|
+
$TURN_COUNTER_FILE = Join-Path $PROJECT_SESSION_DIR "$CURRENT_SESSION_ID.turn"
|
|
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
|
+
}
|
|
82
229
|
}
|
|
83
230
|
}
|
|
84
231
|
|
|
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
|
+
|
|
85
240
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
86
|
-
#
|
|
241
|
+
# GOLDEN LOOP: Initialize session tracking file
|
|
87
242
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
88
|
-
$
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
$input = $inputJson | ConvertFrom-Json
|
|
92
|
-
$sessionId = $input.session_id
|
|
93
|
-
} catch {
|
|
94
|
-
$sessionId = "unknown"
|
|
243
|
+
$EKKOS_DIR = Join-Path $ProjectRoot ".ekkos"
|
|
244
|
+
if (-not (Test-Path $EKKOS_DIR)) {
|
|
245
|
+
New-Item -ItemType Directory -Path $EKKOS_DIR -Force | Out-Null
|
|
95
246
|
}
|
|
247
|
+
$GOLDEN_LOOP_FILE = Join-Path $EKKOS_DIR "golden-loop-current.json"
|
|
96
248
|
|
|
97
|
-
|
|
249
|
+
# Initialize with session start state using Node (no jq)
|
|
250
|
+
$glPathEscaped = $GOLDEN_LOOP_FILE -replace '\\', '\\\\'
|
|
251
|
+
try {
|
|
252
|
+
node -e "
|
|
253
|
+
const fs = require('fs');
|
|
254
|
+
const data = {
|
|
255
|
+
phase: 'idle',
|
|
256
|
+
turn: 0,
|
|
257
|
+
session: '$CURRENT_SESSION_ID',
|
|
258
|
+
timestamp: new Date().toISOString(),
|
|
259
|
+
stats: { retrieved: 0, applied: 0, forged: 0 }
|
|
260
|
+
};
|
|
261
|
+
fs.writeFileSync('$glPathEscaped', JSON.stringify(data, null, 2));
|
|
262
|
+
" 2>$null
|
|
263
|
+
} catch {}
|
|
98
264
|
|
|
99
265
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
-
#
|
|
266
|
+
# AUTO-RESTORE REMOVED: Manual /continue only (saves 79% token burn!)
|
|
101
267
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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)
|
|
274
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
275
|
+
|
|
276
|
+
# Handle Time Machine requests (explicit user action)
|
|
277
|
+
if ($TIME_MACHINE_SESSION) {
|
|
278
|
+
$tmPreview = $TIME_MACHINE_SESSION
|
|
279
|
+
if ($tmPreview.Length -gt 12) {
|
|
280
|
+
$tmPreview = $tmPreview.Substring(0, 12)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
[Console]::Error.WriteLine("")
|
|
284
|
+
[Console]::Error.WriteLine("$MAGENTA${BOLD} TIME MACHINE$RESET $DIM| Restoring past session: ${tmPreview}...$RESET")
|
|
106
285
|
|
|
107
|
-
#
|
|
108
|
-
$
|
|
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
|
|
286
|
+
# Build recall request with turn range
|
|
287
|
+
$RECALL_BODY = "{`"session_id`": `"$TIME_MACHINE_SESSION`", `"last_n`": 15, `"format`": `"summary`"}"
|
|
116
288
|
|
|
117
|
-
|
|
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
|
+
}
|
|
118
307
|
|
|
119
|
-
#
|
|
120
|
-
$
|
|
121
|
-
$
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
}
|
|
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("")
|
|
321
|
+
|
|
322
|
+
# Build turns output for both stderr (display) and stdout (context injection)
|
|
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 { "?" }
|
|
126
336
|
|
|
127
|
-
|
|
337
|
+
$turnLine = "**Turn $tn**: $q..."
|
|
338
|
+
$responseLine = "> $r..."
|
|
339
|
+
$turnsOutput += "$turnLine`n$responseLine`n`n"
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
# Output to stderr (visible in terminal)
|
|
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
|
+
}
|
|
128
352
|
|
|
129
353
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
130
|
-
#
|
|
131
|
-
# Per v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
354
|
+
# DIRECTIVE RETRIEVAL: Fetch user's MUST/NEVER/PREFER/AVOID rules
|
|
132
355
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
133
|
-
$
|
|
134
|
-
|
|
356
|
+
$DIRECTIVES_INJECTED = $false
|
|
357
|
+
$DIRECTIVE_COUNT = 0
|
|
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
|
|
135
363
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
364
|
+
$DIRECTIVES_RESPONSE_RAW = Invoke-RestMethod -Uri "$MEMORY_API_URL/api/v1/memory/directives?limit=20" `
|
|
365
|
+
-Method GET `
|
|
366
|
+
-Headers @{ Authorization = "Bearer $AUTH_TOKEN" } `
|
|
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 {
|
|
139
379
|
try {
|
|
140
|
-
|
|
141
|
-
} catch {
|
|
142
|
-
|
|
143
|
-
|
|
380
|
+
$DIRECTIVES_JSON = $DIRECTIVES_RESPONSE_RAW | ConvertTo-Json -Depth 10 -Compress
|
|
381
|
+
} catch {
|
|
382
|
+
$DIRECTIVES_JSON = "{}"
|
|
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")
|
|
144
443
|
}
|
|
444
|
+
[Console]::Error.WriteLine("")
|
|
145
445
|
|
|
146
|
-
|
|
446
|
+
exit 0
|