@geekbeer/minion 2.46.1 → 2.48.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/config.js CHANGED
@@ -12,6 +12,8 @@
12
12
  * Other optional environment variables:
13
13
  * - AGENT_PORT: Port for the local agent server (default: 8080)
14
14
  * - MINION_USER: System user running the agent (used to resolve home directory)
15
+ * - REFLECTION_TIME: Daily self-reflection time in HH:MM format (e.g., "03:00")
16
+ * - TIMEZONE: Timezone for reflection schedule (default: "Asia/Tokyo")
15
17
  */
16
18
 
17
19
  const os = require('os')
@@ -87,6 +89,11 @@ const config = {
87
89
  // LLM_COMMAND="claude -p '{prompt}'"
88
90
  // LLM_COMMAND="gemini-cli --prompt '{prompt}'"
89
91
  LLM_COMMAND: process.env.LLM_COMMAND || '',
92
+
93
+ // Self-reflection schedule (自己反省時間)
94
+ // Daily time to automatically run end-of-day processing (log + memory extraction + session clear)
95
+ REFLECTION_TIME: process.env.REFLECTION_TIME ?? '03:00', // "HH:MM" format, empty = disabled
96
+ TIMEZONE: process.env.TIMEZONE || 'Asia/Tokyo',
90
97
  }
91
98
 
92
99
  /**
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Shared end-of-day processing logic
3
+ *
4
+ * Extracts daily conversation summary and long-term learnings from the chat session.
5
+ * Used by both the chat route (manual trigger) and the reflection scheduler (automatic).
6
+ *
7
+ * @module core/lib/end-of-day
8
+ */
9
+
10
+ const chatStore = require('../stores/chat-store')
11
+ const memoryStore = require('../stores/memory-store')
12
+ const dailyLogStore = require('../stores/daily-log-store')
13
+
14
+ /**
15
+ * Run end-of-day processing: generate daily log + extract memories from conversation.
16
+ *
17
+ * @param {Object} opts
18
+ * @param {(prompt: string) => Promise<string>} opts.runQuickLlmCall - Platform-specific LLM call function
19
+ * @param {boolean} [opts.clearSession=false] - Whether to clear the chat session after processing
20
+ * @returns {Promise<{ daily_log: string|null, memory_entries_added: number }>}
21
+ */
22
+ async function runEndOfDay({ runQuickLlmCall, clearSession = false }) {
23
+ const session = await chatStore.load()
24
+ if (!session || session.messages.length === 0) {
25
+ return { daily_log: null, memory_entries_added: 0 }
26
+ }
27
+
28
+ // Build conversation text for summarization (last 50 messages)
29
+ const messages = session.messages.slice(-50)
30
+ const conversationText = messages
31
+ .map(m => `${m.role === 'user' ? 'ユーザー' : 'アシスタント'}: ${m.content.substring(0, 500)}`)
32
+ .join('\n\n')
33
+
34
+ const endOfDayPrompt = `以下の会話を分析して、JSON形式で2つの情報を出力してください。
35
+
36
+ 1. daily_summary: 今日の会話の要約(マークダウン形式)。以下を含めてください:
37
+ - 今日やったこと・話したこと
38
+ - 未解決の課題・明日への申し送り
39
+ - 重要な決定事項
40
+
41
+ 2. learnings: 長期的に記憶すべき学び(配列)。各要素は以下の形式:
42
+ - title: 短いタイトル
43
+ - category: user(ユーザーの好み)/ feedback(フィードバック)/ project(プロジェクト情報)/ reference(参照情報)のいずれか
44
+ - content: 詳細な内容
45
+
46
+ 学びがない場合は空配列にしてください。すでに常識的なことは含めないでください。
47
+
48
+ 会話:
49
+ ${conversationText}
50
+
51
+ JSON形式のみで回答してください(\`\`\`jsonブロックは不要):`
52
+
53
+ const today = new Date().toISOString().split('T')[0]
54
+ let dailyLog = null
55
+ let memoriesAdded = 0
56
+
57
+ try {
58
+ const result = await runQuickLlmCall(endOfDayPrompt)
59
+
60
+ // Parse JSON response
61
+ let parsed
62
+ try {
63
+ // Try to extract JSON from the response (may be wrapped in markdown code blocks)
64
+ const jsonMatch = result.match(/\{[\s\S]*\}/)
65
+ parsed = jsonMatch ? JSON.parse(jsonMatch[0]) : JSON.parse(result)
66
+ } catch {
67
+ // Fallback: use raw result as daily summary
68
+ parsed = { daily_summary: result, learnings: [] }
69
+ }
70
+
71
+ // Save daily log
72
+ if (parsed.daily_summary) {
73
+ dailyLog = parsed.daily_summary
74
+ await dailyLogStore.saveLog(today, dailyLog)
75
+ console.log(`[EndOfDay] Saved daily log for ${today}`)
76
+ }
77
+
78
+ // Save learnings to memory
79
+ if (Array.isArray(parsed.learnings)) {
80
+ for (const learning of parsed.learnings) {
81
+ if (learning.title && learning.content) {
82
+ await memoryStore.saveEntry({
83
+ title: learning.title,
84
+ category: learning.category || 'reference',
85
+ content: learning.content,
86
+ })
87
+ memoriesAdded++
88
+ }
89
+ }
90
+ if (memoriesAdded > 0) {
91
+ console.log(`[EndOfDay] Saved ${memoriesAdded} memory entries`)
92
+ }
93
+ }
94
+ } catch (err) {
95
+ console.error('[EndOfDay] Summarization failed:', err.message)
96
+
97
+ // Fallback: save raw conversation summary as daily log
98
+ const fallback = messages.slice(-8)
99
+ .map(m => `**${m.role === 'user' ? 'ユーザー' : 'アシスタント'}**: ${m.content.substring(0, 300)}`)
100
+ .join('\n\n')
101
+ dailyLog = `# ${today} 会話ログ\n\n${fallback}`
102
+ await dailyLogStore.saveLog(today, dailyLog)
103
+ }
104
+
105
+ if (clearSession) {
106
+ await chatStore.clear()
107
+ console.log('[EndOfDay] Session cleared')
108
+ }
109
+
110
+ return { daily_log: today, memory_entries_added: memoriesAdded }
111
+ }
112
+
113
+ module.exports = { runEndOfDay }
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Reflection Scheduler (自己反省スケジューラ)
3
+ *
4
+ * Built-in server-level scheduler that triggers end-of-day processing
5
+ * (daily log generation + memory extraction + session clear) at a configured time.
6
+ *
7
+ * Unlike routines (user-deletable), this is a core architectural feature
8
+ * that ensures the minion regularly consolidates its conversation history.
9
+ *
10
+ * Configuration:
11
+ * REFLECTION_TIME - Time to run daily reflection (format: "HH:MM", e.g., "03:00")
12
+ * TIMEZONE - Timezone for the schedule (default: "Asia/Tokyo")
13
+ *
14
+ * @module core/lib/reflection-scheduler
15
+ */
16
+
17
+ const { Cron } = require('croner')
18
+ const { config } = require('../config')
19
+ const { runEndOfDay } = require('./end-of-day')
20
+
21
+ /** @type {import('croner').Cron | null} */
22
+ let cronJob = null
23
+
24
+ /** @type {boolean} */
25
+ let isRunning = false
26
+
27
+ /** @type {((prompt: string) => Promise<string>) | null} */
28
+ let llmCallFn = null
29
+
30
+ /**
31
+ * Parse "HH:MM" into a cron expression "0 MM HH * * *"
32
+ * @param {string} timeStr - Time in "HH:MM" format
33
+ * @returns {string|null} Cron expression or null if invalid
34
+ */
35
+ function parseToCron(timeStr) {
36
+ if (!timeStr || typeof timeStr !== 'string') return null
37
+ const match = timeStr.match(/^(\d{1,2}):(\d{2})$/)
38
+ if (!match) return null
39
+ const hour = parseInt(match[1], 10)
40
+ const minute = parseInt(match[2], 10)
41
+ if (hour < 0 || hour > 23 || minute < 0 || minute > 59) return null
42
+ return `0 ${minute} ${hour} * * *`
43
+ }
44
+
45
+ /**
46
+ * Start the reflection scheduler.
47
+ * If REFLECTION_TIME is not configured, the scheduler remains inactive.
48
+ *
49
+ * @param {(prompt: string) => Promise<string>} runQuickLlmCall - Platform-specific LLM call function
50
+ */
51
+ function start(runQuickLlmCall) {
52
+ llmCallFn = runQuickLlmCall
53
+
54
+ const reflectionTime = config.REFLECTION_TIME
55
+ if (!reflectionTime) {
56
+ console.log('[ReflectionScheduler] REFLECTION_TIME not configured, scheduler inactive')
57
+ return
58
+ }
59
+
60
+ const cronExpr = parseToCron(reflectionTime)
61
+ if (!cronExpr) {
62
+ console.error(`[ReflectionScheduler] Invalid REFLECTION_TIME: "${reflectionTime}" (expected HH:MM)`)
63
+ return
64
+ }
65
+
66
+ const timezone = config.TIMEZONE || 'Asia/Tokyo'
67
+
68
+ try {
69
+ cronJob = new Cron(cronExpr, { timezone }, async () => {
70
+ await runReflection()
71
+ })
72
+
73
+ const nextRun = cronJob.nextRun()
74
+ console.log(`[ReflectionScheduler] Scheduled at ${reflectionTime} (${timezone}), next run: ${nextRun ? nextRun.toISOString() : 'unknown'}`)
75
+ } catch (err) {
76
+ console.error(`[ReflectionScheduler] Failed to start: ${err.message}`)
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Stop the reflection scheduler.
82
+ */
83
+ function stop() {
84
+ if (cronJob) {
85
+ cronJob.stop()
86
+ cronJob = null
87
+ console.log('[ReflectionScheduler] Stopped')
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Reschedule after config change (e.g., REFLECTION_TIME or TIMEZONE updated via API).
93
+ * Requires that start() was called previously with a runQuickLlmCall function.
94
+ */
95
+ function reschedule() {
96
+ if (!llmCallFn) {
97
+ console.warn('[ReflectionScheduler] Cannot reschedule: no LLM call function registered')
98
+ return
99
+ }
100
+ stop()
101
+ start(llmCallFn)
102
+ }
103
+
104
+ /**
105
+ * Execute the reflection (end-of-day processing).
106
+ * Guards against concurrent execution.
107
+ */
108
+ async function runReflection() {
109
+ if (isRunning) {
110
+ console.log('[ReflectionScheduler] Already running, skipping')
111
+ return
112
+ }
113
+
114
+ if (!llmCallFn) {
115
+ console.error('[ReflectionScheduler] No LLM call function registered')
116
+ return
117
+ }
118
+
119
+ isRunning = true
120
+ console.log('[ReflectionScheduler] Starting daily reflection...')
121
+
122
+ try {
123
+ const result = await runEndOfDay({
124
+ runQuickLlmCall: llmCallFn,
125
+ clearSession: true,
126
+ })
127
+
128
+ if (result.daily_log) {
129
+ console.log(`[ReflectionScheduler] Completed: log=${result.daily_log}, memories=${result.memory_entries_added}`)
130
+ } else {
131
+ console.log('[ReflectionScheduler] Completed: no conversation to process')
132
+ }
133
+ } catch (err) {
134
+ console.error(`[ReflectionScheduler] Failed: ${err.message}`)
135
+ } finally {
136
+ isRunning = false
137
+ }
138
+ }
139
+
140
+ /**
141
+ * Get the current scheduler status.
142
+ * @returns {{ enabled: boolean, reflection_time: string, timezone: string, next_run: string|null }}
143
+ */
144
+ function getStatus() {
145
+ const reflectionTime = config.REFLECTION_TIME || ''
146
+ const timezone = config.TIMEZONE || 'Asia/Tokyo'
147
+ const nextRun = cronJob ? cronJob.nextRun() : null
148
+
149
+ return {
150
+ enabled: !!cronJob,
151
+ reflection_time: reflectionTime,
152
+ timezone,
153
+ next_run: nextRun ? nextRun.toISOString() : null,
154
+ }
155
+ }
156
+
157
+ module.exports = { start, stop, reschedule, getStatus }
@@ -0,0 +1,122 @@
1
+ /**
2
+ * Daily log endpoints
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.
6
+ *
7
+ * Endpoints:
8
+ * GET /api/daily-logs - List all logs (date + size)
9
+ * POST /api/daily-logs - Create a new daily log
10
+ * GET /api/daily-logs/:date - Get a specific day's log
11
+ * PUT /api/daily-logs/:date - Update a daily log
12
+ * DELETE /api/daily-logs/:date - Delete a specific day's log
13
+ */
14
+
15
+ const { verifyToken } = require('../lib/auth')
16
+ const dailyLogStore = require('../stores/daily-log-store')
17
+
18
+ /**
19
+ * Register daily log routes as Fastify plugin
20
+ * @param {import('fastify').FastifyInstance} fastify
21
+ */
22
+ async function dailyLogRoutes(fastify) {
23
+
24
+ // GET /api/daily-logs - List all logs
25
+ fastify.get('/api/daily-logs', async (request, reply) => {
26
+ if (!verifyToken(request)) {
27
+ reply.code(401)
28
+ return { success: false, error: 'Unauthorized' }
29
+ }
30
+
31
+ const logs = await dailyLogStore.listLogs()
32
+ return { success: true, logs }
33
+ })
34
+
35
+ // POST /api/daily-logs - Create a new daily log
36
+ fastify.post('/api/daily-logs', async (request, reply) => {
37
+ if (!verifyToken(request)) {
38
+ reply.code(401)
39
+ return { success: false, error: 'Unauthorized' }
40
+ }
41
+
42
+ const { date, content } = request.body || {}
43
+ if (!date || !content) {
44
+ reply.code(400)
45
+ return { success: false, error: 'date and content are required' }
46
+ }
47
+
48
+ // Check if log already exists
49
+ const existing = await dailyLogStore.loadLog(date)
50
+ if (existing !== null) {
51
+ reply.code(409)
52
+ return { success: false, error: 'Log already exists for this date. Use PUT to update.' }
53
+ }
54
+
55
+ await dailyLogStore.saveLog(date, content)
56
+ console.log(`[DailyLogs] Created log: ${date}`)
57
+ return { success: true, log: { date, content } }
58
+ })
59
+
60
+ // GET /api/daily-logs/:date - Get a specific day's log
61
+ fastify.get('/api/daily-logs/:date', async (request, reply) => {
62
+ if (!verifyToken(request)) {
63
+ reply.code(401)
64
+ return { success: false, error: 'Unauthorized' }
65
+ }
66
+
67
+ const { date } = request.params
68
+ const content = await dailyLogStore.loadLog(date)
69
+ if (content === null) {
70
+ reply.code(404)
71
+ return { success: false, error: 'Log not found' }
72
+ }
73
+
74
+ return { success: true, log: { date, content } }
75
+ })
76
+
77
+ // PUT /api/daily-logs/:date - Update a daily log
78
+ fastify.put('/api/daily-logs/:date', async (request, reply) => {
79
+ if (!verifyToken(request)) {
80
+ reply.code(401)
81
+ return { success: false, error: 'Unauthorized' }
82
+ }
83
+
84
+ const { date } = request.params
85
+ const { content } = request.body || {}
86
+ if (!content) {
87
+ reply.code(400)
88
+ return { success: false, error: 'content is required' }
89
+ }
90
+
91
+ // Check if log exists
92
+ const existing = await dailyLogStore.loadLog(date)
93
+ if (existing === null) {
94
+ reply.code(404)
95
+ return { success: false, error: 'Log not found' }
96
+ }
97
+
98
+ await dailyLogStore.saveLog(date, content)
99
+ console.log(`[DailyLogs] Updated log: ${date}`)
100
+ return { success: true, log: { date, content } }
101
+ })
102
+
103
+ // DELETE /api/daily-logs/:date - Delete a specific day's log
104
+ fastify.delete('/api/daily-logs/:date', async (request, reply) => {
105
+ if (!verifyToken(request)) {
106
+ reply.code(401)
107
+ return { success: false, error: 'Unauthorized' }
108
+ }
109
+
110
+ const { date } = request.params
111
+ const deleted = await dailyLogStore.deleteLog(date)
112
+ if (!deleted) {
113
+ reply.code(404)
114
+ return { success: false, error: 'Log not found' }
115
+ }
116
+
117
+ console.log(`[DailyLogs] Deleted log: ${date}`)
118
+ return { success: true }
119
+ })
120
+ }
121
+
122
+ module.exports = { dailyLogRoutes }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * Memory management endpoints
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/.
6
+ *
7
+ * Endpoints:
8
+ * GET /api/memory - List all memory entries
9
+ * GET /api/memory/:id - Get a single entry
10
+ * POST /api/memory - Create a new entry
11
+ * PUT /api/memory/:id - Update an entry
12
+ * DELETE /api/memory/:id - Delete an entry
13
+ */
14
+
15
+ const { verifyToken } = require('../lib/auth')
16
+ const memoryStore = require('../stores/memory-store')
17
+
18
+ /**
19
+ * Register memory routes as Fastify plugin
20
+ * @param {import('fastify').FastifyInstance} fastify
21
+ */
22
+ async function memoryRoutes(fastify) {
23
+
24
+ // GET /api/memory - List all entries
25
+ fastify.get('/api/memory', async (request, reply) => {
26
+ if (!verifyToken(request)) {
27
+ reply.code(401)
28
+ return { success: false, error: 'Unauthorized' }
29
+ }
30
+
31
+ const entries = await memoryStore.listEntries()
32
+ return { success: true, entries }
33
+ })
34
+
35
+ // GET /api/memory/:id - Get a single entry
36
+ fastify.get('/api/memory/:id', async (request, reply) => {
37
+ if (!verifyToken(request)) {
38
+ reply.code(401)
39
+ return { success: false, error: 'Unauthorized' }
40
+ }
41
+
42
+ const entry = await memoryStore.loadEntry(request.params.id)
43
+ if (!entry) {
44
+ reply.code(404)
45
+ return { success: false, error: 'Entry not found' }
46
+ }
47
+
48
+ return { success: true, entry }
49
+ })
50
+
51
+ // POST /api/memory - Create a new entry
52
+ fastify.post('/api/memory', async (request, reply) => {
53
+ if (!verifyToken(request)) {
54
+ reply.code(401)
55
+ return { success: false, error: 'Unauthorized' }
56
+ }
57
+
58
+ const { title, category, content } = request.body || {}
59
+ if (!title || !content) {
60
+ reply.code(400)
61
+ return { success: false, error: 'title and content are required' }
62
+ }
63
+
64
+ const entry = await memoryStore.saveEntry({ title, category, content })
65
+ console.log(`[Memory] Created entry: ${entry.id} (${entry.title})`)
66
+ return { success: true, entry }
67
+ })
68
+
69
+ // PUT /api/memory/:id - Update an entry
70
+ fastify.put('/api/memory/:id', async (request, reply) => {
71
+ if (!verifyToken(request)) {
72
+ reply.code(401)
73
+ return { success: false, error: 'Unauthorized' }
74
+ }
75
+
76
+ const existing = await memoryStore.loadEntry(request.params.id)
77
+ if (!existing) {
78
+ reply.code(404)
79
+ return { success: false, error: 'Entry not found' }
80
+ }
81
+
82
+ const { title, category, content } = request.body || {}
83
+ const entry = await memoryStore.saveEntry({
84
+ id: request.params.id,
85
+ title: title || existing.title,
86
+ category: category || existing.category,
87
+ content: content !== undefined ? content : existing.content,
88
+ })
89
+
90
+ console.log(`[Memory] Updated entry: ${entry.id} (${entry.title})`)
91
+ return { success: true, entry }
92
+ })
93
+
94
+ // DELETE /api/memory/:id - Delete an entry
95
+ fastify.delete('/api/memory/:id', async (request, reply) => {
96
+ if (!verifyToken(request)) {
97
+ reply.code(401)
98
+ return { success: false, error: 'Unauthorized' }
99
+ }
100
+
101
+ const deleted = await memoryStore.deleteEntry(request.params.id)
102
+ if (!deleted) {
103
+ reply.code(404)
104
+ return { success: false, error: 'Entry not found' }
105
+ }
106
+
107
+ console.log(`[Memory] Deleted entry: ${request.params.id}`)
108
+ return { success: true }
109
+ })
110
+ }
111
+
112
+ module.exports = { memoryRoutes }
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Daily Log Store
3
+ * Stores daily conversation summaries as markdown files.
4
+ * One file per day: $DATA_DIR/daily-logs/YYYY-MM-DD.md
5
+ *
6
+ * These logs serve as short-term memory (recent days),
7
+ * bridging working memory (chat session) and long-term memory (memory-store).
8
+ */
9
+
10
+ const fs = require('fs').promises
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
+ }
36
+
37
+ /**
38
+ * Validate date string format (YYYY-MM-DD)
39
+ */
40
+ function isValidDate(date) {
41
+ return /^\d{4}-\d{2}-\d{2}$/.test(date)
42
+ }
43
+
44
+ /**
45
+ * List all daily logs with date and file size.
46
+ * @returns {Promise<Array<{ date: string, size: number }>>} Sorted descending by date
47
+ */
48
+ async function listLogs() {
49
+ await ensureDir()
50
+ let files
51
+ try {
52
+ files = await fs.readdir(LOGS_DIR)
53
+ } catch {
54
+ return []
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({ date, size: stat.size })
65
+ } catch {
66
+ // Skip unreadable
67
+ }
68
+ }
69
+
70
+ // Sort by date descending (most recent first)
71
+ logs.sort((a, b) => b.date.localeCompare(a.date))
72
+ return logs
73
+ }
74
+
75
+ /**
76
+ * Load a specific day's log.
77
+ * @param {string} date - YYYY-MM-DD format
78
+ * @returns {Promise<string|null>} Log content or null
79
+ */
80
+ async function loadLog(date) {
81
+ if (!isValidDate(date)) return null
82
+ try {
83
+ return await fs.readFile(path.join(LOGS_DIR, `${date}.md`), 'utf-8')
84
+ } catch (err) {
85
+ if (err.code === 'ENOENT') return null
86
+ console.error(`[DailyLogStore] Failed to load log ${date}: ${err.message}`)
87
+ return null
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Save a daily log. Overwrites existing log for the same date.
93
+ * @param {string} date - YYYY-MM-DD format
94
+ * @param {string} content - Markdown content
95
+ */
96
+ async function saveLog(date, content) {
97
+ if (!isValidDate(date)) {
98
+ throw new Error(`Invalid date format: ${date}. Expected YYYY-MM-DD.`)
99
+ }
100
+ await ensureDir()
101
+ await fs.writeFile(path.join(LOGS_DIR, `${date}.md`), content, 'utf-8')
102
+ }
103
+
104
+ /**
105
+ * Delete a daily log.
106
+ * @param {string} date - YYYY-MM-DD format
107
+ * @returns {Promise<boolean>}
108
+ */
109
+ async function deleteLog(date) {
110
+ if (!isValidDate(date)) return false
111
+ try {
112
+ await fs.unlink(path.join(LOGS_DIR, `${date}.md`))
113
+ return true
114
+ } catch (err) {
115
+ if (err.code === 'ENOENT') return false
116
+ console.error(`[DailyLogStore] Failed to delete log ${date}: ${err.message}`)
117
+ return false
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Get the most recent N days of logs.
123
+ * @param {number} days - Number of recent days to fetch
124
+ * @returns {Promise<Array<{ date: string, content: string }>>}
125
+ */
126
+ async function getRecentLogs(days = 3) {
127
+ const all = await listLogs()
128
+ const recent = all.slice(0, days)
129
+
130
+ const results = []
131
+ for (const { date } of recent) {
132
+ const content = await loadLog(date)
133
+ if (content) {
134
+ results.push({ date, content })
135
+ }
136
+ }
137
+ return results
138
+ }
139
+
140
+ /**
141
+ * Get a context snippet of recent daily logs for chat injection.
142
+ * @param {number} days - Number of days to include
143
+ * @param {number} maxChars - Maximum total characters
144
+ * @returns {Promise<string>}
145
+ */
146
+ async function getContextSnippet(days = 3, maxChars = 1500) {
147
+ const logs = await getRecentLogs(days)
148
+ if (logs.length === 0) return ''
149
+
150
+ const parts = []
151
+ let totalLen = 0
152
+
153
+ for (const log of logs) {
154
+ const header = `### ${log.date}`
155
+ const content = log.content.substring(0, Math.floor(maxChars / days))
156
+ const section = `${header}\n${content}`
157
+
158
+ if (totalLen + section.length > maxChars) {
159
+ // Add truncated version
160
+ const remaining = maxChars - totalLen
161
+ if (remaining > 50) {
162
+ parts.push(section.substring(0, remaining) + '...')
163
+ }
164
+ break
165
+ }
166
+ parts.push(section)
167
+ totalLen += section.length + 2
168
+ }
169
+
170
+ return parts.join('\n\n')
171
+ }
172
+
173
+ module.exports = {
174
+ ensureDir,
175
+ listLogs,
176
+ loadLog,
177
+ saveLog,
178
+ deleteLog,
179
+ getRecentLogs,
180
+ getContextSnippet,
181
+ LOGS_DIR,
182
+ }