mio-agent-runtime 0.3.5 → 0.3.6
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 +12 -3
- package/adapters/hermes.js +90 -0
- package/bin/mio.js +253 -247
- package/observe/observer.js +237 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -10,9 +10,9 @@ Mio Agent Runtime 的可发布 CLI 包。
|
|
|
10
10
|
npm install -g mio-agent-runtime
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
> 新机器 / 升级:请安装 `>=0.3.
|
|
14
|
-
>
|
|
15
|
-
>
|
|
13
|
+
> 新机器 / 升级:请安装 `>=0.3.6`(Codex + OpenCode + Hermes 被动观察)。
|
|
14
|
+
> 旧版本只有主动 MCP 规则注入,没有「无感存/取」。安装后执行一次
|
|
15
|
+
> `mio init && mio install <host>`。
|
|
16
16
|
|
|
17
17
|
## Commands
|
|
18
18
|
|
|
@@ -37,6 +37,9 @@ mio --json agents Machine-readable agents
|
|
|
37
37
|
running, restart it once so it reloads both files. `mio observe` also
|
|
38
38
|
passively tails the OpenCode session database (`opencode db`) without
|
|
39
39
|
requiring the model to call Mio tools.
|
|
40
|
+
- Hermes uses `hermes mcp add mio-intelligence --command node --args <server>`
|
|
41
|
+
to register the Mio MCP server. Hermes has native memory and reads
|
|
42
|
+
AGENTS.md Context Files, so retrieval works without Mio rule injection.
|
|
40
43
|
- WorkBuddy uses `~/.workbuddy/mcp.json` and requires each custom MCP server to
|
|
41
44
|
be approved in `~/.workbuddy/mcp-approvals.json`. `mio install workbuddy`
|
|
42
45
|
writes both files and injects Mio usage rules into
|
|
@@ -72,6 +75,12 @@ OpenCode 会话不依赖模型自觉:`mio observe` 通过 `opencode db` 增量
|
|
|
72
75
|
把摘要写回 `<workspace>/AGENTS.md` 的 `MIO_CONTEXT` 块,供 OpenCode 下一会话
|
|
73
76
|
被动取用。首次启动只回填最近 24h 的 part,避免历史会话灌入记忆库。
|
|
74
77
|
|
|
78
|
+
Hermes(Nous hermes-agent)同理:`mio observe` 以只读方式增量读取
|
|
79
|
+
`<LocalAppData>/hermes/state.db`(`messages.id` 自增游标,无需调用 hermes CLI),
|
|
80
|
+
把每个用户回合的 `task_outcome` 写入 `MIO_HOME`(`source=hermes-observer`)。
|
|
81
|
+
Hermes 本身有原生记忆,Mio 只负责让 Hermes 经验进入跨 Agent 生态;仅当会话
|
|
82
|
+
发生在真实 git 仓库时才写回该仓库的 `AGENTS.md`,避免污染用户主目录。
|
|
83
|
+
|
|
75
84
|
## MIO_HOME
|
|
76
85
|
|
|
77
86
|
默认 `~/.mio-intelligence`,可通过环境变量 `MIO_HOME` 覆盖。
|
|
@@ -0,0 +1,90 @@
|
|
|
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 hermesBin() {
|
|
11
|
+
if (process.env.HERMES_BIN && fs.existsSync(process.env.HERMES_BIN)) return process.env.HERMES_BIN
|
|
12
|
+
const candidates = [
|
|
13
|
+
path.join(os.homedir(), 'AppData', 'Local', 'hermes', 'hermes-agent', 'bin', 'hermes.exe'),
|
|
14
|
+
path.join(os.homedir(), '.hermes', 'bin', 'hermes.exe'),
|
|
15
|
+
]
|
|
16
|
+
for (const c of candidates) {
|
|
17
|
+
if (fs.existsSync(c)) return c
|
|
18
|
+
}
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function runHermes(args) {
|
|
23
|
+
const bin = hermesBin()
|
|
24
|
+
// `hermes mcp add` asks to confirm tool enablement interactively; pre-answer
|
|
25
|
+
// it so the install is non-interactive.
|
|
26
|
+
const input = args[0] === 'mcp' && args[1] === 'add' ? 'Y\n' : undefined
|
|
27
|
+
let res
|
|
28
|
+
try {
|
|
29
|
+
res = bin
|
|
30
|
+
? spawnSync(bin, args, { encoding: 'utf8', timeout: 60000, windowsHide: true, input: input })
|
|
31
|
+
: spawnSync('hermes', args, { encoding: 'utf8', timeout: 60000, shell: true, windowsHide: true, input: input })
|
|
32
|
+
} catch (_) {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
if (res.error || res.status !== 0) return null
|
|
36
|
+
return (res.stdout || '') + (res.stderr || '')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isInstalled() {
|
|
40
|
+
const out = runHermes(['mcp', 'list'])
|
|
41
|
+
return Boolean(out && out.indexOf(SERVER_NAME) !== -1)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function install({ node, serverScript, home, workspace, project }) {
|
|
45
|
+
if (!hermesBin() && !isHermesOnPath()) {
|
|
46
|
+
return {
|
|
47
|
+
changed: false,
|
|
48
|
+
message: 'Hermes binary not found. Install hermes-agent first.',
|
|
49
|
+
configPath: null,
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (isInstalled()) {
|
|
53
|
+
return { changed: false, message: 'Hermes Mio MCP already installed.', configPath: null }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const context = JSON.stringify({
|
|
57
|
+
agentId: 'hermes',
|
|
58
|
+
project: project || path.basename(workspace || process.cwd()),
|
|
59
|
+
workspace: workspace || process.cwd(),
|
|
60
|
+
sessionId: 'hermes-session',
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const args = [
|
|
64
|
+
'mcp', 'add', SERVER_NAME,
|
|
65
|
+
'--command', node,
|
|
66
|
+
'--args', serverScript,
|
|
67
|
+
'--env', 'MIO_DATA_DIR=' + home, 'MIO_CONTEXT=' + context,
|
|
68
|
+
]
|
|
69
|
+
const out = runHermes(args)
|
|
70
|
+
const installed = isInstalled()
|
|
71
|
+
|
|
72
|
+
let message
|
|
73
|
+
if (installed) {
|
|
74
|
+
message = 'Hermes Mio MCP installed (config: <LocalAppData>/hermes/config.yaml). Start a new Hermes session to load it.'
|
|
75
|
+
} else {
|
|
76
|
+
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.'
|
|
77
|
+
}
|
|
78
|
+
return { changed: installed, configPath: null, message }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isHermesOnPath() {
|
|
82
|
+
try {
|
|
83
|
+
const res = spawnSync('where', ['hermes'], { encoding: 'utf8', shell: true })
|
|
84
|
+
return res.status === 0 && /hermes/.test(res.stdout || '')
|
|
85
|
+
} catch (_) {
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
module.exports = { hermesBin, isInstalled, install }
|
package/bin/mio.js
CHANGED
|
@@ -1,249 +1,255 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
'use strict'
|
|
3
|
-
|
|
4
|
-
const fs = require('fs')
|
|
5
|
-
const path = require('path')
|
|
6
|
-
const os = require('os')
|
|
7
|
-
|
|
8
|
-
const SERVER_SCRIPT = path.resolve(__dirname, '..', 'server', 'mio-intelligence-mcp', 'index.js')
|
|
9
|
-
const MIO_HOME = process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
|
|
10
|
-
const CONFIG_FILE = path.join(MIO_HOME, 'config.json')
|
|
11
|
-
|
|
12
|
-
const adapters = {
|
|
13
|
-
codex: require('../adapters/codex.js'),
|
|
14
|
-
opencode: require('../adapters/opencode.js'),
|
|
15
|
-
workbuddy: require('../adapters/workbuddy.js'),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
fs.mkdirSync(
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
const fs = require('fs')
|
|
5
|
+
const path = require('path')
|
|
6
|
+
const os = require('os')
|
|
7
|
+
|
|
8
|
+
const SERVER_SCRIPT = path.resolve(__dirname, '..', 'server', 'mio-intelligence-mcp', 'index.js')
|
|
9
|
+
const MIO_HOME = process.env.MIO_HOME || path.join(os.homedir(), '.mio-intelligence')
|
|
10
|
+
const CONFIG_FILE = path.join(MIO_HOME, 'config.json')
|
|
11
|
+
|
|
12
|
+
const adapters = {
|
|
13
|
+
codex: require('../adapters/codex.js'),
|
|
14
|
+
opencode: require('../adapters/opencode.js'),
|
|
15
|
+
workbuddy: require('../adapters/workbuddy.js'),
|
|
16
|
+
hermes: require('../adapters/hermes.js'),
|
|
17
|
+
}
|
|
18
|
+
const observer = require('../observe/observer.js')
|
|
19
|
+
|
|
20
|
+
function ensureHome() {
|
|
21
|
+
fs.mkdirSync(MIO_HOME, { recursive: true })
|
|
22
|
+
fs.mkdirSync(path.join(MIO_HOME, 'logs'), { recursive: true })
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readConfig() {
|
|
26
|
+
const fallback = { version: 1, home: MIO_HOME, agents: {} }
|
|
27
|
+
if (!fs.existsSync(CONFIG_FILE)) return fallback
|
|
28
|
+
try {
|
|
29
|
+
return { ...fallback, ...JSON.parse(fs.readFileSync(CONFIG_FILE, 'utf8')) }
|
|
30
|
+
} catch (_) {
|
|
31
|
+
return fallback
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function writeConfig(config) {
|
|
36
|
+
ensureHome()
|
|
37
|
+
fs.writeFileSync(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, 'utf8')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function jsonOrText(payload, useJson) {
|
|
41
|
+
if (useJson) {
|
|
42
|
+
console.log(JSON.stringify(payload, null, 2))
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
return payload
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function initCommand() {
|
|
49
|
+
ensureHome()
|
|
50
|
+
const config = readConfig()
|
|
51
|
+
if (!config.createdAt) config.createdAt = new Date().toISOString()
|
|
52
|
+
writeConfig(config)
|
|
53
|
+
console.log(`MIO_HOME ready: ${MIO_HOME}`)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function statusCommand(useJson) {
|
|
57
|
+
const config = readConfig()
|
|
58
|
+
const installed = {
|
|
59
|
+
codex: adapters.codex.isInstalled(),
|
|
60
|
+
opencode: adapters.opencode.isInstalled(),
|
|
61
|
+
workbuddy: adapters.workbuddy.isInstalled(),
|
|
62
|
+
hermes: adapters.hermes.isInstalled(),
|
|
63
|
+
}
|
|
64
|
+
const payload = {
|
|
65
|
+
version: config.version,
|
|
66
|
+
home: MIO_HOME,
|
|
67
|
+
serverScript: SERVER_SCRIPT,
|
|
68
|
+
serverScriptExists: fs.existsSync(SERVER_SCRIPT),
|
|
69
|
+
agents: config.agents || {},
|
|
70
|
+
codexInstalled: installed.codex,
|
|
71
|
+
opencodeInstalled: installed.opencode,
|
|
72
|
+
workbuddyInstalled: installed.workbuddy,
|
|
73
|
+
hermesInstalled: installed.hermes,
|
|
74
|
+
hermesObserverReady: fs.existsSync(observer.hermesDbPath()),
|
|
71
75
|
opencodeObserverReady: fs.existsSync(observer.opencodeDbPath()),
|
|
72
|
-
observerRunning: observer.isRunning(MIO_HOME),
|
|
73
|
-
observerPid: observer.isRunning(MIO_HOME) ? observer.readPid(MIO_HOME) : null,
|
|
74
|
-
}
|
|
75
|
-
if (useJson) {
|
|
76
|
-
jsonOrText(payload, true)
|
|
77
|
-
return
|
|
78
|
-
}
|
|
79
|
-
console.log(`MIO_HOME: ${MIO_HOME}`)
|
|
80
|
-
console.log(`MCP Server: ${SERVER_SCRIPT}`)
|
|
81
|
-
console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
|
|
76
|
+
observerRunning: observer.isRunning(MIO_HOME),
|
|
77
|
+
observerPid: observer.isRunning(MIO_HOME) ? observer.readPid(MIO_HOME) : null,
|
|
78
|
+
}
|
|
79
|
+
if (useJson) {
|
|
80
|
+
jsonOrText(payload, true)
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
console.log(`MIO_HOME: ${MIO_HOME}`)
|
|
84
|
+
console.log(`MCP Server: ${SERVER_SCRIPT}`)
|
|
85
|
+
console.log(`Codex MCP: ${installed.codex ? 'installed' : 'not installed'}`)
|
|
82
86
|
console.log(`OpenCode MCP: ${installed.opencode ? 'installed' : 'not installed'}`)
|
|
83
|
-
console.log(`OpenCode observer: ${fs.existsSync(observer.opencodeDbPath()) ? 'ready' : 'no database'}`)
|
|
84
|
-
console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
|
|
85
|
-
console.log(`
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
return
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
return
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
mio
|
|
210
|
-
mio
|
|
211
|
-
mio
|
|
212
|
-
mio
|
|
213
|
-
mio
|
|
214
|
-
mio
|
|
215
|
-
mio --
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
const
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
case '
|
|
230
|
-
return
|
|
231
|
-
case '
|
|
232
|
-
return
|
|
233
|
-
case '
|
|
234
|
-
return
|
|
235
|
-
case '
|
|
236
|
-
return
|
|
237
|
-
case '
|
|
238
|
-
return
|
|
239
|
-
case '
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
87
|
+
console.log(`OpenCode observer: ${fs.existsSync(observer.opencodeDbPath()) ? 'ready' : 'no database'}`)
|
|
88
|
+
console.log(`WorkBuddy MCP: ${installed.workbuddy ? 'installed' : 'not installed'}`)
|
|
89
|
+
console.log(`Hermes MCP: ${installed.hermes ? 'installed' : 'not installed'}`)
|
|
90
|
+
console.log(`Hermes observer: ${fs.existsSync(observer.hermesDbPath()) ? 'ready' : 'no database'}`)
|
|
91
|
+
console.log(`Observer: ${observer.isRunning(MIO_HOME) ? 'running (pid ' + observer.readPid(MIO_HOME) + ')' : 'not running'}`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function agentsCommand(useJson) {
|
|
95
|
+
const config = readConfig()
|
|
96
|
+
const agents = config.agents || {}
|
|
97
|
+
if (useJson) {
|
|
98
|
+
jsonOrText(agents, true)
|
|
99
|
+
return
|
|
100
|
+
}
|
|
101
|
+
const names = Object.keys(agents)
|
|
102
|
+
if (names.length === 0) {
|
|
103
|
+
console.log('No agents installed.')
|
|
104
|
+
return
|
|
105
|
+
}
|
|
106
|
+
for (const name of names) {
|
|
107
|
+
console.log(`${name}\t${agents[name].installedAt || ''}`)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function installCommand(host, useJson) {
|
|
112
|
+
if (!host) {
|
|
113
|
+
console.error('Usage: mio install <codex|opencode|workbuddy|hermes>')
|
|
114
|
+
process.exitCode = 1
|
|
115
|
+
return
|
|
116
|
+
}
|
|
117
|
+
const adapter = adapters[host]
|
|
118
|
+
if (!adapter) {
|
|
119
|
+
console.error(`Unknown host: ${host}`)
|
|
120
|
+
process.exitCode = 1
|
|
121
|
+
return
|
|
122
|
+
}
|
|
123
|
+
ensureHome()
|
|
124
|
+
const config = readConfig()
|
|
125
|
+
let result
|
|
126
|
+
try {
|
|
127
|
+
result = adapter.install({
|
|
128
|
+
node: process.execPath,
|
|
129
|
+
serverScript: SERVER_SCRIPT,
|
|
130
|
+
home: MIO_HOME,
|
|
131
|
+
workspace: process.cwd(),
|
|
132
|
+
})
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.error(`Failed to install ${host}: ${error.message || error}`)
|
|
135
|
+
process.exitCode = 1
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
if (result.changed) {
|
|
139
|
+
config.agents = config.agents || {}
|
|
140
|
+
config.agents[host] = {
|
|
141
|
+
installedAt: new Date().toISOString(),
|
|
142
|
+
configPath: result.configPath || null,
|
|
143
|
+
}
|
|
144
|
+
writeConfig(config)
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
observer.runCycle(MIO_HOME, observer.loadState(MIO_HOME), observer.createSinks(MIO_HOME))
|
|
148
|
+
} catch (_) {}
|
|
149
|
+
let observerStarted = false
|
|
150
|
+
try {
|
|
151
|
+
observerStarted = observer.startBackground(MIO_HOME).started
|
|
152
|
+
} catch (_) {}
|
|
153
|
+
if (useJson) {
|
|
154
|
+
result.observer = {
|
|
155
|
+
running: observer.isRunning(MIO_HOME),
|
|
156
|
+
started: observerStarted,
|
|
157
|
+
}
|
|
158
|
+
jsonOrText(result, true)
|
|
159
|
+
return
|
|
160
|
+
}
|
|
161
|
+
console.log(result.message || `${host} adapter completed.`)
|
|
162
|
+
console.log(
|
|
163
|
+
observer.isRunning(MIO_HOME)
|
|
164
|
+
? 'Observer daemon running.'
|
|
165
|
+
: 'Observer daemon not running. Run `mio observe --start`.'
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function mcpCommand() {
|
|
170
|
+
require(SERVER_SCRIPT)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function observeCommand(args) {
|
|
174
|
+
const flags = args.slice(1)
|
|
175
|
+
const useJson = flags.includes('--json')
|
|
176
|
+
if (flags.includes('--start')) {
|
|
177
|
+
const result = observer.startBackground(MIO_HOME)
|
|
178
|
+
if (useJson) return jsonOrText(result, true)
|
|
179
|
+
console.log(result.started ? 'Observer started.' : 'Observer already running.')
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
if (flags.includes('--stop')) {
|
|
183
|
+
const result = observer.stopBackground(MIO_HOME)
|
|
184
|
+
if (useJson) return jsonOrText(result, true)
|
|
185
|
+
console.log(result.stopped ? 'Observer stopped.' : 'Observer was not running.')
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if (flags.includes('--status')) {
|
|
189
|
+
const running = observer.isRunning(MIO_HOME)
|
|
190
|
+
const pid = observer.readPid(MIO_HOME)
|
|
191
|
+
if (useJson) return jsonOrText({ running, pid }, true)
|
|
192
|
+
console.log(running ? 'Observer: running (pid ' + pid + ')' : 'Observer: not running')
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
if (flags.includes('--once')) {
|
|
196
|
+
const res = observer.runCycle(
|
|
197
|
+
MIO_HOME,
|
|
198
|
+
observer.loadState(MIO_HOME),
|
|
199
|
+
observer.createSinks(MIO_HOME)
|
|
200
|
+
)
|
|
201
|
+
if (useJson) return jsonOrText(res, true)
|
|
202
|
+
console.log('Observed files=' + res.files + ' lines=' + res.lines + ' outcomes=' + res.outcomes)
|
|
203
|
+
return
|
|
204
|
+
}
|
|
205
|
+
console.log('Observing WorkBuddy transcripts into ' + MIO_HOME + ' (Ctrl+C to stop)...')
|
|
206
|
+
observer.startForeground(MIO_HOME)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function help() {
|
|
210
|
+
console.log(`Mio Agent Runtime CLI
|
|
211
|
+
|
|
212
|
+
Usage:
|
|
213
|
+
mio init Initialize MIO_HOME
|
|
214
|
+
mio mcp Start Mio MCP server (stdio)
|
|
215
|
+
mio install <host> Install Mio into a host (codex|opencode|workbuddy|hermes)
|
|
216
|
+
mio status Show runtime and adapter status
|
|
217
|
+
mio agents List installed agents
|
|
218
|
+
mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes
|
|
219
|
+
mio observe --start|--stop|--status|--once Manage the background observer daemon
|
|
220
|
+
mio --json status Machine-readable status
|
|
221
|
+
mio --json agents Machine-readable agents
|
|
222
|
+
`)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function main() {
|
|
226
|
+
const args = process.argv.slice(2)
|
|
227
|
+
const jsonIndex = args.indexOf('--json')
|
|
228
|
+
const useJson = jsonIndex !== -1
|
|
229
|
+
if (useJson) args.splice(jsonIndex, 1)
|
|
230
|
+
const command = args[0] || 'help'
|
|
231
|
+
|
|
232
|
+
switch (command) {
|
|
233
|
+
case 'init':
|
|
234
|
+
return initCommand()
|
|
235
|
+
case 'mcp':
|
|
236
|
+
return mcpCommand()
|
|
237
|
+
case 'install':
|
|
238
|
+
return installCommand(args[1], useJson)
|
|
239
|
+
case 'status':
|
|
240
|
+
return statusCommand(useJson)
|
|
241
|
+
case 'agents':
|
|
242
|
+
return agentsCommand(useJson)
|
|
243
|
+
case 'observe':
|
|
244
|
+
return observeCommand(args)
|
|
245
|
+
case 'help':
|
|
246
|
+
case '--help':
|
|
247
|
+
return help()
|
|
248
|
+
default:
|
|
249
|
+
console.error(`Unknown command: ${command}`)
|
|
250
|
+
help()
|
|
251
|
+
process.exitCode = 1
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
main()
|
package/observe/observer.js
CHANGED
|
@@ -83,6 +83,9 @@ function loadState(home) {
|
|
|
83
83
|
ocDbMtime: parsed.ocDbMtime || null,
|
|
84
84
|
ocPending: parsed.ocPending || false,
|
|
85
85
|
ocTurns: parsed.ocTurns || {},
|
|
86
|
+
hmSeeded: parsed.hmSeeded || false,
|
|
87
|
+
hmLastMsgId: parsed.hmLastMsgId || 0,
|
|
88
|
+
hmTurns: parsed.hmTurns || {},
|
|
86
89
|
}
|
|
87
90
|
}
|
|
88
91
|
} catch (_) {}
|
|
@@ -100,6 +103,9 @@ function loadState(home) {
|
|
|
100
103
|
ocDbMtime: null,
|
|
101
104
|
ocPending: false,
|
|
102
105
|
ocTurns: {},
|
|
106
|
+
hmSeeded: false,
|
|
107
|
+
hmLastMsgId: 0,
|
|
108
|
+
hmTurns: {},
|
|
103
109
|
}
|
|
104
110
|
}
|
|
105
111
|
|
|
@@ -883,6 +889,230 @@ function runOpencodeCycle(home, state, sinks, opts) {
|
|
|
883
889
|
return out
|
|
884
890
|
}
|
|
885
891
|
|
|
892
|
+
// ---------------------------------------------------------------------------
|
|
893
|
+
// Hermes session observation (passive, via its own SQLite state.db)
|
|
894
|
+
// ---------------------------------------------------------------------------
|
|
895
|
+
|
|
896
|
+
// Hermes (Nous Research hermes-agent) keeps every conversation in
|
|
897
|
+
// <LocalAppData>/hermes/state.db. Unlike OpenCode the file is not exclusively
|
|
898
|
+
// locked, and `messages.id` is a monotonic AUTOINCREMENT cursor, so we can tail
|
|
899
|
+
// it read-only with node:sqlite (Node 22+). Each message is one row carrying
|
|
900
|
+
// role / content / finish_reason, so a user row opens a turn and an assistant
|
|
901
|
+
// row with a terminal finish_reason closes it immediately. Hermes already has
|
|
902
|
+
// native memory, so this observer only imports Hermes experience into the Mio
|
|
903
|
+
// ecosystem (traces + memory + AGENTS.md context when a git repo is present).
|
|
904
|
+
|
|
905
|
+
let _hermesSqlite = null
|
|
906
|
+
let _hermesSqliteTried = false
|
|
907
|
+
|
|
908
|
+
function hermesSqliteModule() {
|
|
909
|
+
if (_hermesSqliteTried) return _hermesSqlite
|
|
910
|
+
_hermesSqliteTried = true
|
|
911
|
+
try {
|
|
912
|
+
_hermesSqlite = require('node:sqlite')
|
|
913
|
+
} catch (_) {
|
|
914
|
+
_hermesSqlite = null
|
|
915
|
+
}
|
|
916
|
+
return _hermesSqlite
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function hermesDbPath() {
|
|
920
|
+
return (
|
|
921
|
+
process.env.HERMES_DATA_DIR ||
|
|
922
|
+
path.join(os.homedir(), 'AppData', 'Local', 'hermes', 'state.db')
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
function hermesProjectName(meta) {
|
|
927
|
+
if (meta.git) return path.basename(meta.git)
|
|
928
|
+
if (meta.cwd && meta.cwd !== os.homedir()) return path.basename(meta.cwd)
|
|
929
|
+
return 'hermes'
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
function createHermesTurn(meta) {
|
|
933
|
+
meta = meta || {}
|
|
934
|
+
return {
|
|
935
|
+
id: null,
|
|
936
|
+
ts: null,
|
|
937
|
+
sessionId: meta.sessionId || null,
|
|
938
|
+
cwd: meta.git || null,
|
|
939
|
+
project: hermesProjectName({ git: meta.git, cwd: meta.projectHint }),
|
|
940
|
+
title: meta.title || null,
|
|
941
|
+
agent: meta.agent || null,
|
|
942
|
+
lastText: null,
|
|
943
|
+
toolCount: 0,
|
|
944
|
+
lastMid: null,
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
function hermesOutcomeFor(finish) {
|
|
949
|
+
if (finish === 'error' || finish === 'failure') return 'failure'
|
|
950
|
+
if (finish === 'cancel' || finish === 'aborted' || finish === 'interrupted') return 'aborted'
|
|
951
|
+
return 'success'
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
function finalizeHermesTurn(turn, outcome) {
|
|
955
|
+
if (!turn.id) return null
|
|
956
|
+
const finalized = {
|
|
957
|
+
id: turn.id,
|
|
958
|
+
ts: turn.ts,
|
|
959
|
+
sessionId: turn.sessionId,
|
|
960
|
+
cwd: turn.cwd,
|
|
961
|
+
project: turn.project,
|
|
962
|
+
agent: turn.agent,
|
|
963
|
+
title: turn.title,
|
|
964
|
+
lastText: turn.lastText,
|
|
965
|
+
toolCount: turn.toolCount,
|
|
966
|
+
errors: [],
|
|
967
|
+
outcome: outcome,
|
|
968
|
+
}
|
|
969
|
+
return finalized
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
function feedHermesRow(turn, row) {
|
|
973
|
+
const meta = { sessionId: row.sid, title: row.stitle, git: row.sgit, agent: row.ssource, projectHint: row.scwd }
|
|
974
|
+
|
|
975
|
+
if (row.role === 'user') {
|
|
976
|
+
let finalized = null
|
|
977
|
+
if (turn.id) finalized = finalizeHermesTurn(turn, 'aborted')
|
|
978
|
+
const next = createHermesTurn(meta)
|
|
979
|
+
next.id = Number(row.id)
|
|
980
|
+
next.ts = Math.floor(Number(row.t) * 1000) || Date.now()
|
|
981
|
+
next.lastMid = Number(row.id)
|
|
982
|
+
return { finalized, next }
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
if (row.role === 'assistant') {
|
|
986
|
+
if (!turn.id) return { next: turn }
|
|
987
|
+
if (typeof row.content === 'string' && row.content.trim()) {
|
|
988
|
+
turn.lastText = row.content
|
|
989
|
+
}
|
|
990
|
+
if (row.tool_calls) {
|
|
991
|
+
try {
|
|
992
|
+
const calls = JSON.parse(row.tool_calls)
|
|
993
|
+
if (Array.isArray(calls)) turn.toolCount += calls.length
|
|
994
|
+
} catch (_) {}
|
|
995
|
+
}
|
|
996
|
+
if (row.finish_reason && row.finish_reason !== 'tool_calls') {
|
|
997
|
+
const finalized = finalizeHermesTurn(turn, hermesOutcomeFor(row.finish_reason))
|
|
998
|
+
return { finalized, next: createHermesTurn(meta) }
|
|
999
|
+
}
|
|
1000
|
+
turn.lastMid = Number(row.id)
|
|
1001
|
+
return { next: turn }
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
return { next: turn }
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
function emitHermesOutcome(turn, sinks, state) {
|
|
1008
|
+
if (!turn.id) return false
|
|
1009
|
+
const summary = cleanText(turn.lastText)
|
|
1010
|
+
if (!summary) return false
|
|
1011
|
+
const outcome = turn.outcome
|
|
1012
|
+
const tsToken = turn.ts ? Math.floor(turn.ts / 1000) : Math.floor(Date.now() / 1000)
|
|
1013
|
+
|
|
1014
|
+
sinks.trace({
|
|
1015
|
+
trace_id: 'hermes:' + (turn.sessionId || 'unknown') + ':' + String(turn.id) + ':' + tsToken,
|
|
1016
|
+
event_type: 'task_outcome',
|
|
1017
|
+
outcome: outcome,
|
|
1018
|
+
payload: {
|
|
1019
|
+
host: 'hermes',
|
|
1020
|
+
project: turn.project,
|
|
1021
|
+
sessionId: turn.sessionId,
|
|
1022
|
+
turnId: turn.id,
|
|
1023
|
+
title: turn.title,
|
|
1024
|
+
agent: turn.agent,
|
|
1025
|
+
toolCalls: turn.toolCount,
|
|
1026
|
+
errors: 0,
|
|
1027
|
+
summary: summary.slice(0, 300),
|
|
1028
|
+
source: 'hermes-observer',
|
|
1029
|
+
},
|
|
1030
|
+
agent: 'hermes',
|
|
1031
|
+
host: 'hermes',
|
|
1032
|
+
project: turn.project,
|
|
1033
|
+
})
|
|
1034
|
+
|
|
1035
|
+
if (outcome === 'success' && summary.length >= 10) {
|
|
1036
|
+
sinks.memory({
|
|
1037
|
+
kind: 'note',
|
|
1038
|
+
content: '[hermes] 任务完成: ' + summary.slice(0, 160),
|
|
1039
|
+
tags: ['task-outcome', 'hermes', turn.project],
|
|
1040
|
+
project: turn.project,
|
|
1041
|
+
source: 'hermes-observer',
|
|
1042
|
+
})
|
|
1043
|
+
// Hermes reads AGENTS.md Context Files; write back only into a real repo
|
|
1044
|
+
// workspace so we never pollute the user's home directory.
|
|
1045
|
+
if (turn.cwd) {
|
|
1046
|
+
var ctxTime = new Date(Number(turn.ts) || Date.now()).toLocaleString('zh-CN', { hour12: false })
|
|
1047
|
+
updateProjectContext(state, turn.cwd, ctxTime + ' | ' + outcome + ' | ' + summary.slice(0, 80))
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
return true
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function runHermesCycle(home, state, sinks, opts) {
|
|
1054
|
+
const out = { files: 0, lines: 0, outcomes: 0 }
|
|
1055
|
+
const dbPath = hermesDbPath()
|
|
1056
|
+
if (!fs.existsSync(dbPath)) return out
|
|
1057
|
+
const sqlite = hermesSqliteModule()
|
|
1058
|
+
if (!sqlite) return out
|
|
1059
|
+
let db
|
|
1060
|
+
try {
|
|
1061
|
+
db = new sqlite.DatabaseSync(dbPath, { readOnly: true })
|
|
1062
|
+
} catch (_) {
|
|
1063
|
+
return out
|
|
1064
|
+
}
|
|
1065
|
+
out.files = 1
|
|
1066
|
+
const maxAgeMs = (opts && opts.maxAgeMs) || 24 * 60 * 60 * 1000
|
|
1067
|
+
try {
|
|
1068
|
+
if (!state.hmSeeded) {
|
|
1069
|
+
const cutoff = (Date.now() - maxAgeMs) / 1000
|
|
1070
|
+
const first = db.prepare('SELECT id FROM messages WHERE active = 1 AND timestamp >= ? ORDER BY id LIMIT 1').get(cutoff)
|
|
1071
|
+
if (first) {
|
|
1072
|
+
state.hmLastMsgId = first.id
|
|
1073
|
+
} else {
|
|
1074
|
+
const maxRow = db.prepare('SELECT COALESCE(MAX(id), 0) AS id FROM messages').get()
|
|
1075
|
+
state.hmLastMsgId = maxRow.id
|
|
1076
|
+
}
|
|
1077
|
+
state.hmSeeded = true
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
const limit = 2000
|
|
1081
|
+
const stmt = db.prepare(
|
|
1082
|
+
'SELECT m.id AS id, m.session_id AS sid, m.role AS role, m.content AS content, ' +
|
|
1083
|
+
'm.tool_name AS tool_name, m.tool_calls AS tool_calls, m.timestamp AS t, m.finish_reason AS finish_reason, ' +
|
|
1084
|
+
's.title AS stitle, s.cwd AS scwd, s.git_repo_root AS sgit, s.source AS ssource ' +
|
|
1085
|
+
'FROM messages m LEFT JOIN sessions s ON s.id = m.session_id ' +
|
|
1086
|
+
'WHERE m.id > ? AND m.active = 1 ORDER BY m.id LIMIT ' + limit
|
|
1087
|
+
)
|
|
1088
|
+
const rows = stmt.all(state.hmLastMsgId)
|
|
1089
|
+
if (rows.length === 0) return out
|
|
1090
|
+
|
|
1091
|
+
let lastId = state.hmLastMsgId
|
|
1092
|
+
for (const row of rows) {
|
|
1093
|
+
const rid = Number(row.id)
|
|
1094
|
+
if (rid > lastId) lastId = rid
|
|
1095
|
+
out.lines += 1
|
|
1096
|
+
const sid = row.sid
|
|
1097
|
+
let turn = state.hmTurns[sid]
|
|
1098
|
+
if (!turn) {
|
|
1099
|
+
turn = createHermesTurn({ sessionId: sid, title: row.stitle, git: row.sgit, agent: row.ssource, projectHint: row.scwd })
|
|
1100
|
+
state.hmTurns[sid] = turn
|
|
1101
|
+
}
|
|
1102
|
+
const result = feedHermesRow(turn, row)
|
|
1103
|
+
if (result.finalized) {
|
|
1104
|
+
if (emitHermesOutcome(result.finalized, sinks, state)) out.outcomes += 1
|
|
1105
|
+
}
|
|
1106
|
+
state.hmTurns[sid] = result.next
|
|
1107
|
+
}
|
|
1108
|
+
state.hmLastMsgId = lastId
|
|
1109
|
+
} finally {
|
|
1110
|
+
db.close()
|
|
1111
|
+
}
|
|
1112
|
+
return out
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
|
|
886
1116
|
// ---------------------------------------------------------------------------
|
|
887
1117
|
// cycle
|
|
888
1118
|
// ---------------------------------------------------------------------------
|
|
@@ -970,6 +1200,11 @@ function runCycle(home, state, sinks, opts) {
|
|
|
970
1200
|
out.lines += ocRes.lines
|
|
971
1201
|
out.outcomes += ocRes.outcomes
|
|
972
1202
|
|
|
1203
|
+
const hmRes = runHermesCycle(home, state, sinks, opts)
|
|
1204
|
+
out.files += hmRes.files
|
|
1205
|
+
out.lines += hmRes.lines
|
|
1206
|
+
out.outcomes += hmRes.outcomes
|
|
1207
|
+
|
|
973
1208
|
saveState(home, state)
|
|
974
1209
|
return out
|
|
975
1210
|
}
|
|
@@ -1059,6 +1294,8 @@ module.exports = {
|
|
|
1059
1294
|
runCycle,
|
|
1060
1295
|
opencodeDbPath,
|
|
1061
1296
|
runOpencodeCycle,
|
|
1297
|
+
runHermesCycle,
|
|
1298
|
+
hermesDbPath,
|
|
1062
1299
|
createSinks,
|
|
1063
1300
|
startForeground,
|
|
1064
1301
|
startBackground,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mio-agent-runtime",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Mio Agent Runtime installer and MCP intelligence layer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"README.md"
|
|
16
16
|
],
|
|
17
17
|
"scripts": {
|
|
18
|
-
"check": "node --check bin/mio.js && node --check adapters/codex.js && node --check adapters/opencode.js && node --check adapters/workbuddy.js && node --check server/mio-intelligence-mcp/index.js && node --check observe/observer.js",
|
|
18
|
+
"check": "node --check bin/mio.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 observe/observer.js",
|
|
19
19
|
"prepack": "npm run check"
|
|
20
20
|
},
|
|
21
21
|
"engines": {
|