@ekkos/cli 0.2.9 → 0.2.11

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 (44) hide show
  1. package/dist/agent/daemon.d.ts +86 -0
  2. package/dist/agent/daemon.js +297 -0
  3. package/dist/agent/pty-runner.d.ts +51 -0
  4. package/dist/agent/pty-runner.js +184 -0
  5. package/dist/cache/LocalSessionStore.d.ts +34 -21
  6. package/dist/cache/LocalSessionStore.js +169 -53
  7. package/dist/cache/capture.d.ts +19 -11
  8. package/dist/cache/capture.js +243 -76
  9. package/dist/cache/types.d.ts +14 -1
  10. package/dist/commands/agent.d.ts +44 -0
  11. package/dist/commands/agent.js +300 -0
  12. package/dist/commands/doctor.d.ts +10 -0
  13. package/dist/commands/doctor.js +175 -87
  14. package/dist/commands/hooks.d.ts +109 -0
  15. package/dist/commands/hooks.js +668 -0
  16. package/dist/commands/run.d.ts +2 -0
  17. package/dist/commands/run.js +357 -85
  18. package/dist/commands/setup-remote.d.ts +20 -0
  19. package/dist/commands/setup-remote.js +467 -0
  20. package/dist/index.js +116 -1
  21. package/dist/restore/RestoreOrchestrator.d.ts +17 -3
  22. package/dist/restore/RestoreOrchestrator.js +64 -22
  23. package/dist/utils/paths.d.ts +125 -0
  24. package/dist/utils/paths.js +283 -0
  25. package/dist/utils/state.d.ts +2 -0
  26. package/package.json +1 -1
  27. package/templates/ekkos-manifest.json +223 -0
  28. package/templates/helpers/json-parse.cjs +101 -0
  29. package/templates/hooks/assistant-response.ps1 +256 -0
  30. package/templates/hooks/assistant-response.sh +124 -64
  31. package/templates/hooks/session-start.ps1 +107 -2
  32. package/templates/hooks/session-start.sh +201 -166
  33. package/templates/hooks/stop.ps1 +124 -3
  34. package/templates/hooks/stop.sh +470 -843
  35. package/templates/hooks/user-prompt-submit.ps1 +107 -22
  36. package/templates/hooks/user-prompt-submit.sh +403 -393
  37. package/templates/project-stubs/session-start.ps1 +63 -0
  38. package/templates/project-stubs/session-start.sh +55 -0
  39. package/templates/project-stubs/stop.ps1 +63 -0
  40. package/templates/project-stubs/stop.sh +55 -0
  41. package/templates/project-stubs/user-prompt-submit.ps1 +63 -0
  42. package/templates/project-stubs/user-prompt-submit.sh +55 -0
  43. package/templates/shared/hooks-enabled.json +22 -0
  44. package/templates/shared/session-words.json +45 -0
@@ -17,6 +17,8 @@ export interface EkkosState {
17
17
  turnNumber: number;
18
18
  lastUpdated: string;
19
19
  projectPath: string;
20
+ userId?: string;
21
+ userEmail?: string;
20
22
  }
21
23
  export interface EkkosConfig {
22
24
  hookApiKey?: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "0.2.9",
3
+ "version": "0.2.11",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -0,0 +1,223 @@
1
+ {
2
+ "$schema": "https://ekkos.dev/schemas/manifest-v1.json",
3
+ "manifestVersion": "1.0.0",
4
+ "generatedAt": "2026-01-21T03:41:22.207Z",
5
+ "platforms": {
6
+ "darwin": {
7
+ "configDir": "~/.ekkos",
8
+ "globalHooksDir": "~/.claude/hooks",
9
+ "shell": "bash"
10
+ },
11
+ "linux": {
12
+ "configDir": "~/.ekkos",
13
+ "globalHooksDir": "~/.claude/hooks",
14
+ "shell": "bash"
15
+ },
16
+ "win32": {
17
+ "configDir": "%USERPROFILE%\\.ekkos",
18
+ "globalHooksDir": "%USERPROFILE%\\.claude\\hooks",
19
+ "shell": "powershell"
20
+ }
21
+ },
22
+ "files": {
23
+ "managed": [
24
+ {
25
+ "source": "shared/session-words.json",
26
+ "destination": ".defaults/session-words.json",
27
+ "description": "Default session word lists (managed fallback)",
28
+ "checksum": "c64af03b9ae58d8c94e71886f80e590ad3a72fb2dcdbe02812789fecb4773e1a",
29
+ "overwrite": "always"
30
+ },
31
+ {
32
+ "source": "shared/hooks-enabled.json",
33
+ "destination": ".defaults/hooks-enabled.json",
34
+ "description": "Default hooks enablement config (managed fallback)",
35
+ "checksum": "74b833a1979f1b9467be6dcdeecc024073a4f3c02625b0751f4b1f9d53f143a3",
36
+ "overwrite": "always"
37
+ }
38
+ ],
39
+ "helpers": [
40
+ {
41
+ "source": "helpers/json-parse.cjs",
42
+ "destination": ".helpers/json-parse.cjs",
43
+ "description": "Node-based JSON parser (replaces jq dependency)",
44
+ "checksum": "e421977262b3bffcbec9f6f7a172df6f998f3b4deac64d478ef5a0a17034b026",
45
+ "executable": true,
46
+ "overwrite": "always"
47
+ }
48
+ ],
49
+ "userEditable": [
50
+ {
51
+ "source": "shared/session-words.json",
52
+ "destination": "session-words.json",
53
+ "description": "User-customizable session word lists",
54
+ "checksum": "c64af03b9ae58d8c94e71886f80e590ad3a72fb2dcdbe02812789fecb4773e1a",
55
+ "overwrite": "createOnly"
56
+ },
57
+ {
58
+ "source": "shared/hooks-enabled.json",
59
+ "destination": "hooks-enabled.json",
60
+ "description": "User-controlled hook enablement",
61
+ "checksum": "74b833a1979f1b9467be6dcdeecc024073a4f3c02625b0751f4b1f9d53f143a3",
62
+ "overwrite": "createOnly"
63
+ }
64
+ ],
65
+ "hooks": {
66
+ "bash": [
67
+ {
68
+ "source": "hooks/user-prompt-submit.sh",
69
+ "destination": "user-prompt-submit.sh",
70
+ "description": "User prompt submit hook (Unix)",
71
+ "checksum": "0e006eb7becba874f34fc622c9ee83b3c96a6e00daf95db840369894af94abf3",
72
+ "executable": true
73
+ },
74
+ {
75
+ "source": "hooks/stop.sh",
76
+ "destination": "stop.sh",
77
+ "description": "Session stop hook (Unix)",
78
+ "checksum": "fd436ea847bf6fbe367f8aa61ddf6d97e8605837badc3af8d433b59599ef6615",
79
+ "executable": true
80
+ },
81
+ {
82
+ "source": "hooks/session-start.sh",
83
+ "destination": "session-start.sh",
84
+ "description": "Session start hook (Unix)",
85
+ "checksum": "8d17fd3203045589f0c410f465b0d1e98d6ed24dc3bc99f44878e44e9501a22d",
86
+ "executable": true
87
+ },
88
+ {
89
+ "source": "hooks/assistant-response.sh",
90
+ "destination": "assistant-response.sh",
91
+ "description": "Assistant response hook (Unix)",
92
+ "checksum": "11c3b84aff29552f8a28b59851a4fdc7de4414d28161cca7418dfc93e0c64eac",
93
+ "executable": true
94
+ }
95
+ ],
96
+ "powershell": [
97
+ {
98
+ "source": "hooks/user-prompt-submit.ps1",
99
+ "destination": "user-prompt-submit.ps1",
100
+ "description": "User prompt submit hook (Windows)",
101
+ "checksum": "ba044ec066935a9413811e53fd70f42f0ffd959fc361b746c6b54768ac8c6fb9"
102
+ },
103
+ {
104
+ "source": "hooks/stop.ps1",
105
+ "destination": "stop.ps1",
106
+ "description": "Session stop hook (Windows)",
107
+ "checksum": "455d9dfca8cea2f8289604ca2389b8e43c8af380fbcc5e2eef18382d30e4be6d"
108
+ },
109
+ {
110
+ "source": "hooks/session-start.ps1",
111
+ "destination": "session-start.ps1",
112
+ "description": "Session start hook (Windows)",
113
+ "checksum": "0897603df2b3261857be79108c0831b0fbc3744722b4923ce632844e7c6483c6"
114
+ },
115
+ {
116
+ "source": "hooks/assistant-response.ps1",
117
+ "destination": "assistant-response.ps1",
118
+ "description": "Assistant response hook (Windows)",
119
+ "checksum": "83fb65cec1cb9b1b9ef7a2036c9440e8b6ced16d82a2623b5353899918ae9653"
120
+ }
121
+ ],
122
+ "lib": [
123
+ {
124
+ "source": "hooks/lib/contract.sh",
125
+ "destination": "lib/contract.sh",
126
+ "description": "Hook contract library",
127
+ "checksum": "1d86128a2a4a25e653a02c4eb08dbfd28db53063c618cd43b85f0603a135e8b2",
128
+ "executable": true
129
+ },
130
+ {
131
+ "source": "hooks/lib/state.sh",
132
+ "destination": "lib/state.sh",
133
+ "description": "Hook state management library",
134
+ "checksum": "ba72c00a1fb0f6768e37aff754ad6ac0e8e20aa0b9b393193e2d92357cac5a71",
135
+ "executable": true
136
+ }
137
+ ]
138
+ }
139
+ },
140
+ "projectStubs": {
141
+ "description": "Delegating stub hooks for project-level installation. These source global hooks first, then load project-specific overrides.",
142
+ "bash": [
143
+ {
144
+ "source": "project-stubs/user-prompt-submit.sh",
145
+ "destination": "user-prompt-submit.sh",
146
+ "description": "Project delegating stub for user-prompt-submit (Unix)",
147
+ "checksum": "47cc6b45dbb027e321c136f7c0935bae0d3dc349f01912fbe42ae27d382e297d",
148
+ "executable": true
149
+ },
150
+ {
151
+ "source": "project-stubs/stop.sh",
152
+ "destination": "stop.sh",
153
+ "description": "Project delegating stub for stop (Unix)",
154
+ "checksum": "a429c5ee596e6c975df32feb8ba952c77c0492d91bd9cd80f9eac2e721040eb4",
155
+ "executable": true
156
+ },
157
+ {
158
+ "source": "project-stubs/session-start.sh",
159
+ "destination": "session-start.sh",
160
+ "description": "Project delegating stub for session-start (Unix)",
161
+ "checksum": "189379e81e000153d057e49afd71791d28a6b73baab4b49b39de34918a00c493",
162
+ "executable": true
163
+ }
164
+ ],
165
+ "powershell": [
166
+ {
167
+ "source": "project-stubs/user-prompt-submit.ps1",
168
+ "destination": "user-prompt-submit.ps1",
169
+ "description": "Project delegating stub for user-prompt-submit (Windows)",
170
+ "checksum": "86cfea6a6e1181fc6b9180e82a3e34e1ba8e8b412c65bdf013d2367ea8b93283"
171
+ },
172
+ {
173
+ "source": "project-stubs/stop.ps1",
174
+ "destination": "stop.ps1",
175
+ "description": "Project delegating stub for stop (Windows)",
176
+ "checksum": "042f815ed455c5b5e51291b8740deafaea827c594695b270f4d4e4e5086bc234"
177
+ },
178
+ {
179
+ "source": "project-stubs/session-start.ps1",
180
+ "destination": "session-start.ps1",
181
+ "description": "Project delegating stub for session-start (Windows)",
182
+ "checksum": "14081806c7ca63a67999314f31e14a6c1293e57a8ce64335122f359e3bb8fba4"
183
+ }
184
+ ]
185
+ },
186
+ "cli": {
187
+ "minVersion": "0.2.9",
188
+ "legacySupported": true,
189
+ "requiredCommands": {
190
+ "0.2.9": [
191
+ "run",
192
+ "config"
193
+ ],
194
+ "0.3.0": [
195
+ "run",
196
+ "config",
197
+ "hooks"
198
+ ]
199
+ }
200
+ },
201
+ "validation": {
202
+ "requiredFiles": [
203
+ "shared/session-words.json",
204
+ "shared/hooks-enabled.json",
205
+ "helpers/json-parse.cjs",
206
+ "hooks/user-prompt-submit.sh",
207
+ "hooks/user-prompt-submit.ps1",
208
+ "hooks/stop.sh",
209
+ "hooks/stop.ps1",
210
+ "hooks/session-start.sh",
211
+ "hooks/session-start.ps1",
212
+ "hooks/assistant-response.sh",
213
+ "hooks/assistant-response.ps1",
214
+ "project-stubs/user-prompt-submit.sh",
215
+ "project-stubs/user-prompt-submit.ps1",
216
+ "project-stubs/stop.sh",
217
+ "project-stubs/stop.ps1",
218
+ "project-stubs/session-start.sh",
219
+ "project-stubs/session-start.ps1"
220
+ ],
221
+ "checksumAlgorithm": "sha256"
222
+ }
223
+ }
@@ -0,0 +1,101 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * json-parse.cjs - Minimal JSON parser for ekkOS hooks
4
+ * Eliminates jq dependency for cross-platform compatibility
5
+ *
6
+ * Usage: json-parse.cjs <file> [path]
7
+ *
8
+ * Examples:
9
+ * json-parse.cjs config.json # Output entire file
10
+ * json-parse.cjs config.json .apiKey # Output single value
11
+ * json-parse.cjs words.json .adjectives # Output array (one per line)
12
+ * json-parse.cjs hooks.json .targets.claude.stop # Nested path
13
+ *
14
+ * Exit codes:
15
+ * 0 - Success (including empty/null result)
16
+ * 1 - Error (file not found, invalid JSON, etc.)
17
+ */
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const args = process.argv.slice(2);
23
+
24
+ if (args.length < 1) {
25
+ console.error('Usage: json-parse.cjs <file> [path]');
26
+ console.error('Example: json-parse.cjs config.json .apiKey');
27
+ process.exit(1);
28
+ }
29
+
30
+ const filePath = args[0];
31
+ const jsonPath = args[1] || null;
32
+
33
+ // Check file exists
34
+ if (!fs.existsSync(filePath)) {
35
+ console.error(`Error: File not found: ${filePath}`);
36
+ process.exit(1);
37
+ }
38
+
39
+ let data;
40
+ try {
41
+ const content = fs.readFileSync(filePath, 'utf8');
42
+ data = JSON.parse(content);
43
+ } catch (err) {
44
+ console.error(`Error: Failed to parse JSON: ${err.message}`);
45
+ process.exit(1);
46
+ }
47
+
48
+ // If no path specified, output entire JSON
49
+ if (!jsonPath) {
50
+ console.log(JSON.stringify(data, null, 2));
51
+ process.exit(0);
52
+ }
53
+
54
+ // Parse path and extract value
55
+ // Supports: .foo.bar, .foo[0], .foo.bar[1].baz
56
+ function extractValue(obj, pathStr) {
57
+ if (!pathStr || pathStr === '.') return obj;
58
+
59
+ // Remove leading dot if present
60
+ const cleanPath = pathStr.startsWith('.') ? pathStr.slice(1) : pathStr;
61
+ if (!cleanPath) return obj;
62
+
63
+ // Split on . and [ but keep the brackets for array access
64
+ const parts = cleanPath.split(/\.|\[|\]/).filter(Boolean);
65
+
66
+ let result = obj;
67
+ for (const part of parts) {
68
+ if (result === undefined || result === null) {
69
+ return undefined;
70
+ }
71
+ result = result[part];
72
+ }
73
+ return result;
74
+ }
75
+
76
+ const result = extractValue(data, jsonPath);
77
+
78
+ // Handle different result types
79
+ if (result === undefined || result === null) {
80
+ // Empty output, exit 0 (not an error - just no value)
81
+ process.exit(0);
82
+ }
83
+
84
+ if (Array.isArray(result)) {
85
+ // Output array items one per line (like jq -r '.[]')
86
+ for (const item of result) {
87
+ if (typeof item === 'object') {
88
+ console.log(JSON.stringify(item));
89
+ } else {
90
+ console.log(item);
91
+ }
92
+ }
93
+ } else if (typeof result === 'object') {
94
+ console.log(JSON.stringify(result, null, 2));
95
+ } else if (typeof result === 'boolean') {
96
+ console.log(result ? 'true' : 'false');
97
+ } else {
98
+ console.log(result);
99
+ }
100
+
101
+ process.exit(0);
@@ -0,0 +1,256 @@
1
+ # ═══════════════════════════════════════════════════════════════════════════
2
+ # ekkOS_ Hook: AssistantResponse - Process Claude's response (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
11
+ # ═══════════════════════════════════════════════════════════════════════════
12
+
13
+ $ErrorActionPreference = "SilentlyContinue"
14
+
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
+ $HooksEnabledJson = "$EkkosConfigDir\hooks-enabled.json"
20
+ $HooksEnabledDefault = "$EkkosConfigDir\.defaults\hooks-enabled.json"
21
+ $SessionWordsJson = "$EkkosConfigDir\session-words.json"
22
+ $SessionWordsDefault = "$EkkosConfigDir\.defaults\session-words.json"
23
+
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" }
28
+
29
+ # ═══════════════════════════════════════════════════════════════════════════
30
+ # CHECK ENABLEMENT - Respect hooks-enabled.json
31
+ # ═══════════════════════════════════════════════════════════════════════════
32
+ function Test-HookEnabled {
33
+ param([string]$HookName)
34
+
35
+ $enabledFile = $HooksEnabledJson
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
+
45
+ try {
46
+ $config = Get-Content $enabledFile -Raw | ConvertFrom-Json
47
+
48
+ # Check claude.enabled array
49
+ if ($config.claude -and $config.claude.enabled) {
50
+ $enabledHooks = $config.claude.enabled
51
+ if ($enabledHooks -contains $HookName -or $enabledHooks -contains "assistant-response") {
52
+ return $true
53
+ }
54
+ return $false
55
+ }
56
+
57
+ return $true # Default to enabled if no config
58
+ } catch {
59
+ return $true # Default to enabled on error
60
+ }
61
+ }
62
+
63
+ # Check if this hook is enabled
64
+ if (-not (Test-HookEnabled "assistant-response")) {
65
+ exit 0
66
+ }
67
+
68
+ # ═══════════════════════════════════════════════════════════════════════════
69
+ # Load session words from JSON file - NO HARDCODED ARRAYS
70
+ # ═══════════════════════════════════════════════════════════════════════════
71
+ $script:SessionWords = $null
72
+
73
+ function Load-SessionWords {
74
+ $wordsFile = $SessionWordsJson
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
+
85
+ try {
86
+ $script:SessionWords = Get-Content $wordsFile -Raw | ConvertFrom-Json
87
+ } catch {
88
+ return $null
89
+ }
90
+ }
91
+
92
+ # ═══════════════════════════════════════════════════════════════════════════
93
+ # SESSION NAME (UUID to words) - Uses external session-words.json
94
+ # ═══════════════════════════════════════════════════════════════════════════
95
+ function Convert-UuidToWords {
96
+ param([string]$uuid)
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
+ }
107
+
108
+ $adjectives = $script:SessionWords.adjectives
109
+ $nouns = $script:SessionWords.nouns
110
+ $verbs = $script:SessionWords.verbs
111
+
112
+ if (-not $adjectives -or -not $nouns -or -not $verbs) {
113
+ return "unknown-session"
114
+ }
115
+
116
+ if (-not $uuid -or $uuid -eq "unknown") { return "unknown-session" }
117
+
118
+ $clean = $uuid -replace "-", ""
119
+ if ($clean.Length -lt 6) { return "unknown-session" }
120
+
121
+ try {
122
+ $a = [Convert]::ToInt32($clean.Substring(0,2), 16) % $adjectives.Length
123
+ $n = [Convert]::ToInt32($clean.Substring(2,2), 16) % $nouns.Length
124
+ $an = [Convert]::ToInt32($clean.Substring(4,2), 16) % $verbs.Length
125
+
126
+ return "$($adjectives[$a])-$($nouns[$n])-$($verbs[$an])"
127
+ } catch {
128
+ return "unknown-session"
129
+ }
130
+ }
131
+
132
+ # ═══════════════════════════════════════════════════════════════════════════
133
+ # READ INPUT
134
+ # ═══════════════════════════════════════════════════════════════════════════
135
+ $inputJson = [Console]::In.ReadToEnd()
136
+ if (-not $inputJson) { exit 0 }
137
+
138
+ try {
139
+ $input = $inputJson | ConvertFrom-Json
140
+ } catch {
141
+ exit 0
142
+ }
143
+
144
+ # Extract response content
145
+ $assistantResponse = $input.response
146
+ if (-not $assistantResponse) { $assistantResponse = $input.message }
147
+ if (-not $assistantResponse) { $assistantResponse = $input.content }
148
+ if (-not $assistantResponse) { exit 0 }
149
+
150
+ # Get session ID
151
+ $rawSessionId = $input.session_id
152
+ if (-not $rawSessionId -or $rawSessionId -eq "null") { $rawSessionId = "unknown" }
153
+
154
+ # Fallback: read session_id from saved state
155
+ if ($rawSessionId -eq "unknown") {
156
+ $stateFile = Join-Path $env:USERPROFILE ".claude\state\current-session.json"
157
+ if (Test-Path $stateFile) {
158
+ try {
159
+ $state = Get-Content $stateFile -Raw | ConvertFrom-Json
160
+ $rawSessionId = $state.session_id
161
+ } catch {}
162
+ }
163
+ }
164
+
165
+ $sessionName = Convert-UuidToWords $rawSessionId
166
+
167
+ # ═══════════════════════════════════════════════════════════════════════════
168
+ # READ TURN STATE
169
+ # ═══════════════════════════════════════════════════════════════════════════
170
+ $stateFile = Join-Path $env:USERPROFILE ".claude\state\hook-state.json"
171
+ $turn = 0
172
+
173
+ if (Test-Path $stateFile) {
174
+ try {
175
+ $hookState = Get-Content $stateFile -Raw | ConvertFrom-Json
176
+ $turn = [int]$hookState.turn
177
+ } catch {
178
+ $turn = 0
179
+ }
180
+ }
181
+
182
+ # ═══════════════════════════════════════════════════════════════════════════
183
+ # PATTERN TRACKING (detect [ekkOS_SELECT] blocks)
184
+ # ═══════════════════════════════════════════════════════════════════════════
185
+ $patternIds = @()
186
+ if ($assistantResponse -match '\[ekkOS_SELECT\]') {
187
+ # Extract pattern IDs from SELECT blocks
188
+ $selectMatches = [regex]::Matches($assistantResponse, 'id:\s*([a-zA-Z0-9\-_]+)')
189
+ foreach ($match in $selectMatches) {
190
+ $patternIds += $match.Groups[1].Value
191
+ }
192
+ }
193
+
194
+ # ═══════════════════════════════════════════════════════════════════════════
195
+ # LOCAL CACHE: Tier 0 capture (async, non-blocking)
196
+ # Per v1.2 ADDENDUM: Pass instanceId for namespacing
197
+ # ═══════════════════════════════════════════════════════════════════════════
198
+ $captureCmd = Get-Command "ekkos-capture" -ErrorAction SilentlyContinue
199
+ if ($captureCmd -and $rawSessionId -ne "unknown") {
200
+ try {
201
+ # NEW format: ekkos-capture response <instance_id> <session_id> <turn_id> <response> [tools] [files]
202
+ $responseBase64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($assistantResponse))
203
+ $toolsJson = "[]"
204
+ $filesJson = "[]"
205
+
206
+ # Extract tools used from response
207
+ $toolMatches = [regex]::Matches($assistantResponse, '\[TOOL:\s*([^\]]+)\]')
208
+ if ($toolMatches.Count -gt 0) {
209
+ $tools = $toolMatches | ForEach-Object { $_.Groups[1].Value } | Select-Object -Unique
210
+ $toolsJson = $tools | ConvertTo-Json -Compress
211
+ }
212
+
213
+ Start-Job -ScriptBlock {
214
+ param($instanceId, $sessionId, $turnNum, $responseB64, $tools, $files)
215
+ try {
216
+ $decoded = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($responseB64))
217
+ & ekkos-capture response $instanceId $sessionId $turnNum $decoded $tools $files 2>&1 | Out-Null
218
+ } catch {}
219
+ } -ArgumentList $EkkosInstanceId, $rawSessionId, $turn, $responseBase64, $toolsJson, $filesJson | Out-Null
220
+ } catch {}
221
+ }
222
+
223
+ # ═══════════════════════════════════════════════════════════════════════════
224
+ # WORKING MEMORY: Fast capture to API (async, non-blocking)
225
+ # ═══════════════════════════════════════════════════════════════════════════
226
+ $configFile = Join-Path $EkkosConfigDir "config.json"
227
+ if (Test-Path $configFile) {
228
+ try {
229
+ $config = Get-Content $configFile -Raw | ConvertFrom-Json
230
+ $captureToken = $config.hookApiKey
231
+ if (-not $captureToken) { $captureToken = $config.apiKey }
232
+
233
+ if ($captureToken) {
234
+ Start-Job -ScriptBlock {
235
+ param($token, $instanceId, $sessionId, $sessionName, $turnNum, $response, $patterns)
236
+ $body = @{
237
+ session_id = $sessionId
238
+ session_name = $sessionName
239
+ instance_id = $instanceId
240
+ turn = $turnNum
241
+ response = $response.Substring(0, [Math]::Min(5000, $response.Length))
242
+ pattern_ids = $patterns
243
+ } | ConvertTo-Json
244
+
245
+ Invoke-RestMethod -Uri "https://api.ekkos.dev/api/v1/working/turn" `
246
+ -Method POST `
247
+ -Headers @{ Authorization = "Bearer $token" } `
248
+ -ContentType "application/json" `
249
+ -Body $body -ErrorAction SilentlyContinue
250
+ } -ArgumentList $captureToken, $EkkosInstanceId, $rawSessionId, $sessionName, $turn, $assistantResponse, $patternIds | Out-Null
251
+ }
252
+ } catch {}
253
+ }
254
+
255
+ # Silent exit - assistant-response hook should not produce output
256
+ exit 0