@geekbeer/minion 2.25.0 → 2.32.0

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.
Files changed (47) hide show
  1. package/core/lib/platform.js +117 -0
  2. package/{routes → core/routes}/health.js +1 -1
  3. package/{routes → core/routes}/routines.js +3 -3
  4. package/{routes → core/routes}/skills.js +3 -3
  5. package/{routes → core/routes}/workflows.js +4 -4
  6. package/{chat-store.js → core/stores/chat-store.js} +1 -1
  7. package/{execution-store.js → core/stores/execution-store.js} +1 -1
  8. package/{routine-store.js → core/stores/routine-store.js} +1 -1
  9. package/{workflow-store.js → core/stores/workflow-store.js} +1 -1
  10. package/{minion-cli.sh → linux/minion-cli.sh} +63 -6
  11. package/{routes → linux/routes}/chat.js +3 -3
  12. package/{routes → linux/routes}/commands.js +1 -1
  13. package/{routes → linux/routes}/config.js +3 -3
  14. package/{routes → linux/routes}/directives.js +5 -5
  15. package/{routes → linux/routes}/files.js +2 -2
  16. package/{routes → linux/routes}/terminal.js +2 -2
  17. package/{routine-runner.js → linux/routine-runner.js} +4 -4
  18. package/{server.js → linux/server.js} +71 -36
  19. package/{workflow-runner.js → linux/workflow-runner.js} +4 -4
  20. package/package.json +16 -20
  21. package/win/bin/hq-win.js +18 -0
  22. package/win/bin/hq.ps1 +108 -0
  23. package/win/bin/minion-cli-win.js +20 -0
  24. package/win/lib/llm-checker.js +115 -0
  25. package/win/lib/log-manager.js +119 -0
  26. package/win/lib/process-manager.js +112 -0
  27. package/win/minion-cli.ps1 +869 -0
  28. package/win/routes/chat.js +280 -0
  29. package/win/routes/commands.js +101 -0
  30. package/win/routes/config.js +227 -0
  31. package/win/routes/directives.js +136 -0
  32. package/win/routes/files.js +283 -0
  33. package/win/routes/terminal.js +316 -0
  34. package/win/routine-runner.js +324 -0
  35. package/win/server.js +230 -0
  36. package/win/terminal-server.js +234 -0
  37. package/win/workflow-runner.js +380 -0
  38. package/routes/index.js +0 -106
  39. /package/{api.js → core/api.js} +0 -0
  40. /package/{config.js → core/config.js} +0 -0
  41. /package/{lib → core/lib}/auth.js +0 -0
  42. /package/{lib → core/lib}/llm-checker.js +0 -0
  43. /package/{lib → core/lib}/log-manager.js +0 -0
  44. /package/{routes → core/routes}/auth.js +0 -0
  45. /package/{bin → linux/bin}/hq +0 -0
  46. /package/{lib → linux/lib}/process-manager.js +0 -0
  47. /package/{terminal-proxy.js → linux/terminal-proxy.js} +0 -0
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Cross-platform utility module
3
+ *
4
+ * Provides platform-aware paths, separators, and helpers.
5
+ * Used by win/ modules to avoid hardcoded Unix paths.
6
+ * This file does NOT modify any existing Linux module behavior.
7
+ */
8
+
9
+ const os = require('os')
10
+ const path = require('path')
11
+ const fs = require('fs')
12
+
13
+ const IS_WINDOWS = process.platform === 'win32'
14
+ const PATH_SEPARATOR = path.delimiter // ';' on Windows, ':' on Unix
15
+ const TEMP_DIR = os.tmpdir()
16
+
17
+ /**
18
+ * Resolve the data directory for minion agent persistent files.
19
+ * Windows: %PROGRAMDATA%\minion-agent (or fallback to %USERPROFILE%\.minion-agent)
20
+ * Linux: /opt/minion-agent (existing behavior)
21
+ */
22
+ function resolveDataDir() {
23
+ if (IS_WINDOWS) {
24
+ const programData = process.env.PROGRAMDATA || process.env.ALLUSERSPROFILE
25
+ if (programData) {
26
+ const dir = path.join(programData, 'minion-agent')
27
+ try {
28
+ fs.mkdirSync(dir, { recursive: true })
29
+ return dir
30
+ } catch {
31
+ // Fall through to home-based path
32
+ }
33
+ }
34
+ return path.join(os.homedir(), '.minion-agent')
35
+ }
36
+ return '/opt/minion-agent'
37
+ }
38
+
39
+ const DATA_DIR = resolveDataDir()
40
+ const LOG_DIR = path.join(DATA_DIR, 'logs')
41
+ const MARKER_DIR = path.join(TEMP_DIR, 'minion-executions')
42
+
43
+ /**
44
+ * Build extended PATH including common CLI installation locations.
45
+ * Uses the correct platform separator.
46
+ * @param {string} homeDir - User home directory
47
+ * @returns {string} Extended PATH string
48
+ */
49
+ function buildExtendedPath(homeDir) {
50
+ const additionalPaths = IS_WINDOWS
51
+ ? [
52
+ path.join(homeDir, '.local', 'bin'),
53
+ path.join(homeDir, '.npm-global'),
54
+ path.join(homeDir, '.claude', 'bin'),
55
+ path.join(homeDir, 'AppData', 'Roaming', 'npm'),
56
+ path.join(process.env.PROGRAMFILES || 'C:\\Program Files', 'nodejs'),
57
+ ]
58
+ : [
59
+ path.join(homeDir, 'bin'),
60
+ path.join(homeDir, '.npm-global', 'bin'),
61
+ path.join(homeDir, '.local', 'bin'),
62
+ path.join(homeDir, '.claude', 'bin'),
63
+ '/usr/local/bin',
64
+ ]
65
+
66
+ const currentPath = process.env.PATH || ''
67
+ return [...additionalPaths, currentPath].join(PATH_SEPARATOR)
68
+ }
69
+
70
+ /**
71
+ * Get exit code file path for a session.
72
+ * @param {string} sessionName - Session identifier
73
+ * @returns {string} Path to exit code file
74
+ */
75
+ function getExitCodePath(sessionName) {
76
+ return path.join(TEMP_DIR, `minion-exit-${sessionName}`)
77
+ }
78
+
79
+ /**
80
+ * Get the default shell for spawning processes.
81
+ * @returns {string} Shell executable path
82
+ */
83
+ function getDefaultShell() {
84
+ if (IS_WINDOWS) {
85
+ return process.env.COMSPEC || 'cmd.exe'
86
+ }
87
+ return process.env.SHELL || '/bin/sh'
88
+ }
89
+
90
+ /**
91
+ * Resolve .env file path.
92
+ * Prefers DATA_DIR/.env, falls back to ~/minion.env.
93
+ * @param {string} homeDir - User home directory
94
+ * @returns {string} Path to .env file
95
+ */
96
+ function resolveEnvFilePath(homeDir) {
97
+ const dataEnv = path.join(DATA_DIR, '.env')
98
+ try {
99
+ fs.accessSync(path.dirname(dataEnv), fs.constants.W_OK)
100
+ return dataEnv
101
+ } catch {
102
+ return path.join(homeDir, 'minion.env')
103
+ }
104
+ }
105
+
106
+ module.exports = {
107
+ IS_WINDOWS,
108
+ PATH_SEPARATOR,
109
+ TEMP_DIR,
110
+ DATA_DIR,
111
+ LOG_DIR,
112
+ MARKER_DIR,
113
+ buildExtendedPath,
114
+ getExitCodePath,
115
+ getDefaultShell,
116
+ resolveEnvFilePath,
117
+ }
@@ -7,7 +7,7 @@
7
7
  * - POST /api/status - Update status
8
8
  */
9
9
 
10
- const { version } = require('../package.json')
10
+ const { version } = require('../../package.json')
11
11
  const { getLlmServices, isLlmCommandConfigured } = require('../lib/llm-checker')
12
12
 
13
13
  // Shared status state
@@ -12,8 +12,7 @@
12
12
  */
13
13
 
14
14
  const { verifyToken } = require('../lib/auth')
15
- const routineRunner = require('../routine-runner')
16
- const routineStore = require('../routine-store')
15
+ const routineStore = require('../stores/routine-store')
17
16
  const api = require('../api')
18
17
  const { isHqConfigured } = require('../config')
19
18
 
@@ -21,7 +20,8 @@ const { isHqConfigured } = require('../config')
21
20
  * Register routine routes as Fastify plugin
22
21
  * @param {import('fastify').FastifyInstance} fastify
23
22
  */
24
- async function routineRoutes(fastify) {
23
+ async function routineRoutes(fastify, opts) {
24
+ const { routineRunner } = opts
25
25
  // Receive routines from HQ (upsert: add new, update existing)
26
26
  fastify.post('/api/routines', async (request, reply) => {
27
27
  if (!verifyToken(request)) {
@@ -17,8 +17,7 @@ const crypto = require('crypto')
17
17
  const { verifyToken } = require('../lib/auth')
18
18
  const api = require('../api')
19
19
  const { config, isHqConfigured } = require('../config')
20
- const workflowRunner = require('../workflow-runner')
21
- const executionStore = require('../execution-store')
20
+ const executionStore = require('../stores/execution-store')
22
21
  const logManager = require('../lib/log-manager')
23
22
 
24
23
  /**
@@ -128,7 +127,8 @@ async function pushSkillToHQ(name) {
128
127
  * Register skill routes as Fastify plugin
129
128
  * @param {import('fastify').FastifyInstance} fastify
130
129
  */
131
- async function skillRoutes(fastify) {
130
+ async function skillRoutes(fastify, opts) {
131
+ const { workflowRunner } = opts
132
132
  // List deployed skills from local .claude/skills directory
133
133
  fastify.get('/api/list-skills', async (request, reply) => {
134
134
  if (!verifyToken(request)) {
@@ -20,9 +20,8 @@ const fs = require('fs').promises
20
20
  const path = require('path')
21
21
 
22
22
  const { verifyToken } = require('../lib/auth')
23
- const workflowRunner = require('../workflow-runner')
24
- const workflowStore = require('../workflow-store')
25
- const executionStore = require('../execution-store')
23
+ const workflowStore = require('../stores/workflow-store')
24
+ const executionStore = require('../stores/execution-store')
26
25
  const logManager = require('../lib/log-manager')
27
26
  const api = require('../api')
28
27
  const { config, isHqConfigured } = require('../config')
@@ -32,7 +31,8 @@ const { writeSkillToLocal, pushSkillToHQ } = require('./skills')
32
31
  * Register workflow routes as Fastify plugin
33
32
  * @param {import('fastify').FastifyInstance} fastify
34
33
  */
35
- async function workflowRoutes(fastify) {
34
+ async function workflowRoutes(fastify, opts) {
35
+ const { workflowRunner } = opts
36
36
  // Receive workflows from HQ (upsert: add new, update existing)
37
37
  fastify.post('/api/workflows', async (request, reply) => {
38
38
  if (!verifyToken(request)) {
@@ -7,7 +7,7 @@
7
7
  const fs = require('fs').promises
8
8
  const path = require('path')
9
9
 
10
- const { config } = require('./config')
10
+ const { config } = require('../config')
11
11
 
12
12
  const MAX_MESSAGES = 100
13
13
 
@@ -7,7 +7,7 @@
7
7
  const fs = require('fs').promises
8
8
  const path = require('path')
9
9
 
10
- const { config } = require('./config')
10
+ const { config } = require('../config')
11
11
 
12
12
  // Max executions to keep (older ones are pruned)
13
13
  const MAX_EXECUTIONS = 200
@@ -7,7 +7,7 @@
7
7
  const fs = require('fs').promises
8
8
  const path = require('path')
9
9
 
10
- const { config } = require('./config')
10
+ const { config } = require('../config')
11
11
 
12
12
  // Routine file location: /opt/minion-agent/routines.json (systemd/supervisord)
13
13
  // or ~/routines.json (standalone)
@@ -7,7 +7,7 @@
7
7
  const fs = require('fs').promises
8
8
  const path = require('path')
9
9
 
10
- const { config } = require('./config')
10
+ const { config } = require('../config')
11
11
 
12
12
  // Workflow file location: /opt/minion-agent/workflows.json (systemd/supervisord)
13
13
  // or ~/workflows.json (standalone)
@@ -41,7 +41,7 @@ fi
41
41
 
42
42
  # Resolve version from package.json (installed location)
43
43
  CLI_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
44
- CLI_VERSION="$(node -p "require('${CLI_DIR}/package.json').version")"
44
+ CLI_VERSION="$(node -p "require('${CLI_DIR}/../package.json').version")"
45
45
 
46
46
  # Use sudo only when not running as root
47
47
  SUDO=""
@@ -89,6 +89,28 @@ svc_control() {
89
89
 
90
90
  AGENT_URL="${MINION_AGENT_URL:-http://localhost:8080}"
91
91
 
92
+ # Detect LAN IPv4 address (best-effort)
93
+ detect_lan_ip() {
94
+ local ip=""
95
+ # Method 1: Use ip route to find the source IP for default gateway
96
+ if command -v ip &>/dev/null; then
97
+ ip=$(ip route get 1.1.1.1 2>/dev/null | grep -oP 'src \K[\d.]+' | head -1)
98
+ fi
99
+ # Method 2: Parse ip addr for non-loopback, non-docker, non-APIPA addresses
100
+ if [ -z "$ip" ] && command -v ip &>/dev/null; then
101
+ ip=$(ip -4 addr show scope global 2>/dev/null \
102
+ | grep -oP 'inet \K[\d.]+' \
103
+ | grep -v '^172\.17\.' \
104
+ | grep -v '^169\.254\.' \
105
+ | head -1)
106
+ fi
107
+ # Method 3: hostname -I (available on most Linux distros)
108
+ if [ -z "$ip" ] && command -v hostname &>/dev/null; then
109
+ ip=$(hostname -I 2>/dev/null | awk '{print $1}')
110
+ fi
111
+ echo "$ip"
112
+ }
113
+
92
114
  # Auto-load .env so that API_TOKEN etc. are available in interactive shells
93
115
  ENV_FILE="/opt/minion-agent/.env"
94
116
  if [ -f "$ENV_FILE" ] && [ -r "$ENV_FILE" ]; then
@@ -319,7 +341,7 @@ do_setup() {
319
341
  echo "[6/${TOTAL_STEPS}] Creating service configuration ($PROC_MGR)..."
320
342
  local NPM_ROOT
321
343
  NPM_ROOT="$(npm root -g)"
322
- local SERVER_PATH="${NPM_ROOT}/@geekbeer/minion/server.js"
344
+ local SERVER_PATH="${NPM_ROOT}/@geekbeer/minion/linux/server.js"
323
345
 
324
346
  if [ ! -f "$SERVER_PATH" ]; then
325
347
  echo " ERROR: server.js not found at $SERVER_PATH"
@@ -732,14 +754,33 @@ CFEOF
732
754
  echo "Notifying HQ of setup completion..."
733
755
  local NOTIFY_RESPONSE
734
756
  local HOSTNAME_VAL
757
+ local LAN_IP
735
758
  HOSTNAME_VAL=$(hostname 2>/dev/null || echo "")
759
+ LAN_IP=$(detect_lan_ip)
760
+
761
+ # Build request body with LAN IP detection
762
+ # Docker: use hostname (container name) for internal_ip_address (resolved via Docker DNS)
763
+ # Self-hosted: use LAN IP for both fields (hostname is not resolvable on LAN)
764
+ local BODY
765
+ if [ -f /.dockerenv ]; then
766
+ BODY="{\"internal_ip_address\":\"${HOSTNAME_VAL}\"}"
767
+ elif [ -n "$LAN_IP" ]; then
768
+ BODY="{\"internal_ip_address\":\"${LAN_IP}\",\"ip_address\":\"${LAN_IP}\"}"
769
+ else
770
+ BODY="{\"internal_ip_address\":\"${HOSTNAME_VAL}\"}"
771
+ fi
772
+
736
773
  NOTIFY_RESPONSE=$(curl -sfL -X POST "${HQ_URL}/api/minion/setup-complete" \
737
774
  -H "Content-Type: application/json" \
738
775
  -H "Authorization: Bearer ${API_TOKEN}" \
739
- -d "{\"internal_ip_address\":\"${HOSTNAME_VAL}\"}" 2>&1) || true
776
+ -d "$BODY" 2>&1) || true
740
777
 
741
778
  if echo "$NOTIFY_RESPONSE" | grep -q '"success":true' 2>/dev/null; then
742
- echo " -> HQ notified successfully"
779
+ if [ -n "$LAN_IP" ] && [ ! -f /.dockerenv ]; then
780
+ echo " -> HQ notified successfully (LAN IP: ${LAN_IP})"
781
+ else
782
+ echo " -> HQ notified successfully"
783
+ fi
743
784
  else
744
785
  echo " -> HQ notification skipped (HQ may not be reachable)"
745
786
  fi
@@ -915,14 +956,30 @@ SUPEOF
915
956
  echo "[4/4] Notifying HQ..."
916
957
  local NOTIFY_RESPONSE
917
958
  local HOSTNAME_VAL
959
+ local LAN_IP
918
960
  HOSTNAME_VAL=$(hostname 2>/dev/null || echo "")
961
+ LAN_IP=$(detect_lan_ip)
962
+
963
+ local BODY
964
+ if [ -f /.dockerenv ]; then
965
+ BODY="{\"internal_ip_address\":\"${HOSTNAME_VAL}\"}"
966
+ elif [ -n "$LAN_IP" ]; then
967
+ BODY="{\"internal_ip_address\":\"${LAN_IP}\",\"ip_address\":\"${LAN_IP}\"}"
968
+ else
969
+ BODY="{\"internal_ip_address\":\"${HOSTNAME_VAL}\"}"
970
+ fi
971
+
919
972
  NOTIFY_RESPONSE=$(curl -sfL -X POST "${HQ_URL}/api/minion/setup-complete" \
920
973
  -H "Content-Type: application/json" \
921
974
  -H "Authorization: Bearer ${API_TOKEN}" \
922
- -d "{\"internal_ip_address\":\"${HOSTNAME_VAL}\"}" 2>&1) || true
975
+ -d "$BODY" 2>&1) || true
923
976
 
924
977
  if echo "$NOTIFY_RESPONSE" | grep -q '"success":true' 2>/dev/null; then
925
- echo " -> HQ notified successfully"
978
+ if [ -n "$LAN_IP" ] && [ ! -f /.dockerenv ]; then
979
+ echo " -> HQ notified successfully (LAN IP: ${LAN_IP})"
980
+ else
981
+ echo " -> HQ notified successfully"
982
+ fi
926
983
  else
927
984
  echo " -> Skipped (heartbeat will notify HQ within 30s)"
928
985
  fi
@@ -17,9 +17,9 @@
17
17
  const { spawn } = require('child_process')
18
18
  const fs = require('fs')
19
19
  const path = require('path')
20
- const { verifyToken } = require('../lib/auth')
21
- const { config } = require('../config')
22
- const chatStore = require('../chat-store')
20
+ const { verifyToken } = require('../../core/lib/auth')
21
+ const { config } = require('../../core/config')
22
+ const chatStore = require('../../core/stores/chat-store')
23
23
 
24
24
  /** @type {import('child_process').ChildProcess | null} */
25
25
  let activeChatChild = null
@@ -10,7 +10,7 @@ const { exec } = require('child_process')
10
10
  const { promisify } = require('util')
11
11
  const execAsync = promisify(exec)
12
12
 
13
- const { verifyToken } = require('../lib/auth')
13
+ const { verifyToken } = require('../../core/lib/auth')
14
14
  const { detectProcessManager, buildAllowedCommands } = require('../lib/process-manager')
15
15
 
16
16
  const PROC_MGR = detectProcessManager()
@@ -10,9 +10,9 @@
10
10
  const { execSync } = require('child_process')
11
11
  const fs = require('fs')
12
12
  const path = require('path')
13
- const { verifyToken } = require('../lib/auth')
14
- const { clearLlmCache } = require('../lib/llm-checker')
15
- const { config } = require('../config')
13
+ const { verifyToken } = require('../../core/lib/auth')
14
+ const { clearLlmCache } = require('../../core/lib/llm-checker')
15
+ const { config } = require('../../core/config')
16
16
 
17
17
  /** Keys that can be read/written via the config API */
18
18
  const ALLOWED_ENV_KEYS = ['LLM_COMMAND']
@@ -12,12 +12,12 @@ const fs = require('fs').promises
12
12
  const path = require('path')
13
13
  const crypto = require('crypto')
14
14
 
15
- const { verifyToken } = require('../lib/auth')
16
- const { config } = require('../config')
17
- const { writeSkillToLocal } = require('./skills')
15
+ const { verifyToken } = require('../../core/lib/auth')
16
+ const { config } = require('../../core/config')
17
+ const { writeSkillToLocal } = require('../../core/routes/skills')
18
18
  const workflowRunner = require('../workflow-runner')
19
- const executionStore = require('../execution-store')
20
- const logManager = require('../lib/log-manager')
19
+ const executionStore = require('../../core/stores/execution-store')
20
+ const logManager = require('../../core/lib/log-manager')
21
21
 
22
22
  /**
23
23
  * Parse frontmatter from skill content to extract body
@@ -16,8 +16,8 @@ const fsSync = require('fs')
16
16
  const path = require('path')
17
17
  const { spawn } = require('child_process')
18
18
 
19
- const { verifyToken } = require('../lib/auth')
20
- const { config } = require('../config')
19
+ const { verifyToken } = require('../../core/lib/auth')
20
+ const { config } = require('../../core/config')
21
21
 
22
22
  /** Base directory for file storage */
23
23
  const FILES_DIR = path.join(config.HOME_DIR, 'files')
@@ -20,8 +20,8 @@ const path = require('path')
20
20
  const net = require('net')
21
21
  const execAsync = promisify(exec)
22
22
 
23
- const { verifyToken } = require('../lib/auth')
24
- const { config } = require('../config')
23
+ const { verifyToken } = require('../../core/lib/auth')
24
+ const { config } = require('../../core/config')
25
25
 
26
26
  // Ensure consistent HOME for tmux socket path
27
27
  const homeDir = config.HOME_DIR
@@ -18,10 +18,10 @@ const path = require('path')
18
18
  const fs = require('fs').promises
19
19
  const execAsync = promisify(exec)
20
20
 
21
- const { config } = require('./config')
22
- const executionStore = require('./execution-store')
23
- const routineStore = require('./routine-store')
24
- const logManager = require('./lib/log-manager')
21
+ const { config } = require('../core/config')
22
+ const executionStore = require('../core/stores/execution-store')
23
+ const routineStore = require('../core/stores/routine-store')
24
+ const logManager = require('../core/lib/log-manager')
25
25
 
26
26
  // Active cron jobs keyed by routine ID
27
27
  const activeJobs = new Map()
@@ -1,24 +1,60 @@
1
1
  /**
2
- * Minion Agent HTTP Server
2
+ * Minion Agent HTTP Server (Linux)
3
3
  *
4
- * Entry point for the minion agent. Registers all routes and manages lifecycle.
5
- * See routes/index.js for API documentation.
4
+ * Entry point for the minion agent on Linux.
5
+ * Registers shared routes (from core/) and Linux-specific routes.
6
+ *
7
+ * API Overview:
8
+ * ─────────────────────────────────────────────────────────────────────────────
9
+ * Health & Status: GET/POST /api/health, /api/status
10
+ * Commands: GET /api/commands, POST /api/command
11
+ * Skills: GET /api/list-skills, POST /api/deploy-skill, etc.
12
+ * Workflows: GET/POST/PUT/DELETE /api/workflows, /api/workflows/trigger
13
+ * Routines: GET/POST/PUT/DELETE /api/routines, /api/routines/trigger
14
+ * Terminal: GET/POST /api/terminal/sessions, /send, /kill, /capture
15
+ * Files: GET/POST/DELETE /api/files
16
+ * Directives: POST /api/directive
17
+ * Auth: GET /api/auth/status
18
+ * Chat: POST /api/chat, GET /api/chat/session, POST /api/chat/clear
19
+ * Config: GET /api/config/backup, GET/PUT /api/config/env
20
+ * Executions: GET /api/executions, GET /api/executions/:id, etc.
21
+ * ─────────────────────────────────────────────────────────────────────────────
6
22
  */
7
23
 
8
24
  const fs = require('fs')
9
25
  const path = require('path')
10
26
 
11
27
  const fastify = require('fastify')({ logger: true })
12
- const { config, validate, isHqConfigured } = require('./config')
28
+
29
+ // Package root (one level up from linux/)
30
+ const PACKAGE_ROOT = path.join(__dirname, '..')
31
+
32
+ // Core shared modules
33
+ const { config, validate, isHqConfigured } = require('../core/config')
34
+ const workflowStore = require('../core/stores/workflow-store')
35
+ const routineStore = require('../core/stores/routine-store')
36
+
37
+ // Linux-specific modules
13
38
  const workflowRunner = require('./workflow-runner')
14
- const workflowStore = require('./workflow-store')
15
39
  const routineRunner = require('./routine-runner')
16
- const routineStore = require('./routine-store')
17
-
18
- const { registerRoutes, setOffline, getProcessManager, getAllowedCommands } = require('./routes')
19
40
  const { cleanupTtyd, killStaleTtydProcesses } = require('./routes/terminal')
20
41
  const { startTerminalProxy, stopTerminalProxy } = require('./terminal-proxy')
21
42
 
43
+ // Shared routes (from core/)
44
+ const { healthRoutes, setOffline } = require('../core/routes/health')
45
+ const { skillRoutes } = require('../core/routes/skills')
46
+ const { workflowRoutes } = require('../core/routes/workflows')
47
+ const { routineRoutes } = require('../core/routes/routines')
48
+ const { authRoutes } = require('../core/routes/auth')
49
+
50
+ // Linux-specific routes
51
+ const { commandRoutes, getProcessManager, getAllowedCommands } = require('./routes/commands')
52
+ const { terminalRoutes } = require('./routes/terminal')
53
+ const { fileRoutes } = require('./routes/files')
54
+ const { directiveRoutes } = require('./routes/directives')
55
+ const { chatRoutes } = require('./routes/chat')
56
+ const { configRoutes } = require('./routes/config')
57
+
22
58
  // Validate configuration before starting
23
59
  validate()
24
60
  const PROC_MGR = getProcessManager()
@@ -50,11 +86,9 @@ process.on('SIGINT', () => shutdown('SIGINT'))
50
86
 
51
87
  /**
52
88
  * Sync bundled permissions into ~/.claude/settings.json.
53
- * Merges package-defined allow/deny into the existing settings without
54
- * removing user-added entries or non-permission keys (e.g. mcpServers).
55
89
  */
56
90
  function syncPermissions() {
57
- const bundledPath = path.join(__dirname, 'settings', 'permissions.json')
91
+ const bundledPath = path.join(PACKAGE_ROOT, 'settings', 'permissions.json')
58
92
  const settingsDir = path.join(config.HOME_DIR, '.claude')
59
93
  const settingsPath = path.join(settingsDir, 'settings.json')
60
94
 
@@ -63,13 +97,11 @@ function syncPermissions() {
63
97
 
64
98
  const bundled = JSON.parse(fs.readFileSync(bundledPath, 'utf-8'))
65
99
 
66
- // Read existing settings or start fresh
67
100
  let settings = {}
68
101
  if (fs.existsSync(settingsPath)) {
69
102
  settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8'))
70
103
  }
71
104
 
72
- // Replace permissions section with bundled values
73
105
  settings.permissions = {
74
106
  allow: bundled.allow || [],
75
107
  deny: bundled.deny || [],
@@ -85,10 +117,9 @@ function syncPermissions() {
85
117
 
86
118
  /**
87
119
  * Sync bundled tmux.conf to ~/.tmux.conf.
88
- * Enables mouse-driven scrollback (copy-mode) for the WebSocket terminal.
89
120
  */
90
121
  function syncTmuxConfig() {
91
- const bundledPath = path.join(__dirname, 'settings', 'tmux.conf')
122
+ const bundledPath = path.join(PACKAGE_ROOT, 'settings', 'tmux.conf')
92
123
  const destPath = path.join(config.HOME_DIR, '.tmux.conf')
93
124
 
94
125
  try {
@@ -103,11 +134,9 @@ function syncTmuxConfig() {
103
134
 
104
135
  /**
105
136
  * Sync bundled rules from the package to ~/.claude/rules/.
106
- * Deploys core.md only. Role context is injected per-execution, not as rules.
107
- * Removes legacy files (minion.md, role-*.md) if present.
108
137
  */
109
138
  function syncBundledRules() {
110
- const bundledRulesDir = path.join(__dirname, 'rules')
139
+ const bundledRulesDir = path.join(PACKAGE_ROOT, 'rules')
111
140
  const targetRulesDir = path.join(config.HOME_DIR, '.claude', 'rules')
112
141
 
113
142
  try {
@@ -115,14 +144,12 @@ function syncBundledRules() {
115
144
 
116
145
  fs.mkdirSync(targetRulesDir, { recursive: true })
117
146
 
118
- // Always deploy core.md
119
147
  const coreSrc = path.join(bundledRulesDir, 'core.md')
120
148
  if (fs.existsSync(coreSrc)) {
121
149
  fs.copyFileSync(coreSrc, path.join(targetRulesDir, 'core.md'))
122
150
  console.log('[Rules] Synced: core.md')
123
151
  }
124
152
 
125
- // Remove legacy files if present
126
153
  for (const legacy of ['minion.md', 'role-pm.md', 'role-engineer.md']) {
127
154
  const legacyPath = path.join(targetRulesDir, legacy)
128
155
  if (fs.existsSync(legacyPath)) {
@@ -137,11 +164,9 @@ function syncBundledRules() {
137
164
 
138
165
  /**
139
166
  * Sync bundled role context files to ~/.minion/roles/.
140
- * These are NOT loaded as Claude Code rules — they are injected
141
- * into the prompt at execution time based on the minion's role.
142
167
  */
143
168
  function syncBundledRoles() {
144
- const bundledRolesDir = path.join(__dirname, 'roles')
169
+ const bundledRolesDir = path.join(PACKAGE_ROOT, 'roles')
145
170
  const targetRolesDir = path.join(config.HOME_DIR, '.minion', 'roles')
146
171
 
147
172
  try {
@@ -163,11 +188,9 @@ function syncBundledRoles() {
163
188
 
164
189
  /**
165
190
  * Sync bundled documentation files to ~/.minion/docs/.
166
- * These are reference documents accessed on-demand by Claude Code,
167
- * NOT loaded automatically as rules.
168
191
  */
169
192
  function syncBundledDocs() {
170
- const bundledDocsDir = path.join(__dirname, 'docs')
193
+ const bundledDocsDir = path.join(PACKAGE_ROOT, 'docs')
171
194
  const targetDocsDir = path.join(config.HOME_DIR, '.minion', 'docs')
172
195
 
173
196
  try {
@@ -187,26 +210,38 @@ function syncBundledDocs() {
187
210
  }
188
211
  }
189
212
 
213
+ /**
214
+ * Register all routes (shared + Linux-specific)
215
+ */
216
+ async function registerAllRoutes(app) {
217
+ // Shared routes (from core/) - inject runners via opts
218
+ await app.register(healthRoutes)
219
+ await app.register(skillRoutes, { workflowRunner })
220
+ await app.register(workflowRoutes, { workflowRunner })
221
+ await app.register(routineRoutes, { routineRunner })
222
+ await app.register(authRoutes)
223
+
224
+ // Linux-specific routes
225
+ await app.register(commandRoutes)
226
+ await app.register(terminalRoutes)
227
+ await app.register(fileRoutes)
228
+ await app.register(directiveRoutes)
229
+ await app.register(chatRoutes)
230
+ await app.register(configRoutes)
231
+ }
232
+
190
233
  // Start server
191
234
  async function start() {
192
235
  try {
193
- // Sync bundled rules to ~/.claude/rules/ (core.md only)
236
+ // Sync bundled assets
194
237
  syncBundledRules()
195
-
196
- // Sync bundled roles to ~/.minion/roles/ (injected per-execution)
197
238
  syncBundledRoles()
198
-
199
- // Sync bundled docs to ~/.minion/docs/ (on-demand reference)
200
239
  syncBundledDocs()
201
-
202
- // Sync bundled permissions to ~/.claude/settings.json (broad allow + deny-list)
203
240
  syncPermissions()
204
-
205
- // Sync tmux.conf for mouse scroll support in WebSocket terminal
206
241
  syncTmuxConfig()
207
242
 
208
243
  // Register all routes
209
- await registerRoutes(fastify)
244
+ await registerAllRoutes(fastify)
210
245
 
211
246
  // Listen on all interfaces
212
247
  await fastify.listen({ port: config.AGENT_PORT, host: '0.0.0.0' })