@ekkos/cli 0.3.3 → 1.0.1

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 (81) hide show
  1. package/README.md +57 -0
  2. package/dist/agent/daemon.d.ts +27 -0
  3. package/dist/agent/daemon.js +254 -29
  4. package/dist/agent/health-check.d.ts +35 -0
  5. package/dist/agent/health-check.js +243 -0
  6. package/dist/agent/pty-runner.d.ts +1 -0
  7. package/dist/agent/pty-runner.js +6 -1
  8. package/dist/capture/transcript-repair.d.ts +1 -0
  9. package/dist/capture/transcript-repair.js +12 -1
  10. package/dist/commands/agent.d.ts +6 -0
  11. package/dist/commands/agent.js +244 -0
  12. package/dist/commands/dashboard.d.ts +25 -0
  13. package/dist/commands/dashboard.js +1175 -0
  14. package/dist/commands/run.d.ts +3 -0
  15. package/dist/commands/run.js +503 -350
  16. package/dist/commands/setup-remote.js +146 -37
  17. package/dist/commands/swarm-dashboard.d.ts +20 -0
  18. package/dist/commands/swarm-dashboard.js +735 -0
  19. package/dist/commands/swarm-setup.d.ts +10 -0
  20. package/dist/commands/swarm-setup.js +956 -0
  21. package/dist/commands/swarm.d.ts +46 -0
  22. package/dist/commands/swarm.js +441 -0
  23. package/dist/commands/test-claude.d.ts +16 -0
  24. package/dist/commands/test-claude.js +156 -0
  25. package/dist/commands/usage/blocks.d.ts +8 -0
  26. package/dist/commands/usage/blocks.js +60 -0
  27. package/dist/commands/usage/daily.d.ts +9 -0
  28. package/dist/commands/usage/daily.js +96 -0
  29. package/dist/commands/usage/dashboard.d.ts +8 -0
  30. package/dist/commands/usage/dashboard.js +104 -0
  31. package/dist/commands/usage/formatters.d.ts +41 -0
  32. package/dist/commands/usage/formatters.js +147 -0
  33. package/dist/commands/usage/index.d.ts +13 -0
  34. package/dist/commands/usage/index.js +87 -0
  35. package/dist/commands/usage/monthly.d.ts +8 -0
  36. package/dist/commands/usage/monthly.js +66 -0
  37. package/dist/commands/usage/session.d.ts +11 -0
  38. package/dist/commands/usage/session.js +193 -0
  39. package/dist/commands/usage/weekly.d.ts +9 -0
  40. package/dist/commands/usage/weekly.js +61 -0
  41. package/dist/deploy/instructions.d.ts +5 -2
  42. package/dist/deploy/instructions.js +11 -8
  43. package/dist/index.js +256 -20
  44. package/dist/lib/tmux-scrollbar.d.ts +14 -0
  45. package/dist/lib/tmux-scrollbar.js +296 -0
  46. package/dist/lib/usage-parser.d.ts +95 -5
  47. package/dist/lib/usage-parser.js +416 -71
  48. package/dist/utils/log-rotate.d.ts +18 -0
  49. package/dist/utils/log-rotate.js +74 -0
  50. package/dist/utils/platform.d.ts +2 -0
  51. package/dist/utils/platform.js +3 -1
  52. package/dist/utils/session-binding.d.ts +5 -0
  53. package/dist/utils/session-binding.js +46 -0
  54. package/dist/utils/state.js +4 -0
  55. package/dist/utils/verify-remote-terminal.d.ts +10 -0
  56. package/dist/utils/verify-remote-terminal.js +415 -0
  57. package/package.json +16 -11
  58. package/templates/CLAUDE.md +135 -23
  59. package/templates/cursor-hooks/after-agent-response.sh +0 -0
  60. package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
  61. package/templates/cursor-hooks/stop.sh +0 -0
  62. package/templates/ekkos-manifest.json +5 -5
  63. package/templates/hooks/assistant-response.sh +0 -0
  64. package/templates/hooks/lib/contract.sh +43 -31
  65. package/templates/hooks/lib/count-tokens.cjs +86 -0
  66. package/templates/hooks/lib/ekkos-reminders.sh +98 -0
  67. package/templates/hooks/lib/state.sh +53 -1
  68. package/templates/hooks/session-start.sh +0 -0
  69. package/templates/hooks/stop.sh +150 -388
  70. package/templates/hooks/user-prompt-submit.sh +353 -443
  71. package/templates/plan-template.md +0 -0
  72. package/templates/spec-template.md +0 -0
  73. package/templates/windsurf-hooks/README.md +212 -0
  74. package/templates/windsurf-hooks/hooks.json +9 -2
  75. package/templates/windsurf-hooks/install.sh +148 -0
  76. package/templates/windsurf-hooks/lib/contract.sh +2 -0
  77. package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
  78. package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
  79. package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
  80. package/LICENSE +0 -21
  81. package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
@@ -1,8 +1,6 @@
1
1
  #!/bin/bash
2
2
  # ═══════════════════════════════════════════════════════════════════════════
3
3
  # ekkOS_ Hook: UserPromptSubmit - SEAMLESS CONTEXT CONTINUITY
4
- # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
5
- # EKKOS_MANAGED=1
6
4
  # ═══════════════════════════════════════════════════════════════════════════
7
5
  # ZERO USER ACTION NEEDED:
8
6
  # 1. Tracks turn number and context size
@@ -12,8 +10,12 @@
12
10
 
13
11
  set +e
14
12
 
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
15
+
15
16
  # ═══════════════════════════════════════════════════════════════════════════
16
- # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
17
+ # CONFIG PATHS - No jq dependency (v1.2 spec)
18
+ # Session words live in ~/.ekkos/ so they work in ANY project
17
19
  # ═══════════════════════════════════════════════════════════════════════════
18
20
  EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
19
21
  SESSION_WORDS_JSON="$EKKOS_CONFIG_DIR/session-words.json"
@@ -21,115 +23,13 @@ SESSION_WORDS_DEFAULT="$EKKOS_CONFIG_DIR/.defaults/session-words.json"
21
23
  JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
22
24
 
23
25
  # ═══════════════════════════════════════════════════════════════════════════
24
- # SESSION NAME GENERATION - Uses external session-words.json (NO jq)
25
- # Format: adj-noun-verb (e.g., "cosmic-penguin-runs")
26
+ # JSON PARSING HELPER - No jq required
26
27
  # ═══════════════════════════════════════════════════════════════════════════
27
-
28
- # Arrays loaded from JSON - NOT hardcoded
29
- declare -a ADJECTIVES
30
- declare -a NOUNS
31
- declare -a VERBS
32
- SESSION_WORDS_LOADED=false
33
-
34
- load_session_words() {
35
- if [ "$SESSION_WORDS_LOADED" = "true" ]; then
36
- return 0
37
- fi
38
-
39
- local words_file="$SESSION_WORDS_JSON"
40
-
41
- # Fallback to managed defaults if user file missing/invalid
42
- if [ ! -f "$words_file" ]; then
43
- words_file="$SESSION_WORDS_DEFAULT"
44
- fi
45
-
46
- if [ ! -f "$words_file" ]; then
47
- echo "ERROR: No session-words.json found. Run 'ekkos hooks install --global' to fix." >&2
48
- return 1
49
- fi
50
-
51
- # Check for json-parse helper
52
- if [ ! -f "$JSON_PARSE_HELPER" ]; then
53
- echo "ERROR: json-parse.cjs helper not found. Run 'ekkos hooks install --global' to fix." >&2
54
- return 1
55
- fi
56
-
57
- # Parse using Node helper (no jq dependency)
58
- if command -v node &>/dev/null; then
59
- # Use readarray if available (bash 4+), otherwise use while loop
60
- if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
61
- readarray -t ADJECTIVES < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
62
- readarray -t NOUNS < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
63
- readarray -t VERBS < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
64
- else
65
- # Bash 3 fallback (macOS default)
66
- local i=0
67
- while IFS= read -r line; do
68
- ADJECTIVES[i]="$line"
69
- ((i++))
70
- done < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
71
- i=0
72
- while IFS= read -r line; do
73
- NOUNS[i]="$line"
74
- ((i++))
75
- done < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
76
- i=0
77
- while IFS= read -r line; do
78
- VERBS[i]="$line"
79
- ((i++))
80
- done < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
81
- fi
82
-
83
- if [ ${#ADJECTIVES[@]} -eq 0 ] || [ ${#NOUNS[@]} -eq 0 ] || [ ${#VERBS[@]} -eq 0 ]; then
84
- echo "ERROR: Failed to parse $words_file. Check JSON syntax." >&2
85
- return 1
86
- fi
87
-
88
- SESSION_WORDS_LOADED=true
89
- return 0
90
- else
91
- echo "ERROR: Node.js not found. Install Node.js 18+ to use ekkOS hooks." >&2
92
- return 1
93
- fi
94
- }
95
-
96
- uuid_to_words() {
97
- local uuid="$1"
98
-
99
- # Load session words if not already loaded
100
- if [ "$SESSION_WORDS_LOADED" != "true" ]; then
101
- load_session_words || {
102
- echo "unknown-session-starts"
103
- return
104
- }
105
- fi
106
-
107
- local hex="${uuid//-/}"
108
- hex="${hex:0:12}"
109
- if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
110
- echo "unknown-session-starts"
111
- return
112
- fi
113
- local adj_seed=$((16#${hex:0:4}))
114
- local noun_seed=$((16#${hex:4:4}))
115
- local verb_seed=$((16#${hex:8:4}))
116
- local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
117
- local noun_idx=$((noun_seed % ${#NOUNS[@]}))
118
- local verb_idx=$((verb_seed % ${#VERBS[@]}))
119
- echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
120
- }
121
-
122
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
123
- PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
124
-
125
- INPUT=$(cat)
126
-
127
- # Parse input using json-parse helper (no jq)
128
28
  parse_json_value() {
129
29
  local json="$1"
130
30
  local path="$2"
131
31
  echo "$json" | node -e "
132
- const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
32
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
133
33
  const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
134
34
  let result = data;
135
35
  for (const p of path) {
@@ -140,100 +40,63 @@ if (result !== undefined && result !== null) console.log(result);
140
40
  " 2>/dev/null || echo ""
141
41
  }
142
42
 
43
+ INPUT=$(cat)
143
44
  USER_QUERY=$(parse_json_value "$INPUT" '.query')
144
- if [ -z "$USER_QUERY" ] || [ "$USER_QUERY" = "null" ]; then
145
- USER_QUERY=$(parse_json_value "$INPUT" '.message')
146
- fi
147
- if [ -z "$USER_QUERY" ] || [ "$USER_QUERY" = "null" ]; then
148
- USER_QUERY=$(parse_json_value "$INPUT" '.prompt')
149
- fi
150
-
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')
151
47
  RAW_SESSION_ID=$(parse_json_value "$INPUT" '.session_id')
48
+ [ -z "$RAW_SESSION_ID" ] && RAW_SESSION_ID="unknown"
152
49
  TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
153
50
 
154
51
  [ -z "$USER_QUERY" ] || [ "$USER_QUERY" = "null" ] && exit 0
155
52
 
156
53
  # Fallback: read session_id from saved state if not in INPUT
157
- if [ -z "$RAW_SESSION_ID" ] || [ "$RAW_SESSION_ID" = "unknown" ] || [ "$RAW_SESSION_ID" = "null" ]; then
158
- STATE_FILE="$HOME/.claude/state/current-session.json"
159
- if [ -f "$STATE_FILE" ] && [ -f "$JSON_PARSE_HELPER" ]; then
160
- RAW_SESSION_ID=$(node "$JSON_PARSE_HELPER" "$STATE_FILE" '.session_id' 2>/dev/null || echo "unknown")
161
- fi
162
-
163
- # VSCode extension fallback: Extract session ID from transcript path
164
- # Path format: ~/.claude/projects/<project>/<session-uuid>.jsonl
165
- if [ "$RAW_SESSION_ID" = "unknown" ] && [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
166
- RAW_SESSION_ID=$(basename "$TRANSCRIPT_PATH" .jsonl)
167
- fi
54
+ if [ "$RAW_SESSION_ID" = "unknown" ] || [ "$RAW_SESSION_ID" = "null" ] || [ -z "$RAW_SESSION_ID" ]; then
55
+ STATE_FILE="$HOME/.claude/state/current-session.json"
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)
64
+ fi
168
65
  fi
169
66
 
170
67
  # ═══════════════════════════════════════════════════════════════════════════
171
- # INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
172
- # Detects ALL applicable skills/tools and injects as system reminder
68
+ # SKILL AUTO-FIRE: Detect keywords and inject skill reminders
173
69
  # ═══════════════════════════════════════════════════════════════════════════
174
- SKILL_REMINDERS=()
70
+ SKILL_REMINDER=""
175
71
  QUERY_LOWER=$(echo "$USER_QUERY" | tr '[:upper:]' '[:lower:]')
176
72
 
177
- # Memory First - Debug/Error/Problem solving
178
- if echo "$QUERY_LOWER" | grep -qE '(how do i|debug|error|bug|fix|not working|broken|fails|issue|problem|wrong|crash)'; then
179
- SKILL_REMINDERS+=("SKILL REQUIRED: Call Skill(skill: \"ekkOS_Memory_First\") FIRST before debugging")
180
- fi
181
-
182
- # Recall Triggers - Time-based memory
183
- 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
184
- SKILL_REMINDERS+=("SKILL REQUIRED: Call Skill(skill: \"ekkOS_Deep_Recall\") for time-based memory")
185
- fi
186
-
187
- # Directive Triggers - User preferences
188
- if echo "$QUERY_LOWER" | grep -qE '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on)'; then
189
- SKILL_REMINDERS+=("SKILL REQUIRED: Call Skill(skill: \"ekkOS_Preferences\") to capture directive")
190
- fi
191
-
192
- # Safety Triggers - Destructive actions
193
- if echo "$QUERY_LOWER" | grep -qE '(delete|drop |rm -rf|deploy|push.*main|push.*master|production|migrate|rollback)'; then
194
- SKILL_REMINDERS+=("SAFETY REQUIRED: Call ekkOS_Conflict before this destructive action")
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"
195
76
  fi
196
77
 
197
- # Schema Triggers - Database operations
198
- if echo "$QUERY_LOWER" | grep -qE '(sql|query|supabase|prisma|database|table|column|select |insert |update |where )'; then
199
- SKILL_REMINDERS+=("SCHEMA REQUIRED: Call ekkOS_GetSchema for correct field names")
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"
200
81
  fi
201
82
 
202
- # Secret Triggers - API keys, credentials
203
- if echo "$QUERY_LOWER" | grep -qE '(api key|token|password|credential|secret|my.*key|store.*key)'; then
204
- SKILL_REMINDERS+=("SECRETS: Use ekkOS_StoreSecret to securely save credentials")
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"
205
86
  fi
206
87
 
207
- # Plan Triggers - Complex multi-step tasks
208
- if echo "$QUERY_LOWER" | grep -qE '(implement|build|create.*feature|refactor|migrate|set up|architecture)'; then
209
- SKILL_REMINDERS+=("PLAN REQUIRED: Call ekkOS_Plan for complex multi-step tasks")
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"
210
91
  fi
211
92
 
212
- # Learn Triggers - User expressing success/failure
213
- if echo "$QUERY_LOWER" | grep -qE '(that worked|thanks|perfect|great|awesome|nailed it|solved|fixed it)'; then
214
- SKILL_REMINDERS+=("LEARN: Consider calling ekkOS_Forge to capture this solution as a pattern")
215
- fi
216
-
217
- # Codebase Triggers - Project-specific code search
218
- if echo "$QUERY_LOWER" | grep -qE '(where is|find.*file|search.*code|in this project|in the codebase)'; then
219
- SKILL_REMINDERS+=("CODEBASE: Use ekkOS_Codebase for project-specific code search")
220
- fi
221
-
222
- # Combine skill reminders (only take first 3 to avoid noise)
223
- SKILL_REMINDER=""
224
- REMINDER_COUNT=${#SKILL_REMINDERS[@]}
225
- if [ "$REMINDER_COUNT" -gt 0 ]; then
226
- MAX_REMINDERS=3
227
- [ "$REMINDER_COUNT" -lt "$MAX_REMINDERS" ] && MAX_REMINDERS="$REMINDER_COUNT"
228
- for i in $(seq 0 $((MAX_REMINDERS - 1))); do
229
- [ -n "$SKILL_REMINDER" ] && SKILL_REMINDER="$SKILL_REMINDER
230
- "
231
- SKILL_REMINDER="$SKILL_REMINDER${SKILL_REMINDERS[$i]}"
232
- 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"
233
96
  fi
234
97
 
235
98
  # ═══════════════════════════════════════════════════════════════════════════
236
- # Load auth using json-parse helper (no jq)
99
+ # Load auth - No jq
237
100
  # ═══════════════════════════════════════════════════════════════════════════
238
101
  EKKOS_CONFIG="$HOME/.ekkos/config.json"
239
102
  AUTH_TOKEN=""
@@ -248,7 +111,7 @@ fi
248
111
  MEMORY_API_URL="https://mcp.ekkos.dev"
249
112
 
250
113
  # ═══════════════════════════════════════════════════════════════════════════
251
- # Session ID - NEW ID per conversation (not persisted 24h anymore)
114
+ # Session ID
252
115
  # ═══════════════════════════════════════════════════════════════════════════
253
116
  STATE_DIR="$PROJECT_ROOT/.claude/state"
254
117
  mkdir -p "$STATE_DIR" 2>/dev/null || true
@@ -257,7 +120,14 @@ SESSION_FILE="$STATE_DIR/current-session.json"
257
120
  SESSION_ID="$RAW_SESSION_ID"
258
121
 
259
122
  if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
260
- exit 0
123
+ exit 0
124
+ fi
125
+
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
261
131
  fi
262
132
 
263
133
  echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$SESSION_FILE"
@@ -268,336 +138,376 @@ echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:
268
138
  PROJECT_SESSION_DIR="$STATE_DIR/sessions"
269
139
  mkdir -p "$PROJECT_SESSION_DIR" 2>/dev/null || true
270
140
  TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.turn"
141
+ CONTEXT_SIZE_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.context"
271
142
 
143
+ # Count API round-trips from transcript to match TUI turn counter
272
144
  TURN_NUMBER=1
273
145
  if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
274
- TURN_NUMBER=$(grep -c '"type":"user"' "$TRANSCRIPT_PATH" 2>/dev/null || echo "1")
275
- [ "$TURN_NUMBER" -eq 0 ] && TURN_NUMBER=1
146
+ TURN_NUMBER=$(grep -c '"type":"user"' "$TRANSCRIPT_PATH" 2>/dev/null || echo "1")
147
+ [ "$TURN_NUMBER" -eq 0 ] && TURN_NUMBER=1
276
148
  fi
277
149
 
150
+ # Detect post-clear: saved count higher than transcript means /clear happened
278
151
  SAVED_TURN_COUNT=0
279
152
  [ -f "$TURN_COUNTER_FILE" ] && SAVED_TURN_COUNT=$(cat "$TURN_COUNTER_FILE" 2>/dev/null || echo "0")
280
- TRANSCRIPT_TURN_COUNT=$TURN_NUMBER
281
153
  POST_CLEAR_DETECTED=false
154
+
282
155
  if [ "$SAVED_TURN_COUNT" -gt "$TURN_NUMBER" ]; then
283
- TURN_NUMBER=$((SAVED_TURN_COUNT + 1))
284
- POST_CLEAR_DETECTED=true
156
+ POST_CLEAR_DETECTED=true
157
+ TURN_NUMBER=$((SAVED_TURN_COUNT + 1))
285
158
  fi
159
+
286
160
  echo "$TURN_NUMBER" > "$TURN_COUNTER_FILE"
287
161
 
288
162
  # ═══════════════════════════════════════════════════════════════════════════
289
- # WORKING MEMORY: Fast capture each turn (async, non-blocking)
163
+ # Context size tracking - Uses tokenizer script (single source)
290
164
  # ═══════════════════════════════════════════════════════════════════════════
291
- MEMORY_API_URL="https://mcp.ekkos.dev"
292
- if [ -f "$HOME/.ekkos/config.json" ] && [ -f "$JSON_PARSE_HELPER" ]; then
293
- CAPTURE_TOKEN=$(node "$JSON_PARSE_HELPER" "$HOME/.ekkos/config.json" '.hookApiKey' 2>/dev/null || echo "")
294
- if [ -z "$CAPTURE_TOKEN" ]; then
295
- CAPTURE_TOKEN=$(node "$JSON_PARSE_HELPER" "$HOME/.ekkos/config.json" '.apiKey' 2>/dev/null || echo "")
296
- fi
297
- if [ -n "$CAPTURE_TOKEN" ] && [ "$CAPTURE_TOKEN" != "null" ]; then
298
- # Async capture to Redis/Supabase - doesn't block hook execution
299
- # Build JSON safely using node
300
- (node -e "
301
- const body = JSON.stringify({
302
- session_id: process.argv[1],
303
- turn: parseInt(process.argv[2]),
304
- query: process.argv[3]
305
- });
306
- const https = require('https');
307
- const req = https.request({
308
- hostname: 'mcp.ekkos.dev',
309
- path: '/api/v1/working/fast-capture',
310
- method: 'POST',
311
- headers: {
312
- 'Authorization': 'Bearer ' + process.argv[4],
313
- 'Content-Type': 'application/json'
314
- }
315
- }, () => {});
316
- req.on('error', () => {});
317
- req.write(body);
318
- req.end();
319
- " "$RAW_SESSION_ID" "$TURN_NUMBER" "$USER_QUERY" "$CAPTURE_TOKEN" >/dev/null 2>&1) &
320
- fi
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
180
+ fi
321
181
  fi
322
182
 
323
- # ═══════════════════════════════════════════════════════════════════════════
324
- # LOCAL CACHE: Tier 0 capture for instant /continue (async, non-blocking)
325
- # Per ekkOS Onboarding Spec v1.2 ADDENDUM: Pass instanceId for namespacing
326
- # ═══════════════════════════════════════════════════════════════════════════
327
- if command -v ekkos-capture &>/dev/null; then
328
- if [ -z "$SESSION_NAME" ] || [ "$SESSION_NAME" = "unknown-session-starts" ]; then
329
- SESSION_NAME=$(uuid_to_words "$RAW_SESSION_ID" 2>/dev/null || echo "unknown-session")
330
- fi
331
- # NEW format: ekkos-capture user <instance_id> <session_id> <session_name> <turn_id> <query> [project_path]
332
- INSTANCE_ID="${EKKOS_INSTANCE_ID:-default}"
333
- (ekkos-capture user "$INSTANCE_ID" "$RAW_SESSION_ID" "$SESSION_NAME" "$TURN_NUMBER" "$USER_QUERY" "$PROJECT_ROOT" \
334
- >/dev/null 2>&1) &
335
- fi
183
+ echo "$TOKEN_PERCENT" > "$CONTEXT_SIZE_FILE"
336
184
 
337
185
  # ═══════════════════════════════════════════════════════════════════════════
338
- # GOLDEN LOOP: CAPTURE PHASE - Track turn start
186
+ # COLORS
339
187
  # ═══════════════════════════════════════════════════════════════════════════
340
- GOLDEN_LOOP_FILE="$PROJECT_ROOT/.ekkos/golden-loop-current.json"
341
- mkdir -p "$PROJECT_ROOT/.ekkos" 2>/dev/null || true
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'
342
194
 
343
- # Write current phase to file using node (no jq)
344
- node -e "
345
- const fs = require('fs');
346
- const data = {
347
- phase: 'capture',
348
- turn: $TURN_NUMBER,
349
- session: '$SESSION_ID',
350
- timestamp: new Date().toISOString(),
351
- stats: { retrieved: 0, applied: 0, forged: 0 }
352
- };
353
- fs.writeFileSync('$GOLDEN_LOOP_FILE', JSON.stringify(data, null, 2));
354
- " 2>/dev/null || true
195
+ CURRENT_TIME=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
355
196
 
356
197
  # ═══════════════════════════════════════════════════════════════════════════
357
- # GOLDEN LOOP: RETRIEVE PHASE - Auto-retrieve patterns from ekkOS
198
+ # WORD-BASED SESSION NAMES - No jq, uses ~/.ekkos/ config (works in ANY project)
358
199
  # ═══════════════════════════════════════════════════════════════════════════
359
- EKKOS_API_KEY=""
360
- if [ -f "$HOME/.ekkos/.hookApiKey" ]; then
361
- EKKOS_API_KEY=$(cat "$HOME/.ekkos/.hookApiKey" 2>/dev/null || echo "")
362
- elif [ -f "$HOME/.ekkos/config.json" ] && [ -f "$JSON_PARSE_HELPER" ]; then
363
- EKKOS_API_KEY=$(node "$JSON_PARSE_HELPER" "$HOME/.ekkos/config.json" '.hookApiKey' 2>/dev/null || echo "")
364
- fi
200
+ declare -a ADJECTIVES
201
+ declare -a NOUNS
202
+ declare -a VERBS
203
+ SESSION_WORDS_LOADED=false
365
204
 
366
- RETRIEVED_PATTERNS=""
367
- PATTERN_COUNT=0
368
- RETRIEVED_DIRECTIVES=""
369
- DIRECTIVE_COUNT=0
205
+ load_session_words() {
206
+ if [ "$SESSION_WORDS_LOADED" = "true" ]; then return 0; fi
370
207
 
371
- # ═══════════════════════════════════════════════════════════════════════════
372
- # DIRECTIVE CACHE: Local cache to avoid API calls every turn
373
- # ═══════════════════════════════════════════════════════════════════════════
374
- DIRECTIVE_CACHE_DIR="$HOME/.ekkos/cache"
375
- DIRECTIVE_CACHE_FILE="$DIRECTIVE_CACHE_DIR/directives.json"
376
- DIRECTIVE_CACHE_TTL=3600
377
- mkdir -p "$DIRECTIVE_CACHE_DIR" 2>/dev/null || true
208
+ local words_file="$SESSION_WORDS_JSON"
209
+ [ ! -f "$words_file" ] && words_file="$SESSION_WORDS_DEFAULT"
378
210
 
379
- DIRECTIVE_CACHE_VALID=false
380
- DIRECTIVE_TRIGGER_DETECTED=false
211
+ if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
212
+ ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
213
+ return 1
214
+ fi
381
215
 
382
- if echo "$QUERY_LOWER" | grep -qE '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on|directive|preference)'; then
383
- DIRECTIVE_TRIGGER_DETECTED=true
384
- 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
+ }
238
+
239
+ uuid_to_words() {
240
+ local uuid="$1"
241
+ load_session_words
385
242
 
386
- if [ -f "$DIRECTIVE_CACHE_FILE" ] && [ -f "$JSON_PARSE_HELPER" ]; then
387
- CACHE_TIMESTAMP=$(node "$JSON_PARSE_HELPER" "$DIRECTIVE_CACHE_FILE" '.cached_at' 2>/dev/null || echo "0")
388
- CURRENT_TIMESTAMP=$(date +%s)
389
- CACHE_AGE=$((CURRENT_TIMESTAMP - CACHE_TIMESTAMP))
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
+ }
390
255
 
391
- if [ "$CACHE_AGE" -lt "$DIRECTIVE_CACHE_TTL" ] && [ "$DIRECTIVE_TRIGGER_DETECTED" = "false" ]; then
392
- DIRECTIVE_CACHE_VALID=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"
393
263
  fi
394
264
  fi
395
265
 
396
- SHOULD_INJECT_DIRECTIVES=false
397
- if [ "$TURN_NUMBER" -eq 1 ] || [ "$POST_CLEAR_DETECTED" = "true" ] || [ "$DIRECTIVE_TRIGGER_DETECTED" = "true" ]; then
398
- SHOULD_INJECT_DIRECTIVES=true
399
- fi
266
+ # ═══════════════════════════════════════════════════════════════════════════
267
+ # SINGLE SOURCE OF TRUTH: Update ALL session tracking systems
268
+ # ═══════════════════════════════════════════════════════════════════════════
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
400
283
 
401
- if [ -n "$EKKOS_API_KEY" ] && [ -n "$USER_QUERY" ]; then
402
- # Update phase to RETRIEVE
284
+ # 4. Multi-session tracking - No jq
285
+ ACTIVE_SESSIONS_FILE="$HOME/.ekkos/active-sessions.json"
403
286
  node -e "
404
287
  const fs = require('fs');
405
- const data = {
406
- phase: 'retrieve',
407
- turn: $TURN_NUMBER,
408
- session: '$SESSION_ID',
409
- timestamp: new Date().toISOString(),
410
- stats: { retrieved: 0, applied: 0, forged: 0 }
411
- };
412
- fs.writeFileSync('$GOLDEN_LOOP_FILE', JSON.stringify(data, null, 2));
413
- " 2>/dev/null || true
414
-
415
- # Build sources array
416
- if [ "$DIRECTIVE_CACHE_VALID" = "true" ]; then
417
- SEARCH_SOURCES='["patterns"]'
418
- else
419
- SEARCH_SOURCES='["patterns", "directives"]'
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 &
420
312
  fi
421
313
 
422
- # Call ekkOS MCP gateway using node (no curl dependency for JSON handling)
423
- SEARCH_RESPONSE=$(node -e "
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 "
424
322
  const https = require('https');
425
323
  const body = JSON.stringify({
426
- tool: 'ekkOS_Search',
427
- arguments: {
428
- query: process.argv[1],
429
- limit: 5,
430
- sources: $SEARCH_SOURCES
431
- }
324
+ userId: process.argv[1],
325
+ realSession: process.argv[2],
326
+ projectPath: process.argv[3],
327
+ pendingSession: process.argv[4]
432
328
  });
433
329
  const req = https.request({
434
- hostname: 'api.ekkos.dev',
435
- path: '/api/v1/mcp/call',
330
+ hostname: 'mcp.ekkos.dev',
331
+ path: '/proxy/session/bind',
436
332
  method: 'POST',
437
- headers: {
438
- 'Authorization': 'Bearer ' + process.argv[2],
439
- 'Content-Type': 'application/json',
440
- 'Content-Length': Buffer.byteLength(body)
441
- },
442
- timeout: 2000
443
- }, (res) => {
444
- let data = '';
445
- res.on('data', chunk => data += chunk);
446
- res.on('end', () => console.log(data));
447
- });
448
- req.on('error', () => console.log('{}'));
449
- req.on('timeout', () => { req.destroy(); console.log('{}'); });
333
+ headers: {'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body)}
334
+ }, () => {});
335
+ req.on('error', () => {});
450
336
  req.write(body);
451
337
  req.end();
452
- " "$USER_QUERY" "$EKKOS_API_KEY" 2>/dev/null || echo '{}')
453
-
454
- # Parse pattern count using node
455
- PATTERN_COUNT=$(echo "$SEARCH_RESPONSE" | node -e "
456
- const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
457
- console.log((data?.result?.results?.patterns?.length || 0));
458
- " 2>/dev/null || echo "0")
459
-
460
- if [ "$PATTERN_COUNT" -gt 0 ]; then
461
- # Update golden loop
462
- node -e "
463
- const fs = require('fs');
464
- const data = {
465
- phase: 'inject',
466
- turn: $TURN_NUMBER,
467
- session: '$SESSION_ID',
468
- timestamp: new Date().toISOString(),
469
- stats: { retrieved: $PATTERN_COUNT, applied: 0, forged: 0 }
470
- };
471
- fs.writeFileSync('$GOLDEN_LOOP_FILE', JSON.stringify(data, null, 2));
472
- " 2>/dev/null || true
473
-
474
- # Format patterns for injection
475
- RETRIEVED_PATTERNS=$(echo "$SEARCH_RESPONSE" | node -e "
476
- const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
477
- const patterns = data?.result?.results?.patterns || [];
478
- patterns.forEach(p => {
479
- console.log('**' + (p.title || 'Untitled') + '**');
480
- console.log((p.problem || p.guidance || '') + '\\n');
481
- console.log('## Solution');
482
- console.log((p.solution || p.content || '') + '\\n');
483
- console.log('Success Rate: ' + Math.round((p.success_rate || 0) * 100) + '%');
484
- console.log('Applied: ' + (p.applied_count || 0) + ' times\\n');
485
- });
486
- " 2>/dev/null || echo "")
487
- fi
488
-
489
- # Handle directives
490
- if [ "$DIRECTIVE_CACHE_VALID" = "true" ] && [ -f "$DIRECTIVE_CACHE_FILE" ] && [ -f "$JSON_PARSE_HELPER" ]; then
491
- DIRECTIVE_COUNT=$(node "$JSON_PARSE_HELPER" "$DIRECTIVE_CACHE_FILE" '.count' 2>/dev/null || echo "0")
492
- RETRIEVED_DIRECTIVES=$(node "$JSON_PARSE_HELPER" "$DIRECTIVE_CACHE_FILE" '.formatted' 2>/dev/null || echo "")
493
- else
494
- DIRECTIVE_COUNT=$(echo "$SEARCH_RESPONSE" | node -e "
495
- const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
496
- console.log((data?.result?.results?.directives?.length || 0));
497
- " 2>/dev/null || echo "0")
498
-
499
- if [ "$DIRECTIVE_COUNT" -gt 0 ]; then
500
- RETRIEVED_DIRECTIVES=$(echo "$SEARCH_RESPONSE" | node -e "
501
- const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
502
- const directives = data?.result?.results?.directives || [];
503
- const byType = { MUST: [], NEVER: [], PREFER: [], AVOID: [] };
504
- directives.forEach(d => {
505
- if (byType[d.type]) byType[d.type].push(' - ' + d.rule);
506
- });
507
- let output = 'USER DIRECTIVES (FOLLOW THESE):';
508
- if (byType.MUST.length) output += '\\n\\nMUST:\\n' + byType.MUST.join('\\n');
509
- if (byType.NEVER.length) output += '\\n\\nNEVER:\\n' + byType.NEVER.join('\\n');
510
- if (byType.PREFER.length) output += '\\n\\nPREFER:\\n' + byType.PREFER.join('\\n');
511
- if (byType.AVOID.length) output += '\\n\\nAVOID:\\n' + byType.AVOID.join('\\n');
512
- console.log(output);
513
- " 2>/dev/null || echo "")
514
-
515
- # Save to cache
516
- node -e "
517
- const fs = require('fs');
518
- fs.writeFileSync('$DIRECTIVE_CACHE_FILE', JSON.stringify({
519
- count: $DIRECTIVE_COUNT,
520
- formatted: process.argv[1],
521
- cached_at: Math.floor(Date.now() / 1000)
522
- }, null, 2));
523
- " "$RETRIEVED_DIRECTIVES" 2>/dev/null || true
524
- fi
338
+ " "$EKKOS_USER_ID" "$SESSION_NAME" "$PROJECT_ROOT" "$PENDING_SESSION" 2>/dev/null &
525
339
  fi
526
340
  fi
527
341
 
528
342
  # ═══════════════════════════════════════════════════════════════════════════
529
- # COLORS
343
+ # "/continue" COMMAND: Run AFTER /clear to restore last 5 turns
530
344
  # ═══════════════════════════════════════════════════════════════════════════
531
- CYAN='\033[0;36m'
532
- GREEN='\033[0;32m'
533
- YELLOW='\033[1;33m'
534
- MAGENTA='\033[0;35m'
535
- DIM='\033[2m'
536
- BOLD='\033[1m'
537
- RESET='\033[0m'
345
+ QUERY_LOWER=$(echo "$USER_QUERY" | tr '[:upper:]' '[:lower:]')
538
346
 
539
- CURRENT_TIME=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
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")
540
360
 
541
- # Generate session name
542
- SESSION_NAME=""
543
- if [ -n "$SESSION_ID" ] && [ "$SESSION_ID" != "unknown" ] && [ "$SESSION_ID" != "null" ]; then
544
- SESSION_NAME=$(uuid_to_words "$SESSION_ID")
545
- fi
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")
546
366
 
547
- # Simple status line
548
- echo -e "${CYAN}${BOLD}ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
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 "")
549
372
 
550
- # Output skill reminder if detected
551
- if [ -n "$SKILL_REMINDER" ]; then
552
373
  echo ""
553
- echo -e "${MAGENTA}${BOLD}$SKILL_REMINDER${RESET}"
554
- fi
555
-
556
- # Inject directives FIRST (highest priority)
557
- if [ "$SHOULD_INJECT_DIRECTIVES" = "true" ] && [ -n "$RETRIEVED_DIRECTIVES" ] && [ "$DIRECTIVE_COUNT" -gt 0 ]; then
374
+ echo -e "${GREEN}${BOLD}✓ Session continued${RESET} ${DIM}(${RESTORED_COUNT} turns restored)${RESET}"
558
375
  echo ""
559
- echo "<system-reminder>"
560
- echo -e "$RETRIEVED_DIRECTIVES"
561
- echo "</system-reminder>"
562
- fi
563
376
 
564
- # Inject retrieved patterns into context
565
- if [ -n "$RETRIEVED_PATTERNS" ] && [ "$PATTERN_COUNT" -gt 0 ]; then
566
- echo ""
567
377
  echo "<system-reminder>"
568
- echo "RETRIEVED PATTERNS FROM ekkOS MEMORY ($PATTERN_COUNT patterns found)"
378
+ echo "═══════════════════════════════════════════════════════════════════════"
379
+ echo "CONTEXT RESTORED - Resume seamlessly. DO NOT ask 'what were we doing?'"
380
+ echo "═══════════════════════════════════════════════════════════════════════"
569
381
  echo ""
570
- echo -e "$RETRIEVED_PATTERNS"
382
+ echo "## Last User Request:"
383
+ echo "$LAST_TASK"
571
384
  echo ""
572
- echo "These patterns were automatically retrieved based on your query. Review and apply relevant solutions."
573
- echo "</system-reminder>"
385
+ echo "## Your Last Response (truncated):"
386
+ echo "$LAST_RESPONSE"
574
387
  echo ""
575
- echo "MANDATORY PATTERN ACKNOWLEDGMENT (PatternGuard):"
576
- echo "You MUST acknowledge ALL $PATTERN_COUNT patterns above - either SELECT or SKIP each one."
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}| Turn ${TURN_NUMBER} | ${CURRENT_TIME}${RESET}"
409
+ exit 0
410
+ fi
411
+ fi
412
+
413
+ # ═══════════════════════════════════════════════════════════════════════════
414
+ # COMPACTION DETECTION: If context dropped dramatically, auto-restore
415
+ # Was >50% last turn, now <15% = compaction happened
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}"
420
+
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"
577
435
  echo ""
578
- echo "For patterns you USE:"
579
- echo "[ekkOS_SELECT]"
580
- echo "- id: <pattern_id>"
581
- echo " reason: <1-line why using>"
582
- echo " confidence: <0.0-1.0>"
583
- echo "[/ekkOS_SELECT]"
436
+ echo -e "${MAGENTA}${BOLD}## Recent Context (auto-restored)${RESET}"
584
437
  echo ""
585
- echo "For patterns NOT relevant:"
586
- echo "[ekkOS_SKIP]"
587
- echo "- id: <pattern_id>"
588
- echo " reason: <1-line why not relevant>"
589
- echo "[/ekkOS_SKIP]"
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
+
590
448
  echo ""
591
- echo "AUTOMATIC APPLICATION: When pattern matches your task, APPLY IT immediately without asking."
592
- echo "Track outcome after: ekkOS_Outcome({success: true/false})"
449
+ echo -e "${DIM}Full history: \"turns 1-${TURN_NUMBER}\" or \"recall yesterday\"${RESET}"
450
+ fi
451
+
452
+ echo ""
453
+ echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
454
+
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
460
+
461
+ echo ""
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"
593
480
  echo ""
594
- echo "100% coverage required. This is how the system learns what works."
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
+
494
+ echo ""
495
+ echo -e "${DIM}Full history: \"recall\" or \"turns 1-${TURN_NUMBER}\"${RESET}"
496
+ echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
497
+ echo ""
498
+ echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
499
+
500
+ elif [ "$TOKEN_PERCENT" -ge 50 ]; then
501
+ echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ~${IPC_PERCENT:-0}% IPC | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
502
+
503
+ else
504
+ echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
595
505
  fi
596
506
 
597
- # Inject footer format reminder
598
- if [ -n "$SESSION_NAME" ] && [ "$SESSION_NAME" != "unknown-session" ]; then
599
- echo ""
600
- echo "<footer-format>End responses with: Claude Code ({Model}) - ekkOS - Turn ${TURN_NUMBER} - ${SESSION_NAME} - ${CURRENT_TIME}</footer-format>"
507
+ # Output skill reminder if detected
508
+ if [ -n "$SKILL_REMINDER" ]; then
509
+ echo ""
510
+ echo -e "${MAGENTA}${BOLD}$SKILL_REMINDER${RESET}"
601
511
  fi
602
512
 
603
513
  exit 0