@nextxai/cli 0.1.0 → 0.2.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/CHANGELOG.md +11 -0
- package/dist/ai/client.js +22 -10
- package/dist/ai/local.js +286 -0
- package/dist/ai/models.js +87 -0
- package/dist/ai/nextfarm.js +387 -0
- package/dist/auth/ci.js +1 -1
- package/dist/auth/mcp-oauth.js +10 -23
- package/dist/auth/session.js +7 -1
- package/dist/commands/ai-nextfarm.js +109 -0
- package/dist/commands/ai.js +67 -13
- package/dist/commands/api.js +3 -2
- package/dist/commands/auth.js +7 -3
- package/dist/commands/ci.js +72 -0
- package/dist/commands/help.js +2 -2
- package/dist/commands/mcp.js +6 -4
- package/dist/commands/model.js +106 -0
- package/dist/commands/tools.js +26 -2
- package/dist/commands/util.js +1 -1
- package/dist/commands/webhooks.js +64 -15
- package/dist/core/brand.js +4 -0
- package/dist/core/case.js +23 -0
- package/dist/core/errors.js +4 -0
- package/dist/core/http.js +7 -4
- package/dist/mcp/client.js +5 -2
- package/dist/program.js +5 -0
- package/dist/repl/App.js +121 -18
- package/dist/repl/index.js +1 -1
- package/dist/repl/slash.js +14 -7
- package/package.json +2 -1
|
@@ -9,21 +9,51 @@ import { c, emit, info, json, ok, table, warn } from "../core/output.js";
|
|
|
9
9
|
import { get, post, del, requestRaw, errorFor } from "../core/http.js";
|
|
10
10
|
import { confirm, hasTty } from "../core/prompt.js";
|
|
11
11
|
const when = (iso) => (iso ? iso.slice(0, 19).replace("T", " ") : "");
|
|
12
|
+
const farm = () => brand().platform === "nextfarm";
|
|
13
|
+
/** Signature headers per platform: `X-Nextfarm-Signature` on NextFarm, `…-Signature-256` otherwise (both sha256=HMAC(ts.body)). */
|
|
14
|
+
const sigHeader = () => (farm() ? "x-nextfarm-signature" : "x-nextx-signature-256");
|
|
15
|
+
const tsHeader = () => (farm() ? "x-nextfarm-timestamp" : "x-nextx-timestamp");
|
|
12
16
|
export async function eventTypes() {
|
|
13
|
-
|
|
17
|
+
if (!farm())
|
|
18
|
+
return get("/webhook/public/event-types", { anonymous: true });
|
|
19
|
+
const r = await get("/webhook/public/event-types", { anonymous: true, verbatim: true });
|
|
20
|
+
return r.events.map((e) => ({
|
|
21
|
+
event: e.event, service: e.group, title: currentLang() === "en" ? e.title_en ?? e.title : e.title, description: e.description,
|
|
22
|
+
descriptionEn: e.description_en, planRequired: e.scope, payloadSchema: e.payload_schema, example: e.example,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
/** NextFarm endpoints carry `eventTypes` and no single module. */
|
|
26
|
+
async function listEndpoints() {
|
|
27
|
+
const list = await get("/webhook/endpoints");
|
|
28
|
+
return farm() ? list.map((e) => ({ ...e, events: e.eventTypes ?? [], status: e.isActive ? "active" : "disabled" })) : list;
|
|
29
|
+
}
|
|
30
|
+
/** NextFarm keeps deliveries per endpoint only; the CLI merges them. */
|
|
31
|
+
async function listDeliveries(o) {
|
|
32
|
+
if (!farm()) {
|
|
33
|
+
const path = o.endpoint ? `/webhook/endpoints/${(await findEndpoint(o.endpoint)).id}/deliveries` : "/webhook/deliveries";
|
|
34
|
+
const r = await get(path, { query: { status: o.status, pageSize: o.limit } });
|
|
35
|
+
return Array.isArray(r) ? r : r.items;
|
|
36
|
+
}
|
|
37
|
+
const eps = o.endpoint ? [await findEndpoint(o.endpoint)] : await listEndpoints();
|
|
38
|
+
const all = (await Promise.all(eps.map((e) => get(`/webhook/endpoints/${e.id}/deliveries`, { query: { limit: o.limit } })))).flat();
|
|
39
|
+
return all
|
|
40
|
+
.map((d) => ({ id: d.id, eventType: d.eventType, status: d.status === "delivered" ? "success" : d.status, responseStatus: d.lastStatusCode, durationMs: d.lastDurationMs, retryCount: d.attemptCount, maxRetries: 8, deliveredAt: d.deliveredAt, createdAt: d.createdAt }))
|
|
41
|
+
.filter((d) => !o.status || d.status === o.status)
|
|
42
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
43
|
+
.slice(0, o.limit);
|
|
14
44
|
}
|
|
15
45
|
/** Checks both the NextX header (sha256=HMAC(ts.body)) and Standard Webhooks (v1,base64 HMAC(id.ts.body)). */
|
|
16
46
|
export function verifySignature(secret, body, headers, toleranceSec = 300) {
|
|
17
47
|
const h = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
18
48
|
const eq = (a, b) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
19
|
-
const ts = h[
|
|
49
|
+
const ts = h[tsHeader()] ?? h["webhook-timestamp"];
|
|
20
50
|
if (ts && Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec)
|
|
21
51
|
return { ok: false, reason: tr("dấu thời gian quá cũ", "timestamp too old") };
|
|
22
|
-
if (h[
|
|
52
|
+
if (h[sigHeader()] && ts) {
|
|
23
53
|
const expected = "sha256=" + createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex");
|
|
24
|
-
const got = [h[
|
|
54
|
+
const got = [h[sigHeader()], h[`${sigHeader()}-previous`]].filter(Boolean);
|
|
25
55
|
if (got.some((g) => eq(g, expected)))
|
|
26
|
-
return { ok: true, scheme: "Signature-256" };
|
|
56
|
+
return { ok: true, scheme: farm() ? "X-Nextfarm-Signature" : "Signature-256" };
|
|
27
57
|
}
|
|
28
58
|
if (h["webhook-signature"] && h["webhook-id"] && ts) {
|
|
29
59
|
const key = secret.startsWith("whsec_") ? Buffer.from(secret.slice(6), "base64") : Buffer.from(secret);
|
|
@@ -34,7 +64,7 @@ export function verifySignature(secret, body, headers, toleranceSec = 300) {
|
|
|
34
64
|
return { ok: false, reason: tr("chữ ký không khớp", "signature mismatch") };
|
|
35
65
|
}
|
|
36
66
|
async function findEndpoint(id) {
|
|
37
|
-
const e = (await
|
|
67
|
+
const e = (await listEndpoints()).find((x) => x.id.startsWith(id) || x.url === id);
|
|
38
68
|
if (!e)
|
|
39
69
|
throw new CliError(tr("Không thấy endpoint này.", "Endpoint not found."), Exit.notFound);
|
|
40
70
|
return e;
|
|
@@ -46,7 +76,7 @@ export function registerWebhooks(program) {
|
|
|
46
76
|
let list = await eventTypes();
|
|
47
77
|
if (o.module)
|
|
48
78
|
list = list.filter((e) => e.service.toLowerCase() === o.module.toLowerCase());
|
|
49
|
-
emit(list, () => info(table([tr("Sự kiện", "Event"), tr("Phân hệ", "Module"), tr("Mô tả", "Description"), tr("Gói", "Plan")], list.map((e) => [e.event, e.service, (currentLang() === "en" ? e.descriptionEn ?? e.title : e.title), e.planRequired ?? ""]))));
|
|
79
|
+
emit(list, () => info(table([tr("Sự kiện", "Event"), tr("Phân hệ", "Module"), tr("Mô tả", "Description"), farm() ? tr("Quyền", "Permission") : tr("Gói", "Plan")], list.map((e) => [e.event, e.service, (currentLang() === "en" ? e.descriptionEn ?? e.title : e.title), e.planRequired ?? ""]))));
|
|
50
80
|
});
|
|
51
81
|
wh.command("schema <event>").description(tr("schema và ví dụ payload", "payload schema and example"))
|
|
52
82
|
.action(async (event) => {
|
|
@@ -63,7 +93,7 @@ export function registerWebhooks(program) {
|
|
|
63
93
|
});
|
|
64
94
|
const ep = wh.command("endpoints").description(tr("endpoint nhận webhook của workspace", "your webhook endpoints"));
|
|
65
95
|
ep.command("list", { isDefault: true }).action(async () => {
|
|
66
|
-
const list = await
|
|
96
|
+
const list = await listEndpoints();
|
|
67
97
|
emit(list, () => info(table(["ID", "URL", tr("Sự kiện", "Events"), tr("Trạng thái", "Status"), tr("Lỗi liên tiếp", "Failures")], list.map((e) => [e.id.slice(0, 8), e.url, e.events.length > 3 ? `${e.events.slice(0, 3).join(", ")} +${e.events.length - 3}` : e.events.join(", "), e.isActive ? c.green(e.status) : c.dim(e.status), e.consecutiveFailures || ""]))));
|
|
68
98
|
});
|
|
69
99
|
ep.command("create <url>")
|
|
@@ -71,6 +101,19 @@ export function registerWebhooks(program) {
|
|
|
71
101
|
.option("--description <text>")
|
|
72
102
|
.action(async (url, o) => {
|
|
73
103
|
const events = o.events.split(",").map((s) => s.trim()).filter(Boolean);
|
|
104
|
+
if (farm()) {
|
|
105
|
+
const known = new Set((await eventTypes()).map((t) => t.event));
|
|
106
|
+
const unknown = events.filter((e) => !known.has(e));
|
|
107
|
+
if (unknown.length)
|
|
108
|
+
throw new CliError(tr(`Không có sự kiện: ${unknown.join(", ")}`, `Unknown events: ${unknown.join(", ")}`), Exit.usage, `${brand().bin} webhooks events`);
|
|
109
|
+
const r = await post("/webhook/endpoints", { url, description: o.description, eventTypes: events });
|
|
110
|
+
emit(r, () => {
|
|
111
|
+
ok(tr("Đã tạo endpoint.", "Endpoint created."));
|
|
112
|
+
if (r.secret)
|
|
113
|
+
info(` ${tr("Khoá ký", "Signing secret")}: ${c.bold(String(r.secret))} ${c.dim(tr("(chỉ hiện một lần)", "(shown once)"))}`);
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
74
117
|
const types = await eventTypes();
|
|
75
118
|
const services = [...new Set(events.map((e) => types.find((t) => t.event === e)?.service).filter(Boolean))];
|
|
76
119
|
if (services.length !== 1)
|
|
@@ -98,10 +141,18 @@ export function registerWebhooks(program) {
|
|
|
98
141
|
wh.command("trigger <event>").description(tr("bắn một sự kiện mẫu tới endpoint", "send a sample event to an endpoint"))
|
|
99
142
|
.option("--endpoint <id>", tr("endpoint đích (mặc định: endpoint đầu tiên nhận sự kiện này)", "target endpoint (default: first one subscribed)"))
|
|
100
143
|
.action(async (event, o) => {
|
|
101
|
-
const list = await
|
|
144
|
+
const list = await listEndpoints();
|
|
102
145
|
const target = o.endpoint ? await findEndpoint(o.endpoint) : list.find((e) => e.events.includes(event));
|
|
103
146
|
if (!target)
|
|
104
147
|
throw new CliError(tr("Chưa có endpoint nào nhận sự kiện này.", "No endpoint subscribes to this event."), Exit.notFound, tr(`Dùng \`${brand().bin} webhooks listen --events ${event}\` để nghe về máy.`, `Use \`${brand().bin} webhooks listen --events ${event}\` to receive it locally.`));
|
|
148
|
+
if (farm()) {
|
|
149
|
+
// NextFarm sends a signed `webhook.test` and answers with the receiver's status straight away.
|
|
150
|
+
const t = await post(`/webhook/endpoints/${target.id}/test`, {});
|
|
151
|
+
emit(t, () => (t.ok ? ok : warn)(tr(`Đã gửi webhook.test → ${target.url}`, `Sent webhook.test → ${target.url}`) + ` · ${t.statusCode ? `HTTP ${t.statusCode}` : t.error ?? ""} · ${t.durationMs}ms`));
|
|
152
|
+
if (!t.ok)
|
|
153
|
+
process.exitCode = Exit.general;
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
105
156
|
const r = await post(`/webhook/endpoints/${target.id}/test`, { eventType: event });
|
|
106
157
|
emit(r, () => ok(tr(`Đã gửi ${event} → ${target.url}`, `Sent ${event} → ${target.url}`) + (r.responseStatus ? ` · HTTP ${r.responseStatus}` : "")));
|
|
107
158
|
});
|
|
@@ -110,15 +161,13 @@ export function registerWebhooks(program) {
|
|
|
110
161
|
.option("--endpoint <id>")
|
|
111
162
|
.option("--limit <n>", "", "30")
|
|
112
163
|
.action(async (o) => {
|
|
113
|
-
const
|
|
114
|
-
const r = await get(path, { query: { status: o.status, pageSize: o.limit } });
|
|
115
|
-
const list = Array.isArray(r) ? r : r.items;
|
|
164
|
+
const list = await listDeliveries({ endpoint: o.endpoint, status: o.status, limit: Number(o.limit) });
|
|
116
165
|
emit(list, () => info(table(["ID", tr("Sự kiện", "Event"), tr("Kết quả", "Result"), "HTTP", "ms", tr("Thử lại", "Retries"), tr("Lúc", "At")], list.map((d) => [
|
|
117
166
|
d.id.slice(0, 8), d.eventType, d.status === "success" ? c.green(d.status) : d.status === "failed" ? c.red(d.status) : d.status, d.responseStatus ?? "", d.durationMs ?? "", `${d.retryCount}/${d.maxRetries}`, when(d.createdAt),
|
|
118
167
|
]))));
|
|
119
168
|
});
|
|
120
169
|
wh.command("retry <deliveryId>").description(tr("gửi lại một lượt giao", "retry a delivery")).action(async (id) => {
|
|
121
|
-
const full = id.length >= 32 ? id : (await
|
|
170
|
+
const full = id.length >= 32 ? id : (await listDeliveries({ limit: 100 })).find((d) => d.id.startsWith(id))?.id;
|
|
122
171
|
if (!full)
|
|
123
172
|
throw new CliError(tr("Không thấy lượt giao này.", "Delivery not found."), Exit.notFound);
|
|
124
173
|
const r = await post(`/webhook/deliveries/${full}/retry`, {});
|
|
@@ -139,9 +188,9 @@ export function registerWebhooks(program) {
|
|
|
139
188
|
headers[h.slice(0, i).trim()] = h.slice(i + 1).trim();
|
|
140
189
|
}
|
|
141
190
|
if (o.signature)
|
|
142
|
-
headers[
|
|
191
|
+
headers[sigHeader()] = o.signature;
|
|
143
192
|
if (o.timestamp)
|
|
144
|
-
headers[
|
|
193
|
+
headers[tsHeader()] = o.timestamp;
|
|
145
194
|
const r = verifySignature(o.secret, body, headers);
|
|
146
195
|
emit(r, () => (r.ok ? ok(tr(`Chữ ký hợp lệ (${r.scheme})`, `Valid signature (${r.scheme})`)) : warn(tr(`Không hợp lệ: ${r.reason}`, `Invalid: ${r.reason}`))));
|
|
147
196
|
if (!r.ok)
|
package/dist/core/brand.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// <brand:nextx>
|
|
4
4
|
const NEXTX = {
|
|
5
5
|
id: "nextx",
|
|
6
|
+
platform: "nextx",
|
|
6
7
|
bin: "nextx",
|
|
7
8
|
name: "NextX",
|
|
8
9
|
aiName: "NextX AI",
|
|
@@ -10,6 +11,9 @@ const NEXTX = {
|
|
|
10
11
|
clientId: "nextx-cli",
|
|
11
12
|
envPrefix: "NEXTX",
|
|
12
13
|
mobileApp: "NextX",
|
|
14
|
+
example: { vi: "doanh thu hôm qua", en: "yesterday's revenue" },
|
|
15
|
+
apiExample: "GET /crm/contacts -f q=Minh",
|
|
16
|
+
ciSettings: { vi: "Thiết lập › Ứng dụng & API › CI", en: "Settings › Apps & API › CI" },
|
|
13
17
|
modules: null,
|
|
14
18
|
env: {
|
|
15
19
|
prod: {
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// NextFarm services speak two JSON key styles: snake_case (iam, gis, iot, harvest, ai, report, hrm, connect, sales, einvoice)
|
|
2
|
+
// and camelCase (office, notification, operator, finance). The CLI works in camelCase like the NextFarm web app, so keys
|
|
3
|
+
// are converted on the wire for snake services only. Converting a camel service breaks writes silently: `task_id` does not
|
|
4
|
+
// bind to `TaskId`, the field takes its default and the server still answers 200.
|
|
5
|
+
import { brand } from "./brand.js";
|
|
6
|
+
const SNAKE_SERVICES = ["iam", "gis", "iot", "harvest", "ai", "report", "hrm", "connect", "sales", "einvoice", "webhook"];
|
|
7
|
+
const SNAKE_URL = new RegExp(`/v\\d+/(${SNAKE_SERVICES.join("|")})(/|$|\\?)`);
|
|
8
|
+
/** Snake services with camelCase routes: Excel import keyed by column titles, consent with [JsonPropertyName], billing forwarded to operator. */
|
|
9
|
+
const EXEMPT = [/\/v\d+\/gis\/import(\/|$|\?)/, /\/v\d+\/iam\/automation-rules(\/|$|\?)/, /\/v\d+\/iam\/billing(\/|$|\?)/];
|
|
10
|
+
export function snakeOnWire(url) {
|
|
11
|
+
return brand().platform === "nextfarm" && SNAKE_URL.test(url) && !EXEMPT.some((r) => r.test(url));
|
|
12
|
+
}
|
|
13
|
+
const toSnake = (k) => k.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z])([A-Z][a-z])/g, "$1_$2").toLowerCase();
|
|
14
|
+
const toCamel = (k) => k.replace(/_([a-z0-9])/g, (_, ch) => ch.toUpperCase());
|
|
15
|
+
function convert(v, key) {
|
|
16
|
+
if (Array.isArray(v))
|
|
17
|
+
return v.map((x) => convert(x, key));
|
|
18
|
+
if (v && typeof v === "object" && Object.getPrototypeOf(v) === Object.prototype)
|
|
19
|
+
return Object.fromEntries(Object.entries(v).map(([k, x]) => [key(k), convert(x, key)]));
|
|
20
|
+
return v;
|
|
21
|
+
}
|
|
22
|
+
export const snakeKeys = (v) => convert(v, toSnake);
|
|
23
|
+
export const camelKeys = (v) => convert(v, toCamel);
|
package/dist/core/errors.js
CHANGED
|
@@ -11,6 +11,10 @@ export const Exit = {
|
|
|
11
11
|
notFound: 8,
|
|
12
12
|
interrupted: 130,
|
|
13
13
|
};
|
|
14
|
+
/** The brand's server does not offer this sign-in method yet. */
|
|
15
|
+
export function notYetError(what, instead) {
|
|
16
|
+
return new CliError(`${what} — ${instead}`, Exit.usage);
|
|
17
|
+
}
|
|
14
18
|
export class CliError extends Error {
|
|
15
19
|
exitCode;
|
|
16
20
|
hint;
|
package/dist/core/http.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Every call to the platform goes through here: identity headers, retry on expired token, error → exit code.
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { arch, platform } from "node:os";
|
|
4
|
-
import { fetch, EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
|
4
|
+
import { fetch, FormData, EnvHttpProxyAgent, setGlobalDispatcher } from "undici";
|
|
5
5
|
import { brand } from "./brand.js";
|
|
6
6
|
import { context } from "./context.js";
|
|
7
7
|
import { CliError, Exit } from "./errors.js";
|
|
8
8
|
import { t, currentLang } from "./i18n.js";
|
|
9
9
|
import { VERSION } from "./version.js";
|
|
10
|
+
import { snakeOnWire, snakeKeys, camelKeys } from "./case.js";
|
|
10
11
|
import { currentAuth, refreshSession } from "../auth/session.js";
|
|
11
12
|
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
12
13
|
export const userAgent = () => `${brand().bin}-cli/${VERSION} (${platform()}; ${arch()}; node ${process.versions.node})`;
|
|
@@ -46,10 +47,11 @@ async function send(method, url, opts, auth) {
|
|
|
46
47
|
};
|
|
47
48
|
let body;
|
|
48
49
|
if (opts.body !== undefined) {
|
|
49
|
-
|
|
50
|
+
// Multipart: undici sets the boundary itself, so no Content-Type here.
|
|
51
|
+
if (opts.body instanceof FormData || typeof opts.body === "string")
|
|
50
52
|
body = opts.body;
|
|
51
53
|
else {
|
|
52
|
-
body = JSON.stringify(opts.body);
|
|
54
|
+
body = JSON.stringify(!opts.verbatim && snakeOnWire(url) ? snakeKeys(opts.body) : opts.body);
|
|
53
55
|
headers["Content-Type"] ??= "application/json";
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -158,7 +160,8 @@ export async function request(method, path, opts = {}) {
|
|
|
158
160
|
const requestId = res.headers.get("x-request-id") ?? undefined;
|
|
159
161
|
if (opts.raw)
|
|
160
162
|
return { status: res.status, data: res, headers: res.headers, requestId };
|
|
161
|
-
const
|
|
163
|
+
const body = await readBody(res);
|
|
164
|
+
const data = !opts.verbatim && snakeOnWire(res.url || path) ? camelKeys(body) : body;
|
|
162
165
|
if (!res.ok && !opts.allow?.includes(res.status))
|
|
163
166
|
throw errorFor(res.status, data, res);
|
|
164
167
|
return { status: res.status, data: data, headers: res.headers, requestId };
|
package/dist/mcp/client.js
CHANGED
|
@@ -12,16 +12,19 @@ const PROTOCOL = "2025-06-18";
|
|
|
12
12
|
export class McpClient {
|
|
13
13
|
interactive;
|
|
14
14
|
apiKey;
|
|
15
|
+
url;
|
|
15
16
|
sessionId;
|
|
16
17
|
nextId = 1;
|
|
17
18
|
token;
|
|
18
19
|
ready = false;
|
|
19
|
-
|
|
20
|
+
/** `url`: MCP endpoint other than the hosted OAuth server (NextFarm API keys go through the gateway's /api/v1/mcp). */
|
|
21
|
+
constructor(interactive, apiKey, url) {
|
|
20
22
|
this.interactive = interactive;
|
|
21
23
|
this.apiKey = apiKey;
|
|
24
|
+
this.url = url;
|
|
22
25
|
}
|
|
23
26
|
async rpc(method, params, notification = false) {
|
|
24
|
-
const url = context().endpoints.mcp;
|
|
27
|
+
const url = this.url ?? context().endpoints.mcp;
|
|
25
28
|
this.token ??= this.apiKey ?? (await mcpToken({ interactive: this.interactive }));
|
|
26
29
|
const id = notification ? undefined : this.nextId++;
|
|
27
30
|
const headers = {
|
package/dist/program.js
CHANGED
|
@@ -8,6 +8,7 @@ import { tr } from "./core/i18n.js";
|
|
|
8
8
|
import { registerAuth } from "./commands/auth.js";
|
|
9
9
|
import { registerWorkspace } from "./commands/workspace.js";
|
|
10
10
|
import { registerAi, runRoot } from "./commands/ai.js";
|
|
11
|
+
import { registerModel } from "./commands/model.js";
|
|
11
12
|
import { registerTools } from "./commands/tools.js";
|
|
12
13
|
import { registerApi } from "./commands/api.js";
|
|
13
14
|
import { registerKeys } from "./commands/keys.js";
|
|
@@ -16,6 +17,7 @@ import { registerDocs } from "./commands/docs.js";
|
|
|
16
17
|
import { registerMcp } from "./commands/mcp.js";
|
|
17
18
|
import { registerUtil } from "./commands/util.js";
|
|
18
19
|
import { registerHelp } from "./commands/help.js";
|
|
20
|
+
import { registerCi } from "./commands/ci.js";
|
|
19
21
|
const collect = (v, prev = []) => [...prev, v];
|
|
20
22
|
export function buildProgram() {
|
|
21
23
|
const b = brand();
|
|
@@ -32,6 +34,7 @@ export function buildProgram() {
|
|
|
32
34
|
.option("--sandbox", tr("dùng môi trường thử nghiệm", "use the sandbox environment"))
|
|
33
35
|
.option("-w, --workspace <code>", tr("chạy lệnh này trên workspace khác", "run this command on another workspace"))
|
|
34
36
|
.option("--agent <code>", tr("chọn Agent thay cho Trợ lý chung", "choose an Agent instead of the general assistant"))
|
|
37
|
+
.option("--model <provider/name>", tr("dùng model của bạn qua MCP: claude, gemini, gpt, grok, ollama/<tên>", "use your own model over MCP: claude, gemini, gpt, grok, ollama/<name>"))
|
|
35
38
|
.addOption(new Option("--mode <mode>", tr("chế độ quyền", "permission mode")).choices(["read", "propose", "auto"]))
|
|
36
39
|
.addOption(new Option("--output-format <format>", tr("định dạng đầu ra", "output format")).choices(["text", "json", "stream-json"]))
|
|
37
40
|
.option("--json", tr("viết tắt của --output-format json", "shorthand for --output-format json"))
|
|
@@ -54,12 +57,14 @@ export function buildProgram() {
|
|
|
54
57
|
registerAuth(program);
|
|
55
58
|
registerWorkspace(program);
|
|
56
59
|
registerAi(program);
|
|
60
|
+
registerModel(program);
|
|
57
61
|
registerTools(program);
|
|
58
62
|
registerApi(program);
|
|
59
63
|
registerKeys(program);
|
|
60
64
|
registerWebhooks(program);
|
|
61
65
|
registerDocs(program);
|
|
62
66
|
registerMcp(program);
|
|
67
|
+
registerCi(program);
|
|
63
68
|
registerUtil(program);
|
|
64
69
|
registerHelp(program);
|
|
65
70
|
for (const sub of program.commands)
|
package/dist/repl/App.js
CHANGED
|
@@ -19,7 +19,10 @@ import { deleteSession } from "../auth/credentials.js";
|
|
|
19
19
|
import { findProjectNotes, notesFileName, proposalCard, renderBlocks, runLines } from "../commands/ai.js";
|
|
20
20
|
import { markdown } from "./markdown.js";
|
|
21
21
|
import { Editor, Menu, Select, edit } from "./components.js";
|
|
22
|
-
import {
|
|
22
|
+
import { LocalChat } from "../ai/local.js";
|
|
23
|
+
import { listModels, resolveModel } from "../ai/models.js";
|
|
24
|
+
import { SHORTCUTS, SLASH, matchSlash, slashCommands } from "./slash.js";
|
|
25
|
+
import * as nf from "../ai/nextfarm.js";
|
|
23
26
|
const MODES = ["propose", "auto", "read"];
|
|
24
27
|
const modeLabel = (m) => m === "propose" ? c.cyan(tr("⏵⏵ đề xuất", "⏵⏵ propose")) : m === "auto" ? c.yellow(tr("⏵⏵ tự ghi việc thường", "⏵⏵ auto-write normal")) : c.dim(tr("⏸ chỉ đọc", "⏸ read only"));
|
|
25
28
|
let seq = 0;
|
|
@@ -90,6 +93,7 @@ export function App(props) {
|
|
|
90
93
|
const [modal, setModal] = useState(null);
|
|
91
94
|
const [mode, setMode] = useState(context().mode);
|
|
92
95
|
const [agent, setAgent] = useState(context().agent);
|
|
96
|
+
const [local, setLocal] = useState(() => (props.model ? new LocalChat(props.model, true, context().mode === "read") : undefined));
|
|
93
97
|
const [sessionId, setSessionId] = useState(props.sessionId);
|
|
94
98
|
const [pending, setPending] = useState([]);
|
|
95
99
|
const [usageText, setUsageText] = useState("");
|
|
@@ -124,7 +128,7 @@ export function App(props) {
|
|
|
124
128
|
const line2 = ` ${c.dim(`${process.cwd().replace(process.env.HOME ?? "", "~")}${notes ? ` (${notesFileName()})` : ""} · CLI ${VERSION}`)}`;
|
|
125
129
|
const strip = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
126
130
|
const pad = (s) => s + " ".repeat(Math.max(0, inner + 1 - [...strip(s)].length));
|
|
127
|
-
const title = `─ ${b.aiName} `;
|
|
131
|
+
const title = local ? `─ ${b.name} · model: ${local.model.label} · ${tr("công cụ qua MCP", "tools over MCP")} ` : `─ ${b.aiName} `;
|
|
128
132
|
say([
|
|
129
133
|
c.cyan(`╭${title}${"─".repeat(Math.max(0, inner - title.length + 2))}╮`),
|
|
130
134
|
`${c.cyan("│")}${pad(line1)} ${c.cyan("│")}`,
|
|
@@ -191,6 +195,8 @@ export function App(props) {
|
|
|
191
195
|
say(proposalCard(p).join("\n"));
|
|
192
196
|
if (mode === "read")
|
|
193
197
|
return say(c.dim(tr(" Chế độ chỉ đọc: không ghi. Shift+Tab để đổi.", " Read-only mode: nothing written. Shift+Tab to change.")));
|
|
198
|
+
if (risk === "device")
|
|
199
|
+
return say(c.dim(tr(` Việc chạm thiết bị / tự động hoá chỉ xác nhận trong app ${b.mobileApp}: ${context().endpoints.app}`, ` Device and automation actions are confirmed in the ${b.mobileApp} app: ${context().endpoints.app}`)));
|
|
194
200
|
if (risk === "finance")
|
|
195
201
|
return say(c.dim(tr(` Việc tài chính chỉ xác nhận trong app: ${context().endpoints.app}`, ` Finance actions are confirmed in the app: ${context().endpoints.app}`)));
|
|
196
202
|
const yes = mode === "auto" && risk === "normal" ? true : await askConfirm(p);
|
|
@@ -245,26 +251,38 @@ export function App(props) {
|
|
|
245
251
|
const ctl = new AbortController();
|
|
246
252
|
abortRef.current = ctl;
|
|
247
253
|
try {
|
|
254
|
+
const onChunk = (ch) => {
|
|
255
|
+
const l = liveRef.current;
|
|
256
|
+
if (ch.type === "token")
|
|
257
|
+
l.text += ch.text ?? "";
|
|
258
|
+
else if (ch.type === "activity" && ch.activity)
|
|
259
|
+
l.activities.push(ch.activity);
|
|
260
|
+
else if (ch.type === "agent" && ch.agent)
|
|
261
|
+
l.agent = ch.agent;
|
|
262
|
+
l.reconnecting = undefined;
|
|
263
|
+
};
|
|
264
|
+
if (local) {
|
|
265
|
+
const skipped = atts.filter((a) => a.kind !== "text");
|
|
266
|
+
if (skipped.length)
|
|
267
|
+
say(c.yellow(tr(` ⚠ Model riêng chỉ đọc tệp chữ; bỏ qua ${skipped.map((a) => a.name).join(", ")}.`, ` ⚠ Your own model reads text files only; skipped ${skipped.map((a) => a.name).join(", ")}.`)));
|
|
268
|
+
const out = await local.ask({ text: composeText(clean, atts.filter((a) => a.kind === "text"), notes?.text), signal: ctl.signal, onChunk });
|
|
269
|
+
setLastOutcome(out);
|
|
270
|
+
say(renderAnswer(out));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
248
273
|
const out = await ask({
|
|
249
274
|
sessionId,
|
|
250
275
|
text: composeText(clean, atts, notes?.text),
|
|
251
276
|
attachments: atts,
|
|
252
277
|
agent: thisAgent,
|
|
253
278
|
signal: ctl.signal,
|
|
254
|
-
onChunk
|
|
255
|
-
const l = liveRef.current;
|
|
256
|
-
if (ch.type === "token")
|
|
257
|
-
l.text += ch.text ?? "";
|
|
258
|
-
else if (ch.type === "activity" && ch.activity)
|
|
259
|
-
l.activities.push(ch.activity);
|
|
260
|
-
else if (ch.type === "agent" && ch.agent)
|
|
261
|
-
l.agent = ch.agent;
|
|
262
|
-
l.reconnecting = undefined;
|
|
263
|
-
},
|
|
279
|
+
onChunk,
|
|
264
280
|
onRetry: (n) => liveRef.current && (liveRef.current.reconnecting = n),
|
|
265
281
|
});
|
|
266
282
|
setLastOutcome(out);
|
|
267
|
-
|
|
283
|
+
if (out.sessionId && out.sessionId !== sessionId)
|
|
284
|
+
setSessionId(out.sessionId);
|
|
285
|
+
updateProfile(context().profile, { lastConversation: out.sessionId || sessionId });
|
|
268
286
|
say(renderAnswer(out));
|
|
269
287
|
if (out.proposedAction)
|
|
270
288
|
await handleProposal(out.proposedAction);
|
|
@@ -315,16 +333,88 @@ export function App(props) {
|
|
|
315
333
|
say(`${c.green("✓")} ${tr(`Đã ghi vào ${relative(process.cwd(), target) || notesFileName()} — gửi kèm mọi câu hỏi trong thư mục này.`, `Saved to ${relative(process.cwd(), target) || notesFileName()} — sent with every question in this folder.`)}`);
|
|
316
334
|
}
|
|
317
335
|
// ── slash commands ────────────────────────────────────────────────────────────────────────────
|
|
336
|
+
/** NextFarm versions of the slash commands whose server contract differs. True = handled. */
|
|
337
|
+
async function farmSlash(cmd, arg) {
|
|
338
|
+
switch (cmd) {
|
|
339
|
+
case "undo": {
|
|
340
|
+
const target = nf.undoTarget(arg || undefined);
|
|
341
|
+
const ok = await pick(tr(`Hoàn tác "${target.label}"?`, `Undo "${target.label}"?`), [{ label: tr("Không", "No"), value: "n" }, { label: tr("Hoàn tác", "Undo"), value: "y" }]);
|
|
342
|
+
if (ok === "y")
|
|
343
|
+
say(`${c.green("✓")} ${await nf.undo(target)}`);
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
case "why": {
|
|
347
|
+
const id = arg || lastOutcome?.runId;
|
|
348
|
+
if (!id)
|
|
349
|
+
throw new Error(tr("Chưa có câu trả lời nào trong phiên này.", "No answer in this session yet."));
|
|
350
|
+
say((await nf.runTrace(id)).join("\n"));
|
|
351
|
+
return true;
|
|
352
|
+
}
|
|
353
|
+
case "memory": {
|
|
354
|
+
const m = await nf.memories();
|
|
355
|
+
const [sub, id] = arg.split(/\s+/);
|
|
356
|
+
if (sub === "forget" && id) {
|
|
357
|
+
const hit = m.items.find((x) => x.id.startsWith(id));
|
|
358
|
+
if (!hit)
|
|
359
|
+
throw new Error(tr("Không thấy điều nhớ này.", "Memory not found."));
|
|
360
|
+
await nf.forgetMemory(hit.id);
|
|
361
|
+
say(`${c.green("✓")} ${tr("Đã quên.", "Forgotten.")}`);
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
say(m.items.length ? table(["ID", tr("Điều nhớ", "Memory"), tr("Dùng", "Uses")], m.items.map((x) => [x.id.slice(0, 8), x.display, x.uses])) + (m.canDelete ? c.dim("\n /memory forget <id>") : "") : tr(" Trợ lý chưa nhớ gì về trang trại.", " Nothing remembered about the farm yet."));
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
case "schedule": {
|
|
368
|
+
const m = /^"([^"]+)"\s+(daily|weekly)\s+(?:([1-7])@)?(\d{1,2}:\d{2})$/.exec(arg);
|
|
369
|
+
if (m) {
|
|
370
|
+
const [, q, freq, day, time] = m;
|
|
371
|
+
const s = await nf.createSchedule(q, freq, time.padStart(5, "0"), day ? Number(day) : undefined);
|
|
372
|
+
say(`${c.green("✓")} ${tr(`Đã lên lịch · ${s.when}`, `Scheduled · ${s.when}`)}\n${c.dim(tr(` Mở mục Trợ lý AI trong app ${b.mobileApp} một lần để cho phép chạy nền.`, ` Open the AI assistant in the ${b.mobileApp} app once to allow background runs.`))}`);
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
const list = await nf.schedules();
|
|
376
|
+
say((list.length ? table(["ID", tr("Tên", "Title"), tr("Lịch", "When"), tr("Lần tới", "Next")], list.map((s) => [s.id.slice(0, 8), s.title, s.when, s.nextRunAt.slice(0, 16).replace("T", " ")])) + "\n" : "") + c.dim(tr(' /schedule "nhật ký hôm qua" daily 07:00 · weekly 1@07:00 (1 = thứ Hai)', ' /schedule "yesterday\'s log" daily 07:00 · weekly 1@07:00 (1 = Monday)')));
|
|
377
|
+
}
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
case "sau-benh": {
|
|
381
|
+
if (!arg)
|
|
382
|
+
throw new Error(tr("Cần ảnh: /sau-benh <ảnh.jpg>", "Pass a photo: /sau-benh <photo.jpg>"));
|
|
383
|
+
const [path, ...note] = arg.split(/\s+/);
|
|
384
|
+
await addAttachment(path);
|
|
385
|
+
await send(note.length ? note.join(" ") : tr("Ảnh này cây bị sâu bệnh gì? Mức độ, cách xử lý và lô nào cần kiểm tra thêm.", "What pest or disease is in this photo? Severity, treatment and which plots to check."));
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
case "thoi-tiet":
|
|
389
|
+
await send(arg ? tr(`Thời tiết 3 ngày tới ở ${arg}, có ảnh hưởng gì tới việc phun, tưới, thu hoạch?`, `Weather for the next 3 days at ${arg} — any impact on spraying, irrigation, harvest?`)
|
|
390
|
+
: tr("Thời tiết 3 ngày tới ở trang trại, có ảnh hưởng gì tới việc phun, tưới, thu hoạch?", "Weather for the next 3 days at the farm — any impact on spraying, irrigation, harvest?"));
|
|
391
|
+
return true;
|
|
392
|
+
case "ban-tin": {
|
|
393
|
+
const brief = await nf.latestBrief();
|
|
394
|
+
say(brief ? `${c.bold(brief.title)} ${c.dim(brief.createdAt.slice(0, 16).replace("T", " "))}\n${markdown(brief.text)}` : tr(" Chưa có bản tin sáng nào. Bật trong app: Trợ lý AI › Bản tin sáng.", " No morning brief yet. Turn it on in the app: AI assistant › Morning brief."));
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
397
|
+
default:
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
318
401
|
async function runSlash(line) {
|
|
319
402
|
const [cmd0, ...rest] = line.slice(1).split(/\s+/);
|
|
320
403
|
const cmd = (cmd0 ?? "").toLowerCase();
|
|
321
404
|
const arg = rest.join(" ").trim();
|
|
322
405
|
try {
|
|
406
|
+
// Commands that belong to another backend behave like unknown ones.
|
|
407
|
+
if (SLASH.some((s) => s.name === cmd) && !slashCommands().some((s) => s.name === cmd)) {
|
|
408
|
+
say(`${c.yellow("⚠")} ${tr(`Không có lệnh /${cmd}.`, `No command /${cmd}.`)} ${c.dim("/help")}`);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
if (b.platform === "nextfarm" && (await farmSlash(cmd, arg)))
|
|
412
|
+
return;
|
|
323
413
|
switch (cmd) {
|
|
324
414
|
case "help":
|
|
325
415
|
case "?": {
|
|
326
|
-
const w = Math.max(...
|
|
327
|
-
say([...
|
|
416
|
+
const w = Math.max(...slashCommands().map((s) => s.name.length)) + 2;
|
|
417
|
+
say([...slashCommands().map((s) => ` ${c.cyan(`/${s.name}`.padEnd(w + 1))} ${s.description()}`), "", ...SHORTCUTS.map((s) => ` ${s.keys.padEnd(30)} ${c.dim(s.description())}`)].join("\n"));
|
|
328
418
|
break;
|
|
329
419
|
}
|
|
330
420
|
case "new": {
|
|
@@ -393,9 +483,22 @@ export function App(props) {
|
|
|
393
483
|
say(` ${modeLabel(m)}`);
|
|
394
484
|
break;
|
|
395
485
|
}
|
|
396
|
-
case "model":
|
|
397
|
-
|
|
486
|
+
case "model": {
|
|
487
|
+
if (!arg) {
|
|
488
|
+
const rows = listModels();
|
|
489
|
+
say([
|
|
490
|
+
` ${tr("Đang dùng", "Using")} ${c.bold(local ? local.model.label : b.aiName)}`,
|
|
491
|
+
` ${c.cyan(b.bin.padEnd(10))} ${b.aiName}`,
|
|
492
|
+
...rows.map((r) => ` ${c.cyan(r.name.padEnd(10))} ${r.entry.model ?? ""}${r.hasKey ? "" : c.yellow(tr(" (chưa có khoá)", " (no key)"))}`),
|
|
493
|
+
c.dim(` /model ${b.bin} · /model claude · /model ollama/<model> · ${b.bin} model add <nhà>`),
|
|
494
|
+
].join("\n"));
|
|
495
|
+
break;
|
|
496
|
+
}
|
|
497
|
+
const m = resolveModel(arg);
|
|
498
|
+
setLocal(m ? new LocalChat(m, true, mode === "read") : undefined);
|
|
499
|
+
say(`${c.green("✓")} ${m ? `${tr("Model của bạn", "Your model")}: ${m.label} · ${tr("công cụ qua MCP, ghi chỉ bằng link xác nhận, không trừ lượt AI", "tools over MCP, writes only via a confirmation link, no AI turns used")}` : b.aiName}`);
|
|
398
500
|
break;
|
|
501
|
+
}
|
|
399
502
|
case "attach":
|
|
400
503
|
if (!arg)
|
|
401
504
|
throw new Error(tr("Cần đường dẫn tệp.", "Pass a file path."));
|
|
@@ -765,7 +868,7 @@ export function App(props) {
|
|
|
765
868
|
});
|
|
766
869
|
// ── view ──────────────────────────────────────────────────────────────────────────────────────
|
|
767
870
|
const p = profile();
|
|
768
|
-
const footerRight = c.dim(`${p?.workspaceName ?? ""} · ${context().env}${usageText ? ` · ${usageText}` : ""}${agent ? ` · ${agent}` : ""}`);
|
|
871
|
+
const footerRight = c.dim(`${p?.workspaceName ?? ""} · ${context().env}${local ? ` · ${local.model.label}` : usageText ? ` · ${usageText}` : ""}${agent && !local ? ` · ${agent}` : ""}`);
|
|
769
872
|
const lastReason = live?.activities.filter((a) => a.kind !== "tool").at(-1);
|
|
770
873
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: done, children: (it) => _jsx(Text, { children: it.text }, it.id) }), live ? (_jsxs(Box, { flexDirection: "column", marginTop: 0, children: [live.agent && live.agent.code !== "general" ? _jsx(Text, { color: "cyan", children: `● ${live.agent.name}` }) : null, live.activities.filter((a) => a.kind === "tool").map((a, i) => (_jsx(Text, { color: "gray", children: ` ⏺ ${a.label}` }, i))), live.text ? _jsx(Text, { children: markdown(live.text).replace(/^/gm, " ") }) : null, _jsxs(Text, { color: "gray", children: [_jsx(Spinner, { type: "dots" }), " ", live.reconnecting ? tr(`Mất kết nối, đang nối lại (${live.reconnecting}/2)…`, `Reconnecting (${live.reconnecting}/2)…`) : (lastReason?.label ?? tr("Đang nghĩ…", "Thinking…")), " ", _jsx(Text, { dimColor: true, children: tr("Esc dừng", "Esc to stop") })] })] })) : null, modal?.kind === "select" ? (_jsx(Select, { title: modal.title, items: modal.items, initial: modal.initial, onPick: modal.resolve, hint: tr("↑↓ chọn · Enter · Esc huỷ", "↑↓ choose · Enter · Esc cancel") })) : modal?.kind === "confirm" ? (_jsx(Box, { marginTop: 0, children: riskOf(modal.proposal) === "large" ? (_jsxs(Text, { children: [c.yellow("└ "), tr(`Gõ ${c.bold(confirmWord(modal.proposal))} để xác nhận, Enter để huỷ`, `Type ${c.bold(confirmWord(modal.proposal))} to confirm, Enter to cancel`), " \u203A ", modal.typed, _jsx(Text, { inverse: true, children: " " })] })) : (_jsxs(Text, { children: [c.yellow("└ "), tr("Ghi?", "Write?"), " ", c.bold("[y]"), " ", tr("có", "yes"), " ", c.bold("[n]"), " ", tr("không", "no"), " ", c.bold("[d]"), " ", tr("xem dữ liệu", "show data")] })) })) : (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [pending.length ? _jsx(Text, { color: "gray", children: ` 📎 ${pending.map((a) => a.name).join(", ")}` }) : null, _jsx(Editor, { value: value, cursor: cursor, disabled: busy, placeholder: busy ? "" : tr(`Hỏi ${b.aiName}… (/ để xem lệnh)`, `Ask ${b.aiName}… (/ for commands)`) }), menuItems.length ? _jsx(Menu, { items: menuItems, index: menuIndex }) : null, _jsxs(Box, { justifyContent: "space-between", width: width, children: [_jsx(Text, { children: ` ${modeLabel(mode)} ${c.dim("(Shift+Tab)")}` }), _jsx(Text, { children: footerRight })] })] }))] }));
|
|
771
874
|
}
|
package/dist/repl/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveConversation } from "../commands/ai.js";
|
|
|
5
5
|
export async function startRepl(opts) {
|
|
6
6
|
const sessionId = await resolveConversation(opts.flags);
|
|
7
7
|
const resumed = Boolean(opts.flags.continue || opts.flags.resume);
|
|
8
|
-
const app = render(_jsx(App, { sessionId: sessionId, resumed: resumed, initialQuestion: opts.initialQuestion, files: (opts.flags.file ?? []).slice(0, 3) }), {
|
|
8
|
+
const app = render(_jsx(App, { sessionId: sessionId, resumed: resumed, initialQuestion: opts.initialQuestion, files: (opts.flags.file ?? []).slice(0, 3), model: opts.model }), {
|
|
9
9
|
exitOnCtrlC: false,
|
|
10
10
|
});
|
|
11
11
|
await app.waitUntilExit();
|
package/dist/repl/slash.js
CHANGED
|
@@ -9,22 +9,25 @@ export const SLASH = [
|
|
|
9
9
|
{ name: "rename", args: "<tên>", description: () => tr("Đặt tên hội thoại này", "Rename this conversation") },
|
|
10
10
|
{ name: "pin", description: () => tr("Ghim / bỏ ghim hội thoại", "Pin / unpin this conversation") },
|
|
11
11
|
{ name: "workspace", args: "[mã]", description: () => tr("Đổi workspace đang làm việc", "Switch workspace") },
|
|
12
|
-
{ name: "agent", args: "[mã]", description: () => tr("Chọn Agent: Trợ lý chung, Bán hàng, CSKH…", "Choose an Agent: general, sales, support…") },
|
|
12
|
+
{ name: "agent", args: "[mã]", description: () => tr("Chọn Agent: Trợ lý chung, Bán hàng, CSKH…", "Choose an Agent: general, sales, support…"), platforms: ["nextx"] },
|
|
13
13
|
{ name: "mode", args: "read|propose|auto", description: () => tr("Chế độ quyền: chỉ đọc · đề xuất · tự ghi việc thường", "Permission mode: read · propose · auto") },
|
|
14
14
|
{ name: "model", args: "[tên]", description: () => tr(`Đổi model: ${brand().aiName} hoặc model của bạn`, `Switch model: ${brand().aiName} or your own`) },
|
|
15
15
|
{ name: "attach", args: "<tệp>", description: () => tr("Đính kèm tệp Excel / CSV / ảnh / tệp chữ", "Attach an Excel / CSV / image / text file") },
|
|
16
|
-
{ name: "pending", description: () => tr("Đề xuất ghi đang chờ xác nhận", "Writes waiting for confirmation") },
|
|
16
|
+
{ name: "pending", description: () => tr("Đề xuất ghi đang chờ xác nhận", "Writes waiting for confirmation"), platforms: ["nextx"] },
|
|
17
17
|
{ name: "undo", args: "[id]", description: () => tr("Hoàn tác lần ghi gần nhất", "Undo the last write") },
|
|
18
|
-
{ name: "journal", description: () => tr("Nhật ký những gì AI đã ghi", "Journal of what the AI wrote") },
|
|
18
|
+
{ name: "journal", description: () => tr("Nhật ký những gì AI đã ghi", "Journal of what the AI wrote"), platforms: ["nextx"] },
|
|
19
19
|
{ name: "why", args: "[runId]", description: () => tr("Vì sao AI trả lời vậy (các bước, công cụ đã gọi)", "Why the AI answered that way (steps, tools)") },
|
|
20
20
|
{ name: "memory", description: () => tr("AI đang nhớ gì về công ty — sửa, khoá, quên", "What the AI remembers — edit, lock, forget") },
|
|
21
21
|
{ name: "schedule", args: '"câu" daily 08:00', description: () => tr("Lên lịch một câu hỏi chạy hằng ngày / tuần / tháng", "Schedule a question daily / weekly / monthly") },
|
|
22
|
-
{ name: "library", description: () => tr("Ảnh, tài liệu, tệp AI đã tạo", "Images, documents and files the AI produced") },
|
|
22
|
+
{ name: "library", description: () => tr("Ảnh, tài liệu, tệp AI đã tạo", "Images, documents and files the AI produced"), platforms: ["nextx"] },
|
|
23
|
+
{ name: "sau-benh", args: "<ảnh>", platforms: ["nextfarm"], description: () => tr("Chẩn đoán sâu bệnh từ ảnh lá, quả, thân", "Diagnose pests and diseases from a photo") },
|
|
24
|
+
{ name: "thoi-tiet", args: "[lô]", platforms: ["nextfarm"], description: () => tr("Thời tiết mấy ngày tới ở trang trại / lô", "Weather for the farm or a plot in the coming days") },
|
|
25
|
+
{ name: "ban-tin", platforms: ["nextfarm"], description: () => tr("Bản tin sáng mới nhất của trang trại", "The farm's latest morning brief") },
|
|
23
26
|
{ name: "export", args: "md|json [tệp]", description: () => tr("Xuất hội thoại ra Markdown / JSON", "Export the conversation to Markdown / JSON") },
|
|
24
27
|
{ name: "tools", description: () => tr("Công cụ AI bạn đang được dùng (theo quyền thật)", "AI tools you can use (by your real permissions)") },
|
|
25
|
-
{ name: "usage", description: () => tr("Lượt AI tháng này", "AI turns this month") },
|
|
28
|
+
{ name: "usage", description: () => brand().platform === "nextfarm" ? tr("Câu hỏi AI còn lại tuần này", "AI questions left this week") : tr("Lượt AI tháng này", "AI turns this month") },
|
|
26
29
|
{ name: "feedback", args: "up|down", description: () => tr("Báo câu trả lời vừa rồi tốt / chưa tốt", "Rate the last answer") },
|
|
27
|
-
{ name: "support", args: "<vấn đề>", description: () => tr("Chuyển cho nhân viên hỗ trợ (mở ticket)", "Hand over to a support agent (ticket)") },
|
|
30
|
+
{ name: "support", args: "<vấn đề>", description: () => tr("Chuyển cho nhân viên hỗ trợ (mở ticket)", "Hand over to a support agent (ticket)"), platforms: ["nextx"] },
|
|
28
31
|
{ name: "status", description: () => tr("Tài khoản, workspace, môi trường, phiên bản", "Account, workspace, environment, version") },
|
|
29
32
|
{ name: "doctor", description: () => tr("Kiểm kết nối, đồng hồ, token, gói", "Check connectivity, clock, token, plan") },
|
|
30
33
|
{ name: "config", args: "[khoá giá_trị]", description: () => tr("Cài đặt: ngôn ngữ, màu, thông báo cập nhật", "Settings: language, color, update notices") },
|
|
@@ -47,7 +50,11 @@ export const SHORTCUTS = [
|
|
|
47
50
|
{ keys: "Ctrl+L", description: () => tr("Xoá màn", "Clear screen") },
|
|
48
51
|
{ keys: "Ctrl+C · Ctrl+D", description: () => tr("Huỷ dòng đang gõ (2 lần để thoát) · thoát", "Clear input (twice to exit) · exit") },
|
|
49
52
|
];
|
|
53
|
+
/** Commands of the current brand's backend — menu, `/help` and `help --json` all read this. */
|
|
54
|
+
export function slashCommands() {
|
|
55
|
+
return SLASH.filter((s) => !s.platforms || s.platforms.includes(brand().platform));
|
|
56
|
+
}
|
|
50
57
|
export function matchSlash(prefix) {
|
|
51
58
|
const p = prefix.replace(/^\//, "").toLowerCase();
|
|
52
|
-
return
|
|
59
|
+
return slashCommands().filter((s) => s.name.startsWith(p));
|
|
53
60
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nextxai/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "NextX in your terminal: chat with NextX AI on your workspace data, call the API, manage keys and webhooks",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextx",
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"test": "tsx --test test/*.test.ts"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
+
"@anthropic-ai/sdk": "0.134.0",
|
|
44
45
|
"commander": "14.0.3",
|
|
45
46
|
"ink": "6.3.1",
|
|
46
47
|
"ink-spinner": "5.0.0",
|