@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.
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Variable Store
3
+ *
4
+ * Manages minion-local secrets and variables stored in .env-style files.
5
+ * - Secrets: ~/.minion/.env.secrets (or DATA_DIR/.env.secrets)
6
+ * - Variables: ~/.minion/.env.variables (or DATA_DIR/.env.variables)
7
+ *
8
+ * Files use standard .env format: KEY=value (one per line, # for comments).
9
+ * Secrets never leave the minion; variables are non-sensitive configuration.
10
+ */
11
+
12
+ const fs = require('fs')
13
+ const path = require('path')
14
+ const { DATA_DIR } = require('../lib/platform')
15
+ const { config } = require('../config')
16
+
17
+ /**
18
+ * Resolve file path for a given store type.
19
+ * @param {'secrets' | 'variables'} type
20
+ * @returns {string}
21
+ */
22
+ function getFilePath(type) {
23
+ const filename = type === 'secrets' ? '.env.secrets' : '.env.variables'
24
+ try {
25
+ fs.accessSync(DATA_DIR, fs.constants.W_OK)
26
+ return path.join(DATA_DIR, filename)
27
+ } catch {
28
+ return path.join(config.HOME_DIR, '.minion', filename)
29
+ }
30
+ }
31
+
32
+ /**
33
+ * Parse a .env file into a key-value object.
34
+ * @param {string} filePath
35
+ * @returns {Record<string, string>}
36
+ */
37
+ function parseEnvFile(filePath) {
38
+ const result = {}
39
+ try {
40
+ const content = fs.readFileSync(filePath, 'utf-8')
41
+ for (const line of content.split('\n')) {
42
+ const trimmed = line.trim()
43
+ if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue
44
+ const eqIdx = trimmed.indexOf('=')
45
+ const key = trimmed.slice(0, eqIdx).trim()
46
+ const value = trimmed.slice(eqIdx + 1).trim()
47
+ if (key) result[key] = value
48
+ }
49
+ } catch (err) {
50
+ if (err.code !== 'ENOENT') {
51
+ console.error(`[VariableStore] Failed to read ${filePath}: ${err.message}`)
52
+ }
53
+ }
54
+ return result
55
+ }
56
+
57
+ /**
58
+ * Write a key-value object to a .env file.
59
+ * @param {string} filePath
60
+ * @param {Record<string, string>} data
61
+ */
62
+ function writeEnvFile(filePath, data) {
63
+ const dir = path.dirname(filePath)
64
+ fs.mkdirSync(dir, { recursive: true })
65
+
66
+ const lines = Object.entries(data).map(([key, value]) => `${key}=${value}`)
67
+ fs.writeFileSync(filePath, lines.join('\n') + '\n', 'utf-8')
68
+ }
69
+
70
+ /**
71
+ * Get all key-value pairs for a store type.
72
+ * @param {'secrets' | 'variables'} type
73
+ * @returns {Record<string, string>}
74
+ */
75
+ function getAll(type) {
76
+ return parseEnvFile(getFilePath(type))
77
+ }
78
+
79
+ /**
80
+ * Get a single value by key.
81
+ * @param {'secrets' | 'variables'} type
82
+ * @param {string} key
83
+ * @returns {string | null}
84
+ */
85
+ function get(type, key) {
86
+ const data = getAll(type)
87
+ return data[key] ?? null
88
+ }
89
+
90
+ /**
91
+ * Set a key-value pair (creates or updates).
92
+ * @param {'secrets' | 'variables'} type
93
+ * @param {string} key
94
+ * @param {string} value
95
+ */
96
+ function set(type, key, value) {
97
+ const filePath = getFilePath(type)
98
+ const data = parseEnvFile(filePath)
99
+ data[key] = value
100
+ writeEnvFile(filePath, data)
101
+ console.log(`[VariableStore] Set ${type} key: ${key}`)
102
+ }
103
+
104
+ /**
105
+ * Remove a key.
106
+ * @param {'secrets' | 'variables'} type
107
+ * @param {string} key
108
+ * @returns {boolean} true if key existed
109
+ */
110
+ function remove(type, key) {
111
+ const filePath = getFilePath(type)
112
+ const data = parseEnvFile(filePath)
113
+ if (!(key in data)) return false
114
+ delete data[key]
115
+ writeEnvFile(filePath, data)
116
+ console.log(`[VariableStore] Removed ${type} key: ${key}`)
117
+ return true
118
+ }
119
+
120
+ /**
121
+ * List all keys for a store type.
122
+ * @param {'secrets' | 'variables'} type
123
+ * @returns {string[]}
124
+ */
125
+ function listKeys(type) {
126
+ return Object.keys(getAll(type))
127
+ }
128
+
129
+ /**
130
+ * Build a merged environment object from minion variables and secrets.
131
+ * Used by workflow/routine runners to inject into execution environment.
132
+ * Secrets override variables when keys collide.
133
+ *
134
+ * @param {Record<string, string>} [extraVars] - Additional variables (e.g. project/workflow vars from HQ)
135
+ * @returns {Record<string, string>} Merged key-value pairs
136
+ */
137
+ function buildEnv(extraVars = {}) {
138
+ const variables = getAll('variables')
139
+ const secrets = getAll('secrets')
140
+ // Merge order: variables < secrets < extraVars (later wins)
141
+ return { ...variables, ...secrets, ...extraVars }
142
+ }
143
+
144
+ module.exports = {
145
+ getAll,
146
+ get,
147
+ set,
148
+ remove,
149
+ listKeys,
150
+ buildEnv,
151
+ getFilePath,
152
+ }
@@ -75,6 +75,134 @@ Files are stored in `~/files/`. Max upload size: 50MB.
75
75
  | POST | `/api/files/*` | Upload a file (Content-Type: `application/octet-stream`) |
76
76
  | DELETE | `/api/files/*` | Delete a file or directory |
77
77
 
78
+ ### Memory (Long-term Knowledge)
79
+
80
+ Persistent memory entries stored as markdown files in `$DATA_DIR/memory/`.
81
+ Categories: `user` (user preferences), `feedback` (feedback), `project` (project info), `reference` (references).
82
+
83
+ | Method | Endpoint | Description |
84
+ |--------|----------|-------------|
85
+ | GET | `/api/memory` | List all memory entries (id, title, category, excerpt, timestamps) |
86
+ | GET | `/api/memory/:id` | Get full memory entry |
87
+ | POST | `/api/memory` | Create memory entry. Body: `{title, category, content}` |
88
+ | PUT | `/api/memory/:id` | Update memory entry. Body: `{title?, category?, content?}` |
89
+ | DELETE | `/api/memory/:id` | Delete a memory entry |
90
+
91
+ POST/PUT body:
92
+ ```json
93
+ {
94
+ "title": "ユーザーはレビューで日本語を好む",
95
+ "category": "user",
96
+ "content": "コードレビューのコメントは日本語で記述すること。"
97
+ }
98
+ ```
99
+
100
+ Response (list):
101
+ ```json
102
+ {
103
+ "success": true,
104
+ "entries": [
105
+ {
106
+ "id": "abc123",
107
+ "title": "...",
108
+ "category": "user",
109
+ "excerpt": "...",
110
+ "created_at": "2026-03-12T09:00:00Z",
111
+ "updated_at": "2026-03-12T09:00:00Z"
112
+ }
113
+ ]
114
+ }
115
+ ```
116
+
117
+ ### Daily Logs (Short-term Memory)
118
+
119
+ Daily conversation summaries stored as `$DATA_DIR/daily-logs/YYYY-MM-DD.md`.
120
+ Generated via end-of-day processing or manual creation.
121
+
122
+ | Method | Endpoint | Description |
123
+ |--------|----------|-------------|
124
+ | GET | `/api/daily-logs` | List all logs (date + size, newest first) |
125
+ | POST | `/api/daily-logs` | Create a daily log. Body: `{date, content}` |
126
+ | GET | `/api/daily-logs/:date` | Get a specific day's log content |
127
+ | PUT | `/api/daily-logs/:date` | Update a daily log. Body: `{content}` |
128
+ | DELETE | `/api/daily-logs/:date` | Delete a specific day's log |
129
+
130
+ POST body:
131
+ ```json
132
+ {
133
+ "date": "2026-03-12",
134
+ "content": "## 今日やったこと\n- Feature X を実装\n- Bug Y を修正"
135
+ }
136
+ ```
137
+
138
+ PUT body:
139
+ ```json
140
+ {
141
+ "content": "## 今日やったこと\n- Feature X を実装(更新版)"
142
+ }
143
+ ```
144
+
145
+ Response (list):
146
+ ```json
147
+ {
148
+ "success": true,
149
+ "logs": [
150
+ { "date": "2026-03-12", "size": 1234 },
151
+ { "date": "2026-03-11", "size": 567 }
152
+ ]
153
+ }
154
+ ```
155
+
156
+ ### Chat (End-of-Day Processing)
157
+
158
+ | Method | Endpoint | Description |
159
+ |--------|----------|-------------|
160
+ | POST | `/api/chat/end-of-day` | Generate daily log + extract memories from conversation |
161
+
162
+ Body: `{ "clear_session": false }` (optional, defaults to false)
163
+
164
+ Response:
165
+ ```json
166
+ {
167
+ "success": true,
168
+ "daily_log": "2026-03-12",
169
+ "memory_entries_added": 2
170
+ }
171
+ ```
172
+
173
+ ### Self-Reflection Schedule (自己反省時間)
174
+
175
+ The minion has a built-in daily scheduler that automatically runs end-of-day processing
176
+ (daily log generation + memory extraction + session clear) at a configured time.
177
+
178
+ Configuration via `PUT /api/config/env`:
179
+
180
+ | Key | Format | Default | Description |
181
+ |-----|--------|---------|-------------|
182
+ | `REFLECTION_TIME` | `HH:MM` | (disabled) | Daily reflection time (e.g., `"03:00"`) |
183
+ | `TIMEZONE` | IANA tz | `Asia/Tokyo` | Timezone for the schedule |
184
+
185
+ Example — set reflection at 3:00 AM JST:
186
+ ```bash
187
+ curl -X PUT /api/config/env \
188
+ -H "Authorization: Bearer $TOKEN" \
189
+ -d '{"key": "REFLECTION_TIME", "value": "03:00"}'
190
+ ```
191
+
192
+ The scheduler starts automatically on server boot if `REFLECTION_TIME` is configured.
193
+ Changes via the config API take effect immediately (no restart required).
194
+
195
+ ### Config
196
+
197
+ | Method | Endpoint | Description |
198
+ |--------|----------|-------------|
199
+ | GET | `/api/config/env/:key` | Get env variable value (whitelisted keys only) |
200
+ | PUT | `/api/config/env` | Update env variable. Body: `{key, value}` |
201
+ | GET | `/api/config/backup` | Download config files as tar.gz |
202
+ | POST | `/api/config/restore` | Restore config from tar.gz |
203
+
204
+ Allowed keys: `LLM_COMMAND`, `REFLECTION_TIME`, `TIMEZONE`
205
+
78
206
  ### Commands
79
207
 
80
208
  | Method | Endpoint | Description |
@@ -321,6 +321,8 @@ do_setup() {
321
321
  {
322
322
  echo "${TARGET_USER} ALL=(root) NOPASSWD: ${NPM_BIN} install -g @geekbeer/minion@latest"
323
323
  echo "${TARGET_USER} ALL=(root) NOPASSWD: ${NPM_BIN} install -g @geekbeer/minion@latest --registry *"
324
+ echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/apt-get install *"
325
+ echo "${TARGET_USER} ALL=(root) NOPASSWD: /usr/bin/apt-get remove *"
324
326
 
325
327
  case "$PROC_MGR" in
326
328
  systemd)
@@ -336,6 +338,10 @@ do_setup() {
336
338
  } | $SUDO tee "$SUDOERS_FILE" > /dev/null
337
339
  $SUDO chmod 440 "$SUDOERS_FILE"
338
340
  echo " -> $SUDOERS_FILE created"
341
+
342
+ # Protect nodejs from apt-get remove (minion depends on Node.js runtime)
343
+ $SUDO apt-mark hold nodejs 2>/dev/null || true
344
+ echo " -> nodejs marked as hold (protected from removal)"
339
345
  else
340
346
  echo " -> Skipped (running as root)"
341
347
  fi
@@ -8,11 +8,12 @@
8
8
  * Requires LLM_COMMAND to be configured in minion.env.
9
9
  *
10
10
  * Endpoints:
11
- * POST /api/chat - Send message, get SSE stream
12
- * GET /api/chat/session - Get active session (messages + session_id)
13
- * POST /api/chat/clear - Clear session and start fresh
14
- * POST /api/chat/abort - Kill the active LLM CLI process
15
- * POST /api/chat/reset - Summarize conversation and start fresh session
11
+ * POST /api/chat - Send message, get SSE stream
12
+ * GET /api/chat/session - Get active session (messages + session_id)
13
+ * POST /api/chat/clear - Clear session and start fresh
14
+ * POST /api/chat/abort - Kill the active LLM CLI process
15
+ * POST /api/chat/reset - Summarize conversation and start fresh session
16
+ * POST /api/chat/end-of-day - Generate daily log + extract memories from conversation
16
17
  */
17
18
 
18
19
  const { spawn } = require('child_process')
@@ -21,6 +22,9 @@ const path = require('path')
21
22
  const { verifyToken } = require('../../core/lib/auth')
22
23
  const { config } = require('../../core/config')
23
24
  const chatStore = require('../../core/stores/chat-store')
25
+ const memoryStore = require('../../core/stores/memory-store')
26
+ const dailyLogStore = require('../../core/stores/daily-log-store')
27
+ const { runEndOfDay } = require('../../core/lib/end-of-day')
24
28
 
25
29
  /** @type {import('child_process').ChildProcess | null} */
26
30
  let activeChatChild = null
@@ -45,10 +49,8 @@ async function chatRoutes(fastify) {
45
49
  return { success: false, error: 'message is required' }
46
50
  }
47
51
 
48
- // Build prompt — add context prefix when context is available (new or resumed)
49
- const prompt = context
50
- ? buildContextPrefix(message, context)
51
- : message
52
+ // Build prompt — add memory context on new sessions + page context
53
+ const prompt = await buildContextPrefix(message, context, session_id)
52
54
 
53
55
  // Store user message
54
56
  const currentSessionId = session_id || null
@@ -175,16 +177,51 @@ async function chatRoutes(fastify) {
175
177
  console.log(`[Chat] session reset with summary (${summary?.length || 0} chars)`)
176
178
  return { success: true, summary }
177
179
  })
180
+
181
+ // POST /api/chat/end-of-day - Generate daily log + extract memories
182
+ fastify.post('/api/chat/end-of-day', async (request, reply) => {
183
+ if (!verifyToken(request)) {
184
+ reply.code(401)
185
+ return { success: false, error: 'Unauthorized' }
186
+ }
187
+
188
+ const { clear_session = false } = request.body || {}
189
+
190
+ const result = await runEndOfDay({
191
+ runQuickLlmCall,
192
+ clearSession: clear_session,
193
+ })
194
+
195
+ return { success: true, ...result }
196
+ })
178
197
  }
179
198
 
180
199
  /**
181
200
  * Build context prefix that tells Claude CLI where the user is on the HQ dashboard
182
201
  * and how to fetch details via the `hq` helper command.
202
+ * On new sessions (no session_id), injects minion memory + recent daily logs.
183
203
  * No conversation history injection — Claude CLI handles that via --resume.
184
204
  */
185
- function buildContextPrefix(message, context) {
205
+ async function buildContextPrefix(message, context, sessionId) {
186
206
  const parts = []
187
207
 
208
+ // Inject memory + daily logs on new sessions only (not on --resume)
209
+ if (!sessionId) {
210
+ try {
211
+ const memorySnippet = await memoryStore.getContextSnippet(2000)
212
+ const dailyLogSnippet = await dailyLogStore.getContextSnippet(3, 1500)
213
+
214
+ if (memorySnippet) {
215
+ parts.push('[ミニオンメモリ(長期記憶)]', memorySnippet, '')
216
+ }
217
+ if (dailyLogSnippet) {
218
+ parts.push('[最近のデイリーログ]', dailyLogSnippet, '')
219
+ }
220
+ } catch (err) {
221
+ console.error('[Chat] Failed to load memory/daily-log context:', err.message)
222
+ }
223
+ }
224
+
188
225
  if (context) {
189
226
  switch (context.type) {
190
227
  case 'skill':
@@ -285,7 +322,8 @@ function streamLlmResponse(res, prompt, sessionId) {
285
322
  args.push('--resume', sessionId)
286
323
  }
287
324
 
288
- args.push(prompt)
325
+ // Prompt is passed via stdin (not as CLI argument) to avoid
326
+ // shell argument parsing issues with spaces/special characters.
289
327
 
290
328
  console.log(`[Chat] spawning: ${binary} ${sessionId ? `--resume ${sessionId}` : '(new session)'} (cwd: ${config.HOME_DIR})`)
291
329
 
@@ -304,7 +342,8 @@ function streamLlmResponse(res, prompt, sessionId) {
304
342
  // Track active child process for abort
305
343
  activeChatChild = child
306
344
 
307
- // Close stdin immediately so CLI doesn't wait for input
345
+ // Write prompt to stdin and close claude -p reads from stdin when no positional arg
346
+ child.stdin.write(prompt)
308
347
  child.stdin.end()
309
348
 
310
349
  console.log(`[Chat] child PID: ${child.pid}`)
@@ -509,7 +548,7 @@ function runQuickLlmCall(prompt) {
509
548
  '/bin',
510
549
  ].join(':')
511
550
 
512
- const args = ['-p', '--model', 'haiku', '--max-turns', '1', '--output-format', 'json', prompt]
551
+ const args = ['-p', '--model', 'haiku', '--max-turns', '1', '--output-format', 'json']
513
552
 
514
553
  const child = spawn(binary, args, {
515
554
  cwd: config.HOME_DIR,
@@ -523,6 +562,7 @@ function runQuickLlmCall(prompt) {
523
562
  },
524
563
  })
525
564
 
565
+ child.stdin.write(prompt)
526
566
  child.stdin.end()
527
567
 
528
568
  let stdout = ''
@@ -548,4 +588,4 @@ function runQuickLlmCall(prompt) {
548
588
  })
549
589
  }
550
590
 
551
- module.exports = { chatRoutes }
591
+ module.exports = { chatRoutes, runQuickLlmCall }
@@ -15,8 +15,13 @@ const { clearLlmCache } = require('../../core/lib/llm-checker')
15
15
  const { config, updateConfig } = require('../../core/config')
16
16
  const { resolveEnvFilePath: resolveEnvFilePathFromPlatform } = require('../../core/lib/platform')
17
17
 
18
+ const reflectionScheduler = require('../../core/lib/reflection-scheduler')
19
+
18
20
  /** Keys that can be read/written via the config API */
19
- const ALLOWED_ENV_KEYS = ['LLM_COMMAND']
21
+ const ALLOWED_ENV_KEYS = ['LLM_COMMAND', 'REFLECTION_TIME', 'TIMEZONE']
22
+
23
+ /** Keys that trigger a reflection scheduler reschedule when changed */
24
+ const REFLECTION_KEYS = ['REFLECTION_TIME', 'TIMEZONE']
20
25
 
21
26
  const BACKUP_PATHS = [
22
27
  '~/.claude',
@@ -234,6 +239,11 @@ function configRoutes(fastify, _opts, done) {
234
239
  // Clear LLM cache so health check reflects immediately
235
240
  clearLlmCache()
236
241
 
242
+ // Reschedule reflection if relevant key changed
243
+ if (REFLECTION_KEYS.includes(key)) {
244
+ reflectionScheduler.reschedule()
245
+ }
246
+
237
247
  return { success: true, restart_required: false }
238
248
  } catch (err) {
239
249
  console.error(`[Config] Failed to update ${key} in ${envPath}:`, err.message)
@@ -22,6 +22,7 @@ const { config } = require('../core/config')
22
22
  const executionStore = require('../core/stores/execution-store')
23
23
  const routineStore = require('../core/stores/routine-store')
24
24
  const logManager = require('../core/lib/log-manager')
25
+ const variableStore = require('../core/stores/variable-store')
25
26
 
26
27
  // Active cron jobs keyed by routine ID
27
28
  const activeJobs = new Map()
@@ -155,8 +156,13 @@ async function executeRoutineSession(routine, executionId, skillNames) {
155
156
  const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
156
157
  const execCommand = `${llmCommand}; echo $? > ${exitCodeFile}`
157
158
 
159
+ // Build injected environment: minion variables/secrets (routines don't receive HQ vars)
160
+ const injectedEnv = variableStore.buildEnv()
161
+
158
162
  // Create tmux session with extended environment
159
163
  // Pass execution context as environment variables for /execution-report skill
164
+ const tmuxEnvFlags = Object.entries(injectedEnv)
165
+ .map(([k, v]) => `-e "${k}=${v.replace(/"/g, '\\"')}"`)
160
166
  const tmuxCommand = [
161
167
  'tmux new-session -d',
162
168
  `-s "${sessionName}"`,
@@ -167,6 +173,7 @@ async function executeRoutineSession(routine, executionId, skillNames) {
167
173
  `-e "MINION_EXECUTION_ID=${executionId}"`,
168
174
  `-e "MINION_ROUTINE_ID=${routine.id}"`,
169
175
  `-e "MINION_ROUTINE_NAME=${routine.name.replace(/"/g, '\\"')}"`,
176
+ ...tmuxEnvFlags,
170
177
  `"${execCommand}"`,
171
178
  ].join(' ')
172
179
 
package/linux/server.js CHANGED
@@ -16,6 +16,8 @@
16
16
  * Directives: POST /api/directive
17
17
  * Auth: GET /api/auth/status
18
18
  * Chat: POST /api/chat, GET /api/chat/session, POST /api/chat/clear
19
+ * Variables: GET/PUT/DELETE /api/variables, /api/variables/:key
20
+ * Secrets: GET /api/secrets, PUT/DELETE /api/secrets/:key
19
21
  * Config: GET /api/config/backup, GET/PUT /api/config/env
20
22
  * Executions: GET /api/executions, GET /api/executions/:id, etc.
21
23
  * ─────────────────────────────────────────────────────────────────────────────
@@ -33,6 +35,7 @@ const PACKAGE_ROOT = path.join(__dirname, '..')
33
35
  const { config, validate, isHqConfigured } = require('../core/config')
34
36
  const { sendHeartbeat } = require('../core/api')
35
37
  const { version } = require('../package.json')
38
+ const { getCapabilities } = require('../core/lib/capability-checker')
36
39
  const workflowStore = require('../core/stores/workflow-store')
37
40
  const routineStore = require('../core/stores/routine-store')
38
41
 
@@ -49,6 +52,7 @@ const { startTerminalProxy, stopTerminalProxy } = require('./terminal-proxy')
49
52
  // Pull-model daemons (from core/)
50
53
  const stepPoller = require('../core/lib/step-poller')
51
54
  const revisionWatcher = require('../core/lib/revision-watcher')
55
+ const reflectionScheduler = require('../core/lib/reflection-scheduler')
52
56
 
53
57
  // Shared routes (from core/)
54
58
  const { healthRoutes, setOffline } = require('../core/routes/health')
@@ -57,13 +61,16 @@ const { skillRoutes } = require('../core/routes/skills')
57
61
  const { workflowRoutes } = require('../core/routes/workflows')
58
62
  const { routineRoutes } = require('../core/routes/routines')
59
63
  const { authRoutes } = require('../core/routes/auth')
64
+ const { variableRoutes } = require('../core/routes/variables')
65
+ const { memoryRoutes } = require('../core/routes/memory')
66
+ const { dailyLogRoutes } = require('../core/routes/daily-logs')
60
67
 
61
68
  // Linux-specific routes
62
69
  const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
63
70
  const { terminalRoutes } = require('./routes/terminal')
64
71
  const { fileRoutes } = require('./routes/files')
65
72
  const { directiveRoutes } = require('./routes/directives')
66
- const { chatRoutes } = require('./routes/chat')
73
+ const { chatRoutes, runQuickLlmCall } = require('./routes/chat')
67
74
  const { configRoutes } = require('./routes/config')
68
75
 
69
76
  // Validate configuration before starting
@@ -89,7 +96,7 @@ async function shutdown(signal) {
89
96
  if (isHqConfigured()) {
90
97
  try {
91
98
  await Promise.race([
92
- sendHeartbeat({ status: 'offline', version }),
99
+ sendHeartbeat({ status: 'offline', version, capabilities: getCapabilities() }),
93
100
  new Promise(resolve => setTimeout(resolve, 3000)),
94
101
  ])
95
102
  } catch {
@@ -97,9 +104,10 @@ async function shutdown(signal) {
97
104
  }
98
105
  }
99
106
 
100
- // Stop pollers and runners
107
+ // Stop pollers, runners, and scheduler
101
108
  stepPoller.stop()
102
109
  revisionWatcher.stop()
110
+ reflectionScheduler.stop()
103
111
  workflowRunner.stopAll()
104
112
  routineRunner.stopAll()
105
113
 
@@ -252,6 +260,9 @@ async function registerAllRoutes(app) {
252
260
  await app.register(workflowRoutes, { workflowRunner })
253
261
  await app.register(routineRoutes, { routineRunner })
254
262
  await app.register(authRoutes)
263
+ await app.register(variableRoutes)
264
+ await app.register(memoryRoutes)
265
+ await app.register(dailyLogRoutes)
255
266
 
256
267
  // Linux-specific routes
257
268
  await app.register(commandRoutes)
@@ -311,20 +322,23 @@ async function start() {
311
322
  console.error('[Server] Failed to load cached routines:', err.message)
312
323
  }
313
324
 
325
+ // Start reflection scheduler (self-reflection time)
326
+ reflectionScheduler.start(runQuickLlmCall)
327
+
314
328
  if (isHqConfigured()) {
315
329
  console.log(`[Server] HQ URL: ${config.HQ_URL}`)
316
330
 
317
331
  // Send initial online heartbeat
318
332
  const { getStatus } = require('../core/routes/health')
319
333
  const { currentTask } = getStatus()
320
- sendHeartbeat({ status: 'online', current_task: currentTask, version }).catch(err => {
334
+ sendHeartbeat({ status: 'online', current_task: currentTask, version, capabilities: getCapabilities() }).catch(err => {
321
335
  console.error('[Heartbeat] Initial heartbeat failed:', err.message)
322
336
  })
323
337
 
324
338
  // Start periodic heartbeat
325
339
  heartbeatTimer = setInterval(() => {
326
340
  const { currentStatus, currentTask } = getStatus()
327
- sendHeartbeat({ status: currentStatus, current_task: currentTask, version }).catch(err => {
341
+ sendHeartbeat({ status: currentStatus, current_task: currentTask, version, capabilities: getCapabilities() }).catch(err => {
328
342
  console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
329
343
  })
330
344
  }, HEARTBEAT_INTERVAL_MS)
@@ -20,6 +20,7 @@ const execAsync = promisify(exec)
20
20
  const { config } = require('../core/config')
21
21
  const executionStore = require('../core/stores/execution-store')
22
22
  const workflowStore = require('../core/stores/workflow-store')
23
+ const variableStore = require('../core/stores/variable-store')
23
24
  const logManager = require('../core/lib/log-manager')
24
25
 
25
26
  // Active cron jobs keyed by workflow ID
@@ -117,7 +118,12 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
117
118
  const llmCommand = config.LLM_COMMAND.replace(/\{prompt\}/g, escapedPrompt)
118
119
  const execCommand = `${llmCommand}; echo $? > ${exitCodeFile}`
119
120
 
121
+ // Build injected environment: minion variables/secrets + extra vars from HQ
122
+ const injectedEnv = variableStore.buildEnv(options.extraEnv || {})
123
+
120
124
  // Create tmux session with extended environment
125
+ const tmuxEnvFlags = Object.entries(injectedEnv)
126
+ .map(([k, v]) => `-e "${k}=${v.replace(/"/g, '\\"')}"`)
121
127
  const tmuxCommand = [
122
128
  'tmux new-session -d',
123
129
  `-s "${sessionName}"`,
@@ -125,6 +131,7 @@ async function executeWorkflowSession(workflow, executionId, skillNames, options
125
131
  `-e "DISPLAY=:99"`,
126
132
  `-e "PATH=${extendedPath}"`,
127
133
  `-e "HOME=${homeDir}"`,
134
+ ...tmuxEnvFlags,
128
135
  `"${execCommand}"`,
129
136
  ].join(' ')
130
137
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "2.44.0",
3
+ "version": "2.48.1",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {