mio-agent-runtime 0.10.0 → 0.12.0
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 +153 -11
- package/adapters/hermes.js +59 -5
- package/adapters/host-utils.js +66 -61
- package/bin/mio.js +3216 -2943
- package/observe/observer.js +82 -12
- package/package.json +1 -1
- package/scripts/check-syntax.cjs +16 -2
- package/server/agent-store.js +7 -7
- package/server/creativity-engine.js +5 -0
- package/server/digest.js +3 -18
- package/server/evaluation-store.js +244 -0
- package/server/evolution-report.js +5 -21
- package/server/experience-store.js +3 -3
- package/server/memory-store.js +119 -23
- package/server/mio-intelligence-mcp/index.js +19 -0
- package/server/mio-intelligence-mcp/phase0.js +4 -20
- package/server/observer-store.js +6 -2
- package/server/policy-store.js +3 -3
- package/server/query-log.js +3 -9
- package/server/retention.js +1 -9
- package/server/subscription-store.js +62 -8
- package/server/task-store.js +100 -10
package/README.md
CHANGED
|
@@ -25,19 +25,20 @@ mio config llm --clear Remove the stored LLM config (env vars still apply)
|
|
|
25
25
|
mio config path Print the config.json path
|
|
26
26
|
mio mcp Start Mio MCP server (stdio)
|
|
27
27
|
mio install <host> Install Mio into a host (codex|opencode|workbuddy|hermes|claude)
|
|
28
|
-
mio status Show runtime and adapter status
|
|
28
|
+
mio status Show runtime and adapter status (also reports pending auto-claims)
|
|
29
29
|
mio agents List installed host adapters
|
|
30
30
|
mio agents list List observed agents (from agents.jsonl, --project X)
|
|
31
31
|
mio agents report Report per-agent task/memory/reuse telemetry (--agent X, --project Y)
|
|
32
32
|
mio agents register --agent-id X Register an observed agent (--yes to apply; previews by default)
|
|
33
|
+
mio agents evaluation ADR-017 evaluation metrics: route adoption, behaviour change, recall quality, data hygiene (--project X, --since ISO)
|
|
33
34
|
mio evolution status Show composed evolution module health
|
|
34
35
|
mio evolution report Cross-agent evolution report: ecosystem, agents, memory health, suggestions (--period 24h|7d|30d|all)
|
|
35
|
-
mio evolution shadow record Record a shadow comparison sample
|
|
36
|
-
mio evolution dual-write record Record a dual-write comparison sample
|
|
37
|
-
mio evolution cutover readiness Assess shadow/dual-write cutover readiness
|
|
38
|
-
mio evolution authority plan Preview a gated authority switch plan
|
|
39
|
-
mio evolution migration plan Preview state migration diffs
|
|
40
|
-
mio evolution cutover apply --dry-run Dry-run a cutover plan
|
|
36
|
+
mio evolution shadow record Record a shadow comparison sample (--legacy/--modular JSON; both required)
|
|
37
|
+
mio evolution dual-write record Record a dual-write comparison sample (--legacy-result/--modular-result JSON; required)
|
|
38
|
+
mio evolution cutover readiness Assess shadow/dual-write cutover readiness (--min-shadow-runs/--max-mismatch-rate)
|
|
39
|
+
mio evolution authority plan Preview a gated authority switch plan (--readiness JSON; required)
|
|
40
|
+
mio evolution migration plan Preview state migration diffs (--legacy-records/--modular-records JSON; required)
|
|
41
|
+
mio evolution cutover apply --dry-run Dry-run a cutover plan (--plan JSON and --dry-run are both required)
|
|
41
42
|
mio observe Watch WorkBuddy transcripts and auto-ingest task outcomes (foreground)
|
|
42
43
|
mio observe --start|--stop|--status|--once Manage the background observer daemon
|
|
43
44
|
mio recall "<query>" Search Mio memory from the terminal (--project/--scope/--kind/--tags/--limit)
|
|
@@ -77,9 +78,28 @@ mio --json evolution status Machine-readable evolution module health
|
|
|
77
78
|
mio --version Print the version and exit (-V)
|
|
78
79
|
```
|
|
79
80
|
|
|
81
|
+
## 输出流与退出码
|
|
82
|
+
|
|
83
|
+
所有命令族共用同一套约定(12 个族一致,且每个族都有测试钉住):
|
|
84
|
+
|
|
85
|
+
| 情形 | stdout | stderr | 退出码 |
|
|
86
|
+
|---|---|---|---|
|
|
87
|
+
| 子命令写错 / 必填参数缺失 | **空** | 报错行 + 完整用法 | `1` |
|
|
88
|
+
| 裸命令(如 `mio memory`) | 用法 | 空 | `1` |
|
|
89
|
+
| `--help` | 用法 | 空 | `0` |
|
|
90
|
+
|
|
91
|
+
- **失败时 stdout 必须为空**,因为 `mio --json` 的调用方读的是 stdout——用法文本落在那里
|
|
92
|
+
会被当成 payload 去解析。所以缺参时先给出原因(`--legacy is required`),紧接着把用法与
|
|
93
|
+
示例一起打到 stderr。
|
|
94
|
+
- **裸命令不是错误**,只是"没说清要做什么",所以用法走 stdout;但仍以 `1` 结束,
|
|
95
|
+
这样脚本不会把"什么都没做"当成成功。它打印的用法与 `<命令> --help` 完全相同,
|
|
96
|
+
区别只在退出码(`1` vs `0`)。
|
|
97
|
+
- **例外**:`mio config` 与 `mio agents` 的裸命令是**真命令**(显示配置 / 列 agent),退出 `0`。
|
|
98
|
+
- `mio help` 与 `<命令> --help` 都走 stdout 并退出 `0`。
|
|
99
|
+
|
|
80
100
|
## MCP 工具
|
|
81
101
|
|
|
82
|
-
MCP 服务端在 5 大域共暴露
|
|
102
|
+
MCP 服务端在 5 大域共暴露 49 个工具:
|
|
83
103
|
|
|
84
104
|
### 记忆(11)
|
|
85
105
|
`mio.memory.query` · `mio.memory.record` · `mio.memory.archive` · `mio.memory.merge` · `mio.memory.migrate` · `mio.memory.analyze` · `mio.memory.forget` · `mio.experience.list` · `mio.experience.confirm` · `mio.experience.reuse` · `mio.policy.check`
|
|
@@ -95,8 +115,8 @@ MCP 服务端在 5 大域共暴露 48 个工具:
|
|
|
95
115
|
### 创意引擎(4)
|
|
96
116
|
`mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.list` · `mio.creativity.status`
|
|
97
117
|
|
|
98
|
-
### 任务、Agent 与演化(
|
|
99
|
-
`mio.task.route` · `mio.task.record_outcome` · `mio.agent.register` · `mio.agent.list` · `mio.agent.report` · `mio.host.capabilities` · `mio.evolution.status` · `mio.evolution.report` · `mio.evolution.shadow.record` · `mio.evolution.dual_write.record` · `mio.evolution.cutover.readiness` · `mio.evolution.cutover.apply` · `mio.evolution.migration.plan` · `mio.evolution.authority.plan` · `mio.phase0.report`
|
|
118
|
+
### 任务、Agent 与演化(16)
|
|
119
|
+
`mio.task.route` · `mio.task.record_outcome` · `mio.agent.register` · `mio.agent.list` · `mio.agent.report` · `mio.agent.evaluation` · `mio.host.capabilities` · `mio.evolution.status` · `mio.evolution.report` · `mio.evolution.shadow.record` · `mio.evolution.dual_write.record` · `mio.evolution.cutover.readiness` · `mio.evolution.cutover.apply` · `mio.evolution.migration.plan` · `mio.evolution.authority.plan` · `mio.phase0.report`
|
|
100
120
|
|
|
101
121
|
## 包
|
|
102
122
|
|
|
@@ -460,7 +480,7 @@ mio observer trends --base-dir /path/to/.local/observer
|
|
|
460
480
|
说明:
|
|
461
481
|
|
|
462
482
|
- **两者都委托给与 MCP 服务端完全相同的共享实现** —— `server/insight-store.js` 与 `server/observer-store.js`。这是 `memory-store.js` / `experience-store.js` / `policy-store.js` / `creativity-engine.js` / `agent-store.js` 一路沿用的同一个模式:一份实现、两个入口,因此不可能各自漂移。
|
|
463
|
-
-
|
|
483
|
+
- **`insight generate` 曾经是 MCP 专有**(`mio.insight.generate`):它要调 LLM,CLI 一度把它当作未知子命令拒绝——现在四个 `insight` 子命令在终端上都有入口;`--memory` 与 `--summary` 至少给一个,否则在**任何 LLM 调用之前**就拒绝。`observer collect` / `observer ferment` 此前也被一起挡在 CLI 之外,理由是「它们属于 daemon」——这个理由不成立:`observe/observer.js` 那条 daemon 只 tail WorkBuddy 的 transcript,从不调用这两者。现在它们有了终端入口,默认走全部已配置源:
|
|
464
484
|
|
|
465
485
|
```bash
|
|
466
486
|
mio observer collect # 全部已配置源
|
|
@@ -509,6 +529,72 @@ Note: this advances the cursor; the next digest returns only newer events.
|
|
|
509
529
|
- **`@akemi-mio/insight` 是可选依赖。** 未安装时 `mio insight status` 会失败并提示 `@akemi-mio/insight not installed`,而不是报告一个「看起来没有洞察」的全零结果——全零会掩盖「引擎根本没装」这件事。
|
|
510
530
|
- **观察管线的空目录是正常状态。** 管线没跑过时,`observer status` 各阶段计数为 0 并提示 "No pipeline data yet.",其余视图显示 "No ... found.",都是有效输出而非错误。
|
|
511
531
|
|
|
532
|
+
## 演化(evolution)
|
|
533
|
+
|
|
534
|
+
`mio.evolution.*` 一族也是「一份实现、两个入口」,但它和前面几节有一处形态差异:
|
|
535
|
+
**参数是内联的 JSON 快照,不是文件路径。** 这是这一族最常被误用的地方——`--legacy` 指的是
|
|
536
|
+
「旧实现这次跑出来的结果」,不是某个待读取的文件。
|
|
537
|
+
|
|
538
|
+
```bash
|
|
539
|
+
mio evolution status
|
|
540
|
+
mio evolution report --period 7d
|
|
541
|
+
|
|
542
|
+
# 影子比对:把「旧实现」和「模块化实现」各自的输出喂进来做深比较
|
|
543
|
+
mio evolution shadow record --legacy '{"modules":7}' --modular '{"modules":7}'
|
|
544
|
+
mio evolution shadow record --project akemi-mio --label "status parity" \
|
|
545
|
+
--legacy '{"modules":7}' --modular '{"modules":9}'
|
|
546
|
+
|
|
547
|
+
# 双写样本:--authoritative 声明谁是权威(省略即 legacy),两边各给一份写入结果
|
|
548
|
+
mio evolution dual-write record --authoritative legacy \
|
|
549
|
+
--legacy-result '{"id":"mem-1","ok":true}' \
|
|
550
|
+
--modular-result '{"id":"mem-1","ok":true}'
|
|
551
|
+
|
|
552
|
+
# 就绪度读的是上面两个台账,不是你「觉得」跑了几次
|
|
553
|
+
mio evolution cutover readiness
|
|
554
|
+
mio evolution cutover readiness --min-shadow-runs 5 --max-mismatch-rate 0.1
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
```text
|
|
558
|
+
Shadow comparison shadow_1789912345678_ab12cd: mismatch
|
|
559
|
+
diffs=1
|
|
560
|
+
|
|
561
|
+
Cutover readiness for akemi-mio: fail
|
|
562
|
+
shadow=2 dual-write=0
|
|
563
|
+
reasons: shadow samples 2/5; shadow mismatch rate 0.5; dual-write has no samples
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
各子命令的参数:
|
|
567
|
+
|
|
568
|
+
| 子命令 | 必填 | 可选 |
|
|
569
|
+
|---|---|---|
|
|
570
|
+
| `status` | 无 | 无 |
|
|
571
|
+
| `report` | 无 | `--period`(`24h` / `7d` / `30d` / `all`)、`--project` |
|
|
572
|
+
| `shadow record` | `--legacy <json>`、`--modular <json>` | `--project`、`--label`、`--input <json>` |
|
|
573
|
+
| `dual-write record` | `--legacy-result <json>`、`--modular-result <json>` | `--authoritative`(`legacy` 或 `modular`,默认 `legacy`)、`--project`、`--label`、`--record <json>` |
|
|
574
|
+
| `cutover readiness` | 无 | `--project`、`--min-shadow-runs N`、`--max-mismatch-rate N` |
|
|
575
|
+
| `authority plan` | `--readiness <json>` | `--from`、`--to` |
|
|
576
|
+
| `migration plan` | `--legacy-records <json>`、`--modular-records <json>` | 无 |
|
|
577
|
+
| `cutover apply` | `--dry-run` **和** `--plan <json>` | `--project` |
|
|
578
|
+
|
|
579
|
+
说明:
|
|
580
|
+
|
|
581
|
+
- **缺参时会先给出原因(`--legacy is required`),紧接着打印完整用法与示例**,包括「参数是内联
|
|
582
|
+
JSON」这件事。用法写到 stderr、stdout 保持为空(这是**全命令族的约定**,见上面
|
|
583
|
+
[输出流与退出码](#输出流与退出码) 一节)——所以 `mio --json` 的调用方拿到的是空输出,
|
|
584
|
+
而不是把用法文本当成 JSON 去解析。值不是合法 JSON 时同理,报 `--legacy must be valid JSON`。
|
|
585
|
+
子命令写漏了(如 `mio evolution cutover`)会明确说缺哪个:`needs 'readiness' or 'apply'`。
|
|
586
|
+
- **`cutover apply` 是 dry-run only,而且 `--dry-run` 必须显式写上。** 只给 `--plan` 会被拒绝
|
|
587
|
+
(`authority switch apply is dry-run only; pass dryRun: true ...`),只给 `--dry-run` 则报
|
|
588
|
+
`--plan is required`。它**不会真的切换权威**,返回里 `applied` 恒为 `false`。
|
|
589
|
+
- **记样本会立刻影响 `cutover readiness`。** 记录落在 `<MIO_HOME>/evolution_shadow.jsonl` 与
|
|
590
|
+
`<MIO_HOME>/evolution_dual_write.jsonl`,`readiness` 读的就是这两个文件,所以**别拿测试数据
|
|
591
|
+
往真实 `MIO_HOME` 里写**:`shadow mismatch rate` 会被污染,之后真正的切换判据跟着失真。
|
|
592
|
+
想试跑就把 `MIO_HOME` 指到临时目录。
|
|
593
|
+
- 两边深比较相等 → `matched`;不等 → `mismatch` 并给出 `diffs=N`。
|
|
594
|
+
- `--project` 省略时取当前 git 仓库名,与 `mio recall` / `mio policy check` 的默认口径一致。
|
|
595
|
+
- **`--json` 是全局 flag,必须写在子命令前面**:`mio --json evolution cutover readiness`。
|
|
596
|
+
写在后面不生效。
|
|
597
|
+
|
|
512
598
|
## Agent
|
|
513
599
|
|
|
514
600
|
本仓库里有两种不同的「agent」概念,CLI 把它们分开以免混淆:
|
|
@@ -533,6 +619,62 @@ codex (mcp) idle
|
|
|
533
619
|
|
|
534
620
|
两个子命令都委托给 `server/agent-store.js`——与 `mio.agent.list` / `mio.agent.report` / `mio.agent.register` MCP 工具相同的存储,因此终端与 MCP 服务端报告一致的遥测,不可能漂移。`list` 读取 `agents.jsonl`;`report` 叠加 trace/memory/reuse 的交叉引用。
|
|
535
621
|
|
|
622
|
+
### 评估期指标(`mio agents evaluation`)
|
|
623
|
+
|
|
624
|
+
ADR-017 用四项指标决定控制平面是继续扩展还是回滚。实现见
|
|
625
|
+
`server/evaluation-store.js`,MCP 侧为 `mio.agent.evaluation`。
|
|
626
|
+
|
|
627
|
+
| 指标 | 口径 | 数据来源 |
|
|
628
|
+
|---|---|---|
|
|
629
|
+
| 路由采纳率 | 命中且被采纳 / 命中 | `queries.jsonl` ⚠️ |
|
|
630
|
+
| 行为改变率 | confirmed 且 behaviorChanged / 全部 reuse | `experience_reuse.jsonl` |
|
|
631
|
+
| 召回质量 | 结果导向 reuse 的查询 / 有结果的查询 | `queries.jsonl` ⚠️ |
|
|
632
|
+
| 数据卫生 | 未确认 auto-claim / 全部 reuse | `experience_reuse.jsonl` |
|
|
633
|
+
|
|
634
|
+
**无样本时输出 `no data`,而不是 `0%`**:`0%` 的意思是「测过了、很差」,
|
|
635
|
+
`no data` 才是「没测」。`--json` 下对应 `null`,客户端必须区分这两种情况。
|
|
636
|
+
|
|
637
|
+
**报告会带出 Phase 0 基线作对照。** ADR 要求这些数字「对照 Phase 0 基线
|
|
638
|
+
2026-08-17」来读,但基线此前只作为散文存在于 ADR 里,报告给的是没有参照点的裸值。
|
|
639
|
+
现在 `baseline` 字段(`--json`)与输出里的 `baseline …` 行会并列给出基线值。
|
|
640
|
+
基线以 `observed / required` 形式陈述(与 `phase0.js` 的 `current/required` 渲染一致)。
|
|
641
|
+
这一条对 `verified` 尤其重要:它已从基线的 6 跌到 0,裸看只是一句「0」,
|
|
642
|
+
对照读才是「比起步时少了 6、跌破门槛」。
|
|
643
|
+
|
|
644
|
+
⚠️ **前两项指标读的是一个设计上就会变空的缓冲区**。`queries.jsonl` 不是评估日志,
|
|
645
|
+
而是 auto-claim 的关联缓冲(见 `server/query-log.js`):最多 200 条,超过
|
|
646
|
+
`expiresAt`(默认 1 小时)就被清理,`retention.js` 也把它列在 `EXPIRY_BASED_FILES`。
|
|
647
|
+
所以这两项在实际使用中**几乎总是 `no data`**——不是因为路由没人用,而是因为它的证据
|
|
648
|
+
被有意设计成不长期保留。报告里会打印一句说明,避免读者把「缓冲区为空」误读成
|
|
649
|
+
「功能没被使用」。
|
|
650
|
+
|
|
651
|
+
⚠️ **行为改变率停在 0% 通常是确认纪律问题,不是功能问题**。未确认的 auto-claim
|
|
652
|
+
既不进记忆排序也不进任务路由,是「哑重」;`mio.experience.confirm` 会把
|
|
653
|
+
`behaviorChanged` 置为 true,从而把它们转入有效证据。真实数据核查发现:确认这一步
|
|
654
|
+
在生产里从未被执行过(全部 reuse 记录的 `confirmed` 字段缺省),于是行为改变率恒为
|
|
655
|
+
0%、数据卫生恒为「93% 未确认」。因此 `mio status` 现在会直接报出全局待确认数以及
|
|
656
|
+
其中**跨 Agent** 的条数:
|
|
657
|
+
|
|
658
|
+
```
|
|
659
|
+
mio status
|
|
660
|
+
mio experience list --status pending # 看具体是哪些
|
|
661
|
+
mio experience confirm --ids a,b,c # 确认(见下方口径)
|
|
662
|
+
```
|
|
663
|
+
|
|
664
|
+
⚠️ **但「待确认 20 条」不等于「该确认 20 条」,不要批量全确认。** ADR-017 的确认口径
|
|
665
|
+
(`docs/adr-017-mio-agent-control-plane.md`,原文出现两次)是:
|
|
666
|
+
|
|
667
|
+
> 仅**跨 Agent 且真实改变行为**才 `experience.confirm`;**同 Agent / 弱关联 / 自报
|
|
668
|
+
> 一律不确认**,避免数据污染。
|
|
669
|
+
|
|
670
|
+
同 Agent 的 auto-claim(如 `codex` -> `codex`)在定义上就不可能满足这条,所以原始
|
|
671
|
+
pending 数**高估了可确认的积压**。`mio status` 因此同时报出跨 Agent 的条数,例如
|
|
672
|
+
`Pending auto-claims: 20 (12 cross-agent)`——那 8 条同 Agent 的**不应被确认**。
|
|
673
|
+
|
|
674
|
+
跨 Agent 也只是**必要条件**:确认前仍须人工核对该 target agent 的真实输出是否
|
|
675
|
+
**明确引用**了 source 记忆并据此改变方案。把不合条件的记录一并 confirm,正是这条
|
|
676
|
+
ADR 要防的数据污染,而且会让行为改变率这个指标本身失真。
|
|
677
|
+
|
|
536
678
|
### 注册(`mio agents register`)
|
|
537
679
|
|
|
538
680
|
`register` 是这一组里唯一的**写操作**,因此沿用与 `mio memory archive` 相同的约定:
|
package/adapters/hermes.js
CHANGED
|
@@ -46,6 +46,34 @@ function hermesBin() {
|
|
|
46
46
|
return null
|
|
47
47
|
}
|
|
48
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
|
+
|
|
49
77
|
function runHermes(args) {
|
|
50
78
|
const bin = hermesBin()
|
|
51
79
|
// `hermes mcp add` asks to confirm tool enablement interactively; pre-answer
|
|
@@ -55,8 +83,8 @@ function runHermes(args) {
|
|
|
55
83
|
try {
|
|
56
84
|
const isCmdShim = Boolean(bin && /\.(cmd|bat)$/i.test(bin))
|
|
57
85
|
res = bin
|
|
58
|
-
?
|
|
59
|
-
:
|
|
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 })
|
|
60
88
|
} catch (_) {
|
|
61
89
|
return null
|
|
62
90
|
}
|
|
@@ -65,8 +93,29 @@ function runHermes(args) {
|
|
|
65
93
|
}
|
|
66
94
|
|
|
67
95
|
function isInstalled() {
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|
|
70
119
|
}
|
|
71
120
|
|
|
72
121
|
function install({ node, serverScript, home, workspace, project }) {
|
|
@@ -112,7 +161,12 @@ function install({ node, serverScript, home, workspace, project }) {
|
|
|
112
161
|
|
|
113
162
|
function isHermesOnPath() {
|
|
114
163
|
try {
|
|
115
|
-
|
|
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
|
+
})
|
|
116
170
|
return res.status === 0 && /hermes/.test(res.stdout || '')
|
|
117
171
|
} catch (_) {
|
|
118
172
|
return false
|
package/adapters/host-utils.js
CHANGED
|
@@ -1,62 +1,67 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fs = require('fs')
|
|
4
|
-
const { spawnSync } = require('child_process')
|
|
5
|
-
|
|
6
|
-
// Windows only: returns the earliest creation time (epoch ms) of a running
|
|
7
|
-
// process whose ExecutablePath contains `exePathFragment`, or null when the
|
|
8
|
-
// host app is not running / detection is unavailable.
|
|
9
|
-
function earliestRunningSince(exePathFragment) {
|
|
10
|
-
if (process.platform !== 'win32') return null
|
|
11
|
-
try {
|
|
12
|
-
const escaped = String(exePathFragment).replace(/'/g, "''")
|
|
13
|
-
const script =
|
|
14
|
-
"Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like '*" +
|
|
15
|
-
escaped +
|
|
16
|
-
"*' } | ForEach-Object { [long]($_.CreationDate.ToUniversalTime() - [datetime]'1970-01-01').TotalMilliseconds }"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const { spawnSync } = require('child_process')
|
|
5
|
+
|
|
6
|
+
// Windows only: returns the earliest creation time (epoch ms) of a running
|
|
7
|
+
// process whose ExecutablePath contains `exePathFragment`, or null when the
|
|
8
|
+
// host app is not running / detection is unavailable.
|
|
9
|
+
function earliestRunningSince(exePathFragment) {
|
|
10
|
+
if (process.platform !== 'win32') return null
|
|
11
|
+
try {
|
|
12
|
+
const escaped = String(exePathFragment).replace(/'/g, "''")
|
|
13
|
+
const script =
|
|
14
|
+
"Get-CimInstance Win32_Process | Where-Object { $_.ExecutablePath -like '*" +
|
|
15
|
+
escaped +
|
|
16
|
+
"*' } | ForEach-Object { [long]($_.CreationDate.ToUniversalTime() - [datetime]'1970-01-01').TotalMilliseconds }"
|
|
17
|
+
// stdio[0] must not be a pipe. On Windows a piped stdin makes spawnSync
|
|
18
|
+
// fail with EBUSY before powershell starts: status is null and stderr is
|
|
19
|
+
// empty, so this probe returned null every time and the "host is running an
|
|
20
|
+
// older config" warning never fired. powershell reads no stdin here.
|
|
21
|
+
const res = spawnSync('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', script], {
|
|
22
|
+
encoding: 'utf8',
|
|
23
|
+
timeout: 10000,
|
|
24
|
+
windowsHide: true,
|
|
25
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
26
|
+
})
|
|
27
|
+
if (res.error || res.status !== 0) return null
|
|
28
|
+
const times = (res.stdout || '')
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter(Boolean)
|
|
32
|
+
.map(Number)
|
|
33
|
+
.filter((value) => Number.isFinite(value) && value > 0)
|
|
34
|
+
if (times.length === 0) return null
|
|
35
|
+
return Math.min(...times)
|
|
36
|
+
} catch (_) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Returns a human-readable warning when the host app is currently running from
|
|
42
|
+
// a config that predates its process start (i.e. a restart is required to load
|
|
43
|
+
// the updated Mio MCP config). Returns null when no restart is needed.
|
|
44
|
+
function restartWarning({ hostLabel, exeName, exePathFragment, configPath }) {
|
|
45
|
+
const since = earliestRunningSince(exePathFragment)
|
|
46
|
+
if (since === null) return null
|
|
47
|
+
let mtime = null
|
|
48
|
+
try {
|
|
49
|
+
mtime = fs.statSync(configPath).mtimeMs
|
|
50
|
+
} catch (_) {
|
|
51
|
+
return null
|
|
52
|
+
}
|
|
53
|
+
if (!(mtime > since)) return null
|
|
54
|
+
const started = new Date(since).toISOString().replace('T', ' ').slice(0, 19) + ' UTC'
|
|
55
|
+
return (
|
|
56
|
+
hostLabel +
|
|
57
|
+
' is running with an older Mio config (app started ' +
|
|
58
|
+
started +
|
|
59
|
+
', config updated after). Fully quit ' +
|
|
60
|
+
hostLabel +
|
|
61
|
+
' (end all ' +
|
|
62
|
+
exeName +
|
|
63
|
+
' processes) and relaunch to load the updated Mio MCP config.'
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
module.exports = { earliestRunningSince, restartWarning }
|