mio-agent-runtime 0.5.15 → 0.5.16

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/bin/mio.js CHANGED
@@ -1,478 +1,477 @@
1
- #!/usr/bin/env node
2
- 'use strict'
3
-
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
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')
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
12
- const MIO_HOME = process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
13
- const CONFIG_FILE = path.join(MIO_HOME, 'config.json')
14
-
15
- const adapters = {
16
- codex: require('../adapters/codex.js'),
17
- opencode: require('../adapters/opencode.js'),
18
- workbuddy: require('../adapters/workbuddy.js'),
19
- hermes: require('../adapters/hermes.js'),
20
- }
7
+ const { execFileSync } = require('child_process')
8
+
9
+ const SERVER_SCRIPT = path.resolve(__dirname, '..', 'server', 'mio-intelligence-mcp', 'index.js')
10
+ const MIO_HOME = process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
11
+ const CONFIG_FILE = path.join(MIO_HOME, 'config.json')
12
+
13
+ const adapters = {
14
+ codex: require('../adapters/codex.js'),
15
+ opencode: require('../adapters/opencode.js'),
16
+ workbuddy: require('../adapters/workbuddy.js'),
17
+ hermes: require('../adapters/hermes.js'),
18
+ }
21
19
  const observer = require('../observe/observer.js')
20
+ const { getEvolutionStatus, formatEvolutionStatusText } = require('../server/runtime-modules.js')
21
+ const { createEvolutionCutoverTools } = require('../server/evolution-cutover.js')
22
22
 
23
- function ensureHome() {
24
- fs.mkdirSync(MIO_HOME, { recursive: true })
25
- fs.mkdirSync(path.join(MIO_HOME, 'logs'), { recursive: true })
26
- }
27
-
28
- function readConfig() {
29
- const fallback = { version: 1, home: MIO_HOME, agents: {} }
30
- if (!fs.existsSync(CONFIG_FILE)) return fallback
31
- try {
32
- return { ...fallback, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }
33
- } catch (_) {
34
- return fallback
35
- }
23
+ function appendJsonl(file, value) {
24
+ fs.mkdirSync(path.dirname(file), { recursive: true })
25
+ fs.appendFileSync(file, `${JSON.stringify(value)}\n`, 'utf8')
36
26
  }
37
27
 
38
- function writeConfig(config) {
39
- ensureHome()
40
- fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
28
+ function readJsonl(file) {
29
+ if (!fs.existsSync(file)) return []
30
+ return fs
31
+ .readFileSync(file, 'utf8')
32
+ .split(/\r?\n/)
33
+ .filter(Boolean)
34
+ .map((line) => {
35
+ try {
36
+ return JSON.parse(line)
37
+ } catch (_) {
38
+ return null
39
+ }
40
+ })
41
+ .filter(Boolean)
41
42
  }
42
43
 
43
- function jsonOrText(payload, useJson) {
44
- if (useJson) {
45
- console.log(JSON.stringify(payload, null, 2))
46
- return
47
- }
48
- return payload
44
+ function projectName() {
45
+ try {
46
+ const commonDir = execFileSync(
47
+ 'git',
48
+ ['rev-parse', '--path-format=absolute', '--git-common-dir'],
49
+ {
50
+ cwd: process.cwd(),
51
+ encoding: 'utf8',
52
+ stdio: ['ignore', 'pipe', 'ignore'],
53
+ }
54
+ ).trim()
55
+ if (commonDir) return path.basename(path.dirname(commonDir))
56
+ } catch (_) {}
57
+ return path.basename(process.cwd())
49
58
  }
50
59
 
51
- function versionCommand() {
52
- const pkg = require(path.resolve(__dirname, '..', 'package.json'))
53
- console.log(pkg.version)
54
- }
60
+ const evolutionCutover = createEvolutionCutoverTools({
61
+ dataDir: MIO_HOME,
62
+ appendJsonl,
63
+ readJsonl,
64
+ projectName,
65
+ })
55
66
 
56
- function initCommand() {
57
- ensureHome()
58
- const config = readConfig()
59
- if (!config.createdAt) config.createdAt = new Date().toISOString()
60
- writeConfig(config)
61
- console.log(`MIO_HOME ready: ${MIO_HOME}`)
67
+ function optionValue(args, name) {
68
+ const index = args.indexOf(name)
69
+ if (index === -1) return undefined
70
+ return args[index + 1]
62
71
  }
63
72
 
64
- function statusCommand(useJson) {
65
- const config = readConfig()
66
- const codexRestart = adapters.codex.restartWarningMessage()
67
- const opencodeRestart = adapters.opencode.restartWarningMessage()
68
- const installed = {
69
- codex: adapters.codex.isInstalled(),
70
- opencode: adapters.opencode.isInstalled(),
71
- workbuddy: adapters.workbuddy.isInstalled(),
72
- hermes: adapters.hermes.isInstalled(),
73
- }
74
- const payload = {
75
- version: config.version,
76
- home: MIO_HOME,
77
- serverScript: SERVER_SCRIPT,
78
- serverScriptExists: fs.existsSync(SERVER_SCRIPT),
79
- agents: config.agents || {},
80
- codexInstalled: installed.codex,
81
- opencodeInstalled: installed.opencode,
82
- workbuddyInstalled: installed.workbuddy,
83
- hermesInstalled: installed.hermes,
84
- codexNeedsRestart: Boolean(codexRestart),
85
- opencodeNeedsRestart: Boolean(opencodeRestart),
86
- codexRestartHint: codexRestart,
87
- opencodeRestartHint: opencodeRestart,
88
- hermesObserverReady: fs.existsSync(observer.hermesDbPath()),
89
- opencodeObserverReady: fs.existsSync(observer.opencodeDbPath()),
90
- observerRunning: observer.isRunning(MIO_HOME),
91
- observerPid: observer.isRunning(MIO_HOME) ? observer.readPid(MIO_HOME) : null,
92
- }
93
- if (useJson) {
94
- jsonOrText(payload, true)
95
- return
96
- }
97
- console.log(`MIO_HOME: ${MIO_HOME}`)
98
- console.log(`MCP Server: ${SERVER_SCRIPT}`)
99
- console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
100
- if (codexRestart) console.log(`Codex app: running with an older Mio config - fully restart Codex to load it`)
101
- console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
102
- if (opencodeRestart) console.log(`OpenCode Desktop: running with an older Mio config - restart it to load it`)
103
- console.log(`OpenCode observer: ${fs.existsSync(observer.opencodeDbPath()) ? 'ready' : 'no database'}`)
104
- console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
105
- console.log(`Hermes MCP: ${installed.hermes ? 'installed' : 'not installed'}`)
106
- console.log(`Hermes observer: ${fs.existsSync(observer.hermesDbPath()) ? 'ready' : 'no database'}`)
107
- console.log(`Observer: ${observer.isRunning(MIO_HOME) ? 'running (pid ' + observer.readPid(MIO_HOME) + ')' : 'not running'}`)
73
+ function flagPresent(args, name) {
74
+ return args.includes(name)
108
75
  }
109
76
 
110
- function agentsCommand(useJson) {
111
- const config = readConfig()
112
- const agents = config.agents || {}
113
- if (useJson) {
114
- jsonOrText(agents, true)
115
- return
116
- }
117
- const names = Object.keys(agents)
118
- if (names.length === 0) {
119
- console.log('No agents installed.')
120
- return
121
- }
122
- for (const name of names) {
123
- console.log(`${name}\t${agents[name].installedAt || ''}`)
124
- }
125
- }
126
-
127
- function installCommand(host, useJson) {
128
- if (!host) {
129
- console.error('Usage: mio install <codex|opencode|workbuddy|hermes>')
130
- process.exitCode = 1
131
- return
132
- }
133
- const adapter = adapters[host]
134
- if (!adapter) {
135
- console.error(`Unknown host: ${host}`)
136
- process.exitCode = 1
137
- return
138
- }
139
- ensureHome()
140
- const config = readConfig()
141
- let result
77
+ function parseJsonOption(args, name) {
78
+ const raw = optionValue(args, name)
79
+ if (raw === undefined) throw new Error(`${name} is required`)
142
80
  try {
143
- result = adapter.install({
144
- node: process.execPath,
145
- serverScript: SERVER_SCRIPT,
146
- home: MIO_HOME,
147
- workspace: process.cwd(),
148
- })
81
+ return JSON.parse(raw)
149
82
  } catch (error) {
150
- console.error(`Failed to install ${host}: ${error.message || error}`)
151
- process.exitCode = 1
152
- return
83
+ throw new Error(`${name} must be valid JSON`)
153
84
  }
154
- if (result.changed) {
155
- config.agents = config.agents || {}
156
- config.agents[host] = {
157
- installedAt: new Date().toISOString(),
158
- configPath: result.configPath || null,
159
- }
160
- writeConfig(config)
161
- }
162
- try {
163
- observer.runCycle(MIO_HOME, observer.loadState(MIO_HOME), observer.createSinks(MIO_HOME))
164
- } catch (_) {}
165
- let observerStarted = false
166
- try {
167
- observerStarted = observer.startBackground(MIO_HOME).started
168
- } catch (_) {}
169
- if (useJson) {
170
- result.observer = {
171
- running: observer.isRunning(MIO_HOME),
172
- started: observerStarted,
173
- }
174
- jsonOrText(result, true)
175
- return
176
- }
177
- console.log(result.message || `${host} adapter completed.`)
178
- console.log(
179
- observer.isRunning(MIO_HOME)
180
- ? 'Observer daemon running.'
181
- : 'Observer daemon not running. Run `mio observe --start`.'
182
- )
183
- }
184
-
185
- function mcpCommand() {
186
- require(SERVER_SCRIPT)
187
85
  }
188
86
 
189
- function observeCommand(args) {
190
- const flags = args.slice(1)
191
- const useJson = flags.includes('--json')
192
- if (flags.includes('--start')) {
193
- const result = observer.startBackground(MIO_HOME)
194
- if (useJson) return jsonOrText(result, true)
195
- console.log(result.started ? 'Observer started.' : 'Observer already running.')
196
- return
197
- }
198
- if (flags.includes('--stop')) {
199
- const result = observer.stopBackground(MIO_HOME)
200
- if (useJson) return jsonOrText(result, true)
201
- console.log(result.stopped ? 'Observer stopped.' : 'Observer was not running.')
202
- return
203
- }
204
- if (flags.includes('--status')) {
205
- const running = observer.isRunning(MIO_HOME)
206
- const pid = observer.readPid(MIO_HOME)
207
- if (useJson) return jsonOrText({ running, pid }, true)
208
- console.log(running ? 'Observer: running (pid ' + pid + ')' : 'Observer: not running')
209
- return
210
- }
211
- if (flags.includes('--once')) {
212
- const res = observer.runCycle(
213
- MIO_HOME,
214
- observer.loadState(MIO_HOME),
215
- observer.createSinks(MIO_HOME)
216
- )
217
- if (useJson) return jsonOrText(res, true)
218
- console.log('Observed files=' + res.files + ' lines=' + res.lines + ' outcomes=' + res.outcomes)
219
- return
220
- }
221
- console.log('Observing WorkBuddy transcripts into ' + MIO_HOME + ' (Ctrl+C to stop)...')
222
- observer.startForeground(MIO_HOME)
87
+ function parseNumberOption(args, name) {
88
+ const raw = optionValue(args, name)
89
+ if (raw === undefined) return undefined
90
+ const value = Number(raw)
91
+ if (!Number.isFinite(value)) throw new Error(`${name} must be a number`)
92
+ return value
223
93
  }
224
94
 
225
- function uninstallCommand(host, useJson) {
226
- if (!host) {
227
- console.error('Usage: mio uninstall <codex|opencode|workbuddy|hermes>')
228
- process.exitCode = 1
229
- return
230
- }
231
- const adapter = adapters[host]
232
- if (!adapter) {
233
- console.error(`Unknown host: ${host}`)
234
- process.exitCode = 1
235
- return
236
- }
237
- let result
238
- try {
239
- result = adapter.uninstall({
240
- node: process.execPath,
241
- serverScript: SERVER_SCRIPT,
242
- home: MIO_HOME,
243
- workspace: process.cwd(),
244
- })
245
- } catch (error) {
246
- console.error(`Failed to uninstall ${host}: ${error.message || error}`)
247
- process.exitCode = 1
248
- return
249
- }
250
- if (result.changed) {
251
- const config = readConfig()
252
- if (config.agents && config.agents[host]) {
253
- delete config.agents[host]
254
- writeConfig(config)
255
- }
256
- const remaining = Object.keys(config.agents || {}).length
257
- if (remaining === 0 && observer.isRunning(MIO_HOME)) {
258
- observer.stopBackground(MIO_HOME)
259
- }
260
- }
95
+ function evolutionStatus(useJson) {
96
+ const payload = getEvolutionStatus()
261
97
  if (useJson) {
262
- jsonOrText(result, true)
98
+ jsonOrText(payload, true)
263
99
  return
264
100
  }
265
- console.log(result.message || `${host} adapter removed.`)
101
+ console.log(formatEvolutionStatusText(payload))
266
102
  }
267
103
 
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
- }
104
+ function printCutoverReadiness(readiness, useJson) {
105
+ if (useJson) return jsonOrText(readiness, true)
106
+ console.log(`Cutover readiness for ${readiness.project}: ${readiness.status}`)
107
+ console.log(`shadow=${readiness.shadowRuns} dual-write=${readiness.dualWriteRuns}`)
108
+ if (readiness.reasons.length > 0) console.log(`reasons: ${readiness.reasons.join('; ')}`)
276
109
  }
277
110
 
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
111
+ function printAuthorityPlan(plan, useJson) {
112
+ if (useJson) return jsonOrText(plan, true)
113
+ console.log(`Authority switch ${plan.from} -> ${plan.to}: ${plan.approved ? 'ready' : 'blocked'}`)
114
+ for (const action of plan.actions) console.log(`- ${action.type}`)
115
+ for (const blocker of plan.blockers) console.log(`blocked: ${blocker}`)
282
116
  }
283
117
 
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
118
+ function printMigrationPlan(plan, useJson) {
119
+ if (useJson) return jsonOrText(plan, true)
120
+ console.log(`Migration plan: ${plan.ready ? 'ready' : 'conflicts'}`)
121
+ console.log(
122
+ `legacy=${plan.summary.legacy} modular=${plan.summary.modular} toCreate=${plan.summary.toCreate} conflicts=${plan.summary.conflicts}`
123
+ )
288
124
  }
289
125
 
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.')
126
+ function printCutoverDryRun(result, useJson) {
127
+ if (useJson) return jsonOrText(result, true)
128
+ console.log(`Cutover apply dry-run: ${result.status}`)
129
+ console.log(`applied=${result.applied}`)
130
+ for (const action of result.actions) console.log(`- ${action.type}`)
325
131
  }
326
132
 
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.')
133
+ function printShadowRecord(record, useJson) {
134
+ if (useJson) return jsonOrText(record, true)
135
+ console.log(`Shadow comparison ${record.id}: ${record.matched ? 'matched' : 'mismatch'}`)
136
+ if (record.diffs.length > 0) console.log(`diffs=${record.diffs.length}`)
359
137
  }
360
138
 
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 }
139
+ function printDualWriteRecord(record, useJson) {
140
+ if (useJson) return jsonOrText(record, true)
141
+ console.log(`Dual-write sample ${record.id}: authoritative=${record.authoritative}`)
142
+ console.log(`shadow=${record.shadow.matched ? 'matched' : 'mismatch'}`)
366
143
  }
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)
144
+
145
+ async function evolutionCommand(args, useJson) {
146
+ if (args[1] === 'status') return evolutionStatus(useJson)
147
+ try {
148
+ if (args[1] === 'shadow' && args[2] === 'record') {
149
+ return printShadowRecord(
150
+ await evolutionCutover.recordShadowComparison({
151
+ project: optionValue(args, '--project'),
152
+ label: optionValue(args, '--label'),
153
+ legacy: parseJsonOption(args, '--legacy'),
154
+ modular: parseJsonOption(args, '--modular'),
155
+ input: optionValue(args, '--input') === undefined ? undefined : parseJsonOption(args, '--input'),
156
+ }),
157
+ useJson,
158
+ )
159
+ }
160
+ if (args[1] === 'dual-write' && args[2] === 'record') {
161
+ return printDualWriteRecord(
162
+ await evolutionCutover.recordDualWrite({
163
+ project: optionValue(args, '--project'),
164
+ label: optionValue(args, '--label'),
165
+ authoritative: optionValue(args, '--authoritative'),
166
+ legacyResult: parseJsonOption(args, '--legacy-result'),
167
+ modularResult: parseJsonOption(args, '--modular-result'),
168
+ record: optionValue(args, '--record') === undefined ? undefined : parseJsonOption(args, '--record'),
169
+ }),
170
+ useJson,
171
+ )
172
+ }
173
+ if (args[1] === 'cutover' && args[2] === 'readiness') {
174
+ return printCutoverReadiness(
175
+ evolutionCutover.cutoverReadiness({
176
+ project: optionValue(args, '--project'),
177
+ minShadowRuns: parseNumberOption(args, '--min-shadow-runs'),
178
+ maxMismatchRate: parseNumberOption(args, '--max-mismatch-rate'),
179
+ }),
180
+ useJson,
181
+ )
182
+ }
183
+ if (args[1] === 'cutover' && args[2] === 'apply') {
184
+ return printCutoverDryRun(
185
+ evolutionCutover.applyCutover({
186
+ dryRun: flagPresent(args, '--dry-run'),
187
+ project: optionValue(args, '--project'),
188
+ plan: parseJsonOption(args, '--plan'),
189
+ }),
190
+ useJson,
191
+ )
192
+ }
193
+ if (args[1] === 'authority' && args[2] === 'plan') {
194
+ return printAuthorityPlan(
195
+ evolutionCutover.authorityPlan({
196
+ readiness: parseJsonOption(args, '--readiness'),
197
+ from: optionValue(args, '--from'),
198
+ to: optionValue(args, '--to'),
199
+ }),
200
+ useJson,
201
+ )
202
+ }
203
+ if (args[1] === 'migration' && args[2] === 'plan') {
204
+ return printMigrationPlan(
205
+ evolutionCutover.migrationPlan({
206
+ legacyRecords: parseJsonOption(args, '--legacy-records'),
207
+ modularRecords: parseJsonOption(args, '--modular-records'),
208
+ }),
209
+ useJson,
210
+ )
211
+ }
212
+ } catch (error) {
213
+ console.error(error.message || error)
214
+ process.exitCode = 1
410
215
  return
411
216
  }
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 (_) {} })
217
+ console.error('Usage: mio evolution status|shadow record|dual-write record|cutover readiness|cutover apply|authority plan|migration plan')
218
+ process.exitCode = 1
414
219
  }
415
-
416
- function help() {
417
- console.log(`Mio Agent Runtime CLI
418
-
419
- Usage:
420
- mio version Show installed Mio version
421
- mio init Initialize MIO_HOME
422
- mio mcp Start Mio MCP server (stdio)
423
- mio install <host> Install Mio into a host (codex|opencode|workbuddy|hermes)
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
220
+
221
+ function ensureHome() {
222
+ fs.mkdirSync(MIO_HOME, { recursive: true })
223
+ fs.mkdirSync(path.join(MIO_HOME, 'logs'), { recursive: true })
224
+ }
225
+
226
+ function readConfig() {
227
+ const fallback = { version: 1, home: MIO_HOME, agents: {} }
228
+ if (!fs.existsSync(CONFIG_FILE)) return fallback
229
+ try {
230
+ return { ...fallback, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }
231
+ } catch (_) {
232
+ return fallback
233
+ }
234
+ }
235
+
236
+ function writeConfig(config) {
237
+ ensureHome()
238
+ fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
239
+ }
240
+
241
+ function jsonOrText(payload, useJson) {
242
+ if (useJson) {
243
+ console.log(JSON.stringify(payload, null, 2))
244
+ return
245
+ }
246
+ return payload
247
+ }
248
+
249
+ function initCommand() {
250
+ ensureHome()
251
+ const config = readConfig()
252
+ if (!config.createdAt) config.createdAt = new Date().toISOString()
253
+ writeConfig(config)
254
+ console.log(`MIO_HOME ready: ${MIO_HOME}`)
255
+ }
256
+
257
+ function statusCommand(useJson) {
258
+ const config = readConfig()
259
+ const codexRestart = adapters.codex.restartWarningMessage()
260
+ const opencodeRestart = adapters.opencode.restartWarningMessage()
261
+ const installed = {
262
+ codex: adapters.codex.isInstalled(),
263
+ opencode: adapters.opencode.isInstalled(),
264
+ workbuddy: adapters.workbuddy.isInstalled(),
265
+ hermes: adapters.hermes.isInstalled(),
266
+ }
267
+ const payload = {
268
+ version: config.version,
269
+ home: MIO_HOME,
270
+ serverScript: SERVER_SCRIPT,
271
+ serverScriptExists: fs.existsSync(SERVER_SCRIPT),
272
+ agents: config.agents || {},
273
+ codexInstalled: installed.codex,
274
+ opencodeInstalled: installed.opencode,
275
+ workbuddyInstalled: installed.workbuddy,
276
+ hermesInstalled: installed.hermes,
277
+ codexNeedsRestart: Boolean(codexRestart),
278
+ opencodeNeedsRestart: Boolean(opencodeRestart),
279
+ codexRestartHint: codexRestart,
280
+ opencodeRestartHint: opencodeRestart,
281
+ hermesObserverReady: fs.existsSync(observer.hermesDbPath()),
282
+ opencodeObserverReady: fs.existsSync(observer.opencodeDbPath()),
283
+ observerRunning: observer.isRunning(MIO_HOME),
284
+ observerPid: observer.isRunning(MIO_HOME) ? observer.readPid(MIO_HOME) : null,
285
+ }
286
+ if (useJson) {
287
+ jsonOrText(payload, true)
288
+ return
289
+ }
290
+ console.log(`MIO_HOME: ${MIO_HOME}`)
291
+ console.log(`MCP Server: ${SERVER_SCRIPT}`)
292
+ console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
293
+ if (codexRestart) console.log(`Codex app: running with an older Mio config - fully restart Codex to load it`)
294
+ console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
295
+ if (opencodeRestart) console.log(`OpenCode Desktop: running with an older Mio config - restart it to load it`)
296
+ console.log(`OpenCode observer: ${fs.existsSync(observer.opencodeDbPath()) ? 'ready' : 'no database'}`)
297
+ console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
298
+ console.log(`Hermes MCP: ${installed.hermes ? 'installed' : 'not installed'}`)
299
+ console.log(`Hermes observer: ${fs.existsSync(observer.hermesDbPath()) ? 'ready' : 'no database'}`)
300
+ console.log(`Observer: ${observer.isRunning(MIO_HOME) ? 'running (pid ' + observer.readPid(MIO_HOME) + ')' : 'not running'}`)
301
+ }
302
+
303
+ function agentsCommand(useJson) {
304
+ const config = readConfig()
305
+ const agents = config.agents || {}
306
+ if (useJson) {
307
+ jsonOrText(agents, true)
308
+ return
309
+ }
310
+ const names = Object.keys(agents)
311
+ if (names.length === 0) {
312
+ console.log('No agents installed.')
313
+ return
314
+ }
315
+ for (const name of names) {
316
+ console.log(`${name}\t${agents[name].installedAt || ''}`)
317
+ }
318
+ }
319
+
320
+ function installCommand(host, useJson) {
321
+ if (!host) {
322
+ console.error('Usage: mio install <codex|opencode|workbuddy|hermes>')
323
+ process.exitCode = 1
324
+ return
325
+ }
326
+ const adapter = adapters[host]
327
+ if (!adapter) {
328
+ console.error(`Unknown host: ${host}`)
329
+ process.exitCode = 1
330
+ return
331
+ }
332
+ ensureHome()
333
+ const config = readConfig()
334
+ let result
335
+ try {
336
+ result = adapter.install({
337
+ node: process.execPath,
338
+ serverScript: SERVER_SCRIPT,
339
+ home: MIO_HOME,
340
+ workspace: process.cwd(),
341
+ })
342
+ } catch (error) {
343
+ console.error(`Failed to install ${host}: ${error.message || error}`)
344
+ process.exitCode = 1
345
+ return
346
+ }
347
+ if (result.changed) {
348
+ config.agents = config.agents || {}
349
+ config.agents[host] = {
350
+ installedAt: new Date().toISOString(),
351
+ configPath: result.configPath || null,
352
+ }
353
+ writeConfig(config)
354
+ }
355
+ try {
356
+ observer.runCycle(MIO_HOME, observer.loadState(MIO_HOME), observer.createSinks(MIO_HOME))
357
+ } catch (_) {}
358
+ let observerStarted = false
359
+ try {
360
+ observerStarted = observer.startBackground(MIO_HOME).started
361
+ } catch (_) {}
362
+ if (useJson) {
363
+ result.observer = {
364
+ running: observer.isRunning(MIO_HOME),
365
+ started: observerStarted,
366
+ }
367
+ jsonOrText(result, true)
368
+ return
369
+ }
370
+ console.log(result.message || `${host} adapter completed.`)
371
+ console.log(
372
+ observer.isRunning(MIO_HOME)
373
+ ? 'Observer daemon running.'
374
+ : 'Observer daemon not running. Run `mio observe --start`.'
375
+ )
376
+ }
377
+
378
+ function mcpCommand() {
379
+ require(SERVER_SCRIPT)
380
+ }
381
+
382
+ function observeCommand(args) {
383
+ const flags = args.slice(1)
384
+ const useJson = flags.includes('--json')
385
+ if (flags.includes('--start')) {
386
+ const result = observer.startBackground(MIO_HOME)
387
+ if (useJson) return jsonOrText(result, true)
388
+ console.log(result.started ? 'Observer started.' : 'Observer already running.')
389
+ return
390
+ }
391
+ if (flags.includes('--stop')) {
392
+ const result = observer.stopBackground(MIO_HOME)
393
+ if (useJson) return jsonOrText(result, true)
394
+ console.log(result.stopped ? 'Observer stopped.' : 'Observer was not running.')
395
+ return
396
+ }
397
+ if (flags.includes('--status')) {
398
+ const running = observer.isRunning(MIO_HOME)
399
+ const pid = observer.readPid(MIO_HOME)
400
+ if (useJson) return jsonOrText({ running, pid }, true)
401
+ console.log(running ? 'Observer: running (pid ' + pid + ')' : 'Observer: not running')
402
+ return
403
+ }
404
+ if (flags.includes('--once')) {
405
+ const res = observer.runCycle(
406
+ MIO_HOME,
407
+ observer.loadState(MIO_HOME),
408
+ observer.createSinks(MIO_HOME)
409
+ )
410
+ if (useJson) return jsonOrText(res, true)
411
+ console.log('Observed files=' + res.files + ' lines=' + res.lines + ' outcomes=' + res.outcomes)
412
+ return
413
+ }
414
+ console.log('Observing WorkBuddy transcripts into ' + MIO_HOME + ' (Ctrl+C to stop)...')
415
+ observer.startForeground(MIO_HOME)
416
+ }
417
+
418
+ function help() {
419
+ console.log(`Mio Agent Runtime CLI
420
+
421
+ Usage:
422
+ mio init Initialize MIO_HOME
423
+ mio mcp Start Mio MCP server (stdio)
424
+ mio install <host> Install Mio into a host (codex|opencode|workbuddy|hermes)
427
425
  mio status Show runtime and adapter status
428
426
  mio agents List installed agents
427
+ mio evolution status Show composed evolution module health
428
+ mio evolution shadow record Record a shadow comparison sample
429
+ mio evolution dual-write record Record a dual-write comparison sample
430
+ mio evolution cutover readiness Assess shadow/dual-write cutover readiness
431
+ mio evolution authority plan Preview a gated authority switch plan
432
+ mio evolution migration plan Preview state migration diffs
433
+ mio evolution cutover apply --dry-run Dry-run a cutover plan without switching authority
429
434
  mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes
430
435
  mio observe --start|--stop|--status|--once Manage the background observer daemon
431
- mio dashboard [--start|--stop|--status|--open] Start the live dashboard HTTP server
432
436
  mio --json status Machine-readable status
433
437
  mio --json agents Machine-readable agents
438
+ mio --json evolution status Machine-readable evolution module health
434
439
  `)
435
440
  }
436
-
437
- function main() {
438
- const args = process.argv.slice(2)
439
- const jsonIndex = args.indexOf('--json')
440
- const useJson = jsonIndex !== -1
441
- if (useJson) args.splice(jsonIndex, 1)
442
- const command = args[0] || 'help'
443
-
444
- switch (command) {
445
- case 'version':
446
- case '--version':
447
- return versionCommand()
448
- case 'init':
449
- return initCommand()
450
- case 'mcp':
451
- return mcpCommand()
452
- case 'install':
453
- return installCommand(args[1], useJson)
454
- case 'uninstall':
455
- return uninstallCommand(args[1], useJson)
456
- case 'upgrade':
457
- return upgradeCommand(useJson)
458
- case 'doctor':
459
- return doctorCommand(useJson)
460
- case 'status':
461
- return statusCommand(useJson)
462
- case 'agents':
463
- return agentsCommand(useJson)
441
+
442
+ async function main() {
443
+ const args = process.argv.slice(2)
444
+ const jsonIndex = args.indexOf('--json')
445
+ const useJson = jsonIndex !== -1
446
+ if (useJson) args.splice(jsonIndex, 1)
447
+ const command = args[0] || 'help'
448
+
449
+ switch (command) {
450
+ case 'init':
451
+ return initCommand()
452
+ case 'mcp':
453
+ return mcpCommand()
454
+ case 'install':
455
+ return installCommand(args[1], useJson)
456
+ case 'status':
457
+ return statusCommand(useJson)
458
+ case 'agents':
459
+ return agentsCommand(useJson)
464
460
  case 'observe':
465
461
  return observeCommand(args)
466
- case 'dashboard':
467
- return dashboardCommand(args)
468
- case 'help':
469
- case '--help':
470
- return help()
471
- default:
472
- console.error(`Unknown command: ${command}`)
473
- help()
474
- process.exitCode = 1
475
- }
462
+ case 'evolution':
463
+ return evolutionCommand(args, useJson)
464
+ case 'help':
465
+ case '--help':
466
+ return help()
467
+ default:
468
+ console.error(`Unknown command: ${command}`)
469
+ help()
470
+ process.exitCode = 1
471
+ }
476
472
  }
477
473
 
478
- main()
474
+ main().catch((error) => {
475
+ console.error(error.message || error)
476
+ process.exitCode = 1
477
+ })