mio-agent-runtime 0.13.3 → 0.13.5
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 +19 -5
- package/adapters/claude-code.js +6 -6
- package/adapters/codex.js +121 -121
- package/adapters/hermes.js +182 -176
- package/adapters/opencode.js +203 -176
- package/adapters/workbuddy.js +197 -196
- package/bin/mio.js +93 -12
- package/observe/observer.js +1876 -1867
- package/package.json +3 -3
- package/server/creativity-engine.js +238 -67
- package/server/evolution-cutover.js +5 -2
- package/server/insight-store.js +37 -11
- package/server/llm-client.js +29 -8
- package/server/mio-intelligence-mcp/README.md +173 -173
- package/server/mio-intelligence-mcp/index.js +27 -6
- package/server/mio-intelligence-mcp/package.json +17 -17
- package/server/mio-intelligence-mcp/phase0.js +365 -365
- package/server/observer-store.js +12 -11
- package/server/task-store.js +475 -475
package/adapters/hermes.js
CHANGED
|
@@ -1,176 +1,182 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fs = require('fs')
|
|
4
|
-
const path = require('path')
|
|
5
|
-
const os = require('os')
|
|
6
|
-
const { spawnSync } = require('child_process')
|
|
7
|
-
|
|
8
|
-
const SERVER_NAME = 'mio-intelligence'
|
|
9
|
-
|
|
10
|
-
function hermesHome() {
|
|
11
|
-
return process.env.HERMES_HOME || path.join(os.homedir(), 'AppData', 'Local', 'hermes')
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function hermesConfigPath() {
|
|
15
|
-
return path.join(hermesHome(), 'config.yaml')
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function configNeedsRepair() {
|
|
19
|
-
const cfg = hermesConfigPath()
|
|
20
|
-
if (!fs.existsSync(cfg)) return false
|
|
21
|
-
try {
|
|
22
|
-
const src = fs.readFileSync(cfg, 'utf8')
|
|
23
|
-
// Legacy installs stored the env pairs as plain args (--env KEY=VALUE)
|
|
24
|
-
// because --env came after --args; Hermes now stores them under env:.
|
|
25
|
-
return /- MIO_DATA_DIR=/.test(src) || /- MIO_CONTEXT=/.test(src)
|
|
26
|
-
} catch (_) {
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function hermesBin() {
|
|
32
|
-
if (process.env.HERMES_BIN && fs.existsSync(process.env.HERMES_BIN)) return process.env.HERMES_BIN
|
|
33
|
-
const home = hermesHome()
|
|
34
|
-
const candidates = [
|
|
35
|
-
// Official Windows native install: venv script registered on User PATH
|
|
36
|
-
path.join(home, 'hermes-agent', 'venv', 'Scripts', 'hermes.exe'),
|
|
37
|
-
// Newer installer shim under <LocalAppData>/hermes/bin
|
|
38
|
-
path.join(home, 'bin', 'hermes.cmd'),
|
|
39
|
-
// Legacy layout
|
|
40
|
-
path.join(home, 'hermes-agent', 'bin', 'hermes.exe'),
|
|
41
|
-
path.join(os.homedir(), '.hermes', 'bin', 'hermes.exe'),
|
|
42
|
-
]
|
|
43
|
-
for (const c of candidates) {
|
|
44
|
-
if (fs.existsSync(c)) return c
|
|
45
|
-
}
|
|
46
|
-
return null
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// spawnSync() with a piped stdin fails on Windows with EBUSY before the child
|
|
50
|
-
// even starts: status is null and stderr is empty, so every call below used to
|
|
51
|
-
// return null and the Hermes adapter silently reported "not installed".
|
|
52
|
-
// `stdio[0] = 'ignore'` fixes the calls that read no stdin. When there IS a
|
|
53
|
-
// prompt answer to deliver it has to travel through a real file descriptor:
|
|
54
|
-
// passing `input` alongside a non-pipe stdin makes Node drop the payload with
|
|
55
|
-
// no error at all, which would leave `hermes mcp add` waiting forever for an
|
|
56
|
-
// answer that never arrives -- worse than the crash it replaces.
|
|
57
|
-
function spawnWithOptionalInput(command, args, opts) {
|
|
58
|
-
const { input, ...rest } = opts
|
|
59
|
-
if (input === undefined) {
|
|
60
|
-
return spawnSync(command, args, { ...rest, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
61
|
-
}
|
|
62
|
-
const file = path.join(os.tmpdir(), 'mio-hermes-stdin-' + process.pid + '-' + Date.now())
|
|
63
|
-
fs.writeFileSync(file, input)
|
|
64
|
-
const fd = fs.openSync(file, 'r')
|
|
65
|
-
try {
|
|
66
|
-
return spawnSync(command, args, { ...rest, stdio: [fd, 'pipe', 'pipe'] })
|
|
67
|
-
} finally {
|
|
68
|
-
try {
|
|
69
|
-
fs.closeSync(fd)
|
|
70
|
-
} catch (_) {}
|
|
71
|
-
try {
|
|
72
|
-
fs.unlinkSync(file)
|
|
73
|
-
} catch (_) {}
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function runHermes(args) {
|
|
78
|
-
const bin = hermesBin()
|
|
79
|
-
// `hermes mcp add` asks to confirm tool enablement interactively; pre-answer
|
|
80
|
-
// it so the install is non-interactive.
|
|
81
|
-
const input = args[0] === 'mcp' && args[1] === 'add' ? 'Y\n' : undefined
|
|
82
|
-
let res
|
|
83
|
-
try {
|
|
84
|
-
const isCmdShim = Boolean(bin && /\.(cmd|bat)$/i.test(bin))
|
|
85
|
-
res = bin
|
|
86
|
-
? spawnWithOptionalInput(bin, args, { encoding: 'utf8', timeout: 60000, windowsHide: true, input: input, shell: isCmdShim })
|
|
87
|
-
: spawnWithOptionalInput('hermes', args, { encoding: 'utf8', timeout: 60000, shell: true, windowsHide: true, input: input })
|
|
88
|
-
} catch (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
return
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
'
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const { spawnSync } = require('child_process')
|
|
7
|
+
|
|
8
|
+
const SERVER_NAME = 'mio-intelligence'
|
|
9
|
+
|
|
10
|
+
function hermesHome() {
|
|
11
|
+
return process.env.HERMES_HOME || path.join(os.homedir(), 'AppData', 'Local', 'hermes')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function hermesConfigPath() {
|
|
15
|
+
return path.join(hermesHome(), 'config.yaml')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function configNeedsRepair() {
|
|
19
|
+
const cfg = hermesConfigPath()
|
|
20
|
+
if (!fs.existsSync(cfg)) return false
|
|
21
|
+
try {
|
|
22
|
+
const src = fs.readFileSync(cfg, 'utf8')
|
|
23
|
+
// Legacy installs stored the env pairs as plain args (--env KEY=VALUE)
|
|
24
|
+
// because --env came after --args; Hermes now stores them under env:.
|
|
25
|
+
return /- MIO_DATA_DIR=/.test(src) || /- MIO_CONTEXT=/.test(src)
|
|
26
|
+
} catch (_) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function hermesBin() {
|
|
32
|
+
if (process.env.HERMES_BIN && fs.existsSync(process.env.HERMES_BIN)) return process.env.HERMES_BIN
|
|
33
|
+
const home = hermesHome()
|
|
34
|
+
const candidates = [
|
|
35
|
+
// Official Windows native install: venv script registered on User PATH
|
|
36
|
+
path.join(home, 'hermes-agent', 'venv', 'Scripts', 'hermes.exe'),
|
|
37
|
+
// Newer installer shim under <LocalAppData>/hermes/bin
|
|
38
|
+
path.join(home, 'bin', 'hermes.cmd'),
|
|
39
|
+
// Legacy layout
|
|
40
|
+
path.join(home, 'hermes-agent', 'bin', 'hermes.exe'),
|
|
41
|
+
path.join(os.homedir(), '.hermes', 'bin', 'hermes.exe'),
|
|
42
|
+
]
|
|
43
|
+
for (const c of candidates) {
|
|
44
|
+
if (fs.existsSync(c)) return c
|
|
45
|
+
}
|
|
46
|
+
return null
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// spawnSync() with a piped stdin fails on Windows with EBUSY before the child
|
|
50
|
+
// even starts: status is null and stderr is empty, so every call below used to
|
|
51
|
+
// return null and the Hermes adapter silently reported "not installed".
|
|
52
|
+
// `stdio[0] = 'ignore'` fixes the calls that read no stdin. When there IS a
|
|
53
|
+
// prompt answer to deliver it has to travel through a real file descriptor:
|
|
54
|
+
// passing `input` alongside a non-pipe stdin makes Node drop the payload with
|
|
55
|
+
// no error at all, which would leave `hermes mcp add` waiting forever for an
|
|
56
|
+
// answer that never arrives -- worse than the crash it replaces.
|
|
57
|
+
function spawnWithOptionalInput(command, args, opts) {
|
|
58
|
+
const { input, ...rest } = opts
|
|
59
|
+
if (input === undefined) {
|
|
60
|
+
return spawnSync(command, args, { ...rest, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
61
|
+
}
|
|
62
|
+
const file = path.join(os.tmpdir(), 'mio-hermes-stdin-' + process.pid + '-' + Date.now())
|
|
63
|
+
fs.writeFileSync(file, input)
|
|
64
|
+
const fd = fs.openSync(file, 'r')
|
|
65
|
+
try {
|
|
66
|
+
return spawnSync(command, args, { ...rest, stdio: [fd, 'pipe', 'pipe'] })
|
|
67
|
+
} finally {
|
|
68
|
+
try {
|
|
69
|
+
fs.closeSync(fd)
|
|
70
|
+
} catch (_) {}
|
|
71
|
+
try {
|
|
72
|
+
fs.unlinkSync(file)
|
|
73
|
+
} catch (_) {}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function runHermes(args) {
|
|
78
|
+
const bin = hermesBin()
|
|
79
|
+
// `hermes mcp add` asks to confirm tool enablement interactively; pre-answer
|
|
80
|
+
// it so the install is non-interactive.
|
|
81
|
+
const input = args[0] === 'mcp' && args[1] === 'add' ? 'Y\n' : undefined
|
|
82
|
+
let res
|
|
83
|
+
try {
|
|
84
|
+
const isCmdShim = Boolean(bin && /\.(cmd|bat)$/i.test(bin))
|
|
85
|
+
res = bin
|
|
86
|
+
? spawnWithOptionalInput(bin, args, { encoding: 'utf8', timeout: 60000, windowsHide: true, input: input, shell: isCmdShim })
|
|
87
|
+
: spawnWithOptionalInput('hermes', args, { encoding: 'utf8', timeout: 60000, shell: true, windowsHide: true, input: input })
|
|
88
|
+
} catch (err) {
|
|
89
|
+
const msg = `hermes ${args.join(' ')} failed: ${err.message || err}`
|
|
90
|
+
console.error(msg)
|
|
91
|
+
throw new Error(msg)
|
|
92
|
+
}
|
|
93
|
+
if (res.error || res.status !== 0) {
|
|
94
|
+
const msg = `hermes ${args.join(' ')} exited ${res.status}: ${res.stderr || res.stdout || 'no output'}`
|
|
95
|
+
console.error(msg)
|
|
96
|
+
throw new Error(msg)
|
|
97
|
+
}
|
|
98
|
+
return (res.stdout || '') + (res.stderr || '')
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function isInstalled() {
|
|
102
|
+
// Read Hermes's own config instead of asking the binary. `hermes mcp list`
|
|
103
|
+
// costs ~1.8 s here (measured: Hermes is a packaged Python executable with a
|
|
104
|
+
// slow cold start), and listHostCapabilities() calls this once per host, so
|
|
105
|
+
// the moment the spawn started working again `mio.host.capabilities` went
|
|
106
|
+
// from ~3 ms to ~2 s. Hermes records registered MCP servers in config.yaml
|
|
107
|
+
// under `mcp_servers:`, so the file answers the same question in ~1 ms.
|
|
108
|
+
//
|
|
109
|
+
// Anchored on the YAML key rather than a plain substring: the config also
|
|
110
|
+
// contains the server script path, which includes "mio-intelligence-mcp" and
|
|
111
|
+
// would make a loose contains() report "installed" for a config that merely
|
|
112
|
+
// mentions the path.
|
|
113
|
+
//
|
|
114
|
+
// Trade-off, deliberately accepted: if a future Hermes stores MCP config
|
|
115
|
+
// somewhere this does not read, this reports false. That is no worse than
|
|
116
|
+
// what it did for a long time -- the spawn used to fail silently, so the
|
|
117
|
+
// answer was always false regardless.
|
|
118
|
+
const cfg = hermesConfigPath()
|
|
119
|
+
if (!fs.existsSync(cfg)) return false
|
|
120
|
+
try {
|
|
121
|
+
return new RegExp('^\\s*' + SERVER_NAME + '\\s*:', 'm').test(fs.readFileSync(cfg, 'utf8'))
|
|
122
|
+
} catch (_) {
|
|
123
|
+
return false
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function install({ node, serverScript, home, workspace, project }) {
|
|
128
|
+
if (!hermesBin() && !isHermesOnPath()) {
|
|
129
|
+
return {
|
|
130
|
+
changed: false,
|
|
131
|
+
message: 'Hermes binary not found. Install hermes-agent first.',
|
|
132
|
+
configPath: null,
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (isInstalled()) {
|
|
136
|
+
if (!configNeedsRepair()) {
|
|
137
|
+
return { changed: false, message: 'Hermes Mio MCP already installed.', configPath: null }
|
|
138
|
+
}
|
|
139
|
+
runHermes(['mcp', 'remove', SERVER_NAME])
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const context = JSON.stringify({
|
|
143
|
+
agentId: 'hermes',
|
|
144
|
+
project: project || path.basename(workspace || process.cwd()),
|
|
145
|
+
workspace: workspace || process.cwd(),
|
|
146
|
+
sessionId: 'hermes-session',
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
// Hermes requires --env before --args: --args swallows everything after it.
|
|
150
|
+
const args = [
|
|
151
|
+
'mcp', 'add', SERVER_NAME,
|
|
152
|
+
'--command', node,
|
|
153
|
+
'--env', 'MIO_DATA_DIR=' + home, 'MIO_CONTEXT=' + context,
|
|
154
|
+
'--args', serverScript,
|
|
155
|
+
]
|
|
156
|
+
const out = runHermes(args)
|
|
157
|
+
const installed = isInstalled()
|
|
158
|
+
|
|
159
|
+
let message
|
|
160
|
+
if (installed) {
|
|
161
|
+
message = 'Hermes Mio MCP installed (config: <LocalAppData>/hermes/config.yaml). Start a new Hermes session to load it.'
|
|
162
|
+
} else {
|
|
163
|
+
message = 'Hermes mcp add returned but Mio was not detected. Run `hermes mcp add ' + SERVER_NAME + ' --command ' + node + ' --args ' + serverScript + '` manually and answer Y to the prompt.'
|
|
164
|
+
}
|
|
165
|
+
return { changed: installed, configPath: null, message }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function isHermesOnPath() {
|
|
169
|
+
try {
|
|
170
|
+
// stdio[0] must not be a pipe: see spawnWithOptionalInput above.
|
|
171
|
+
const res = spawnSync('where', ['hermes'], {
|
|
172
|
+
encoding: 'utf8',
|
|
173
|
+
shell: true,
|
|
174
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
175
|
+
})
|
|
176
|
+
return res.status === 0 && /hermes/.test(res.stdout || '')
|
|
177
|
+
} catch (_) {
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
module.exports = { hermesBin, isInstalled, install }
|