@ekkos/cli 0.2.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.
Files changed (135) hide show
  1. package/dist/cache/LocalSessionStore.d.ts +129 -0
  2. package/dist/cache/LocalSessionStore.js +688 -0
  3. package/dist/cache/capture.d.ts +26 -0
  4. package/dist/cache/capture.js +461 -0
  5. package/dist/cache/index.d.ts +7 -0
  6. package/dist/cache/index.js +23 -0
  7. package/dist/cache/types.d.ts +147 -0
  8. package/dist/cache/types.js +40 -0
  9. package/dist/commands/init.d.ts +9 -0
  10. package/dist/commands/init.js +478 -0
  11. package/dist/commands/run.d.ts +12 -0
  12. package/dist/commands/run.js +829 -0
  13. package/dist/commands/setup.d.ts +6 -0
  14. package/dist/commands/setup.js +658 -0
  15. package/dist/commands/status.d.ts +1 -0
  16. package/dist/commands/status.js +109 -0
  17. package/dist/commands/test.d.ts +1 -0
  18. package/dist/commands/test.js +157 -0
  19. package/dist/deploy/agents.d.ts +15 -0
  20. package/dist/deploy/agents.js +72 -0
  21. package/dist/deploy/hooks.d.ts +16 -0
  22. package/dist/deploy/hooks.js +121 -0
  23. package/dist/deploy/index.d.ts +7 -0
  24. package/dist/deploy/index.js +24 -0
  25. package/dist/deploy/instructions.d.ts +12 -0
  26. package/dist/deploy/instructions.js +36 -0
  27. package/dist/deploy/mcp.d.ts +19 -0
  28. package/dist/deploy/mcp.js +109 -0
  29. package/dist/deploy/plugins.d.ts +19 -0
  30. package/dist/deploy/plugins.js +62 -0
  31. package/dist/deploy/settings.d.ts +8 -0
  32. package/dist/deploy/settings.js +84 -0
  33. package/dist/deploy/skills.d.ts +19 -0
  34. package/dist/deploy/skills.js +60 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +71 -0
  37. package/dist/restore/RestoreOrchestrator.d.ts +48 -0
  38. package/dist/restore/RestoreOrchestrator.js +481 -0
  39. package/dist/restore/index.d.ts +4 -0
  40. package/dist/restore/index.js +20 -0
  41. package/dist/utils/platform.d.ts +29 -0
  42. package/dist/utils/platform.js +65 -0
  43. package/dist/utils/session-words.json +119 -0
  44. package/dist/utils/state.d.ts +57 -0
  45. package/dist/utils/state.js +186 -0
  46. package/dist/utils/templates.d.ts +24 -0
  47. package/dist/utils/templates.js +118 -0
  48. package/package.json +48 -0
  49. package/templates/CLAUDE.md +287 -0
  50. package/templates/README.md +378 -0
  51. package/templates/agents/README.md +182 -0
  52. package/templates/agents/code-reviewer.md +166 -0
  53. package/templates/agents/debug-detective.md +169 -0
  54. package/templates/agents/ekkOS_Vercel.md +99 -0
  55. package/templates/agents/extension-manager.md +229 -0
  56. package/templates/agents/git-companion.md +185 -0
  57. package/templates/agents/github-test-agent.md +321 -0
  58. package/templates/agents/railway-manager.md +179 -0
  59. package/templates/claude-plugins/PHASE2_COMPLETION.md +346 -0
  60. package/templates/claude-plugins/PLUGIN_PROPOSALS.md +1776 -0
  61. package/templates/claude-plugins/README.md +587 -0
  62. package/templates/claude-plugins/agents/code-reviewer.json +14 -0
  63. package/templates/claude-plugins/agents/debug-detective.json +15 -0
  64. package/templates/claude-plugins/agents/git-companion.json +14 -0
  65. package/templates/claude-plugins/blog-manager/.claude-plugin/plugin.json +8 -0
  66. package/templates/claude-plugins/blog-manager/commands/blog.md +691 -0
  67. package/templates/claude-plugins/golden-loop-monitor/.claude-plugin/plugin.json +8 -0
  68. package/templates/claude-plugins/golden-loop-monitor/commands/loop-status.md +434 -0
  69. package/templates/claude-plugins/learning-tracker/.claude-plugin/plugin.json +8 -0
  70. package/templates/claude-plugins/learning-tracker/commands/my-patterns.md +282 -0
  71. package/templates/claude-plugins/memory-lens/.claude-plugin/plugin.json +8 -0
  72. package/templates/claude-plugins/memory-lens/commands/memory-search.md +181 -0
  73. package/templates/claude-plugins/pattern-coach/.claude-plugin/plugin.json +8 -0
  74. package/templates/claude-plugins/pattern-coach/commands/forge.md +365 -0
  75. package/templates/claude-plugins/project-schema-validator/.claude-plugin/plugin.json +8 -0
  76. package/templates/claude-plugins/project-schema-validator/commands/validate-schema.md +582 -0
  77. package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +819 -0
  78. package/templates/claude-plugins-admin/README.md +446 -0
  79. package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +8 -0
  80. package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +595 -0
  81. package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +8 -0
  82. package/templates/claude-plugins-admin/backend-agent/commands/backend.md +798 -0
  83. package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +8 -0
  84. package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +554 -0
  85. package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +8 -0
  86. package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +881 -0
  87. package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +8 -0
  88. package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +85 -0
  89. package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +8 -0
  90. package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +569 -0
  91. package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +8 -0
  92. package/templates/claude-plugins-admin/qa-agent/commands/qa.md +863 -0
  93. package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +8 -0
  94. package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +732 -0
  95. package/templates/commands/continue.md +47 -0
  96. package/templates/cursor-hooks/after-agent-response.sh +117 -0
  97. package/templates/cursor-hooks/before-submit-prompt.sh +419 -0
  98. package/templates/cursor-hooks/hooks.json +20 -0
  99. package/templates/cursor-hooks/lib/contract.sh +320 -0
  100. package/templates/cursor-hooks/stop.sh +75 -0
  101. package/templates/cursor-rules/ekkos-memory.md +187 -0
  102. package/templates/hooks/assistant-response.sh +96 -0
  103. package/templates/hooks/hooks.json +28 -0
  104. package/templates/hooks/lib/contract.sh +320 -0
  105. package/templates/hooks/lib/state.sh +158 -0
  106. package/templates/hooks/session-start.ps1 +41 -0
  107. package/templates/hooks/session-start.sh +318 -0
  108. package/templates/hooks/stop.ps1 +16 -0
  109. package/templates/hooks/stop.sh +989 -0
  110. package/templates/hooks/user-prompt-submit.ps1 +174 -0
  111. package/templates/hooks/user-prompt-submit.sh +587 -0
  112. package/templates/hooks-node/lib/state.js +187 -0
  113. package/templates/hooks-node/stop.js +416 -0
  114. package/templates/hooks-node/user-prompt-submit.js +337 -0
  115. package/templates/plan-template.md +306 -0
  116. package/templates/rules/00-hooks-contract.mdc +89 -0
  117. package/templates/rules/30-ekkos-core.mdc +188 -0
  118. package/templates/rules/31-ekkos-messages.mdc +78 -0
  119. package/templates/skills/continue/SKILL.md +169 -0
  120. package/templates/skills/ekkOS_Deep_Recall/Skill.md +282 -0
  121. package/templates/skills/ekkOS_Learn/Skill.md +265 -0
  122. package/templates/skills/ekkOS_Memory_First/Skill.md +206 -0
  123. package/templates/skills/ekkOS_Plan_Assist/Skill.md +302 -0
  124. package/templates/skills/ekkOS_Preferences/Skill.md +247 -0
  125. package/templates/skills/ekkOS_Reflect/Skill.md +257 -0
  126. package/templates/skills/ekkOS_Safety/Skill.md +265 -0
  127. package/templates/skills/ekkOS_Schema/Skill.md +251 -0
  128. package/templates/skills/ekkOS_Summary/Skill.md +257 -0
  129. package/templates/skills/ekkOS_Vault/Skill.md +287 -0
  130. package/templates/skills/permissions/Skill.md +322 -0
  131. package/templates/spec-template.md +159 -0
  132. package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
  133. package/templates/windsurf-hooks/hooks.json +10 -0
  134. package/templates/windsurf-hooks/lib/contract.sh +320 -0
  135. package/templates/windsurf-rules/ekkos-memory.md +129 -0
@@ -0,0 +1,989 @@
1
+ #!/bin/bash
2
+ # ═══════════════════════════════════════════════════════════════════════════
3
+ # ekkOS_ Hook: Stop - FULL CONTEXT CAPTURE
4
+ # ═══════════════════════════════════════════════════════════════════════════
5
+ # Captures FULL turn content to L2 (episodic memory):
6
+ # - Full user query
7
+ # - Full assistant response (no truncation)
8
+ # - Complete file changes with edit content (old_string → new_string)
9
+ # ═══════════════════════════════════════════════════════════════════════════
10
+
11
+ set +e
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
15
+ STATE_DIR="$PROJECT_ROOT/.claude/state"
16
+
17
+ INPUT=$(cat)
18
+
19
+ RAW_SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
20
+ TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
21
+ MODEL_USED=$(echo "$INPUT" | jq -r '.model // "claude-sonnet-4-5"')
22
+
23
+ # DEBUG: Log hook input (full INPUT for debugging)
24
+ echo "[ekkOS DEBUG] $(date -u +%H:%M:%S) stop.sh: session=$RAW_SESSION_ID, transcript_path=$TRANSCRIPT_PATH" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
25
+ echo "[ekkOS DEBUG] $(date -u +%H:%M:%S) stop.sh: transcript exists=$([ -f "$TRANSCRIPT_PATH" ] && echo 'yes' || echo 'no')" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
26
+ echo "[ekkOS DEBUG] INPUT keys: $(echo "$INPUT" | jq -r 'keys | join(",")')" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
27
+
28
+ # ═══════════════════════════════════════════════════════════════════════════
29
+ # Session ID - Try Claude's input first, fallback to state file
30
+ # ═══════════════════════════════════════════════════════════════════════════
31
+ SESSION_ID="$RAW_SESSION_ID"
32
+
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
+ if [ -z "$SESSION_ID" ] || [ "$SESSION_ID" = "unknown" ] || [ "$SESSION_ID" = "null" ]; then
43
+ exit 0
44
+ fi
45
+
46
+ # ═══════════════════════════════════════════════════════════════════════════
47
+ # Load auth
48
+ # ═══════════════════════════════════════════════════════════════════════════
49
+ EKKOS_CONFIG="$HOME/.ekkos/config.json"
50
+ AUTH_TOKEN=""
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')
60
+ fi
61
+
62
+ [ -z "$AUTH_TOKEN" ] && exit 0
63
+
64
+ MEMORY_API_URL="https://mcp.ekkos.dev"
65
+
66
+ # ═══════════════════════════════════════════════════════════════════════════
67
+ # WORD-BASED SESSION NAMES: Convert UUID to memorable 3-word name
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
71
+ # ═══════════════════════════════════════════════════════════════════════════
72
+ ADJECTIVES=(
73
+ "cosmic" "turbo" "mega" "hyper" "quantum" "atomic" "stellar" "epic"
74
+ "mighty" "groovy" "zippy" "snappy" "jazzy" "funky" "zesty" "peppy"
75
+ "spicy" "crispy" "fluffy" "sparkly" "chunky" "bouncy" "bubbly" "sassy"
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
+ )
117
+
118
+ # Convert UUID to 3-word name deterministically
119
+ uuid_to_words() {
120
+ local uuid="$1"
121
+ local hex="${uuid//-/}"
122
+ hex="${hex:0:12}"
123
+
124
+ if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
125
+ echo "unknown-session-starts"
126
+ return
127
+ fi
128
+
129
+ local adj_seed=$((16#${hex:0:4}))
130
+ local noun_seed=$((16#${hex:4:4}))
131
+ local verb_seed=$((16#${hex:8:4}))
132
+
133
+ local adj_idx=$((adj_seed % ${#ADJECTIVES[@]}))
134
+ local noun_idx=$((noun_seed % ${#NOUNS[@]}))
135
+ local verb_idx=$((verb_seed % ${#VERBS[@]}))
136
+
137
+ echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}-${VERBS[$verb_idx]}"
138
+ }
139
+
140
+ # Generate session name from UUID
141
+ SESSION_NAME=""
142
+ if [ -n "$SESSION_ID" ] && [ "$SESSION_ID" != "unknown" ] && [ "$SESSION_ID" != "null" ]; then
143
+ SESSION_NAME=$(uuid_to_words "$SESSION_ID")
144
+ fi
145
+
146
+ # ═══════════════════════════════════════════════════════════════════════════
147
+ # Get turn number from local counter
148
+ # ═══════════════════════════════════════════════════════════════════════════
149
+ PROJECT_SESSION_DIR="$STATE_DIR/sessions"
150
+ TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${SESSION_ID}.turn"
151
+ TURN_NUMBER=1
152
+ [ -f "$TURN_COUNTER_FILE" ] && TURN_NUMBER=$(cat "$TURN_COUNTER_FILE" 2>/dev/null || echo "1")
153
+
154
+ # ═══════════════════════════════════════════════════════════════════════════
155
+ # AUTO-CLEAR DETECTION (EARLY): Must run BEFORE any early exits
156
+ # If context >= 92%, write flag for ekkos run wrapper immediately
157
+ # ═══════════════════════════════════════════════════════════════════════════
158
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
159
+ MAX_TOKENS=200000
160
+
161
+ # Calculate context percentage
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))
169
+
170
+ # More accurate in high-context scenarios
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
175
+
176
+ # If context >= 92%, write flag file for ekkos run wrapper
177
+ if [ "$TOKEN_PERCENT" -ge 92 ]; then
178
+ AUTO_CLEAR_FLAG="$HOME/.ekkos/auto-clear.flag"
179
+ TIMESTAMP_EPOCH=$(date +%s)
180
+ echo "${TOKEN_PERCENT}:${SESSION_NAME}:${TIMESTAMP_EPOCH}" > "$AUTO_CLEAR_FLAG"
181
+ echo "[ekkOS] Context at ${TOKEN_PERCENT}% - auto-clear flag written (session: $SESSION_NAME)" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
182
+ fi
183
+ fi
184
+
185
+ # ═══════════════════════════════════════════════════════════════════════════
186
+ # Check for interruption - skip capture if request was interrupted
187
+ # ═══════════════════════════════════════════════════════════════════════════
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
+
191
+ # Skip capture for interrupted/cancelled requests
192
+ if [ "$IS_INTERRUPTED" = "true" ] || [ "$STOP_REASON" = "user_cancelled" ] || [ "$STOP_REASON" = "interrupted" ]; then
193
+ exit 0
194
+ fi
195
+
196
+ # ═══════════════════════════════════════════════════════════════════════════
197
+ # Extract conversation from transcript
198
+ # ═══════════════════════════════════════════════════════════════════════════
199
+ LAST_USER=""
200
+ LAST_ASSISTANT=""
201
+ FILE_CHANGES="[]"
202
+
203
+ if [ -n "$TRANSCRIPT_PATH" ] && [ -f "$TRANSCRIPT_PATH" ]; then
204
+ # Extract user messages:
205
+ # Content can be STRING or ARRAY of {type: "text", text: "..."} objects
206
+ # Filter out system prefixes (<) and tool_results
207
+ LAST_USER=$(cat "$TRANSCRIPT_PATH" | jq -r '
208
+ select(.type == "user")
209
+ | .message.content
210
+ | if type == "string" then
211
+ if startswith("<") then empty else . end
212
+ elif type == "array" then
213
+ .[] | select(.type == "text") | .text | select(startswith("<") | not)
214
+ else empty end
215
+ ' 2>/dev/null | tail -1 || echo "")
216
+
217
+ # DEBUG: Show what we extracted
218
+ echo "[ekkOS DEBUG] LAST_USER length=${#LAST_USER}, first 50: '${LAST_USER:0:50}'" >> "$HOME/.ekkos/capture-debug.log" 2>/dev/null
219
+
220
+ # Log if empty (but don't exit - we still want to try extracting assistant response)
221
+ if [ -z "$LAST_USER" ]; then
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 "")
296
+
297
+ # Combine assistant response with tool results
298
+ if [ -n "$TOOL_RESULTS" ]; then
299
+ LAST_ASSISTANT="${LAST_ASSISTANT}${TOOL_RESULTS}"
300
+ fi
301
+ fi
302
+
303
+ # Fallback: get last assistant message if timestamp method fails
304
+ if [ -z "$LAST_ASSISTANT" ]; then
305
+ LAST_ASSISTANT=$(cat "$TRANSCRIPT_PATH" | jq -rs '
306
+ [.[] | select(.type == "assistant")] | last |
307
+ .message.content |
308
+ if type == "string" then .
309
+ elif type == "array" then
310
+ [.[] |
311
+ if .type == "text" then .text
312
+ elif .type == "tool_use" then
313
+ "\n[TOOL: " + .name + "]\n" +
314
+ (if .name == "Bash" then "$ " + (.input.command // "") + "\n"
315
+ elif .name == "Read" then "Reading: " + (.input.file_path // "") + "\n"
316
+ elif .name == "Write" then "Writing: " + (.input.file_path // "") + "\n"
317
+ elif .name == "Edit" then "Editing: " + (.input.file_path // "") + "\n"
318
+ else (.input | tostring | .[0:500]) + "\n"
319
+ end)
320
+ elif .type == "thinking" then "\n[THINKING]\n" + (.thinking // .text // "") + "\n[/THINKING]\n"
321
+ else empty
322
+ end
323
+ ] | join("")
324
+ else empty end
325
+ ' 2>/dev/null || echo "")
326
+ fi
327
+
328
+ # Extract file changes WITH FULL EDIT CONTENT for perfect context restoration
329
+ # Includes old_string/new_string for edits, content for writes
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 "[]")
346
+ fi
347
+
348
+ # ═══════════════════════════════════════════════════════════════════════════
349
+ # Capture to L2 (episodic memory) - SYNCHRONOUS for reliability
350
+ # Background was causing missed captures when Claude Code exits fast
351
+ # ═══════════════════════════════════════════════════════════════════════════
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
+ if [ -n "$LAST_USER" ] && [ -n "$LAST_ASSISTANT" ]; then
357
+ PAYLOAD_FILE=$(mktemp /tmp/ekkos-capture.XXXXXX.json)
358
+ TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
359
+
360
+ jq -n \
361
+ --arg user_query "$LAST_USER" \
362
+ --arg assistant_response "$LAST_ASSISTANT" \
363
+ --arg session_id "$SESSION_ID" \
364
+ --arg user_id "${USER_ID:-system}" \
365
+ --arg model_used "$MODEL_USED" \
366
+ --arg captured_at "$TIMESTAMP" \
367
+ --argjson file_changes "${FILE_CHANGES:-[]}" \
368
+ '{
369
+ user_query: $user_query,
370
+ assistant_response: $assistant_response,
371
+ session_id: $session_id,
372
+ user_id: $user_id,
373
+ file_changes: $file_changes,
374
+ metadata: {
375
+ source: "claude-code",
376
+ model_used: $model_used,
377
+ captured_at: $captured_at,
378
+ file_changes: $file_changes,
379
+ minimal_hook: true
380
+ }
381
+ }' > "$PAYLOAD_FILE" 2>/dev/null
382
+
383
+ if jq empty "$PAYLOAD_FILE" 2>/dev/null; then
384
+ # Retry with backoff for L2 episodic capture
385
+ for RETRY in 1 2 3; do
386
+ CAPTURE_RESULT=$(curl -s -w "\n%{http_code}" -X POST "$MEMORY_API_URL/api/v1/memory/capture" \
387
+ -H "Authorization: Bearer $AUTH_TOKEN" \
388
+ -H "Content-Type: application/json" \
389
+ -d "@$PAYLOAD_FILE" \
390
+ --connect-timeout 3 \
391
+ --max-time 5 2>/dev/null || echo -e "\n000")
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))
728
+ 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
+
758
+ if jq empty "$REDIS_PAYLOAD_FILE" 2>/dev/null; then
759
+ # Retry with backoff for Redis working memory (critical for /continue)
760
+ MAX_RETRIES=3
761
+ RETRY=0
762
+ REDIS_SUCCESS=false
763
+
764
+ while [ $RETRY -lt $MAX_RETRIES ] && [ "$REDIS_SUCCESS" = "false" ]; do
765
+ REDIS_RESULT=$(curl -s -w "\n%{http_code}" -X POST "$MEMORY_API_URL/api/v1/working/turn" \
766
+ -H "Authorization: Bearer $AUTH_TOKEN" \
767
+ -H "Content-Type: application/json" \
768
+ -d "@$REDIS_PAYLOAD_FILE" \
769
+ --connect-timeout 3 \
770
+ --max-time 5 2>/dev/null || echo -e "\n000")
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
+ fi
968
+
969
+ # ═══════════════════════════════════════════════════════════════════════════
970
+ # Update local .ekkos/current-focus.md (if exists) - SILENT
971
+ # ═══════════════════════════════════════════════════════════════════════════
972
+ EKKOS_LOCAL_DIR="$PROJECT_ROOT/.ekkos"
973
+ if [ -d "$EKKOS_LOCAL_DIR" ] && [ -n "$LAST_USER" ]; then
974
+ FOCUS_FILE="$EKKOS_LOCAL_DIR/current-focus.md"
975
+ TASK_SUMMARY="${LAST_USER:0:100}"
976
+ [ ${#LAST_USER} -gt 100 ] && TASK_SUMMARY="${TASK_SUMMARY}..."
977
+
978
+ cat > "$FOCUS_FILE" << EOF
979
+ ---
980
+ last_updated: $(date -u +%Y-%m-%dT%H:%M:%SZ)
981
+ session_id: ${SESSION_ID}
982
+ ---
983
+
984
+ # Current Focus
985
+ ${TASK_SUMMARY}
986
+ EOF
987
+ fi
988
+
989
+ exit 0