prepia 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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/prepia.mjs +119 -0
  4. package/package.json +53 -0
  5. package/skill/SKILL.md +148 -0
  6. package/skill/config.json +29 -0
  7. package/src/analytics/dashboard.mjs +84 -0
  8. package/src/analytics/tracker.mjs +131 -0
  9. package/src/api/middleware.mjs +219 -0
  10. package/src/api/routes.mjs +142 -0
  11. package/src/api/server.mjs +150 -0
  12. package/src/cache/disk-store.mjs +199 -0
  13. package/src/cache/manager.mjs +142 -0
  14. package/src/cache/memory-store.mjs +205 -0
  15. package/src/chain/dag.mjs +209 -0
  16. package/src/chain/executor.mjs +103 -0
  17. package/src/chain/scheduler.mjs +89 -0
  18. package/src/client/adapters.mjs +483 -0
  19. package/src/client/connector.mjs +391 -0
  20. package/src/client/index.mjs +483 -0
  21. package/src/client/websocket.mjs +353 -0
  22. package/src/core/context-packager.mjs +169 -0
  23. package/src/core/engine.mjs +338 -0
  24. package/src/core/event-bus.mjs +84 -0
  25. package/src/core/prepimshot.mjs +120 -0
  26. package/src/core/task-decomposer.mjs +158 -0
  27. package/src/edge/lite.mjs +90 -0
  28. package/src/guard/checker.mjs +123 -0
  29. package/src/guard/fact-checker.mjs +105 -0
  30. package/src/guard/hallucination.mjs +108 -0
  31. package/src/index.mjs +67 -0
  32. package/src/models/local-model.mjs +171 -0
  33. package/src/models/provider.mjs +192 -0
  34. package/src/models/router.mjs +156 -0
  35. package/src/morph/optimizer.mjs +142 -0
  36. package/src/network/p2p.mjs +146 -0
  37. package/src/persona/detector.mjs +118 -0
  38. package/src/plugins/loader.mjs +120 -0
  39. package/src/plugins/registry.mjs +164 -0
  40. package/src/plugins/sandbox.mjs +79 -0
  41. package/src/rate/limiter.mjs +145 -0
  42. package/src/rate/shield.mjs +150 -0
  43. package/src/script/executor.mjs +164 -0
  44. package/src/script/parser.mjs +134 -0
  45. package/src/security/privacy.mjs +108 -0
  46. package/src/security/sanitizer.mjs +133 -0
  47. package/src/shadow/daemon.mjs +128 -0
  48. package/src/stream/handler.mjs +204 -0
  49. package/src/tools/calculator.mjs +312 -0
  50. package/src/tools/file-ops.mjs +138 -0
  51. package/src/tools/http-client.mjs +127 -0
  52. package/src/tools/orchestrator.mjs +205 -0
  53. package/src/tools/web-scraper.mjs +159 -0
  54. package/src/tools/web-search.mjs +129 -0
  55. package/src/vault/knowledge-base.mjs +207 -0
  56. package/src/vault/pattern-learner.mjs +192 -0
  57. package/workflows/analyze.json +32 -0
  58. package/workflows/automate.json +32 -0
  59. package/workflows/research.json +37 -0
  60. package/workflows/summarize.json +32 -0
@@ -0,0 +1,150 @@
1
+ /**
2
+ * @fileoverview HTTP API server using node:http.
3
+ * @module api/server
4
+ */
5
+
6
+ import http from 'node:http';
7
+ import { parseBody, cors, RateLimitMiddleware, AuthMiddleware, errorHandler, requestLogger } from './middleware.mjs';
8
+ import { createRoutes } from './routes.mjs';
9
+
10
+ export class Server {
11
+ /**
12
+ * @param {Object} engine - Prepia engine instance
13
+ * @param {Object} [options]
14
+ * @param {number} [options.port=3000] - Server port
15
+ * @param {string} [options.host='0.0.0.0'] - Bind address
16
+ * @param {string[]} [options.apiKeys] - Valid API keys
17
+ * @param {boolean} [options.authRequired=false] - Require authentication
18
+ * @param {number} [options.rateLimit=100] - Requests per minute
19
+ */
20
+ constructor(engine, options = {}) {
21
+ this._engine = engine;
22
+ this._port = options.port ?? 3000;
23
+ this._host = options.host ?? '0.0.0.0';
24
+ this._server = null;
25
+ this._routes = createRoutes(engine);
26
+ this._rateLimiter = new RateLimitMiddleware({ maxRequests: options.rateLimit ?? 100 });
27
+ this._auth = new AuthMiddleware({
28
+ apiKeys: options.apiKeys,
29
+ required: options.authRequired ?? false,
30
+ });
31
+ }
32
+
33
+ /**
34
+ * Start the HTTP server.
35
+ * @returns {Promise<void>}
36
+ */
37
+ start() {
38
+ return new Promise((resolve) => {
39
+ this._server = http.createServer(async (req, res) => {
40
+ const logger = requestLogger(req);
41
+
42
+ try {
43
+ // CORS
44
+ cors(req, res);
45
+ if (req.method === 'OPTIONS') {
46
+ logger.end(res);
47
+ return;
48
+ }
49
+
50
+ // Auth
51
+ const authResult = this._auth.authenticate(req);
52
+ if (!authResult.authenticated) {
53
+ res.writeHead(401, { 'Content-Type': 'application/json' });
54
+ res.end(JSON.stringify({ error: { message: 'Unauthorized' } }));
55
+ logger.end(res);
56
+ return;
57
+ }
58
+
59
+ // Rate limiting
60
+ const rateResult = this._rateLimiter.check(authResult.clientId);
61
+ res.setHeader('X-RateLimit-Remaining', rateResult.remaining);
62
+ if (!rateResult.allowed) {
63
+ res.writeHead(429, {
64
+ 'Content-Type': 'application/json',
65
+ 'Retry-After': Math.ceil(rateResult.retryAfter / 1000),
66
+ });
67
+ res.end(JSON.stringify({ error: { message: 'Rate limit exceeded', retryAfter: rateResult.retryAfter } }));
68
+ logger.end(res);
69
+ return;
70
+ }
71
+
72
+ // Parse body
73
+ let body = null;
74
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
75
+ body = await parseBody(req);
76
+ }
77
+
78
+ // Route
79
+ await this._routes.route(req, res, body);
80
+ } catch (err) {
81
+ errorHandler(err, res);
82
+ }
83
+
84
+ logger.end(res);
85
+ });
86
+
87
+ this._server.listen(this._port, this._host, () => {
88
+ console.log(`Prepia API server running on http://${this._host}:${this._port}`);
89
+ resolve();
90
+ });
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Stop the HTTP server.
96
+ * @returns {Promise<void>}
97
+ */
98
+ stop() {
99
+ return new Promise((resolve, reject) => {
100
+ if (!this._server) {
101
+ resolve();
102
+ return;
103
+ }
104
+ this._server.close((err) => {
105
+ if (err) reject(err);
106
+ else resolve();
107
+ });
108
+ });
109
+ }
110
+
111
+ /**
112
+ * Get the server address.
113
+ * @returns {Object|null}
114
+ */
115
+ address() {
116
+ return this._server?.address() || null;
117
+ }
118
+
119
+ /**
120
+ * Get the port the server is listening on.
121
+ * @returns {number}
122
+ */
123
+ get port() {
124
+ return this._server?.address()?.port || this._port;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Create and start a Prepia API server.
130
+ * @param {Object} engine - Prepia engine
131
+ * @param {Object} [options]
132
+ * @returns {Promise<Server>}
133
+ */
134
+ export async function createServer(engine, options = {}) {
135
+ const server = new Server(engine, options);
136
+ await server.start();
137
+ return server;
138
+ }
139
+
140
+ // If run directly, start a standalone server
141
+ if (import.meta.url === `file://${process.argv[1]}`) {
142
+ const { PrepiaEngine } = await import('../core/engine.mjs');
143
+ const engine = new PrepiaEngine();
144
+ const server = await createServer(engine, {
145
+ port: parseInt(process.env.PREPIA_PORT || '3000'),
146
+ });
147
+ console.log(`Server running at http://localhost:${server.port}`);
148
+ }
149
+
150
+ export default { Server, createServer };
@@ -0,0 +1,199 @@
1
+ /**
2
+ * @fileoverview Persistent disk cache using JSON files.
3
+ * @module cache/disk-store
4
+ */
5
+
6
+ import { promises as fs } from 'node:fs';
7
+ import path from 'node:path';
8
+ import crypto from 'node:crypto';
9
+
10
+ export class DiskStore {
11
+ /**
12
+ * Create a new DiskStore.
13
+ * @param {Object} [options]
14
+ * @param {string} [options.cacheDir='.prepia/cache'] - Cache directory path
15
+ * @param {number} [options.defaultTTL=3600000] - Default TTL in ms (1 hour)
16
+ * @param {number} [options.maxSize=10000] - Maximum number of entries
17
+ */
18
+ constructor(options = {}) {
19
+ this._cacheDir = options.cacheDir ?? '.prepia/cache';
20
+ this._defaultTTL = options.defaultTTL ?? 60 * 60 * 1000;
21
+ this._maxSize = options.maxSize ?? 10000;
22
+ this._initialized = false;
23
+ }
24
+
25
+ /**
26
+ * Initialize the cache directory.
27
+ */
28
+ async init() {
29
+ if (this._initialized) return;
30
+ await fs.mkdir(this._cacheDir, { recursive: true });
31
+ this._initialized = true;
32
+ }
33
+
34
+ /**
35
+ * Generate a safe filename from a cache key.
36
+ * @param {string} key
37
+ * @returns {string}
38
+ * @private
39
+ */
40
+ _keyToFilename(key) {
41
+ const hash = crypto.createHash('sha256').update(key).digest('hex');
42
+ return path.join(this._cacheDir, `${hash}.json`);
43
+ }
44
+
45
+ /**
46
+ * Get a value from disk cache.
47
+ * @param {string} key
48
+ * @returns {Promise<*>} The cached value or undefined
49
+ */
50
+ async get(key) {
51
+ await this.init();
52
+ const filepath = this._keyToFilename(key);
53
+ try {
54
+ const data = await fs.readFile(filepath, 'utf-8');
55
+ const entry = JSON.parse(data);
56
+ if (Date.now() > entry.expiresAt) {
57
+ await fs.unlink(filepath).catch(() => {});
58
+ return undefined;
59
+ }
60
+ entry.hits = (entry.hits || 0) + 1;
61
+ await fs.writeFile(filepath, JSON.stringify(entry), 'utf-8').catch(() => {});
62
+ return entry.value;
63
+ } catch {
64
+ return undefined;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Set a value in disk cache.
70
+ * @param {string} key
71
+ * @param {*} value
72
+ * @param {number} [ttl] - TTL in ms
73
+ */
74
+ async set(key, value, ttl) {
75
+ await this.init();
76
+ const now = Date.now();
77
+ const entry = {
78
+ key,
79
+ value,
80
+ createdAt: now,
81
+ expiresAt: now + (ttl ?? this._defaultTTL),
82
+ hits: 0,
83
+ };
84
+ const filepath = this._keyToFilename(key);
85
+ await fs.writeFile(filepath, JSON.stringify(entry), 'utf-8');
86
+ }
87
+
88
+ /**
89
+ * Check if a key exists and is not expired.
90
+ * @param {string} key
91
+ * @returns {Promise<boolean>}
92
+ */
93
+ async has(key) {
94
+ await this.init();
95
+ const filepath = this._keyToFilename(key);
96
+ try {
97
+ const data = await fs.readFile(filepath, 'utf-8');
98
+ const entry = JSON.parse(data);
99
+ if (Date.now() > entry.expiresAt) {
100
+ await fs.unlink(filepath).catch(() => {});
101
+ return false;
102
+ }
103
+ return true;
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Delete a key from disk cache.
111
+ * @param {string} key
112
+ * @returns {Promise<boolean>}
113
+ */
114
+ async delete(key) {
115
+ await this.init();
116
+ const filepath = this._keyToFilename(key);
117
+ try {
118
+ await fs.unlink(filepath);
119
+ return true;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Clear all cache entries.
127
+ */
128
+ async clear() {
129
+ await this.init();
130
+ try {
131
+ const files = await fs.readdir(this._cacheDir);
132
+ await Promise.all(
133
+ files
134
+ .filter(f => f.endsWith('.json'))
135
+ .map(f => fs.unlink(path.join(this._cacheDir, f)).catch(() => {}))
136
+ );
137
+ } catch {
138
+ // Directory may not exist yet
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Remove expired entries.
144
+ * @returns {Promise<number>} Number of entries removed
145
+ */
146
+ async cleanup() {
147
+ await this.init();
148
+ let removed = 0;
149
+ try {
150
+ const files = await fs.readdir(this._cacheDir);
151
+ const now = Date.now();
152
+ for (const file of files) {
153
+ if (!file.endsWith('.json')) continue;
154
+ const filepath = path.join(this._cacheDir, file);
155
+ try {
156
+ const data = await fs.readFile(filepath, 'utf-8');
157
+ const entry = JSON.parse(data);
158
+ if (now > entry.expiresAt) {
159
+ await fs.unlink(filepath);
160
+ removed++;
161
+ }
162
+ } catch {
163
+ // Corrupted file, remove it
164
+ await fs.unlink(filepath).catch(() => {});
165
+ removed++;
166
+ }
167
+ }
168
+ } catch {
169
+ // Directory may not exist
170
+ }
171
+ return removed;
172
+ }
173
+
174
+ /**
175
+ * Get the count of cached entries.
176
+ * @returns {Promise<number>}
177
+ */
178
+ async size() {
179
+ await this.init();
180
+ try {
181
+ const files = await fs.readdir(this._cacheDir);
182
+ return files.filter(f => f.endsWith('.json')).length;
183
+ } catch {
184
+ return 0;
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Generate a cache key from parameters.
190
+ * @param {Object} params
191
+ * @returns {string}
192
+ */
193
+ static generateKey(params) {
194
+ const str = typeof params === 'string' ? params : JSON.stringify(params, Object.keys(params).sort());
195
+ return crypto.createHash('sha256').update(str).digest('hex');
196
+ }
197
+ }
198
+
199
+ export default DiskStore;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * @fileoverview Cache orchestration - coordinates memory and disk stores.
3
+ * @module cache/manager
4
+ */
5
+
6
+ import crypto from 'node:crypto';
7
+ import { MemoryStore } from './memory-store.mjs';
8
+ import { DiskStore } from './disk-store.mjs';
9
+
10
+ export class CacheManager {
11
+ /**
12
+ * Create a new CacheManager.
13
+ * @param {Object} [options]
14
+ * @param {number} [options.memoryMaxSize=1000] - Memory cache max entries
15
+ * @param {number} [options.memoryTTL=300000] - Memory TTL (5 min)
16
+ * @param {number} [options.diskTTL=3600000] - Disk TTL (1 hour)
17
+ * @param {string} [options.cacheDir='.prepia/cache'] - Disk cache directory
18
+ * @param {boolean} [options.enableDisk=true] - Enable disk persistence
19
+ */
20
+ constructor(options = {}) {
21
+ this.memory = new MemoryStore({
22
+ maxSize: options.memoryMaxSize ?? 1000,
23
+ defaultTTL: options.memoryTTL ?? 5 * 60 * 1000,
24
+ });
25
+ this.disk = new DiskStore({
26
+ cacheDir: options.cacheDir ?? '.prepia/cache',
27
+ defaultTTL: options.diskTTL ?? 60 * 60 * 1000,
28
+ });
29
+ this._enableDisk = options.enableDisk ?? true;
30
+ }
31
+
32
+ /**
33
+ * Generate a cache key from task parameters.
34
+ * @param {Object|string} params
35
+ * @returns {string}
36
+ */
37
+ generateKey(params) {
38
+ const str = typeof params === 'string' ? params : JSON.stringify(params, Object.keys(params).sort());
39
+ return crypto.createHash('sha256').update(str).digest('hex');
40
+ }
41
+
42
+ /**
43
+ * Get a value - checks memory first, then disk.
44
+ * Populates memory from disk on disk hit.
45
+ * @param {string} key
46
+ * @returns {Promise<*>}
47
+ */
48
+ async get(key) {
49
+ // Try memory first
50
+ const memResult = this.memory.get(key);
51
+ if (memResult !== undefined) return memResult;
52
+
53
+ // Try disk if enabled
54
+ if (this._enableDisk) {
55
+ const diskResult = await this.disk.get(key);
56
+ if (diskResult !== undefined) {
57
+ // Promote to memory
58
+ this.memory.set(key, diskResult);
59
+ return diskResult;
60
+ }
61
+ }
62
+
63
+ return undefined;
64
+ }
65
+
66
+ /**
67
+ * Set a value in both memory and disk cache.
68
+ * @param {string} key
69
+ * @param {*} value
70
+ * @param {Object} [options]
71
+ * @param {number} [options.memoryTTL] - Memory TTL override
72
+ * @param {number} [options.diskTTL] - Disk TTL override
73
+ * @param {boolean} [options.memoryOnly=false] - Only store in memory
74
+ */
75
+ async set(key, value, options = {}) {
76
+ this.memory.set(key, value, options.memoryTTL);
77
+ if (this._enableDisk && !options.memoryOnly) {
78
+ await this.disk.set(key, value, options.diskTTL);
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Check if key exists in either cache.
84
+ * @param {string} key
85
+ * @returns {Promise<boolean>}
86
+ */
87
+ async has(key) {
88
+ if (this.memory.has(key)) return true;
89
+ if (this._enableDisk) return this.disk.has(key);
90
+ return false;
91
+ }
92
+
93
+ /**
94
+ * Delete a key from both caches.
95
+ * @param {string} key
96
+ * @returns {Promise<boolean>}
97
+ */
98
+ async delete(key) {
99
+ const memDeleted = this.memory.delete(key);
100
+ let diskDeleted = false;
101
+ if (this._enableDisk) {
102
+ diskDeleted = await this.disk.delete(key);
103
+ }
104
+ return memDeleted || diskDeleted;
105
+ }
106
+
107
+ /**
108
+ * Clear all caches.
109
+ */
110
+ async clear() {
111
+ this.memory.clear();
112
+ if (this._enableDisk) {
113
+ await this.disk.clear();
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Cleanup expired entries from both caches.
119
+ * @returns {Promise<Object>} Cleanup stats
120
+ */
121
+ async cleanup() {
122
+ const memCleaned = this.memory.cleanup();
123
+ let diskCleaned = 0;
124
+ if (this._enableDisk) {
125
+ diskCleaned = await this.disk.cleanup();
126
+ }
127
+ return { memoryCleaned: memCleaned, diskCleaned };
128
+ }
129
+
130
+ /**
131
+ * Get combined stats.
132
+ * @returns {Object}
133
+ */
134
+ stats() {
135
+ return {
136
+ memory: this.memory.stats(),
137
+ diskEnabled: this._enableDisk,
138
+ };
139
+ }
140
+ }
141
+
142
+ export default CacheManager;
@@ -0,0 +1,205 @@
1
+ /**
2
+ * @fileoverview In-memory LRU cache with TTL support.
3
+ * @module cache/memory-store
4
+ */
5
+
6
+ /**
7
+ * @typedef {Object} CacheEntry
8
+ * @property {*} value - Cached value
9
+ * @property {number} expiresAt - Expiration timestamp (ms)
10
+ * @property {number} createdAt - Creation timestamp (ms)
11
+ * @property {number} size - Estimated size in bytes
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} CacheStats
16
+ * @property {number} hits - Cache hit count
17
+ * @property {number} misses - Cache miss count
18
+ * @property {number} evictions - Eviction count
19
+ * @property {number} sets - Set operation count
20
+ * @property {number} deletes - Delete operation count
21
+ * @property {number} size - Current entry count
22
+ * @property {number} maxSize - Maximum entries
23
+ */
24
+
25
+ export class MemoryStore {
26
+ /**
27
+ * Create a new MemoryStore.
28
+ * @param {Object} [options]
29
+ * @param {number} [options.maxSize=1000] - Maximum number of entries
30
+ * @param {number} [options.defaultTTL=300000] - Default TTL in ms (5 min)
31
+ */
32
+ constructor(options = {}) {
33
+ this._maxSize = options.maxSize ?? 1000;
34
+ this._defaultTTL = options.defaultTTL ?? 5 * 60 * 1000;
35
+ /** @type {Map<string, CacheEntry>} */
36
+ this._store = new Map();
37
+ this._stats = { hits: 0, misses: 0, evictions: 0, sets: 0, deletes: 0 };
38
+ }
39
+
40
+ /**
41
+ * Get a value from cache.
42
+ * @param {string} key
43
+ * @returns {*} The cached value or undefined
44
+ */
45
+ get(key) {
46
+ const entry = this._store.get(key);
47
+ if (!entry) {
48
+ this._stats.misses++;
49
+ return undefined;
50
+ }
51
+ if (Date.now() > entry.expiresAt) {
52
+ this._store.delete(key);
53
+ this._stats.misses++;
54
+ return undefined;
55
+ }
56
+ // Move to end (most recently used) by re-inserting
57
+ this._store.delete(key);
58
+ this._store.set(key, entry);
59
+ this._stats.hits++;
60
+ return entry.value;
61
+ }
62
+
63
+ /**
64
+ * Set a value in cache.
65
+ * @param {string} key
66
+ * @param {*} value
67
+ * @param {number} [ttl] - TTL in ms, defaults to store default
68
+ */
69
+ set(key, value, ttl) {
70
+ const now = Date.now();
71
+ const effectiveTTL = ttl ?? this._defaultTTL;
72
+ const entry = {
73
+ value,
74
+ expiresAt: now + effectiveTTL,
75
+ createdAt: now,
76
+ size: this._estimateSize(value),
77
+ };
78
+
79
+ // If key exists, remove first to update position
80
+ if (this._store.has(key)) {
81
+ this._store.delete(key);
82
+ }
83
+
84
+ // Evict oldest if at capacity
85
+ while (this._store.size >= this._maxSize) {
86
+ const oldestKey = this._store.keys().next().value;
87
+ this._store.delete(oldestKey);
88
+ this._stats.evictions++;
89
+ }
90
+
91
+ this._store.set(key, entry);
92
+ this._stats.sets++;
93
+ }
94
+
95
+ /**
96
+ * Check if a key exists and is not expired.
97
+ * @param {string} key
98
+ * @returns {boolean}
99
+ */
100
+ has(key) {
101
+ const entry = this._store.get(key);
102
+ if (!entry) return false;
103
+ if (Date.now() > entry.expiresAt) {
104
+ this._store.delete(key);
105
+ return false;
106
+ }
107
+ return true;
108
+ }
109
+
110
+ /**
111
+ * Delete a key from cache.
112
+ * @param {string} key
113
+ * @returns {boolean} Whether the key existed
114
+ */
115
+ delete(key) {
116
+ const existed = this._store.delete(key);
117
+ if (existed) this._stats.deletes++;
118
+ return existed;
119
+ }
120
+
121
+ /**
122
+ * Clear all entries.
123
+ */
124
+ clear() {
125
+ this._store.clear();
126
+ }
127
+
128
+ /**
129
+ * Get the number of entries.
130
+ * @returns {number}
131
+ */
132
+ get size() {
133
+ return this._store.size;
134
+ }
135
+
136
+ /**
137
+ * Get cache statistics.
138
+ * @returns {CacheStats}
139
+ */
140
+ stats() {
141
+ return {
142
+ ...this._stats,
143
+ size: this._store.size,
144
+ maxSize: this._maxSize,
145
+ };
146
+ }
147
+
148
+ /**
149
+ * Reset statistics.
150
+ */
151
+ resetStats() {
152
+ this._stats = { hits: 0, misses: 0, evictions: 0, sets: 0, deletes: 0 };
153
+ }
154
+
155
+ /**
156
+ * Remove all expired entries.
157
+ * @returns {number} Number of entries removed
158
+ */
159
+ cleanup() {
160
+ const now = Date.now();
161
+ let removed = 0;
162
+ for (const [key, entry] of this._store) {
163
+ if (now > entry.expiresAt) {
164
+ this._store.delete(key);
165
+ removed++;
166
+ }
167
+ }
168
+ return removed;
169
+ }
170
+
171
+ /**
172
+ * Get all keys (non-expired).
173
+ * @returns {string[]}
174
+ */
175
+ keys() {
176
+ const now = Date.now();
177
+ const result = [];
178
+ for (const [key, entry] of this._store) {
179
+ if (now <= entry.expiresAt) {
180
+ result.push(key);
181
+ }
182
+ }
183
+ return result;
184
+ }
185
+
186
+ /**
187
+ * Estimate size of a value in bytes.
188
+ * @param {*} value
189
+ * @returns {number}
190
+ * @private
191
+ */
192
+ _estimateSize(value) {
193
+ if (value === null || value === undefined) return 0;
194
+ if (typeof value === 'string') return value.length * 2;
195
+ if (typeof value === 'number') return 8;
196
+ if (typeof value === 'boolean') return 4;
197
+ try {
198
+ return JSON.stringify(value).length * 2;
199
+ } catch {
200
+ return 100;
201
+ }
202
+ }
203
+ }
204
+
205
+ export default MemoryStore;