exomind 0.3.2 → 0.3.4
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 +14 -0
- package/dist/cli.js +104 -32
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
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
|
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.
|
|
3122
|
+
version: "0.3.4",
|
|
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"
|
|
@@ -3172,10 +3172,14 @@ function opTimeout(defaultMs) {
|
|
|
3172
3172
|
var ApiError = class extends Error {
|
|
3173
3173
|
status;
|
|
3174
3174
|
detail;
|
|
3175
|
-
|
|
3175
|
+
headers;
|
|
3176
|
+
body;
|
|
3177
|
+
constructor(status, detail, headers = {}, body = null) {
|
|
3176
3178
|
super(`HTTP ${status}: ${detail}`);
|
|
3177
3179
|
this.status = status;
|
|
3178
3180
|
this.detail = detail;
|
|
3181
|
+
this.headers = headers;
|
|
3182
|
+
this.body = body;
|
|
3179
3183
|
}
|
|
3180
3184
|
};
|
|
3181
3185
|
var ApiClient = class {
|
|
@@ -3219,13 +3223,18 @@ var ApiClient = class {
|
|
|
3219
3223
|
});
|
|
3220
3224
|
const text = await res.text();
|
|
3221
3225
|
if (!res.ok) {
|
|
3226
|
+
const headers = {};
|
|
3227
|
+
res.headers?.forEach((v, k) => {
|
|
3228
|
+
headers[k.toLowerCase()] = v;
|
|
3229
|
+
});
|
|
3222
3230
|
let detail = text || res.statusText;
|
|
3231
|
+
let body = null;
|
|
3223
3232
|
try {
|
|
3224
|
-
|
|
3225
|
-
detail =
|
|
3233
|
+
body = JSON.parse(text);
|
|
3234
|
+
detail = body.detail || body.message || JSON.stringify(body);
|
|
3226
3235
|
} catch {
|
|
3227
3236
|
}
|
|
3228
|
-
throw new ApiError(res.status, String(detail).slice(0, 800));
|
|
3237
|
+
throw new ApiError(res.status, String(detail).slice(0, 800), headers, body);
|
|
3229
3238
|
}
|
|
3230
3239
|
if (opts.text) return text;
|
|
3231
3240
|
try {
|
|
@@ -3881,6 +3890,55 @@ function planIngestest(files, manifest, force) {
|
|
|
3881
3890
|
}
|
|
3882
3891
|
return { toIngest, toSkip };
|
|
3883
3892
|
}
|
|
3893
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
3894
|
+
var suspended = false;
|
|
3895
|
+
async function ingestWithRetry(client, payload, timeoutMs) {
|
|
3896
|
+
let rateAttempts = 0;
|
|
3897
|
+
let quotaWaits = 0;
|
|
3898
|
+
while (true) {
|
|
3899
|
+
try {
|
|
3900
|
+
return await client.post("/ingest", payload, { timeoutMs });
|
|
3901
|
+
} catch (e) {
|
|
3902
|
+
if (!(e instanceof ApiError) || e.status !== 429) throw e;
|
|
3903
|
+
const type = e.body?.type;
|
|
3904
|
+
if (type === "daily_quota") {
|
|
3905
|
+
if (++quotaWaits > 3) throw new ApiError(429, "\u914D\u989D\u8FDE\u7EED 3 \u4E2A\u81EA\u7136\u65E5\u672A\u6062\u590D,\u653E\u5F03");
|
|
3906
|
+
await suspendUntilMidnight(Number(e.body?.reset ?? 0));
|
|
3907
|
+
continue;
|
|
3908
|
+
}
|
|
3909
|
+
if (type === "rate_limit") {
|
|
3910
|
+
if (++rateAttempts >= 5) throw new ApiError(429, "\u5E76\u53D1\u9650\u6D41,\u91CD\u8BD5 5 \u6B21\u4ECD\u5931\u8D25");
|
|
3911
|
+
const retry = Number(e.headers["retry-after"] ?? e.body?.retry_after ?? 5);
|
|
3912
|
+
process.stderr.write(dim(` \u23F8 \u5E76\u53D1\u9650\u6D41,${retry}s \u540E\u91CD\u8BD5
|
|
3913
|
+
`));
|
|
3914
|
+
await sleep(retry * 1e3);
|
|
3915
|
+
continue;
|
|
3916
|
+
}
|
|
3917
|
+
throw e;
|
|
3918
|
+
}
|
|
3919
|
+
}
|
|
3920
|
+
}
|
|
3921
|
+
async function suspendUntilMidnight(resetEpoch) {
|
|
3922
|
+
suspended = true;
|
|
3923
|
+
const now = Date.now();
|
|
3924
|
+
const waitMs = resetEpoch > 0 ? Math.max(1e3, resetEpoch * 1e3 - now) : 60 * 1e3;
|
|
3925
|
+
process.stderr.write(
|
|
3926
|
+
dim(` \u23F8 \u4ECA\u65E5\u914D\u989D\u5DF2\u6EE1,\u6302\u8D77\u5230\u6B21\u65E5 0 \u70B9\u7EED\u8DD1(\u7EA6 ${Math.ceil(waitMs / 6e4)} \u5206\u949F)
|
|
3927
|
+
`)
|
|
3928
|
+
);
|
|
3929
|
+
process.stderr.write(dim(" Ctrl+C \u53EF\u5B89\u5168\u9000\u51FA(\u5DF2\u4FDD\u5B58\u8FDB\u5EA6),\u6B21\u65E5\u91CD\u8DD1\u540C\u547D\u4EE4\u5373\u53EF\u7EED\u8DD1\n"));
|
|
3930
|
+
const start = Date.now();
|
|
3931
|
+
while (Date.now() - start < waitMs) {
|
|
3932
|
+
const remaining = waitMs - (Date.now() - start);
|
|
3933
|
+
await sleep(Math.min(6e4, remaining));
|
|
3934
|
+
const remainMin = Math.ceil((waitMs - (Date.now() - start)) / 6e4);
|
|
3935
|
+
if (remainMin > 0) process.stderr.write(`\r ${dim(`\u5269\u4F59\u7EA6 ${remainMin} \u5206\u949F`)} `);
|
|
3936
|
+
}
|
|
3937
|
+
process.stderr.write(`
|
|
3938
|
+
${dim("\u5230\u8FBE 0 \u70B9,\u7EE7\u7EED\u6444\u5165\u2026")}
|
|
3939
|
+
`);
|
|
3940
|
+
suspended = false;
|
|
3941
|
+
}
|
|
3884
3942
|
async function runDirIngestest(client, opts, dir) {
|
|
3885
3943
|
const files = walkDir(dir, !!opts.recursive, opts.pattern || "*.md");
|
|
3886
3944
|
if (!files.length) {
|
|
@@ -3894,39 +3952,51 @@ async function runDirIngestest(client, opts, dir) {
|
|
|
3894
3952
|
`\u76EE\u5F55 ${dir}: ${total} \u6587\u4EF6 \u2014 \u5F85\u6444\u5165 ${plan.toIngest.length},\u8DF3\u8FC7 ${plan.toSkip.length}
|
|
3895
3953
|
`
|
|
3896
3954
|
);
|
|
3955
|
+
const onSigInt = () => {
|
|
3956
|
+
if (suspended) {
|
|
3957
|
+
saveManifest(manifest);
|
|
3958
|
+
process.stderr.write("\n\u5DF2\u4FDD\u5B58\u8FDB\u5EA6,\u9000\u51FA\u3002\u6B21\u65E5\u91CD\u8DD1\u540C\u547D\u4EE4\u5373\u53EF\u7EED\u8DD1\u3002\n");
|
|
3959
|
+
}
|
|
3960
|
+
process.exit(130);
|
|
3961
|
+
};
|
|
3962
|
+
process.on("SIGINT", onSigInt);
|
|
3897
3963
|
let added = 0;
|
|
3898
3964
|
let updated = 0;
|
|
3899
3965
|
let failed = 0;
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3966
|
+
try {
|
|
3967
|
+
for (let i = 0; i < plan.toIngest.length; i++) {
|
|
3968
|
+
const f = plan.toIngest[i];
|
|
3969
|
+
const prev = manifest[f.path];
|
|
3970
|
+
process.stderr.write(`\u23F3 [${i + 1}/${plan.toIngest.length}] ${path4.basename(f.path)}\u2026
|
|
3904
3971
|
`);
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3972
|
+
try {
|
|
3973
|
+
const res = await ingestWithRetry(
|
|
3974
|
+
client,
|
|
3975
|
+
{ content: f.content, title: f.title, tags: opts.tag },
|
|
3976
|
+
opTimeout(3e5)
|
|
3977
|
+
);
|
|
3978
|
+
recordFile(manifest, f.path, f.content, f.title);
|
|
3979
|
+
saveManifest(manifest);
|
|
3980
|
+
if (prev) {
|
|
3981
|
+
updated++;
|
|
3982
|
+
process.stderr.write(` ${green("\u2713")} \u66F4\u65B0 \u2014 \u5B9E\u4F53 ${res.entities ?? 0}/\u6982\u5FF5 ${res.concepts ?? 0}
|
|
3916
3983
|
`);
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3984
|
+
} else {
|
|
3985
|
+
added++;
|
|
3986
|
+
process.stderr.write(` ${green("\u2713")} \u65B0\u589E \u2014 \u5B9E\u4F53 ${res.entities ?? 0}/\u6982\u5FF5 ${res.concepts ?? 0}
|
|
3920
3987
|
`);
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3988
|
+
}
|
|
3989
|
+
} catch (e) {
|
|
3990
|
+
failed++;
|
|
3991
|
+
process.stderr.write(` ${red("\u2717")} ${e.message}
|
|
3925
3992
|
`);
|
|
3993
|
+
}
|
|
3926
3994
|
}
|
|
3995
|
+
cleanupStale(manifest, dir, files);
|
|
3996
|
+
saveManifest(manifest);
|
|
3997
|
+
} finally {
|
|
3998
|
+
process.removeListener("SIGINT", onSigInt);
|
|
3927
3999
|
}
|
|
3928
|
-
cleanupStale(manifest, dir, files);
|
|
3929
|
-
saveManifest(manifest);
|
|
3930
4000
|
const allUpToDate = added + updated === 0 && plan.toSkip.length > 0 && failed === 0;
|
|
3931
4001
|
output(
|
|
3932
4002
|
{ added, updated, skipped: plan.toSkip.length, failed, total, dir, allUpToDate },
|
|
@@ -4228,22 +4298,24 @@ async function login(_client, opts) {
|
|
|
4228
4298
|
token = await prompt("\u51ED\u8BC1: ");
|
|
4229
4299
|
}
|
|
4230
4300
|
if (!token) throw new Error("\u672A\u63D0\u4F9B\u51ED\u8BC1");
|
|
4231
|
-
saveConfig({ base_url: baseUrl, api_key: token });
|
|
4232
4301
|
const probe = new ApiClient({ base_url: baseUrl, api_key: token });
|
|
4302
|
+
let verified = true;
|
|
4233
4303
|
try {
|
|
4234
4304
|
await probe.get("/keywords");
|
|
4235
4305
|
} catch (e) {
|
|
4236
4306
|
if (e instanceof ApiError && (e.status === 401 || e.status === 403)) {
|
|
4237
4307
|
throw new Error(`\u51ED\u8BC1\u6821\u9A8C\u5931\u8D25 (HTTP ${e.status}): ${e.detail}`);
|
|
4238
4308
|
}
|
|
4309
|
+
verified = false;
|
|
4239
4310
|
console.log(
|
|
4240
4311
|
yellow(
|
|
4241
|
-
`\u8B66\u544A: \u65E0\u6CD5\u8FDE\u63A5 ${baseUrl} \u6821\u9A8C\u51ED\u8BC1 (${e instanceof Error ? e.message : String(e)})\u3002\u914D\u7F6E\
|
|
4312
|
+
`\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
4313
|
)
|
|
4243
4314
|
);
|
|
4244
4315
|
}
|
|
4316
|
+
saveConfig({ base_url: baseUrl, api_key: token });
|
|
4245
4317
|
const hint2 = token.length > 12 ? `${token.slice(0, 8)}\u2026${token.slice(-4)}` : token;
|
|
4246
|
-
console.log(ok("\u767B\u5F55\u6210\u529F"));
|
|
4318
|
+
console.log(ok(verified ? "\u767B\u5F55\u6210\u529F" : "\u767B\u5F55\u6210\u529F(\u672A\u6821\u9A8C)"));
|
|
4247
4319
|
console.log(dim(` \u670D\u52A1\u5668: ${baseUrl}`));
|
|
4248
4320
|
console.log(dim(` \u51ED\u8BC1: ${hint2} (${token.startsWith("gh_") ? "GitHub token" : "API Key"})`));
|
|
4249
4321
|
}
|