@ekkos/cli 0.2.9 → 0.2.10

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.
Files changed (35) hide show
  1. package/dist/cache/LocalSessionStore.d.ts +34 -21
  2. package/dist/cache/LocalSessionStore.js +169 -53
  3. package/dist/cache/capture.d.ts +19 -11
  4. package/dist/cache/capture.js +243 -76
  5. package/dist/cache/types.d.ts +14 -1
  6. package/dist/commands/doctor.d.ts +10 -0
  7. package/dist/commands/doctor.js +148 -73
  8. package/dist/commands/hooks.d.ts +109 -0
  9. package/dist/commands/hooks.js +668 -0
  10. package/dist/commands/run.d.ts +1 -0
  11. package/dist/commands/run.js +69 -21
  12. package/dist/index.js +42 -1
  13. package/dist/restore/RestoreOrchestrator.d.ts +17 -3
  14. package/dist/restore/RestoreOrchestrator.js +64 -22
  15. package/dist/utils/paths.d.ts +125 -0
  16. package/dist/utils/paths.js +283 -0
  17. package/package.json +1 -1
  18. package/templates/ekkos-manifest.json +223 -0
  19. package/templates/helpers/json-parse.cjs +101 -0
  20. package/templates/hooks/assistant-response.ps1 +256 -0
  21. package/templates/hooks/assistant-response.sh +124 -64
  22. package/templates/hooks/session-start.ps1 +107 -2
  23. package/templates/hooks/session-start.sh +201 -166
  24. package/templates/hooks/stop.ps1 +124 -3
  25. package/templates/hooks/stop.sh +470 -843
  26. package/templates/hooks/user-prompt-submit.ps1 +107 -22
  27. package/templates/hooks/user-prompt-submit.sh +403 -393
  28. package/templates/project-stubs/session-start.ps1 +63 -0
  29. package/templates/project-stubs/session-start.sh +55 -0
  30. package/templates/project-stubs/stop.ps1 +63 -0
  31. package/templates/project-stubs/stop.sh +55 -0
  32. package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
  33. package/templates/project-stubs/user-prompt-submit.sh +55 -0
  34. package/templates/shared/hooks-enabled.json +22 -0
  35. package/templates/shared/session-words.json +45 -0
@@ -1,10 +1,90 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
2
  # ekkOS_ Hook: SessionStart - Initialize session (Windows)
3
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
+ # EKKOS_MANAGED=1
5
+ # EKKOS_MANIFEST_SHA256=<computed-at-build>
6
+ # EKKOS_TEMPLATE_VERSION=1.0.0
7
+ #
8
+ # Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
9
+ # - All persisted records MUST include: instanceId, sessionId, sessionName
10
+ # - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
3
11
  # ═══════════════════════════════════════════════════════════════════════════
4
12
 
5
13
  $ErrorActionPreference = "SilentlyContinue"
6
14
 
7
- # Read input
15
+ # ═══════════════════════════════════════════════════════════════════════════
16
+ # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
17
+ # ═══════════════════════════════════════════════════════════════════════════
18
+ $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
19
+ $SessionWordsJson = "$EkkosConfigDir\session-words.json"
20
+ $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
21
+
22
+ # ═══════════════════════════════════════════════════════════════════════════
23
+ # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
24
+ # ═══════════════════════════════════════════════════════════════════════════
25
+ $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
26
+
27
+ # ═══════════════════════════════════════════════════════════════════════════
28
+ # Load session words from JSON file - NO HARDCODED ARRAYS
29
+ # ═══════════════════════════════════════════════════════════════════════════
30
+ $script:SessionWords = $null
31
+
32
+ function Load-SessionWords {
33
+ $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
+
43
+ try {
44
+ $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
45
+ } catch {
46
+ return $null
47
+ }
48
+ }
49
+
50
+ function Convert-UuidToWords {
51
+ param([string]$uuid)
52
+
53
+ if (-not $script:SessionWords) {
54
+ Load-SessionWords
55
+ }
56
+
57
+ if (-not $script:SessionWords) {
58
+ return "unknown-session"
59
+ }
60
+
61
+ $adjectives = $script:SessionWords.adjectives
62
+ $nouns = $script:SessionWords.nouns
63
+ $verbs = $script:SessionWords.verbs
64
+
65
+ if (-not $adjectives -or -not $nouns -or -not $verbs) {
66
+ return "unknown-session"
67
+ }
68
+
69
+ if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
70
+
71
+ $clean = $uuid -replace "-", ""
72
+ if ($clean.Length -lt 6) { return "unknown-session" }
73
+
74
+ try {
75
+ $a = [Convert]::ToInt32($clean.Substring(0,2), 16) % $adjectives.Length
76
+ $n = [Convert]::ToInt32($clean.Substring(2,2), 16) % $nouns.Length
77
+ $an = [Convert]::ToInt32($clean.Substring(4,2), 16) % $verbs.Length
78
+
79
+ return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
80
+ } catch {
81
+ return "unknown-session"
82
+ }
83
+ }
84
+
85
+ # ═══════════════════════════════════════════════════════════════════════════
86
+ # READ INPUT
87
+ # ═══════════════════════════════════════════════════════════════════════════
8
88
  $inputJson = [Console]::In.ReadToEnd()
9
89
 
10
90
  try {
@@ -14,7 +94,11 @@ try {
14
94
  $sessionId = "unknown"
15
95
  }
16
96
 
17
- # Initialize state directory
97
+ $sessionName = Convert-UuidToWords $sessionId
98
+
99
+ # ═══════════════════════════════════════════════════════════════════════════
100
+ # INITIALIZE STATE DIRECTORY
101
+ # ═══════════════════════════════════════════════════════════════════════════
18
102
  $stateDir = Join-Path $env:USERPROFILE ".claude\state"
19
103
  if (-not (Test-Path $stateDir)) {
20
104
  New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
@@ -25,6 +109,8 @@ $stateFile = Join-Path $stateDir "hook-state.json"
25
109
  $state = @{
26
110
  turn = 0
27
111
  session_id = $sessionId
112
+ session_name = $sessionName
113
+ instance_id = $EkkosInstanceId
28
114
  started_at = (Get-Date).ToString("o")
29
115
  } | ConvertTo-Json
30
116
 
@@ -34,8 +120,27 @@ Set-Content -Path $stateFile -Value $state -Force
34
120
  $sessionFile = Join-Path $stateDir "current-session.json"
35
121
  $sessionData = @{
36
122
  session_id = $sessionId
123
+ session_name = $sessionName
124
+ instance_id = $EkkosInstanceId
37
125
  } | ConvertTo-Json
38
126
 
39
127
  Set-Content -Path $sessionFile -Value $sessionData -Force
40
128
 
129
+ # ═══════════════════════════════════════════════════════════════════════════
130
+ # LOCAL CACHE: Initialize session in Tier 0 cache
131
+ # Per v1.2 ADDENDUM: Pass instanceId for namespacing
132
+ # ═══════════════════════════════════════════════════════════════════════════
133
+ $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
134
+ if ($captureCmd -and $sessionId -ne "unknown") {
135
+ try {
136
+ # NEW format: ekkos-capture init <instance_id> <session_id> <session_name>
137
+ Start-Job -ScriptBlock {
138
+ param($instanceId, $sessId, $sessName)
139
+ try {
140
+ & ekkos-capture init $instanceId $sessId $sessName 2>&1 | Out-Null
141
+ } catch {}
142
+ } -ArgumentList $EkkosInstanceId, $sessionId, $sessionName | Out-Null
143
+ } catch {}
144
+ }
145
+
41
146
  Write-Output "ekkOS session initialized"
@@ -1,6 +1,8 @@
1
1
  #!/bin/bash
2
2
  # ═══════════════════════════════════════════════════════════════════════════
3
3
  # ekkOS_ Hook: SessionStart - MINIMAL + AUTO-RESTORE + TIME MACHINE CONTINUE
4
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
5
+ # EKKOS_MANAGED=1
4
6
  # ═══════════════════════════════════════════════════════════════════════════
5
7
  # This hook does THREE things:
6
8
  # 1. Check for pending Time Machine "Continue from here" requests
@@ -15,6 +17,8 @@
15
17
  # FAST TRIM FLOW:
16
18
  # User runs /clear → session-start detects fresh session →
17
19
  # Checks L2 for recent turns → Auto-injects last 15 turns → Seamless continuity
20
+ #
21
+ # Per spec v1.2 Addendum: NO jq dependency
18
22
  # ═══════════════════════════════════════════════════════════════════════════
19
23
 
20
24
  set +e
@@ -22,10 +26,36 @@ set +e
22
26
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
27
  PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
24
28
 
29
+ # ═══════════════════════════════════════════════════════════════════════════
30
+ # CONFIG PATHS - Per spec v1.2 Addendum
31
+ # ═══════════════════════════════════════════════════════════════════════════
32
+ EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
33
+ JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
34
+
25
35
  INPUT=$(cat)
26
- SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
27
- TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
28
- SOURCE=$(echo "$INPUT" | jq -r '.source // "unknown"') # "startup", "resume", or "clear"
36
+
37
+ # Parse input using Node (no jq)
38
+ parse_json_value() {
39
+ local json="$1"
40
+ local path="$2"
41
+ echo "$json" | node -e "
42
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
43
+ const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
44
+ let result = data;
45
+ for (const p of path) {
46
+ if (result === undefined || result === null) { result = undefined; break; }
47
+ result = result[p];
48
+ }
49
+ if (result !== undefined && result !== null) console.log(result);
50
+ " 2>/dev/null || echo ""
51
+ }
52
+
53
+ SESSION_ID=$(parse_json_value "$INPUT" '.session_id')
54
+ [ -z "$SESSION_ID" ] && SESSION_ID="unknown"
55
+
56
+ TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
57
+ SOURCE=$(parse_json_value "$INPUT" '.source')
58
+ [ -z "$SOURCE" ] && SOURCE="unknown"
29
59
 
30
60
  # ═══════════════════════════════════════════════════════════════════════════
31
61
  # Load auth
@@ -34,13 +64,16 @@ EKKOS_CONFIG="$HOME/.ekkos/config.json"
34
64
  AUTH_TOKEN=""
35
65
  USER_ID=""
36
66
 
37
- if [ -f "$EKKOS_CONFIG" ]; then
38
- AUTH_TOKEN=$(jq -r '.hookApiKey // .apiKey // ""' "$EKKOS_CONFIG" 2>/dev/null || echo "")
39
- USER_ID=$(jq -r '.userId // ""' "$EKKOS_CONFIG" 2>/dev/null || echo "")
67
+ if [ -f "$EKKOS_CONFIG" ] && [ -f "$JSON_PARSE_HELPER" ]; then
68
+ AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.hookApiKey' 2>/dev/null || echo "")
69
+ if [ -z "$AUTH_TOKEN" ]; then
70
+ AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.apiKey' 2>/dev/null || echo "")
71
+ fi
72
+ USER_ID=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.userId' 2>/dev/null || echo "")
40
73
  fi
41
74
 
42
75
  if [ -z "$AUTH_TOKEN" ] && [ -f "$PROJECT_ROOT/.env.local" ]; then
43
- AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | tr -d '\r')
76
+ AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | tr -d '\r')
44
77
  fi
45
78
 
46
79
  [ -z "$AUTH_TOKEN" ] && exit 0
@@ -57,39 +90,55 @@ TIME_MACHINE_TO_TURN=""
57
90
 
58
91
  # Check via env var first, then API
59
92
  if [ -n "$RESTORE_REQUEST_ID" ]; then
60
- echo -e "\033[0;35mTime Machine request detected: $RESTORE_REQUEST_ID\033[0m" >&2
93
+ echo -e "\033[0;35m Time Machine request detected: $RESTORE_REQUEST_ID\033[0m" >&2
61
94
  fi
62
95
 
63
96
  # Check API for pending requests (if we have user_id)
64
97
  if [ -z "$TIME_MACHINE_SESSION" ] && [ -n "$USER_ID" ]; then
65
- PENDING_RESPONSE=$(curl -s -X GET "$MEMORY_API_URL/api/v1/context/restore-request/pending?user_id=$USER_ID" \
66
- -H "Authorization: Bearer $AUTH_TOKEN" \
67
- --connect-timeout 2 \
68
- --max-time 3 2>/dev/null || echo '{}')
69
-
70
- IS_PENDING=$(echo "$PENDING_RESPONSE" | jq -r '.pending // false' 2>/dev/null)
71
-
72
- if [ "$IS_PENDING" = "true" ]; then
73
- TIME_MACHINE_SESSION=$(echo "$PENDING_RESPONSE" | jq -r '.request.session_id // ""')
74
- TIME_MACHINE_FROM_TURN=$(echo "$PENDING_RESPONSE" | jq -r '.request.from_turn // ""')
75
- TIME_MACHINE_TO_TURN=$(echo "$PENDING_RESPONSE" | jq -r '.request.to_turn // ""')
76
- RESTORE_REQUEST_ID=$(echo "$PENDING_RESPONSE" | jq -r '.request.request_id // ""')
77
-
78
- if [ -n "$TIME_MACHINE_SESSION" ]; then
79
- echo "" >&2
80
- echo -e "\033[0;35m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m" >&2
81
- echo -e "\033[0;35m\033[1m⏰ TIME MACHINE\033[0m \033[2m| Restoring session from web request...\033[0m" >&2
82
- echo -e "\033[0;35m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m" >&2
83
-
84
- # Mark request as consumed
85
- curl -s -X POST "$MEMORY_API_URL/api/v1/context/restore-request/consume" \
98
+ PENDING_RESPONSE=$(curl -s -X GET "$MEMORY_API_URL/api/v1/context/restore-request/pending?user_id=$USER_ID" \
86
99
  -H "Authorization: Bearer $AUTH_TOKEN" \
87
- -H "Content-Type: application/json" \
88
- -d "{\"request_id\": \"$RESTORE_REQUEST_ID\"}" \
89
100
  --connect-timeout 2 \
90
- --max-time 3 >/dev/null 2>&1 || true
101
+ --max-time 3 2>/dev/null || echo '{}')
102
+
103
+ # Parse using Node (no jq)
104
+ IS_PENDING=$(echo "$PENDING_RESPONSE" | node -e "
105
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
106
+ console.log(data.pending || false);
107
+ " 2>/dev/null || echo "false")
108
+
109
+ if [ "$IS_PENDING" = "true" ]; then
110
+ TIME_MACHINE_SESSION=$(echo "$PENDING_RESPONSE" | node -e "
111
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
112
+ console.log(d.request?.session_id || '');
113
+ " 2>/dev/null || echo "")
114
+ TIME_MACHINE_FROM_TURN=$(echo "$PENDING_RESPONSE" | node -e "
115
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
116
+ console.log(d.request?.from_turn || '');
117
+ " 2>/dev/null || echo "")
118
+ TIME_MACHINE_TO_TURN=$(echo "$PENDING_RESPONSE" | node -e "
119
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
120
+ console.log(d.request?.to_turn || '');
121
+ " 2>/dev/null || echo "")
122
+ RESTORE_REQUEST_ID=$(echo "$PENDING_RESPONSE" | node -e "
123
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
124
+ console.log(d.request?.request_id || '');
125
+ " 2>/dev/null || echo "")
126
+
127
+ if [ -n "$TIME_MACHINE_SESSION" ]; then
128
+ echo "" >&2
129
+ echo -e "\033[0;35m------------------------------------------------------------------------\033[0m" >&2
130
+ echo -e "\033[0;35m\033[1m TIME MACHINE\033[0m \033[2m| Restoring session from web request...\033[0m" >&2
131
+ echo -e "\033[0;35m------------------------------------------------------------------------\033[0m" >&2
132
+
133
+ # Mark request as consumed
134
+ curl -s -X POST "$MEMORY_API_URL/api/v1/context/restore-request/consume" \
135
+ -H "Authorization: Bearer $AUTH_TOKEN" \
136
+ -H "Content-Type: application/json" \
137
+ -d "{\"request_id\": \"$RESTORE_REQUEST_ID\"}" \
138
+ --connect-timeout 2 \
139
+ --max-time 3 >/dev/null 2>&1 || true
140
+ fi
91
141
  fi
92
- fi
93
142
  fi
94
143
 
95
144
  # ═══════════════════════════════════════════════════════════════════════════
@@ -111,24 +160,24 @@ MOST_RECENT_SESSION=""
111
160
  SAVED_TURN_COUNT=0
112
161
 
113
162
  if [ -n "$CURRENT_SESSION_ID" ] && [ "$CURRENT_SESSION_ID" != "unknown" ]; then
114
- # Check if THIS session has saved turns (for /clear continuity)
115
- TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${CURRENT_SESSION_ID}.turn"
116
- if [ -f "$TURN_COUNTER_FILE" ]; then
117
- SAVED_TURN_COUNT=$(cat "$TURN_COUNTER_FILE" 2>/dev/null || echo "0")
118
- MOST_RECENT_SESSION="$CURRENT_SESSION_ID"
119
- else
120
- # Fresh start: find most recent session in project
121
- MOST_RECENT_FILE=$(ls -t "$PROJECT_SESSION_DIR"/*.turn 2>/dev/null | head -1)
122
- if [ -n "$MOST_RECENT_FILE" ]; then
123
- MOST_RECENT_SESSION=$(basename "$MOST_RECENT_FILE" .turn)
124
- SAVED_TURN_COUNT=$(cat "$MOST_RECENT_FILE" 2>/dev/null || echo "0")
163
+ # Check if THIS session has saved turns (for /clear continuity)
164
+ TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${CURRENT_SESSION_ID}.turn"
165
+ if [ -f "$TURN_COUNTER_FILE" ]; then
166
+ SAVED_TURN_COUNT=$(cat "$TURN_COUNTER_FILE" 2>/dev/null || echo "0")
167
+ MOST_RECENT_SESSION="$CURRENT_SESSION_ID"
168
+ else
169
+ # Fresh start: find most recent session in project
170
+ MOST_RECENT_FILE=$(ls -t "$PROJECT_SESSION_DIR"/*.turn 2>/dev/null | head -1)
171
+ if [ -n "$MOST_RECENT_FILE" ]; then
172
+ MOST_RECENT_SESSION=$(basename "$MOST_RECENT_FILE" .turn)
173
+ SAVED_TURN_COUNT=$(cat "$MOST_RECENT_FILE" 2>/dev/null || echo "0")
174
+ fi
125
175
  fi
126
- fi
127
176
  fi
128
177
 
129
178
  # Save current session info
130
179
  if [ -n "$CURRENT_SESSION_ID" ]; then
131
- cat > "$SESSION_FILE" << EOF
180
+ cat > "$SESSION_FILE" << EOF
132
181
  {
133
182
  "session_id": "$CURRENT_SESSION_ID",
134
183
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
@@ -138,28 +187,23 @@ EOF
138
187
  fi
139
188
 
140
189
  # ═══════════════════════════════════════════════════════════════════════════
141
- # 🔄 GOLDEN LOOP: Initialize session tracking file
190
+ # GOLDEN LOOP: Initialize session tracking file
142
191
  # ═══════════════════════════════════════════════════════════════════════════
143
192
  GOLDEN_LOOP_FILE="$PROJECT_ROOT/.ekkos/golden-loop-current.json"
144
193
  mkdir -p "$PROJECT_ROOT/.ekkos" 2>/dev/null || true
145
194
 
146
- # Initialize with session start state
147
- jq -n \
148
- --arg phase "idle" \
149
- --argjson turn 0 \
150
- --arg session "${CURRENT_SESSION_ID}" \
151
- --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
152
- '{
153
- phase: $phase,
154
- turn: $turn,
155
- session: $session,
156
- timestamp: $timestamp,
157
- stats: {
158
- retrieved: 0,
159
- applied: 0,
160
- forged: 0
161
- }
162
- }' > "$GOLDEN_LOOP_FILE" 2>/dev/null || true
195
+ # Initialize with session start state using Node (no jq)
196
+ node -e "
197
+ const fs = require('fs');
198
+ const data = {
199
+ phase: 'idle',
200
+ turn: 0,
201
+ session: '$CURRENT_SESSION_ID',
202
+ timestamp: new Date().toISOString(),
203
+ stats: { retrieved: 0, applied: 0, forged: 0 }
204
+ };
205
+ fs.writeFileSync('$GOLDEN_LOOP_FILE', JSON.stringify(data, null, 2));
206
+ " 2>/dev/null || true
163
207
 
164
208
  # ═══════════════════════════════════════════════════════════════════════════
165
209
  # COLORS
@@ -185,44 +229,56 @@ RESET='\033[0m'
185
229
 
186
230
  # Handle Time Machine requests (explicit user action)
187
231
  if [ -n "$TIME_MACHINE_SESSION" ]; then
188
- echo "" >&2
189
- echo -e "${MAGENTA}${BOLD}⏰ TIME MACHINE${RESET} ${DIM}| Restoring past session: ${TIME_MACHINE_SESSION:0:12}...${RESET}" >&2
190
-
191
- # Build recall request with turn range
192
- RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"last_n\": 15, \"format\": \"summary\"}"
193
-
194
- if [ -n "$TIME_MACHINE_FROM_TURN" ] && [ -n "$TIME_MACHINE_TO_TURN" ]; then
195
- RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"from_turn\": ${TIME_MACHINE_FROM_TURN}, \"to_turn\": ${TIME_MACHINE_TO_TURN}, \"format\": \"summary\"}"
196
- elif [ -n "$TIME_MACHINE_FROM_TURN" ]; then
197
- RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"from_turn\": ${TIME_MACHINE_FROM_TURN}, \"format\": \"summary\"}"
198
- fi
199
-
200
- # Fetch turns from L2
201
- RESTORE_RESPONSE=$(curl -s -X POST "$MEMORY_API_URL/api/v1/turns/recall" \
202
- -H "Authorization: Bearer $AUTH_TOKEN" \
203
- -H "Content-Type: application/json" \
204
- -d "$RECALL_BODY" \
205
- --connect-timeout 3 \
206
- --max-time 5 2>/dev/null || echo '{"error":"timeout"}')
207
-
208
- # Check if we got turns back
209
- RESTORED_COUNT=$(echo "$RESTORE_RESPONSE" | jq '.turns // [] | length' 2>/dev/null || echo "0")
210
-
211
- if [ "$RESTORED_COUNT" -gt 0 ]; then
212
- echo -e "${MAGENTA} ✓${RESET} Restored ${RESTORED_COUNT} turns from past session" >&2
213
- echo "" >&2
214
- echo -e "${MAGENTA}${BOLD}## Time Machine Context${RESET}" >&2
215
232
  echo "" >&2
233
+ echo -e "${MAGENTA}${BOLD} TIME MACHINE${RESET} ${DIM}| Restoring past session: ${TIME_MACHINE_SESSION:0:12}...${RESET}" >&2
216
234
 
217
- # Output the turns as context (stderr for user, stdout for Claude)
218
- TURNS_OUTPUT=$(echo "$RESTORE_RESPONSE" | jq -r '.turns[]? | "**Turn \(.turn_number // "?")**: \(.user_query[:100] // "...")...\n> \(.assistant_response[:200] // "...")...\n"' 2>/dev/null || true)
219
- echo "$TURNS_OUTPUT" >&2
220
- echo "$TURNS_OUTPUT" # Also to stdout for Claude's context
235
+ # Build recall request with turn range
236
+ RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"last_n\": 15, \"format\": \"summary\"}"
221
237
 
222
- echo "" >&2
223
- echo -e "${DIM}You've traveled to a past session. Continue from here!${RESET}" >&2
224
- echo "" >&2
225
- fi
238
+ if [ -n "$TIME_MACHINE_FROM_TURN" ] && [ -n "$TIME_MACHINE_TO_TURN" ]; then
239
+ RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"from_turn\": ${TIME_MACHINE_FROM_TURN}, \"to_turn\": ${TIME_MACHINE_TO_TURN}, \"format\": \"summary\"}"
240
+ elif [ -n "$TIME_MACHINE_FROM_TURN" ]; then
241
+ RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"from_turn\": ${TIME_MACHINE_FROM_TURN}, \"format\": \"summary\"}"
242
+ fi
243
+
244
+ # Fetch turns from L2
245
+ RESTORE_RESPONSE=$(curl -s -X POST "$MEMORY_API_URL/api/v1/turns/recall" \
246
+ -H "Authorization: Bearer $AUTH_TOKEN" \
247
+ -H "Content-Type: application/json" \
248
+ -d "$RECALL_BODY" \
249
+ --connect-timeout 3 \
250
+ --max-time 5 2>/dev/null || echo '{"error":"timeout"}')
251
+
252
+ # Check if we got turns back using Node (no jq)
253
+ RESTORED_COUNT=$(echo "$RESTORE_RESPONSE" | node -e "
254
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
255
+ console.log((d.turns || []).length);
256
+ " 2>/dev/null || echo "0")
257
+
258
+ if [ "$RESTORED_COUNT" -gt 0 ]; then
259
+ echo -e "${MAGENTA} ✓${RESET} Restored ${RESTORED_COUNT} turns from past session" >&2
260
+ echo "" >&2
261
+ echo -e "${MAGENTA}${BOLD}## Time Machine Context${RESET}" >&2
262
+ echo "" >&2
263
+
264
+ # Output the turns as context using Node (no jq)
265
+ TURNS_OUTPUT=$(echo "$RESTORE_RESPONSE" | node -e "
266
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
267
+ (d.turns || []).forEach(t => {
268
+ const q = (t.user_query || '').substring(0, 100);
269
+ const r = (t.assistant_response || '').substring(0, 200);
270
+ console.log('**Turn ' + (t.turn_number || '?') + '**: ' + q + '...');
271
+ console.log('> ' + r + '...');
272
+ console.log('');
273
+ });
274
+ " 2>/dev/null || echo "")
275
+ echo "$TURNS_OUTPUT" >&2
276
+ echo "$TURNS_OUTPUT"
277
+
278
+ echo "" >&2
279
+ echo -e "${DIM}You've traveled to a past session. Continue from here!${RESET}" >&2
280
+ echo "" >&2
281
+ fi
226
282
  fi
227
283
 
228
284
  # ═══════════════════════════════════════════════════════════════════════════
@@ -233,85 +289,64 @@ DIRECTIVE_COUNT=0
233
289
 
234
290
  # Only fetch if we have auth
235
291
  if [ -n "$AUTH_TOKEN" ]; then
236
- # Fetch directives (top 20 by priority to avoid token bloat)
237
- DIRECTIVES_RESPONSE=$(curl -s -X GET "$MEMORY_API_URL/api/v1/memory/directives?limit=20" \
238
- -H "Authorization: Bearer $AUTH_TOKEN" \
239
- --connect-timeout 2 \
240
- --max-time 3 2>/dev/null || echo '{}')
241
-
242
- # Parse response
243
- DIRECTIVE_COUNT=$(echo "$DIRECTIVES_RESPONSE" | jq '.count // 0' 2>/dev/null || echo "0")
244
-
245
- if [ "$DIRECTIVE_COUNT" -gt 0 ]; then
246
- DIRECTIVES_INJECTED=true
247
-
248
- # Extract MUST/NEVER/PREFER/AVOID arrays
249
- MUST_RULES=$(echo "$DIRECTIVES_RESPONSE" | jq -r '.MUST[]?.rule // empty' 2>/dev/null | head -5)
250
- NEVER_RULES=$(echo "$DIRECTIVES_RESPONSE" | jq -r '.NEVER[]?.rule // empty' 2>/dev/null | head -5)
251
- PREFER_RULES=$(echo "$DIRECTIVES_RESPONSE" | jq -r '.PREFER[]?.rule // empty' 2>/dev/null | head -5)
252
- AVOID_RULES=$(echo "$DIRECTIVES_RESPONSE" | jq -r '.AVOID[]?.rule // empty' 2>/dev/null | head -5)
253
-
254
- # Build compact directive block for injection
255
- echo "<system-reminder>"
256
- echo "USER DIRECTIVES (FOLLOW THESE):"
257
- echo ""
258
-
259
- if [ -n "$MUST_RULES" ]; then
260
- echo "MUST:"
261
- echo "$MUST_RULES" | while read -r rule; do
262
- [ -n "$rule" ] && echo " • $rule"
263
- done
264
- fi
265
-
266
- if [ -n "$NEVER_RULES" ]; then
267
- echo "NEVER:"
268
- echo "$NEVER_RULES" | while read -r rule; do
269
- [ -n "$rule" ] && echo " • $rule"
270
- done
271
- fi
272
-
273
- if [ -n "$PREFER_RULES" ]; then
274
- echo "PREFER:"
275
- echo "$PREFER_RULES" | while read -r rule; do
276
- [ -n "$rule" ] && echo " • $rule"
277
- done
278
- fi
292
+ # Fetch directives (top 20 by priority to avoid token bloat)
293
+ DIRECTIVES_RESPONSE=$(curl -s -X GET "$MEMORY_API_URL/api/v1/memory/directives?limit=20" \
294
+ -H "Authorization: Bearer $AUTH_TOKEN" \
295
+ --connect-timeout 2 \
296
+ --max-time 3 2>/dev/null || echo '{}')
297
+
298
+ # Parse response using Node (no jq)
299
+ DIRECTIVE_COUNT=$(echo "$DIRECTIVES_RESPONSE" | node -e "
300
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
301
+ console.log(d.count || 0);
302
+ " 2>/dev/null || echo "0")
303
+
304
+ if [ "$DIRECTIVE_COUNT" -gt 0 ]; then
305
+ DIRECTIVES_INJECTED=true
306
+
307
+ # Extract MUST/NEVER/PREFER/AVOID arrays using Node
308
+ echo "<system-reminder>"
309
+ echo "USER DIRECTIVES (FOLLOW THESE):"
310
+ echo ""
311
+
312
+ echo "$DIRECTIVES_RESPONSE" | node -e "
313
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
314
+ const types = ['MUST', 'NEVER', 'PREFER', 'AVOID'];
315
+ types.forEach(type => {
316
+ const rules = (d[type] || []).slice(0, 5);
317
+ if (rules.length > 0) {
318
+ console.log(type + ':');
319
+ rules.forEach(r => console.log(' - ' + (r.rule || '')));
320
+ }
321
+ });
322
+ " 2>/dev/null || true
279
323
 
280
- if [ -n "$AVOID_RULES" ]; then
281
- echo "AVOID:"
282
- echo "$AVOID_RULES" | while read -r rule; do
283
- [ -n "$rule" ] && echo " • $rule"
284
- done
324
+ echo "</system-reminder>"
325
+ echo -e "${GREEN} ${DIRECTIVE_COUNT} directives loaded${RESET}" >&2
285
326
  fi
286
-
287
- echo "</system-reminder>"
288
-
289
- echo -e "${GREEN}📋 ${DIRECTIVE_COUNT} directives loaded${RESET}" >&2
290
- fi
291
327
  fi
292
328
 
293
329
  # Simple status display (no auto-restore)
294
330
  if [ "$SAVED_TURN_COUNT" -gt 0 ]; then
295
- # New session or few turns - just show status
296
- echo "" >&2
297
- if [ "$SAVED_TURN_COUNT" -gt 0 ]; then
298
- echo -e "${CYAN}${BOLD}🧠 ekkOS${RESET} ${DIM}|${RESET} Session: ${CURRENT_SESSION_ID:-$SESSION_ID} ${DIM}|${RESET} ${GREEN}${SAVED_TURN_COUNT} turns${RESET}" >&2
299
- else
300
- echo -e "${CYAN}${BOLD}🧠 ekkOS${RESET} ${DIM}|${RESET} Session: ${CURRENT_SESSION_ID:-$SESSION_ID} ${DIM}| New session${RESET}" >&2
301
- fi
331
+ echo "" >&2
332
+ if [ "$SAVED_TURN_COUNT" -gt 0 ]; then
333
+ echo -e "${CYAN}${BOLD} ekkOS${RESET} ${DIM}|${RESET} Session: ${CURRENT_SESSION_ID:-$SESSION_ID} ${DIM}|${RESET} ${GREEN}${SAVED_TURN_COUNT} turns${RESET}" >&2
334
+ else
335
+ echo -e "${CYAN}${BOLD} ekkOS${RESET} ${DIM}|${RESET} Session: ${CURRENT_SESSION_ID:-$SESSION_ID} ${DIM}| New session${RESET}" >&2
336
+ fi
302
337
  fi
303
338
 
304
339
  # Final confirmation that's always visible
305
340
  if [ -n "$TIME_MACHINE_SESSION" ]; then
306
- echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
307
- echo -e "${MAGENTA}⏰${RESET} Time Machine active · Restored from session ${TIME_MACHINE_SESSION:0:12}..." >&2
308
- echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
341
+ echo -e "${MAGENTA}------------------------------------------------------------------------${RESET}" >&2
342
+ echo -e "${MAGENTA} ${RESET} Time Machine active - Restored from session ${TIME_MACHINE_SESSION:0:12}..." >&2
343
+ echo -e "${MAGENTA}------------------------------------------------------------------------${RESET}" >&2
309
344
  elif [ "$SAVED_TURN_COUNT" -gt 0 ]; then
310
- echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
311
- echo -e "${GREEN}✓${RESET} Session continued · ${SAVED_TURN_COUNT} turns preserved · Ready to resume" >&2
312
- echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
345
+ echo -e "${GREEN}------------------------------------------------------------------------${RESET}" >&2
346
+ echo -e "${GREEN}✓${RESET} Session continued - ${SAVED_TURN_COUNT} turns preserved - Ready to resume" >&2
347
+ echo -e "${GREEN}------------------------------------------------------------------------${RESET}" >&2
313
348
  else
314
- echo -e "${CYAN}✓${RESET} New session started" >&2
349
+ echo -e "${CYAN}✓${RESET} New session started" >&2
315
350
  fi
316
351
  echo "" >&2
317
352