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