@geekbeer/minion 2.32.0 → 2.42.3

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 (42) hide show
  1. package/.env.example +0 -3
  2. package/README.md +0 -1
  3. package/core/api.js +13 -0
  4. package/core/config.js +50 -5
  5. package/core/lib/llm-checker.js +9 -16
  6. package/core/lib/log-manager.js +7 -3
  7. package/core/lib/platform.js +10 -15
  8. package/core/lib/revision-watcher.js +252 -0
  9. package/core/lib/step-poller.js +222 -0
  10. package/core/lib/strip-ansi.js +18 -0
  11. package/core/lib/workflow-orchestrator.js +382 -0
  12. package/core/routes/diagnose.js +296 -0
  13. package/core/routes/health.js +27 -0
  14. package/core/routes/routines.js +15 -10
  15. package/core/routes/skills.js +4 -1
  16. package/core/routes/workflows.js +49 -2
  17. package/core/stores/chat-store.js +12 -5
  18. package/core/stores/execution-store.js +4 -4
  19. package/core/stores/routine-store.js +7 -7
  20. package/core/stores/workflow-store.js +5 -6
  21. package/linux/lib/process-manager.js +14 -0
  22. package/linux/minion-cli.sh +57 -16
  23. package/linux/routes/chat.js +182 -20
  24. package/linux/routes/config.js +8 -12
  25. package/linux/routine-runner.js +5 -4
  26. package/linux/server.js +53 -1
  27. package/linux/workflow-runner.js +25 -61
  28. package/package.json +1 -1
  29. package/roles/pm.md +11 -12
  30. package/win/lib/process-manager.js +15 -0
  31. package/win/minion-cli.ps1 +79 -17
  32. package/win/routes/chat.js +178 -14
  33. package/win/routes/config.js +7 -3
  34. package/win/routes/directives.js +1 -1
  35. package/win/routes/terminal.js +19 -0
  36. package/win/routine-runner.js +5 -3
  37. package/win/server.js +53 -0
  38. package/win/terminal-server.js +8 -0
  39. package/win/workflow-runner.js +32 -44
  40. package/skills/execution-report/SKILL.md +0 -106
  41. package/win/lib/llm-checker.js +0 -115
  42. package/win/lib/log-manager.js +0 -119
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Diagnostic endpoint for checking all essential services
3
+ *
4
+ * Endpoints:
5
+ * - GET /api/diagnose - Run full diagnostic check
6
+ *
7
+ * Checks:
8
+ * - Minion Server (Fastify) status
9
+ * - HQ connectivity (heartbeat reachability)
10
+ * - Cloudflare Tunnel process
11
+ * - VNC / websockify (display server)
12
+ * - Terminal proxy (ttyd / terminal-server)
13
+ * - LLM CLI availability (Claude, Gemini, Codex)
14
+ * - Environment variable configuration
15
+ */
16
+
17
+ const { execSync } = require('child_process')
18
+ const { config, isHqConfigured } = require('../config')
19
+ const { version } = require('../../package.json')
20
+ const { getLlmServices, isLlmCommandConfigured } = require('../lib/llm-checker')
21
+ const { IS_WINDOWS } = require('../lib/platform')
22
+ const { getStatus } = require('./health')
23
+
24
+ /**
25
+ * Check if a process is running by name.
26
+ * @param {string} name - Process name to search for
27
+ * @returns {boolean}
28
+ */
29
+ function isProcessRunning(name) {
30
+ try {
31
+ if (IS_WINDOWS) {
32
+ const out = execSync(`tasklist /FI "IMAGENAME eq ${name}" /NH`, {
33
+ encoding: 'utf-8',
34
+ timeout: 5000,
35
+ stdio: 'pipe',
36
+ })
37
+ return out.toLowerCase().includes(name.toLowerCase())
38
+ } else {
39
+ execSync(`pgrep -f "${name}"`, {
40
+ encoding: 'utf-8',
41
+ timeout: 5000,
42
+ stdio: 'pipe',
43
+ })
44
+ return true
45
+ }
46
+ } catch {
47
+ return false
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Check if a local TCP port is listening.
53
+ * @param {number} port
54
+ * @returns {boolean}
55
+ */
56
+ function isPortListening(port) {
57
+ try {
58
+ if (IS_WINDOWS) {
59
+ const out = execSync(`netstat -an | findstr ":${port} "`, {
60
+ encoding: 'utf-8',
61
+ timeout: 5000,
62
+ stdio: 'pipe',
63
+ })
64
+ return out.includes('LISTENING')
65
+ } else {
66
+ const out = execSync(`ss -tlnp 2>/dev/null | grep ":${port} " || netstat -tlnp 2>/dev/null | grep ":${port} "`, {
67
+ encoding: 'utf-8',
68
+ timeout: 5000,
69
+ stdio: 'pipe',
70
+ })
71
+ return out.trim().length > 0
72
+ }
73
+ } catch {
74
+ return false
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Check HQ connectivity by sending a test heartbeat.
80
+ * @returns {Promise<{ ok: boolean, latency_ms?: number, error?: string }>}
81
+ */
82
+ async function checkHqConnectivity() {
83
+ if (!isHqConfigured()) {
84
+ return { ok: false, error: 'HQ not configured (standalone mode)' }
85
+ }
86
+
87
+ const url = `${config.HQ_URL}/api/minion/heartbeat`
88
+ const { currentStatus, currentTask } = getStatus()
89
+ const start = Date.now()
90
+
91
+ try {
92
+ const response = await fetch(url, {
93
+ method: 'POST',
94
+ headers: {
95
+ 'Content-Type': 'application/json',
96
+ 'Authorization': `Bearer ${config.API_TOKEN}`,
97
+ },
98
+ body: JSON.stringify({ status: currentStatus, current_task: currentTask, version }),
99
+ signal: AbortSignal.timeout(10000),
100
+ })
101
+ const latency = Date.now() - start
102
+
103
+ if (!response.ok) {
104
+ const data = await response.json().catch(() => ({}))
105
+ return { ok: false, latency_ms: latency, error: `HTTP ${response.status}: ${data.error || response.statusText}` }
106
+ }
107
+
108
+ return { ok: true, latency_ms: latency }
109
+ } catch (err) {
110
+ const latency = Date.now() - start
111
+ return { ok: false, latency_ms: latency, error: err.message }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Check Cloudflare Tunnel status.
117
+ * @returns {{ running: boolean, details?: string }}
118
+ */
119
+ function checkTunnel() {
120
+ if (IS_WINDOWS) {
121
+ const running = isProcessRunning('cloudflared.exe')
122
+ return { running, details: running ? 'cloudflared.exe process found' : 'cloudflared.exe not running' }
123
+ }
124
+
125
+ // Linux: check for cloudflared process
126
+ const running = isProcessRunning('cloudflared')
127
+ if (!running) {
128
+ return { running: false, details: 'cloudflared not running' }
129
+ }
130
+
131
+ // Check if it's a tunnel connector (not just any cloudflared)
132
+ try {
133
+ const out = execSync('pgrep -af "cloudflared.*tunnel"', {
134
+ encoding: 'utf-8',
135
+ timeout: 5000,
136
+ stdio: 'pipe',
137
+ }).trim()
138
+ return { running: true, details: out.split('\n')[0] || 'cloudflared tunnel running' }
139
+ } catch {
140
+ return { running: true, details: 'cloudflared running (tunnel mode unconfirmed)' }
141
+ }
142
+ }
143
+
144
+ /**
145
+ * Check VNC / display server status.
146
+ * @returns {{ running: boolean, port?: number, details?: string }}
147
+ */
148
+ function checkVnc() {
149
+ if (IS_WINDOWS) {
150
+ // TightVNC on Windows
151
+ const running = isProcessRunning('tvnserver.exe')
152
+ const websockify = isPortListening(6080)
153
+ return {
154
+ running: running && websockify,
155
+ details: [
156
+ `TightVNC: ${running ? 'running' : 'not running'}`,
157
+ `websockify (:6080): ${websockify ? 'listening' : 'not listening'}`,
158
+ ].join(', '),
159
+ }
160
+ }
161
+
162
+ // Linux: Xvfb + x11vnc + websockify (noVNC)
163
+ const xvfb = isProcessRunning('Xvfb')
164
+ const vnc = isProcessRunning('x11vnc')
165
+ const websockify = isPortListening(6080)
166
+ return {
167
+ running: xvfb && websockify,
168
+ details: [
169
+ `Xvfb: ${xvfb ? 'running' : 'not running'}`,
170
+ `x11vnc: ${vnc ? 'running' : 'not running'}`,
171
+ `websockify (:6080): ${websockify ? 'listening' : 'not listening'}`,
172
+ ].join(', '),
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Check terminal proxy status.
178
+ * @returns {{ running: boolean, port?: number, details?: string }}
179
+ */
180
+ function checkTerminal() {
181
+ if (IS_WINDOWS) {
182
+ // Windows terminal server runs on port 7681
183
+ const listening = isPortListening(7681)
184
+ return {
185
+ running: listening,
186
+ details: `terminal-server (:7681): ${listening ? 'listening' : 'not listening'}`,
187
+ }
188
+ }
189
+
190
+ // Linux: ttyd on port 7681
191
+ const ttyd = isProcessRunning('ttyd')
192
+ const listening = isPortListening(7681)
193
+ return {
194
+ running: ttyd || listening,
195
+ details: [
196
+ `ttyd: ${ttyd ? 'running' : 'not running'}`,
197
+ `port 7681: ${listening ? 'listening' : 'not listening'}`,
198
+ ].join(', '),
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Check LLM service availability.
204
+ * @returns {{ services: object[], llm_command: { configured: boolean, value: string } }}
205
+ */
206
+ function checkLlm() {
207
+ return {
208
+ services: getLlmServices(),
209
+ llm_command: {
210
+ configured: isLlmCommandConfigured(),
211
+ value: config.LLM_COMMAND || '(not set)',
212
+ },
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Check environment variable configuration.
218
+ * @returns {{ configured: string[], missing: string[] }}
219
+ */
220
+ function checkEnv() {
221
+ const required = ['HQ_URL', 'API_TOKEN', 'MINION_ID']
222
+ const optional = ['AGENT_PORT', 'LLM_COMMAND', 'HEARTBEAT_INTERVAL', 'MINION_USER']
223
+
224
+ const configured = []
225
+ const missing = []
226
+
227
+ for (const key of required) {
228
+ if (config[key] || process.env[key]) {
229
+ configured.push(key)
230
+ } else {
231
+ missing.push(key)
232
+ }
233
+ }
234
+ for (const key of optional) {
235
+ if (config[key] || process.env[key]) {
236
+ configured.push(key)
237
+ }
238
+ }
239
+
240
+ return { configured, missing }
241
+ }
242
+
243
+ /**
244
+ * Register diagnose routes as Fastify plugin
245
+ * @param {import('fastify').FastifyInstance} fastify
246
+ */
247
+ async function diagnoseRoutes(fastify) {
248
+ fastify.get('/api/diagnose', async () => {
249
+ const { currentStatus, currentTask } = getStatus()
250
+
251
+ // Run HQ check (async) in parallel with sync checks
252
+ const [hq, tunnel, vnc, terminal, llm, env] = await Promise.all([
253
+ checkHqConnectivity(),
254
+ Promise.resolve(checkTunnel()),
255
+ Promise.resolve(checkVnc()),
256
+ Promise.resolve(checkTerminal()),
257
+ Promise.resolve(checkLlm()),
258
+ Promise.resolve(checkEnv()),
259
+ ])
260
+
261
+ // Build summary: count ok/warn/fail
262
+ const checks = {
263
+ agent: { ok: true, details: `status=${currentStatus}, uptime=${Math.floor(process.uptime())}s` },
264
+ hq: { ok: hq.ok, details: hq.error || `latency=${hq.latency_ms}ms` },
265
+ tunnel: { ok: tunnel.running, details: tunnel.details },
266
+ vnc: { ok: vnc.running, details: vnc.details },
267
+ terminal: { ok: terminal.running, details: terminal.details },
268
+ llm: {
269
+ ok: llm.services.some(s => s.authenticated) && llm.llm_command.configured,
270
+ details: llm.services.map(s => `${s.name}:${s.authenticated ? 'ok' : 'ng'}`).join(', ')
271
+ + ` | llm_command:${llm.llm_command.configured ? 'ok' : 'ng'}`,
272
+ },
273
+ env: {
274
+ ok: env.missing.length === 0,
275
+ details: env.missing.length === 0
276
+ ? `all configured (${env.configured.join(', ')})`
277
+ : `missing: ${env.missing.join(', ')}`,
278
+ },
279
+ }
280
+
281
+ const okCount = Object.values(checks).filter(c => c.ok).length
282
+ const totalCount = Object.keys(checks).length
283
+ const allOk = okCount === totalCount
284
+
285
+ return {
286
+ summary: allOk ? 'ALL OK' : `${okCount}/${totalCount} checks passed`,
287
+ version,
288
+ platform: IS_WINDOWS ? 'windows' : 'linux',
289
+ timestamp: new Date().toISOString(),
290
+ current_task: currentTask,
291
+ checks,
292
+ }
293
+ })
294
+ }
295
+
296
+ module.exports = { diagnoseRoutes }
@@ -8,8 +8,15 @@
8
8
  */
9
9
 
10
10
  const { version } = require('../../package.json')
11
+ const { config, isHqConfigured } = require('../config')
12
+ const { sendHeartbeat } = require('../api')
11
13
  const { getLlmServices, isLlmCommandConfigured } = require('../lib/llm-checker')
12
14
 
15
+ function maskToken(token) {
16
+ if (!token || token.length < 8) return token ? '***' : ''
17
+ return token.slice(0, 3) + '...' + token.slice(-3)
18
+ }
19
+
13
20
  // Shared status state
14
21
  let currentStatus = 'online'
15
22
  let currentTask = null
@@ -51,6 +58,15 @@ async function healthRoutes(fastify) {
51
58
  timestamp: new Date().toISOString(),
52
59
  llm_services: getLlmServices(),
53
60
  llm_command_configured: isLlmCommandConfigured(),
61
+ env: {
62
+ HQ_URL: config.HQ_URL || '',
63
+ MINION_ID: config.MINION_ID || '',
64
+ MINION_USER: process.env.MINION_USER || '',
65
+ AGENT_PORT: config.AGENT_PORT,
66
+ API_TOKEN: maskToken(config.API_TOKEN),
67
+ LLM_COMMAND: config.LLM_COMMAND || '',
68
+ HEARTBEAT_INTERVAL: process.env.HEARTBEAT_INTERVAL || '',
69
+ },
54
70
  }
55
71
  })
56
72
 
@@ -58,18 +74,29 @@ async function healthRoutes(fastify) {
58
74
  fastify.post('/api/status', async (request, reply) => {
59
75
  const { status, current_task } = request.body || {}
60
76
 
77
+ let changed = false
78
+
61
79
  if (status) {
62
80
  if (!['online', 'offline', 'busy'].includes(status)) {
63
81
  reply.code(400)
64
82
  return { success: false, error: `Invalid status: ${status}` }
65
83
  }
84
+ if (currentStatus !== status) changed = true
66
85
  currentStatus = status
67
86
  }
68
87
 
69
88
  if (current_task !== undefined) {
89
+ if (currentTask !== (current_task || null)) changed = true
70
90
  currentTask = current_task || null
71
91
  }
72
92
 
93
+ // Push status change to HQ immediately via heartbeat
94
+ if (changed && isHqConfigured()) {
95
+ sendHeartbeat({ status: currentStatus, current_task: currentTask, version }).catch(err => {
96
+ console.error('[Heartbeat] Status-change heartbeat failed:', err.message)
97
+ })
98
+ }
99
+
73
100
  return { success: true }
74
101
  })
75
102
  }
@@ -81,7 +81,7 @@ async function routineRoutes(fastify, opts) {
81
81
 
82
82
  // Merge routine data with runtime status
83
83
  const result = routines.map(r => {
84
- const runtimeInfo = status.routines.find(rr => rr.id === r.id)
84
+ const runtimeInfo = status.routines.find(rr => String(rr.id) === String(r.id))
85
85
  return {
86
86
  ...r,
87
87
  next_run: runtimeInfo?.next_run || null,
@@ -149,13 +149,15 @@ async function routineRoutes(fastify, opts) {
149
149
  const { id } = request.params
150
150
  const updates = request.body || {}
151
151
 
152
- console.log(`[Routines] Updating routine schedule: ${id}`)
152
+ console.log(`[Routines] Updating routine schedule: ${id} (type: ${typeof id})`)
153
153
 
154
154
  try {
155
155
  const routines = await routineStore.load()
156
- const index = routines.findIndex(r => r.id === id)
156
+ const index = routines.findIndex(r => String(r.id) === String(id))
157
157
 
158
158
  if (index < 0) {
159
+ const storedIds = routines.map(r => `${r.name}(id=${r.id}, type=${typeof r.id})`).join(', ')
160
+ console.error(`[Routines] Schedule update: routine not found. Requested id=${id}. Stored: [${storedIds}]`)
159
161
  reply.code(404)
160
162
  return { success: false, error: 'Routine not found' }
161
163
  }
@@ -195,7 +197,7 @@ async function routineRoutes(fastify, opts) {
195
197
 
196
198
  try {
197
199
  const routines = await routineStore.load()
198
- const index = routines.findIndex(r => r.id === id)
200
+ const index = routines.findIndex(r => String(r.id) === String(id))
199
201
 
200
202
  if (index < 0) {
201
203
  reply.code(404)
@@ -265,20 +267,23 @@ async function routineRoutes(fastify, opts) {
265
267
  return { success: false, error: 'Unauthorized' }
266
268
  }
267
269
 
268
- const { routine_id } = request.body || {}
270
+ const { routine_id, routine_name } = request.body || {}
269
271
 
270
- if (!routine_id) {
272
+ if (!routine_id && !routine_name) {
271
273
  reply.code(400)
272
- return { success: false, error: 'routine_id is required' }
274
+ return { success: false, error: 'routine_id or routine_name is required' }
273
275
  }
274
276
 
275
- // Find the routine in local store
277
+ // Find the routine in local store (try id first, then name fallback)
276
278
  const routines = await routineStore.load()
277
- const routine = routines.find(r => r.id === routine_id)
279
+ const routine = routines.find(r => routine_id != null && String(r.id) === String(routine_id))
280
+ || (routine_name && routines.find(r => r.name === routine_name))
278
281
 
279
282
  if (!routine) {
283
+ const storedIds = routines.map(r => `${r.name}(${r.id || 'no-id'})`).join(', ')
284
+ console.error(`[Routines] Trigger: routine not found. Requested id=${routine_id}, name=${routine_name}. Stored: [${storedIds}]`)
280
285
  reply.code(404)
281
- return { success: false, error: 'Routine not found' }
286
+ return { success: false, error: `Routine not found (id=${routine_id}). Available: [${storedIds}]` }
282
287
  }
283
288
 
284
289
  console.log(`[Routines] Manual trigger for: ${routine.name}`)
@@ -51,7 +51,7 @@ function parseFrontmatter(content) {
51
51
  * @param {Array<{filename: string, content: string}>} [opts.references] - Reference files
52
52
  * @returns {Promise<{path: string, references_count: number}>}
53
53
  */
54
- async function writeSkillToLocal(name, { content, description, display_name, references = [] }) {
54
+ async function writeSkillToLocal(name, { content, description, display_name, type, references = [] }) {
55
55
  const skillDir = path.join(config.HOME_DIR, '.claude', 'skills', name)
56
56
  const referencesDir = path.join(skillDir, 'references')
57
57
 
@@ -62,6 +62,7 @@ async function writeSkillToLocal(name, { content, description, display_name, ref
62
62
  const frontmatterLines = [
63
63
  `name: ${name}`,
64
64
  display_name ? `display_name: ${display_name}` : null,
65
+ type ? `type: ${type}` : null,
65
66
  `description: ${description || ''}`,
66
67
  ].filter(Boolean).join('\n')
67
68
 
@@ -118,6 +119,7 @@ async function pushSkillToHQ(name) {
118
119
  display_name: metadata.display_name || metadata.name || name,
119
120
  description: metadata.description || '',
120
121
  content: body,
122
+ type: metadata.type || 'workflow',
121
123
  references,
122
124
  }),
123
125
  })
@@ -240,6 +242,7 @@ async function skillRoutes(fastify, opts) {
240
242
  content: skill.content,
241
243
  description: skill.description,
242
244
  display_name: skill.display_name,
245
+ type: skill.type,
243
246
  references: skill.references || [],
244
247
  })
245
248
 
@@ -14,10 +14,12 @@
14
14
  * - GET /api/executions/:id - Get single execution details
15
15
  * - GET /api/executions/:id/log - Get execution log file content
16
16
  * - POST /api/executions/:id/outcome - Update execution outcome (no auth, local)
17
+ * - POST /api/workflows/orchestrate - Start multi-minion orchestration (from HQ)
17
18
  */
18
19
 
19
20
  const fs = require('fs').promises
20
21
  const path = require('path')
22
+ const { orchestrate } = require('../lib/workflow-orchestrator')
21
23
 
22
24
  const { verifyToken } = require('../lib/auth')
23
25
  const workflowStore = require('../stores/workflow-store')
@@ -493,8 +495,53 @@ async function workflowRoutes(fastify, opts) {
493
495
  }
494
496
  })
495
497
 
496
- // Update execution outcome (called by /execution-report skill)
497
- // This endpoint does NOT require auth - skills run in the same environment
498
+ // Orchestrate a multi-minion workflow execution (called by HQ)
499
+ fastify.post('/api/workflows/orchestrate', async (request, reply) => {
500
+ if (!verifyToken(request)) {
501
+ reply.code(401)
502
+ return { success: false, error: 'Unauthorized' }
503
+ }
504
+
505
+ const { execution_id, steps, hq_url } = request.body || {}
506
+
507
+ if (!execution_id || !Array.isArray(steps)) {
508
+ reply.code(400)
509
+ return { success: false, error: 'execution_id and steps[] are required' }
510
+ }
511
+
512
+ console.log(`[Workflows] Starting orchestration for execution: ${execution_id} (${steps.length} steps)`)
513
+
514
+ // Load optional PM revision policy
515
+ let revisionPolicy = ''
516
+ try {
517
+ const policyPath = path.join(config.HOME_DIR, '.minion', 'revision-policy.md')
518
+ revisionPolicy = await fs.readFile(policyPath, 'utf-8')
519
+ } catch {
520
+ // No custom policy — use defaults
521
+ }
522
+
523
+ // Run orchestration asynchronously
524
+ orchestrate({ executionId: execution_id, steps, hqUrl: hq_url, revisionPolicy })
525
+ .then((result) => {
526
+ if (result.success) {
527
+ console.log(`[Workflows] Orchestration completed: ${execution_id}`)
528
+ } else {
529
+ console.error(`[Workflows] Orchestration failed: ${execution_id} — ${result.error}`)
530
+ }
531
+ })
532
+ .catch((err) => {
533
+ console.error(`[Workflows] Orchestration error: ${execution_id} — ${err.message}`)
534
+ })
535
+
536
+ return {
537
+ success: true,
538
+ message: 'Orchestration started',
539
+ execution_id,
540
+ }
541
+ })
542
+
543
+ // Update execution outcome (called by workflow runner after completion)
544
+ // This endpoint does NOT require auth - runs in the same environment
498
545
  fastify.post('/api/executions/:id/outcome', async (request, reply) => {
499
546
  const { id } = request.params
500
547
  const { outcome, summary, details } = request.body || {}
@@ -8,18 +8,18 @@ const fs = require('fs').promises
8
8
  const path = require('path')
9
9
 
10
10
  const { config } = require('../config')
11
+ const { DATA_DIR } = require('../lib/platform')
11
12
 
12
13
  const MAX_MESSAGES = 100
13
14
 
14
15
  /**
15
16
  * Get chat session file path
16
- * Uses /opt/minion-agent/ if available, otherwise home dir
17
+ * Uses DATA_DIR if available (platform-aware), otherwise home dir
17
18
  */
18
19
  function getFilePath() {
19
- const optPath = '/opt/minion-agent/chat-session.json'
20
20
  try {
21
- require('fs').accessSync(path.dirname(optPath))
22
- return optPath
21
+ require('fs').accessSync(DATA_DIR)
22
+ return path.join(DATA_DIR, 'chat-session.json')
23
23
  } catch {
24
24
  return path.join(config.HOME_DIR, 'chat-session.json')
25
25
  }
@@ -61,8 +61,9 @@ async function save(session) {
61
61
  * Creates a new session if none exists
62
62
  * @param {string} sessionId - Claude CLI session ID
63
63
  * @param {{ role: string, content: string }} msg - Message to add
64
+ * @param {number} [turnCount] - Optional turn count to update session with
64
65
  */
65
- async function addMessage(sessionId, msg) {
66
+ async function addMessage(sessionId, msg, turnCount) {
66
67
  let session = await load()
67
68
 
68
69
  // If session_id changed, start a new session
@@ -70,6 +71,7 @@ async function addMessage(sessionId, msg) {
70
71
  session = {
71
72
  session_id: sessionId,
72
73
  messages: [],
74
+ turn_count: 0,
73
75
  created_at: Date.now(),
74
76
  updated_at: Date.now(),
75
77
  }
@@ -82,6 +84,11 @@ async function addMessage(sessionId, msg) {
82
84
  })
83
85
  session.updated_at = Date.now()
84
86
 
87
+ // Update turn count if provided
88
+ if (typeof turnCount === 'number' && turnCount > 0) {
89
+ session.turn_count = (session.turn_count || 0) + turnCount
90
+ }
91
+
85
92
  // Prune old messages
86
93
  if (session.messages.length > MAX_MESSAGES) {
87
94
  session.messages = session.messages.slice(-MAX_MESSAGES)
@@ -8,19 +8,19 @@ const fs = require('fs').promises
8
8
  const path = require('path')
9
9
 
10
10
  const { config } = require('../config')
11
+ const { DATA_DIR } = require('../lib/platform')
11
12
 
12
13
  // Max executions to keep (older ones are pruned)
13
14
  const MAX_EXECUTIONS = 200
14
15
 
15
16
  /**
16
17
  * Get execution file path
17
- * Uses /opt/minion-agent/ if available, otherwise home dir
18
+ * Uses DATA_DIR if available (platform-aware), otherwise home dir
18
19
  */
19
20
  function getExecutionFilePath() {
20
- const optPath = '/opt/minion-agent/executions.json'
21
21
  try {
22
- require('fs').accessSync(path.dirname(optPath))
23
- return optPath
22
+ require('fs').accessSync(DATA_DIR)
23
+ return path.join(DATA_DIR, 'executions.json')
24
24
  } catch {
25
25
  return path.join(config.HOME_DIR, 'executions.json')
26
26
  }
@@ -8,14 +8,14 @@ const fs = require('fs').promises
8
8
  const path = require('path')
9
9
 
10
10
  const { config } = require('../config')
11
+ const { DATA_DIR } = require('../lib/platform')
11
12
 
12
- // Routine file location: /opt/minion-agent/routines.json (systemd/supervisord)
13
- // or ~/routines.json (standalone)
13
+ // Routine file location: DATA_DIR/routines.json (platform-aware)
14
+ // or ~/routines.json (fallback)
14
15
  function getRoutineFilePath() {
15
- const optPath = '/opt/minion-agent/routines.json'
16
16
  try {
17
- require('fs').accessSync(path.dirname(optPath))
18
- return optPath
17
+ require('fs').accessSync(DATA_DIR)
18
+ return path.join(DATA_DIR, 'routines.json')
19
19
  } catch {
20
20
  return path.join(config.HOME_DIR, 'routines.json')
21
21
  }
@@ -60,7 +60,7 @@ async function save(routines) {
60
60
  */
61
61
  async function updateLastRun(routineId) {
62
62
  const routines = await load()
63
- const routine = routines.find(r => r.id === routineId)
63
+ const routine = routines.find(r => String(r.id) === String(routineId))
64
64
  if (routine) {
65
65
  routine.last_run = new Date().toISOString()
66
66
  await save(routines)
@@ -85,7 +85,7 @@ async function findByName(name) {
85
85
  */
86
86
  async function upsertFromHQ(routineData) {
87
87
  const routines = await load()
88
- const index = routines.findIndex(r => r.id === routineData.id)
88
+ const index = routines.findIndex(r => String(r.id) === String(routineData.id))
89
89
 
90
90
  if (index >= 0) {
91
91
  // Update from HQ (preserve local-only fields if any)
@@ -8,15 +8,14 @@ const fs = require('fs').promises
8
8
  const path = require('path')
9
9
 
10
10
  const { config } = require('../config')
11
+ const { DATA_DIR } = require('../lib/platform')
11
12
 
12
- // Workflow file location: /opt/minion-agent/workflows.json (systemd/supervisord)
13
- // or ~/workflows.json (standalone)
13
+ // Workflow file location: DATA_DIR/workflows.json (platform-aware)
14
+ // or ~/workflows.json (fallback)
14
15
  function getWorkflowFilePath() {
15
- const optPath = '/opt/minion-agent/workflows.json'
16
- // Use /opt path if it exists (production), otherwise home dir
17
16
  try {
18
- require('fs').accessSync(path.dirname(optPath))
19
- return optPath
17
+ require('fs').accessSync(DATA_DIR)
18
+ return path.join(DATA_DIR, 'workflows.json')
20
19
  } catch {
21
20
  return path.join(config.HOME_DIR, 'workflows.json')
22
21
  }