@ekkos/cli 1.3.5 → 1.3.7

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 ekkOS
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -71,7 +71,9 @@ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12
71
71
  // ── Pricing ──
72
72
  // Pricing per MTok from https://platform.claude.com/docs/en/about-claude/pricing
73
73
  const MODEL_PRICING = {
74
+ 'claude-opus-4-6-20260514': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
74
75
  'claude-opus-4-6': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
76
+ 'claude-sonnet-4-6-20260514': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
75
77
  'claude-opus-4-5-20250620': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
76
78
  'claude-sonnet-4-6': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
77
79
  'claude-sonnet-4-5-20250929': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
@@ -96,12 +98,23 @@ function calculateTurnCost(model, usage) {
96
98
  (usage.cache_creation_tokens / 1000000) * p.cacheWrite +
97
99
  (usage.cache_read_tokens / 1000000) * p.cacheRead);
98
100
  }
99
- function getModelCtxSize(model) {
100
- if (model.includes('opus'))
101
+ function getModelCtxSize(model, contextTierHint) {
102
+ const normalized = (model || '').toLowerCase();
103
+ if (contextTierHint === '1m')
104
+ return 1000000;
105
+ // Claude 4.6 has GA 1M context in Claude Code/API.
106
+ if (/^claude-(?:opus|sonnet)-4-6(?:$|-)/.test(normalized))
107
+ return 1000000;
108
+ // Keep Gemini on the 1M class in dashboard context math.
109
+ if (normalized.startsWith('gemini-'))
110
+ return 1000000;
111
+ if (normalized.includes('1m'))
112
+ return 1000000;
113
+ if (normalized.includes('opus'))
101
114
  return 200000;
102
- if (model.includes('haiku'))
115
+ if (normalized.includes('haiku'))
103
116
  return 200000;
104
- if (model.includes('sonnet'))
117
+ if (normalized.includes('sonnet'))
105
118
  return 200000;
106
119
  return 200000; // Default Anthropic context
107
120
  }
@@ -183,12 +196,15 @@ function parseJsonlFile(jsonlPath, sessionName) {
183
196
  const evictionState = typeof entry.message._ekkos_eviction_state === 'string'
184
197
  ? entry.message._ekkos_eviction_state
185
198
  : (parseCacheHintValue(cacheHint, 'eviction') || 'unknown');
199
+ const contextTierHint = typeof entry.message._ekkos_context_tier === 'string'
200
+ ? entry.message._ekkos_context_tier.trim().toLowerCase()
201
+ : undefined;
186
202
  const inputTokens = usage.input_tokens || 0;
187
203
  const outputTokens = usage.output_tokens || 0;
188
204
  const cacheReadTokens = usage.cache_read_input_tokens || 0;
189
205
  const cacheCreationTokens = usage.cache_creation_input_tokens || 0;
190
206
  const contextTokens = inputTokens + cacheReadTokens + cacheCreationTokens;
191
- const modelCtxSize = getModelCtxSize(model);
207
+ const modelCtxSize = getModelCtxSize(routedModel, contextTierHint);
192
208
  const contextPct = (contextTokens / modelCtxSize) * 100;
193
209
  const ts = entry.timestamp || new Date().toISOString();
194
210
  if (!startedAt)
@@ -215,6 +231,7 @@ function parseJsonlFile(jsonlPath, sessionName) {
215
231
  const turnData = {
216
232
  turn: turnNum,
217
233
  contextPct,
234
+ modelContextSize: modelCtxSize,
218
235
  input: inputTokens,
219
236
  cacheRead: cacheReadTokens,
220
237
  cacheCreate: cacheCreationTokens,
@@ -267,7 +284,9 @@ function parseJsonlFile(jsonlPath, sessionName) {
267
284
  const currentContextTokens = lastTurn
268
285
  ? lastTurn.input + lastTurn.cacheRead + lastTurn.cacheCreate
269
286
  : 0;
270
- const modelContextSize = getModelCtxSize(model);
287
+ const modelContextSize = lastTurn
288
+ ? lastTurn.modelContextSize
289
+ : getModelCtxSize(model);
271
290
  return {
272
291
  sessionName,
273
292
  model,
@@ -1100,8 +1119,8 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1100
1119
  try {
1101
1120
  const ctxPct = Math.min(data.currentContextPct, 100);
1102
1121
  const ctxColor = ctxPct < 50 ? 'green' : ctxPct < 80 ? 'yellow' : 'red';
1103
- const tokensK = Math.round(data.currentContextTokens / 1000);
1104
- const maxK = Math.round(data.modelContextSize / 1000);
1122
+ const tokensLabel = fmtK(data.currentContextTokens);
1123
+ const maxLabel = fmtK(data.modelContextSize);
1105
1124
  // Visual progress bar (fills available width)
1106
1125
  const contextInnerWidth = Math.max(10, contextBox.width - 2);
1107
1126
  // Extend bar slightly closer to mascot while keeping a small visual gap.
@@ -1121,7 +1140,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1121
1140
  const hitColor = data.cacheHitRate >= 80 ? 'green' : data.cacheHitRate >= 50 ? 'yellow' : 'red';
1122
1141
  const cappedMax = Math.min(data.maxContextPct, 100);
1123
1142
  contextBox.setContent(` ${bar}\n` +
1124
- ` {${ctxColor}-fg}${ctxPct.toFixed(0)}%{/${ctxColor}-fg} ${tokensK}K/${maxK}K` +
1143
+ ` {${ctxColor}-fg}${ctxPct.toFixed(0)}%{/${ctxColor}-fg} ${tokensLabel}/${maxLabel}` +
1125
1144
  ` {white-fg}Input{/white-fg} $${breakdown.input.toFixed(2)}` +
1126
1145
  ` {green-fg}Read{/green-fg} $${breakdown.read.toFixed(2)}` +
1127
1146
  ` {yellow-fg}Write{/yellow-fg} $${breakdown.write.toFixed(2)}` +
@@ -1766,7 +1785,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
1766
1785
  const tier = cachedProfile.organization?.rate_limit_tier ?? 'unknown';
1767
1786
  const status = cachedProfile.organization?.subscription_status ?? 'unknown';
1768
1787
  const extraUsage = cachedProfile.organization?.has_extra_usage_enabled
1769
- ? ' {green-fg}extra usage on{/green-fg}'
1788
+ ? ' {green-fg}extra usage on{/green-fg} {gray-fg}(legacy >200k only){/gray-fg}'
1770
1789
  : '';
1771
1790
  line2 =
1772
1791
  ` {bold}Plan:{/bold} ${plan} {bold}Tier:{/bold} ${tier} {bold}Sub:{/bold} ${status}${extraUsage}`;
@@ -154,8 +154,19 @@ async function gemini(options = {}) {
154
154
  console.log(chalk_1.default.cyan(' 🧠 ekkOS_Continuum Loaded!'));
155
155
  }
156
156
  console.log('');
157
+ // Extract any trailing arguments meant for the inner CLI
158
+ // We look for 'gemini' in process.argv and pass everything after it.
159
+ // If the user did `ekkos gemini -m gemini-3.1-pro-preview`, we want to pass `-m gemini-3.1-pro-preview`
160
+ let extraArgs = [];
161
+ const geminiIdx = process.argv.indexOf('gemini');
162
+ if (geminiIdx !== -1) {
163
+ extraArgs = process.argv.slice(geminiIdx + 1).filter(a => {
164
+ // Filter out ekkos wrapper options
165
+ return !['--skip-proxy', '-v', '--verbose'].includes(a) && !(a === '-s' || a === '--session' || process.argv[process.argv.indexOf(a) - 1] === '-s' || process.argv[process.argv.indexOf(a) - 1] === '--session');
166
+ });
167
+ }
157
168
  // Spawn Gemini CLI — stdio: inherit for full terminal passthrough
158
- const child = (0, child_process_1.spawn)(geminiPath, [], {
169
+ const child = (0, child_process_1.spawn)(geminiPath, extraArgs, {
159
170
  stdio: 'inherit',
160
171
  env,
161
172
  cwd: process.cwd(),
package/dist/index.js CHANGED
@@ -59,6 +59,7 @@ const scan_1 = require("./commands/scan");
59
59
  const chalk_1 = __importDefault(require("chalk"));
60
60
  const fs = __importStar(require("fs"));
61
61
  const path = __importStar(require("path"));
62
+ const child_process_1 = require("child_process");
62
63
  // Get version from package.json (CommonJS compatible)
63
64
  const pkgPath = path.resolve(__dirname, '../package.json');
64
65
  const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
@@ -75,16 +76,12 @@ commander_1.program
75
76
  .addHelpText('after', [
76
77
  '',
77
78
  chalk_1.default.cyan.bold('Examples:'),
78
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos')} ${chalk_1.default.gray('Start Claude Code with ekkOS memory (default: run)')}`,
79
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos init')} ${chalk_1.default.gray('First-time setup authenticate + configure your IDE')}`,
80
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos scan')} ${chalk_1.default.gray('Scan repo structure and seed system registry')}`,
81
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos scan --compile')} ${chalk_1.default.gray('Scan, seed, and trigger compile pass')}`,
82
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run --dashboard')} ${chalk_1.default.gray('Launch with live usage dashboard (tmux split)')}`,
83
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run -b')} ${chalk_1.default.gray('Launch with bypass permissions mode')}`,
84
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos doctor --fix')} ${chalk_1.default.gray('Check and auto-fix system prerequisites')}`,
85
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos usage daily')} ${chalk_1.default.gray("View today's token usage and costs")}`,
86
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos gemini')} ${chalk_1.default.gray('Launch Gemini CLI with ekkOS memory proxy')}`,
87
- ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos swarm launch -t "build X"')} ${chalk_1.default.gray('Launch parallel workers on a task')}`,
79
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos')} ${chalk_1.default.gray('Start Claude Code with ekkOS memory (default)')}`,
80
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos codex')} ${chalk_1.default.gray('Start Codex (OpenAI) mode')}`,
81
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos daemon start')} ${chalk_1.default.gray('Start the background mobile sync service')}`,
82
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos connect gemini')} ${chalk_1.default.gray('Securely store your Gemini API key in the cloud')}`,
83
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos agent create --path ./repo')} ${chalk_1.default.gray('Spawn a headless remote agent in a directory')}`,
84
+ ` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run --dashboard')} ${chalk_1.default.gray('Launch Claude with live usage dashboard')}`,
88
85
  '',
89
86
  chalk_1.default.gray(' Run ') + chalk_1.default.white('ekkos <command> --help') + chalk_1.default.gray(' for detailed options on any command.'),
90
87
  '',
@@ -152,38 +149,48 @@ commander_1.program
152
149
  title: 'Getting Started',
153
150
  icon: '▸',
154
151
  commands: [
155
- { name: 'init', desc: 'Authenticate and configure your IDE (Claude, Cursor, Windsurf)' },
152
+ { name: 'init', desc: 'First-time setup authenticate and configure IDEs' },
156
153
  { name: 'scan', desc: 'Scan repo structure and seed ekkOS system registry' },
157
- { name: 'status', desc: 'Show memory status and installation info' },
158
- { name: 'test', desc: 'Test connection to ekkOS memory API' },
159
- { name: 'doctor', desc: 'Check system prerequisites (Node, PTY, Claude, MCP)' },
154
+ { name: 'status', desc: 'Show overall system status (Memory, Sync Daemon, Auth)' },
155
+ { name: 'doctor', desc: 'Check system prerequisites and fix runaway processes' },
160
156
  ],
161
157
  },
162
158
  {
163
- title: 'Running',
159
+ title: 'Launch Agents (Local + Mobile Synced)',
164
160
  icon: '▸',
165
161
  commands: [
166
- { name: 'run', desc: 'Launch Claude Code with ekkOS memory', note: 'default' },
167
- { name: 'test-claude', desc: 'Launch Claude with proxy only (no PTY) for debugging' },
168
- { name: 'sessions', desc: 'List active Claude Code sessions' },
169
- { name: 'claw', desc: 'OpenClaw integration status and upgrade controls' },
162
+ { name: 'run', desc: 'Launch Claude Code with memory and mobile control', note: 'default' },
163
+ { name: 'codex', desc: 'Launch Codex (OpenAI) with mobile control' },
164
+ { name: 'gemini', desc: 'Launch Gemini mode (ACP) with mobile control' },
165
+ { name: 'acp', desc: 'Launch a generic ACP-compatible agent' },
170
166
  ],
171
167
  },
172
168
  {
173
- title: 'Monitoring & Usage',
169
+ title: 'Mobile Sync & Cloud (Synk)',
174
170
  icon: '▸',
175
171
  commands: [
176
- { name: 'usage', desc: 'Token usage and cost tracking (daily, weekly, monthly, session)' },
177
- { name: 'dashboard', desc: 'Live TUI dashboard for session monitoring' },
178
- { name: 'stream', desc: 'Stream capture status and management' },
179
- { name: 'hooks', desc: '[DEPRECATED] Hooks no longer needed use `ekkos run`' },
172
+ { name: 'daemon', desc: 'Manage background mobile sync service (start, stop, status)' },
173
+ { name: 'connect', desc: 'Securely store AI vendor API keys in the cloud' },
174
+ { name: 'sandbox', desc: 'Configure OS-level sandboxing for agent execution' },
175
+ { name: 'notify', desc: 'Send push notifications to your Synk mobile app' },
180
176
  ],
181
177
  },
182
178
  {
183
- title: 'Swarm (Multi-Agent)',
179
+ title: 'Headless Agents (Remote Control)',
184
180
  icon: '▸',
185
181
  commands: [
186
- { name: 'swarm', desc: 'Parallel workers, Q-learning routing, swarm dashboard' },
182
+ { name: 'agent', desc: 'Manage remote headless agents (create, list, stop)' },
183
+ { name: 'swarm', desc: 'Parallel workers, Q-learning routing, and swarm dashboard' },
184
+ ],
185
+ },
186
+ {
187
+ title: 'Monitoring & Usage',
188
+ icon: '▸',
189
+ commands: [
190
+ { name: 'auth', desc: 'Manage authentication for Memory, Agents, and Mobile App' },
191
+ { name: 'usage', desc: 'Token usage and cost tracking (daily, weekly, monthly, session)' },
192
+ { name: 'dashboard', desc: 'Live TUI dashboard for session monitoring' },
193
+ { name: 'sessions', desc: 'List active local CLI sessions' },
187
194
  ],
188
195
  },
189
196
  ];
@@ -501,6 +508,74 @@ swarmCmd
501
508
  (0, swarm_setup_1.swarmSetup)();
502
509
  });
503
510
  swarmCmd.addCommand(swarm_dashboard_1.swarmDashboardCommand);
511
+ // --- Remote & Agent Wrapper Helpers ---
512
+ function runRemoteCommand(command, ...args) {
513
+ const isAgent = command === 'agent';
514
+ const pkgName = isAgent ? 'ekkos-agent' : 'ekkos-remote';
515
+ const binName = isAgent ? 'ekkos-agent.mjs' : 'ekkos-synk.mjs';
516
+ // Try resolving local monorepo first
517
+ let binPath = path.resolve(__dirname, '../../' + pkgName + '/bin/' + binName);
518
+ // If we are installed globally (dist folder structure), resolve relatively
519
+ if (!fs.existsSync(binPath)) {
520
+ try {
521
+ const globalPath = require.resolve('@ekkos/' + (isAgent ? 'agent' : 'remote') + '/package.json');
522
+ binPath = path.join(path.dirname(globalPath), 'bin', binName);
523
+ }
524
+ catch (e) {
525
+ // Fallback relative to current dist
526
+ binPath = path.resolve(__dirname, '../../' + pkgName + '/bin/' + binName);
527
+ }
528
+ }
529
+ const p = (0, child_process_1.spawn)('node', [binPath, ...args], {
530
+ stdio: 'inherit',
531
+ env: process.env
532
+ });
533
+ p.on('exit', (code) => {
534
+ process.exit(code || 0);
535
+ });
536
+ }
537
+ // Mobile Sync & Cloud (Synk) Commands
538
+ commander_1.program
539
+ .command('auth')
540
+ .description('Manage authentication for Memory, Agents, and Mobile App')
541
+ .allowUnknownOption()
542
+ .action(() => runRemoteCommand('synk', 'auth', ...process.argv.slice(3)));
543
+ commander_1.program
544
+ .command('codex')
545
+ .description('Launch Codex (OpenAI) with mobile control')
546
+ .allowUnknownOption()
547
+ .action(() => runRemoteCommand('synk', 'codex', ...process.argv.slice(3)));
548
+ commander_1.program
549
+ .command('acp')
550
+ .description('Launch a generic ACP-compatible agent')
551
+ .allowUnknownOption()
552
+ .action(() => runRemoteCommand('synk', 'acp', ...process.argv.slice(3)));
553
+ commander_1.program
554
+ .command('daemon')
555
+ .description('Manage background mobile sync service (start, stop, status)')
556
+ .allowUnknownOption()
557
+ .action(() => runRemoteCommand('synk', 'daemon', ...process.argv.slice(3)));
558
+ commander_1.program
559
+ .command('connect')
560
+ .description('Securely store AI vendor API keys in the cloud')
561
+ .allowUnknownOption()
562
+ .action(() => runRemoteCommand('synk', 'connect', ...process.argv.slice(3)));
563
+ commander_1.program
564
+ .command('sandbox')
565
+ .description('Configure OS-level sandboxing for agent execution')
566
+ .allowUnknownOption()
567
+ .action(() => runRemoteCommand('synk', 'sandbox', ...process.argv.slice(3)));
568
+ commander_1.program
569
+ .command('notify')
570
+ .description('Send push notifications to your Synk mobile app')
571
+ .allowUnknownOption()
572
+ .action(() => runRemoteCommand('synk', 'notify', ...process.argv.slice(3)));
573
+ // Headless Agents (Remote Control)
574
+ commander_1.program
575
+ .command('agent')
576
+ .description('Manage remote headless agents (create, list, stop)')
577
+ .allowUnknownOption()
578
+ .action(() => runRemoteCommand('agent', ...process.argv.slice(3)));
504
579
  // Handle `-help` (single dash) — rewrite to `--help` for Commander compatibility
505
580
  const helpIdx = process.argv.indexOf('-help');
506
581
  if (helpIdx !== -1) {
@@ -485,7 +485,7 @@ function calculateContextPercentage(usage) {
485
485
  */
486
486
  async function fetchPatternMetrics(sessionId) {
487
487
  try {
488
- const response = await fetch(`http://localhost:3001/api/v1/session/stats?session_id=${sessionId}`, {
488
+ const response = await fetch(`${process.env.EKKOS_MCP_URL || 'https://mcp.ekkos.dev'}/api/v1/session/stats?session_id=${sessionId}`, {
489
489
  method: 'GET',
490
490
  headers: {
491
491
  'Content-Type': 'application/json',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ekkos/cli",
3
- "version": "1.3.5",
3
+ "version": "1.3.7",
4
4
  "description": "Setup ekkOS memory for AI coding assistants (Claude Code, Cursor, Windsurf)",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -8,13 +8,6 @@
8
8
  "cli": "dist/index.js",
9
9
  "ekkos-capture": "dist/cache/capture.js"
10
10
  },
11
- "scripts": {
12
- "build": "tsc",
13
- "dev": "tsx src/index.ts",
14
- "prepack": "node scripts/build-templates.js prepack",
15
- "postpack": "node scripts/build-templates.js postpack",
16
- "prepublishOnly": "npm run build"
17
- },
18
11
  "keywords": [
19
12
  "ekkos",
20
13
  "ai",
@@ -28,7 +21,6 @@
28
21
  "author": "ekkOS",
29
22
  "license": "MIT",
30
23
  "dependencies": {
31
- "@ekkos/prometheus": "workspace:*",
32
24
  "@supabase/supabase-js": "^2.39.8",
33
25
  "axios": "^1.7.0",
34
26
  "blessed": "^0.1.81",
@@ -56,7 +48,12 @@
56
48
  },
57
49
  "files": [
58
50
  "dist",
51
+ "!dist/cron",
59
52
  "templates/CLAUDE.md",
60
53
  "templates/skills"
61
- ]
62
- }
54
+ ],
55
+ "scripts": {
56
+ "build": "tsc",
57
+ "dev": "tsx src/index.ts"
58
+ }
59
+ }
@@ -1,7 +0,0 @@
1
- /**
2
- * ekkOS Cron Jobs
3
- * ================
4
- *
5
- * Background jobs for ekkOS maintenance and evolution.
6
- */
7
- export { evaluatePromotions, queryPatternStats, writePatchConfig, type PromoterConfig, type PromotionResult, type PromotedPatchConfig, type PromotedPattern, } from './promoter.js';
@@ -1,13 +0,0 @@
1
- "use strict";
2
- /**
3
- * ekkOS Cron Jobs
4
- * ================
5
- *
6
- * Background jobs for ekkOS maintenance and evolution.
7
- */
8
- Object.defineProperty(exports, "__esModule", { value: true });
9
- exports.writePatchConfig = exports.queryPatternStats = exports.evaluatePromotions = void 0;
10
- var promoter_js_1 = require("./promoter.js");
11
- Object.defineProperty(exports, "evaluatePromotions", { enumerable: true, get: function () { return promoter_js_1.evaluatePromotions; } });
12
- Object.defineProperty(exports, "queryPatternStats", { enumerable: true, get: function () { return promoter_js_1.queryPatternStats; } });
13
- Object.defineProperty(exports, "writePatchConfig", { enumerable: true, get: function () { return promoter_js_1.writePatchConfig; } });
@@ -1,70 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * ekkOS PROMETHEUS Pattern Promoter
4
- * ==================================
5
- *
6
- * Daily cron job that evaluates patterns for promotion to constitutional memory.
7
- *
8
- * Constitutional patterns are injected into the system prompt via @ekkos/patch,
9
- * making them "instincts" that don't require retrieval.
10
- *
11
- * Promotion Criteria (default):
12
- * - Success rate ≥ 85%
13
- * - Applications ≥ 5
14
- * - Skip rate ≤ 10%
15
- * - Confidence ≥ 70%
16
- * - Used within last 30 days
17
- * - Used in ≥ 2 unique sessions
18
- *
19
- * Usage:
20
- * npx ekkos-promote # Run promotion evaluation
21
- * npx ekkos-promote --dry-run # Preview without applying changes
22
- * npx ekkos-promote --user <uuid> # Promote for specific user
23
- *
24
- * Schedule via launchd (macOS) or cron:
25
- * 0 6 * * * /path/to/node /path/to/ekkos-promote
26
- */
27
- import { SupabaseClient } from '@supabase/supabase-js';
28
- import { type PatternStats } from '@ekkos/prometheus';
29
- interface PromoterConfig {
30
- supabaseUrl: string;
31
- supabaseKey: string;
32
- dryRun: boolean;
33
- userId?: string;
34
- patchConfigPath: string;
35
- verbose: boolean;
36
- }
37
- interface PromotionResult {
38
- evaluated: number;
39
- promoted: number;
40
- demoted: number;
41
- patterns: Array<{
42
- patternId: string;
43
- title: string;
44
- action: 'promoted' | 'demoted' | 'unchanged';
45
- score: number;
46
- reason?: string;
47
- }>;
48
- patchConfig?: PromotedPatchConfig;
49
- }
50
- interface PromotedPattern {
51
- id: string;
52
- title: string;
53
- problem: string;
54
- solution: string;
55
- promotedAt: string;
56
- successRate: number;
57
- appliedCount: number;
58
- tags?: string[];
59
- }
60
- interface PromotedPatchConfig {
61
- version: string;
62
- generatedAt: string;
63
- promotedPatterns: PromotedPattern[];
64
- totalPatterns: number;
65
- }
66
- declare function queryPatternStats(supabase: SupabaseClient, userId?: string): Promise<PatternStats[]>;
67
- declare function evaluatePromotions(supabase: SupabaseClient, config: PromoterConfig): Promise<PromotionResult>;
68
- declare function writePatchConfig(config: PromotedPatchConfig, outputPath: string): void;
69
- export { evaluatePromotions, queryPatternStats, writePatchConfig };
70
- export type { PromoterConfig, PromotionResult, PromotedPatchConfig, PromotedPattern };
@@ -1,403 +0,0 @@
1
- #!/usr/bin/env node
2
- "use strict";
3
- /**
4
- * ekkOS PROMETHEUS Pattern Promoter
5
- * ==================================
6
- *
7
- * Daily cron job that evaluates patterns for promotion to constitutional memory.
8
- *
9
- * Constitutional patterns are injected into the system prompt via @ekkos/patch,
10
- * making them "instincts" that don't require retrieval.
11
- *
12
- * Promotion Criteria (default):
13
- * - Success rate ≥ 85%
14
- * - Applications ≥ 5
15
- * - Skip rate ≤ 10%
16
- * - Confidence ≥ 70%
17
- * - Used within last 30 days
18
- * - Used in ≥ 2 unique sessions
19
- *
20
- * Usage:
21
- * npx ekkos-promote # Run promotion evaluation
22
- * npx ekkos-promote --dry-run # Preview without applying changes
23
- * npx ekkos-promote --user <uuid> # Promote for specific user
24
- *
25
- * Schedule via launchd (macOS) or cron:
26
- * 0 6 * * * /path/to/node /path/to/ekkos-promote
27
- */
28
- var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
29
- if (k2 === undefined) k2 = k;
30
- var desc = Object.getOwnPropertyDescriptor(m, k);
31
- if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
32
- desc = { enumerable: true, get: function() { return m[k]; } };
33
- }
34
- Object.defineProperty(o, k2, desc);
35
- }) : (function(o, m, k, k2) {
36
- if (k2 === undefined) k2 = k;
37
- o[k2] = m[k];
38
- }));
39
- var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
40
- Object.defineProperty(o, "default", { enumerable: true, value: v });
41
- }) : function(o, v) {
42
- o["default"] = v;
43
- });
44
- var __importStar = (this && this.__importStar) || (function () {
45
- var ownKeys = function(o) {
46
- ownKeys = Object.getOwnPropertyNames || function (o) {
47
- var ar = [];
48
- for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
49
- return ar;
50
- };
51
- return ownKeys(o);
52
- };
53
- return function (mod) {
54
- if (mod && mod.__esModule) return mod;
55
- var result = {};
56
- if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
57
- __setModuleDefault(result, mod);
58
- return result;
59
- };
60
- })();
61
- Object.defineProperty(exports, "__esModule", { value: true });
62
- exports.evaluatePromotions = evaluatePromotions;
63
- exports.queryPatternStats = queryPatternStats;
64
- exports.writePatchConfig = writePatchConfig;
65
- const supabase_js_1 = require("@supabase/supabase-js");
66
- const prometheus_1 = require("@ekkos/prometheus");
67
- const fs = __importStar(require("fs"));
68
- const path = __importStar(require("path"));
69
- const os = __importStar(require("os"));
70
- // ═══════════════════════════════════════════════════════════════════════════
71
- // Pattern Stats Query
72
- // ═══════════════════════════════════════════════════════════════════════════
73
- async function queryPatternStats(supabase, userId) {
74
- // Query patterns with their application stats
75
- let query = supabase
76
- .from('patterns')
77
- .select(`
78
- pattern_id,
79
- title,
80
- content,
81
- success_rate,
82
- applied_count,
83
- confidence_score,
84
- created_at,
85
- updated_at,
86
- tags,
87
- user_id
88
- `)
89
- .eq('quarantined', false)
90
- .gte('applied_count', 1); // Only consider applied patterns
91
- if (userId) {
92
- query = query.eq('user_id', userId);
93
- }
94
- const { data: patterns, error } = await query;
95
- if (error) {
96
- throw new Error(`Failed to query patterns: ${error.message}`);
97
- }
98
- if (!patterns || patterns.length === 0) {
99
- return [];
100
- }
101
- // Get application details for skip rate and session count
102
- const patternIds = patterns.map(p => p.pattern_id);
103
- const { data: applications } = await supabase
104
- .from('pattern_applications')
105
- .select('pattern_id, outcome_success, created_at, user_id')
106
- .in('pattern_id', patternIds);
107
- const { data: skips } = await supabase
108
- .from('pattern_skips')
109
- .select('pattern_id')
110
- .in('pattern_id', patternIds);
111
- // Aggregate stats per pattern
112
- const appsByPattern = new Map();
113
- const skipsByPattern = new Map();
114
- for (const app of applications || []) {
115
- if (!appsByPattern.has(app.pattern_id)) {
116
- appsByPattern.set(app.pattern_id, []);
117
- }
118
- appsByPattern.get(app.pattern_id).push(app);
119
- }
120
- for (const skip of skips || []) {
121
- const count = skipsByPattern.get(skip.pattern_id) || 0;
122
- skipsByPattern.set(skip.pattern_id, count + 1);
123
- }
124
- // Build PatternStats for each pattern
125
- return patterns.map(p => {
126
- const apps = appsByPattern.get(p.pattern_id) || [];
127
- const skipCount = skipsByPattern.get(p.pattern_id) || 0;
128
- const successCount = apps.filter(a => a.outcome_success === true).length;
129
- const failureCount = apps.filter(a => a.outcome_success === false).length;
130
- const totalApplications = successCount + failureCount;
131
- // Count unique sessions (using created_at date as proxy)
132
- const uniqueDates = new Set(apps.map(a => new Date(a.created_at).toISOString().split('T')[0]));
133
- const stats = {
134
- patternId: p.pattern_id,
135
- title: p.title || 'Untitled',
136
- successCount,
137
- failureCount,
138
- skipCount,
139
- totalApplications: totalApplications || p.applied_count || 0,
140
- uniqueSessions: uniqueDates.size,
141
- firstApplied: new Date(p.created_at),
142
- lastApplied: new Date(p.updated_at || p.created_at),
143
- currentConfidence: p.confidence_score || p.success_rate || 0.5,
144
- tags: p.tags,
145
- };
146
- return stats;
147
- });
148
- }
149
- // ═══════════════════════════════════════════════════════════════════════════
150
- // Promotion Logic
151
- // ═══════════════════════════════════════════════════════════════════════════
152
- async function evaluatePromotions(supabase, config) {
153
- const result = {
154
- evaluated: 0,
155
- promoted: 0,
156
- demoted: 0,
157
- patterns: [],
158
- };
159
- // Get pattern stats
160
- const patternStats = await queryPatternStats(supabase, config.userId);
161
- result.evaluated = patternStats.length;
162
- if (config.verbose) {
163
- console.log(`[PROMOTER] Evaluating ${patternStats.length} patterns...`);
164
- }
165
- // Get currently promoted patterns (from promoted_patterns table or metadata)
166
- const { data: currentlyPromoted } = await supabase
167
- .from('patterns')
168
- .select('pattern_id')
169
- .eq('evolution_stage', 'constitutional');
170
- const promotedSet = new Set((currentlyPromoted || []).map(p => p.pattern_id));
171
- const promotedPatterns = [];
172
- // Evaluate each pattern
173
- for (const stats of patternStats) {
174
- const evaluation = prometheus_1.promotionEvaluator.evaluate(stats);
175
- const wasPromoted = promotedSet.has(stats.patternId);
176
- if (evaluation.eligible && !wasPromoted) {
177
- // PROMOTE: Pattern meets criteria and wasn't promoted before
178
- result.promoted++;
179
- result.patterns.push({
180
- patternId: stats.patternId,
181
- title: stats.title,
182
- action: 'promoted',
183
- score: evaluation.score,
184
- });
185
- if (!config.dryRun) {
186
- // Update pattern to constitutional
187
- await supabase
188
- .from('patterns')
189
- .update({
190
- evolution_stage: 'constitutional',
191
- promoted_at: new Date().toISOString(),
192
- })
193
- .eq('pattern_id', stats.patternId);
194
- // Emit PATTERN_PROMOTED event
195
- prometheus_1.learningEvents.emitPatternPromoted({
196
- id: stats.patternId,
197
- title: stats.title,
198
- confidenceBefore: stats.currentConfidence,
199
- confidenceAfter: 1.0,
200
- tier: 'constitutional',
201
- successRate: stats.successCount / Math.max(stats.totalApplications, 1),
202
- appliedCount: stats.totalApplications,
203
- }, {
204
- daysInEpisodic: Math.floor((Date.now() - stats.firstApplied.getTime()) / (1000 * 60 * 60 * 24)),
205
- totalApplications: stats.totalApplications,
206
- finalSuccessRate: stats.successCount / Math.max(stats.totalApplications, 1),
207
- });
208
- }
209
- // Fetch full pattern for patch config
210
- const { data: fullPattern } = await supabase
211
- .from('patterns')
212
- .select('content, tags')
213
- .eq('pattern_id', stats.patternId)
214
- .single();
215
- if (fullPattern) {
216
- // Parse problem/solution from content
217
- const content = fullPattern.content || '';
218
- const problemMatch = content.match(/## Problem\n([\s\S]*?)(?=\n## Solution|$)/);
219
- const solutionMatch = content.match(/## Solution\n([\s\S]*?)$/);
220
- promotedPatterns.push({
221
- id: stats.patternId,
222
- title: stats.title,
223
- problem: problemMatch?.[1]?.trim() || 'N/A',
224
- solution: solutionMatch?.[1]?.trim() || content,
225
- promotedAt: new Date().toISOString(),
226
- successRate: stats.successCount / Math.max(stats.totalApplications, 1),
227
- appliedCount: stats.totalApplications,
228
- tags: fullPattern.tags,
229
- });
230
- }
231
- if (config.verbose) {
232
- console.log(` ✓ PROMOTE: "${stats.title}" (score: ${evaluation.score.toFixed(2)})`);
233
- }
234
- }
235
- else if (!evaluation.eligible && wasPromoted) {
236
- // DEMOTE: Pattern no longer meets criteria
237
- result.demoted++;
238
- // Find the main blocker for demotion reason
239
- const reason = evaluation.blockers[0]?.includes('success')
240
- ? 'low_success_rate'
241
- : evaluation.blockers[0]?.includes('skip')
242
- ? 'high_skip_rate'
243
- : 'outdated';
244
- result.patterns.push({
245
- patternId: stats.patternId,
246
- title: stats.title,
247
- action: 'demoted',
248
- score: evaluation.score,
249
- reason,
250
- });
251
- if (!config.dryRun) {
252
- // Update pattern back to episodic
253
- await supabase
254
- .from('patterns')
255
- .update({
256
- evolution_stage: 'episodic',
257
- demoted_at: new Date().toISOString(),
258
- })
259
- .eq('pattern_id', stats.patternId);
260
- // Emit PATTERN_DEMOTED event
261
- prometheus_1.learningEvents.emitPatternDemoted({
262
- id: stats.patternId,
263
- title: stats.title,
264
- confidenceBefore: 1.0,
265
- confidenceAfter: stats.currentConfidence,
266
- tier: 'episodic',
267
- successRate: stats.successCount / Math.max(stats.totalApplications, 1),
268
- appliedCount: stats.totalApplications,
269
- }, reason);
270
- }
271
- if (config.verbose) {
272
- console.log(` ✗ DEMOTE: "${stats.title}" (${reason})`);
273
- }
274
- }
275
- else {
276
- result.patterns.push({
277
- patternId: stats.patternId,
278
- title: stats.title,
279
- action: 'unchanged',
280
- score: evaluation.score,
281
- });
282
- }
283
- }
284
- // Also include patterns that are already promoted and still eligible
285
- for (const stats of patternStats) {
286
- const evaluation = prometheus_1.promotionEvaluator.evaluate(stats);
287
- if (evaluation.eligible && promotedSet.has(stats.patternId)) {
288
- // Fetch and include in patch config
289
- const { data: fullPattern } = await supabase
290
- .from('patterns')
291
- .select('content, tags')
292
- .eq('pattern_id', stats.patternId)
293
- .single();
294
- if (fullPattern) {
295
- const content = fullPattern.content || '';
296
- const problemMatch = content.match(/## Problem\n([\s\S]*?)(?=\n## Solution|$)/);
297
- const solutionMatch = content.match(/## Solution\n([\s\S]*?)$/);
298
- promotedPatterns.push({
299
- id: stats.patternId,
300
- title: stats.title,
301
- problem: problemMatch?.[1]?.trim() || 'N/A',
302
- solution: solutionMatch?.[1]?.trim() || content,
303
- promotedAt: new Date().toISOString(),
304
- successRate: stats.successCount / Math.max(stats.totalApplications, 1),
305
- appliedCount: stats.totalApplications,
306
- tags: fullPattern.tags,
307
- });
308
- }
309
- }
310
- }
311
- // Generate patch config
312
- result.patchConfig = {
313
- version: '1.0.0',
314
- generatedAt: new Date().toISOString(),
315
- promotedPatterns,
316
- totalPatterns: promotedPatterns.length,
317
- };
318
- return result;
319
- }
320
- // ═══════════════════════════════════════════════════════════════════════════
321
- // Patch Config Generation
322
- // ═══════════════════════════════════════════════════════════════════════════
323
- function writePatchConfig(config, outputPath) {
324
- const dir = path.dirname(outputPath);
325
- if (!fs.existsSync(dir)) {
326
- fs.mkdirSync(dir, { recursive: true });
327
- }
328
- fs.writeFileSync(outputPath, JSON.stringify(config, null, 2));
329
- console.log(`[PROMOTER] Wrote patch config to: ${outputPath}`);
330
- }
331
- // ═══════════════════════════════════════════════════════════════════════════
332
- // CLI Entry Point
333
- // ═══════════════════════════════════════════════════════════════════════════
334
- async function main() {
335
- const args = process.argv.slice(2);
336
- const config = {
337
- supabaseUrl: process.env.SUPABASE_URL || process.env.NEXT_PUBLIC_SUPABASE_URL || '',
338
- supabaseKey: process.env.SUPABASE_SECRET_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY || '',
339
- dryRun: args.includes('--dry-run'),
340
- userId: args.includes('--user') ? args[args.indexOf('--user') + 1] : undefined,
341
- patchConfigPath: path.join(os.homedir(), '.ekkos', 'promoted-patterns.json'),
342
- verbose: args.includes('--verbose') || args.includes('-v'),
343
- };
344
- // Allow custom output path
345
- if (args.includes('--output')) {
346
- config.patchConfigPath = args[args.indexOf('--output') + 1];
347
- }
348
- console.log('═══════════════════════════════════════════════════════════════');
349
- console.log(' ekkOS PROMETHEUS Pattern Promoter');
350
- console.log('═══════════════════════════════════════════════════════════════');
351
- console.log(` Mode: ${config.dryRun ? 'DRY RUN (no changes)' : 'LIVE'}`);
352
- console.log(` User: ${config.userId || 'All users'}`);
353
- console.log(` Output: ${config.patchConfigPath}`);
354
- console.log('═══════════════════════════════════════════════════════════════');
355
- console.log('');
356
- if (!config.supabaseUrl || !config.supabaseKey) {
357
- console.error('[ERROR] Missing SUPABASE_URL or SUPABASE_SECRET_KEY environment variables');
358
- process.exit(1);
359
- }
360
- const supabase = (0, supabase_js_1.createClient)(config.supabaseUrl, config.supabaseKey);
361
- try {
362
- const result = await evaluatePromotions(supabase, config);
363
- console.log('');
364
- console.log('═══════════════════════════════════════════════════════════════');
365
- console.log(' Results');
366
- console.log('═══════════════════════════════════════════════════════════════');
367
- console.log(` Patterns evaluated: ${result.evaluated}`);
368
- console.log(` Promoted: ${result.promoted}`);
369
- console.log(` Demoted: ${result.demoted}`);
370
- console.log(` Constitutional total: ${result.patchConfig?.totalPatterns || 0}`);
371
- console.log('═══════════════════════════════════════════════════════════════');
372
- // Write patch config
373
- if (!config.dryRun && result.patchConfig) {
374
- writePatchConfig(result.patchConfig, config.patchConfigPath);
375
- }
376
- else if (config.dryRun && result.patchConfig) {
377
- console.log('\n[DRY RUN] Would write patch config with:');
378
- console.log(` - ${result.patchConfig.totalPatterns} promoted patterns`);
379
- }
380
- // Summary of changes
381
- if (result.patterns.filter(p => p.action !== 'unchanged').length > 0) {
382
- console.log('\nChanges:');
383
- for (const p of result.patterns) {
384
- if (p.action === 'promoted') {
385
- console.log(` 🎓 PROMOTED: "${p.title}" (score: ${p.score.toFixed(2)})`);
386
- }
387
- else if (p.action === 'demoted') {
388
- console.log(` ⬇️ DEMOTED: "${p.title}" (${p.reason})`);
389
- }
390
- }
391
- }
392
- else {
393
- console.log('\nNo promotion changes needed.');
394
- }
395
- console.log('\n✓ Promotion evaluation complete');
396
- }
397
- catch (error) {
398
- console.error('[ERROR]', error);
399
- process.exit(1);
400
- }
401
- }
402
- // Run if executed directly
403
- main().catch(console.error);