@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.
- package/dist/cache/LocalSessionStore.d.ts +34 -21
- package/dist/cache/LocalSessionStore.js +169 -53
- package/dist/cache/capture.d.ts +19 -11
- package/dist/cache/capture.js +243 -76
- package/dist/cache/types.d.ts +14 -1
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +148 -73
- package/dist/commands/hooks.d.ts +109 -0
- package/dist/commands/hooks.js +668 -0
- package/dist/commands/run.d.ts +1 -0
- package/dist/commands/run.js +69 -21
- package/dist/index.js +42 -1
- package/dist/restore/RestoreOrchestrator.d.ts +17 -3
- package/dist/restore/RestoreOrchestrator.js +64 -22
- package/dist/utils/paths.d.ts +125 -0
- package/dist/utils/paths.js +283 -0
- package/dist/utils/session-words.json +30 -111
- package/package.json +1 -1
- package/templates/ekkos-manifest.json +223 -0
- package/templates/helpers/json-parse.cjs +101 -0
- package/templates/hooks/assistant-response.ps1 +256 -0
- package/templates/hooks/assistant-response.sh +124 -64
- package/templates/hooks/session-start.ps1 +107 -2
- package/templates/hooks/session-start.sh +201 -166
- package/templates/hooks/stop.ps1 +124 -3
- package/templates/hooks/stop.sh +470 -843
- package/templates/hooks/user-prompt-submit.ps1 +107 -22
- package/templates/hooks/user-prompt-submit.sh +403 -393
- package/templates/project-stubs/session-start.ps1 +63 -0
- package/templates/project-stubs/session-start.sh +55 -0
- package/templates/project-stubs/stop.ps1 +63 -0
- package/templates/project-stubs/stop.sh +55 -0
- package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
- package/templates/project-stubs/user-prompt-submit.sh +55 -0
- package/templates/shared/hooks-enabled.json +22 -0
- 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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
#
|
|
218
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
-
|
|
281
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
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
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
349
|
+
echo -e "${CYAN}✓${RESET} New session started" >&2
|
|
315
350
|
fi
|
|
316
351
|
echo "" >&2
|
|
317
352
|
|
package/templates/hooks/stop.ps1
CHANGED
|
@@ -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
|
-
#
|
|
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
|
-
#
|
|
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
|
}
|