@ekkos/cli 1.0.35 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +57 -0
- package/dist/commands/dashboard.js +561 -186
- package/dist/deploy/settings.js +13 -26
- package/package.json +2 -4
- package/templates/CLAUDE.md +135 -23
- package/templates/ekkos-manifest.json +8 -8
- package/templates/hooks/assistant-response.ps1 +256 -160
- package/templates/hooks/assistant-response.sh +130 -66
- package/templates/hooks/hooks.json +24 -6
- package/templates/hooks/lib/contract.sh +43 -31
- package/templates/hooks/lib/count-tokens.cjs +0 -0
- package/templates/hooks/lib/ekkos-reminders.sh +0 -0
- package/templates/hooks/lib/state.sh +53 -1
- package/templates/hooks/session-start.ps1 +91 -391
- package/templates/hooks/session-start.sh +201 -166
- package/templates/hooks/stop.ps1 +202 -341
- package/templates/hooks/stop.sh +275 -948
- package/templates/hooks/user-prompt-submit.ps1 +224 -548
- package/templates/hooks/user-prompt-submit.sh +382 -456
- package/templates/plan-template.md +0 -0
- package/templates/spec-template.md +0 -0
- package/templates/windsurf-hooks/hooks.json +9 -2
- package/templates/windsurf-hooks/install.sh +0 -0
- package/templates/windsurf-hooks/lib/contract.sh +2 -0
- package/templates/windsurf-hooks/post-cascade-response.sh +0 -0
- package/templates/windsurf-hooks/pre-user-prompt.sh +0 -0
- package/templates/agents/README.md +0 -182
- package/templates/agents/code-reviewer.md +0 -166
- package/templates/agents/debug-detective.md +0 -169
- package/templates/agents/ekkOS_Vercel.md +0 -99
- package/templates/agents/extension-manager.md +0 -229
- package/templates/agents/git-companion.md +0 -185
- package/templates/agents/github-test-agent.md +0 -321
- package/templates/agents/railway-manager.md +0 -179
- package/templates/windsurf-hooks/before-submit-prompt.sh +0 -238
- package/templates/windsurf-skills/ekkos-memory/SKILL.md +0 -219
package/templates/hooks/stop.sh
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
3
|
-
# ekkOS_ Hook: Stop -
|
|
3
|
+
# ekkOS_ Hook: Stop - Captures turns to BOTH Working (Redis) and Episodic (Supabase)
|
|
4
|
+
# NO jq dependency - uses Node.js for all JSON parsing
|
|
4
5
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
5
|
-
#
|
|
6
|
-
# -
|
|
7
|
-
#
|
|
8
|
-
#
|
|
6
|
+
# This hook captures every turn to:
|
|
7
|
+
# 1. Working Sessions (Redis) - Fast hot cache for /continue
|
|
8
|
+
# 2. Episodic Memory (Supabase) - Permanent cold storage
|
|
9
|
+
#
|
|
10
|
+
# NO compliance checking - skills handle that
|
|
11
|
+
# NO PatternGuard validation - skills handle that
|
|
12
|
+
# NO verbose output - just capture silently
|
|
9
13
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
10
14
|
|
|
11
15
|
set +e
|
|
@@ -14,1018 +18,341 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
|
14
18
|
PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
|
|
15
19
|
STATE_DIR="$PROJECT_ROOT/.claude/state"
|
|
16
20
|
|
|
21
|
+
mkdir -p "$STATE_DIR" 2>/dev/null
|
|
22
|
+
|
|
23
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
24
|
+
# CONFIG PATHS - No jq dependency (v1.2 spec)
|
|
25
|
+
# Session words live in ~/.ekkos/ so they work in ANY project
|
|
26
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
27
|
+
EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
|
|
28
|
+
SESSION_WORDS_JSON="$EKKOS_CONFIG_DIR/session-words.json"
|
|
29
|
+
SESSION_WORDS_DEFAULT="$EKKOS_CONFIG_DIR/.defaults/session-words.json"
|
|
30
|
+
JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
|
|
31
|
+
|
|
17
32
|
INPUT=$(cat)
|
|
18
33
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
34
|
+
# JSON parsing helper (no jq)
|
|
35
|
+
parse_json_value() {
|
|
36
|
+
local json="$1"
|
|
37
|
+
local path="$2"
|
|
38
|
+
echo "$json" | node -e "
|
|
39
|
+
const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
|
|
40
|
+
const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
|
|
41
|
+
let result = data;
|
|
42
|
+
for (const p of path) {
|
|
43
|
+
if (result === undefined || result === null) { result = undefined; break; }
|
|
44
|
+
result = result[p];
|
|
45
|
+
}
|
|
46
|
+
if (result !== undefined && result !== null) console.log(result);
|
|
47
|
+
" 2>/dev/null || echo ""
|
|
48
|
+
}
|
|
22
49
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
50
|
+
RAW_SESSION_ID=$(parse_json_value "$INPUT" '.session_id')
|
|
51
|
+
[ -z "$RAW_SESSION_ID" ] && RAW_SESSION_ID="unknown"
|
|
52
|
+
TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
|
|
53
|
+
MODEL_USED=$(parse_json_value "$INPUT" '.model')
|
|
54
|
+
[ -z "$MODEL_USED" ] && MODEL_USED="claude-sonnet-4-5"
|
|
27
55
|
|
|
28
56
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
29
|
-
# Session ID
|
|
57
|
+
# Session ID
|
|
30
58
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
31
59
|
SESSION_ID="$RAW_SESSION_ID"
|
|
32
60
|
|
|
33
|
-
# Fallback: Read from state file if input doesn't have valid session_id
|
|
34
|
-
if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
|
|
35
|
-
STATE_FILE="$HOME/.claude/state/current-session.json"
|
|
36
|
-
if [ -f "$STATE_FILE" ]; then
|
|
37
|
-
SESSION_ID=$(jq -r '.session_id // ""' "$STATE_FILE" 2>/dev/null || echo "")
|
|
38
|
-
fi
|
|
39
|
-
fi
|
|
40
|
-
|
|
41
|
-
# Skip if still no valid session ID
|
|
42
61
|
if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
|
|
43
62
|
exit 0
|
|
44
63
|
fi
|
|
45
64
|
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
USER_ID=""
|
|
52
|
-
|
|
53
|
-
if [ -f "$EKKOS_CONFIG" ]; then
|
|
54
|
-
AUTH_TOKEN=$(jq -r '.hookApiKey // .apiKey // ""' "$EKKOS_CONFIG" 2>/dev/null || echo "")
|
|
55
|
-
USER_ID=$(jq -r '.userId // ""' "$EKKOS_CONFIG" 2>/dev/null || echo "")
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
if [ -z "$AUTH_TOKEN" ] && [ -f "$PROJECT_ROOT/.env.local" ]; then
|
|
59
|
-
AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | tr -d '\r')
|
|
65
|
+
# Check if SESSION_ID is a UUID (8-4-4-4-12 format)
|
|
66
|
+
UUID_REGEX='^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$'
|
|
67
|
+
IS_UUID=false
|
|
68
|
+
if [[ "$SESSION_ID" =~ $UUID_REGEX ]]; then
|
|
69
|
+
IS_UUID=true
|
|
60
70
|
fi
|
|
61
71
|
|
|
62
|
-
[ -z "$AUTH_TOKEN" ] && exit 0
|
|
63
|
-
|
|
64
|
-
MEMORY_API_URL="https://mcp.ekkos.dev"
|
|
65
|
-
|
|
66
72
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
-
# WORD-BASED SESSION NAMES
|
|
68
|
-
# Format: adj-noun-verb (e.g., "cosmic-penguin-runs")
|
|
69
|
-
# 100 × 100 × 100 = 1,000,000 combinations (vs 10,000 with 2-word)
|
|
70
|
-
# Matches server-side session-names.ts algorithm
|
|
73
|
+
# WORD-BASED SESSION NAMES - No jq, uses ~/.ekkos/ config (works in ANY project)
|
|
71
74
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
72
|
-
ADJECTIVES
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
"slick" "sleek" "bold" "nifty" "perky" "plucky" "witty" "nimble"
|
|
77
|
-
"dapper" "fancy" "quirky" "punchy" "swift" "brave" "clever" "dandy"
|
|
78
|
-
"eager" "fiery" "golden" "hasty" "icy" "jolly" "keen" "lively"
|
|
79
|
-
"merry" "noble" "odd" "plush" "quick" "royal" "silly" "tidy"
|
|
80
|
-
"ultra" "vivid" "wacky" "zany" "alpha" "beta" "cyber" "delta"
|
|
81
|
-
"electric" "foggy" "giga" "hazy" "ionic" "jumpy" "kinky" "lunar"
|
|
82
|
-
"magic" "nerdy" "omega" "pixel" "quaint" "retro" "solar" "techno"
|
|
83
|
-
"unified" "viral" "wonky" "xerox" "yappy" "zen" "agile" "binary"
|
|
84
|
-
"chrome" "disco" "elastic" "fizzy" "glossy" "humble" "itchy" "jiffy"
|
|
85
|
-
"kooky" "loopy" "moody" "noisy"
|
|
86
|
-
)
|
|
87
|
-
NOUNS=(
|
|
88
|
-
"penguin" "panda" "otter" "narwhal" "alpaca" "llama" "badger" "walrus"
|
|
89
|
-
"waffle" "pickle" "noodle" "pretzel" "muffin" "taco" "nugget" "biscuit"
|
|
90
|
-
"rocket" "comet" "nebula" "quasar" "meteor" "photon" "pulsar" "nova"
|
|
91
|
-
"ninja" "pirate" "wizard" "robot" "yeti" "phoenix" "sphinx" "kraken"
|
|
92
|
-
"thunder" "blizzard" "tornado" "avalanche" "mango" "kiwi" "banana" "coconut"
|
|
93
|
-
"donut" "espresso" "falafel" "gyro" "hummus" "icecream" "jambon" "kebab"
|
|
94
|
-
"latte" "mocha" "nachos" "olive" "pasta" "quinoa" "ramen" "sushi"
|
|
95
|
-
"tamale" "udon" "velvet" "wasabi" "xmas" "yogurt" "ziti" "anchor"
|
|
96
|
-
"beacon" "canyon" "drifter" "echo" "falcon" "glacier" "harbor" "island"
|
|
97
|
-
"jetpack" "kayak" "lagoon" "meadow" "nebula" "orbit" "parrot" "quest"
|
|
98
|
-
"rapids" "summit" "tunnel" "umbrella" "volcano" "whisper" "xylophone" "yacht"
|
|
99
|
-
"zephyr" "acorn" "bobcat" "cactus" "dolphin" "eagle" "ferret" "gopher"
|
|
100
|
-
"hedgehog" "iguana" "jackal" "koala"
|
|
101
|
-
)
|
|
102
|
-
VERBS=(
|
|
103
|
-
"runs" "jumps" "flies" "swims" "dives" "soars" "glides" "dashes"
|
|
104
|
-
"zooms" "zips" "spins" "twirls" "bounces" "floats" "drifts" "sails"
|
|
105
|
-
"climbs" "leaps" "hops" "skips" "rolls" "slides" "surfs" "rides"
|
|
106
|
-
"builds" "creates" "forges" "shapes" "crafts" "designs" "codes" "types"
|
|
107
|
-
"thinks" "dreams" "learns" "grows" "blooms" "shines" "glows" "sparks"
|
|
108
|
-
"sings" "hums" "calls" "beeps" "clicks" "taps" "pings" "chimes"
|
|
109
|
-
"wins" "leads" "helps" "saves" "guards" "shields" "heals" "fixes"
|
|
110
|
-
"starts" "begins" "launches" "ignites" "blazes" "flares" "bursts" "pops"
|
|
111
|
-
"waves" "nods" "winks" "grins" "smiles" "laughs" "cheers" "claps"
|
|
112
|
-
"seeks" "finds" "spots" "tracks" "hunts" "chases" "catches" "grabs"
|
|
113
|
-
"pushes" "pulls" "lifts" "throws" "kicks" "punts" "bats" "swings"
|
|
114
|
-
"reads" "writes" "draws" "paints" "sculpts" "carves" "molds" "weaves"
|
|
115
|
-
"cooks" "bakes" "grills" "fries"
|
|
116
|
-
)
|
|
75
|
+
declare -a ADJECTIVES
|
|
76
|
+
declare -a NOUNS
|
|
77
|
+
declare -a VERBS
|
|
78
|
+
SESSION_WORDS_LOADED=false
|
|
117
79
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
local uuid="$1"
|
|
121
|
-
local hex="${uuid//-/}"
|
|
122
|
-
hex="${hex:0:12}"
|
|
80
|
+
load_session_words() {
|
|
81
|
+
if [ "$SESSION_WORDS_LOADED" = "true" ]; then return 0; fi
|
|
123
82
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return
|
|
127
|
-
fi
|
|
83
|
+
local words_file="$SESSION_WORDS_JSON"
|
|
84
|
+
[ ! -f "$words_file" ] && words_file="$SESSION_WORDS_DEFAULT"
|
|
128
85
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
86
|
+
if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
|
|
87
|
+
ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
132
90
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
91
|
+
if command -v node &>/dev/null; then
|
|
92
|
+
if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
|
|
93
|
+
readarray -t ADJECTIVES < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
|
|
94
|
+
readarray -t NOUNS < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
|
|
95
|
+
readarray -t VERBS < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
|
|
96
|
+
else
|
|
97
|
+
local i=0
|
|
98
|
+
while IFS= read -r line; do ADJECTIVES[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
|
|
99
|
+
i=0
|
|
100
|
+
while IFS= read -r line; do NOUNS[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
|
|
101
|
+
i=0
|
|
102
|
+
while IFS= read -r line; do VERBS[i]="$line"; ((i++)); done < <(node "$JSON_PARSE_HELPER" "$words_file" '.verbs' 2>/dev/null)
|
|
103
|
+
fi
|
|
104
|
+
[ ${#ADJECTIVES[@]} -eq 0 ] && ADJECTIVES=("unknown")
|
|
105
|
+
[ ${#NOUNS[@]} -eq 0 ] && NOUNS=("session")
|
|
106
|
+
[ ${#VERBS[@]} -eq 0 ] && VERBS=("starts")
|
|
107
|
+
SESSION_WORDS_LOADED=true
|
|
108
|
+
return 0
|
|
109
|
+
fi
|
|
110
|
+
ADJECTIVES=("unknown"); NOUNS=("session"); VERBS=("starts")
|
|
111
|
+
return 1
|
|
112
|
+
}
|
|
136
113
|
|
|
137
|
-
|
|
114
|
+
uuid_to_words() {
|
|
115
|
+
local uuid="$1"
|
|
116
|
+
load_session_words
|
|
117
|
+
|
|
118
|
+
local hex="${uuid//-/}"
|
|
119
|
+
hex="${hex:0:12}"
|
|
120
|
+
[[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]] && echo "unknown-session-starts" && return
|
|
121
|
+
|
|
122
|
+
local adj_seed=$((16#${hex:0:4}))
|
|
123
|
+
local noun_seed=$((16#${hex:4:4}))
|
|
124
|
+
local verb_seed=$((16#${hex:8:4}))
|
|
125
|
+
local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
|
|
126
|
+
local noun_idx=$((noun_seed % ${#NOUNS[@]}))
|
|
127
|
+
local verb_idx=$((verb_seed % ${#VERBS[@]}))
|
|
128
|
+
echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
|
|
138
129
|
}
|
|
139
130
|
|
|
140
|
-
|
|
141
|
-
SESSION_NAME
|
|
142
|
-
|
|
143
|
-
|
|
131
|
+
if [ "$IS_UUID" = true ]; then
|
|
132
|
+
SESSION_NAME=$(uuid_to_words "$SESSION_ID")
|
|
133
|
+
else
|
|
134
|
+
SESSION_NAME="$SESSION_ID"
|
|
144
135
|
fi
|
|
145
136
|
|
|
146
137
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
147
|
-
#
|
|
138
|
+
# Turn Number - read from turn file written by user-prompt-submit.sh
|
|
148
139
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
149
|
-
|
|
150
|
-
|
|
140
|
+
TURN_FILE="$STATE_DIR/sessions/${SESSION_ID}.turn"
|
|
141
|
+
mkdir -p "$STATE_DIR/sessions" 2>/dev/null
|
|
151
142
|
TURN_NUMBER=1
|
|
152
|
-
[ -f "$
|
|
143
|
+
[ -f "$TURN_FILE" ] && TURN_NUMBER=$(cat "$TURN_FILE" 2>/dev/null || echo "1")
|
|
153
144
|
|
|
154
145
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
155
|
-
#
|
|
156
|
-
# If context >= 92%, write flag for ekkos run wrapper immediately
|
|
146
|
+
# Load auth - No jq
|
|
157
147
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
158
|
-
|
|
159
|
-
|
|
148
|
+
EKKOS_CONFIG="$HOME/.ekkos/config.json"
|
|
149
|
+
AUTH_TOKEN=""
|
|
150
|
+
USER_ID=""
|
|
151
|
+
|
|
152
|
+
if [ -f "$EKKOS_CONFIG" ] && [ -f "$JSON_PARSE_HELPER" ]; then
|
|
153
|
+
AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.hookApiKey' 2>/dev/null || echo "")
|
|
154
|
+
[ -z "$AUTH_TOKEN" ] && AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.apiKey' 2>/dev/null || echo "")
|
|
155
|
+
USER_ID=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.userId' 2>/dev/null || echo "")
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
if [ -z "$AUTH_TOKEN" ] && [ -f "$PROJECT_ROOT/.env.local" ]; then
|
|
159
|
+
AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | tr -d '\r')
|
|
160
|
+
fi
|
|
160
161
|
|
|
161
|
-
|
|
162
|
-
if stat -f%z "$TRANSCRIPT_PATH" >/dev/null 2>&1; then
|
|
163
|
-
FILE_SIZE=$(stat -f%z "$TRANSCRIPT_PATH")
|
|
164
|
-
else
|
|
165
|
-
FILE_SIZE=$(stat -c%s "$TRANSCRIPT_PATH" 2>/dev/null || echo "0")
|
|
166
|
-
fi
|
|
167
|
-
ROUGH_TOKENS=$((FILE_SIZE / 4))
|
|
168
|
-
TOKEN_PERCENT=$((ROUGH_TOKENS * 100 / MAX_TOKENS))
|
|
162
|
+
[ -z "$AUTH_TOKEN" ] && exit 0
|
|
169
163
|
|
|
170
|
-
|
|
171
|
-
if [ "$TOKEN_PERCENT" -gt 50 ]; then
|
|
172
|
-
WORD_COUNT=$(wc -w < "$TRANSCRIPT_PATH" 2>/dev/null | tr -d ' ' || echo "0")
|
|
173
|
-
TOKEN_PERCENT=$((WORD_COUNT * 13 / 10 * 100 / MAX_TOKENS))
|
|
174
|
-
fi
|
|
164
|
+
MEMORY_API_URL="https://mcp.ekkos.dev"
|
|
175
165
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
166
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
167
|
+
# SESSION BINDING: Bridge _pending → real session name for proxy eviction
|
|
168
|
+
# The CLI may be in spawn pass-through mode (no PTY = blind to TUI output),
|
|
169
|
+
# so the stop hook (which IS sighted) must bind the session.
|
|
170
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
171
|
+
if [ -n "$SESSION_NAME" ] && [ "$SESSION_NAME" != "unknown-session-starts" ] && [ -n "$USER_ID" ]; then
|
|
172
|
+
PROJECT_PATH_FOR_BIND=$(pwd)
|
|
173
|
+
PENDING_SESSION_FOR_BIND="${EKKOS_PENDING_SESSION:-_pending}"
|
|
174
|
+
curl -s -X POST "$MEMORY_API_URL/proxy/session/bind" \
|
|
175
|
+
-H "Content-Type: application/json" \
|
|
176
|
+
-d "{\"userId\":\"$USER_ID\",\"realSession\":\"$SESSION_NAME\",\"projectPath\":\"$PROJECT_PATH_FOR_BIND\",\"pendingSession\":\"$PENDING_SESSION_FOR_BIND\"}" \
|
|
177
|
+
--connect-timeout 1 \
|
|
178
|
+
--max-time 2 >/dev/null 2>&1 &
|
|
183
179
|
fi
|
|
184
180
|
|
|
185
181
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
186
|
-
#
|
|
182
|
+
# EVICTION: Handled by IPC (In-Place Progressive Compression) in the proxy.
|
|
183
|
+
# No hook-side eviction needed — passthrough is default for cache stability.
|
|
187
184
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
188
|
-
IS_INTERRUPTED=$(echo "$INPUT" | jq -r '.interrupted // false' 2>/dev/null || echo "false")
|
|
189
|
-
STOP_REASON=$(echo "$INPUT" | jq -r '.stop_reason // ""' 2>/dev/null || echo "")
|
|
190
185
|
|
|
191
|
-
#
|
|
186
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
187
|
+
# Check for interruption - No jq
|
|
188
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
189
|
+
IS_INTERRUPTED=$(parse_json_value "$INPUT" '.interrupted')
|
|
190
|
+
[ -z "$IS_INTERRUPTED" ] && IS_INTERRUPTED="false"
|
|
191
|
+
STOP_REASON=$(parse_json_value "$INPUT" '.stop_reason')
|
|
192
|
+
|
|
192
193
|
if [ "$IS_INTERRUPTED" = "true" ] || [ "$STOP_REASON" = "user_cancelled" ] || [ "$STOP_REASON" = "interrupted" ]; then
|
|
193
194
|
exit 0
|
|
194
195
|
fi
|
|
195
196
|
|
|
196
197
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
197
|
-
# Extract conversation from transcript
|
|
198
|
+
# Extract conversation from transcript using Node (no jq)
|
|
198
199
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
199
200
|
LAST_USER=""
|
|
200
201
|
LAST_ASSISTANT=""
|
|
201
202
|
FILE_CHANGES="[]"
|
|
202
203
|
|
|
203
204
|
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
echo "[ekkOS] Turn $TURN_NUMBER: LAST_USER empty, will try to get assistant response anyway (session: $SESSION_NAME)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
|
|
223
|
-
echo "[ekkOS DEBUG] Transcript line count: $(wc -l < "$TRANSCRIPT_PATH" 2>/dev/null || echo 0)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
|
|
224
|
-
# Don't exit - continue to extract assistant response for local cache
|
|
225
|
-
fi
|
|
226
|
-
if [[ "$LAST_USER" == *"[Request interrupted"* ]] || \
|
|
227
|
-
[[ "$LAST_USER" == *"interrupted by user"* ]]; then
|
|
228
|
-
echo "[ekkOS] Turn $TURN_NUMBER skipped: interruption marker (session: $SESSION_NAME)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
|
|
229
|
-
exit 0
|
|
230
|
-
fi
|
|
231
|
-
|
|
232
|
-
# Get timestamp of last valid user message (handles both string and array content)
|
|
233
|
-
LAST_USER_TIME=$(cat "$TRANSCRIPT_PATH" | jq -r '
|
|
234
|
-
select(.type == "user")
|
|
235
|
-
| select(
|
|
236
|
-
(.message.content | type == "string" and (startswith("<") | not)) or
|
|
237
|
-
(.message.content | type == "array" and any(.[]; .type == "text" and (.text | startswith("<") | not)))
|
|
238
|
-
)
|
|
239
|
-
| .timestamp
|
|
240
|
-
' 2>/dev/null | tail -1 || echo "")
|
|
241
|
-
|
|
242
|
-
if [ -n "$LAST_USER_TIME" ]; then
|
|
243
|
-
# Get assistant response after user message - FULL CONTENT including tool calls
|
|
244
|
-
# Captures: text blocks, tool_use (with name + input), and extended_thinking
|
|
245
|
-
LAST_ASSISTANT=$(cat "$TRANSCRIPT_PATH" | jq -rs --arg time "$LAST_USER_TIME" '
|
|
246
|
-
[.[] | select(.type == "assistant" and .timestamp > $time)] | last |
|
|
247
|
-
.message.content |
|
|
248
|
-
if type == "string" then .
|
|
249
|
-
elif type == "array" then
|
|
250
|
-
[.[] |
|
|
251
|
-
if .type == "text" then .text
|
|
252
|
-
elif .type == "tool_use" then
|
|
253
|
-
"\n[TOOL: " + .name + "]\n" +
|
|
254
|
-
(if .name == "Bash" then "$ " + (.input.command // "") + "\n"
|
|
255
|
-
elif .name == "Read" then "Reading: " + (.input.file_path // "") + "\n"
|
|
256
|
-
elif .name == "Write" then "Writing: " + (.input.file_path // "") + "\n"
|
|
257
|
-
elif .name == "Edit" then "Editing: " + (.input.file_path // "") + "\n"
|
|
258
|
-
elif .name == "Grep" then "Searching: " + (.input.pattern // "") + "\n"
|
|
259
|
-
elif .name == "Glob" then "Finding: " + (.input.pattern // "") + "\n"
|
|
260
|
-
elif .name == "WebFetch" then "Fetching: " + (.input.url // "") + "\n"
|
|
261
|
-
elif .name == "Task" then "Agent: " + (.input.subagent_type // "") + " - " + (.input.description // "") + "\n"
|
|
262
|
-
else (.input | tostring | .[0:500]) + "\n"
|
|
263
|
-
end)
|
|
264
|
-
elif .type == "thinking" then "\n[THINKING]\n" + (.thinking // .text // "") + "\n[/THINKING]\n"
|
|
265
|
-
else empty
|
|
266
|
-
end
|
|
267
|
-
] | join("")
|
|
268
|
-
else empty end
|
|
269
|
-
' 2>/dev/null || echo "")
|
|
270
|
-
|
|
271
|
-
# Also capture tool_results that follow this assistant message
|
|
272
|
-
TOOL_RESULTS=$(cat "$TRANSCRIPT_PATH" | jq -rs --arg time "$LAST_USER_TIME" '
|
|
273
|
-
[.[] | select(.timestamp > $time)] |
|
|
274
|
-
# Get tool results between last assistant and next user message
|
|
275
|
-
[.[] | select(.type == "tool_result" or (.type == "user" and (.message.content | type == "array") and (.message.content | any(.type == "tool_result"))))] |
|
|
276
|
-
.[0:10] | # Limit to first 10 tool results
|
|
277
|
-
[.[] |
|
|
278
|
-
if .type == "tool_result" then
|
|
279
|
-
"\n[RESULT: " + (.tool_use_id // "unknown")[0:8] + "]\n" +
|
|
280
|
-
(if (.content | type == "string") then (.content | .[0:2000])
|
|
281
|
-
elif (.content | type == "array") then ([.content[] | select(.type == "text") | .text] | join("\n") | .[0:2000])
|
|
282
|
-
else ""
|
|
283
|
-
end) + "\n"
|
|
284
|
-
elif .type == "user" then
|
|
285
|
-
([.message.content[] | select(.type == "tool_result") |
|
|
286
|
-
"\n[RESULT: " + (.tool_use_id // "unknown")[0:8] + "]\n" +
|
|
287
|
-
(if (.content | type == "string") then (.content | .[0:2000])
|
|
288
|
-
elif (.content | type == "array") then ([.content[] | select(.type == "text") | .text] | join("\n") | .[0:2000])
|
|
289
|
-
else ""
|
|
290
|
-
end) + "\n"
|
|
291
|
-
] | join(""))
|
|
292
|
-
else ""
|
|
293
|
-
end
|
|
294
|
-
] | join("")
|
|
295
|
-
' 2>/dev/null || echo "")
|
|
205
|
+
EXTRACTION=$(node -e "
|
|
206
|
+
const fs = require('fs');
|
|
207
|
+
const lines = fs.readFileSync('$TRANSCRIPT_PATH', 'utf8').split('\n').filter(Boolean);
|
|
208
|
+
const entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
|
|
209
|
+
|
|
210
|
+
let lastUser = '', lastUserTime = '';
|
|
211
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
212
|
+
const e = entries[i];
|
|
213
|
+
if (e.type === 'user') {
|
|
214
|
+
const content = e.message?.content;
|
|
215
|
+
if (typeof content === 'string' && !content.startsWith('<')) {
|
|
216
|
+
lastUser = content; lastUserTime = e.timestamp || ''; break;
|
|
217
|
+
} else if (Array.isArray(content)) {
|
|
218
|
+
const textPart = content.find(c => c.type === 'text' && !c.text?.startsWith('<'));
|
|
219
|
+
if (textPart) { lastUser = textPart.text; lastUserTime = e.timestamp || ''; break; }
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
296
223
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
224
|
+
let lastAssistant = '';
|
|
225
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
226
|
+
const e = entries[i];
|
|
227
|
+
if (e.type === 'assistant' && (!lastUserTime || e.timestamp >= lastUserTime)) {
|
|
228
|
+
const content = e.message?.content;
|
|
229
|
+
if (typeof content === 'string') { lastAssistant = content; break; }
|
|
230
|
+
else if (Array.isArray(content)) {
|
|
231
|
+
const parts = content.map(c => {
|
|
232
|
+
if (c.type === 'text') return c.text;
|
|
233
|
+
if (c.type === 'tool_use') return '[TOOL: ' + c.name + ']';
|
|
234
|
+
if (c.type === 'thinking') return '[THINKING]' + (c.thinking || c.text || '') + '[/THINKING]';
|
|
235
|
+
return '';
|
|
236
|
+
}).filter(Boolean);
|
|
237
|
+
lastAssistant = parts.join('\n'); break;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
302
241
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
else empty end
|
|
325
|
-
' 2>/dev/null || echo "")
|
|
326
|
-
fi
|
|
242
|
+
const fileChanges = [];
|
|
243
|
+
entries.filter(e => e.type === 'assistant').forEach(e => {
|
|
244
|
+
const content = e.message?.content;
|
|
245
|
+
if (Array.isArray(content)) {
|
|
246
|
+
content.filter(c => c.type === 'tool_use' && ['Edit', 'Write', 'Read'].includes(c.name)).forEach(c => {
|
|
247
|
+
fileChanges.push({tool: c.name, path: c.input?.file_path || c.input?.path, action: c.name.toLowerCase()});
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
console.log(JSON.stringify({
|
|
253
|
+
user: lastUser,
|
|
254
|
+
assistant: lastAssistant.substring(0, 50000),
|
|
255
|
+
fileChanges: fileChanges.slice(0, 20)
|
|
256
|
+
}));
|
|
257
|
+
" 2>/dev/null || echo '{"user":"","assistant":"","fileChanges":[]}')
|
|
258
|
+
|
|
259
|
+
LAST_USER=$(echo "$EXTRACTION" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(d.user||'')" 2>/dev/null || echo "")
|
|
260
|
+
LAST_ASSISTANT=$(echo "$EXTRACTION" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(d.assistant||'')" 2>/dev/null || echo "")
|
|
261
|
+
FILE_CHANGES=$(echo "$EXTRACTION" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8'));console.log(JSON.stringify(d.fileChanges||[]))" 2>/dev/null || echo "[]")
|
|
262
|
+
fi
|
|
327
263
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
FILE_CHANGES=$(cat "$TRANSCRIPT_PATH" | jq -s '
|
|
331
|
-
[.[] | select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") |
|
|
332
|
-
select(.name == "Edit" or .name == "Write" or .name == "Read") |
|
|
333
|
-
{
|
|
334
|
-
tool: .name,
|
|
335
|
-
path: (.input.file_path // .input.path),
|
|
336
|
-
action: (if .name == "Edit" then "edit" elif .name == "Write" then "write" else "read" end),
|
|
337
|
-
# Full edit details for context restoration
|
|
338
|
-
old_string: (if .name == "Edit" then (.input.old_string // null) else null end),
|
|
339
|
-
new_string: (if .name == "Edit" then (.input.new_string // null) else null end),
|
|
340
|
-
# Write content (truncated to 2000 chars to avoid massive payloads)
|
|
341
|
-
content: (if .name == "Write" then (.input.content[:2000] // null) else null end),
|
|
342
|
-
replace_all: (if .name == "Edit" then (.input.replace_all // false) else null end)
|
|
343
|
-
}
|
|
344
|
-
] | map(select(.path != null))
|
|
345
|
-
' 2>/dev/null || echo "[]")
|
|
264
|
+
if [ -z "$LAST_USER" ] || [[ "$LAST_USER" == *"[Request interrupted"* ]]; then
|
|
265
|
+
exit 0
|
|
346
266
|
fi
|
|
347
267
|
|
|
348
268
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
349
|
-
# Capture to
|
|
350
|
-
# Background was causing missed captures when Claude Code exits fast
|
|
269
|
+
# Capture to BOTH Working Sessions (Redis) AND Episodic (Supabase) - No jq
|
|
351
270
|
# ═══════════════════════════════════════════════════════════════════════════
|
|
352
|
-
if [ -z "$LAST_ASSISTANT" ]; then
|
|
353
|
-
echo "[ekkOS] Turn $TURN_NUMBER skipped: LAST_ASSISTANT empty (session: $SESSION_NAME)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
|
|
354
|
-
fi
|
|
355
|
-
|
|
356
271
|
if [ -n "$LAST_USER" ] && [ -n "$LAST_ASSISTANT" ]; then
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
}
|
|
381
|
-
|
|
272
|
+
(
|
|
273
|
+
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
274
|
+
|
|
275
|
+
TOOLS_USED=$(echo "$FILE_CHANGES" | node -e "
|
|
276
|
+
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8') || '[]');
|
|
277
|
+
console.log(JSON.stringify([...new Set(d.map(f => f.tool).filter(Boolean))]));
|
|
278
|
+
" 2>/dev/null || echo "[]")
|
|
279
|
+
|
|
280
|
+
FILES_REF=$(echo "$FILE_CHANGES" | node -e "
|
|
281
|
+
const d = JSON.parse(require('fs').readFileSync('/dev/stdin','utf8') || '[]');
|
|
282
|
+
console.log(JSON.stringify([...new Set(d.map(f => f.path).filter(Boolean))]));
|
|
283
|
+
" 2>/dev/null || echo "[]")
|
|
284
|
+
|
|
285
|
+
# Token breakdown from tokenizer script
|
|
286
|
+
TOTAL_TOKENS=0
|
|
287
|
+
INPUT_TOKENS=0
|
|
288
|
+
CACHE_READ_TOKENS=0
|
|
289
|
+
CACHE_CREATION_TOKENS=0
|
|
290
|
+
TOKENIZER_SCRIPT="$SCRIPT_DIR/lib/count-tokens.cjs"
|
|
291
|
+
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ] && [ -f "$TOKENIZER_SCRIPT" ]; then
|
|
292
|
+
TOKEN_JSON=$(node "$TOKENIZER_SCRIPT" "$TRANSCRIPT_PATH" --json 2>/dev/null || echo '{}')
|
|
293
|
+
TOTAL_TOKENS=$(echo "$TOKEN_JSON" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.total_tokens||0)" 2>/dev/null || echo "0")
|
|
294
|
+
INPUT_TOKENS=$(echo "$TOKEN_JSON" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.input_tokens||0)" 2>/dev/null || echo "0")
|
|
295
|
+
CACHE_READ_TOKENS=$(echo "$TOKEN_JSON" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.cache_read_tokens||0)" 2>/dev/null || echo "0")
|
|
296
|
+
CACHE_CREATION_TOKENS=$(echo "$TOKEN_JSON" | node -e "const d=JSON.parse(require('fs').readFileSync('/dev/stdin','utf8')||'{}');console.log(d.cache_creation_tokens||0)" 2>/dev/null || echo "0")
|
|
297
|
+
[[ ! "$TOTAL_TOKENS" =~ ^[0-9]+$ ]] && TOTAL_TOKENS=0
|
|
298
|
+
fi
|
|
382
299
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
300
|
+
# 1. WORKING SESSIONS (Redis) - No jq payload building
|
|
301
|
+
WORKING_PAYLOAD=$(node -e "
|
|
302
|
+
console.log(JSON.stringify({
|
|
303
|
+
session_name: process.argv[1],
|
|
304
|
+
turn_number: parseInt(process.argv[2]),
|
|
305
|
+
user_query: process.argv[3],
|
|
306
|
+
agent_response: process.argv[4].substring(0, 50000),
|
|
307
|
+
model: process.argv[5],
|
|
308
|
+
tools_used: JSON.parse(process.argv[6] || '[]'),
|
|
309
|
+
files_referenced: JSON.parse(process.argv[7] || '[]'),
|
|
310
|
+
total_context_tokens: parseInt(process.argv[8]) || 0,
|
|
311
|
+
token_breakdown: {
|
|
312
|
+
input_tokens: parseInt(process.argv[9]) || 0,
|
|
313
|
+
cache_read_tokens: parseInt(process.argv[10]) || 0,
|
|
314
|
+
cache_creation_tokens: parseInt(process.argv[11]) || 0
|
|
315
|
+
}
|
|
316
|
+
}));
|
|
317
|
+
" "$SESSION_NAME" "$TURN_NUMBER" "$LAST_USER" "$LAST_ASSISTANT" "$MODEL_USED" "$TOOLS_USED" "$FILES_REF" "$TOTAL_TOKENS" "$INPUT_TOKENS" "$CACHE_READ_TOKENS" "$CACHE_CREATION_TOKENS" 2>/dev/null)
|
|
318
|
+
|
|
319
|
+
if [ -n "$WORKING_PAYLOAD" ]; then
|
|
320
|
+
curl -s -X POST "$MEMORY_API_URL/api/v1/working/turn" \
|
|
387
321
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
388
322
|
-H "Content-Type: application/json" \
|
|
389
|
-
-d "
|
|
323
|
+
-d "$WORKING_PAYLOAD" \
|
|
390
324
|
--connect-timeout 3 \
|
|
391
|
-
--max-time 5
|
|
392
|
-
|
|
393
|
-
HTTP_CODE=$(echo "$CAPTURE_RESULT" | tail -1)
|
|
394
|
-
|
|
395
|
-
if [ "$HTTP_CODE" = "200" ] || [ "$HTTP_CODE" = "201" ]; then
|
|
396
|
-
break
|
|
397
|
-
fi
|
|
398
|
-
[ $RETRY -lt 3 ] && sleep 0.5
|
|
399
|
-
done
|
|
400
|
-
|
|
401
|
-
if [ "$HTTP_CODE" != "200" ] && [ "$HTTP_CODE" != "201" ]; then
|
|
402
|
-
echo "[ekkOS] L2 capture failed after 3 attempts: HTTP $HTTP_CODE" >&2
|
|
403
|
-
mkdir -p "$HOME/.ekkos/wal" 2>/dev/null
|
|
404
|
-
cp "$PAYLOAD_FILE" "$HOME/.ekkos/wal/l2-$(date +%s)-$$.json" 2>/dev/null
|
|
405
|
-
fi
|
|
406
|
-
fi
|
|
407
|
-
|
|
408
|
-
rm -f "$PAYLOAD_FILE" 2>/dev/null
|
|
409
|
-
fi
|
|
410
|
-
|
|
411
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
412
|
-
# REDIS WORKING MEMORY: Store verbatim turn in multi-session hot cache
|
|
413
|
-
# 5 sessions × 20 turns = 100 turns total for instant context restoration
|
|
414
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
415
|
-
if [ -n "$LAST_USER" ] && [ -n "$LAST_ASSISTANT" ] && [ -n "$SESSION_NAME" ]; then
|
|
416
|
-
REDIS_PAYLOAD_FILE=$(mktemp /tmp/ekkos-redis.XXXXXX.json)
|
|
417
|
-
|
|
418
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
419
|
-
# SECRET SCRUBBING: Detect and store secrets, replace with references
|
|
420
|
-
# Patterns: API keys, tokens, passwords → stored in L11 Secrets vault
|
|
421
|
-
# Source: GitHub secret scanning patterns + community lists
|
|
422
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
423
|
-
store_secret() {
|
|
424
|
-
local service="$1"
|
|
425
|
-
local secret="$2"
|
|
426
|
-
local type="$3"
|
|
427
|
-
curl -s -X POST "$MEMORY_API_URL/api/v1/secrets" \
|
|
428
|
-
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
429
|
-
-H "Content-Type: application/json" \
|
|
430
|
-
-d "{\"service\":\"$service\",\"value\":\"$secret\",\"type\":\"$type\"}" \
|
|
431
|
-
--connect-timeout 1 --max-time 2 >/dev/null 2>&1 &
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
scrub_secrets() {
|
|
435
|
-
local text="$1"
|
|
436
|
-
local scrubbed="$text"
|
|
437
|
-
|
|
438
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
439
|
-
# OpenAI (sk-..., sk-proj-...)
|
|
440
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
441
|
-
while [[ "$scrubbed" =~ (sk-proj-[a-zA-Z0-9_-]{20,}) ]]; do
|
|
442
|
-
local secret="${BASH_REMATCH[1]}"
|
|
443
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
444
|
-
store_secret "openai_proj_$hash" "$secret" "api_key"
|
|
445
|
-
scrubbed="${scrubbed//$secret/[SECRET:openai_proj_$hash:api_key]}"
|
|
446
|
-
done
|
|
447
|
-
while [[ "$scrubbed" =~ (sk-[a-zA-Z0-9]{20,}) ]]; do
|
|
448
|
-
local secret="${BASH_REMATCH[1]}"
|
|
449
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
450
|
-
store_secret "openai_$hash" "$secret" "api_key"
|
|
451
|
-
scrubbed="${scrubbed//$secret/[SECRET:openai_$hash:api_key]}"
|
|
452
|
-
done
|
|
453
|
-
|
|
454
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
455
|
-
# Anthropic (sk-ant-...)
|
|
456
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
457
|
-
while [[ "$scrubbed" =~ (sk-ant-[a-zA-Z0-9_-]{20,}) ]]; do
|
|
458
|
-
local secret="${BASH_REMATCH[1]}"
|
|
459
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
460
|
-
store_secret "anthropic_$hash" "$secret" "api_key"
|
|
461
|
-
scrubbed="${scrubbed//$secret/[SECRET:anthropic_$hash:api_key]}"
|
|
462
|
-
done
|
|
463
|
-
|
|
464
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
465
|
-
# GitHub (ghp_, gho_, ghu_, ghs_, ghr_)
|
|
466
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
467
|
-
while [[ "$scrubbed" =~ (ghp_[a-zA-Z0-9]{36}) ]]; do
|
|
468
|
-
local secret="${BASH_REMATCH[1]}"
|
|
469
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
470
|
-
store_secret "github_pat_$hash" "$secret" "token"
|
|
471
|
-
scrubbed="${scrubbed//$secret/[SECRET:github_pat_$hash:token]}"
|
|
472
|
-
done
|
|
473
|
-
while [[ "$scrubbed" =~ (gho_[a-zA-Z0-9]{36}) ]]; do
|
|
474
|
-
local secret="${BASH_REMATCH[1]}"
|
|
475
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
476
|
-
store_secret "github_oauth_$hash" "$secret" "token"
|
|
477
|
-
scrubbed="${scrubbed//$secret/[SECRET:github_oauth_$hash:token]}"
|
|
478
|
-
done
|
|
479
|
-
while [[ "$scrubbed" =~ (ghu_[a-zA-Z0-9]{36}) ]]; do
|
|
480
|
-
local secret="${BASH_REMATCH[1]}"
|
|
481
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
482
|
-
store_secret "github_user_$hash" "$secret" "token"
|
|
483
|
-
scrubbed="${scrubbed//$secret/[SECRET:github_user_$hash:token]}"
|
|
484
|
-
done
|
|
485
|
-
while [[ "$scrubbed" =~ (ghs_[a-zA-Z0-9]{36}) ]]; do
|
|
486
|
-
local secret="${BASH_REMATCH[1]}"
|
|
487
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
488
|
-
store_secret "github_app_$hash" "$secret" "token"
|
|
489
|
-
scrubbed="${scrubbed//$secret/[SECRET:github_app_$hash:token]}"
|
|
490
|
-
done
|
|
491
|
-
while [[ "$scrubbed" =~ (ghr_[a-zA-Z0-9]{36}) ]]; do
|
|
492
|
-
local secret="${BASH_REMATCH[1]}"
|
|
493
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
494
|
-
store_secret "github_refresh_$hash" "$secret" "token"
|
|
495
|
-
scrubbed="${scrubbed//$secret/[SECRET:github_refresh_$hash:token]}"
|
|
496
|
-
done
|
|
497
|
-
|
|
498
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
499
|
-
# GitLab (glpat-...)
|
|
500
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
501
|
-
while [[ "$scrubbed" =~ (glpat-[a-zA-Z0-9_-]{20,}) ]]; do
|
|
502
|
-
local secret="${BASH_REMATCH[1]}"
|
|
503
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
504
|
-
store_secret "gitlab_$hash" "$secret" "token"
|
|
505
|
-
scrubbed="${scrubbed//$secret/[SECRET:gitlab_$hash:token]}"
|
|
506
|
-
done
|
|
507
|
-
|
|
508
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
509
|
-
# AWS (AKIA...)
|
|
510
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
511
|
-
while [[ "$scrubbed" =~ (AKIA[A-Z0-9]{16}) ]]; do
|
|
512
|
-
local secret="${BASH_REMATCH[1]}"
|
|
513
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
514
|
-
store_secret "aws_$hash" "$secret" "api_key"
|
|
515
|
-
scrubbed="${scrubbed//$secret/[SECRET:aws_$hash:api_key]}"
|
|
516
|
-
done
|
|
517
|
-
|
|
518
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
519
|
-
# Stripe (sk_live_, sk_test_, pk_live_, pk_test_)
|
|
520
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
521
|
-
while [[ "$scrubbed" =~ (sk_live_[a-zA-Z0-9]{24,}) ]]; do
|
|
522
|
-
local secret="${BASH_REMATCH[1]}"
|
|
523
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
524
|
-
store_secret "stripe_live_$hash" "$secret" "api_key"
|
|
525
|
-
scrubbed="${scrubbed//$secret/[SECRET:stripe_live_$hash:api_key]}"
|
|
526
|
-
done
|
|
527
|
-
while [[ "$scrubbed" =~ (sk_test_[a-zA-Z0-9]{24,}) ]]; do
|
|
528
|
-
local secret="${BASH_REMATCH[1]}"
|
|
529
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
530
|
-
store_secret "stripe_test_$hash" "$secret" "api_key"
|
|
531
|
-
scrubbed="${scrubbed//$secret/[SECRET:stripe_test_$hash:api_key]}"
|
|
532
|
-
done
|
|
533
|
-
while [[ "$scrubbed" =~ (rk_live_[a-zA-Z0-9]{24,}) ]]; do
|
|
534
|
-
local secret="${BASH_REMATCH[1]}"
|
|
535
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
536
|
-
store_secret "stripe_restricted_$hash" "$secret" "api_key"
|
|
537
|
-
scrubbed="${scrubbed//$secret/[SECRET:stripe_restricted_$hash:api_key]}"
|
|
538
|
-
done
|
|
539
|
-
|
|
540
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
541
|
-
# Slack (xoxb-, xoxp-, xoxa-, xoxs-)
|
|
542
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
543
|
-
while [[ "$scrubbed" =~ (xoxb-[0-9a-zA-Z-]{24,}) ]]; do
|
|
544
|
-
local secret="${BASH_REMATCH[1]}"
|
|
545
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
546
|
-
store_secret "slack_bot_$hash" "$secret" "token"
|
|
547
|
-
scrubbed="${scrubbed//$secret/[SECRET:slack_bot_$hash:token]}"
|
|
548
|
-
done
|
|
549
|
-
while [[ "$scrubbed" =~ (xoxp-[0-9a-zA-Z-]{24,}) ]]; do
|
|
550
|
-
local secret="${BASH_REMATCH[1]}"
|
|
551
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
552
|
-
store_secret "slack_user_$hash" "$secret" "token"
|
|
553
|
-
scrubbed="${scrubbed//$secret/[SECRET:slack_user_$hash:token]}"
|
|
554
|
-
done
|
|
555
|
-
|
|
556
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
557
|
-
# Google (AIza...)
|
|
558
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
559
|
-
while [[ "$scrubbed" =~ (AIza[0-9A-Za-z_-]{35}) ]]; do
|
|
560
|
-
local secret="${BASH_REMATCH[1]}"
|
|
561
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
562
|
-
store_secret "google_$hash" "$secret" "api_key"
|
|
563
|
-
scrubbed="${scrubbed//$secret/[SECRET:google_$hash:api_key]}"
|
|
564
|
-
done
|
|
565
|
-
|
|
566
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
567
|
-
# Twilio (SK...)
|
|
568
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
569
|
-
while [[ "$scrubbed" =~ (SK[0-9a-fA-F]{32}) ]]; do
|
|
570
|
-
local secret="${BASH_REMATCH[1]}"
|
|
571
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
572
|
-
store_secret "twilio_$hash" "$secret" "api_key"
|
|
573
|
-
scrubbed="${scrubbed//$secret/[SECRET:twilio_$hash:api_key]}"
|
|
574
|
-
done
|
|
575
|
-
|
|
576
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
577
|
-
# SendGrid (SG....)
|
|
578
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
579
|
-
while [[ "$scrubbed" =~ (SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}) ]]; do
|
|
580
|
-
local secret="${BASH_REMATCH[1]}"
|
|
581
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
582
|
-
store_secret "sendgrid_$hash" "$secret" "api_key"
|
|
583
|
-
scrubbed="${scrubbed//$secret/[SECRET:sendgrid_$hash:api_key]}"
|
|
584
|
-
done
|
|
585
|
-
|
|
586
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
587
|
-
# Mailgun (key-...)
|
|
588
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
589
|
-
while [[ "$scrubbed" =~ (key-[0-9a-zA-Z]{32}) ]]; do
|
|
590
|
-
local secret="${BASH_REMATCH[1]}"
|
|
591
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
592
|
-
store_secret "mailgun_$hash" "$secret" "api_key"
|
|
593
|
-
scrubbed="${scrubbed//$secret/[SECRET:mailgun_$hash:api_key]}"
|
|
594
|
-
done
|
|
595
|
-
|
|
596
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
597
|
-
# DigitalOcean (dop_v1_...)
|
|
598
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
599
|
-
while [[ "$scrubbed" =~ (dop_v1_[a-z0-9]{64}) ]]; do
|
|
600
|
-
local secret="${BASH_REMATCH[1]}"
|
|
601
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
602
|
-
store_secret "digitalocean_$hash" "$secret" "token"
|
|
603
|
-
scrubbed="${scrubbed//$secret/[SECRET:digitalocean_$hash:token]}"
|
|
604
|
-
done
|
|
605
|
-
|
|
606
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
607
|
-
# Shopify (shpat_...)
|
|
608
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
609
|
-
while [[ "$scrubbed" =~ (shpat_[0-9a-fA-F]{32}) ]]; do
|
|
610
|
-
local secret="${BASH_REMATCH[1]}"
|
|
611
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
612
|
-
store_secret "shopify_$hash" "$secret" "token"
|
|
613
|
-
scrubbed="${scrubbed//$secret/[SECRET:shopify_$hash:token]}"
|
|
614
|
-
done
|
|
615
|
-
|
|
616
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
617
|
-
# npm (npm_...)
|
|
618
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
619
|
-
while [[ "$scrubbed" =~ (npm_[a-zA-Z0-9]{36}) ]]; do
|
|
620
|
-
local secret="${BASH_REMATCH[1]}"
|
|
621
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
622
|
-
store_secret "npm_$hash" "$secret" "token"
|
|
623
|
-
scrubbed="${scrubbed//$secret/[SECRET:npm_$hash:token]}"
|
|
624
|
-
done
|
|
625
|
-
|
|
626
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
627
|
-
# PyPI (pypi-...)
|
|
628
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
629
|
-
while [[ "$scrubbed" =~ (pypi-[A-Za-z0-9_-]{50,}) ]]; do
|
|
630
|
-
local secret="${BASH_REMATCH[1]}"
|
|
631
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
632
|
-
store_secret "pypi_$hash" "$secret" "token"
|
|
633
|
-
scrubbed="${scrubbed//$secret/[SECRET:pypi_$hash:token]}"
|
|
634
|
-
done
|
|
635
|
-
|
|
636
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
637
|
-
# Supabase (sbp_...)
|
|
638
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
639
|
-
while [[ "$scrubbed" =~ (sbp_[a-zA-Z0-9]{40,}) ]]; do
|
|
640
|
-
local secret="${BASH_REMATCH[1]}"
|
|
641
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
642
|
-
store_secret "supabase_$hash" "$secret" "api_key"
|
|
643
|
-
scrubbed="${scrubbed//$secret/[SECRET:supabase_$hash:api_key]}"
|
|
644
|
-
done
|
|
645
|
-
|
|
646
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
647
|
-
# Discord Bot Token
|
|
648
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
649
|
-
while [[ "$scrubbed" =~ ([MN][A-Za-z0-9]{23,}\.[A-Za-z0-9_-]{6}\.[A-Za-z0-9_-]{27}) ]]; do
|
|
650
|
-
local secret="${BASH_REMATCH[1]}"
|
|
651
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
652
|
-
store_secret "discord_$hash" "$secret" "token"
|
|
653
|
-
scrubbed="${scrubbed//$secret/[SECRET:discord_$hash:token]}"
|
|
654
|
-
done
|
|
655
|
-
|
|
656
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
657
|
-
# Vercel (vercel_...)
|
|
658
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
659
|
-
while [[ "$scrubbed" =~ (vercel_[a-zA-Z0-9]{24,}) ]]; do
|
|
660
|
-
local secret="${BASH_REMATCH[1]}"
|
|
661
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
662
|
-
store_secret "vercel_$hash" "$secret" "token"
|
|
663
|
-
scrubbed="${scrubbed//$secret/[SECRET:vercel_$hash:token]}"
|
|
664
|
-
done
|
|
665
|
-
|
|
666
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
667
|
-
# Heroku (heroku_...)
|
|
668
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
669
|
-
while [[ "$scrubbed" =~ (heroku_[a-zA-Z0-9_-]{30,}) ]]; do
|
|
670
|
-
local secret="${BASH_REMATCH[1]}"
|
|
671
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
672
|
-
store_secret "heroku_$hash" "$secret" "api_key"
|
|
673
|
-
scrubbed="${scrubbed//$secret/[SECRET:heroku_$hash:api_key]}"
|
|
674
|
-
done
|
|
675
|
-
|
|
676
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
677
|
-
# Datadog (dd...)
|
|
678
|
-
# ─────────────────────────────────────────────────────────────────────────
|
|
679
|
-
while [[ "$scrubbed" =~ (ddapi_[a-zA-Z0-9]{32,}) ]]; do
|
|
680
|
-
local secret="${BASH_REMATCH[1]}"
|
|
681
|
-
local hash=$(echo -n "$secret" | md5 | cut -c1-8)
|
|
682
|
-
store_secret "datadog_$hash" "$secret" "api_key"
|
|
683
|
-
scrubbed="${scrubbed//$secret/[SECRET:datadog_$hash:api_key]}"
|
|
684
|
-
done
|
|
685
|
-
|
|
686
|
-
echo "$scrubbed"
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
# Scrub user query and assistant response
|
|
690
|
-
SCRUBBED_USER=$(scrub_secrets "$LAST_USER")
|
|
691
|
-
SCRUBBED_ASSISTANT=$(scrub_secrets "$LAST_ASSISTANT")
|
|
692
|
-
|
|
693
|
-
# Extract tools used from assistant response (simple grep for tool names)
|
|
694
|
-
TOOLS_USED=$(echo "$SCRUBBED_ASSISTANT" | grep -oE '\[TOOL: [^\]]+\]' | sed 's/\[TOOL: //g; s/\]//g' | sort -u | jq -R -s -c 'split("\n") | map(select(. != ""))')
|
|
695
|
-
[ -z "$TOOLS_USED" ] && TOOLS_USED="[]"
|
|
696
|
-
|
|
697
|
-
# Extract files referenced from file changes
|
|
698
|
-
FILES_REFERENCED=$(echo "$FILE_CHANGES" | jq -c '[.[].path] | unique // []' 2>/dev/null || echo "[]")
|
|
699
|
-
|
|
700
|
-
# Build edits array from file changes (write and edit actions only)
|
|
701
|
-
EDITS=$(echo "$FILE_CHANGES" | jq -c '[.[] | select(.action == "edit" or .action == "write") | {file_path: .path, action: .action, diff: (if .old_string then ("old: " + (.old_string | .[0:200]) + "\nnew: " + (.new_string | .[0:200])) else (.content | .[0:500]) end)}]' 2>/dev/null || echo "[]")
|
|
702
|
-
|
|
703
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
704
|
-
# ACCURATE TOKEN TRACKING: Extract REAL token counts from Anthropic API response
|
|
705
|
-
# This gives us exact context usage instead of rough estimation
|
|
706
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
707
|
-
TOTAL_CONTEXT_TOKENS=0
|
|
708
|
-
INPUT_TOKENS=0
|
|
709
|
-
OUTPUT_TOKENS=0
|
|
710
|
-
|
|
711
|
-
if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
|
|
712
|
-
# Get the last assistant message with usage data (macOS compatible)
|
|
713
|
-
# tac doesn't exist on macOS, use grep | tail instead
|
|
714
|
-
LAST_USAGE=$(grep '"usage"' "$TRANSCRIPT_PATH" 2>/dev/null | tail -1)
|
|
715
|
-
|
|
716
|
-
if [ -n "$LAST_USAGE" ]; then
|
|
717
|
-
# Extract token counts from Anthropic API usage object
|
|
718
|
-
INPUT_TOKENS=$(echo "$LAST_USAGE" | jq -r '
|
|
719
|
-
(.message.usage.input_tokens // 0) +
|
|
720
|
-
(.message.usage.cache_creation_input_tokens // 0) +
|
|
721
|
-
(.message.usage.cache_read_input_tokens // 0)
|
|
722
|
-
' 2>/dev/null || echo "0")
|
|
723
|
-
|
|
724
|
-
OUTPUT_TOKENS=$(echo "$LAST_USAGE" | jq -r '.message.usage.output_tokens // 0' 2>/dev/null || echo "0")
|
|
725
|
-
|
|
726
|
-
# Total context = input + output
|
|
727
|
-
TOTAL_CONTEXT_TOKENS=$((INPUT_TOKENS + OUTPUT_TOKENS))
|
|
325
|
+
--max-time 5 >/dev/null 2>&1 || true
|
|
728
326
|
fi
|
|
729
|
-
fi
|
|
730
|
-
|
|
731
|
-
jq -n \
|
|
732
|
-
--arg session_name "$SESSION_NAME" \
|
|
733
|
-
--argjson turn_number "$TURN_NUMBER" \
|
|
734
|
-
--arg user_query "$SCRUBBED_USER" \
|
|
735
|
-
--arg agent_response "$SCRUBBED_ASSISTANT" \
|
|
736
|
-
--arg model "$MODEL_USED" \
|
|
737
|
-
--argjson tools_used "$TOOLS_USED" \
|
|
738
|
-
--argjson files_referenced "$FILES_REFERENCED" \
|
|
739
|
-
--argjson edits "$EDITS" \
|
|
740
|
-
--argjson total_context_tokens "$TOTAL_CONTEXT_TOKENS" \
|
|
741
|
-
--argjson input_tokens "$INPUT_TOKENS" \
|
|
742
|
-
--argjson output_tokens "$OUTPUT_TOKENS" \
|
|
743
|
-
'{
|
|
744
|
-
session_name: $session_name,
|
|
745
|
-
turn_number: $turn_number,
|
|
746
|
-
user_query: $user_query,
|
|
747
|
-
agent_response: $agent_response,
|
|
748
|
-
model: $model,
|
|
749
|
-
tools_used: $tools_used,
|
|
750
|
-
files_referenced: $files_referenced,
|
|
751
|
-
edits: $edits,
|
|
752
|
-
patterns_used: [],
|
|
753
|
-
total_context_tokens: $total_context_tokens,
|
|
754
|
-
input_tokens: $input_tokens,
|
|
755
|
-
output_tokens: $output_tokens
|
|
756
|
-
}' > "$REDIS_PAYLOAD_FILE" 2>/dev/null
|
|
757
327
|
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
328
|
+
# 2. EPISODIC MEMORY (Supabase) - No jq payload building
|
|
329
|
+
EPISODIC_PAYLOAD=$(node -e "
|
|
330
|
+
console.log(JSON.stringify({
|
|
331
|
+
user_query: process.argv[1],
|
|
332
|
+
assistant_response: process.argv[2],
|
|
333
|
+
session_id: process.argv[3],
|
|
334
|
+
user_id: process.argv[4] || 'system',
|
|
335
|
+
file_changes: JSON.parse(process.argv[5] || '[]'),
|
|
336
|
+
metadata: {
|
|
337
|
+
source: 'claude-code',
|
|
338
|
+
model_used: process.argv[6],
|
|
339
|
+
captured_at: process.argv[7],
|
|
340
|
+
turn_number: parseInt(process.argv[8]) || 1,
|
|
341
|
+
session_name: process.argv[9],
|
|
342
|
+
minimal_hook: true
|
|
343
|
+
}
|
|
344
|
+
}));
|
|
345
|
+
" "$LAST_USER" "$LAST_ASSISTANT" "$SESSION_ID" "${USER_ID:-system}" "$FILE_CHANGES" "$MODEL_USED" "$TIMESTAMP" "$TURN_NUMBER" "$SESSION_NAME" 2>/dev/null)
|
|
763
346
|
|
|
764
|
-
|
|
765
|
-
|
|
347
|
+
if [ -n "$EPISODIC_PAYLOAD" ]; then
|
|
348
|
+
curl -s -X POST "$MEMORY_API_URL/api/v1/memory/capture" \
|
|
766
349
|
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
767
350
|
-H "Content-Type: application/json" \
|
|
768
|
-
-d "
|
|
351
|
+
-d "$EPISODIC_PAYLOAD" \
|
|
769
352
|
--connect-timeout 3 \
|
|
770
|
-
--max-time 5
|
|
771
|
-
|
|
772
|
-
REDIS_HTTP_CODE=$(echo "$REDIS_RESULT" | tail -1)
|
|
773
|
-
|
|
774
|
-
if [ "$REDIS_HTTP_CODE" = "200" ] || [ "$REDIS_HTTP_CODE" = "201" ]; then
|
|
775
|
-
REDIS_SUCCESS=true
|
|
776
|
-
else
|
|
777
|
-
RETRY=$((RETRY + 1))
|
|
778
|
-
[ $RETRY -lt $MAX_RETRIES ] && sleep 0.3
|
|
779
|
-
fi
|
|
780
|
-
done
|
|
781
|
-
|
|
782
|
-
# Log final failure with context
|
|
783
|
-
if [ "$REDIS_SUCCESS" = "false" ]; then
|
|
784
|
-
echo "[ekkOS] Redis capture failed after $MAX_RETRIES attempts: HTTP $REDIS_HTTP_CODE (session: $SESSION_NAME, turn: $TURN_NUMBER)" >&2
|
|
785
|
-
# Write-ahead log for recovery
|
|
786
|
-
WAL_DIR="$HOME/.ekkos/wal"
|
|
787
|
-
mkdir -p "$WAL_DIR" 2>/dev/null
|
|
788
|
-
cp "$REDIS_PAYLOAD_FILE" "$WAL_DIR/redis-$(date +%s)-$$.json" 2>/dev/null
|
|
789
|
-
else
|
|
790
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
791
|
-
# 🎯 ACK: Update local cache ACK cursor after successful Redis flush
|
|
792
|
-
# This enables safe pruning of turns that are backed up to Redis
|
|
793
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
794
|
-
if command -v ekkos-capture &>/dev/null && [ -n "$SESSION_ID" ]; then
|
|
795
|
-
(ekkos-capture ack "$SESSION_ID" "$TURN_NUMBER" >/dev/null 2>&1) &
|
|
796
|
-
fi
|
|
797
|
-
fi
|
|
798
|
-
fi
|
|
799
|
-
|
|
800
|
-
rm -f "$REDIS_PAYLOAD_FILE" 2>/dev/null
|
|
801
|
-
|
|
802
|
-
# ═════════════════════════════════════════════════════════════════════════
|
|
803
|
-
# ⚡ FAST CAPTURE: Structured context for instant /continue (parallel)
|
|
804
|
-
# Lightweight extraction - no LLM, pure parsing for ~1-2k token restoration
|
|
805
|
-
# ═════════════════════════════════════════════════════════════════════════
|
|
806
|
-
|
|
807
|
-
# Extract user intent patterns (no LLM needed)
|
|
808
|
-
USER_DECISION=""
|
|
809
|
-
USER_CORRECTION=""
|
|
810
|
-
USER_PREFERENCE=""
|
|
811
|
-
|
|
812
|
-
# Decision patterns: yes/no/ok/go ahead/use X instead
|
|
813
|
-
USER_DECISION=$(echo "$SCRUBBED_USER" | grep -oiE "^(yes|no|ok|do it|go ahead|approved|confirmed|use .{1,30} instead)" | head -1 || echo "")
|
|
814
|
-
|
|
815
|
-
# Correction patterns
|
|
816
|
-
USER_CORRECTION=$(echo "$SCRUBBED_USER" | grep -oiE "(actually|no,? I meant|not that|wrong|instead)" | head -1 || echo "")
|
|
817
|
-
|
|
818
|
-
# Preference patterns
|
|
819
|
-
USER_PREFERENCE=$(echo "$SCRUBBED_USER" | grep -oiE "(always|never|I prefer|don.t|avoid) .{1,50}" | head -1 || echo "")
|
|
820
|
-
|
|
821
|
-
# Extract errors from assistant response
|
|
822
|
-
ERRORS_FOUND=$(echo "$SCRUBBED_ASSISTANT" | grep -oiE "(error|failed|cannot|exception|not found).{0,80}" | head -3 | jq -R -s -c 'split("\n") | map(select(. != ""))' || echo "[]")
|
|
823
|
-
[ -z "$ERRORS_FOUND" ] && ERRORS_FOUND="[]"
|
|
824
|
-
|
|
825
|
-
# Get git status (fast, local only)
|
|
826
|
-
GIT_CHANGED=$(git diff --name-only 2>/dev/null | head -10 | jq -R -s -c 'split("\n") | map(select(. != ""))' || echo "[]")
|
|
827
|
-
GIT_STAT=$(git diff --stat 2>/dev/null | tail -1 | tr -d '\n' || echo "")
|
|
828
|
-
|
|
829
|
-
# Extract commands from Bash tool calls (first 50 chars each)
|
|
830
|
-
COMMANDS_RUN=$(echo "$SCRUBBED_ASSISTANT" | grep -oE '\$ [^\n]{1,50}' | head -5 | sed 's/^\$ //' | jq -R -s -c 'split("\n") | map(select(. != ""))' || echo "[]")
|
|
831
|
-
[ -z "$COMMANDS_RUN" ] && COMMANDS_RUN="[]"
|
|
832
|
-
|
|
833
|
-
# Build fast-capture payload
|
|
834
|
-
FAST_PAYLOAD=$(jq -n \
|
|
835
|
-
--arg session_name "$SESSION_NAME" \
|
|
836
|
-
--argjson turn_number "$TURN_NUMBER" \
|
|
837
|
-
--arg user_intent "${SCRUBBED_USER:0:200}" \
|
|
838
|
-
--arg user_decision "$USER_DECISION" \
|
|
839
|
-
--arg user_correction "$USER_CORRECTION" \
|
|
840
|
-
--arg user_preference "$USER_PREFERENCE" \
|
|
841
|
-
--argjson tools_used "$TOOLS_USED" \
|
|
842
|
-
--argjson files_modified "$FILES_REFERENCED" \
|
|
843
|
-
--argjson commands_run "$COMMANDS_RUN" \
|
|
844
|
-
--argjson errors "$ERRORS_FOUND" \
|
|
845
|
-
--argjson git_files_changed "$GIT_CHANGED" \
|
|
846
|
-
--arg git_diff_stat "$GIT_STAT" \
|
|
847
|
-
--arg outcome "success" \
|
|
848
|
-
'{
|
|
849
|
-
session_name: $session_name,
|
|
850
|
-
turn_number: $turn_number,
|
|
851
|
-
user_intent: $user_intent,
|
|
852
|
-
user_decision: (if $user_decision == "" then null else $user_decision end),
|
|
853
|
-
user_correction: (if $user_correction == "" then null else $user_correction end),
|
|
854
|
-
user_preference: (if $user_preference == "" then null else $user_preference end),
|
|
855
|
-
tools_used: $tools_used,
|
|
856
|
-
files_modified: $files_modified,
|
|
857
|
-
commands_run: $commands_run,
|
|
858
|
-
errors: $errors,
|
|
859
|
-
git_files_changed: $git_files_changed,
|
|
860
|
-
git_diff_stat: (if $git_diff_stat == "" then null else $git_diff_stat end),
|
|
861
|
-
outcome: $outcome
|
|
862
|
-
}' 2>/dev/null)
|
|
863
|
-
|
|
864
|
-
# Fire fast-capture in background (non-blocking, <20ms)
|
|
865
|
-
if [ -n "$FAST_PAYLOAD" ]; then
|
|
866
|
-
curl -s -X POST "$MEMORY_API_URL/api/v1/working/fast-capture" \
|
|
867
|
-
-H "Authorization: Bearer $AUTH_TOKEN" \
|
|
868
|
-
-H "Content-Type: application/json" \
|
|
869
|
-
-d "$FAST_PAYLOAD" \
|
|
870
|
-
--connect-timeout 1 \
|
|
871
|
-
--max-time 2 >/dev/null 2>&1 &
|
|
872
|
-
fi
|
|
873
|
-
|
|
874
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
875
|
-
# 💾 LOCAL CACHE: Tier 0 - Update turn with assistant response
|
|
876
|
-
# Updates the turn created by user-prompt-submit hook with the response
|
|
877
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
878
|
-
if command -v ekkos-capture &>/dev/null && [ -n "$SESSION_ID" ]; then
|
|
879
|
-
# Escape response for shell (use base64 for safety with complex content)
|
|
880
|
-
RESPONSE_B64=$(echo "$SCRUBBED_ASSISTANT" | base64 2>/dev/null || echo "")
|
|
881
|
-
if [ -n "$RESPONSE_B64" ]; then
|
|
882
|
-
# Decode and pass to capture command (handles newlines and special chars)
|
|
883
|
-
DECODED_RESPONSE=$(echo "$RESPONSE_B64" | base64 -d 2>/dev/null || echo "")
|
|
884
|
-
if [ -n "$DECODED_RESPONSE" ]; then
|
|
885
|
-
(ekkos-capture response "$SESSION_ID" "$TURN_NUMBER" "$DECODED_RESPONSE" "$TOOLS_USED" "$FILES_REFERENCED" \
|
|
886
|
-
>/dev/null 2>&1) &
|
|
887
|
-
fi
|
|
888
|
-
fi
|
|
889
|
-
fi
|
|
890
|
-
fi
|
|
891
|
-
|
|
892
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
893
|
-
# 💾 FALLBACK LOCAL CACHE UPDATE: Even if L2/Redis capture was skipped
|
|
894
|
-
# This ensures local cache gets updated with assistant response for /continue
|
|
895
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
896
|
-
if [ -n "$LAST_ASSISTANT" ] && command -v ekkos-capture &>/dev/null && [ -n "$SESSION_ID" ]; then
|
|
897
|
-
# Only run if we didn't already update (check if inside the main block or not)
|
|
898
|
-
# This handles the case where LAST_USER was empty but LAST_ASSISTANT is available
|
|
899
|
-
if [ -z "$LAST_USER" ]; then
|
|
900
|
-
echo "[ekkOS DEBUG] Fallback local cache update: LAST_ASSISTANT available, updating turn $TURN_NUMBER" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
|
|
901
|
-
RESPONSE_B64=$(echo "$LAST_ASSISTANT" | base64 2>/dev/null || echo "")
|
|
902
|
-
if [ -n "$RESPONSE_B64" ]; then
|
|
903
|
-
DECODED_RESPONSE=$(echo "$RESPONSE_B64" | base64 -d 2>/dev/null || echo "")
|
|
904
|
-
if [ -n "$DECODED_RESPONSE" ]; then
|
|
905
|
-
TOOLS_USED=$(echo "$LAST_ASSISTANT" | grep -oE '\[TOOL: [^\]]+\]' | sed 's/\[TOOL: //g; s/\]//g' | sort -u | jq -R -s -c 'split("\n") | map(select(. != ""))' 2>/dev/null || echo "[]")
|
|
906
|
-
FILES_REFERENCED="[]"
|
|
907
|
-
(ekkos-capture response "$SESSION_ID" "$TURN_NUMBER" "$DECODED_RESPONSE" "$TOOLS_USED" "$FILES_REFERENCED" \
|
|
908
|
-
>/dev/null 2>&1) &
|
|
909
|
-
echo "[ekkOS] Turn $TURN_NUMBER: Local cache updated via fallback (session: $SESSION_NAME)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
|
|
910
|
-
fi
|
|
911
|
-
fi
|
|
912
|
-
fi
|
|
913
|
-
fi
|
|
914
|
-
|
|
915
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
916
|
-
# 🔄 GOLDEN LOOP: DETECT PHASES FROM RESPONSE
|
|
917
|
-
# ═══════════════════════════════════════════════════════════════════════════
|
|
918
|
-
GOLDEN_LOOP_FILE="$PROJECT_ROOT/.ekkos/golden-loop-current.json"
|
|
919
|
-
|
|
920
|
-
if [ -n "$LAST_ASSISTANT" ] && [ -f "$GOLDEN_LOOP_FILE" ]; then
|
|
921
|
-
# Detect phases from agent response
|
|
922
|
-
RETRIEVED=0
|
|
923
|
-
APPLIED=0
|
|
924
|
-
FORGED=0
|
|
925
|
-
|
|
926
|
-
# 🔍 RETRIEVE: Count ekkOS_Search calls (MCP tool invocations)
|
|
927
|
-
RETRIEVED=$(echo "$LAST_ASSISTANT" | grep -c "mcp__ekkos-memory__ekkOS_Search" 2>/dev/null || echo "0")
|
|
928
|
-
[ "$RETRIEVED" -eq 0 ] && RETRIEVED=$(echo "$LAST_ASSISTANT" | grep -c "ekkOS_Search" 2>/dev/null || echo "0")
|
|
929
|
-
|
|
930
|
-
# 💉 INJECT: Count [ekkOS_SELECT] pattern acknowledgments
|
|
931
|
-
APPLIED=$(echo "$LAST_ASSISTANT" | grep -c "\[ekkOS_SELECT\]" 2>/dev/null || echo "0")
|
|
932
|
-
|
|
933
|
-
# 📊 MEASURE: Count ekkOS_Forge calls (pattern creation)
|
|
934
|
-
FORGED=$(echo "$LAST_ASSISTANT" | grep -c "mcp__ekkos-memory__ekkOS_Forge" 2>/dev/null || echo "0")
|
|
935
|
-
[ "$FORGED" -eq 0 ] && FORGED=$(echo "$LAST_ASSISTANT" | grep -c "ekkOS_Forge" 2>/dev/null || echo "0")
|
|
936
|
-
|
|
937
|
-
# Determine current phase based on what's happening
|
|
938
|
-
CURRENT_PHASE="complete"
|
|
939
|
-
if [ "$FORGED" -gt 0 ]; then
|
|
940
|
-
CURRENT_PHASE="measure"
|
|
941
|
-
elif [ "$APPLIED" -gt 0 ]; then
|
|
942
|
-
CURRENT_PHASE="inject"
|
|
943
|
-
elif [ "$RETRIEVED" -gt 0 ]; then
|
|
944
|
-
CURRENT_PHASE="retrieve"
|
|
945
|
-
fi
|
|
946
|
-
|
|
947
|
-
# Update Golden Loop file with detected stats
|
|
948
|
-
jq -n \
|
|
949
|
-
--arg phase "$CURRENT_PHASE" \
|
|
950
|
-
--argjson turn "$TURN_NUMBER" \
|
|
951
|
-
--arg session "$SESSION_NAME" \
|
|
952
|
-
--arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
|
|
953
|
-
--argjson retrieved "$RETRIEVED" \
|
|
954
|
-
--argjson applied "$APPLIED" \
|
|
955
|
-
--argjson forged "$FORGED" \
|
|
956
|
-
'{
|
|
957
|
-
phase: $phase,
|
|
958
|
-
turn: $turn,
|
|
959
|
-
session: $session,
|
|
960
|
-
timestamp: $timestamp,
|
|
961
|
-
stats: {
|
|
962
|
-
retrieved: $retrieved,
|
|
963
|
-
applied: $applied,
|
|
964
|
-
forged: $forged
|
|
965
|
-
}
|
|
966
|
-
}' > "$GOLDEN_LOOP_FILE" 2>/dev/null || true
|
|
967
|
-
|
|
968
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
969
|
-
# 📊 GOLDEN LOOP STEP 5: AUTO-OUTCOME — Close the feedback loop
|
|
970
|
-
# Parses [ekkOS_SELECT] blocks → extracts pattern IDs → calls ekkOS_Outcome
|
|
971
|
-
# Feeds: pattern_applications → pattern_application_links → PROMETHEUS delta
|
|
972
|
-
# ekkOS_Outcome is ENHANCED: auto-creates Track record (no separate Track call needed)
|
|
973
|
-
# ═══════════════════════════════════════════════════════════════════════
|
|
974
|
-
if [ "$APPLIED" -gt 0 ] && [ -n "$EKKOS_API_KEY" ]; then
|
|
975
|
-
# Extract pattern IDs from [ekkOS_SELECT] blocks
|
|
976
|
-
# Format: - id: <pattern_id> (UUID format: 8-4-4-4-12 hex)
|
|
977
|
-
SELECTED_IDS=$(echo "$LAST_ASSISTANT" | grep -oE 'id: [a-f0-9-]{36}' | sed 's/id: //' | sort -u || echo "")
|
|
978
|
-
|
|
979
|
-
# Also try compact format: id:<pattern_id>
|
|
980
|
-
if [ -z "$SELECTED_IDS" ]; then
|
|
981
|
-
SELECTED_IDS=$(echo "$LAST_ASSISTANT" | grep -oE 'id:[a-f0-9-]{36}' | sed 's/id://' | sort -u || echo "")
|
|
982
|
-
fi
|
|
983
|
-
|
|
984
|
-
if [ -n "$SELECTED_IDS" ]; then
|
|
985
|
-
# Build memory_ids JSON array using jq
|
|
986
|
-
MEMORY_IDS_JSON=$(echo "$SELECTED_IDS" | jq -R -s 'split("\n") | map(select(length > 0))' 2>/dev/null || echo "[]")
|
|
987
|
-
SELECTED_COUNT=$(echo "$SELECTED_IDS" | wc -l | tr -d ' ')
|
|
988
|
-
|
|
989
|
-
# Infer outcome: success unless unresolved errors detected
|
|
990
|
-
# The heuristic checks for error indicators WITHOUT resolution markers
|
|
991
|
-
OUTCOME_SUCCESS=true
|
|
992
|
-
HAS_ERRORS=$(echo "$LAST_ASSISTANT" | grep -ciE '(error|failed|cannot|exception|not found|bug|broken|not working|traceback|panic)' 2>/dev/null || echo "0")
|
|
993
|
-
HAS_RESOLUTION=$(echo "$LAST_ASSISTANT" | grep -ciE '(fixed|resolved|working now|succeeded|completed successfully|done|solved|corrected)' 2>/dev/null || echo "0")
|
|
994
|
-
|
|
995
|
-
if [ "$HAS_ERRORS" -gt 0 ] && [ "$HAS_RESOLUTION" -eq 0 ]; then
|
|
996
|
-
OUTCOME_SUCCESS=false
|
|
997
|
-
fi
|
|
998
|
-
|
|
999
|
-
# Call ekkOS_Outcome via MCP gateway (async, non-blocking)
|
|
1000
|
-
# Enhanced endpoint: auto-creates Track record from memory_ids
|
|
1001
|
-
# Populates: pattern_applications.applied_at, pattern_applications.outcome_success
|
|
1002
|
-
# Increments: patterns.applied_count, patterns.last_applied_at
|
|
1003
|
-
# Links: pattern_application_links for cross-layer PROMETHEUS delta
|
|
1004
|
-
OUTCOME_BODY=$(jq -n \
|
|
1005
|
-
--argjson memory_ids "$MEMORY_IDS_JSON" \
|
|
1006
|
-
--argjson success "$OUTCOME_SUCCESS" \
|
|
1007
|
-
--arg model_used "$MODEL_USED" \
|
|
1008
|
-
'{
|
|
1009
|
-
tool: "ekkOS_Outcome",
|
|
1010
|
-
arguments: {
|
|
1011
|
-
memory_ids: $memory_ids,
|
|
1012
|
-
success: $success,
|
|
1013
|
-
model_used: $model_used
|
|
1014
|
-
}
|
|
1015
|
-
}' 2>/dev/null)
|
|
1016
|
-
|
|
1017
|
-
if [ -n "$OUTCOME_BODY" ]; then
|
|
1018
|
-
(curl -s -X POST "$MEMORY_API_URL/api/v1/mcp/call" \
|
|
1019
|
-
-H "Authorization: Bearer $EKKOS_API_KEY" \
|
|
1020
|
-
-H "Content-Type: application/json" \
|
|
1021
|
-
-d "$OUTCOME_BODY" \
|
|
1022
|
-
--connect-timeout 2 \
|
|
1023
|
-
--max-time 3 >/dev/null 2>&1) &
|
|
1024
|
-
|
|
1025
|
-
echo "[ekkOS] Auto-outcome: ${SELECTED_COUNT} patterns, success=${OUTCOME_SUCCESS} (session: $SESSION_NAME, turn: $TURN_NUMBER)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
|
|
1026
|
-
fi
|
|
353
|
+
--max-time 5 >/dev/null 2>&1 || true
|
|
1027
354
|
fi
|
|
1028
|
-
|
|
355
|
+
) &
|
|
1029
356
|
fi
|
|
1030
357
|
|
|
1031
358
|
# ═══════════════════════════════════════════════════════════════════════════
|