@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.
- package/dist/agent/daemon.d.ts +86 -0
- package/dist/agent/daemon.js +297 -0
- package/dist/agent/pty-runner.d.ts +51 -0
- package/dist/agent/pty-runner.js +184 -0
- 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/agent.d.ts +44 -0
- package/dist/commands/agent.js +300 -0
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +175 -87
- package/dist/commands/hooks.d.ts +109 -0
- package/dist/commands/hooks.js +668 -0
- package/dist/commands/run.d.ts +2 -0
- package/dist/commands/run.js +357 -85
- package/dist/commands/setup-remote.d.ts +20 -0
- package/dist/commands/setup-remote.js +467 -0
- package/dist/index.js +116 -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/state.d.ts +2 -0
- 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
package/dist/utils/state.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -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
|