mio-agent-runtime 0.5.9 → 0.5.11

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/README.md CHANGED
@@ -54,6 +54,9 @@ Restart your agent after install. That's it.
54
54
  | `mio init` | Create `~/.mio-intelligence` and default config |
55
55
  | `mio install <host>` | Wire MCP + rules + observer into a host |
56
56
  | `mio uninstall <host>` | Remove Mio MCP, rules, approvals, and observer from a host |
57
+ | `mio upgrade` | Upgrade Mio via npm and re-wire all installed hosts to the new server |
58
+ | `mio doctor` | Diagnose install/runtime issues: hosts, observer, version drift, PATH |
59
+ | `mio dashboard [--start\|--stop\|--status\|--open]` | Start the live dashboard HTTP server (browser view of MIO_HOME data) |
57
60
  | `mio mcp` | Start MCP server (stdio, for agent integration) |
58
61
  | `mio status` | Show runtime state, adapter status, observer PID |
59
62
  | `mio agents` | List installed hosts |
@@ -239,6 +242,8 @@ WorkBuddy ─┘ + Observer ......... automatic data collection
239
242
  | >= 0.5.0 | Idea generation + evolution suggestions |
240
243
  | >= 0.5.6 | Rewritten documentation |
241
244
  | >= 0.5.9 | `mio uninstall <host>` command |
245
+ | >= 0.5.10 | `mio upgrade` and `mio doctor` commands |
246
+ | >= 0.5.11 | Live web dashboard (`mio dashboard` + `mio.dashboard.*`) |
242
247
 
243
248
  ---
244
249
 
package/bin/mio.js CHANGED
@@ -4,8 +4,11 @@
4
4
  const fs = require('fs')
5
5
  const path = require('path')
6
6
  const os = require('os')
7
+ const { spawnSync, spawn } = require('child_process')
7
8
 
8
9
  const SERVER_SCRIPT = path.resolve(__dirname, '..', 'server', 'mio-intelligence-mcp', 'index.js')
10
+ const SERVER_SCRIPT_DASH = path.resolve(__dirname, '..', 'server', 'mio-intelligence-mcp', 'dashboard-server.js')
11
+ const DASH_PORT = 8787
9
12
  const MIO_HOME = process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
10
13
  const CONFIG_FILE = path.join(MIO_HOME, 'config.json')
11
14
 
@@ -262,6 +265,154 @@ function uninstallCommand(host, useJson) {
262
265
  console.log(result.message || `${host} adapter removed.`)
263
266
  }
264
267
 
268
+ function runNpm(args) {
269
+ try {
270
+ const res = spawnSync('npm', args, { encoding: 'utf8', shell: true, windowsHide: true })
271
+ if (res.error) return { ok: false, error: res.error.message, stdout: '', stderr: '' }
272
+ return { ok: true, status: res.status, stdout: res.stdout || '', stderr: res.stderr || '' }
273
+ } catch (e) {
274
+ return { ok: false, error: e.message, stdout: '', stderr: '' }
275
+ }
276
+ }
277
+
278
+ function npmGlobalVersion() {
279
+ const r = runNpm(['ls', '-g', 'mio-agent-runtime'])
280
+ const m = r.stdout && r.stdout.match(/mio-agent-runtime@(\d+\.\d+\.\d+)/)
281
+ return m ? m[1] : null
282
+ }
283
+
284
+ function npmLatestVersion() {
285
+ const r = runNpm(['view', 'mio-agent-runtime', 'version'])
286
+ const m = r.stdout && r.stdout.trim().match(/(\d+\.\d+\.\d+)/)
287
+ return m ? m[1] : null
288
+ }
289
+
290
+ function upgradeCommand(useJson) {
291
+ const before = npmGlobalVersion()
292
+ const installRes = runNpm(['install', '-g', 'mio-agent-runtime@latest'])
293
+ if (!installRes.ok || installRes.status !== 0) {
294
+ const msg = 'npm install failed: ' + (installRes.error || installRes.stderr || 'unknown error')
295
+ if (useJson) return jsonOrText({ ok: false, error: msg }, true)
296
+ console.error(msg)
297
+ process.exitCode = 1
298
+ return
299
+ }
300
+ const after = npmGlobalVersion()
301
+ const prefix = (() => { const r = runNpm(['config', 'get', 'prefix']); return r.stdout ? r.stdout.trim() : '' })()
302
+ const isWin = process.platform === 'win32'
303
+ const globalServer = prefix ? path.join(prefix, 'node_modules', 'mio-agent-runtime', 'server', 'mio-intelligence-mcp', 'index.js') : SERVER_SCRIPT
304
+ const globalNode = prefix ? (isWin ? path.join(prefix, 'node.exe') : path.join(prefix, 'bin', 'node')) : process.execPath
305
+ const nodeBin = fs.existsSync(globalNode) ? globalNode : process.execPath
306
+ const config = readConfig()
307
+ const hosts = Object.keys(config.agents || {})
308
+ const rewired = []
309
+ for (const host of hosts) {
310
+ const adapter = adapters[host]
311
+ if (!adapter) continue
312
+ try {
313
+ const result = adapter.install({ node: nodeBin, serverScript: globalServer, home: MIO_HOME, workspace: process.cwd() })
314
+ if (result.changed) rewired.push(host)
315
+ } catch (_) {}
316
+ }
317
+ let observerStarted = false
318
+ try { observerStarted = observer.startBackground(MIO_HOME).started } catch (_) {}
319
+ const payload = { ok: true, before, after, rewiredHosts: rewired, observerStarted }
320
+ if (useJson) return jsonOrText(payload, true)
321
+ console.log('Upgraded Mio: ' + (before || '?') + ' -> ' + (after || '?'))
322
+ if (rewired.length) console.log('Re-wired hosts: ' + rewired.join(', '))
323
+ else console.log('All installed hosts already up to date.')
324
+ console.log(observerStarted ? 'Observer started.' : 'Observer already running.')
325
+ }
326
+
327
+ function doctorCommand(useJson) {
328
+ const checks = []
329
+ function add(name, ok, detail) { checks.push({ name: name, ok: Boolean(ok), detail: detail || '' }) }
330
+ add('MIO_HOME exists', fs.existsSync(MIO_HOME), MIO_HOME)
331
+ if (fs.existsSync(MIO_HOME)) {
332
+ try { fs.accessSync(MIO_HOME, fs.constants.W_OK); add('MIO_HOME writable', true) }
333
+ catch (_) { add('MIO_HOME writable', false, 'no write permission') }
334
+ }
335
+ add('MCP server present', fs.existsSync(SERVER_SCRIPT), SERVER_SCRIPT)
336
+ for (const host of ['codex', 'opencode', 'workbuddy', 'hermes']) {
337
+ const adapter = adapters[host]
338
+ const installed = adapter ? adapter.isInstalled() : false
339
+ add('host:' + host + ' installed', installed)
340
+ }
341
+ const obsRunning = observer.isRunning(MIO_HOME)
342
+ add('observer running', obsRunning, obsRunning ? 'pid ' + observer.readPid(MIO_HOME) : 'not running')
343
+ const installed = npmGlobalVersion()
344
+ const latest = npmLatestVersion()
345
+ add('npm up to date', installed === latest, 'installed ' + installed + ' / latest ' + latest)
346
+ const prefix = (() => { const r = runNpm(['config', 'get', 'prefix']); return r.stdout ? r.stdout.trim() : '' })()
347
+ const execPrefix = path.dirname(process.execPath)
348
+ add('global bin on PATH', prefix && execPrefix && prefix.toLowerCase() === execPrefix.toLowerCase(), 'npm prefix ' + prefix + ' / node ' + execPrefix)
349
+ const stray0 = path.join(process.cwd(), '0')
350
+ add('no stray 0 file', !fs.existsSync(stray0), fs.existsSync(stray0) ? stray0 : '')
351
+ const failed = checks.filter(function (c) { return !c.ok })
352
+ const payload = { ok: failed.length === 0, checks: checks, failedCount: failed.length }
353
+ if (useJson) return jsonOrText(payload, true)
354
+ for (const c of checks) {
355
+ console.log((c.ok ? 'OK ' : 'FAIL ') + c.name + (c.detail ? ' (' + c.detail + ')' : ''))
356
+ }
357
+ console.log('')
358
+ console.log(failed.length === 0 ? 'All checks passed.' : failed.length + ' check(s) failed. Run `mio upgrade` or reinstall affected hosts.')
359
+ }
360
+
361
+ function dashPidPath() { return path.join(MIO_HOME, 'dashboard.pid') }
362
+ function isDashRunning() {
363
+ const pidFile = dashPidPath()
364
+ if (!fs.existsSync(pidFile)) return false
365
+ try { process.kill(parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10), 0); return true } catch (_) { return false }
366
+ }
367
+ function startDashBackground() {
368
+ if (isDashRunning()) return { started: false, already: true }
369
+ fs.mkdirSync(MIO_HOME, { recursive: true })
370
+ const child = spawn(process.execPath, [SERVER_SCRIPT_DASH, String(DASH_PORT)], { env: Object.assign({}, process.env, { MIO_DATA_DIR: MIO_HOME }), detached: true, stdio: 'ignore' })
371
+ child.unref()
372
+ fs.writeFileSync(dashPidPath(), String(child.pid))
373
+ return { started: true, pid: child.pid }
374
+ }
375
+ function stopDash() {
376
+ const pidFile = dashPidPath()
377
+ if (!fs.existsSync(pidFile)) return { stopped: false }
378
+ try { process.kill(parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10), 'SIGTERM') } catch (_) {}
379
+ try { fs.unlinkSync(pidFile) } catch (_) {}
380
+ return { stopped: true }
381
+ }
382
+ function dashboardCommand(args) {
383
+ const flags = args.slice(1)
384
+ const useJson = flags.includes('--json')
385
+ const url = 'http://localhost:' + DASH_PORT
386
+ if (flags.includes('--start')) {
387
+ const r = startDashBackground()
388
+ const started = r.started
389
+ if (useJson) return jsonOrText({ running: true, started: started, url: url }, true)
390
+ console.log(started ? 'Dashboard started at ' + url : 'Dashboard already running at ' + url)
391
+ return
392
+ }
393
+ if (flags.includes('--stop')) {
394
+ const r = stopDash()
395
+ if (useJson) return jsonOrText({ running: false, stopped: r.stopped }, true)
396
+ console.log(r.stopped ? 'Dashboard stopped.' : 'Dashboard was not running.')
397
+ return
398
+ }
399
+ if (flags.includes('--status')) {
400
+ const running = isDashRunning()
401
+ if (useJson) return jsonOrText({ running: running, url: running ? url : null }, true)
402
+ console.log(running ? 'Dashboard: running (' + url + ')' : 'Dashboard: not running')
403
+ return
404
+ }
405
+ if (flags.includes('--open')) {
406
+ startDashBackground()
407
+ try { require('child_process').exec('start ' + url) } catch (_) {}
408
+ if (useJson) return jsonOrText({ running: true, url: url }, true)
409
+ console.log('Dashboard opened at ' + url)
410
+ return
411
+ }
412
+ const child = spawn(process.execPath, [SERVER_SCRIPT_DASH, String(DASH_PORT)], { env: Object.assign({}, process.env, { MIO_DATA_DIR: MIO_HOME }), stdio: 'inherit' })
413
+ child.on('exit', function () { try { fs.unlinkSync(dashPidPath()) } catch (_) {} })
414
+ }
415
+
265
416
  function help() {
266
417
  console.log(`Mio Agent Runtime CLI
267
418
 
@@ -271,10 +422,13 @@ Usage:
271
422
  mio mcp Start Mio MCP server (stdio)
272
423
  mio install <host> Install Mio into a host (codex|opencode|workbuddy|hermes)
273
424
  mio uninstall <host> Remove Mio from a host
425
+ mio upgrade Upgrade Mio and re-wire all installed hosts
426
+ mio doctor Diagnose install and runtime issues
274
427
  mio status Show runtime and adapter status
275
428
  mio agents List installed agents
276
429
  mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes
277
430
  mio observe --start|--stop|--status|--once Manage the background observer daemon
431
+ mio dashboard [--start|--stop|--status|--open] Start the live dashboard HTTP server
278
432
  mio --json status Machine-readable status
279
433
  mio --json agents Machine-readable agents
280
434
  `)
@@ -299,12 +453,18 @@ function main() {
299
453
  return installCommand(args[1], useJson)
300
454
  case 'uninstall':
301
455
  return uninstallCommand(args[1], useJson)
456
+ case 'upgrade':
457
+ return upgradeCommand(useJson)
458
+ case 'doctor':
459
+ return doctorCommand(useJson)
302
460
  case 'status':
303
461
  return statusCommand(useJson)
304
462
  case 'agents':
305
463
  return agentsCommand(useJson)
306
464
  case 'observe':
307
465
  return observeCommand(args)
466
+ case 'dashboard':
467
+ return dashboardCommand(args)
308
468
  case 'help':
309
469
  case '--help':
310
470
  return help()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mio-agent-runtime",
3
- "version": "0.5.9",
3
+ "version": "0.5.11",
4
4
  "description": "Mio Agent Runtime installer and MCP intelligence layer",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -16,7 +16,7 @@
16
16
  "!server/mio-intelligence-mcp/__tests__"
17
17
  ],
18
18
  "scripts": {
19
- "check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check observe/observer.js",
19
+ "check": "node --check bin/mio.js && node --check adapters/host-utils.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check adapters/hermes.js && node --check server/mio-intelligence-mcp/index.js && node --check server/mio-intelligence-mcp/phase0.js && node --check server/mio-intelligence-mcp/dashboard-server.js && node --check observe/observer.js",
20
20
  "prepack": "npm run check"
21
21
  },
22
22
  "engines": {
@@ -0,0 +1,171 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const http = require('http')
4
+ const os = require('os')
5
+ const { spawn } = require('child_process')
6
+
7
+ function dataDir() {
8
+ return process.env.MIO_DATA_DIR || path.join(os.homedir(), '.mio-intelligence')
9
+ }
10
+
11
+ function readJsonl(file) {
12
+ const full = path.join(dataDir(), file)
13
+ if (!fs.existsSync(full)) return []
14
+ try {
15
+ return fs.readFileSync(full, 'utf8')
16
+ .split('\n')
17
+ .map(function (l) { return l.trim() })
18
+ .filter(function (l) { return l.length })
19
+ .map(function (l) {
20
+ try { return JSON.parse(l) } catch (_) { return null }
21
+ })
22
+ .filter(Boolean)
23
+ } catch (_) {
24
+ return []
25
+ }
26
+ }
27
+
28
+ function tally(rows, keyFn) {
29
+ const out = {}
30
+ for (const r of rows) {
31
+ const k = keyFn(r)
32
+ if (k == null || k === '') continue
33
+ out[k] = (out[k] || 0) + 1
34
+ }
35
+ return out
36
+ }
37
+
38
+ function topN(obj, n) {
39
+ return Object.keys(obj)
40
+ .sort(function (a, b) { return obj[b] - obj[a] })
41
+ .slice(0, n)
42
+ .map(function (k) { return { key: k, count: obj[k] } })
43
+ }
44
+
45
+ function aggregate() {
46
+ const memory = readJsonl('memory.jsonl')
47
+ const traces = readJsonl('traces.jsonl')
48
+ const experience = readJsonl('experience_reuse.jsonl')
49
+ const ideas = readJsonl('ideas.jsonl')
50
+ const suggestions = readJsonl('suggestions.jsonl')
51
+ const agents = readJsonl('agents.jsonl')
52
+
53
+ const memoryByKind = tally(memory, function (r) { return r.kind })
54
+ const memoryBySource = tally(memory, function (r) { return r.source })
55
+ const memoryByProject = tally(memory, function (r) { return r.project })
56
+
57
+ const tracesByHost = tally(traces, function (r) { return r.host || 'unknown' })
58
+ const tracesByOutcome = tally(traces, function (r) { return r.outcome || 'unknown' })
59
+ const tracesByEventType = tally(traces, function (r) { return r.event_type || 'unknown' })
60
+
61
+ const taskOutcomes = traces.filter(function (t) { return t.event_type === 'task_outcome' })
62
+ const successCount = taskOutcomes.filter(function (t) { return t.outcome === 'success' }).length
63
+ const taskTotal = taskOutcomes.length
64
+ const successRate = taskTotal ? (successCount / taskTotal * 100).toFixed(1) : '0.0'
65
+
66
+ const verified = experience.filter(function (e) {
67
+ return e.reuse && e.behaviorChanged && e.outcomeImproved
68
+ }).length
69
+ const reuseCount = experience.filter(function (e) { return e.reuse }).length
70
+ const crossAgent = experience.filter(function (e) {
71
+ return e.sourceAgent && e.targetAgent && e.sourceAgent !== e.targetAgent
72
+ }).length
73
+
74
+ const hosts = Object.keys(tracesByHost).filter(function (h) { return h && h !== 'mcp' })
75
+ const phase0 = {
76
+ hosts: hosts.length,
77
+ tasks: taskTotal,
78
+ verifiedReuse: verified,
79
+ successRate: successRate,
80
+ thresholds: { hosts: 2, tasks: 20, verifiedReuse: 5 },
81
+ passed: hosts.length >= 2 && taskTotal >= 20 && verified >= 5,
82
+ }
83
+
84
+ function recent(rows, n) {
85
+ return rows.slice(-n).map(function (r) {
86
+ return {
87
+ id: r.id,
88
+ timestamp: r.timestamp,
89
+ kind: r.kind || r.event_type || null,
90
+ source: r.source || r.host || null,
91
+ text: (r.content || r.notes || r.trace_id || '').toString().slice(0, 160),
92
+ }
93
+ })
94
+ }
95
+
96
+ return {
97
+ generatedAt: new Date().toISOString(),
98
+ dataDir: dataDir(),
99
+ summary: {
100
+ memory: memory.length,
101
+ traces: traces.length,
102
+ experience: experience.length,
103
+ ideas: ideas.length,
104
+ suggestions: suggestions.length,
105
+ agents: agents.length,
106
+ },
107
+ memoryByKind: topN(memoryByKind, 10),
108
+ memoryBySource: topN(memoryBySource, 10),
109
+ memoryByProject: topN(memoryByProject, 10),
110
+ tracesByHost: topN(tracesByHost, 10),
111
+ tracesByOutcome: topN(tracesByOutcome, 10),
112
+ tracesByEventType: topN(tracesByEventType, 10),
113
+ experience: {
114
+ total: experience.length,
115
+ reuse: reuseCount,
116
+ verified: verified,
117
+ crossAgent: crossAgent,
118
+ reuseRate: experience.length ? (reuseCount / experience.length * 100).toFixed(1) : '0.0',
119
+ },
120
+ agents: agents.slice(-10),
121
+ phase0: phase0,
122
+ ideas: ideas.slice(-10).map(function (i) {
123
+ return { id: i.id, title: i.title, goal: i.goal, strategy: i.provenance && i.provenance.strategy }
124
+ }),
125
+ suggestions: suggestions.slice(-10).map(function (s) {
126
+ return { id: s.id, title: s.title, nextAction: s.nextAction, impact: s.impact }
127
+ }),
128
+ recentMemory: recent(memory, 10).reverse(),
129
+ recentTraces: recent(traces, 10).reverse(),
130
+ }
131
+ }
132
+
133
+ function startServer(port, htmlPath) {
134
+ const server = http.createServer(function (req, res) {
135
+ const url = (req.url || '/').split('?')[0]
136
+ if (url === '/api/data' || url === '/api/health') {
137
+ res.setHeader('Content-Type', 'application/json; charset=utf-8')
138
+ res.setHeader('Access-Control-Allow-Origin', '*')
139
+ if (url === '/api/health') {
140
+ res.end(JSON.stringify({ ok: true }))
141
+ return
142
+ }
143
+ res.end(JSON.stringify(aggregate(), null, 2))
144
+ return
145
+ }
146
+ if (url === '/' || url === '/index.html' || url === '/dashboard') {
147
+ res.setHeader('Content-Type', 'text/html; charset=utf-8')
148
+ if (htmlPath && fs.existsSync(htmlPath)) {
149
+ res.end(fs.readFileSync(htmlPath, 'utf8'))
150
+ } else {
151
+ res.end('<h1>Mio Dashboard</h1><p>HTML not found at ' + htmlPath + '</p>')
152
+ }
153
+ return
154
+ }
155
+ res.statusCode = 404
156
+ res.end('Not found')
157
+ })
158
+ server.listen(port, function () {
159
+ console.log('Mio dashboard running at http://localhost:' + port)
160
+ })
161
+ return server
162
+ }
163
+
164
+ // When run directly: `node dashboard-server.js [port]`
165
+ if (require.main === module) {
166
+ const port = parseInt(process.argv[2] || process.env.MIO_DASHBOARD_PORT || '8787', 10)
167
+ const htmlPath = path.join(__dirname, '..', '..', 'mio-dashboard.html')
168
+ startServer(port, htmlPath)
169
+ }
170
+
171
+ module.exports = { startServer, aggregate, dataDir }
@@ -1729,11 +1729,67 @@ async function callTool(name, args = {}) {
1729
1729
  return listIdeas(args)
1730
1730
  case 'mio.evolution.suggest':
1731
1731
  return evolutionSuggest(args)
1732
+ case 'mio.dashboard.start':
1733
+ return handleDashboardStart(args)
1734
+ case 'mio.dashboard.stop':
1735
+ return handleDashboardStop()
1736
+ case 'mio.dashboard.status':
1737
+ return handleDashboardStatus()
1732
1738
  default:
1733
1739
  throw new Error(`Unknown tool: ${name}`)
1734
1740
  }
1735
1741
  }
1736
1742
 
1743
+ const DASHBOARD_PORT = 8787
1744
+ const dashboardPidPath = () => path.join(dataDir, 'dashboard.pid')
1745
+
1746
+ function isDashboardRunning() {
1747
+ const pidFile = dashboardPidPath()
1748
+ if (!fs.existsSync(pidFile)) return false
1749
+ try {
1750
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
1751
+ process.kill(pid, 0)
1752
+ return true
1753
+ } catch (_) {
1754
+ return false
1755
+ }
1756
+ }
1757
+
1758
+ function handleDashboardStart(args = {}) {
1759
+ const port = parseInt(args.port || process.env.MIO_DASHBOARD_PORT || DASHBOARD_PORT, 10)
1760
+ const url = 'http://localhost:' + port
1761
+ if (isDashboardRunning()) {
1762
+ if (args.open) { try { require('child_process').exec('start ' + url) } catch (_) {} }
1763
+ return { url: url, running: true, message: 'Dashboard already running at ' + url }
1764
+ }
1765
+ const serverScript = path.join(__dirname, 'dashboard-server.js')
1766
+ const child = require('child_process').spawn(process.execPath, [serverScript, String(port)], {
1767
+ env: Object.assign({}, process.env, { MIO_DATA_DIR: dataDir }),
1768
+ detached: true,
1769
+ stdio: 'ignore',
1770
+ })
1771
+ child.unref()
1772
+ fs.writeFileSync(dashboardPidPath(), String(child.pid))
1773
+ if (args.open) { try { require('child_process').exec('start ' + url) } catch (_) {} }
1774
+ return { url: url, running: true, pid: child.pid, message: 'Dashboard started at ' + url }
1775
+ }
1776
+
1777
+ function handleDashboardStop() {
1778
+ const pidFile = dashboardPidPath()
1779
+ if (!fs.existsSync(pidFile)) return { running: false, message: 'Dashboard not running.' }
1780
+ try {
1781
+ const pid = parseInt(fs.readFileSync(pidFile, 'utf8').trim(), 10)
1782
+ process.kill(pid, 'SIGTERM')
1783
+ } catch (_) {}
1784
+ try { fs.unlinkSync(pidFile) } catch (_) {}
1785
+ return { running: false, message: 'Dashboard stopped.' }
1786
+ }
1787
+
1788
+ function handleDashboardStatus() {
1789
+ const running = isDashboardRunning()
1790
+ return { running: running, url: running ? 'http://localhost:' + DASHBOARD_PORT : null }
1791
+ }
1792
+
1737
1793
  function jsonRpcResult(id, result) {
1738
1794
  return { jsonrpc: '2.0', id, result }
1739
1795
  }