@ekkos/cli 0.2.8 → 0.2.10

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 (36) hide show
  1. package/dist/cache/LocalSessionStore.d.ts +34 -21
  2. package/dist/cache/LocalSessionStore.js +169 -53
  3. package/dist/cache/capture.d.ts +19 -11
  4. package/dist/cache/capture.js +243 -76
  5. package/dist/cache/types.d.ts +14 -1
  6. package/dist/commands/doctor.d.ts +10 -0
  7. package/dist/commands/doctor.js +148 -73
  8. package/dist/commands/hooks.d.ts +109 -0
  9. package/dist/commands/hooks.js +668 -0
  10. package/dist/commands/run.d.ts +1 -0
  11. package/dist/commands/run.js +69 -21
  12. package/dist/index.js +42 -1
  13. package/dist/restore/RestoreOrchestrator.d.ts +17 -3
  14. package/dist/restore/RestoreOrchestrator.js +64 -22
  15. package/dist/utils/paths.d.ts +125 -0
  16. package/dist/utils/paths.js +283 -0
  17. package/dist/utils/session-words.json +30 -111
  18. package/package.json +1 -1
  19. package/templates/ekkos-manifest.json +223 -0
  20. package/templates/helpers/json-parse.cjs +101 -0
  21. package/templates/hooks/assistant-response.ps1 +256 -0
  22. package/templates/hooks/assistant-response.sh +124 -64
  23. package/templates/hooks/session-start.ps1 +107 -2
  24. package/templates/hooks/session-start.sh +201 -166
  25. package/templates/hooks/stop.ps1 +124 -3
  26. package/templates/hooks/stop.sh +470 -843
  27. package/templates/hooks/user-prompt-submit.ps1 +107 -22
  28. package/templates/hooks/user-prompt-submit.sh +403 -393
  29. package/templates/project-stubs/session-start.ps1 +63 -0
  30. package/templates/project-stubs/session-start.sh +55 -0
  31. package/templates/project-stubs/stop.ps1 +63 -0
  32. package/templates/project-stubs/stop.sh +55 -0
  33. package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
  34. package/templates/project-stubs/user-prompt-submit.sh +55 -0
  35. package/templates/shared/hooks-enabled.json +22 -0
  36. package/templates/shared/session-words.json +45 -0
@@ -1,6 +1,8 @@
1
1
  #!/bin/bash
2
2
  # ═══════════════════════════════════════════════════════════════════════════
3
3
  # ekkOS_ Hook: SessionStart - MINIMAL + AUTO-RESTORE + TIME MACHINE CONTINUE
4
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
5
+ # EKKOS_MANAGED=1
4
6
  # ═══════════════════════════════════════════════════════════════════════════
5
7
  # This hook does THREE things:
6
8
  # 1. Check for pending Time Machine "Continue from here" requests
@@ -15,6 +17,8 @@
15
17
  # FAST TRIM FLOW:
16
18
  # User runs /clear → session-start detects fresh session →
17
19
  # Checks L2 for recent turns → Auto-injects last 15 turns → Seamless continuity
20
+ #
21
+ # Per spec v1.2 Addendum: NO jq dependency
18
22
  # ═══════════════════════════════════════════════════════════════════════════
19
23
 
20
24
  set +e
@@ -22,10 +26,36 @@ set +e
22
26
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
23
27
  PROJECT_ROOT="$(dirname "$(dirname "$SCRIPT_DIR")")"
24
28
 
29
+ # ═══════════════════════════════════════════════════════════════════════════
30
+ # CONFIG PATHS - Per spec v1.2 Addendum
31
+ # ═══════════════════════════════════════════════════════════════════════════
32
+ EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
33
+ JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
34
+
25
35
  INPUT=$(cat)
26
- SESSION_ID=$(echo "$INPUT" | jq -r '.session_id // "unknown"')
27
- TRANSCRIPT_PATH=$(echo "$INPUT" | jq -r '.transcript_path // ""')
28
- SOURCE=$(echo "$INPUT" | jq -r '.source // "unknown"') # "startup", "resume", or "clear"
36
+
37
+ # Parse input using Node (no jq)
38
+ parse_json_value() {
39
+ local json="$1"
40
+ local path="$2"
41
+ echo "$json" | node -e "
42
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
43
+ const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
44
+ let result = data;
45
+ for (const p of path) {
46
+ if (result === undefined || result === null) { result = undefined; break; }
47
+ result = result[p];
48
+ }
49
+ if (result !== undefined && result !== null) console.log(result);
50
+ " 2>/dev/null || echo ""
51
+ }
52
+
53
+ SESSION_ID=$(parse_json_value "$INPUT" '.session_id')
54
+ [ -z "$SESSION_ID" ] && SESSION_ID="unknown"
55
+
56
+ TRANSCRIPT_PATH=$(parse_json_value "$INPUT" '.transcript_path')
57
+ SOURCE=$(parse_json_value "$INPUT" '.source')
58
+ [ -z "$SOURCE" ] && SOURCE="unknown"
29
59
 
30
60
  # ═══════════════════════════════════════════════════════════════════════════
31
61
  # Load auth
@@ -34,13 +64,16 @@ EKKOS_CONFIG="$HOME/.ekkos/config.json"
34
64
  AUTH_TOKEN=""
35
65
  USER_ID=""
36
66
 
37
- if [ -f "$EKKOS_CONFIG" ]; then
38
- AUTH_TOKEN=$(jq -r '.hookApiKey // .apiKey // ""' "$EKKOS_CONFIG" 2>/dev/null || echo "")
39
- USER_ID=$(jq -r '.userId // ""' "$EKKOS_CONFIG" 2>/dev/null || echo "")
67
+ if [ -f "$EKKOS_CONFIG" ] && [ -f "$JSON_PARSE_HELPER" ]; then
68
+ AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.hookApiKey' 2>/dev/null || echo "")
69
+ if [ -z "$AUTH_TOKEN" ]; then
70
+ AUTH_TOKEN=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.apiKey' 2>/dev/null || echo "")
71
+ fi
72
+ USER_ID=$(node "$JSON_PARSE_HELPER" "$EKKOS_CONFIG" '.userId' 2>/dev/null || echo "")
40
73
  fi
41
74
 
42
75
  if [ -z "$AUTH_TOKEN" ] && [ -f "$PROJECT_ROOT/.env.local" ]; then
43
- AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | tr -d '\r')
76
+ AUTH_TOKEN=$(grep -E "^SUPABASE_SECRET_KEY=" "$PROJECT_ROOT/.env.local" | cut -d'=' -f2- | tr -d '"' | tr -d "'" | tr -d '\r')
44
77
  fi
45
78
 
46
79
  [ -z "$AUTH_TOKEN" ] && exit 0
@@ -57,39 +90,55 @@ TIME_MACHINE_TO_TURN=""
57
90
 
58
91
  # Check via env var first, then API
59
92
  if [ -n "$RESTORE_REQUEST_ID" ]; then
60
- echo -e "\033[0;35mTime Machine request detected: $RESTORE_REQUEST_ID\033[0m" >&2
93
+ echo -e "\033[0;35m Time Machine request detected: $RESTORE_REQUEST_ID\033[0m" >&2
61
94
  fi
62
95
 
63
96
  # Check API for pending requests (if we have user_id)
64
97
  if [ -z "$TIME_MACHINE_SESSION" ] && [ -n "$USER_ID" ]; then
65
- PENDING_RESPONSE=$(curl -s -X GET "$MEMORY_API_URL/api/v1/context/restore-request/pending?user_id=$USER_ID" \
66
- -H "Authorization: Bearer $AUTH_TOKEN" \
67
- --connect-timeout 2 \
68
- --max-time 3 2>/dev/null || echo '{}')
69
-
70
- IS_PENDING=$(echo "$PENDING_RESPONSE" | jq -r '.pending // false' 2>/dev/null)
71
-
72
- if [ "$IS_PENDING" = "true" ]; then
73
- TIME_MACHINE_SESSION=$(echo "$PENDING_RESPONSE" | jq -r '.request.session_id // ""')
74
- TIME_MACHINE_FROM_TURN=$(echo "$PENDING_RESPONSE" | jq -r '.request.from_turn // ""')
75
- TIME_MACHINE_TO_TURN=$(echo "$PENDING_RESPONSE" | jq -r '.request.to_turn // ""')
76
- RESTORE_REQUEST_ID=$(echo "$PENDING_RESPONSE" | jq -r '.request.request_id // ""')
77
-
78
- if [ -n "$TIME_MACHINE_SESSION" ]; then
79
- echo "" >&2
80
- echo -e "\033[0;35m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m" >&2
81
- echo -e "\033[0;35m\033[1m⏰ TIME MACHINE\033[0m \033[2m| Restoring session from web request...\033[0m" >&2
82
- echo -e "\033[0;35m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m" >&2
83
-
84
- # Mark request as consumed
85
- curl -s -X POST "$MEMORY_API_URL/api/v1/context/restore-request/consume" \
98
+ PENDING_RESPONSE=$(curl -s -X GET "$MEMORY_API_URL/api/v1/context/restore-request/pending?user_id=$USER_ID" \
86
99
  -H "Authorization: Bearer $AUTH_TOKEN" \
87
- -H "Content-Type: application/json" \
88
- -d "{\"request_id\": \"$RESTORE_REQUEST_ID\"}" \
89
100
  --connect-timeout 2 \
90
- --max-time 3 >/dev/null 2>&1 || true
101
+ --max-time 3 2>/dev/null || echo '{}')
102
+
103
+ # Parse using Node (no jq)
104
+ IS_PENDING=$(echo "$PENDING_RESPONSE" | node -e "
105
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
106
+ console.log(data.pending || false);
107
+ " 2>/dev/null || echo "false")
108
+
109
+ if [ "$IS_PENDING" = "true" ]; then
110
+ TIME_MACHINE_SESSION=$(echo "$PENDING_RESPONSE" | node -e "
111
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
112
+ console.log(d.request?.session_id || '');
113
+ " 2>/dev/null || echo "")
114
+ TIME_MACHINE_FROM_TURN=$(echo "$PENDING_RESPONSE" | node -e "
115
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
116
+ console.log(d.request?.from_turn || '');
117
+ " 2>/dev/null || echo "")
118
+ TIME_MACHINE_TO_TURN=$(echo "$PENDING_RESPONSE" | node -e "
119
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
120
+ console.log(d.request?.to_turn || '');
121
+ " 2>/dev/null || echo "")
122
+ RESTORE_REQUEST_ID=$(echo "$PENDING_RESPONSE" | node -e "
123
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
124
+ console.log(d.request?.request_id || '');
125
+ " 2>/dev/null || echo "")
126
+
127
+ if [ -n "$TIME_MACHINE_SESSION" ]; then
128
+ echo "" >&2
129
+ echo -e "\033[0;35m------------------------------------------------------------------------\033[0m" >&2
130
+ echo -e "\033[0;35m\033[1m TIME MACHINE\033[0m \033[2m| Restoring session from web request...\033[0m" >&2
131
+ echo -e "\033[0;35m------------------------------------------------------------------------\033[0m" >&2
132
+
133
+ # Mark request as consumed
134
+ curl -s -X POST "$MEMORY_API_URL/api/v1/context/restore-request/consume" \
135
+ -H "Authorization: Bearer $AUTH_TOKEN" \
136
+ -H "Content-Type: application/json" \
137
+ -d "{\"request_id\": \"$RESTORE_REQUEST_ID\"}" \
138
+ --connect-timeout 2 \
139
+ --max-time 3 >/dev/null 2>&1 || true
140
+ fi
91
141
  fi
92
- fi
93
142
  fi
94
143
 
95
144
  # ═══════════════════════════════════════════════════════════════════════════
@@ -111,24 +160,24 @@ MOST_RECENT_SESSION=""
111
160
  SAVED_TURN_COUNT=0
112
161
 
113
162
  if [ -n "$CURRENT_SESSION_ID" ] && [ "$CURRENT_SESSION_ID" != "unknown" ]; then
114
- # Check if THIS session has saved turns (for /clear continuity)
115
- TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${CURRENT_SESSION_ID}.turn"
116
- if [ -f "$TURN_COUNTER_FILE" ]; then
117
- SAVED_TURN_COUNT=$(cat "$TURN_COUNTER_FILE" 2>/dev/null || echo "0")
118
- MOST_RECENT_SESSION="$CURRENT_SESSION_ID"
119
- else
120
- # Fresh start: find most recent session in project
121
- MOST_RECENT_FILE=$(ls -t "$PROJECT_SESSION_DIR"/*.turn 2>/dev/null | head -1)
122
- if [ -n "$MOST_RECENT_FILE" ]; then
123
- MOST_RECENT_SESSION=$(basename "$MOST_RECENT_FILE" .turn)
124
- SAVED_TURN_COUNT=$(cat "$MOST_RECENT_FILE" 2>/dev/null || echo "0")
163
+ # Check if THIS session has saved turns (for /clear continuity)
164
+ TURN_COUNTER_FILE="$PROJECT_SESSION_DIR/${CURRENT_SESSION_ID}.turn"
165
+ if [ -f "$TURN_COUNTER_FILE" ]; then
166
+ SAVED_TURN_COUNT=$(cat "$TURN_COUNTER_FILE" 2>/dev/null || echo "0")
167
+ MOST_RECENT_SESSION="$CURRENT_SESSION_ID"
168
+ else
169
+ # Fresh start: find most recent session in project
170
+ MOST_RECENT_FILE=$(ls -t "$PROJECT_SESSION_DIR"/*.turn 2>/dev/null | head -1)
171
+ if [ -n "$MOST_RECENT_FILE" ]; then
172
+ MOST_RECENT_SESSION=$(basename "$MOST_RECENT_FILE" .turn)
173
+ SAVED_TURN_COUNT=$(cat "$MOST_RECENT_FILE" 2>/dev/null || echo "0")
174
+ fi
125
175
  fi
126
- fi
127
176
  fi
128
177
 
129
178
  # Save current session info
130
179
  if [ -n "$CURRENT_SESSION_ID" ]; then
131
- cat > "$SESSION_FILE" << EOF
180
+ cat > "$SESSION_FILE" << EOF
132
181
  {
133
182
  "session_id": "$CURRENT_SESSION_ID",
134
183
  "timestamp": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
@@ -138,28 +187,23 @@ EOF
138
187
  fi
139
188
 
140
189
  # ═══════════════════════════════════════════════════════════════════════════
141
- # 🔄 GOLDEN LOOP: Initialize session tracking file
190
+ # GOLDEN LOOP: Initialize session tracking file
142
191
  # ═══════════════════════════════════════════════════════════════════════════
143
192
  GOLDEN_LOOP_FILE="$PROJECT_ROOT/.ekkos/golden-loop-current.json"
144
193
  mkdir -p "$PROJECT_ROOT/.ekkos" 2>/dev/null || true
145
194
 
146
- # Initialize with session start state
147
- jq -n \
148
- --arg phase "idle" \
149
- --argjson turn 0 \
150
- --arg session "${CURRENT_SESSION_ID}" \
151
- --arg timestamp "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
152
- '{
153
- phase: $phase,
154
- turn: $turn,
155
- session: $session,
156
- timestamp: $timestamp,
157
- stats: {
158
- retrieved: 0,
159
- applied: 0,
160
- forged: 0
161
- }
162
- }' > "$GOLDEN_LOOP_FILE" 2>/dev/null || true
195
+ # Initialize with session start state using Node (no jq)
196
+ node -e "
197
+ const fs = require('fs');
198
+ const data = {
199
+ phase: 'idle',
200
+ turn: 0,
201
+ session: '$CURRENT_SESSION_ID',
202
+ timestamp: new Date().toISOString(),
203
+ stats: { retrieved: 0, applied: 0, forged: 0 }
204
+ };
205
+ fs.writeFileSync('$GOLDEN_LOOP_FILE', JSON.stringify(data, null, 2));
206
+ " 2>/dev/null || true
163
207
 
164
208
  # ═══════════════════════════════════════════════════════════════════════════
165
209
  # COLORS
@@ -185,44 +229,56 @@ RESET='\033[0m'
185
229
 
186
230
  # Handle Time Machine requests (explicit user action)
187
231
  if [ -n "$TIME_MACHINE_SESSION" ]; then
188
- echo "" >&2
189
- echo -e "${MAGENTA}${BOLD}⏰ TIME MACHINE${RESET} ${DIM}| Restoring past session: ${TIME_MACHINE_SESSION:0:12}...${RESET}" >&2
190
-
191
- # Build recall request with turn range
192
- RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"last_n\": 15, \"format\": \"summary\"}"
193
-
194
- if [ -n "$TIME_MACHINE_FROM_TURN" ] && [ -n "$TIME_MACHINE_TO_TURN" ]; then
195
- RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"from_turn\": ${TIME_MACHINE_FROM_TURN}, \"to_turn\": ${TIME_MACHINE_TO_TURN}, \"format\": \"summary\"}"
196
- elif [ -n "$TIME_MACHINE_FROM_TURN" ]; then
197
- RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"from_turn\": ${TIME_MACHINE_FROM_TURN}, \"format\": \"summary\"}"
198
- fi
199
-
200
- # Fetch turns from L2
201
- RESTORE_RESPONSE=$(curl -s -X POST "$MEMORY_API_URL/api/v1/turns/recall" \
202
- -H "Authorization: Bearer $AUTH_TOKEN" \
203
- -H "Content-Type: application/json" \
204
- -d "$RECALL_BODY" \
205
- --connect-timeout 3 \
206
- --max-time 5 2>/dev/null || echo '{"error":"timeout"}')
207
-
208
- # Check if we got turns back
209
- RESTORED_COUNT=$(echo "$RESTORE_RESPONSE" | jq '.turns // [] | length' 2>/dev/null || echo "0")
210
-
211
- if [ "$RESTORED_COUNT" -gt 0 ]; then
212
- echo -e "${MAGENTA} ✓${RESET} Restored ${RESTORED_COUNT} turns from past session" >&2
213
- echo "" >&2
214
- echo -e "${MAGENTA}${BOLD}## Time Machine Context${RESET}" >&2
215
232
  echo "" >&2
233
+ echo -e "${MAGENTA}${BOLD} TIME MACHINE${RESET} ${DIM}| Restoring past session: ${TIME_MACHINE_SESSION:0:12}...${RESET}" >&2
216
234
 
217
- # Output the turns as context (stderr for user, stdout for Claude)
218
- TURNS_OUTPUT=$(echo "$RESTORE_RESPONSE" | jq -r '.turns[]? | "**Turn \(.turn_number // "?")**: \(.user_query[:100] // "...")...\n> \(.assistant_response[:200] // "...")...\n"' 2>/dev/null || true)
219
- echo "$TURNS_OUTPUT" >&2
220
- echo "$TURNS_OUTPUT" # Also to stdout for Claude's context
235
+ # Build recall request with turn range
236
+ RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"last_n\": 15, \"format\": \"summary\"}"
221
237
 
222
- echo "" >&2
223
- echo -e "${DIM}You've traveled to a past session. Continue from here!${RESET}" >&2
224
- echo "" >&2
225
- fi
238
+ if [ -n "$TIME_MACHINE_FROM_TURN" ] && [ -n "$TIME_MACHINE_TO_TURN" ]; then
239
+ RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"from_turn\": ${TIME_MACHINE_FROM_TURN}, \"to_turn\": ${TIME_MACHINE_TO_TURN}, \"format\": \"summary\"}"
240
+ elif [ -n "$TIME_MACHINE_FROM_TURN" ]; then
241
+ RECALL_BODY="{\"session_id\": \"${TIME_MACHINE_SESSION}\", \"from_turn\": ${TIME_MACHINE_FROM_TURN}, \"format\": \"summary\"}"
242
+ fi
243
+
244
+ # Fetch turns from L2
245
+ RESTORE_RESPONSE=$(curl -s -X POST "$MEMORY_API_URL/api/v1/turns/recall" \
246
+ -H "Authorization: Bearer $AUTH_TOKEN" \
247
+ -H "Content-Type: application/json" \
248
+ -d "$RECALL_BODY" \
249
+ --connect-timeout 3 \
250
+ --max-time 5 2>/dev/null || echo '{"error":"timeout"}')
251
+
252
+ # Check if we got turns back using Node (no jq)
253
+ RESTORED_COUNT=$(echo "$RESTORE_RESPONSE" | node -e "
254
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
255
+ console.log((d.turns || []).length);
256
+ " 2>/dev/null || echo "0")
257
+
258
+ if [ "$RESTORED_COUNT" -gt 0 ]; then
259
+ echo -e "${MAGENTA} ✓${RESET} Restored ${RESTORED_COUNT} turns from past session" >&2
260
+ echo "" >&2
261
+ echo -e "${MAGENTA}${BOLD}## Time Machine Context${RESET}" >&2
262
+ echo "" >&2
263
+
264
+ # Output the turns as context using Node (no jq)
265
+ TURNS_OUTPUT=$(echo "$RESTORE_RESPONSE" | node -e "
266
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
267
+ (d.turns || []).forEach(t => {
268
+ const q = (t.user_query || '').substring(0, 100);
269
+ const r = (t.assistant_response || '').substring(0, 200);
270
+ console.log('**Turn ' + (t.turn_number || '?') + '**: ' + q + '...');
271
+ console.log('> ' + r + '...');
272
+ console.log('');
273
+ });
274
+ " 2>/dev/null || echo "")
275
+ echo "$TURNS_OUTPUT" >&2
276
+ echo "$TURNS_OUTPUT"
277
+
278
+ echo "" >&2
279
+ echo -e "${DIM}You've traveled to a past session. Continue from here!${RESET}" >&2
280
+ echo "" >&2
281
+ fi
226
282
  fi
227
283
 
228
284
  # ═══════════════════════════════════════════════════════════════════════════
@@ -233,85 +289,64 @@ DIRECTIVE_COUNT=0
233
289
 
234
290
  # Only fetch if we have auth
235
291
  if [ -n "$AUTH_TOKEN" ]; then
236
- # Fetch directives (top 20 by priority to avoid token bloat)
237
- DIRECTIVES_RESPONSE=$(curl -s -X GET "$MEMORY_API_URL/api/v1/memory/directives?limit=20" \
238
- -H "Authorization: Bearer $AUTH_TOKEN" \
239
- --connect-timeout 2 \
240
- --max-time 3 2>/dev/null || echo '{}')
241
-
242
- # Parse response
243
- DIRECTIVE_COUNT=$(echo "$DIRECTIVES_RESPONSE" | jq '.count // 0' 2>/dev/null || echo "0")
244
-
245
- if [ "$DIRECTIVE_COUNT" -gt 0 ]; then
246
- DIRECTIVES_INJECTED=true
247
-
248
- # Extract MUST/NEVER/PREFER/AVOID arrays
249
- MUST_RULES=$(echo "$DIRECTIVES_RESPONSE" | jq -r '.MUST[]?.rule // empty' 2>/dev/null | head -5)
250
- NEVER_RULES=$(echo "$DIRECTIVES_RESPONSE" | jq -r '.NEVER[]?.rule // empty' 2>/dev/null | head -5)
251
- PREFER_RULES=$(echo "$DIRECTIVES_RESPONSE" | jq -r '.PREFER[]?.rule // empty' 2>/dev/null | head -5)
252
- AVOID_RULES=$(echo "$DIRECTIVES_RESPONSE" | jq -r '.AVOID[]?.rule // empty' 2>/dev/null | head -5)
253
-
254
- # Build compact directive block for injection
255
- echo "<system-reminder>"
256
- echo "USER DIRECTIVES (FOLLOW THESE):"
257
- echo ""
258
-
259
- if [ -n "$MUST_RULES" ]; then
260
- echo "MUST:"
261
- echo "$MUST_RULES" | while read -r rule; do
262
- [ -n "$rule" ] && echo " • $rule"
263
- done
264
- fi
265
-
266
- if [ -n "$NEVER_RULES" ]; then
267
- echo "NEVER:"
268
- echo "$NEVER_RULES" | while read -r rule; do
269
- [ -n "$rule" ] && echo " • $rule"
270
- done
271
- fi
272
-
273
- if [ -n "$PREFER_RULES" ]; then
274
- echo "PREFER:"
275
- echo "$PREFER_RULES" | while read -r rule; do
276
- [ -n "$rule" ] && echo " • $rule"
277
- done
278
- fi
292
+ # Fetch directives (top 20 by priority to avoid token bloat)
293
+ DIRECTIVES_RESPONSE=$(curl -s -X GET "$MEMORY_API_URL/api/v1/memory/directives?limit=20" \
294
+ -H "Authorization: Bearer $AUTH_TOKEN" \
295
+ --connect-timeout 2 \
296
+ --max-time 3 2>/dev/null || echo '{}')
297
+
298
+ # Parse response using Node (no jq)
299
+ DIRECTIVE_COUNT=$(echo "$DIRECTIVES_RESPONSE" | node -e "
300
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
301
+ console.log(d.count || 0);
302
+ " 2>/dev/null || echo "0")
303
+
304
+ if [ "$DIRECTIVE_COUNT" -gt 0 ]; then
305
+ DIRECTIVES_INJECTED=true
306
+
307
+ # Extract MUST/NEVER/PREFER/AVOID arrays using Node
308
+ echo "<system-reminder>"
309
+ echo "USER DIRECTIVES (FOLLOW THESE):"
310
+ echo ""
311
+
312
+ echo "$DIRECTIVES_RESPONSE" | node -e "
313
+ const d = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
314
+ const types = ['MUST', 'NEVER', 'PREFER', 'AVOID'];
315
+ types.forEach(type => {
316
+ const rules = (d[type] || []).slice(0, 5);
317
+ if (rules.length > 0) {
318
+ console.log(type + ':');
319
+ rules.forEach(r => console.log(' - ' + (r.rule || '')));
320
+ }
321
+ });
322
+ " 2>/dev/null || true
279
323
 
280
- if [ -n "$AVOID_RULES" ]; then
281
- echo "AVOID:"
282
- echo "$AVOID_RULES" | while read -r rule; do
283
- [ -n "$rule" ] && echo " • $rule"
284
- done
324
+ echo "</system-reminder>"
325
+ echo -e "${GREEN} ${DIRECTIVE_COUNT} directives loaded${RESET}" >&2
285
326
  fi
286
-
287
- echo "</system-reminder>"
288
-
289
- echo -e "${GREEN}📋 ${DIRECTIVE_COUNT} directives loaded${RESET}" >&2
290
- fi
291
327
  fi
292
328
 
293
329
  # Simple status display (no auto-restore)
294
330
  if [ "$SAVED_TURN_COUNT" -gt 0 ]; then
295
- # New session or few turns - just show status
296
- echo "" >&2
297
- if [ "$SAVED_TURN_COUNT" -gt 0 ]; then
298
- echo -e "${CYAN}${BOLD}🧠 ekkOS${RESET} ${DIM}|${RESET} Session: ${CURRENT_SESSION_ID:-$SESSION_ID} ${DIM}|${RESET} ${GREEN}${SAVED_TURN_COUNT} turns${RESET}" >&2
299
- else
300
- echo -e "${CYAN}${BOLD}🧠 ekkOS${RESET} ${DIM}|${RESET} Session: ${CURRENT_SESSION_ID:-$SESSION_ID} ${DIM}| New session${RESET}" >&2
301
- fi
331
+ echo "" >&2
332
+ if [ "$SAVED_TURN_COUNT" -gt 0 ]; then
333
+ echo -e "${CYAN}${BOLD} ekkOS${RESET} ${DIM}|${RESET} Session: ${CURRENT_SESSION_ID:-$SESSION_ID} ${DIM}|${RESET} ${GREEN}${SAVED_TURN_COUNT} turns${RESET}" >&2
334
+ else
335
+ echo -e "${CYAN}${BOLD} ekkOS${RESET} ${DIM}|${RESET} Session: ${CURRENT_SESSION_ID:-$SESSION_ID} ${DIM}| New session${RESET}" >&2
336
+ fi
302
337
  fi
303
338
 
304
339
  # Final confirmation that's always visible
305
340
  if [ -n "$TIME_MACHINE_SESSION" ]; then
306
- echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
307
- echo -e "${MAGENTA}⏰${RESET} Time Machine active · Restored from session ${TIME_MACHINE_SESSION:0:12}..." >&2
308
- echo -e "${MAGENTA}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
341
+ echo -e "${MAGENTA}------------------------------------------------------------------------${RESET}" >&2
342
+ echo -e "${MAGENTA} ${RESET} Time Machine active - Restored from session ${TIME_MACHINE_SESSION:0:12}..." >&2
343
+ echo -e "${MAGENTA}------------------------------------------------------------------------${RESET}" >&2
309
344
  elif [ "$SAVED_TURN_COUNT" -gt 0 ]; then
310
- echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
311
- echo -e "${GREEN}✓${RESET} Session continued · ${SAVED_TURN_COUNT} turns preserved · Ready to resume" >&2
312
- echo -e "${GREEN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${RESET}" >&2
345
+ echo -e "${GREEN}------------------------------------------------------------------------${RESET}" >&2
346
+ echo -e "${GREEN}✓${RESET} Session continued - ${SAVED_TURN_COUNT} turns preserved - Ready to resume" >&2
347
+ echo -e "${GREEN}------------------------------------------------------------------------${RESET}" >&2
313
348
  else
314
- echo -e "${CYAN}✓${RESET} New session started" >&2
349
+ echo -e "${CYAN}✓${RESET} New session started" >&2
315
350
  fi
316
351
  echo "" >&2
317
352
 
@@ -1,14 +1,135 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
- # ekkOS_ Hook: Stop - Session cleanup (Windows)
2
+ # ekkOS_ Hook: Stop - Session cleanup and capture finalization (Windows)
3
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
+ # EKKOS_MANAGED=1
5
+ # EKKOS_MANIFEST_SHA256=<computed-at-build>
6
+ # EKKOS_TEMPLATE_VERSION=1.0.0
7
+ #
8
+ # Per ekkOS Onboarding Spec v1.2 FINAL + ADDENDUM:
9
+ # - All persisted records MUST include: instanceId, sessionId, sessionName
10
+ # - Uses EKKOS_INSTANCE_ID env var for multi-session isolation
3
11
  # ═══════════════════════════════════════════════════════════════════════════
4
12
 
5
13
  $ErrorActionPreference = "SilentlyContinue"
6
14
 
7
- # Read input
15
+ # ═══════════════════════════════════════════════════════════════════════════
16
+ # CONFIG PATHS
17
+ # ═══════════════════════════════════════════════════════════════════════════
18
+ $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
19
+ $SessionWordsJson = "$EkkosConfigDir\session-words.json"
20
+ $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
21
+
22
+ # ═══════════════════════════════════════════════════════════════════════════
23
+ # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
24
+ # ═══════════════════════════════════════════════════════════════════════════
25
+ $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
26
+
27
+ # ═══════════════════════════════════════════════════════════════════════════
28
+ # Load session words from JSON file - NO HARDCODED ARRAYS
29
+ # ═══════════════════════════════════════════════════════════════════════════
30
+ $script:SessionWords = $null
31
+
32
+ function Load-SessionWords {
33
+ $wordsFile = $SessionWordsJson
34
+
35
+ if (-not (Test-Path $wordsFile)) {
36
+ $wordsFile = $SessionWordsDefault
37
+ }
38
+
39
+ if (-not (Test-Path $wordsFile)) {
40
+ return $null
41
+ }
42
+
43
+ try {
44
+ $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
45
+ } catch {
46
+ return $null
47
+ }
48
+ }
49
+
50
+ function Convert-UuidToWords {
51
+ param([string]$uuid)
52
+
53
+ if (-not $script:SessionWords) {
54
+ Load-SessionWords
55
+ }
56
+
57
+ if (-not $script:SessionWords) {
58
+ return "unknown-session"
59
+ }
60
+
61
+ $adjectives = $script:SessionWords.adjectives
62
+ $nouns = $script:SessionWords.nouns
63
+ $verbs = $script:SessionWords.verbs
64
+
65
+ if (-not $adjectives -or -not $nouns -or -not $verbs) {
66
+ return "unknown-session"
67
+ }
68
+
69
+ if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
70
+
71
+ $clean = $uuid -replace "-", ""
72
+ if ($clean.Length -lt 6) { return "unknown-session" }
73
+
74
+ try {
75
+ $a = [Convert]::ToInt32($clean.Substring(0,2), 16) % $adjectives.Length
76
+ $n = [Convert]::ToInt32($clean.Substring(2,2), 16) % $nouns.Length
77
+ $an = [Convert]::ToInt32($clean.Substring(4,2), 16) % $verbs.Length
78
+
79
+ return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
80
+ } catch {
81
+ return "unknown-session"
82
+ }
83
+ }
84
+
85
+ # ═══════════════════════════════════════════════════════════════════════════
86
+ # READ INPUT
87
+ # ═══════════════════════════════════════════════════════════════════════════
8
88
  $inputJson = [Console]::In.ReadToEnd()
9
89
 
10
- # Clean up state
90
+ # Get session ID from state
11
91
  $stateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
92
+ $sessionFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
93
+ $rawSessionId = "unknown"
94
+ $turn = 0
95
+
96
+ if (Test-Path $stateFile) {
97
+ try {
98
+ $hookState = Get-Content $stateFile -Raw | ConvertFrom-Json
99
+ $rawSessionId = $hookState.session_id
100
+ $turn = [int]$hookState.turn
101
+ } catch {}
102
+ }
103
+
104
+ if ($rawSessionId -eq "unknown" -and (Test-Path $sessionFile)) {
105
+ try {
106
+ $sessionData = Get-Content $sessionFile -Raw | ConvertFrom-Json
107
+ $rawSessionId = $sessionData.session_id
108
+ } catch {}
109
+ }
110
+
111
+ $sessionName = Convert-UuidToWords $rawSessionId
112
+
113
+ # ═══════════════════════════════════════════════════════════════════════════
114
+ # LOCAL CACHE: ACK turn to mark as synced
115
+ # Per v1.2 ADDENDUM: Pass instanceId for namespacing
116
+ # ═══════════════════════════════════════════════════════════════════════════
117
+ $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
118
+ if ($captureCmd -and $rawSessionId -ne "unknown") {
119
+ try {
120
+ # ACK format: ekkos-capture ack <session_id> <turn_id> --instance=ID
121
+ Start-Job -ScriptBlock {
122
+ param($instanceId, $sessionId, $turnNum)
123
+ try {
124
+ & ekkos-capture ack $sessionId $turnNum "--instance=$instanceId" 2>&1 | Out-Null
125
+ } catch {}
126
+ } -ArgumentList $EkkosInstanceId, $rawSessionId, $turn | Out-Null
127
+ } catch {}
128
+ }
129
+
130
+ # ═══════════════════════════════════════════════════════════════════════════
131
+ # CLEAN UP STATE FILES
132
+ # ═══════════════════════════════════════════════════════════════════════════
12
133
  if (Test-Path $stateFile) {
13
134
  Remove-Item $stateFile -Force
14
135
  }