@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.
- package/dist/cache/LocalSessionStore.d.ts +129 -0
- package/dist/cache/LocalSessionStore.js +688 -0
- package/dist/cache/capture.d.ts +26 -0
- package/dist/cache/capture.js +461 -0
- package/dist/cache/index.d.ts +7 -0
- package/dist/cache/index.js +23 -0
- package/dist/cache/types.d.ts +147 -0
- package/dist/cache/types.js +40 -0
- package/dist/commands/init.d.ts +9 -0
- package/dist/commands/init.js +478 -0
- package/dist/commands/run.d.ts +12 -0
- package/dist/commands/run.js +829 -0
- package/dist/commands/setup.d.ts +6 -0
- package/dist/commands/setup.js +658 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +109 -0
- package/dist/commands/test.d.ts +1 -0
- package/dist/commands/test.js +157 -0
- package/dist/deploy/agents.d.ts +15 -0
- package/dist/deploy/agents.js +72 -0
- package/dist/deploy/hooks.d.ts +16 -0
- package/dist/deploy/hooks.js +121 -0
- package/dist/deploy/index.d.ts +7 -0
- package/dist/deploy/index.js +24 -0
- package/dist/deploy/instructions.d.ts +12 -0
- package/dist/deploy/instructions.js +36 -0
- package/dist/deploy/mcp.d.ts +19 -0
- package/dist/deploy/mcp.js +109 -0
- package/dist/deploy/plugins.d.ts +19 -0
- package/dist/deploy/plugins.js +62 -0
- package/dist/deploy/settings.d.ts +8 -0
- package/dist/deploy/settings.js +84 -0
- package/dist/deploy/skills.d.ts +19 -0
- package/dist/deploy/skills.js +60 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +71 -0
- package/dist/restore/RestoreOrchestrator.d.ts +48 -0
- package/dist/restore/RestoreOrchestrator.js +481 -0
- package/dist/restore/index.d.ts +4 -0
- package/dist/restore/index.js +20 -0
- package/dist/utils/platform.d.ts +29 -0
- package/dist/utils/platform.js +65 -0
- package/dist/utils/session-words.json +119 -0
- package/dist/utils/state.d.ts +57 -0
- package/dist/utils/state.js +186 -0
- package/dist/utils/templates.d.ts +24 -0
- package/dist/utils/templates.js +118 -0
- package/package.json +48 -0
- package/templates/CLAUDE.md +287 -0
- package/templates/README.md +378 -0
- package/templates/agents/README.md +182 -0
- package/templates/agents/code-reviewer.md +166 -0
- package/templates/agents/debug-detective.md +169 -0
- package/templates/agents/ekkOS_Vercel.md +99 -0
- package/templates/agents/extension-manager.md +229 -0
- package/templates/agents/git-companion.md +185 -0
- package/templates/agents/github-test-agent.md +321 -0
- package/templates/agents/railway-manager.md +179 -0
- package/templates/claude-plugins/PHASE2_COMPLETION.md +346 -0
- package/templates/claude-plugins/PLUGIN_PROPOSALS.md +1776 -0
- package/templates/claude-plugins/README.md +587 -0
- package/templates/claude-plugins/agents/code-reviewer.json +14 -0
- package/templates/claude-plugins/agents/debug-detective.json +15 -0
- package/templates/claude-plugins/agents/git-companion.json +14 -0
- package/templates/claude-plugins/blog-manager/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/blog-manager/commands/blog.md +691 -0
- package/templates/claude-plugins/golden-loop-monitor/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/golden-loop-monitor/commands/loop-status.md +434 -0
- package/templates/claude-plugins/learning-tracker/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/learning-tracker/commands/my-patterns.md +282 -0
- package/templates/claude-plugins/memory-lens/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/memory-lens/commands/memory-search.md +181 -0
- package/templates/claude-plugins/pattern-coach/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/pattern-coach/commands/forge.md +365 -0
- package/templates/claude-plugins/project-schema-validator/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins/project-schema-validator/commands/validate-schema.md +582 -0
- package/templates/claude-plugins-admin/AGENT_TEAM_PROPOSALS.md +819 -0
- package/templates/claude-plugins-admin/README.md +446 -0
- package/templates/claude-plugins-admin/autonomous-admin-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/autonomous-admin-agent/commands/agent.md +595 -0
- package/templates/claude-plugins-admin/backend-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/backend-agent/commands/backend.md +798 -0
- package/templates/claude-plugins-admin/deploy-guardian/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/deploy-guardian/commands/deploy.md +554 -0
- package/templates/claude-plugins-admin/frontend-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/frontend-agent/commands/frontend.md +881 -0
- package/templates/claude-plugins-admin/mcp-server-manager/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/mcp-server-manager/commands/mcp.md +85 -0
- package/templates/claude-plugins-admin/memory-system-monitor/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/memory-system-monitor/commands/memory-health.md +569 -0
- package/templates/claude-plugins-admin/qa-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/qa-agent/commands/qa.md +863 -0
- package/templates/claude-plugins-admin/tech-lead-agent/.claude-plugin/plugin.json +8 -0
- package/templates/claude-plugins-admin/tech-lead-agent/commands/lead.md +732 -0
- package/templates/commands/continue.md +47 -0
- package/templates/cursor-hooks/after-agent-response.sh +117 -0
- package/templates/cursor-hooks/before-submit-prompt.sh +419 -0
- package/templates/cursor-hooks/hooks.json +20 -0
- package/templates/cursor-hooks/lib/contract.sh +320 -0
- package/templates/cursor-hooks/stop.sh +75 -0
- package/templates/cursor-rules/ekkos-memory.md +187 -0
- package/templates/hooks/assistant-response.sh +96 -0
- package/templates/hooks/hooks.json +28 -0
- package/templates/hooks/lib/contract.sh +320 -0
- package/templates/hooks/lib/state.sh +158 -0
- package/templates/hooks/session-start.ps1 +41 -0
- package/templates/hooks/session-start.sh +318 -0
- package/templates/hooks/stop.ps1 +16 -0
- package/templates/hooks/stop.sh +989 -0
- package/templates/hooks/user-prompt-submit.ps1 +174 -0
- package/templates/hooks/user-prompt-submit.sh +587 -0
- package/templates/hooks-node/lib/state.js +187 -0
- package/templates/hooks-node/stop.js +416 -0
- package/templates/hooks-node/user-prompt-submit.js +337 -0
- package/templates/plan-template.md +306 -0
- package/templates/rules/00-hooks-contract.mdc +89 -0
- package/templates/rules/30-ekkos-core.mdc +188 -0
- package/templates/rules/31-ekkos-messages.mdc +78 -0
- package/templates/skills/continue/SKILL.md +169 -0
- package/templates/skills/ekkOS_Deep_Recall/Skill.md +282 -0
- package/templates/skills/ekkOS_Learn/Skill.md +265 -0
- package/templates/skills/ekkOS_Memory_First/Skill.md +206 -0
- package/templates/skills/ekkOS_Plan_Assist/Skill.md +302 -0
- package/templates/skills/ekkOS_Preferences/Skill.md +247 -0
- package/templates/skills/ekkOS_Reflect/Skill.md +257 -0
- package/templates/skills/ekkOS_Safety/Skill.md +265 -0
- package/templates/skills/ekkOS_Schema/Skill.md +251 -0
- package/templates/skills/ekkOS_Summary/Skill.md +257 -0
- package/templates/skills/ekkOS_Vault/Skill.md +287 -0
- package/templates/skills/permissions/Skill.md +322 -0
- package/templates/spec-template.md +159 -0
- package/templates/windsurf-hooks/before-submit-prompt.sh +238 -0
- package/templates/windsurf-hooks/hooks.json +10 -0
- package/templates/windsurf-hooks/lib/contract.sh +320 -0
- 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));
|