@juspay/hippocampus 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +37 -67
- package/dist/client.js +241 -0
- package/dist/errors.js +19 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +6 -318
- package/dist/logger.js +61 -0
- package/dist/storage/redis.d.ts +14 -0
- package/dist/storage/redis.js +98 -0
- package/dist/storage/s3.d.ts +13 -0
- package/dist/storage/s3.js +108 -0
- package/dist/storage/sqlite.d.ts +11 -0
- package/dist/storage/sqlite.js +102 -0
- package/dist/types.d.ts +44 -159
- package/package.json +32 -17
package/dist/index.js
CHANGED
|
@@ -1,318 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
this.details = details;
|
|
8
|
-
this.name = 'HippocampusError';
|
|
9
|
-
}
|
|
10
|
-
static fromResponse(status, body) {
|
|
11
|
-
if (body && typeof body === 'object' && 'error' in body) {
|
|
12
|
-
const err = body.error;
|
|
13
|
-
return new HippocampusError(status, err.message || 'Unknown error', err.details);
|
|
14
|
-
}
|
|
15
|
-
return new HippocampusError(status, `HTTP ${status}`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const ALL_LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
20
|
-
function parseLogLevels(input) {
|
|
21
|
-
const trimmed = input.trim().toLowerCase();
|
|
22
|
-
if (trimmed === 'off' || trimmed === '') {
|
|
23
|
-
return new Set();
|
|
24
|
-
}
|
|
25
|
-
const levels = trimmed
|
|
26
|
-
.split(',')
|
|
27
|
-
.map((s) => s.trim())
|
|
28
|
-
.filter(Boolean);
|
|
29
|
-
return new Set(levels.filter((l) => ALL_LEVELS.includes(l)));
|
|
30
|
-
}
|
|
31
|
-
let enabledLevels = null;
|
|
32
|
-
function getEnabledLevels() {
|
|
33
|
-
if (enabledLevels === null) {
|
|
34
|
-
enabledLevels = parseLogLevels(process.env.HC_LOG_LEVEL || 'off');
|
|
35
|
-
}
|
|
36
|
-
return enabledLevels;
|
|
37
|
-
}
|
|
38
|
-
function shouldLog(level) {
|
|
39
|
-
return getEnabledLevels().has(level);
|
|
40
|
-
}
|
|
41
|
-
function formatMessage(level, message, context) {
|
|
42
|
-
const timestamp = new Date().toISOString();
|
|
43
|
-
const base = `[${timestamp}] [HIPPOCAMPUS@0.1.0] [${level.toUpperCase()}] ${message}`;
|
|
44
|
-
if (context && Object.keys(context).length > 0) {
|
|
45
|
-
return `${base} ${JSON.stringify(context)}`;
|
|
46
|
-
}
|
|
47
|
-
return base;
|
|
48
|
-
}
|
|
49
|
-
const logger = {
|
|
50
|
-
/**
|
|
51
|
-
* Programmatically override which log levels are active.
|
|
52
|
-
* Pass an array of levels or `'off'` to disable all logging.
|
|
53
|
-
*/
|
|
54
|
-
setLevels(levels) {
|
|
55
|
-
enabledLevels = levels === 'off' ? new Set() : new Set(levels);
|
|
56
|
-
},
|
|
57
|
-
debug(message, context) {
|
|
58
|
-
if (shouldLog('debug')) {
|
|
59
|
-
console.debug(formatMessage('debug', message, context));
|
|
60
|
-
}
|
|
61
|
-
},
|
|
62
|
-
info(message, context) {
|
|
63
|
-
if (shouldLog('info')) {
|
|
64
|
-
console.info(formatMessage('info', message, context));
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
warn(message, context) {
|
|
68
|
-
if (shouldLog('warn')) {
|
|
69
|
-
console.warn(formatMessage('warn', message, context));
|
|
70
|
-
}
|
|
71
|
-
},
|
|
72
|
-
error(message, context) {
|
|
73
|
-
if (shouldLog('error')) {
|
|
74
|
-
console.error(formatMessage('error', message, context));
|
|
75
|
-
}
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
class Hippocampus {
|
|
80
|
-
baseUrl;
|
|
81
|
-
headers;
|
|
82
|
-
retries;
|
|
83
|
-
retryDelay;
|
|
84
|
-
constructor(options = {}) {
|
|
85
|
-
const baseUrl = options.baseUrl || process.env.HC_BASE_URL || 'http://localhost:4477';
|
|
86
|
-
const apiKey = options.apiKey || process.env.HC_API_KEY;
|
|
87
|
-
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
88
|
-
this.headers = {
|
|
89
|
-
'Content-Type': 'application/json',
|
|
90
|
-
...(apiKey ? { 'X-API-Key': apiKey } : {}),
|
|
91
|
-
...options.headers,
|
|
92
|
-
};
|
|
93
|
-
this.retries = options.retries ?? 0;
|
|
94
|
-
this.retryDelay = options.retryDelay ?? 1000;
|
|
95
|
-
logger.debug('Hippocampus client initialized', {
|
|
96
|
-
baseUrl: this.baseUrl,
|
|
97
|
-
retries: this.retries,
|
|
98
|
-
retryDelay: this.retryDelay,
|
|
99
|
-
hasApiKey: !!apiKey,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
// --- Engrams ---
|
|
103
|
-
async addMemory(input) {
|
|
104
|
-
logger.info('Adding memory', { ownerId: input.ownerId, strand: input.strand });
|
|
105
|
-
const result = await this.post('/api/engrams', input);
|
|
106
|
-
logger.info('Memory added', { count: result.engrams.length });
|
|
107
|
-
return result;
|
|
108
|
-
}
|
|
109
|
-
async listEngrams(ownerId, options) {
|
|
110
|
-
logger.debug('Listing engrams', { ownerId, ...options });
|
|
111
|
-
const params = new URLSearchParams({ ownerId });
|
|
112
|
-
if (options?.limit) {
|
|
113
|
-
params.set('limit', String(options.limit));
|
|
114
|
-
}
|
|
115
|
-
if (options?.offset) {
|
|
116
|
-
params.set('offset', String(options.offset));
|
|
117
|
-
}
|
|
118
|
-
if (options?.strand) {
|
|
119
|
-
params.set('strand', options.strand);
|
|
120
|
-
}
|
|
121
|
-
const result = await this.get(`/api/engrams?${params}`);
|
|
122
|
-
logger.debug('Engrams listed', { total: result.total });
|
|
123
|
-
return result;
|
|
124
|
-
}
|
|
125
|
-
async getEngram(id) {
|
|
126
|
-
logger.debug('Getting engram', { id });
|
|
127
|
-
return this.get(`/api/engrams/${id}`);
|
|
128
|
-
}
|
|
129
|
-
async updateEngram(id, input) {
|
|
130
|
-
logger.info('Updating engram', { id });
|
|
131
|
-
const result = await this.patch(`/api/engrams/${id}`, input);
|
|
132
|
-
logger.info('Engram updated', { id });
|
|
133
|
-
return result;
|
|
134
|
-
}
|
|
135
|
-
async deleteEngram(id) {
|
|
136
|
-
logger.info('Deleting engram', { id });
|
|
137
|
-
await this.del(`/api/engrams/${id}`);
|
|
138
|
-
logger.info('Engram deleted', { id });
|
|
139
|
-
}
|
|
140
|
-
async search(query) {
|
|
141
|
-
logger.info('Searching engrams', { ownerId: query.ownerId, query: query.query, limit: query.limit });
|
|
142
|
-
const result = await this.post('/api/engrams/search', query);
|
|
143
|
-
logger.info('Search completed', { hits: result.hits.length, total: result.total, took: result.took });
|
|
144
|
-
return result;
|
|
145
|
-
}
|
|
146
|
-
async reinforceEngram(id, boost) {
|
|
147
|
-
logger.debug('Reinforcing engram', { id, boost });
|
|
148
|
-
const result = await this.post(`/api/engrams/${id}/reinforce`, { boost });
|
|
149
|
-
logger.debug('Engram reinforced', { id, signal: result.engram.signal });
|
|
150
|
-
return result;
|
|
151
|
-
}
|
|
152
|
-
// --- Chronicles ---
|
|
153
|
-
async recordChronicle(input) {
|
|
154
|
-
logger.info('Recording chronicle', { ownerId: input.ownerId, entity: input.entity, attribute: input.attribute });
|
|
155
|
-
const result = await this.post('/api/chronicles', input);
|
|
156
|
-
logger.info('Chronicle recorded', { id: result.chronicle.id });
|
|
157
|
-
return result;
|
|
158
|
-
}
|
|
159
|
-
async queryChronicles(query) {
|
|
160
|
-
logger.debug('Querying chronicles', { ownerId: query.ownerId, entity: query.entity, attribute: query.attribute });
|
|
161
|
-
const params = new URLSearchParams({ ownerId: query.ownerId });
|
|
162
|
-
if (query.entity) {
|
|
163
|
-
params.set('entity', query.entity);
|
|
164
|
-
}
|
|
165
|
-
if (query.attribute) {
|
|
166
|
-
params.set('attribute', query.attribute);
|
|
167
|
-
}
|
|
168
|
-
if (query.at) {
|
|
169
|
-
params.set('at', query.at);
|
|
170
|
-
}
|
|
171
|
-
if (query.from) {
|
|
172
|
-
params.set('from', query.from);
|
|
173
|
-
}
|
|
174
|
-
if (query.to) {
|
|
175
|
-
params.set('to', query.to);
|
|
176
|
-
}
|
|
177
|
-
if (query.limit) {
|
|
178
|
-
params.set('limit', String(query.limit));
|
|
179
|
-
}
|
|
180
|
-
if (query.offset) {
|
|
181
|
-
params.set('offset', String(query.offset));
|
|
182
|
-
}
|
|
183
|
-
const result = await this.get(`/api/chronicles?${params}`);
|
|
184
|
-
logger.debug('Chronicles queried', { total: result.total });
|
|
185
|
-
return result;
|
|
186
|
-
}
|
|
187
|
-
async getCurrentFact(ownerId, entity, attribute) {
|
|
188
|
-
logger.debug('Getting current fact', { ownerId, entity, attribute });
|
|
189
|
-
const params = new URLSearchParams({ ownerId, entity, attribute });
|
|
190
|
-
return this.get(`/api/chronicles/current?${params}`);
|
|
191
|
-
}
|
|
192
|
-
async getTimeline(ownerId, entity) {
|
|
193
|
-
logger.debug('Getting timeline', { ownerId, entity });
|
|
194
|
-
const params = new URLSearchParams({ ownerId, entity });
|
|
195
|
-
const result = await this.get(`/api/chronicles/timeline?${params}`);
|
|
196
|
-
logger.debug('Timeline retrieved', { count: result.chronicles.length });
|
|
197
|
-
return result;
|
|
198
|
-
}
|
|
199
|
-
async getChronicle(id) {
|
|
200
|
-
logger.debug('Getting chronicle', { id });
|
|
201
|
-
return this.get(`/api/chronicles/${id}`);
|
|
202
|
-
}
|
|
203
|
-
async updateChronicle(id, input) {
|
|
204
|
-
logger.info('Updating chronicle', { id });
|
|
205
|
-
const result = await this.patch(`/api/chronicles/${id}`, input);
|
|
206
|
-
logger.info('Chronicle updated', { id });
|
|
207
|
-
return result;
|
|
208
|
-
}
|
|
209
|
-
async expireChronicle(id) {
|
|
210
|
-
logger.info('Expiring chronicle', { id });
|
|
211
|
-
await this.del(`/api/chronicles/${id}`);
|
|
212
|
-
logger.info('Chronicle expired', { id });
|
|
213
|
-
}
|
|
214
|
-
// --- Nexuses ---
|
|
215
|
-
async createNexus(input) {
|
|
216
|
-
logger.info('Creating nexus', { originId: input.originId, linkedId: input.linkedId, bondType: input.bondType });
|
|
217
|
-
const result = await this.post('/api/nexuses', input);
|
|
218
|
-
logger.info('Nexus created', { id: result.nexus.id });
|
|
219
|
-
return result;
|
|
220
|
-
}
|
|
221
|
-
async getRelatedChronicles(chronicleId) {
|
|
222
|
-
logger.debug('Getting related chronicles', { chronicleId });
|
|
223
|
-
const result = await this.get(`/api/chronicles/${chronicleId}/related`);
|
|
224
|
-
logger.debug('Related chronicles retrieved', { count: result.related.length });
|
|
225
|
-
return result;
|
|
226
|
-
}
|
|
227
|
-
// --- System ---
|
|
228
|
-
async health() {
|
|
229
|
-
logger.debug('Health check');
|
|
230
|
-
return this.get('/api/health');
|
|
231
|
-
}
|
|
232
|
-
async status() {
|
|
233
|
-
logger.debug('Status check');
|
|
234
|
-
return this.get('/api/status');
|
|
235
|
-
}
|
|
236
|
-
async runDecay(ownerId) {
|
|
237
|
-
logger.info('Running decay', { ownerId });
|
|
238
|
-
const result = await this.post('/api/decay/run', { ownerId });
|
|
239
|
-
logger.info('Decay completed', { affected: result.affected });
|
|
240
|
-
return result;
|
|
241
|
-
}
|
|
242
|
-
// --- HTTP helpers ---
|
|
243
|
-
async request(method, path, body) {
|
|
244
|
-
const url = `${this.baseUrl}${path}`;
|
|
245
|
-
let lastError = null;
|
|
246
|
-
for (let attempt = 0; attempt <= this.retries; attempt++) {
|
|
247
|
-
try {
|
|
248
|
-
if (attempt > 0) {
|
|
249
|
-
logger.warn('Retrying request', { method, path, attempt, maxRetries: this.retries });
|
|
250
|
-
}
|
|
251
|
-
logger.debug('Sending request', { method, url });
|
|
252
|
-
const response = await fetch(url, {
|
|
253
|
-
method,
|
|
254
|
-
headers: this.headers,
|
|
255
|
-
body: body ? JSON.stringify(body) : undefined,
|
|
256
|
-
});
|
|
257
|
-
if (response.status === 204) {
|
|
258
|
-
logger.debug('Request successful (204 No Content)', { method, path });
|
|
259
|
-
return undefined;
|
|
260
|
-
}
|
|
261
|
-
const json = await response.json();
|
|
262
|
-
if (!response.ok) {
|
|
263
|
-
const error = HippocampusError.fromResponse(response.status, json);
|
|
264
|
-
logger.error('Request failed with HTTP error', {
|
|
265
|
-
method,
|
|
266
|
-
path,
|
|
267
|
-
statusCode: response.status,
|
|
268
|
-
message: error.message,
|
|
269
|
-
});
|
|
270
|
-
throw error;
|
|
271
|
-
}
|
|
272
|
-
logger.debug('Request successful', { method, path, status: response.status });
|
|
273
|
-
return json;
|
|
274
|
-
}
|
|
275
|
-
catch (error) {
|
|
276
|
-
lastError = error instanceof Error ? error : new Error(String(error));
|
|
277
|
-
// Don't retry client errors (4xx)
|
|
278
|
-
if (error instanceof HippocampusError && error.statusCode >= 400 && error.statusCode < 500) {
|
|
279
|
-
throw error;
|
|
280
|
-
}
|
|
281
|
-
if (attempt < this.retries) {
|
|
282
|
-
const delay = this.retryDelay * Math.pow(2, attempt);
|
|
283
|
-
logger.warn('Request failed, will retry', {
|
|
284
|
-
method,
|
|
285
|
-
path,
|
|
286
|
-
attempt,
|
|
287
|
-
nextRetryIn: `${delay}ms`,
|
|
288
|
-
error: lastError.message,
|
|
289
|
-
});
|
|
290
|
-
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
291
|
-
}
|
|
292
|
-
else {
|
|
293
|
-
logger.error('Request failed after all retries', {
|
|
294
|
-
method,
|
|
295
|
-
path,
|
|
296
|
-
attempts: attempt + 1,
|
|
297
|
-
error: lastError.message,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
throw lastError || new Error('Request failed');
|
|
303
|
-
}
|
|
304
|
-
get(path) {
|
|
305
|
-
return this.request('GET', path);
|
|
306
|
-
}
|
|
307
|
-
post(path, body) {
|
|
308
|
-
return this.request('POST', path, body);
|
|
309
|
-
}
|
|
310
|
-
patch(path, body) {
|
|
311
|
-
return this.request('PATCH', path, body);
|
|
312
|
-
}
|
|
313
|
-
del(path) {
|
|
314
|
-
return this.request('DELETE', path);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
export { Hippocampus, HippocampusError, logger };
|
|
1
|
+
export { Hippocampus } from './client.js';
|
|
2
|
+
export { HippocampusError } from './errors.js';
|
|
3
|
+
export { logger } from './logger.js';
|
|
4
|
+
export { SqliteStorage } from './storage/sqlite.js';
|
|
5
|
+
export { RedisStorage } from './storage/redis.js';
|
|
6
|
+
export { S3Storage } from './storage/s3.js';
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const ALL_LEVELS = ['debug', 'info', 'warn', 'error'];
|
|
2
|
+
function parseLogLevels(input) {
|
|
3
|
+
const trimmed = input.trim().toLowerCase();
|
|
4
|
+
if (trimmed === 'off' || trimmed === '') {
|
|
5
|
+
return new Set();
|
|
6
|
+
}
|
|
7
|
+
const levels = trimmed
|
|
8
|
+
.split(',')
|
|
9
|
+
.map((s) => s.trim())
|
|
10
|
+
.filter(Boolean);
|
|
11
|
+
return new Set(levels.filter((l) => ALL_LEVELS.includes(l)));
|
|
12
|
+
}
|
|
13
|
+
let enabledLevels = null;
|
|
14
|
+
function getEnabledLevels() {
|
|
15
|
+
if (enabledLevels === null) {
|
|
16
|
+
enabledLevels = parseLogLevels(process.env.HC_LOG_LEVEL || 'off');
|
|
17
|
+
}
|
|
18
|
+
return enabledLevels;
|
|
19
|
+
}
|
|
20
|
+
function shouldLog(level) {
|
|
21
|
+
return getEnabledLevels().has(level);
|
|
22
|
+
}
|
|
23
|
+
function formatMessage(level, message, context) {
|
|
24
|
+
const timestamp = new Date().toISOString();
|
|
25
|
+
const base = `[${timestamp}] [HIPPOCAMPUS@0.1.1] [${level.toUpperCase()}] ${message}`;
|
|
26
|
+
if (context && Object.keys(context).length > 0) {
|
|
27
|
+
return `${base} ${JSON.stringify(context)}`;
|
|
28
|
+
}
|
|
29
|
+
return base;
|
|
30
|
+
}
|
|
31
|
+
const logger = {
|
|
32
|
+
/**
|
|
33
|
+
* Programmatically override which log levels are active.
|
|
34
|
+
* Pass an array of levels or `'off'` to disable all logging.
|
|
35
|
+
*/
|
|
36
|
+
setLevels(levels) {
|
|
37
|
+
enabledLevels = levels === 'off' ? new Set() : new Set(levels);
|
|
38
|
+
},
|
|
39
|
+
debug(message, context) {
|
|
40
|
+
if (shouldLog('debug')) {
|
|
41
|
+
console.debug(formatMessage('debug', message, context));
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
info(message, context) {
|
|
45
|
+
if (shouldLog('info')) {
|
|
46
|
+
console.info(formatMessage('info', message, context));
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
warn(message, context) {
|
|
50
|
+
if (shouldLog('warn')) {
|
|
51
|
+
console.warn(formatMessage('warn', message, context));
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
error(message, context) {
|
|
55
|
+
if (shouldLog('error')) {
|
|
56
|
+
console.error(formatMessage('error', message, context));
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export { logger };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { StorageBackend, RedisStorageConfig } from '../types';
|
|
2
|
+
export declare class RedisStorage implements StorageBackend {
|
|
3
|
+
private client;
|
|
4
|
+
private keyPrefix;
|
|
5
|
+
private ttl;
|
|
6
|
+
private config;
|
|
7
|
+
constructor(config: RedisStorageConfig);
|
|
8
|
+
private ensureClient;
|
|
9
|
+
private key;
|
|
10
|
+
get(ownerId: string): Promise<string | null>;
|
|
11
|
+
set(ownerId: string, memory: string): Promise<void>;
|
|
12
|
+
delete(ownerId: string): Promise<void>;
|
|
13
|
+
close(): Promise<void>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { logger } from '../logger.js';
|
|
2
|
+
|
|
3
|
+
class RedisStorage {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
client = null;
|
|
6
|
+
keyPrefix;
|
|
7
|
+
ttl;
|
|
8
|
+
config;
|
|
9
|
+
constructor(config) {
|
|
10
|
+
this.config = config;
|
|
11
|
+
this.keyPrefix = config.keyPrefix || 'hippocampus:memory:';
|
|
12
|
+
this.ttl = config.ttl || 0;
|
|
13
|
+
}
|
|
14
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
15
|
+
async ensureClient() {
|
|
16
|
+
if (this.client) {
|
|
17
|
+
return this.client;
|
|
18
|
+
}
|
|
19
|
+
const { createClient } = await import('redis');
|
|
20
|
+
const host = this.config.host || process.env.REDIS_HOST || 'localhost';
|
|
21
|
+
const port = this.config.port || parseInt(process.env.REDIS_PORT || '6379', 10);
|
|
22
|
+
const password = this.config.password || process.env.REDIS_PASSWORD || undefined;
|
|
23
|
+
const db = this.config.db ?? parseInt(process.env.REDIS_DB || '0', 10);
|
|
24
|
+
this.client = createClient({
|
|
25
|
+
socket: { host, port },
|
|
26
|
+
password,
|
|
27
|
+
database: db,
|
|
28
|
+
});
|
|
29
|
+
this.client.on('error', (err) => {
|
|
30
|
+
logger.error('Redis client error', { error: err.message });
|
|
31
|
+
});
|
|
32
|
+
await this.client.connect();
|
|
33
|
+
logger.info('Redis storage initialized', { host, port, db, keyPrefix: this.keyPrefix });
|
|
34
|
+
return this.client;
|
|
35
|
+
}
|
|
36
|
+
key(ownerId) {
|
|
37
|
+
return `${this.keyPrefix}${ownerId}`;
|
|
38
|
+
}
|
|
39
|
+
async get(ownerId) {
|
|
40
|
+
try {
|
|
41
|
+
const client = await this.ensureClient();
|
|
42
|
+
return await client.get(this.key(ownerId));
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
logger.error('Redis get failed', {
|
|
46
|
+
ownerId,
|
|
47
|
+
error: error instanceof Error ? error.message : String(error),
|
|
48
|
+
});
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async set(ownerId, memory) {
|
|
53
|
+
try {
|
|
54
|
+
const client = await this.ensureClient();
|
|
55
|
+
if (this.ttl > 0) {
|
|
56
|
+
await client.set(this.key(ownerId), memory, { EX: this.ttl });
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
await client.set(this.key(ownerId), memory);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
logger.error('Redis set failed', {
|
|
64
|
+
ownerId,
|
|
65
|
+
error: error instanceof Error ? error.message : String(error),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async delete(ownerId) {
|
|
70
|
+
try {
|
|
71
|
+
const client = await this.ensureClient();
|
|
72
|
+
await client.del(this.key(ownerId));
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
logger.error('Redis delete failed', {
|
|
76
|
+
ownerId,
|
|
77
|
+
error: error instanceof Error ? error.message : String(error),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async close() {
|
|
82
|
+
try {
|
|
83
|
+
if (this.client) {
|
|
84
|
+
await this.client.quit();
|
|
85
|
+
this.client = null;
|
|
86
|
+
logger.info('Redis storage closed');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
logger.error('Redis close failed', {
|
|
91
|
+
error: error instanceof Error ? error.message : String(error),
|
|
92
|
+
});
|
|
93
|
+
this.client = null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { RedisStorage };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { StorageBackend, S3StorageConfig } from '../types';
|
|
2
|
+
export declare class S3Storage implements StorageBackend {
|
|
3
|
+
private s3;
|
|
4
|
+
private bucket;
|
|
5
|
+
private prefix;
|
|
6
|
+
constructor(config: S3StorageConfig);
|
|
7
|
+
private ensureClient;
|
|
8
|
+
private objectKey;
|
|
9
|
+
get(ownerId: string): Promise<string | null>;
|
|
10
|
+
set(ownerId: string, memory: string): Promise<void>;
|
|
11
|
+
delete(ownerId: string): Promise<void>;
|
|
12
|
+
close(): Promise<void>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { logger } from '../logger.js';
|
|
2
|
+
|
|
3
|
+
class S3Storage {
|
|
4
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
5
|
+
s3 = null;
|
|
6
|
+
bucket;
|
|
7
|
+
prefix;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.bucket = config.bucket;
|
|
10
|
+
this.prefix = config.prefix || 'hippocampus/memories/';
|
|
11
|
+
// Ensure prefix ends with /
|
|
12
|
+
if (this.prefix && !this.prefix.endsWith('/')) {
|
|
13
|
+
this.prefix += '/';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
17
|
+
async ensureClient() {
|
|
18
|
+
if (this.s3) {
|
|
19
|
+
return this.s3;
|
|
20
|
+
}
|
|
21
|
+
const { S3Client } = await import('@aws-sdk/client-s3');
|
|
22
|
+
this.s3 = new S3Client({});
|
|
23
|
+
logger.info('S3 storage initialized', {
|
|
24
|
+
bucket: this.bucket,
|
|
25
|
+
prefix: this.prefix,
|
|
26
|
+
});
|
|
27
|
+
return this.s3;
|
|
28
|
+
}
|
|
29
|
+
objectKey(ownerId) {
|
|
30
|
+
return `${this.prefix}${ownerId}`;
|
|
31
|
+
}
|
|
32
|
+
async get(ownerId) {
|
|
33
|
+
try {
|
|
34
|
+
const s3 = await this.ensureClient();
|
|
35
|
+
const { GetObjectCommand } = await import('@aws-sdk/client-s3');
|
|
36
|
+
const response = await s3.send(new GetObjectCommand({
|
|
37
|
+
Bucket: this.bucket,
|
|
38
|
+
Key: this.objectKey(ownerId),
|
|
39
|
+
}));
|
|
40
|
+
return (await response.Body?.transformToString('utf-8')) ?? null;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
// NoSuchKey = ownerId not found — not an error
|
|
44
|
+
if (error && typeof error === 'object' && 'name' in error && error.name === 'NoSuchKey') {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
logger.error('S3 get failed', {
|
|
48
|
+
ownerId,
|
|
49
|
+
bucket: this.bucket,
|
|
50
|
+
error: error instanceof Error ? error.message : String(error),
|
|
51
|
+
});
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async set(ownerId, memory) {
|
|
56
|
+
try {
|
|
57
|
+
const s3 = await this.ensureClient();
|
|
58
|
+
const { PutObjectCommand } = await import('@aws-sdk/client-s3');
|
|
59
|
+
await s3.send(new PutObjectCommand({
|
|
60
|
+
Bucket: this.bucket,
|
|
61
|
+
Key: this.objectKey(ownerId),
|
|
62
|
+
Body: memory,
|
|
63
|
+
ContentType: 'text/plain; charset=utf-8',
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
logger.error('S3 set failed', {
|
|
68
|
+
ownerId,
|
|
69
|
+
bucket: this.bucket,
|
|
70
|
+
error: error instanceof Error ? error.message : String(error),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async delete(ownerId) {
|
|
75
|
+
try {
|
|
76
|
+
const s3 = await this.ensureClient();
|
|
77
|
+
const { DeleteObjectCommand } = await import('@aws-sdk/client-s3');
|
|
78
|
+
await s3.send(new DeleteObjectCommand({
|
|
79
|
+
Bucket: this.bucket,
|
|
80
|
+
Key: this.objectKey(ownerId),
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
logger.error('S3 delete failed', {
|
|
85
|
+
ownerId,
|
|
86
|
+
bucket: this.bucket,
|
|
87
|
+
error: error instanceof Error ? error.message : String(error),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async close() {
|
|
92
|
+
try {
|
|
93
|
+
if (this.s3) {
|
|
94
|
+
this.s3.destroy?.();
|
|
95
|
+
this.s3 = null;
|
|
96
|
+
logger.info('S3 storage closed');
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
logger.error('S3 close failed', {
|
|
101
|
+
error: error instanceof Error ? error.message : String(error),
|
|
102
|
+
});
|
|
103
|
+
this.s3 = null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export { S3Storage };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { StorageBackend, SqliteStorageConfig } from '../types';
|
|
2
|
+
export declare class SqliteStorage implements StorageBackend {
|
|
3
|
+
private db;
|
|
4
|
+
private dbPath;
|
|
5
|
+
constructor(config?: SqliteStorageConfig);
|
|
6
|
+
private ensureDb;
|
|
7
|
+
get(ownerId: string): Promise<string | null>;
|
|
8
|
+
set(ownerId: string, memory: string): Promise<void>;
|
|
9
|
+
delete(ownerId: string): Promise<void>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|