exomind 0.4.2 → 0.5.1

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 CHANGED
@@ -56,13 +56,18 @@ exomind feedback "entities/Redis.md" positive
56
56
 
57
57
  > 把 API Key 当密码对待:不提交进仓库、不贴进聊天。脚本化登录用 `exomind login --api-key ...`(注意别让 key 进 shell 历史或进程列表);交互式 `exomind login` 的 prompt 读取不走 shell 历史。
58
58
 
59
- ## 接入 Claude Code(一条命令,默认全装)
59
+ ## 接入 Claude Code / Codex(一条命令,默认全装)
60
60
 
61
61
  ```bash
62
- exomind install # 装 skill + hook + MCP,全部自动(幂等+备份)
63
- # 不想要某项: exomind install --no-hook 或 --no-mcp
62
+ exomind install # 装 skill(Claude+Codex)+ hook(Claude)+ MCP(各宿主),全部自动(幂等+备份)
63
+ # 只装某个宿主: exomind install --host codex (claude | codex | opencode)
64
+ # 跳过某项: --no-skill / --no-hook / --no-mcp
64
65
  ```
65
66
 
67
+ **Codex**(skill + MCP,无 hook):装到 `$CODEX_HOME/skills/exomind/`(缺省 `~/.codex/`),MCP 写入 `$CODEX_HOME/config.toml`。**Codex 没有 Claude 的 UserPromptSubmit hook**,靠 skill 的触发词(jdit/存档/查询)驱动;MCP `ingest` 只收文本,目录导入走 `exomind ingest --dir`。装完**重启 Codex**,skill 列表应出现 `exomind`。
68
+
69
+ 排查各宿主装没装上:`exomind doctor`(或 `exomind --json doctor`)。
70
+
66
71
  一行完成三层,免手改任何 JSON:
67
72
  - **① MCP 工具**(能力):写 `~/.claude.json` 的 `mcpServers.exomind` → Agent 拿到 `mcp__exomind__*` 确定性工具。
68
73
  - **② skill**(指导):拷到 `~/.claude/skills/exomind/`。
@@ -77,13 +82,14 @@ exomind install # 装 skill + hook + MCP,全部自动(幂等+备份)
77
82
 
78
83
  **升级**:`npm i -g exomind@latest && exomind install`(幂等,刷新 skill/hook/mcp;`~/.exomind/` 的 config 与 manifest 保留)。注意:`npm i -g` 只换二进制(CLI+MCP 自动用新),**skill 是拷贝的,需 `exomind install` 才刷新**。
79
84
 
80
- ## 关于 MCP 工具层(Claude Code + OpenCode 都已默认装)
85
+ ## 关于 MCP 工具层(Claude Code / OpenCode / Codex 都已默认装)
81
86
 
82
- `exomind install` 一次写**两个宿主**的 MCP 配置(都幂等+备份,互不干扰,各读各的):
87
+ `exomind install` 一次写**三个宿主**的 MCP 配置(都幂等+备份,互不干扰,各读各的):
83
88
  - **Claude Code**:`~/.claude.json` → `mcpServers.exomind`
84
89
  - **OpenCode**:`~/.config/opencode/opencode.json` → `mcp.exomind`
90
+ - **Codex**:`$CODEX_HOME/config.toml` → `[mcp_servers.exomind]`
85
91
 
86
- 只关 MCP:`exomind install --no-mcp`(两个宿主都不写)。手写/其它宿主(如 Cursor)参考 [docs/mcp.md](./docs/mcp.md)。
92
+ 只关 MCP:`exomind install --no-mcp`(三个宿主都不写)。手写/其它宿主(如 Cursor)参考 [docs/mcp.md](./docs/mcp.md)。
87
93
 
88
94
  `exomind mcp` 是本地 stdio MCP server,把 ingest/query/search/entity/relations/stats 暴露为 typed tool;复用同一份凭证,三平台都能跑(本地 stdio,不涉及远程 SSE 的 Windows 坑)。
89
95
 
@@ -100,7 +106,9 @@ exomind install # 装 skill + hook + MCP,全部自动(幂等+备份)
100
106
  | `review` / `review mark` | FSRS-5 复习队列与评分 |
101
107
  | `synthesize` / `topics` / `gaps` / `daily` | 主题综合、选题、缺口、每日摘要 |
102
108
  | `feedback` | 质量反馈(影响搜索排名) |
103
- | `hook` / `install` | UserPromptSubmit 钩子、安装 skill+hook |
109
+ | `install` | skill(Claude+Codex)+ hook(Claude)+ MCP;`--host`/`--no-skill`/`--no-hook`/`--no-mcp` |
110
+ | `doctor` | 诊断各宿主 skill/hook/MCP/鉴权 状态(`--json`) |
111
+ | `hook` | UserPromptSubmit 钩子(由 install 配置) |
104
112
 
105
113
  完整命令参考与排错见 **[CLI 命令指南](./docs/cli-guide.md)**。
106
114
 
@@ -143,7 +151,8 @@ src/
143
151
  io.ts stdin / 文件读取
144
152
  hook.ts UserPromptSubmit 钩子(替代 bash hook)
145
153
  commands/ 每个命令一个文件
146
- skill/SKILL.md Claude Code skill 源(install 时拷贝)
154
+ skill/claude/SKILL.md Claude Code skill 源(install 时拷贝)
155
+ skill/codex/SKILL.md Codex skill 源(install 时拷到 $CODEX_HOME/skills/exomind/)
147
156
  test/ node:test 单元 + 协议级 e2e
148
157
  ```
149
158
 
package/dist/cli.js CHANGED
@@ -965,8 +965,8 @@ var require_command = __commonJS({
965
965
  "use strict";
966
966
  var EventEmitter = require("events").EventEmitter;
967
967
  var childProcess = require("child_process");
968
- var path7 = require("path");
969
- var fs7 = require("fs");
968
+ var path8 = require("path");
969
+ var fs8 = require("fs");
970
970
  var process2 = require("process");
971
971
  var { Argument: Argument2, humanReadableArgName } = require_argument();
972
972
  var { CommanderError: CommanderError2 } = require_error();
@@ -1898,11 +1898,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1898
1898
  let launchWithNode = false;
1899
1899
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1900
1900
  function findFile(baseDir, baseName) {
1901
- const localBin = path7.resolve(baseDir, baseName);
1902
- if (fs7.existsSync(localBin)) return localBin;
1903
- if (sourceExt.includes(path7.extname(baseName))) return void 0;
1901
+ const localBin = path8.resolve(baseDir, baseName);
1902
+ if (fs8.existsSync(localBin)) return localBin;
1903
+ if (sourceExt.includes(path8.extname(baseName))) return void 0;
1904
1904
  const foundExt = sourceExt.find(
1905
- (ext) => fs7.existsSync(`${localBin}${ext}`)
1905
+ (ext) => fs8.existsSync(`${localBin}${ext}`)
1906
1906
  );
1907
1907
  if (foundExt) return `${localBin}${foundExt}`;
1908
1908
  return void 0;
@@ -1914,21 +1914,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1914
1914
  if (this._scriptPath) {
1915
1915
  let resolvedScriptPath;
1916
1916
  try {
1917
- resolvedScriptPath = fs7.realpathSync(this._scriptPath);
1917
+ resolvedScriptPath = fs8.realpathSync(this._scriptPath);
1918
1918
  } catch (err) {
1919
1919
  resolvedScriptPath = this._scriptPath;
1920
1920
  }
1921
- executableDir = path7.resolve(
1922
- path7.dirname(resolvedScriptPath),
1921
+ executableDir = path8.resolve(
1922
+ path8.dirname(resolvedScriptPath),
1923
1923
  executableDir
1924
1924
  );
1925
1925
  }
1926
1926
  if (executableDir) {
1927
1927
  let localFile = findFile(executableDir, executableFile);
1928
1928
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1929
- const legacyName = path7.basename(
1929
+ const legacyName = path8.basename(
1930
1930
  this._scriptPath,
1931
- path7.extname(this._scriptPath)
1931
+ path8.extname(this._scriptPath)
1932
1932
  );
1933
1933
  if (legacyName !== this._name) {
1934
1934
  localFile = findFile(
@@ -1939,7 +1939,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1939
1939
  }
1940
1940
  executableFile = localFile || executableFile;
1941
1941
  }
1942
- launchWithNode = sourceExt.includes(path7.extname(executableFile));
1942
+ launchWithNode = sourceExt.includes(path8.extname(executableFile));
1943
1943
  let proc;
1944
1944
  if (process2.platform !== "win32") {
1945
1945
  if (launchWithNode) {
@@ -2779,7 +2779,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2779
2779
  * @return {Command}
2780
2780
  */
2781
2781
  nameFromFilename(filename) {
2782
- this._name = path7.basename(filename, path7.extname(filename));
2782
+ this._name = path8.basename(filename, path8.extname(filename));
2783
2783
  return this;
2784
2784
  }
2785
2785
  /**
@@ -2793,9 +2793,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2793
2793
  * @param {string} [path]
2794
2794
  * @return {(string|null|Command)}
2795
2795
  */
2796
- executableDir(path8) {
2797
- if (path8 === void 0) return this._executableDir;
2798
- this._executableDir = path8;
2796
+ executableDir(path9) {
2797
+ if (path9 === void 0) return this._executableDir;
2798
+ this._executableDir = path9;
2799
2799
  return this;
2800
2800
  }
2801
2801
  /**
@@ -3119,7 +3119,7 @@ var {
3119
3119
  // package.json
3120
3120
  var package_default = {
3121
3121
  name: "exomind",
3122
- version: "0.4.2",
3122
+ version: "0.5.1",
3123
3123
  description: "ExoMind \u8DE8\u5E73\u53F0\u547D\u4EE4\u884C\u5BA2\u6237\u7AEF \u2014 \u901A\u8FC7 REST \u4E0E ExoMind \u77E5\u8BC6\u5E93\u4EA4\u4E92(ingest/query/search/review),\u66FF\u4EE3 Windows \u4E0D\u53EF\u7528\u7684 MCP \u5BA2\u6237\u7AEF\u3002",
3124
3124
  bin: {
3125
3125
  exomind: "dist/cli.js"
@@ -3487,7 +3487,7 @@ function hasTechTerm(msg) {
3487
3487
  }
3488
3488
  function isSecretWord(msg) {
3489
3489
  const m = msg.trim();
3490
- return /^(存档|jdit)[\s!!.。??、]*$/i.test(m);
3490
+ return /^(存档|jdit)[\s!.,;:!?。?、~…]*$/i.test(m);
3491
3491
  }
3492
3492
  function matchesExperience(msg) {
3493
3493
  if (msg.length < 50) return false;
@@ -3615,9 +3615,16 @@ async function runHook(client) {
3615
3615
  if (msg.length < 8) return;
3616
3616
  if (CONFIRM_WORDS.has(msg.toLowerCase())) return;
3617
3617
  }
3618
- if (isSecret && msg.length < 20 && now - dedup.lastArchive > COOLDOWN_MS) {
3619
- outputs.push(ARCHIVE_INSTRUCTION);
3620
- dedup.lastArchive = now;
3618
+ if (isSecret && msg.length < 20) {
3619
+ if (now - dedup.lastArchive > COOLDOWN_MS) {
3620
+ outputs.push(ARCHIVE_INSTRUCTION);
3621
+ dedup.lastArchive = now;
3622
+ } else {
3623
+ const remain = Math.ceil((COOLDOWN_MS - (now - dedup.lastArchive)) / 1e3);
3624
+ outputs.push(
3625
+ `[ExoMind] \u5B58\u6863\u51B7\u5374\u4E2D,${remain} \u79D2\u540E\u53EF\u518D\u6B21\u89E6\u53D1(30 \u5206\u949F\u9632\u91CD\u590D\u6444\u5165)\u3002\u6697\u53F7\u5DF2\u8BC6\u522B\u3002\u5982\u9700\u7ACB\u5373\u5B58\u6863,\u76F4\u63A5\u8FD0\u884C \`exomind ingest\`\u3002`
3626
+ );
3627
+ }
3621
3628
  } else if (!isSecret) {
3622
3629
  if (matchesExperience(msg)) outputs.push(EXPERIENCE_INSTRUCTION);
3623
3630
  else if (matchesResearch(msg)) outputs.push(RESEARCH_INSTRUCTION);
@@ -4502,7 +4509,8 @@ var path6 = __toESM(require("path"));
4502
4509
  var os2 = __toESM(require("os"));
4503
4510
  var import_node_child_process = require("child_process");
4504
4511
  var PKG_ROOT = path6.resolve(__dirname, "..");
4505
- var SKILL_SRC = path6.join(PKG_ROOT, "skill", "SKILL.md");
4512
+ var CLAUDE_SKILL_SRC = path6.join(PKG_ROOT, "skill", "claude", "SKILL.md");
4513
+ var CODEX_SKILL_SRC = path6.join(PKG_ROOT, "skill", "codex", "SKILL.md");
4506
4514
  function readJson2(file) {
4507
4515
  try {
4508
4516
  return JSON.parse(fs6.readFileSync(file, "utf-8"));
@@ -4535,19 +4543,23 @@ function pruneBackups(file, keep) {
4535
4543
  } catch {
4536
4544
  }
4537
4545
  }
4538
- function purgeMcpExomind(file) {
4539
- const d = readJson2(file);
4540
- if (!d) return false;
4541
- const grp = d.mcpServers;
4542
- if (!grp || !grp.exomind) return false;
4543
- delete grp.exomind;
4544
- if (Object.keys(grp).length === 0) delete d.mcpServers;
4545
- backup(file);
4546
- fs6.writeFileSync(file, JSON.stringify(d, null, 2) + "\n");
4547
- return true;
4546
+ function resolveCodexHome(env = process.env, homeDir = os2.homedir()) {
4547
+ const configured = (env.CODEX_HOME ?? "").trim();
4548
+ return configured ? path6.resolve(configured) : path6.join(homeDir, ".codex");
4549
+ }
4550
+ function looksLikeValidToml(text) {
4551
+ if (!text.trim()) return true;
4552
+ if (/\x00/.test(text)) return false;
4553
+ let brackets = 0;
4554
+ for (const ch of text) {
4555
+ if (ch === "[") brackets++;
4556
+ else if (ch === "]") brackets--;
4557
+ if (brackets < 0) return false;
4558
+ }
4559
+ return brackets === 0;
4548
4560
  }
4549
- function configureCodexMcp() {
4550
- const codexConfig = path6.join(os2.homedir(), ".codex", "config.toml");
4561
+ function configureCodexMcp(codexHome) {
4562
+ const codexConfig = path6.join(codexHome, "config.toml");
4551
4563
  try {
4552
4564
  fs6.mkdirSync(path6.dirname(codexConfig), { recursive: true });
4553
4565
  } catch {
@@ -4558,8 +4570,16 @@ function configureCodexMcp() {
4558
4570
  } catch {
4559
4571
  existing = "";
4560
4572
  }
4573
+ if (existing && !looksLikeValidToml(existing)) {
4574
+ return {
4575
+ ok: false,
4576
+ written: false,
4577
+ file: codexConfig,
4578
+ reason: `config.toml \u7591\u4F3C\u635F\u574F(\u62EC\u53F7\u4E0D\u914D\u5E73/\u542B\u4E8C\u8FDB\u5236),\u5DF2\u4FDD\u7559\u539F\u6587\u4EF6\u672A\u6539: ${codexConfig}`
4579
+ };
4580
+ }
4561
4581
  if (/^\[mcp_servers\.exomind\]\s*$/m.test(existing)) {
4562
- return false;
4582
+ return { ok: true, written: false, file: codexConfig };
4563
4583
  }
4564
4584
  backup(codexConfig);
4565
4585
  const prefix = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
@@ -4568,6 +4588,34 @@ command = "exomind"
4568
4588
  args = ["mcp"]
4569
4589
  `;
4570
4590
  fs6.writeFileSync(codexConfig, existing + snippet);
4591
+ const after = fs6.readFileSync(codexConfig, "utf-8");
4592
+ if (!/^\[mcp_servers\.exomind\]\s*$/m.test(after) || !looksLikeValidToml(after)) {
4593
+ return { ok: false, written: true, file: codexConfig, reason: "\u5199\u540E\u590D\u9A8C\u5931\u8D25(\u6BB5\u672A\u5199\u5165\u6216\u6587\u4EF6\u635F\u574F)" };
4594
+ }
4595
+ return { ok: true, written: true, file: codexConfig };
4596
+ }
4597
+ function installCodexSkill(codexHome, skillSrc = CODEX_SKILL_SRC) {
4598
+ const destDir = path6.join(codexHome, "skills", "exomind");
4599
+ const dest = path6.join(destDir, "SKILL.md");
4600
+ if (!fs6.existsSync(skillSrc)) {
4601
+ return { ok: false, dest, reason: `skill \u6E90\u4E0D\u5B58\u5728: ${skillSrc}` };
4602
+ }
4603
+ fs6.mkdirSync(destDir, { recursive: true });
4604
+ backup(dest);
4605
+ const tmp = `${dest}.tmp-${Date.now()}`;
4606
+ fs6.writeFileSync(tmp, fs6.readFileSync(skillSrc));
4607
+ fs6.renameSync(tmp, dest);
4608
+ return { ok: true, dest };
4609
+ }
4610
+ function purgeMcpExomind(file) {
4611
+ const d = readJson2(file);
4612
+ if (!d) return false;
4613
+ const grp = d.mcpServers;
4614
+ if (!grp || !grp.exomind) return false;
4615
+ delete grp.exomind;
4616
+ if (Object.keys(grp).length === 0) delete d.mcpServers;
4617
+ backup(file);
4618
+ fs6.writeFileSync(file, JSON.stringify(d, null, 2) + "\n");
4571
4619
  return true;
4572
4620
  }
4573
4621
  function checkMcp(timeoutMs = 6e3) {
@@ -4615,16 +4663,34 @@ function checkMcp(timeoutMs = 6e3) {
4615
4663
  });
4616
4664
  }
4617
4665
  async function install(client, opts) {
4666
+ if (opts.host && !["claude", "codex", "opencode"].includes(opts.host)) {
4667
+ throw new Error(`\u672A\u77E5 --host: ${opts.host}(\u53EF\u9009: claude | codex | opencode)`);
4668
+ }
4618
4669
  const claudeDir = path6.join(os2.homedir(), ".claude");
4619
4670
  const skillDestDir = path6.join(claudeDir, "skills", "exomind");
4620
- if (!fs6.existsSync(SKILL_SRC)) {
4621
- throw new Error(`skill \u6E90\u4E0D\u5B58\u5728: ${SKILL_SRC}(npm \u5305\u53EF\u80FD\u635F\u574F,\u6216\u5F00\u53D1\u6A21\u5F0F\u4E0B\u672A\u6784\u5EFA)`);
4671
+ const codexHome = resolveCodexHome();
4672
+ const want = (h) => !opts.host || opts.host === h;
4673
+ if (opts.skill !== false) {
4674
+ if (want("claude")) {
4675
+ if (!fs6.existsSync(CLAUDE_SKILL_SRC)) {
4676
+ throw new Error(`Claude skill \u6E90\u4E0D\u5B58\u5728: ${CLAUDE_SKILL_SRC}(npm \u5305\u53EF\u80FD\u635F\u574F,\u6216\u5F00\u53D1\u6A21\u5F0F\u4E0B\u672A\u6784\u5EFA)`);
4677
+ }
4678
+ fs6.mkdirSync(skillDestDir, { recursive: true });
4679
+ fs6.copyFileSync(CLAUDE_SKILL_SRC, path6.join(skillDestDir, "SKILL.md"));
4680
+ console.log(ok("\u5DF2\u5B89\u88C5 Claude Code skill"));
4681
+ console.log(dim(` \u2192 ${skillDestDir}/SKILL.md`));
4682
+ }
4683
+ if (want("codex")) {
4684
+ const r = installCodexSkill(codexHome);
4685
+ if (r.ok) {
4686
+ console.log(ok("\u5DF2\u5B89\u88C5 Codex skill"));
4687
+ console.log(dim(` \u2192 ${r.dest}`));
4688
+ } else {
4689
+ console.log(yellow(`\u2717 Codex skill \u5B89\u88C5\u5931\u8D25: ${r.reason}`));
4690
+ }
4691
+ }
4622
4692
  }
4623
- fs6.mkdirSync(skillDestDir, { recursive: true });
4624
- fs6.copyFileSync(SKILL_SRC, path6.join(skillDestDir, "SKILL.md"));
4625
- console.log(ok("\u5DF2\u5B89\u88C5 Claude Code skill"));
4626
- console.log(dim(` \u2192 ${skillDestDir}/SKILL.md`));
4627
- if (opts.hook !== false) {
4693
+ if (opts.hook !== false && want("claude")) {
4628
4694
  const settingsFile = path6.join(claudeDir, "settings.json");
4629
4695
  fs6.mkdirSync(claudeDir, { recursive: true });
4630
4696
  backup(settingsFile);
@@ -4640,40 +4706,53 @@ async function install(client, opts) {
4640
4706
  hooks.UserPromptSubmit = kept;
4641
4707
  settings.hooks = hooks;
4642
4708
  fs6.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + "\n");
4643
- console.log(ok("\u5DF2\u914D\u7F6E UserPromptSubmit hook \u2192 exomind hook"));
4644
- console.log(dim(" \u91CD\u542F Claude Code \u751F\u6548\u3002hook \u51FA\u9519\u4E0D\u4F1A\u963B\u585E\u4F60\u7684\u8F93\u5165(\u9519\u8BEF\u53EA\u8FDB stderr)\u3002"));
4709
+ console.log(ok("\u5DF2\u914D\u7F6E UserPromptSubmit hook \u2192 exomind hook (Claude Code \u72EC\u6709)"));
4710
+ console.log(dim(" Codex \u5F53\u524D\u65E0\u5BF9\u5E94 prompt-submit hook,\u9760 skill \u89E6\u53D1(jdit/\u5B58\u6863/\u67E5\u8BE2)\u3002"));
4645
4711
  }
4646
4712
  if (opts.mcp !== false) {
4647
- const ccJson = path6.join(os2.homedir(), ".claude.json");
4648
- backup(ccJson);
4649
- const cc = readJson2(ccJson) ?? {};
4650
- const ccMcp = cc.mcpServers ?? {};
4651
- ccMcp.exomind = { command: "exomind", args: ["mcp"] };
4652
- cc.mcpServers = ccMcp;
4653
- fs6.writeFileSync(ccJson, JSON.stringify(cc, null, 2) + "\n");
4654
- const ocJson = path6.join(os2.homedir(), ".config", "opencode", "opencode.json");
4655
- fs6.mkdirSync(path6.dirname(ocJson), { recursive: true });
4656
- backup(ocJson);
4657
- const oc = readJson2(ocJson) ?? {};
4658
- const ocMcp = oc.mcp ?? {};
4659
- ocMcp.exomind = { type: "local", command: ["exomind", "mcp"] };
4660
- oc.mcp = ocMcp;
4661
- fs6.writeFileSync(ocJson, JSON.stringify(oc, null, 2) + "\n");
4662
- configureCodexMcp();
4713
+ const configured = [];
4714
+ if (want("claude")) {
4715
+ const ccJson = path6.join(os2.homedir(), ".claude.json");
4716
+ backup(ccJson);
4717
+ const cc = readJson2(ccJson) ?? {};
4718
+ const ccMcp = cc.mcpServers ?? {};
4719
+ ccMcp.exomind = { command: "exomind", args: ["mcp"] };
4720
+ cc.mcpServers = ccMcp;
4721
+ fs6.writeFileSync(ccJson, JSON.stringify(cc, null, 2) + "\n");
4722
+ configured.push("Claude Code (~/.claude.json)");
4723
+ }
4724
+ if (want("opencode")) {
4725
+ const ocJson = path6.join(os2.homedir(), ".config", "opencode", "opencode.json");
4726
+ fs6.mkdirSync(path6.dirname(ocJson), { recursive: true });
4727
+ backup(ocJson);
4728
+ const oc = readJson2(ocJson) ?? {};
4729
+ const ocMcp = oc.mcp ?? {};
4730
+ ocMcp.exomind = { type: "local", command: ["exomind", "mcp"] };
4731
+ oc.mcp = ocMcp;
4732
+ fs6.writeFileSync(ocJson, JSON.stringify(oc, null, 2) + "\n");
4733
+ configured.push("OpenCode (~/.config/opencode/opencode.json)");
4734
+ }
4735
+ if (want("codex")) {
4736
+ const r = configureCodexMcp(codexHome);
4737
+ if (r.ok) configured.push(`Codex (${r.file})`);
4738
+ else console.log(yellow(`\u2717 Codex MCP \u914D\u7F6E\u5931\u8D25: ${r.reason}`));
4739
+ }
4663
4740
  const purged = [];
4664
- if (purgeMcpExomind(path6.join(claudeDir, "settings.json"))) purged.push("~/.claude/settings.json");
4741
+ if (want("claude") && purgeMcpExomind(path6.join(claudeDir, "settings.json"))) {
4742
+ purged.push("~/.claude/settings.json");
4743
+ }
4665
4744
  let curDir = process.cwd();
4666
4745
  for (let i = 0; i < 12 && curDir !== path6.dirname(curDir); i++) {
4667
4746
  const f = path6.join(curDir, ".mcp.json");
4668
4747
  if (fs6.existsSync(f) && purgeMcpExomind(f)) purged.push(f.replace(os2.homedir(), "~"));
4669
4748
  curDir = path6.dirname(curDir);
4670
4749
  }
4671
- if (purged.length) console.log(dim(` \u2192 \u6E05\u7406\u6B8B\u7559 exomind MCP \u6761\u76EE\uFF08\u907F\u514D\u8986\u76D6 stdio\uFF09: ${purged.join(", ")}`));
4672
- console.log(ok("\u5DF2\u914D\u7F6E MCP server \u2192 exomind mcp"));
4673
- console.log(dim(" \u2192 Claude Code: ~/.claude.json (mcpServers.exomind)"));
4674
- console.log(dim(" \u2192 OpenCode: ~/.config/opencode/opencode.json (mcp.exomind)"));
4675
- console.log(dim(" \u2192 Codex: ~/.codex/config.toml ([mcp_servers.exomind])"));
4676
- console.log(dim(" \u91CD\u542F\u5BF9\u5E94 Agent \u2192 \u62FF\u5230 mcp__exomind__* \u5DE5\u5177"));
4750
+ if (purged.length) console.log(dim(` \u2192 \u6E05\u7406\u6B8B\u7559 exomind MCP \u6761\u76EE(\u907F\u514D\u8986\u76D6 stdio): ${purged.join(", ")}`));
4751
+ if (configured.length) {
4752
+ console.log(ok("\u5DF2\u914D\u7F6E MCP server \u2192 exomind mcp"));
4753
+ for (const c of configured) console.log(dim(` \u2192 ${c}`));
4754
+ console.log(dim(" \u91CD\u542F\u5BF9\u5E94 Agent \u2192 \u62FF\u5230 mcp__exomind__* \u5DE5\u5177"));
4755
+ }
4677
4756
  }
4678
4757
  const cfgFile = path6.join(os2.homedir(), ".exomind", "config.json");
4679
4758
  const cfg = readJson2(cfgFile);
@@ -4682,7 +4761,7 @@ async function install(client, opts) {
4682
4761
  cfg.base_url = DEFAULT_BASE_URL;
4683
4762
  fs6.writeFileSync(cfgFile, JSON.stringify(cfg, null, 2) + "\n");
4684
4763
  console.log(ok(`\u5DF2\u8FC1\u79FB base_url: d.youhuale.cn \u2192 ${DEFAULT_BASE_URL}`));
4685
- console.log(dim(" (stdio MCP \u5B50\u8FDB\u7A0B\u8BFB\u6B64\u914D\u7F6E\uFF0C\u91CD\u542F Agent \u540E\u7528\u65B0\u57DF\u540D)"));
4764
+ console.log(dim(" (stdio MCP \u5B50\u8FDB\u7A0B\u8BFB\u6B64\u914D\u7F6E,\u91CD\u542F Agent \u540E\u7528\u65B0\u57DF\u540D)"));
4686
4765
  }
4687
4766
  const live = loadConfig();
4688
4767
  const keyHint = live.api_key ? `${live.api_key.slice(0, 8)}\u2026${live.api_key.slice(-4)}` : "";
@@ -4699,25 +4778,139 @@ async function install(client, opts) {
4699
4778
  }
4700
4779
  }
4701
4780
  const mcp = live.api_key ? await checkMcp() : { ok: false, detail: "\u672A\u914D\u7F6E API Key" };
4781
+ const codexSkillOk = fs6.existsSync(path6.join(codexHome, "skills", "exomind", "SKILL.md"));
4782
+ const codexMcfgOk = (() => {
4783
+ try {
4784
+ return /^\[mcp_servers\.exomind\]\s*$/m.test(fs6.readFileSync(path6.join(codexHome, "config.toml"), "utf-8"));
4785
+ } catch {
4786
+ return false;
4787
+ }
4788
+ })();
4702
4789
  console.log(yellow("\n\u53EF\u7528\u6027\u81EA\u68C0:"));
4703
4790
  if (!live.api_key) {
4704
- console.log(dim(" \uFF08\u672A\u914D\u7F6E API Key\uFF0C\u8DF3\u8FC7\u81EA\u68C0\uFF1Bexomind login \u540E\u91CD\u8DD1 install \u53EF\u81EA\u68C0\uFF09"));
4791
+ console.log(dim(" (\u672A\u914D\u7F6E API Key,\u8DF3\u8FC7\u670D\u52A1\u7AEF\u81EA\u68C0;exomind login \u540E\u91CD\u8DD1 install \u53EF\u81EA\u68C0)"));
4705
4792
  } else {
4706
4793
  console.log(
4707
- me && me.authenticated ? ok(` \u670D\u52A1\u5668\u8FDE\u901A + \u9274\u6743\u6709\u6548\uFF08tenant ${me.tenant_id} \xB7 ${keyHint}\uFF09`) : yellow(" \u2717 \u670D\u52A1\u5668\u8FDE\u901A/\u9274\u6743\u5F02\u5E38 \u2192 exomind me \u6838\u9A8C")
4794
+ me && me.authenticated ? ok(` \u670D\u52A1\u5668\u8FDE\u901A + \u9274\u6743\u6709\u6548(tenant ${me.tenant_id} \xB7 ${keyHint})`) : yellow(" \u2717 \u670D\u52A1\u5668\u8FDE\u901A/\u9274\u6743\u5F02\u5E38 \u2192 exomind me \u6838\u9A8C")
4708
4795
  );
4709
- console.log(mcp.ok ? ok(` MCP \u670D\u52A1\u7AEF\u53EF\u7528\uFF08${mcp.detail}\uFF09`) : yellow(` \u2717 MCP \u670D\u52A1\u7AEF\u5F02\u5E38\uFF1A${mcp.detail}`));
4796
+ console.log(mcp.ok ? ok(` MCP \u670D\u52A1\u7AEF\u53EF\u7528(${mcp.detail})`) : yellow(` \u2717 MCP \u670D\u52A1\u7AEF\u5F02\u5E38: ${mcp.detail}`));
4797
+ }
4798
+ if (want("codex")) {
4799
+ console.log(codexSkillOk ? ok(" Codex skill \u5DF2\u5B89\u88C5") : yellow(" \u2717 Codex skill \u672A\u5B89\u88C5"));
4800
+ console.log(codexMcfgOk ? ok(" Codex MCP \u914D\u7F6E\u5B58\u5728") : yellow(" \u2717 Codex MCP \u914D\u7F6E\u7F3A\u5931"));
4801
+ console.log(dim(" \u25B3 Codex \u65E0 UserPromptSubmit hook,\u4F9D\u8D56 skill \u89E6\u53D1(jdit/\u5B58\u6863/\u67E5\u8BE2)"));
4710
4802
  }
4711
4803
  console.log(yellow("\n\u4E0B\u4E00\u6B65:"));
4712
4804
  if (!live.api_key) {
4713
- console.log(dim(" 1. exomind login\uFF08\u7C98\u8D34 API Key\uFF09"));
4714
- console.log(dim(" 2. \u91CD\u542F Claude Code\uFF08\u52A0\u8F7D skill/hook/MCP\uFF09\u2192 mcp__exomind__* \u751F\u6548"));
4805
+ console.log(dim(" 1. exomind login(\u7C98\u8D34 API Key)"));
4806
+ console.log(dim(" 2. \u91CD\u542F Agent(Claude Code / Codex)\u52A0\u8F7D skill/MCP \u2192 mcp__exomind__* \u751F\u6548"));
4715
4807
  } else if (me && me.authenticated && mcp.ok) {
4716
- console.log(dim(" \u81EA\u68C0\u5168\u8FC7 \u2192 \u91CD\u542F Claude Code\uFF08skill/hook/MCP \u542F\u52A8\u65F6\u8F7D\u5165\uFF09\u5373\u751F\u6548"));
4808
+ console.log(dim(" \u81EA\u68C0\u5168\u8FC7 \u2192 \u91CD\u542F Agent(skill/MCP \u542F\u52A8\u65F6\u8F7D\u5165)\u5373\u751F\u6548"));
4717
4809
  } else {
4718
- console.log(yellow("\u81EA\u68C0\u6709\u5F02\u5E38\uFF0C\u8BF7\u6309\u4E0A\u65B9 \u2717 \u63D0\u793A\u6838\u9A8C\u540E\u518D\u91CD\u542F\u4F7F\u7528\u3002"));
4810
+ console.log(yellow(" \u81EA\u68C0\u6709\u5F02\u5E38,\u6309\u4E0A\u65B9 \u2717 \u63D0\u793A\u6838\u9A8C\u540E\u518D\u91CD\u542F\u4F7F\u7528\u3002"));
4811
+ }
4812
+ }
4813
+
4814
+ // src/commands/doctor.ts
4815
+ var fs7 = __toESM(require("fs"));
4816
+ var path7 = __toESM(require("path"));
4817
+ var os3 = __toESM(require("os"));
4818
+ function readText(p) {
4819
+ try {
4820
+ return fs7.readFileSync(p, "utf-8");
4821
+ } catch {
4822
+ return "";
4823
+ }
4824
+ }
4825
+ function fileExists(p) {
4826
+ try {
4827
+ return fs7.existsSync(p);
4828
+ } catch {
4829
+ return false;
4719
4830
  }
4720
4831
  }
4832
+ function mcpServersExomind(jsonFile, key) {
4833
+ try {
4834
+ const d = JSON.parse(readText(jsonFile));
4835
+ const grp = key === "mcpServers" ? d.mcpServers : d.mcp;
4836
+ return !!(grp && grp.exomind);
4837
+ } catch {
4838
+ return false;
4839
+ }
4840
+ }
4841
+ function badge(s) {
4842
+ if (s === "ok") return ok("ok");
4843
+ if (s === "unsupported") return dim("\u2014 n/a");
4844
+ if (s === "missing") return yellow("\u2717 \u7F3A\u5931");
4845
+ return red(`\u2717 ${s}`);
4846
+ }
4847
+ async function doctor(client) {
4848
+ const codexHome = resolveCodexHome();
4849
+ const checks = [];
4850
+ const claudeSkill = path7.join(os3.homedir(), ".claude", "skills", "exomind", "SKILL.md");
4851
+ const claudeSettings = path7.join(os3.homedir(), ".claude", "settings.json");
4852
+ const claudeJson = path7.join(os3.homedir(), ".claude.json");
4853
+ checks.push({
4854
+ host: "claude",
4855
+ skill: fileExists(claudeSkill) ? "ok" : "missing",
4856
+ hook: /exomind/.test(readText(claudeSettings)) ? "ok" : "missing",
4857
+ mcpConfig: mcpServersExomind(claudeJson, "mcpServers") ? "ok" : "missing",
4858
+ paths: [claudeSkill, claudeSettings, claudeJson]
4859
+ });
4860
+ const codexSkill = path7.join(codexHome, "skills", "exomind", "SKILL.md");
4861
+ const codexConfig = path7.join(codexHome, "config.toml");
4862
+ checks.push({
4863
+ host: "codex",
4864
+ skill: fileExists(codexSkill) ? "ok" : "missing",
4865
+ hook: "unsupported",
4866
+ mcpConfig: /^\[mcp_servers\.exomind\]\s*$/m.test(readText(codexConfig)) ? "ok" : "missing",
4867
+ paths: [codexSkill, codexConfig],
4868
+ detail: "Codex \u65E0 prompt-submit hook,\u9760 skill \u89E6\u53D1(jdit/\u5B58\u6863/\u67E5\u8BE2)"
4869
+ });
4870
+ const ocJson = path7.join(os3.homedir(), ".config", "opencode", "opencode.json");
4871
+ checks.push({
4872
+ host: "opencode",
4873
+ skill: "unsupported",
4874
+ hook: "unsupported",
4875
+ mcpConfig: mcpServersExomind(ocJson, "mcp") ? "ok" : "missing",
4876
+ paths: [ocJson]
4877
+ });
4878
+ let authed = false;
4879
+ let authErr = "";
4880
+ try {
4881
+ const me = await client.get("/auth/me", { timeoutMs: 6e3 });
4882
+ authed = !!me.authenticated;
4883
+ } catch (e) {
4884
+ authErr = e instanceof Error ? e.message : String(e);
4885
+ }
4886
+ const mcpInit = await checkMcp();
4887
+ if (isJsonMode()) {
4888
+ console.log(
4889
+ JSON.stringify(
4890
+ {
4891
+ codexHome,
4892
+ hosts: checks,
4893
+ auth: { authenticated: authed, error: authErr || void 0 },
4894
+ mcpInitialize: { ok: mcpInit.ok, detail: mcpInit.detail }
4895
+ },
4896
+ null,
4897
+ 2
4898
+ )
4899
+ );
4900
+ return;
4901
+ }
4902
+ console.log(yellow("ExoMind \u5B89\u88C5\u8BCA\u65AD:"));
4903
+ console.log(dim(` Codex home: ${codexHome}`));
4904
+ for (const c of checks) {
4905
+ console.log(` ${c.host}: skill ${badge(c.skill)} hook ${badge(c.hook)} mcp ${badge(c.mcpConfig)}`);
4906
+ if (c.detail) console.log(dim(` ${c.detail}`));
4907
+ }
4908
+ console.log(
4909
+ ` auth: ${authed ? ok("\u2713 \u5DF2\u767B\u5F55") : yellow("\u2717 \u672A\u767B\u5F55/\u5F02\u5E38")}${authErr ? dim(" " + authErr) : ""}`
4910
+ );
4911
+ console.log(` mcp init: ${mcpInit.ok ? ok("\u2713 " + mcpInit.detail) : yellow("\u2717 " + mcpInit.detail)}`);
4912
+ console.log(dim("\n \u7F3A\u9879\u7528 `exomind install [--host <claude|codex|opencode>]` \u8865\u9F50;\u6539\u5B8C\u91CD\u542F\u5BF9\u5E94 Agent\u3002"));
4913
+ }
4721
4914
 
4722
4915
  // src/cli.ts
4723
4916
  var VERSION = package_default.version;
@@ -4807,7 +5000,8 @@ program2.command("mcp").description("\u542F\u52A8 stdio MCP server(\u4F9B Claude
4807
5000
  const client = new ApiClient(cfg);
4808
5001
  await runMcpServer(client);
4809
5002
  });
4810
- program2.command("install").description("\u5B89\u88C5 skill + hook + MCP(\u9ED8\u8BA4\u5168\u88C5;--no-hook / --no-mcp \u5173\u95ED\u67D0\u9879)").option("--no-hook", "\u4E0D\u5199 UserPromptSubmit hook").option("--no-mcp", "\u4E0D\u5199 MCP server \u914D\u7F6E(Claude Code + OpenCode \u90FD\u4E0D\u5199)").action(run(install));
5003
+ program2.command("install").description("\u5B89\u88C5 skill(Claude+Codex)+ hook(Claude)+ MCP(\u5404\u5BBF\u4E3B);\u9ED8\u8BA4\u5168\u88C5,--host \u9009\u5355\u5BBF\u4E3B").option("--host <h>", "\u53EA\u88C5\u67D0\u4E2A\u5BBF\u4E3B: claude | codex | opencode(\u7F3A\u7701\u5168\u88C5)").option("--no-skill", "\u4E0D\u5237\u65B0 skill(\u4E0D\u5220\u5DF2\u6709)").option("--no-hook", "\u4E0D\u5199 UserPromptSubmit hook(\u4EC5 Claude)").option("--no-mcp", "\u4E0D\u5199 MCP server \u914D\u7F6E").action(run(install));
5004
+ program2.command("doctor").description("\u8BCA\u65AD\u5404\u5BBF\u4E3B skill/hook/MCP/\u9274\u6743 \u72B6\u6001(install \u540E\u6392\u67E5)").action(run(doctor));
4811
5005
  async function main() {
4812
5006
  const first = process.argv[2];
4813
5007
  if (first && /^(-v|--version)$/.test(first)) {