@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.
Files changed (36) hide show
  1. package/README.md +57 -0
  2. package/dist/commands/dashboard.js +561 -186
  3. package/dist/deploy/settings.js +13 -26
  4. package/package.json +2 -4
  5. package/templates/CLAUDE.md +135 -23
  6. package/templates/ekkos-manifest.json +8 -8
  7. package/templates/hooks/assistant-response.ps1 +256 -160
  8. package/templates/hooks/assistant-response.sh +130 -66
  9. package/templates/hooks/hooks.json +24 -6
  10. package/templates/hooks/lib/contract.sh +43 -31
  11. package/templates/hooks/lib/count-tokens.cjs +0 -0
  12. package/templates/hooks/lib/ekkos-reminders.sh +0 -0
  13. package/templates/hooks/lib/state.sh +53 -1
  14. package/templates/hooks/session-start.ps1 +91 -391
  15. package/templates/hooks/session-start.sh +201 -166
  16. package/templates/hooks/stop.ps1 +202 -341
  17. package/templates/hooks/stop.sh +275 -948
  18. package/templates/hooks/user-prompt-submit.ps1 +224 -548
  19. package/templates/hooks/user-prompt-submit.sh +382 -456
  20. package/templates/plan-template.md +0 -0
  21. package/templates/spec-template.md +0 -0
  22. package/templates/windsurf-hooks/hooks.json +9 -2
  23. package/templates/windsurf-hooks/install.sh +0 -0
  24. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  25. package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
  26. package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
  27. package/templates/agents/README.md +0 -182
  28. package/templates/agents/code-reviewer.md +0 -166
  29. package/templates/agents/debug-detective.md +0 -169
  30. package/templates/agents/ekkOS_Vercel.md +0 -99
  31. package/templates/agents/extension-manager.md +0 -229
  32. package/templates/agents/git-companion.md +0 -185
  33. package/templates/agents/github-test-agent.md +0 -321
  34. package/templates/agents/railway-manager.md +0 -179
  35. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
  36. package/templates/windsurf-skills/ekkos-memory/SKILL.md +0 -219
@@ -10,207 +10,126 @@
10
10
 
11
11
  set +e
12
12
 
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
15
+
13
16
  # ═══════════════════════════════════════════════════════════════════════════
14
- # SESSION NAME GENERATION (must be defined early - used by ekkos-capture)
15
- # Format: adj-noun-verb (e.g., "cosmic-penguin-runs")
16
- # 100 × 100 × 100 = 1,000,000 combinations
17
+ # CONFIG PATHS - No jq dependency (v1.2 spec)
18
+ # Session words live in ~/.ekkos/ so they work in ANY project
17
19
  # ═══════════════════════════════════════════════════════════════════════════
18
- ADJECTIVES=(
19
- "cosmic" "turbo" "mega" "hyper" "quantum" "atomic" "stellar" "epic"
20
- "mighty" "groovy" "zippy" "snappy" "jazzy" "funky" "zesty" "peppy"
21
- "spicy" "crispy" "fluffy" "sparkly" "chunky" "bouncy" "bubbly" "sassy"
22
- "slick" "sleek" "bold" "nifty" "perky" "plucky" "witty" "nimble"
23
- "dapper" "fancy" "quirky" "punchy" "swift" "brave" "clever" "dandy"
24
- "eager" "fiery" "golden" "hasty" "icy" "jolly" "keen" "lively"
25
- "merry" "noble" "odd" "plush" "quick" "royal" "silly" "tidy"
26
- "ultra" "vivid" "wacky" "zany" "alpha" "beta" "cyber" "delta"
27
- "electric" "foggy" "giga" "hazy" "ionic" "jumpy" "kinky" "lunar"
28
- "magic" "nerdy" "omega" "pixel" "quaint" "retro" "solar" "techno"
29
- "unified" "viral" "wonky" "xerox" "yappy" "zen" "agile" "binary"
30
- "chrome" "disco" "elastic" "fizzy" "glossy" "humble" "itchy" "jiffy"
31
- "kooky" "loopy" "moody" "noisy"
32
- )
33
- NOUNS=(
34
- "penguin" "panda" "otter" "narwhal" "alpaca" "llama" "badger" "walrus"
35
- "waffle" "pickle" "noodle" "pretzel" "muffin" "taco" "nugget" "biscuit"
36
- "rocket" "comet" "nebula" "quasar" "meteor" "photon" "pulsar" "nova"
37
- "ninja" "pirate" "wizard" "robot" "yeti" "phoenix" "sphinx" "kraken"
38
- "thunder" "blizzard" "tornado" "avalanche" "mango" "kiwi" "banana" "coconut"
39
- "donut" "espresso" "falafel" "gyro" "hummus" "icecream" "jambon" "kebab"
40
- "latte" "mocha" "nachos" "olive" "pasta" "quinoa" "ramen" "sushi"
41
- "tamale" "udon" "velvet" "wasabi" "xmas" "yogurt" "ziti" "anchor"
42
- "beacon" "canyon" "drifter" "echo" "falcon" "glacier" "harbor" "island"
43
- "jetpack" "kayak" "lagoon" "meadow" "nebula" "orbit" "parrot" "quest"
44
- "rapids" "summit" "tunnel" "umbrella" "volcano" "whisper" "xylophone" "yacht"
45
- "zephyr" "acorn" "bobcat" "cactus" "dolphin" "eagle" "ferret" "gopher"
46
- "hedgehog" "iguana" "jackal" "koala"
47
- )
48
- VERBS=(
49
- "runs" "jumps" "flies" "swims" "dives" "soars" "glides" "dashes"
50
- "zooms" "zips" "spins" "twirls" "bounces" "floats" "drifts" "sails"
51
- "climbs" "leaps" "hops" "skips" "rolls" "slides" "surfs" "rides"
52
- "builds" "creates" "forges" "shapes" "crafts" "designs" "codes" "types"
53
- "thinks" "dreams" "learns" "grows" "blooms" "shines" "glows" "sparks"
54
- "sings" "hums" "calls" "beeps" "clicks" "taps" "pings" "chimes"
55
- "wins" "leads" "helps" "saves" "guards" "shields" "heals" "fixes"
56
- "starts" "begins" "launches" "ignites" "blazes" "flares" "bursts" "pops"
57
- "waves" "nods" "winks" "grins" "smiles" "laughs" "cheers" "claps"
58
- "seeks" "finds" "spots" "tracks" "hunts" "chases" "catches" "grabs"
59
- "pushes" "pulls" "lifts" "throws" "kicks" "punts" "bats" "swings"
60
- "reads" "writes" "draws" "paints" "sculpts" "carves" "molds" "weaves"
61
- "cooks" "bakes" "grills" "fries"
62
- )
20
+ EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
21
+ SESSION_WORDS_JSON="$EKKOS_CONFIG_DIR/session-words.json"
22
+ SESSION_WORDS_DEFAULT="$EKKOS_CONFIG_DIR/.defaults/session-words.json"
23
+ JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
63
24
 
64
- uuid_to_words() {
65
- local uuid="$1"
66
- local hex="${uuid//-/}"
67
- hex="${hex:0:12}"
68
- if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
69
- echo "unknown-session-starts"
70
- return
71
- fi
72
- local adj_seed=$((16#${hex:0:4}))
73
- local noun_seed=$((16#${hex:4:4}))
74
- local verb_seed=$((16#${hex:8:4}))
75
- local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
76
- local noun_idx=$((noun_seed % ${#NOUNS[@]}))
77
- local verb_idx=$((verb_seed % ${#VERBS[@]}))
78
- echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
25
+ # ═══════════════════════════════════════════════════════════════════════════
26
+ # JSON PARSING HELPER - No jq required
27
+ # ═══════════════════════════════════════════════════════════════════════════
28
+ parse_json_value() {
29
+ local json="$1"
30
+ local path="$2"
31
+ echo "$json" | node -e "
32
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
33
+ const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
34
+ let result = data;
35
+ for (const p of path) {
36
+ if (result === undefined || result === null) { result = undefined; break; }
37
+ result = result[p];
38
+ }
39
+ if (result !== undefined && result !== null) console.log(result);
40
+ " 2>/dev/null || echo ""
79
41
  }
80
-
81
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
82
- PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
83
42
 
84
43
  INPUT=$(cat)
85
- USER_QUERY=$(echo "$INPUT" | jq -r '.query // .message // .prompt // ""')
86
- RAW_SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
87
- TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
44
+ USER_QUERY=$(parse_json_value "$INPUT" '.query')
45
+ [ -z "$USER_QUERY" ] || [ "$USER_QUERY" = "null" ] && USER_QUERY=$(parse_json_value "$INPUT" '.message')
46
+ [ -z "$USER_QUERY" ] || [ "$USER_QUERY" = "null" ] && USER_QUERY=$(parse_json_value "$INPUT" '.prompt')
47
+ RAW_SESSION_ID=$(parse_json_value "$INPUT" '.session_id')
48
+ [ -z "$RAW_SESSION_ID" ] && RAW_SESSION_ID="unknown"
49
+ TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
88
50
 
89
51
  [ -z "$USER_QUERY" ] || [ "$USER_QUERY" = "null" ] && exit 0
90
52
 
91
53
  # Fallback: read session_id from saved state if not in INPUT
92
54
  if [ "$RAW_SESSION_ID" = "unknown" ] || [ "$RAW_SESSION_ID" = "null" ] || [ -z "$RAW_SESSION_ID" ]; then
93
55
  STATE_FILE="$HOME/.claude/state/current-session.json"
94
- if [ -f "$STATE_FILE" ]; then
95
- RAW_SESSION_ID=$(jq -r '.session_id // "unknown"' "$STATE_FILE" 2>/dev/null || echo "unknown")
56
+ if [ -f "$STATE_FILE" ] && [ -f "$JSON_PARSE_HELPER" ]; then
57
+ RAW_SESSION_ID=$(node "$JSON_PARSE_HELPER" "$STATE_FILE" '.session_id' 2>/dev/null || echo "unknown")
58
+ fi
59
+
60
+ # VSCode extension fallback: Extract session ID from transcript path
61
+ # Path format: ~/.claude/projects/<project>/<session-uuid>.jsonl
62
+ if [ "$RAW_SESSION_ID" = "unknown" ] && [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
63
+ RAW_SESSION_ID=$(basename "$TRANSCRIPT_PATH" .jsonl)
96
64
  fi
97
65
  fi
98
66
 
99
67
  # ═══════════════════════════════════════════════════════════════════════════
100
- # INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
101
- # Detects ALL applicable skills/tools and injects as system reminder
68
+ # SKILL AUTO-FIRE: Detect keywords and inject skill reminders
102
69
  # ═══════════════════════════════════════════════════════════════════════════
103
- SKILL_REMINDERS=()
70
+ SKILL_REMINDER=""
104
71
  QUERY_LOWER=$(echo "$USER_QUERY" | tr '[:upper:]' '[:lower:]')
105
72
 
106
- # ─────────────────────────────────────────────────────────────────────────────
107
- # MANDATORY TRIGGERS (Always check ekkOS first)
108
- # ─────────────────────────────────────────────────────────────────────────────
109
-
110
- # Memory First - Debug/Error/Problem solving
111
- if echo "$QUERY_LOWER" | grep -qE '(how do i|debug|error|bug|fix|not working|broken|fails|issue|problem|wrong|crash)'; then
112
- SKILL_REMINDERS+=("🔧 SKILL REQUIRED: Call Skill(skill: \"ekkOS_Memory_First\") FIRST before debugging")
73
+ # Memory First triggers
74
+ if echo "$QUERY_LOWER" | grep -qE '(how do i|debug|error|bug|fix|not working|broken|fails|issue)'; then
75
+ SKILL_REMINDER="🔧 SKILL REQUIRED: Call Skill(skill: \"ekkOS_Memory_First\") FIRST before debugging"
113
76
  fi
114
77
 
115
- # ─────────────────────────────────────────────────────────────────────────────
116
- # RECALL TRIGGERS (Time-based memory)
117
- # ─────────────────────────────────────────────────────────────────────────────
118
- if echo "$QUERY_LOWER" | grep -qE '(yesterday|last week|last month|remember when|what did we|where did we leave|before|earlier|previous|ago)'; then
119
- SKILL_REMINDERS+=("📅 SKILL REQUIRED: Call Skill(skill: \"ekkOS_Deep_Recall\") for time-based memory")
78
+ # Recall triggers
79
+ if echo "$QUERY_LOWER" | grep -qE '(yesterday|last week|remember when|what did we|where did we leave)'; then
80
+ SKILL_REMINDER="📅 SKILL REQUIRED: Call Skill(skill: \"ekkOS_Recall\") for time-based memory"
120
81
  fi
121
82
 
122
- # ─────────────────────────────────────────────────────────────────────────────
123
- # DIRECTIVE TRIGGERS (User preferences)
124
- # ─────────────────────────────────────────────────────────────────────────────
125
- if echo "$QUERY_LOWER" | grep -qE '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on)'; then
126
- SKILL_REMINDERS+=("⚙️ SKILL REQUIRED: Call Skill(skill: \"ekkOS_Preferences\") to capture directive")
83
+ # Preferences triggers
84
+ if echo "$QUERY_LOWER" | grep -qE '(always |never |i prefer|dont |don.t |avoid )'; then
85
+ SKILL_REMINDER="⚙️ SKILL REQUIRED: Call Skill(skill: \"ekkOS_Preferences\") to capture directive"
127
86
  fi
128
87
 
129
- # ─────────────────────────────────────────────────────────────────────────────
130
- # SAFETY TRIGGERS (Destructive actions)
131
- # ─────────────────────────────────────────────────────────────────────────────
132
- if echo "$QUERY_LOWER" | grep -qE '(delete|drop |rm -rf|deploy|push.*main|push.*master|production|migrate|rollback)'; then
133
- SKILL_REMINDERS+=("⚠️ SAFETY REQUIRED: Call ekkOS_Conflict before this destructive action")
88
+ # Safety triggers
89
+ if echo "$QUERY_LOWER" | grep -qE '(delete|drop |rm -rf|deploy|push.*main|push.*master)'; then
90
+ SKILL_REMINDER="⚠️ SKILL REQUIRED: Call Skill(skill: \"ekkOS_Safety\") before destructive action"
134
91
  fi
135
92
 
136
- # ─────────────────────────────────────────────────────────────────────────────
137
- # SCHEMA TRIGGERS (Database operations)
138
- # ─────────────────────────────────────────────────────────────────────────────
139
- if echo "$QUERY_LOWER" | grep -qE '(sql|query|supabase|prisma|database|table|column|select |insert |update |where )'; then
140
- SKILL_REMINDERS+=("🗄️ SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names")
141
- fi
142
-
143
- # ─────────────────────────────────────────────────────────────────────────────
144
- # SECRET TRIGGERS (API keys, credentials)
145
- # ─────────────────────────────────────────────────────────────────────────────
146
- if echo "$QUERY_LOWER" | grep -qE '(api key|token|password|credential|secret|my.*key|store.*key)'; then
147
- SKILL_REMINDERS+=("🔐 SECRETS: Use ekkOS_StoreSecret to securely save credentials")
148
- fi
149
-
150
- # ─────────────────────────────────────────────────────────────────────────────
151
- # PLAN TRIGGERS (Complex multi-step tasks)
152
- # ─────────────────────────────────────────────────────────────────────────────
153
- if echo "$QUERY_LOWER" | grep -qE '(implement|build|create.*feature|refactor|migrate|set up|architecture)'; then
154
- SKILL_REMINDERS+=("📋 PLAN REQUIRED: Call ekkOS_Plan for complex multi-step tasks")
155
- fi
156
-
157
- # ─────────────────────────────────────────────────────────────────────────────
158
- # LEARN TRIGGERS (User expressing success/failure)
159
- # ─────────────────────────────────────────────────────────────────────────────
160
- if echo "$QUERY_LOWER" | grep -qE '(that worked|thanks|perfect|great|awesome|nailed it|solved|fixed it)'; then
161
- SKILL_REMINDERS+=("🎯 LEARN: Consider calling ekkOS_Forge to capture this solution as a pattern")
162
- fi
163
-
164
- # ─────────────────────────────────────────────────────────────────────────────
165
- # CODEBASE TRIGGERS (Project-specific code search)
166
- # ─────────────────────────────────────────────────────────────────────────────
167
- if echo "$QUERY_LOWER" | grep -qE '(where is|find.*file|search.*code|in this project|in the codebase)'; then
168
- SKILL_REMINDERS+=("🔍 CODEBASE: Use ekkOS_Codebase for project-specific code search")
169
- fi
170
-
171
- # Combine skill reminders (only take first 3 to avoid noise)
172
- SKILL_REMINDER=""
173
- REMINDER_COUNT=${#SKILL_REMINDERS[@]}
174
- if [ "$REMINDER_COUNT" -gt 0 ]; then
175
- # Take up to 3 most relevant reminders
176
- MAX_REMINDERS=3
177
- [ "$REMINDER_COUNT" -lt "$MAX_REMINDERS" ] && MAX_REMINDERS="$REMINDER_COUNT"
178
- for i in $(seq 0 $((MAX_REMINDERS - 1))); do
179
- [ -n "$SKILL_REMINDER" ] && SKILL_REMINDER="$SKILL_REMINDER
180
- "
181
- SKILL_REMINDER="$SKILL_REMINDER${SKILL_REMINDERS[$i]}"
182
- done
93
+ # Schema triggers
94
+ if echo "$QUERY_LOWER" | grep -qE '(sql|query|supabase|prisma|database|table|column)'; then
95
+ SKILL_REMINDER="🗄️ SKILL REQUIRED: Call Skill(skill: \"ekkOS_Schema\") for correct field names"
183
96
  fi
184
97
 
185
98
  # ═══════════════════════════════════════════════════════════════════════════
186
- # Load auth
99
+ # Load auth - No jq
187
100
  # ═══════════════════════════════════════════════════════════════════════════
188
101
  EKKOS_CONFIG="$HOME/.ekkos/config.json"
189
102
  AUTH_TOKEN=""
190
- if [ -f "$EKKOS_CONFIG" ]; then
191
- AUTH_TOKEN=$(jq -r '.hookApiKey // .apiKey // ""' "$EKKOS_CONFIG" 2>/dev/null || echo "")
103
+ if [ -f "$EKKOS_CONFIG" ] && [ -f "$JSON_PARSE_HELPER" ]; then
104
+ AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.hookApiKey' 2>/dev/null || echo "")
105
+ if [ -z "$AUTH_TOKEN" ]; then
106
+ AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.apiKey' 2>/dev/null || echo "")
107
+ fi
192
108
  fi
193
109
  [ -z "$AUTH_TOKEN" ] && AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" 2>/dev/null | cut -d'=' -f2- | tr -d '"' | tr -d "'" || echo "")
194
110
 
195
111
  MEMORY_API_URL="https://mcp.ekkos.dev"
196
112
 
197
113
  # ═══════════════════════════════════════════════════════════════════════════
198
- # Session ID - NEW ID per conversation (not persisted 24h anymore)
199
- # Each Claude Code session gets unique ID for proper Time Machine grouping
114
+ # Session ID
200
115
  # ═══════════════════════════════════════════════════════════════════════════
201
116
  STATE_DIR="$PROJECT_ROOT/.claude/state"
202
117
  mkdir -p "$STATE_DIR" 2>/dev/null || true
203
118
  SESSION_FILE="$STATE_DIR/current-session.json"
204
119
 
205
- # Use Claude's RAW_SESSION_ID exclusively
206
120
  SESSION_ID="$RAW_SESSION_ID"
207
121
 
208
- # Skip if no valid session ID from Claude
209
122
  if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
210
123
  exit 0
211
124
  fi
212
125
 
213
- # Save for other hooks to reference (but don't reuse across conversations)
126
+ # Check if SESSION_ID is a UUID (8-4-4-4-12 format)
127
+ UUID_REGEX='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
128
+ IS_UUID=false
129
+ if [[ "$SESSION_ID" =~ $UUID_REGEX ]]; then
130
+ IS_UUID=true
131
+ fi
132
+
214
133
  echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$SESSION_FILE"
215
134
 
216
135
  # ═══════════════════════════════════════════════════════════════════════════
@@ -219,369 +138,376 @@ echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:
219
138
  PROJECT_SESSION_DIR="$STATE_DIR/sessions"
220
139
  mkdir -p "$PROJECT_SESSION_DIR" 2>/dev/null || true
221
140
  TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.turn"
141
+ CONTEXT_SIZE_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.context"
222
142
 
223
- # Count actual user messages in transcript for accurate turn number
143
+ # Count API round-trips from transcript to match TUI turn counter
224
144
  TURN_NUMBER=1
225
145
  if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
226
- # Count user message entries in JSONL transcript
227
146
  TURN_NUMBER=$(grep -c '"type":"user"' "$TRANSCRIPT_PATH" 2>/dev/null || echo "1")
228
147
  [ "$TURN_NUMBER" -eq 0 ] && TURN_NUMBER=1
229
148
  fi
230
149
 
231
- # PRESERVE HISTORY: Don't overwrite if saved count is higher (after /clear)
150
+ # Detect post-clear: saved count higher than transcript means /clear happened
232
151
  SAVED_TURN_COUNT=0
233
152
  [ -f "$TURN_COUNTER_FILE" ] && SAVED_TURN_COUNT=$(cat "$TURN_COUNTER_FILE" 2>/dev/null || echo "0")
234
- TRANSCRIPT_TURN_COUNT=$TURN_NUMBER # Save for post-clear detection
235
153
  POST_CLEAR_DETECTED=false
154
+
236
155
  if [ "$SAVED_TURN_COUNT" -gt "$TURN_NUMBER" ]; then
237
- # Post-clear: INCREMENT from saved count (not just copy it)
238
- TURN_NUMBER=$((SAVED_TURN_COUNT + 1))
239
156
  POST_CLEAR_DETECTED=true
157
+ TURN_NUMBER=$((SAVED_TURN_COUNT + 1))
240
158
  fi
241
- echo "$TURN_NUMBER" > "$TURN_COUNTER_FILE"
242
159
 
243
- # ═══════════════════════════════════════════════════════════════════════════
244
- # 🧠 WORKING MEMORY: Fast capture each turn (async, non-blocking)
245
- # ═══════════════════════════════════════════════════════════════════════════
246
- MEMORY_API_URL="https://mcp.ekkos.dev"
247
- if [ -f "$HOME/.ekkos/config.json" ]; then
248
- CAPTURE_TOKEN=$(jq -r '.hookApiKey // .apiKey // ""' "$HOME/.ekkos/config.json" 2>/dev/null || echo "")
249
- if [ -n "$CAPTURE_TOKEN" ] && [ "$CAPTURE_TOKEN" != "null" ]; then
250
- # Async capture to Redis/Supabase - doesn't block hook execution
251
- (curl -s -X POST "$MEMORY_API_URL/api/v1/working/fast-capture" \
252
- -H "Authorization: Bearer $CAPTURE_TOKEN" \
253
- -H "Content-Type: application/json" \
254
- -d "{\"session_id\":\"$RAW_SESSION_ID\",\"turn\":$TURN_NUMBER,\"query\":$(echo "$USER_QUERY" | jq -Rs .)}" \
255
- >/dev/null 2>&1) &
256
- fi
257
- fi
160
+ echo "$TURN_NUMBER" > "$TURN_COUNTER_FILE"
258
161
 
259
162
  # ═══════════════════════════════════════════════════════════════════════════
260
- # 💾 LOCAL CACHE: Tier 0 capture for instant /continue (async, non-blocking)
163
+ # Context size tracking - Uses tokenizer script (single source)
261
164
  # ═══════════════════════════════════════════════════════════════════════════
262
- if command -v ekkos-capture &>/dev/null; then
263
- # Generate session name if not already set
264
- if [ -z "$SESSION_NAME" ] || [ "$SESSION_NAME" = "unknown-session-starts" ]; then
265
- SESSION_NAME=$(uuid_to_words "$RAW_SESSION_ID" 2>/dev/null || echo "unknown-session")
165
+ PREV_CONTEXT_PERCENT=0
166
+ [ -f "$CONTEXT_SIZE_FILE" ] && PREV_CONTEXT_PERCENT=$(cat "$CONTEXT_SIZE_FILE" 2>/dev/null || echo "0")
167
+
168
+ TOKEN_PERCENT=0
169
+ MAX_TOKENS=200000
170
+ TOKENIZER_SCRIPT="$SCRIPT_DIR/lib/count-tokens.cjs"
171
+
172
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] && [ -f "$TOKENIZER_SCRIPT" ]; then
173
+ TOKEN_COUNT=$(node "$TOKENIZER_SCRIPT" "$TRANSCRIPT_PATH" 2>/dev/null || echo "0")
174
+ if [[ "$TOKEN_COUNT" =~ ^[0-9]+$ ]] && [ "$TOKEN_COUNT" -gt 0 ]; then
175
+ TOKEN_PERCENT=$((TOKEN_COUNT * 100 / MAX_TOKENS))
176
+ [ "$TOKEN_PERCENT" -gt 100 ] && TOKEN_PERCENT=100
177
+ # In proxy mode, IPC compresses ~65-70% — show estimated post-compression %
178
+ IPC_PERCENT=$((TOKEN_PERCENT * 30 / 100))
179
+ [ "$IPC_PERCENT" -lt 1 ] && IPC_PERCENT=1
266
180
  fi
267
- # Async local capture - writes to ~/.ekkos/cache/sessions/<uuid>.jsonl
268
- (ekkos-capture user "$RAW_SESSION_ID" "$SESSION_NAME" "$TURN_NUMBER" "$USER_QUERY" "$PROJECT_ROOT" \
269
- >/dev/null 2>&1) &
270
181
  fi
271
182
 
272
- # ═══════════════════════════════════════════════════════════════════════════
273
- # 📥 GOLDEN LOOP: CAPTURE PHASE - Track turn start
274
- # ═══════════════════════════════════════════════════════════════════════════
275
- GOLDEN_LOOP_FILE="$PROJECT_ROOT/.ekkos/golden-loop-current.json"
276
- mkdir -p "$PROJECT_ROOT/.ekkos" 2>/dev/null || true
277
-
278
- # Write current phase to file (extension watches this for real-time updates)
279
- jq -n \
280
- --arg phase "capture" \
281
- --argjson turn "$TURN_NUMBER" \
282
- --arg session "$SESSION_ID" \
283
- --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
284
- '{
285
- phase: $phase,
286
- turn: $turn,
287
- session: $session,
288
- timestamp: $timestamp,
289
- stats: {
290
- retrieved: 0,
291
- applied: 0,
292
- forged: 0
293
- }
294
- }' > "$GOLDEN_LOOP_FILE" 2>/dev/null || true
183
+ echo "$TOKEN_PERCENT" > "$CONTEXT_SIZE_FILE"
295
184
 
296
185
  # ═══════════════════════════════════════════════════════════════════════════
297
- # 🔍 GOLDEN LOOP: RETRIEVE PHASE - Auto-retrieve patterns from ekkOS
186
+ # COLORS
298
187
  # ═══════════════════════════════════════════════════════════════════════════
299
- EKKOS_API_KEY=""
300
- if [ -f "$HOME/.ekkos/.hookApiKey" ]; then
301
- EKKOS_API_KEY=$(cat "$HOME/.ekkos/.hookApiKey" 2>/dev/null || echo "")
302
- elif [ -f "$HOME/.ekkos/config.json" ]; then
303
- EKKOS_API_KEY=$(jq -r '.hookApiKey // ""' "$HOME/.ekkos/config.json" 2>/dev/null || echo "")
304
- fi
188
+ CYAN='\033[0;36m'
189
+ GREEN='\033[0;32m'
190
+ MAGENTA='\033[0;35m'
191
+ DIM='\033[2m'
192
+ BOLD='\033[1m'
193
+ RESET='\033[0m'
305
194
 
306
- RETRIEVED_PATTERNS=""
307
- PATTERN_COUNT=0
308
- RETRIEVED_DIRECTIVES=""
309
- DIRECTIVE_COUNT=0
195
+ CURRENT_TIME=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
310
196
 
311
197
  # ═══════════════════════════════════════════════════════════════════════════
312
- # 📦 DIRECTIVE CACHE: Local cache to avoid API calls every turn
313
- # Only fetch from API if:
314
- # 1. Cache doesn't exist
315
- # 2. Cache is >1 hour old
316
- # 3. Directive-related trigger detected in query
198
+ # WORD-BASED SESSION NAMES - No jq, uses ~/.ekkos/ config (works in ANY project)
317
199
  # ═══════════════════════════════════════════════════════════════════════════
318
- DIRECTIVE_CACHE_DIR="$HOME/.ekkos/cache"
319
- DIRECTIVE_CACHE_FILE="$DIRECTIVE_CACHE_DIR/directives.json"
320
- DIRECTIVE_CACHE_TTL=3600 # 1 hour in seconds
321
- mkdir -p "$DIRECTIVE_CACHE_DIR" 2>/dev/null || true
322
-
323
- # Check if we need to refresh directive cache
324
- DIRECTIVE_CACHE_VALID=false
325
- DIRECTIVE_TRIGGER_DETECTED=false
326
-
327
- # Smart detection: Check if query mentions directive-related keywords
328
- if echo "$QUERY_LOWER" | grep -qE '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on|directive|preference)'; then
329
- DIRECTIVE_TRIGGER_DETECTED=true
330
- fi
200
+ declare -a ADJECTIVES
201
+ declare -a NOUNS
202
+ declare -a VERBS
203
+ SESSION_WORDS_LOADED=false
331
204
 
332
- if [ -f "$DIRECTIVE_CACHE_FILE" ]; then
333
- CACHE_TIMESTAMP=$(jq -r '.cached_at // 0' "$DIRECTIVE_CACHE_FILE" 2>/dev/null || echo "0")
334
- CURRENT_TIMESTAMP=$(date +%s)
335
- CACHE_AGE=$((CURRENT_TIMESTAMP - CACHE_TIMESTAMP))
205
+ load_session_words() {
206
+ if [ "$SESSION_WORDS_LOADED" = "true" ]; then return 0; fi
336
207
 
337
- # Cache is valid if <1 hour old AND no directive trigger detected
338
- if [ "$CACHE_AGE" -lt "$DIRECTIVE_CACHE_TTL" ] && [ "$DIRECTIVE_TRIGGER_DETECTED" = "false" ]; then
339
- DIRECTIVE_CACHE_VALID=true
340
- fi
341
- fi
208
+ local words_file="$SESSION_WORDS_JSON"
209
+ [ ! -f "$words_file" ] && words_file="$SESSION_WORDS_DEFAULT"
342
210
 
343
- # Decide whether to inject directives this turn
344
- # SMART INJECTION: Only on Turn 1, post-clear, or directive trigger
345
- SHOULD_INJECT_DIRECTIVES=false
346
- if [ "$TURN_NUMBER" -eq 1 ] || [ "$POST_CLEAR_DETECTED" = "true" ] || [ "$DIRECTIVE_TRIGGER_DETECTED" = "true" ]; then
347
- SHOULD_INJECT_DIRECTIVES=true
348
- fi
211
+ if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
212
+ ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
213
+ return 1
214
+ fi
349
215
 
350
- if [ -n "$EKKOS_API_KEY" ] && [ -n "$USER_QUERY" ]; then
351
- # Update phase to RETRIEVE
352
- jq -n \
353
- --arg phase "retrieve" \
354
- --argjson turn "$TURN_NUMBER" \
355
- --arg session "$SESSION_ID" \
356
- --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
357
- '{
358
- phase: $phase,
359
- turn: $turn,
360
- session: $session,
361
- timestamp: $timestamp,
362
- stats: {
363
- retrieved: 0,
364
- applied: 0,
365
- forged: 0
366
- }
367
- }' > "$GOLDEN_LOOP_FILE" 2>/dev/null || true
368
-
369
- # ═══════════════════════════════════════════════════════════════════════════
370
- # PATTERN RETRIEVAL: Always search patterns (they're query-specific)
371
- # DIRECTIVE RETRIEVAL: Only if cache is invalid or trigger detected
372
- # ═══════════════════════════════════════════════════════════════════════════
373
-
374
- # Build sources array - always include patterns, conditionally include directives
375
- if [ "$DIRECTIVE_CACHE_VALID" = "true" ]; then
376
- SEARCH_SOURCES='["patterns"]'
377
- else
378
- SEARCH_SOURCES='["patterns", "directives"]'
379
- fi
216
+ if command -v node &>/dev/null; then
217
+ if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
218
+ readarray -t ADJECTIVES < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
219
+ readarray -t NOUNS < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
220
+ readarray -t VERBS < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
221
+ else
222
+ local i=0
223
+ while IFS= read -r line; do ADJECTIVES[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
224
+ i=0
225
+ while IFS= read -r line; do NOUNS[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
226
+ i=0
227
+ while IFS= read -r line; do VERBS[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
228
+ fi
229
+ [ ${#ADJECTIVES[@]} -eq 0 ] && ADJECTIVES=("unknown")
230
+ [ ${#NOUNS[@]} -eq 0 ] && NOUNS=("session")
231
+ [ ${#VERBS[@]} -eq 0 ] && VERBS=("starts")
232
+ SESSION_WORDS_LOADED=true
233
+ return 0
234
+ fi
235
+ ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
236
+ return 1
237
+ }
380
238
 
381
- # Call ekkOS MCP gateway
382
- SEARCH_RESPONSE=$(curl -s -X POST "https://api.ekkos.dev/api/v1/mcp/call" \
383
- -H "Authorization: Bearer $EKKOS_API_KEY" \
384
- -H "Content-Type: application/json" \
385
- --max-time 2 \
386
- -d "{\"tool\": \"ekkOS_Search\", \"arguments\": {\"query\": $(echo "$USER_QUERY" | jq -Rs .), \"limit\": 5, \"sources\": $SEARCH_SOURCES}}" 2>/dev/null || echo '{"result": {"results": {"patterns": [], "directives": []}}}')
387
-
388
- # Count patterns retrieved (MCP response: .result.results.patterns)
389
- PATTERN_COUNT=$(echo "$SEARCH_RESPONSE" | jq -r '.result.results.patterns | length' 2>/dev/null || echo "0")
390
-
391
- # Update golden loop with retrieved count
392
- if [ "$PATTERN_COUNT" -gt 0 ]; then
393
- jq -n \
394
- --arg phase "inject" \
395
- --argjson turn "$TURN_NUMBER" \
396
- --arg session "$SESSION_ID" \
397
- --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
398
- --argjson retrieved "$PATTERN_COUNT" \
399
- '{
400
- phase: $phase,
401
- turn: $turn,
402
- session: $session,
403
- timestamp: $timestamp,
404
- stats: {
405
- retrieved: $retrieved,
406
- applied: 0,
407
- forged: 0
408
- }
409
- }' > "$GOLDEN_LOOP_FILE" 2>/dev/null || true
410
-
411
- # Format patterns for injection (MCP response: .result.results.patterns)
412
- RETRIEVED_PATTERNS=$(echo "$SEARCH_RESPONSE" | jq -r '
413
- .result.results.patterns[]? |
414
- "**\(.title)**\n\(.problem // .guidance // "")\n\n## Solution\n\(.solution // .content // "")\n\nSuccess Rate: \((.success_rate // 0) * 100)%\nApplied: \(.applied_count // 0) times\n"
415
- ' 2>/dev/null || echo "")
416
- fi
239
+ uuid_to_words() {
240
+ local uuid="$1"
241
+ load_session_words
242
+
243
+ local hex="${uuid//-/}"
244
+ hex="${hex:0:12}"
245
+ [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]] && echo "unknown-session-starts" && return
246
+
247
+ local adj_seed=$((16#${hex:0:4}))
248
+ local noun_seed=$((16#${hex:4:4}))
249
+ local verb_seed=$((16#${hex:8:4}))
250
+ local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
251
+ local noun_idx=$((noun_seed % ${#NOUNS[@]}))
252
+ local verb_idx=$((verb_seed % ${#VERBS[@]}))
253
+ echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
254
+ }
417
255
 
418
- # ═══════════════════════════════════════════════════════════════════════════
419
- # DIRECTIVE HANDLING: Use cache if valid, otherwise process from response
420
- # ═══════════════════════════════════════════════════════════════════════════
421
- if [ "$DIRECTIVE_CACHE_VALID" = "true" ]; then
422
- # Load directives from cache
423
- DIRECTIVE_COUNT=$(jq -r '.count // 0' "$DIRECTIVE_CACHE_FILE" 2>/dev/null || echo "0")
424
- RETRIEVED_DIRECTIVES=$(jq -r '.formatted // ""' "$DIRECTIVE_CACHE_FILE" 2>/dev/null || echo "")
425
- else
426
- # Extract and format DIRECTIVES from API response
427
- DIRECTIVE_COUNT=$(echo "$SEARCH_RESPONSE" | jq -r '.result.results.directives | length' 2>/dev/null || echo "0")
428
-
429
- if [ "$DIRECTIVE_COUNT" -gt 0 ]; then
430
- # Group directives by type for clean display
431
- MUST_DIRECTIVES=$(echo "$SEARCH_RESPONSE" | jq -r '.result.results.directives[] | select(.type == "MUST") | " • \(.rule)"' 2>/dev/null || echo "")
432
- NEVER_DIRECTIVES=$(echo "$SEARCH_RESPONSE" | jq -r '.result.results.directives[] | select(.type == "NEVER") | " • \(.rule)"' 2>/dev/null || echo "")
433
- PREFER_DIRECTIVES=$(echo "$SEARCH_RESPONSE" | jq -r '.result.results.directives[] | select(.type == "PREFER") | " • \(.rule)"' 2>/dev/null || echo "")
434
- AVOID_DIRECTIVES=$(echo "$SEARCH_RESPONSE" | jq -r '.result.results.directives[] | select(.type == "AVOID") | " • \(.rule)"' 2>/dev/null || echo "")
435
-
436
- # Build directive section
437
- if [ -n "$MUST_DIRECTIVES" ] || [ -n "$NEVER_DIRECTIVES" ] || [ -n "$PREFER_DIRECTIVES" ] || [ -n "$AVOID_DIRECTIVES" ]; then
438
- RETRIEVED_DIRECTIVES="🔴 USER DIRECTIVES (FOLLOW THESE):"
439
- [ -n "$MUST_DIRECTIVES" ] && RETRIEVED_DIRECTIVES="$RETRIEVED_DIRECTIVES
440
-
441
- MUST:
442
- $MUST_DIRECTIVES"
443
- [ -n "$NEVER_DIRECTIVES" ] && RETRIEVED_DIRECTIVES="$RETRIEVED_DIRECTIVES
444
-
445
- NEVER:
446
- $NEVER_DIRECTIVES"
447
- [ -n "$PREFER_DIRECTIVES" ] && RETRIEVED_DIRECTIVES="$RETRIEVED_DIRECTIVES
448
-
449
- PREFER:
450
- $PREFER_DIRECTIVES"
451
- [ -n "$AVOID_DIRECTIVES" ] && RETRIEVED_DIRECTIVES="$RETRIEVED_DIRECTIVES
452
-
453
- AVOID:
454
- $AVOID_DIRECTIVES"
455
- fi
456
-
457
- # Save to cache for future turns
458
- jq -n \
459
- --argjson count "$DIRECTIVE_COUNT" \
460
- --arg formatted "$RETRIEVED_DIRECTIVES" \
461
- --argjson cached_at "$(date +%s)" \
462
- '{
463
- count: $count,
464
- formatted: $formatted,
465
- cached_at: $cached_at
466
- }' > "$DIRECTIVE_CACHE_FILE" 2>/dev/null || true
256
+ # Generate session name (or use as-is if already word-based)
257
+ SESSION_NAME=""
258
+ if [ -n "$SESSION_ID" ] && [ "$SESSION_ID" != "unknown" ] && [ "$SESSION_ID" != "null" ]; then
259
+ if [ "$IS_UUID" = true ]; then
260
+ SESSION_NAME=$(uuid_to_words "$SESSION_ID")
261
+ else
262
+ SESSION_NAME="$SESSION_ID"
467
263
  fi
468
- fi
469
264
  fi
470
265
 
471
- # Context tracking removed - Claude Code handles its own context management
472
-
473
266
  # ═══════════════════════════════════════════════════════════════════════════
474
- # COLORS
267
+ # SINGLE SOURCE OF TRUTH: Update ALL session tracking systems
475
268
  # ═══════════════════════════════════════════════════════════════════════════
476
- CYAN='\033[0;36m'
477
- GREEN='\033[0;32m'
478
- YELLOW='\033[1;33m'
479
- MAGENTA='\033[0;35m'
480
- DIM='\033[2m'
481
- BOLD='\033[1m'
482
- RESET='\033[0m'
483
-
484
- CURRENT_TIME=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
269
+ if [ -n "$SESSION_NAME" ]; then
270
+ TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
271
+
272
+ # 1. Project-level state file
273
+ echo "{\"session_id\": \"$SESSION_ID\", \"session_name\": \"$SESSION_NAME\", \"timestamp\": \"$TIMESTAMP\"}" > "$SESSION_FILE" 2>/dev/null || true
274
+
275
+ # 2. Global ekkOS state (for extension LOCAL-FIRST read)
276
+ EKKOS_GLOBAL_STATE="$HOME/.ekkos/current-session.json"
277
+ mkdir -p "$HOME/.ekkos" 2>/dev/null || true
278
+ echo "{\"session_id\": \"$SESSION_ID\", \"session_name\": \"$SESSION_NAME\", \"project\": \"$PROJECT_ROOT\", \"timestamp\": \"$TIMESTAMP\"}" > "$EKKOS_GLOBAL_STATE" 2>/dev/null || true
279
+
280
+ # 3. CLI state file
281
+ CLI_STATE_FILE="$HOME/.ekkos/state.json"
282
+ echo "{\"sessionId\": \"$SESSION_ID\", \"sessionName\": \"$SESSION_NAME\", \"turnNumber\": $TURN_NUMBER, \"lastUpdated\": \"$TIMESTAMP\", \"projectPath\": \"$PROJECT_ROOT\"}" > "$CLI_STATE_FILE" 2>/dev/null || true
283
+
284
+ # 4. Multi-session tracking - No jq
285
+ ACTIVE_SESSIONS_FILE="$HOME/.ekkos/active-sessions.json"
286
+ node -e "
287
+ const fs = require('fs');
288
+ const sid = process.argv[1], sname = process.argv[2], ts = process.argv[3], proj = process.argv[4];
289
+ try {
290
+ let sessions = [];
291
+ if (fs.existsSync('$ACTIVE_SESSIONS_FILE')) {
292
+ sessions = JSON.parse(fs.readFileSync('$ACTIVE_SESSIONS_FILE', 'utf8') || '[]');
293
+ }
294
+ const idx = sessions.findIndex(s => s.sessionId === sid);
295
+ if (idx >= 0) {
296
+ sessions[idx] = {...sessions[idx], sessionName: sname, lastHeartbeat: ts, projectPath: proj};
297
+ } else {
298
+ sessions.push({sessionId: sid, sessionName: sname, pid: 0, startedAt: ts, projectPath: proj, lastHeartbeat: ts});
299
+ }
300
+ fs.writeFileSync('$ACTIVE_SESSIONS_FILE', JSON.stringify(sessions, null, 2));
301
+ } catch(e) {}
302
+ " "$SESSION_ID" "$SESSION_NAME" "$TIMESTAMP" "$PROJECT_ROOT" 2>/dev/null || true
303
+
304
+ # 5. Update Redis via API
305
+ if [ -n "$AUTH_TOKEN" ]; then
306
+ curl -s -X POST "$MEMORY_API_URL/api/v1/working/session/current" \
307
+ -H "Authorization: Bearer $AUTH_TOKEN" \
308
+ -H "Content-Type: application/json" \
309
+ -d "{\"session_name\": \"$SESSION_NAME\"}" \
310
+ --connect-timeout 2 \
311
+ --max-time 3 >/dev/null 2>&1 &
312
+ fi
485
313
 
486
- # Generate session name (arrays and uuid_to_words defined at top of file)
487
- SESSION_NAME=""
488
- if [ -n "$SESSION_ID" ] && [ "$SESSION_ID" != "unknown" ] && [ "$SESSION_ID" != "null" ]; then
489
- SESSION_NAME=$(uuid_to_words "$SESSION_ID")
314
+ # 6. CRITICAL: Bind session name to proxy for R2 eviction paths - No jq
315
+ EKKOS_USER_ID=""
316
+ if [ -f "$HOME/.ekkos/config.json" ] && [ -f "$JSON_PARSE_HELPER" ]; then
317
+ EKKOS_USER_ID=$(node "$JSON_PARSE_HELPER" "$HOME/.ekkos/config.json" '.userId' 2>/dev/null || echo "")
318
+ fi
319
+ if [ -n "$EKKOS_USER_ID" ] && [ -n "$SESSION_NAME" ]; then
320
+ PENDING_SESSION="${EKKOS_PENDING_SESSION:-_pending}"
321
+ node -e "
322
+ const https = require('https');
323
+ const body = JSON.stringify({
324
+ userId: process.argv[1],
325
+ realSession: process.argv[2],
326
+ projectPath: process.argv[3],
327
+ pendingSession: process.argv[4]
328
+ });
329
+ const req = https.request({
330
+ hostname: 'mcp.ekkos.dev',
331
+ path: '/proxy/session/bind',
332
+ method: 'POST',
333
+ headers: {'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body)}
334
+ }, () => {});
335
+ req.on('error', () => {});
336
+ req.write(body);
337
+ req.end();
338
+ " "$EKKOS_USER_ID" "$SESSION_NAME" "$PROJECT_ROOT" "$PENDING_SESSION" 2>/dev/null &
339
+ fi
490
340
  fi
491
341
 
492
342
  # ═══════════════════════════════════════════════════════════════════════════
493
- # "/continue" COMMAND: Delegated to Skill system (DO NOT INTERCEPT)
494
- # ═══════════════════════════════════════════════════════════════════════════
495
- # REMOVED: Hook used to intercept /continue and do simple restoration
496
- # NOW: Let /continue Skill handle it - supports session names + intelligent narrative
497
- #
498
- # Why this changed:
499
- # - Hook was ignoring session name argument (always used "current")
500
- # - Hook couldn't provide intelligent narrative briefing
501
- # - Skill system now has proper name→UUID resolution in API
502
- #
503
- # OLD BEHAVIOR (removed):
504
- # - Hook caught /continue → API call with "current" → exit
505
- # NEW BEHAVIOR:
506
- # - /continue groovy-cactus → Skill system → API with session name → intelligent briefing
343
+ # "/continue" COMMAND: Run AFTER /clear to restore last 5 turns
507
344
  # ═══════════════════════════════════════════════════════════════════════════
345
+ QUERY_LOWER=$(echo "$USER_QUERY" | tr '[:upper:]' '[:lower:]')
508
346
 
509
- # Note: Session name is shown in status line for easy reference:
510
- # Example: "🧠 ekkOS Memory | Turn 42 | groovy-cactus | 2026-01-12 09:40 AM EST"
347
+ if [[ "$USER_QUERY" == "/continue" ]] || [[ "$USER_QUERY" =~ ^/continue[[:space:]] ]] || [[ "$QUERY_LOWER" == "continue" ]] || [[ "$QUERY_LOWER" == "continue." ]]; then
348
+ if [ -n "$AUTH_TOKEN" ]; then
349
+ RESTORE_RESPONSE=$(curl -s -X POST "$MEMORY_API_URL/api/v1/turns/recall" \
350
+ -H "Authorization: Bearer $AUTH_TOKEN" \
351
+ -H "Content-Type: application/json" \
352
+ -d "{\"session_id\": \"current\", \"last_n\": 5, \"format\": \"detailed\"}" \
353
+ --connect-timeout 3 \
354
+ --max-time 5 2>/dev/null || echo '{"turns":[]}')
355
+
356
+ RESTORED_COUNT=$(echo "$RESTORE_RESPONSE" | node -e "
357
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
358
+ console.log((d.turns || []).length);
359
+ " 2>/dev/null || echo "0")
360
+
361
+ LAST_TASK=$(echo "$RESTORE_RESPONSE" | node -e "
362
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
363
+ const turns = d.turns || [];
364
+ console.log((turns[turns.length-1]?.user_query || 'unknown task').substring(0, 200));
365
+ " 2>/dev/null || echo "unknown task")
366
+
367
+ LAST_RESPONSE=$(echo "$RESTORE_RESPONSE" | node -e "
368
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
369
+ const turns = d.turns || [];
370
+ console.log((turns[turns.length-1]?.assistant_response || '').substring(0, 500));
371
+ " 2>/dev/null || echo "")
372
+
373
+ echo ""
374
+ echo -e "${GREEN}${BOLD}✓ Session continued${RESET} ${DIM}(${RESTORED_COUNT} turns restored)${RESET}"
375
+ echo ""
376
+
377
+ echo "<system-reminder>"
378
+ echo "═══════════════════════════════════════════════════════════════════════"
379
+ echo "CONTEXT RESTORED - Resume seamlessly. DO NOT ask 'what were we doing?'"
380
+ echo "═══════════════════════════════════════════════════════════════════════"
381
+ echo ""
382
+ echo "## Last User Request:"
383
+ echo "$LAST_TASK"
384
+ echo ""
385
+ echo "## Your Last Response (truncated):"
386
+ echo "$LAST_RESPONSE"
387
+ echo ""
388
+
389
+ if [ "$RESTORED_COUNT" -gt 1 ]; then
390
+ echo "## Recent Context (older → newer):"
391
+ echo "$RESTORE_RESPONSE" | node -e "
392
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
393
+ const turns = d.turns || [];
394
+ turns.slice(0, -1).forEach(t => {
395
+ const q = (t.user_query || '...').substring(0, 100);
396
+ console.log('- Turn ' + (t.turn_number || '?') + ': ' + q + '...');
397
+ });
398
+ " 2>/dev/null || true
399
+ echo ""
400
+ fi
401
+
402
+ echo "═══════════════════════════════════════════════════════════════════════"
403
+ echo "INSTRUCTION: Start your response with '✓ **Continuing** -' then pick up"
404
+ echo "exactly where you left off. If mid-task, continue it. If done, ask what's next."
405
+ echo "═══════════════════════════════════════════════════════════════════════"
406
+ echo "</system-reminder>"
407
+
408
+ echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ${CURRENT_TIME}${RESET}"
409
+ exit 0
410
+ fi
411
+ fi
511
412
 
512
413
  # ═══════════════════════════════════════════════════════════════════════════
513
- # AUTO-RESTORE REMOVED: Manual /continue only (saves 79% token burn!)
514
- # ═══════════════════════════════════════════════════════════════════════════
515
- # WHY REMOVED:
516
- # - Auto-restore burns 5,000 tokens per turn (250K tokens over 50 turns)
517
- # - Manual /continue: 2,000 tokens once + clean slate (52K total = 79% savings!)
518
- # - Manual /continue is 10x more powerful (Bash + multi-source + narrative)
519
- # - User has control (can choose session, can skip if starting fresh)
520
- # - Explicit > implicit (user knows exactly what's happening)
521
- #
522
- # OLD BEHAVIOR (removed):
523
- # - Compaction detection → auto-inject 10 turns
524
- # - Post-clear detection → auto-inject 10 turns
525
- # NEW BEHAVIOR:
526
- # - User types: /continue groovy-cactus
527
- # - Skill runs with full Bash power + intelligent narrative
414
+ # COMPACTION DETECTION: If context dropped dramatically, auto-restore
415
+ # Was >50% last turn, now <15% = compaction happened
528
416
  # ═══════════════════════════════════════════════════════════════════════════
417
+ if [ "$PREV_CONTEXT_PERCENT" -gt 50 ] && [ "$TOKEN_PERCENT" -lt 15 ] && [ -n "$AUTH_TOKEN" ]; then
418
+ echo ""
419
+ echo -e "${GREEN}${BOLD}🔄 CONTEXT RESTORED${RESET} ${DIM}| Compaction detected | Auto-loading recent turns...${RESET}"
529
420
 
530
- # Simple status line - no context warnings, Claude handles its own context
531
- echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
421
+ RESTORE_RESPONSE=$(curl -s -X POST "$MEMORY_API_URL/api/v1/turns/recall" \
422
+ -H "Authorization: Bearer $AUTH_TOKEN" \
423
+ -H "Content-Type: application/json" \
424
+ -d "{\"session_id\": \"${SESSION_ID}\", \"last_n\": 10, \"format\": \"summary\"}" \
425
+ --connect-timeout 3 \
426
+ --max-time 5 2>/dev/null || echo '{"turns":[]}')
427
+
428
+ RESTORED_COUNT=$(echo "$RESTORE_RESPONSE" | node -e "
429
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
430
+ console.log((d.turns || []).length);
431
+ " 2>/dev/null || echo "0")
432
+
433
+ if [ "$RESTORED_COUNT" -gt 0 ]; then
434
+ echo -e "${GREEN} ✓${RESET} Restored ${RESTORED_COUNT} turns from Layer 2"
435
+ echo ""
436
+ echo -e "${MAGENTA}${BOLD}## Recent Context (auto-restored)${RESET}"
437
+ echo ""
438
+
439
+ echo "$RESTORE_RESPONSE" | node -e "
440
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
441
+ (d.turns || []).forEach(t => {
442
+ const q = (t.user_query || '...').substring(0, 120);
443
+ const a = (t.assistant_response || '...').substring(0, 250);
444
+ console.log('**Turn ' + (t.turn_number || '?') + '**: ' + q + '...\n> ' + a + '...\n');
445
+ });
446
+ " 2>/dev/null || true
447
+
448
+ echo ""
449
+ echo -e "${DIM}Full history: \"turns 1-${TURN_NUMBER}\" or \"recall yesterday\"${RESET}"
450
+ fi
532
451
 
533
- # Output skill reminder if detected
534
- if [ -n "$SKILL_REMINDER" ]; then
535
452
  echo ""
536
- echo -e "${MAGENTA}${BOLD}$SKILL_REMINDER${RESET}"
537
- fi
453
+ echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
538
454
 
539
- # 💉 GOLDEN LOOP: INJECT PHASE - Inject directives FIRST (highest priority)
540
- # SMART INJECTION: Only on Turn 1, post-clear, or directive trigger
541
- if [ "$SHOULD_INJECT_DIRECTIVES" = "true" ] && [ -n "$RETRIEVED_DIRECTIVES" ] && [ "$DIRECTIVE_COUNT" -gt 0 ]; then
542
- echo ""
543
- echo "<system-reminder>"
544
- echo -e "$RETRIEVED_DIRECTIVES"
545
- echo "</system-reminder>"
546
- fi
455
+ elif [ "$POST_CLEAR_DETECTED" = true ] && [ -n "$AUTH_TOKEN" ]; then
456
+ # /clear detected - show visible restoration banner
457
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
458
+ echo -e "${GREEN}${BOLD}🔄 SESSION CONTINUED${RESET} ${DIM}| ${TURN_NUMBER} turns preserved | Context restored${RESET}" >&2
459
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
547
460
 
548
- # 💉 GOLDEN LOOP: INJECT PHASE - Inject retrieved patterns into context
549
- if [ -n "$RETRIEVED_PATTERNS" ] && [ "$PATTERN_COUNT" -gt 0 ]; then
550
- echo ""
551
- echo "<system-reminder>"
552
- echo "🔍 RETRIEVED PATTERNS FROM ekkOS MEMORY ($PATTERN_COUNT patterns found)"
553
461
  echo ""
554
- echo -e "$RETRIEVED_PATTERNS"
555
- echo ""
556
- echo "These patterns were automatically retrieved based on your query. Review and apply relevant solutions."
557
- echo "</system-reminder>"
558
- echo ""
559
- echo "🔴 MANDATORY PATTERN ACKNOWLEDGMENT (PatternGuard):"
560
- echo "You MUST acknowledge ALL $PATTERN_COUNT patterns above - either SELECT or SKIP each one."
561
- echo ""
562
- echo "For patterns you USE:"
563
- echo "[ekkOS_SELECT]"
564
- echo "- id: <pattern_id>"
565
- echo " reason: <1-line why using>"
566
- echo " confidence: <0.0-1.0>"
567
- echo "[/ekkOS_SELECT]"
568
- echo ""
569
- echo "For patterns NOT relevant:"
570
- echo "[ekkOS_SKIP]"
571
- echo "- id: <pattern_id>"
572
- echo " reason: <1-line why not relevant>"
573
- echo "[/ekkOS_SKIP]"
462
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
463
+ echo -e "${GREEN}${BOLD}🔄 SESSION CONTINUED${RESET} ${DIM}| ${TURN_NUMBER} turns preserved | Restoring context...${RESET}"
464
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
465
+
466
+ RESTORE_RESPONSE=$(curl -s -X POST "$MEMORY_API_URL/api/v1/turns/recall" \
467
+ -H "Authorization: Bearer $AUTH_TOKEN" \
468
+ -H "Content-Type: application/json" \
469
+ -d "{\"session_id\": \"${SESSION_ID}\", \"last_n\": 10, \"format\": \"summary\"}" \
470
+ --connect-timeout 3 \
471
+ --max-time 5 2>/dev/null || echo '{"turns":[]}')
472
+
473
+ RESTORED_COUNT=$(echo "$RESTORE_RESPONSE" | node -e "
474
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
475
+ console.log((d.turns || []).length);
476
+ " 2>/dev/null || echo "0")
477
+
478
+ if [ "$RESTORED_COUNT" -gt 0 ]; then
479
+ echo -e "${GREEN} ✓${RESET} Restored ${RESTORED_COUNT} recent turns"
480
+ echo ""
481
+
482
+ echo "$RESTORE_RESPONSE" | node -e "
483
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
484
+ (d.turns || []).forEach(t => {
485
+ const q = (t.query_preview || t.user_query || '...').substring(0, 80);
486
+ const a = (t.response_preview || t.assistant_response || '...').substring(0, 150);
487
+ console.log('**Turn ' + (t.turn_number || '?') + '**: ' + q + '...\n> ' + a + '...\n');
488
+ });
489
+ " 2>/dev/null || true
490
+ else
491
+ echo -e "${GREEN} ✓${RESET} History preserved (${TURN_NUMBER} turns)"
492
+ fi
493
+
574
494
  echo ""
575
- echo " AUTOMATIC APPLICATION: When pattern matches your task, APPLY IT immediately without asking."
576
- echo "Track outcome after: ekkOS_Outcome({success: true/false})"
495
+ echo -e "${DIM}Full history: \"recall\" or \"turns 1-${TURN_NUMBER}\"${RESET}"
496
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
577
497
  echo ""
578
- echo "100% coverage required. This is how the system learns what works."
498
+ echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
499
+
500
+ elif [ "$TOKEN_PERCENT" -ge 50 ]; then
501
+ echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ~${IPC_PERCENT:-0}% IPC | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
502
+
503
+ else
504
+ echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
579
505
  fi
580
506
 
581
- # Inject footer format reminder (helps Claude remember session name)
582
- if [ -n "$SESSION_NAME" ] && [ "$SESSION_NAME" != "unknown-session" ]; then
507
+ # Output skill reminder if detected
508
+ if [ -n "$SKILL_REMINDER" ]; then
583
509
  echo ""
584
- echo "<footer-format>End responses with: Claude Code ({Model}) · 🧠 **ekkOS_™** · Turn ${TURN_NUMBER} · ${SESSION_NAME} · 📅 ${CURRENT_TIME}</footer-format>"
510
+ echo -e "${MAGENTA}${BOLD}$SKILL_REMINDER${RESET}"
585
511
  fi
586
512
 
587
513
  exit 0