echopai 2.3.0 → 2.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.
Files changed (54) hide show
  1. package/README.md +63 -348
  2. package/dist/bin.js +8388 -190
  3. package/package.json +11 -13
  4. package/dist/_generated/commands.js +0 -378
  5. package/dist/_generated/help.js +0 -295
  6. package/dist/_generated/operations.js +0 -2385
  7. package/dist/runtime/auth.js +0 -95
  8. package/dist/runtime/envelope.js +0 -52
  9. package/dist/runtime/errors.js +0 -186
  10. package/dist/runtime/filters.js +0 -153
  11. package/dist/runtime/format.js +0 -143
  12. package/dist/runtime/http.js +0 -65
  13. package/dist/runtime/idempotency.js +0 -18
  14. package/dist/runtime/invoker.js +0 -391
  15. package/dist/runtime/io.js +0 -16
  16. package/dist/runtime/paginator.js +0 -146
  17. package/dist/runtime/trace.js +0 -99
  18. package/dist/runtime/tty.js +0 -51
  19. package/dist/runtime/update_check.js +0 -120
  20. package/dist/runtime/update_worker.js +0 -63
  21. package/dist/runtime/verb_cmd.js +0 -72
  22. package/dist/runtime/verb_runner.js +0 -152
  23. package/dist/runtime/whoami_cache.js +0 -109
  24. package/dist/tools/api.js +0 -81
  25. package/dist/tools/completion.js +0 -116
  26. package/dist/tools/config.js +0 -123
  27. package/dist/tools/doctor.js +0 -183
  28. package/dist/tools/login.js +0 -99
  29. package/dist/tools/mcp.js +0 -141
  30. package/dist/tools/raw.js +0 -96
  31. package/dist/tools/schema.js +0 -58
  32. package/dist/tools/trace.js +0 -54
  33. package/dist/tools/upgrade.js +0 -103
  34. package/dist/tools/welcome.js +0 -225
  35. package/dist/tools/whoami.js +0 -132
  36. package/dist/verbs/_spec.js +0 -15
  37. package/dist/verbs/announcements.js +0 -195
  38. package/dist/verbs/bars_batch.js +0 -66
  39. package/dist/verbs/chart.js +0 -110
  40. package/dist/verbs/concepts.js +0 -393
  41. package/dist/verbs/digest.js +0 -351
  42. package/dist/verbs/financials.js +0 -212
  43. package/dist/verbs/hot.js +0 -29
  44. package/dist/verbs/index.js +0 -88
  45. package/dist/verbs/limit_up.js +0 -156
  46. package/dist/verbs/lookup.js +0 -72
  47. package/dist/verbs/market.js +0 -185
  48. package/dist/verbs/news.js +0 -81
  49. package/dist/verbs/quote.js +0 -53
  50. package/dist/verbs/scan.js +0 -42
  51. package/dist/verbs/search.js +0 -105
  52. package/dist/verbs/sentiment.js +0 -231
  53. package/dist/verbs/views.js +0 -85
  54. package/dist/version.js +0 -5
@@ -1,116 +0,0 @@
1
- /**
2
- * `echopai completion <bash|zsh|fish>` 输出 shell completion 脚本。
3
- *
4
- * 不依赖 tabtab,自己生成(30 行 bash / 20 行 zsh / 15 行 fish)。
5
- * 用 `echopai schema list` 实时拿命令列表。
6
- *
7
- * bash: echopai completion bash > ~/.local/share/bash-completion/completions/echopai
8
- * zsh: echopai completion zsh > ~/.zsh/completions/_echopai (需要 fpath 含此目录)
9
- * fish: echopai completion fish > ~/.config/fish/completions/echopai.fish
10
- */
11
- import { Command } from "commander";
12
- import { listOperations } from "../_generated/operations.js";
13
- export function buildCompletionCommand() {
14
- return new Command("completion")
15
- .argument("<shell>", "bash | zsh | fish")
16
- .description("Print shell completion script for echopai")
17
- .action((shell) => {
18
- const ops = listOperations();
19
- // groups: map noun → set of verbs
20
- const tree = new Map();
21
- for (const op of ops) {
22
- const parts = op.cliName.split(" ");
23
- const noun = parts[0];
24
- const verb = parts[1] || "";
25
- if (!tree.has(noun))
26
- tree.set(noun, new Set());
27
- if (verb)
28
- tree.get(noun).add(verb);
29
- }
30
- const nouns = [...tree.keys()].sort();
31
- // hand-curated tools
32
- const tools = ["login", "logout", "status", "config", "api", "schema", "completion"];
33
- const allCmds = [...nouns, ...tools].sort();
34
- let out = "";
35
- if (shell === "bash") {
36
- out = bashCompletion(allCmds, tree);
37
- }
38
- else if (shell === "zsh") {
39
- out = zshCompletion(allCmds, tree);
40
- }
41
- else if (shell === "fish") {
42
- out = fishCompletion(allCmds, tree);
43
- }
44
- else {
45
- process.stderr.write(JSON.stringify({ error: { code: "invalid_args", message: `Unsupported shell '${shell}'`, recovery_hint: "Use bash | zsh | fish." } }) + "\n");
46
- process.exit(1);
47
- }
48
- process.stdout.write(out);
49
- process.exit(0);
50
- });
51
- }
52
- function bashCompletion(cmds, tree) {
53
- const subcaseLines = [...tree.entries()]
54
- .map(([noun, verbs]) => ` ${noun}) COMPREPLY=( $(compgen -W "${[...verbs].sort().join(" ")}" -- "$cur") );;`)
55
- .join("\n");
56
- return `# echopai bash completion
57
- _echopai() {
58
- local cur prev
59
- cur="\${COMP_WORDS[COMP_CWORD]}"
60
- prev="\${COMP_WORDS[COMP_CWORD-1]}"
61
- if [ "$COMP_CWORD" = "1" ]; then
62
- COMPREPLY=( $(compgen -W "${cmds.join(" ")}" -- "$cur") )
63
- return
64
- fi
65
- case "\${COMP_WORDS[1]}" in
66
- ${subcaseLines}
67
- config) COMPREPLY=( $(compgen -W "show set profile" -- "$cur") );;
68
- schema) COMPREPLY=( $(compgen -W "list get export" -- "$cur") );;
69
- esac
70
- }
71
- complete -F _echopai echopai
72
- `;
73
- }
74
- function zshCompletion(cmds, tree) {
75
- const subcases = [...tree.entries()]
76
- .map(([noun, verbs]) => ` "${noun}") _values "${noun} verb" ${[...verbs].sort().map((v) => `"${v}"`).join(" ")} ;;`)
77
- .join("\n");
78
- return `#compdef echopai
79
- _echopai() {
80
- local -a cmds
81
- cmds=(${cmds.map((c) => `"${c}"`).join(" ")})
82
- if (( CURRENT == 2 )); then
83
- _values "echopai command" $cmds
84
- return
85
- fi
86
- case "$words[2]" in
87
- ${subcases}
88
- config) _values "config sub" "show" "set" "profile" ;;
89
- schema) _values "schema sub" "list" "get" "export" ;;
90
- esac
91
- }
92
- _echopai "$@"
93
- `;
94
- }
95
- function fishCompletion(cmds, tree) {
96
- const lines = [`# echopai fish completion`];
97
- lines.push(`complete -c echopai -f`);
98
- for (const c of cmds) {
99
- lines.push(`complete -c echopai -n '__fish_use_subcommand' -a "${c}"`);
100
- }
101
- for (const [noun, verbs] of tree.entries()) {
102
- for (const verb of [...verbs].sort()) {
103
- lines.push(`complete -c echopai -n "__fish_seen_subcommand_from ${noun}" -a "${verb}"`);
104
- }
105
- }
106
- // hand-curated
107
- for (const [parent, sub] of [
108
- ["config", ["show", "set", "profile"]],
109
- ["schema", ["list", "get", "export"]],
110
- ]) {
111
- for (const v of sub) {
112
- lines.push(`complete -c echopai -n "__fish_seen_subcommand_from ${parent}" -a "${v}"`);
113
- }
114
- }
115
- return lines.join("\n") + "\n";
116
- }
@@ -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
- }
@@ -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
- }
@@ -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
- }