mio-agent-runtime 0.13.4 → 0.14.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 +44 -14
- package/adapters/codex.js +121 -121
- package/adapters/hermes.js +182 -176
- package/adapters/opencode.js +203 -202
- package/adapters/workbuddy.js +197 -197
- package/bin/mio.js +223 -15
- package/observe/observer.js +1876 -1867
- package/package.json +5 -3
- package/scripts/link-workspace.cjs +52 -0
- package/server/creativity-engine.js +279 -114
- package/server/creativity-sources.js +186 -0
- package/server/evolution-cutover.js +5 -2
- package/server/idea-generate.js +136 -0
- 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 +103 -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/README.md
CHANGED
|
@@ -55,8 +55,9 @@ mio memory migrate --ids a,b --scope global|project Move records between the p
|
|
|
55
55
|
mio policy check "<action>" Check the historical risk of an action before running it (--project; reads global MIO_HOME)
|
|
56
56
|
mio creativity status Show creativity hypothesis counts and recent top ideas (reads global MIO_HOME)
|
|
57
57
|
mio creativity list List creativity hypotheses (--status active|validated|rejected|draft, --limit N)
|
|
58
|
-
mio creativity generate Generate hypotheses from 2+ --source "name|content" (calls an LLM)
|
|
58
|
+
mio creativity generate Generate hypotheses from 2+ --source "name|content" (or --from-insights; calls an LLM)
|
|
59
59
|
mio creativity ferment Review and refine active hypotheses (calls an LLM)
|
|
60
|
+
mio idea generate --goal "..." Grounded idea pipeline: auto sources + novelty gate, persisted (calls an LLM)
|
|
60
61
|
mio insight status Insight counts: total, reported, unreported, high-value (needs @akemi-mio/insight)
|
|
61
62
|
mio insight list List insights (--unreported, --min-score N, --detector X, --limit N)
|
|
62
63
|
mio insight generate Generate insights from context (--memory "kind|content"/--summary "text"; calls an LLM)
|
|
@@ -101,7 +102,7 @@ mio --version Print the version and exit (-V)
|
|
|
101
102
|
|
|
102
103
|
## MCP 工具
|
|
103
104
|
|
|
104
|
-
MCP 服务端在 5 大域共暴露
|
|
105
|
+
MCP 服务端在 5 大域共暴露 51 个工具:
|
|
105
106
|
|
|
106
107
|
### 记忆(11)
|
|
107
108
|
`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`
|
|
@@ -114,15 +115,15 @@ MCP 服务端在 5 大域共暴露 50 个工具:
|
|
|
114
115
|
### 洞察自省(4)
|
|
115
116
|
`mio.insight.status` · `mio.insight.list` · `mio.insight.mark_reported` · `mio.insight.generate`
|
|
116
117
|
|
|
117
|
-
### 创意引擎(
|
|
118
|
-
`mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.list` · `mio.creativity.status`
|
|
118
|
+
### 创意引擎(5)
|
|
119
|
+
`mio.creativity.generate` · `mio.creativity.ferment` · `mio.creativity.list` · `mio.creativity.status` · `mio.idea.generate`
|
|
119
120
|
|
|
120
121
|
### 任务、Agent 与演化(16)
|
|
121
122
|
`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`
|
|
122
123
|
|
|
123
124
|
## 包
|
|
124
125
|
|
|
125
|
-
`mio-agent-runtime` 依赖发布在 `@akemi-mio` scope 下的 **
|
|
126
|
+
`mio-agent-runtime` 依赖发布在 `@akemi-mio` scope 下的 **10** 个包(声明于 `package.json`,均为 `0.1.0`)。它们仅在源码变更时才发布,所以普通的 CLI 发布不会重新发布这些包。
|
|
126
127
|
|
|
127
128
|
| 依赖 | 说明 |
|
|
128
129
|
|---|---|
|
|
@@ -135,6 +136,7 @@ MCP 服务端在 5 大域共暴露 50 个工具:
|
|
|
135
136
|
| `@akemi-mio/evolution-scheduler` | 演化调度器 |
|
|
136
137
|
| `@akemi-mio/observer` | ObserverService —— 多源数据采集、趋势分析、深度研究、世界模型、发酵、DAG 状态机、自演化引擎(唯一运行时依赖 `undici`,只用于走代理采集) |
|
|
137
138
|
| `@akemi-mio/insight` | InsightService —— 基于 LLM 的洞察生成、冲突/漂移/重复/停滞目标/摩擦检测器、在场服务、洞察打分(零 npm 依赖) |
|
|
139
|
+
| `@akemi-mio/creativity` | IdeaGenerator / NoveltyScorer / SourceAggregator —— `mio idea generate` 与创意引擎的生成算法(依赖 `@akemi-mio/core`) |
|
|
138
140
|
|
|
139
141
|
以下 `@akemi-mio` 包同样位于本 monorepo 中并独立发布,但**不是**本 CLI 的依赖——其源码从不 import 它们,因此未声明在 `package.json` 里:
|
|
140
142
|
|
|
@@ -146,9 +148,9 @@ MCP 服务端在 5 大域共暴露 50 个工具:
|
|
|
146
148
|
|
|
147
149
|
它们可直接被其它 Node 项目消费。`mio-agent-runtime` 自身是 CLI + MCP server 包。
|
|
148
150
|
|
|
149
|
-
###
|
|
151
|
+
### 曾经不发布的两个包(现已发布)
|
|
150
152
|
|
|
151
|
-
`@akemi-mio/core` 与 `@akemi-mio/creativity`
|
|
153
|
+
`@akemi-mio/core` 与 `@akemi-mio/creativity` 自 `mio-agent-runtime@0.14.0` 起发布到 npm(`0.1.0`)。`@akemi-mio/creativity` 是本 CLI 的直接依赖,`@akemi-mio/core` 经由它传递引入;其它 Node 项目可直接安装这两个包。
|
|
152
154
|
|
|
153
155
|
## 宿主说明
|
|
154
156
|
|
|
@@ -359,7 +361,7 @@ Historically risky: ...
|
|
|
359
361
|
|
|
360
362
|
说明:
|
|
361
363
|
|
|
362
|
-
- **它读取全局 `MIO_HOME`,而非每项目目录。** MCP 服务端把数据目录解析为 `MIO_DATA_DIR || cwd/.mio-intelligence`,因此从任意目录发起的 MCP 调用看到的是空存储;CLI
|
|
364
|
+
- **它读取全局 `MIO_HOME`,而非每项目目录。** MCP 服务端把数据目录解析为 `MIO_DATA_DIR || cwd/.mio-intelligence`,因此从任意目录发起的 MCP 调用看到的是空存储;CLI 刻意指向累积的全局日志。`mio mcp` 是例外:它先把 `MIO_DATA_DIR` 钉到 `MIO_HOME` 再启动服务端,于是 stdio 入口与终端读写同一份数据。注意:各桌面宿主适配器(codex、opencode、workbuddy、hermes、claude-code)在启动 MCP 服务端时会**显式传入**各自的 `MIO_DATA_DIR`(指向宿主的配置目录),因此它们**不共享** CLI 的 `MIO_HOME`;只有 `mio mcp`(直接 stdio 连接)才与终端读写同一份数据。用 `--project` 把 trace 集合限定到某个项目。
|
|
363
365
|
- **风险即失败占比**,`>= 0.4` 为高,`>= 0.2` 为中。`retry` 与 `aborted` 与 `failure`、`error` 一并计为失败。无匹配历史报告 `UNKNOWN`,与 `LOW`(干净记录)不同。
|
|
364
366
|
- **匹配可能被通用 token 带偏。** 匹配是对动作 token 的宽松 OR,所以像 `task` 这样的动作几乎匹配每条 trace,因为关键词 `task` 几乎出现在所有 payload 中。当动作中*每个* token 都至少出现在半数候选 trace 里时,CLI 会打印警告,而不是让等级自说自话:
|
|
365
367
|
|
|
@@ -386,7 +388,7 @@ mio creativity list --json
|
|
|
386
388
|
```text
|
|
387
389
|
Creativity engine:
|
|
388
390
|
hypotheses: 5 combos: 3 experiments: 1
|
|
389
|
-
active: 2 validated: 1 rejected: 1
|
|
391
|
+
active: 2 validated: 1 rejected: 1 draft: 1
|
|
390
392
|
|
|
391
393
|
Recent top ideas:
|
|
392
394
|
- Plugin architecture (novelty=80 feasibility=70 impact=90 score=240)
|
|
@@ -396,7 +398,10 @@ Recent top ideas:
|
|
|
396
398
|
说明:
|
|
397
399
|
|
|
398
400
|
- **它委托给与 MCP 服务端相同的 `CreativityEngine`**,因此终端的 `mio creativity list` 看到的是与 `mio.creativity.list` 工具完全一致的假设。存储位于 `<MIO_HOME>/creativity/creativity-hypotheses.jsonl`;CLI 指向全局 `MIO_HOME`,与 `mio policy check` 一致。
|
|
401
|
+
- **`list` 默认只返回最新 20 条**,与 `mio.creativity.list` 的 schema 承诺一致;`--limit 0`(或 MCP 的 `limit: 0`)才是「全部」。存储很大时默认全量返回会把工具结果撑爆。
|
|
402
|
+
- **`status` 计入 `draft`。** `draft` 是发酵中的真实状态(`ferment` 会同时处理 `draft` 与 `active`),此前漏计导致 `status` 与 `list --status draft` 对不上。
|
|
399
403
|
- **空存储是正常状态。** 引擎运行之前,`status` 报告全零计数,`list` 显示 "No hypotheses match." 两者都是有效输出,不是错误。
|
|
404
|
+
- **缓存会在文件变化时失效。** 三个 JSONL 的读取按 `(size, mtime)` 校验,因此另一个进程写入后长驻的 MCP 服务端无需重启就能看到——与 `server/insight-store.js` 同一套守卫。
|
|
400
405
|
|
|
401
406
|
### 生成与发酵(`generate` / `ferment`)
|
|
402
407
|
|
|
@@ -405,19 +410,44 @@ Recent top ideas:
|
|
|
405
410
|
|
|
406
411
|
```bash
|
|
407
412
|
mio creativity generate --source "auth|token rotation" --source "cache|write-through"
|
|
413
|
+
mio creativity generate --from-insights --source "auth|token rotation"
|
|
408
414
|
mio creativity ferment --limit 3
|
|
409
415
|
```
|
|
410
416
|
|
|
411
417
|
- `--source "名称|内容"` **至少两个**:引擎是把概念两两配对来产生新假设的,
|
|
412
418
|
一个来源在构造上就不可能产出组合(此时**不会**调用 LLM,直接返回
|
|
413
|
-
`need at least 2 sources
|
|
414
|
-
-
|
|
419
|
+
`need at least 2 sources`)。显式来源不足 2 个时,本地自动来源(memory / traces / 已存假设;MCP 侧还含 insight 与 observer 趋势)会先把列表补齐到 2——显式 `--source` 永远排在前面且不被替换;补齐后仍不足 2 个才报上面的错。
|
|
420
|
+
- **`--from-insights`(MCP 侧为 `fromInsights: true`)用已存洞察补足概念来源**:
|
|
421
|
+
读取 `mio insight list` 能看到的同一份存储,按分数取前 10 条映射成
|
|
422
|
+
`{ name, content, type: 'insight' }`,再与显式的 `--source` 合并。它只在
|
|
423
|
+
`@akemi-mio/insight` 已安装且至少存有一条洞察时有意义,否则明确报错而不是
|
|
424
|
+
静默当作「没有来源」。因为来源可由它提供,`mio.creativity.generate` 的
|
|
425
|
+
`sources` 不再是 schema 必填项。
|
|
426
|
+
- `ferment` 会复核 `active` / `draft` 假设并更新分数;`verdict=promote` 且总分 > 200 时
|
|
415
427
|
升为 `validated`,`verdict=reject` 则标记为 `rejected`。
|
|
428
|
+
- **失败的 LLM 调用会出现在结果里,不再被吞掉。** 返回值带 `errors`
|
|
429
|
+
(`generate` 是 `pair` + 原因,`ferment` 是 `id` + 原因),CLI 也会在正文后
|
|
430
|
+
打印 `N pair(s) failed:`。此前 `catch {}` 让「端点挂了」与「这一对确实没有
|
|
431
|
+
新意」长得一模一样——`ideas: []` / `Nothing fermented` 两种输出都无法区分。
|
|
432
|
+
|
|
433
|
+
### 目标驱动的 idea 流水线(`mio idea generate`)
|
|
434
|
+
|
|
435
|
+
一个 goal 直接产出可实验的假设(MCP 面为 `mio.idea.generate`),来源全程本地优先组装:
|
|
436
|
+
|
|
437
|
+
```bash
|
|
438
|
+
mio idea generate --goal "降低 MCP 调用延迟" --context "近三天 p95 上升" --constraint "不引入新依赖"
|
|
439
|
+
mio idea generate --goal "扩大命令覆盖" --json
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
- **来源顺序**:goal → context → constraints → 记忆 grounding(取前 3 条,`scope: all`)→ 本地自动来源(memory / traces / 已存假设,MCP 侧还含 insight 与 observer 趋势),按 `origin` 去重;凑不足 2 个时返回 `reason` 文案而不是报错。
|
|
443
|
+
- **每条 idea 都持久化**到 `<MIO_HOME>/creativity/creativity-hypotheses.jsonl`(`draft`,带 `provenance`:strategy / technique / relatedMemoryIds / 命中来源 / generatedAt),**绝不写入 Mio memory**。
|
|
444
|
+
- **novelty 门禁**:与已 `rejected` 假设近似重复的候选落盘为 `rejected` 并带 `rejectionReason`;与近期假设相似的只降分。拒绝的假设同样入库,供后续去重。
|
|
445
|
+
- `--num` 钳制在 1..3(默认 3);`--json` 返回完整结构 `{ ideas, generatedAt, groundedWith, persistedIds }`。
|
|
416
446
|
|
|
417
447
|
#### LLM 配置
|
|
418
448
|
|
|
419
|
-
只有
|
|
420
|
-
`mio insight generate`、`mio observer ferment`。它们共用
|
|
449
|
+
只有 5 条命令会调用大模型:`mio creativity generate` / `ferment`、
|
|
450
|
+
`mio idea generate`、`mio insight generate`、`mio observer ferment`。它们共用
|
|
421
451
|
`server/llm-client.js` 这一份实现,配置也共用同一套解析顺序:
|
|
422
452
|
|
|
423
453
|
```
|
|
@@ -491,7 +521,7 @@ mio observer trends --base-dir /path/to/.local/observer
|
|
|
491
521
|
mio observer ferment --session morning # morning|afternoon|night
|
|
492
522
|
```
|
|
493
523
|
|
|
494
|
-
两者都依赖可选包 `@akemi-mio/observer`(未安装时提示 `not installed`,不是空结果)。`collect` 会**联网**抓取,`ferment` 需要 LLM;它们**不做预览**,因为预览意味着把数据抓两遍。单个源失败不会中断整次运行——该源会以 `errors: ...` 出现在输出里,其余源照常统计。
|
|
524
|
+
两者都依赖可选包 `@akemi-mio/observer`(未安装时提示 `not installed`,不是空结果)。`collect` 会**联网**抓取,`ferment` 需要 LLM;它们**不做预览**,因为预览意味着把数据抓两遍。单个源失败不会中断整次运行——该源会以 `errors: ...` 出现在输出里,其余源照常统计。`collect` 会**联网**抓取,`ferment` 需要 LLM;它们**不做预览**,因为预览意味着把数据抓两遍。单个源失败不会中断整次运行——该源会以 `errors: ...` 出现在输出里,其余源照常统计。
|
|
495
525
|
- **`mio observer pipeline` 是研究 DAG 的入口**(`collect → trend → tension → research → multi-brain → compose → world model → publish → self-evolve`)。此前 `runPipeline` / `tickPipeline` / `forcePipeline` 在 mio-agent-runtime 里**没有任何调用方**,所以 trends / research / insights 默认永远是空的,只有手动实例化服务才能跑通。它同样依赖 `@akemi-mio/observer`,会**联网并调用多次 LLM**,因此和 `subscribe` 一样**默认只预览**(读今天的 DAG 状态与解析后的 LLM 端点,不构造服务、不建目录),`--run` 才真正执行:
|
|
496
526
|
|
|
497
527
|
```bash
|
package/adapters/codex.js
CHANGED
|
@@ -1,122 +1,122 @@
|
|
|
1
|
-
'use strict'
|
|
2
|
-
|
|
3
|
-
const fs = require('fs')
|
|
4
|
-
const path = require('path')
|
|
5
|
-
const os = require('os')
|
|
6
|
-
const { restartWarning } = require('./host-utils')
|
|
7
|
-
|
|
8
|
-
const SERVER_SECTION = '[mcp_servers.mio-intelligence]'
|
|
9
|
-
const ENV_SECTION = '[mcp_servers.mio-intelligence.env]'
|
|
10
|
-
|
|
11
|
-
function configPath() {
|
|
12
|
-
return path.join(os.homedir(), '.codex', 'config.toml')
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function isInstalled() {
|
|
16
|
-
const target = configPath()
|
|
17
|
-
if (!fs.existsSync(target)) return false
|
|
18
|
-
return fs.readFileSync(target, 'utf8').includes(SERVER_SECTION)
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Legacy installs pointed args at the worktree copy of the server
|
|
22
|
-
// (server/mio-intelligence-mcp/index.js) instead of this package's copy
|
|
23
|
-
// (server/mio-intelligence-mcp/index.js). Detect staleness by comparing the
|
|
24
|
-
// configured args path with the server path this package ships.
|
|
25
|
-
function configNeedsRepair(serverScript) {
|
|
26
|
-
const target = configPath()
|
|
27
|
-
if (!fs.existsSync(target)) return false
|
|
28
|
-
try {
|
|
29
|
-
const src = fs.readFileSync(target, 'utf8')
|
|
30
|
-
if (!src.includes(SERVER_SECTION)) return false
|
|
31
|
-
const needle = serverScript.replace(/\\/g, '/').toLowerCase()
|
|
32
|
-
return !src.replace(/\\/g, '/').toLowerCase().includes(needle)
|
|
33
|
-
} catch (_) {
|
|
34
|
-
return false
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function installMessage(installed, restartHint) {
|
|
39
|
-
let message = installed
|
|
40
|
-
? 'Codex Mio MCP repaired (server path updated). Restart Codex to load it.'
|
|
41
|
-
: 'Codex Mio MCP installed. Restart Codex to load it.'
|
|
42
|
-
if (restartHint) message += ' ' + restartHint
|
|
43
|
-
return message
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function restartWarningMessage() {
|
|
47
|
-
return restartWarning({
|
|
48
|
-
hostLabel: 'Codex',
|
|
49
|
-
exeName: 'codex.exe',
|
|
50
|
-
exePathFragment: 'OpenAI.Codex',
|
|
51
|
-
configPath: configPath(),
|
|
52
|
-
})
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function stripMioSection(toml) {
|
|
56
|
-
const lines = toml.replace(/\r\n/g, '\n').split('\n')
|
|
57
|
-
const out = []
|
|
58
|
-
let skipping = false
|
|
59
|
-
for (const line of lines) {
|
|
60
|
-
const trimmed = line.trim()
|
|
61
|
-
if (trimmed === SERVER_SECTION || trimmed === ENV_SECTION) {
|
|
62
|
-
skipping = true
|
|
63
|
-
continue
|
|
64
|
-
}
|
|
65
|
-
if (skipping && /^\[.*\]$/.test(trimmed)) {
|
|
66
|
-
skipping = false
|
|
67
|
-
}
|
|
68
|
-
if (!skipping) out.push(line)
|
|
69
|
-
}
|
|
70
|
-
return out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function install({ node, serverScript, home, workspace }) {
|
|
74
|
-
const target = configPath()
|
|
75
|
-
const backup = `${target}.bak-mio-${Date.now()}`
|
|
76
|
-
const installed = isInstalled()
|
|
77
|
-
if (installed && !configNeedsRepair(serverScript)) {
|
|
78
|
-
return {
|
|
79
|
-
changed: false,
|
|
80
|
-
configPath: target,
|
|
81
|
-
message: 'Codex Mio MCP already installed.',
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
if (fs.existsSync(target)) {
|
|
86
|
-
fs.copyFileSync(target, backup)
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const context = JSON.stringify({
|
|
90
|
-
agentId: 'codex',
|
|
91
|
-
workspace: workspace || process.cwd(),
|
|
92
|
-
sessionId: 'codex-session',
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
96
|
-
let toml = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
97
|
-
toml = stripMioSection(toml)
|
|
98
|
-
toml = toml.replace(/\n{3,}/g, '\n\n')
|
|
99
|
-
if (!toml.endsWith('\n')) toml += '\n'
|
|
100
|
-
const section = [
|
|
101
|
-
'',
|
|
102
|
-
SERVER_SECTION,
|
|
103
|
-
`command = '${node}'`,
|
|
104
|
-
`args = ['${serverScript}']`,
|
|
105
|
-
'startup_timeout_sec = 30',
|
|
106
|
-
'',
|
|
107
|
-
ENV_SECTION,
|
|
108
|
-
`MIO_DATA_DIR = '${home}'`,
|
|
109
|
-
`MIO_CONTEXT = '${context}'`,
|
|
110
|
-
'',
|
|
111
|
-
].join('\n')
|
|
112
|
-
fs.writeFileSync(target, `${toml}${section}\n`, 'utf8')
|
|
113
|
-
|
|
114
|
-
return {
|
|
115
|
-
changed: true,
|
|
116
|
-
configPath: target,
|
|
117
|
-
backup,
|
|
118
|
-
message: installMessage(installed, restartWarningMessage()),
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const fs = require('fs')
|
|
4
|
+
const path = require('path')
|
|
5
|
+
const os = require('os')
|
|
6
|
+
const { restartWarning } = require('./host-utils')
|
|
7
|
+
|
|
8
|
+
const SERVER_SECTION = '[mcp_servers.mio-intelligence]'
|
|
9
|
+
const ENV_SECTION = '[mcp_servers.mio-intelligence.env]'
|
|
10
|
+
|
|
11
|
+
function configPath() {
|
|
12
|
+
return path.join(os.homedir(), '.codex', 'config.toml')
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isInstalled() {
|
|
16
|
+
const target = configPath()
|
|
17
|
+
if (!fs.existsSync(target)) return false
|
|
18
|
+
return fs.readFileSync(target, 'utf8').includes(SERVER_SECTION)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Legacy installs pointed args at the worktree copy of the server
|
|
22
|
+
// (server/mio-intelligence-mcp/index.js) instead of this package's copy
|
|
23
|
+
// (server/mio-intelligence-mcp/index.js). Detect staleness by comparing the
|
|
24
|
+
// configured args path with the server path this package ships.
|
|
25
|
+
function configNeedsRepair(serverScript) {
|
|
26
|
+
const target = configPath()
|
|
27
|
+
if (!fs.existsSync(target)) return false
|
|
28
|
+
try {
|
|
29
|
+
const src = fs.readFileSync(target, 'utf8')
|
|
30
|
+
if (!src.includes(SERVER_SECTION)) return false
|
|
31
|
+
const needle = serverScript.replace(/\\/g, '/').toLowerCase()
|
|
32
|
+
return !src.replace(/\\/g, '/').toLowerCase().includes(needle)
|
|
33
|
+
} catch (_) {
|
|
34
|
+
return false
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function installMessage(installed, restartHint) {
|
|
39
|
+
let message = installed
|
|
40
|
+
? 'Codex Mio MCP repaired (server path updated). Restart Codex to load it.'
|
|
41
|
+
: 'Codex Mio MCP installed. Restart Codex to load it.'
|
|
42
|
+
if (restartHint) message += ' ' + restartHint
|
|
43
|
+
return message
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function restartWarningMessage() {
|
|
47
|
+
return restartWarning({
|
|
48
|
+
hostLabel: 'Codex',
|
|
49
|
+
exeName: 'codex.exe',
|
|
50
|
+
exePathFragment: 'OpenAI.Codex',
|
|
51
|
+
configPath: configPath(),
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function stripMioSection(toml) {
|
|
56
|
+
const lines = toml.replace(/\r\n/g, '\n').split('\n')
|
|
57
|
+
const out = []
|
|
58
|
+
let skipping = false
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
const trimmed = line.trim()
|
|
61
|
+
if (trimmed === SERVER_SECTION || trimmed === ENV_SECTION) {
|
|
62
|
+
skipping = true
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
if (skipping && /^\[.*\]$/.test(trimmed)) {
|
|
66
|
+
skipping = false
|
|
67
|
+
}
|
|
68
|
+
if (!skipping) out.push(line)
|
|
69
|
+
}
|
|
70
|
+
return out.join('\n').replace(/\n{3,}/g, '\n\n').trimEnd() + '\n'
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function install({ node, serverScript, home, workspace }) {
|
|
74
|
+
const target = configPath()
|
|
75
|
+
const backup = `${target}.bak-mio-${Date.now()}`
|
|
76
|
+
const installed = isInstalled()
|
|
77
|
+
if (installed && !configNeedsRepair(serverScript)) {
|
|
78
|
+
return {
|
|
79
|
+
changed: false,
|
|
80
|
+
configPath: target,
|
|
81
|
+
message: 'Codex Mio MCP already installed.',
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (fs.existsSync(target)) {
|
|
86
|
+
fs.copyFileSync(target, backup)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const context = JSON.stringify({
|
|
90
|
+
agentId: 'codex',
|
|
91
|
+
workspace: workspace || process.cwd(),
|
|
92
|
+
sessionId: 'codex-session',
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
96
|
+
let toml = fs.existsSync(target) ? fs.readFileSync(target, 'utf8') : ''
|
|
97
|
+
toml = stripMioSection(toml)
|
|
98
|
+
toml = toml.replace(/\n{3,}/g, '\n\n')
|
|
99
|
+
if (!toml.endsWith('\n')) toml += '\n'
|
|
100
|
+
const section = [
|
|
101
|
+
'',
|
|
102
|
+
SERVER_SECTION,
|
|
103
|
+
`command = '${node}'`,
|
|
104
|
+
`args = ['${serverScript}']`,
|
|
105
|
+
'startup_timeout_sec = 30',
|
|
106
|
+
'',
|
|
107
|
+
ENV_SECTION,
|
|
108
|
+
`MIO_DATA_DIR = '${home}'`,
|
|
109
|
+
`MIO_CONTEXT = '${context}'`,
|
|
110
|
+
'',
|
|
111
|
+
].join('\n')
|
|
112
|
+
fs.writeFileSync(target, `${toml}${section}\n`, 'utf8')
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
changed: true,
|
|
116
|
+
configPath: target,
|
|
117
|
+
backup,
|
|
118
|
+
message: installMessage(installed, restartWarningMessage()),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
122
|
module.exports = { configPath, isInstalled, install, restartWarningMessage }
|