@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,90 @@
1
+ // User-facing strings. Vietnamese first; English for --lang en / LANG=en_*.
2
+ import { brand, envVar } from "./brand.js";
3
+ import { loadConfig } from "./config.js";
4
+ let lang;
5
+ export function setLang(l) {
6
+ lang = l;
7
+ }
8
+ export function currentLang() {
9
+ if (lang)
10
+ return lang;
11
+ const fromEnv = envVar("LANG");
12
+ if (fromEnv === "en" || fromEnv === "vi")
13
+ return fromEnv;
14
+ try {
15
+ const c = loadConfig().lang;
16
+ if (c)
17
+ return c;
18
+ }
19
+ catch {
20
+ // unreadable config falls back to the system locale
21
+ }
22
+ const sys = process.env.LC_ALL ?? process.env.LC_MESSAGES ?? process.env.LANG ?? "";
23
+ return sys.toLowerCase().startsWith("en") ? "en" : "vi";
24
+ }
25
+ const vi = {
26
+ "auth.notLoggedIn": "Chưa đăng nhập.",
27
+ "auth.notLoggedIn.hint": "Chạy `{bin} login` (hoặc đặt {prefix}_API_KEY cho CI).",
28
+ "auth.expired": "Phiên trên máy này đã hết.",
29
+ "auth.expired.hint": "Đăng nhập lại: `{bin} login`",
30
+ "auth.revoked": "Phiên trên máy này đã bị đăng xuất (từ web hoặc quản trị).",
31
+ "auth.clock": "Đồng hồ máy lệch {min} phút so với máy chủ, token bị coi là sai.",
32
+ "auth.clock.hint": "Bật đồng bộ giờ rồi thử lại (`{bin} doctor`).",
33
+ "auth.apiKeyNoChat": "Khoá API không dùng được chế độ chat AI (đó là danh tính agent, không phải người).",
34
+ "auth.apiKeyNoChat.hint": "Dùng `{bin} tools call …`, hoặc `{bin} login --ci` trong CI.",
35
+ "net.failed": "Không kết nối được tới {host}.",
36
+ "net.failed.hint": "Kiểm mạng, proxy (HTTPS_PROXY) hoặc chạy `{bin} doctor`.",
37
+ "net.tls": "Không tin được chứng chỉ của máy chủ (có thể do proxy công ty).",
38
+ "net.tls.hint": "Đặt NODE_EXTRA_CA_CERTS=<tệp CA> hoặc HTTPS_PROXY=…",
39
+ "http.forbidden": "Không đủ quyền.",
40
+ "http.notFound": "Không tìm thấy.",
41
+ "http.rateLimited": "Hỏi nhanh quá, thử lại sau {sec} giây.",
42
+ "http.server": "{name} đang lỗi (mã yêu cầu {rid}).",
43
+ "http.server.hint": "Thử lại sau, hoặc `/support` kèm mã này.",
44
+ "http.tooOld": "Bản {cur} đã cũ, máy chủ cần ≥ {min}.",
45
+ "http.tooOld.hint": "Chạy `{bin} update`.",
46
+ "ws.gone": "Bạn không còn ở workspace này, hoặc workspace đang tạm khoá.",
47
+ "ws.gone.hint": "`{bin} workspace list`",
48
+ "ws.none": "Tài khoản chưa có công ty nào.",
49
+ "ws.none.hint": "Tạo tại {app} rồi chạy lại `{bin} login`.",
50
+ "keychain.fallback": "Không có Keychain. Token lưu tại {file} (chỉ bạn đọc được).",
51
+ "interrupted": "Đã huỷ.",
52
+ };
53
+ const en = {
54
+ "auth.notLoggedIn": "Not logged in.",
55
+ "auth.notLoggedIn.hint": "Run `{bin} login` (or set {prefix}_API_KEY in CI).",
56
+ "auth.expired": "Your session on this machine has expired.",
57
+ "auth.expired.hint": "Log in again: `{bin} login`",
58
+ "auth.revoked": "This machine was signed out (from the web or by an admin).",
59
+ "auth.clock": "Your clock is {min} minutes off from the server, so tokens look invalid.",
60
+ "auth.clock.hint": "Turn on automatic time sync and retry (`{bin} doctor`).",
61
+ "auth.apiKeyNoChat": "API keys cannot use AI chat (an API key is an agent identity, not a person).",
62
+ "auth.apiKeyNoChat.hint": "Use `{bin} tools call …`, or `{bin} login --ci` in CI.",
63
+ "net.failed": "Cannot reach {host}.",
64
+ "net.failed.hint": "Check your network, proxy (HTTPS_PROXY) or run `{bin} doctor`.",
65
+ "net.tls": "The server certificate is not trusted (often a corporate proxy).",
66
+ "net.tls.hint": "Set NODE_EXTRA_CA_CERTS=<ca file> or HTTPS_PROXY=…",
67
+ "http.forbidden": "Permission denied.",
68
+ "http.notFound": "Not found.",
69
+ "http.rateLimited": "Too many requests, retry in {sec} seconds.",
70
+ "http.server": "{name} returned an error (request id {rid}).",
71
+ "http.server.hint": "Try again later, or `/support` with this id.",
72
+ "http.tooOld": "Version {cur} is too old, the server needs ≥ {min}.",
73
+ "http.tooOld.hint": "Run `{bin} update`.",
74
+ "ws.gone": "You are no longer in this workspace, or it is suspended.",
75
+ "ws.gone.hint": "`{bin} workspace list`",
76
+ "ws.none": "Your account has no company yet.",
77
+ "ws.none.hint": "Create one at {app}, then run `{bin} login` again.",
78
+ "keychain.fallback": "No system keychain. Tokens are stored in {file} (readable only by you).",
79
+ "interrupted": "Cancelled.",
80
+ };
81
+ export function t(key, vars = {}) {
82
+ const dict = currentLang() === "en" ? en : vi;
83
+ const b = brand();
84
+ const all = { bin: b.bin, prefix: b.envPrefix, name: b.name, ...vars };
85
+ return (dict[key] ?? vi[key] ?? key).replace(/\{(\w+)\}/g, (_, k) => String(all[k] ?? `{${k}}`));
86
+ }
87
+ /** Inline bilingual helper for one-off strings: tr("Đã lưu", "Saved"). */
88
+ export function tr(viText, enText) {
89
+ return currentLang() === "en" ? enText : viText;
90
+ }
@@ -0,0 +1,44 @@
1
+ // Cross-process lock around token refresh. The server already tolerates concurrent refreshes of the same
2
+ // token (30 s grace + 7 day previous-token window), so this only avoids redundant calls: the process that
3
+ // waits re-reads the stored session instead of refreshing again.
4
+ import { openSync, closeSync, unlinkSync, statSync, writeSync } from "node:fs";
5
+ import { setTimeout as sleep } from "node:timers/promises";
6
+ const STALE_MS = 30_000;
7
+ export async function withFileLock(path, fn, timeoutMs = 20_000) {
8
+ const started = Date.now();
9
+ let fd;
10
+ while (fd === undefined) {
11
+ try {
12
+ fd = openSync(path, "wx", 0o600);
13
+ writeSync(fd, String(process.pid));
14
+ }
15
+ catch (e) {
16
+ if (e.code !== "EEXIST")
17
+ throw e;
18
+ try {
19
+ if (Date.now() - statSync(path).mtimeMs > STALE_MS)
20
+ unlinkSync(path);
21
+ }
22
+ catch {
23
+ // removed by the holder meanwhile
24
+ }
25
+ if (Date.now() - started > timeoutMs)
26
+ break;
27
+ await sleep(100);
28
+ }
29
+ }
30
+ try {
31
+ return await fn();
32
+ }
33
+ finally {
34
+ if (fd !== undefined) {
35
+ closeSync(fd);
36
+ try {
37
+ unlinkSync(path);
38
+ }
39
+ catch {
40
+ // already removed
41
+ }
42
+ }
43
+ }
44
+ }
@@ -0,0 +1,74 @@
1
+ // Plain (non-Ink) output for subcommands. Colors follow NO_COLOR / --no-color / non-TTY.
2
+ import { stdout, stderr } from "node:process";
3
+ let colorOn = stdout.isTTY === true && !process.env.NO_COLOR;
4
+ let quiet = false;
5
+ let format = "text";
6
+ export function setOutput(opts) {
7
+ if (opts.color === false)
8
+ colorOn = false;
9
+ if (opts.quiet !== undefined)
10
+ quiet = opts.quiet;
11
+ if (opts.format)
12
+ format = opts.format;
13
+ }
14
+ export const outputFormat = () => format;
15
+ export const isJson = () => format !== "text";
16
+ const wrap = (open, close) => (s) => (colorOn ? `\u001b[${open}m${s}\u001b[${close}m` : s);
17
+ export const c = {
18
+ bold: wrap(1, 22),
19
+ dim: wrap(2, 22),
20
+ red: wrap(31, 39),
21
+ green: wrap(32, 39),
22
+ yellow: wrap(33, 39),
23
+ cyan: wrap(36, 39),
24
+ };
25
+ export function info(line = "") {
26
+ if (!quiet && !isJson())
27
+ stdout.write(`${line}\n`);
28
+ }
29
+ export function ok(line) {
30
+ if (!quiet && !isJson())
31
+ stdout.write(`${c.green("✓")} ${line}\n`);
32
+ }
33
+ export function warn(line) {
34
+ stderr.write(`${c.yellow("⚠")} ${line}\n`);
35
+ }
36
+ export function fail(line, hint) {
37
+ stderr.write(`${c.red("✗")} ${line}\n`);
38
+ if (hint)
39
+ stderr.write(` ${c.dim(hint)}\n`);
40
+ }
41
+ /** Drops null/undefined fields so JSON output stays readable; arrays keep their length. */
42
+ export function compact(v) {
43
+ if (Array.isArray(v))
44
+ return v.map(compact);
45
+ if (v && typeof v === "object")
46
+ return Object.fromEntries(Object.entries(v).filter(([, x]) => x !== null && x !== undefined).map(([k, x]) => [k, compact(x)]));
47
+ return v;
48
+ }
49
+ export function json(value) {
50
+ stdout.write(`${JSON.stringify(compact(value), null, format === "json" ? 2 : 0)}\n`);
51
+ }
52
+ /** Prints `value` as JSON in json mode, otherwise calls `text`. */
53
+ export function emit(value, text) {
54
+ if (isJson())
55
+ json(value);
56
+ else
57
+ text();
58
+ }
59
+ const width = (s) => [...s.replace(/\u001b\[[0-9;]*m/g, "")].length;
60
+ /** Simple aligned table with box borders, used for list commands. */
61
+ export function table(headers, rows, align = []) {
62
+ const cells = rows.map((r) => r.map((v) => (v === null || v === undefined ? "" : String(v))));
63
+ const w = headers.map((h, i) => Math.max(width(h), ...cells.map((r) => width(r[i] ?? ""))));
64
+ const pad = (s, i) => {
65
+ const n = (w[i] ?? 0) - width(s);
66
+ return align[i] === "r" ? " ".repeat(n) + s : s + " ".repeat(n);
67
+ };
68
+ const line = (l, m, r) => l + w.map((n) => "─".repeat(n + 2)).join(m) + r;
69
+ const row = (r) => "│" + r.map((s, i) => ` ${pad(s, i)} `).join("│") + "│";
70
+ return [line("┌", "┬", "┐"), row(headers.map((h) => c.bold(h))), line("├", "┼", "┤"), ...cells.map(row), line("└", "┴", "┘")].join("\n");
71
+ }
72
+ export function money(n) {
73
+ return new Intl.NumberFormat("vi-VN").format(n);
74
+ }
@@ -0,0 +1,29 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { mkdirSync } from "node:fs";
4
+ import { brand } from "./brand.js";
5
+ function base(kind) {
6
+ const b = brand().bin;
7
+ if (process.platform === "win32") {
8
+ const root = process.env.APPDATA ?? join(homedir(), "AppData", "Roaming");
9
+ return join(root, b, kind === "state" ? "state" : "");
10
+ }
11
+ if (kind === "config")
12
+ return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), b);
13
+ return join(process.env.XDG_STATE_HOME ?? join(homedir(), ".local", "state"), b);
14
+ }
15
+ export function configDir() {
16
+ const d = base("config");
17
+ mkdirSync(d, { recursive: true, mode: 0o700 });
18
+ return d;
19
+ }
20
+ export function stateDir() {
21
+ const d = base("state");
22
+ mkdirSync(d, { recursive: true, mode: 0o700 });
23
+ return d;
24
+ }
25
+ export const configFile = () => join(configDir(), "config.toml");
26
+ export const credentialsFile = () => join(configDir(), "credentials.json");
27
+ export const historyFile = () => join(stateDir(), "history");
28
+ export const lockFile = (profile) => join(stateDir(), `${profile}.lock`);
29
+ export const updateCheckFile = () => join(stateDir(), "update-check.json");
@@ -0,0 +1,137 @@
1
+ // Minimal prompts for subcommands (the REPL has its own Ink components). All prompts refuse to run
2
+ // without a TTY so scripts never hang waiting for input.
3
+ import { emitKeypressEvents } from "node:readline";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { stdin, stderr } from "node:process";
6
+ import { CliError, Exit } from "./errors.js";
7
+ import { c } from "./output.js";
8
+ import { t, tr } from "./i18n.js";
9
+ export const hasTty = () => stdin.isTTY === true && stderr.isTTY === true;
10
+ function needTty() {
11
+ if (!hasTty())
12
+ throw new CliError(tr("Cần terminal tương tác cho bước này.", "This step needs an interactive terminal."), Exit.usage);
13
+ }
14
+ export async function ask(question, signal) {
15
+ needTty();
16
+ const rl = createInterface({ input: stdin, output: stderr });
17
+ try {
18
+ return (await rl.question(question, signal ? { signal } : {})).trim();
19
+ }
20
+ finally {
21
+ rl.close();
22
+ }
23
+ }
24
+ /** [y/N] — anything but y/yes/có/c is no. Ctrl+C = no. */
25
+ export async function confirm(question) {
26
+ needTty();
27
+ try {
28
+ const a = (await ask(`${question} ${c.bold("[y/N]")} `)).toLowerCase();
29
+ return a === "y" || a === "yes" || a === "c" || a === "có" || a === "co";
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ /** Typed confirmation for large writes: the user must type `expected` exactly. */
36
+ export async function confirmTyped(question, expected) {
37
+ needTty();
38
+ const a = await ask(`${question} ${tr("Gõ", "Type")} ${c.bold(expected)} ${tr("để xác nhận, Enter để huỷ", "to confirm, Enter to cancel")} › `);
39
+ return a === expected;
40
+ }
41
+ /** Arrow-key single select. Returns undefined on Esc / Ctrl+C. */
42
+ export async function select(title, choices, initial = 0) {
43
+ needTty();
44
+ if (choices.length === 0)
45
+ return undefined;
46
+ let i = Math.min(initial, choices.length - 1);
47
+ const out = stderr;
48
+ emitKeypressEvents(stdin);
49
+ const wasRaw = stdin.isRaw;
50
+ stdin.setRawMode(true);
51
+ stdin.resume();
52
+ const draw = (first) => {
53
+ if (!first)
54
+ out.write(`\u001b[${choices.length}A`);
55
+ choices.forEach((ch, k) => {
56
+ const mark = k === i ? c.cyan("❯ ") : " ";
57
+ const label = k === i ? c.cyan(ch.label) : ch.label;
58
+ out.write(`\u001b[2K${mark}${label}${ch.hint ? " " + c.dim(ch.hint) : ""}\n`);
59
+ });
60
+ };
61
+ out.write(`${title}\n`);
62
+ draw(true);
63
+ return new Promise((resolve) => {
64
+ const done = (v) => {
65
+ stdin.off("keypress", onKey);
66
+ stdin.setRawMode(wasRaw);
67
+ stdin.pause();
68
+ resolve(v);
69
+ };
70
+ const onKey = (_, key) => {
71
+ if (key.ctrl && key.name === "c")
72
+ return done(undefined);
73
+ if (key.name === "escape")
74
+ return done(undefined);
75
+ if (key.name === "up" || key.name === "k")
76
+ i = (i - 1 + choices.length) % choices.length;
77
+ else if (key.name === "down" || key.name === "j")
78
+ i = (i + 1) % choices.length;
79
+ else if (key.name === "return")
80
+ return done(choices[i].value);
81
+ else
82
+ return;
83
+ draw(false);
84
+ };
85
+ stdin.on("keypress", onKey);
86
+ });
87
+ }
88
+ /** Multi select with space; Enter accepts. */
89
+ export async function multiSelect(title, choices) {
90
+ needTty();
91
+ let i = 0;
92
+ const on = new Set();
93
+ const out = stderr;
94
+ emitKeypressEvents(stdin);
95
+ const wasRaw = stdin.isRaw;
96
+ stdin.setRawMode(true);
97
+ stdin.resume();
98
+ const draw = (first) => {
99
+ if (!first)
100
+ out.write(`\u001b[${choices.length + 1}A`);
101
+ choices.forEach((ch, k) => {
102
+ const box = on.has(k) ? c.green("◼") : "◻";
103
+ const label = k === i ? c.cyan(ch.label) : ch.label;
104
+ out.write(`\u001b[2K${k === i ? c.cyan("❯ ") : " "}${box} ${label}${ch.hint ? " " + c.dim(ch.hint) : ""}\n`);
105
+ });
106
+ out.write(`\u001b[2K${c.dim(tr(" Space chọn · Enter xong · Esc huỷ", " Space toggle · Enter accept · Esc cancel"))}\n`);
107
+ };
108
+ out.write(`${title}\n`);
109
+ draw(true);
110
+ return new Promise((resolve) => {
111
+ const done = (v) => {
112
+ stdin.off("keypress", onKey);
113
+ stdin.setRawMode(wasRaw);
114
+ stdin.pause();
115
+ resolve(v);
116
+ };
117
+ const onKey = (_, key) => {
118
+ if ((key.ctrl && key.name === "c") || key.name === "escape")
119
+ return done(undefined);
120
+ if (key.name === "up")
121
+ i = (i - 1 + choices.length) % choices.length;
122
+ else if (key.name === "down")
123
+ i = (i + 1) % choices.length;
124
+ else if (key.name === "space")
125
+ on.has(i) ? on.delete(i) : on.add(i);
126
+ else if (key.name === "return")
127
+ return done([...on].sort().map((k) => choices[k].value));
128
+ else
129
+ return;
130
+ draw(false);
131
+ };
132
+ stdin.on("keypress", onKey);
133
+ });
134
+ }
135
+ export function interrupted() {
136
+ return new CliError(t("interrupted"), Exit.interrupted);
137
+ }
@@ -0,0 +1,13 @@
1
+ // Standalone binaries get the version injected at build time (--define __CLI_VERSION__); npm installs read package.json.
2
+ import { createRequire } from "node:module";
3
+ function read() {
4
+ if (typeof __CLI_VERSION__ === "string")
5
+ return __CLI_VERSION__;
6
+ try {
7
+ return createRequire(import.meta.url)("../../package.json").version;
8
+ }
9
+ catch {
10
+ return "0.0.0-dev";
11
+ }
12
+ }
13
+ export const VERSION = read();
package/dist/main.js ADDED
@@ -0,0 +1,38 @@
1
+ import { buildProgram } from "./program.js";
2
+ import { CliError, Exit, isCliError } from "./core/errors.js";
3
+ import { fail, isJson } from "./core/output.js";
4
+ import { maybeNotifyUpdate } from "./commands/util.js";
5
+ import { setLang } from "./core/i18n.js";
6
+ import { envVar } from "./core/brand.js";
7
+ export async function main(argv) {
8
+ // Help text is built before flags are parsed, so honour --lang early.
9
+ const li = argv.indexOf("--lang");
10
+ const early = li > 0 ? argv[li + 1] : argv.find((a) => a.startsWith("--lang="))?.slice(7);
11
+ if (early === "en" || early === "vi")
12
+ setLang(early);
13
+ const program = buildProgram();
14
+ program.exitOverride();
15
+ try {
16
+ await program.parseAsync(argv);
17
+ await maybeNotifyUpdate();
18
+ }
19
+ catch (e) {
20
+ process.exitCode = report(e);
21
+ }
22
+ }
23
+ function report(e) {
24
+ const err = e;
25
+ // commander: --help / --version / usage errors
26
+ if (err?.code === "commander.helpDisplayed" || err?.code === "commander.version" || err?.code === "commander.help")
27
+ return Exit.ok;
28
+ if (typeof err?.code === "string" && err.code.startsWith("commander."))
29
+ return Exit.usage;
30
+ const ce = isCliError(e) ? e : new CliError(e?.message ?? String(e), Exit.general);
31
+ if (isJson())
32
+ process.stderr.write(`${JSON.stringify({ error: { message: ce.message, hint: ce.hint, exitCode: ce.exitCode, requestId: ce.requestId } })}\n`);
33
+ else
34
+ fail(ce.message + (ce.requestId && !ce.message.includes(ce.requestId) ? ` (${ce.requestId})` : ""), ce.hint);
35
+ if (envVar("DEBUG") === "1" && !isCliError(e))
36
+ console.error(e);
37
+ return ce.exitCode;
38
+ }
@@ -0,0 +1,114 @@
1
+ // Minimal MCP client over Streamable HTTP (JSON-RPC 2.0): initialize, tools/list, tools/call.
2
+ // Responses may arrive as JSON or as an SSE stream carrying the JSON-RPC message.
3
+ import { fetch } from "undici";
4
+ import { context } from "../core/context.js";
5
+ import { CliError, Exit } from "../core/errors.js";
6
+ import { tr } from "../core/i18n.js";
7
+ import { errorFor, networkError, userAgent } from "../core/http.js";
8
+ import { VERSION } from "../core/version.js";
9
+ import { brand } from "../core/brand.js";
10
+ import { mcpToken, deleteGrant } from "../auth/mcp-oauth.js";
11
+ const PROTOCOL = "2025-06-18";
12
+ export class McpClient {
13
+ interactive;
14
+ apiKey;
15
+ sessionId;
16
+ nextId = 1;
17
+ token;
18
+ ready = false;
19
+ constructor(interactive, apiKey) {
20
+ this.interactive = interactive;
21
+ this.apiKey = apiKey;
22
+ }
23
+ async rpc(method, params, notification = false) {
24
+ const url = context().endpoints.mcp;
25
+ this.token ??= this.apiKey ?? (await mcpToken({ interactive: this.interactive }));
26
+ const id = notification ? undefined : this.nextId++;
27
+ const headers = {
28
+ "Content-Type": "application/json",
29
+ Accept: "application/json, text/event-stream",
30
+ "User-Agent": userAgent(),
31
+ "MCP-Protocol-Version": PROTOCOL,
32
+ Authorization: `Bearer ${this.token}`,
33
+ };
34
+ if (this.sessionId)
35
+ headers["Mcp-Session-Id"] = this.sessionId;
36
+ let res;
37
+ try {
38
+ res = await fetch(url, { method: "POST", headers, body: JSON.stringify({ jsonrpc: "2.0", ...(id ? { id } : {}), method, ...(params ? { params } : {}) }) });
39
+ }
40
+ catch (e) {
41
+ throw networkError(e, url);
42
+ }
43
+ if (res.status === 401 && !this.apiKey) {
44
+ // Grant revoked or scopes changed: drop it so the next run asks again.
45
+ deleteGrant();
46
+ throw new CliError(tr("Quyền công cụ AI của CLI đã hết hoặc bị thu hồi.", "The CLI's AI tool access expired or was revoked."), Exit.auth, `${brand().bin} tools login`);
47
+ }
48
+ const sid = res.headers.get("mcp-session-id");
49
+ if (sid)
50
+ this.sessionId = sid;
51
+ if (notification) {
52
+ await res.body?.cancel();
53
+ return undefined;
54
+ }
55
+ const ct = res.headers.get("content-type") ?? "";
56
+ let msg;
57
+ if (ct.includes("text/event-stream")) {
58
+ const text = await res.text();
59
+ for (const frame of text.split("\n\n")) {
60
+ const data = frame.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).join("\n");
61
+ if (!data)
62
+ continue;
63
+ const m = JSON.parse(data);
64
+ if (m.id === id)
65
+ msg = m;
66
+ }
67
+ }
68
+ else {
69
+ const body = await res.json().catch(() => undefined);
70
+ if (!res.ok)
71
+ throw errorFor(res.status, body, res);
72
+ msg = body;
73
+ }
74
+ if (!msg)
75
+ throw new CliError(tr("Máy chủ MCP không trả lời.", "The MCP server did not answer."), Exit.general);
76
+ if (msg.error)
77
+ throw new CliError(msg.error.message, msg.error.code === -32602 ? Exit.usage : Exit.general);
78
+ return msg.result;
79
+ }
80
+ async init() {
81
+ if (this.ready)
82
+ return;
83
+ await this.rpc("initialize", { protocolVersion: PROTOCOL, capabilities: {}, clientInfo: { name: `${brand().bin}-cli`, version: VERSION } });
84
+ await this.rpc("notifications/initialized", undefined, true);
85
+ this.ready = true;
86
+ }
87
+ async listTools() {
88
+ await this.init();
89
+ const all = [];
90
+ let cursor;
91
+ do {
92
+ const r = await this.rpc("tools/list", cursor ? { cursor } : {});
93
+ all.push(...r.tools);
94
+ cursor = r.nextCursor;
95
+ } while (cursor);
96
+ return all;
97
+ }
98
+ async callTool(name, args) {
99
+ await this.init();
100
+ return this.rpc("tools/call", { name, arguments: args });
101
+ }
102
+ }
103
+ /** Joins text content of a tool result; parses JSON when the text is JSON. */
104
+ export function resultValue(r) {
105
+ if (r.structuredContent !== undefined)
106
+ return r.structuredContent;
107
+ const text = (r.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join("\n");
108
+ try {
109
+ return JSON.parse(text);
110
+ }
111
+ catch {
112
+ return text;
113
+ }
114
+ }
@@ -0,0 +1,102 @@
1
+ import { Command, Option } from "commander";
2
+ import { brand } from "./core/brand.js";
3
+ import { initContext } from "./core/context.js";
4
+ import { setOutput } from "./core/output.js";
5
+ import { setLang } from "./core/i18n.js";
6
+ import { VERSION } from "./core/version.js";
7
+ import { tr } from "./core/i18n.js";
8
+ import { registerAuth } from "./commands/auth.js";
9
+ import { registerWorkspace } from "./commands/workspace.js";
10
+ import { registerAi, runRoot } from "./commands/ai.js";
11
+ import { registerTools } from "./commands/tools.js";
12
+ import { registerApi } from "./commands/api.js";
13
+ import { registerKeys } from "./commands/keys.js";
14
+ import { registerWebhooks } from "./commands/webhooks.js";
15
+ import { registerDocs } from "./commands/docs.js";
16
+ import { registerMcp } from "./commands/mcp.js";
17
+ import { registerUtil } from "./commands/util.js";
18
+ import { registerHelp } from "./commands/help.js";
19
+ const collect = (v, prev = []) => [...prev, v];
20
+ export function buildProgram() {
21
+ const b = brand();
22
+ const program = new Command(b.bin)
23
+ .description(tr(`${b.aiName} và công cụ nhà phát triển trong terminal`, `${b.aiName} and developer tools in your terminal`))
24
+ .version(VERSION, "-v, --version", tr("in phiên bản", "print the version"))
25
+ .helpOption("-h, --help", tr("trợ giúp", "show help"))
26
+ .argument("[question...]", tr("câu hỏi gửi ngay khi mở chat", "question to send when chat opens"))
27
+ .option("-p, --print [question]", tr("không tương tác: in câu trả lời rồi thoát (đọc thêm từ stdin)", "non-interactive: print the answer and exit (reads stdin too)"))
28
+ .option("-c, --continue", tr("nối hội thoại gần nhất", "continue the last conversation"))
29
+ .option("-r, --resume [id]", tr("mở lại hội thoại cũ (không id = chọn trong danh sách)", "resume a conversation (no id = pick from a list)"))
30
+ .option("-f, --file <path>", tr("đính kèm tệp Excel/CSV, ảnh hoặc tệp chữ (lặp được, tối đa 3)", "attach an Excel/CSV, image or text file (repeatable, max 3)"), collect)
31
+ .addOption(new Option("--profile <name>", tr("hồ sơ đăng nhập", "login profile")))
32
+ .option("--sandbox", tr("dùng môi trường thử nghiệm", "use the sandbox environment"))
33
+ .option("-w, --workspace <code>", tr("chạy lệnh này trên workspace khác", "run this command on another workspace"))
34
+ .option("--agent <code>", tr("chọn Agent thay cho Trợ lý chung", "choose an Agent instead of the general assistant"))
35
+ .addOption(new Option("--mode <mode>", tr("chế độ quyền", "permission mode")).choices(["read", "propose", "auto"]))
36
+ .addOption(new Option("--output-format <format>", tr("định dạng đầu ra", "output format")).choices(["text", "json", "stream-json"]))
37
+ .option("--json", tr("viết tắt của --output-format json", "shorthand for --output-format json"))
38
+ .addOption(new Option("--lang <lang>", tr("ngôn ngữ", "language")).choices(["vi", "en"]))
39
+ .option("--no-color", tr("tắt màu", "disable colors"))
40
+ .option("--quiet", tr("chỉ in kết quả", "print results only"))
41
+ .option("--verbose", tr("in chi tiết", "verbose output"))
42
+ .option("--debug", tr("in request-id và thời gian từng cửa (không in token)", "print request ids and timings (never tokens)"))
43
+ .enablePositionalOptions()
44
+ .showSuggestionAfterError(true)
45
+ .configureHelp({ sortSubcommands: false, showGlobalOptions: true });
46
+ program.hook("preAction", (_this, action) => {
47
+ // Global flags work before or after the subcommand: `nextx --sandbox login` and `nextx login --sandbox`.
48
+ const o = { ...program.opts(), ...pickGlobals(action.opts()) };
49
+ setLang(o.lang);
50
+ setOutput({ color: o.color, quiet: o.quiet, format: o.json ? "json" : o.outputFormat });
51
+ initContext(o);
52
+ });
53
+ program.action(async (question) => runRoot(program.opts(), question));
54
+ registerAuth(program);
55
+ registerWorkspace(program);
56
+ registerAi(program);
57
+ registerTools(program);
58
+ registerApi(program);
59
+ registerKeys(program);
60
+ registerWebhooks(program);
61
+ registerDocs(program);
62
+ registerMcp(program);
63
+ registerUtil(program);
64
+ registerHelp(program);
65
+ for (const sub of program.commands)
66
+ addGlobals(sub);
67
+ return program;
68
+ }
69
+ /** Flags every subcommand accepts too (hidden in its help; listed once under the root). `-f` stays root-only: `api -f` means a field. */
70
+ const GLOBALS = [
71
+ ["--profile <name>", "profile"],
72
+ ["--sandbox", "sandbox"],
73
+ ["-w, --workspace <code>", "workspace"],
74
+ ["--agent <code>", "agent"],
75
+ ["--mode <mode>", "mode", ["read", "propose", "auto"]],
76
+ ["--output-format <format>", "outputFormat", ["text", "json", "stream-json"]],
77
+ ["--json", "json"],
78
+ ["--lang <lang>", "lang", ["vi", "en"]],
79
+ ["--no-color", "color"],
80
+ ["--quiet", "quiet"],
81
+ ["--verbose", "verbose"],
82
+ ["--debug", "debug"],
83
+ ];
84
+ function addGlobals(cmd) {
85
+ for (const [flags, , choices] of GLOBALS) {
86
+ const o = new Option(flags).hideHelp();
87
+ if (choices)
88
+ o.choices(choices);
89
+ const taken = cmd.options.some((x) => x.long === o.long || (o.short && x.short === o.short));
90
+ if (!taken)
91
+ cmd.addOption(o);
92
+ }
93
+ for (const sub of cmd.commands)
94
+ addGlobals(sub);
95
+ }
96
+ function pickGlobals(o) {
97
+ const out = {};
98
+ for (const [, key] of GLOBALS)
99
+ if (o[key] !== undefined && !(key === "color" && o[key] === true))
100
+ out[key] = o[key];
101
+ return out;
102
+ }