@geekbeer/minion 2.32.0 → 2.33.4

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 CHANGED
@@ -19,13 +19,13 @@ const { execSync } = require('child_process')
19
19
 
20
20
  /**
21
21
  * Resolve the correct home directory for the minion user.
22
- * In supervisord environments, os.homedir() may return /root because
23
- * the HOME env var is not inherited. This function uses MINION_USER
24
- * (set in .env during setup) to look up the correct home via getent passwd.
22
+ * On Linux, supervisord environments may set HOME=/root incorrectly.
23
+ * This function uses MINION_USER + getent passwd to find the correct home.
24
+ * On Windows, os.homedir() is always correct (returns %USERPROFILE%).
25
25
  */
26
26
  function resolveHomeDir() {
27
27
  const minionUser = process.env.MINION_USER
28
- if (minionUser) {
28
+ if (minionUser && process.platform !== 'win32') {
29
29
  try {
30
30
  const entry = execSync(`getent passwd ${minionUser}`, { encoding: 'utf-8' }).trim()
31
31
  const home = entry.split(':')[5]
@@ -10,26 +10,13 @@ const fs = require('fs')
10
10
  const path = require('path')
11
11
  const { execSync } = require('child_process')
12
12
  const { config } = require('../config')
13
+ const { IS_WINDOWS, buildExtendedPath } = require('./platform')
13
14
 
14
15
  const CACHE_TTL_MS = 60000
15
16
 
16
17
  let cachedResult = null
17
18
  let cachedAt = 0
18
19
 
19
- /**
20
- * Build extended PATH that includes common Claude CLI installation locations
21
- */
22
- function getExtendedPath() {
23
- const additionalPaths = [
24
- path.join(config.HOME_DIR, '.local', 'bin'),
25
- path.join(config.HOME_DIR, 'bin'),
26
- path.join(config.HOME_DIR, '.npm-global', 'bin'),
27
- path.join(config.HOME_DIR, '.claude', 'bin'),
28
- '/usr/local/bin',
29
- ]
30
- return [...additionalPaths, process.env.PATH || '/usr/bin:/bin'].join(':')
31
- }
32
-
33
20
  /**
34
21
  * Check Claude Code authentication.
35
22
  * First checks known credential file locations (fast path),
@@ -57,14 +44,16 @@ function isClaudeAuthenticated() {
57
44
  // Fallback: check via claude CLI command (handles newer credential storage)
58
45
  try {
59
46
  const claudePath = path.join(config.HOME_DIR, '.local', 'bin', 'claude')
60
- const claudeBin = fs.existsSync(claudePath) ? claudePath : 'claude'
47
+ const claudeBin = (!IS_WINDOWS && fs.existsSync(claudePath)) ? claudePath : 'claude'
61
48
  execSync(`${claudeBin} auth whoami`, {
62
49
  encoding: 'utf-8',
63
50
  timeout: 5000,
64
51
  stdio: 'pipe',
65
52
  env: {
53
+ ...process.env,
66
54
  HOME: config.HOME_DIR,
67
- PATH: getExtendedPath(),
55
+ ...(IS_WINDOWS && { USERPROFILE: config.HOME_DIR }),
56
+ PATH: buildExtendedPath(config.HOME_DIR),
68
57
  },
69
58
  })
70
59
  return true
@@ -83,6 +72,10 @@ function isGeminiAuthenticated() {
83
72
  const possiblePaths = [
84
73
  path.join(config.HOME_DIR, '.config', 'gemini'),
85
74
  path.join(config.HOME_DIR, '.config', 'gcloud', 'application_default_credentials.json'),
75
+ // Windows-specific locations
76
+ ...(IS_WINDOWS ? [
77
+ path.join(config.HOME_DIR, 'AppData', 'Roaming', 'gcloud', 'application_default_credentials.json'),
78
+ ] : []),
86
79
  ]
87
80
  for (const p of possiblePaths) {
88
81
  try {
@@ -6,9 +6,10 @@
6
6
 
7
7
  const fs = require('fs').promises
8
8
  const path = require('path')
9
+ const platform = require('./platform')
9
10
 
10
- // Log storage configuration
11
- const LOG_DIR = '/opt/minion-agent/logs'
11
+ // Log storage configuration (platform-aware via platform.js)
12
+ const LOG_DIR = platform.LOG_DIR
12
13
  const MAX_LOG_FILES = 100
13
14
 
14
15
  /**
@@ -2,8 +2,8 @@
2
2
  * Cross-platform utility module
3
3
  *
4
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.
5
+ * Used by core/ modules (stores, log-manager, llm-checker) and win/ modules
6
+ * to provide consistent cross-platform behavior.
7
7
  */
8
8
 
9
9
  const os = require('os')
@@ -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
  }
@@ -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
  }
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@geekbeer/minion",
3
- "version": "2.32.0",
3
+ "version": "2.33.4",
4
4
  "description": "AI Agent runtime for Minion - manages status and skill deployment on VPS",
5
5
  "main": "linux/server.js",
6
6
  "bin": {
@@ -423,8 +423,16 @@ if (`$vncExe) {
423
423
 
424
424
  # Start cloudflared tunnel if configured
425
425
  `$cfConfig = Join-Path `$env:USERPROFILE '.cloudflared\config.yml'
426
- if ((Test-Path `$cfConfig) -and (Get-Command cloudflared -ErrorAction SilentlyContinue)) {
427
- Start-Process -FilePath (Get-Command cloudflared).Source -ArgumentList 'tunnel', 'run' -WindowStyle Hidden
426
+ if (Test-Path `$cfConfig) {
427
+ `$cfExe = `$null
428
+ if (Get-Command cloudflared -ErrorAction SilentlyContinue) {
429
+ `$cfExe = (Get-Command cloudflared).Source
430
+ } elseif (Test-Path (Join-Path `$DataDir 'cloudflared.exe')) {
431
+ `$cfExe = Join-Path `$DataDir 'cloudflared.exe'
432
+ }
433
+ if (`$cfExe) {
434
+ Start-Process -FilePath `$cfExe -ArgumentList 'tunnel', 'run' -WindowStyle Hidden
435
+ }
428
436
  }
429
437
 
430
438
  # Watchdog loop: restart node if it crashes
@@ -606,18 +614,18 @@ Remove-Item `$PidFile -Force -ErrorAction SilentlyContinue
606
614
  Write-Host " Fetching tunnel configuration from HQ..."
607
615
  try {
608
616
  $headers = @{ 'Authorization' = "Bearer $ApiToken" }
609
- $tunnelData = Invoke-RestMethod -Uri "$HqUrl/api/minion/tunnel-credentials" -Headers $headers
617
+ $tunnelData = Invoke-RestMethod -Uri "$HqUrl/api/minion/tunnel-credentials?platform=windows" -Headers $headers
610
618
 
611
619
  if ($tunnelData.tunnel_id) {
612
620
  $cfConfigDir = Join-Path $env:USERPROFILE '.cloudflared'
613
621
  New-Item -Path $cfConfigDir -ItemType Directory -Force | Out-Null
614
622
 
615
- # Save credentials
616
- $tunnelData.credentials_json | Set-Content (Join-Path $cfConfigDir "$($tunnelData.tunnel_id).json") -Encoding UTF8
623
+ # Save credentials (BOM-free UTF-8, required by cloudflared)
624
+ [System.IO.File]::WriteAllText((Join-Path $cfConfigDir "$($tunnelData.tunnel_id).json"), $tunnelData.credentials_json, [System.Text.UTF8Encoding]::new($false))
617
625
  Write-Detail "Tunnel credentials saved"
618
626
 
619
- # Save config
620
- $tunnelData.config_yml | Set-Content (Join-Path $cfConfigDir 'config.yml') -Encoding UTF8
627
+ # Save config (BOM-free UTF-8)
628
+ [System.IO.File]::WriteAllText((Join-Path $cfConfigDir 'config.yml'), $tunnelData.config_yml, [System.Text.UTF8Encoding]::new($false))
621
629
  Write-Detail "Tunnel config saved (will run as user process on start)"
622
630
  }
623
631
  else {
@@ -9,7 +9,7 @@ const fs = require('fs')
9
9
  const path = require('path')
10
10
  const zlib = require('zlib')
11
11
  const { verifyToken } = require('../../core/lib/auth')
12
- const { clearLlmCache } = require('../lib/llm-checker')
12
+ const { clearLlmCache } = require('../../core/lib/llm-checker')
13
13
  const { config } = require('../../core/config')
14
14
  const { resolveEnvFilePath } = require('../../core/lib/platform')
15
15
 
@@ -13,7 +13,7 @@ const { config } = require('../../core/config')
13
13
  const { writeSkillToLocal } = require('../../core/routes/skills')
14
14
  const workflowRunner = require('../workflow-runner')
15
15
  const executionStore = require('../../core/stores/execution-store')
16
- const logManager = require('../lib/log-manager')
16
+ const logManager = require('../../core/lib/log-manager')
17
17
 
18
18
  function parseFrontmatter(content) {
19
19
  const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/)
@@ -58,18 +58,37 @@ function createPtySession(sessionName, command) {
58
58
  completed: false,
59
59
  exitCode: null,
60
60
  startedAt: new Date().toISOString(),
61
+ wsClients: new Set(),
61
62
  }
62
63
 
64
+ // ttyd message type for output (must match terminal-server.js)
65
+ const MSG_OUTPUT = 0x30 // '0'
66
+
63
67
  ptyProcess.onData((data) => {
64
68
  session.buffer += data
65
69
  if (session.buffer.length > 1024 * 1024) {
66
70
  session.buffer = session.buffer.slice(-512 * 1024)
67
71
  }
72
+ // Broadcast to all WebSocket clients using ttyd OUTPUT format
73
+ if (session.wsClients && session.wsClients.size > 0) {
74
+ const outBuf = Buffer.alloc(1 + Buffer.byteLength(data))
75
+ outBuf[0] = MSG_OUTPUT
76
+ outBuf.write(data, 1)
77
+ for (const ws of session.wsClients) {
78
+ try {
79
+ if (ws.readyState === 1) ws.send(outBuf)
80
+ } catch { /* ignore */ }
81
+ }
82
+ }
68
83
  })
69
84
 
70
85
  ptyProcess.onExit(({ exitCode }) => {
71
86
  session.completed = true
72
87
  session.exitCode = exitCode
88
+ // Close all WebSocket clients
89
+ for (const ws of (session.wsClients || [])) {
90
+ try { ws.close() } catch { /* ignore */ }
91
+ }
73
92
  })
74
93
 
75
94
  activeSessions.set(sessionName, session)
@@ -14,7 +14,7 @@ const fsSync = require('fs')
14
14
  const { config } = require('../core/config')
15
15
  const executionStore = require('../core/stores/execution-store')
16
16
  const routineStore = require('../core/stores/routine-store')
17
- const logManager = require('./lib/log-manager')
17
+ const logManager = require('../core/lib/log-manager')
18
18
  const { MARKER_DIR, buildExtendedPath } = require('../core/lib/platform')
19
19
  const { activeSessions } = require('./workflow-runner')
20
20
 
@@ -168,6 +168,14 @@ function startTerminalServer() {
168
168
  const titleMsg = Buffer.from([MSG_SET_TITLE, ...Buffer.from(JSON.stringify({ title: sessionName }))])
169
169
  ws.send(titleMsg)
170
170
 
171
+ // Replay buffered output so reconnecting clients see prior content
172
+ if (session.buffer && session.buffer.length > 0) {
173
+ const outBuf = Buffer.alloc(1 + Buffer.byteLength(session.buffer))
174
+ outBuf[0] = MSG_OUTPUT
175
+ outBuf.write(session.buffer, 1)
176
+ try { ws.send(outBuf) } catch { /* ignore */ }
177
+ }
178
+
171
179
  // Handle incoming messages (ttyd protocol)
172
180
  ws.on('message', (data) => {
173
181
  if (!session.pty || session.completed) return
@@ -20,7 +20,7 @@ const fsSync = require('fs')
20
20
  const { config } = require('../core/config')
21
21
  const executionStore = require('../core/stores/execution-store')
22
22
  const workflowStore = require('../core/stores/workflow-store')
23
- const logManager = require('./lib/log-manager')
23
+ const logManager = require('../core/lib/log-manager')
24
24
  const { MARKER_DIR, buildExtendedPath } = require('../core/lib/platform')
25
25
 
26
26
  // Active cron jobs keyed by workflow ID
@@ -1,115 +0,0 @@
1
- /**
2
- * Windows LLM Service authentication checker
3
- *
4
- * Same logic as lib/llm-checker.js but with Windows-compatible paths
5
- * and PATH separator handling.
6
- */
7
-
8
- const fs = require('fs')
9
- const path = require('path')
10
- const { execSync } = require('child_process')
11
- const { config } = require('../../core/config')
12
- const { buildExtendedPath } = require('../../core/lib/platform')
13
-
14
- const CACHE_TTL_MS = 60000
15
- let cachedResult = null
16
- let cachedAt = 0
17
-
18
- function isClaudeAuthenticated() {
19
- const candidates = [
20
- path.join(config.HOME_DIR, '.claude', '.credentials.json'),
21
- path.join(config.HOME_DIR, '.claude', 'credentials.json'),
22
- ]
23
- for (const p of candidates) {
24
- try {
25
- if (fs.existsSync(p)) {
26
- const content = fs.readFileSync(p, 'utf-8')
27
- const parsed = JSON.parse(content)
28
- if (parsed && Object.keys(parsed).length > 0) return true
29
- }
30
- } catch { /* not authenticated */ }
31
- }
32
-
33
- try {
34
- const claudePath = path.join(config.HOME_DIR, '.local', 'bin', 'claude')
35
- const claudeBin = fs.existsSync(claudePath) ? claudePath : 'claude'
36
- execSync(`${claudeBin} auth whoami`, {
37
- encoding: 'utf-8',
38
- timeout: 5000,
39
- stdio: 'pipe',
40
- env: {
41
- ...process.env,
42
- HOME: config.HOME_DIR,
43
- USERPROFILE: config.HOME_DIR,
44
- PATH: buildExtendedPath(config.HOME_DIR),
45
- },
46
- })
47
- return true
48
- } catch {
49
- return false
50
- }
51
- }
52
-
53
- function isGeminiAuthenticated() {
54
- if (process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY) return true
55
-
56
- const possiblePaths = [
57
- path.join(config.HOME_DIR, '.config', 'gemini'),
58
- path.join(config.HOME_DIR, '.config', 'gcloud', 'application_default_credentials.json'),
59
- // Windows-specific locations
60
- path.join(config.HOME_DIR, 'AppData', 'Roaming', 'gcloud', 'application_default_credentials.json'),
61
- ]
62
- for (const p of possiblePaths) {
63
- try {
64
- if (!fs.existsSync(p)) continue
65
- const stat = fs.statSync(p)
66
- if (stat.isDirectory()) {
67
- if (fs.readdirSync(p).length > 0) return true
68
- } else {
69
- if (fs.readFileSync(p, 'utf-8').trim().length > 0) return true
70
- }
71
- } catch { /* ignore */ }
72
- }
73
- return false
74
- }
75
-
76
- function isCodexAuthenticated() {
77
- if (process.env.OPENAI_API_KEY) return true
78
- const codexConfig = path.join(config.HOME_DIR, '.codex')
79
- try {
80
- if (fs.existsSync(codexConfig) && fs.statSync(codexConfig).isDirectory()) {
81
- if (fs.readdirSync(codexConfig).length > 0) return true
82
- }
83
- } catch { /* ignore */ }
84
- return false
85
- }
86
-
87
- const SERVICE_DEFINITIONS = [
88
- { name: 'claude', display_name: 'Claude Code', check: isClaudeAuthenticated },
89
- { name: 'gemini', display_name: 'Gemini CLI', check: isGeminiAuthenticated },
90
- { name: 'codex', display_name: 'Codex', check: isCodexAuthenticated },
91
- ]
92
-
93
- function isLlmCommandConfigured() {
94
- return !!config.LLM_COMMAND
95
- }
96
-
97
- function getLlmServices() {
98
- const now = Date.now()
99
- if (cachedResult && (now - cachedAt) < CACHE_TTL_MS) {
100
- return cachedResult
101
- }
102
- const services = SERVICE_DEFINITIONS.map(({ name, display_name, check }) => ({
103
- name, display_name, authenticated: check(),
104
- }))
105
- cachedResult = services
106
- cachedAt = now
107
- return services
108
- }
109
-
110
- function clearLlmCache() {
111
- cachedResult = null
112
- cachedAt = 0
113
- }
114
-
115
- module.exports = { getLlmServices, clearLlmCache, isLlmCommandConfigured }
@@ -1,119 +0,0 @@
1
- /**
2
- * Windows Log Manager
3
- *
4
- * Manages workflow execution log files using platform-aware paths.
5
- * Drop-in replacement for lib/log-manager.js on Windows.
6
- */
7
-
8
- const fs = require('fs').promises
9
- const path = require('path')
10
- const { LOG_DIR } = require('../../core/lib/platform')
11
-
12
- const MAX_LOG_FILES = 100
13
-
14
- async function ensureLogDir() {
15
- try {
16
- await fs.mkdir(LOG_DIR, { recursive: true })
17
- } catch (err) {
18
- console.error(`[LogManager] Failed to create log directory: ${err.message}`)
19
- throw err
20
- }
21
- }
22
-
23
- function getLogPath(executionId) {
24
- return path.join(LOG_DIR, `${executionId}.log`)
25
- }
26
-
27
- async function logExists(executionId) {
28
- try {
29
- await fs.access(getLogPath(executionId))
30
- return true
31
- } catch {
32
- return false
33
- }
34
- }
35
-
36
- async function readLog(executionId, options = {}) {
37
- const logPath = getLogPath(executionId)
38
- try {
39
- const stats = await fs.stat(logPath)
40
- const content = await fs.readFile(logPath, 'utf-8')
41
- const allLines = content.split('\n')
42
-
43
- let resultContent = content
44
- if (options.tail && options.tail > 0) {
45
- resultContent = allLines.slice(-options.tail).join('\n')
46
- }
47
-
48
- return { content: resultContent, lines: allLines.length, size: stats.size }
49
- } catch (err) {
50
- if (err.code === 'ENOENT') return null
51
- throw err
52
- }
53
- }
54
-
55
- async function pruneOldLogs() {
56
- try {
57
- try { await fs.access(LOG_DIR) } catch { return }
58
-
59
- const files = await fs.readdir(LOG_DIR)
60
- const logFiles = files.filter(f => f.endsWith('.log'))
61
- if (logFiles.length <= MAX_LOG_FILES) return
62
-
63
- console.log(`[LogManager] Pruning logs: ${logFiles.length} files (max: ${MAX_LOG_FILES})`)
64
-
65
- const fileStats = await Promise.all(
66
- logFiles.map(async f => {
67
- try {
68
- const stats = await fs.stat(path.join(LOG_DIR, f))
69
- return { name: f, mtime: stats.mtime }
70
- } catch { return null }
71
- })
72
- )
73
-
74
- const validFiles = fileStats.filter(f => f !== null)
75
- validFiles.sort((a, b) => a.mtime - b.mtime)
76
-
77
- const toDelete = validFiles.slice(0, validFiles.length - MAX_LOG_FILES)
78
- for (const file of toDelete) {
79
- try {
80
- await fs.unlink(path.join(LOG_DIR, file.name))
81
- console.log(`[LogManager] Deleted old log: ${file.name}`)
82
- } catch (err) {
83
- console.error(`[LogManager] Failed to delete ${file.name}: ${err.message}`)
84
- }
85
- }
86
- console.log(`[LogManager] Pruned ${toDelete.length} old log files`)
87
- } catch (err) {
88
- console.error(`[LogManager] Failed to prune logs: ${err.message}`)
89
- }
90
- }
91
-
92
- async function getLogStats() {
93
- try {
94
- try { await fs.access(LOG_DIR) } catch { return { count: 0, totalSize: 0 } }
95
- const files = await fs.readdir(LOG_DIR)
96
- const logFiles = files.filter(f => f.endsWith('.log'))
97
- let totalSize = 0
98
- for (const f of logFiles) {
99
- try {
100
- const stats = await fs.stat(path.join(LOG_DIR, f))
101
- totalSize += stats.size
102
- } catch { /* skip */ }
103
- }
104
- return { count: logFiles.length, totalSize }
105
- } catch {
106
- return { count: 0, totalSize: 0 }
107
- }
108
- }
109
-
110
- module.exports = {
111
- LOG_DIR,
112
- MAX_LOG_FILES,
113
- ensureLogDir,
114
- getLogPath,
115
- logExists,
116
- readLog,
117
- pruneOldLogs,
118
- getLogStats,
119
- }