@geekbeer/minion 3.8.1 → 3.9.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 +17 -0
- package/core/lib/end-of-day.js +3 -0
- package/core/routes/memory.js +17 -10
- package/core/stores/memory-store.js +70 -21
- package/docs/api-reference.md +11 -4
- package/linux/routes/chat.js +3 -0
- package/package.json +1 -1
- package/settings/permissions.json +2 -1
- package/win/routes/chat.js +3 -0
package/core/db.js
CHANGED
|
@@ -425,6 +425,23 @@ function migrateSchema(db) {
|
|
|
425
425
|
|
|
426
426
|
console.log('[DB] Migration 2 complete: emails FTS5 added')
|
|
427
427
|
}
|
|
428
|
+
|
|
429
|
+
if (currentVersion < 3) {
|
|
430
|
+
console.log('[DB] Migration 3: Adding project_id to memories...')
|
|
431
|
+
|
|
432
|
+
db.exec(`
|
|
433
|
+
-- Add nullable project_id column to memories table.
|
|
434
|
+
-- NULL = universal memory (cross-project knowledge).
|
|
435
|
+
-- Set = project-scoped memory.
|
|
436
|
+
ALTER TABLE memories ADD COLUMN project_id TEXT DEFAULT NULL;
|
|
437
|
+
|
|
438
|
+
CREATE INDEX idx_memories_project_id ON memories(project_id);
|
|
439
|
+
|
|
440
|
+
INSERT INTO schema_version (version) VALUES (3);
|
|
441
|
+
`)
|
|
442
|
+
|
|
443
|
+
console.log('[DB] Migration 3 complete: memories.project_id added')
|
|
444
|
+
}
|
|
428
445
|
}
|
|
429
446
|
|
|
430
447
|
/**
|
package/core/lib/end-of-day.js
CHANGED
|
@@ -42,8 +42,10 @@ async function runEndOfDay({ runQuickLlmCall, clearSession = false }) {
|
|
|
42
42
|
- title: 短いタイトル
|
|
43
43
|
- category: user(ユーザーの好み)/ feedback(フィードバック)/ project(プロジェクト情報)/ reference(参照情報)のいずれか
|
|
44
44
|
- content: 詳細な内容
|
|
45
|
+
- project_id: この学びが特定プロジェクトに固有の場合はそのプロジェクトID(UUID)。汎用的なノウハウやユーザーの好みなど、プロジェクト横断で活用すべき学びの場合はnull
|
|
45
46
|
|
|
46
47
|
学びがない場合は空配列にしてください。すでに常識的なことは含めないでください。
|
|
48
|
+
特定のプロジェクトの技術スタック・ルール・設計判断などはproject_idを設定し、汎用的なスキルや知見はnullにしてください。
|
|
47
49
|
|
|
48
50
|
会話:
|
|
49
51
|
${conversationText}
|
|
@@ -83,6 +85,7 @@ JSON形式のみで回答してください(\`\`\`jsonブロックは不要)
|
|
|
83
85
|
title: learning.title,
|
|
84
86
|
category: learning.category || 'reference',
|
|
85
87
|
content: learning.content,
|
|
88
|
+
project_id: learning.project_id || null,
|
|
86
89
|
})
|
|
87
90
|
memoriesAdded++
|
|
88
91
|
}
|
package/core/routes/memory.js
CHANGED
|
@@ -4,11 +4,17 @@
|
|
|
4
4
|
* Provides CRUD + search for the minion's long-term memory entries.
|
|
5
5
|
* Memory is stored in SQLite with FTS5 full-text search.
|
|
6
6
|
*
|
|
7
|
+
* project_id scoping:
|
|
8
|
+
* - Entries with project_id are scoped to a specific project.
|
|
9
|
+
* - Entries without project_id are universal (cross-project knowledge).
|
|
10
|
+
* - Queries with ?project_id=xxx return scoped + universal entries.
|
|
11
|
+
*
|
|
7
12
|
* Endpoints:
|
|
8
13
|
* GET /api/memory - List all entries (or search with ?search=keyword)
|
|
14
|
+
* Optional: ?project_id=xxx to scope results
|
|
9
15
|
* GET /api/memory/:id - Get a single entry
|
|
10
|
-
* POST /api/memory - Create a new entry
|
|
11
|
-
* PUT /api/memory/:id - Update an entry
|
|
16
|
+
* POST /api/memory - Create a new entry (optional project_id in body)
|
|
17
|
+
* PUT /api/memory/:id - Update an entry (optional project_id in body)
|
|
12
18
|
* DELETE /api/memory/:id - Delete an entry
|
|
13
19
|
*/
|
|
14
20
|
|
|
@@ -28,13 +34,13 @@ async function memoryRoutes(fastify) {
|
|
|
28
34
|
return { success: false, error: 'Unauthorized' }
|
|
29
35
|
}
|
|
30
36
|
|
|
31
|
-
const { search } = request.query || {}
|
|
37
|
+
const { search, project_id } = request.query || {}
|
|
32
38
|
if (search) {
|
|
33
|
-
const entries = memoryStore.searchEntries(search)
|
|
39
|
+
const entries = memoryStore.searchEntries(search, { projectId: project_id })
|
|
34
40
|
return { success: true, entries }
|
|
35
41
|
}
|
|
36
42
|
|
|
37
|
-
const entries = memoryStore.listEntries()
|
|
43
|
+
const entries = memoryStore.listEntries({ projectId: project_id })
|
|
38
44
|
return { success: true, entries }
|
|
39
45
|
})
|
|
40
46
|
|
|
@@ -61,14 +67,14 @@ async function memoryRoutes(fastify) {
|
|
|
61
67
|
return { success: false, error: 'Unauthorized' }
|
|
62
68
|
}
|
|
63
69
|
|
|
64
|
-
const { title, category, content } = request.body || {}
|
|
70
|
+
const { title, category, content, project_id } = request.body || {}
|
|
65
71
|
if (!title || !content) {
|
|
66
72
|
reply.code(400)
|
|
67
73
|
return { success: false, error: 'title and content are required' }
|
|
68
74
|
}
|
|
69
75
|
|
|
70
|
-
const entry = memoryStore.saveEntry({ title, category, content })
|
|
71
|
-
console.log(`[Memory] Created entry: ${entry.id} (${entry.title})`)
|
|
76
|
+
const entry = memoryStore.saveEntry({ title, category, content, project_id })
|
|
77
|
+
console.log(`[Memory] Created entry: ${entry.id} (${entry.title})${entry.project_id ? ` [project:${entry.project_id}]` : ' [universal]'}`)
|
|
72
78
|
return { success: true, entry }
|
|
73
79
|
})
|
|
74
80
|
|
|
@@ -85,15 +91,16 @@ async function memoryRoutes(fastify) {
|
|
|
85
91
|
return { success: false, error: 'Entry not found' }
|
|
86
92
|
}
|
|
87
93
|
|
|
88
|
-
const { title, category, content } = request.body || {}
|
|
94
|
+
const { title, category, content, project_id } = request.body || {}
|
|
89
95
|
const entry = memoryStore.saveEntry({
|
|
90
96
|
id: request.params.id,
|
|
91
97
|
title: title || existing.title,
|
|
92
98
|
category: category || existing.category,
|
|
93
99
|
content: content !== undefined ? content : existing.content,
|
|
100
|
+
project_id: project_id !== undefined ? project_id : existing.project_id,
|
|
94
101
|
})
|
|
95
102
|
|
|
96
|
-
console.log(`[Memory] Updated entry: ${entry.id} (${entry.title})`)
|
|
103
|
+
console.log(`[Memory] Updated entry: ${entry.id} (${entry.title})${entry.project_id ? ` [project:${entry.project_id}]` : ' [universal]'}`)
|
|
97
104
|
return { success: true, entry }
|
|
98
105
|
})
|
|
99
106
|
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
* Supports full-text search via FTS5.
|
|
5
5
|
*
|
|
6
6
|
* Categories: user, feedback, project, reference
|
|
7
|
+
*
|
|
8
|
+
* project_id scoping:
|
|
9
|
+
* - NULL project_id = universal memory (cross-project knowledge/know-how)
|
|
10
|
+
* - Set project_id = project-scoped memory
|
|
11
|
+
* - When querying with a project_id, returns both scoped + universal entries
|
|
7
12
|
*/
|
|
8
13
|
|
|
9
14
|
const crypto = require('crypto')
|
|
@@ -13,17 +18,31 @@ const VALID_CATEGORIES = ['user', 'feedback', 'project', 'reference']
|
|
|
13
18
|
|
|
14
19
|
/**
|
|
15
20
|
* List all memory entries (metadata + excerpt, no full body).
|
|
16
|
-
* @
|
|
21
|
+
* @param {{ projectId?: string }} [opts] - Optional filter options
|
|
22
|
+
* - projectId: If set, returns entries for that project + universal (NULL) entries.
|
|
23
|
+
* If omitted, returns all entries.
|
|
24
|
+
* @returns {Array<{ id, title, category, project_id, created_at, updated_at, excerpt }>}
|
|
17
25
|
*/
|
|
18
|
-
function listEntries() {
|
|
26
|
+
function listEntries(opts = {}) {
|
|
19
27
|
const db = getDb()
|
|
20
|
-
const
|
|
21
|
-
|
|
28
|
+
const { projectId } = opts
|
|
29
|
+
|
|
30
|
+
if (projectId) {
|
|
31
|
+
return db.prepare(`
|
|
32
|
+
SELECT id, title, category, project_id, created_at, updated_at,
|
|
33
|
+
substr(content, 1, 200) as excerpt
|
|
34
|
+
FROM memories
|
|
35
|
+
WHERE project_id = ? OR project_id IS NULL
|
|
36
|
+
ORDER BY updated_at DESC
|
|
37
|
+
`).all(projectId)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return db.prepare(`
|
|
41
|
+
SELECT id, title, category, project_id, created_at, updated_at,
|
|
22
42
|
substr(content, 1, 200) as excerpt
|
|
23
43
|
FROM memories
|
|
24
44
|
ORDER BY updated_at DESC
|
|
25
45
|
`).all()
|
|
26
|
-
return rows
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
/**
|
|
@@ -39,7 +58,8 @@ function loadEntry(id) {
|
|
|
39
58
|
|
|
40
59
|
/**
|
|
41
60
|
* Save (create or update) a memory entry.
|
|
42
|
-
* @param {{ id?, title, category, content }} entry
|
|
61
|
+
* @param {{ id?, title, category, content, project_id? }} entry
|
|
62
|
+
* - project_id: UUID of the project this memory belongs to, or null/undefined for universal
|
|
43
63
|
* @returns {object} The saved entry
|
|
44
64
|
*/
|
|
45
65
|
function saveEntry(entry) {
|
|
@@ -47,6 +67,7 @@ function saveEntry(entry) {
|
|
|
47
67
|
const now = new Date().toISOString()
|
|
48
68
|
const id = entry.id || crypto.randomBytes(6).toString('hex')
|
|
49
69
|
const category = VALID_CATEGORIES.includes(entry.category) ? entry.category : 'reference'
|
|
70
|
+
const projectId = entry.project_id || null
|
|
50
71
|
|
|
51
72
|
const existing = db.prepare('SELECT created_at FROM memories WHERE id = ?').get(id)
|
|
52
73
|
|
|
@@ -55,20 +76,21 @@ function saveEntry(entry) {
|
|
|
55
76
|
title: entry.title || 'Untitled',
|
|
56
77
|
category,
|
|
57
78
|
content: entry.content || '',
|
|
79
|
+
project_id: projectId,
|
|
58
80
|
created_at: existing ? existing.created_at : now,
|
|
59
81
|
updated_at: now,
|
|
60
82
|
}
|
|
61
83
|
|
|
62
84
|
if (existing) {
|
|
63
85
|
db.prepare(`
|
|
64
|
-
UPDATE memories SET title = ?, category = ?, content = ?, updated_at = ?
|
|
86
|
+
UPDATE memories SET title = ?, category = ?, content = ?, project_id = ?, updated_at = ?
|
|
65
87
|
WHERE id = ?
|
|
66
|
-
`).run(full.title, full.category, full.content, full.updated_at, full.id)
|
|
88
|
+
`).run(full.title, full.category, full.content, full.project_id, full.updated_at, full.id)
|
|
67
89
|
} else {
|
|
68
90
|
db.prepare(`
|
|
69
|
-
INSERT INTO memories (id, title, category, content, created_at, updated_at)
|
|
70
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
71
|
-
`).run(full.id, full.title, full.category, full.content, full.created_at, full.updated_at)
|
|
91
|
+
INSERT INTO memories (id, title, category, content, project_id, created_at, updated_at)
|
|
92
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
93
|
+
`).run(full.id, full.title, full.category, full.content, full.project_id, full.created_at, full.updated_at)
|
|
72
94
|
}
|
|
73
95
|
|
|
74
96
|
return full
|
|
@@ -89,11 +111,14 @@ function deleteEntry(id) {
|
|
|
89
111
|
* Search memory entries using FTS5 full-text search (trigram tokenizer).
|
|
90
112
|
* Falls back to LIKE for queries shorter than 3 characters (trigram minimum).
|
|
91
113
|
* @param {string} query - Search query
|
|
92
|
-
* @param {number}
|
|
93
|
-
*
|
|
114
|
+
* @param {{ limit?: number, projectId?: string }} [opts] - Search options
|
|
115
|
+
* - limit: Max results (default 20)
|
|
116
|
+
* - projectId: If set, returns matches for that project + universal (NULL) entries
|
|
117
|
+
* @returns {Array<{ id, title, category, project_id, content, created_at, updated_at }>}
|
|
94
118
|
*/
|
|
95
|
-
function searchEntries(query,
|
|
119
|
+
function searchEntries(query, opts = {}) {
|
|
96
120
|
const db = getDb()
|
|
121
|
+
const { limit = 20, projectId } = typeof opts === 'number' ? { limit: opts } : opts
|
|
97
122
|
const terms = query.split(/\s+/).filter(Boolean)
|
|
98
123
|
if (terms.length === 0) return []
|
|
99
124
|
|
|
@@ -108,11 +133,18 @@ function searchEntries(query, limit = 20) {
|
|
|
108
133
|
const like = `%${t}%`
|
|
109
134
|
return [like, like]
|
|
110
135
|
})
|
|
136
|
+
|
|
137
|
+
let projectFilter = ''
|
|
138
|
+
if (projectId) {
|
|
139
|
+
projectFilter = ' AND (project_id = ? OR project_id IS NULL)'
|
|
140
|
+
params.push(projectId)
|
|
141
|
+
}
|
|
142
|
+
|
|
111
143
|
params.push(limit)
|
|
112
144
|
return db.prepare(`
|
|
113
|
-
SELECT id, title, category, content, created_at, updated_at
|
|
145
|
+
SELECT id, title, category, project_id, content, created_at, updated_at
|
|
114
146
|
FROM memories
|
|
115
|
-
WHERE ${conditions}
|
|
147
|
+
WHERE ${conditions}${projectFilter}
|
|
116
148
|
ORDER BY updated_at DESC
|
|
117
149
|
LIMIT ?
|
|
118
150
|
`).all(...params)
|
|
@@ -120,8 +152,21 @@ function searchEntries(query, limit = 20) {
|
|
|
120
152
|
|
|
121
153
|
// FTS5 trigram search: wrap each term in double quotes for exact substring match
|
|
122
154
|
const sanitized = terms.map(t => `"${t.replace(/"/g, '')}"`).join(' ')
|
|
155
|
+
|
|
156
|
+
if (projectId) {
|
|
157
|
+
return db.prepare(`
|
|
158
|
+
SELECT m.id, m.title, m.category, m.project_id, m.content, m.created_at, m.updated_at
|
|
159
|
+
FROM memories m
|
|
160
|
+
JOIN memories_fts f ON m.rowid = f.rowid
|
|
161
|
+
WHERE memories_fts MATCH ?
|
|
162
|
+
AND (m.project_id = ? OR m.project_id IS NULL)
|
|
163
|
+
ORDER BY rank
|
|
164
|
+
LIMIT ?
|
|
165
|
+
`).all(sanitized, projectId, limit)
|
|
166
|
+
}
|
|
167
|
+
|
|
123
168
|
return db.prepare(`
|
|
124
|
-
SELECT m.id, m.title, m.category, m.content, m.created_at, m.updated_at
|
|
169
|
+
SELECT m.id, m.title, m.category, m.project_id, m.content, m.created_at, m.updated_at
|
|
125
170
|
FROM memories m
|
|
126
171
|
JOIN memories_fts f ON m.rowid = f.rowid
|
|
127
172
|
WHERE memories_fts MATCH ?
|
|
@@ -133,18 +178,22 @@ function searchEntries(query, limit = 20) {
|
|
|
133
178
|
/**
|
|
134
179
|
* Get a context snippet suitable for chat injection.
|
|
135
180
|
* Returns the most important memory entries as a formatted string.
|
|
136
|
-
* @param {
|
|
181
|
+
* @param {{ maxChars?: number, projectId?: string }} [opts] - Options
|
|
182
|
+
* - maxChars: Maximum characters to return (default 2000)
|
|
183
|
+
* - projectId: If set, includes project-scoped + universal entries
|
|
137
184
|
* @returns {string}
|
|
138
185
|
*/
|
|
139
|
-
function getContextSnippet(
|
|
140
|
-
const
|
|
186
|
+
function getContextSnippet(opts = {}) {
|
|
187
|
+
const { maxChars = 2000, projectId } = typeof opts === 'number' ? { maxChars: opts } : opts
|
|
188
|
+
const entries = listEntries({ projectId })
|
|
141
189
|
if (entries.length === 0) return ''
|
|
142
190
|
|
|
143
191
|
const parts = []
|
|
144
192
|
let totalLen = 0
|
|
145
193
|
|
|
146
194
|
for (const e of entries) {
|
|
147
|
-
const
|
|
195
|
+
const scope = e.project_id ? '[project-scoped]' : '[universal]'
|
|
196
|
+
const line = `${scope} [${e.category}] ${e.title}: ${e.excerpt}`
|
|
148
197
|
if (totalLen + line.length > maxChars) break
|
|
149
198
|
parts.push(line)
|
|
150
199
|
totalLen += line.length + 1
|
package/docs/api-reference.md
CHANGED
|
@@ -83,19 +83,26 @@ Full-text search supported via FTS5.
|
|
|
83
83
|
|
|
84
84
|
| Method | Endpoint | Description |
|
|
85
85
|
|--------|----------|-------------|
|
|
86
|
-
| GET | `/api/memory` | List all memory entries (id, title, category, excerpt, timestamps) |
|
|
86
|
+
| GET | `/api/memory` | List all memory entries (id, title, category, project_id, excerpt, timestamps) |
|
|
87
87
|
| GET | `/api/memory?search=keyword` | Full-text search on title and content (FTS5) |
|
|
88
|
+
| GET | `/api/memory?project_id=xxx` | List entries scoped to project + universal entries |
|
|
89
|
+
| GET | `/api/memory?search=keyword&project_id=xxx` | Search within project scope + universal |
|
|
88
90
|
| GET | `/api/memory/:id` | Get full memory entry |
|
|
89
|
-
| POST | `/api/memory` | Create memory entry. Body: `{title, category, content}` |
|
|
90
|
-
| PUT | `/api/memory/:id` | Update memory entry. Body: `{title?, category?, content?}` |
|
|
91
|
+
| POST | `/api/memory` | Create memory entry. Body: `{title, category, content, project_id?}` |
|
|
92
|
+
| PUT | `/api/memory/:id` | Update memory entry. Body: `{title?, category?, content?, project_id?}` |
|
|
91
93
|
| DELETE | `/api/memory/:id` | Delete a memory entry |
|
|
92
94
|
|
|
95
|
+
**Project scoping**: `project_id` is optional. When set, the entry is scoped to that project.
|
|
96
|
+
When `project_id` is omitted (NULL), the entry is universal (cross-project knowledge).
|
|
97
|
+
Queries with `?project_id=xxx` return both project-scoped AND universal entries.
|
|
98
|
+
|
|
93
99
|
POST/PUT body:
|
|
94
100
|
```json
|
|
95
101
|
{
|
|
96
102
|
"title": "ユーザーはレビューで日本語を好む",
|
|
97
103
|
"category": "user",
|
|
98
|
-
"content": "コードレビューのコメントは日本語で記述すること。"
|
|
104
|
+
"content": "コードレビューのコメントは日本語で記述すること。",
|
|
105
|
+
"project_id": null
|
|
99
106
|
}
|
|
100
107
|
```
|
|
101
108
|
|
package/linux/routes/chat.js
CHANGED
|
@@ -246,6 +246,9 @@ async function buildContextPrefix(message, context, sessionId) {
|
|
|
246
246
|
'# メモリ検索(キーワードで全文検索)',
|
|
247
247
|
`curl -H "Authorization: Bearer $API_TOKEN" "${baseUrl}/api/memory?search=キーワード"`,
|
|
248
248
|
'',
|
|
249
|
+
'# プロジェクトスコープでメモリ検索(プロジェクト固有 + 汎用メモリを返す)',
|
|
250
|
+
`curl -H "Authorization: Bearer $API_TOKEN" "${baseUrl}/api/memory?search=キーワード&project_id=プロジェクトUUID"`,
|
|
251
|
+
'',
|
|
249
252
|
'# メモリ一覧',
|
|
250
253
|
`curl -H "Authorization: Bearer $API_TOKEN" ${baseUrl}/api/memory`,
|
|
251
254
|
'',
|
package/package.json
CHANGED
package/win/routes/chat.js
CHANGED
|
@@ -208,6 +208,9 @@ async function buildContextPrefix(message, context, sessionId) {
|
|
|
208
208
|
'# メモリ検索(キーワードで全文検索)',
|
|
209
209
|
`curl -H "Authorization: Bearer $API_TOKEN" "${baseUrl}/api/memory?search=キーワード"`,
|
|
210
210
|
'',
|
|
211
|
+
'# プロジェクトスコープでメモリ検索(プロジェクト固有 + 汎用メモリを返す)',
|
|
212
|
+
`curl -H "Authorization: Bearer $API_TOKEN" "${baseUrl}/api/memory?search=キーワード&project_id=プロジェクトUUID"`,
|
|
213
|
+
'',
|
|
211
214
|
'# メモリ一覧',
|
|
212
215
|
`curl -H "Authorization: Bearer $API_TOKEN" ${baseUrl}/api/memory`,
|
|
213
216
|
'',
|