@ekkos/cli 1.0.36 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,42 +4,29 @@ exports.deployClaudeSettings = deployClaudeSettings;
4
4
  exports.areHooksConfigured = areHooksConfigured;
5
5
  const fs_1 = require("fs");
6
6
  const platform_1 = require("../utils/platform");
7
+ function makeHook(command) {
8
+ return [{ matcher: '*', hooks: [{ type: 'command', command, timeout: 5000 }] }];
9
+ }
7
10
  /**
8
11
  * Generate the hooks configuration for Claude Code settings.json
12
+ * Uses the correct Claude Code format: [{ matcher, hooks: [{ type, command, timeout }] }]
9
13
  */
10
14
  function generateHooksConfig() {
11
- const hooksDir = `${platform_1.HOME_DIR}/.claude/hooks`;
15
+ const hooksDir = platform_1.CLAUDE_HOOKS_DIR;
12
16
  if (platform_1.isWindows) {
13
- // Windows uses PowerShell
17
+ const ps = (name) => `powershell -ExecutionPolicy Bypass -File "${hooksDir}\\${name}.ps1"`;
14
18
  return {
15
- SessionStart: [
16
- { type: 'command', command: `powershell -ExecutionPolicy Bypass -File "${hooksDir}/session-start.ps1"` }
17
- ],
18
- UserPromptSubmit: [
19
- { type: 'command', command: `powershell -ExecutionPolicy Bypass -File "${hooksDir}/user-prompt-submit.ps1"` }
20
- ],
21
- Stop: [
22
- { type: 'command', command: `powershell -ExecutionPolicy Bypass -File "${hooksDir}/stop.ps1"` }
23
- ],
24
- AssistantResponse: [
25
- { type: 'command', command: `powershell -ExecutionPolicy Bypass -File "${hooksDir}/assistant-response.ps1"` }
26
- ]
19
+ SessionStart: makeHook(ps('session-start')),
20
+ UserPromptSubmit: makeHook(ps('user-prompt-submit')),
21
+ Stop: makeHook(ps('stop')),
27
22
  };
28
23
  }
29
24
  // Unix uses bash
25
+ const sh = (name) => `bash "${hooksDir}/${name}.sh"`;
30
26
  return {
31
- SessionStart: [
32
- { type: 'command', command: `bash ${hooksDir}/session-start.sh` }
33
- ],
34
- UserPromptSubmit: [
35
- { type: 'command', command: `bash ${hooksDir}/user-prompt-submit.sh` }
36
- ],
37
- Stop: [
38
- { type: 'command', command: `bash ${hooksDir}/stop.sh` }
39
- ],
40
- AssistantResponse: [
41
- { type: 'command', command: `bash ${hooksDir}/assistant-response.sh` }
42
- ]
27
+ SessionStart: makeHook(sh('session-start')),
28
+ UserPromptSubmit: makeHook(sh('user-prompt-submit')),
29
+ Stop: makeHook(sh('stop')),
43
30
  };
44
31
  }
45
32
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.0.36",
3
+ "version": "1.1.0",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -2,66 +2,117 @@
2
2
  # ekkOS_ Hook: AssistantResponse - Process Claude's response (Windows)
3
3
  # MANAGED BY ekkos-connect - DO NOT EDIT DIRECTLY
4
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
5
11
  # ═══════════════════════════════════════════════════════════════════════════
6
12
 
7
13
  $ErrorActionPreference = "SilentlyContinue"
8
14
 
9
15
  # ═══════════════════════════════════════════════════════════════════════════
10
- # CONFIG PATHS
16
+ # CONFIG PATHS - No hardcoded word arrays per spec v1.2 Addendum
11
17
  # ═══════════════════════════════════════════════════════════════════════════
12
18
  $EkkosConfigDir = if ($env:EKKOS_CONFIG_DIR) { $env:EKKOS_CONFIG_DIR } else { "$env:USERPROFILE\.ekkos" }
13
19
  $HooksEnabledJson = "$EkkosConfigDir\hooks-enabled.json"
14
20
  $HooksEnabledDefault = "$EkkosConfigDir\.defaults\hooks-enabled.json"
15
21
  $SessionWordsJson = "$EkkosConfigDir\session-words.json"
16
22
  $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
17
- $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
18
23
 
19
- $MemoryApiUrl = "https://api.ekkos.dev"
24
+ # ═══════════════════════════════════════════════════════════════════════════
25
+ # INSTANCE ID - Multi-session isolation per v1.2 ADDENDUM
26
+ # ═══════════════════════════════════════════════════════════════════════════
27
+ $EkkosInstanceId = if ($env:EKKOS_INSTANCE_ID) { $env:EKKOS_INSTANCE_ID } else { "default" }
20
28
 
21
29
  # ═══════════════════════════════════════════════════════════════════════════
22
- # CHECK ENABLEMENT
30
+ # CHECK ENABLEMENT - Respect hooks-enabled.json
23
31
  # ═══════════════════════════════════════════════════════════════════════════
24
32
  function Test-HookEnabled {
25
33
  param([string]$HookName)
34
+
26
35
  $enabledFile = $HooksEnabledJson
27
- if (-not (Test-Path $enabledFile)) { $enabledFile = $HooksEnabledDefault }
28
- if (-not (Test-Path $enabledFile)) { return $true }
36
+ if (-not (Test-Path $enabledFile)) {
37
+ $enabledFile = $HooksEnabledDefault
38
+ }
39
+
40
+ if (-not (Test-Path $enabledFile)) {
41
+ # No enablement file = all enabled by default
42
+ return $true
43
+ }
44
+
29
45
  try {
30
46
  $config = Get-Content $enabledFile -Raw | ConvertFrom-Json
47
+
48
+ # Check claude.enabled array
31
49
  if ($config.claude -and $config.claude.enabled) {
32
50
  $enabledHooks = $config.claude.enabled
33
- if ($enabledHooks -contains $HookName -or $enabledHooks -contains "assistant-response") { return $true }
51
+ if ($enabledHooks -contains $HookName -or $enabledHooks -contains "assistant-response") {
52
+ return $true
53
+ }
34
54
  return $false
35
55
  }
36
- return $true
37
- } catch { return $true }
56
+
57
+ return $true # Default to enabled if no config
58
+ } catch {
59
+ return $true # Default to enabled on error
60
+ }
38
61
  }
39
62
 
40
- if (-not (Test-HookEnabled "assistant-response")) { exit 0 }
63
+ # Check if this hook is enabled
64
+ if (-not (Test-HookEnabled "assistant-response")) {
65
+ exit 0
66
+ }
41
67
 
42
68
  # ═══════════════════════════════════════════════════════════════════════════
43
- # Load session words - NO HARDCODED ARRAYS
69
+ # Load session words from JSON file - NO HARDCODED ARRAYS
44
70
  # ═══════════════════════════════════════════════════════════════════════════
45
71
  $script:SessionWords = $null
46
72
 
47
73
  function Load-SessionWords {
48
74
  $wordsFile = $SessionWordsJson
49
- if (-not (Test-Path $wordsFile)) { $wordsFile = $SessionWordsDefault }
50
- if (-not (Test-Path $wordsFile)) { return $null }
75
+
76
+ # Fallback to managed defaults if user file missing/invalid
77
+ if (-not (Test-Path $wordsFile)) {
78
+ $wordsFile = $SessionWordsDefault
79
+ }
80
+
81
+ if (-not (Test-Path $wordsFile)) {
82
+ return $null
83
+ }
84
+
51
85
  try {
52
86
  $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
53
- } catch { return $null }
87
+ } catch {
88
+ return $null
89
+ }
54
90
  }
55
91
 
92
+ # ═══════════════════════════════════════════════════════════════════════════
93
+ # SESSION NAME (UUID to words) - Uses external session-words.json
94
+ # ═══════════════════════════════════════════════════════════════════════════
56
95
  function Convert-UuidToWords {
57
96
  param([string]$uuid)
58
- if (-not $script:SessionWords) { Load-SessionWords }
59
- if (-not $script:SessionWords) { return "unknown-session" }
97
+
98
+ # Load session words if not already loaded
99
+ if (-not $script:SessionWords) {
100
+ Load-SessionWords
101
+ }
102
+
103
+ # Handle missing session words gracefully
104
+ if (-not $script:SessionWords) {
105
+ return "unknown-session"
106
+ }
60
107
 
61
108
  $adjectives = $script:SessionWords.adjectives
62
109
  $nouns = $script:SessionWords.nouns
63
110
  $verbs = $script:SessionWords.verbs
64
- if (-not $adjectives -or -not $nouns -or -not $verbs) { return "unknown-session" }
111
+
112
+ if (-not $adjectives -or -not $nouns -or -not $verbs) {
113
+ return "unknown-session"
114
+ }
115
+
65
116
  if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
66
117
 
67
118
  $clean = $uuid -replace "-", ""
@@ -71,8 +122,11 @@ function Convert-UuidToWords {
71
122
  $a = [Convert]::ToInt32($clean.Substring(0,4), 16) % $adjectives.Length
72
123
  $n = [Convert]::ToInt32($clean.Substring(4,4), 16) % $nouns.Length
73
124
  $an = [Convert]::ToInt32($clean.Substring(8,4), 16) % $verbs.Length
125
+
74
126
  return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
75
- } catch { return "unknown-session" }
127
+ } catch {
128
+ return "unknown-session"
129
+ }
76
130
  }
77
131
 
78
132
  # ═══════════════════════════════════════════════════════════════════════════
@@ -81,16 +135,23 @@ function Convert-UuidToWords {
81
135
  $inputJson = [Console]::In.ReadToEnd()
82
136
  if (-not $inputJson) { exit 0 }
83
137
 
84
- try { $input = $inputJson | ConvertFrom-Json } catch { exit 0 }
138
+ try {
139
+ $input = $inputJson | ConvertFrom-Json
140
+ } catch {
141
+ exit 0
142
+ }
85
143
 
144
+ # Extract response content
86
145
  $assistantResponse = $input.response
87
146
  if (-not $assistantResponse) { $assistantResponse = $input.message }
88
147
  if (-not $assistantResponse) { $assistantResponse = $input.content }
89
148
  if (-not $assistantResponse) { exit 0 }
90
149
 
150
+ # Get session ID
91
151
  $rawSessionId = $input.session_id
92
152
  if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
93
153
 
154
+ # Fallback: read session_id from saved state
94
155
  if ($rawSessionId -eq "unknown") {
95
156
  $stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
96
157
  if (Test-Path $stateFile) {
@@ -106,13 +167,16 @@ $sessionName = Convert-UuidToWords $rawSessionId
106
167
  # ═══════════════════════════════════════════════════════════════════════════
107
168
  # READ TURN STATE
108
169
  # ═══════════════════════════════════════════════════════════════════════════
109
- $hookStateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
170
+ $stateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
110
171
  $turn = 0
111
- if (Test-Path $hookStateFile) {
172
+
173
+ if (Test-Path $stateFile) {
112
174
  try {
113
- $hookState = Get-Content $hookStateFile -Raw | ConvertFrom-Json
175
+ $hookState = Get-Content $stateFile -Raw | ConvertFrom-Json
114
176
  $turn = [int]$hookState.turn
115
- } catch { $turn = 0 }
177
+ } catch {
178
+ $turn = 0
179
+ }
116
180
  }
117
181
 
118
182
  # ═══════════════════════════════════════════════════════════════════════════
@@ -120,6 +184,7 @@ if (Test-Path $hookStateFile) {
120
184
  # ═══════════════════════════════════════════════════════════════════════════
121
185
  $patternIds = @()
122
186
  if ($assistantResponse -match '\[ekkOS_SELECT\]') {
187
+ # Extract pattern IDs from SELECT blocks
123
188
  $selectMatches = [regex]::Matches($assistantResponse, 'id:\s*([a-zA-Z0-9\-_]+)')
124
189
  foreach ($match in $selectMatches) {
125
190
  $patternIds += $match.Groups[1].Value
@@ -127,15 +192,18 @@ if ($assistantResponse -match '\[ekkOS_SELECT\]') {
127
192
  }
128
193
 
129
194
  # ═══════════════════════════════════════════════════════════════════════════
130
- # LOCAL CACHE: Tier 0 capture (async)
195
+ # LOCAL CACHE: Tier 0 capture (async, non-blocking)
196
+ # Per v1.2 ADDENDUM: Pass instanceId for namespacing
131
197
  # ═══════════════════════════════════════════════════════════════════════════
132
198
  $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
133
199
  if ($captureCmd -and $rawSessionId -ne "unknown") {
134
200
  try {
201
+ # NEW format: ekkos-capture response <instance_id> <session_id> <turn_id> <response> [tools] [files]
135
202
  $responseBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($assistantResponse))
136
203
  $toolsJson = "[]"
137
204
  $filesJson = "[]"
138
205
 
206
+ # Extract tools used from response
139
207
  $toolMatches = [regex]::Matches($assistantResponse, '\[TOOL:\s*([^\]]+)\]')
140
208
  if ($toolMatches.Count -gt 0) {
141
209
  $tools = $toolMatches | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique
@@ -153,7 +221,7 @@ if ($captureCmd -and $rawSessionId -ne "unknown") {
153
221
  }
154
222
 
155
223
  # ═══════════════════════════════════════════════════════════════════════════
156
- # WORKING MEMORY: Fast capture to API (async)
224
+ # WORKING MEMORY: Fast capture to API (async, non-blocking)
157
225
  # ═══════════════════════════════════════════════════════════════════════════
158
226
  $configFile = Join-Path $EkkosConfigDir "config.json"
159
227
  if (Test-Path $configFile) {
@@ -174,7 +242,7 @@ if (Test-Path $configFile) {
174
242
  pattern_ids = $patterns
175
243
  } | ConvertTo-Json -Depth 10
176
244
 
177
- Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/working/turn" `
245
+ Invoke-RestMethod -Uri "https://mcp.ekkos.dev/api/v1/working/turn" `
178
246
  -Method POST `
179
247
  -Headers @{ Authorization = "Bearer $token" } `
180
248
  -ContentType "application/json" `
@@ -2,26 +2,38 @@
2
2
  "hooks": {
3
3
  "SessionStart": [
4
4
  {
5
- "type": "command",
6
- "command": "bash .claude/hooks/session-start.sh"
5
+ "matcher": "*",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "bash .claude/hooks/session-start.sh",
10
+ "timeout": 5000
11
+ }
12
+ ]
7
13
  }
8
14
  ],
9
15
  "UserPromptSubmit": [
10
16
  {
11
- "type": "command",
12
- "command": "bash .claude/hooks/user-prompt-submit.sh"
17
+ "matcher": "*",
18
+ "hooks": [
19
+ {
20
+ "type": "command",
21
+ "command": "bash .claude/hooks/user-prompt-submit.sh",
22
+ "timeout": 5000
23
+ }
24
+ ]
13
25
  }
14
26
  ],
15
27
  "Stop": [
16
28
  {
17
- "type": "command",
18
- "command": "bash .claude/hooks/stop.sh"
19
- }
20
- ],
21
- "AssistantResponse": [
22
- {
23
- "type": "command",
24
- "command": "bash .claude/hooks/assistant-response.sh"
29
+ "matcher": "*",
30
+ "hooks": [
31
+ {
32
+ "type": "command",
33
+ "command": "bash .claude/hooks/stop.sh",
34
+ "timeout": 5000
35
+ }
36
+ ]
25
37
  }
26
38
  ]
27
39
  }
File without changes
File without changes