@modusensus/dsh-mneme 0.1.0 → 0.1.2
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/README.md +178 -179
- package/cordis.patch.yml +15 -0
- package/lib/api.js +59 -59
- package/lib/client.js +187 -187
- package/lib/config.js +15 -15
- package/lib/dream/decisions.js +121 -121
- package/lib/dream.js +205 -205
- package/lib/index.js +86 -86
- package/lib/inject.js +22 -22
- package/lib/mirror.js +131 -131
- package/lib/service.js +157 -157
- package/lib/store.js +230 -230
- package/lib/summarize.js +171 -171
- package/lib/tools.js +221 -221
- package/package.json +58 -32
- package/src/api.js +59 -59
- package/src/config.js +15 -15
- package/src/dream/decisions.js +121 -121
- package/src/dream.js +205 -205
- package/src/index.js +86 -86
- package/src/inject.js +22 -22
- package/src/mirror.js +131 -131
- package/src/service.js +157 -157
- package/src/store.js +230 -230
- package/src/summarize.js +171 -171
- package/src/tools.js +221 -221
package/lib/store.js
CHANGED
|
@@ -1,230 +1,230 @@
|
|
|
1
|
-
import { DatabaseSync } from "node:sqlite";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
|
-
|
|
4
|
-
const SCHEMA = `
|
|
5
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
6
|
-
id TEXT PRIMARY KEY,
|
|
7
|
-
type TEXT NOT NULL,
|
|
8
|
-
title TEXT NOT NULL,
|
|
9
|
-
content TEXT NOT NULL,
|
|
10
|
-
tags TEXT NOT NULL DEFAULT '[]',
|
|
11
|
-
importance INTEGER NOT NULL DEFAULT 3,
|
|
12
|
-
forgotten INTEGER NOT NULL DEFAULT 0,
|
|
13
|
-
archived INTEGER NOT NULL DEFAULT 0,
|
|
14
|
-
source TEXT,
|
|
15
|
-
created_at TEXT NOT NULL,
|
|
16
|
-
updated_at TEXT NOT NULL
|
|
17
|
-
);
|
|
18
|
-
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
19
|
-
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
20
|
-
`;
|
|
21
|
-
|
|
22
|
-
const TYPES = new Set(["preference", "project", "decision", "history", "summary"]);
|
|
23
|
-
|
|
24
|
-
// Pure helpers: no shared module state.
|
|
25
|
-
|
|
26
|
-
function sanitizePage(limit, offset, defaultLimit) {
|
|
27
|
-
const lim = Number.isInteger(limit) && limit > 0 ? limit : defaultLimit;
|
|
28
|
-
const off = Number.isInteger(offset) && offset > 0 ? offset : 0;
|
|
29
|
-
return { limit: lim, offset: off };
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function escapeLike(q) {
|
|
33
|
-
return q.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function parseTags(raw) {
|
|
37
|
-
try {
|
|
38
|
-
const arr = JSON.parse(raw);
|
|
39
|
-
return Array.isArray(arr) ? arr : [];
|
|
40
|
-
} catch {
|
|
41
|
-
return [];
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function toRow(row) {
|
|
46
|
-
if (!row) return undefined;
|
|
47
|
-
return {
|
|
48
|
-
id: row.id,
|
|
49
|
-
type: row.type,
|
|
50
|
-
title: row.title,
|
|
51
|
-
content: row.content,
|
|
52
|
-
tags: parseTags(row.tags),
|
|
53
|
-
importance: row.importance,
|
|
54
|
-
forgotten: row.forgotten === 1,
|
|
55
|
-
archived: row.archived === 1,
|
|
56
|
-
source: row.source ?? undefined,
|
|
57
|
-
created_at: row.created_at,
|
|
58
|
-
updated_at: row.updated_at
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function createStore(path) {
|
|
63
|
-
const db = new DatabaseSync(path);
|
|
64
|
-
db.exec("PRAGMA journal_mode = WAL;");
|
|
65
|
-
db.exec(SCHEMA);
|
|
66
|
-
|
|
67
|
-
// Schema migration: add archived column to legacy databases (idempotent)
|
|
68
|
-
const columns = db.prepare("PRAGMA table_info(memories)").all().map((c) => c.name);
|
|
69
|
-
if (!columns.includes("archived")) {
|
|
70
|
-
db.exec("ALTER TABLE memories ADD COLUMN archived INTEGER NOT NULL DEFAULT 0");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Per-instance monotonic timestamp guard: consecutive writes within the same
|
|
74
|
-
// millisecond must still produce strictly increasing timestamps (test asserts
|
|
75
|
-
// updated_at != created_at). State lives in the store closure, not module scope.
|
|
76
|
-
let lastTs = "";
|
|
77
|
-
function nowIso() {
|
|
78
|
-
let ts = new Date().toISOString();
|
|
79
|
-
if (lastTs && ts <= lastTs) {
|
|
80
|
-
const d = new Date(lastTs);
|
|
81
|
-
d.setMilliseconds(d.getMilliseconds() + 1);
|
|
82
|
-
ts = d.toISOString();
|
|
83
|
-
}
|
|
84
|
-
lastTs = ts;
|
|
85
|
-
return ts;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
function count(type, { includeForgotten = false, includeArchived = false } = {}) {
|
|
89
|
-
const clauses = [];
|
|
90
|
-
const params = [];
|
|
91
|
-
if (type !== undefined) {
|
|
92
|
-
clauses.push("type = ?");
|
|
93
|
-
params.push(type);
|
|
94
|
-
}
|
|
95
|
-
if (!includeForgotten) {
|
|
96
|
-
clauses.push("forgotten = 0");
|
|
97
|
-
}
|
|
98
|
-
if (!includeArchived) {
|
|
99
|
-
clauses.push("archived = 0");
|
|
100
|
-
}
|
|
101
|
-
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
102
|
-
return db.prepare(`SELECT count(*) AS c FROM memories ${where}`).get(...params).c;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function getById(id) {
|
|
106
|
-
const row = db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
107
|
-
return toRow(row);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function save(memory) {
|
|
111
|
-
const id = memory.id ?? randomUUID();
|
|
112
|
-
const type = memory.type;
|
|
113
|
-
if (!TYPES.has(type)) throw new Error(`invalid memory type: ${type}`);
|
|
114
|
-
if (memory.tags !== undefined && !Array.isArray(memory.tags)) {
|
|
115
|
-
throw new Error("tags must be an array");
|
|
116
|
-
}
|
|
117
|
-
const now = nowIso();
|
|
118
|
-
const tags = JSON.stringify(memory.tags ?? []);
|
|
119
|
-
const importance = Number.isInteger(memory.importance) ? memory.importance : 3;
|
|
120
|
-
db.prepare(
|
|
121
|
-
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, source, created_at, updated_at)
|
|
122
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`
|
|
123
|
-
).run(id, type, memory.title, memory.content, tags, importance, memory.source ?? null, now, now);
|
|
124
|
-
return getById(id);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function update(id, patch) {
|
|
128
|
-
const existing = getById(id);
|
|
129
|
-
if (!existing) throw new Error(`memory not found: ${id}`);
|
|
130
|
-
const type = patch.type ?? existing.type;
|
|
131
|
-
if (!TYPES.has(type)) throw new Error(`invalid memory type: ${type}`);
|
|
132
|
-
if (patch.tags !== undefined && !Array.isArray(patch.tags)) {
|
|
133
|
-
throw new Error("tags must be an array");
|
|
134
|
-
}
|
|
135
|
-
const now = nowIso();
|
|
136
|
-
db.prepare(
|
|
137
|
-
`UPDATE memories SET type=?, title=?, content=?, tags=?, importance=?, source=?, updated_at=? WHERE id=?`
|
|
138
|
-
).run(
|
|
139
|
-
type,
|
|
140
|
-
patch.title ?? existing.title,
|
|
141
|
-
patch.content ?? existing.content,
|
|
142
|
-
JSON.stringify(patch.tags ?? existing.tags),
|
|
143
|
-
Number.isInteger(patch.importance) ? patch.importance : existing.importance,
|
|
144
|
-
patch.source !== undefined ? patch.source : (existing.source ?? null),
|
|
145
|
-
now,
|
|
146
|
-
id
|
|
147
|
-
);
|
|
148
|
-
return getById(id);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function remove(id) {
|
|
152
|
-
db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function setForget(id, forgotten) {
|
|
156
|
-
db.prepare("UPDATE memories SET forgotten = ?, updated_at = ? WHERE id = ?")
|
|
157
|
-
.run(forgotten === true || forgotten === 1 ? 1 : 0, nowIso(), id);
|
|
158
|
-
return getById(id);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function setArchived(id, archived) {
|
|
162
|
-
db.prepare("UPDATE memories SET archived = ?, updated_at = ? WHERE id = ?")
|
|
163
|
-
.run(archived ? 1 : 0, nowIso(), id);
|
|
164
|
-
return getById(id);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function list({ type, limit = 50, offset = 0, includeForgotten = false, includeArchived = false } = {}) {
|
|
168
|
-
const clauses = [];
|
|
169
|
-
const params = [];
|
|
170
|
-
if (type) {
|
|
171
|
-
clauses.push("type = ?");
|
|
172
|
-
params.push(type);
|
|
173
|
-
}
|
|
174
|
-
if (!includeForgotten) {
|
|
175
|
-
clauses.push("forgotten = 0");
|
|
176
|
-
}
|
|
177
|
-
if (!includeArchived) {
|
|
178
|
-
clauses.push("archived = 0");
|
|
179
|
-
}
|
|
180
|
-
const { limit: lim, offset: off } = sanitizePage(limit, offset, 50);
|
|
181
|
-
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
182
|
-
const rows = db.prepare(
|
|
183
|
-
`SELECT * FROM memories ${where} ORDER BY importance DESC, updated_at DESC, id LIMIT ? OFFSET ?`
|
|
184
|
-
).all(...params, lim, off);
|
|
185
|
-
return rows.map(toRow);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function all() {
|
|
189
|
-
const rows = db.prepare("SELECT * FROM memories ORDER BY updated_at DESC").all();
|
|
190
|
-
return rows.map(toRow);
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function search(query, { limit = 20, includeArchived = false } = {}) {
|
|
194
|
-
const q = String(query).trim();
|
|
195
|
-
if (!q) return [];
|
|
196
|
-
// FTS5 over unicode61 (English + long phrases); LIKE fallback covers CJK substring.
|
|
197
|
-
// LIKE wildcards in the query are escaped so user input is matched literally.
|
|
198
|
-
const like = `%${escapeLike(q)}%`;
|
|
199
|
-
const { limit: lim } = sanitizePage(limit, 0, 20);
|
|
200
|
-
const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
|
|
201
|
-
const rows = db.prepare(
|
|
202
|
-
`SELECT * FROM memories
|
|
203
|
-
WHERE ${archivedFilter}forgotten = 0 AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')
|
|
204
|
-
ORDER BY
|
|
205
|
-
CASE WHEN title LIKE ? ESCAPE '\\' THEN 0 ELSE 1 END,
|
|
206
|
-
importance DESC,
|
|
207
|
-
updated_at DESC,
|
|
208
|
-
id
|
|
209
|
-
LIMIT ?`
|
|
210
|
-
).all(like, like, like, like, lim);
|
|
211
|
-
return rows.map(toRow);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
return {
|
|
215
|
-
db,
|
|
216
|
-
count,
|
|
217
|
-
getById,
|
|
218
|
-
save,
|
|
219
|
-
update,
|
|
220
|
-
remove,
|
|
221
|
-
setForget,
|
|
222
|
-
setArchived,
|
|
223
|
-
list,
|
|
224
|
-
all,
|
|
225
|
-
search,
|
|
226
|
-
close() {
|
|
227
|
-
db.close();
|
|
228
|
-
}
|
|
229
|
-
};
|
|
230
|
-
}
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
|
|
4
|
+
const SCHEMA = `
|
|
5
|
+
CREATE TABLE IF NOT EXISTS memories (
|
|
6
|
+
id TEXT PRIMARY KEY,
|
|
7
|
+
type TEXT NOT NULL,
|
|
8
|
+
title TEXT NOT NULL,
|
|
9
|
+
content TEXT NOT NULL,
|
|
10
|
+
tags TEXT NOT NULL DEFAULT '[]',
|
|
11
|
+
importance INTEGER NOT NULL DEFAULT 3,
|
|
12
|
+
forgotten INTEGER NOT NULL DEFAULT 0,
|
|
13
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
14
|
+
source TEXT,
|
|
15
|
+
created_at TEXT NOT NULL,
|
|
16
|
+
updated_at TEXT NOT NULL
|
|
17
|
+
);
|
|
18
|
+
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
19
|
+
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
20
|
+
`;
|
|
21
|
+
|
|
22
|
+
const TYPES = new Set(["preference", "project", "decision", "history", "summary"]);
|
|
23
|
+
|
|
24
|
+
// Pure helpers: no shared module state.
|
|
25
|
+
|
|
26
|
+
function sanitizePage(limit, offset, defaultLimit) {
|
|
27
|
+
const lim = Number.isInteger(limit) && limit > 0 ? limit : defaultLimit;
|
|
28
|
+
const off = Number.isInteger(offset) && offset > 0 ? offset : 0;
|
|
29
|
+
return { limit: lim, offset: off };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function escapeLike(q) {
|
|
33
|
+
return q.replace(/[\\%_]/g, (c) => `\\${c}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function parseTags(raw) {
|
|
37
|
+
try {
|
|
38
|
+
const arr = JSON.parse(raw);
|
|
39
|
+
return Array.isArray(arr) ? arr : [];
|
|
40
|
+
} catch {
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function toRow(row) {
|
|
46
|
+
if (!row) return undefined;
|
|
47
|
+
return {
|
|
48
|
+
id: row.id,
|
|
49
|
+
type: row.type,
|
|
50
|
+
title: row.title,
|
|
51
|
+
content: row.content,
|
|
52
|
+
tags: parseTags(row.tags),
|
|
53
|
+
importance: row.importance,
|
|
54
|
+
forgotten: row.forgotten === 1,
|
|
55
|
+
archived: row.archived === 1,
|
|
56
|
+
source: row.source ?? undefined,
|
|
57
|
+
created_at: row.created_at,
|
|
58
|
+
updated_at: row.updated_at
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createStore(path) {
|
|
63
|
+
const db = new DatabaseSync(path);
|
|
64
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
65
|
+
db.exec(SCHEMA);
|
|
66
|
+
|
|
67
|
+
// Schema migration: add archived column to legacy databases (idempotent)
|
|
68
|
+
const columns = db.prepare("PRAGMA table_info(memories)").all().map((c) => c.name);
|
|
69
|
+
if (!columns.includes("archived")) {
|
|
70
|
+
db.exec("ALTER TABLE memories ADD COLUMN archived INTEGER NOT NULL DEFAULT 0");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Per-instance monotonic timestamp guard: consecutive writes within the same
|
|
74
|
+
// millisecond must still produce strictly increasing timestamps (test asserts
|
|
75
|
+
// updated_at != created_at). State lives in the store closure, not module scope.
|
|
76
|
+
let lastTs = "";
|
|
77
|
+
function nowIso() {
|
|
78
|
+
let ts = new Date().toISOString();
|
|
79
|
+
if (lastTs && ts <= lastTs) {
|
|
80
|
+
const d = new Date(lastTs);
|
|
81
|
+
d.setMilliseconds(d.getMilliseconds() + 1);
|
|
82
|
+
ts = d.toISOString();
|
|
83
|
+
}
|
|
84
|
+
lastTs = ts;
|
|
85
|
+
return ts;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function count(type, { includeForgotten = false, includeArchived = false } = {}) {
|
|
89
|
+
const clauses = [];
|
|
90
|
+
const params = [];
|
|
91
|
+
if (type !== undefined) {
|
|
92
|
+
clauses.push("type = ?");
|
|
93
|
+
params.push(type);
|
|
94
|
+
}
|
|
95
|
+
if (!includeForgotten) {
|
|
96
|
+
clauses.push("forgotten = 0");
|
|
97
|
+
}
|
|
98
|
+
if (!includeArchived) {
|
|
99
|
+
clauses.push("archived = 0");
|
|
100
|
+
}
|
|
101
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
102
|
+
return db.prepare(`SELECT count(*) AS c FROM memories ${where}`).get(...params).c;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getById(id) {
|
|
106
|
+
const row = db.prepare("SELECT * FROM memories WHERE id = ?").get(id);
|
|
107
|
+
return toRow(row);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function save(memory) {
|
|
111
|
+
const id = memory.id ?? randomUUID();
|
|
112
|
+
const type = memory.type;
|
|
113
|
+
if (!TYPES.has(type)) throw new Error(`invalid memory type: ${type}`);
|
|
114
|
+
if (memory.tags !== undefined && !Array.isArray(memory.tags)) {
|
|
115
|
+
throw new Error("tags must be an array");
|
|
116
|
+
}
|
|
117
|
+
const now = nowIso();
|
|
118
|
+
const tags = JSON.stringify(memory.tags ?? []);
|
|
119
|
+
const importance = Number.isInteger(memory.importance) ? memory.importance : 3;
|
|
120
|
+
db.prepare(
|
|
121
|
+
`INSERT INTO memories (id, type, title, content, tags, importance, forgotten, source, created_at, updated_at)
|
|
122
|
+
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?, ?)`
|
|
123
|
+
).run(id, type, memory.title, memory.content, tags, importance, memory.source ?? null, now, now);
|
|
124
|
+
return getById(id);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function update(id, patch) {
|
|
128
|
+
const existing = getById(id);
|
|
129
|
+
if (!existing) throw new Error(`memory not found: ${id}`);
|
|
130
|
+
const type = patch.type ?? existing.type;
|
|
131
|
+
if (!TYPES.has(type)) throw new Error(`invalid memory type: ${type}`);
|
|
132
|
+
if (patch.tags !== undefined && !Array.isArray(patch.tags)) {
|
|
133
|
+
throw new Error("tags must be an array");
|
|
134
|
+
}
|
|
135
|
+
const now = nowIso();
|
|
136
|
+
db.prepare(
|
|
137
|
+
`UPDATE memories SET type=?, title=?, content=?, tags=?, importance=?, source=?, updated_at=? WHERE id=?`
|
|
138
|
+
).run(
|
|
139
|
+
type,
|
|
140
|
+
patch.title ?? existing.title,
|
|
141
|
+
patch.content ?? existing.content,
|
|
142
|
+
JSON.stringify(patch.tags ?? existing.tags),
|
|
143
|
+
Number.isInteger(patch.importance) ? patch.importance : existing.importance,
|
|
144
|
+
patch.source !== undefined ? patch.source : (existing.source ?? null),
|
|
145
|
+
now,
|
|
146
|
+
id
|
|
147
|
+
);
|
|
148
|
+
return getById(id);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function remove(id) {
|
|
152
|
+
db.prepare("DELETE FROM memories WHERE id = ?").run(id);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function setForget(id, forgotten) {
|
|
156
|
+
db.prepare("UPDATE memories SET forgotten = ?, updated_at = ? WHERE id = ?")
|
|
157
|
+
.run(forgotten === true || forgotten === 1 ? 1 : 0, nowIso(), id);
|
|
158
|
+
return getById(id);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function setArchived(id, archived) {
|
|
162
|
+
db.prepare("UPDATE memories SET archived = ?, updated_at = ? WHERE id = ?")
|
|
163
|
+
.run(archived ? 1 : 0, nowIso(), id);
|
|
164
|
+
return getById(id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function list({ type, limit = 50, offset = 0, includeForgotten = false, includeArchived = false } = {}) {
|
|
168
|
+
const clauses = [];
|
|
169
|
+
const params = [];
|
|
170
|
+
if (type) {
|
|
171
|
+
clauses.push("type = ?");
|
|
172
|
+
params.push(type);
|
|
173
|
+
}
|
|
174
|
+
if (!includeForgotten) {
|
|
175
|
+
clauses.push("forgotten = 0");
|
|
176
|
+
}
|
|
177
|
+
if (!includeArchived) {
|
|
178
|
+
clauses.push("archived = 0");
|
|
179
|
+
}
|
|
180
|
+
const { limit: lim, offset: off } = sanitizePage(limit, offset, 50);
|
|
181
|
+
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
182
|
+
const rows = db.prepare(
|
|
183
|
+
`SELECT * FROM memories ${where} ORDER BY importance DESC, updated_at DESC, id LIMIT ? OFFSET ?`
|
|
184
|
+
).all(...params, lim, off);
|
|
185
|
+
return rows.map(toRow);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function all() {
|
|
189
|
+
const rows = db.prepare("SELECT * FROM memories ORDER BY updated_at DESC").all();
|
|
190
|
+
return rows.map(toRow);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function search(query, { limit = 20, includeArchived = false } = {}) {
|
|
194
|
+
const q = String(query).trim();
|
|
195
|
+
if (!q) return [];
|
|
196
|
+
// FTS5 over unicode61 (English + long phrases); LIKE fallback covers CJK substring.
|
|
197
|
+
// LIKE wildcards in the query are escaped so user input is matched literally.
|
|
198
|
+
const like = `%${escapeLike(q)}%`;
|
|
199
|
+
const { limit: lim } = sanitizePage(limit, 0, 20);
|
|
200
|
+
const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
|
|
201
|
+
const rows = db.prepare(
|
|
202
|
+
`SELECT * FROM memories
|
|
203
|
+
WHERE ${archivedFilter}forgotten = 0 AND (title LIKE ? ESCAPE '\\' OR content LIKE ? ESCAPE '\\' OR tags LIKE ? ESCAPE '\\')
|
|
204
|
+
ORDER BY
|
|
205
|
+
CASE WHEN title LIKE ? ESCAPE '\\' THEN 0 ELSE 1 END,
|
|
206
|
+
importance DESC,
|
|
207
|
+
updated_at DESC,
|
|
208
|
+
id
|
|
209
|
+
LIMIT ?`
|
|
210
|
+
).all(like, like, like, like, lim);
|
|
211
|
+
return rows.map(toRow);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
db,
|
|
216
|
+
count,
|
|
217
|
+
getById,
|
|
218
|
+
save,
|
|
219
|
+
update,
|
|
220
|
+
remove,
|
|
221
|
+
setForget,
|
|
222
|
+
setArchived,
|
|
223
|
+
list,
|
|
224
|
+
all,
|
|
225
|
+
search,
|
|
226
|
+
close() {
|
|
227
|
+
db.close();
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|