@nextxai/cli 0.0.0-stage → 0.1.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 (43) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/LICENSE +21 -0
  3. package/README.md +53 -2
  4. package/dist/ai/client.js +221 -0
  5. package/dist/auth/ci.js +64 -0
  6. package/dist/auth/credentials.js +192 -0
  7. package/dist/auth/device.js +76 -0
  8. package/dist/auth/login.js +145 -0
  9. package/dist/auth/loopback.js +98 -0
  10. package/dist/auth/mcp-oauth.js +174 -0
  11. package/dist/auth/session.js +205 -0
  12. package/dist/bin/nextx.js +5 -0
  13. package/dist/commands/ai.js +514 -0
  14. package/dist/commands/api.js +87 -0
  15. package/dist/commands/auth.js +165 -0
  16. package/dist/commands/docs.js +55 -0
  17. package/dist/commands/help.js +48 -0
  18. package/dist/commands/keys.js +85 -0
  19. package/dist/commands/mcp.js +202 -0
  20. package/dist/commands/tools.js +186 -0
  21. package/dist/commands/util.js +224 -0
  22. package/dist/commands/webhooks.js +249 -0
  23. package/dist/commands/workspace.js +44 -0
  24. package/dist/core/brand.js +51 -0
  25. package/dist/core/config.js +52 -0
  26. package/dist/core/context.js +42 -0
  27. package/dist/core/errors.js +28 -0
  28. package/dist/core/http.js +170 -0
  29. package/dist/core/i18n.js +90 -0
  30. package/dist/core/lock.js +44 -0
  31. package/dist/core/output.js +74 -0
  32. package/dist/core/paths.js +29 -0
  33. package/dist/core/prompt.js +137 -0
  34. package/dist/core/version.js +13 -0
  35. package/dist/main.js +38 -0
  36. package/dist/mcp/client.js +114 -0
  37. package/dist/program.js +102 -0
  38. package/dist/repl/App.js +771 -0
  39. package/dist/repl/components.js +66 -0
  40. package/dist/repl/index.js +12 -0
  41. package/dist/repl/markdown.js +57 -0
  42. package/dist/repl/slash.js +53 -0
  43. package/package.json +62 -4
@@ -0,0 +1,165 @@
1
+ import { brand } from "../core/brand.js";
2
+ import { context } from "../core/context.js";
3
+ import { CliError, Exit } from "../core/errors.js";
4
+ import { tr } from "../core/i18n.js";
5
+ import { c, emit, info, ok, table, warn } from "../core/output.js";
6
+ import { get, del } from "../core/http.js";
7
+ import { getProfile, loadConfig, removeProfile } from "../core/config.js";
8
+ import { deleteSession, readSession, storageKind } from "../auth/credentials.js";
9
+ import { authSource, currentAuth, logoutServer, decodeJwt } from "../auth/session.js";
10
+ import { loginQr, loginWeb } from "../auth/login.js";
11
+ import { loginDevice } from "../auth/device.js";
12
+ import { loginCi } from "../auth/ci.js";
13
+ import { VERSION } from "../core/version.js";
14
+ /** SSH sessions and Linux boxes without a display cannot open a browser. */
15
+ export function browserAvailable() {
16
+ if (process.env.SSH_CONNECTION || process.env.SSH_TTY)
17
+ return false;
18
+ if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY)
19
+ return false;
20
+ return true;
21
+ }
22
+ export async function usage() {
23
+ try {
24
+ return await get("/operator/widget/support/usage");
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ function printLogin(r) {
31
+ const p = getProfile(context().profile);
32
+ ok(tr(`Đã đăng nhập ${c.bold(r.userName ?? "")} (${r.userEmail ?? ""})`, `Logged in as ${c.bold(r.userName ?? "")} (${r.userEmail ?? ""})`));
33
+ info(` ${tr("Workspace", "Workspace")} ${c.bold(r.workspaceName ?? p?.workspaceName ?? "")} · ${context().env === "prod" ? "prod" : c.yellow("sandbox")}`);
34
+ info(` ${tr("Phiên", "Session")} ${tr("tự gia hạn khi dùng, hết hạn nếu bỏ không 30 ngày", "renews while used, expires after 30 idle days")} · ${storageKind() === "keychain" ? "Keychain" : tr("tệp 0600", "0600 file")}`);
35
+ info(c.dim(` ${tr("Thử", "Try")}: ${brand().bin} · ${brand().bin} -p "${tr("doanh thu hôm qua", "yesterday's revenue")}" · ${brand().bin} help`));
36
+ }
37
+ export function registerAuth(program) {
38
+ const b = brand();
39
+ program
40
+ .command("login")
41
+ .description(tr("đăng nhập (tự chọn trình duyệt hoặc QR)", "log in (browser or QR, chosen automatically)"))
42
+ .option("--web", tr("đăng nhập qua trình duyệt", "log in with the browser"))
43
+ .option("--qr", tr(`quét mã bằng app ${b.mobileApp} (SSH, máy không có trình duyệt)`, `scan with the ${b.mobileApp} app (SSH, headless machines)`))
44
+ .option("--device", tr("nhập mã trên máy khác (RFC 8628)", "enter a code on another device (RFC 8628)"))
45
+ .option("--ci", tr("trong GitHub Actions/GitLab CI: đổi token OIDC lấy token ngắn hạn", "in GitHub Actions/GitLab CI: exchange the OIDC token for a short-lived token"))
46
+ .option("--no-browser", tr("không tự mở trình duyệt, chỉ in link", "do not open the browser, print the link only"))
47
+ .action(async (o) => {
48
+ const ws = context().workspace;
49
+ let r;
50
+ if (o.ci)
51
+ r = await loginCi({ workspace: ws });
52
+ else if (o.device)
53
+ r = await loginDevice({ workspace: ws });
54
+ else if (o.qr || (!o.web && !browserAvailable()))
55
+ r = await loginQr({ workspace: ws });
56
+ else
57
+ r = await loginWeb({ workspace: ws, noBrowser: o.browser === false });
58
+ const u = await usage();
59
+ emit({ user: { name: r.userName, email: r.userEmail }, workspace: r.workspaceName, env: context().env, via: r.session.via, ai: u ?? null }, () => {
60
+ printLogin(r);
61
+ if (u)
62
+ info(` ${tr("Gói AI", "AI plan")} ${c.bold(u.planName ?? "-")}${u.limit ? ` · ${tr("còn", "left")} ${u.remaining}/${u.limit}` : ""}`);
63
+ });
64
+ });
65
+ program
66
+ .command("logout")
67
+ .description(tr("đăng xuất hồ sơ này (thu hồi phiên trên máy chủ)", "log out this profile (revokes the server session)"))
68
+ .option("--all", tr("đăng xuất mọi hồ sơ trên máy này", "log out every profile on this machine"))
69
+ .action(async (o) => {
70
+ const profiles = o.all ? Object.keys(loadConfig().profiles) : [context().profile];
71
+ for (const name of profiles) {
72
+ const { setContext, initContext } = await import("../core/context.js");
73
+ if (name !== context().profile) {
74
+ initContext({ profile: name });
75
+ setContext({ profile: name });
76
+ }
77
+ if (!readSession(name))
78
+ continue;
79
+ const revoked = await logoutServer();
80
+ deleteSession(name);
81
+ removeProfile(name);
82
+ if (revoked)
83
+ ok(tr(`Đã đăng xuất (${name})`, `Logged out (${name})`));
84
+ else
85
+ warn(tr(`Đã xoá phiên trên máy (${name}), nhưng không tới được máy chủ: phiên còn sống tới khi hết hạn. Thu hồi tại ${context().endpoints.id} › Thiết bị`, `Removed the local session (${name}), but the server was unreachable: it stays valid until it expires. Revoke it at ${context().endpoints.id} › Devices`));
86
+ }
87
+ });
88
+ program
89
+ .command("status")
90
+ .description(tr("tài khoản, workspace, môi trường, gói AI, phiên", "account, workspace, environment, AI plan, session"))
91
+ .action(async () => {
92
+ const src = authSource();
93
+ const p = getProfile(context().profile);
94
+ const base = { version: VERSION, profile: context().profile, env: context().env, api: context().endpoints.api, identity: src };
95
+ if (src === "none") {
96
+ emit({ ...base, loggedIn: false }, () => {
97
+ info(`${b.name} CLI ${VERSION} · ${tr("hồ sơ", "profile")} ${context().profile} · ${context().env}`);
98
+ warn(tr(`Chưa đăng nhập. Chạy \`${b.bin} login\``, `Not logged in. Run \`${b.bin} login\``));
99
+ });
100
+ process.exitCode = Exit.auth;
101
+ return;
102
+ }
103
+ if (src === "apiKey") {
104
+ emit({ ...base, loggedIn: true, kind: "apiKey" }, () => info(tr(`Đang dùng khoá API từ ${b.envPrefix}_API_KEY (danh tính agent).`, `Using the API key from ${b.envPrefix}_API_KEY (agent identity).`)));
105
+ return;
106
+ }
107
+ const auth = await currentAuth({ required: true });
108
+ const me = await get("/iam/auth/me");
109
+ const u = await usage();
110
+ const s = readSession(context().profile);
111
+ const claims = auth?.kind !== "apiKey" ? decodeJwt(auth.token) : {};
112
+ const data = {
113
+ ...base,
114
+ loggedIn: true,
115
+ user: { name: me.fullName, email: me.email },
116
+ workspace: me.workspace,
117
+ groups: me.groups,
118
+ ai: u ?? null,
119
+ session: s ? { via: s.via, accessExpiresAt: new Date(s.expiresAt).toISOString(), storage: storageKind() } : null,
120
+ tokenExp: claims.exp ? new Date(claims.exp * 1000).toISOString() : null,
121
+ };
122
+ emit(data, () => {
123
+ info(`${c.bold(me.fullName ?? "")} ${c.dim(me.email ?? "")}`);
124
+ info(` ${tr("Workspace", "Workspace")} ${c.bold(me.workspace?.name ?? "")} ${c.dim(me.workspace?.code ?? "")} · ${(me.groups ?? []).join(", ")}`);
125
+ info(` ${tr("Môi trường", "Environment")} ${context().env === "prod" ? c.red("prod") : c.yellow("sandbox")} · ${context().endpoints.api}`);
126
+ if (u)
127
+ info(` ${tr("Gói AI", "AI plan")} ${u.planName ?? "-"}${u.limit ? ` · ${tr("đã dùng", "used")} ${u.used}/${u.limit}, ${tr("mở lại", "resets")} ${u.resetsAt?.slice(0, 10)}` : ""}`);
128
+ info(` ${tr("Danh tính", "Identity")} ${src === "token" ? `${b.envPrefix}_TOKEN` : `${tr("phiên", "session")} (${s?.via}) · ${storageKind() === "keychain" ? "Keychain" : tr("tệp", "file")}`}`);
129
+ info(` ${tr("Hồ sơ", "Profile")} ${context().profile}${p?.agent ? ` · agent ${p.agent}` : ""} · CLI ${VERSION}`);
130
+ });
131
+ });
132
+ const sessions = program.command("sessions").description(tr("phiên đăng nhập của bạn trên mọi thiết bị", "your login sessions on all devices"));
133
+ sessions
134
+ .command("list", { isDefault: true })
135
+ .description(tr("danh sách phiên", "list sessions"))
136
+ .action(async () => {
137
+ const list = await get("/iam/sessions");
138
+ const device = (s) => {
139
+ const cli = /^([a-z]+)-cli\/(\S+) \(([^;)]+)/.exec(s.userAgent ?? "");
140
+ if (cli)
141
+ return `${s.browserName ?? `${brand().name} CLI ${cli[2]}`} · ${s.osName ?? cli[3]}`;
142
+ return s.deviceName ?? ([s.osName, s.browserName].filter(Boolean).join(" · ") || s.userAgent || "");
143
+ };
144
+ emit(list, () => info(table(["", "ID", tr("Thiết bị", "Device"), tr("Nơi", "Where"), tr("Hoạt động", "Last active")], list.map((s) => [s.isCurrentSession ? c.green("●") : "", s.id.slice(0, 8), device(s).slice(0, 40), s.city ?? s.ipAddress ?? "", (s.lastActivity ?? s.createdAt ?? "").slice(0, 16).replace("T", " ")]))));
145
+ });
146
+ sessions
147
+ .command("revoke [id]")
148
+ .description(tr("thu hồi một phiên, hoặc --others để thu hồi mọi phiên khác", "revoke one session, or --others for all other sessions"))
149
+ .option("--others", tr("mọi phiên trừ phiên này", "every session except this one"))
150
+ .action(async (id, o) => {
151
+ if (o.others) {
152
+ await del("/iam/sessions");
153
+ ok(tr("Đã thu hồi mọi phiên khác.", "Revoked all other sessions."));
154
+ return;
155
+ }
156
+ if (!id)
157
+ throw new CliError(tr("Cần id phiên (xem `sessions list`) hoặc --others.", "Pass a session id (see `sessions list`) or --others."), Exit.usage);
158
+ const list = await get("/iam/sessions");
159
+ const hit = list.find((s) => s.id === id || s.id.startsWith(id));
160
+ if (!hit)
161
+ throw new CliError(tr("Không thấy phiên này.", "Session not found."), Exit.notFound);
162
+ await del(`/iam/sessions/${hit.id}`);
163
+ ok(tr(`Đã thu hồi phiên ${hit.id.slice(0, 8)}.`, `Revoked session ${hit.id.slice(0, 8)}.`));
164
+ });
165
+ }
@@ -0,0 +1,55 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { context } from "../core/context.js";
3
+ import { tr } from "../core/i18n.js";
4
+ import { c, info, json, ok } from "../core/output.js";
5
+ import { get } from "../core/http.js";
6
+ const TOPICS = {
7
+ cli: "/docs/cli",
8
+ quickstart: "/docs/quickstart",
9
+ "api-keys": "/docs/api-keys",
10
+ oauth: "/docs/oauth",
11
+ mcp: "/docs/mcp",
12
+ "ai-tools": "/docs/ai-tools",
13
+ webhooks: "/docs/webhooks",
14
+ sdk: "/docs/sdk",
15
+ errors: "/docs/errors",
16
+ "rate-limit": "/docs/rate-limit",
17
+ sandbox: "/docs/sandbox",
18
+ changelog: "/docs/changelog",
19
+ versioning: "/docs/versioning",
20
+ security: "/docs/security",
21
+ };
22
+ export function registerDocs(program) {
23
+ program
24
+ .command("openapi")
25
+ .description(tr("tải đặc tả OpenAPI công khai", "download the public OpenAPI spec"))
26
+ .command("pull")
27
+ .option("--module <m>", tr("chỉ một phân hệ", "one module only"))
28
+ .option("-o, --out <file>", "", "openapi.json")
29
+ .action(async (o) => {
30
+ const spec = await get(`${context().endpoints.docs}/openapi.json`, { anonymous: true, baseUrl: "", query: { module: o.module } });
31
+ writeFileSync(o.out, JSON.stringify(spec, null, 2));
32
+ const paths = Object.keys(spec.paths ?? {}).length;
33
+ ok(tr(`Đã lưu ${o.out} · ${paths} đường`, `Saved ${o.out} · ${paths} paths`));
34
+ });
35
+ program
36
+ .command("docs [topic]")
37
+ .description(tr(`mở tài liệu (${Object.keys(TOPICS).join(", ")})`, `open the docs (${Object.keys(TOPICS).join(", ")})`))
38
+ .action(async (topic) => {
39
+ const url = `${context().endpoints.docs}${topic ? TOPICS[topic] ?? `/docs/${topic}` : "/docs"}`;
40
+ info(c.dim(url));
41
+ const open = (await import("open")).default;
42
+ await open(url).catch(() => undefined);
43
+ });
44
+ program
45
+ .command("changelog")
46
+ .description(tr("thay đổi API gần đây", "recent API changes"))
47
+ .action(async () => {
48
+ const spec = await get(`${context().endpoints.docs}/openapi.json`, { anonymous: true, baseUrl: "" });
49
+ const log = spec.info?.["x-changelog"];
50
+ if (log)
51
+ json(log);
52
+ else
53
+ info(`${tr("Phiên bản API", "API version")} ${spec.info?.version ?? "?"} · ${context().endpoints.docs}/docs/changelog`);
54
+ });
55
+ }
@@ -0,0 +1,48 @@
1
+ import { brand } from "../core/brand.js";
2
+ import { tr } from "../core/i18n.js";
3
+ import { json } from "../core/output.js";
4
+ import { VERSION } from "../core/version.js";
5
+ import { Exit } from "../core/errors.js";
6
+ import { SLASH, SHORTCUTS } from "../repl/slash.js";
7
+ const opt = (o) => ({ flags: o.flags, description: o.description, ...(o.defaultValue !== undefined ? { default: o.defaultValue } : {}), ...(o.argChoices ? { choices: o.argChoices } : {}) });
8
+ function walk(cmd) {
9
+ return {
10
+ name: cmd.name(),
11
+ aliases: cmd.aliases(),
12
+ usage: cmd.usage(),
13
+ description: cmd.description(),
14
+ arguments: cmd.registeredArguments.map((a) => ({ name: a.name(), required: a.required, variadic: a.variadic, description: a.description })),
15
+ options: cmd.options.filter((o) => !o.hidden).map(opt),
16
+ commands: cmd.commands.filter((c) => !c._hidden && !c.name().startsWith("__")).map(walk),
17
+ };
18
+ }
19
+ /** Machine-readable command tree; the developer portal renders /docs/cli from this. */
20
+ export function helpTree(program) {
21
+ const b = brand();
22
+ return {
23
+ bin: b.bin,
24
+ package: b.pkg,
25
+ version: VERSION,
26
+ brand: b.id,
27
+ root: walk(program),
28
+ slashCommands: SLASH.map((s) => ({ name: `/${s.name}`, args: s.args ?? null, description: s.description() })),
29
+ shortcuts: SHORTCUTS.map((s) => ({ keys: s.keys, description: s.description() })),
30
+ exitCodes: Object.entries(Exit).map(([name, code]) => ({ name, code })),
31
+ environment: ["API_KEY", "TOKEN", "PROFILE", "ENV", "WORKSPACE", "LANG", "DEBUG", "NO_UPDATE_CHECK", "NO_KEYCHAIN", "ID_TOKEN"].map((v) => `${b.envPrefix}_${v}`),
32
+ };
33
+ }
34
+ export function registerHelp(program) {
35
+ program
36
+ .command("help [command...]")
37
+ .description(tr("trợ giúp (--json: toàn bộ cây lệnh cho máy đọc)", "help (--json: full command tree, machine readable)"))
38
+ .option("--json", tr("in cây lệnh dạng JSON", "print the command tree as JSON"))
39
+ .action((path = [], o) => {
40
+ if (o.json || program.opts().json)
41
+ return json(helpTree(program));
42
+ let cmd = program;
43
+ for (const p of path)
44
+ cmd = cmd.commands.find((c) => c.name() === p || c.aliases().includes(p)) ?? cmd;
45
+ cmd.outputHelp();
46
+ });
47
+ program.addHelpCommand(false);
48
+ }
@@ -0,0 +1,85 @@
1
+ import { brand } from "../core/brand.js";
2
+ import { CliError, Exit } from "../core/errors.js";
3
+ import { tr } from "../core/i18n.js";
4
+ import { c, emit, info, ok, table, warn } from "../core/output.js";
5
+ import { get, post, del } from "../core/http.js";
6
+ import { confirm, hasTty } from "../core/prompt.js";
7
+ import { requirePerson } from "../auth/session.js";
8
+ /** 90d, 12h, 2w → ISO date from now. */
9
+ export function parseDuration(s) {
10
+ const m = /^(\d+)([hdwmy])$/.exec(s.trim());
11
+ if (!m)
12
+ throw new CliError(tr(`Thời hạn không hợp lệ: ${s} (vd 30d, 12h, 6m, 1y)`, `Invalid duration: ${s} (e.g. 30d, 12h, 6m, 1y)`), Exit.usage);
13
+ const n = Number(m[1]);
14
+ const ms = { h: 3600e3, d: 86400e3, w: 7 * 86400e3, m: 30 * 86400e3, y: 365 * 86400e3 }[m[2]];
15
+ return new Date(Date.now() + n * ms).toISOString();
16
+ }
17
+ async function findKey(id) {
18
+ const k = (await get("/iam/api-keys")).find((x) => x.id === id || x.id.startsWith(id) || x.keyPrefix === id || x.name === id);
19
+ if (!k)
20
+ throw new CliError(tr("Không thấy khoá này.", "Key not found."), Exit.notFound);
21
+ return k;
22
+ }
23
+ export function registerKeys(program) {
24
+ const keys = program.command("keys").description(tr("khoá API của workspace", "workspace API keys"));
25
+ keys.hook("preAction", async () => void (await requirePerson()));
26
+ keys.command("list", { isDefault: true }).description(tr("danh sách khoá", "list keys")).action(async () => {
27
+ const list = await get("/iam/api-keys");
28
+ emit(list, () => info(table(["ID", tr("Tên", "Name"), tr("Đầu khoá", "Prefix"), tr("Quyền", "Scopes"), tr("Hết hạn", "Expires"), ""], list.map((k) => [
29
+ k.id.slice(0, 8), k.name, k.keyPrefix, k.scopes.length ? `${k.scopes.slice(0, 3).join(", ")}${k.scopes.length > 3 ? ` +${k.scopes.length - 3}` : ""}` : c.yellow(tr("không giới hạn", "unlimited")),
30
+ k.expiresAt?.slice(0, 10) ?? "", k.revokedAt ? c.red(tr("đã thu hồi", "revoked")) : k.isActive ? c.green("●") : c.dim(tr("tắt", "off")),
31
+ ]))));
32
+ });
33
+ keys.command("create")
34
+ .description(tr("tạo khoá mới (in khoá đúng một lần)", "create a key (printed once)"))
35
+ .requiredOption("--name <name>")
36
+ .requiredOption("--scopes <codes>", tr("mã quyền, phẩy ngăn cách, vd crm.contacts.view,sales.orders.view", "permission codes, comma separated, e.g. crm.contacts.view,sales.orders.view"))
37
+ .option("--expires <dur>", tr("thời hạn: 30d, 6m, 1y", "lifetime: 30d, 6m, 1y"), "90d")
38
+ .option("--description <text>")
39
+ .action(async (o) => {
40
+ const r = await post("/iam/api-keys", {
41
+ name: o.name,
42
+ scopes: o.scopes.split(",").map((s) => s.trim()).filter(Boolean),
43
+ expiresAt: parseDuration(o.expires),
44
+ description: o.description,
45
+ source: "integration",
46
+ });
47
+ const raw = String(r.rawKey ?? r.key ?? r.apiKey ?? "");
48
+ emit(r, () => {
49
+ ok(tr(`Đã tạo khoá "${o.name}"`, `Created key "${o.name}"`));
50
+ info(`\n ${c.bold(raw)}\n`);
51
+ warn(tr("Chỉ hiện một lần. Lưu ngay vào nơi an toàn (biến bí mật của CI, trình quản lý mật khẩu).", "Shown only once. Store it now (CI secret, password manager)."));
52
+ info(c.dim(` ${brand().envPrefix}_API_KEY=${raw.slice(0, 8)}… ${brand().bin} tools list`));
53
+ });
54
+ });
55
+ keys.command("revoke <id>").description(tr("thu hồi khoá (dừng ngay)", "revoke a key (stops immediately)")).action(async (id) => {
56
+ const k = await findKey(id);
57
+ if (hasTty() && !(await confirm(tr(`Thu hồi khoá "${k.name}" (${k.keyPrefix})? Mọi hệ thống đang dùng sẽ ngừng chạy.`, `Revoke "${k.name}" (${k.keyPrefix})? Anything using it stops working.`))))
58
+ return;
59
+ await post(`/iam/api-keys/${k.id}/revoke`, {});
60
+ ok(tr("Đã thu hồi.", "Revoked."));
61
+ });
62
+ keys.command("delete <id>").description(tr("xoá khoá", "delete a key")).action(async (id) => {
63
+ const k = await findKey(id);
64
+ if (hasTty() && !(await confirm(tr(`Xoá khoá "${k.name}"?`, `Delete "${k.name}"?`))))
65
+ return;
66
+ await del(`/iam/api-keys/${k.id}`);
67
+ ok(tr("Đã xoá.", "Deleted."));
68
+ });
69
+ keys.command("usage [id]").description(tr("số lượt gọi 7 ngày", "calls over 7 days")).action(async (id) => {
70
+ const u = await get("/iam/api-keys/usage");
71
+ const rows = (u.keys ?? []).filter((k) => !id || k.id.startsWith(id));
72
+ emit(u, () => {
73
+ if (!rows.length)
74
+ return void info(JSON.stringify(u, null, 2));
75
+ info(table([tr("Khoá", "Key"), ...(rows[0]?.days ?? []).map((d) => d.date.slice(5))], rows.map((k) => [k.name ?? k.id.slice(0, 8), ...(k.days ?? []).map((d) => d.count)])));
76
+ if (u.limitPerMinute)
77
+ info(c.dim(` ${tr("Hạn mức", "Limit")}: ${u.limitPerMinute}/${tr("phút", "min")}`));
78
+ });
79
+ });
80
+ keys.command("calls <id>").description(tr("50 lượt gọi gần nhất của một khoá", "last 50 calls of a key")).action(async (id) => {
81
+ const k = await findKey(id);
82
+ const calls = await get(`/iam/api-keys/${k.id}/calls`);
83
+ emit(calls, () => info(table([tr("Lúc", "At"), "", tr("Đường", "Path"), tr("Kết quả", "Status"), "ms"], calls.map((x) => [(x.at ?? "").slice(0, 19).replace("T", " "), x.method ?? "", x.path ?? "", String(x.status ?? ""), x.durationMs ?? ""]))));
84
+ });
85
+ }
@@ -0,0 +1,202 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync } from "node:fs";
2
+ import { homedir, platform } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+ import { spawnSync } from "node:child_process";
5
+ import { createInterface } from "node:readline";
6
+ import { fetch } from "undici";
7
+ import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
8
+ import { brand } from "../core/brand.js";
9
+ import { context } from "../core/context.js";
10
+ import { CliError, Exit } from "../core/errors.js";
11
+ import { tr } from "../core/i18n.js";
12
+ import { c, info, ok, warn } from "../core/output.js";
13
+ import { hasTty, multiSelect } from "../core/prompt.js";
14
+ import { userAgent } from "../core/http.js";
15
+ import { mcpToken } from "../auth/mcp-oauth.js";
16
+ const home = homedir();
17
+ const appSupport = () => platform() === "darwin" ? join(home, "Library", "Application Support") : platform() === "win32" ? process.env.APPDATA ?? join(home, "AppData", "Roaming") : join(home, ".config");
18
+ const obj = (cfg, k) => (cfg[k] ??= {});
19
+ /** Clients in A→Z order: no assistant vendor is favoured. */
20
+ const TARGETS = [
21
+ { id: "chatgpt", name: "ChatGPT", kind: "manual" },
22
+ { id: "claude", name: "Claude Code", kind: "command" },
23
+ {
24
+ id: "claude-desktop", name: "Claude Desktop", kind: "file-json",
25
+ file: () => join(appSupport(), "Claude", "claude_desktop_config.json"),
26
+ apply: (cfg, key, _u, stdio) => void (obj(cfg, "mcpServers")[key] = stdio),
27
+ remove: (cfg, key) => void delete obj(cfg, "mcpServers")[key],
28
+ },
29
+ {
30
+ id: "codex", name: "Codex", kind: "file-toml",
31
+ file: () => join(process.env.CODEX_HOME ?? join(home, ".codex"), "config.toml"),
32
+ apply: (cfg, key, _u, stdio) => void (obj(cfg, "mcp_servers")[key] = stdio),
33
+ remove: (cfg, key) => void delete obj(cfg, "mcp_servers")[key],
34
+ },
35
+ { id: "copilot", name: "Copilot", kind: "manual" },
36
+ {
37
+ id: "cursor", name: "Cursor", kind: "file-json",
38
+ file: () => join(home, ".cursor", "mcp.json"),
39
+ apply: (cfg, key, url) => void (obj(cfg, "mcpServers")[key] = { url }),
40
+ remove: (cfg, key) => void delete obj(cfg, "mcpServers")[key],
41
+ },
42
+ {
43
+ id: "gemini", name: "Gemini CLI", kind: "file-json",
44
+ file: () => join(home, ".gemini", "settings.json"),
45
+ apply: (cfg, key, url) => void (obj(cfg, "mcpServers")[key] = { httpUrl: url }),
46
+ remove: (cfg, key) => void delete obj(cfg, "mcpServers")[key],
47
+ },
48
+ { id: "grok", name: "Grok", kind: "manual" },
49
+ {
50
+ id: "vscode", name: "VS Code", kind: "file-json",
51
+ file: () => join(appSupport(), "Code", "User", "mcp.json"),
52
+ apply: (cfg, key, url) => void (obj(cfg, "servers")[key] = { type: "http", url }),
53
+ remove: (cfg, key) => void delete obj(cfg, "servers")[key],
54
+ },
55
+ {
56
+ id: "windsurf", name: "Windsurf", kind: "file-json",
57
+ file: () => join(home, ".codeium", "windsurf", "mcp_config.json"),
58
+ apply: (cfg, key, url) => void (obj(cfg, "mcpServers")[key] = { serverUrl: url }),
59
+ remove: (cfg, key) => void delete obj(cfg, "mcpServers")[key],
60
+ },
61
+ ];
62
+ function readCfg(t) {
63
+ const f = t.file();
64
+ if (!existsSync(f))
65
+ return {};
66
+ const raw = readFileSync(f, "utf8");
67
+ if (!raw.trim())
68
+ return {};
69
+ return t.kind === "file-toml" ? parseToml(raw) : JSON.parse(raw);
70
+ }
71
+ function writeCfg(t, cfg) {
72
+ const f = t.file();
73
+ mkdirSync(dirname(f), { recursive: true });
74
+ if (existsSync(f))
75
+ copyFileSync(f, `${f}.bak`);
76
+ writeFileSync(f, t.kind === "file-toml" ? stringifyToml(cfg) : `${JSON.stringify(cfg, null, 2)}\n`);
77
+ }
78
+ function installOne(t, url) {
79
+ const key = brand().bin;
80
+ const stdio = { command: brand().bin, args: ["mcp", "serve", "--stdio", ...(context().env === "sandbox" ? ["--sandbox"] : [])] };
81
+ if (t.kind === "manual") {
82
+ info(`${c.cyan("•")} ${t.name}: ${tr("vào Cài đặt › Kết nối (Connectors) › Thêm, dán", "open Settings › Connectors › Add, paste")} ${c.bold(url)}`);
83
+ return;
84
+ }
85
+ if (t.kind === "command") {
86
+ const r = spawnSync("claude", ["mcp", "add", "--transport", "http", "--scope", "user", key, url], { stdio: "pipe", encoding: "utf8" });
87
+ if (r.error)
88
+ return info(`${c.cyan("•")} ${t.name}: ${tr("chạy", "run")} ${c.bold(`claude mcp add --transport http ${key} ${url}`)}`);
89
+ if (r.status === 0)
90
+ return ok(`${t.name}: ${tr(`đã thêm "${key}" → ${url}`, `added "${key}" → ${url}`)}`);
91
+ return warn(`${t.name}: ${(r.stderr || r.stdout).trim()}`);
92
+ }
93
+ const cfg = readCfg(t);
94
+ t.apply(cfg, key, url, stdio);
95
+ writeCfg(t, cfg);
96
+ ok(`${t.name}: ${tr(`đã thêm "${key}" vào`, `added "${key}" to`)} ${c.dim(t.file())}`);
97
+ }
98
+ export function registerMcp(program) {
99
+ const mcp = program.command("mcp").description(tr("kết nối máy chủ MCP vào trợ lý AI khác", "connect the MCP server to other AI assistants"));
100
+ mcp.command("url").description(tr("in địa chỉ máy chủ MCP", "print the MCP server URL")).action(() => info(context().endpoints.mcp));
101
+ mcp.command("install [clients...]")
102
+ .description(tr(`cài vào: ${TARGETS.map((t) => t.id).join(", ")}`, `install into: ${TARGETS.map((t) => t.id).join(", ")}`))
103
+ .action(async (ids) => {
104
+ const url = context().endpoints.mcp;
105
+ let chosen = TARGETS.filter((t) => ids.includes(t.id));
106
+ const unknown = ids.filter((i) => !TARGETS.some((t) => t.id === i));
107
+ if (unknown.length)
108
+ throw new CliError(tr(`Không biết client: ${unknown.join(", ")}`, `Unknown client: ${unknown.join(", ")}`), Exit.usage);
109
+ if (!chosen.length) {
110
+ if (!hasTty()) {
111
+ info(url);
112
+ return;
113
+ }
114
+ chosen = (await multiSelect(tr("Cài máy chủ MCP vào client nào? (mặc định: mọi client MCP)", "Install the MCP server into which clients? (default: any MCP client)"), TARGETS.map((t) => ({ label: t.name, value: t })))) ?? [];
115
+ }
116
+ for (const t of chosen)
117
+ installOne(t, url);
118
+ if (chosen.length)
119
+ info(c.dim(tr(" Lần đầu mỗi client sẽ mở trình duyệt để bạn đồng ý quyền.", " Each client opens the browser once so you can approve access.")));
120
+ });
121
+ mcp.command("uninstall <client>").action((id) => {
122
+ const t = TARGETS.find((x) => x.id === id);
123
+ if (!t)
124
+ throw new CliError(tr("Không biết client này.", "Unknown client."), Exit.usage);
125
+ if (t.kind === "command")
126
+ return void spawnSync("claude", ["mcp", "remove", brand().bin], { stdio: "inherit" });
127
+ if (t.kind === "manual")
128
+ return info(tr(`${t.name}: gỡ trong Cài đặt › Kết nối.`, `${t.name}: remove it in Settings › Connectors.`));
129
+ const cfg = readCfg(t);
130
+ t.remove(cfg, brand().bin);
131
+ writeCfg(t, cfg);
132
+ ok(tr(`Đã gỡ khỏi ${t.name}.`, `Removed from ${t.name}.`));
133
+ });
134
+ mcp.command("serve")
135
+ .description(tr("chạy MCP qua stdio bằng quyền của CLI (cho client không hỗ trợ OAuth)", "serve MCP over stdio with the CLI's grant (for clients without OAuth)"))
136
+ .option("--stdio", tr("dùng stdin/stdout (mặc định)", "use stdin/stdout (default)"))
137
+ .action(async () => serveStdio());
138
+ }
139
+ /** Line-delimited JSON-RPC on stdio ⇄ Streamable HTTP to the hosted server. Logs go to stderr only. */
140
+ async function serveStdio() {
141
+ const url = context().endpoints.mcp;
142
+ let token = await mcpToken({ interactive: false });
143
+ let sessionId;
144
+ const rl = createInterface({ input: process.stdin });
145
+ const write = (m) => process.stdout.write(`${JSON.stringify(m)}\n`);
146
+ for await (const line of rl) {
147
+ if (!line.trim())
148
+ continue;
149
+ let msg;
150
+ try {
151
+ msg = JSON.parse(line);
152
+ }
153
+ catch {
154
+ continue;
155
+ }
156
+ const send = async () => fetch(url, {
157
+ method: "POST",
158
+ headers: {
159
+ "Content-Type": "application/json",
160
+ Accept: "application/json, text/event-stream",
161
+ Authorization: `Bearer ${token}`,
162
+ "User-Agent": userAgent(),
163
+ ...(sessionId ? { "Mcp-Session-Id": sessionId } : {}),
164
+ },
165
+ body: line,
166
+ });
167
+ try {
168
+ let res = await send();
169
+ if (res.status === 401) {
170
+ token = await mcpToken({ interactive: false });
171
+ res = await send();
172
+ }
173
+ const sid = res.headers.get("mcp-session-id");
174
+ if (sid)
175
+ sessionId = sid;
176
+ if (msg.id === undefined) {
177
+ await res.body?.cancel();
178
+ continue;
179
+ }
180
+ const ct = res.headers.get("content-type") ?? "";
181
+ if (ct.includes("text/event-stream")) {
182
+ const text = await res.text();
183
+ for (const frame of text.split("\n\n")) {
184
+ const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("\n");
185
+ if (data)
186
+ process.stdout.write(`${data}\n`);
187
+ }
188
+ }
189
+ else {
190
+ const body = await res.text();
191
+ if (body)
192
+ process.stdout.write(`${body.trim()}\n`);
193
+ else
194
+ write({ jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: `HTTP ${res.status}` } });
195
+ }
196
+ }
197
+ catch (e) {
198
+ process.stderr.write(`${e.message}\n`);
199
+ write({ jsonrpc: "2.0", id: msg.id, error: { code: -32603, message: e.message } });
200
+ }
201
+ }
202
+ }