@geekbeer/minion 2.23.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 +44 -4
  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} +245 -4
  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
@@ -3,16 +3,16 @@
3
3
  *
4
4
  * Endpoints:
5
5
  * - GET /api/routines - List all routines with status
6
- * - POST /api/routines - Receive routines from HQ (upsert/additive)
6
+ * - POST /api/routines - Receive routines (upsert/additive)
7
7
  * - POST /api/routines/sync - Pull routines from HQ and sync locally
8
8
  * - PUT /api/routines/:id/schedule - Update a routine schedule (cron_expression, is_active)
9
9
  * - DELETE /api/routines/:id - Remove a routine
10
+ * - POST /api/routines/bulk-toggle - Set is_active for all routines at once
10
11
  * - POST /api/routines/trigger - Manual trigger for a routine
11
12
  */
12
13
 
13
14
  const { verifyToken } = require('../lib/auth')
14
- const routineRunner = require('../routine-runner')
15
- const routineStore = require('../routine-store')
15
+ const routineStore = require('../stores/routine-store')
16
16
  const api = require('../api')
17
17
  const { isHqConfigured } = require('../config')
18
18
 
@@ -20,7 +20,8 @@ const { isHqConfigured } = require('../config')
20
20
  * Register routine routes as Fastify plugin
21
21
  * @param {import('fastify').FastifyInstance} fastify
22
22
  */
23
- async function routineRoutes(fastify) {
23
+ async function routineRoutes(fastify, opts) {
24
+ const { routineRunner } = opts
24
25
  // Receive routines from HQ (upsert: add new, update existing)
25
26
  fastify.post('/api/routines', async (request, reply) => {
26
27
  if (!verifyToken(request)) {
@@ -218,6 +219,45 @@ async function routineRoutes(fastify) {
218
219
  }
219
220
  })
220
221
 
222
+ // Bulk toggle: set is_active for all routines at once
223
+ fastify.post('/api/routines/bulk-toggle', async (request, reply) => {
224
+ if (!verifyToken(request)) {
225
+ reply.code(401)
226
+ return { success: false, error: 'Unauthorized' }
227
+ }
228
+
229
+ const { is_active } = request.body || {}
230
+
231
+ if (typeof is_active !== 'boolean') {
232
+ reply.code(400)
233
+ return { success: false, error: 'is_active (boolean) is required' }
234
+ }
235
+
236
+ console.log(`[Routines] Bulk toggle: setting all routines is_active=${is_active}`)
237
+
238
+ try {
239
+ const routines = await routineStore.load()
240
+
241
+ for (const routine of routines) {
242
+ routine.is_active = is_active
243
+ }
244
+
245
+ await routineStore.save(routines)
246
+ routineRunner.loadRoutines(routines)
247
+
248
+ const count = routines.length
249
+ return {
250
+ success: true,
251
+ message: `${count} routines set to is_active=${is_active}`,
252
+ count,
253
+ }
254
+ } catch (error) {
255
+ console.error(`[Routines] Failed to bulk toggle: ${error.message}`)
256
+ reply.code(500)
257
+ return { success: false, error: error.message }
258
+ }
259
+ })
260
+
221
261
  // Manual trigger: run a routine immediately
222
262
  fastify.post('/api/routines/trigger', async (request, reply) => {
223
263
  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)
@@ -5,6 +5,7 @@
5
5
  #
6
6
  # Usage:
7
7
  # sudo minion-cli setup [options] # Set up minion agent service (root)
8
+ # sudo minion-cli reconfigure [options] # Re-register with new HQ credentials (root)
8
9
  # sudo minion-cli start # Start agent service (root)
9
10
  # sudo minion-cli stop # Stop agent service (root)
10
11
  # sudo minion-cli restart # Restart agent service (root)
@@ -40,7 +41,7 @@ fi
40
41
 
41
42
  # Resolve version from package.json (installed location)
42
43
  CLI_DIR="$(cd "$(dirname "$(readlink -f "$0")")" && pwd)"
43
- CLI_VERSION="$(node -p "require('${CLI_DIR}/package.json').version")"
44
+ CLI_VERSION="$(node -p "require('${CLI_DIR}/../package.json').version")"
44
45
 
45
46
  # Use sudo only when not running as root
46
47
  SUDO=""
@@ -88,6 +89,28 @@ svc_control() {
88
89
 
89
90
  AGENT_URL="${MINION_AGENT_URL:-http://localhost:8080}"
90
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
+
91
114
  # Auto-load .env so that API_TOKEN etc. are available in interactive shells
92
115
  ENV_FILE="/opt/minion-agent/.env"
93
116
  if [ -f "$ENV_FILE" ] && [ -r "$ENV_FILE" ]; then
@@ -318,7 +341,7 @@ do_setup() {
318
341
  echo "[6/${TOTAL_STEPS}] Creating service configuration ($PROC_MGR)..."
319
342
  local NPM_ROOT
320
343
  NPM_ROOT="$(npm root -g)"
321
- local SERVER_PATH="${NPM_ROOT}/@geekbeer/minion/server.js"
344
+ local SERVER_PATH="${NPM_ROOT}/@geekbeer/minion/linux/server.js"
322
345
 
323
346
  if [ ! -f "$SERVER_PATH" ]; then
324
347
  echo " ERROR: server.js not found at $SERVER_PATH"
@@ -731,14 +754,33 @@ CFEOF
731
754
  echo "Notifying HQ of setup completion..."
732
755
  local NOTIFY_RESPONSE
733
756
  local HOSTNAME_VAL
757
+ local LAN_IP
734
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
+
735
773
  NOTIFY_RESPONSE=$(curl -sfL -X POST "${HQ_URL}/api/minion/setup-complete" \
736
774
  -H "Content-Type: application/json" \
737
775
  -H "Authorization: Bearer ${API_TOKEN}" \
738
- -d "{\"internal_ip_address\":\"${HOSTNAME_VAL}\"}" 2>&1) || true
776
+ -d "$BODY" 2>&1) || true
739
777
 
740
778
  if echo "$NOTIFY_RESPONSE" | grep -q '"success":true' 2>/dev/null; then
741
- 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
742
784
  else
743
785
  echo " -> HQ notification skipped (HQ may not be reachable)"
744
786
  fi
@@ -763,6 +805,193 @@ CFEOF
763
805
  fi
764
806
  }
765
807
 
808
+ # ============================================================
809
+ # reconfigure subcommand
810
+ # ============================================================
811
+ do_reconfigure() {
812
+ local HQ_URL=""
813
+ local MINION_ID=""
814
+ local API_TOKEN=""
815
+
816
+ # Parse arguments
817
+ while [[ $# -gt 0 ]]; do
818
+ case "$1" in
819
+ --hq-url)
820
+ HQ_URL="$2"
821
+ shift 2
822
+ ;;
823
+ --minion-id)
824
+ MINION_ID="$2"
825
+ shift 2
826
+ ;;
827
+ --api-token)
828
+ API_TOKEN="$2"
829
+ shift 2
830
+ ;;
831
+ *)
832
+ echo "Unknown option: $1"
833
+ echo "Usage: sudo minion-cli reconfigure --hq-url <URL> --minion-id <UUID> --api-token <TOKEN>"
834
+ exit 1
835
+ ;;
836
+ esac
837
+ done
838
+
839
+ # Validate required arguments
840
+ if [ -z "$HQ_URL" ] || [ -z "$MINION_ID" ] || [ -z "$API_TOKEN" ]; then
841
+ echo "ERROR: All three options are required: --hq-url, --minion-id, --api-token"
842
+ echo "Usage: sudo minion-cli reconfigure --hq-url <URL> --minion-id <UUID> --api-token <TOKEN>"
843
+ exit 1
844
+ fi
845
+
846
+ # Check that .env exists (setup must have been run before)
847
+ if [ ! -f /opt/minion-agent/.env ]; then
848
+ echo "ERROR: /opt/minion-agent/.env not found."
849
+ echo "It looks like minion-cli setup has not been run on this server."
850
+ echo "Please run 'sudo minion-cli setup' first for initial installation."
851
+ exit 1
852
+ fi
853
+
854
+ echo "========================================="
855
+ echo " @geekbeer/minion Reconfigure"
856
+ echo "========================================="
857
+ echo "HQ: $HQ_URL"
858
+ echo "Minion ID: $MINION_ID"
859
+ echo ""
860
+
861
+ # Step 1: Read existing .env and preserve non-credential keys
862
+ echo "[1/4] Updating .env credentials..."
863
+ local ENV_CONTENT=""
864
+ ENV_CONTENT+="# Minion Agent Configuration\n"
865
+ ENV_CONTENT+="# Reconfigured by minion-cli reconfigure\n\n"
866
+ ENV_CONTENT+="HQ_URL=${HQ_URL}\n"
867
+ ENV_CONTENT+="API_TOKEN=${API_TOKEN}\n"
868
+ ENV_CONTENT+="MINION_ID=${MINION_ID}\n"
869
+
870
+ # Preserve non-credential keys from existing .env
871
+ while IFS='=' read -r key value; do
872
+ [[ -z "$key" || "$key" == \#* ]] && continue
873
+ case "$key" in
874
+ HQ_URL|API_TOKEN|MINION_ID) ;; # Skip — already set above
875
+ *) ENV_CONTENT+="${key}=${value}\n" ;;
876
+ esac
877
+ done < /opt/minion-agent/.env
878
+
879
+ echo -e "$ENV_CONTENT" | $SUDO tee /opt/minion-agent/.env > /dev/null
880
+ echo " -> /opt/minion-agent/.env updated"
881
+
882
+ # Step 2: Update process manager config & restart service
883
+ echo "[2/4] Restarting minion-agent service..."
884
+ if [ -z "$PROC_MGR" ]; then
885
+ echo " WARNING: No supported process manager found"
886
+ echo " Please restart the minion-agent service manually"
887
+ elif [ "$PROC_MGR" = "supervisord" ]; then
888
+ # supervisord bakes env vars into conf — must regenerate
889
+ local CONF_FILE="/etc/supervisor/conf.d/minion-agent.conf"
890
+ if [ -f "$CONF_FILE" ]; then
891
+ # Read current conf to preserve command, user, and other settings
892
+ local CURRENT_COMMAND
893
+ CURRENT_COMMAND=$(grep '^command=' "$CONF_FILE" | head -1)
894
+ local CURRENT_DIRECTORY
895
+ CURRENT_DIRECTORY=$(grep '^directory=' "$CONF_FILE" | head -1)
896
+ local CURRENT_USER_LINE
897
+ CURRENT_USER_LINE=$(grep '^user=' "$CONF_FILE" || echo "")
898
+
899
+ # Rebuild environment line from updated .env
900
+ local ENV_LINE="environment="
901
+ # Detect home dir for the service user
902
+ local SVC_USER
903
+ SVC_USER=$(echo "$CURRENT_USER_LINE" | sed 's/user=//')
904
+ local SVC_HOME="$HOME"
905
+ if [ -n "$SVC_USER" ]; then
906
+ SVC_HOME=$(getent passwd "$SVC_USER" | cut -d: -f6 || echo "$HOME")
907
+ fi
908
+ local ENV_PAIRS=("HOME=\"${SVC_HOME}\"" "DISPLAY=\":99\"")
909
+ while IFS='=' read -r key value; do
910
+ [[ -z "$key" || "$key" == \#* ]] && continue
911
+ ENV_PAIRS+=("${key}=\"${value}\"")
912
+ done < /opt/minion-agent/.env
913
+ ENV_LINE+="$(IFS=,; echo "${ENV_PAIRS[*]}")"
914
+
915
+ $SUDO tee "$CONF_FILE" > /dev/null <<SUPEOF
916
+ [program:minion-agent]
917
+ ${CURRENT_COMMAND}
918
+ ${CURRENT_DIRECTORY}
919
+ ${CURRENT_USER_LINE}
920
+ ${ENV_LINE}
921
+ autorestart=true
922
+ priority=500
923
+ startsecs=3
924
+ stdout_logfile=/var/log/supervisor/minion-agent.log
925
+ stderr_logfile=/var/log/supervisor/minion-agent.log
926
+ SUPEOF
927
+ echo " -> supervisord conf updated"
928
+ fi
929
+ $SUDO supervisorctl reread > /dev/null 2>&1
930
+ $SUDO supervisorctl update > /dev/null 2>&1
931
+ echo " -> minion-agent restarted (supervisord)"
932
+ else
933
+ # systemd reads .env via EnvironmentFile — just restart
934
+ svc_control restart
935
+ echo " -> minion-agent restarted (systemd)"
936
+ fi
937
+
938
+ # Step 3: Health check
939
+ echo "[3/4] Verifying agent health..."
940
+ local HEALTH_OK=false
941
+ for i in $(seq 1 5); do
942
+ if curl -sf http://localhost:8080/api/health > /dev/null 2>&1; then
943
+ HEALTH_OK=true
944
+ break
945
+ fi
946
+ sleep 2
947
+ done
948
+ if [ "$HEALTH_OK" = true ]; then
949
+ echo " -> Agent is healthy"
950
+ else
951
+ echo " WARNING: Agent health check failed after 5 attempts"
952
+ echo " Check logs for details"
953
+ fi
954
+
955
+ # Step 4: Notify HQ (best-effort — heartbeat will also notify automatically)
956
+ echo "[4/4] Notifying HQ..."
957
+ local NOTIFY_RESPONSE
958
+ local HOSTNAME_VAL
959
+ local LAN_IP
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
+
972
+ NOTIFY_RESPONSE=$(curl -sfL -X POST "${HQ_URL}/api/minion/setup-complete" \
973
+ -H "Content-Type: application/json" \
974
+ -H "Authorization: Bearer ${API_TOKEN}" \
975
+ -d "$BODY" 2>&1) || true
976
+
977
+ if echo "$NOTIFY_RESPONSE" | grep -q '"success":true' 2>/dev/null; then
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
983
+ else
984
+ echo " -> Skipped (heartbeat will notify HQ within 30s)"
985
+ fi
986
+
987
+ echo ""
988
+ echo "========================================="
989
+ echo " Reconfigure Complete!"
990
+ echo "========================================="
991
+ echo ""
992
+ echo "The minion should appear online in HQ shortly."
993
+ }
994
+
766
995
  # ============================================================
767
996
  # Main command dispatch
768
997
  # ============================================================
@@ -776,6 +1005,12 @@ case "${1:-}" in
776
1005
  do_setup "$@"
777
1006
  ;;
778
1007
 
1008
+ reconfigure)
1009
+ require_root reconfigure
1010
+ shift
1011
+ do_reconfigure "$@"
1012
+ ;;
1013
+
779
1014
  status)
780
1015
  curl -s "$AGENT_URL/api/status" | jq .
781
1016
  ;;
@@ -866,6 +1101,7 @@ case "${1:-}" in
866
1101
  echo ""
867
1102
  echo "Usage:"
868
1103
  echo " sudo minion-cli setup [options] # Set up agent service (root)"
1104
+ echo " sudo minion-cli reconfigure [options] # Re-register with new HQ credentials (root)"
869
1105
  echo " sudo minion-cli start # Start agent service (root)"
870
1106
  echo " sudo minion-cli stop # Stop agent service (root)"
871
1107
  echo " sudo minion-cli restart # Restart agent service (root)"
@@ -882,6 +1118,11 @@ case "${1:-}" in
882
1118
  echo " --api-token <TOKEN> API token (optional)"
883
1119
  echo " --setup-tunnel Set up cloudflared tunnel (requires --hq-url, --api-token)"
884
1120
  echo ""
1121
+ echo "Reconfigure options:"
1122
+ echo " --hq-url <URL> HQ server URL (required)"
1123
+ echo " --minion-id <UUID> Minion ID (required)"
1124
+ echo " --api-token <TOKEN> API token (required)"
1125
+ echo ""
885
1126
  echo "Status values: online, offline, busy"
886
1127
  echo ""
887
1128
  echo "Environment:"
@@ -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')