@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 +7 -0
- package/core/lib/capability-checker.js +105 -0
- package/core/lib/end-of-day.js +113 -0
- package/core/lib/reflection-scheduler.js +157 -0
- package/core/lib/step-poller.js +4 -0
- package/core/routes/daily-logs.js +122 -0
- package/core/routes/health.js +2 -0
- package/core/routes/memory.js +112 -0
- package/core/routes/skills.js +2 -1
- package/core/routes/variables.js +131 -0
- package/core/stores/daily-log-store.js +182 -0
- package/core/stores/memory-store.js +256 -0
- package/core/stores/variable-store.js +152 -0
- package/docs/api-reference.md +128 -0
- package/linux/minion-cli.sh +6 -0
- package/linux/routes/chat.js +54 -14
- package/linux/routes/config.js +11 -1
- package/linux/routine-runner.js +7 -0
- package/linux/server.js +19 -5
- package/linux/workflow-runner.js +7 -0
- package/package.json +1 -1
- package/win/lib/process-manager.js +85 -8
- package/win/routes/chat.js +53 -10
- package/win/routes/config.js +12 -1
- package/win/routine-runner.js +4 -0
- package/win/server.js +16 -4
- package/win/workflow-runner.js +4 -1
|
@@ -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
|
+
}
|
package/docs/api-reference.md
CHANGED
|
@@ -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 |
|
package/linux/minion-cli.sh
CHANGED
|
@@ -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
|
package/linux/routes/chat.js
CHANGED
|
@@ -8,11 +8,12 @@
|
|
|
8
8
|
* Requires LLM_COMMAND to be configured in minion.env.
|
|
9
9
|
*
|
|
10
10
|
* Endpoints:
|
|
11
|
-
* POST /api/chat
|
|
12
|
-
* GET /api/chat/session
|
|
13
|
-
* POST /api/chat/clear
|
|
14
|
-
* POST /api/chat/abort
|
|
15
|
-
* POST /api/chat/reset
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
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'
|
|
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 }
|
package/linux/routes/config.js
CHANGED
|
@@ -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)
|
package/linux/routine-runner.js
CHANGED
|
@@ -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
|
|
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)
|
package/linux/workflow-runner.js
CHANGED
|
@@ -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
|
|