mio-agent-runtime 0.13.4 → 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.
@@ -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
- return null
90
- }
91
- if (res.error || res.status !== 0) return null
92
- return (res.stdout || '') + (res.stderr || '')
93
- }
94
-
95
- function isInstalled() {
96
- // Read Hermes's own config instead of asking the binary. `hermes mcp list`
97
- // costs ~1.8 s here (measured: Hermes is a packaged Python executable with a
98
- // slow cold start), and listHostCapabilities() calls this once per host, so
99
- // the moment the spawn started working again `mio.host.capabilities` went
100
- // from ~3 ms to ~2 s. Hermes records registered MCP servers in config.yaml
101
- // under `mcp_servers:`, so the file answers the same question in ~1 ms.
102
- //
103
- // Anchored on the YAML key rather than a plain substring: the config also
104
- // contains the server script path, which includes "mio-intelligence-mcp" and
105
- // would make a loose contains() report "installed" for a config that merely
106
- // mentions the path.
107
- //
108
- // Trade-off, deliberately accepted: if a future Hermes stores MCP config
109
- // somewhere this does not read, this reports false. That is no worse than
110
- // what it did for a long time -- the spawn used to fail silently, so the
111
- // answer was always false regardless.
112
- const cfg = hermesConfigPath()
113
- if (!fs.existsSync(cfg)) return false
114
- try {
115
- return new RegExp('^\\s*' + SERVER_NAME + '\\s*:', 'm').test(fs.readFileSync(cfg, 'utf8'))
116
- } catch (_) {
117
- return false
118
- }
119
- }
120
-
121
- function install({ node, serverScript, home, workspace, project }) {
122
- if (!hermesBin() && !isHermesOnPath()) {
123
- return {
124
- changed: false,
125
- message: 'Hermes binary not found. Install hermes-agent first.',
126
- configPath: null,
127
- }
128
- }
129
- if (isInstalled()) {
130
- if (!configNeedsRepair()) {
131
- return { changed: false, message: 'Hermes Mio MCP already installed.', configPath: null }
132
- }
133
- runHermes(['mcp', 'remove', SERVER_NAME])
134
- }
135
-
136
- const context = JSON.stringify({
137
- agentId: 'hermes',
138
- project: project || path.basename(workspace || process.cwd()),
139
- workspace: workspace || process.cwd(),
140
- sessionId: 'hermes-session',
141
- })
142
-
143
- // Hermes requires --env before --args: --args swallows everything after it.
144
- const args = [
145
- 'mcp', 'add', SERVER_NAME,
146
- '--command', node,
147
- '--env', 'MIO_DATA_DIR=' + home, 'MIO_CONTEXT=' + context,
148
- '--args', serverScript,
149
- ]
150
- const out = runHermes(args)
151
- const installed = isInstalled()
152
-
153
- let message
154
- if (installed) {
155
- message = 'Hermes Mio MCP installed (config: <LocalAppData>/hermes/config.yaml). Start a new Hermes session to load it.'
156
- } else {
157
- 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.'
158
- }
159
- return { changed: installed, configPath: null, message }
160
- }
161
-
162
- function isHermesOnPath() {
163
- try {
164
- // stdio[0] must not be a pipe: see spawnWithOptionalInput above.
165
- const res = spawnSync('where', ['hermes'], {
166
- encoding: 'utf8',
167
- shell: true,
168
- stdio: ['ignore', 'pipe', 'pipe'],
169
- })
170
- return res.status === 0 && /hermes/.test(res.stdout || '')
171
- } catch (_) {
172
- return false
173
- }
174
- }
175
-
176
- module.exports = { hermesBin, isInstalled, install }
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 }