@ekkos/cli 1.0.33 → 1.0.35
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/dist/capture/jsonl-rewriter.js +72 -7
- package/dist/commands/dashboard.js +186 -557
- package/dist/commands/init.js +3 -15
- package/dist/commands/run.js +221 -259
- package/dist/commands/setup.js +0 -47
- package/dist/commands/swarm-dashboard.js +4 -13
- package/dist/deploy/instructions.d.ts +2 -5
- package/dist/deploy/instructions.js +8 -11
- package/dist/deploy/settings.js +21 -15
- package/dist/deploy/skills.d.ts +0 -8
- package/dist/deploy/skills.js +0 -26
- package/dist/index.js +2 -2
- package/dist/lib/usage-parser.js +1 -2
- package/dist/utils/platform.d.ts +0 -3
- package/dist/utils/platform.js +1 -4
- package/dist/utils/session-binding.d.ts +1 -1
- package/dist/utils/session-binding.js +2 -3
- package/package.json +4 -2
- package/templates/CLAUDE.md +23 -135
- package/templates/agents/README.md +182 -0
- package/templates/agents/code-reviewer.md +166 -0
- package/templates/agents/debug-detective.md +169 -0
- package/templates/agents/ekkOS_Vercel.md +99 -0
- package/templates/agents/extension-manager.md +229 -0
- package/templates/agents/git-companion.md +185 -0
- package/templates/agents/github-test-agent.md +321 -0
- package/templates/agents/railway-manager.md +179 -0
- package/templates/ekkos-manifest.json +8 -8
- package/templates/hooks/assistant-response.ps1 +160 -256
- package/templates/hooks/assistant-response.sh +66 -130
- package/templates/hooks/hooks.json +0 -6
- package/templates/hooks/lib/contract.sh +31 -43
- package/templates/hooks/lib/count-tokens.cjs +0 -0
- package/templates/hooks/lib/ekkos-reminders.sh +0 -0
- package/templates/hooks/lib/state.sh +1 -53
- package/templates/hooks/session-start.ps1 +391 -91
- package/templates/hooks/session-start.sh +166 -201
- package/templates/hooks/stop.ps1 +341 -202
- package/templates/hooks/stop.sh +948 -275
- package/templates/hooks/user-prompt-submit.ps1 +548 -224
- package/templates/hooks/user-prompt-submit.sh +456 -382
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
- package/templates/windsurf-hooks/hooks.json +2 -9
- package/templates/windsurf-hooks/install.sh +0 -0
- package/templates/windsurf-hooks/lib/contract.sh +0 -2
- package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
- package/README.md +0 -57
|
@@ -10,126 +10,207 @@
|
|
|
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
|
-
|
|
16
13
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
17
|
-
#
|
|
18
|
-
#
|
|
14
|
+
# SESSION NAME GENERATION (must be defined early - used by ekkos-capture)
|
|
15
|
+
# Format: adj-noun-verb (e.g., "cosmic-penguin-runs")
|
|
16
|
+
# 100 × 100 × 100 = 1,000,000 combinations
|
|
19
17
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
18
|
+
ADJECTIVES=(
|
|
19
|
+
"cosmic" "turbo" "mega" "hyper" "quantum" "atomic" "stellar" "epic"
|
|
20
|
+
"mighty" "groovy" "zippy" "snappy" "jazzy" "funky" "zesty" "peppy"
|
|
21
|
+
"spicy" "crispy" "fluffy" "sparkly" "chunky" "bouncy" "bubbly" "sassy"
|
|
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
|
+
)
|
|
24
63
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
" 2>/dev/null || echo ""
|
|
64
|
+
uuid_to_words() {
|
|
65
|
+
local uuid="$1"
|
|
66
|
+
local hex="${uuid//-/}"
|
|
67
|
+
hex="${hex:0:12}"
|
|
68
|
+
if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
|
|
69
|
+
echo "unknown-session-starts"
|
|
70
|
+
return
|
|
71
|
+
fi
|
|
72
|
+
local adj_seed=$((16#${hex:0:4}))
|
|
73
|
+
local noun_seed=$((16#${hex:4:4}))
|
|
74
|
+
local verb_seed=$((16#${hex:8:4}))
|
|
75
|
+
local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
|
|
76
|
+
local noun_idx=$((noun_seed % ${#NOUNS[@]}))
|
|
77
|
+
local verb_idx=$((verb_seed % ${#VERBS[@]}))
|
|
78
|
+
echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
|
|
41
79
|
}
|
|
42
80
|
|
|
81
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
82
|
+
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
83
|
+
|
|
43
84
|
INPUT=$(cat)
|
|
44
|
-
USER_QUERY=$(
|
|
45
|
-
|
|
46
|
-
|
|
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')
|
|
85
|
+
USER_QUERY=$(echo "$INPUT" | jq -r '.query // .message // .prompt // ""')
|
|
86
|
+
RAW_SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
|
|
87
|
+
TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
|
|
50
88
|
|
|
51
89
|
[ -z "$USER_QUERY" ] || [ "$USER_QUERY" = "null" ] && exit 0
|
|
52
90
|
|
|
53
91
|
# Fallback: read session_id from saved state if not in INPUT
|
|
54
92
|
if [ "$RAW_SESSION_ID" = "unknown" ] || [ "$RAW_SESSION_ID" = "null" ] || [ -z "$RAW_SESSION_ID" ]; then
|
|
55
93
|
STATE_FILE="$HOME/.claude/state/current-session.json"
|
|
56
|
-
if [ -f "$STATE_FILE" ]
|
|
57
|
-
RAW_SESSION_ID=$(
|
|
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)
|
|
94
|
+
if [ -f "$STATE_FILE" ]; then
|
|
95
|
+
RAW_SESSION_ID=$(jq -r '.session_id // "unknown"' "$STATE_FILE" 2>/dev/null || echo "unknown")
|
|
64
96
|
fi
|
|
65
97
|
fi
|
|
66
98
|
|
|
67
99
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
68
|
-
#
|
|
100
|
+
# INTELLIGENT TOOL ROUTER: Multi-trigger skill detection
|
|
101
|
+
# Detects ALL applicable skills/tools and injects as system reminder
|
|
69
102
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
70
|
-
|
|
103
|
+
SKILL_REMINDERS=()
|
|
71
104
|
QUERY_LOWER=$(echo "$USER_QUERY" | tr '[:upper:]' '[:lower:]')
|
|
72
105
|
|
|
73
|
-
#
|
|
74
|
-
|
|
75
|
-
|
|
106
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
+
# MANDATORY TRIGGERS (Always check ekkOS first)
|
|
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")
|
|
76
113
|
fi
|
|
77
114
|
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
115
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
116
|
+
# RECALL TRIGGERS (Time-based memory)
|
|
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")
|
|
81
120
|
fi
|
|
82
121
|
|
|
83
|
-
#
|
|
84
|
-
|
|
85
|
-
|
|
122
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
# DIRECTIVE TRIGGERS (User preferences)
|
|
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")
|
|
86
127
|
fi
|
|
87
128
|
|
|
88
|
-
#
|
|
89
|
-
|
|
90
|
-
|
|
129
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
# SAFETY TRIGGERS (Destructive actions)
|
|
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")
|
|
91
134
|
fi
|
|
92
135
|
|
|
93
|
-
#
|
|
94
|
-
|
|
95
|
-
|
|
136
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
137
|
+
# SCHEMA TRIGGERS (Database operations)
|
|
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
|
|
96
183
|
fi
|
|
97
184
|
|
|
98
185
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
99
|
-
# Load auth
|
|
186
|
+
# Load auth
|
|
100
187
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
101
188
|
EKKOS_CONFIG="$HOME/.ekkos/config.json"
|
|
102
189
|
AUTH_TOKEN=""
|
|
103
|
-
if [ -f "$EKKOS_CONFIG" ]
|
|
104
|
-
|
|
105
|
-
if [ -z "$AUTH_TOKEN" ]; then
|
|
106
|
-
AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.apiKey' 2>/dev/null || echo "")
|
|
107
|
-
fi
|
|
190
|
+
if [ -f "$EKKOS_CONFIG" ]; then
|
|
191
|
+
AUTH_TOKEN=$(jq -r '.hookApiKey // .apiKey // ""' "$EKKOS_CONFIG" 2>/dev/null || echo "")
|
|
108
192
|
fi
|
|
109
193
|
[ -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 "")
|
|
110
194
|
|
|
111
195
|
MEMORY_API_URL="https://mcp.ekkos.dev"
|
|
112
196
|
|
|
113
197
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
114
|
-
# Session ID
|
|
198
|
+
# Session ID - NEW ID per conversation (not persisted 24h anymore)
|
|
199
|
+
# Each Claude Code session gets unique ID for proper Time Machine grouping
|
|
115
200
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
116
201
|
STATE_DIR="$PROJECT_ROOT/.claude/state"
|
|
117
202
|
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
118
203
|
SESSION_FILE="$STATE_DIR/current-session.json"
|
|
119
204
|
|
|
205
|
+
# Use Claude's RAW_SESSION_ID exclusively
|
|
120
206
|
SESSION_ID="$RAW_SESSION_ID"
|
|
121
207
|
|
|
208
|
+
# Skip if no valid session ID from Claude
|
|
122
209
|
if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
|
|
123
210
|
exit 0
|
|
124
211
|
fi
|
|
125
212
|
|
|
126
|
-
#
|
|
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
|
-
|
|
213
|
+
# Save for other hooks to reference (but don't reuse across conversations)
|
|
133
214
|
echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$SESSION_FILE"
|
|
134
215
|
|
|
135
216
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -138,376 +219,369 @@ echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:
|
|
|
138
219
|
PROJECT_SESSION_DIR="$STATE_DIR/sessions"
|
|
139
220
|
mkdir -p "$PROJECT_SESSION_DIR" 2>/dev/null || true
|
|
140
221
|
TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.turn"
|
|
141
|
-
CONTEXT_SIZE_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.context"
|
|
142
222
|
|
|
143
|
-
# Count
|
|
223
|
+
# Count actual user messages in transcript for accurate turn number
|
|
144
224
|
TURN_NUMBER=1
|
|
145
225
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
226
|
+
# Count user message entries in JSONL transcript
|
|
146
227
|
TURN_NUMBER=$(grep -c '"type":"user"' "$TRANSCRIPT_PATH" 2>/dev/null || echo "1")
|
|
147
228
|
[ "$TURN_NUMBER" -eq 0 ] && TURN_NUMBER=1
|
|
148
229
|
fi
|
|
149
230
|
|
|
150
|
-
#
|
|
231
|
+
# PRESERVE HISTORY: Don't overwrite if saved count is higher (after /clear)
|
|
151
232
|
SAVED_TURN_COUNT=0
|
|
152
233
|
[ -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
|
|
153
235
|
POST_CLEAR_DETECTED=false
|
|
154
|
-
|
|
155
236
|
if [ "$SAVED_TURN_COUNT" -gt "$TURN_NUMBER" ]; then
|
|
156
|
-
|
|
237
|
+
# Post-clear: INCREMENT from saved count (not just copy it)
|
|
157
238
|
TURN_NUMBER=$((SAVED_TURN_COUNT + 1))
|
|
239
|
+
POST_CLEAR_DETECTED=true
|
|
158
240
|
fi
|
|
159
|
-
|
|
160
241
|
echo "$TURN_NUMBER" > "$TURN_COUNTER_FILE"
|
|
161
242
|
|
|
162
243
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
163
|
-
#
|
|
244
|
+
# 🧠 WORKING MEMORY: Fast capture each turn (async, non-blocking)
|
|
164
245
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
165
|
-
|
|
166
|
-
[ -f "$
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
|
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) &
|
|
180
256
|
fi
|
|
181
257
|
fi
|
|
182
258
|
|
|
183
|
-
|
|
259
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
260
|
+
# 💾 LOCAL CACHE: Tier 0 capture for instant /continue (async, non-blocking)
|
|
261
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
262
|
+
if command -v ekkos-capture &>/dev/null; then
|
|
263
|
+
# Generate session name if not already set
|
|
264
|
+
if [ -z "$SESSION_NAME" ] || [ "$SESSION_NAME" = "unknown-session-starts" ]; then
|
|
265
|
+
SESSION_NAME=$(uuid_to_words "$RAW_SESSION_ID" 2>/dev/null || echo "unknown-session")
|
|
266
|
+
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
|
+
fi
|
|
184
271
|
|
|
185
272
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
186
|
-
#
|
|
273
|
+
# 📥 GOLDEN LOOP: CAPTURE PHASE - Track turn start
|
|
187
274
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
194
295
|
|
|
195
|
-
|
|
296
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
297
|
+
# 🔍 GOLDEN LOOP: RETRIEVE PHASE - Auto-retrieve patterns from ekkOS
|
|
298
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
299
|
+
EKKOS_API_KEY=""
|
|
300
|
+
if [ -f "$HOME/.ekkos/.hookApiKey" ]; then
|
|
301
|
+
EKKOS_API_KEY=$(cat "$HOME/.ekkos/.hookApiKey" 2>/dev/null || echo "")
|
|
302
|
+
elif [ -f "$HOME/.ekkos/config.json" ]; then
|
|
303
|
+
EKKOS_API_KEY=$(jq -r '.hookApiKey // ""' "$HOME/.ekkos/config.json" 2>/dev/null || echo "")
|
|
304
|
+
fi
|
|
305
|
+
|
|
306
|
+
RETRIEVED_PATTERNS=""
|
|
307
|
+
PATTERN_COUNT=0
|
|
308
|
+
RETRIEVED_DIRECTIVES=""
|
|
309
|
+
DIRECTIVE_COUNT=0
|
|
196
310
|
|
|
197
311
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
198
|
-
#
|
|
312
|
+
# 📦 DIRECTIVE CACHE: Local cache to avoid API calls every turn
|
|
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
|
|
199
317
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
318
|
+
DIRECTIVE_CACHE_DIR="$HOME/.ekkos/cache"
|
|
319
|
+
DIRECTIVE_CACHE_FILE="$DIRECTIVE_CACHE_DIR/directives.json"
|
|
320
|
+
DIRECTIVE_CACHE_TTL=3600 # 1 hour in seconds
|
|
321
|
+
mkdir -p "$DIRECTIVE_CACHE_DIR" 2>/dev/null || true
|
|
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
|
|
204
331
|
|
|
205
|
-
|
|
206
|
-
|
|
332
|
+
if [ -f "$DIRECTIVE_CACHE_FILE" ]; then
|
|
333
|
+
CACHE_TIMESTAMP=$(jq -r '.cached_at // 0' "$DIRECTIVE_CACHE_FILE" 2>/dev/null || echo "0")
|
|
334
|
+
CURRENT_TIMESTAMP=$(date +%s)
|
|
335
|
+
CACHE_AGE=$((CURRENT_TIMESTAMP - CACHE_TIMESTAMP))
|
|
207
336
|
|
|
208
|
-
|
|
209
|
-
|
|
337
|
+
# Cache is valid if <1 hour old AND no directive trigger detected
|
|
338
|
+
if [ "$CACHE_AGE" -lt "$DIRECTIVE_CACHE_TTL" ] && [ "$DIRECTIVE_TRIGGER_DETECTED" = "false" ]; then
|
|
339
|
+
DIRECTIVE_CACHE_VALID=true
|
|
340
|
+
fi
|
|
341
|
+
fi
|
|
210
342
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
343
|
+
# Decide whether to inject directives this turn
|
|
344
|
+
# SMART INJECTION: Only on Turn 1, post-clear, or directive trigger
|
|
345
|
+
SHOULD_INJECT_DIRECTIVES=false
|
|
346
|
+
if [ "$TURN_NUMBER" -eq 1 ] || [ "$POST_CLEAR_DETECTED" = "true" ] || [ "$DIRECTIVE_TRIGGER_DETECTED" = "true" ]; then
|
|
347
|
+
SHOULD_INJECT_DIRECTIVES=true
|
|
348
|
+
fi
|
|
215
349
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
350
|
+
if [ -n "$EKKOS_API_KEY" ] && [ -n "$USER_QUERY" ]; then
|
|
351
|
+
# Update phase to RETRIEVE
|
|
352
|
+
jq -n \
|
|
353
|
+
--arg phase "retrieve" \
|
|
354
|
+
--argjson turn "$TURN_NUMBER" \
|
|
355
|
+
--arg session "$SESSION_ID" \
|
|
356
|
+
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
357
|
+
'{
|
|
358
|
+
phase: $phase,
|
|
359
|
+
turn: $turn,
|
|
360
|
+
session: $session,
|
|
361
|
+
timestamp: $timestamp,
|
|
362
|
+
stats: {
|
|
363
|
+
retrieved: 0,
|
|
364
|
+
applied: 0,
|
|
365
|
+
forged: 0
|
|
366
|
+
}
|
|
367
|
+
}' > "$GOLDEN_LOOP_FILE" 2>/dev/null || true
|
|
368
|
+
|
|
369
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
370
|
+
# PATTERN RETRIEVAL: Always search patterns (they're query-specific)
|
|
371
|
+
# DIRECTIVE RETRIEVAL: Only if cache is invalid or trigger detected
|
|
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
|
|
238
380
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
381
|
+
# Call ekkOS MCP gateway
|
|
382
|
+
SEARCH_RESPONSE=$(curl -s -X POST "https://api.ekkos.dev/api/v1/mcp/call" \
|
|
383
|
+
-H "Authorization: Bearer $EKKOS_API_KEY" \
|
|
384
|
+
-H "Content-Type: application/json" \
|
|
385
|
+
--max-time 2 \
|
|
386
|
+
-d "{\"tool\": \"ekkOS_Search\", \"arguments\": {\"query\": $(echo "$USER_QUERY" | jq -Rs .), \"limit\": 5, \"sources\": $SEARCH_SOURCES}}" 2>/dev/null || echo '{"result": {"results": {"patterns": [], "directives": []}}}')
|
|
387
|
+
|
|
388
|
+
# Count patterns retrieved (MCP response: .result.results.patterns)
|
|
389
|
+
PATTERN_COUNT=$(echo "$SEARCH_RESPONSE" | jq -r '.result.results.patterns | length' 2>/dev/null || echo "0")
|
|
390
|
+
|
|
391
|
+
# Update golden loop with retrieved count
|
|
392
|
+
if [ "$PATTERN_COUNT" -gt 0 ]; then
|
|
393
|
+
jq -n \
|
|
394
|
+
--arg phase "inject" \
|
|
395
|
+
--argjson turn "$TURN_NUMBER" \
|
|
396
|
+
--arg session "$SESSION_ID" \
|
|
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
|
|
255
417
|
|
|
256
|
-
#
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
418
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
419
|
+
# DIRECTIVE HANDLING: Use cache if valid, otherwise process from response
|
|
420
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
421
|
+
if [ "$DIRECTIVE_CACHE_VALID" = "true" ]; then
|
|
422
|
+
# Load directives from cache
|
|
423
|
+
DIRECTIVE_COUNT=$(jq -r '.count // 0' "$DIRECTIVE_CACHE_FILE" 2>/dev/null || echo "0")
|
|
424
|
+
RETRIEVED_DIRECTIVES=$(jq -r '.formatted // ""' "$DIRECTIVE_CACHE_FILE" 2>/dev/null || echo "")
|
|
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
|
|
263
467
|
fi
|
|
468
|
+
fi
|
|
264
469
|
fi
|
|
265
470
|
|
|
471
|
+
# Context tracking removed - Claude Code handles its own context management
|
|
472
|
+
|
|
266
473
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
267
|
-
#
|
|
474
|
+
# COLORS
|
|
268
475
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
|
476
|
+
CYAN='\033[0;36m'
|
|
477
|
+
GREEN='\033[0;32m'
|
|
478
|
+
YELLOW='\033[1;33m'
|
|
479
|
+
MAGENTA='\033[0;35m'
|
|
480
|
+
DIM='\033[2m'
|
|
481
|
+
BOLD='\033[1m'
|
|
482
|
+
RESET='\033[0m'
|
|
313
483
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
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: 'mcp.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
|
|
484
|
+
CURRENT_TIME=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
|
|
485
|
+
|
|
486
|
+
# Generate session name (arrays and uuid_to_words defined at top of file)
|
|
487
|
+
SESSION_NAME=""
|
|
488
|
+
if [ -n "$SESSION_ID" ] && [ "$SESSION_ID" != "unknown" ] && [ "$SESSION_ID" != "null" ]; then
|
|
489
|
+
SESSION_NAME=$(uuid_to_words "$SESSION_ID")
|
|
340
490
|
fi
|
|
341
491
|
|
|
342
492
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
343
|
-
# "/continue" COMMAND:
|
|
493
|
+
# "/continue" COMMAND: Delegated to Skill system (DO NOT INTERCEPT)
|
|
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
|
|
344
507
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
345
|
-
QUERY_LOWER=$(echo "$USER_QUERY" | tr '[:upper:]' '[:lower:]')
|
|
346
|
-
|
|
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
508
|
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
fi
|
|
411
|
-
fi
|
|
509
|
+
# Note: Session name is shown in status line for easy reference:
|
|
510
|
+
# Example: "🧠 ekkOS Memory | Turn 42 | groovy-cactus | 2026-01-12 09:40 AM EST"
|
|
412
511
|
|
|
413
512
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
414
|
-
#
|
|
415
|
-
#
|
|
513
|
+
# AUTO-RESTORE REMOVED: Manual /continue only (saves 79% token burn!)
|
|
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
|
|
416
528
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
417
|
-
if [ "$PREV_CONTEXT_PERCENT" -gt 50 ] && [ "$TOKEN_PERCENT" -lt 15 ] && [ -n "$AUTH_TOKEN" ]; then
|
|
418
|
-
echo ""
|
|
419
|
-
echo -e "${GREEN}${BOLD}🔄 CONTEXT RESTORED${RESET} ${DIM}| Compaction detected | Auto-loading recent turns...${RESET}"
|
|
420
529
|
|
|
421
|
-
|
|
422
|
-
|
|
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
|
|
530
|
+
# Simple status line - no context warnings, Claude handles its own context
|
|
531
|
+
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
451
532
|
|
|
533
|
+
# Output skill reminder if detected
|
|
534
|
+
if [ -n "$SKILL_REMINDER" ]; then
|
|
452
535
|
echo ""
|
|
453
|
-
echo -e "${
|
|
454
|
-
|
|
455
|
-
elif [ "$POST_CLEAR_DETECTED" = true ] && [ -n "$AUTH_TOKEN" ]; then
|
|
456
|
-
# /clear detected - show visible restoration banner
|
|
457
|
-
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
|
|
458
|
-
echo -e "${GREEN}${BOLD}🔄 SESSION CONTINUED${RESET} ${DIM}| ${TURN_NUMBER} turns preserved | Context restored${RESET}" >&2
|
|
459
|
-
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
|
|
536
|
+
echo -e "${MAGENTA}${BOLD}$SKILL_REMINDER${RESET}"
|
|
537
|
+
fi
|
|
460
538
|
|
|
539
|
+
# 💉 GOLDEN LOOP: INJECT PHASE - Inject directives FIRST (highest priority)
|
|
540
|
+
# SMART INJECTION: Only on Turn 1, post-clear, or directive trigger
|
|
541
|
+
if [ "$SHOULD_INJECT_DIRECTIVES" = "true" ] && [ -n "$RETRIEVED_DIRECTIVES" ] && [ "$DIRECTIVE_COUNT" -gt 0 ]; then
|
|
461
542
|
echo ""
|
|
462
|
-
echo -
|
|
463
|
-
echo -e "$
|
|
464
|
-
echo -
|
|
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
|
|
543
|
+
echo "<system-reminder>"
|
|
544
|
+
echo -e "$RETRIEVED_DIRECTIVES"
|
|
545
|
+
echo "</system-reminder>"
|
|
546
|
+
fi
|
|
493
547
|
|
|
548
|
+
# 💉 GOLDEN LOOP: INJECT PHASE - Inject retrieved patterns into context
|
|
549
|
+
if [ -n "$RETRIEVED_PATTERNS" ] && [ "$PATTERN_COUNT" -gt 0 ]; then
|
|
494
550
|
echo ""
|
|
495
|
-
echo -
|
|
496
|
-
echo
|
|
551
|
+
echo "<system-reminder>"
|
|
552
|
+
echo "🔍 RETRIEVED PATTERNS FROM ekkOS MEMORY ($PATTERN_COUNT patterns found)"
|
|
497
553
|
echo ""
|
|
498
|
-
echo -e "$
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
echo -
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
echo
|
|
554
|
+
echo -e "$RETRIEVED_PATTERNS"
|
|
555
|
+
echo ""
|
|
556
|
+
echo "These patterns were automatically retrieved based on your query. Review and apply relevant solutions."
|
|
557
|
+
echo "</system-reminder>"
|
|
558
|
+
echo ""
|
|
559
|
+
echo "🔴 MANDATORY PATTERN ACKNOWLEDGMENT (PatternGuard):"
|
|
560
|
+
echo "You MUST acknowledge ALL $PATTERN_COUNT patterns above - either SELECT or SKIP each one."
|
|
561
|
+
echo ""
|
|
562
|
+
echo "For patterns you USE:"
|
|
563
|
+
echo "[ekkOS_SELECT]"
|
|
564
|
+
echo "- id: <pattern_id>"
|
|
565
|
+
echo " reason: <1-line why using>"
|
|
566
|
+
echo " confidence: <0.0-1.0>"
|
|
567
|
+
echo "[/ekkOS_SELECT]"
|
|
568
|
+
echo ""
|
|
569
|
+
echo "For patterns NOT relevant:"
|
|
570
|
+
echo "[ekkOS_SKIP]"
|
|
571
|
+
echo "- id: <pattern_id>"
|
|
572
|
+
echo " reason: <1-line why not relevant>"
|
|
573
|
+
echo "[/ekkOS_SKIP]"
|
|
574
|
+
echo ""
|
|
575
|
+
echo "⚡ AUTOMATIC APPLICATION: When pattern matches your task, APPLY IT immediately without asking."
|
|
576
|
+
echo "Track outcome after: ekkOS_Outcome({success: true/false})"
|
|
577
|
+
echo ""
|
|
578
|
+
echo "100% coverage required. This is how the system learns what works."
|
|
505
579
|
fi
|
|
506
580
|
|
|
507
|
-
#
|
|
508
|
-
if [ -n "$
|
|
581
|
+
# Inject footer format reminder (helps Claude remember session name)
|
|
582
|
+
if [ -n "$SESSION_NAME" ] && [ "$SESSION_NAME" != "unknown-session" ]; then
|
|
509
583
|
echo ""
|
|
510
|
-
echo -
|
|
584
|
+
echo "<footer-format>End responses with: Claude Code ({Model}) · 🧠 **ekkOS_™** · Turn ${TURN_NUMBER} · ${SESSION_NAME} · 📅 ${CURRENT_TIME}</footer-format>"
|
|
511
585
|
fi
|
|
512
586
|
|
|
513
587
|
exit 0
|