echopai 2.2.0 → 2.4.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 +63 -348
- package/dist/bin.js +8302 -149
- package/package.json +11 -13
- package/dist/_generated/commands.js +0 -282
- package/dist/_generated/help.js +0 -195
- package/dist/_generated/operations.js +0 -1529
- package/dist/runtime/auth.js +0 -95
- package/dist/runtime/envelope.js +0 -52
- package/dist/runtime/errors.js +0 -186
- package/dist/runtime/filters.js +0 -153
- package/dist/runtime/format.js +0 -143
- package/dist/runtime/http.js +0 -65
- package/dist/runtime/idempotency.js +0 -18
- package/dist/runtime/invoker.js +0 -387
- package/dist/runtime/io.js +0 -16
- package/dist/runtime/paginator.js +0 -146
- package/dist/runtime/trace.js +0 -99
- package/dist/runtime/tty.js +0 -51
- package/dist/runtime/verb_cmd.js +0 -70
- package/dist/runtime/verb_runner.js +0 -152
- package/dist/runtime/whoami_cache.js +0 -109
- package/dist/tools/api.js +0 -81
- package/dist/tools/completion.js +0 -116
- package/dist/tools/config.js +0 -123
- package/dist/tools/doctor.js +0 -183
- package/dist/tools/login.js +0 -99
- package/dist/tools/mcp.js +0 -141
- package/dist/tools/raw.js +0 -96
- package/dist/tools/schema.js +0 -58
- package/dist/tools/trace.js +0 -54
- package/dist/tools/welcome.js +0 -190
- package/dist/tools/whoami.js +0 -132
- package/dist/verbs/_spec.js +0 -15
- package/dist/verbs/bars_batch.js +0 -66
- package/dist/verbs/chart.js +0 -110
- package/dist/verbs/digest.js +0 -344
- package/dist/verbs/financials.js +0 -212
- package/dist/verbs/hot.js +0 -29
- package/dist/verbs/index.js +0 -57
- package/dist/verbs/lookup.js +0 -72
- package/dist/verbs/news.js +0 -67
- package/dist/verbs/quote.js +0 -53
- package/dist/verbs/scan.js +0 -42
- package/dist/verbs/search.js +0 -105
- package/dist/verbs/sentiment.js +0 -46
- package/dist/verbs/views.js +0 -83
- package/dist/version.js +0 -5
package/dist/tools/config.js
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai config [...]`
|
|
3
|
-
*
|
|
4
|
-
* echopai config show 显示完整 config
|
|
5
|
-
* echopai config set default_profile <name> 切默认 profile
|
|
6
|
-
*
|
|
7
|
-
* echopai config profile add <name> --key <eps> [--base-url <url>]
|
|
8
|
-
* echopai config profile rm <name>
|
|
9
|
-
* echopai config profile list
|
|
10
|
-
* echopai config profile switch <name> ≡ set default_profile
|
|
11
|
-
*
|
|
12
|
-
* 配置文件 ~/.config/echopai/config.toml(XDG)。文件 mode 0600。
|
|
13
|
-
*/
|
|
14
|
-
import { Command } from "commander";
|
|
15
|
-
import { configPath, readConfigFile, writeConfigFile, } from "../runtime/auth.js";
|
|
16
|
-
export function buildConfigCommand() {
|
|
17
|
-
const cmd = new Command("config").description("Manage CLI config (~/.config/echopai/config.toml)");
|
|
18
|
-
cmd
|
|
19
|
-
.command("show")
|
|
20
|
-
.description("Print current config (key 字段会脱敏成 eps_live_***_***)")
|
|
21
|
-
.action(() => {
|
|
22
|
-
const cfg = readConfigFile();
|
|
23
|
-
const masked = JSON.parse(JSON.stringify(cfg));
|
|
24
|
-
if (masked.profiles) {
|
|
25
|
-
for (const p of Object.values(masked.profiles)) {
|
|
26
|
-
if (p && typeof p === "object" && typeof p.key === "string") {
|
|
27
|
-
p.key = redactKey(p.key);
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
process.stdout.write(JSON.stringify({ data: masked, meta: { path: configPath() } }, null, 2) + "\n");
|
|
32
|
-
process.exit(0);
|
|
33
|
-
});
|
|
34
|
-
cmd
|
|
35
|
-
.command("set <key> <value>")
|
|
36
|
-
.description("Set a top-level config key (currently only 'default_profile' is recognized)")
|
|
37
|
-
.action((key, value) => {
|
|
38
|
-
const cfg = readConfigFile();
|
|
39
|
-
if (key === "default_profile") {
|
|
40
|
-
cfg.default_profile = value;
|
|
41
|
-
writeConfigFile(cfg);
|
|
42
|
-
process.stdout.write(JSON.stringify({ data: { ok: true }, meta: { path: configPath() } }) + "\n");
|
|
43
|
-
process.exit(0);
|
|
44
|
-
}
|
|
45
|
-
writeError("invalid_args", `Unknown top-level config key '${key}'`, "Use one of: default_profile", 1);
|
|
46
|
-
});
|
|
47
|
-
const profile = cmd.command("profile").description("Manage credential profiles");
|
|
48
|
-
profile
|
|
49
|
-
.command("add <name>")
|
|
50
|
-
.description("Add or overwrite a profile")
|
|
51
|
-
.requiredOption("--key <eps_live_X_Y>", "Bearer credential")
|
|
52
|
-
.option("--base-url <url>", "API base URL", "https://api.echopai.com")
|
|
53
|
-
.action((name, opts) => {
|
|
54
|
-
const cfg = readConfigFile();
|
|
55
|
-
cfg.profiles = cfg.profiles ?? {};
|
|
56
|
-
cfg.profiles[name] = { key: opts.key, base_url: opts.baseUrl };
|
|
57
|
-
if (!cfg.default_profile)
|
|
58
|
-
cfg.default_profile = name;
|
|
59
|
-
writeConfigFile(cfg);
|
|
60
|
-
process.stdout.write(JSON.stringify({ data: { ok: true, name, default: cfg.default_profile === name }, meta: { path: configPath() } }) + "\n");
|
|
61
|
-
process.exit(0);
|
|
62
|
-
});
|
|
63
|
-
profile
|
|
64
|
-
.command("rm <name>")
|
|
65
|
-
.description("Remove a profile")
|
|
66
|
-
.action((name) => {
|
|
67
|
-
const cfg = readConfigFile();
|
|
68
|
-
if (!cfg.profiles?.[name]) {
|
|
69
|
-
writeError("profile_not_found", `Profile '${name}' not in config.`, undefined, 1);
|
|
70
|
-
}
|
|
71
|
-
delete cfg.profiles[name];
|
|
72
|
-
if (cfg.default_profile === name)
|
|
73
|
-
cfg.default_profile = undefined;
|
|
74
|
-
writeConfigFile(cfg);
|
|
75
|
-
process.stdout.write(JSON.stringify({ data: { ok: true, removed: name }, meta: { path: configPath() } }) + "\n");
|
|
76
|
-
process.exit(0);
|
|
77
|
-
});
|
|
78
|
-
profile
|
|
79
|
-
.command("list")
|
|
80
|
-
.description("List all profiles (keys redacted)")
|
|
81
|
-
.action(() => {
|
|
82
|
-
const cfg = readConfigFile();
|
|
83
|
-
const items = Object.entries(cfg.profiles ?? {}).map(([name, p]) => ({
|
|
84
|
-
name,
|
|
85
|
-
is_default: cfg.default_profile === name,
|
|
86
|
-
base_url: p?.base_url ?? "https://api.echopai.com",
|
|
87
|
-
key_redacted: p?.key ? redactKey(p.key) : null,
|
|
88
|
-
}));
|
|
89
|
-
process.stdout.write(JSON.stringify({ data: items, meta: { path: configPath() } }) + "\n");
|
|
90
|
-
process.exit(0);
|
|
91
|
-
});
|
|
92
|
-
profile
|
|
93
|
-
.command("switch <name>")
|
|
94
|
-
.description("Set default_profile")
|
|
95
|
-
.action((name) => {
|
|
96
|
-
const cfg = readConfigFile();
|
|
97
|
-
if (!cfg.profiles?.[name]) {
|
|
98
|
-
writeError("profile_not_found", `Profile '${name}' not in config.`, "Run `echopai config profile list` to see available.", 1);
|
|
99
|
-
}
|
|
100
|
-
cfg.default_profile = name;
|
|
101
|
-
writeConfigFile(cfg);
|
|
102
|
-
process.stdout.write(JSON.stringify({ data: { ok: true, default_profile: name } }) + "\n");
|
|
103
|
-
process.exit(0);
|
|
104
|
-
});
|
|
105
|
-
return cmd;
|
|
106
|
-
}
|
|
107
|
-
export function redactKey(key) {
|
|
108
|
-
// eps_live_<lookup>_<secret> → eps_live_<lookup>_*** (last 4 chars)
|
|
109
|
-
if (!key.startsWith("eps_live_"))
|
|
110
|
-
return "eps_***";
|
|
111
|
-
const parts = key.split("_");
|
|
112
|
-
if (parts.length < 4)
|
|
113
|
-
return "eps_live_***";
|
|
114
|
-
const last = parts.slice(3).join("_");
|
|
115
|
-
return `eps_live_${parts[2]}_***${last.slice(-4)}`;
|
|
116
|
-
}
|
|
117
|
-
function writeError(code, message, recovery_hint, exitCode) {
|
|
118
|
-
const env = { error: { code, message } };
|
|
119
|
-
if (recovery_hint)
|
|
120
|
-
env.error.recovery_hint = recovery_hint;
|
|
121
|
-
process.stderr.write(JSON.stringify(env) + "\n");
|
|
122
|
-
process.exit(exitCode);
|
|
123
|
-
}
|
package/dist/tools/doctor.js
DELETED
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai doctor`
|
|
3
|
-
*
|
|
4
|
-
* 跑一组健康检查,每条 ok | warn | fail。所有 ok → exit 0;存在 fail → exit 1。
|
|
5
|
-
*
|
|
6
|
-
* 当前覆盖(Phase 2.4 起步集):
|
|
7
|
-
* credential 本地 token / profile 能解析
|
|
8
|
-
* whoami /v1/auth/whoami 可达且 200
|
|
9
|
-
* operations token 至少能调到一条 operation
|
|
10
|
-
* api_version CLI 期望 api_version 与 server 报告兼容
|
|
11
|
-
*
|
|
12
|
-
* 未来扩展:clock_skew (Date header) / ws_news (1-frame 探测) / scope_coverage
|
|
13
|
-
* (verb tier 满足度)。
|
|
14
|
-
*/
|
|
15
|
-
import { Command, Option } from "commander";
|
|
16
|
-
import { listOperations } from "../_generated/operations.js";
|
|
17
|
-
import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
|
|
18
|
-
import { CallApiError } from "../runtime/errors.js";
|
|
19
|
-
import { isTtyHuman, cyan, dim, green, red, yellow } from "../runtime/tty.js";
|
|
20
|
-
import { clearWhoamiCache, getWhoami } from "../runtime/whoami_cache.js";
|
|
21
|
-
import { deriveOperationAvailability } from "./whoami.js";
|
|
22
|
-
import { CLI_VERSION } from "../version.js";
|
|
23
|
-
const EXPECTED_API_VERSION = "2026-05"; // bump when CLI hard-depends on server change
|
|
24
|
-
export async function runDoctor(ctx) {
|
|
25
|
-
const checks = [];
|
|
26
|
-
const resolveFn = ctx.resolveCredentialsImpl ?? resolveCredentials;
|
|
27
|
-
const whoamiFn = ctx.getWhoamiImpl ?? getWhoami;
|
|
28
|
-
// 1. credential resolution
|
|
29
|
-
let creds = null;
|
|
30
|
-
try {
|
|
31
|
-
creds = resolveFn({});
|
|
32
|
-
checks.push({ name: "credential", status: "ok" });
|
|
33
|
-
}
|
|
34
|
-
catch (e) {
|
|
35
|
-
if (e instanceof AuthMissingError) {
|
|
36
|
-
checks.push({
|
|
37
|
-
name: "credential",
|
|
38
|
-
status: "fail",
|
|
39
|
-
message: e.message,
|
|
40
|
-
recovery_hint: e.recovery_hint ?? "Run `echopai login` or set `ECHOPAI_KEY`.",
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
else {
|
|
44
|
-
checks.push({
|
|
45
|
-
name: "credential",
|
|
46
|
-
status: "fail",
|
|
47
|
-
message: e instanceof Error ? e.message : String(e),
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
if (!creds) {
|
|
52
|
-
// Subsequent checks all require a credential.
|
|
53
|
-
for (const name of ["whoami", "operations", "api_version"]) {
|
|
54
|
-
checks.push({
|
|
55
|
-
name,
|
|
56
|
-
status: "fail",
|
|
57
|
-
message: "skipped: credential unavailable",
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
return { ok: false, cli_version: ctx.cliVersion, checks };
|
|
61
|
-
}
|
|
62
|
-
// 2. whoami reachable
|
|
63
|
-
let whoami;
|
|
64
|
-
try {
|
|
65
|
-
whoami = await whoamiFn({ baseUrl: creds.baseUrl, bearer: creds.key, cliVersion: ctx.cliVersion }, { force: true });
|
|
66
|
-
checks.push({
|
|
67
|
-
name: "whoami",
|
|
68
|
-
status: "ok",
|
|
69
|
-
info: {
|
|
70
|
-
kind: whoami.kind,
|
|
71
|
-
scope_count: whoami.scopes.length,
|
|
72
|
-
...(whoami.app_slug ? { app_slug: whoami.app_slug } : {}),
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
catch (e) {
|
|
77
|
-
const code = e instanceof CallApiError ? e.code : "internal_error";
|
|
78
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
79
|
-
const hint = e instanceof CallApiError ? e.recovery_hint : undefined;
|
|
80
|
-
checks.push({
|
|
81
|
-
name: "whoami",
|
|
82
|
-
status: "fail",
|
|
83
|
-
message: `${code}: ${msg}`,
|
|
84
|
-
...(hint ? { recovery_hint: hint } : {}),
|
|
85
|
-
});
|
|
86
|
-
// Without whoami, downstream checks are meaningless.
|
|
87
|
-
for (const name of ["operations", "api_version"]) {
|
|
88
|
-
checks.push({
|
|
89
|
-
name,
|
|
90
|
-
status: "fail",
|
|
91
|
-
message: "skipped: whoami failed",
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
return { ok: false, cli_version: ctx.cliVersion, checks };
|
|
95
|
-
}
|
|
96
|
-
// 3. operations: at least one usable op
|
|
97
|
-
const { available, unavailable } = deriveOperationAvailability(new Set(whoami.scopes), listOperations());
|
|
98
|
-
if (available.length === 0) {
|
|
99
|
-
checks.push({
|
|
100
|
-
name: "operations",
|
|
101
|
-
status: "fail",
|
|
102
|
-
message: `Token scopes ${JSON.stringify(whoami.scopes)} grant access to 0 operations.`,
|
|
103
|
-
recovery_hint: "Request additional scopes (e.g. news:read / quote:l1) from your app admin.",
|
|
104
|
-
info: { available: 0, unavailable: unavailable.length },
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
checks.push({
|
|
109
|
-
name: "operations",
|
|
110
|
-
status: "ok",
|
|
111
|
-
info: { available: available.length, unavailable: unavailable.length },
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
// 4. api_version compatibility
|
|
115
|
-
const serverVersion = whoami.api_version;
|
|
116
|
-
if (!serverVersion) {
|
|
117
|
-
checks.push({
|
|
118
|
-
name: "api_version",
|
|
119
|
-
status: "warn",
|
|
120
|
-
message: "Server did not report api_version (older deployment).",
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
else if (serverVersion !== EXPECTED_API_VERSION) {
|
|
124
|
-
checks.push({
|
|
125
|
-
name: "api_version",
|
|
126
|
-
status: "warn",
|
|
127
|
-
message: `CLI expects ${EXPECTED_API_VERSION}, server reports ${serverVersion}.`,
|
|
128
|
-
recovery_hint: "Usually safe (additive changes). Upgrade CLI if you hit unexpected errors.",
|
|
129
|
-
info: { cli_expected: EXPECTED_API_VERSION, server: serverVersion },
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
else {
|
|
133
|
-
checks.push({
|
|
134
|
-
name: "api_version",
|
|
135
|
-
status: "ok",
|
|
136
|
-
info: { version: serverVersion },
|
|
137
|
-
});
|
|
138
|
-
}
|
|
139
|
-
const ok = checks.every((c) => c.status !== "fail");
|
|
140
|
-
return { ok, cli_version: ctx.cliVersion, checks };
|
|
141
|
-
}
|
|
142
|
-
export function renderDoctorReport(report) {
|
|
143
|
-
if (!isTtyHuman)
|
|
144
|
-
return JSON.stringify({ data: report }) + "\n";
|
|
145
|
-
const lines = [];
|
|
146
|
-
lines.push(`${cyan("echopai doctor")} ${dim("cli " + report.cli_version)}`);
|
|
147
|
-
lines.push("");
|
|
148
|
-
for (const c of report.checks) {
|
|
149
|
-
const icon = c.status === "ok" ? green("✓") : c.status === "warn" ? yellow("!") : red("✗");
|
|
150
|
-
const head = ` ${icon} ${c.name}`;
|
|
151
|
-
if (c.message) {
|
|
152
|
-
lines.push(`${head} ${dim("—")} ${c.message}`);
|
|
153
|
-
}
|
|
154
|
-
else if (c.info) {
|
|
155
|
-
lines.push(`${head} ${dim(JSON.stringify(c.info))}`);
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
lines.push(head);
|
|
159
|
-
}
|
|
160
|
-
if (c.recovery_hint)
|
|
161
|
-
lines.push(` ${cyan("hint:")} ${c.recovery_hint}`);
|
|
162
|
-
}
|
|
163
|
-
lines.push("");
|
|
164
|
-
lines.push(report.ok ? green("All checks passed.") : red("One or more checks failed."));
|
|
165
|
-
return lines.join("\n") + "\n";
|
|
166
|
-
}
|
|
167
|
-
export function buildDoctorCommand() {
|
|
168
|
-
const cmd = new Command("doctor").description("Diagnose CLI environment, credential, server reachability, and capability");
|
|
169
|
-
cmd.addOption(new Option("--no-cache", "Bypass whoami cache for fresh check"));
|
|
170
|
-
cmd.action(async (opts) => {
|
|
171
|
-
if (opts.cache === false)
|
|
172
|
-
clearWhoamiCache();
|
|
173
|
-
const report = await runDoctor({ cliVersion: CLI_VERSION });
|
|
174
|
-
if (isTtyHuman) {
|
|
175
|
-
process.stdout.write(renderDoctorReport(report));
|
|
176
|
-
}
|
|
177
|
-
else {
|
|
178
|
-
process.stdout.write(JSON.stringify({ data: report }) + "\n");
|
|
179
|
-
}
|
|
180
|
-
process.exit(report.ok ? 0 : 1);
|
|
181
|
-
});
|
|
182
|
-
return cmd;
|
|
183
|
-
}
|
package/dist/tools/login.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai login --key <eps_live_X_Y> [--profile <name>] [--base-url <url>]`
|
|
3
|
-
* `echopai logout [--profile <name>]`
|
|
4
|
-
* `echopai status [--profile <name>]`
|
|
5
|
-
*
|
|
6
|
-
* login 把 key 写入 config profile(默认 'default')。不在 login 时 round-trip
|
|
7
|
-
* 验证 key —— 第一次实际调用会自然报 auth_invalid。
|
|
8
|
-
*
|
|
9
|
-
* status 显示当前 profile / base_url / 脱敏 key prefix。
|
|
10
|
-
*/
|
|
11
|
-
import { Command } from "commander";
|
|
12
|
-
import { configPath, readConfigFile, resolveCredentials, writeConfigFile, AuthMissingError, } from "../runtime/auth.js";
|
|
13
|
-
import { redactKey } from "./config.js";
|
|
14
|
-
export function buildLoginCommand() {
|
|
15
|
-
return new Command("login")
|
|
16
|
-
.description("Save Bearer credential to config profile")
|
|
17
|
-
.requiredOption("--key <eps_live_X_Y>", "Bearer credential issued by EchoPai admin console")
|
|
18
|
-
.option("--profile <name>", "Profile to save under", "default")
|
|
19
|
-
.option("--base-url <url>", "API base URL", "https://api.echopai.com")
|
|
20
|
-
.action((opts) => {
|
|
21
|
-
const cfg = readConfigFile();
|
|
22
|
-
cfg.profiles = cfg.profiles ?? {};
|
|
23
|
-
cfg.profiles[opts.profile] = { key: opts.key, base_url: opts.baseUrl };
|
|
24
|
-
if (!cfg.default_profile)
|
|
25
|
-
cfg.default_profile = opts.profile;
|
|
26
|
-
writeConfigFile(cfg);
|
|
27
|
-
process.stdout.write(JSON.stringify({
|
|
28
|
-
data: {
|
|
29
|
-
ok: true,
|
|
30
|
-
profile: opts.profile,
|
|
31
|
-
base_url: opts.baseUrl,
|
|
32
|
-
key_redacted: redactKey(opts.key),
|
|
33
|
-
default: cfg.default_profile === opts.profile,
|
|
34
|
-
},
|
|
35
|
-
meta: { path: configPath() },
|
|
36
|
-
}) + "\n");
|
|
37
|
-
process.exit(0);
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
export function buildLogoutCommand() {
|
|
41
|
-
return new Command("logout")
|
|
42
|
-
.description("Remove Bearer credential from config profile")
|
|
43
|
-
.option("--profile <name>", "Profile to clear (default = default_profile)")
|
|
44
|
-
.action((opts) => {
|
|
45
|
-
const cfg = readConfigFile();
|
|
46
|
-
const target = opts.profile || cfg.default_profile;
|
|
47
|
-
if (!target || !cfg.profiles?.[target]) {
|
|
48
|
-
process.stderr.write(JSON.stringify({
|
|
49
|
-
error: {
|
|
50
|
-
code: "profile_not_found",
|
|
51
|
-
message: `No profile '${target ?? "(none)"}' configured.`,
|
|
52
|
-
},
|
|
53
|
-
}) + "\n");
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
delete cfg.profiles[target];
|
|
57
|
-
if (cfg.default_profile === target)
|
|
58
|
-
cfg.default_profile = undefined;
|
|
59
|
-
writeConfigFile(cfg);
|
|
60
|
-
process.stdout.write(JSON.stringify({ data: { ok: true, removed_profile: target }, meta: { path: configPath() } }) + "\n");
|
|
61
|
-
process.exit(0);
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
export function buildStatusCommand() {
|
|
65
|
-
return new Command("status")
|
|
66
|
-
.description("Show current credential / profile / base URL (key redacted)")
|
|
67
|
-
.option("--profile <name>", "Profile to inspect")
|
|
68
|
-
.action((opts) => {
|
|
69
|
-
try {
|
|
70
|
-
const resolveOpts = {};
|
|
71
|
-
if (opts.profile)
|
|
72
|
-
resolveOpts.profile = opts.profile;
|
|
73
|
-
const c = resolveCredentials(resolveOpts);
|
|
74
|
-
process.stdout.write(JSON.stringify({
|
|
75
|
-
data: {
|
|
76
|
-
source: process.env.ECHOPAI_KEY && !opts.profile
|
|
77
|
-
? "env"
|
|
78
|
-
: c.profile
|
|
79
|
-
? "config"
|
|
80
|
-
: "env",
|
|
81
|
-
profile: c.profile,
|
|
82
|
-
base_url: c.baseUrl,
|
|
83
|
-
key_redacted: redactKey(c.key),
|
|
84
|
-
},
|
|
85
|
-
meta: { config_path: configPath() },
|
|
86
|
-
}) + "\n");
|
|
87
|
-
process.exit(0);
|
|
88
|
-
}
|
|
89
|
-
catch (e) {
|
|
90
|
-
if (e instanceof AuthMissingError) {
|
|
91
|
-
process.stderr.write(JSON.stringify({
|
|
92
|
-
error: { code: "auth_missing", message: e.message, recovery_hint: e.recovery_hint },
|
|
93
|
-
}) + "\n");
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
throw e;
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
}
|
package/dist/tools/mcp.js
DELETED
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai mcp serve` — stdio MCP server.
|
|
3
|
-
*
|
|
4
|
-
* 启动流程:
|
|
5
|
-
* 1. 解析凭据 (ECHOPAI_KEY / ECHOPAI_PROFILE / --profile)
|
|
6
|
-
* 2. getWhoami(channel="mcp") 拿 token scopes
|
|
7
|
-
* 3. 用 OPERATIONS.scopesAny 把 ALL_VERB_SPECS 过滤到 token 可调子集
|
|
8
|
-
* 4. registerTool 每个可用 verb (Zod inputSchema 直接给 SDK)
|
|
9
|
-
* 5. connect stdio transport, 进 message loop
|
|
10
|
-
*
|
|
11
|
-
* Tool handler:
|
|
12
|
-
* - 收到 tools/call → 调 spec.handler(args, ctx) (channel="mcp" 注入 header)
|
|
13
|
-
* - 成功 → 返 MCP content block (JSON envelope 序列化为 text)
|
|
14
|
-
* - 失败 → CallApiError 映射为 MCP isError=true tool response
|
|
15
|
-
*
|
|
16
|
-
* 错误日志走 stderr (MCP stdio 协议: stdout 仅 JSON-RPC, stderr 可任意文本)。
|
|
17
|
-
*/
|
|
18
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
19
|
-
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
20
|
-
import { Command, Option } from "commander";
|
|
21
|
-
import { OPERATIONS } from "../_generated/operations.js";
|
|
22
|
-
import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
|
|
23
|
-
import { CallApiError } from "../runtime/errors.js";
|
|
24
|
-
import { getWhoami } from "../runtime/whoami_cache.js";
|
|
25
|
-
import { ALL_VERB_SPECS } from "../verbs/index.js";
|
|
26
|
-
import { CLI_VERSION } from "../version.js";
|
|
27
|
-
/** Verb is available iff at least one backing op is callable with token scopes. */
|
|
28
|
-
export function verbAvailable(spec, tokenScopes) {
|
|
29
|
-
if (spec.backingOps.length === 0)
|
|
30
|
-
return true;
|
|
31
|
-
for (const opKey of spec.backingOps) {
|
|
32
|
-
const op = OPERATIONS[opKey];
|
|
33
|
-
if (!op)
|
|
34
|
-
continue;
|
|
35
|
-
if (op.scopesAny.length === 0)
|
|
36
|
-
return true;
|
|
37
|
-
if (op.scopesAny.some((s) => tokenScopes.has(s)))
|
|
38
|
-
return true;
|
|
39
|
-
}
|
|
40
|
-
return false;
|
|
41
|
-
}
|
|
42
|
-
export function filterAvailableVerbs(specs, tokenScopes) {
|
|
43
|
-
return specs.filter((s) => verbAvailable(s, tokenScopes));
|
|
44
|
-
}
|
|
45
|
-
export function envelopeToToolResponse(envelope) {
|
|
46
|
-
return {
|
|
47
|
-
content: [{ type: "text", text: JSON.stringify(envelope) }],
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
export function errorToToolResponse(e) {
|
|
51
|
-
if (e instanceof CallApiError) {
|
|
52
|
-
const env = {
|
|
53
|
-
error: {
|
|
54
|
-
code: e.code,
|
|
55
|
-
message: e.message,
|
|
56
|
-
retryable: e.retryable,
|
|
57
|
-
...(e.recovery_hint ? { recovery_hint: e.recovery_hint } : {}),
|
|
58
|
-
...(e.requestId ? { request_id: e.requestId } : {}),
|
|
59
|
-
},
|
|
60
|
-
};
|
|
61
|
-
return { content: [{ type: "text", text: JSON.stringify(env) }], isError: true };
|
|
62
|
-
}
|
|
63
|
-
const env = {
|
|
64
|
-
error: {
|
|
65
|
-
code: "internal_error",
|
|
66
|
-
message: e instanceof Error ? e.message : String(e),
|
|
67
|
-
retryable: false,
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
return { content: [{ type: "text", text: JSON.stringify(env) }], isError: true };
|
|
71
|
-
}
|
|
72
|
-
export function buildMcpCommand() {
|
|
73
|
-
const mcp = new Command("mcp").description("Model Context Protocol bridge (expose curated verbs to MCP hosts)");
|
|
74
|
-
mcp
|
|
75
|
-
.command("serve")
|
|
76
|
-
.description("Run an MCP stdio server on this process. For Claude Desktop / Cursor / Claude Code.")
|
|
77
|
-
.addOption(new Option("--profile <name>", "Use a specific profile"))
|
|
78
|
-
.action(async (opts) => {
|
|
79
|
-
let creds;
|
|
80
|
-
try {
|
|
81
|
-
creds = resolveCredentials(opts.profile ? { profile: opts.profile } : {});
|
|
82
|
-
}
|
|
83
|
-
catch (e) {
|
|
84
|
-
if (e instanceof AuthMissingError) {
|
|
85
|
-
process.stderr.write(`[mcp] credential resolution failed: ${e.message}\n` +
|
|
86
|
-
`[mcp] hint: ${e.recovery_hint ?? "Run `echopai login` or set ECHOPAI_KEY."}\n`);
|
|
87
|
-
process.exit(1);
|
|
88
|
-
}
|
|
89
|
-
throw e;
|
|
90
|
-
}
|
|
91
|
-
let whoami;
|
|
92
|
-
try {
|
|
93
|
-
whoami = await getWhoami({
|
|
94
|
-
baseUrl: creds.baseUrl,
|
|
95
|
-
bearer: creds.key,
|
|
96
|
-
cliVersion: CLI_VERSION,
|
|
97
|
-
channel: "mcp",
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
catch (e) {
|
|
101
|
-
process.stderr.write(`[mcp] whoami failed: ${e instanceof Error ? e.message : String(e)}\n`);
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
const tokenScopes = new Set(whoami.scopes);
|
|
105
|
-
const availableVerbs = filterAvailableVerbs(ALL_VERB_SPECS, tokenScopes);
|
|
106
|
-
process.stderr.write(`[mcp] kind=${whoami.kind} scopes=[${whoami.scopes.join(",")}]\n` +
|
|
107
|
-
`[mcp] exposing ${availableVerbs.length}/${ALL_VERB_SPECS.length} curated verbs as MCP tools\n`);
|
|
108
|
-
const server = new McpServer({
|
|
109
|
-
name: "echopai",
|
|
110
|
-
version: CLI_VERSION,
|
|
111
|
-
title: "EchoPai",
|
|
112
|
-
}, {
|
|
113
|
-
capabilities: { tools: {} },
|
|
114
|
-
});
|
|
115
|
-
const handlerCtx = {
|
|
116
|
-
baseUrl: creds.baseUrl,
|
|
117
|
-
bearer: creds.key,
|
|
118
|
-
cliVersion: CLI_VERSION,
|
|
119
|
-
channel: "mcp",
|
|
120
|
-
};
|
|
121
|
-
for (const spec of availableVerbs) {
|
|
122
|
-
server.registerTool(spec.name, {
|
|
123
|
-
description: spec.description,
|
|
124
|
-
inputSchema: spec.inputSchema,
|
|
125
|
-
}, async (args) => {
|
|
126
|
-
try {
|
|
127
|
-
const envelope = await spec.handler(args, handlerCtx);
|
|
128
|
-
return envelopeToToolResponse(envelope);
|
|
129
|
-
}
|
|
130
|
-
catch (e) {
|
|
131
|
-
return errorToToolResponse(e);
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
const transport = new StdioServerTransport();
|
|
136
|
-
await server.connect(transport);
|
|
137
|
-
process.stderr.write("[mcp] connected on stdio; waiting for messages\n");
|
|
138
|
-
// The server runs until stdin closes; SDK handles shutdown.
|
|
139
|
-
});
|
|
140
|
-
return mcp;
|
|
141
|
-
}
|
package/dist/tools/raw.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `echopai raw call <method> <path> [--data '{...}'] [--query k=v ...]`
|
|
3
|
-
*
|
|
4
|
-
* Raw HTTP passthrough — 给未 codegen 的端点 / debug / 应急用。
|
|
5
|
-
* 跳过 Ajv pre-flight 校验、跳过 envelope 解析;行为 = curl + Bearer 注入 + base URL。
|
|
6
|
-
*
|
|
7
|
-
* 自动注入 X-Client / X-Client-Channel / X-Request-Id (与 spec-driven 调用同源),
|
|
8
|
-
* 这样 server 端 audit 能识别这是 CLI 流量。
|
|
9
|
-
*
|
|
10
|
-
* `echopai api call ...` 保留为 deprecated alias 一个 release (tools/api.ts);
|
|
11
|
-
* 下一个 major 移除。
|
|
12
|
-
*/
|
|
13
|
-
import { Command } from "commander";
|
|
14
|
-
import { fetch as undiciFetch } from "undici";
|
|
15
|
-
import { resolveCredentials, AuthMissingError } from "../runtime/auth.js";
|
|
16
|
-
import { buildHttpHeaders } from "../runtime/http.js";
|
|
17
|
-
import { CLI_VERSION } from "../version.js";
|
|
18
|
-
export function buildRawCallCommand() {
|
|
19
|
-
const call = new Command("call")
|
|
20
|
-
.description("Raw HTTP passthrough (arbitrary <method> <path>; debug / non-codegen endpoints)")
|
|
21
|
-
.argument("<method>", "HTTP method (GET|POST|PUT|PATCH|DELETE|HEAD)")
|
|
22
|
-
.argument("<path>", "URL path (relative; base URL injected from credential)")
|
|
23
|
-
.option("-d, --data <json>", "POST/PUT/PATCH body (JSON string)")
|
|
24
|
-
.option("-q, --query <kv...>", "Query string entries: k=v")
|
|
25
|
-
.option("--header <kv...>", "Extra headers: K=V");
|
|
26
|
-
call.action(async (method, urlPath, opts) => {
|
|
27
|
-
const m = method.toUpperCase();
|
|
28
|
-
if (!["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD"].includes(m)) {
|
|
29
|
-
return die("invalid_args", `Unknown HTTP method: ${method}`, 1);
|
|
30
|
-
}
|
|
31
|
-
let creds;
|
|
32
|
-
try {
|
|
33
|
-
creds = resolveCredentials({});
|
|
34
|
-
}
|
|
35
|
-
catch (e) {
|
|
36
|
-
if (e instanceof AuthMissingError) {
|
|
37
|
-
return die("auth_missing", e.message, 1, e.recovery_hint);
|
|
38
|
-
}
|
|
39
|
-
throw e;
|
|
40
|
-
}
|
|
41
|
-
let url = creds.baseUrl + (urlPath.startsWith("/") ? urlPath : "/" + urlPath);
|
|
42
|
-
if (opts.query?.length) {
|
|
43
|
-
const qs = opts.query
|
|
44
|
-
.map((kv) => {
|
|
45
|
-
const idx = kv.indexOf("=");
|
|
46
|
-
if (idx === -1)
|
|
47
|
-
return null;
|
|
48
|
-
return `${encodeURIComponent(kv.slice(0, idx))}=${encodeURIComponent(kv.slice(idx + 1))}`;
|
|
49
|
-
})
|
|
50
|
-
.filter((s) => s !== null)
|
|
51
|
-
.join("&");
|
|
52
|
-
if (qs)
|
|
53
|
-
url += (url.includes("?") ? "&" : "?") + qs;
|
|
54
|
-
}
|
|
55
|
-
// Use centralized header builder (Phase 1.1) — automatically injects
|
|
56
|
-
// X-Client / X-Client-Channel / X-Request-Id. Raw passthrough should
|
|
57
|
-
// still be tagged as CLI traffic in server audit.
|
|
58
|
-
const { headers } = buildHttpHeaders({
|
|
59
|
-
bearer: creds.key,
|
|
60
|
-
cliVersion: CLI_VERSION,
|
|
61
|
-
});
|
|
62
|
-
if (opts.header) {
|
|
63
|
-
for (const kv of opts.header) {
|
|
64
|
-
const idx = kv.indexOf("=");
|
|
65
|
-
if (idx === -1)
|
|
66
|
-
continue;
|
|
67
|
-
headers.set(kv.slice(0, idx), kv.slice(idx + 1));
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
const init = { method: m, headers };
|
|
71
|
-
if (opts.data) {
|
|
72
|
-
if (!headers.has("content-type")) {
|
|
73
|
-
headers.set("content-type", "application/json");
|
|
74
|
-
}
|
|
75
|
-
init.body = opts.data;
|
|
76
|
-
}
|
|
77
|
-
const res = await undiciFetch(url, init);
|
|
78
|
-
const body = await res.text();
|
|
79
|
-
process.stdout.write(body + (body.endsWith("\n") ? "" : "\n"));
|
|
80
|
-
process.exit(res.status >= 400 && res.status < 500
|
|
81
|
-
? 1
|
|
82
|
-
: res.status >= 500
|
|
83
|
-
? 2
|
|
84
|
-
: 0);
|
|
85
|
-
});
|
|
86
|
-
return call;
|
|
87
|
-
}
|
|
88
|
-
function die(code, message, exitCode, recovery_hint) {
|
|
89
|
-
const env = {
|
|
90
|
-
error: { code, message, retryable: false },
|
|
91
|
-
};
|
|
92
|
-
if (recovery_hint)
|
|
93
|
-
env.error.recovery_hint = recovery_hint;
|
|
94
|
-
process.stderr.write(JSON.stringify(env) + "\n");
|
|
95
|
-
process.exit(exitCode);
|
|
96
|
-
}
|