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 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
 
@@ -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()
@@ -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
- } catch (_) {}
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
- } catch (_) {}
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.10",
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
  }