@ekkos/cli 0.3.3 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/agent/daemon.d.ts +27 -0
- package/dist/agent/daemon.js +254 -29
- package/dist/agent/health-check.d.ts +35 -0
- package/dist/agent/health-check.js +243 -0
- package/dist/agent/pty-runner.d.ts +1 -0
- package/dist/agent/pty-runner.js +6 -1
- package/dist/capture/transcript-repair.d.ts +1 -0
- package/dist/capture/transcript-repair.js +12 -1
- package/dist/commands/agent.d.ts +6 -0
- package/dist/commands/agent.js +244 -0
- package/dist/commands/dashboard.d.ts +25 -0
- package/dist/commands/dashboard.js +1175 -0
- package/dist/commands/run.d.ts +3 -0
- package/dist/commands/run.js +503 -350
- package/dist/commands/setup-remote.js +146 -37
- package/dist/commands/swarm-dashboard.d.ts +20 -0
- package/dist/commands/swarm-dashboard.js +735 -0
- package/dist/commands/swarm-setup.d.ts +10 -0
- package/dist/commands/swarm-setup.js +956 -0
- package/dist/commands/swarm.d.ts +46 -0
- package/dist/commands/swarm.js +441 -0
- package/dist/commands/test-claude.d.ts +16 -0
- package/dist/commands/test-claude.js +156 -0
- package/dist/commands/usage/blocks.d.ts +8 -0
- package/dist/commands/usage/blocks.js +60 -0
- package/dist/commands/usage/daily.d.ts +9 -0
- package/dist/commands/usage/daily.js +96 -0
- package/dist/commands/usage/dashboard.d.ts +8 -0
- package/dist/commands/usage/dashboard.js +104 -0
- package/dist/commands/usage/formatters.d.ts +41 -0
- package/dist/commands/usage/formatters.js +147 -0
- package/dist/commands/usage/index.d.ts +13 -0
- package/dist/commands/usage/index.js +87 -0
- package/dist/commands/usage/monthly.d.ts +8 -0
- package/dist/commands/usage/monthly.js +66 -0
- package/dist/commands/usage/session.d.ts +11 -0
- package/dist/commands/usage/session.js +193 -0
- package/dist/commands/usage/weekly.d.ts +9 -0
- package/dist/commands/usage/weekly.js +61 -0
- package/dist/deploy/instructions.d.ts +5 -2
- package/dist/deploy/instructions.js +11 -8
- package/dist/index.js +256 -20
- package/dist/lib/tmux-scrollbar.d.ts +14 -0
- package/dist/lib/tmux-scrollbar.js +296 -0
- package/dist/lib/usage-parser.d.ts +95 -5
- package/dist/lib/usage-parser.js +416 -71
- package/dist/utils/log-rotate.d.ts +18 -0
- package/dist/utils/log-rotate.js +74 -0
- package/dist/utils/platform.d.ts +2 -0
- package/dist/utils/platform.js +3 -1
- package/dist/utils/session-binding.d.ts +5 -0
- package/dist/utils/session-binding.js +46 -0
- package/dist/utils/state.js +4 -0
- package/dist/utils/verify-remote-terminal.d.ts +10 -0
- package/dist/utils/verify-remote-terminal.js +415 -0
- package/package.json +16 -11
- package/templates/CLAUDE.md +135 -23
- package/templates/cursor-hooks/after-agent-response.sh +0 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
- package/templates/cursor-hooks/stop.sh +0 -0
- package/templates/ekkos-manifest.json +5 -5
- package/templates/hooks/assistant-response.sh +0 -0
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/count-tokens.cjs +86 -0
- package/templates/hooks/lib/ekkos-reminders.sh +98 -0
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/session-start.sh +0 -0
- package/templates/hooks/stop.sh +150 -388
- package/templates/hooks/user-prompt-submit.sh +353 -443
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/windsurf-hooks/README.md +212 -0
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/install.sh +148 -0
- package/templates/windsurf-hooks/lib/contract.sh +2 -0
- package/templates/windsurf-hooks/post-cascade-response.sh +251 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +435 -0
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +219 -0
- package/LICENSE +0 -21
- package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
3
3
|
# ekkOS_ Hook: UserPromptSubmit - SEAMLESS CONTEXT CONTINUITY
|
|
4
|
-
# MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
|
|
5
|
-
# EKKOS_MANAGED=1
|
|
6
4
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
7
5
|
# ZERO USER ACTION NEEDED:
|
|
8
6
|
# 1. Tracks turn number and context size
|
|
@@ -12,8 +10,12 @@
|
|
|
12
10
|
|
|
13
11
|
set +e
|
|
14
12
|
|
|
13
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
14
|
+
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
15
|
+
|
|
15
16
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
16
|
-
# CONFIG PATHS - No
|
|
17
|
+
# CONFIG PATHS - No jq dependency (v1.2 spec)
|
|
18
|
+
# Session words live in ~/.ekkos/ so they work in ANY project
|
|
17
19
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
18
20
|
EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
|
|
19
21
|
SESSION_WORDS_JSON="$EKKOS_CONFIG_DIR/session-words.json"
|
|
@@ -21,115 +23,13 @@ SESSION_WORDS_DEFAULT="$EKKOS_CONFIG_DIR/.defaults/session-words.json"
|
|
|
21
23
|
JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
|
|
22
24
|
|
|
23
25
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
-
#
|
|
25
|
-
# Format: adj-noun-verb (e.g., "cosmic-penguin-runs")
|
|
26
|
+
# JSON PARSING HELPER - No jq required
|
|
26
27
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
-
|
|
28
|
-
# Arrays loaded from JSON - NOT hardcoded
|
|
29
|
-
declare -a ADJECTIVES
|
|
30
|
-
declare -a NOUNS
|
|
31
|
-
declare -a VERBS
|
|
32
|
-
SESSION_WORDS_LOADED=false
|
|
33
|
-
|
|
34
|
-
load_session_words() {
|
|
35
|
-
if [ "$SESSION_WORDS_LOADED" = "true" ]; then
|
|
36
|
-
return 0
|
|
37
|
-
fi
|
|
38
|
-
|
|
39
|
-
local words_file="$SESSION_WORDS_JSON"
|
|
40
|
-
|
|
41
|
-
# Fallback to managed defaults if user file missing/invalid
|
|
42
|
-
if [ ! -f "$words_file" ]; then
|
|
43
|
-
words_file="$SESSION_WORDS_DEFAULT"
|
|
44
|
-
fi
|
|
45
|
-
|
|
46
|
-
if [ ! -f "$words_file" ]; then
|
|
47
|
-
echo "ERROR: No session-words.json found. Run 'ekkos hooks install --global' to fix." >&2
|
|
48
|
-
return 1
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
# Check for json-parse helper
|
|
52
|
-
if [ ! -f "$JSON_PARSE_HELPER" ]; then
|
|
53
|
-
echo "ERROR: json-parse.cjs helper not found. Run 'ekkos hooks install --global' to fix." >&2
|
|
54
|
-
return 1
|
|
55
|
-
fi
|
|
56
|
-
|
|
57
|
-
# Parse using Node helper (no jq dependency)
|
|
58
|
-
if command -v node &>/dev/null; then
|
|
59
|
-
# Use readarray if available (bash 4+), otherwise use while loop
|
|
60
|
-
if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
|
|
61
|
-
readarray -t ADJECTIVES < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
|
|
62
|
-
readarray -t NOUNS < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
|
|
63
|
-
readarray -t VERBS < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
|
|
64
|
-
else
|
|
65
|
-
# Bash 3 fallback (macOS default)
|
|
66
|
-
local i=0
|
|
67
|
-
while IFS= read -r line; do
|
|
68
|
-
ADJECTIVES[i]="$line"
|
|
69
|
-
((i++))
|
|
70
|
-
done < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
|
|
71
|
-
i=0
|
|
72
|
-
while IFS= read -r line; do
|
|
73
|
-
NOUNS[i]="$line"
|
|
74
|
-
((i++))
|
|
75
|
-
done < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
|
|
76
|
-
i=0
|
|
77
|
-
while IFS= read -r line; do
|
|
78
|
-
VERBS[i]="$line"
|
|
79
|
-
((i++))
|
|
80
|
-
done < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
|
|
81
|
-
fi
|
|
82
|
-
|
|
83
|
-
if [ ${#ADJECTIVES[@]} -eq 0 ] || [ ${#NOUNS[@]} -eq 0 ] || [ ${#VERBS[@]} -eq 0 ]; then
|
|
84
|
-
echo "ERROR: Failed to parse $words_file. Check JSON syntax." >&2
|
|
85
|
-
return 1
|
|
86
|
-
fi
|
|
87
|
-
|
|
88
|
-
SESSION_WORDS_LOADED=true
|
|
89
|
-
return 0
|
|
90
|
-
else
|
|
91
|
-
echo "ERROR: Node.js not found. Install Node.js 18+ to use ekkOS hooks." >&2
|
|
92
|
-
return 1
|
|
93
|
-
fi
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
uuid_to_words() {
|
|
97
|
-
local uuid="$1"
|
|
98
|
-
|
|
99
|
-
# Load session words if not already loaded
|
|
100
|
-
if [ "$SESSION_WORDS_LOADED" != "true" ]; then
|
|
101
|
-
load_session_words || {
|
|
102
|
-
echo "unknown-session-starts"
|
|
103
|
-
return
|
|
104
|
-
}
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
local hex="${uuid//-/}"
|
|
108
|
-
hex="${hex:0:12}"
|
|
109
|
-
if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
|
|
110
|
-
echo "unknown-session-starts"
|
|
111
|
-
return
|
|
112
|
-
fi
|
|
113
|
-
local adj_seed=$((16#${hex:0:4}))
|
|
114
|
-
local noun_seed=$((16#${hex:4:4}))
|
|
115
|
-
local verb_seed=$((16#${hex:8:4}))
|
|
116
|
-
local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
|
|
117
|
-
local noun_idx=$((noun_seed % ${#NOUNS[@]}))
|
|
118
|
-
local verb_idx=$((verb_seed % ${#VERBS[@]}))
|
|
119
|
-
echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
123
|
-
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
124
|
-
|
|
125
|
-
INPUT=$(cat)
|
|
126
|
-
|
|
127
|
-
# Parse input using json-parse helper (no jq)
|
|
128
28
|
parse_json_value() {
|
|
129
29
|
local json="$1"
|
|
130
30
|
local path="$2"
|
|
131
31
|
echo "$json" | node -e "
|
|
132
|
-
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8'));
|
|
32
|
+
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
133
33
|
const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
|
|
134
34
|
let result = data;
|
|
135
35
|
for (const p of path) {
|
|
@@ -140,100 +40,63 @@ if (result !== undefined && result !== null) console.log(result);
|
|
|
140
40
|
" 2>/dev/null || echo ""
|
|
141
41
|
}
|
|
142
42
|
|
|
43
|
+
INPUT=$(cat)
|
|
143
44
|
USER_QUERY=$(parse_json_value "$INPUT" '.query')
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
fi
|
|
147
|
-
if [ -z "$USER_QUERY" ] || [ "$USER_QUERY" = "null" ]; then
|
|
148
|
-
USER_QUERY=$(parse_json_value "$INPUT" '.prompt')
|
|
149
|
-
fi
|
|
150
|
-
|
|
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')
|
|
151
47
|
RAW_SESSION_ID=$(parse_json_value "$INPUT" '.session_id')
|
|
48
|
+
[ -z "$RAW_SESSION_ID" ] && RAW_SESSION_ID="unknown"
|
|
152
49
|
TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
|
|
153
50
|
|
|
154
51
|
[ -z "$USER_QUERY" ] || [ "$USER_QUERY" = "null" ] && exit 0
|
|
155
52
|
|
|
156
53
|
# Fallback: read session_id from saved state if not in INPUT
|
|
157
|
-
if [
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
54
|
+
if [ "$RAW_SESSION_ID" = "unknown" ] || [ "$RAW_SESSION_ID" = "null" ] || [ -z "$RAW_SESSION_ID" ]; then
|
|
55
|
+
STATE_FILE="$HOME/.claude/state/current-session.json"
|
|
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)
|
|
64
|
+
fi
|
|
168
65
|
fi
|
|
169
66
|
|
|
170
67
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
171
|
-
#
|
|
172
|
-
# Detects ALL applicable skills/tools and injects as system reminder
|
|
68
|
+
# SKILL AUTO-FIRE: Detect keywords and inject skill reminders
|
|
173
69
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
174
|
-
|
|
70
|
+
SKILL_REMINDER=""
|
|
175
71
|
QUERY_LOWER=$(echo "$USER_QUERY" | tr '[:upper:]' '[:lower:]')
|
|
176
72
|
|
|
177
|
-
# Memory First
|
|
178
|
-
if echo "$QUERY_LOWER" | grep -qE '(how do i|debug|error|bug|fix|not working|broken|fails|issue
|
|
179
|
-
|
|
180
|
-
fi
|
|
181
|
-
|
|
182
|
-
# Recall Triggers - Time-based memory
|
|
183
|
-
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
|
|
184
|
-
SKILL_REMINDERS+=("SKILL REQUIRED: Call Skill(skill: \"ekkOS_Deep_Recall\") for time-based memory")
|
|
185
|
-
fi
|
|
186
|
-
|
|
187
|
-
# Directive Triggers - User preferences
|
|
188
|
-
if echo "$QUERY_LOWER" | grep -qE '(always |never |i prefer|i like |dont |don.t |avoid |remember that |from now on)'; then
|
|
189
|
-
SKILL_REMINDERS+=("SKILL REQUIRED: Call Skill(skill: \"ekkOS_Preferences\") to capture directive")
|
|
190
|
-
fi
|
|
191
|
-
|
|
192
|
-
# Safety Triggers - Destructive actions
|
|
193
|
-
if echo "$QUERY_LOWER" | grep -qE '(delete|drop |rm -rf|deploy|push.*main|push.*master|production|migrate|rollback)'; then
|
|
194
|
-
SKILL_REMINDERS+=("SAFETY REQUIRED: Call ekkOS_Conflict before this destructive action")
|
|
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"
|
|
195
76
|
fi
|
|
196
77
|
|
|
197
|
-
#
|
|
198
|
-
if echo "$QUERY_LOWER" | grep -qE '(
|
|
199
|
-
|
|
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"
|
|
200
81
|
fi
|
|
201
82
|
|
|
202
|
-
#
|
|
203
|
-
if echo "$QUERY_LOWER" | grep -qE '(
|
|
204
|
-
|
|
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"
|
|
205
86
|
fi
|
|
206
87
|
|
|
207
|
-
#
|
|
208
|
-
if echo "$QUERY_LOWER" | grep -qE '(
|
|
209
|
-
|
|
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"
|
|
210
91
|
fi
|
|
211
92
|
|
|
212
|
-
#
|
|
213
|
-
if echo "$QUERY_LOWER" | grep -qE '(
|
|
214
|
-
|
|
215
|
-
fi
|
|
216
|
-
|
|
217
|
-
# Codebase Triggers - Project-specific code search
|
|
218
|
-
if echo "$QUERY_LOWER" | grep -qE '(where is|find.*file|search.*code|in this project|in the codebase)'; then
|
|
219
|
-
SKILL_REMINDERS+=("CODEBASE: Use ekkOS_Codebase for project-specific code search")
|
|
220
|
-
fi
|
|
221
|
-
|
|
222
|
-
# Combine skill reminders (only take first 3 to avoid noise)
|
|
223
|
-
SKILL_REMINDER=""
|
|
224
|
-
REMINDER_COUNT=${#SKILL_REMINDERS[@]}
|
|
225
|
-
if [ "$REMINDER_COUNT" -gt 0 ]; then
|
|
226
|
-
MAX_REMINDERS=3
|
|
227
|
-
[ "$REMINDER_COUNT" -lt "$MAX_REMINDERS" ] && MAX_REMINDERS="$REMINDER_COUNT"
|
|
228
|
-
for i in $(seq 0 $((MAX_REMINDERS - 1))); do
|
|
229
|
-
[ -n "$SKILL_REMINDER" ] && SKILL_REMINDER="$SKILL_REMINDER
|
|
230
|
-
"
|
|
231
|
-
SKILL_REMINDER="$SKILL_REMINDER${SKILL_REMINDERS[$i]}"
|
|
232
|
-
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"
|
|
233
96
|
fi
|
|
234
97
|
|
|
235
98
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
236
|
-
# Load auth
|
|
99
|
+
# Load auth - No jq
|
|
237
100
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
238
101
|
EKKOS_CONFIG="$HOME/.ekkos/config.json"
|
|
239
102
|
AUTH_TOKEN=""
|
|
@@ -248,7 +111,7 @@ fi
|
|
|
248
111
|
MEMORY_API_URL="https://mcp.ekkos.dev"
|
|
249
112
|
|
|
250
113
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
251
|
-
# Session ID
|
|
114
|
+
# Session ID
|
|
252
115
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
253
116
|
STATE_DIR="$PROJECT_ROOT/.claude/state"
|
|
254
117
|
mkdir -p "$STATE_DIR" 2>/dev/null || true
|
|
@@ -257,7 +120,14 @@ SESSION_FILE="$STATE_DIR/current-session.json"
|
|
|
257
120
|
SESSION_ID="$RAW_SESSION_ID"
|
|
258
121
|
|
|
259
122
|
if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
|
|
260
|
-
|
|
123
|
+
exit 0
|
|
124
|
+
fi
|
|
125
|
+
|
|
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
|
|
261
131
|
fi
|
|
262
132
|
|
|
263
133
|
echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"}" > "$SESSION_FILE"
|
|
@@ -268,336 +138,376 @@ echo "{\"session_id\": \"$SESSION_ID\", \"timestamp\": \"$(date -u +%Y-%m-%dT%H:
|
|
|
268
138
|
PROJECT_SESSION_DIR="$STATE_DIR/sessions"
|
|
269
139
|
mkdir -p "$PROJECT_SESSION_DIR" 2>/dev/null || true
|
|
270
140
|
TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.turn"
|
|
141
|
+
CONTEXT_SIZE_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.context"
|
|
271
142
|
|
|
143
|
+
# Count API round-trips from transcript to match TUI turn counter
|
|
272
144
|
TURN_NUMBER=1
|
|
273
145
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
274
|
-
|
|
275
|
-
|
|
146
|
+
TURN_NUMBER=$(grep -c '"type":"user"' "$TRANSCRIPT_PATH" 2>/dev/null || echo "1")
|
|
147
|
+
[ "$TURN_NUMBER" -eq 0 ] && TURN_NUMBER=1
|
|
276
148
|
fi
|
|
277
149
|
|
|
150
|
+
# Detect post-clear: saved count higher than transcript means /clear happened
|
|
278
151
|
SAVED_TURN_COUNT=0
|
|
279
152
|
[ -f "$TURN_COUNTER_FILE" ] && SAVED_TURN_COUNT=$(cat "$TURN_COUNTER_FILE" 2>/dev/null || echo "0")
|
|
280
|
-
TRANSCRIPT_TURN_COUNT=$TURN_NUMBER
|
|
281
153
|
POST_CLEAR_DETECTED=false
|
|
154
|
+
|
|
282
155
|
if [ "$SAVED_TURN_COUNT" -gt "$TURN_NUMBER" ]; then
|
|
283
|
-
|
|
284
|
-
|
|
156
|
+
POST_CLEAR_DETECTED=true
|
|
157
|
+
TURN_NUMBER=$((SAVED_TURN_COUNT + 1))
|
|
285
158
|
fi
|
|
159
|
+
|
|
286
160
|
echo "$TURN_NUMBER" > "$TURN_COUNTER_FILE"
|
|
287
161
|
|
|
288
162
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
289
|
-
#
|
|
163
|
+
# Context size tracking - Uses tokenizer script (single source)
|
|
290
164
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const req = https.request({
|
|
308
|
-
hostname: 'mcp.ekkos.dev',
|
|
309
|
-
path: '/api/v1/working/fast-capture',
|
|
310
|
-
method: 'POST',
|
|
311
|
-
headers: {
|
|
312
|
-
'Authorization': 'Bearer ' + process.argv[4],
|
|
313
|
-
'Content-Type': 'application/json'
|
|
314
|
-
}
|
|
315
|
-
}, () => {});
|
|
316
|
-
req.on('error', () => {});
|
|
317
|
-
req.write(body);
|
|
318
|
-
req.end();
|
|
319
|
-
" "$RAW_SESSION_ID" "$TURN_NUMBER" "$USER_QUERY" "$CAPTURE_TOKEN" >/dev/null 2>&1) &
|
|
320
|
-
fi
|
|
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
|
|
180
|
+
fi
|
|
321
181
|
fi
|
|
322
182
|
|
|
323
|
-
|
|
324
|
-
# LOCAL CACHE: Tier 0 capture for instant /continue (async, non-blocking)
|
|
325
|
-
# Per ekkOS Onboarding Spec v1.2 ADDENDUM: Pass instanceId for namespacing
|
|
326
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
327
|
-
if command -v ekkos-capture &>/dev/null; then
|
|
328
|
-
if [ -z "$SESSION_NAME" ] || [ "$SESSION_NAME" = "unknown-session-starts" ]; then
|
|
329
|
-
SESSION_NAME=$(uuid_to_words "$RAW_SESSION_ID" 2>/dev/null || echo "unknown-session")
|
|
330
|
-
fi
|
|
331
|
-
# NEW format: ekkos-capture user <instance_id> <session_id> <session_name> <turn_id> <query> [project_path]
|
|
332
|
-
INSTANCE_ID="${EKKOS_INSTANCE_ID:-default}"
|
|
333
|
-
(ekkos-capture user "$INSTANCE_ID" "$RAW_SESSION_ID" "$SESSION_NAME" "$TURN_NUMBER" "$USER_QUERY" "$PROJECT_ROOT" \
|
|
334
|
-
>/dev/null 2>&1) &
|
|
335
|
-
fi
|
|
183
|
+
echo "$TOKEN_PERCENT" > "$CONTEXT_SIZE_FILE"
|
|
336
184
|
|
|
337
185
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
338
|
-
#
|
|
186
|
+
# COLORS
|
|
339
187
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
340
|
-
|
|
341
|
-
|
|
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'
|
|
342
194
|
|
|
343
|
-
|
|
344
|
-
node -e "
|
|
345
|
-
const fs = require('fs');
|
|
346
|
-
const data = {
|
|
347
|
-
phase: 'capture',
|
|
348
|
-
turn: $TURN_NUMBER,
|
|
349
|
-
session: '$SESSION_ID',
|
|
350
|
-
timestamp: new Date().toISOString(),
|
|
351
|
-
stats: { retrieved: 0, applied: 0, forged: 0 }
|
|
352
|
-
};
|
|
353
|
-
fs.writeFileSync('$GOLDEN_LOOP_FILE', JSON.stringify(data, null, 2));
|
|
354
|
-
" 2>/dev/null || true
|
|
195
|
+
CURRENT_TIME=$(date "+%Y-%m-%d %I:%M:%S %p %Z")
|
|
355
196
|
|
|
356
197
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
357
|
-
#
|
|
198
|
+
# WORD-BASED SESSION NAMES - No jq, uses ~/.ekkos/ config (works in ANY project)
|
|
358
199
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
EKKOS_API_KEY=$(node "$JSON_PARSE_HELPER" "$HOME/.ekkos/config.json" '.hookApiKey' 2>/dev/null || echo "")
|
|
364
|
-
fi
|
|
200
|
+
declare -a ADJECTIVES
|
|
201
|
+
declare -a NOUNS
|
|
202
|
+
declare -a VERBS
|
|
203
|
+
SESSION_WORDS_LOADED=false
|
|
365
204
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
RETRIEVED_DIRECTIVES=""
|
|
369
|
-
DIRECTIVE_COUNT=0
|
|
205
|
+
load_session_words() {
|
|
206
|
+
if [ "$SESSION_WORDS_LOADED" = "true" ]; then return 0; fi
|
|
370
207
|
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
374
|
-
DIRECTIVE_CACHE_DIR="$HOME/.ekkos/cache"
|
|
375
|
-
DIRECTIVE_CACHE_FILE="$DIRECTIVE_CACHE_DIR/directives.json"
|
|
376
|
-
DIRECTIVE_CACHE_TTL=3600
|
|
377
|
-
mkdir -p "$DIRECTIVE_CACHE_DIR" 2>/dev/null || true
|
|
208
|
+
local words_file="$SESSION_WORDS_JSON"
|
|
209
|
+
[ ! -f "$words_file" ] && words_file="$SESSION_WORDS_DEFAULT"
|
|
378
210
|
|
|
379
|
-
|
|
380
|
-
|
|
211
|
+
if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
|
|
212
|
+
ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
|
|
213
|
+
return 1
|
|
214
|
+
fi
|
|
381
215
|
|
|
382
|
-
if
|
|
383
|
-
|
|
384
|
-
|
|
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
|
+
}
|
|
238
|
+
|
|
239
|
+
uuid_to_words() {
|
|
240
|
+
local uuid="$1"
|
|
241
|
+
load_session_words
|
|
385
242
|
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
}
|
|
390
255
|
|
|
391
|
-
|
|
392
|
-
|
|
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"
|
|
393
263
|
fi
|
|
394
264
|
fi
|
|
395
265
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
266
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
267
|
+
# SINGLE SOURCE OF TRUTH: Update ALL session tracking systems
|
|
268
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
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
|
|
400
283
|
|
|
401
|
-
|
|
402
|
-
|
|
284
|
+
# 4. Multi-session tracking - No jq
|
|
285
|
+
ACTIVE_SESSIONS_FILE="$HOME/.ekkos/active-sessions.json"
|
|
403
286
|
node -e "
|
|
404
287
|
const fs = require('fs');
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
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 &
|
|
420
312
|
fi
|
|
421
313
|
|
|
422
|
-
#
|
|
423
|
-
|
|
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 "
|
|
424
322
|
const https = require('https');
|
|
425
323
|
const body = JSON.stringify({
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
sources: $SEARCH_SOURCES
|
|
431
|
-
}
|
|
324
|
+
userId: process.argv[1],
|
|
325
|
+
realSession: process.argv[2],
|
|
326
|
+
projectPath: process.argv[3],
|
|
327
|
+
pendingSession: process.argv[4]
|
|
432
328
|
});
|
|
433
329
|
const req = https.request({
|
|
434
|
-
hostname: '
|
|
435
|
-
path: '/
|
|
330
|
+
hostname: 'mcp.ekkos.dev',
|
|
331
|
+
path: '/proxy/session/bind',
|
|
436
332
|
method: 'POST',
|
|
437
|
-
headers: {
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
'Content-Length': Buffer.byteLength(body)
|
|
441
|
-
},
|
|
442
|
-
timeout: 2000
|
|
443
|
-
}, (res) => {
|
|
444
|
-
let data = '';
|
|
445
|
-
res.on('data', chunk => data += chunk);
|
|
446
|
-
res.on('end', () => console.log(data));
|
|
447
|
-
});
|
|
448
|
-
req.on('error', () => console.log('{}'));
|
|
449
|
-
req.on('timeout', () => { req.destroy(); console.log('{}'); });
|
|
333
|
+
headers: {'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body)}
|
|
334
|
+
}, () => {});
|
|
335
|
+
req.on('error', () => {});
|
|
450
336
|
req.write(body);
|
|
451
337
|
req.end();
|
|
452
|
-
" "$
|
|
453
|
-
|
|
454
|
-
# Parse pattern count using node
|
|
455
|
-
PATTERN_COUNT=$(echo "$SEARCH_RESPONSE" | node -e "
|
|
456
|
-
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
457
|
-
console.log((data?.result?.results?.patterns?.length || 0));
|
|
458
|
-
" 2>/dev/null || echo "0")
|
|
459
|
-
|
|
460
|
-
if [ "$PATTERN_COUNT" -gt 0 ]; then
|
|
461
|
-
# Update golden loop
|
|
462
|
-
node -e "
|
|
463
|
-
const fs = require('fs');
|
|
464
|
-
const data = {
|
|
465
|
-
phase: 'inject',
|
|
466
|
-
turn: $TURN_NUMBER,
|
|
467
|
-
session: '$SESSION_ID',
|
|
468
|
-
timestamp: new Date().toISOString(),
|
|
469
|
-
stats: { retrieved: $PATTERN_COUNT, applied: 0, forged: 0 }
|
|
470
|
-
};
|
|
471
|
-
fs.writeFileSync('$GOLDEN_LOOP_FILE', JSON.stringify(data, null, 2));
|
|
472
|
-
" 2>/dev/null || true
|
|
473
|
-
|
|
474
|
-
# Format patterns for injection
|
|
475
|
-
RETRIEVED_PATTERNS=$(echo "$SEARCH_RESPONSE" | node -e "
|
|
476
|
-
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
477
|
-
const patterns = data?.result?.results?.patterns || [];
|
|
478
|
-
patterns.forEach(p => {
|
|
479
|
-
console.log('**' + (p.title || 'Untitled') + '**');
|
|
480
|
-
console.log((p.problem || p.guidance || '') + '\\n');
|
|
481
|
-
console.log('## Solution');
|
|
482
|
-
console.log((p.solution || p.content || '') + '\\n');
|
|
483
|
-
console.log('Success Rate: ' + Math.round((p.success_rate || 0) * 100) + '%');
|
|
484
|
-
console.log('Applied: ' + (p.applied_count || 0) + ' times\\n');
|
|
485
|
-
});
|
|
486
|
-
" 2>/dev/null || echo "")
|
|
487
|
-
fi
|
|
488
|
-
|
|
489
|
-
# Handle directives
|
|
490
|
-
if [ "$DIRECTIVE_CACHE_VALID" = "true" ] && [ -f "$DIRECTIVE_CACHE_FILE" ] && [ -f "$JSON_PARSE_HELPER" ]; then
|
|
491
|
-
DIRECTIVE_COUNT=$(node "$JSON_PARSE_HELPER" "$DIRECTIVE_CACHE_FILE" '.count' 2>/dev/null || echo "0")
|
|
492
|
-
RETRIEVED_DIRECTIVES=$(node "$JSON_PARSE_HELPER" "$DIRECTIVE_CACHE_FILE" '.formatted' 2>/dev/null || echo "")
|
|
493
|
-
else
|
|
494
|
-
DIRECTIVE_COUNT=$(echo "$SEARCH_RESPONSE" | node -e "
|
|
495
|
-
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
496
|
-
console.log((data?.result?.results?.directives?.length || 0));
|
|
497
|
-
" 2>/dev/null || echo "0")
|
|
498
|
-
|
|
499
|
-
if [ "$DIRECTIVE_COUNT" -gt 0 ]; then
|
|
500
|
-
RETRIEVED_DIRECTIVES=$(echo "$SEARCH_RESPONSE" | node -e "
|
|
501
|
-
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
502
|
-
const directives = data?.result?.results?.directives || [];
|
|
503
|
-
const byType = { MUST: [], NEVER: [], PREFER: [], AVOID: [] };
|
|
504
|
-
directives.forEach(d => {
|
|
505
|
-
if (byType[d.type]) byType[d.type].push(' - ' + d.rule);
|
|
506
|
-
});
|
|
507
|
-
let output = 'USER DIRECTIVES (FOLLOW THESE):';
|
|
508
|
-
if (byType.MUST.length) output += '\\n\\nMUST:\\n' + byType.MUST.join('\\n');
|
|
509
|
-
if (byType.NEVER.length) output += '\\n\\nNEVER:\\n' + byType.NEVER.join('\\n');
|
|
510
|
-
if (byType.PREFER.length) output += '\\n\\nPREFER:\\n' + byType.PREFER.join('\\n');
|
|
511
|
-
if (byType.AVOID.length) output += '\\n\\nAVOID:\\n' + byType.AVOID.join('\\n');
|
|
512
|
-
console.log(output);
|
|
513
|
-
" 2>/dev/null || echo "")
|
|
514
|
-
|
|
515
|
-
# Save to cache
|
|
516
|
-
node -e "
|
|
517
|
-
const fs = require('fs');
|
|
518
|
-
fs.writeFileSync('$DIRECTIVE_CACHE_FILE', JSON.stringify({
|
|
519
|
-
count: $DIRECTIVE_COUNT,
|
|
520
|
-
formatted: process.argv[1],
|
|
521
|
-
cached_at: Math.floor(Date.now() / 1000)
|
|
522
|
-
}, null, 2));
|
|
523
|
-
" "$RETRIEVED_DIRECTIVES" 2>/dev/null || true
|
|
524
|
-
fi
|
|
338
|
+
" "$EKKOS_USER_ID" "$SESSION_NAME" "$PROJECT_ROOT" "$PENDING_SESSION" 2>/dev/null &
|
|
525
339
|
fi
|
|
526
340
|
fi
|
|
527
341
|
|
|
528
342
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
529
|
-
#
|
|
343
|
+
# "/continue" COMMAND: Run AFTER /clear to restore last 5 turns
|
|
530
344
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
531
|
-
|
|
532
|
-
GREEN='\033[0;32m'
|
|
533
|
-
YELLOW='\033[1;33m'
|
|
534
|
-
MAGENTA='\033[0;35m'
|
|
535
|
-
DIM='\033[2m'
|
|
536
|
-
BOLD='\033[1m'
|
|
537
|
-
RESET='\033[0m'
|
|
345
|
+
QUERY_LOWER=$(echo "$USER_QUERY" | tr '[:upper:]' '[:lower:]')
|
|
538
346
|
|
|
539
|
-
|
|
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")
|
|
540
360
|
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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")
|
|
546
366
|
|
|
547
|
-
|
|
548
|
-
|
|
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 "")
|
|
549
372
|
|
|
550
|
-
# Output skill reminder if detected
|
|
551
|
-
if [ -n "$SKILL_REMINDER" ]; then
|
|
552
373
|
echo ""
|
|
553
|
-
echo -e "${
|
|
554
|
-
fi
|
|
555
|
-
|
|
556
|
-
# Inject directives FIRST (highest priority)
|
|
557
|
-
if [ "$SHOULD_INJECT_DIRECTIVES" = "true" ] && [ -n "$RETRIEVED_DIRECTIVES" ] && [ "$DIRECTIVE_COUNT" -gt 0 ]; then
|
|
374
|
+
echo -e "${GREEN}${BOLD}✓ Session continued${RESET} ${DIM}(${RESTORED_COUNT} turns restored)${RESET}"
|
|
558
375
|
echo ""
|
|
559
|
-
echo "<system-reminder>"
|
|
560
|
-
echo -e "$RETRIEVED_DIRECTIVES"
|
|
561
|
-
echo "</system-reminder>"
|
|
562
|
-
fi
|
|
563
376
|
|
|
564
|
-
# Inject retrieved patterns into context
|
|
565
|
-
if [ -n "$RETRIEVED_PATTERNS" ] && [ "$PATTERN_COUNT" -gt 0 ]; then
|
|
566
|
-
echo ""
|
|
567
377
|
echo "<system-reminder>"
|
|
568
|
-
echo "
|
|
378
|
+
echo "═══════════════════════════════════════════════════════════════════════"
|
|
379
|
+
echo "CONTEXT RESTORED - Resume seamlessly. DO NOT ask 'what were we doing?'"
|
|
380
|
+
echo "═══════════════════════════════════════════════════════════════════════"
|
|
569
381
|
echo ""
|
|
570
|
-
echo
|
|
382
|
+
echo "## Last User Request:"
|
|
383
|
+
echo "$LAST_TASK"
|
|
571
384
|
echo ""
|
|
572
|
-
echo "
|
|
573
|
-
echo "
|
|
385
|
+
echo "## Your Last Response (truncated):"
|
|
386
|
+
echo "$LAST_RESPONSE"
|
|
574
387
|
echo ""
|
|
575
|
-
|
|
576
|
-
|
|
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}| Turn ${TURN_NUMBER} | ${CURRENT_TIME}${RESET}"
|
|
409
|
+
exit 0
|
|
410
|
+
fi
|
|
411
|
+
fi
|
|
412
|
+
|
|
413
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
414
|
+
# COMPACTION DETECTION: If context dropped dramatically, auto-restore
|
|
415
|
+
# Was >50% last turn, now <15% = compaction happened
|
|
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}"
|
|
420
|
+
|
|
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"
|
|
577
435
|
echo ""
|
|
578
|
-
echo "
|
|
579
|
-
echo "[ekkOS_SELECT]"
|
|
580
|
-
echo "- id: <pattern_id>"
|
|
581
|
-
echo " reason: <1-line why using>"
|
|
582
|
-
echo " confidence: <0.0-1.0>"
|
|
583
|
-
echo "[/ekkOS_SELECT]"
|
|
436
|
+
echo -e "${MAGENTA}${BOLD}## Recent Context (auto-restored)${RESET}"
|
|
584
437
|
echo ""
|
|
585
|
-
|
|
586
|
-
echo "
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
+
|
|
590
448
|
echo ""
|
|
591
|
-
echo "
|
|
592
|
-
|
|
449
|
+
echo -e "${DIM}Full history: \"turns 1-${TURN_NUMBER}\" or \"recall yesterday\"${RESET}"
|
|
450
|
+
fi
|
|
451
|
+
|
|
452
|
+
echo ""
|
|
453
|
+
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
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
|
|
460
|
+
|
|
461
|
+
echo ""
|
|
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"
|
|
593
480
|
echo ""
|
|
594
|
-
|
|
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
|
+
|
|
494
|
+
echo ""
|
|
495
|
+
echo -e "${DIM}Full history: \"recall\" or \"turns 1-${TURN_NUMBER}\"${RESET}"
|
|
496
|
+
echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}"
|
|
497
|
+
echo ""
|
|
498
|
+
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
499
|
+
|
|
500
|
+
elif [ "$TOKEN_PERCENT" -ge 50 ]; then
|
|
501
|
+
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ~${IPC_PERCENT:-0}% IPC | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
502
|
+
|
|
503
|
+
else
|
|
504
|
+
echo -e "${CYAN}${BOLD}🧠 ekkOS Memory${RESET} ${DIM}| Turn ${TURN_NUMBER} | ${SESSION_NAME} | ${CURRENT_TIME}${RESET}"
|
|
595
505
|
fi
|
|
596
506
|
|
|
597
|
-
#
|
|
598
|
-
if [ -n "$
|
|
599
|
-
|
|
600
|
-
|
|
507
|
+
# Output skill reminder if detected
|
|
508
|
+
if [ -n "$SKILL_REMINDER" ]; then
|
|
509
|
+
echo ""
|
|
510
|
+
echo -e "${MAGENTA}${BOLD}$SKILL_REMINDER${RESET}"
|
|
601
511
|
fi
|
|
602
512
|
|
|
603
513
|
exit 0
|