@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,15 +1,15 @@
1
1
  #!/bin/bash
2
2
  # ═══════════════════════════════════════════════════════════════════════════
3
- # ekkOS_ Hook: Stop - FULL CONTEXT CAPTURE
4
- # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
5
- # EKKOS_MANAGED=1
3
+ # ekkOS_ Hook: Stop - Captures turns to BOTH Working (Redis) and Episodic (Supabase)
4
+ # NO jq dependency - uses Node.js for all JSON parsing
6
5
  # ═══════════════════════════════════════════════════════════════════════════
7
- # Captures FULL turn content to L2 (episodic memory):
8
- # - Full user query
9
- # - Full assistant response (no truncation)
10
- # - Complete file changes with edit content (old_string → new_string)
6
+ # This hook captures every turn to:
7
+ # 1. Working Sessions (Redis) - Fast hot cache for /continue
8
+ # 2. Episodic Memory (Supabase) - Permanent cold storage
11
9
  #
12
- # Per spec v1.2 Addendum: NO jq dependency, NO hardcoded arrays
10
+ # NO compliance checking - skills handle that
11
+ # NO PatternGuard validation - skills handle that
12
+ # NO verbose output - just capture silently
13
13
  # ═══════════════════════════════════════════════════════════════════════════
14
14
 
15
15
  set +e
@@ -18,8 +18,11 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
18
  PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
19
19
  STATE_DIR="$PROJECT_ROOT/.claude/state"
20
20
 
21
+ mkdir -p "$STATE_DIR" 2>/dev/null
22
+
21
23
  # ═══════════════════════════════════════════════════════════════════════════
22
- # CONFIG PATHS - Per spec v1.2 Addendum
24
+ # CONFIG PATHS - No jq dependency (v1.2 spec)
25
+ # Session words live in ~/.ekkos/ so they work in ANY project
23
26
  # ═══════════════════════════════════════════════════════════════════════════
24
27
  EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
25
28
  SESSION_WORDS_JSON="$EKKOS_CONFIG_DIR/session-words.json"
@@ -28,7 +31,7 @@ JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
28
31
 
29
32
  INPUT=$(cat)
30
33
 
31
- # Parse JSON using Node (no jq)
34
+ # JSON parsing helper (no jq)
32
35
  parse_json_value() {
33
36
  local json="$1"
34
37
  local path="$2"
@@ -46,57 +49,28 @@ if (result !== undefined && result !== null) console.log(result);
46
49
 
47
50
  RAW_SESSION_ID=$(parse_json_value "$INPUT" '.session_id')
48
51
  [ -z "$RAW_SESSION_ID" ] && RAW_SESSION_ID="unknown"
49
-
50
52
  TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
51
53
  MODEL_USED=$(parse_json_value "$INPUT" '.model')
52
54
  [ -z "$MODEL_USED" ] && MODEL_USED="claude-sonnet-4-5"
53
55
 
54
- # DEBUG: Log hook input
55
- echo "[ekkOS DEBUG] $(date -u +%H:%M:%S) stop.sh: session=$RAW_SESSION_ID, transcript_path=$TRANSCRIPT_PATH" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
56
-
57
56
  # ═══════════════════════════════════════════════════════════════════════════
58
- # Session ID - Try Claude's input first, fallback to state file
57
+ # Session ID
59
58
  # ═══════════════════════════════════════════════════════════════════════════
60
59
  SESSION_ID="$RAW_SESSION_ID"
61
60
 
62
- # Fallback: Read from state file if input doesn't have valid session_id
63
61
  if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
64
- STATE_FILE="$HOME/.claude/state/current-session.json"
65
- if [ -f "$STATE_FILE" ] && [ -f "$JSON_PARSE_HELPER" ]; then
66
- SESSION_ID=$(node "$JSON_PARSE_HELPER" "$STATE_FILE" '.session_id' 2>/dev/null || echo "")
67
- fi
68
- fi
69
-
70
- # Skip if still no valid session ID
71
- if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
72
- exit 0
73
- fi
74
-
75
- # ═══════════════════════════════════════════════════════════════════════════
76
- # Load auth
77
- # ═══════════════════════════════════════════════════════════════════════════
78
- EKKOS_CONFIG="$HOME/.ekkos/config.json"
79
- AUTH_TOKEN=""
80
- USER_ID=""
81
-
82
- if [ -f "$EKKOS_CONFIG" ] && [ -f "$JSON_PARSE_HELPER" ]; then
83
- AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.hookApiKey' 2>/dev/null || echo "")
84
- if [ -z "$AUTH_TOKEN" ]; then
85
- AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.apiKey' 2>/dev/null || echo "")
86
- fi
87
- USER_ID=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.userId' 2>/dev/null || echo "")
62
+ exit 0
88
63
  fi
89
64
 
90
- if [ -z "$AUTH_TOKEN" ] && [ -f "$PROJECT_ROOT/.env.local" ]; then
91
- AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | tr -d '\r')
65
+ # Check if SESSION_ID is a UUID (8-4-4-4-12 format)
66
+ UUID_REGEX='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
67
+ IS_UUID=false
68
+ if [[ "$SESSION_ID" =~ $UUID_REGEX ]]; then
69
+ IS_UUID=true
92
70
  fi
93
71
 
94
- [ -z "$AUTH_TOKEN" ] && exit 0
95
-
96
- MEMORY_API_URL="https://mcp.ekkos.dev"
97
-
98
72
  # ═══════════════════════════════════════════════════════════════════════════
99
- # WORD-BASED SESSION NAMES - Uses external session-words.json (NO hardcoded arrays)
73
+ # WORD-BASED SESSION NAMES - No jq, uses ~/.ekkos/ config (works in ANY project)
100
74
  # ═══════════════════════════════════════════════════════════════════════════
101
75
  declare -a ADJECTIVES
102
76
  declare -a NOUNS
@@ -104,16 +78,13 @@ declare -a VERBS
104
78
  SESSION_WORDS_LOADED=false
105
79
 
106
80
  load_session_words() {
107
- if [ "$SESSION_WORDS_LOADED" = "true" ]; then
108
- return 0
109
- fi
81
+ if [ "$SESSION_WORDS_LOADED" = "true" ]; then return 0; fi
110
82
 
111
83
  local words_file="$SESSION_WORDS_JSON"
112
- if [ ! -f "$words_file" ]; then
113
- words_file="$SESSION_WORDS_DEFAULT"
114
- fi
84
+ [ ! -f "$words_file" ] && words_file="$SESSION_WORDS_DEFAULT"
115
85
 
116
86
  if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
87
+ ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
117
88
  return 1
118
89
  fi
119
90
 
@@ -130,93 +101,97 @@ load_session_words() {
130
101
  i=0
131
102
  while IFS= read -r line; do VERBS[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
132
103
  fi
133
-
134
- if [ ${#ADJECTIVES[@]} -gt 0 ] && [ ${#NOUNS[@]} -gt 0 ] && [ ${#VERBS[@]} -gt 0 ]; then
135
- SESSION_WORDS_LOADED=true
136
- return 0
137
- fi
104
+ [ ${#ADJECTIVES[@]} -eq 0 ] && ADJECTIVES=("unknown")
105
+ [ ${#NOUNS[@]} -eq 0 ] && NOUNS=("session")
106
+ [ ${#VERBS[@]} -eq 0 ] && VERBS=("starts")
107
+ SESSION_WORDS_LOADED=true
108
+ return 0
138
109
  fi
110
+ ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
139
111
  return 1
140
112
  }
141
113
 
142
114
  uuid_to_words() {
143
115
  local uuid="$1"
144
-
145
- load_session_words || {
146
- echo "unknown-session-starts"
147
- return
148
- }
116
+ load_session_words
149
117
 
150
118
  local hex="${uuid//-/}"
151
119
  hex="${hex:0:12}"
152
-
153
- if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
154
- echo "unknown-session-starts"
155
- return
156
- fi
120
+ [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]] && echo "unknown-session-starts" && return
157
121
 
158
122
  local adj_seed=$((16#${hex:0:4}))
159
123
  local noun_seed=$((16#${hex:4:4}))
160
124
  local verb_seed=$((16#${hex:8:4}))
161
-
162
125
  local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
163
126
  local noun_idx=$((noun_seed % ${#NOUNS[@]}))
164
127
  local verb_idx=$((verb_seed % ${#VERBS[@]}))
165
-
166
128
  echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
167
129
  }
168
130
 
169
- # Generate session name from UUID
170
- SESSION_NAME=""
171
- if [ -n "$SESSION_ID" ] && [ "$SESSION_ID" != "unknown" ] && [ "$SESSION_ID" != "null" ]; then
131
+ if [ "$IS_UUID" = true ]; then
172
132
  SESSION_NAME=$(uuid_to_words "$SESSION_ID")
133
+ else
134
+ SESSION_NAME="$SESSION_ID"
173
135
  fi
174
136
 
175
137
  # ═══════════════════════════════════════════════════════════════════════════
176
- # Get turn number from local counter
138
+ # Turn Number - read from turn file written by user-prompt-submit.sh
177
139
  # ═══════════════════════════════════════════════════════════════════════════
178
- PROJECT_SESSION_DIR="$STATE_DIR/sessions"
179
- TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.turn"
140
+ TURN_FILE="$STATE_DIR/sessions/${SESSION_ID}.turn"
141
+ mkdir -p "$STATE_DIR/sessions" 2>/dev/null
180
142
  TURN_NUMBER=1
181
- [ -f "$TURN_COUNTER_FILE" ] && TURN_NUMBER=$(cat "$TURN_COUNTER_FILE" 2>/dev/null || echo "1")
143
+ [ -f "$TURN_FILE" ] && TURN_NUMBER=$(cat "$TURN_FILE" 2>/dev/null || echo "1")
182
144
 
183
145
  # ═══════════════════════════════════════════════════════════════════════════
184
- # AUTO-CLEAR DETECTION (EARLY): Must run BEFORE any early exits
146
+ # Load auth - No jq
185
147
  # ═══════════════════════════════════════════════════════════════════════════
186
- if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
187
- MAX_TOKENS=200000
148
+ EKKOS_CONFIG="$HOME/.ekkos/config.json"
149
+ AUTH_TOKEN=""
150
+ USER_ID=""
188
151
 
189
- if stat -f%z "$TRANSCRIPT_PATH" >/dev/null 2>&1; then
190
- FILE_SIZE=$(stat -f%z "$TRANSCRIPT_PATH")
191
- else
192
- FILE_SIZE=$(stat -c%s "$TRANSCRIPT_PATH" 2>/dev/null || echo "0")
193
- fi
194
- ROUGH_TOKENS=$((FILE_SIZE / 4))
195
- TOKEN_PERCENT=$((ROUGH_TOKENS * 100 / MAX_TOKENS))
152
+ if [ -f "$EKKOS_CONFIG" ] && [ -f "$JSON_PARSE_HELPER" ]; then
153
+ AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.hookApiKey' 2>/dev/null || echo "")
154
+ [ -z "$AUTH_TOKEN" ] && AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.apiKey' 2>/dev/null || echo "")
155
+ USER_ID=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.userId' 2>/dev/null || echo "")
156
+ fi
196
157
 
197
- if [ "$TOKEN_PERCENT" -gt 50 ]; then
198
- WORD_COUNT=$(wc -w < "$TRANSCRIPT_PATH" 2>/dev/null | tr -d ' ' || echo "0")
199
- TOKEN_PERCENT=$((WORD_COUNT * 13 / 10 * 100 / MAX_TOKENS))
200
- fi
158
+ if [ -z "$AUTH_TOKEN" ] && [ -f "$PROJECT_ROOT/.env.local" ]; then
159
+ AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | tr -d '\r')
160
+ fi
201
161
 
202
- if [ "$TOKEN_PERCENT" -ge 92 ]; then
203
- AUTO_CLEAR_FLAG="$HOME/.ekkos/auto-clear.flag"
204
- TIMESTAMP_EPOCH=$(date +%s)
205
- echo "${TOKEN_PERCENT}:${SESSION_NAME}:${TIMESTAMP_EPOCH}" > "$AUTO_CLEAR_FLAG"
206
- echo "[ekkOS] Context at ${TOKEN_PERCENT}% - auto-clear flag written (session: $SESSION_NAME)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
207
- fi
162
+ [ -z "$AUTH_TOKEN" ] && exit 0
163
+
164
+ MEMORY_API_URL="https://mcp.ekkos.dev"
165
+
166
+ # ═══════════════════════════════════════════════════════════════════════════
167
+ # SESSION BINDING: Bridge _pending → real session name for proxy eviction
168
+ # The CLI may be in spawn pass-through mode (no PTY = blind to TUI output),
169
+ # so the stop hook (which IS sighted) must bind the session.
170
+ # ═══════════════════════════════════════════════════════════════════════════
171
+ if [ -n "$SESSION_NAME" ] && [ "$SESSION_NAME" != "unknown-session-starts" ] && [ -n "$USER_ID" ]; then
172
+ PROJECT_PATH_FOR_BIND=$(pwd)
173
+ PENDING_SESSION_FOR_BIND="${EKKOS_PENDING_SESSION:-_pending}"
174
+ curl -s -X POST "$MEMORY_API_URL/proxy/session/bind" \
175
+ -H "Content-Type: application/json" \
176
+ -d "{\"userId\":\"$USER_ID\",\"realSession\":\"$SESSION_NAME\",\"projectPath\":\"$PROJECT_PATH_FOR_BIND\",\"pendingSession\":\"$PENDING_SESSION_FOR_BIND\"}" \
177
+ --connect-timeout 1 \
178
+ --max-time 2 >/dev/null 2>&1 &
208
179
  fi
209
180
 
210
181
  # ═══════════════════════════════════════════════════════════════════════════
211
- # Check for interruption - skip capture if request was interrupted
182
+ # EVICTION: Handled by IPC (In-Place Progressive Compression) in the proxy.
183
+ # No hook-side eviction needed — passthrough is default for cache stability.
184
+ # ═══════════════════════════════════════════════════════════════════════════
185
+
186
+ # ═══════════════════════════════════════════════════════════════════════════
187
+ # Check for interruption - No jq
212
188
  # ═══════════════════════════════════════════════════════════════════════════
213
189
  IS_INTERRUPTED=$(parse_json_value "$INPUT" '.interrupted')
214
190
  [ -z "$IS_INTERRUPTED" ] && IS_INTERRUPTED="false"
215
-
216
191
  STOP_REASON=$(parse_json_value "$INPUT" '.stop_reason')
217
192
 
218
193
  if [ "$IS_INTERRUPTED" = "true" ] || [ "$STOP_REASON" = "user_cancelled" ] || [ "$STOP_REASON" = "interrupted" ]; then
219
- exit 0
194
+ exit 0
220
195
  fi
221
196
 
222
197
  # ═══════════════════════════════════════════════════════════════════════════
@@ -227,75 +202,53 @@ LAST_ASSISTANT=""
227
202
  FILE_CHANGES="[]"
228
203
 
229
204
  if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
230
- # Extract using Node - handles complex JSON reliably
231
205
  EXTRACTION=$(node -e "
232
206
  const fs = require('fs');
233
- const lines = fs.readFileSync('$TRANSCRIPT_PATH', 'utf8').split('\\n').filter(Boolean);
207
+ const lines = fs.readFileSync('$TRANSCRIPT_PATH', 'utf8').split('\n').filter(Boolean);
234
208
  const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
235
209
 
236
- // Get last user message (not starting with <)
237
- let lastUser = '';
238
- let lastUserTime = '';
210
+ let lastUser = '', lastUserTime = '';
239
211
  for (let i = entries.length - 1; i >= 0; i--) {
240
212
  const e = entries[i];
241
213
  if (e.type === 'user') {
242
214
  const content = e.message?.content;
243
215
  if (typeof content === 'string' && !content.startsWith('<')) {
244
- lastUser = content;
245
- lastUserTime = e.timestamp || '';
246
- break;
216
+ lastUser = content; lastUserTime = e.timestamp || ''; break;
247
217
  } else if (Array.isArray(content)) {
248
218
  const textPart = content.find(c => c.type === 'text' && !c.text?.startsWith('<'));
249
- if (textPart) {
250
- lastUser = textPart.text;
251
- lastUserTime = e.timestamp || '';
252
- break;
253
- }
219
+ if (textPart) { lastUser = textPart.text; lastUserTime = e.timestamp || ''; break; }
254
220
  }
255
221
  }
256
222
  }
257
223
 
258
- // Get last assistant message (after the user message)
259
224
  let lastAssistant = '';
260
225
  for (let i = entries.length - 1; i >= 0; i--) {
261
226
  const e = entries[i];
262
227
  if (e.type === 'assistant' && (!lastUserTime || e.timestamp >= lastUserTime)) {
263
228
  const content = e.message?.content;
264
- if (typeof content === 'string') {
265
- lastAssistant = content;
266
- break;
267
- } else if (Array.isArray(content)) {
229
+ if (typeof content === 'string') { lastAssistant = content; break; }
230
+ else if (Array.isArray(content)) {
268
231
  const parts = content.map(c => {
269
232
  if (c.type === 'text') return c.text;
270
233
  if (c.type === 'tool_use') return '[TOOL: ' + c.name + ']';
271
234
  if (c.type === 'thinking') return '[THINKING]' + (c.thinking || c.text || '') + '[/THINKING]';
272
235
  return '';
273
236
  }).filter(Boolean);
274
- lastAssistant = parts.join('\\n');
275
- break;
237
+ lastAssistant = parts.join('\n'); break;
276
238
  }
277
239
  }
278
240
  }
279
241
 
280
- // Extract file changes
281
242
  const fileChanges = [];
282
243
  entries.filter(e => e.type === 'assistant').forEach(e => {
283
244
  const content = e.message?.content;
284
245
  if (Array.isArray(content)) {
285
246
  content.filter(c => c.type === 'tool_use' && ['Edit', 'Write', 'Read'].includes(c.name)).forEach(c => {
286
- fileChanges.push({
287
- tool: c.name,
288
- path: c.input?.file_path || c.input?.path,
289
- action: c.name.toLowerCase(),
290
- old_string: c.name === 'Edit' ? (c.input?.old_string || '').substring(0, 200) : null,
291
- new_string: c.name === 'Edit' ? (c.input?.new_string || '').substring(0, 200) : null,
292
- content: c.name === 'Write' ? (c.input?.content || '').substring(0, 500) : null
293
- });
247
+ fileChanges.push({tool: c.name, path: c.input?.file_path || c.input?.path, action: c.name.toLowerCase()});
294
248
  });
295
249
  }
296
250
  });
297
251
 
298
- // Output as JSON
299
252
  console.log(JSON.stringify({
300
253
  user: lastUser,
301
254
  assistant: lastAssistant.substring(0, 50000),
@@ -308,113 +261,45 @@ console.log(JSON.stringify({
308
261
  FILE_CHANGES=$(echo "$EXTRACTION" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(JSON.stringify(d.fileChanges||[]))" 2>/dev/null || echo "[]")
309
262
  fi
310
263
 
311
- # Check for interruption markers
312
- if [[ "$LAST_USER" == *"[Request interrupted"* ]] || [[ "$LAST_USER" == *"interrupted by user"* ]]; then
313
- echo "[ekkOS] Turn $TURN_NUMBER skipped: interruption marker (session: $SESSION_NAME)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
264
+ if [ -z "$LAST_USER" ] || [[ "$LAST_USER" == *"[Request interrupted"* ]]; then
314
265
  exit 0
315
266
  fi
316
267
 
317
268
  # ═══════════════════════════════════════════════════════════════════════════
318
- # Capture to L2 (episodic memory) - SYNCHRONOUS for reliability
269
+ # Capture to BOTH Working Sessions (Redis) AND Episodic (Supabase) - No jq
319
270
  # ═══════════════════════════════════════════════════════════════════════════
320
- if [ -z "$LAST_ASSISTANT" ]; then
321
- echo "[ekkOS] Turn $TURN_NUMBER skipped: LAST_ASSISTANT empty (session: $SESSION_NAME)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
322
- fi
323
-
324
271
  if [ -n "$LAST_USER" ] && [ -n "$LAST_ASSISTANT" ]; then
325
- PAYLOAD_FILE=$(mktemp /tmp/ekkos-capture.XXXXXX.json)
272
+ (
326
273
  TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
327
274
 
328
- # Build payload using Node (no jq)
329
- node -e "
330
- const fs = require('fs');
331
- const payload = {
332
- user_query: process.argv[1],
333
- assistant_response: process.argv[2],
334
- session_id: process.argv[3],
335
- user_id: process.argv[4] || 'system',
336
- file_changes: JSON.parse(process.argv[5] || '[]'),
337
- metadata: {
338
- source: 'claude-code',
339
- model_used: process.argv[6],
340
- captured_at: process.argv[7],
341
- minimal_hook: true
342
- }
343
- };
344
- fs.writeFileSync('$PAYLOAD_FILE', JSON.stringify(payload));
345
- " "$LAST_USER" "$LAST_ASSISTANT" "$SESSION_ID" "${USER_ID:-system}" "$FILE_CHANGES" "$MODEL_USED" "$TIMESTAMP" 2>/dev/null
346
-
347
- # Validate and send
348
- if node -e "JSON.parse(require('fs').readFileSync('$PAYLOAD_FILE','utf8'))" 2>/dev/null; then
349
- for RETRY in 1 2 3; do
350
- CAPTURE_RESULT=$(curl -s -w "\n%{http_code}" -X POST "$MEMORY_API_URL/api/v1/memory/capture" \
351
- -H "Authorization: Bearer $AUTH_TOKEN" \
352
- -H "Content-Type: application/json" \
353
- -d "@$PAYLOAD_FILE" \
354
- --connect-timeout 3 \
355
- --max-time 5 2>/dev/null || echo -e "\n000")
356
-
357
- HTTP_CODE=$(echo "$CAPTURE_RESULT" | tail -1)
358
-
359
- if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
360
- break
361
- fi
362
- [ $RETRY -lt 3 ] && sleep 0.5
363
- done
364
-
365
- if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
366
- echo "[ekkOS] L2 capture failed after 3 attempts: HTTP $HTTP_CODE" >&2
367
- mkdir -p "$HOME/.ekkos/wal" 2>/dev/null
368
- cp "$PAYLOAD_FILE" "$HOME/.ekkos/wal/l2-$(date +%s)-$$.json" 2>/dev/null
369
- fi
370
- fi
371
-
372
- rm -f "$PAYLOAD_FILE" 2>/dev/null
373
- fi
374
-
375
- # ═══════════════════════════════════════════════════════════════════════════
376
- # REDIS WORKING MEMORY: Store verbatim turn in multi-session hot cache
377
- # ═══════════════════════════════════════════════════════════════════════════
378
- if [ -n "$LAST_USER" ] && [ -n "$LAST_ASSISTANT" ] && [ -n "$SESSION_NAME" ]; then
379
- REDIS_PAYLOAD_FILE=$(mktemp /tmp/ekkos-redis.XXXXXX.json)
380
-
381
- # Extract tools used
382
- TOOLS_USED=$(echo "$LAST_ASSISTANT" | grep -oE '\[TOOL: [^\]]+\]' | sed 's/\[TOOL: //g; s/\]//g' | sort -u | node -e "
383
- const lines = require('fs').readFileSync('/dev/stdin','utf8').split('\\n').filter(Boolean);
384
- console.log(JSON.stringify(lines));
275
+ TOOLS_USED=$(echo "$FILE_CHANGES" | node -e "
276
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8') || '[]');
277
+ console.log(JSON.stringify([...new Set(d.map(f => f.tool).filter(Boolean))]));
385
278
  " 2>/dev/null || echo "[]")
386
279
 
387
- # Extract files referenced
388
- FILES_REFERENCED=$(echo "$FILE_CHANGES" | node -e "
280
+ FILES_REF=$(echo "$FILE_CHANGES" | node -e "
389
281
  const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8') || '[]');
390
282
  console.log(JSON.stringify([...new Set(d.map(f => f.path).filter(Boolean))]));
391
283
  " 2>/dev/null || echo "[]")
392
284
 
393
- # Get token counts from transcript
394
- TOTAL_CONTEXT_TOKENS=0
285
+ # Token breakdown from tokenizer script
286
+ TOTAL_TOKENS=0
395
287
  INPUT_TOKENS=0
396
- OUTPUT_TOKENS=0
397
-
398
- if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
399
- TOKEN_DATA=$(grep '"usage"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1 | node -e "
400
- const line = require('fs').readFileSync('/dev/stdin','utf8');
401
- try {
402
- const d = JSON.parse(line);
403
- const u = d.message?.usage || {};
404
- const input = (u.input_tokens || 0) + (u.cache_creation_input_tokens || 0) + (u.cache_read_input_tokens || 0);
405
- const output = u.output_tokens || 0;
406
- console.log(input + ':' + output);
407
- } catch { console.log('0:0'); }
408
- " 2>/dev/null || echo "0:0")
409
- INPUT_TOKENS=$(echo "$TOKEN_DATA" | cut -d: -f1)
410
- OUTPUT_TOKENS=$(echo "$TOKEN_DATA" | cut -d: -f2)
411
- TOTAL_CONTEXT_TOKENS=$((INPUT_TOKENS + OUTPUT_TOKENS))
288
+ CACHE_READ_TOKENS=0
289
+ CACHE_CREATION_TOKENS=0
290
+ TOKENIZER_SCRIPT="$SCRIPT_DIR/lib/count-tokens.cjs"
291
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] && [ -f "$TOKENIZER_SCRIPT" ]; then
292
+ TOKEN_JSON=$(node "$TOKENIZER_SCRIPT" "$TRANSCRIPT_PATH" --json 2>/dev/null || echo '{}')
293
+ TOTAL_TOKENS=$(echo "$TOKEN_JSON" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.total_tokens||0)" 2>/dev/null || echo "0")
294
+ INPUT_TOKENS=$(echo "$TOKEN_JSON" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.input_tokens||0)" 2>/dev/null || echo "0")
295
+ CACHE_READ_TOKENS=$(echo "$TOKEN_JSON" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.cache_read_tokens||0)" 2>/dev/null || echo "0")
296
+ CACHE_CREATION_TOKENS=$(echo "$TOKEN_JSON" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.cache_creation_tokens||0)" 2>/dev/null || echo "0")
297
+ [[ ! "$TOTAL_TOKENS" =~ ^[0-9]+$ ]] && TOTAL_TOKENS=0
412
298
  fi
413
299
 
414
- # Build Redis payload
415
- node -e "
416
- const fs = require('fs');
417
- const payload = {
300
+ # 1. WORKING SESSIONS (Redis) - No jq payload building
301
+ WORKING_PAYLOAD=$(node -e "
302
+ console.log(JSON.stringify({
418
303
  session_name: process.argv[1],
419
304
  turn_number: parseInt(process.argv[2]),
420
305
  user_query: process.argv[3],
@@ -422,187 +307,64 @@ const payload = {
422
307
  model: process.argv[5],
423
308
  tools_used: JSON.parse(process.argv[6] || '[]'),
424
309
  files_referenced: JSON.parse(process.argv[7] || '[]'),
425
- edits: [],
426
- patterns_used: [],
427
310
  total_context_tokens: parseInt(process.argv[8]) || 0,
428
- input_tokens: parseInt(process.argv[9]) || 0,
429
- output_tokens: parseInt(process.argv[10]) || 0
430
- };
431
- fs.writeFileSync('$REDIS_PAYLOAD_FILE', JSON.stringify(payload));
432
- " "$SESSION_NAME" "$TURN_NUMBER" "$LAST_USER" "$LAST_ASSISTANT" "$MODEL_USED" "$TOOLS_USED" "$FILES_REFERENCED" "$TOTAL_CONTEXT_TOKENS" "$INPUT_TOKENS" "$OUTPUT_TOKENS" 2>/dev/null
433
-
434
- if node -e "JSON.parse(require('fs').readFileSync('$REDIS_PAYLOAD_FILE','utf8'))" 2>/dev/null; then
435
- MAX_RETRIES=3
436
- RETRY=0
437
- REDIS_SUCCESS=false
438
-
439
- while [ $RETRY -lt $MAX_RETRIES ] && [ "$REDIS_SUCCESS" = "false" ]; do
440
- REDIS_RESULT=$(curl -s -w "\n%{http_code}" -X POST "$MEMORY_API_URL/api/v1/working/turn" \
441
- -H "Authorization: Bearer $AUTH_TOKEN" \
442
- -H "Content-Type: application/json" \
443
- -d "@$REDIS_PAYLOAD_FILE" \
444
- --connect-timeout 3 \
445
- --max-time 5 2>/dev/null || echo -e "\n000")
446
-
447
- REDIS_HTTP_CODE=$(echo "$REDIS_RESULT" | tail -1)
448
-
449
- if [ "$REDIS_HTTP_CODE" = "200" ] || [ "$REDIS_HTTP_CODE" = "201" ]; then
450
- REDIS_SUCCESS=true
451
- else
452
- RETRY=$((RETRY + 1))
453
- [ $RETRY -lt $MAX_RETRIES ] && sleep 0.3
454
- fi
455
- done
456
-
457
- if [ "$REDIS_SUCCESS" = "false" ]; then
458
- echo "[ekkOS] Redis capture failed after $MAX_RETRIES attempts: HTTP $REDIS_HTTP_CODE (session: $SESSION_NAME, turn: $TURN_NUMBER)" >&2
459
- WAL_DIR="$HOME/.ekkos/wal"
460
- mkdir -p "$WAL_DIR" 2>/dev/null
461
- cp "$REDIS_PAYLOAD_FILE" "$WAL_DIR/redis-$(date +%s)-$$.json" 2>/dev/null
462
- else
463
- # ACK: Update local cache cursor
464
- # Per ekkOS Onboarding Spec v1.2 ADDENDUM: Pass instanceId for namespacing
465
- if command -v ekkos-capture &>/dev/null && [ -n "$SESSION_ID" ]; then
466
- INSTANCE_ID="${EKKOS_INSTANCE_ID:-default}"
467
- (ekkos-capture ack "$SESSION_ID" "$TURN_NUMBER" --instance="$INSTANCE_ID" >/dev/null 2>&1) &
468
- fi
469
- fi
311
+ token_breakdown: {
312
+ input_tokens: parseInt(process.argv[9]) || 0,
313
+ cache_read_tokens: parseInt(process.argv[10]) || 0,
314
+ cache_creation_tokens: parseInt(process.argv[11]) || 0
315
+ }
316
+ }));
317
+ " "$SESSION_NAME" "$TURN_NUMBER" "$LAST_USER" "$LAST_ASSISTANT" "$MODEL_USED" "$TOOLS_USED" "$FILES_REF" "$TOTAL_TOKENS" "$INPUT_TOKENS" "$CACHE_READ_TOKENS" "$CACHE_CREATION_TOKENS" 2>/dev/null)
318
+
319
+ if [ -n "$WORKING_PAYLOAD" ]; then
320
+ curl -s -X POST "$MEMORY_API_URL/api/v1/working/turn" \
321
+ -H "Authorization: Bearer $AUTH_TOKEN" \
322
+ -H "Content-Type: application/json" \
323
+ -d "$WORKING_PAYLOAD" \
324
+ --connect-timeout 3 \
325
+ --max-time 5 >/dev/null 2>&1 || true
470
326
  fi
471
327
 
472
- rm -f "$REDIS_PAYLOAD_FILE" 2>/dev/null
473
-
474
- # ═══════════════════════════════════════════════════════════════════════════
475
- # FAST CAPTURE: Structured context for instant /continue
476
- # ═══════════════════════════════════════════════════════════════════════════
477
- USER_DECISION=$(echo "$LAST_USER" | grep -oiE "^(yes|no|ok|do it|go ahead|approved|confirmed|use .{1,30} instead)" | head -1 || echo "")
478
- USER_CORRECTION=$(echo "$LAST_USER" | grep -oiE "(actually|no,? I meant|not that|wrong|instead)" | head -1 || echo "")
479
- USER_PREFERENCE=$(echo "$LAST_USER" | grep -oiE "(always|never|I prefer|don.t|avoid) .{1,50}" | head -1 || echo "")
480
-
481
- ERRORS_FOUND=$(echo "$LAST_ASSISTANT" | grep -oiE "(error|failed|cannot|exception|not found).{0,80}" | head -3 | node -e "
482
- const lines = require('fs').readFileSync('/dev/stdin','utf8').split('\\n').filter(Boolean);
483
- console.log(JSON.stringify(lines));
484
- " 2>/dev/null || echo "[]")
485
-
486
- GIT_CHANGED=$(git diff --name-only 2>/dev/null | head -10 | node -e "
487
- const lines = require('fs').readFileSync('/dev/stdin','utf8').split('\\n').filter(Boolean);
488
- console.log(JSON.stringify(lines));
489
- " 2>/dev/null || echo "[]")
490
-
491
- GIT_STAT=$(git diff --stat 2>/dev/null | tail -1 | tr -d '\n' || echo "")
492
-
493
- COMMANDS_RUN=$(echo "$LAST_ASSISTANT" | grep -oE '\$ [^\n]{1,50}' | head -5 | sed 's/^\$ //' | node -e "
494
- const lines = require('fs').readFileSync('/dev/stdin','utf8').split('\\n').filter(Boolean);
495
- console.log(JSON.stringify(lines));
496
- " 2>/dev/null || echo "[]")
497
-
498
- # Build and send fast-capture
499
- FAST_PAYLOAD=$(node -e "
328
+ # 2. EPISODIC MEMORY (Supabase) - No jq payload building
329
+ EPISODIC_PAYLOAD=$(node -e "
500
330
  console.log(JSON.stringify({
501
- session_name: process.argv[1],
502
- turn_number: parseInt(process.argv[2]),
503
- user_intent: process.argv[3].substring(0, 200),
504
- user_decision: process.argv[4] || null,
505
- user_correction: process.argv[5] || null,
506
- user_preference: process.argv[6] || null,
507
- tools_used: JSON.parse(process.argv[7] || '[]'),
508
- files_modified: JSON.parse(process.argv[8] || '[]'),
509
- commands_run: JSON.parse(process.argv[9] || '[]'),
510
- errors: JSON.parse(process.argv[10] || '[]'),
511
- git_files_changed: JSON.parse(process.argv[11] || '[]'),
512
- git_diff_stat: process.argv[12] || null,
513
- outcome: 'success'
331
+ user_query: process.argv[1],
332
+ assistant_response: process.argv[2],
333
+ session_id: process.argv[3],
334
+ user_id: process.argv[4] || 'system',
335
+ file_changes: JSON.parse(process.argv[5] || '[]'),
336
+ metadata: {
337
+ source: 'claude-code',
338
+ model_used: process.argv[6],
339
+ captured_at: process.argv[7],
340
+ turn_number: parseInt(process.argv[8]) || 1,
341
+ session_name: process.argv[9],
342
+ minimal_hook: true
343
+ }
514
344
  }));
515
- " "$SESSION_NAME" "$TURN_NUMBER" "$LAST_USER" "$USER_DECISION" "$USER_CORRECTION" "$USER_PREFERENCE" "$TOOLS_USED" "$FILES_REFERENCED" "$COMMANDS_RUN" "$ERRORS_FOUND" "$GIT_CHANGED" "$GIT_STAT" 2>/dev/null)
516
-
517
- if [ -n "$FAST_PAYLOAD" ]; then
518
- curl -s -X POST "$MEMORY_API_URL/api/v1/working/fast-capture" \
519
- -H "Authorization: Bearer $AUTH_TOKEN" \
520
- -H "Content-Type: application/json" \
521
- -d "$FAST_PAYLOAD" \
522
- --connect-timeout 1 \
523
- --max-time 2 >/dev/null 2>&1 &
345
+ " "$LAST_USER" "$LAST_ASSISTANT" "$SESSION_ID" "${USER_ID:-system}" "$FILE_CHANGES" "$MODEL_USED" "$TIMESTAMP" "$TURN_NUMBER" "$SESSION_NAME" 2>/dev/null)
346
+
347
+ if [ -n "$EPISODIC_PAYLOAD" ]; then
348
+ curl -s -X POST "$MEMORY_API_URL/api/v1/memory/capture" \
349
+ -H "Authorization: Bearer $AUTH_TOKEN" \
350
+ -H "Content-Type: application/json" \
351
+ -d "$EPISODIC_PAYLOAD" \
352
+ --connect-timeout 3 \
353
+ --max-time 5 >/dev/null 2>&1 || true
524
354
  fi
525
-
526
- # ═══════════════════════════════════════════════════════════════════════════
527
- # LOCAL CACHE: Tier 0 - Update turn with assistant response
528
- # Per ekkOS Onboarding Spec v1.2 ADDENDUM: Pass instanceId for namespacing
529
- # ═══════════════════════════════════════════════════════════════════════════
530
- if command -v ekkos-capture &>/dev/null && [ -n "$SESSION_ID" ]; then
531
- RESPONSE_B64=$(echo "$LAST_ASSISTANT" | base64 2>/dev/null || echo "")
532
- if [ -n "$RESPONSE_B64" ]; then
533
- DECODED_RESPONSE=$(echo "$RESPONSE_B64" | base64 -d 2>/dev/null || echo "")
534
- if [ -n "$DECODED_RESPONSE" ]; then
535
- # NEW format: ekkos-capture response <instance_id> <session_id> <turn_id> <response> [tools] [files]
536
- INSTANCE_ID="${EKKOS_INSTANCE_ID:-default}"
537
- (ekkos-capture response "$INSTANCE_ID" "$SESSION_ID" "$TURN_NUMBER" "$DECODED_RESPONSE" "$TOOLS_USED" "$FILES_REFERENCED" \
538
- >/dev/null 2>&1) &
539
- fi
540
- fi
541
- fi
542
- fi
543
-
544
- # ═══════════════════════════════════════════════════════════════════════════
545
- # FALLBACK LOCAL CACHE UPDATE
546
- # Per ekkOS Onboarding Spec v1.2 ADDENDUM: Pass instanceId for namespacing
547
- # ═══════════════════════════════════════════════════════════════════════════
548
- if [ -n "$LAST_ASSISTANT" ] && command -v ekkos-capture &>/dev/null && [ -n "$SESSION_ID" ]; then
549
- if [ -z "$LAST_USER" ]; then
550
- echo "[ekkOS DEBUG] Fallback local cache update: LAST_ASSISTANT available, updating turn $TURN_NUMBER" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
551
- RESPONSE_B64=$(echo "$LAST_ASSISTANT" | base64 2>/dev/null || echo "")
552
- if [ -n "$RESPONSE_B64" ]; then
553
- DECODED_RESPONSE=$(echo "$RESPONSE_B64" | base64 -d 2>/dev/null || echo "")
554
- if [ -n "$DECODED_RESPONSE" ]; then
555
- TOOLS_USED=$(echo "$LAST_ASSISTANT" | grep -oE '\[TOOL: [^\]]+\]' | sed 's/\[TOOL: //g; s/\]//g' | sort -u | node -e "
556
- const lines = require('fs').readFileSync('/dev/stdin','utf8').split('\\n').filter(Boolean);
557
- console.log(JSON.stringify(lines));
558
- " 2>/dev/null || echo "[]")
559
- # NEW format: ekkos-capture response <instance_id> <session_id> <turn_id> <response> [tools] [files]
560
- INSTANCE_ID="${EKKOS_INSTANCE_ID:-default}"
561
- (ekkos-capture response "$INSTANCE_ID" "$SESSION_ID" "$TURN_NUMBER" "$DECODED_RESPONSE" "$TOOLS_USED" "[]" \
562
- >/dev/null 2>&1) &
563
- fi
564
- fi
565
- fi
566
- fi
567
-
568
- # ═══════════════════════════════════════════════════════════════════════════
569
- # GOLDEN LOOP: DETECT PHASES FROM RESPONSE
570
- # ═══════════════════════════════════════════════════════════════════════════
571
- GOLDEN_LOOP_FILE="$PROJECT_ROOT/.ekkos/golden-loop-current.json"
572
-
573
- if [ -n "$LAST_ASSISTANT" ] && [ -f "$GOLDEN_LOOP_FILE" ]; then
574
- RETRIEVED=$(echo "$LAST_ASSISTANT" | grep -c "ekkOS_Search" 2>/dev/null || echo "0")
575
- APPLIED=$(echo "$LAST_ASSISTANT" | grep -c "\[ekkOS_SELECT\]" 2>/dev/null || echo "0")
576
- FORGED=$(echo "$LAST_ASSISTANT" | grep -c "ekkOS_Forge" 2>/dev/null || echo "0")
577
-
578
- CURRENT_PHASE="complete"
579
- [ "$FORGED" -gt 0 ] && CURRENT_PHASE="measure"
580
- [ "$APPLIED" -gt 0 ] && CURRENT_PHASE="inject"
581
- [ "$RETRIEVED" -gt 0 ] && CURRENT_PHASE="retrieve"
582
-
583
- node -e "
584
- const fs = require('fs');
585
- const data = {
586
- phase: '$CURRENT_PHASE',
587
- turn: $TURN_NUMBER,
588
- session: '$SESSION_NAME',
589
- timestamp: new Date().toISOString(),
590
- stats: { retrieved: $RETRIEVED, applied: $APPLIED, forged: $FORGED }
591
- };
592
- fs.writeFileSync('$GOLDEN_LOOP_FILE', JSON.stringify(data, null, 2));
593
- " 2>/dev/null || true
355
+ ) &
594
356
  fi
595
357
 
596
358
  # ═══════════════════════════════════════════════════════════════════════════
597
- # Update local .ekkos/current-focus.md
359
+ # Update local .ekkos/current-focus.md (if exists) - SILENT
598
360
  # ═══════════════════════════════════════════════════════════════════════════
599
361
  EKKOS_LOCAL_DIR="$PROJECT_ROOT/.ekkos"
600
362
  if [ -d "$EKKOS_LOCAL_DIR" ] && [ -n "$LAST_USER" ]; then
601
- FOCUS_FILE="$EKKOS_LOCAL_DIR/current-focus.md"
602
- TASK_SUMMARY="${LAST_USER:0:100}"
603
- [ ${#LAST_USER} -gt 100 ] && TASK_SUMMARY="${TASK_SUMMARY}..."
363
+ FOCUS_FILE="$EKKOS_LOCAL_DIR/current-focus.md"
364
+ TASK_SUMMARY="${LAST_USER:0:100}"
365
+ [ ${#LAST_USER} -gt 100 ] && TASK_SUMMARY="${TASK_SUMMARY}..."
604
366
 
605
- cat > "$FOCUS_FILE" << EOF
367
+ cat > "$FOCUS_FILE" << EOF
606
368
  ---
607
369
  last_updated: $(date -u +%Y-%m-%dT%H:%M:%SZ)
608
370
  session_id: ${SESSION_ID}