@1mbrain/core 0.1.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/package.json +42 -0
- package/src/config.ts +50 -0
- package/src/db/index.ts +38 -0
- package/src/db/postgres-provider.ts +525 -0
- package/src/db/sqlite-provider.ts +548 -0
- package/src/embedding/index.ts +56 -0
- package/src/embedding/keyword-provider.ts +71 -0
- package/src/embedding/ollama-provider.ts +78 -0
- package/src/embedding/openai-provider.ts +99 -0
- package/src/engine.ts +1073 -0
- package/src/events.ts +142 -0
- package/src/index.ts +83 -0
- package/src/logger.ts +31 -0
- package/src/passport.ts +118 -0
- package/src/ranking-policy.ts +563 -0
- package/src/schemas.ts +114 -0
- package/src/types.ts +229 -0
- package/tests/benchmark.ts +125 -0
- package/tests/embedding.test.ts +119 -0
- package/tests/engine.test.ts +1017 -0
- package/tests/passport.test.ts +83 -0
- package/tests/ranking-policy.test.ts +268 -0
- package/tsconfig.json +9 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +18 -0
package/src/events.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event Bus
|
|
3
|
+
*
|
|
4
|
+
* Handles memory events for the dashboard WebSocket stream.
|
|
5
|
+
* Supports Redis pub/sub for multi-instance deployments and
|
|
6
|
+
* an in-memory fallback for single-instance / development use.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MemoryEvent } from './types.js';
|
|
10
|
+
import { createChildLogger } from './logger.js';
|
|
11
|
+
|
|
12
|
+
const log = createChildLogger('event-bus');
|
|
13
|
+
|
|
14
|
+
export type MemoryEventHandler = (event: MemoryEvent) => void;
|
|
15
|
+
|
|
16
|
+
export interface EventBus {
|
|
17
|
+
publish(event: MemoryEvent): Promise<void>;
|
|
18
|
+
subscribe(handler: MemoryEventHandler): () => void;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ─── In-Memory Event Bus (development / single instance) ─
|
|
23
|
+
|
|
24
|
+
export class InMemoryEventBus implements EventBus {
|
|
25
|
+
private handlers = new Set<MemoryEventHandler>();
|
|
26
|
+
|
|
27
|
+
async publish(event: MemoryEvent): Promise<void> {
|
|
28
|
+
log.debug({ type: event.type, memoryId: event.memoryId }, 'Publishing event (in-memory)');
|
|
29
|
+
for (const handler of this.handlers) {
|
|
30
|
+
try {
|
|
31
|
+
handler(event);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
log.error({ err }, 'Event handler error');
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
subscribe(handler: MemoryEventHandler): () => void {
|
|
39
|
+
this.handlers.add(handler);
|
|
40
|
+
log.debug({ subscriberCount: this.handlers.size }, 'Subscriber added');
|
|
41
|
+
|
|
42
|
+
return () => {
|
|
43
|
+
this.handlers.delete(handler);
|
|
44
|
+
log.debug({ subscriberCount: this.handlers.size }, 'Subscriber removed');
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async close(): Promise<void> {
|
|
49
|
+
this.handlers.clear();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ─── Redis Event Bus (production / multi-instance) ──────
|
|
54
|
+
|
|
55
|
+
export class RedisEventBus implements EventBus {
|
|
56
|
+
private handlers = new Set<MemoryEventHandler>();
|
|
57
|
+
private publisher: RedisClient | null = null;
|
|
58
|
+
private subscriber: RedisClient | null = null;
|
|
59
|
+
private readonly channel = '1mbrain:events';
|
|
60
|
+
private readonly redisUrl: string;
|
|
61
|
+
|
|
62
|
+
constructor(redisUrl: string) {
|
|
63
|
+
this.redisUrl = redisUrl;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async initialize(): Promise<void> {
|
|
67
|
+
const { default: Redis } = await import('ioredis');
|
|
68
|
+
|
|
69
|
+
this.publisher = new Redis(this.redisUrl) as unknown as RedisClient;
|
|
70
|
+
this.subscriber = new Redis(this.redisUrl) as unknown as RedisClient;
|
|
71
|
+
|
|
72
|
+
await this.subscriber.subscribe(this.channel);
|
|
73
|
+
this.subscriber.on('message', (_channel: string, message: string) => {
|
|
74
|
+
try {
|
|
75
|
+
const event = JSON.parse(message) as MemoryEvent;
|
|
76
|
+
event.timestamp = new Date(event.timestamp);
|
|
77
|
+
for (const handler of this.handlers) {
|
|
78
|
+
try {
|
|
79
|
+
handler(event);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
log.error({ err }, 'Event handler error');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
} catch (err) {
|
|
85
|
+
log.error({ err, message }, 'Failed to parse event');
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
log.info({ channel: this.channel }, 'Redis event bus initialized');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async publish(event: MemoryEvent): Promise<void> {
|
|
93
|
+
if (!this.publisher) {
|
|
94
|
+
throw new Error('Redis event bus not initialized');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const serialized = JSON.stringify(event);
|
|
98
|
+
await this.publisher.publish(this.channel, serialized);
|
|
99
|
+
log.debug({ type: event.type, memoryId: event.memoryId }, 'Publishing event (Redis)');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
subscribe(handler: MemoryEventHandler): () => void {
|
|
103
|
+
this.handlers.add(handler);
|
|
104
|
+
return () => {
|
|
105
|
+
this.handlers.delete(handler);
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async close(): Promise<void> {
|
|
110
|
+
if (this.subscriber) {
|
|
111
|
+
await this.subscriber.unsubscribe(this.channel);
|
|
112
|
+
await this.subscriber.quit();
|
|
113
|
+
}
|
|
114
|
+
if (this.publisher) {
|
|
115
|
+
await this.publisher.quit();
|
|
116
|
+
}
|
|
117
|
+
this.handlers.clear();
|
|
118
|
+
log.info('Redis event bus closed');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Minimal Redis client interface to avoid tight coupling
|
|
123
|
+
interface RedisClient {
|
|
124
|
+
publish(channel: string, message: string): Promise<number>;
|
|
125
|
+
subscribe(channel: string): Promise<void>;
|
|
126
|
+
unsubscribe(channel: string): Promise<void>;
|
|
127
|
+
on(event: string, handler: (...args: string[]) => void): void;
|
|
128
|
+
quit(): Promise<void>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Factory ────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
export async function createEventBus(redisUrl?: string): Promise<EventBus> {
|
|
134
|
+
if (redisUrl) {
|
|
135
|
+
const bus = new RedisEventBus(redisUrl);
|
|
136
|
+
await bus.initialize();
|
|
137
|
+
return bus;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
log.info('Using in-memory event bus (no Redis URL configured)');
|
|
141
|
+
return new InMemoryEventBus();
|
|
142
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @1mbrain/core
|
|
3
|
+
*
|
|
4
|
+
* Public API surface for the core package.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Types
|
|
8
|
+
export type {
|
|
9
|
+
Memory,
|
|
10
|
+
MemoryType,
|
|
11
|
+
Association,
|
|
12
|
+
AssociationOrigin,
|
|
13
|
+
AssociationRelationType,
|
|
14
|
+
CreateMemoryInput,
|
|
15
|
+
SearchMemoryInput,
|
|
16
|
+
SearchResult,
|
|
17
|
+
CreateAssociationInput,
|
|
18
|
+
MemoryPassport,
|
|
19
|
+
MemoryPassportEnvelope,
|
|
20
|
+
EmbeddingProvider,
|
|
21
|
+
DatabaseProvider,
|
|
22
|
+
MemoryEvent,
|
|
23
|
+
MemoryEventType,
|
|
24
|
+
OneMBrainConfig,
|
|
25
|
+
} from './types.js';
|
|
26
|
+
|
|
27
|
+
// Schemas
|
|
28
|
+
export {
|
|
29
|
+
CreateMemorySchema,
|
|
30
|
+
SearchMemorySchema,
|
|
31
|
+
CreateAssociationSchema,
|
|
32
|
+
ExportPassportSchema,
|
|
33
|
+
ImportPassportSchema,
|
|
34
|
+
MemoryTypeSchema,
|
|
35
|
+
AssociationOriginSchema,
|
|
36
|
+
AssociationRelationTypeSchema,
|
|
37
|
+
} from './schemas.js';
|
|
38
|
+
|
|
39
|
+
export type {
|
|
40
|
+
CreateMemoryPayload,
|
|
41
|
+
SearchMemoryQuery,
|
|
42
|
+
CreateAssociationPayload,
|
|
43
|
+
ExportPassportPayload,
|
|
44
|
+
ImportPassportPayload,
|
|
45
|
+
} from './schemas.js';
|
|
46
|
+
|
|
47
|
+
// Engine
|
|
48
|
+
export { MemoryEngine } from './engine.js';
|
|
49
|
+
export { RankingPolicy, analyzeQueryIntent } from './ranking-policy.js';
|
|
50
|
+
export type { QueryIntent, RankedSearchResult, RankingOutcome } from './ranking-policy.js';
|
|
51
|
+
|
|
52
|
+
// Memory Passport
|
|
53
|
+
export {
|
|
54
|
+
createPassportEnvelope,
|
|
55
|
+
deserializePassport,
|
|
56
|
+
normalizeEncryptionKey,
|
|
57
|
+
openPassportEnvelope,
|
|
58
|
+
serializePassport,
|
|
59
|
+
} from './passport.js';
|
|
60
|
+
|
|
61
|
+
// Database
|
|
62
|
+
export {
|
|
63
|
+
createDatabaseProvider,
|
|
64
|
+
SqliteDatabaseProvider,
|
|
65
|
+
PostgresDatabaseProvider,
|
|
66
|
+
} from './db/index.js';
|
|
67
|
+
|
|
68
|
+
// Embedding
|
|
69
|
+
export {
|
|
70
|
+
createEmbeddingProvider,
|
|
71
|
+
OpenAIEmbeddingProvider,
|
|
72
|
+
OllamaEmbeddingProvider,
|
|
73
|
+
} from './embedding/index.js';
|
|
74
|
+
|
|
75
|
+
// Events
|
|
76
|
+
export { createEventBus, InMemoryEventBus, RedisEventBus } from './events.js';
|
|
77
|
+
export type { EventBus, MemoryEventHandler } from './events.js';
|
|
78
|
+
|
|
79
|
+
// Config
|
|
80
|
+
export { loadConfig } from './config.js';
|
|
81
|
+
|
|
82
|
+
// Logger
|
|
83
|
+
export { logger, createChildLogger } from './logger.js';
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pino Logger
|
|
3
|
+
*
|
|
4
|
+
* Structured logging used throughout 1MBrain.
|
|
5
|
+
* Logs in JSON format for production, pretty-printed in development.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import pino from 'pino';
|
|
9
|
+
|
|
10
|
+
const isDev = process.env.NODE_ENV !== 'production';
|
|
11
|
+
|
|
12
|
+
export const logger = pino({
|
|
13
|
+
level: process.env.LOG_LEVEL || (isDev ? 'debug' : 'info'),
|
|
14
|
+
transport: isDev
|
|
15
|
+
? {
|
|
16
|
+
target: 'pino-pretty',
|
|
17
|
+
options: {
|
|
18
|
+
colorize: true,
|
|
19
|
+
translateTime: 'HH:MM:ss',
|
|
20
|
+
ignore: 'pid,hostname',
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
: undefined,
|
|
24
|
+
base: {
|
|
25
|
+
service: '1mbrain',
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export function createChildLogger(name: string) {
|
|
30
|
+
return logger.child({ component: name });
|
|
31
|
+
}
|
package/src/passport.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
3
|
+
import type { Association, Memory, MemoryPassport, MemoryPassportEnvelope } from './types.js';
|
|
4
|
+
|
|
5
|
+
type SerializedMemory = Omit<Memory, 'createdAt' | 'lastAccessedAt'> & {
|
|
6
|
+
createdAt: string;
|
|
7
|
+
lastAccessedAt: string;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type SerializedAssociation = Omit<Association, 'createdAt'> & {
|
|
11
|
+
createdAt: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type SerializedPassport = Omit<MemoryPassport, 'exportedAt' | 'memories' | 'associations'> & {
|
|
15
|
+
exportedAt: string;
|
|
16
|
+
memories: SerializedMemory[];
|
|
17
|
+
associations: SerializedAssociation[];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function serializePassport(passport: MemoryPassport): Buffer {
|
|
21
|
+
return Buffer.from(JSON.stringify(passport), 'utf8');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function deserializePassport(input: Buffer | string): MemoryPassport {
|
|
25
|
+
const raw = typeof input === 'string' ? input : input.toString('utf8');
|
|
26
|
+
const parsed = JSON.parse(raw) as SerializedPassport;
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
...parsed,
|
|
30
|
+
exportedAt: new Date(parsed.exportedAt),
|
|
31
|
+
memories: parsed.memories.map((memory) => ({
|
|
32
|
+
...memory,
|
|
33
|
+
createdAt: new Date(memory.createdAt),
|
|
34
|
+
lastAccessedAt: new Date(memory.lastAccessedAt),
|
|
35
|
+
})),
|
|
36
|
+
associations: parsed.associations.map((association) => ({
|
|
37
|
+
...association,
|
|
38
|
+
createdAt: new Date(association.createdAt),
|
|
39
|
+
})),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createPassportEnvelope(
|
|
44
|
+
passport: MemoryPassport,
|
|
45
|
+
encryptionKey: string,
|
|
46
|
+
): MemoryPassportEnvelope {
|
|
47
|
+
const key = normalizeEncryptionKey(encryptionKey);
|
|
48
|
+
const iv = randomBytes(12);
|
|
49
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
|
50
|
+
const compressed = gzipSync(serializePassport(passport));
|
|
51
|
+
const encrypted = Buffer.concat([cipher.update(compressed), cipher.final()]);
|
|
52
|
+
const authTag = cipher.getAuthTag();
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
format: '1mbrain.passport.envelope',
|
|
56
|
+
version: passport.version,
|
|
57
|
+
exportedAt: new Date(passport.exportedAt).toISOString(),
|
|
58
|
+
sourceAgent: passport.sourceAgent,
|
|
59
|
+
compression: 'gzip',
|
|
60
|
+
encoding: 'base64',
|
|
61
|
+
encryption: {
|
|
62
|
+
algorithm: 'aes-256-gcm',
|
|
63
|
+
iv: iv.toString('base64'),
|
|
64
|
+
authTag: authTag.toString('base64'),
|
|
65
|
+
},
|
|
66
|
+
payload: encrypted.toString('base64'),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function openPassportEnvelope(
|
|
71
|
+
envelope: MemoryPassportEnvelope,
|
|
72
|
+
encryptionKey: string,
|
|
73
|
+
): MemoryPassport {
|
|
74
|
+
if (envelope.format !== '1mbrain.passport.envelope') {
|
|
75
|
+
throw new Error('Unsupported Memory Passport envelope format');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
envelope.compression !== 'gzip' ||
|
|
80
|
+
envelope.encoding !== 'base64' ||
|
|
81
|
+
envelope.encryption.algorithm !== 'aes-256-gcm'
|
|
82
|
+
) {
|
|
83
|
+
throw new Error('Unsupported Memory Passport envelope encoding');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const key = normalizeEncryptionKey(encryptionKey);
|
|
87
|
+
const decipher = createDecipheriv(
|
|
88
|
+
'aes-256-gcm',
|
|
89
|
+
key,
|
|
90
|
+
Buffer.from(envelope.encryption.iv, 'base64'),
|
|
91
|
+
);
|
|
92
|
+
decipher.setAuthTag(Buffer.from(envelope.encryption.authTag, 'base64'));
|
|
93
|
+
|
|
94
|
+
const compressed = Buffer.concat([
|
|
95
|
+
decipher.update(Buffer.from(envelope.payload, 'base64')),
|
|
96
|
+
decipher.final(),
|
|
97
|
+
]);
|
|
98
|
+
|
|
99
|
+
return deserializePassport(gunzipSync(compressed));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function normalizeEncryptionKey(input: string): Buffer {
|
|
103
|
+
const trimmed = input.trim();
|
|
104
|
+
|
|
105
|
+
if (/^[a-fA-F0-9]{64}$/.test(trimmed)) {
|
|
106
|
+
return Buffer.from(trimmed, 'hex');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const base64 = Buffer.from(trimmed, 'base64');
|
|
110
|
+
if (
|
|
111
|
+
base64.length === 32 &&
|
|
112
|
+
base64.toString('base64').replace(/=+$/, '') === trimmed.replace(/=+$/, '')
|
|
113
|
+
) {
|
|
114
|
+
return base64;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return createHash('sha256').update(trimmed).digest();
|
|
118
|
+
}
|