@geekbeer/minion 3.36.0 → 3.40.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/core/db/helpers.js +18 -0
- package/core/db/index.js +146 -0
- package/core/db/migrations/000_initial_schema.js +157 -0
- package/core/db/migrations/001_fts_trigram.js +78 -0
- package/core/db/migrations/002_emails_fts.js +41 -0
- package/core/db/migrations/003_memories_project_id.js +17 -0
- package/core/db/migrations/004_chat_sessions_workspace.js +18 -0
- package/core/db/migrations/005_todos_session_injection.js +19 -0
- package/core/db/migrations/006_daily_logs_workspace.js +69 -0
- package/core/db/migrations/007_workspace_scoping.js +29 -0
- package/core/db/migrations/008_todos_workspace.js +22 -0
- package/core/db/migrations/index.js +41 -0
- package/core/lib/config-warnings.js +16 -8
- package/core/lib/end-of-day.js +30 -14
- package/core/lib/reflection-scheduler.js +23 -9
- package/core/lib/thread-watcher.js +3 -0
- package/core/routes/daily-logs.js +64 -27
- package/core/routes/routines.js +6 -2
- package/core/routes/skills.js +4 -0
- package/core/routes/todos.js +20 -7
- package/core/routes/workflows.js +17 -7
- package/core/stores/daily-log-store.js +61 -30
- package/core/stores/execution-store.js +40 -18
- package/core/stores/routine-store.js +32 -14
- package/core/stores/todo-store.js +37 -10
- package/core/stores/workflow-store.js +34 -13
- package/docs/api-reference.md +66 -25
- package/linux/routes/chat.js +14 -9
- package/linux/routes/directives.js +4 -0
- package/linux/routine-runner.js +2 -0
- package/linux/workflow-runner.js +2 -0
- package/package.json +4 -2
- package/rules/core.md +1 -0
- package/scripts/new-migration.js +53 -0
- package/win/routes/chat.js +14 -9
- package/win/routes/directives.js +4 -0
- package/win/routine-runner.js +2 -0
- package/win/workflow-runner.js +2 -0
- package/core/db.js +0 -583
package/core/db.js
DELETED
|
@@ -1,583 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQLite Database Module
|
|
3
|
-
* Provides a singleton SQLite database connection for all minion stores.
|
|
4
|
-
* Uses better-sqlite3 (synchronous API) with WAL mode for performance.
|
|
5
|
-
* Falls back to Node.js built-in node:sqlite when better-sqlite3 is unavailable
|
|
6
|
-
* (e.g., no prebuilt binaries for the current Node.js version on Windows).
|
|
7
|
-
*
|
|
8
|
-
* Database file: $DATA_DIR/minion.db
|
|
9
|
-
*
|
|
10
|
-
* FTS5 uses the trigram tokenizer for Japanese/CJK language support.
|
|
11
|
-
* Trigram splits text into 3-character sequences, enabling substring search
|
|
12
|
-
* in any language without a language-specific tokenizer.
|
|
13
|
-
* Note: trigram requires search queries of 3+ characters.
|
|
14
|
-
* Shorter queries fall back to LIKE-based search in the store layer.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
const path = require('path')
|
|
18
|
-
const fs = require('fs')
|
|
19
|
-
|
|
20
|
-
const { DATA_DIR } = require('./lib/platform')
|
|
21
|
-
const { config } = require('./config')
|
|
22
|
-
|
|
23
|
-
let _db = null
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Resolve the database file path.
|
|
27
|
-
* Uses DATA_DIR if accessible, otherwise falls back to HOME_DIR.
|
|
28
|
-
*/
|
|
29
|
-
function resolveDbPath() {
|
|
30
|
-
try {
|
|
31
|
-
fs.accessSync(DATA_DIR, fs.constants.W_OK)
|
|
32
|
-
return path.join(DATA_DIR, 'minion.db')
|
|
33
|
-
} catch {
|
|
34
|
-
return path.join(config.HOME_DIR, 'minion.db')
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Open a database connection using better-sqlite3 or node:sqlite fallback.
|
|
40
|
-
* @param {string} dbPath
|
|
41
|
-
* @returns {import('better-sqlite3').Database}
|
|
42
|
-
*/
|
|
43
|
-
function openDatabase(dbPath) {
|
|
44
|
-
// Try better-sqlite3 first (fastest, has prebuilt binaries for most platforms)
|
|
45
|
-
try {
|
|
46
|
-
const Database = require('better-sqlite3')
|
|
47
|
-
const db = new Database(dbPath)
|
|
48
|
-
db.pragma('journal_mode = WAL')
|
|
49
|
-
db.pragma('foreign_keys = ON')
|
|
50
|
-
console.log('[DB] Using better-sqlite3')
|
|
51
|
-
return db
|
|
52
|
-
} catch (e) {
|
|
53
|
-
console.log(`[DB] better-sqlite3 unavailable (${e.message}), trying node:sqlite...`)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Fallback to Node.js built-in sqlite (available in Node 22.5.0+)
|
|
57
|
-
try {
|
|
58
|
-
const { DatabaseSync } = require('node:sqlite')
|
|
59
|
-
const db = new DatabaseSync(dbPath)
|
|
60
|
-
db.exec('PRAGMA journal_mode = WAL')
|
|
61
|
-
db.exec('PRAGMA foreign_keys = ON')
|
|
62
|
-
|
|
63
|
-
// Polyfill db.pragma() for compatibility with better-sqlite3 API
|
|
64
|
-
db.pragma = function (str) {
|
|
65
|
-
const stmt = db.prepare(`PRAGMA ${str}`)
|
|
66
|
-
return stmt.get()
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Polyfill db.transaction() for compatibility with better-sqlite3 API
|
|
70
|
-
db.transaction = function (fn) {
|
|
71
|
-
return function (...args) {
|
|
72
|
-
db.exec('BEGIN')
|
|
73
|
-
try {
|
|
74
|
-
const result = fn(...args)
|
|
75
|
-
db.exec('COMMIT')
|
|
76
|
-
return result
|
|
77
|
-
} catch (e) {
|
|
78
|
-
db.exec('ROLLBACK')
|
|
79
|
-
throw e
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
console.log('[DB] Using node:sqlite (built-in)')
|
|
85
|
-
return db
|
|
86
|
-
} catch (e) {
|
|
87
|
-
throw new Error(
|
|
88
|
-
`[DB] No SQLite driver available. Install better-sqlite3 or use Node.js 22.5.0+ for built-in sqlite support. ` +
|
|
89
|
-
`better-sqlite3 error: check that Python and build tools are installed, or use Node.js 22 LTS which has prebuilt binaries. ` +
|
|
90
|
-
`node:sqlite error: ${e.message}`
|
|
91
|
-
)
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* Get the singleton database connection.
|
|
97
|
-
* Initializes the database and schema on first call.
|
|
98
|
-
* @returns {import('better-sqlite3').Database}
|
|
99
|
-
*/
|
|
100
|
-
function getDb() {
|
|
101
|
-
if (_db) return _db
|
|
102
|
-
|
|
103
|
-
const dbPath = resolveDbPath()
|
|
104
|
-
fs.mkdirSync(path.dirname(dbPath), { recursive: true })
|
|
105
|
-
|
|
106
|
-
_db = openDatabase(dbPath)
|
|
107
|
-
|
|
108
|
-
initSchema(_db)
|
|
109
|
-
migrateSchema(_db)
|
|
110
|
-
|
|
111
|
-
console.log(`[DB] SQLite database initialized: ${dbPath}`)
|
|
112
|
-
return _db
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Initialize all tables, indices, FTS virtual tables, and triggers.
|
|
117
|
-
* All statements use IF NOT EXISTS for idempotency.
|
|
118
|
-
*/
|
|
119
|
-
function initSchema(db) {
|
|
120
|
-
db.exec(`
|
|
121
|
-
-- ==================== memories ====================
|
|
122
|
-
CREATE TABLE IF NOT EXISTS memories (
|
|
123
|
-
id TEXT PRIMARY KEY,
|
|
124
|
-
title TEXT NOT NULL DEFAULT 'Untitled',
|
|
125
|
-
category TEXT NOT NULL DEFAULT 'reference'
|
|
126
|
-
CHECK (category IN ('user', 'feedback', 'project', 'reference')),
|
|
127
|
-
content TEXT NOT NULL DEFAULT '',
|
|
128
|
-
created_at TEXT NOT NULL,
|
|
129
|
-
updated_at TEXT NOT NULL
|
|
130
|
-
);
|
|
131
|
-
|
|
132
|
-
CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
|
|
133
|
-
CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at DESC);
|
|
134
|
-
|
|
135
|
-
-- FTS5 with trigram tokenizer for Japanese/CJK support
|
|
136
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
|
|
137
|
-
title,
|
|
138
|
-
content,
|
|
139
|
-
content=memories,
|
|
140
|
-
content_rowid=rowid,
|
|
141
|
-
tokenize='trigram'
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
-- Triggers to keep FTS index in sync with memories table
|
|
145
|
-
CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
|
|
146
|
-
INSERT INTO memories_fts(rowid, title, content)
|
|
147
|
-
VALUES (new.rowid, new.title, new.content);
|
|
148
|
-
END;
|
|
149
|
-
|
|
150
|
-
CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
|
|
151
|
-
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
152
|
-
VALUES ('delete', old.rowid, old.title, old.content);
|
|
153
|
-
END;
|
|
154
|
-
|
|
155
|
-
CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
|
|
156
|
-
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
157
|
-
VALUES ('delete', old.rowid, old.title, old.content);
|
|
158
|
-
INSERT INTO memories_fts(rowid, title, content)
|
|
159
|
-
VALUES (new.rowid, new.title, new.content);
|
|
160
|
-
END;
|
|
161
|
-
|
|
162
|
-
-- ==================== daily_logs ====================
|
|
163
|
-
CREATE TABLE IF NOT EXISTS daily_logs (
|
|
164
|
-
date TEXT PRIMARY KEY,
|
|
165
|
-
content TEXT NOT NULL DEFAULT '',
|
|
166
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
167
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
-- FTS5 with trigram tokenizer for Japanese/CJK support
|
|
171
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS daily_logs_fts USING fts5(
|
|
172
|
-
content,
|
|
173
|
-
content=daily_logs,
|
|
174
|
-
content_rowid=rowid,
|
|
175
|
-
tokenize='trigram'
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
CREATE TRIGGER IF NOT EXISTS daily_logs_ai AFTER INSERT ON daily_logs BEGIN
|
|
179
|
-
INSERT INTO daily_logs_fts(rowid, content)
|
|
180
|
-
VALUES (new.rowid, new.content);
|
|
181
|
-
END;
|
|
182
|
-
|
|
183
|
-
CREATE TRIGGER IF NOT EXISTS daily_logs_ad AFTER DELETE ON daily_logs BEGIN
|
|
184
|
-
INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
|
|
185
|
-
VALUES ('delete', old.rowid, old.content);
|
|
186
|
-
END;
|
|
187
|
-
|
|
188
|
-
CREATE TRIGGER IF NOT EXISTS daily_logs_au AFTER UPDATE ON daily_logs BEGIN
|
|
189
|
-
INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
|
|
190
|
-
VALUES ('delete', old.rowid, old.content);
|
|
191
|
-
INSERT INTO daily_logs_fts(rowid, content)
|
|
192
|
-
VALUES (new.rowid, new.content);
|
|
193
|
-
END;
|
|
194
|
-
|
|
195
|
-
-- ==================== workspaces (cache from HQ) ====================
|
|
196
|
-
CREATE TABLE IF NOT EXISTS workspaces (
|
|
197
|
-
id TEXT PRIMARY KEY,
|
|
198
|
-
name TEXT NOT NULL,
|
|
199
|
-
slug TEXT NOT NULL,
|
|
200
|
-
updated_at INTEGER NOT NULL
|
|
201
|
-
);
|
|
202
|
-
|
|
203
|
-
-- ==================== chat_sessions ====================
|
|
204
|
-
CREATE TABLE IF NOT EXISTS chat_sessions (
|
|
205
|
-
session_id TEXT PRIMARY KEY,
|
|
206
|
-
workspace_id TEXT,
|
|
207
|
-
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
208
|
-
created_at INTEGER NOT NULL,
|
|
209
|
-
updated_at INTEGER NOT NULL
|
|
210
|
-
);
|
|
211
|
-
|
|
212
|
-
CREATE TABLE IF NOT EXISTS chat_messages (
|
|
213
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
214
|
-
session_id TEXT NOT NULL REFERENCES chat_sessions(session_id) ON DELETE CASCADE,
|
|
215
|
-
role TEXT NOT NULL,
|
|
216
|
-
content TEXT NOT NULL,
|
|
217
|
-
timestamp INTEGER NOT NULL
|
|
218
|
-
);
|
|
219
|
-
|
|
220
|
-
CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id, id);
|
|
221
|
-
|
|
222
|
-
-- ==================== executions ====================
|
|
223
|
-
CREATE TABLE IF NOT EXISTS executions (
|
|
224
|
-
id TEXT PRIMARY KEY,
|
|
225
|
-
skill_name TEXT,
|
|
226
|
-
workflow_id TEXT,
|
|
227
|
-
status TEXT,
|
|
228
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
229
|
-
data TEXT NOT NULL
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
CREATE INDEX IF NOT EXISTS idx_executions_workflow ON executions(workflow_id);
|
|
233
|
-
CREATE INDEX IF NOT EXISTS idx_executions_created ON executions(created_at DESC);
|
|
234
|
-
|
|
235
|
-
-- ==================== routines ====================
|
|
236
|
-
CREATE TABLE IF NOT EXISTS routines (
|
|
237
|
-
id TEXT PRIMARY KEY,
|
|
238
|
-
name TEXT NOT NULL,
|
|
239
|
-
data TEXT NOT NULL
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
CREATE INDEX IF NOT EXISTS idx_routines_name ON routines(name);
|
|
243
|
-
|
|
244
|
-
-- ==================== workflows ====================
|
|
245
|
-
CREATE TABLE IF NOT EXISTS workflows (
|
|
246
|
-
id TEXT PRIMARY KEY,
|
|
247
|
-
name TEXT NOT NULL,
|
|
248
|
-
data TEXT NOT NULL
|
|
249
|
-
);
|
|
250
|
-
|
|
251
|
-
CREATE INDEX IF NOT EXISTS idx_workflows_name ON workflows(name);
|
|
252
|
-
|
|
253
|
-
-- ==================== todos ====================
|
|
254
|
-
CREATE TABLE IF NOT EXISTS todos (
|
|
255
|
-
id TEXT PRIMARY KEY,
|
|
256
|
-
title TEXT NOT NULL,
|
|
257
|
-
description TEXT,
|
|
258
|
-
status TEXT NOT NULL DEFAULT 'pending'
|
|
259
|
-
CHECK (status IN ('pending', 'in_progress', 'done', 'cancelled')),
|
|
260
|
-
priority TEXT NOT NULL DEFAULT 'normal'
|
|
261
|
-
CHECK (priority IN ('low', 'normal', 'high', 'urgent')),
|
|
262
|
-
source_type TEXT
|
|
263
|
-
CHECK (source_type IS NULL OR source_type IN ('thread', 'workflow', 'directive', 'user', 'self')),
|
|
264
|
-
source_id TEXT,
|
|
265
|
-
project_id TEXT,
|
|
266
|
-
due_at TEXT,
|
|
267
|
-
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
268
|
-
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
269
|
-
completed_at TEXT,
|
|
270
|
-
data TEXT,
|
|
271
|
-
session_id TEXT,
|
|
272
|
-
injection_count INTEGER NOT NULL DEFAULT 0
|
|
273
|
-
);
|
|
274
|
-
|
|
275
|
-
CREATE INDEX IF NOT EXISTS idx_todos_status ON todos(status);
|
|
276
|
-
CREATE INDEX IF NOT EXISTS idx_todos_project ON todos(project_id);
|
|
277
|
-
CREATE INDEX IF NOT EXISTS idx_todos_priority ON todos(priority);
|
|
278
|
-
|
|
279
|
-
-- ==================== emails ====================
|
|
280
|
-
CREATE TABLE IF NOT EXISTS emails (
|
|
281
|
-
id TEXT PRIMARY KEY,
|
|
282
|
-
from_address TEXT NOT NULL,
|
|
283
|
-
to_address TEXT NOT NULL,
|
|
284
|
-
subject TEXT DEFAULT '',
|
|
285
|
-
body_text TEXT DEFAULT '',
|
|
286
|
-
body_html TEXT DEFAULT '',
|
|
287
|
-
received_at TEXT NOT NULL,
|
|
288
|
-
is_read INTEGER DEFAULT 0,
|
|
289
|
-
labels TEXT DEFAULT '[]'
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
CREATE INDEX IF NOT EXISTS idx_emails_received_at ON emails(received_at DESC);
|
|
293
|
-
CREATE INDEX IF NOT EXISTS idx_emails_from ON emails(from_address);
|
|
294
|
-
CREATE INDEX IF NOT EXISTS idx_emails_is_read ON emails(is_read);
|
|
295
|
-
|
|
296
|
-
-- ==================== email_attachments ====================
|
|
297
|
-
CREATE TABLE IF NOT EXISTS email_attachments (
|
|
298
|
-
id TEXT PRIMARY KEY,
|
|
299
|
-
email_id TEXT NOT NULL REFERENCES emails(id) ON DELETE CASCADE,
|
|
300
|
-
filename TEXT NOT NULL,
|
|
301
|
-
content_type TEXT,
|
|
302
|
-
size_bytes INTEGER DEFAULT 0,
|
|
303
|
-
approved INTEGER DEFAULT 0,
|
|
304
|
-
data BLOB
|
|
305
|
-
);
|
|
306
|
-
|
|
307
|
-
CREATE INDEX IF NOT EXISTS idx_email_attachments_email ON email_attachments(email_id);
|
|
308
|
-
|
|
309
|
-
-- ==================== schema_version ====================
|
|
310
|
-
CREATE TABLE IF NOT EXISTS schema_version (
|
|
311
|
-
version INTEGER PRIMARY KEY,
|
|
312
|
-
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
313
|
-
);
|
|
314
|
-
`)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Run schema migrations for existing databases.
|
|
319
|
-
* Each migration checks schema_version to avoid re-running.
|
|
320
|
-
*/
|
|
321
|
-
function migrateSchema(db) {
|
|
322
|
-
const currentVersion = db.prepare(
|
|
323
|
-
'SELECT COALESCE(MAX(version), 0) as v FROM schema_version'
|
|
324
|
-
).get().v
|
|
325
|
-
|
|
326
|
-
if (currentVersion < 1) {
|
|
327
|
-
// Migration 1: Recreate FTS tables with trigram tokenizer for Japanese support.
|
|
328
|
-
// The default unicode61 tokenizer doesn't tokenize CJK text correctly.
|
|
329
|
-
console.log('[DB] Migration 1: Switching FTS5 to trigram tokenizer...')
|
|
330
|
-
|
|
331
|
-
db.exec(`
|
|
332
|
-
-- Drop old FTS tables and triggers (may use unicode61 tokenizer)
|
|
333
|
-
DROP TRIGGER IF EXISTS memories_ai;
|
|
334
|
-
DROP TRIGGER IF EXISTS memories_ad;
|
|
335
|
-
DROP TRIGGER IF EXISTS memories_au;
|
|
336
|
-
DROP TABLE IF EXISTS memories_fts;
|
|
337
|
-
|
|
338
|
-
DROP TRIGGER IF EXISTS daily_logs_ai;
|
|
339
|
-
DROP TRIGGER IF EXISTS daily_logs_ad;
|
|
340
|
-
DROP TRIGGER IF EXISTS daily_logs_au;
|
|
341
|
-
DROP TABLE IF EXISTS daily_logs_fts;
|
|
342
|
-
|
|
343
|
-
-- Recreate with trigram tokenizer
|
|
344
|
-
CREATE VIRTUAL TABLE memories_fts USING fts5(
|
|
345
|
-
title,
|
|
346
|
-
content,
|
|
347
|
-
content=memories,
|
|
348
|
-
content_rowid=rowid,
|
|
349
|
-
tokenize='trigram'
|
|
350
|
-
);
|
|
351
|
-
|
|
352
|
-
CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
|
|
353
|
-
INSERT INTO memories_fts(rowid, title, content)
|
|
354
|
-
VALUES (new.rowid, new.title, new.content);
|
|
355
|
-
END;
|
|
356
|
-
|
|
357
|
-
CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN
|
|
358
|
-
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
359
|
-
VALUES ('delete', old.rowid, old.title, old.content);
|
|
360
|
-
END;
|
|
361
|
-
|
|
362
|
-
CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
|
|
363
|
-
INSERT INTO memories_fts(memories_fts, rowid, title, content)
|
|
364
|
-
VALUES ('delete', old.rowid, old.title, old.content);
|
|
365
|
-
INSERT INTO memories_fts(rowid, title, content)
|
|
366
|
-
VALUES (new.rowid, new.title, new.content);
|
|
367
|
-
END;
|
|
368
|
-
|
|
369
|
-
CREATE VIRTUAL TABLE daily_logs_fts USING fts5(
|
|
370
|
-
content,
|
|
371
|
-
content=daily_logs,
|
|
372
|
-
content_rowid=rowid,
|
|
373
|
-
tokenize='trigram'
|
|
374
|
-
);
|
|
375
|
-
|
|
376
|
-
CREATE TRIGGER daily_logs_ai AFTER INSERT ON daily_logs BEGIN
|
|
377
|
-
INSERT INTO daily_logs_fts(rowid, content)
|
|
378
|
-
VALUES (new.rowid, new.content);
|
|
379
|
-
END;
|
|
380
|
-
|
|
381
|
-
CREATE TRIGGER daily_logs_ad AFTER DELETE ON daily_logs BEGIN
|
|
382
|
-
INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
|
|
383
|
-
VALUES ('delete', old.rowid, old.content);
|
|
384
|
-
END;
|
|
385
|
-
|
|
386
|
-
CREATE TRIGGER daily_logs_au AFTER UPDATE ON daily_logs BEGIN
|
|
387
|
-
INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
|
|
388
|
-
VALUES ('delete', old.rowid, old.content);
|
|
389
|
-
INSERT INTO daily_logs_fts(rowid, content)
|
|
390
|
-
VALUES (new.rowid, new.content);
|
|
391
|
-
END;
|
|
392
|
-
|
|
393
|
-
-- Rebuild FTS index from existing data
|
|
394
|
-
INSERT INTO memories_fts(memories_fts) VALUES ('rebuild');
|
|
395
|
-
INSERT INTO daily_logs_fts(daily_logs_fts) VALUES ('rebuild');
|
|
396
|
-
|
|
397
|
-
INSERT INTO schema_version (version) VALUES (1);
|
|
398
|
-
`)
|
|
399
|
-
|
|
400
|
-
console.log('[DB] Migration 1 complete: FTS5 trigram tokenizer applied')
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
if (currentVersion < 2) {
|
|
404
|
-
try {
|
|
405
|
-
console.log('[DB] Migration 2: Adding emails FTS5...')
|
|
406
|
-
|
|
407
|
-
db.exec(`
|
|
408
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS emails_fts USING fts5(
|
|
409
|
-
subject,
|
|
410
|
-
body_text,
|
|
411
|
-
content=emails,
|
|
412
|
-
content_rowid=rowid,
|
|
413
|
-
tokenize='trigram'
|
|
414
|
-
);
|
|
415
|
-
|
|
416
|
-
CREATE TRIGGER IF NOT EXISTS emails_ai AFTER INSERT ON emails BEGIN
|
|
417
|
-
INSERT INTO emails_fts(rowid, subject, body_text)
|
|
418
|
-
VALUES (new.rowid, new.subject, new.body_text);
|
|
419
|
-
END;
|
|
420
|
-
|
|
421
|
-
CREATE TRIGGER IF NOT EXISTS emails_ad AFTER DELETE ON emails BEGIN
|
|
422
|
-
INSERT INTO emails_fts(emails_fts, rowid, subject, body_text)
|
|
423
|
-
VALUES ('delete', old.rowid, old.subject, old.body_text);
|
|
424
|
-
END;
|
|
425
|
-
|
|
426
|
-
CREATE TRIGGER IF NOT EXISTS emails_au AFTER UPDATE ON emails BEGIN
|
|
427
|
-
INSERT INTO emails_fts(emails_fts, rowid, subject, body_text)
|
|
428
|
-
VALUES ('delete', old.rowid, old.subject, old.body_text);
|
|
429
|
-
INSERT INTO emails_fts(rowid, subject, body_text)
|
|
430
|
-
VALUES (new.rowid, new.subject, new.body_text);
|
|
431
|
-
END;
|
|
432
|
-
|
|
433
|
-
INSERT INTO emails_fts(emails_fts) VALUES ('rebuild');
|
|
434
|
-
|
|
435
|
-
INSERT INTO schema_version (version) VALUES (2);
|
|
436
|
-
`)
|
|
437
|
-
|
|
438
|
-
console.log('[DB] Migration 2 complete: emails FTS5 added')
|
|
439
|
-
} catch (err) {
|
|
440
|
-
console.warn(`[DB] Migration 2 skipped (emails table may not exist yet): ${err.message}`)
|
|
441
|
-
// Mark as applied so it doesn't block subsequent migrations.
|
|
442
|
-
// emails FTS will be created by initSchema on next fresh DB.
|
|
443
|
-
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (2)") } catch {}
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
if (currentVersion < 3) {
|
|
448
|
-
try {
|
|
449
|
-
console.log('[DB] Migration 3: Adding project_id to memories...')
|
|
450
|
-
|
|
451
|
-
db.exec(`
|
|
452
|
-
ALTER TABLE memories ADD COLUMN project_id TEXT DEFAULT NULL;
|
|
453
|
-
|
|
454
|
-
CREATE INDEX IF NOT EXISTS idx_memories_project_id ON memories(project_id);
|
|
455
|
-
|
|
456
|
-
INSERT INTO schema_version (version) VALUES (3);
|
|
457
|
-
`)
|
|
458
|
-
|
|
459
|
-
console.log('[DB] Migration 3 complete: memories.project_id added')
|
|
460
|
-
} catch (err) {
|
|
461
|
-
if (hasColumn(db, 'memories', 'project_id')) {
|
|
462
|
-
console.warn(`[DB] Migration 3 skipped (column already exists): ${err.message}`)
|
|
463
|
-
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (3)") } catch {}
|
|
464
|
-
} else {
|
|
465
|
-
console.error(`[DB] Migration 3 FAILED — memories.project_id not added: ${err.message}`)
|
|
466
|
-
}
|
|
467
|
-
}
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (currentVersion < 4) {
|
|
471
|
-
try {
|
|
472
|
-
console.log('[DB] Migration 4: Adding workspace_id to chat_sessions...')
|
|
473
|
-
|
|
474
|
-
db.exec(`
|
|
475
|
-
ALTER TABLE chat_sessions ADD COLUMN workspace_id TEXT DEFAULT NULL;
|
|
476
|
-
|
|
477
|
-
CREATE INDEX IF NOT EXISTS idx_chat_sessions_workspace
|
|
478
|
-
ON chat_sessions(workspace_id, updated_at DESC);
|
|
479
|
-
|
|
480
|
-
INSERT INTO schema_version (version) VALUES (4);
|
|
481
|
-
`)
|
|
482
|
-
|
|
483
|
-
console.log('[DB] Migration 4 complete: chat_sessions.workspace_id added')
|
|
484
|
-
} catch (err) {
|
|
485
|
-
if (hasColumn(db, 'chat_sessions', 'workspace_id')) {
|
|
486
|
-
console.warn(`[DB] Migration 4 skipped (column already exists): ${err.message}`)
|
|
487
|
-
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (4)") } catch {}
|
|
488
|
-
} else {
|
|
489
|
-
console.error(`[DB] Migration 4 FAILED — chat_sessions.workspace_id not added: ${err.message}`)
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
if (currentVersion < 5) {
|
|
495
|
-
try {
|
|
496
|
-
console.log('[DB] Migration 5: Adding session_id / injection_count to todos...')
|
|
497
|
-
|
|
498
|
-
db.exec(`
|
|
499
|
-
ALTER TABLE todos ADD COLUMN session_id TEXT;
|
|
500
|
-
ALTER TABLE todos ADD COLUMN injection_count INTEGER NOT NULL DEFAULT 0;
|
|
501
|
-
|
|
502
|
-
CREATE INDEX IF NOT EXISTS idx_todos_session ON todos(session_id);
|
|
503
|
-
|
|
504
|
-
INSERT INTO schema_version (version) VALUES (5);
|
|
505
|
-
`)
|
|
506
|
-
|
|
507
|
-
console.log('[DB] Migration 5 complete: todos.session_id / injection_count added')
|
|
508
|
-
} catch (err) {
|
|
509
|
-
if (hasColumn(db, 'todos', 'session_id') && hasColumn(db, 'todos', 'injection_count')) {
|
|
510
|
-
console.warn(`[DB] Migration 5 skipped (columns already exist): ${err.message}`)
|
|
511
|
-
try { db.exec("INSERT OR IGNORE INTO schema_version (version) VALUES (5)") } catch {}
|
|
512
|
-
} else {
|
|
513
|
-
console.error(`[DB] Migration 5 FAILED — todos columns not added: ${err.message}`)
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
// Repair step: fix DBs where migrations were incorrectly marked as done
|
|
519
|
-
// but columns were never actually added (caused by the old catch-all error handling)
|
|
520
|
-
repairMissingColumns(db)
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
/**
|
|
524
|
-
* Check if a table has a specific column.
|
|
525
|
-
* @param {import('better-sqlite3').Database} db
|
|
526
|
-
* @param {string} table
|
|
527
|
-
* @param {string} column
|
|
528
|
-
* @returns {boolean}
|
|
529
|
-
*/
|
|
530
|
-
function hasColumn(db, table, column) {
|
|
531
|
-
const cols = db.prepare(`PRAGMA table_info(${table})`).all()
|
|
532
|
-
return cols.some(c => c.name === column)
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
/**
|
|
536
|
-
* Repair missing columns that should have been added by migrations.
|
|
537
|
-
* Handles DBs where migrations were marked as done but ALTER TABLE actually failed.
|
|
538
|
-
*/
|
|
539
|
-
function repairMissingColumns(db) {
|
|
540
|
-
const repairs = [
|
|
541
|
-
{ table: 'memories', column: 'project_id', sql: 'ALTER TABLE memories ADD COLUMN project_id TEXT DEFAULT NULL' },
|
|
542
|
-
{ table: 'chat_sessions', column: 'workspace_id', sql: 'ALTER TABLE chat_sessions ADD COLUMN workspace_id TEXT DEFAULT NULL' },
|
|
543
|
-
{ table: 'todos', column: 'session_id', sql: 'ALTER TABLE todos ADD COLUMN session_id TEXT' },
|
|
544
|
-
{ table: 'todos', column: 'injection_count', sql: 'ALTER TABLE todos ADD COLUMN injection_count INTEGER NOT NULL DEFAULT 0' },
|
|
545
|
-
]
|
|
546
|
-
|
|
547
|
-
for (const { table, column, sql } of repairs) {
|
|
548
|
-
if (!hasColumn(db, table, column)) {
|
|
549
|
-
try {
|
|
550
|
-
console.warn(`[DB] Repair: adding missing column ${table}.${column}`)
|
|
551
|
-
db.exec(sql)
|
|
552
|
-
console.log(`[DB] Repair: ${table}.${column} added successfully`)
|
|
553
|
-
} catch (err) {
|
|
554
|
-
console.error(`[DB] Repair FAILED for ${table}.${column}: ${err.message}`)
|
|
555
|
-
}
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
// Ensure indexes exist after repair
|
|
560
|
-
try {
|
|
561
|
-
db.exec(`
|
|
562
|
-
CREATE INDEX IF NOT EXISTS idx_memories_project_id ON memories(project_id);
|
|
563
|
-
CREATE INDEX IF NOT EXISTS idx_chat_sessions_workspace ON chat_sessions(workspace_id, updated_at DESC);
|
|
564
|
-
CREATE INDEX IF NOT EXISTS idx_todos_session ON todos(session_id);
|
|
565
|
-
`)
|
|
566
|
-
} catch (err) {
|
|
567
|
-
console.warn(`[DB] Repair index creation skipped: ${err.message}`)
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
/**
|
|
572
|
-
* Close the database connection.
|
|
573
|
-
* Call this during graceful shutdown to flush WAL.
|
|
574
|
-
*/
|
|
575
|
-
function closeDb() {
|
|
576
|
-
if (_db) {
|
|
577
|
-
_db.close()
|
|
578
|
-
_db = null
|
|
579
|
-
console.log('[DB] SQLite database closed')
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
module.exports = { getDb, closeDb }
|