@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 +297 -0
- package/core/lib/thread-watcher.js +3 -6
- package/core/routes/daily-logs.js +17 -11
- package/core/routes/memory.js +16 -10
- package/core/stores/chat-store.js +138 -76
- package/core/stores/daily-log-store.js +84 -105
- package/core/stores/execution-store.js +59 -84
- package/core/stores/memory-store.js +84 -177
- package/core/stores/routine-store.js +55 -72
- package/core/stores/workflow-store.js +52 -69
- package/docs/api-reference.md +6 -2
- package/linux/routes/chat.js +54 -7
- package/linux/server.js +3 -1
- package/package.json +2 -1
- package/roles/engineer.md +12 -0
- package/roles/pm.md +16 -0
- package/rules/core.md +66 -0
- package/win/routes/chat.js +54 -7
- package/win/server.js +2 -0
|
@@ -1,113 +1,175 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Chat Session Store
|
|
3
|
-
* Persists
|
|
4
|
-
*
|
|
2
|
+
* Chat Session Store (SQLite)
|
|
3
|
+
* Persists chat sessions and messages.
|
|
4
|
+
* Supports multiple sessions: one active + archived past sessions.
|
|
5
|
+
* Claude CLI manages conversation context via --resume.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
-
const path = require('path')
|
|
9
|
-
|
|
10
|
-
const { config } = require('../config')
|
|
11
|
-
const { DATA_DIR } = require('../lib/platform')
|
|
8
|
+
const { getDb } = require('../db')
|
|
12
9
|
|
|
13
10
|
const MAX_MESSAGES = 100
|
|
14
11
|
|
|
15
12
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
13
|
+
* Load the active (most recent) chat session
|
|
14
|
+
* @returns {object|null} Session object or null if none exists
|
|
18
15
|
*/
|
|
19
|
-
function
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
16
|
+
function load() {
|
|
17
|
+
const db = getDb()
|
|
18
|
+
const session = db.prepare(
|
|
19
|
+
'SELECT * FROM chat_sessions ORDER BY updated_at DESC LIMIT 1'
|
|
20
|
+
).get()
|
|
21
|
+
if (!session) return null
|
|
22
|
+
|
|
23
|
+
const messages = db.prepare(
|
|
24
|
+
'SELECT role, content, timestamp FROM chat_messages WHERE session_id = ? ORDER BY id'
|
|
25
|
+
).all(session.session_id)
|
|
26
|
+
|
|
27
|
+
return {
|
|
28
|
+
session_id: session.session_id,
|
|
29
|
+
messages,
|
|
30
|
+
turn_count: session.turn_count,
|
|
31
|
+
created_at: session.created_at,
|
|
32
|
+
updated_at: session.updated_at,
|
|
25
33
|
}
|
|
26
34
|
}
|
|
27
35
|
|
|
28
|
-
const SESSION_FILE = getFilePath()
|
|
29
|
-
|
|
30
36
|
/**
|
|
31
|
-
* Load
|
|
32
|
-
* @
|
|
37
|
+
* Load a specific session by ID
|
|
38
|
+
* @param {string} sessionId
|
|
39
|
+
* @returns {object|null}
|
|
33
40
|
*/
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
function loadById(sessionId) {
|
|
42
|
+
const db = getDb()
|
|
43
|
+
const session = db.prepare('SELECT * FROM chat_sessions WHERE session_id = ?').get(sessionId)
|
|
44
|
+
if (!session) return null
|
|
45
|
+
|
|
46
|
+
const messages = db.prepare(
|
|
47
|
+
'SELECT role, content, timestamp FROM chat_messages WHERE session_id = ? ORDER BY id'
|
|
48
|
+
).all(session.session_id)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
session_id: session.session_id,
|
|
52
|
+
messages,
|
|
53
|
+
turn_count: session.turn_count,
|
|
54
|
+
created_at: session.created_at,
|
|
55
|
+
updated_at: session.updated_at,
|
|
44
56
|
}
|
|
45
57
|
}
|
|
46
58
|
|
|
47
59
|
/**
|
|
48
|
-
*
|
|
60
|
+
* List all sessions (metadata only, no messages)
|
|
61
|
+
* @param {number} limit
|
|
62
|
+
* @returns {Array<{ session_id, turn_count, message_count, created_at, updated_at }>}
|
|
63
|
+
*/
|
|
64
|
+
function listSessions(limit = 50) {
|
|
65
|
+
const db = getDb()
|
|
66
|
+
return db.prepare(`
|
|
67
|
+
SELECT
|
|
68
|
+
s.session_id,
|
|
69
|
+
s.turn_count,
|
|
70
|
+
s.created_at,
|
|
71
|
+
s.updated_at,
|
|
72
|
+
(SELECT COUNT(*) FROM chat_messages WHERE session_id = s.session_id) as message_count
|
|
73
|
+
FROM chat_sessions s
|
|
74
|
+
ORDER BY s.updated_at DESC
|
|
75
|
+
LIMIT ?
|
|
76
|
+
`).all(limit)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Save the active chat session (full replace of this session only)
|
|
49
81
|
* @param {object} session - Session object
|
|
50
82
|
*/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
83
|
+
function save(session) {
|
|
84
|
+
const db = getDb()
|
|
85
|
+
const tx = db.transaction(() => {
|
|
86
|
+
// Remove only this session (preserve others)
|
|
87
|
+
db.prepare('DELETE FROM chat_sessions WHERE session_id = ?').run(session.session_id)
|
|
88
|
+
|
|
89
|
+
// Insert session
|
|
90
|
+
db.prepare(
|
|
91
|
+
'INSERT INTO chat_sessions (session_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?)'
|
|
92
|
+
).run(session.session_id, session.turn_count || 0, session.created_at, session.updated_at)
|
|
93
|
+
|
|
94
|
+
// Insert messages
|
|
95
|
+
const insertMsg = db.prepare(
|
|
96
|
+
'INSERT INTO chat_messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)'
|
|
97
|
+
)
|
|
98
|
+
for (const msg of (session.messages || [])) {
|
|
99
|
+
insertMsg.run(session.session_id, msg.role, msg.content, msg.timestamp)
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
tx()
|
|
57
103
|
}
|
|
58
104
|
|
|
59
105
|
/**
|
|
60
|
-
* Add a message to the active session
|
|
61
|
-
* Creates a new session if none exists
|
|
106
|
+
* Add a message to the active session.
|
|
107
|
+
* Creates a new session if none exists or session_id changed.
|
|
108
|
+
* Past sessions are preserved (not deleted).
|
|
62
109
|
* @param {string} sessionId - Claude CLI session ID
|
|
63
110
|
* @param {{ role: string, content: string }} msg - Message to add
|
|
64
|
-
* @param {number} [turnCount] - Optional turn count to
|
|
111
|
+
* @param {number} [turnCount] - Optional turn count to add
|
|
65
112
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
turn_count
|
|
75
|
-
|
|
76
|
-
updated_at: Date.now(),
|
|
77
|
-
}
|
|
113
|
+
function addMessage(sessionId, msg, turnCount) {
|
|
114
|
+
const db = getDb()
|
|
115
|
+
const existing = db.prepare('SELECT * FROM chat_sessions WHERE session_id = ?').get(sessionId)
|
|
116
|
+
|
|
117
|
+
// If session doesn't exist, create a new one (past sessions remain in DB)
|
|
118
|
+
if (!existing) {
|
|
119
|
+
const now = Date.now()
|
|
120
|
+
db.prepare(
|
|
121
|
+
'INSERT INTO chat_sessions (session_id, turn_count, created_at, updated_at) VALUES (?, ?, ?, ?)'
|
|
122
|
+
).run(sessionId, 0, now, now)
|
|
78
123
|
}
|
|
79
124
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
})
|
|
85
|
-
session.updated_at = Date.now()
|
|
125
|
+
// Insert message
|
|
126
|
+
db.prepare(
|
|
127
|
+
'INSERT INTO chat_messages (session_id, role, content, timestamp) VALUES (?, ?, ?, ?)'
|
|
128
|
+
).run(sessionId, msg.role, msg.content, Date.now())
|
|
86
129
|
|
|
87
|
-
// Update
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
130
|
+
// Update session metadata
|
|
131
|
+
const newTurnCount = (turnCount && turnCount > 0) ? turnCount : 0
|
|
132
|
+
db.prepare(`
|
|
133
|
+
UPDATE chat_sessions
|
|
134
|
+
SET updated_at = ?,
|
|
135
|
+
turn_count = turn_count + ?
|
|
136
|
+
WHERE session_id = ?
|
|
137
|
+
`).run(Date.now(), newTurnCount, sessionId)
|
|
91
138
|
|
|
92
|
-
// Prune old messages
|
|
93
|
-
|
|
94
|
-
|
|
139
|
+
// Prune old messages for this session (keep only MAX_MESSAGES most recent)
|
|
140
|
+
const count = db.prepare('SELECT COUNT(*) as cnt FROM chat_messages WHERE session_id = ?').get(sessionId).cnt
|
|
141
|
+
if (count > MAX_MESSAGES) {
|
|
142
|
+
db.prepare(`
|
|
143
|
+
DELETE FROM chat_messages WHERE id IN (
|
|
144
|
+
SELECT id FROM chat_messages WHERE session_id = ?
|
|
145
|
+
ORDER BY id ASC LIMIT ?
|
|
146
|
+
)
|
|
147
|
+
`).run(sessionId, count - MAX_MESSAGES)
|
|
95
148
|
}
|
|
96
|
-
|
|
97
|
-
await save(session)
|
|
98
149
|
}
|
|
99
150
|
|
|
100
151
|
/**
|
|
101
|
-
* Clear the active session
|
|
152
|
+
* Clear the active session (remove from DB).
|
|
153
|
+
* This deletes only the most recent session.
|
|
154
|
+
* For reset flows, use clearActive() which preserves the session in history.
|
|
102
155
|
*/
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
console.error(`[ChatStore] Failed to clear session: ${err.message}`)
|
|
109
|
-
}
|
|
156
|
+
function clear() {
|
|
157
|
+
const db = getDb()
|
|
158
|
+
const active = db.prepare('SELECT session_id FROM chat_sessions ORDER BY updated_at DESC LIMIT 1').get()
|
|
159
|
+
if (active) {
|
|
160
|
+
db.prepare('DELETE FROM chat_sessions WHERE session_id = ?').run(active.session_id)
|
|
110
161
|
}
|
|
111
162
|
}
|
|
112
163
|
|
|
113
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Delete a specific session by ID
|
|
166
|
+
* @param {string} sessionId
|
|
167
|
+
* @returns {boolean}
|
|
168
|
+
*/
|
|
169
|
+
function deleteSession(sessionId) {
|
|
170
|
+
const db = getDb()
|
|
171
|
+
const result = db.prepare('DELETE FROM chat_sessions WHERE session_id = ?').run(sessionId)
|
|
172
|
+
return result.changes > 0
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { load, loadById, listSessions, save, addMessage, clear, deleteSession }
|
|
@@ -1,38 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Daily Log Store
|
|
3
|
-
* Stores daily conversation summaries
|
|
4
|
-
* One
|
|
5
|
-
*
|
|
6
|
-
* These logs serve as short-term memory (recent days),
|
|
7
|
-
* bridging working memory (chat session) and long-term memory (memory-store).
|
|
2
|
+
* Daily Log Store (SQLite)
|
|
3
|
+
* Stores daily conversation summaries.
|
|
4
|
+
* One entry per day, keyed by YYYY-MM-DD date string.
|
|
5
|
+
* Supports full-text search via FTS5.
|
|
8
6
|
*/
|
|
9
7
|
|
|
10
|
-
const
|
|
11
|
-
const path = require('path')
|
|
12
|
-
|
|
13
|
-
const { config } = require('../config')
|
|
14
|
-
const { DATA_DIR } = require('../lib/platform')
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Resolve daily logs directory path
|
|
18
|
-
*/
|
|
19
|
-
function getLogsDir() {
|
|
20
|
-
try {
|
|
21
|
-
require('fs').accessSync(DATA_DIR)
|
|
22
|
-
return path.join(DATA_DIR, 'daily-logs')
|
|
23
|
-
} catch {
|
|
24
|
-
return path.join(config.HOME_DIR, 'daily-logs')
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const LOGS_DIR = getLogsDir()
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Ensure the daily-logs directory exists
|
|
32
|
-
*/
|
|
33
|
-
async function ensureDir() {
|
|
34
|
-
await fs.mkdir(LOGS_DIR, { recursive: true })
|
|
35
|
-
}
|
|
8
|
+
const { getDb } = require('../db')
|
|
36
9
|
|
|
37
10
|
/**
|
|
38
11
|
* Validate date string format (YYYY-MM-DD)
|
|
@@ -42,114 +15,122 @@ function isValidDate(date) {
|
|
|
42
15
|
}
|
|
43
16
|
|
|
44
17
|
/**
|
|
45
|
-
* List all daily logs with date,
|
|
46
|
-
* @returns {
|
|
18
|
+
* List all daily logs with date, content size, and timestamps.
|
|
19
|
+
* @returns {Array<{ date, size, created_at, updated_at }>} Sorted descending by date
|
|
47
20
|
*/
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const logs = []
|
|
58
|
-
for (const file of files) {
|
|
59
|
-
if (!file.endsWith('.md')) continue
|
|
60
|
-
const date = file.replace('.md', '')
|
|
61
|
-
if (!isValidDate(date)) continue
|
|
62
|
-
try {
|
|
63
|
-
const stat = await fs.stat(path.join(LOGS_DIR, file))
|
|
64
|
-
logs.push({
|
|
65
|
-
date,
|
|
66
|
-
size: stat.size,
|
|
67
|
-
created_at: stat.birthtime.toISOString(),
|
|
68
|
-
updated_at: stat.mtime.toISOString(),
|
|
69
|
-
})
|
|
70
|
-
} catch {
|
|
71
|
-
// Skip unreadable
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
// Sort by date descending (most recent first)
|
|
76
|
-
logs.sort((a, b) => b.date.localeCompare(a.date))
|
|
77
|
-
return logs
|
|
21
|
+
function listLogs() {
|
|
22
|
+
const db = getDb()
|
|
23
|
+
const rows = db.prepare(`
|
|
24
|
+
SELECT date, length(content) as size, created_at, updated_at
|
|
25
|
+
FROM daily_logs
|
|
26
|
+
ORDER BY date DESC
|
|
27
|
+
`).all()
|
|
28
|
+
return rows
|
|
78
29
|
}
|
|
79
30
|
|
|
80
31
|
/**
|
|
81
32
|
* Load a specific day's log.
|
|
82
33
|
* @param {string} date - YYYY-MM-DD format
|
|
83
|
-
* @returns {
|
|
34
|
+
* @returns {string|null} Log content or null
|
|
84
35
|
*/
|
|
85
|
-
|
|
36
|
+
function loadLog(date) {
|
|
86
37
|
if (!isValidDate(date)) return null
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (err.code === 'ENOENT') return null
|
|
91
|
-
console.error(`[DailyLogStore] Failed to load log ${date}: ${err.message}`)
|
|
92
|
-
return null
|
|
93
|
-
}
|
|
38
|
+
const db = getDb()
|
|
39
|
+
const row = db.prepare('SELECT content FROM daily_logs WHERE date = ?').get(date)
|
|
40
|
+
return row ? row.content : null
|
|
94
41
|
}
|
|
95
42
|
|
|
96
43
|
/**
|
|
97
|
-
* Save a daily log.
|
|
44
|
+
* Save a daily log. Upserts (overwrites existing log for the same date).
|
|
98
45
|
* @param {string} date - YYYY-MM-DD format
|
|
99
46
|
* @param {string} content - Markdown content
|
|
100
47
|
*/
|
|
101
|
-
|
|
48
|
+
function saveLog(date, content) {
|
|
102
49
|
if (!isValidDate(date)) {
|
|
103
50
|
throw new Error(`Invalid date format: ${date}. Expected YYYY-MM-DD.`)
|
|
104
51
|
}
|
|
105
|
-
|
|
106
|
-
|
|
52
|
+
const db = getDb()
|
|
53
|
+
const now = new Date().toISOString()
|
|
54
|
+
const existing = db.prepare('SELECT date FROM daily_logs WHERE date = ?').get(date)
|
|
55
|
+
|
|
56
|
+
if (existing) {
|
|
57
|
+
db.prepare('UPDATE daily_logs SET content = ?, updated_at = ? WHERE date = ?').run(content, now, date)
|
|
58
|
+
} else {
|
|
59
|
+
db.prepare('INSERT INTO daily_logs (date, content, created_at, updated_at) VALUES (?, ?, ?, ?)').run(
|
|
60
|
+
date, content, now, now
|
|
61
|
+
)
|
|
62
|
+
}
|
|
107
63
|
}
|
|
108
64
|
|
|
109
65
|
/**
|
|
110
66
|
* Delete a daily log.
|
|
111
67
|
* @param {string} date - YYYY-MM-DD format
|
|
112
|
-
* @returns {
|
|
68
|
+
* @returns {boolean}
|
|
113
69
|
*/
|
|
114
|
-
|
|
70
|
+
function deleteLog(date) {
|
|
115
71
|
if (!isValidDate(date)) return false
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
} catch (err) {
|
|
120
|
-
if (err.code === 'ENOENT') return false
|
|
121
|
-
console.error(`[DailyLogStore] Failed to delete log ${date}: ${err.message}`)
|
|
122
|
-
return false
|
|
123
|
-
}
|
|
72
|
+
const db = getDb()
|
|
73
|
+
const result = db.prepare('DELETE FROM daily_logs WHERE date = ?').run(date)
|
|
74
|
+
return result.changes > 0
|
|
124
75
|
}
|
|
125
76
|
|
|
126
77
|
/**
|
|
127
78
|
* Get the most recent N days of logs.
|
|
128
79
|
* @param {number} days - Number of recent days to fetch
|
|
129
|
-
* @returns {
|
|
80
|
+
* @returns {Array<{ date, content }>}
|
|
130
81
|
*/
|
|
131
|
-
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
82
|
+
function getRecentLogs(days = 3) {
|
|
83
|
+
const db = getDb()
|
|
84
|
+
const rows = db.prepare('SELECT date, content FROM daily_logs ORDER BY date DESC LIMIT ?').all(days)
|
|
85
|
+
return rows
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Search daily logs using FTS5 full-text search (trigram tokenizer).
|
|
90
|
+
* Falls back to LIKE for queries shorter than 3 characters (trigram minimum).
|
|
91
|
+
* @param {string} query - Search query
|
|
92
|
+
* @param {number} limit - Max results
|
|
93
|
+
* @returns {Array<{ date, content, created_at, updated_at }>}
|
|
94
|
+
*/
|
|
95
|
+
function searchLogs(query, limit = 20) {
|
|
96
|
+
const db = getDb()
|
|
97
|
+
const terms = query.split(/\s+/).filter(Boolean)
|
|
98
|
+
if (terms.length === 0) return []
|
|
99
|
+
|
|
100
|
+
const hasShortTerm = terms.some(t => [...t].length < 3)
|
|
101
|
+
|
|
102
|
+
if (hasShortTerm) {
|
|
103
|
+
const conditions = terms.map(() => 'content LIKE ?').join(' AND ')
|
|
104
|
+
const params = terms.map(t => `%${t}%`)
|
|
105
|
+
params.push(limit)
|
|
106
|
+
return db.prepare(`
|
|
107
|
+
SELECT date, content, created_at, updated_at
|
|
108
|
+
FROM daily_logs
|
|
109
|
+
WHERE ${conditions}
|
|
110
|
+
ORDER BY date DESC
|
|
111
|
+
LIMIT ?
|
|
112
|
+
`).all(...params)
|
|
141
113
|
}
|
|
142
|
-
|
|
114
|
+
|
|
115
|
+
const sanitized = terms.map(t => `"${t.replace(/"/g, '')}"`).join(' ')
|
|
116
|
+
return db.prepare(`
|
|
117
|
+
SELECT d.date, d.content, d.created_at, d.updated_at
|
|
118
|
+
FROM daily_logs d
|
|
119
|
+
JOIN daily_logs_fts f ON d.rowid = f.rowid
|
|
120
|
+
WHERE daily_logs_fts MATCH ?
|
|
121
|
+
ORDER BY rank
|
|
122
|
+
LIMIT ?
|
|
123
|
+
`).all(sanitized, limit)
|
|
143
124
|
}
|
|
144
125
|
|
|
145
126
|
/**
|
|
146
127
|
* Get a context snippet of recent daily logs for chat injection.
|
|
147
128
|
* @param {number} days - Number of days to include
|
|
148
129
|
* @param {number} maxChars - Maximum total characters
|
|
149
|
-
* @returns {
|
|
130
|
+
* @returns {string}
|
|
150
131
|
*/
|
|
151
|
-
|
|
152
|
-
const logs =
|
|
132
|
+
function getContextSnippet(days = 3, maxChars = 1500) {
|
|
133
|
+
const logs = getRecentLogs(days)
|
|
153
134
|
if (logs.length === 0) return ''
|
|
154
135
|
|
|
155
136
|
const parts = []
|
|
@@ -161,7 +142,6 @@ async function getContextSnippet(days = 3, maxChars = 1500) {
|
|
|
161
142
|
const section = `${header}\n${content}`
|
|
162
143
|
|
|
163
144
|
if (totalLen + section.length > maxChars) {
|
|
164
|
-
// Add truncated version
|
|
165
145
|
const remaining = maxChars - totalLen
|
|
166
146
|
if (remaining > 50) {
|
|
167
147
|
parts.push(section.substring(0, remaining) + '...')
|
|
@@ -176,12 +156,11 @@ async function getContextSnippet(days = 3, maxChars = 1500) {
|
|
|
176
156
|
}
|
|
177
157
|
|
|
178
158
|
module.exports = {
|
|
179
|
-
ensureDir,
|
|
180
159
|
listLogs,
|
|
181
160
|
loadLog,
|
|
182
161
|
saveLog,
|
|
183
162
|
deleteLog,
|
|
184
163
|
getRecentLogs,
|
|
164
|
+
searchLogs,
|
|
185
165
|
getContextSnippet,
|
|
186
|
-
LOGS_DIR,
|
|
187
166
|
}
|