@ekkos/cli 0.2.18 → 0.3.3

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 (42) hide show
  1. package/LICENSE +21 -0
  2. package/dist/capture/eviction-client.d.ts +139 -0
  3. package/dist/capture/eviction-client.js +454 -0
  4. package/dist/capture/index.d.ts +2 -0
  5. package/dist/capture/index.js +2 -0
  6. package/dist/capture/jsonl-rewriter.d.ts +96 -0
  7. package/dist/capture/jsonl-rewriter.js +1369 -0
  8. package/dist/capture/transcript-repair.d.ts +50 -0
  9. package/dist/capture/transcript-repair.js +308 -0
  10. package/dist/commands/doctor.js +23 -1
  11. package/dist/commands/run.d.ts +2 -0
  12. package/dist/commands/run.js +1229 -293
  13. package/dist/commands/usage.d.ts +7 -0
  14. package/dist/commands/usage.js +214 -0
  15. package/dist/cron/index.d.ts +7 -0
  16. package/dist/cron/index.js +13 -0
  17. package/dist/cron/promoter.d.ts +70 -0
  18. package/dist/cron/promoter.js +403 -0
  19. package/dist/index.js +24 -3
  20. package/dist/lib/usage-monitor.d.ts +47 -0
  21. package/dist/lib/usage-monitor.js +124 -0
  22. package/dist/lib/usage-parser.d.ts +72 -0
  23. package/dist/lib/usage-parser.js +238 -0
  24. package/dist/restore/RestoreOrchestrator.d.ts +4 -0
  25. package/dist/restore/RestoreOrchestrator.js +118 -30
  26. package/package.json +12 -12
  27. package/templates/cursor-hooks/after-agent-response.sh +0 -0
  28. package/templates/cursor-hooks/before-submit-prompt.sh +0 -0
  29. package/templates/cursor-hooks/stop.sh +0 -0
  30. package/templates/ekkos-manifest.json +2 -2
  31. package/templates/hooks/assistant-response.sh +0 -0
  32. package/templates/hooks/session-start.sh +0 -0
  33. package/templates/plan-template.md +0 -0
  34. package/templates/spec-template.md +0 -0
  35. package/templates/agents/README.md +0 -182
  36. package/templates/agents/code-reviewer.md +0 -166
  37. package/templates/agents/debug-detective.md +0 -169
  38. package/templates/agents/ekkOS_Vercel.md +0 -99
  39. package/templates/agents/extension-manager.md +0 -229
  40. package/templates/agents/git-companion.md +0 -185
  41. package/templates/agents/github-test-agent.md +0 -321
  42. package/templates/agents/railway-manager.md +0 -215
@@ -0,0 +1,403 @@
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);
package/dist/index.js CHANGED
@@ -48,6 +48,7 @@ const hooks_1 = require("./commands/hooks");
48
48
  const setup_remote_1 = require("./commands/setup-remote");
49
49
  const agent_1 = require("./commands/agent");
50
50
  const state_1 = require("./utils/state");
51
+ const usage_1 = require("./commands/usage");
51
52
  const chalk_1 = __importDefault(require("chalk"));
52
53
  const fs = __importStar(require("fs"));
53
54
  const path = __importStar(require("path"));
@@ -88,15 +89,19 @@ commander_1.program
88
89
  .option('-v, --verbose', 'Show debug output')
89
90
  .option('-d, --doctor', 'Run diagnostics before starting')
90
91
  .option('-r, --research', 'Auto-run research agent on startup (scans arXiv for new AI papers)')
91
- .option('--no-inject', 'Monitor-only mode (detect context wall but print instructions instead of auto-inject)')
92
+ .option('--skip-inject', 'Monitor-only mode (detect context wall but print instructions instead of auto-inject)')
93
+ .option('--skip-dna', 'Skip ccDNA injection (bypass Claude Code patching)')
94
+ .option('--skip-proxy', 'Skip API proxy (use direct Anthropic API, disables seamless context eviction)')
92
95
  .action((options) => {
93
96
  (0, run_1.run)({
94
97
  session: options.session,
95
98
  bypass: options.bypass,
96
99
  verbose: options.verbose,
97
100
  doctor: options.doctor,
98
- noInject: options.noInject,
99
- research: options.research
101
+ noInject: options.skipInject,
102
+ research: options.research,
103
+ noDna: options.skipDna,
104
+ noProxy: options.skipProxy
100
105
  });
101
106
  });
102
107
  // Doctor command - check system prerequisites
@@ -169,6 +174,22 @@ hooksCmd
169
174
  .action((options) => {
170
175
  (0, hooks_1.hooksStatus)({ verbose: options.verbose });
171
176
  });
177
+ // Usage command - track session token usage
178
+ commander_1.program
179
+ .command('usage [session-id]')
180
+ .description('Track Claude Code session token usage and context percentage')
181
+ .option('--instance <id>', 'Instance ID (project path identifier)')
182
+ .option('--list', 'List available sessions')
183
+ .action((sessionId, options) => {
184
+ const args = [];
185
+ if (sessionId)
186
+ args.push(sessionId);
187
+ if (options.instance)
188
+ args.push('--instance', options.instance);
189
+ if (options.list)
190
+ args.push('--list');
191
+ (0, usage_1.usageCommand)(args);
192
+ });
172
193
  // Sessions command - list active Claude Code sessions (swarm support)
173
194
  commander_1.program
174
195
  .command('sessions')
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Simple usage monitoring for ekkOS embedding costs
3
+ * Tracks daily usage to prevent runaway costs
4
+ */
5
+ interface UsageRecord {
6
+ date: string;
7
+ embeddings: number;
8
+ cost: number;
9
+ model: string;
10
+ }
11
+ interface DailyUsage {
12
+ date: string;
13
+ totalEmbeddings: number;
14
+ totalCost: number;
15
+ byModel: Record<string, {
16
+ count: number;
17
+ cost: number;
18
+ }>;
19
+ }
20
+ /**
21
+ * Load usage records
22
+ */
23
+ export declare function loadUsage(): Promise<UsageRecord[]>;
24
+ /**
25
+ * Record an embedding generation
26
+ */
27
+ export declare function recordEmbedding(model?: string, tokens?: number): Promise<void>;
28
+ /**
29
+ * Get usage for a specific date
30
+ */
31
+ export declare function getUsageForDate(date: string): Promise<DailyUsage>;
32
+ /**
33
+ * Get usage for the last N days
34
+ */
35
+ export declare function getUsageForDays(days: number): Promise<DailyUsage[]>;
36
+ /**
37
+ * Check if usage exceeds threshold
38
+ */
39
+ export declare function checkThreshold(dailyLimit?: number, costLimit?: number): Promise<{
40
+ exceeded: boolean;
41
+ usage: DailyUsage;
42
+ }>;
43
+ /**
44
+ * Clear old usage data (keep last 90 days)
45
+ */
46
+ export declare function pruneOldUsage(): Promise<void>;
47
+ export {};
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ /**
3
+ * Simple usage monitoring for ekkOS embedding costs
4
+ * Tracks daily usage to prevent runaway costs
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.loadUsage = loadUsage;
11
+ exports.recordEmbedding = recordEmbedding;
12
+ exports.getUsageForDate = getUsageForDate;
13
+ exports.getUsageForDays = getUsageForDays;
14
+ exports.checkThreshold = checkThreshold;
15
+ exports.pruneOldUsage = pruneOldUsage;
16
+ const promises_1 = __importDefault(require("fs/promises"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const os_1 = require("os");
19
+ const USAGE_FILE = path_1.default.join((0, os_1.homedir)(), '.ekkos', 'usage.json');
20
+ const COST_PER_1M_TOKENS = 0.02; // OpenAI text-embedding-3-small
21
+ const AVG_TOKENS_PER_EMBEDDING = 100; // Conservative estimate
22
+ /**
23
+ * Ensure usage file exists
24
+ */
25
+ async function ensureUsageFile() {
26
+ const dir = path_1.default.dirname(USAGE_FILE);
27
+ await promises_1.default.mkdir(dir, { recursive: true });
28
+ try {
29
+ await promises_1.default.access(USAGE_FILE);
30
+ }
31
+ catch {
32
+ await promises_1.default.writeFile(USAGE_FILE, JSON.stringify([], null, 2));
33
+ }
34
+ }
35
+ /**
36
+ * Load usage records
37
+ */
38
+ async function loadUsage() {
39
+ await ensureUsageFile();
40
+ const data = await promises_1.default.readFile(USAGE_FILE, 'utf-8');
41
+ return JSON.parse(data);
42
+ }
43
+ /**
44
+ * Save usage records
45
+ */
46
+ async function saveUsage(records) {
47
+ await ensureUsageFile();
48
+ await promises_1.default.writeFile(USAGE_FILE, JSON.stringify(records, null, 2));
49
+ }
50
+ /**
51
+ * Record an embedding generation
52
+ */
53
+ async function recordEmbedding(model = 'text-embedding-3-small', tokens = AVG_TOKENS_PER_EMBEDDING) {
54
+ const cost = (tokens / 1000000) * COST_PER_1M_TOKENS;
55
+ const today = new Date().toISOString().split('T')[0];
56
+ const records = await loadUsage();
57
+ records.push({
58
+ date: today,
59
+ embeddings: 1,
60
+ cost,
61
+ model,
62
+ });
63
+ await saveUsage(records);
64
+ }
65
+ /**
66
+ * Get usage for a specific date
67
+ */
68
+ async function getUsageForDate(date) {
69
+ const records = await loadUsage();
70
+ const dayRecords = records.filter(r => r.date === date);
71
+ const byModel = {};
72
+ let totalEmbeddings = 0;
73
+ let totalCost = 0;
74
+ for (const record of dayRecords) {
75
+ totalEmbeddings += record.embeddings;
76
+ totalCost += record.cost;
77
+ if (!byModel[record.model]) {
78
+ byModel[record.model] = { count: 0, cost: 0 };
79
+ }
80
+ byModel[record.model].count += record.embeddings;
81
+ byModel[record.model].cost += record.cost;
82
+ }
83
+ return {
84
+ date,
85
+ totalEmbeddings,
86
+ totalCost,
87
+ byModel,
88
+ };
89
+ }
90
+ /**
91
+ * Get usage for the last N days
92
+ */
93
+ async function getUsageForDays(days) {
94
+ const results = [];
95
+ const today = new Date();
96
+ for (let i = 0; i < days; i++) {
97
+ const date = new Date(today);
98
+ date.setDate(date.getDate() - i);
99
+ const dateStr = date.toISOString().split('T')[0];
100
+ const usage = await getUsageForDate(dateStr);
101
+ results.push(usage);
102
+ }
103
+ return results;
104
+ }
105
+ /**
106
+ * Check if usage exceeds threshold
107
+ */
108
+ async function checkThreshold(dailyLimit = 10000, costLimit = 10.0) {
109
+ const today = new Date().toISOString().split('T')[0];
110
+ const usage = await getUsageForDate(today);
111
+ const exceeded = usage.totalEmbeddings > dailyLimit || usage.totalCost > costLimit;
112
+ return { exceeded, usage };
113
+ }
114
+ /**
115
+ * Clear old usage data (keep last 90 days)
116
+ */
117
+ async function pruneOldUsage() {
118
+ const records = await loadUsage();
119
+ const cutoff = new Date();
120
+ cutoff.setDate(cutoff.getDate() - 90);
121
+ const cutoffStr = cutoff.toISOString().split('T')[0];
122
+ const pruned = records.filter(r => r.date >= cutoffStr);
123
+ await saveUsage(pruned);
124
+ }