@geekbeer/minion 3.43.0 → 3.49.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 +3 -1
- package/core/lib/board-task-context.js +87 -0
- package/core/lib/board-task-poller.js +210 -0
- package/core/lib/concurrency-manager.js +56 -0
- package/core/lib/dag-step-poller.js +16 -10
- package/core/lib/end-of-day.js +20 -6
- package/core/lib/platform.js +39 -19
- package/core/routes/daemons.js +2 -0
- package/core/routes/diagnose.js +27 -2
- package/docs/api-reference.md +73 -1
- package/linux/board-task-runner.js +227 -0
- package/linux/routes/chat.js +26 -6
- package/linux/server.js +5 -0
- package/mac/bin/hq +4 -0
- package/mac/board-task-runner.js +4 -0
- package/mac/lib/process-manager.js +109 -0
- package/mac/minion-cli.sh +1353 -0
- package/mac/routes/chat.js +7 -0
- package/mac/routes/commands.js +119 -0
- package/mac/routes/config.js +8 -0
- package/mac/routes/directives.js +6 -0
- package/mac/routes/files.js +6 -0
- package/mac/routes/terminal.js +7 -0
- package/mac/routine-runner.js +4 -0
- package/mac/server.js +413 -0
- package/mac/terminal-proxy.js +6 -0
- package/mac/vnc-auth-proxy.js +402 -0
- package/mac/workflow-runner.js +7 -0
- package/package.json +6 -2
- package/postinstall.js +33 -12
- package/rules/core.md +30 -0
- package/win/board-task-runner.js +181 -0
- package/win/routes/chat.js +24 -6
- package/win/routes/terminal.js +8 -0
- package/win/server.js +5 -0
- package/win/wsl-session-server.js +136 -1
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS chat routes — re-exports the Linux implementation.
|
|
3
|
+
*
|
|
4
|
+
* Spawns the configured LLM CLI (Claude Code / Gemini / Codex) for SSE
|
|
5
|
+
* streaming. spawn() and PATH resolution work identically on macOS.
|
|
6
|
+
*/
|
|
7
|
+
module.exports = require('../../linux/routes/chat')
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command execution endpoints (macOS)
|
|
3
|
+
*
|
|
4
|
+
* Endpoints:
|
|
5
|
+
* - GET /api/commands - List available commands
|
|
6
|
+
* - POST /api/command - Execute a whitelisted command
|
|
7
|
+
*
|
|
8
|
+
* Identical to linux/routes/commands.js apart from the process-manager source —
|
|
9
|
+
* macOS uses launchd, so we import from mac/lib/ (not linux/lib/).
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { exec } = require('child_process')
|
|
13
|
+
const { promisify } = require('util')
|
|
14
|
+
const execAsync = promisify(exec)
|
|
15
|
+
|
|
16
|
+
const { verifyToken } = require('../../core/lib/auth')
|
|
17
|
+
const { detectProcessManager, buildAllowedCommands } = require('../lib/process-manager')
|
|
18
|
+
|
|
19
|
+
const PROC_MGR = detectProcessManager()
|
|
20
|
+
const ALLOWED_COMMANDS = buildAllowedCommands(PROC_MGR)
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Register command routes as Fastify plugin
|
|
24
|
+
* @param {import('fastify').FastifyInstance} fastify
|
|
25
|
+
*/
|
|
26
|
+
async function commandRoutes(fastify) {
|
|
27
|
+
fastify.get('/api/commands', async (request, reply) => {
|
|
28
|
+
if (!verifyToken(request)) {
|
|
29
|
+
reply.code(401)
|
|
30
|
+
return { success: false, error: 'Unauthorized' }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
commands: Object.entries(ALLOWED_COMMANDS).map(([name, info]) => ({
|
|
35
|
+
name,
|
|
36
|
+
description: info.description,
|
|
37
|
+
})),
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
fastify.post('/api/command', async (request, reply) => {
|
|
42
|
+
if (!verifyToken(request)) {
|
|
43
|
+
reply.code(401)
|
|
44
|
+
return { success: false, error: 'Unauthorized' }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const { command } = request.body || {}
|
|
48
|
+
|
|
49
|
+
if (!command) {
|
|
50
|
+
reply.code(400)
|
|
51
|
+
return { success: false, error: 'command is required' }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const allowedCommand = ALLOWED_COMMANDS[command]
|
|
55
|
+
if (!allowedCommand) {
|
|
56
|
+
reply.code(403)
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
error: `Command '${command}' is not allowed`,
|
|
60
|
+
allowed_commands: Object.keys(ALLOWED_COMMANDS),
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log(`[Command] Executing: ${command}`)
|
|
65
|
+
|
|
66
|
+
// Deferred commands kill the current process, so respond first.
|
|
67
|
+
if (allowedCommand.deferred) {
|
|
68
|
+
console.log(`[Command] Scheduling deferred command: ${command}`)
|
|
69
|
+
setTimeout(() => {
|
|
70
|
+
exec(allowedCommand.command, { timeout: 60000 }, (err) => {
|
|
71
|
+
if (err) console.error(`[Command] Deferred command failed: ${command} - ${err.message}`)
|
|
72
|
+
else console.log(`[Command] Deferred command completed: ${command}`)
|
|
73
|
+
})
|
|
74
|
+
}, 1000)
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
success: true,
|
|
78
|
+
command,
|
|
79
|
+
output: 'Command scheduled for execution',
|
|
80
|
+
deferred: true,
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const { stdout, stderr } = await execAsync(allowedCommand.command, { timeout: 60000 })
|
|
86
|
+
console.log(`[Command] Success: ${command}`)
|
|
87
|
+
return {
|
|
88
|
+
success: true,
|
|
89
|
+
command,
|
|
90
|
+
output: stdout,
|
|
91
|
+
stderr: stderr || undefined,
|
|
92
|
+
}
|
|
93
|
+
} catch (error) {
|
|
94
|
+
console.error(`[Command] Failed: ${command} - ${error.message}`)
|
|
95
|
+
reply.code(500)
|
|
96
|
+
return {
|
|
97
|
+
success: false,
|
|
98
|
+
command,
|
|
99
|
+
error: error.message,
|
|
100
|
+
stdout: error.stdout,
|
|
101
|
+
stderr: error.stderr,
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getProcessManager() {
|
|
108
|
+
return PROC_MGR
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getAllowedCommands() {
|
|
112
|
+
return ALLOWED_COMMANDS
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
commandRoutes,
|
|
117
|
+
getProcessManager,
|
|
118
|
+
getAllowedCommands,
|
|
119
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS config routes — re-exports the Linux implementation.
|
|
3
|
+
*
|
|
4
|
+
* Audited 2026-04: linux/routes/config.js has no Linux-specific paths
|
|
5
|
+
* (no /etc/cloudflared, no systemd). It uses ~/... paths expanded via
|
|
6
|
+
* process.env.HOME and BSD-compatible tar -czf / -xzf flags.
|
|
7
|
+
*/
|
|
8
|
+
module.exports = require('../../linux/routes/config')
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* macOS terminal routes — re-exports the Linux implementation.
|
|
3
|
+
*
|
|
4
|
+
* Linux's terminal.js manages tmux sessions and ttyd processes; both tools
|
|
5
|
+
* are POSIX-compatible and work identically on macOS via Homebrew.
|
|
6
|
+
*/
|
|
7
|
+
module.exports = require('../../linux/routes/terminal')
|
package/mac/server.js
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minion Agent HTTP Server (macOS)
|
|
3
|
+
*
|
|
4
|
+
* Entry point for the minion agent on macOS.
|
|
5
|
+
* Registers shared routes (from core/) and macOS-specific routes.
|
|
6
|
+
*
|
|
7
|
+
* Architecture notes (macOS):
|
|
8
|
+
* - Runs as a LaunchAgent under the dedicated `minion` user (~/Library/LaunchAgents/).
|
|
9
|
+
* - DATA_DIR is ~/.minion (resolved by core/lib/platform.js).
|
|
10
|
+
* - VNC: native macOS Screen Sharing on port 5900 + websockify front on port 6080.
|
|
11
|
+
* - Terminal: tmux + ttyd (same as Linux), via Homebrew.
|
|
12
|
+
*
|
|
13
|
+
* API surface is identical to the Linux build — most routes are re-exports
|
|
14
|
+
* of the Linux implementation; only commands.js / process-manager.js differ
|
|
15
|
+
* because launchctl replaces systemd/supervisord.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs')
|
|
19
|
+
const path = require('path')
|
|
20
|
+
|
|
21
|
+
const fastify = require('fastify')({ logger: true })
|
|
22
|
+
|
|
23
|
+
// Package root (one level up from mac/)
|
|
24
|
+
const PACKAGE_ROOT = path.join(__dirname, '..')
|
|
25
|
+
|
|
26
|
+
// Core shared modules
|
|
27
|
+
const { config, validate, isHqConfigured } = require('../core/config')
|
|
28
|
+
const { sendHeartbeat } = require('../core/api')
|
|
29
|
+
const { version } = require('../package.json')
|
|
30
|
+
|
|
31
|
+
const workflowStore = require('../core/stores/workflow-store')
|
|
32
|
+
const routineStore = require('../core/stores/routine-store')
|
|
33
|
+
|
|
34
|
+
// Heartbeat interval: fixed at 30s (not user-configurable)
|
|
35
|
+
const HEARTBEAT_INTERVAL_MS = 30_000
|
|
36
|
+
let heartbeatTimer = null
|
|
37
|
+
let lastBeatAt = null
|
|
38
|
+
|
|
39
|
+
// macOS-specific modules (most re-export the Linux implementation)
|
|
40
|
+
const workflowRunner = require('./workflow-runner')
|
|
41
|
+
const routineRunner = require('./routine-runner')
|
|
42
|
+
const boardTaskRunner = require('./board-task-runner')
|
|
43
|
+
const { cleanupTtyd, killStaleTtydProcesses } = require('./routes/terminal')
|
|
44
|
+
const { startTerminalProxy, stopTerminalProxy } = require('./terminal-proxy')
|
|
45
|
+
|
|
46
|
+
// Config warnings (included in heartbeat)
|
|
47
|
+
const { getConfigWarnings } = require('../core/lib/config-warnings')
|
|
48
|
+
|
|
49
|
+
// Pull-model daemons (from core/)
|
|
50
|
+
const stepPoller = require('../core/lib/step-poller')
|
|
51
|
+
const dagStepPoller = require('../core/lib/dag-step-poller')
|
|
52
|
+
const boardTaskPoller = require('../core/lib/board-task-poller')
|
|
53
|
+
const revisionWatcher = require('../core/lib/revision-watcher')
|
|
54
|
+
const reflectionScheduler = require('../core/lib/reflection-scheduler')
|
|
55
|
+
const threadWatcher = require('../core/lib/thread-watcher')
|
|
56
|
+
const runningTasks = require('../core/lib/running-tasks')
|
|
57
|
+
|
|
58
|
+
// Shared routes (from core/)
|
|
59
|
+
const { healthRoutes, setOffline } = require('../core/routes/health')
|
|
60
|
+
const { diagnoseRoutes } = require('../core/routes/diagnose')
|
|
61
|
+
const { skillRoutes } = require('../core/routes/skills')
|
|
62
|
+
const { workflowRoutes } = require('../core/routes/workflows')
|
|
63
|
+
const { routineRoutes } = require('../core/routes/routines')
|
|
64
|
+
const { authRoutes } = require('../core/routes/auth')
|
|
65
|
+
const { variableRoutes } = require('../core/routes/variables')
|
|
66
|
+
const { memoryRoutes } = require('../core/routes/memory')
|
|
67
|
+
const { dailyLogRoutes } = require('../core/routes/daily-logs')
|
|
68
|
+
const { sudoersRoutes } = require('../core/routes/sudoers')
|
|
69
|
+
const { permissionRoutes } = require('../core/routes/permissions')
|
|
70
|
+
const { threadRoutes } = require('../core/routes/threads')
|
|
71
|
+
const { todoRoutes } = require('../core/routes/todos')
|
|
72
|
+
const { emailRoutes } = require('../core/routes/emails')
|
|
73
|
+
const { daemonRoutes } = require('../core/routes/daemons')
|
|
74
|
+
const { llmRoutes } = require('../core/routes/llm')
|
|
75
|
+
|
|
76
|
+
// macOS-specific routes
|
|
77
|
+
const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
|
|
78
|
+
const { terminalRoutes } = require('./routes/terminal')
|
|
79
|
+
const { fileRoutes } = require('./routes/files')
|
|
80
|
+
const { directiveRoutes } = require('./routes/directives')
|
|
81
|
+
const { chatRoutes, runQuickLlmCall } = require('./routes/chat')
|
|
82
|
+
const { configRoutes } = require('./routes/config')
|
|
83
|
+
|
|
84
|
+
// Validate configuration before starting
|
|
85
|
+
validate()
|
|
86
|
+
const PROC_MGR = getProcessManager()
|
|
87
|
+
const ALLOWED_COMMANDS = getAllowedCommands()
|
|
88
|
+
console.log(`[Config] Process manager: ${PROC_MGR}`)
|
|
89
|
+
console.log(`[Config] Available commands: ${Object.keys(ALLOWED_COMMANDS).join(', ')}`)
|
|
90
|
+
|
|
91
|
+
// Graceful shutdown
|
|
92
|
+
async function shutdown(signal) {
|
|
93
|
+
console.log(`[Server] Received ${signal}, shutting down...`)
|
|
94
|
+
|
|
95
|
+
setOffline()
|
|
96
|
+
|
|
97
|
+
// Stop heartbeat timer
|
|
98
|
+
if (heartbeatTimer) {
|
|
99
|
+
clearInterval(heartbeatTimer)
|
|
100
|
+
heartbeatTimer = null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Clear running tasks and send offline heartbeat to HQ (best-effort)
|
|
104
|
+
runningTasks.clear()
|
|
105
|
+
if (isHqConfigured()) {
|
|
106
|
+
try {
|
|
107
|
+
await Promise.race([
|
|
108
|
+
sendHeartbeat({ status: 'offline', running_tasks: [], version }),
|
|
109
|
+
new Promise(resolve => setTimeout(resolve, 3000)),
|
|
110
|
+
])
|
|
111
|
+
} catch {
|
|
112
|
+
// Best-effort — don't block shutdown
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Stop pollers, runners, and scheduler
|
|
117
|
+
stepPoller.stop()
|
|
118
|
+
dagStepPoller.stop()
|
|
119
|
+
boardTaskPoller.stop()
|
|
120
|
+
revisionWatcher.stop()
|
|
121
|
+
reflectionScheduler.stop()
|
|
122
|
+
threadWatcher.stop()
|
|
123
|
+
workflowRunner.stopAll()
|
|
124
|
+
routineRunner.stopAll()
|
|
125
|
+
|
|
126
|
+
// Stop terminal proxy and all ttyd processes
|
|
127
|
+
stopTerminalProxy()
|
|
128
|
+
cleanupTtyd()
|
|
129
|
+
|
|
130
|
+
// Close database and server
|
|
131
|
+
const { closeDb } = require('../core/db')
|
|
132
|
+
closeDb()
|
|
133
|
+
await fastify.close()
|
|
134
|
+
process.exit(0)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'))
|
|
138
|
+
process.on('SIGINT', () => shutdown('SIGINT'))
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Sync bundled permissions into ~/.claude/settings.json.
|
|
142
|
+
*/
|
|
143
|
+
function syncPermissions() {
|
|
144
|
+
const bundledPath = path.join(PACKAGE_ROOT, 'settings', 'permissions.json')
|
|
145
|
+
const settingsDir = path.join(config.HOME_DIR, '.claude')
|
|
146
|
+
const settingsPath = path.join(settingsDir, 'settings.json')
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
if (!fs.existsSync(bundledPath)) return
|
|
150
|
+
|
|
151
|
+
const bundled = JSON.parse(fs.readFileSync(bundledPath, 'utf-8'))
|
|
152
|
+
|
|
153
|
+
let settings = {}
|
|
154
|
+
if (fs.existsSync(settingsPath)) {
|
|
155
|
+
settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
settings.permissions = {
|
|
159
|
+
allow: bundled.allow || [],
|
|
160
|
+
deny: bundled.deny || [],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
fs.mkdirSync(settingsDir, { recursive: true })
|
|
164
|
+
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8')
|
|
165
|
+
console.log(`[Permissions] Synced: allow=${bundled.allow.length}, deny=${bundled.deny.length}`)
|
|
166
|
+
} catch (err) {
|
|
167
|
+
console.error(`[Permissions] Failed to sync permissions: ${err.message}`)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Sync bundled tmux.conf to ~/.tmux.conf.
|
|
173
|
+
*/
|
|
174
|
+
function syncTmuxConfig() {
|
|
175
|
+
const bundledPath = path.join(PACKAGE_ROOT, 'settings', 'tmux.conf')
|
|
176
|
+
const destPath = path.join(config.HOME_DIR, '.tmux.conf')
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
if (!fs.existsSync(bundledPath)) return
|
|
180
|
+
|
|
181
|
+
fs.copyFileSync(bundledPath, destPath)
|
|
182
|
+
console.log('[Tmux] Synced tmux.conf')
|
|
183
|
+
} catch (err) {
|
|
184
|
+
console.error(`[Tmux] Failed to sync tmux.conf: ${err.message}`)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Sync bundled rules from the package to ~/.claude/rules/.
|
|
190
|
+
*/
|
|
191
|
+
function syncBundledRules() {
|
|
192
|
+
const bundledRulesDir = path.join(PACKAGE_ROOT, 'rules')
|
|
193
|
+
const targetRulesDir = path.join(config.HOME_DIR, '.claude', 'rules')
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
if (!fs.existsSync(bundledRulesDir)) return
|
|
197
|
+
|
|
198
|
+
fs.mkdirSync(targetRulesDir, { recursive: true })
|
|
199
|
+
|
|
200
|
+
const coreSrc = path.join(bundledRulesDir, 'core.md')
|
|
201
|
+
if (fs.existsSync(coreSrc)) {
|
|
202
|
+
fs.copyFileSync(coreSrc, path.join(targetRulesDir, 'core.md'))
|
|
203
|
+
console.log('[Rules] Synced: core.md')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
for (const legacy of ['minion.md', 'role-pm.md', 'role-engineer.md']) {
|
|
207
|
+
const legacyPath = path.join(targetRulesDir, legacy)
|
|
208
|
+
if (fs.existsSync(legacyPath)) {
|
|
209
|
+
fs.unlinkSync(legacyPath)
|
|
210
|
+
console.log(`[Rules] Removed legacy: ${legacy}`)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
console.error(`[Rules] Failed to sync bundled rules: ${err.message}`)
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Sync bundled role context files to ~/.minion/roles/.
|
|
220
|
+
*/
|
|
221
|
+
function syncBundledRoles() {
|
|
222
|
+
const bundledRolesDir = path.join(PACKAGE_ROOT, 'roles')
|
|
223
|
+
const targetRolesDir = path.join(config.HOME_DIR, '.minion', 'roles')
|
|
224
|
+
|
|
225
|
+
try {
|
|
226
|
+
if (!fs.existsSync(bundledRolesDir)) return
|
|
227
|
+
|
|
228
|
+
fs.mkdirSync(targetRolesDir, { recursive: true })
|
|
229
|
+
|
|
230
|
+
for (const file of fs.readdirSync(bundledRolesDir)) {
|
|
231
|
+
if (!file.endsWith('.md')) continue
|
|
232
|
+
const src = path.join(bundledRolesDir, file)
|
|
233
|
+
const dest = path.join(targetRolesDir, file)
|
|
234
|
+
fs.copyFileSync(src, dest)
|
|
235
|
+
console.log(`[Roles] Synced: ${file}`)
|
|
236
|
+
}
|
|
237
|
+
} catch (err) {
|
|
238
|
+
console.error(`[Roles] Failed to sync bundled roles: ${err.message}`)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Sync bundled documentation files to ~/.minion/docs/.
|
|
244
|
+
*/
|
|
245
|
+
function syncBundledDocs() {
|
|
246
|
+
const bundledDocsDir = path.join(PACKAGE_ROOT, 'docs')
|
|
247
|
+
const targetDocsDir = path.join(config.HOME_DIR, '.minion', 'docs')
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
if (!fs.existsSync(bundledDocsDir)) return
|
|
251
|
+
|
|
252
|
+
fs.mkdirSync(targetDocsDir, { recursive: true })
|
|
253
|
+
|
|
254
|
+
for (const file of fs.readdirSync(bundledDocsDir)) {
|
|
255
|
+
if (!file.endsWith('.md')) continue
|
|
256
|
+
const src = path.join(bundledDocsDir, file)
|
|
257
|
+
const dest = path.join(targetDocsDir, file)
|
|
258
|
+
fs.copyFileSync(src, dest)
|
|
259
|
+
console.log(`[Docs] Synced: ${file}`)
|
|
260
|
+
}
|
|
261
|
+
} catch (err) {
|
|
262
|
+
console.error(`[Docs] Failed to sync bundled docs: ${err.message}`)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Register all routes (shared + macOS-specific)
|
|
268
|
+
*/
|
|
269
|
+
async function registerAllRoutes(app) {
|
|
270
|
+
// Shared routes (from core/) - inject runners via opts
|
|
271
|
+
await app.register(healthRoutes)
|
|
272
|
+
await app.register(diagnoseRoutes)
|
|
273
|
+
await app.register(skillRoutes, { workflowRunner })
|
|
274
|
+
await app.register(workflowRoutes, { workflowRunner })
|
|
275
|
+
await app.register(routineRoutes, { routineRunner })
|
|
276
|
+
await app.register(authRoutes)
|
|
277
|
+
await app.register(variableRoutes)
|
|
278
|
+
await app.register(memoryRoutes)
|
|
279
|
+
await app.register(dailyLogRoutes)
|
|
280
|
+
await app.register(sudoersRoutes)
|
|
281
|
+
await app.register(permissionRoutes)
|
|
282
|
+
await app.register(threadRoutes)
|
|
283
|
+
await app.register(todoRoutes)
|
|
284
|
+
await app.register(emailRoutes)
|
|
285
|
+
await app.register(daemonRoutes, { heartbeatStatus: () => ({ running: !!heartbeatTimer, last_beat_at: lastBeatAt }) })
|
|
286
|
+
await app.register(llmRoutes)
|
|
287
|
+
|
|
288
|
+
// macOS-specific routes
|
|
289
|
+
await app.register(commandRoutes)
|
|
290
|
+
await app.register(terminalRoutes)
|
|
291
|
+
await app.register(fileRoutes)
|
|
292
|
+
await app.register(directiveRoutes)
|
|
293
|
+
await app.register(chatRoutes)
|
|
294
|
+
await app.register(configRoutes)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Start server
|
|
298
|
+
async function start() {
|
|
299
|
+
try {
|
|
300
|
+
// Extend process.env so all child processes (tmux, spawn) inherit correct environment.
|
|
301
|
+
// This eliminates the need for per-invocation PATH building in runners/chat.
|
|
302
|
+
const { buildExtendedPath } = require('../core/lib/platform')
|
|
303
|
+
process.env.PATH = buildExtendedPath(config.HOME_DIR)
|
|
304
|
+
process.env.HOME = config.HOME_DIR
|
|
305
|
+
// No DISPLAY env var on macOS — native Screen Sharing uses WindowServer,
|
|
306
|
+
// not X11. Setting DISPLAY=:99 (Linux pattern) would break GUI child processes.
|
|
307
|
+
|
|
308
|
+
// Load minion secrets into process.env for child process inheritance.
|
|
309
|
+
// Variables are NOT loaded here — they use {{VAR}} template expansion in skill content.
|
|
310
|
+
const variableStore = require('../core/stores/variable-store')
|
|
311
|
+
const minionEnv = variableStore.buildEnv()
|
|
312
|
+
for (const [key, value] of Object.entries(minionEnv)) {
|
|
313
|
+
if (!(key in process.env)) process.env[key] = value
|
|
314
|
+
}
|
|
315
|
+
console.log(`[Server] Loaded ${Object.keys(minionEnv).length} minion secrets into process.env`)
|
|
316
|
+
|
|
317
|
+
// Sync bundled assets
|
|
318
|
+
syncBundledRules()
|
|
319
|
+
syncBundledRoles()
|
|
320
|
+
syncBundledDocs()
|
|
321
|
+
syncPermissions()
|
|
322
|
+
syncTmuxConfig()
|
|
323
|
+
|
|
324
|
+
// Register all routes
|
|
325
|
+
await registerAllRoutes(fastify)
|
|
326
|
+
|
|
327
|
+
// Listen on all interfaces
|
|
328
|
+
await fastify.listen({ port: config.AGENT_PORT, host: '0.0.0.0' })
|
|
329
|
+
|
|
330
|
+
console.log(`[Server] Minion agent listening on port ${config.AGENT_PORT}`)
|
|
331
|
+
|
|
332
|
+
// Kill any stale ttyd processes from a previous run, then start terminal proxy
|
|
333
|
+
await killStaleTtydProcesses()
|
|
334
|
+
try {
|
|
335
|
+
await startTerminalProxy()
|
|
336
|
+
} catch (err) {
|
|
337
|
+
console.error(`[Server] Terminal proxy failed to start: ${err.message}`)
|
|
338
|
+
console.error('[Server] Terminal WebSocket connections will not work')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// Load cached workflows and start workflow runner
|
|
342
|
+
try {
|
|
343
|
+
const cachedWorkflows = await workflowStore.load()
|
|
344
|
+
if (cachedWorkflows.length > 0) {
|
|
345
|
+
console.log(`[Server] Loading ${cachedWorkflows.length} cached workflows`)
|
|
346
|
+
workflowRunner.loadWorkflows(cachedWorkflows)
|
|
347
|
+
}
|
|
348
|
+
} catch (err) {
|
|
349
|
+
console.error('[Server] Failed to load cached workflows:', err.message)
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Load cached routines and start routine runner
|
|
353
|
+
try {
|
|
354
|
+
const cachedRoutines = await routineStore.load()
|
|
355
|
+
if (cachedRoutines.length > 0) {
|
|
356
|
+
console.log(`[Server] Loading ${cachedRoutines.length} cached routines`)
|
|
357
|
+
routineRunner.loadRoutines(cachedRoutines)
|
|
358
|
+
}
|
|
359
|
+
} catch (err) {
|
|
360
|
+
console.error('[Server] Failed to load cached routines:', err.message)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Start reflection scheduler (self-reflection time)
|
|
364
|
+
reflectionScheduler.start(runQuickLlmCall)
|
|
365
|
+
|
|
366
|
+
if (isHqConfigured()) {
|
|
367
|
+
console.log(`[Server] HQ URL: ${config.HQ_URL}`)
|
|
368
|
+
|
|
369
|
+
// Send initial online heartbeat
|
|
370
|
+
const { getStatus } = require('../core/routes/health')
|
|
371
|
+
const { currentTask } = getStatus()
|
|
372
|
+
const todoStore = require('../core/stores/todo-store')
|
|
373
|
+
const getTodoSummary = () => { try { return todoStore.getSummary() } catch { return null } }
|
|
374
|
+
const workspaceStore = require('../core/stores/workspace-store')
|
|
375
|
+
const syncWorkspaces = (response) => {
|
|
376
|
+
if (response && Array.isArray(response.workspaces)) {
|
|
377
|
+
workspaceStore.upsertAll(response.workspaces)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
sendHeartbeat({ status: 'online', current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(syncWorkspaces).catch(err => {
|
|
382
|
+
console.error('[Heartbeat] Initial heartbeat failed:', err.message)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
// Start periodic heartbeat
|
|
386
|
+
heartbeatTimer = setInterval(() => {
|
|
387
|
+
const { currentStatus, currentTask } = getStatus()
|
|
388
|
+
sendHeartbeat({ status: currentStatus, current_task: currentTask, running_tasks: runningTasks.getAll(), config_warnings: getConfigWarnings(), todo_summary: getTodoSummary(), version }).then(response => {
|
|
389
|
+
lastBeatAt = new Date().toISOString()
|
|
390
|
+
syncWorkspaces(response)
|
|
391
|
+
}).catch(err => {
|
|
392
|
+
console.error('[Heartbeat] Periodic heartbeat failed:', err.message)
|
|
393
|
+
})
|
|
394
|
+
}, HEARTBEAT_INTERVAL_MS)
|
|
395
|
+
console.log(`[Heartbeat] Sending every ${HEARTBEAT_INTERVAL_MS / 1000}s`)
|
|
396
|
+
|
|
397
|
+
// Start Pull-model daemons
|
|
398
|
+
stepPoller.start()
|
|
399
|
+
dagStepPoller.start()
|
|
400
|
+
boardTaskPoller.setRunner(boardTaskRunner)
|
|
401
|
+
boardTaskPoller.start()
|
|
402
|
+
revisionWatcher.start()
|
|
403
|
+
threadWatcher.start(runQuickLlmCall)
|
|
404
|
+
} else {
|
|
405
|
+
console.log('[Server] Running in standalone mode (no HQ connection)')
|
|
406
|
+
}
|
|
407
|
+
} catch (err) {
|
|
408
|
+
fastify.log.error(err)
|
|
409
|
+
process.exit(1)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
start()
|