exomind 0.4.2 → 0.5.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 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.0",
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"
@@ -4502,7 +4502,8 @@ var path6 = __toESM(require("path"));
4502
4502
  var os2 = __toESM(require("os"));
4503
4503
  var import_node_child_process = require("child_process");
4504
4504
  var PKG_ROOT = path6.resolve(__dirname, "..");
4505
- var SKILL_SRC = path6.join(PKG_ROOT, "skill", "SKILL.md");
4505
+ var CLAUDE_SKILL_SRC = path6.join(PKG_ROOT, "skill", "claude", "SKILL.md");
4506
+ var CODEX_SKILL_SRC = path6.join(PKG_ROOT, "skill", "codex", "SKILL.md");
4506
4507
  function readJson2(file) {
4507
4508
  try {
4508
4509
  return JSON.parse(fs6.readFileSync(file, "utf-8"));
@@ -4535,19 +4536,23 @@ function pruneBackups(file, keep) {
4535
4536
  } catch {
4536
4537
  }
4537
4538
  }
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;
4539
+ function resolveCodexHome(env = process.env, homeDir = os2.homedir()) {
4540
+ const configured = (env.CODEX_HOME ?? "").trim();
4541
+ return configured ? path6.resolve(configured) : path6.join(homeDir, ".codex");
4548
4542
  }
4549
- function configureCodexMcp() {
4550
- const codexConfig = path6.join(os2.homedir(), ".codex", "config.toml");
4543
+ function looksLikeValidToml(text) {
4544
+ if (!text.trim()) return true;
4545
+ if (/\x00/.test(text)) return false;
4546
+ let brackets = 0;
4547
+ for (const ch of text) {
4548
+ if (ch === "[") brackets++;
4549
+ else if (ch === "]") brackets--;
4550
+ if (brackets < 0) return false;
4551
+ }
4552
+ return brackets === 0;
4553
+ }
4554
+ function configureCodexMcp(codexHome) {
4555
+ const codexConfig = path6.join(codexHome, "config.toml");
4551
4556
  try {
4552
4557
  fs6.mkdirSync(path6.dirname(codexConfig), { recursive: true });
4553
4558
  } catch {
@@ -4558,8 +4563,16 @@ function configureCodexMcp() {
4558
4563
  } catch {
4559
4564
  existing = "";
4560
4565
  }
4566
+ if (existing && !looksLikeValidToml(existing)) {
4567
+ return {
4568
+ ok: false,
4569
+ written: false,
4570
+ file: codexConfig,
4571
+ 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}`
4572
+ };
4573
+ }
4561
4574
  if (/^\[mcp_servers\.exomind\]\s*$/m.test(existing)) {
4562
- return false;
4575
+ return { ok: true, written: false, file: codexConfig };
4563
4576
  }
4564
4577
  backup(codexConfig);
4565
4578
  const prefix = existing && !existing.endsWith("\n") ? "\n\n" : existing ? "\n" : "";
@@ -4568,6 +4581,34 @@ command = "exomind"
4568
4581
  args = ["mcp"]
4569
4582
  `;
4570
4583
  fs6.writeFileSync(codexConfig, existing + snippet);
4584
+ const after = fs6.readFileSync(codexConfig, "utf-8");
4585
+ if (!/^\[mcp_servers\.exomind\]\s*$/m.test(after) || !looksLikeValidToml(after)) {
4586
+ return { ok: false, written: true, file: codexConfig, reason: "\u5199\u540E\u590D\u9A8C\u5931\u8D25(\u6BB5\u672A\u5199\u5165\u6216\u6587\u4EF6\u635F\u574F)" };
4587
+ }
4588
+ return { ok: true, written: true, file: codexConfig };
4589
+ }
4590
+ function installCodexSkill(codexHome, skillSrc = CODEX_SKILL_SRC) {
4591
+ const destDir = path6.join(codexHome, "skills", "exomind");
4592
+ const dest = path6.join(destDir, "SKILL.md");
4593
+ if (!fs6.existsSync(skillSrc)) {
4594
+ return { ok: false, dest, reason: `skill \u6E90\u4E0D\u5B58\u5728: ${skillSrc}` };
4595
+ }
4596
+ fs6.mkdirSync(destDir, { recursive: true });
4597
+ backup(dest);
4598
+ const tmp = `${dest}.tmp-${Date.now()}`;
4599
+ fs6.writeFileSync(tmp, fs6.readFileSync(skillSrc));
4600
+ fs6.renameSync(tmp, dest);
4601
+ return { ok: true, dest };
4602
+ }
4603
+ function purgeMcpExomind(file) {
4604
+ const d = readJson2(file);
4605
+ if (!d) return false;
4606
+ const grp = d.mcpServers;
4607
+ if (!grp || !grp.exomind) return false;
4608
+ delete grp.exomind;
4609
+ if (Object.keys(grp).length === 0) delete d.mcpServers;
4610
+ backup(file);
4611
+ fs6.writeFileSync(file, JSON.stringify(d, null, 2) + "\n");
4571
4612
  return true;
4572
4613
  }
4573
4614
  function checkMcp(timeoutMs = 6e3) {
@@ -4615,16 +4656,34 @@ function checkMcp(timeoutMs = 6e3) {
4615
4656
  });
4616
4657
  }
4617
4658
  async function install(client, opts) {
4659
+ if (opts.host && !["claude", "codex", "opencode"].includes(opts.host)) {
4660
+ throw new Error(`\u672A\u77E5 --host: ${opts.host}(\u53EF\u9009: claude | codex | opencode)`);
4661
+ }
4618
4662
  const claudeDir = path6.join(os2.homedir(), ".claude");
4619
4663
  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)`);
4664
+ const codexHome = resolveCodexHome();
4665
+ const want = (h) => !opts.host || opts.host === h;
4666
+ if (opts.skill !== false) {
4667
+ if (want("claude")) {
4668
+ if (!fs6.existsSync(CLAUDE_SKILL_SRC)) {
4669
+ 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)`);
4670
+ }
4671
+ fs6.mkdirSync(skillDestDir, { recursive: true });
4672
+ fs6.copyFileSync(CLAUDE_SKILL_SRC, path6.join(skillDestDir, "SKILL.md"));
4673
+ console.log(ok("\u5DF2\u5B89\u88C5 Claude Code skill"));
4674
+ console.log(dim(` \u2192 ${skillDestDir}/SKILL.md`));
4675
+ }
4676
+ if (want("codex")) {
4677
+ const r = installCodexSkill(codexHome);
4678
+ if (r.ok) {
4679
+ console.log(ok("\u5DF2\u5B89\u88C5 Codex skill"));
4680
+ console.log(dim(` \u2192 ${r.dest}`));
4681
+ } else {
4682
+ console.log(yellow(`\u2717 Codex skill \u5B89\u88C5\u5931\u8D25: ${r.reason}`));
4683
+ }
4684
+ }
4622
4685
  }
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) {
4686
+ if (opts.hook !== false && want("claude")) {
4628
4687
  const settingsFile = path6.join(claudeDir, "settings.json");
4629
4688
  fs6.mkdirSync(claudeDir, { recursive: true });
4630
4689
  backup(settingsFile);
@@ -4640,40 +4699,53 @@ async function install(client, opts) {
4640
4699
  hooks.UserPromptSubmit = kept;
4641
4700
  settings.hooks = hooks;
4642
4701
  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"));
4702
+ console.log(ok("\u5DF2\u914D\u7F6E UserPromptSubmit hook \u2192 exomind hook (Claude Code \u72EC\u6709)"));
4703
+ console.log(dim(" Codex \u5F53\u524D\u65E0\u5BF9\u5E94 prompt-submit hook,\u9760 skill \u89E6\u53D1(jdit/\u5B58\u6863/\u67E5\u8BE2)\u3002"));
4645
4704
  }
4646
4705
  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();
4706
+ const configured = [];
4707
+ if (want("claude")) {
4708
+ const ccJson = path6.join(os2.homedir(), ".claude.json");
4709
+ backup(ccJson);
4710
+ const cc = readJson2(ccJson) ?? {};
4711
+ const ccMcp = cc.mcpServers ?? {};
4712
+ ccMcp.exomind = { command: "exomind", args: ["mcp"] };
4713
+ cc.mcpServers = ccMcp;
4714
+ fs6.writeFileSync(ccJson, JSON.stringify(cc, null, 2) + "\n");
4715
+ configured.push("Claude Code (~/.claude.json)");
4716
+ }
4717
+ if (want("opencode")) {
4718
+ const ocJson = path6.join(os2.homedir(), ".config", "opencode", "opencode.json");
4719
+ fs6.mkdirSync(path6.dirname(ocJson), { recursive: true });
4720
+ backup(ocJson);
4721
+ const oc = readJson2(ocJson) ?? {};
4722
+ const ocMcp = oc.mcp ?? {};
4723
+ ocMcp.exomind = { type: "local", command: ["exomind", "mcp"] };
4724
+ oc.mcp = ocMcp;
4725
+ fs6.writeFileSync(ocJson, JSON.stringify(oc, null, 2) + "\n");
4726
+ configured.push("OpenCode (~/.config/opencode/opencode.json)");
4727
+ }
4728
+ if (want("codex")) {
4729
+ const r = configureCodexMcp(codexHome);
4730
+ if (r.ok) configured.push(`Codex (${r.file})`);
4731
+ else console.log(yellow(`\u2717 Codex MCP \u914D\u7F6E\u5931\u8D25: ${r.reason}`));
4732
+ }
4663
4733
  const purged = [];
4664
- if (purgeMcpExomind(path6.join(claudeDir, "settings.json"))) purged.push("~/.claude/settings.json");
4734
+ if (want("claude") && purgeMcpExomind(path6.join(claudeDir, "settings.json"))) {
4735
+ purged.push("~/.claude/settings.json");
4736
+ }
4665
4737
  let curDir = process.cwd();
4666
4738
  for (let i = 0; i < 12 && curDir !== path6.dirname(curDir); i++) {
4667
4739
  const f = path6.join(curDir, ".mcp.json");
4668
4740
  if (fs6.existsSync(f) && purgeMcpExomind(f)) purged.push(f.replace(os2.homedir(), "~"));
4669
4741
  curDir = path6.dirname(curDir);
4670
4742
  }
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"));
4743
+ if (purged.length) console.log(dim(` \u2192 \u6E05\u7406\u6B8B\u7559 exomind MCP \u6761\u76EE(\u907F\u514D\u8986\u76D6 stdio): ${purged.join(", ")}`));
4744
+ if (configured.length) {
4745
+ console.log(ok("\u5DF2\u914D\u7F6E MCP server \u2192 exomind mcp"));
4746
+ for (const c of configured) console.log(dim(` \u2192 ${c}`));
4747
+ console.log(dim(" \u91CD\u542F\u5BF9\u5E94 Agent \u2192 \u62FF\u5230 mcp__exomind__* \u5DE5\u5177"));
4748
+ }
4677
4749
  }
4678
4750
  const cfgFile = path6.join(os2.homedir(), ".exomind", "config.json");
4679
4751
  const cfg = readJson2(cfgFile);
@@ -4682,7 +4754,7 @@ async function install(client, opts) {
4682
4754
  cfg.base_url = DEFAULT_BASE_URL;
4683
4755
  fs6.writeFileSync(cfgFile, JSON.stringify(cfg, null, 2) + "\n");
4684
4756
  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)"));
4757
+ console.log(dim(" (stdio MCP \u5B50\u8FDB\u7A0B\u8BFB\u6B64\u914D\u7F6E,\u91CD\u542F Agent \u540E\u7528\u65B0\u57DF\u540D)"));
4686
4758
  }
4687
4759
  const live = loadConfig();
4688
4760
  const keyHint = live.api_key ? `${live.api_key.slice(0, 8)}\u2026${live.api_key.slice(-4)}` : "";
@@ -4699,25 +4771,139 @@ async function install(client, opts) {
4699
4771
  }
4700
4772
  }
4701
4773
  const mcp = live.api_key ? await checkMcp() : { ok: false, detail: "\u672A\u914D\u7F6E API Key" };
4774
+ const codexSkillOk = fs6.existsSync(path6.join(codexHome, "skills", "exomind", "SKILL.md"));
4775
+ const codexMcfgOk = (() => {
4776
+ try {
4777
+ return /^\[mcp_servers\.exomind\]\s*$/m.test(fs6.readFileSync(path6.join(codexHome, "config.toml"), "utf-8"));
4778
+ } catch {
4779
+ return false;
4780
+ }
4781
+ })();
4702
4782
  console.log(yellow("\n\u53EF\u7528\u6027\u81EA\u68C0:"));
4703
4783
  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"));
4784
+ 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
4785
  } else {
4706
4786
  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")
4787
+ 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
4788
  );
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}`));
4789
+ console.log(mcp.ok ? ok(` MCP \u670D\u52A1\u7AEF\u53EF\u7528(${mcp.detail})`) : yellow(` \u2717 MCP \u670D\u52A1\u7AEF\u5F02\u5E38: ${mcp.detail}`));
4790
+ }
4791
+ if (want("codex")) {
4792
+ console.log(codexSkillOk ? ok(" Codex skill \u5DF2\u5B89\u88C5") : yellow(" \u2717 Codex skill \u672A\u5B89\u88C5"));
4793
+ console.log(codexMcfgOk ? ok(" Codex MCP \u914D\u7F6E\u5B58\u5728") : yellow(" \u2717 Codex MCP \u914D\u7F6E\u7F3A\u5931"));
4794
+ console.log(dim(" \u25B3 Codex \u65E0 UserPromptSubmit hook,\u4F9D\u8D56 skill \u89E6\u53D1(jdit/\u5B58\u6863/\u67E5\u8BE2)"));
4710
4795
  }
4711
4796
  console.log(yellow("\n\u4E0B\u4E00\u6B65:"));
4712
4797
  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"));
4798
+ console.log(dim(" 1. exomind login(\u7C98\u8D34 API Key)"));
4799
+ console.log(dim(" 2. \u91CD\u542F Agent(Claude Code / Codex)\u52A0\u8F7D skill/MCP \u2192 mcp__exomind__* \u751F\u6548"));
4715
4800
  } 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"));
4801
+ console.log(dim(" \u81EA\u68C0\u5168\u8FC7 \u2192 \u91CD\u542F Agent(skill/MCP \u542F\u52A8\u65F6\u8F7D\u5165)\u5373\u751F\u6548"));
4717
4802
  } 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"));
4803
+ console.log(yellow(" \u81EA\u68C0\u6709\u5F02\u5E38,\u6309\u4E0A\u65B9 \u2717 \u63D0\u793A\u6838\u9A8C\u540E\u518D\u91CD\u542F\u4F7F\u7528\u3002"));
4804
+ }
4805
+ }
4806
+
4807
+ // src/commands/doctor.ts
4808
+ var fs7 = __toESM(require("fs"));
4809
+ var path7 = __toESM(require("path"));
4810
+ var os3 = __toESM(require("os"));
4811
+ function readText(p) {
4812
+ try {
4813
+ return fs7.readFileSync(p, "utf-8");
4814
+ } catch {
4815
+ return "";
4719
4816
  }
4720
4817
  }
4818
+ function fileExists(p) {
4819
+ try {
4820
+ return fs7.existsSync(p);
4821
+ } catch {
4822
+ return false;
4823
+ }
4824
+ }
4825
+ function mcpServersExomind(jsonFile, key) {
4826
+ try {
4827
+ const d = JSON.parse(readText(jsonFile));
4828
+ const grp = key === "mcpServers" ? d.mcpServers : d.mcp;
4829
+ return !!(grp && grp.exomind);
4830
+ } catch {
4831
+ return false;
4832
+ }
4833
+ }
4834
+ function badge(s) {
4835
+ if (s === "ok") return ok("ok");
4836
+ if (s === "unsupported") return dim("\u2014 n/a");
4837
+ if (s === "missing") return yellow("\u2717 \u7F3A\u5931");
4838
+ return red(`\u2717 ${s}`);
4839
+ }
4840
+ async function doctor(client) {
4841
+ const codexHome = resolveCodexHome();
4842
+ const checks = [];
4843
+ const claudeSkill = path7.join(os3.homedir(), ".claude", "skills", "exomind", "SKILL.md");
4844
+ const claudeSettings = path7.join(os3.homedir(), ".claude", "settings.json");
4845
+ const claudeJson = path7.join(os3.homedir(), ".claude.json");
4846
+ checks.push({
4847
+ host: "claude",
4848
+ skill: fileExists(claudeSkill) ? "ok" : "missing",
4849
+ hook: /exomind/.test(readText(claudeSettings)) ? "ok" : "missing",
4850
+ mcpConfig: mcpServersExomind(claudeJson, "mcpServers") ? "ok" : "missing",
4851
+ paths: [claudeSkill, claudeSettings, claudeJson]
4852
+ });
4853
+ const codexSkill = path7.join(codexHome, "skills", "exomind", "SKILL.md");
4854
+ const codexConfig = path7.join(codexHome, "config.toml");
4855
+ checks.push({
4856
+ host: "codex",
4857
+ skill: fileExists(codexSkill) ? "ok" : "missing",
4858
+ hook: "unsupported",
4859
+ mcpConfig: /^\[mcp_servers\.exomind\]\s*$/m.test(readText(codexConfig)) ? "ok" : "missing",
4860
+ paths: [codexSkill, codexConfig],
4861
+ detail: "Codex \u65E0 prompt-submit hook,\u9760 skill \u89E6\u53D1(jdit/\u5B58\u6863/\u67E5\u8BE2)"
4862
+ });
4863
+ const ocJson = path7.join(os3.homedir(), ".config", "opencode", "opencode.json");
4864
+ checks.push({
4865
+ host: "opencode",
4866
+ skill: "unsupported",
4867
+ hook: "unsupported",
4868
+ mcpConfig: mcpServersExomind(ocJson, "mcp") ? "ok" : "missing",
4869
+ paths: [ocJson]
4870
+ });
4871
+ let authed = false;
4872
+ let authErr = "";
4873
+ try {
4874
+ const me = await client.get("/auth/me", { timeoutMs: 6e3 });
4875
+ authed = !!me.authenticated;
4876
+ } catch (e) {
4877
+ authErr = e instanceof Error ? e.message : String(e);
4878
+ }
4879
+ const mcpInit = await checkMcp();
4880
+ if (isJsonMode()) {
4881
+ console.log(
4882
+ JSON.stringify(
4883
+ {
4884
+ codexHome,
4885
+ hosts: checks,
4886
+ auth: { authenticated: authed, error: authErr || void 0 },
4887
+ mcpInitialize: { ok: mcpInit.ok, detail: mcpInit.detail }
4888
+ },
4889
+ null,
4890
+ 2
4891
+ )
4892
+ );
4893
+ return;
4894
+ }
4895
+ console.log(yellow("ExoMind \u5B89\u88C5\u8BCA\u65AD:"));
4896
+ console.log(dim(` Codex home: ${codexHome}`));
4897
+ for (const c of checks) {
4898
+ console.log(` ${c.host}: skill ${badge(c.skill)} hook ${badge(c.hook)} mcp ${badge(c.mcpConfig)}`);
4899
+ if (c.detail) console.log(dim(` ${c.detail}`));
4900
+ }
4901
+ console.log(
4902
+ ` auth: ${authed ? ok("\u2713 \u5DF2\u767B\u5F55") : yellow("\u2717 \u672A\u767B\u5F55/\u5F02\u5E38")}${authErr ? dim(" " + authErr) : ""}`
4903
+ );
4904
+ console.log(` mcp init: ${mcpInit.ok ? ok("\u2713 " + mcpInit.detail) : yellow("\u2717 " + mcpInit.detail)}`);
4905
+ console.log(dim("\n \u7F3A\u9879\u7528 `exomind install [--host <claude|codex|opencode>]` \u8865\u9F50;\u6539\u5B8C\u91CD\u542F\u5BF9\u5E94 Agent\u3002"));
4906
+ }
4721
4907
 
4722
4908
  // src/cli.ts
4723
4909
  var VERSION = package_default.version;
@@ -4807,7 +4993,8 @@ program2.command("mcp").description("\u542F\u52A8 stdio MCP server(\u4F9B Claude
4807
4993
  const client = new ApiClient(cfg);
4808
4994
  await runMcpServer(client);
4809
4995
  });
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));
4996
+ 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));
4997
+ program2.command("doctor").description("\u8BCA\u65AD\u5404\u5BBF\u4E3B skill/hook/MCP/\u9274\u6743 \u72B6\u6001(install \u540E\u6392\u67E5)").action(run(doctor));
4811
4998
  async function main() {
4812
4999
  const first = process.argv[2];
4813
5000
  if (first && /^(-v|--version)$/.test(first)) {