@geekbeer/minion 2.70.2 → 2.73.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/core/db.js ADDED
@@ -0,0 +1,297 @@
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
+ *
6
+ * Database file: $DATA_DIR/minion.db
7
+ *
8
+ * FTS5 uses the trigram tokenizer for Japanese/CJK language support.
9
+ * Trigram splits text into 3-character sequences, enabling substring search
10
+ * in any language without a language-specific tokenizer.
11
+ * Note: trigram requires search queries of 3+ characters.
12
+ * Shorter queries fall back to LIKE-based search in the store layer.
13
+ */
14
+
15
+ const path = require('path')
16
+ const fs = require('fs')
17
+
18
+ const { DATA_DIR } = require('./lib/platform')
19
+ const { config } = require('./config')
20
+
21
+ let _db = null
22
+
23
+ /**
24
+ * Resolve the database file path.
25
+ * Uses DATA_DIR if accessible, otherwise falls back to HOME_DIR.
26
+ */
27
+ function resolveDbPath() {
28
+ try {
29
+ fs.accessSync(DATA_DIR, fs.constants.W_OK)
30
+ return path.join(DATA_DIR, 'minion.db')
31
+ } catch {
32
+ return path.join(config.HOME_DIR, 'minion.db')
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Get the singleton database connection.
38
+ * Initializes the database and schema on first call.
39
+ * @returns {import('better-sqlite3').Database}
40
+ */
41
+ function getDb() {
42
+ if (_db) return _db
43
+
44
+ const dbPath = resolveDbPath()
45
+ fs.mkdirSync(path.dirname(dbPath), { recursive: true })
46
+
47
+ const Database = require('better-sqlite3')
48
+ _db = new Database(dbPath)
49
+
50
+ // Performance & integrity settings
51
+ _db.pragma('journal_mode = WAL')
52
+ _db.pragma('foreign_keys = ON')
53
+
54
+ initSchema(_db)
55
+ migrateSchema(_db)
56
+
57
+ console.log(`[DB] SQLite database initialized: ${dbPath}`)
58
+ return _db
59
+ }
60
+
61
+ /**
62
+ * Initialize all tables, indices, FTS virtual tables, and triggers.
63
+ * All statements use IF NOT EXISTS for idempotency.
64
+ */
65
+ function initSchema(db) {
66
+ db.exec(`
67
+ -- ==================== memories ====================
68
+ CREATE TABLE IF NOT EXISTS memories (
69
+ id TEXT PRIMARY KEY,
70
+ title TEXT NOT NULL DEFAULT 'Untitled',
71
+ category TEXT NOT NULL DEFAULT 'reference'
72
+ CHECK (category IN ('user', 'feedback', 'project', 'reference')),
73
+ content TEXT NOT NULL DEFAULT '',
74
+ created_at TEXT NOT NULL,
75
+ updated_at TEXT NOT NULL
76
+ );
77
+
78
+ CREATE INDEX IF NOT EXISTS idx_memories_category ON memories(category);
79
+ CREATE INDEX IF NOT EXISTS idx_memories_updated_at ON memories(updated_at DESC);
80
+
81
+ -- FTS5 with trigram tokenizer for Japanese/CJK support
82
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
83
+ title,
84
+ content,
85
+ content=memories,
86
+ content_rowid=rowid,
87
+ tokenize='trigram'
88
+ );
89
+
90
+ -- Triggers to keep FTS index in sync with memories table
91
+ CREATE TRIGGER IF NOT EXISTS memories_ai AFTER INSERT ON memories BEGIN
92
+ INSERT INTO memories_fts(rowid, title, content)
93
+ VALUES (new.rowid, new.title, new.content);
94
+ END;
95
+
96
+ CREATE TRIGGER IF NOT EXISTS memories_ad AFTER DELETE ON memories BEGIN
97
+ INSERT INTO memories_fts(memories_fts, rowid, title, content)
98
+ VALUES ('delete', old.rowid, old.title, old.content);
99
+ END;
100
+
101
+ CREATE TRIGGER IF NOT EXISTS memories_au AFTER UPDATE ON memories BEGIN
102
+ INSERT INTO memories_fts(memories_fts, rowid, title, content)
103
+ VALUES ('delete', old.rowid, old.title, old.content);
104
+ INSERT INTO memories_fts(rowid, title, content)
105
+ VALUES (new.rowid, new.title, new.content);
106
+ END;
107
+
108
+ -- ==================== daily_logs ====================
109
+ CREATE TABLE IF NOT EXISTS daily_logs (
110
+ date TEXT PRIMARY KEY,
111
+ content TEXT NOT NULL DEFAULT '',
112
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
113
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
114
+ );
115
+
116
+ -- FTS5 with trigram tokenizer for Japanese/CJK support
117
+ CREATE VIRTUAL TABLE IF NOT EXISTS daily_logs_fts USING fts5(
118
+ content,
119
+ content=daily_logs,
120
+ content_rowid=rowid,
121
+ tokenize='trigram'
122
+ );
123
+
124
+ CREATE TRIGGER IF NOT EXISTS daily_logs_ai AFTER INSERT ON daily_logs BEGIN
125
+ INSERT INTO daily_logs_fts(rowid, content)
126
+ VALUES (new.rowid, new.content);
127
+ END;
128
+
129
+ CREATE TRIGGER IF NOT EXISTS daily_logs_ad AFTER DELETE ON daily_logs BEGIN
130
+ INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
131
+ VALUES ('delete', old.rowid, old.content);
132
+ END;
133
+
134
+ CREATE TRIGGER IF NOT EXISTS daily_logs_au AFTER UPDATE ON daily_logs BEGIN
135
+ INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
136
+ VALUES ('delete', old.rowid, old.content);
137
+ INSERT INTO daily_logs_fts(rowid, content)
138
+ VALUES (new.rowid, new.content);
139
+ END;
140
+
141
+ -- ==================== chat_sessions ====================
142
+ CREATE TABLE IF NOT EXISTS chat_sessions (
143
+ session_id TEXT PRIMARY KEY,
144
+ turn_count INTEGER NOT NULL DEFAULT 0,
145
+ created_at INTEGER NOT NULL,
146
+ updated_at INTEGER NOT NULL
147
+ );
148
+
149
+ CREATE TABLE IF NOT EXISTS chat_messages (
150
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
151
+ session_id TEXT NOT NULL REFERENCES chat_sessions(session_id) ON DELETE CASCADE,
152
+ role TEXT NOT NULL,
153
+ content TEXT NOT NULL,
154
+ timestamp INTEGER NOT NULL
155
+ );
156
+
157
+ CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id, id);
158
+
159
+ -- ==================== executions ====================
160
+ CREATE TABLE IF NOT EXISTS executions (
161
+ id TEXT PRIMARY KEY,
162
+ skill_name TEXT,
163
+ workflow_id TEXT,
164
+ status TEXT,
165
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
166
+ data TEXT NOT NULL
167
+ );
168
+
169
+ CREATE INDEX IF NOT EXISTS idx_executions_workflow ON executions(workflow_id);
170
+ CREATE INDEX IF NOT EXISTS idx_executions_created ON executions(created_at DESC);
171
+
172
+ -- ==================== routines ====================
173
+ CREATE TABLE IF NOT EXISTS routines (
174
+ id TEXT PRIMARY KEY,
175
+ name TEXT NOT NULL,
176
+ data TEXT NOT NULL
177
+ );
178
+
179
+ CREATE INDEX IF NOT EXISTS idx_routines_name ON routines(name);
180
+
181
+ -- ==================== workflows ====================
182
+ CREATE TABLE IF NOT EXISTS workflows (
183
+ id TEXT PRIMARY KEY,
184
+ name TEXT NOT NULL,
185
+ data TEXT NOT NULL
186
+ );
187
+
188
+ CREATE INDEX IF NOT EXISTS idx_workflows_name ON workflows(name);
189
+
190
+ -- ==================== schema_version ====================
191
+ CREATE TABLE IF NOT EXISTS schema_version (
192
+ version INTEGER PRIMARY KEY,
193
+ applied_at TEXT NOT NULL DEFAULT (datetime('now'))
194
+ );
195
+ `)
196
+ }
197
+
198
+ /**
199
+ * Run schema migrations for existing databases.
200
+ * Each migration checks schema_version to avoid re-running.
201
+ */
202
+ function migrateSchema(db) {
203
+ const currentVersion = db.prepare(
204
+ 'SELECT COALESCE(MAX(version), 0) as v FROM schema_version'
205
+ ).get().v
206
+
207
+ if (currentVersion < 1) {
208
+ // Migration 1: Recreate FTS tables with trigram tokenizer for Japanese support.
209
+ // The default unicode61 tokenizer doesn't tokenize CJK text correctly.
210
+ console.log('[DB] Migration 1: Switching FTS5 to trigram tokenizer...')
211
+
212
+ db.exec(`
213
+ -- Drop old FTS tables and triggers (may use unicode61 tokenizer)
214
+ DROP TRIGGER IF EXISTS memories_ai;
215
+ DROP TRIGGER IF EXISTS memories_ad;
216
+ DROP TRIGGER IF EXISTS memories_au;
217
+ DROP TABLE IF EXISTS memories_fts;
218
+
219
+ DROP TRIGGER IF EXISTS daily_logs_ai;
220
+ DROP TRIGGER IF EXISTS daily_logs_ad;
221
+ DROP TRIGGER IF EXISTS daily_logs_au;
222
+ DROP TABLE IF EXISTS daily_logs_fts;
223
+
224
+ -- Recreate with trigram tokenizer
225
+ CREATE VIRTUAL TABLE memories_fts USING fts5(
226
+ title,
227
+ content,
228
+ content=memories,
229
+ content_rowid=rowid,
230
+ tokenize='trigram'
231
+ );
232
+
233
+ CREATE TRIGGER memories_ai AFTER INSERT ON memories BEGIN
234
+ INSERT INTO memories_fts(rowid, title, content)
235
+ VALUES (new.rowid, new.title, new.content);
236
+ END;
237
+
238
+ CREATE TRIGGER memories_ad AFTER DELETE ON memories BEGIN
239
+ INSERT INTO memories_fts(memories_fts, rowid, title, content)
240
+ VALUES ('delete', old.rowid, old.title, old.content);
241
+ END;
242
+
243
+ CREATE TRIGGER memories_au AFTER UPDATE ON memories BEGIN
244
+ INSERT INTO memories_fts(memories_fts, rowid, title, content)
245
+ VALUES ('delete', old.rowid, old.title, old.content);
246
+ INSERT INTO memories_fts(rowid, title, content)
247
+ VALUES (new.rowid, new.title, new.content);
248
+ END;
249
+
250
+ CREATE VIRTUAL TABLE daily_logs_fts USING fts5(
251
+ content,
252
+ content=daily_logs,
253
+ content_rowid=rowid,
254
+ tokenize='trigram'
255
+ );
256
+
257
+ CREATE TRIGGER daily_logs_ai AFTER INSERT ON daily_logs BEGIN
258
+ INSERT INTO daily_logs_fts(rowid, content)
259
+ VALUES (new.rowid, new.content);
260
+ END;
261
+
262
+ CREATE TRIGGER daily_logs_ad AFTER DELETE ON daily_logs BEGIN
263
+ INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
264
+ VALUES ('delete', old.rowid, old.content);
265
+ END;
266
+
267
+ CREATE TRIGGER daily_logs_au AFTER UPDATE ON daily_logs BEGIN
268
+ INSERT INTO daily_logs_fts(daily_logs_fts, rowid, content)
269
+ VALUES ('delete', old.rowid, old.content);
270
+ INSERT INTO daily_logs_fts(rowid, content)
271
+ VALUES (new.rowid, new.content);
272
+ END;
273
+
274
+ -- Rebuild FTS index from existing data
275
+ INSERT INTO memories_fts(memories_fts) VALUES ('rebuild');
276
+ INSERT INTO daily_logs_fts(daily_logs_fts) VALUES ('rebuild');
277
+
278
+ INSERT INTO schema_version (version) VALUES (1);
279
+ `)
280
+
281
+ console.log('[DB] Migration 1 complete: FTS5 trigram tokenizer applied')
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Close the database connection.
287
+ * Call this during graceful shutdown to flush WAL.
288
+ */
289
+ function closeDb() {
290
+ if (_db) {
291
+ _db.close()
292
+ _db = null
293
+ console.log('[DB] SQLite database closed')
294
+ }
295
+ }
296
+
297
+ module.exports = { getDb, closeDb }
@@ -194,8 +194,8 @@ async function evaluateWithLlm(threadSummary, threadDetail, allMessages, newMess
194
194
  const messageHistory = recentMessages
195
195
  .map(m => {
196
196
  const sender = m.sender_minion_id
197
- ? (m.sender_minion_id === config.MINION_ID ? 'You' : `Minion(${m.sender_minion_id.slice(0, 8)})`)
198
- : 'User'
197
+ ? (m.sender_minion_id === config.MINION_ID ? 'You' : (m.sender_name || `Minion(${m.sender_minion_id.slice(0, 8)})`))
198
+ : (m.sender_name || 'User')
199
199
  return `[${sender}] ${m.content}`
200
200
  })
201
201
  .join('\n')
@@ -238,10 +238,7 @@ ${messageHistory || '(メッセージなし)'}
238
238
  - 自分のロール(${myProject.role})に関連する話題か
239
239
  - 自分が貢献できる知見や意見があるか
240
240
  - 既に十分な回答がある場合は重複を避ける
241
- - 人間に聞くべき場合は @user メンションを含めて返信する
242
-
243
- フォーマットルール:
244
- - 他のミニオンに言及する場合は必ず Minion(IDの先頭8文字) の形式を使うこと(例: メッセージ履歴中の Minion(abc12345) をそのまま使用)`
241
+ - 人間に聞くべき場合は @user メンションを含めて返信する`
245
242
 
246
243
  try {
247
244
  const result = await llmCallFn(prompt)
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Daily log endpoints
3
3
  *
4
- * Provides CRUD access to daily conversation summaries.
5
- * Logs are stored as markdown files in $DATA_DIR/daily-logs/YYYY-MM-DD.md.
4
+ * Provides CRUD + search for daily conversation summaries.
5
+ * Logs are stored in SQLite with FTS5 full-text search.
6
6
  *
7
7
  * Endpoints:
8
- * GET /api/daily-logs - List all logs (date + size)
8
+ * GET /api/daily-logs - List all logs (or search with ?search=keyword)
9
9
  * POST /api/daily-logs - Create a new daily log
10
10
  * GET /api/daily-logs/:date - Get a specific day's log
11
11
  * PUT /api/daily-logs/:date - Update a daily log
@@ -21,14 +21,20 @@ const dailyLogStore = require('../stores/daily-log-store')
21
21
  */
22
22
  async function dailyLogRoutes(fastify) {
23
23
 
24
- // GET /api/daily-logs - List all logs
24
+ // GET /api/daily-logs - List or search logs
25
25
  fastify.get('/api/daily-logs', async (request, reply) => {
26
26
  if (!verifyToken(request)) {
27
27
  reply.code(401)
28
28
  return { success: false, error: 'Unauthorized' }
29
29
  }
30
30
 
31
- const logs = await dailyLogStore.listLogs()
31
+ const { search } = request.query || {}
32
+ if (search) {
33
+ const logs = dailyLogStore.searchLogs(search)
34
+ return { success: true, logs }
35
+ }
36
+
37
+ const logs = dailyLogStore.listLogs()
32
38
  return { success: true, logs }
33
39
  })
34
40
 
@@ -46,13 +52,13 @@ async function dailyLogRoutes(fastify) {
46
52
  }
47
53
 
48
54
  // Check if log already exists
49
- const existing = await dailyLogStore.loadLog(date)
55
+ const existing = dailyLogStore.loadLog(date)
50
56
  if (existing !== null) {
51
57
  reply.code(409)
52
58
  return { success: false, error: 'Log already exists for this date. Use PUT to update.' }
53
59
  }
54
60
 
55
- await dailyLogStore.saveLog(date, content)
61
+ dailyLogStore.saveLog(date, content)
56
62
  console.log(`[DailyLogs] Created log: ${date}`)
57
63
  return { success: true, log: { date, content } }
58
64
  })
@@ -65,7 +71,7 @@ async function dailyLogRoutes(fastify) {
65
71
  }
66
72
 
67
73
  const { date } = request.params
68
- const content = await dailyLogStore.loadLog(date)
74
+ const content = dailyLogStore.loadLog(date)
69
75
  if (content === null) {
70
76
  reply.code(404)
71
77
  return { success: false, error: 'Log not found' }
@@ -89,13 +95,13 @@ async function dailyLogRoutes(fastify) {
89
95
  }
90
96
 
91
97
  // Check if log exists
92
- const existing = await dailyLogStore.loadLog(date)
98
+ const existing = dailyLogStore.loadLog(date)
93
99
  if (existing === null) {
94
100
  reply.code(404)
95
101
  return { success: false, error: 'Log not found' }
96
102
  }
97
103
 
98
- await dailyLogStore.saveLog(date, content)
104
+ dailyLogStore.saveLog(date, content)
99
105
  console.log(`[DailyLogs] Updated log: ${date}`)
100
106
  return { success: true, log: { date, content } }
101
107
  })
@@ -108,7 +114,7 @@ async function dailyLogRoutes(fastify) {
108
114
  }
109
115
 
110
116
  const { date } = request.params
111
- const deleted = await dailyLogStore.deleteLog(date)
117
+ const deleted = dailyLogStore.deleteLog(date)
112
118
  if (!deleted) {
113
119
  reply.code(404)
114
120
  return { success: false, error: 'Log not found' }
@@ -1,11 +1,11 @@
1
1
  /**
2
2
  * Memory management endpoints
3
3
  *
4
- * Provides CRUD for the minion's long-term memory entries.
5
- * Memory is stored as markdown files with YAML frontmatter in $DATA_DIR/memory/.
4
+ * Provides CRUD + search for the minion's long-term memory entries.
5
+ * Memory is stored in SQLite with FTS5 full-text search.
6
6
  *
7
7
  * Endpoints:
8
- * GET /api/memory - List all memory entries
8
+ * GET /api/memory - List all entries (or search with ?search=keyword)
9
9
  * GET /api/memory/:id - Get a single entry
10
10
  * POST /api/memory - Create a new entry
11
11
  * PUT /api/memory/:id - Update an entry
@@ -21,14 +21,20 @@ const memoryStore = require('../stores/memory-store')
21
21
  */
22
22
  async function memoryRoutes(fastify) {
23
23
 
24
- // GET /api/memory - List all entries
24
+ // GET /api/memory - List or search entries
25
25
  fastify.get('/api/memory', async (request, reply) => {
26
26
  if (!verifyToken(request)) {
27
27
  reply.code(401)
28
28
  return { success: false, error: 'Unauthorized' }
29
29
  }
30
30
 
31
- const entries = await memoryStore.listEntries()
31
+ const { search } = request.query || {}
32
+ if (search) {
33
+ const entries = memoryStore.searchEntries(search)
34
+ return { success: true, entries }
35
+ }
36
+
37
+ const entries = memoryStore.listEntries()
32
38
  return { success: true, entries }
33
39
  })
34
40
 
@@ -39,7 +45,7 @@ async function memoryRoutes(fastify) {
39
45
  return { success: false, error: 'Unauthorized' }
40
46
  }
41
47
 
42
- const entry = await memoryStore.loadEntry(request.params.id)
48
+ const entry = memoryStore.loadEntry(request.params.id)
43
49
  if (!entry) {
44
50
  reply.code(404)
45
51
  return { success: false, error: 'Entry not found' }
@@ -61,7 +67,7 @@ async function memoryRoutes(fastify) {
61
67
  return { success: false, error: 'title and content are required' }
62
68
  }
63
69
 
64
- const entry = await memoryStore.saveEntry({ title, category, content })
70
+ const entry = memoryStore.saveEntry({ title, category, content })
65
71
  console.log(`[Memory] Created entry: ${entry.id} (${entry.title})`)
66
72
  return { success: true, entry }
67
73
  })
@@ -73,14 +79,14 @@ async function memoryRoutes(fastify) {
73
79
  return { success: false, error: 'Unauthorized' }
74
80
  }
75
81
 
76
- const existing = await memoryStore.loadEntry(request.params.id)
82
+ const existing = memoryStore.loadEntry(request.params.id)
77
83
  if (!existing) {
78
84
  reply.code(404)
79
85
  return { success: false, error: 'Entry not found' }
80
86
  }
81
87
 
82
88
  const { title, category, content } = request.body || {}
83
- const entry = await memoryStore.saveEntry({
89
+ const entry = memoryStore.saveEntry({
84
90
  id: request.params.id,
85
91
  title: title || existing.title,
86
92
  category: category || existing.category,
@@ -98,7 +104,7 @@ async function memoryRoutes(fastify) {
98
104
  return { success: false, error: 'Unauthorized' }
99
105
  }
100
106
 
101
- const deleted = await memoryStore.deleteEntry(request.params.id)
107
+ const deleted = memoryStore.deleteEntry(request.params.id)
102
108
  if (!deleted) {
103
109
  reply.code(404)
104
110
  return { success: false, error: 'Entry not found' }