@ekkos/cli 0.2.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.
Files changed (135) hide show
  1. package/dist/cache/LocalSessionStore.d.ts +129 -0
  2. package/dist/cache/LocalSessionStore.js +688 -0
  3. package/dist/cache/capture.d.ts +26 -0
  4. package/dist/cache/capture.js +461 -0
  5. package/dist/cache/index.d.ts +7 -0
  6. package/dist/cache/index.js +23 -0
  7. package/dist/cache/types.d.ts +147 -0
  8. package/dist/cache/types.js +40 -0
  9. package/dist/commands/init.d.ts +9 -0
  10. package/dist/commands/init.js +478 -0
  11. package/dist/commands/run.d.ts +12 -0
  12. package/dist/commands/run.js +829 -0
  13. package/dist/commands/setup.d.ts +6 -0
  14. package/dist/commands/setup.js +658 -0
  15. package/dist/commands/status.d.ts +1 -0
  16. package/dist/commands/status.js +109 -0
  17. package/dist/commands/test.d.ts +1 -0
  18. package/dist/commands/test.js +157 -0
  19. package/dist/deploy/agents.d.ts +15 -0
  20. package/dist/deploy/agents.js +72 -0
  21. package/dist/deploy/hooks.d.ts +16 -0
  22. package/dist/deploy/hooks.js +121 -0
  23. package/dist/deploy/index.d.ts +7 -0
  24. package/dist/deploy/index.js +24 -0
  25. package/dist/deploy/instructions.d.ts +12 -0
  26. package/dist/deploy/instructions.js +36 -0
  27. package/dist/deploy/mcp.d.ts +19 -0
  28. package/dist/deploy/mcp.js +109 -0
  29. package/dist/deploy/plugins.d.ts +19 -0
  30. package/dist/deploy/plugins.js +62 -0
  31. package/dist/deploy/settings.d.ts +8 -0
  32. package/dist/deploy/settings.js +84 -0
  33. package/dist/deploy/skills.d.ts +19 -0
  34. package/dist/deploy/skills.js +60 -0
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +71 -0
  37. package/dist/restore/RestoreOrchestrator.d.ts +48 -0
  38. package/dist/restore/RestoreOrchestrator.js +481 -0
  39. package/dist/restore/index.d.ts +4 -0
  40. package/dist/restore/index.js +20 -0
  41. package/dist/utils/platform.d.ts +29 -0
  42. package/dist/utils/platform.js +65 -0
  43. package/dist/utils/session-words.json +119 -0
  44. package/dist/utils/state.d.ts +57 -0
  45. package/dist/utils/state.js +186 -0
  46. package/dist/utils/templates.d.ts +24 -0
  47. package/dist/utils/templates.js +118 -0
  48. package/package.json +48 -0
  49. package/templates/CLAUDE.md +287 -0
  50. package/templates/README.md +378 -0
  51. package/templates/agents/README.md +182 -0
  52. package/templates/agents/code-reviewer.md +166 -0
  53. package/templates/agents/debug-detective.md +169 -0
  54. package/templates/agents/ekkOS_Vercel.md +99 -0
  55. package/templates/agents/extension-manager.md +229 -0
  56. package/templates/agents/git-companion.md +185 -0
  57. package/templates/agents/github-test-agent.md +321 -0
  58. package/templates/agents/railway-manager.md +179 -0
  59. package/templates/claude-plugins/PHASE2_COMPLETION.md +346 -0
  60. package/templates/claude-plugins/PLUGIN_PROPOSALS.md +1776 -0
  61. package/templates/claude-plugins/README.md +587 -0
  62. package/templates/claude-plugins/agents/code-reviewer.json +14 -0
  63. package/templates/claude-plugins/agents/debug-detective.json +15 -0
  64. package/templates/claude-plugins/agents/git-companion.json +14 -0
  65. package/templates/claude-plugins/blog-manager/.claude-plugin/plugin.json +8 -0
  66. package/templates/claude-plugins/blog-manager/commands/blog.md +691 -0
  67. package/templates/claude-plugins/golden-loop-monitor/.claude-plugin/plugin.json +8 -0
  68. package/templates/claude-plugins/golden-loop-monitor/commands/loop-status.md +434 -0
  69. package/templates/claude-plugins/learning-tracker/.claude-plugin/plugin.json +8 -0
  70. package/templates/claude-plugins/learning-tracker/commands/my-patterns.md +282 -0
  71. package/templates/claude-plugins/memory-lens/.claude-plugin/plugin.json +8 -0
  72. package/templates/claude-plugins/memory-lens/commands/memory-search.md +181 -0
  73. package/templates/claude-plugins/pattern-coach/.claude-plugin/plugin.json +8 -0
  74. package/templates/claude-plugins/pattern-coach/commands/forge.md +365 -0
  75. package/templates/claude-plugins/project-schema-validator/.claude-plugin/plugin.json +8 -0
  76. package/templates/claude-plugins/project-schema-validator/commands/validate-schema.md +582 -0
  77. package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +819 -0
  78. package/templates/claude-plugins-admin/README.md +446 -0
  79. package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +8 -0
  80. package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +595 -0
  81. package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +8 -0
  82. package/templates/claude-plugins-admin/backend-agent/commands/backend.md +798 -0
  83. package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +8 -0
  84. package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +554 -0
  85. package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +8 -0
  86. package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +881 -0
  87. package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +8 -0
  88. package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +85 -0
  89. package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +8 -0
  90. package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +569 -0
  91. package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +8 -0
  92. package/templates/claude-plugins-admin/qa-agent/commands/qa.md +863 -0
  93. package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +8 -0
  94. package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +732 -0
  95. package/templates/commands/continue.md +47 -0
  96. package/templates/cursor-hooks/after-agent-response.sh +117 -0
  97. package/templates/cursor-hooks/before-submit-prompt.sh +419 -0
  98. package/templates/cursor-hooks/hooks.json +20 -0
  99. package/templates/cursor-hooks/lib/contract.sh +320 -0
  100. package/templates/cursor-hooks/stop.sh +75 -0
  101. package/templates/cursor-rules/ekkos-memory.md +187 -0
  102. package/templates/hooks/assistant-response.sh +96 -0
  103. package/templates/hooks/hooks.json +28 -0
  104. package/templates/hooks/lib/contract.sh +320 -0
  105. package/templates/hooks/lib/state.sh +158 -0
  106. package/templates/hooks/session-start.ps1 +41 -0
  107. package/templates/hooks/session-start.sh +318 -0
  108. package/templates/hooks/stop.ps1 +16 -0
  109. package/templates/hooks/stop.sh +989 -0
  110. package/templates/hooks/user-prompt-submit.ps1 +174 -0
  111. package/templates/hooks/user-prompt-submit.sh +587 -0
  112. package/templates/hooks-node/lib/state.js +187 -0
  113. package/templates/hooks-node/stop.js +416 -0
  114. package/templates/hooks-node/user-prompt-submit.js +337 -0
  115. package/templates/plan-template.md +306 -0
  116. package/templates/rules/00-hooks-contract.mdc +89 -0
  117. package/templates/rules/30-ekkos-core.mdc +188 -0
  118. package/templates/rules/31-ekkos-messages.mdc +78 -0
  119. package/templates/skills/continue/SKILL.md +169 -0
  120. package/templates/skills/ekkOS_Deep_Recall/Skill.md +282 -0
  121. package/templates/skills/ekkOS_Learn/Skill.md +265 -0
  122. package/templates/skills/ekkOS_Memory_First/Skill.md +206 -0
  123. package/templates/skills/ekkOS_Plan_Assist/Skill.md +302 -0
  124. package/templates/skills/ekkOS_Preferences/Skill.md +247 -0
  125. package/templates/skills/ekkOS_Reflect/Skill.md +257 -0
  126. package/templates/skills/ekkOS_Safety/Skill.md +265 -0
  127. package/templates/skills/ekkOS_Schema/Skill.md +251 -0
  128. package/templates/skills/ekkOS_Summary/Skill.md +257 -0
  129. package/templates/skills/ekkOS_Vault/Skill.md +287 -0
  130. package/templates/skills/permissions/Skill.md +322 -0
  131. package/templates/spec-template.md +159 -0
  132. package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
  133. package/templates/windsurf-hooks/hooks.json +10 -0
  134. package/templates/windsurf-hooks/lib/contract.sh +320 -0
  135. package/templates/windsurf-rules/ekkos-memory.md +129 -0
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ekkOS™ Hook State Management Library (Node.js - Cross-Platform)
4
+ * Shared state functions for coordinating between hooks
5
+ * Works on Windows, macOS, and Linux without bash/curl/jq dependencies
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const crypto = require('crypto');
11
+
12
+ // Get project root (where .claude directory lives)
13
+ function getProjectRoot() {
14
+ const scriptDir = __dirname;
15
+ return path.dirname(path.dirname(path.dirname(scriptDir)));
16
+ }
17
+
18
+ const PROJECT_ROOT = getProjectRoot();
19
+ const STATE_DIR = path.join(PROJECT_ROOT, '.claude', 'state');
20
+
21
+ // Ensure state directory exists
22
+ function ensureStateDir() {
23
+ if (!fs.existsSync(STATE_DIR)) {
24
+ fs.mkdirSync(STATE_DIR, { recursive: true });
25
+ }
26
+ }
27
+
28
+ // ═══════════════════════════════════════════════════════════════════════════
29
+ // State File Management
30
+ // ═══════════════════════════════════════════════════════════════════════════
31
+
32
+ function savePatterns(sessionId, patterns, modelUsed) {
33
+ ensureStateDir();
34
+ const stateFile = path.join(STATE_DIR, `patterns-${sessionId}.json`);
35
+
36
+ const data = {
37
+ patterns: patterns,
38
+ model_used: modelUsed,
39
+ saved_at: new Date().toISOString()
40
+ };
41
+
42
+ fs.writeFileSync(stateFile, JSON.stringify(data, null, 2));
43
+ }
44
+
45
+ function loadPatterns(sessionId) {
46
+ const stateFile = path.join(STATE_DIR, `patterns-${sessionId}.json`);
47
+
48
+ if (fs.existsSync(stateFile)) {
49
+ try {
50
+ return JSON.parse(fs.readFileSync(stateFile, 'utf8'));
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+ return {};
56
+ }
57
+
58
+ function clearPatterns(sessionId) {
59
+ const stateFile = path.join(STATE_DIR, `patterns-${sessionId}.json`);
60
+ if (fs.existsSync(stateFile)) {
61
+ fs.unlinkSync(stateFile);
62
+ }
63
+ }
64
+
65
+ // ═══════════════════════════════════════════════════════════════════════════
66
+ // Capture Deduplication
67
+ // ═══════════════════════════════════════════════════════════════════════════
68
+
69
+ function generateTurnHash(userQuery, assistantResponse) {
70
+ return crypto.createHash('md5').update(`${userQuery}${assistantResponse}`).digest('hex');
71
+ }
72
+
73
+ function wasTurnCaptured(sessionId, turnHash) {
74
+ const captureLog = path.join(STATE_DIR, `captures-${sessionId}.log`);
75
+
76
+ if (fs.existsSync(captureLog)) {
77
+ const hashes = fs.readFileSync(captureLog, 'utf8').split('\n');
78
+ return hashes.includes(turnHash);
79
+ }
80
+ return false;
81
+ }
82
+
83
+ function markTurnCaptured(sessionId, turnHash) {
84
+ ensureStateDir();
85
+ const captureLog = path.join(STATE_DIR, `captures-${sessionId}.log`);
86
+
87
+ fs.appendFileSync(captureLog, `${turnHash}\n`);
88
+
89
+ // Keep only last 100 hashes
90
+ try {
91
+ const hashes = fs.readFileSync(captureLog, 'utf8').split('\n').filter(Boolean);
92
+ if (hashes.length > 100) {
93
+ fs.writeFileSync(captureLog, hashes.slice(-100).join('\n') + '\n');
94
+ }
95
+ } catch {
96
+ // Ignore cleanup errors
97
+ }
98
+ }
99
+
100
+ // ═══════════════════════════════════════════════════════════════════════════
101
+ // Lock Management
102
+ // ═══════════════════════════════════════════════════════════════════════════
103
+
104
+ function acquireLock(sessionId) {
105
+ ensureStateDir();
106
+ const lockFile = path.join(STATE_DIR, `lock-${sessionId}.lock`);
107
+ const timeout = 5000; // 5 seconds
108
+ const startTime = Date.now();
109
+
110
+ while (fs.existsSync(lockFile)) {
111
+ if (Date.now() - startTime > timeout) {
112
+ // Check if lock is stale (older than 10 seconds)
113
+ try {
114
+ const stat = fs.statSync(lockFile);
115
+ if (Date.now() - stat.mtimeMs > 10000) {
116
+ fs.unlinkSync(lockFile);
117
+ break;
118
+ }
119
+ } catch {
120
+ break;
121
+ }
122
+ return false;
123
+ }
124
+ // Wait 100ms
125
+ const waitUntil = Date.now() + 100;
126
+ while (Date.now() < waitUntil) {}
127
+ }
128
+
129
+ fs.writeFileSync(lockFile, process.pid.toString());
130
+ return true;
131
+ }
132
+
133
+ function releaseLock(sessionId) {
134
+ const lockFile = path.join(STATE_DIR, `lock-${sessionId}.lock`);
135
+
136
+ try {
137
+ const content = fs.readFileSync(lockFile, 'utf8');
138
+ if (content.trim() === process.pid.toString()) {
139
+ fs.unlinkSync(lockFile);
140
+ }
141
+ } catch {
142
+ // Lock file doesn't exist or can't be read
143
+ }
144
+ }
145
+
146
+ // ═══════════════════════════════════════════════════════════════════════════
147
+ // Cleanup
148
+ // ═══════════════════════════════════════════════════════════════════════════
149
+
150
+ function cleanupOldState() {
151
+ if (!fs.existsSync(STATE_DIR)) return;
152
+
153
+ const now = Date.now();
154
+ const oneDayMs = 24 * 60 * 60 * 1000;
155
+
156
+ try {
157
+ const files = fs.readdirSync(STATE_DIR);
158
+ for (const file of files) {
159
+ const filePath = path.join(STATE_DIR, file);
160
+ try {
161
+ const stat = fs.statSync(filePath);
162
+ if (now - stat.mtimeMs > oneDayMs) {
163
+ fs.unlinkSync(filePath);
164
+ }
165
+ } catch {
166
+ // Ignore file errors
167
+ }
168
+ }
169
+ } catch {
170
+ // Ignore directory errors
171
+ }
172
+ }
173
+
174
+ module.exports = {
175
+ PROJECT_ROOT,
176
+ STATE_DIR,
177
+ ensureStateDir,
178
+ savePatterns,
179
+ loadPatterns,
180
+ clearPatterns,
181
+ generateTurnHash,
182
+ wasTurnCaptured,
183
+ markTurnCaptured,
184
+ acquireLock,
185
+ releaseLock,
186
+ cleanupOldState
187
+ };
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ═══════════════════════════════════════════════════════════════════════════
4
+ * ekkOS_ Hook: Stop (Claude Code) - CAPTURE + ANALYZE
5
+ * Node.js version - Works on Windows, macOS, Linux without bash dependencies
6
+ *
7
+ * ARCHITECTURE: Dumb Hook, Smart Backend
8
+ * ═══════════════════════════════════════════════════════════════════════════
9
+ */
10
+
11
+ const https = require('https');
12
+ const http = require('http');
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const os = require('os');
16
+
17
+ // Load state management
18
+ const state = require('./lib/state');
19
+
20
+ // ═══════════════════════════════════════════════════════════════════════════
21
+ // ANSI Color Codes
22
+ // ═══════════════════════════════════════════════════════════════════════════
23
+ const CYAN = '\x1b[0;36m';
24
+ const GREEN = '\x1b[0;32m';
25
+ const YELLOW = '\x1b[1;33m';
26
+ const BLUE = '\x1b[0;34m';
27
+ const RED = '\x1b[0;31m';
28
+ const DIM = '\x1b[2m';
29
+ const BOLD = '\x1b[1m';
30
+ const RESET = '\x1b[0m';
31
+
32
+ // ═══════════════════════════════════════════════════════════════════════════
33
+ // HTTP Request Helper
34
+ // ═══════════════════════════════════════════════════════════════════════════
35
+ function httpRequest(url, options, data) {
36
+ return new Promise((resolve, reject) => {
37
+ const urlObj = new URL(url);
38
+ const lib = urlObj.protocol === 'https:' ? https : http;
39
+ const timeout = options.timeout || 10000;
40
+
41
+ const req = lib.request({
42
+ hostname: urlObj.hostname,
43
+ port: urlObj.port || (urlObj.protocol === 'https:' ? 443 : 80),
44
+ path: urlObj.pathname + urlObj.search,
45
+ method: options.method || 'GET',
46
+ headers: options.headers || {},
47
+ timeout: timeout
48
+ }, (res) => {
49
+ let body = '';
50
+ res.on('data', chunk => body += chunk);
51
+ res.on('end', () => {
52
+ try {
53
+ resolve(JSON.parse(body));
54
+ } catch {
55
+ resolve({ raw: body });
56
+ }
57
+ });
58
+ });
59
+
60
+ req.on('error', (e) => resolve({ error: e.message }));
61
+ req.on('timeout', () => {
62
+ req.destroy();
63
+ resolve({ error: 'timeout' });
64
+ });
65
+
66
+ if (data) req.write(typeof data === 'string' ? data : JSON.stringify(data));
67
+ req.end();
68
+ });
69
+ }
70
+
71
+ // ═══════════════════════════════════════════════════════════════════════════
72
+ // Load Auth Token
73
+ // ═══════════════════════════════════════════════════════════════════════════
74
+ function loadAuthToken() {
75
+ const homeDir = os.homedir();
76
+ const ekkosConfig = path.join(homeDir, '.ekkos', 'config.json');
77
+ let authToken = '';
78
+ let userId = '';
79
+
80
+ // 1. First try ~/.ekkos/config.json (set by VS Code extension - most portable)
81
+ // Prefer hookApiKey (scoped key for hooks) over apiKey (legacy)
82
+ if (fs.existsSync(ekkosConfig)) {
83
+ try {
84
+ const config = JSON.parse(fs.readFileSync(ekkosConfig, 'utf8'));
85
+ authToken = config.hookApiKey || config.apiKey || '';
86
+ userId = config.userId || '';
87
+ } catch {}
88
+ }
89
+
90
+ // 2. Then try project .env.local
91
+ if (!authToken) {
92
+ const envLocal = path.join(state.PROJECT_ROOT, '.env.local');
93
+ if (fs.existsSync(envLocal)) {
94
+ try {
95
+ const content = fs.readFileSync(envLocal, 'utf8');
96
+ const match = content.match(/^SUPABASE_SECRET_KEY=(.+)$/m);
97
+ if (match) {
98
+ authToken = match[1].replace(/["']/g, '').trim();
99
+ }
100
+ } catch {}
101
+ }
102
+ }
103
+
104
+ // 3. Finally try environment variable
105
+ if (!authToken) {
106
+ authToken = process.env.SUPABASE_SECRET_KEY || '';
107
+ }
108
+
109
+ return { authToken, userId };
110
+ }
111
+
112
+ // ═══════════════════════════════════════════════════════════════════════════
113
+ // Main Hook Logic
114
+ // ═══════════════════════════════════════════════════════════════════════════
115
+ async function main() {
116
+ // Read JSON input from stdin
117
+ let inputData = '';
118
+ for await (const chunk of process.stdin) {
119
+ inputData += chunk;
120
+ }
121
+
122
+ let input;
123
+ try {
124
+ input = JSON.parse(inputData);
125
+ } catch {
126
+ process.exit(0);
127
+ }
128
+
129
+ const sessionId = input.session_id || 'unknown';
130
+ const transcriptPath = input.transcript_path || '';
131
+ let modelUsed = input.model || 'claude-sonnet-4-5';
132
+ const timestamp = new Date().toISOString();
133
+
134
+ // Load auth
135
+ const { authToken, userId } = loadAuthToken();
136
+ if (!authToken) {
137
+ process.exit(0);
138
+ }
139
+
140
+ const MEMORY_API_URL = 'https://mcp.ekkos.dev';
141
+
142
+ // Extract conversation from transcript
143
+ let lastUser = '';
144
+ let lastAssistant = '';
145
+
146
+ if (transcriptPath && fs.existsSync(transcriptPath)) {
147
+ try {
148
+ const transcriptContent = fs.readFileSync(transcriptPath, 'utf8');
149
+ const lines = transcriptContent.split('\n').filter(Boolean).map(l => {
150
+ try { return JSON.parse(l); } catch { return null; }
151
+ }).filter(Boolean);
152
+
153
+ // Find last user query (from queue-operation enqueue)
154
+ const enqueues = lines.filter(l => l.type === 'queue-operation' && l.operation === 'enqueue');
155
+ if (enqueues.length > 0) {
156
+ const lastEnqueue = enqueues[enqueues.length - 1];
157
+ if (lastEnqueue.content) {
158
+ const textContent = lastEnqueue.content.find(c => c.type === 'text');
159
+ if (textContent) lastUser = textContent.text;
160
+ }
161
+ }
162
+
163
+ // Find assistant messages after last user query
164
+ const assistantMsgs = lines.filter(l => l.type === 'assistant');
165
+ if (assistantMsgs.length > 0) {
166
+ const lastAssistantMsg = assistantMsgs[assistantMsgs.length - 1];
167
+ if (lastAssistantMsg.message?.content) {
168
+ if (Array.isArray(lastAssistantMsg.message.content)) {
169
+ lastAssistant = lastAssistantMsg.message.content
170
+ .filter(c => c.type === 'text')
171
+ .map(c => c.text)
172
+ .join(' ');
173
+ } else {
174
+ lastAssistant = lastAssistantMsg.message.content;
175
+ }
176
+ }
177
+ }
178
+ } catch {}
179
+ }
180
+
181
+ // Fallback: use response from input
182
+ if (!lastAssistant) {
183
+ lastAssistant = input.response || '';
184
+ }
185
+
186
+ // Load patterns from RETRIEVE step
187
+ const storedData = state.loadPatterns(sessionId);
188
+ const patterns = storedData.patterns || [];
189
+ const patternCount = patterns.length;
190
+ const patternIds = patterns.map(p => p.id || p.pattern_id).filter(Boolean);
191
+ if (storedData.model_used) modelUsed = storedData.model_used;
192
+ const taskId = storedData.task_id || '';
193
+
194
+ // ═══════════════════════════════════════════════════════════════════════════
195
+ // Pattern Acknowledgment Detection (PatternGuard)
196
+ // ═══════════════════════════════════════════════════════════════════════════
197
+ let appliedPatternIds = [];
198
+ let skippedPatternIds = [];
199
+
200
+ if (lastAssistant && patternCount > 0) {
201
+ // Check for [ekkOS_SELECT] block
202
+ const selectMatch = lastAssistant.match(/\[ekkOS_SELECT\]([\s\S]*?)\[\/ekkOS_SELECT\]/);
203
+ if (selectMatch) {
204
+ const idMatches = selectMatch[1].matchAll(/id:\s*([a-f0-9-]+)/g);
205
+ for (const m of idMatches) {
206
+ if (m[1].length >= 8) {
207
+ appliedPatternIds.push(m[1]);
208
+ console.error(`[ekkOS_SELECT] Applied: ${m[1].substring(0, 8)}...`);
209
+ }
210
+ }
211
+ }
212
+
213
+ // Check for [ekkOS_SKIP] block
214
+ const skipMatch = lastAssistant.match(/\[ekkOS_SKIP\]([\s\S]*?)\[\/ekkOS_SKIP\]/);
215
+ if (skipMatch) {
216
+ const idMatches = skipMatch[1].matchAll(/id:\s*([a-f0-9-]+)/g);
217
+ for (const m of idMatches) {
218
+ if (m[1].length >= 8) {
219
+ skippedPatternIds.push(m[1]);
220
+ console.error(`[ekkOS_SKIP] Skipped: ${m[1].substring(0, 8)}...`);
221
+ }
222
+ }
223
+ }
224
+
225
+ // Calculate coverage
226
+ const acknowledged = appliedPatternIds.length + skippedPatternIds.length;
227
+ if (patternCount > 0) {
228
+ const coverage = Math.round((acknowledged / patternCount) * 100);
229
+ if (coverage < 100) {
230
+ console.error(`${YELLOW}[PatternGuard] Coverage: ${coverage}% (${acknowledged}/${patternCount} patterns acknowledged)${RESET}`);
231
+ } else {
232
+ console.error(`${GREEN}[PatternGuard] 100% coverage - all patterns acknowledged${RESET}`);
233
+ }
234
+ }
235
+
236
+ // Legacy: Check for [ekkOS_APPLY] markers
237
+ if (appliedPatternIds.length === 0 && skippedPatternIds.length === 0) {
238
+ for (const pattern of patterns) {
239
+ const title = pattern.title || '';
240
+ const pid = pattern.id || pattern.pattern_id || '';
241
+ if (lastAssistant.includes('[ekkOS_APPLY]') && lastAssistant.includes(title)) {
242
+ appliedPatternIds.push(pid);
243
+ console.error(`[ekkOS_APPLY_DETECTED] Pattern: "${title}"`);
244
+ }
245
+ }
246
+ }
247
+ }
248
+
249
+ console.log('');
250
+
251
+ // ═══════════════════════════════════════════════════════════════════════════
252
+ // [ekkOS_LEARN_DETECT] Check for manual forge markers
253
+ // ═══════════════════════════════════════════════════════════════════════════
254
+ let forgeCount = 0;
255
+ if (lastAssistant) {
256
+ const forgeMatches = lastAssistant.matchAll(/\[ekkOS_LEARN\][^"]*"([^"]+)"/g);
257
+ const forgeMarkers = [...forgeMatches];
258
+
259
+ if (forgeMarkers.length > 0) {
260
+ console.log(`${YELLOW}+${RESET} ${YELLOW}[[[[ekkOS_Learn detect]]]]${RESET} ${DIM}found forge markers${RESET}`);
261
+
262
+ for (const marker of forgeMarkers) {
263
+ const patternTitle = marker[1];
264
+ if (patternTitle) {
265
+ console.log(` ${GREEN}+${RESET} forging: "${patternTitle}"`);
266
+ forgeCount++;
267
+
268
+ // Send to forge endpoint (don't wait)
269
+ httpRequest(`${MEMORY_API_URL}/api/v1/patterns`, {
270
+ method: 'POST',
271
+ headers: {
272
+ 'Authorization': `Bearer ${authToken}`,
273
+ 'Content-Type': 'application/json'
274
+ },
275
+ timeout: 10000
276
+ }, {
277
+ title: patternTitle,
278
+ problem: (lastUser || '').substring(0, 500),
279
+ solution: (lastAssistant || '').substring(0, 2000),
280
+ tags: ['hook-detected', 'golden-loop', 'claude-code-node'],
281
+ source: 'claude-code-hook-node',
282
+ confidence: 0.85
283
+ }).then(res => {
284
+ if (res.pattern_id || res.id) {
285
+ const pid = res.pattern_id || res.id;
286
+ console.error(` ${GREEN}✓${RESET} forged ${DIM}(ID: ${pid.substring(0, 8)}...)${RESET}`);
287
+ }
288
+ }).catch(() => {});
289
+ }
290
+ }
291
+ }
292
+ }
293
+
294
+ // ═══════════════════════════════════════════════════════════════════════════
295
+ // [ekkOS_CAPTURE] Send data to backend
296
+ // ═══════════════════════════════════════════════════════════════════════════
297
+ let captured = false;
298
+ if (lastUser && lastAssistant) {
299
+ console.log(`${CYAN}+${RESET} ${CYAN}[[[[ekkOS_Capture]]]]${RESET} ${DIM}sending to memory substrate${RESET}`);
300
+
301
+ const payload = {
302
+ user_query: lastUser,
303
+ assistant_response: lastAssistant,
304
+ session_id: `claude-code-${sessionId}`,
305
+ user_id: userId || 'system',
306
+ patterns_retrieved: patternIds,
307
+ patterns_applied: appliedPatternIds,
308
+ metadata: {
309
+ source: 'claude-code-node',
310
+ model_used: modelUsed,
311
+ patterns_retrieved_count: patternCount,
312
+ patterns_applied_count: appliedPatternIds.length,
313
+ task_id: taskId,
314
+ captured_at: timestamp,
315
+ auto_apply_detection: true,
316
+ user_id: userId || 'system'
317
+ }
318
+ };
319
+
320
+ const captureResponse = await httpRequest(`${MEMORY_API_URL}/api/v1/memory/capture`, {
321
+ method: 'POST',
322
+ headers: {
323
+ 'Authorization': `Bearer ${authToken}`,
324
+ 'Content-Type': 'application/json'
325
+ },
326
+ timeout: 10000
327
+ }, payload);
328
+
329
+ if (captureResponse.conversation_id) {
330
+ const convId = captureResponse.conversation_id;
331
+ console.log(` ${GREEN}+${RESET} saved ${DIM}(ID: ${convId.substring(0, 8)}...)${RESET}`);
332
+ console.log(`${BLUE}+${RESET} ${BLUE}[[[[ekkOS_Analyze]]]]${RESET} ${DIM}queued for async processing${RESET}`);
333
+ captured = true;
334
+ } else {
335
+ console.log(` ${RED}-${RESET} failed to save`);
336
+ }
337
+ } else {
338
+ console.log(`${DIM}-${RESET} ${DIM}[[[[ekkOS_Capture]]]]${RESET} ${DIM}skipped (no content)${RESET}`);
339
+ }
340
+
341
+ // Cleanup state file
342
+ state.clearPatterns(sessionId);
343
+
344
+ // ═══════════════════════════════════════════════════════════════════════════
345
+ // [ekkOS_REFLEX] Send turn_end event to trigger 3-Judge evaluation
346
+ // ═══════════════════════════════════════════════════════════════════════════
347
+ const REFLEX_API_URL = process.env.REFLEX_API_URL || 'https://mcp.ekkos.dev/api/v1/reflex/log';
348
+
349
+ if (captured && lastUser && lastAssistant) {
350
+ console.log(`${YELLOW}+${RESET} ${YELLOW}[[[[ekkOS_3-Judges]]]]${RESET} ${DIM}triggering consensus evaluation${RESET}`);
351
+
352
+ // Send in background (don't wait)
353
+ httpRequest(REFLEX_API_URL, {
354
+ method: 'POST',
355
+ headers: {
356
+ 'Authorization': `Bearer ${authToken}`,
357
+ 'Content-Type': 'application/json'
358
+ },
359
+ timeout: 10000
360
+ }, {
361
+ action: 'turn_end',
362
+ summary: `Claude Code session: ${(lastUser || '').substring(0, 100)}...`,
363
+ details: {
364
+ user_query: lastUser,
365
+ assistant_response: (lastAssistant || '').substring(0, 2000),
366
+ source: 'claude-code-node',
367
+ model_used: modelUsed,
368
+ patterns_retrieved: patternCount,
369
+ patterns_applied: appliedPatternIds.length,
370
+ session_id: `claude-code-${sessionId}`,
371
+ timestamp: timestamp
372
+ },
373
+ learn: {
374
+ lookups: patternCount,
375
+ reuse: appliedPatternIds.length,
376
+ saves: 0,
377
+ abstain: 0
378
+ },
379
+ context: {
380
+ source: 'claude-code-node',
381
+ task_id: taskId
382
+ }
383
+ }).then(res => {
384
+ if (res.id) {
385
+ console.error(` ${GREEN}+${RESET} event queued ${DIM}(ID: ${res.id.substring(0, 8)}...)${RESET}`);
386
+ }
387
+ }).catch(() => {});
388
+ }
389
+
390
+ // ═══════════════════════════════════════════════════════════════════════════
391
+ // Summary
392
+ // ═══════════════════════════════════════════════════════════════════════════
393
+ console.log('');
394
+ console.log(`${CYAN}${BOLD}[[[[ekkOS_Golden loop]]]]${RESET}`);
395
+ console.log('');
396
+
397
+ if (captured) {
398
+ console.log(` ${GREEN}+${RESET} capture: saved to substrate`);
399
+ console.log(` ${GREEN}+${RESET} 3-judge: consensus eval queued`);
400
+ } else {
401
+ console.log(` ${DIM}-${RESET} capture: skipped (no content)`);
402
+ }
403
+ console.log(` ${GREEN}+${RESET} patterns: ${patternCount} retrieved`);
404
+ if (forgeCount > 0) {
405
+ console.log(` ${GREEN}+${RESET} forged: ${forgeCount} new patterns (from [ekkOS_LEARN] markers)`);
406
+ }
407
+ console.log(` ${GREEN}+${RESET} analyze: async backend processing`);
408
+ console.log('');
409
+
410
+ // Small delay to let background requests complete
411
+ await new Promise(resolve => setTimeout(resolve, 100));
412
+
413
+ process.exit(0);
414
+ }
415
+
416
+ main().catch(() => process.exit(0));