@ekkos/cli 1.2.18 → 1.3.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 (47) hide show
  1. package/dist/cache/capture.js +0 -0
  2. package/dist/commands/dashboard.js +57 -49
  3. package/dist/commands/hooks.d.ts +25 -36
  4. package/dist/commands/hooks.js +43 -615
  5. package/dist/commands/init.js +7 -23
  6. package/dist/commands/run.js +90 -3
  7. package/dist/commands/setup.js +10 -352
  8. package/dist/deploy/hooks.d.ts +8 -5
  9. package/dist/deploy/hooks.js +12 -105
  10. package/dist/deploy/settings.d.ts +8 -2
  11. package/dist/deploy/settings.js +22 -51
  12. package/dist/index.js +17 -39
  13. package/dist/utils/state.js +7 -2
  14. package/package.json +1 -1
  15. package/templates/CLAUDE.md +82 -292
  16. package/templates/cursor-rules/ekkos-memory.md +48 -108
  17. package/templates/windsurf-rules/ekkos-memory.md +62 -64
  18. package/templates/cursor-hooks/after-agent-response.sh +0 -117
  19. package/templates/cursor-hooks/before-submit-prompt.sh +0 -419
  20. package/templates/cursor-hooks/hooks.json +0 -20
  21. package/templates/cursor-hooks/lib/contract.sh +0 -320
  22. package/templates/cursor-hooks/stop.sh +0 -75
  23. package/templates/hooks/assistant-response.ps1 +0 -256
  24. package/templates/hooks/assistant-response.sh +0 -160
  25. package/templates/hooks/hooks.json +0 -40
  26. package/templates/hooks/lib/contract.sh +0 -332
  27. package/templates/hooks/lib/count-tokens.cjs +0 -86
  28. package/templates/hooks/lib/ekkos-reminders.sh +0 -98
  29. package/templates/hooks/lib/state.sh +0 -210
  30. package/templates/hooks/session-start.ps1 +0 -146
  31. package/templates/hooks/session-start.sh +0 -353
  32. package/templates/hooks/stop.ps1 +0 -349
  33. package/templates/hooks/stop.sh +0 -382
  34. package/templates/hooks/user-prompt-submit.ps1 +0 -419
  35. package/templates/hooks/user-prompt-submit.sh +0 -516
  36. package/templates/project-stubs/session-start.ps1 +0 -63
  37. package/templates/project-stubs/session-start.sh +0 -55
  38. package/templates/project-stubs/stop.ps1 +0 -63
  39. package/templates/project-stubs/stop.sh +0 -55
  40. package/templates/project-stubs/user-prompt-submit.ps1 +0 -63
  41. package/templates/project-stubs/user-prompt-submit.sh +0 -55
  42. package/templates/windsurf-hooks/README.md +0 -212
  43. package/templates/windsurf-hooks/hooks.json +0 -17
  44. package/templates/windsurf-hooks/install.sh +0 -148
  45. package/templates/windsurf-hooks/lib/contract.sh +0 -322
  46. package/templates/windsurf-hooks/post-cascade-response.sh +0 -251
  47. package/templates/windsurf-hooks/pre-user-prompt.sh +0 -435
@@ -1,382 +0,0 @@
1
- #!/bin/bash
2
- # ═══════════════════════════════════════════════════════════════════════════
3
- # ekkOS_ Hook: Stop - Captures turns to BOTH Working (Redis) and Episodic (Supabase)
4
- # NO jq dependency - uses Node.js for all JSON parsing
5
- # ═══════════════════════════════════════════════════════════════════════════
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
9
- #
10
- # NO compliance checking - skills handle that
11
- # NO PatternGuard validation - skills handle that
12
- # NO verbose output - just capture silently
13
- # ═══════════════════════════════════════════════════════════════════════════
14
-
15
- set +e
16
-
17
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
18
- PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
19
- STATE_DIR="$PROJECT_ROOT/.claude/state"
20
-
21
- # ═══════════════════════════════════════════════════════════════════════════
22
- # PROXY MODE DETECTION
23
- # ═══════════════════════════════════════════════════════════════════════════
24
- PROXY_MODE=false
25
- if [[ "$ANTHROPIC_BASE_URL" == *"proxy.ekkos.dev"* ]]; then
26
- PROXY_MODE=true
27
- fi
28
-
29
- mkdir -p "$STATE_DIR" 2>/dev/null
30
-
31
- # ═══════════════════════════════════════════════════════════════════════════
32
- # CONFIG PATHS - No jq dependency (v1.2 spec)
33
- # Session words live in ~/.ekkos/ so they work in ANY project
34
- # ═══════════════════════════════════════════════════════════════════════════
35
- EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
36
- SESSION_WORDS_JSON="$EKKOS_CONFIG_DIR/session-words.json"
37
- SESSION_WORDS_DEFAULT="$EKKOS_CONFIG_DIR/.defaults/session-words.json"
38
- JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
39
-
40
- INPUT=$(cat)
41
-
42
- # JSON parsing helper (no jq)
43
- parse_json_value() {
44
- local json="$1"
45
- local path="$2"
46
- echo "$json" | node -e "
47
- const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
48
- const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
49
- let result = data;
50
- for (const p of path) {
51
- if (result === undefined || result === null) { result = undefined; break; }
52
- result = result[p];
53
- }
54
- if (result !== undefined && result !== null) console.log(result);
55
- " 2>/dev/null || echo ""
56
- }
57
-
58
- RAW_SESSION_ID=$(parse_json_value "$INPUT" '.session_id')
59
- [ -z "$RAW_SESSION_ID" ] && RAW_SESSION_ID="unknown"
60
- TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
61
- MODEL_USED=$(parse_json_value "$INPUT" '.model')
62
- [ -z "$MODEL_USED" ] && MODEL_USED="claude-sonnet-4-5"
63
-
64
- # ═══════════════════════════════════════════════════════════════════════════
65
- # PROXY MODE: Slim hook path (local cleanup only)
66
- # ═══════════════════════════════════════════════════════════════════════════
67
- if [ "$PROXY_MODE" = "true" ]; then
68
- rm -f "$STATE_DIR/hook-state.json" "$HOME/.claude/state/hook-state.json" 2>/dev/null || true
69
- exit 0
70
- fi
71
-
72
- # ═══════════════════════════════════════════════════════════════════════════
73
- # Session ID
74
- # ═══════════════════════════════════════════════════════════════════════════
75
- SESSION_ID="$RAW_SESSION_ID"
76
-
77
- if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
78
- exit 0
79
- fi
80
-
81
- # Check if SESSION_ID is a UUID (8-4-4-4-12 format)
82
- UUID_REGEX='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
83
- IS_UUID=false
84
- if [[ "$SESSION_ID" =~ $UUID_REGEX ]]; then
85
- IS_UUID=true
86
- fi
87
-
88
- # ═══════════════════════════════════════════════════════════════════════════
89
- # WORD-BASED SESSION NAMES - No jq, uses ~/.ekkos/ config (works in ANY project)
90
- # ═══════════════════════════════════════════════════════════════════════════
91
- declare -a ADJECTIVES
92
- declare -a NOUNS
93
- declare -a VERBS
94
- SESSION_WORDS_LOADED=false
95
-
96
- load_session_words() {
97
- if [ "$SESSION_WORDS_LOADED" = "true" ]; then return 0; fi
98
-
99
- local words_file="$SESSION_WORDS_JSON"
100
- [ ! -f "$words_file" ] && words_file="$SESSION_WORDS_DEFAULT"
101
-
102
- if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
103
- ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
104
- return 1
105
- fi
106
-
107
- if command -v node &>/dev/null; then
108
- if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
109
- readarray -t ADJECTIVES < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
110
- readarray -t NOUNS < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
111
- readarray -t VERBS < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
112
- else
113
- local i=0
114
- while IFS= read -r line; do ADJECTIVES[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
115
- i=0
116
- while IFS= read -r line; do NOUNS[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
117
- i=0
118
- while IFS= read -r line; do VERBS[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
119
- fi
120
- [ ${#ADJECTIVES[@]} -eq 0 ] && ADJECTIVES=("unknown")
121
- [ ${#NOUNS[@]} -eq 0 ] && NOUNS=("session")
122
- [ ${#VERBS[@]} -eq 0 ] && VERBS=("starts")
123
- SESSION_WORDS_LOADED=true
124
- return 0
125
- fi
126
- ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
127
- return 1
128
- }
129
-
130
- uuid_to_words() {
131
- local uuid="$1"
132
- load_session_words
133
-
134
- local hex="${uuid//-/}"
135
- hex="${hex:0:12}"
136
- [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]] && echo "unknown-session-starts" && return
137
-
138
- local adj_seed=$((16#${hex:0:4}))
139
- local noun_seed=$((16#${hex:4:4}))
140
- local verb_seed=$((16#${hex:8:4}))
141
- local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
142
- local noun_idx=$((noun_seed % ${#NOUNS[@]}))
143
- local verb_idx=$((verb_seed % ${#VERBS[@]}))
144
- echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
145
- }
146
-
147
- if [ "$IS_UUID" = true ]; then
148
- SESSION_NAME=$(uuid_to_words "$SESSION_ID")
149
- else
150
- SESSION_NAME="$SESSION_ID"
151
- fi
152
-
153
- # ═══════════════════════════════════════════════════════════════════════════
154
- # Turn Number - read from turn file written by user-prompt-submit.sh
155
- # ═══════════════════════════════════════════════════════════════════════════
156
- TURN_FILE="$STATE_DIR/sessions/${SESSION_ID}.turn"
157
- mkdir -p "$STATE_DIR/sessions" 2>/dev/null
158
- TURN_NUMBER=1
159
- [ -f "$TURN_FILE" ] && TURN_NUMBER=$(cat "$TURN_FILE" 2>/dev/null || echo "1")
160
-
161
- # ═══════════════════════════════════════════════════════════════════════════
162
- # Load auth - No jq
163
- # ═══════════════════════════════════════════════════════════════════════════
164
- EKKOS_CONFIG="$HOME/.ekkos/config.json"
165
- AUTH_TOKEN=""
166
- USER_ID=""
167
-
168
- if [ -f "$EKKOS_CONFIG" ] && [ -f "$JSON_PARSE_HELPER" ]; then
169
- AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.hookApiKey' 2>/dev/null || echo "")
170
- [ -z "$AUTH_TOKEN" ] && AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.apiKey' 2>/dev/null || echo "")
171
- USER_ID=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.userId' 2>/dev/null || echo "")
172
- fi
173
-
174
- if [ -z "$AUTH_TOKEN" ] && [ -f "$PROJECT_ROOT/.env.local" ]; then
175
- AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | tr -d '\r')
176
- fi
177
-
178
- [ -z "$AUTH_TOKEN" ] && exit 0
179
-
180
- MEMORY_API_URL="https://mcp.ekkos.dev"
181
-
182
- # Session binding removed — proxy now self-resolves _pending → word-based
183
- # names inline via uuidToSessionName(). No external bind call needed.
184
-
185
- # ═══════════════════════════════════════════════════════════════════════════
186
- # EVICTION: Handled by IPC (In-Place Progressive Compression) in the proxy.
187
- # No hook-side eviction needed — passthrough is default for cache stability.
188
- # ═══════════════════════════════════════════════════════════════════════════
189
-
190
- # ═══════════════════════════════════════════════════════════════════════════
191
- # Check for interruption - No jq
192
- # ═══════════════════════════════════════════════════════════════════════════
193
- IS_INTERRUPTED=$(parse_json_value "$INPUT" '.interrupted')
194
- [ -z "$IS_INTERRUPTED" ] && IS_INTERRUPTED="false"
195
- STOP_REASON=$(parse_json_value "$INPUT" '.stop_reason')
196
-
197
- if [ "$IS_INTERRUPTED" = "true" ] || [ "$STOP_REASON" = "user_cancelled" ] || [ "$STOP_REASON" = "interrupted" ]; then
198
- exit 0
199
- fi
200
-
201
- # ═══════════════════════════════════════════════════════════════════════════
202
- # Extract conversation from transcript using Node (no jq)
203
- # ═══════════════════════════════════════════════════════════════════════════
204
- LAST_USER=""
205
- LAST_ASSISTANT=""
206
- FILE_CHANGES="[]"
207
-
208
- if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
209
- EXTRACTION=$(node -e "
210
- const fs = require('fs');
211
- const lines = fs.readFileSync('$TRANSCRIPT_PATH', 'utf8').split('\n').filter(Boolean);
212
- const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
213
-
214
- let lastUser = '', lastUserTime = '';
215
- for (let i = entries.length - 1; i >= 0; i--) {
216
- const e = entries[i];
217
- if (e.type === 'user') {
218
- const content = e.message?.content;
219
- if (typeof content === 'string' && !content.startsWith('<')) {
220
- lastUser = content; lastUserTime = e.timestamp || ''; break;
221
- } else if (Array.isArray(content)) {
222
- const textPart = content.find(c => c.type === 'text' && !c.text?.startsWith('<'));
223
- if (textPart) { lastUser = textPart.text; lastUserTime = e.timestamp || ''; break; }
224
- }
225
- }
226
- }
227
-
228
- let lastAssistant = '';
229
- for (let i = entries.length - 1; i >= 0; i--) {
230
- const e = entries[i];
231
- if (e.type === 'assistant' && (!lastUserTime || e.timestamp >= lastUserTime)) {
232
- const content = e.message?.content;
233
- if (typeof content === 'string') { lastAssistant = content; break; }
234
- else if (Array.isArray(content)) {
235
- const parts = content.map(c => {
236
- if (c.type === 'text') return c.text;
237
- if (c.type === 'tool_use') return '[TOOL: ' + c.name + ']';
238
- if (c.type === 'thinking') return '[THINKING]' + (c.thinking || c.text || '') + '[/THINKING]';
239
- return '';
240
- }).filter(Boolean);
241
- lastAssistant = parts.join('\n'); break;
242
- }
243
- }
244
- }
245
-
246
- const fileChanges = [];
247
- entries.filter(e => e.type === 'assistant').forEach(e => {
248
- const content = e.message?.content;
249
- if (Array.isArray(content)) {
250
- content.filter(c => c.type === 'tool_use' && ['Edit', 'Write', 'Read'].includes(c.name)).forEach(c => {
251
- fileChanges.push({tool: c.name, path: c.input?.file_path || c.input?.path, action: c.name.toLowerCase()});
252
- });
253
- }
254
- });
255
-
256
- console.log(JSON.stringify({
257
- user: lastUser,
258
- assistant: lastAssistant.substring(0, 50000),
259
- fileChanges: fileChanges.slice(0, 20)
260
- }));
261
- " 2>/dev/null || echo '{"user":"","assistant":"","fileChanges":[]}')
262
-
263
- LAST_USER=$(echo "$EXTRACTION" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(d.user||'')" 2>/dev/null || echo "")
264
- LAST_ASSISTANT=$(echo "$EXTRACTION" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(d.assistant||'')" 2>/dev/null || echo "")
265
- 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 "[]")
266
- fi
267
-
268
- if [ -z "$LAST_USER" ] || [[ "$LAST_USER" == *"[Request interrupted"* ]]; then
269
- exit 0
270
- fi
271
-
272
- # ═══════════════════════════════════════════════════════════════════════════
273
- # Capture to BOTH Working Sessions (Redis) AND Episodic (Supabase) - No jq
274
- # ═══════════════════════════════════════════════════════════════════════════
275
- if [ -n "$LAST_USER" ] && [ -n "$LAST_ASSISTANT" ]; then
276
- (
277
- TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
278
-
279
- TOOLS_USED=$(echo "$FILE_CHANGES" | node -e "
280
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8') || '[]');
281
- console.log(JSON.stringify([...new Set(d.map(f => f.tool).filter(Boolean))]));
282
- " 2>/dev/null || echo "[]")
283
-
284
- FILES_REF=$(echo "$FILE_CHANGES" | node -e "
285
- const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8') || '[]');
286
- console.log(JSON.stringify([...new Set(d.map(f => f.path).filter(Boolean))]));
287
- " 2>/dev/null || echo "[]")
288
-
289
- # Token breakdown from tokenizer script
290
- TOTAL_TOKENS=0
291
- INPUT_TOKENS=0
292
- CACHE_READ_TOKENS=0
293
- CACHE_CREATION_TOKENS=0
294
- TOKENIZER_SCRIPT="$SCRIPT_DIR/lib/count-tokens.cjs"
295
- if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] && [ -f "$TOKENIZER_SCRIPT" ]; then
296
- TOKEN_JSON=$(node "$TOKENIZER_SCRIPT" "$TRANSCRIPT_PATH" --json 2>/dev/null || echo '{}')
297
- 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")
298
- 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")
299
- 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")
300
- 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")
301
- [[ ! "$TOTAL_TOKENS" =~ ^[0-9]+$ ]] && TOTAL_TOKENS=0
302
- fi
303
-
304
- # 1. WORKING SESSIONS (Redis) - No jq payload building
305
- WORKING_PAYLOAD=$(node -e "
306
- console.log(JSON.stringify({
307
- session_name: process.argv[1],
308
- turn_number: parseInt(process.argv[2]),
309
- user_query: process.argv[3],
310
- agent_response: process.argv[4].substring(0, 50000),
311
- model: process.argv[5],
312
- tools_used: JSON.parse(process.argv[6] || '[]'),
313
- files_referenced: JSON.parse(process.argv[7] || '[]'),
314
- total_context_tokens: parseInt(process.argv[8]) || 0,
315
- token_breakdown: {
316
- input_tokens: parseInt(process.argv[9]) || 0,
317
- cache_read_tokens: parseInt(process.argv[10]) || 0,
318
- cache_creation_tokens: parseInt(process.argv[11]) || 0
319
- }
320
- }));
321
- " "$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)
322
-
323
- if [ -n "$WORKING_PAYLOAD" ]; then
324
- curl -s -X POST "$MEMORY_API_URL/api/v1/working/turn" \
325
- -H "Authorization: Bearer $AUTH_TOKEN" \
326
- -H "Content-Type: application/json" \
327
- -d "$WORKING_PAYLOAD" \
328
- --connect-timeout 3 \
329
- --max-time 5 >/dev/null 2>&1 || true
330
- fi
331
-
332
- # 2. EPISODIC MEMORY (Supabase) - No jq payload building
333
- EPISODIC_PAYLOAD=$(node -e "
334
- console.log(JSON.stringify({
335
- user_query: process.argv[1],
336
- assistant_response: process.argv[2],
337
- session_id: process.argv[3],
338
- user_id: process.argv[4] || 'system',
339
- file_changes: JSON.parse(process.argv[5] || '[]'),
340
- metadata: {
341
- source: 'claude-code',
342
- model_used: process.argv[6],
343
- captured_at: process.argv[7],
344
- turn_number: parseInt(process.argv[8]) || 1,
345
- session_name: process.argv[9],
346
- minimal_hook: true
347
- }
348
- }));
349
- " "$LAST_USER" "$LAST_ASSISTANT" "$SESSION_ID" "${USER_ID:-system}" "$FILE_CHANGES" "$MODEL_USED" "$TIMESTAMP" "$TURN_NUMBER" "$SESSION_NAME" 2>/dev/null)
350
-
351
- if [ -n "$EPISODIC_PAYLOAD" ]; then
352
- curl -s -X POST "$MEMORY_API_URL/api/v1/memory/capture" \
353
- -H "Authorization: Bearer $AUTH_TOKEN" \
354
- -H "Content-Type: application/json" \
355
- -d "$EPISODIC_PAYLOAD" \
356
- --connect-timeout 3 \
357
- --max-time 5 >/dev/null 2>&1 || true
358
- fi
359
- ) &
360
- fi
361
-
362
- # ═══════════════════════════════════════════════════════════════════════════
363
- # Update local .ekkos/current-focus.md (if exists) - SILENT
364
- # ═══════════════════════════════════════════════════════════════════════════
365
- EKKOS_LOCAL_DIR="$PROJECT_ROOT/.ekkos"
366
- if [ -d "$EKKOS_LOCAL_DIR" ] && [ -n "$LAST_USER" ]; then
367
- FOCUS_FILE="$EKKOS_LOCAL_DIR/current-focus.md"
368
- TASK_SUMMARY="${LAST_USER:0:100}"
369
- [ ${#LAST_USER} -gt 100 ] && TASK_SUMMARY="${TASK_SUMMARY}..."
370
-
371
- cat > "$FOCUS_FILE" << EOF
372
- ---
373
- last_updated: $(date -u +%Y-%m-%dT%H:%M:%SZ)
374
- session_id: ${SESSION_ID}
375
- ---
376
-
377
- # Current Focus
378
- ${TASK_SUMMARY}
379
- EOF
380
- fi
381
-
382
- exit 0