@geekbeer/minion 2.44.0 → 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,105 @@
1
+ /**
2
+ * Capability checker
3
+ *
4
+ * Detects MCP servers configured in ~/.mcp.json and available CLI tools.
5
+ * Results are cached in memory for 5 minutes to avoid excessive filesystem/process checks.
6
+ * Follows the same caching pattern as llm-checker.js.
7
+ */
8
+
9
+ const fs = require('fs')
10
+ const path = require('path')
11
+ const { execSync } = require('child_process')
12
+ const { config } = require('../config')
13
+ const { IS_WINDOWS, buildExtendedPath } = require('./platform')
14
+
15
+ const CACHE_TTL_MS = 300000 // 5 minutes
16
+
17
+ let cachedResult = null
18
+ let cachedAt = 0
19
+
20
+ /**
21
+ * Detect MCP servers from ~/.mcp.json.
22
+ * Parses the Claude Code MCP configuration and returns configured server names.
23
+ * @returns {{ name: string, configured: boolean }[]}
24
+ */
25
+ function getMcpServers() {
26
+ const mcpPath = path.join(config.HOME_DIR, '.mcp.json')
27
+ try {
28
+ if (!fs.existsSync(mcpPath)) return []
29
+ const content = fs.readFileSync(mcpPath, 'utf-8')
30
+ const parsed = JSON.parse(content)
31
+ const servers = parsed.mcpServers || parsed.servers || {}
32
+ return Object.keys(servers).map(name => ({
33
+ name,
34
+ configured: true,
35
+ }))
36
+ } catch {
37
+ return []
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Check if a CLI tool is available and get its version.
43
+ * @param {string} name - Tool name (e.g., 'git', 'node')
44
+ * @returns {{ name: string, available: boolean, version?: string }}
45
+ */
46
+ function checkTool(name) {
47
+ const extendedPath = buildExtendedPath(config.HOME_DIR)
48
+ const env = {
49
+ ...process.env,
50
+ HOME: config.HOME_DIR,
51
+ ...(IS_WINDOWS && { USERPROFILE: config.HOME_DIR }),
52
+ PATH: extendedPath,
53
+ }
54
+ const execOpts = { encoding: 'utf-8', timeout: 5000, stdio: 'pipe', env }
55
+
56
+ try {
57
+ // Check existence
58
+ const whichCmd = IS_WINDOWS ? 'where' : 'which'
59
+ execSync(`${whichCmd} ${name}`, execOpts)
60
+
61
+ // Get version
62
+ let version = null
63
+ try {
64
+ const out = execSync(`${name} --version`, execOpts).trim()
65
+ // Extract version number from output (e.g., "git version 2.43.0" → "2.43.0")
66
+ const match = out.match(/(\d+\.\d+[\.\d]*)/)
67
+ if (match) version = match[1]
68
+ } catch {
69
+ // Tool exists but --version failed, that's ok
70
+ }
71
+
72
+ return { name, available: true, ...(version && { version }) }
73
+ } catch {
74
+ return { name, available: false }
75
+ }
76
+ }
77
+
78
+ /** CLI tools to detect */
79
+ const TOOL_NAMES = ['git', 'node', 'npx', 'claude', 'docker', 'tmux']
80
+
81
+ /**
82
+ * Get all capabilities (MCP servers + CLI tools), cached for 5 minutes.
83
+ * @returns {{ mcp_servers: { name: string, configured: boolean }[], cli_tools: { name: string, available: boolean, version?: string }[] }}
84
+ */
85
+ function getCapabilities() {
86
+ const now = Date.now()
87
+ if (cachedResult && (now - cachedAt) < CACHE_TTL_MS) {
88
+ return cachedResult
89
+ }
90
+
91
+ const mcp_servers = getMcpServers()
92
+ const cli_tools = TOOL_NAMES.map(name => checkTool(name))
93
+
94
+ cachedResult = { mcp_servers, cli_tools }
95
+ cachedAt = now
96
+ return cachedResult
97
+ }
98
+
99
+ /** Clear the cached capability status */
100
+ function clearCapabilityCache() {
101
+ cachedResult = null
102
+ cachedAt = 0
103
+ }
104
+
105
+ module.exports = { getCapabilities, clearCapabilityCache }
@@ -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 }
@@ -94,6 +94,7 @@ async function executeStep(step) {
94
94
  assigned_role,
95
95
  skill_name,
96
96
  revision_feedback,
97
+ extra_env,
97
98
  } = step
98
99
 
99
100
  console.log(
@@ -151,6 +152,9 @@ async function executeStep(step) {
151
152
  if (revision_feedback) {
152
153
  runPayload.revision_feedback = revision_feedback
153
154
  }
155
+ if (extra_env && typeof extra_env === 'object') {
156
+ runPayload.extra_env = extra_env
157
+ }
154
158
 
155
159
  const runUrl = `http://localhost:${config.AGENT_PORT || 8080}/api/skills/run`
156
160
  const runResp = await fetch(runUrl, {
@@ -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 }
@@ -11,6 +11,7 @@ const { version } = require('../../package.json')
11
11
  const { config, isHqConfigured } = require('../config')
12
12
  const { sendHeartbeat } = require('../api')
13
13
  const { getLlmServices, isLlmCommandConfigured } = require('../lib/llm-checker')
14
+ const { getCapabilities } = require('../lib/capability-checker')
14
15
 
15
16
  function maskToken(token) {
16
17
  if (!token || token.length < 8) return token ? '***' : ''
@@ -58,6 +59,7 @@ async function healthRoutes(fastify) {
58
59
  timestamp: new Date().toISOString(),
59
60
  llm_services: getLlmServices(),
60
61
  llm_command_configured: isLlmCommandConfigured(),
62
+ capabilities: getCapabilities(),
61
63
  env: {
62
64
  HQ_URL: config.HQ_URL || '',
63
65
  MINION_ID: config.MINION_ID || '',
@@ -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 }
@@ -291,7 +291,7 @@ async function skillRoutes(fastify, opts) {
291
291
  return { success: false, error: 'Unauthorized' }
292
292
  }
293
293
 
294
- const { skill_name, execution_id, step_index, workflow_name, role, revision_feedback } = request.body || {}
294
+ const { skill_name, execution_id, step_index, workflow_name, role, revision_feedback, extra_env } = request.body || {}
295
295
 
296
296
  if (!skill_name) {
297
297
  reply.code(400)
@@ -340,6 +340,7 @@ async function skillRoutes(fastify, opts) {
340
340
  const runOptions = {}
341
341
  if (role) runOptions.role = role
342
342
  if (revision_feedback) runOptions.revisionFeedback = revision_feedback
343
+ if (extra_env && typeof extra_env === 'object') runOptions.extraEnv = extra_env
343
344
 
344
345
  // Run asynchronously — respond immediately
345
346
  const executionPromise = (async () => {