@hbarefoot/engram 1.0.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/bin/engram.js ADDED
@@ -0,0 +1,421 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { startMCPServer } from '../src/server/mcp.js';
5
+ import { startRESTServer } from '../src/server/rest.js';
6
+ import { loadConfig, getDatabasePath, getModelsPath } from '../src/config/index.js';
7
+ import { initDatabase, createMemory, getMemory, deleteMemory, listMemories, getStats } from '../src/memory/store.js';
8
+ import { recallMemories, formatRecallResults } from '../src/memory/recall.js';
9
+ import { consolidate, getConflicts } from '../src/memory/consolidate.js';
10
+ import { validateContent } from '../src/extract/secrets.js';
11
+ import { extractMemory } from '../src/extract/rules.js';
12
+ import { exportToStatic } from '../src/export/static.js';
13
+ import * as logger from '../src/utils/logger.js';
14
+ import { readFileSync } from 'fs';
15
+ import { fileURLToPath } from 'url';
16
+ import { dirname, join } from 'path';
17
+ import fs from 'fs';
18
+
19
+ // Read version from package.json
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+ const packageJson = JSON.parse(
23
+ readFileSync(join(__dirname, '../package.json'), 'utf-8')
24
+ );
25
+
26
+ const program = new Command();
27
+
28
+ program
29
+ .name('engram')
30
+ .description('Persistent memory for AI agents - SQLite for agent state')
31
+ .version(packageJson.version);
32
+
33
+ // Start server command
34
+ program
35
+ .command('start')
36
+ .description('Start the Engram server (MCP + REST + Dashboard)')
37
+ .option('--mcp-only', 'Start only the MCP server (stdio mode)')
38
+ .option('--port <port>', 'Custom port for REST API', '3838')
39
+ .option('--config <path>', 'Path to config file')
40
+ .action(async (options) => {
41
+ if (options.mcpOnly) {
42
+ // Start MCP server only (stdio mode)
43
+ logger.info('Starting Engram MCP server (stdio mode)...');
44
+ await startMCPServer(options.config);
45
+ } else {
46
+ // Start REST API server
47
+ const config = loadConfig(options.config);
48
+ const port = parseInt(options.port);
49
+
50
+ logger.info('Starting Engram REST API server...');
51
+ logger.info(`Server will be available at http://localhost:${port}`);
52
+ logger.warn('Dashboard UI not yet implemented (Phase 5 pending)');
53
+
54
+ await startRESTServer(config, port);
55
+
56
+ // Keep process alive
57
+ process.on('SIGINT', () => {
58
+ logger.info('Received SIGINT, shutting down...');
59
+ process.exit(0);
60
+ });
61
+
62
+ process.on('SIGTERM', () => {
63
+ logger.info('Received SIGTERM, shutting down...');
64
+ process.exit(0);
65
+ });
66
+ }
67
+ });
68
+
69
+ // Remember command (CLI version)
70
+ program
71
+ .command('remember <content>')
72
+ .description('Store a memory from command line')
73
+ .option('-c, --category <type>', 'Memory category (preference|fact|pattern|decision|outcome)', 'fact')
74
+ .option('-e, --entity <name>', 'What this is about')
75
+ .option('--confidence <score>', 'Confidence score (0-1)', parseFloat, 0.8)
76
+ .option('-n, --namespace <name>', 'Project/scope namespace', 'default')
77
+ .option('--config <path>', 'Path to config file')
78
+ .action(async (content, options) => {
79
+ try {
80
+ const config = loadConfig(options.config);
81
+ const db = initDatabase(getDatabasePath(config));
82
+
83
+ // Validate content
84
+ const validation = validateContent(content, { autoRedact: config.security?.secretDetection !== false });
85
+
86
+ if (!validation.valid) {
87
+ console.error('❌ Cannot store memory:', validation.errors.join(', '));
88
+ process.exit(1);
89
+ }
90
+
91
+ // Extract if needed
92
+ let memoryData = {
93
+ content: validation.content,
94
+ category: options.category,
95
+ entity: options.entity,
96
+ confidence: options.confidence,
97
+ namespace: options.namespace,
98
+ source: 'cli'
99
+ };
100
+
101
+ if (!options.entity) {
102
+ const extracted = extractMemory(content, { source: 'cli', namespace: options.namespace });
103
+ memoryData.entity = extracted.entity;
104
+ }
105
+
106
+ // Generate embedding
107
+ try {
108
+ const { generateEmbedding } = await import('../src/embed/index.js');
109
+ const embedding = await generateEmbedding(validation.content, getModelsPath(config));
110
+ memoryData.embedding = embedding;
111
+ } catch (error) {
112
+ logger.warn('Failed to generate embedding', { error: error.message });
113
+ }
114
+
115
+ const memory = createMemory(db, memoryData);
116
+
117
+ console.log('✅ Memory stored successfully!');
118
+ console.log(`ID: ${memory.id}`);
119
+ console.log(`Category: ${memory.category}`);
120
+ console.log(`Entity: ${memory.entity || 'none'}`);
121
+ console.log(`Confidence: ${memory.confidence}`);
122
+ console.log(`Namespace: ${memory.namespace}`);
123
+
124
+ if (validation.warnings?.length > 0) {
125
+ console.log(`\n⚠️ Warnings: ${validation.warnings.join(', ')}`);
126
+ }
127
+
128
+ db.close();
129
+ } catch (error) {
130
+ console.error('❌ Error:', error.message);
131
+ process.exit(1);
132
+ }
133
+ });
134
+
135
+ // Recall command
136
+ program
137
+ .command('recall <query>')
138
+ .description('Recall memories matching a query')
139
+ .option('-l, --limit <n>', 'Max results (default 5)', parseInt, 5)
140
+ .option('-c, --category <type>', 'Filter by category')
141
+ .option('-n, --namespace <name>', 'Filter by namespace')
142
+ .option('--threshold <score>', 'Minimum relevance score (0-1)', parseFloat, 0.3)
143
+ .option('--config <path>', 'Path to config file')
144
+ .action(async (query, options) => {
145
+ try {
146
+ const config = loadConfig(options.config);
147
+ const db = initDatabase(getDatabasePath(config));
148
+ const modelsPath = getModelsPath(config);
149
+
150
+ const memories = await recallMemories(
151
+ db,
152
+ query,
153
+ {
154
+ limit: options.limit,
155
+ category: options.category,
156
+ namespace: options.namespace,
157
+ threshold: options.threshold
158
+ },
159
+ modelsPath
160
+ );
161
+
162
+ const formatted = formatRecallResults(memories);
163
+ console.log(formatted);
164
+
165
+ db.close();
166
+ } catch (error) {
167
+ console.error('❌ Error:', error.message);
168
+ process.exit(1);
169
+ }
170
+ });
171
+
172
+ // Forget command
173
+ program
174
+ .command('forget <id>')
175
+ .description('Delete a memory by ID')
176
+ .option('--config <path>', 'Path to config file')
177
+ .action((id, options) => {
178
+ try {
179
+ const config = loadConfig(options.config);
180
+ const db = initDatabase(getDatabasePath(config));
181
+
182
+ const memory = getMemory(db, id);
183
+
184
+ if (!memory) {
185
+ console.error(`❌ Memory not found: ${id}`);
186
+ db.close();
187
+ process.exit(1);
188
+ }
189
+
190
+ const deleted = deleteMemory(db, id);
191
+
192
+ if (deleted) {
193
+ console.log(`✅ Memory deleted: ${id}`);
194
+ console.log(`Content: ${memory.content}`);
195
+ } else {
196
+ console.error(`❌ Failed to delete memory: ${id}`);
197
+ }
198
+
199
+ db.close();
200
+ } catch (error) {
201
+ console.error('❌ Error:', error.message);
202
+ process.exit(1);
203
+ }
204
+ });
205
+
206
+ // List command
207
+ program
208
+ .command('list')
209
+ .description('List all memories (paginated)')
210
+ .option('-l, --limit <n>', 'Max results', parseInt, 50)
211
+ .option('--offset <n>', 'Offset for pagination', parseInt, 0)
212
+ .option('-c, --category <type>', 'Filter by category')
213
+ .option('-n, --namespace <name>', 'Filter by namespace')
214
+ .option('--config <path>', 'Path to config file')
215
+ .action((options) => {
216
+ try {
217
+ const config = loadConfig(options.config);
218
+ const db = initDatabase(getDatabasePath(config));
219
+
220
+ const memories = listMemories(db, {
221
+ limit: options.limit,
222
+ offset: options.offset,
223
+ category: options.category,
224
+ namespace: options.namespace
225
+ });
226
+
227
+ console.log(`\nFound ${memories.length} memories:\n`);
228
+
229
+ memories.forEach((memory, index) => {
230
+ console.log(`[${index + 1}] ${memory.id.substring(0, 8)}`);
231
+ console.log(` ${memory.content}`);
232
+ console.log(` Category: ${memory.category} | Entity: ${memory.entity || 'none'} | Confidence: ${memory.confidence}`);
233
+ console.log(` Namespace: ${memory.namespace} | Accessed: ${memory.access_count} times`);
234
+ console.log('');
235
+ });
236
+
237
+ db.close();
238
+ } catch (error) {
239
+ console.error('❌ Error:', error.message);
240
+ process.exit(1);
241
+ }
242
+ });
243
+
244
+ // Status command
245
+ program
246
+ .command('status')
247
+ .description('Show Engram status and statistics')
248
+ .option('--config <path>', 'Path to config file')
249
+ .action(async (options) => {
250
+ try {
251
+ const config = loadConfig(options.config);
252
+ const db = initDatabase(getDatabasePath(config));
253
+ const stats = getStats(db);
254
+
255
+ console.log('\n📊 Engram Status\n');
256
+
257
+ console.log('Memory Statistics:');
258
+ console.log(` Total memories: ${stats.total}`);
259
+ console.log(` With embeddings: ${stats.withEmbeddings}`);
260
+ console.log(` By category:`);
261
+ for (const [category, count] of Object.entries(stats.byCategory)) {
262
+ console.log(` - ${category}: ${count}`);
263
+ }
264
+ console.log(` By namespace:`);
265
+ for (const [namespace, count] of Object.entries(stats.byNamespace)) {
266
+ console.log(` - ${namespace}: ${count}`);
267
+ }
268
+
269
+ // Model info
270
+ try {
271
+ const { getModelInfo } = await import('../src/embed/index.js');
272
+ const modelInfo = getModelInfo(getModelsPath(config));
273
+
274
+ console.log('\n🤖 Embedding Model:');
275
+ console.log(` Name: ${modelInfo.name}`);
276
+ console.log(` Available: ${modelInfo.available ? '✅' : '❌'}`);
277
+ console.log(` Cached: ${modelInfo.cached ? '✅' : '❌'}`);
278
+ console.log(` Size: ${modelInfo.sizeMB} MB`);
279
+ console.log(` Path: ${modelInfo.path}`);
280
+ } catch (error) {
281
+ console.log('\n🤖 Embedding Model: ❌ Not available');
282
+ }
283
+
284
+ console.log('\n⚙️ Configuration:');
285
+ console.log(` Data directory: ${config.dataDir}`);
286
+ console.log(` Default namespace: ${config.defaults.namespace}`);
287
+ console.log(` Recall limit: ${config.defaults.recallLimit}`);
288
+ console.log(` Secret detection: ${config.security.secretDetection ? '✅' : '❌'}`);
289
+ console.log('');
290
+
291
+ db.close();
292
+ } catch (error) {
293
+ console.error('❌ Error:', error.message);
294
+ process.exit(1);
295
+ }
296
+ });
297
+
298
+ // Consolidate command
299
+ program
300
+ .command('consolidate')
301
+ .description('Run consolidation manually')
302
+ .option('--no-duplicates', 'Skip duplicate detection')
303
+ .option('--no-contradictions', 'Skip contradiction detection')
304
+ .option('--no-decay', 'Skip confidence decay')
305
+ .option('--cleanup-stale', 'Enable stale memory cleanup')
306
+ .option('--config <path>', 'Path to config file')
307
+ .action(async (options) => {
308
+ try {
309
+ const config = loadConfig(options.config);
310
+ const db = initDatabase(getDatabasePath(config));
311
+
312
+ console.log('🔄 Running consolidation...\n');
313
+
314
+ const results = await consolidate(db, {
315
+ detectDuplicates: options.duplicates !== false,
316
+ detectContradictions: options.contradictions !== false,
317
+ applyDecay: options.decay !== false,
318
+ cleanupStale: options.cleanupStale === true
319
+ });
320
+
321
+ console.log('✅ Consolidation complete!');
322
+ console.log(` Duplicates removed: ${results.duplicatesRemoved}`);
323
+ console.log(` Contradictions detected: ${results.contradictionsDetected}`);
324
+ console.log(` Memories decayed: ${results.memoriesDecayed}`);
325
+ console.log(` Stale memories cleaned: ${results.staleMemoriesCleaned}`);
326
+ console.log(` Duration: ${results.duration}ms`);
327
+ console.log('');
328
+
329
+ db.close();
330
+ } catch (error) {
331
+ console.error('❌ Error:', error.message);
332
+ process.exit(1);
333
+ }
334
+ });
335
+
336
+ // Conflicts command
337
+ program
338
+ .command('conflicts')
339
+ .description('Show detected contradictions')
340
+ .option('--config <path>', 'Path to config file')
341
+ .action((options) => {
342
+ try {
343
+ const config = loadConfig(options.config);
344
+ const db = initDatabase(getDatabasePath(config));
345
+
346
+ const conflicts = getConflicts(db);
347
+
348
+ if (conflicts.length === 0) {
349
+ console.log('✅ No conflicts detected');
350
+ } else {
351
+ console.log(`\n⚠️ Found ${conflicts.length} conflict(s):\n`);
352
+
353
+ conflicts.forEach((conflict, index) => {
354
+ console.log(`Conflict ${index + 1}: ${conflict.conflictId}`);
355
+ conflict.memories.forEach((memory, mIndex) => {
356
+ console.log(` [${String.fromCharCode(65 + mIndex)}] ${memory.id.substring(0, 8)}: ${memory.content}`);
357
+ });
358
+ console.log('');
359
+ });
360
+ }
361
+
362
+ db.close();
363
+ } catch (error) {
364
+ console.error('❌ Error:', error.message);
365
+ process.exit(1);
366
+ }
367
+ });
368
+
369
+ // Export context command
370
+ program
371
+ .command('export-context')
372
+ .description('Export memories to a static context file')
373
+ .requiredOption('-n, --namespace <name>', 'Namespace to export from')
374
+ .option('-o, --output <path>', 'Output file path (stdout if omitted)')
375
+ .option('-f, --format <type>', 'Format: markdown, claude, txt, json', 'markdown')
376
+ .option('-c, --categories <list>', 'Comma-separated list of categories')
377
+ .option('--min-confidence <score>', 'Minimum confidence (0-1)', parseFloat, 0.5)
378
+ .option('--min-access <count>', 'Minimum access count', parseInt, 0)
379
+ .option('--include-low-feedback', 'Include memories with negative feedback', false)
380
+ .option('--group-by <field>', 'Group by: category, entity, none', 'category')
381
+ .option('--header <text>', 'Custom header text')
382
+ .option('--footer <text>', 'Custom footer text')
383
+ .option('--config <path>', 'Path to config file')
384
+ .action((options) => {
385
+ try {
386
+ const config = loadConfig(options.config);
387
+ const db = initDatabase(getDatabasePath(config));
388
+
389
+ const categories = options.categories ? options.categories.split(',') : undefined;
390
+
391
+ const result = exportToStatic(db, {
392
+ namespace: options.namespace,
393
+ format: options.format,
394
+ categories,
395
+ minConfidence: options.minConfidence,
396
+ minAccess: options.minAccess,
397
+ includeLowFeedback: options.includeLowFeedback,
398
+ groupBy: options.groupBy,
399
+ header: options.header,
400
+ footer: options.footer
401
+ });
402
+
403
+ if (options.output) {
404
+ // Write to file
405
+ fs.writeFileSync(options.output, result.content, 'utf8');
406
+ console.log(`✅ Exported ${result.stats.totalExported} memories to ${options.output}`);
407
+ console.log(` Size: ${result.stats.sizeKB} KB`);
408
+ console.log(` By category:`, result.stats.byCategory);
409
+ } else {
410
+ // Output to stdout
411
+ console.log(result.content);
412
+ }
413
+
414
+ db.close();
415
+ } catch (error) {
416
+ console.error('❌ Error:', error.message);
417
+ process.exit(1);
418
+ }
419
+ });
420
+
421
+ program.parse(process.argv);
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.inset-0{top:0;right:0;bottom:0;left:0}.right-2{right:.5rem}.top-2{top:.5rem}.z-50{z-index:50}.mx-auto{margin-left:auto;margin-right:auto}.-mb-px{margin-bottom:-1px}.-ml-1{margin-left:-.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-5{margin-left:1.25rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.block{display:block}.flex{display:flex}.inline-flex{display:inline-flex}.grid{display:grid}.hidden{display:none}.h-10{height:2.5rem}.h-12{height:3rem}.h-16{height:4rem}.h-2{height:.5rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.max-h-\[90vh\]{max-height:90vh}.min-h-screen{min-height:100vh}.w-0{width:0px}.w-10{width:2.5rem}.w-12{width:3rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-full{width:100%}.min-w-0{min-width:0px}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-7xl{max-width:80rem}.flex-1{flex:1 1 0%}.flex-shrink-0{flex-shrink:0}@keyframes spin{to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite}.list-inside{list-style-position:inside}.list-decimal{list-style-type:decimal}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-wrap{flex-wrap:wrap}.items-start{align-items:flex-start}.items-center{align-items:center}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.75rem * var(--tw-space-x-reverse));margin-left:calc(.75rem * calc(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(1rem * var(--tw-space-x-reverse));margin-left:calc(1rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.75rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse: 0;border-top-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px * var(--tw-divide-y-reverse))}.divide-gray-200>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(229 231 235 / var(--tw-divide-opacity, 1))}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.break-all{word-break:break-all}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.border{border-width:1px}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-b-2{border-bottom-width:2px}.border-t{border-top-width:1px}.border-blue-200{--tw-border-opacity: 1;border-color:rgb(191 219 254 / var(--tw-border-opacity, 1))}.border-gray-200{--tw-border-opacity: 1;border-color:rgb(229 231 235 / var(--tw-border-opacity, 1))}.border-gray-300{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.border-green-200{--tw-border-opacity: 1;border-color:rgb(187 247 208 / var(--tw-border-opacity, 1))}.border-primary-500{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.border-primary-600{--tw-border-opacity: 1;border-color:rgb(2 132 199 / var(--tw-border-opacity, 1))}.border-red-200{--tw-border-opacity: 1;border-color:rgb(254 202 202 / var(--tw-border-opacity, 1))}.border-transparent{border-color:transparent}.border-yellow-200{--tw-border-opacity: 1;border-color:rgb(254 240 138 / var(--tw-border-opacity, 1))}.bg-blue-100{--tw-bg-opacity: 1;background-color:rgb(219 234 254 / var(--tw-bg-opacity, 1))}.bg-blue-50{--tw-bg-opacity: 1;background-color:rgb(239 246 255 / var(--tw-bg-opacity, 1))}.bg-gray-100{--tw-bg-opacity: 1;background-color:rgb(243 244 246 / var(--tw-bg-opacity, 1))}.bg-gray-200{--tw-bg-opacity: 1;background-color:rgb(229 231 235 / var(--tw-bg-opacity, 1))}.bg-gray-50{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.bg-gray-500{--tw-bg-opacity: 1;background-color:rgb(107 114 128 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-green-100{--tw-bg-opacity: 1;background-color:rgb(220 252 231 / var(--tw-bg-opacity, 1))}.bg-green-50{--tw-bg-opacity: 1;background-color:rgb(240 253 244 / var(--tw-bg-opacity, 1))}.bg-green-600{--tw-bg-opacity: 1;background-color:rgb(22 163 74 / var(--tw-bg-opacity, 1))}.bg-primary-100{--tw-bg-opacity: 1;background-color:rgb(224 242 254 / var(--tw-bg-opacity, 1))}.bg-primary-50{--tw-bg-opacity: 1;background-color:rgb(240 249 255 / var(--tw-bg-opacity, 1))}.bg-primary-600{--tw-bg-opacity: 1;background-color:rgb(2 132 199 / var(--tw-bg-opacity, 1))}.bg-purple-100{--tw-bg-opacity: 1;background-color:rgb(243 232 255 / var(--tw-bg-opacity, 1))}.bg-red-100{--tw-bg-opacity: 1;background-color:rgb(254 226 226 / var(--tw-bg-opacity, 1))}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-white{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1))}.bg-yellow-100{--tw-bg-opacity: 1;background-color:rgb(254 249 195 / var(--tw-bg-opacity, 1))}.bg-yellow-50{--tw-bg-opacity: 1;background-color:rgb(254 252 232 / var(--tw-bg-opacity, 1))}.bg-yellow-600{--tw-bg-opacity: 1;background-color:rgb(202 138 4 / var(--tw-bg-opacity, 1))}.bg-opacity-75{--tw-bg-opacity: .75}.p-2{padding:.5rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-5{padding:1.25rem}.p-6{padding:1.5rem}.p-8{padding:2rem}.px-1{padding-left:.25rem;padding-right:.25rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-2\.5{padding-left:.625rem;padding-right:.625rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-12{padding-top:3rem;padding-bottom:3rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.py-6{padding-top:1.5rem;padding-bottom:1.5rem}.py-8{padding-top:2rem;padding-bottom:2rem}.pt-1{padding-top:.25rem}.pt-4{padding-top:1rem}.pt-6{padding-top:1.5rem}.text-left{text-align:left}.text-center{text-align:center}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-2xl{font-size:1.5rem;line-height:2rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-6xl{font-size:3.75rem;line-height:1}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.capitalize{text-transform:capitalize}.text-blue-800{--tw-text-opacity: 1;color:rgb(30 64 175 / var(--tw-text-opacity, 1))}.text-blue-900{--tw-text-opacity: 1;color:rgb(30 58 138 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-gray-700{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.text-gray-800{--tw-text-opacity: 1;color:rgb(31 41 55 / var(--tw-text-opacity, 1))}.text-gray-900{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.text-green-500{--tw-text-opacity: 1;color:rgb(34 197 94 / var(--tw-text-opacity, 1))}.text-green-600{--tw-text-opacity: 1;color:rgb(22 163 74 / var(--tw-text-opacity, 1))}.text-green-800{--tw-text-opacity: 1;color:rgb(22 101 52 / var(--tw-text-opacity, 1))}.text-green-900{--tw-text-opacity: 1;color:rgb(20 83 45 / var(--tw-text-opacity, 1))}.text-primary-600{--tw-text-opacity: 1;color:rgb(2 132 199 / var(--tw-text-opacity, 1))}.text-primary-800{--tw-text-opacity: 1;color:rgb(7 89 133 / var(--tw-text-opacity, 1))}.text-purple-800{--tw-text-opacity: 1;color:rgb(107 33 168 / var(--tw-text-opacity, 1))}.text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-800{--tw-text-opacity: 1;color:rgb(153 27 27 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-600{--tw-text-opacity: 1;color:rgb(202 138 4 / var(--tw-text-opacity, 1))}.text-yellow-800{--tw-text-opacity: 1;color:rgb(133 77 14 / var(--tw-text-opacity, 1))}.text-yellow-900{--tw-text-opacity: 1;color:rgb(113 63 18 / var(--tw-text-opacity, 1))}.opacity-100{opacity:1}.opacity-25{opacity:.25}.opacity-50{opacity:.5}.opacity-75{opacity:.75}.shadow{--tw-shadow: 0 1px 3px 0 rgb(0 0 0 / .1), 0 1px 2px -1px rgb(0 0 0 / .1);--tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-shadow{transition-property:box-shadow;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}:root{font-family:Inter,system-ui,Avenir,Helvetica,Arial,sans-serif;line-height:1.5;font-weight:400;color-scheme:light dark;color:#ffffffde;background-color:#242424;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}body{margin:0;min-height:100vh}#root{min-height:100vh}@media(prefers-color-scheme:light){:root{color:#213547;background-color:#fff}}.hover\:border-gray-300:hover{--tw-border-opacity: 1;border-color:rgb(209 213 219 / var(--tw-border-opacity, 1))}.hover\:bg-gray-50:hover{--tw-bg-opacity: 1;background-color:rgb(249 250 251 / var(--tw-bg-opacity, 1))}.hover\:bg-primary-700:hover{--tw-bg-opacity: 1;background-color:rgb(3 105 161 / var(--tw-bg-opacity, 1))}.hover\:text-gray-500:hover{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.hover\:text-gray-700:hover{--tw-text-opacity: 1;color:rgb(55 65 81 / var(--tw-text-opacity, 1))}.hover\:text-gray-900:hover{--tw-text-opacity: 1;color:rgb(17 24 39 / var(--tw-text-opacity, 1))}.hover\:text-primary-700:hover{--tw-text-opacity: 1;color:rgb(3 105 161 / var(--tw-text-opacity, 1))}.hover\:text-red-900:hover{--tw-text-opacity: 1;color:rgb(127 29 29 / var(--tw-text-opacity, 1))}.hover\:underline:hover{text-decoration-line:underline}.hover\:shadow-md:hover{--tw-shadow: 0 4px 6px -1px rgb(0 0 0 / .1), 0 2px 4px -2px rgb(0 0 0 / .1);--tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.focus\:border-primary-500:focus{--tw-border-opacity: 1;border-color:rgb(14 165 233 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}.focus\:ring-2:focus{--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000)}.focus\:ring-primary-500:focus{--tw-ring-opacity: 1;--tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity, 1))}.focus\:ring-offset-2:focus{--tw-ring-offset-width: 2px}.disabled\:cursor-not-allowed:disabled{cursor:not-allowed}.disabled\:opacity-50:disabled{opacity:.5}@media(min-width:640px){.sm\:ml-6{margin-left:1.5rem}.sm\:flex{display:flex}.sm\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.sm\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}.sm\:space-x-8>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(2rem * var(--tw-space-x-reverse));margin-left:calc(2rem * calc(1 - var(--tw-space-x-reverse)))}.sm\:px-6{padding-left:1.5rem;padding-right:1.5rem}}@media(min-width:768px){.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(min-width:1024px){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.lg\:px-8{padding-left:2rem;padding-right:2rem}}@media(prefers-color-scheme:dark){.dark\:divide-gray-700>:not([hidden])~:not([hidden]){--tw-divide-opacity: 1;border-color:rgb(55 65 81 / var(--tw-divide-opacity, 1))}.dark\:border-blue-800{--tw-border-opacity: 1;border-color:rgb(30 64 175 / var(--tw-border-opacity, 1))}.dark\:border-gray-600{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.dark\:border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.dark\:border-green-800{--tw-border-opacity: 1;border-color:rgb(22 101 52 / var(--tw-border-opacity, 1))}.dark\:border-red-800{--tw-border-opacity: 1;border-color:rgb(153 27 27 / var(--tw-border-opacity, 1))}.dark\:border-yellow-800{--tw-border-opacity: 1;border-color:rgb(133 77 14 / var(--tw-border-opacity, 1))}.dark\:bg-blue-900{--tw-bg-opacity: 1;background-color:rgb(30 58 138 / var(--tw-bg-opacity, 1))}.dark\:bg-blue-900\/20{background-color:#1e3a8a33}.dark\:bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-700\/50{background-color:#37415180}.dark\:bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.dark\:bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.dark\:bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.dark\:bg-green-900{--tw-bg-opacity: 1;background-color:rgb(20 83 45 / var(--tw-bg-opacity, 1))}.dark\:bg-green-900\/20{background-color:#14532d33}.dark\:bg-primary-500{--tw-bg-opacity: 1;background-color:rgb(14 165 233 / var(--tw-bg-opacity, 1))}.dark\:bg-primary-800{--tw-bg-opacity: 1;background-color:rgb(7 89 133 / var(--tw-bg-opacity, 1))}.dark\:bg-primary-900{--tw-bg-opacity: 1;background-color:rgb(12 74 110 / var(--tw-bg-opacity, 1))}.dark\:bg-primary-900\/20{background-color:#0c4a6e33}.dark\:bg-purple-900{--tw-bg-opacity: 1;background-color:rgb(88 28 135 / var(--tw-bg-opacity, 1))}.dark\:bg-red-900{--tw-bg-opacity: 1;background-color:rgb(127 29 29 / var(--tw-bg-opacity, 1))}.dark\:bg-red-900\/20{background-color:#7f1d1d33}.dark\:bg-yellow-900{--tw-bg-opacity: 1;background-color:rgb(113 63 18 / var(--tw-bg-opacity, 1))}.dark\:bg-yellow-900\/20{background-color:#713f1233}.dark\:text-blue-200{--tw-text-opacity: 1;color:rgb(191 219 254 / var(--tw-text-opacity, 1))}.dark\:text-blue-300{--tw-text-opacity: 1;color:rgb(147 197 253 / var(--tw-text-opacity, 1))}.dark\:text-gray-200{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.dark\:text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.dark\:text-green-200{--tw-text-opacity: 1;color:rgb(187 247 208 / var(--tw-text-opacity, 1))}.dark\:text-green-300{--tw-text-opacity: 1;color:rgb(134 239 172 / var(--tw-text-opacity, 1))}.dark\:text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.dark\:text-primary-200{--tw-text-opacity: 1;color:rgb(186 230 253 / var(--tw-text-opacity, 1))}.dark\:text-primary-400{--tw-text-opacity: 1;color:rgb(56 189 248 / var(--tw-text-opacity, 1))}.dark\:text-purple-200{--tw-text-opacity: 1;color:rgb(233 213 255 / var(--tw-text-opacity, 1))}.dark\:text-red-200{--tw-text-opacity: 1;color:rgb(254 202 202 / var(--tw-text-opacity, 1))}.dark\:text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.dark\:text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.dark\:text-yellow-200{--tw-text-opacity: 1;color:rgb(254 240 138 / var(--tw-text-opacity, 1))}.dark\:text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.dark\:hover\:border-gray-600:hover{--tw-border-opacity: 1;border-color:rgb(75 85 99 / var(--tw-border-opacity, 1))}.dark\:hover\:bg-gray-600:hover{--tw-bg-opacity: 1;background-color:rgb(75 85 99 / var(--tw-bg-opacity, 1))}.dark\:hover\:bg-gray-700\/50:hover{background-color:#37415180}.dark\:hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.dark\:hover\:text-primary-300:hover{--tw-text-opacity: 1;color:rgb(125 211 252 / var(--tw-text-opacity, 1))}.dark\:hover\:text-red-300:hover{--tw-text-opacity: 1;color:rgb(252 165 165 / var(--tw-text-opacity, 1))}.dark\:hover\:text-white:hover{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}}