@nuzo/memory-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.
@@ -0,0 +1,241 @@
1
+ import Database from "better-sqlite3";
2
+ import { NuzoMemoryError } from "../errors.js";
3
+ import { migrate } from "./schema.js";
4
+ export class SQLiteMemoryDatabase {
5
+ database;
6
+ transactionQueue = Promise.resolve();
7
+ constructor(options) {
8
+ this.database = new Database(options.path);
9
+ try {
10
+ migrate(this.database);
11
+ }
12
+ catch (error) {
13
+ this.database.close();
14
+ throw error;
15
+ }
16
+ }
17
+ close() {
18
+ this.database.close();
19
+ }
20
+ getSchemaVersion() {
21
+ return this.database.pragma("user_version", { simple: true });
22
+ }
23
+ async run(operation) {
24
+ const previous = this.transactionQueue;
25
+ let release;
26
+ this.transactionQueue = new Promise((resolve) => {
27
+ release = resolve;
28
+ });
29
+ await previous;
30
+ let started = false;
31
+ try {
32
+ this.database.exec("BEGIN IMMEDIATE");
33
+ started = true;
34
+ const result = await operation();
35
+ this.database.exec("COMMIT");
36
+ return result;
37
+ }
38
+ catch (error) {
39
+ if (started && this.database.inTransaction) {
40
+ this.database.exec("ROLLBACK");
41
+ }
42
+ throw error;
43
+ }
44
+ finally {
45
+ release();
46
+ }
47
+ }
48
+ async create(memory) {
49
+ this.database
50
+ .prepare(`
51
+ INSERT INTO memories (
52
+ id, scope, kind, content, tags, source, confidence,
53
+ created_at, updated_at, last_used_at, archived_at
54
+ )
55
+ VALUES (
56
+ @id, @scope, @kind, @content, @tags, @source, @confidence,
57
+ @created_at, @updated_at, @last_used_at, @archived_at
58
+ )
59
+ `)
60
+ .run(toMemoryRow(memory));
61
+ }
62
+ async update(memory) {
63
+ this.database
64
+ .prepare(`
65
+ UPDATE memories
66
+ SET scope = @scope,
67
+ kind = @kind,
68
+ content = @content,
69
+ tags = @tags,
70
+ source = @source,
71
+ confidence = @confidence,
72
+ created_at = @created_at,
73
+ updated_at = @updated_at,
74
+ last_used_at = @last_used_at,
75
+ archived_at = @archived_at
76
+ WHERE id = @id
77
+ `)
78
+ .run(toMemoryRow(memory));
79
+ }
80
+ async findById(id) {
81
+ const row = this.database.prepare("SELECT * FROM memories WHERE id = ?").get(id);
82
+ return row ? fromMemoryRow(row) : null;
83
+ }
84
+ async archive(id, archivedAt) {
85
+ this.database
86
+ .prepare("UPDATE memories SET archived_at = @archived_at, updated_at = @archived_at WHERE id = @id")
87
+ .run({ id, archived_at: archivedAt.toISOString() });
88
+ }
89
+ async delete(id) {
90
+ this.database.prepare("DELETE FROM memories WHERE id = ?").run(id);
91
+ }
92
+ async index(memory) {
93
+ this.database.prepare("DELETE FROM memories_fts WHERE id = ?").run(memory.id);
94
+ if (memory.archivedAt) {
95
+ return;
96
+ }
97
+ this.database
98
+ .prepare("INSERT INTO memories_fts (id, scope, content, tags) VALUES (?, ?, ?, ?)")
99
+ .run(memory.id, memory.scope, memory.content, memory.tags.join(" "));
100
+ }
101
+ async remove(memoryId) {
102
+ this.database.prepare("DELETE FROM memories_fts WHERE id = ?").run(memoryId);
103
+ }
104
+ async search(input) {
105
+ const limit = input.limit ?? 8;
106
+ const query = toFtsQuery(input.query);
107
+ if (!query) {
108
+ return [];
109
+ }
110
+ const scopeClause = input.includeGlobal === true
111
+ ? "AND (m.scope = @scope OR m.scope = 'user:default')"
112
+ : "AND m.scope = @scope";
113
+ const rows = this.database
114
+ .prepare(`
115
+ SELECT m.*, bm25(memories_fts) * -1 AS score
116
+ FROM memories_fts
117
+ JOIN memories m ON m.id = memories_fts.id
118
+ WHERE memories_fts MATCH @query
119
+ AND m.archived_at IS NULL
120
+ ${scopeClause}
121
+ ORDER BY bm25(memories_fts)
122
+ LIMIT @limit
123
+ `)
124
+ .all({ query, scope: input.scope, limit });
125
+ return rows.map((row) => ({
126
+ memory: fromMemoryRow(row),
127
+ score: row.score,
128
+ reason: `Matched FTS query: ${query}`,
129
+ }));
130
+ }
131
+ async append(event) {
132
+ this.database
133
+ .prepare(`
134
+ INSERT INTO memory_events (id, memory_id, event_type, actor, payload, created_at)
135
+ VALUES (@id, @memory_id, @event_type, @actor, @payload, @created_at)
136
+ `)
137
+ .run(toEventRow(event));
138
+ }
139
+ async list(memoryIdOrFilter) {
140
+ if (typeof memoryIdOrFilter !== "string") {
141
+ return this.listMemories(memoryIdOrFilter);
142
+ }
143
+ const rows = this.database
144
+ .prepare("SELECT * FROM memory_events WHERE memory_id = ? ORDER BY created_at ASC")
145
+ .all(memoryIdOrFilter);
146
+ return rows.map(fromEventRow);
147
+ }
148
+ async listMemories(filter) {
149
+ const where = [];
150
+ const params = {};
151
+ if (filter.scope) {
152
+ where.push("scope = @scope");
153
+ params.scope = filter.scope;
154
+ }
155
+ if (filter.includeArchived !== true) {
156
+ where.push("archived_at IS NULL");
157
+ }
158
+ const sql = `
159
+ SELECT *
160
+ FROM memories
161
+ ${where.length > 0 ? `WHERE ${where.join(" AND ")}` : ""}
162
+ ORDER BY updated_at DESC, created_at DESC
163
+ `;
164
+ const rows = this.database.prepare(sql).all(params);
165
+ return rows
166
+ .map(fromMemoryRow)
167
+ .filter((memory) => !filter.tags || filter.tags.every((tag) => memory.tags.includes(tag)));
168
+ }
169
+ }
170
+ function toMemoryRow(memory) {
171
+ return {
172
+ id: memory.id,
173
+ scope: memory.scope,
174
+ kind: memory.kind,
175
+ content: memory.content,
176
+ tags: JSON.stringify(memory.tags),
177
+ source: memory.source,
178
+ confidence: memory.confidence,
179
+ created_at: memory.createdAt.toISOString(),
180
+ updated_at: memory.updatedAt.toISOString(),
181
+ last_used_at: memory.lastUsedAt?.toISOString() ?? null,
182
+ archived_at: memory.archivedAt?.toISOString() ?? null,
183
+ };
184
+ }
185
+ function fromMemoryRow(row) {
186
+ return {
187
+ id: row.id,
188
+ scope: row.scope,
189
+ kind: row.kind,
190
+ content: row.content,
191
+ tags: parseTags(row.tags),
192
+ source: row.source,
193
+ confidence: row.confidence,
194
+ createdAt: new Date(row.created_at),
195
+ updatedAt: new Date(row.updated_at),
196
+ lastUsedAt: row.last_used_at ? new Date(row.last_used_at) : null,
197
+ archivedAt: row.archived_at ? new Date(row.archived_at) : null,
198
+ };
199
+ }
200
+ function toEventRow(event) {
201
+ return {
202
+ id: event.id,
203
+ memory_id: event.memoryId,
204
+ event_type: event.eventType,
205
+ actor: event.actor,
206
+ payload: JSON.stringify(event.payload),
207
+ created_at: event.createdAt.toISOString(),
208
+ };
209
+ }
210
+ function fromEventRow(row) {
211
+ return {
212
+ id: row.id,
213
+ memoryId: row.memory_id,
214
+ eventType: row.event_type,
215
+ actor: row.actor,
216
+ payload: parsePayload(row.payload),
217
+ createdAt: new Date(row.created_at),
218
+ };
219
+ }
220
+ function parseTags(value) {
221
+ const parsed = JSON.parse(value);
222
+ if (!Array.isArray(parsed) || !parsed.every((tag) => typeof tag === "string")) {
223
+ throw new NuzoMemoryError("MEMORY_TAGS_INVALID", "Stored memory tags are invalid.");
224
+ }
225
+ return parsed;
226
+ }
227
+ function parsePayload(value) {
228
+ const parsed = JSON.parse(value);
229
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
230
+ throw new NuzoMemoryError("MEMORY_EVENT_PAYLOAD_INVALID", "Stored memory event payload is invalid.");
231
+ }
232
+ return parsed;
233
+ }
234
+ function toFtsQuery(query) {
235
+ return query
236
+ .trim()
237
+ .split(/\W+/)
238
+ .filter(Boolean)
239
+ .map((term) => `"${term.replaceAll('"', '""')}"`)
240
+ .join(" OR ");
241
+ }
@@ -0,0 +1,3 @@
1
+ import type Database from "better-sqlite3";
2
+ export declare const schemaVersion = 1;
3
+ export declare function migrate(database: Database.Database): void;
@@ -0,0 +1,54 @@
1
+ import { NuzoMemoryError } from "../errors.js";
2
+ export const schemaVersion = 1;
3
+ export function migrate(database) {
4
+ database.pragma("journal_mode = WAL");
5
+ database.pragma("foreign_keys = ON");
6
+ const currentVersion = database.pragma("user_version", { simple: true });
7
+ if (currentVersion > schemaVersion) {
8
+ throw new NuzoMemoryError("MEMORY_SCHEMA_UNSUPPORTED", "SQLite memory schema is newer than this Nuzo version supports.", {
9
+ currentVersion,
10
+ supportedVersion: schemaVersion,
11
+ });
12
+ }
13
+ if (currentVersion < 1) {
14
+ migrateToV1(database);
15
+ database.pragma(`user_version = ${schemaVersion}`);
16
+ }
17
+ }
18
+ function migrateToV1(database) {
19
+ database.exec(`
20
+ CREATE TABLE IF NOT EXISTS memories (
21
+ id TEXT PRIMARY KEY,
22
+ scope TEXT NOT NULL,
23
+ kind TEXT NOT NULL,
24
+ content TEXT NOT NULL,
25
+ tags TEXT NOT NULL DEFAULT '[]',
26
+ source TEXT NOT NULL,
27
+ confidence REAL NOT NULL DEFAULT 1.0,
28
+ created_at TEXT NOT NULL,
29
+ updated_at TEXT NOT NULL,
30
+ last_used_at TEXT,
31
+ archived_at TEXT
32
+ );
33
+
34
+ CREATE TABLE IF NOT EXISTS memory_events (
35
+ id TEXT PRIMARY KEY,
36
+ memory_id TEXT,
37
+ event_type TEXT NOT NULL,
38
+ actor TEXT NOT NULL,
39
+ payload TEXT NOT NULL,
40
+ created_at TEXT NOT NULL
41
+ );
42
+
43
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
44
+ id UNINDEXED,
45
+ scope UNINDEXED,
46
+ content,
47
+ tags
48
+ );
49
+
50
+ CREATE INDEX IF NOT EXISTS idx_memories_scope ON memories(scope);
51
+ CREATE INDEX IF NOT EXISTS idx_memories_archived_at ON memories(archived_at);
52
+ CREATE INDEX IF NOT EXISTS idx_memory_events_memory_id ON memory_events(memory_id);
53
+ `);
54
+ }
@@ -0,0 +1,114 @@
1
+ export type MemoryKind = "preference" | "project_decision" | "fact" | "instruction" | "note";
2
+ export declare const memoryKinds: readonly ["preference", "project_decision", "fact", "instruction", "note"];
3
+ export type MemoryScope = `user:${string}` | `project:${string}` | `agent:${string}` | `team:${string}`;
4
+ export interface MemoryRecord {
5
+ id: string;
6
+ scope: MemoryScope;
7
+ kind: MemoryKind;
8
+ content: string;
9
+ tags: string[];
10
+ source: string;
11
+ confidence: number;
12
+ createdAt: Date;
13
+ updatedAt: Date;
14
+ lastUsedAt: Date | null;
15
+ archivedAt: Date | null;
16
+ }
17
+ export interface MemoryEvent {
18
+ id: string;
19
+ memoryId: string | null;
20
+ eventType: "memory.created" | "memory.updated" | "memory.archived" | "memory.deleted" | "memory.imported" | "memory.exported" | "memory.recalled";
21
+ actor: string;
22
+ payload: Record<string, unknown>;
23
+ createdAt: Date;
24
+ }
25
+ export interface RememberMemoryInput {
26
+ content: string;
27
+ kind: MemoryKind;
28
+ scope: MemoryScope;
29
+ tags?: string[];
30
+ source: string;
31
+ confidence?: number;
32
+ }
33
+ export interface RecallMemoriesInput {
34
+ query: string;
35
+ scope: MemoryScope;
36
+ limit?: number;
37
+ includeGlobal?: boolean;
38
+ recordUsage?: boolean;
39
+ }
40
+ export interface ListMemoriesInput {
41
+ scope?: MemoryScope;
42
+ tags?: string[];
43
+ includeArchived?: boolean;
44
+ }
45
+ export interface ForgetMemoryInput {
46
+ id: string;
47
+ mode?: "archive" | "delete";
48
+ confirm?: boolean;
49
+ actor: string;
50
+ reason?: string;
51
+ }
52
+ export interface ForgetMemoriesInput {
53
+ scope?: MemoryScope;
54
+ tags?: string[];
55
+ all?: boolean;
56
+ mode?: "archive" | "delete";
57
+ confirm?: boolean;
58
+ dryRun?: boolean;
59
+ actor: string;
60
+ reason?: string;
61
+ }
62
+ export interface ForgetMemoriesResult {
63
+ matched: number;
64
+ affected: number;
65
+ mode: "archive" | "delete";
66
+ dryRun: boolean;
67
+ ids: string[];
68
+ }
69
+ export interface UpdateMemoryInput {
70
+ id: string;
71
+ content?: string;
72
+ kind?: MemoryKind;
73
+ scope?: MemoryScope;
74
+ tags?: string[];
75
+ confidence?: number;
76
+ actor: string;
77
+ }
78
+ export interface ExportMemoriesInput extends ListMemoriesInput {
79
+ actor: string;
80
+ }
81
+ export interface ImportMemoriesInput {
82
+ document: MemoryExportDocument;
83
+ actor: string;
84
+ scope?: MemoryScope;
85
+ dryRun?: boolean;
86
+ }
87
+ export interface ImportMemoriesResult {
88
+ imported: number;
89
+ skipped: number;
90
+ dryRun: boolean;
91
+ }
92
+ export interface MemoryExportDocument {
93
+ format: "nuzo-memory-export";
94
+ version: 1;
95
+ exported_at: string;
96
+ memories: MemoryExportItem[];
97
+ }
98
+ export interface MemoryExportItem {
99
+ scope: MemoryScope;
100
+ kind: MemoryKind;
101
+ content: string;
102
+ tags: string[];
103
+ source: string;
104
+ confidence: number;
105
+ created_at: string;
106
+ updated_at: string;
107
+ last_used_at: string | null;
108
+ archived_at: string | null;
109
+ }
110
+ export interface RecallMemoryResult {
111
+ memory: MemoryRecord;
112
+ score: number;
113
+ reason: string;
114
+ }
package/dist/types.js ADDED
@@ -0,0 +1,7 @@
1
+ export const memoryKinds = [
2
+ "preference",
3
+ "project_decision",
4
+ "fact",
5
+ "instruction",
6
+ "note",
7
+ ];
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@nuzo/memory-core",
3
+ "version": "0.1.0",
4
+ "description": "Core memory lifecycle, ports, and domain contracts for Nuzo.",
5
+ "keywords": [
6
+ "ai-agents",
7
+ "memory",
8
+ "local-first",
9
+ "sqlite"
10
+ ],
11
+ "license": "Apache-2.0",
12
+ "type": "module",
13
+ "engines": {
14
+ "node": ">=22",
15
+ "npm": ">=10"
16
+ },
17
+ "main": "dist/index.js",
18
+ "types": "dist/index.d.ts",
19
+ "exports": {
20
+ ".": {
21
+ "types": "./dist/index.d.ts",
22
+ "import": "./dist/index.js"
23
+ }
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "README.md",
28
+ "LICENSE"
29
+ ],
30
+ "dependencies": {
31
+ "better-sqlite3": "^12.10.0"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/fabionfsc/nuzo-memory.git",
39
+ "directory": "packages/core"
40
+ },
41
+ "homepage": "https://nuzo.com.br/",
42
+ "bugs": {
43
+ "url": "https://github.com/fabionfsc/nuzo-memory/issues"
44
+ }
45
+ }