mio-agent-runtime 0.5.10 → 0.5.12
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 +2 -0
- package/adapters/hermes.js +2 -2
- package/bin/mio.js +61 -1
- package/observe/observer.js +11 -2
- package/package.json +2 -2
- package/server/mio-intelligence-mcp/dashboard-server.js +171 -0
- package/server/mio-intelligence-mcp/index.js +56 -0
package/README.md
CHANGED
|
@@ -56,6 +56,7 @@ Restart your agent after install. That's it.
|
|
|
56
56
|
| `mio uninstall <host>` | Remove Mio MCP, rules, approvals, and observer from a host |
|
|
57
57
|
| `mio upgrade` | Upgrade Mio via npm and re-wire all installed hosts to the new server |
|
|
58
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) |
|
|
59
60
|
| `mio mcp` | Start MCP server (stdio, for agent integration) |
|
|
60
61
|
| `mio status` | Show runtime state, adapter status, observer PID |
|
|
61
62
|
| `mio agents` | List installed hosts |
|
|
@@ -242,6 +243,7 @@ WorkBuddy ─┘ + Observer ......... automatic data collection
|
|
|
242
243
|
| >= 0.5.6 | Rewritten documentation |
|
|
243
244
|
| >= 0.5.9 | `mio uninstall <host>` command |
|
|
244
245
|
| >= 0.5.10 | `mio upgrade` and `mio doctor` commands |
|
|
246
|
+
| >= 0.5.11 | Live web dashboard (`mio dashboard` + `mio.dashboard.*`) |
|
|
245
247
|
|
|
246
248
|
---
|
|
247
249
|
|
package/adapters/hermes.js
CHANGED
|
@@ -55,8 +55,8 @@ function runHermes(args) {
|
|
|
55
55
|
try {
|
|
56
56
|
const isCmdShim = Boolean(bin && /\.(cmd|bat)$/i.test(bin))
|
|
57
57
|
res = bin
|
|
58
|
-
? spawnSync(bin, args, { encoding: 'utf8', timeout: 60000, windowsHide: true, input: input, shell: isCmdShim })
|
|
59
|
-
: spawnSync('hermes', args, { encoding: 'utf8', timeout: 60000, shell: true, windowsHide: true, input: input })
|
|
58
|
+
? spawnSync(bin, args, { encoding: 'utf8', timeout: 60000, windowsHide: true, input: input, shell: isCmdShim, cwd: hermesHome() })
|
|
59
|
+
: spawnSync('hermes', args, { encoding: 'utf8', timeout: 60000, shell: true, windowsHide: true, input: input, cwd: hermesHome() })
|
|
60
60
|
} catch (_) {
|
|
61
61
|
return null
|
|
62
62
|
}
|
package/bin/mio.js
CHANGED
|
@@ -4,9 +4,11 @@
|
|
|
4
4
|
const fs = require('fs')
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const os = require('os')
|
|
7
|
-
const { spawnSync } = require('child_process')
|
|
7
|
+
const { spawnSync, spawn } = require('child_process')
|
|
8
8
|
|
|
9
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
|
|
10
12
|
const MIO_HOME = process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
|
|
11
13
|
const CONFIG_FILE = path.join(MIO_HOME, 'config.json')
|
|
12
14
|
|
|
@@ -356,6 +358,61 @@ function doctorCommand(useJson) {
|
|
|
356
358
|
console.log(failed.length === 0 ? 'All checks passed.' : failed.length + ' check(s) failed. Run `mio upgrade` or reinstall affected hosts.')
|
|
357
359
|
}
|
|
358
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
|
+
|
|
359
416
|
function help() {
|
|
360
417
|
console.log(`Mio Agent Runtime CLI
|
|
361
418
|
|
|
@@ -371,6 +428,7 @@ Usage:
|
|
|
371
428
|
mio agents List installed agents
|
|
372
429
|
mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes
|
|
373
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
|
|
374
432
|
mio --json status Machine-readable status
|
|
375
433
|
mio --json agents Machine-readable agents
|
|
376
434
|
`)
|
|
@@ -405,6 +463,8 @@ function main() {
|
|
|
405
463
|
return agentsCommand(useJson)
|
|
406
464
|
case 'observe':
|
|
407
465
|
return observeCommand(args)
|
|
466
|
+
case 'dashboard':
|
|
467
|
+
return dashboardCommand(args)
|
|
408
468
|
case 'help':
|
|
409
469
|
case '--help':
|
|
410
470
|
return help()
|
package/observe/observer.js
CHANGED
|
@@ -300,7 +300,10 @@ function updateProjectContext(state, cwd, line) {
|
|
|
300
300
|
content = content.trimEnd() + '\n\n' + block + '\n'
|
|
301
301
|
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
302
302
|
fs.writeFileSync(target, content, 'utf8')
|
|
303
|
-
|
|
303
|
+
log(homeDir(), 'write-back OK: MIO_CONTEXT -> ' + target)
|
|
304
|
+
} catch (e) {
|
|
305
|
+
log(homeDir(), 'write-back FAIL: MIO_CONTEXT -> ' + target + ': ' + (e && e.message || e))
|
|
306
|
+
}
|
|
304
307
|
}
|
|
305
308
|
|
|
306
309
|
function localDateKey(ts) {
|
|
@@ -326,7 +329,10 @@ function writeWorkbuddyDigest(turn, outcome, summary) {
|
|
|
326
329
|
'\n## Mio 自动摘要 (' + time + ')\n' +
|
|
327
330
|
'- outcome: ' + outcome + ' | tools: ' + turn.toolCount + ' | ' + summary.slice(0, 160) + '\n'
|
|
328
331
|
fs.appendFileSync(file, entry, 'utf8')
|
|
329
|
-
|
|
332
|
+
log(homeDir(), 'write-back OK: workbuddy digest -> ' + file)
|
|
333
|
+
} catch (e) {
|
|
334
|
+
log(homeDir(), 'write-back FAIL: workbuddy digest -> ' + file + ': ' + (e && e.message || e))
|
|
335
|
+
}
|
|
330
336
|
}
|
|
331
337
|
|
|
332
338
|
function emitWorkbuddyOutcome(turn, sinks, state) {
|
|
@@ -370,6 +376,7 @@ function emitWorkbuddyOutcome(turn, sinks, state) {
|
|
|
370
376
|
project: turn.project,
|
|
371
377
|
source: 'workbuddy-observer',
|
|
372
378
|
})
|
|
379
|
+
log(homeDir(), '[workbuddy] write-back: ' + memLabel + ' -> memory + ' + (turn.cwd || '?') + '/AGENTS.md')
|
|
373
380
|
writeWorkbuddyDigest(turn, outcome, summary)
|
|
374
381
|
var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
|
|
375
382
|
updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80))
|
|
@@ -800,6 +807,7 @@ function emitOpencodeOutcome(turn, sinks, state) {
|
|
|
800
807
|
project: turn.project,
|
|
801
808
|
source: 'opencode-observer',
|
|
802
809
|
})
|
|
810
|
+
log(homeDir(), '[opencode] write-back: ' + memLabel + ' -> memory + ' + (turn.cwd || '?') + '/AGENTS.md')
|
|
803
811
|
var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
|
|
804
812
|
updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80))
|
|
805
813
|
}
|
|
@@ -1060,6 +1068,7 @@ function emitHermesOutcome(turn, sinks, state) {
|
|
|
1060
1068
|
project: turn.project,
|
|
1061
1069
|
source: 'hermes-observer',
|
|
1062
1070
|
})
|
|
1071
|
+
log(homeDir(), '[hermes] write-back: ' + memLabel + ' -> memory')
|
|
1063
1072
|
// Hermes reads AGENTS.md Context Files; write back only into a real repo
|
|
1064
1073
|
// workspace so we never pollute the user's home directory.
|
|
1065
1074
|
if (turn.cwd) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.12",
|
|
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
|
}
|