@ekkos/cli 0.2.18 → 1.0.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.
- package/README.md +57 -0
- package/dist/agent/daemon.d.ts +27 -0
- package/dist/agent/daemon.js +254 -29
- package/dist/agent/health-check.d.ts +35 -0
- package/dist/agent/health-check.js +243 -0
- package/dist/agent/pty-runner.d.ts +1 -0
- package/dist/agent/pty-runner.js +6 -1
- package/dist/capture/eviction-client.d.ts +139 -0
- package/dist/capture/eviction-client.js +454 -0
- package/dist/capture/index.d.ts +2 -0
- package/dist/capture/index.js +2 -0
- package/dist/capture/jsonl-rewriter.d.ts +96 -0
- package/dist/capture/jsonl-rewriter.js +1369 -0
- package/dist/capture/transcript-repair.d.ts +51 -0
- package/dist/capture/transcript-repair.js +319 -0
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +244 -0
- package/dist/commands/dashboard.d.ts +25 -0
- package/dist/commands/dashboard.js +1175 -0
- package/dist/commands/doctor.js +23 -1
- package/dist/commands/run.d.ts +5 -0
- package/dist/commands/run.js +1605 -516
- package/dist/commands/setup-remote.js +146 -37
- package/dist/commands/swarm-dashboard.d.ts +20 -0
- package/dist/commands/swarm-dashboard.js +735 -0
- package/dist/commands/swarm-setup.d.ts +10 -0
- package/dist/commands/swarm-setup.js +956 -0
- package/dist/commands/swarm.d.ts +46 -0
- package/dist/commands/swarm.js +441 -0
- package/dist/commands/test-claude.d.ts +16 -0
- package/dist/commands/test-claude.js +156 -0
- package/dist/commands/usage/blocks.d.ts +8 -0
- package/dist/commands/usage/blocks.js +60 -0
- package/dist/commands/usage/daily.d.ts +9 -0
- package/dist/commands/usage/daily.js +96 -0
- package/dist/commands/usage/dashboard.d.ts +8 -0
- package/dist/commands/usage/dashboard.js +104 -0
- package/dist/commands/usage/formatters.d.ts +41 -0
- package/dist/commands/usage/formatters.js +147 -0
- package/dist/commands/usage/index.d.ts +13 -0
- package/dist/commands/usage/index.js +87 -0
- package/dist/commands/usage/monthly.d.ts +8 -0
- package/dist/commands/usage/monthly.js +66 -0
- package/dist/commands/usage/session.d.ts +11 -0
- package/dist/commands/usage/session.js +193 -0
- package/dist/commands/usage/weekly.d.ts +9 -0
- package/dist/commands/usage/weekly.js +61 -0
- package/dist/commands/usage.d.ts +7 -0
- package/dist/commands/usage.js +214 -0
- package/dist/cron/index.d.ts +7 -0
- package/dist/cron/index.js +13 -0
- package/dist/cron/promoter.d.ts +70 -0
- package/dist/cron/promoter.js +403 -0
- package/dist/deploy/instructions.d.ts +5 -2
- package/dist/deploy/instructions.js +11 -8
- package/dist/index.js +262 -5
- package/dist/lib/tmux-scrollbar.d.ts +14 -0
- package/dist/lib/tmux-scrollbar.js +296 -0
- package/dist/lib/usage-monitor.d.ts +47 -0
- package/dist/lib/usage-monitor.js +124 -0
- package/dist/lib/usage-parser.d.ts +162 -0
- package/dist/lib/usage-parser.js +583 -0
- package/dist/restore/RestoreOrchestrator.d.ts +4 -0
- package/dist/restore/RestoreOrchestrator.js +118 -30
- package/dist/utils/log-rotate.d.ts +18 -0
- package/dist/utils/log-rotate.js +74 -0
- package/dist/utils/platform.d.ts +2 -0
- package/dist/utils/platform.js +3 -1
- package/dist/utils/session-binding.d.ts +5 -0
- package/dist/utils/session-binding.js +46 -0
- package/dist/utils/state.js +4 -0
- package/dist/utils/verify-remote-terminal.d.ts +10 -0
- package/dist/utils/verify-remote-terminal.js +415 -0
- package/package.json +9 -2
- package/templates/CLAUDE.md +135 -23
- package/templates/ekkos-manifest.json +5 -5
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/count-tokens.cjs +86 -0
- package/templates/hooks/lib/ekkos-reminders.sh +98 -0
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/stop.sh +150 -388
- package/templates/hooks/user-prompt-submit.sh +353 -443
- package/templates/windsurf-hooks/README.md +212 -0
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/install.sh +148 -0
- package/templates/windsurf-hooks/lib/contract.sh +2 -0
- package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
- package/templates/agents/README.md +0 -182
- package/templates/agents/code-reviewer.md +0 -166
- package/templates/agents/debug-detective.md +0 -169
- package/templates/agents/ekkOS_Vercel.md +0 -99
- package/templates/agents/extension-manager.md +0 -229
- package/templates/agents/git-companion.md +0 -185
- package/templates/agents/github-test-agent.md +0 -321
- package/templates/agents/railway-manager.md +0 -215
- package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
package/templates/hooks/stop.sh
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
-
# ekkOS_ Hook: Stop -
|
|
4
|
-
#
|
|
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
|
-
#
|
|
8
|
-
# -
|
|
9
|
-
#
|
|
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
|
-
#
|
|
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 -
|
|
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
|
-
#
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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 -
|
|
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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
138
|
+
# Turn Number - read from turn file written by user-prompt-submit.sh
|
|
177
139
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
178
|
-
|
|
179
|
-
|
|
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 "$
|
|
143
|
+
[ -f "$TURN_FILE" ] && TURN_NUMBER=$(cat "$TURN_FILE" 2>/dev/null || echo "1")
|
|
182
144
|
|
|
183
145
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
184
|
-
#
|
|
146
|
+
# Load auth - No jq
|
|
185
147
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
186
|
-
|
|
187
|
-
|
|
148
|
+
EKKOS_CONFIG="$HOME/.ekkos/config.json"
|
|
149
|
+
AUTH_TOKEN=""
|
|
150
|
+
USER_ID=""
|
|
188
151
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
272
|
+
(
|
|
326
273
|
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
327
274
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
394
|
-
|
|
285
|
+
# Token breakdown from tokenizer script
|
|
286
|
+
TOTAL_TOKENS=0
|
|
395
287
|
INPUT_TOKENS=0
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
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
|
-
#
|
|
415
|
-
node -e "
|
|
416
|
-
|
|
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
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
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
|
-
|
|
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
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
" "$
|
|
516
|
-
|
|
517
|
-
if [ -n "$
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
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
|
-
|
|
602
|
-
|
|
603
|
-
|
|
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
|
-
|
|
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}
|