exomind 0.3.1 → 0.3.3

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
@@ -42,6 +42,20 @@ exomind feedback "entities/Redis.md" positive
42
42
 
43
43
  加 `--json` 获取机器可读输出(脚本/管道):`exomind --json stats | jq .total_nodes`。
44
44
 
45
+ ## 凭证管理
46
+
47
+ `exomind login` 输入的凭证持久化到本地,后续命令自动读取,无需重复登录。
48
+
49
+ - **存储位置**:`~/.exomind/config.json`(明文 JSON),文件权限 `0600`(仅所有者可读写;Windows 无 POSIX 权限则忽略)。
50
+ - **凭证类型**:服务端 `auth_middleware` 同时接受 **API Key**(从 `d.youhuale.cn/ui/account` 复制)与 **GitHub token**(`gh_` 前缀);两者统一以 `Authorization: Bearer` 发送,CLI 不关心是哪种。
51
+ - **读取优先级**:`config.json` 的 `api_key` → 环境变量 `EXOMIND_API_KEY` → 旧版遗留文件 `~/.claude/scripts/.exomind-api-key`(向后兼容老安装)。三者任一存在即免登录。
52
+ - **CI / 免登录**:只设环境变量 `EXOMIND_API_KEY`(可选 `EXOMIND_BASE_URL`)即可,完全不写本地文件。`config.json` 优先级高于环境变量,故已 `login` 的机器需 `--api-key` 才能临时覆盖。
53
+ - **校验行为**:`login` 先探活 `/keywords` **通过后再落盘**——401/403 时**不写文件**,避免无效凭证覆盖已有有效配置;网络错误无法判定时仍保存并提示"登录成功(未校验)",稍后用 `exomind whoami` 复核。
54
+ - **换号 / 登出**:重新 `exomind login` 覆盖旧凭证;彻底登出删 `~/.exomind/config.json`。
55
+ - **安全提醒**:文件是**明文**,任何能读你 home 目录的进程都能拿到 key。共享机器 / 不信任环境请用环境变量,不要用 `login`。
56
+
57
+ > 把 API Key 当密码对待:不提交进仓库、不贴进聊天。脚本化登录用 `exomind login --api-key ...`(注意别让 key 进 shell 历史或进程列表);交互式 `exomind login` 的 prompt 读取不走 shell 历史。
58
+
45
59
  ## 接入 Claude Code(一条命令,默认全装)
46
60
 
47
61
  ```bash
@@ -61,14 +75,17 @@ exomind install # 装 skill + hook + MCP,全部自动(幂等+备份)
61
75
 
62
76
  完整接入步骤见服务端仓库 `myExoMindManager/docs/new-machine-setup.md`。
63
77
 
64
- ## 关于 MCP 工具层(Claude Code 已默认装;OpenCode 需手写)
78
+ **升级**:`npm i -g exomind@latest && exomind install`(幂等,刷新 skill/hook/mcp;`~/.exomind/` config 与 manifest 保留)。注意:`npm i -g` 只换二进制(CLI+MCP 自动用新),**skill 是拷贝的,需 `exomind install` 才刷新**。
79
+
80
+ ## 关于 MCP 工具层(Claude Code + OpenCode 都已默认装)
65
81
 
66
- `exomind install` 已默认把 MCP 写进 Claude Code 的 `~/.claude.json`(见上)。这里仅给**其它宿主/手写**场景:
82
+ `exomind install` 一次写**两个宿主**的 MCP 配置(都幂等+备份,互不干扰,各读各的):
83
+ - **Claude Code**:`~/.claude.json` → `mcpServers.exomind`
84
+ - **OpenCode**:`~/.config/opencode/opencode.json` → `mcp.exomind`
67
85
 
68
- - **OpenCode**(`opencode.json`):`{ "mcp": { "exomind": { "type": "local", "command": ["exomind", "mcp"] } } }`
69
- - **手写 Claude Code**:`~/.claude.json` 或项目 `.mcp.json`:`{ "mcpServers": { "exomind": { "command": "exomind", "args": ["mcp"] } } }`
86
+ 只关 MCP:`exomind install --no-mcp`(两个宿主都不写)。手写/其它宿主(如 Cursor)参考 [docs/mcp.md](./docs/mcp.md)。
70
87
 
71
- `exomind mcp` 是本地 stdio MCP server,把 ingest/query/search/entity/relations/stats 暴露为 typed tool;复用同一份凭证,三平台都能跑(本地 stdio,不涉及远程 SSE 的 Windows 坑)。详见 [三层模式说明](./docs/mcp.md)
88
+ `exomind mcp` 是本地 stdio MCP server,把 ingest/query/search/entity/relations/stats 暴露为 typed tool;复用同一份凭证,三平台都能跑(本地 stdio,不涉及远程 SSE 的 Windows 坑)。
72
89
 
73
90
  ## 命令一览
74
91
 
package/dist/cli.js CHANGED
@@ -3119,7 +3119,7 @@ var {
3119
3119
  // package.json
3120
3120
  var package_default = {
3121
3121
  name: "exomind",
3122
- version: "0.3.1",
3122
+ version: "0.3.3",
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"
@@ -4228,22 +4228,24 @@ async function login(_client, opts) {
4228
4228
  token = await prompt("\u51ED\u8BC1: ");
4229
4229
  }
4230
4230
  if (!token) throw new Error("\u672A\u63D0\u4F9B\u51ED\u8BC1");
4231
- saveConfig({ base_url: baseUrl, api_key: token });
4232
4231
  const probe = new ApiClient({ base_url: baseUrl, api_key: token });
4232
+ let verified = true;
4233
4233
  try {
4234
4234
  await probe.get("/keywords");
4235
4235
  } catch (e) {
4236
4236
  if (e instanceof ApiError && (e.status === 401 || e.status === 403)) {
4237
4237
  throw new Error(`\u51ED\u8BC1\u6821\u9A8C\u5931\u8D25 (HTTP ${e.status}): ${e.detail}`);
4238
4238
  }
4239
+ verified = false;
4239
4240
  console.log(
4240
4241
  yellow(
4241
- `\u8B66\u544A: \u65E0\u6CD5\u8FDE\u63A5 ${baseUrl} \u6821\u9A8C\u51ED\u8BC1 (${e instanceof Error ? e.message : String(e)})\u3002\u914D\u7F6E\u5DF2\u4FDD\u5B58,\u7A0D\u540E\u53EF\u7528 exomind whoami \u9A8C\u8BC1\u3002`
4242
+ `\u8B66\u544A: \u65E0\u6CD5\u8FDE\u63A5 ${baseUrl} \u6821\u9A8C\u51ED\u8BC1 (${e instanceof Error ? e.message : String(e)})\u3002\u914D\u7F6E\u4ECD\u5C06\u4FDD\u5B58,\u7A0D\u540E\u53EF\u7528 exomind whoami \u9A8C\u8BC1\u3002`
4242
4243
  )
4243
4244
  );
4244
4245
  }
4246
+ saveConfig({ base_url: baseUrl, api_key: token });
4245
4247
  const hint2 = token.length > 12 ? `${token.slice(0, 8)}\u2026${token.slice(-4)}` : token;
4246
- console.log(ok("\u767B\u5F55\u6210\u529F"));
4248
+ console.log(ok(verified ? "\u767B\u5F55\u6210\u529F" : "\u767B\u5F55\u6210\u529F(\u672A\u6821\u9A8C)"));
4247
4249
  console.log(dim(` \u670D\u52A1\u5668: ${baseUrl}`));
4248
4250
  console.log(dim(` \u51ED\u8BC1: ${hint2} (${token.startsWith("gh_") ? "GitHub token" : "API Key"})`));
4249
4251
  }
@@ -4294,7 +4296,23 @@ function backup(file) {
4294
4296
  const bak = `${file}.bak-${Date.now()}`;
4295
4297
  try {
4296
4298
  fs6.copyFileSync(file, bak);
4297
- console.log(dim(` (\u5DF2\u5907\u4EFD\u539F\u914D\u7F6E \u2192 ${path6.basename(bak)})`));
4299
+ console.log(dim(` (\u5DF2\u5907\u4EFD \u2192 ${path6.basename(bak)})`));
4300
+ } catch {
4301
+ return;
4302
+ }
4303
+ pruneBackups(file, 3);
4304
+ }
4305
+ function pruneBackups(file, keep) {
4306
+ try {
4307
+ const dir = path6.dirname(file);
4308
+ const prefix = path6.basename(file) + ".bak-";
4309
+ const olds = fs6.readdirSync(dir).filter((f) => f.startsWith(prefix)).map((f) => ({ f, mt: fs6.statSync(path6.join(dir, f)).mtimeMs })).sort((a, b) => b.mt - a.mt);
4310
+ for (const o of olds.slice(keep)) {
4311
+ try {
4312
+ fs6.unlinkSync(path6.join(dir, o.f));
4313
+ } catch {
4314
+ }
4315
+ }
4298
4316
  } catch {
4299
4317
  }
4300
4318
  }
@@ -4328,15 +4346,25 @@ async function install(_client, opts) {
4328
4346
  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"));
4329
4347
  }
4330
4348
  if (opts.mcp !== false) {
4331
- const claudeJson = path6.join(os2.homedir(), ".claude.json");
4332
- backup(claudeJson);
4333
- const cc = readJson2(claudeJson) ?? {};
4334
- const mcpServers = cc.mcpServers ?? {};
4335
- mcpServers.exomind = { command: "exomind", args: ["mcp"] };
4336
- cc.mcpServers = mcpServers;
4337
- fs6.writeFileSync(claudeJson, JSON.stringify(cc, null, 2) + "\n");
4349
+ const ccJson = path6.join(os2.homedir(), ".claude.json");
4350
+ backup(ccJson);
4351
+ const cc = readJson2(ccJson) ?? {};
4352
+ const ccMcp = cc.mcpServers ?? {};
4353
+ ccMcp.exomind = { command: "exomind", args: ["mcp"] };
4354
+ cc.mcpServers = ccMcp;
4355
+ fs6.writeFileSync(ccJson, JSON.stringify(cc, null, 2) + "\n");
4356
+ const ocJson = path6.join(os2.homedir(), ".config", "opencode", "opencode.json");
4357
+ fs6.mkdirSync(path6.dirname(ocJson), { recursive: true });
4358
+ backup(ocJson);
4359
+ const oc = readJson2(ocJson) ?? {};
4360
+ const ocMcp = oc.mcp ?? {};
4361
+ ocMcp.exomind = { type: "local", command: ["exomind", "mcp"] };
4362
+ oc.mcp = ocMcp;
4363
+ fs6.writeFileSync(ocJson, JSON.stringify(oc, null, 2) + "\n");
4338
4364
  console.log(ok("\u5DF2\u914D\u7F6E MCP server \u2192 exomind mcp"));
4339
- console.log(dim(" \u2192 ~/.claude.json (mcpServers.exomind);\u91CD\u542F Claude Code \u540E Agent \u62FF\u5230 mcp__exomind__* \u5DE5\u5177"));
4365
+ console.log(dim(" \u2192 Claude Code: ~/.claude.json (mcpServers.exomind)"));
4366
+ console.log(dim(" \u2192 OpenCode: ~/.config/opencode/opencode.json (mcp.exomind)"));
4367
+ console.log(dim(" \u91CD\u542F\u5BF9\u5E94 Agent \u2192 \u62FF\u5230 mcp__exomind__* \u5DE5\u5177"));
4340
4368
  }
4341
4369
  console.log(yellow("\n\u4E0B\u4E00\u6B65:"));
4342
4370
  console.log(dim(" 1. \u82E5\u672A\u914D\u7F6E\u51ED\u8BC1: exomind login"));
@@ -4424,7 +4452,7 @@ program2.command("mcp").description("\u542F\u52A8 stdio MCP server(\u4F9B Claude
4424
4452
  const client = new ApiClient(cfg);
4425
4453
  await runMcpServer(client);
4426
4454
  });
4427
- 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").action(run(install));
4455
+ 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));
4428
4456
  async function main() {
4429
4457
  const first = process.argv[2];
4430
4458
  if (first && /^(-v|--version)$/.test(first)) {