@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,14 @@
1
1
  #!/bin/bash
2
- # Post-response hook: Validates and enforces ekkOS footer format
2
+ # ═══════════════════════════════════════════════════════════════════════════
3
+ # ekkOS_ Hook: AssistantResponse - Validates and enforces footer format
4
+ # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
5
+ # EKKOS_MANAGED=1
6
+ # ═══════════════════════════════════════════════════════════════════════════
3
7
  # Runs AFTER assistant response, checks footer compliance
8
+ # Per spec v1.2 Addendum: NO jq, NO hardcoded arrays
9
+ # ═══════════════════════════════════════════════════════════════════════════
10
+
11
+ set +e
4
12
 
5
13
  RESPONSE_FILE="$1"
6
14
  HOOK_ENV="$2"
@@ -10,57 +18,109 @@ if [[ ! -f "$RESPONSE_FILE" ]]; then
10
18
  exit 0
11
19
  fi
12
20
 
13
- # Parse metadata from hook environment
14
- SESSION_ID=$(echo "$HOOK_ENV" | jq -r '.sessionId // "unknown"')
15
- TURN=$(echo "$HOOK_ENV" | jq -r '.turn // 0')
16
- CONTEXT_PERCENT=$(echo "$HOOK_ENV" | jq -r '.contextUsagePercent // 0')
17
- MODEL=$(echo "$HOOK_ENV" | jq -r '.model // "Claude Code (Opus 4.5)"')
21
+ # ═══════════════════════════════════════════════════════════════════════════
22
+ # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
23
+ # ═══════════════════════════════════════════════════════════════════════════
24
+ EKKOS_CONFIG_DIR="${EKKOS_CONFIG_DIR:-$HOME/.ekkos}"
25
+ SESSION_WORDS_JSON="$EKKOS_CONFIG_DIR/session-words.json"
26
+ SESSION_WORDS_DEFAULT="$EKKOS_CONFIG_DIR/.defaults/session-words.json"
27
+ JSON_PARSE_HELPER="$EKKOS_CONFIG_DIR/.helpers/json-parse.cjs"
28
+
29
+ # Parse metadata from hook environment using Node (no jq)
30
+ parse_hook_env() {
31
+ local json="$1"
32
+ local path="$2"
33
+ echo "$json" | node -e "
34
+ const data = JSON.parse(require('fs').readFileSync('/dev/stdin', 'utf8') || '{}');
35
+ const path = '$path'.replace(/^\./,'').split('.').filter(Boolean);
36
+ let result = data;
37
+ for (const p of path) {
38
+ if (result === undefined || result === null) { result = undefined; break; }
39
+ result = result[p];
40
+ }
41
+ if (result !== undefined && result !== null) console.log(result);
42
+ " 2>/dev/null || echo ""
43
+ }
44
+
45
+ SESSION_ID=$(parse_hook_env "$HOOK_ENV" '.sessionId')
46
+ [ -z "$SESSION_ID" ] && SESSION_ID="unknown"
47
+
48
+ TURN=$(parse_hook_env "$HOOK_ENV" '.turn')
49
+ [ -z "$TURN" ] && TURN="0"
50
+
51
+ CONTEXT_PERCENT=$(parse_hook_env "$HOOK_ENV" '.contextUsagePercent')
52
+ [ -z "$CONTEXT_PERCENT" ] && CONTEXT_PERCENT="0"
53
+
54
+ MODEL=$(parse_hook_env "$HOOK_ENV" '.model')
55
+ [ -z "$MODEL" ] && MODEL="Claude Code (Opus 4.5)"
56
+
57
+ # ═══════════════════════════════════════════════════════════════════════════
58
+ # Session name conversion - Uses external session-words.json (NO hardcoded arrays)
59
+ # ═══════════════════════════════════════════════════════════════════════════
60
+ declare -a ADJECTIVES
61
+ declare -a NOUNS
62
+ SESSION_WORDS_LOADED=false
63
+
64
+ load_session_words() {
65
+ if [ "$SESSION_WORDS_LOADED" = "true" ]; then
66
+ return 0
67
+ fi
68
+
69
+ local words_file="$SESSION_WORDS_JSON"
70
+ if [ ! -f "$words_file" ]; then
71
+ words_file="$SESSION_WORDS_DEFAULT"
72
+ fi
73
+
74
+ if [ ! -f "$words_file" ] || [ ! -f "$JSON_PARSE_HELPER" ]; then
75
+ return 1
76
+ fi
77
+
78
+ if command -v node &>/dev/null; then
79
+ if [ "${BASH_VERSINFO[0]}" -ge 4 ]; then
80
+ readarray -t ADJECTIVES < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
81
+ readarray -t NOUNS < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
82
+ else
83
+ local i=0
84
+ while IFS= read -r line; do
85
+ ADJECTIVES[i]="$line"
86
+ ((i++))
87
+ done < <(node "$JSON_PARSE_HELPER" "$words_file" '.adjectives' 2>/dev/null)
88
+ i=0
89
+ while IFS= read -r line; do
90
+ NOUNS[i]="$line"
91
+ ((i++))
92
+ done < <(node "$JSON_PARSE_HELPER" "$words_file" '.nouns' 2>/dev/null)
93
+ fi
94
+
95
+ if [ ${#ADJECTIVES[@]} -gt 0 ] && [ ${#NOUNS[@]} -gt 0 ]; then
96
+ SESSION_WORDS_LOADED=true
97
+ return 0
98
+ fi
99
+ fi
100
+ return 1
101
+ }
18
102
 
19
- # Convert session UUID to word-based name
20
103
  convert_uuid_to_name() {
21
- local uuid="$1"
22
-
23
- # Word lists (same as MCP)
24
- local ADJECTIVES=("cosmic" "turbo" "mega" "hyper" "quantum" "atomic" "stellar" "epic"
25
- "mighty" "groovy" "zippy" "snappy" "jazzy" "funky" "zesty" "peppy"
26
- "spicy" "crispy" "fluffy" "sparkly" "chunky" "bouncy" "bubbly" "sassy"
27
- "slick" "sleek" "bold" "nifty" "perky" "plucky" "witty" "nimble"
28
- "dapper" "fancy" "quirky" "punchy" "swift" "brave" "clever" "dandy"
29
- "eager" "fiery" "golden" "hasty" "icy" "jolly" "keen" "lively"
30
- "merry" "noble" "odd" "plush" "quick" "royal" "silly" "tidy"
31
- "ultra" "vivid" "wacky" "zany" "alpha" "beta" "cyber" "delta"
32
- "electric" "foggy" "giga" "hazy" "ionic" "jumpy" "kinky" "lunar"
33
- "magic" "nerdy" "omega" "pixel" "quaint" "retro" "solar" "techno"
34
- "unified" "viral" "wonky" "xerox" "yappy" "zen" "agile" "binary"
35
- "chrome" "disco" "elastic" "fizzy" "glossy" "humble" "itchy" "jiffy"
36
- "kooky" "loopy" "moody" "noisy")
37
-
38
- local NOUNS=("penguin" "panda" "otter" "narwhal" "alpaca" "llama" "badger" "walrus"
39
- "waffle" "pickle" "noodle" "pretzel" "muffin" "taco" "nugget" "biscuit"
40
- "rocket" "comet" "nebula" "quasar" "meteor" "photon" "pulsar" "nova"
41
- "ninja" "pirate" "wizard" "robot" "yeti" "phoenix" "sphinx" "kraken"
42
- "thunder" "blizzard" "tornado" "avalanche" "mango" "kiwi" "banana" "coconut"
43
- "donut" "espresso" "falafel" "gyro" "hummus" "icecream" "jambon" "kebab"
44
- "latte" "mocha" "nachos" "olive" "pasta" "quinoa" "ramen" "sushi"
45
- "tamale" "udon" "velvet" "wasabi" "xmas" "yogurt" "ziti" "anchor"
46
- "beacon" "canyon" "drifter" "echo" "falcon" "glacier" "harbor" "island"
47
- "jetpack" "kayak" "lagoon" "meadow" "orbit" "parrot" "quest"
48
- "rapids" "summit" "tunnel" "umbrella" "volcano" "whisper" "xylophone" "yacht"
49
- "zephyr" "acorn" "bobcat" "cactus" "dolphin" "eagle" "ferret" "gopher"
50
- "hedgehog" "iguana" "jackal" "koala")
51
-
52
- # Extract first 8 hex chars
53
- local hex="${uuid:0:8}"
54
- hex="${hex//-/}"
55
-
56
- # Convert to number
57
- local num=$((16#$hex))
58
-
59
- # Calculate indices
60
- local adj_idx=$((num % 100))
61
- local noun_idx=$(((num / 100) % 100))
62
-
63
- echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}"
104
+ local uuid="$1"
105
+
106
+ load_session_words || {
107
+ echo "unknown-session"
108
+ return
109
+ }
110
+
111
+ local hex="${uuid:0:8}"
112
+ hex="${hex//-/}"
113
+
114
+ if [[ ! "$hex" =~ ^[0-9a-fA-F]+$ ]]; then
115
+ echo "unknown-session"
116
+ return
117
+ fi
118
+
119
+ local num=$((16#$hex))
120
+ local adj_idx=$((num % ${#ADJECTIVES[@]}))
121
+ local noun_idx=$(((num / ${#ADJECTIVES[@]}) % ${#NOUNS[@]}))
122
+
123
+ echo "${ADJECTIVES[$adj_idx]}-${NOUNS[$noun_idx]}"
64
124
  }
65
125
 
66
126
  SESSION_NAME=$(convert_uuid_to_name "$SESSION_ID")
@@ -68,7 +128,7 @@ TIMESTAMP=$(date "+%Y-%m-%d %I:%M %p %Z")
68
128
 
69
129
  # Required footer format
70
130
  REQUIRED_FOOTER="---
71
- $MODEL · $SESSION_NAME · Turn $TURN · ${CONTEXT_PERCENT}% · 🧠 **ekkOS_™** · 📅 $TIMESTAMP"
131
+ $MODEL - $SESSION_NAME - Turn $TURN - ${CONTEXT_PERCENT}% - ekkOS - $TIMESTAMP"
72
132
 
73
133
  # Check if response has correct footer
74
134
  RESPONSE_CONTENT=$(cat "$RESPONSE_FILE")
@@ -76,21 +136,21 @@ LAST_LINE=$(echo "$RESPONSE_CONTENT" | tail -1)
76
136
 
77
137
  # Check if footer exists and is correct
78
138
  if [[ "$LAST_LINE" == *"ekkOS"* ]] && [[ "$LAST_LINE" == *"Turn"* ]]; then
79
- # Footer exists - validate format
80
- if [[ "$LAST_LINE" == *"Turn $TURN"* ]] && [[ "$LAST_LINE" == *"${CONTEXT_PERCENT}%"* ]] && [[ "$LAST_LINE" == *"$SESSION_NAME"* ]]; then
81
- # Footer is correct
82
- exit 0
83
- else
84
- # Footer exists but is malformed - replace it
85
- RESPONSE_WITHOUT_FOOTER=$(echo "$RESPONSE_CONTENT" | head -n -2) # Remove last 2 lines (--- and footer)
86
- echo "$RESPONSE_WITHOUT_FOOTER" > "$RESPONSE_FILE"
139
+ # Footer exists - validate format
140
+ if [[ "$LAST_LINE" == *"Turn $TURN"* ]] && [[ "$LAST_LINE" == *"${CONTEXT_PERCENT}%"* ]] && [[ "$LAST_LINE" == *"$SESSION_NAME"* ]]; then
141
+ # Footer is correct
142
+ exit 0
143
+ else
144
+ # Footer exists but is malformed - replace it
145
+ RESPONSE_WITHOUT_FOOTER=$(echo "$RESPONSE_CONTENT" | head -n -2)
146
+ echo "$RESPONSE_WITHOUT_FOOTER" > "$RESPONSE_FILE"
147
+ echo "" >> "$RESPONSE_FILE"
148
+ echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
149
+ fi
150
+ else
151
+ # Footer missing - append it
87
152
  echo "" >> "$RESPONSE_FILE"
88
153
  echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
89
- fi
90
- else
91
- # Footer missing - append it
92
- echo "" >> "$RESPONSE_FILE"
93
- echo "$REQUIRED_FOOTER" >> "$RESPONSE_FILE"
94
154
  fi
95
155
 
96
156
  exit 0
@@ -1,10 +1,90 @@
1
1
  # ═══════════════════════════════════════════════════════════════════════════
2
2
  # ekkOS_ Hook: SessionStart - Initialize session (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 - No hardcoded word arrays per spec v1.2 Addendum
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
  try {
@@ -14,7 +94,11 @@ try {
14
94
  $sessionId = "unknown"
15
95
  }
16
96
 
17
- # Initialize state directory
97
+ $sessionName = Convert-UuidToWords $sessionId
98
+
99
+ # ═══════════════════════════════════════════════════════════════════════════
100
+ # INITIALIZE STATE DIRECTORY
101
+ # ═══════════════════════════════════════════════════════════════════════════
18
102
  $stateDir = Join-Path $env:USERPROFILE ".claude\state"
19
103
  if (-not (Test-Path $stateDir)) {
20
104
  New-Item -ItemType Directory -Path $stateDir -Force | Out-Null
@@ -25,6 +109,8 @@ $stateFile = Join-Path $stateDir "hook-state.json"
25
109
  $state = @{
26
110
  turn = 0
27
111
  session_id = $sessionId
112
+ session_name = $sessionName
113
+ instance_id = $EkkosInstanceId
28
114
  started_at = (Get-Date).ToString("o")
29
115
  } | ConvertTo-Json
30
116
 
@@ -34,8 +120,27 @@ Set-Content -Path $stateFile -Value $state -Force
34
120
  $sessionFile = Join-Path $stateDir "current-session.json"
35
121
  $sessionData = @{
36
122
  session_id = $sessionId
123
+ session_name = $sessionName
124
+ instance_id = $EkkosInstanceId
37
125
  } | ConvertTo-Json
38
126
 
39
127
  Set-Content -Path $sessionFile -Value $sessionData -Force
40
128
 
129
+ # ═══════════════════════════════════════════════════════════════════════════
130
+ # LOCAL CACHE: Initialize session in Tier 0 cache
131
+ # Per v1.2 ADDENDUM: Pass instanceId for namespacing
132
+ # ═══════════════════════════════════════════════════════════════════════════
133
+ $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
134
+ if ($captureCmd -and $sessionId -ne "unknown") {
135
+ try {
136
+ # NEW format: ekkos-capture init <instance_id> <session_id> <session_name>
137
+ Start-Job -ScriptBlock {
138
+ param($instanceId, $sessId, $sessName)
139
+ try {
140
+ & ekkos-capture init $instanceId $sessId $sessName 2>&1 | Out-Null
141
+ } catch {}
142
+ } -ArgumentList $EkkosInstanceId, $sessionId, $sessionName | Out-Null
143
+ } catch {}
144
+ }
145
+
41
146
  Write-Output "ekkOS session initialized"