@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
package/dist/commands/ai.js
CHANGED
|
@@ -10,7 +10,11 @@ import { get, post, put, del } from "../core/http.js";
|
|
|
10
10
|
import { getProfile, updateProfile } from "../core/config.js";
|
|
11
11
|
import { hasTty, confirm, confirmTyped, select } from "../core/prompt.js";
|
|
12
12
|
import { requirePerson, withWorkspace } from "../auth/session.js";
|
|
13
|
-
import { ask, attach, composeText, confirmAction, listConversations, openConversation, renameConversation, pinConversation, deleteConversation, history, riskOf, confirmWord, } from "../ai/client.js";
|
|
13
|
+
import { ask, attach, attachmentKind, composeText, confirmAction, listConversations, openConversation, renameConversation, pinConversation, deleteConversation, history, riskOf, confirmWord, } from "../ai/client.js";
|
|
14
|
+
import { LocalChat } from "../ai/local.js";
|
|
15
|
+
import { mcpToken } from "../auth/mcp-oauth.js";
|
|
16
|
+
import { defaultModel, resolveModel } from "../ai/models.js";
|
|
17
|
+
import { registerAiNextFarm } from "./ai-nextfarm.js";
|
|
14
18
|
// ── NEXTX.md ───────────────────────────────────────────────────────────────────────────────────────
|
|
15
19
|
const NOTES_MAX = 4096;
|
|
16
20
|
export function notesFileName() {
|
|
@@ -64,7 +68,8 @@ export function renderBlocks(blocks) {
|
|
|
64
68
|
export function proposalCard(a) {
|
|
65
69
|
const env = context().env === "prod" ? c.red(tr("GHI VÀO PROD", "WRITES TO PROD")) : c.yellow("SANDBOX");
|
|
66
70
|
const risk = riskOf(a);
|
|
67
|
-
const tag = risk === "large" ? c.red(tr("GHI LỚN · ", "LARGE WRITE · ")) : risk === "finance" ? c.red(tr("TÀI CHÍNH · ", "FINANCE · "))
|
|
71
|
+
const tag = risk === "large" ? c.red(tr("GHI LỚN · ", "LARGE WRITE · ")) : risk === "finance" ? c.red(tr("TÀI CHÍNH · ", "FINANCE · "))
|
|
72
|
+
: risk === "device" ? c.red(tr("THIẾT BỊ · ", "DEVICE · ")) : "";
|
|
68
73
|
const left = a.expiresAt ? Math.max(0, Math.round((Date.parse(a.expiresAt) - Date.now()) / 60000)) : 15;
|
|
69
74
|
const lines = [
|
|
70
75
|
c.yellow(`┌ ${tr("Đề xuất", "Proposal")} `) + tag + env + c.yellow(` · ${tr(`hết hạn sau ${left} phút`, `expires in ${left} min`)}`),
|
|
@@ -89,13 +94,16 @@ export async function handleProposal(sessionId, a, mode = context().mode) {
|
|
|
89
94
|
return { written: false, message: r.message };
|
|
90
95
|
}
|
|
91
96
|
const risk = riskOf(a);
|
|
92
|
-
|
|
97
|
+
// NextFarm proposals are confirmed by id; the default platform's by reopening the conversation.
|
|
98
|
+
const resume = brand().platform === "nextfarm" ? `${brand().bin} ai apply ${a.nonce}` : `${brand().bin} -r ${sessionId}`;
|
|
93
99
|
if (mode === "read") {
|
|
94
100
|
info(c.dim(tr(" Chế độ chỉ đọc: không ghi. Đổi chế độ: --mode propose", " Read-only mode: nothing written. Switch with --mode propose")));
|
|
95
101
|
return { written: false };
|
|
96
102
|
}
|
|
97
|
-
if (risk === "finance") {
|
|
98
|
-
info(c.dim(
|
|
103
|
+
if (risk === "finance" || risk === "device") {
|
|
104
|
+
info(c.dim(risk === "device"
|
|
105
|
+
? tr(` Việc chạm thiết bị / tự động hoá chỉ xác nhận trong app: ${context().endpoints.app}`, ` Device and automation actions are confirmed in the app only: ${context().endpoints.app}`)
|
|
106
|
+
: tr(` Việc tài chính chỉ xác nhận trong app: ${context().endpoints.app}`, ` Finance actions are confirmed in the app only: ${context().endpoints.app}`)));
|
|
99
107
|
return { written: false };
|
|
100
108
|
}
|
|
101
109
|
if (!hasTty()) {
|
|
@@ -154,17 +162,59 @@ export async function resolveConversation(o) {
|
|
|
154
162
|
}
|
|
155
163
|
export async function runRoot(o, words) {
|
|
156
164
|
const printMode = o.print !== undefined && o.print !== false;
|
|
165
|
+
const spec = o.model ?? defaultModel();
|
|
166
|
+
const model = spec ? resolveModel(spec) : undefined;
|
|
157
167
|
if (!printMode && hasTty() && outputFormat() === "text") {
|
|
158
168
|
await requirePerson();
|
|
169
|
+
// The MCP grant may need a browser round-trip; do it before the chat UI owns the terminal.
|
|
170
|
+
if (model)
|
|
171
|
+
await mcpToken({ interactive: true });
|
|
159
172
|
const { startRepl } = await import("../repl/index.js");
|
|
160
|
-
return withWorkspace(context().workspace, () => startRepl({ flags: o, initialQuestion: words.join(" ").trim() || undefined }));
|
|
173
|
+
return withWorkspace(context().workspace, () => startRepl({ flags: o, initialQuestion: words.join(" ").trim() || undefined, model }));
|
|
161
174
|
}
|
|
162
175
|
const q = [typeof o.print === "string" ? o.print : "", ...words].join(" ").trim();
|
|
163
176
|
const piped = await readStdin();
|
|
164
177
|
if (!q && !piped)
|
|
165
|
-
throw new CliError(tr(`Thiếu câu hỏi. Ví dụ: ${brand().bin} -p "
|
|
178
|
+
throw new CliError(tr(`Thiếu câu hỏi. Ví dụ: ${brand().bin} -p "${brand().example.vi}"`, `Missing question. Example: ${brand().bin} -p "${brand().example.en}"`), Exit.usage);
|
|
166
179
|
await requirePerson();
|
|
167
|
-
|
|
180
|
+
const question = q || tr("Xử lý nội dung đính kèm.", "Process the attached content.");
|
|
181
|
+
await withWorkspace(context().workspace, () => (model ? printLocal(o, model, question, piped) : printAsk(o, question, piped)));
|
|
182
|
+
}
|
|
183
|
+
/** -p with --model: same output contract as the platform assistant, answered by the user's model. */
|
|
184
|
+
async function printLocal(o, model, question, piped) {
|
|
185
|
+
const attachments = [];
|
|
186
|
+
for (const f of (o.file ?? []).slice(0, 3)) {
|
|
187
|
+
if (attachmentKind(f) !== "text")
|
|
188
|
+
throw new CliError(tr(`Model riêng chỉ đọc tệp chữ: ${f}`, `Your own model reads text files only: ${f}`), Exit.usage);
|
|
189
|
+
attachments.push(await attach(f));
|
|
190
|
+
}
|
|
191
|
+
if (piped)
|
|
192
|
+
attachments.push({ path: "-", name: "stdin", kind: "text", text: piped.slice(0, 20_000) });
|
|
193
|
+
const fmt = outputFormat();
|
|
194
|
+
const chat = new LocalChat(model, hasTty(), context().mode === "read");
|
|
195
|
+
const outcome = await chat.ask({
|
|
196
|
+
text: composeText(question, attachments, findProjectNotes()?.text),
|
|
197
|
+
onChunk: (ch) => {
|
|
198
|
+
if (fmt === "stream-json")
|
|
199
|
+
return json(ch);
|
|
200
|
+
if (fmt !== "text")
|
|
201
|
+
return;
|
|
202
|
+
if (ch.type === "token")
|
|
203
|
+
process.stdout.write(ch.text ?? "");
|
|
204
|
+
else if (ch.type === "activity" && ch.activity && !o.quiet)
|
|
205
|
+
process.stderr.write(c.dim(`⏺ ${ch.activity.label}\n`));
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
if (fmt === "json")
|
|
209
|
+
json({ model: model.label, text: outcome.text, notices: outcome.notices, tools: outcome.activities.map((a) => a.toolName), workspace: getProfile(context().profile)?.workspaceCode ?? null, env: context().env, ended: outcome.ended });
|
|
210
|
+
else if (fmt === "text") {
|
|
211
|
+
if (outcome.text && !outcome.text.endsWith("\n"))
|
|
212
|
+
process.stdout.write("\n");
|
|
213
|
+
for (const n of outcome.notices)
|
|
214
|
+
process.stderr.write(`${c.yellow("⚠")} ${n.message}\n`);
|
|
215
|
+
}
|
|
216
|
+
if (outcome.ended === "error")
|
|
217
|
+
process.exitCode = Exit.general;
|
|
168
218
|
}
|
|
169
219
|
async function printAsk(o, question, piped) {
|
|
170
220
|
const sessionId = await resolveConversation(o);
|
|
@@ -195,11 +245,12 @@ async function printAsk(o, question, piped) {
|
|
|
195
245
|
},
|
|
196
246
|
onRetry: (n) => stderr.write(c.yellow(tr(`⚠ Mất kết nối, đang nối lại (${n}/2)…\n`, `⚠ Connection lost, reconnecting (${n}/2)…\n`))),
|
|
197
247
|
});
|
|
198
|
-
|
|
248
|
+
const conversationId = outcome.sessionId || sessionId;
|
|
249
|
+
updateProfile(context().profile, { lastConversation: conversationId });
|
|
199
250
|
const proposal = outcome.proposedAction;
|
|
200
251
|
if (fmt === "json") {
|
|
201
252
|
json({
|
|
202
|
-
conversationId
|
|
253
|
+
conversationId,
|
|
203
254
|
runId: outcome.runId ?? null,
|
|
204
255
|
messageId: outcome.done?.messageId ?? null,
|
|
205
256
|
text: outcome.text,
|
|
@@ -207,7 +258,8 @@ async function printAsk(o, question, piped) {
|
|
|
207
258
|
followUps: outcome.done?.followUps ?? [],
|
|
208
259
|
notices: outcome.notices,
|
|
209
260
|
agent: outcome.agent ?? null,
|
|
210
|
-
proposals: proposal ? [{ kind: proposal.kind, label: proposal.label, prompt: proposal.confirmPrompt, writes: proposal.writes, expiresAt: proposal.expiresAt ?? null, risk: riskOf(proposal),
|
|
261
|
+
proposals: proposal ? [{ kind: proposal.kind, label: proposal.label, prompt: proposal.confirmPrompt, writes: proposal.writes, expiresAt: proposal.expiresAt ?? null, risk: riskOf(proposal),
|
|
262
|
+
confirm: brand().platform === "nextfarm" ? `${brand().bin} ai apply ${proposal.nonce}` : `${brand().bin} -r ${conversationId}` }] : [],
|
|
211
263
|
workspace: getProfile(context().profile)?.workspaceCode ?? null,
|
|
212
264
|
env: context().env,
|
|
213
265
|
ended: outcome.ended,
|
|
@@ -225,7 +277,7 @@ async function printAsk(o, question, piped) {
|
|
|
225
277
|
if (proposal) {
|
|
226
278
|
for (const l of proposalCard(proposal))
|
|
227
279
|
stderr.write(`${l}\n`);
|
|
228
|
-
await handleProposal(
|
|
280
|
+
await handleProposal(conversationId, proposal);
|
|
229
281
|
}
|
|
230
282
|
}
|
|
231
283
|
if (outcome.notices.some((n) => n.code === "ai_not_purchased"))
|
|
@@ -303,6 +355,8 @@ export function registerAi(program) {
|
|
|
303
355
|
await deleteConversation(id);
|
|
304
356
|
ok(tr("Đã xoá.", "Deleted."));
|
|
305
357
|
});
|
|
358
|
+
if (brand().platform === "nextfarm")
|
|
359
|
+
return registerAiNextFarm(ai, program);
|
|
306
360
|
ai.command("why [runId]")
|
|
307
361
|
.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 and tools"))
|
|
308
362
|
.action(async (runId) => {
|
|
@@ -464,7 +518,7 @@ export function registerAi(program) {
|
|
|
464
518
|
});
|
|
465
519
|
ai.command("usage").description(tr("lượt AI tháng này", "AI turns this month"))
|
|
466
520
|
.action(async () => {
|
|
467
|
-
const u = await
|
|
521
|
+
const u = await (await import("../ai/client.js")).usage();
|
|
468
522
|
emit(u, () => info(`${tr("Gói", "Plan")} ${c.bold(u.planName ?? "-")} · ${tr("đã dùng", "used")} ${u.used ?? 0}${u.limit ? `/${u.limit}` : ""} · ${tr("mở lại", "resets")} ${u.resetsAt?.slice(0, 10) ?? ""}`));
|
|
469
523
|
});
|
|
470
524
|
ai.command("feedback <messageId> <vote>").description(tr("báo câu trả lời tốt (up) / chưa tốt (down)", "rate an answer: up / down"))
|
package/dist/commands/api.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
+
import { brand } from "../core/brand.js";
|
|
2
3
|
import { context } from "../core/context.js";
|
|
3
4
|
import { CliError, Exit } from "../core/errors.js";
|
|
4
5
|
import { tr } from "../core/i18n.js";
|
|
@@ -25,7 +26,7 @@ export function select(value, path) {
|
|
|
25
26
|
export function registerApi(program) {
|
|
26
27
|
program
|
|
27
28
|
.command("api <method> <path>")
|
|
28
|
-
.description(tr(
|
|
29
|
+
.description(tr(`gọi thẳng API (vd: api ${brand().apiExample})`, `call the API directly (e.g. api ${brand().apiExample})`))
|
|
29
30
|
.option("-f, --raw-field <key=value>", tr("tham số dạng chuỗi: query với GET/DELETE, thân JSON với POST/PUT/PATCH (lặp được)", "string parameter: query for GET/DELETE, JSON body for POST/PUT/PATCH (repeatable)"), (v, p = []) => [...p, v])
|
|
30
31
|
.option("-F, --field <key=value>", tr("như -f nhưng đoán kiểu: số, true/false, JSON", "like -f but typed: numbers, true/false, JSON"), (v, p = []) => [...p, v])
|
|
31
32
|
.option("--input <file>", tr("thân request từ tệp JSON (- = stdin)", "request body from a JSON file (- = stdin)"))
|
|
@@ -61,7 +62,7 @@ export function registerApi(program) {
|
|
|
61
62
|
let page = Number(query?.pageNumber ?? query?.page ?? 1);
|
|
62
63
|
for (;;) {
|
|
63
64
|
const q = o.paginate ? { ...query, pageNumber: page } : query;
|
|
64
|
-
const r = await request(method, path, { query: q, body, headers, idempotencyKey: o.idempotencyKey });
|
|
65
|
+
const r = await request(method, path, { query: q, body, headers, idempotencyKey: o.idempotencyKey, verbatim: true });
|
|
65
66
|
if (o.include) {
|
|
66
67
|
info(c.dim(`HTTP ${r.status}`));
|
|
67
68
|
r.headers.forEach?.((v, k) => info(c.dim(`${k}: ${v}`)));
|
package/dist/commands/auth.js
CHANGED
|
@@ -21,7 +21,7 @@ export function browserAvailable() {
|
|
|
21
21
|
}
|
|
22
22
|
export async function usage() {
|
|
23
23
|
try {
|
|
24
|
-
return await
|
|
24
|
+
return await (await import("../ai/client.js")).usage();
|
|
25
25
|
}
|
|
26
26
|
catch {
|
|
27
27
|
return undefined;
|
|
@@ -31,8 +31,8 @@ function printLogin(r) {
|
|
|
31
31
|
const p = getProfile(context().profile);
|
|
32
32
|
ok(tr(`Đã đăng nhập ${c.bold(r.userName ?? "")} (${r.userEmail ?? ""})`, `Logged in as ${c.bold(r.userName ?? "")} (${r.userEmail ?? ""})`));
|
|
33
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(
|
|
34
|
+
info(` ${tr("Phiên", "Session")} ${r.session.via === "ci" ? tr("15 phút, không gia hạn — hết thì đăng nhập CI lại", "15 minutes, no renewal — logs in again through CI when it ends") : 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(brand().example.vi, brand().example.en)}" · ${brand().bin} help`));
|
|
36
36
|
}
|
|
37
37
|
export function registerAuth(program) {
|
|
38
38
|
const b = brand();
|
|
@@ -47,6 +47,10 @@ export function registerAuth(program) {
|
|
|
47
47
|
.action(async (o) => {
|
|
48
48
|
const ws = context().workspace;
|
|
49
49
|
let r;
|
|
50
|
+
if ((o.ci && b.notYet?.includes("ci")) || (o.device && b.notYet?.includes("device"))) {
|
|
51
|
+
const { notYetError } = await import("../core/errors.js");
|
|
52
|
+
throw notYetError(tr(`${b.name} chưa mở ${o.ci ? "đăng nhập CI bằng OIDC" : "đăng nhập bằng mã thiết bị"}`, `${b.name} does not offer ${o.ci ? "CI OIDC login" : "device-code login"} yet`), o.ci ? tr(`dùng khoá API: ${b.envPrefix}_API_KEY`, `use an API key: ${b.envPrefix}_API_KEY`) : tr(`dùng ${b.bin} login --qr (quét bằng app ${b.mobileApp}) hoặc --no-browser`, `use ${b.bin} login --qr (scan with the ${b.mobileApp} app) or --no-browser`));
|
|
53
|
+
}
|
|
50
54
|
if (o.ci)
|
|
51
55
|
r = await loginCi({ workspace: ws });
|
|
52
56
|
else if (o.device)
|
|
@@ -0,0 +1,72 @@
|
|
|
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 } from "../core/output.js";
|
|
5
|
+
import { get, post } from "../core/http.js";
|
|
6
|
+
import { confirm, hasTty } from "../core/prompt.js";
|
|
7
|
+
import { requirePerson } from "../auth/session.js";
|
|
8
|
+
import { parseDuration } from "./keys.js";
|
|
9
|
+
const when = (s) => (s ? s.slice(0, 16).replace("T", " ") : "—");
|
|
10
|
+
export function workflowSample(repo = "acme/web", ws = "my-workspace") {
|
|
11
|
+
const b = brand();
|
|
12
|
+
return `# .github/workflows/report.yml (${repo})
|
|
13
|
+
permissions:
|
|
14
|
+
id-token: write # cho phép lấy token OIDC — không cần secret nào
|
|
15
|
+
contents: read
|
|
16
|
+
jobs:
|
|
17
|
+
report:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- run: npx -y ${b.pkg} login --ci --workspace ${ws}
|
|
21
|
+
- run: npx -y ${b.pkg} -p "${tr(b.example.vi, b.example.en)}" --json > report.json`;
|
|
22
|
+
}
|
|
23
|
+
export function registerCi(program) {
|
|
24
|
+
const ci = program.command("ci").description(tr("đăng nhập từ CI không cần bí mật (OIDC)", "secretless CI login (OIDC)"));
|
|
25
|
+
const trust = ci.command("trust").description(tr("kho CI được phép đăng nhập workspace này", "CI repositories allowed to log in to this workspace"));
|
|
26
|
+
trust.hook("preAction", async () => {
|
|
27
|
+
if (brand().notYet?.includes("ci")) {
|
|
28
|
+
const { notYetError } = await import("../core/errors.js");
|
|
29
|
+
throw notYetError(tr(`${brand().name} chưa mở đăng nhập CI bằng OIDC`, `${brand().name} does not offer CI OIDC login yet`), tr(`dùng khoá API: ${brand().envPrefix}_API_KEY`, `use an API key: ${brand().envPrefix}_API_KEY`));
|
|
30
|
+
}
|
|
31
|
+
await requirePerson();
|
|
32
|
+
});
|
|
33
|
+
trust.command("list", { isDefault: true }).action(async () => {
|
|
34
|
+
const list = await get("/iam/ci-trusts");
|
|
35
|
+
emit(list, () => info(list.length
|
|
36
|
+
? table(["ID", tr("Kho", "Repository"), "Ref", "Env", tr("Dùng lần cuối", "Last used"), ""], list.map((t) => [
|
|
37
|
+
t.id.slice(0, 8), `${t.provider}:${t.repository}`, t.refPattern, t.environment ?? "", when(t.lastUsedAt), t.revokedAt ? c.red(tr("đã thu hồi", "revoked")) : c.green("●"),
|
|
38
|
+
]))
|
|
39
|
+
: tr(`Chưa có kho nào. Thêm: ${brand().bin} ci trust add --repo owner/repo`, `None yet. Add one: ${brand().bin} ci trust add --repo owner/repo`)));
|
|
40
|
+
});
|
|
41
|
+
trust.command("add")
|
|
42
|
+
.description(tr("cho một kho đăng nhập (token đứng tên bạn, sống 15 phút)", "allow a repository to log in (tokens act as you, live 15 minutes)"))
|
|
43
|
+
.requiredOption("--repo <owner/repo>")
|
|
44
|
+
.option("--provider <p>", "github | gitlab", "github")
|
|
45
|
+
.option("--ref <ref>", tr("nhánh hoặc tiền tố tag, vd main · refs/tags/v*", "branch or tag prefix, e.g. main · refs/tags/v*"), "main")
|
|
46
|
+
.option("--environment <name>", tr("environment GitHub bắt buộc (khuyên dùng cho nhánh chính)", "required GitHub environment (recommended for the main branch)"))
|
|
47
|
+
.option("--expires <dur>", tr("thời hạn: 90d, 1y", "lifetime: 90d, 1y"))
|
|
48
|
+
.option("--name <name>")
|
|
49
|
+
.action(async (o) => {
|
|
50
|
+
const t = await post("/iam/ci-trusts", {
|
|
51
|
+
provider: o.provider, repository: o.repo, refPattern: o.ref, environment: o.environment, name: o.name,
|
|
52
|
+
expiresAt: o.expires ? parseDuration(o.expires) : null,
|
|
53
|
+
});
|
|
54
|
+
emit(t, () => {
|
|
55
|
+
ok(tr(`Đã cho ${t.provider}:${t.repository} (${t.refPattern}) đăng nhập`, `Allowed ${t.provider}:${t.repository} (${t.refPattern})`));
|
|
56
|
+
info(c.dim(tr("\nMẫu workflow:", "\nSample workflow:")));
|
|
57
|
+
info(workflowSample(t.repository));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
trust.command("revoke <id>").description(tr("thu hồi (có hiệu lực ngay)", "revoke (immediate)")).action(async (id) => {
|
|
61
|
+
const t = (await get("/iam/ci-trusts")).find((x) => x.id.startsWith(id) || x.repository === id);
|
|
62
|
+
if (!t)
|
|
63
|
+
throw new CliError(tr("Không thấy khai báo này.", "Not found."), Exit.notFound);
|
|
64
|
+
if (hasTty() && !(await confirm(tr(`Thu hồi ${t.repository}?`, `Revoke ${t.repository}?`))))
|
|
65
|
+
return;
|
|
66
|
+
await post(`/iam/ci-trusts/${t.id}/revoke`, {});
|
|
67
|
+
ok(tr("Đã thu hồi.", "Revoked."));
|
|
68
|
+
});
|
|
69
|
+
ci.command("example").description(tr("in mẫu workflow GitHub Actions", "print a sample GitHub Actions workflow"))
|
|
70
|
+
.option("--repo <owner/repo>").option("--workspace <code>")
|
|
71
|
+
.action((o) => info(workflowSample(o.repo, o.workspace)));
|
|
72
|
+
}
|
package/dist/commands/help.js
CHANGED
|
@@ -3,7 +3,7 @@ import { tr } from "../core/i18n.js";
|
|
|
3
3
|
import { json } from "../core/output.js";
|
|
4
4
|
import { VERSION } from "../core/version.js";
|
|
5
5
|
import { Exit } from "../core/errors.js";
|
|
6
|
-
import {
|
|
6
|
+
import { SHORTCUTS, slashCommands } from "../repl/slash.js";
|
|
7
7
|
const opt = (o) => ({ flags: o.flags, description: o.description, ...(o.defaultValue !== undefined ? { default: o.defaultValue } : {}), ...(o.argChoices ? { choices: o.argChoices } : {}) });
|
|
8
8
|
function walk(cmd) {
|
|
9
9
|
return {
|
|
@@ -25,7 +25,7 @@ export function helpTree(program) {
|
|
|
25
25
|
version: VERSION,
|
|
26
26
|
brand: b.id,
|
|
27
27
|
root: walk(program),
|
|
28
|
-
slashCommands:
|
|
28
|
+
slashCommands: slashCommands().map((s) => ({ name: `/${s.name}`, args: s.args ?? null, description: s.description() })),
|
|
29
29
|
shortcuts: SHORTCUTS.map((s) => ({ keys: s.keys, description: s.description() })),
|
|
30
30
|
exitCodes: Object.entries(Exit).map(([name, code]) => ({ name, code })),
|
|
31
31
|
environment: ["API_KEY", "TOKEN", "PROFILE", "ENV", "WORKSPACE", "LANG", "DEBUG", "NO_UPDATE_CHECK", "NO_KEYCHAIN", "ID_TOKEN"].map((v) => `${b.envPrefix}_${v}`),
|
package/dist/commands/mcp.js
CHANGED
|
@@ -5,7 +5,7 @@ import { spawnSync } from "node:child_process";
|
|
|
5
5
|
import { createInterface } from "node:readline";
|
|
6
6
|
import { fetch } from "undici";
|
|
7
7
|
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
|
8
|
-
import { brand } from "../core/brand.js";
|
|
8
|
+
import { brand, envVar } from "../core/brand.js";
|
|
9
9
|
import { context } from "../core/context.js";
|
|
10
10
|
import { CliError, Exit } from "../core/errors.js";
|
|
11
11
|
import { tr } from "../core/i18n.js";
|
|
@@ -138,8 +138,10 @@ export function registerMcp(program) {
|
|
|
138
138
|
}
|
|
139
139
|
/** Line-delimited JSON-RPC on stdio ⇄ Streamable HTTP to the hosted server. Logs go to stderr only. */
|
|
140
140
|
async function serveStdio() {
|
|
141
|
-
|
|
142
|
-
|
|
141
|
+
// NextFarm API keys speak MCP through the gateway (same read-only route rules as the key everywhere else).
|
|
142
|
+
const apiKey = brand().platform === "nextfarm" ? envVar("API_KEY") : undefined;
|
|
143
|
+
const url = apiKey ? `${context().endpoints.api}/api/v1/mcp` : context().endpoints.mcp;
|
|
144
|
+
let token = apiKey ?? (await mcpToken({ interactive: false }));
|
|
143
145
|
let sessionId;
|
|
144
146
|
const rl = createInterface({ input: process.stdin });
|
|
145
147
|
const write = (m) => process.stdout.write(`${JSON.stringify(m)}\n`);
|
|
@@ -166,7 +168,7 @@ async function serveStdio() {
|
|
|
166
168
|
});
|
|
167
169
|
try {
|
|
168
170
|
let res = await send();
|
|
169
|
-
if (res.status === 401) {
|
|
171
|
+
if (res.status === 401 && !apiKey) {
|
|
170
172
|
token = await mcpToken({ interactive: false });
|
|
171
173
|
res = await send();
|
|
172
174
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
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 } from "../core/output.js";
|
|
5
|
+
import { hasTty } from "../core/prompt.js";
|
|
6
|
+
import { PROVIDERS, addModel, defaultModel, isProvider, listModels, removeModel, resolveModel, setDefaultModel } from "../ai/models.js";
|
|
7
|
+
async function readAll() {
|
|
8
|
+
const chunks = [];
|
|
9
|
+
for await (const ch of process.stdin)
|
|
10
|
+
chunks.push(ch);
|
|
11
|
+
return Buffer.concat(chunks).toString("utf8").trim();
|
|
12
|
+
}
|
|
13
|
+
/** Reads a key from the terminal without echoing it. */
|
|
14
|
+
async function readHidden(question) {
|
|
15
|
+
process.stderr.write(question);
|
|
16
|
+
const stdin = process.stdin;
|
|
17
|
+
stdin.setRawMode?.(true);
|
|
18
|
+
stdin.resume();
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
let value = "";
|
|
21
|
+
const onData = (d) => {
|
|
22
|
+
for (const ch of d.toString("utf8")) {
|
|
23
|
+
if (ch === "\r" || ch === "\n") {
|
|
24
|
+
done();
|
|
25
|
+
return resolve(value.trim());
|
|
26
|
+
}
|
|
27
|
+
if (ch === "\u0003") {
|
|
28
|
+
done();
|
|
29
|
+
return reject(new CliError(tr("Đã huỷ.", "Cancelled."), Exit.interrupted));
|
|
30
|
+
}
|
|
31
|
+
if (ch === "\u007f")
|
|
32
|
+
value = value.slice(0, -1);
|
|
33
|
+
else
|
|
34
|
+
value += ch;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const done = () => {
|
|
38
|
+
stdin.off("data", onData);
|
|
39
|
+
stdin.setRawMode?.(false);
|
|
40
|
+
stdin.pause();
|
|
41
|
+
process.stderr.write("\n");
|
|
42
|
+
};
|
|
43
|
+
stdin.on("data", onData);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
export function registerModel(program) {
|
|
47
|
+
const b = brand();
|
|
48
|
+
const model = program
|
|
49
|
+
.command("model")
|
|
50
|
+
.description(tr(`model của bạn cho --model (khoá lưu keychain, không gửi về ${b.name})`, `your own models for --model (keys stay in the keychain, never sent to ${b.name})`));
|
|
51
|
+
model
|
|
52
|
+
.command("list")
|
|
53
|
+
.description(tr("các model đã thêm", "models you added"))
|
|
54
|
+
.action(() => {
|
|
55
|
+
const rows = listModels();
|
|
56
|
+
const def = defaultModel();
|
|
57
|
+
emit({ default: def ?? b.bin, models: rows.map((r) => ({ name: r.name, provider: r.entry.provider, model: r.entry.model ?? PROVIDERS[r.entry.provider].defaultModel ?? null, url: r.entry.url ?? null, hasKey: r.hasKey })) }, () => {
|
|
58
|
+
info(table([tr("Tên", "Name"), tr("Nhà", "Provider"), "Model", tr("Khoá", "Key"), ""], [
|
|
59
|
+
[b.bin, b.name, b.aiName, "—", def ? "" : c.green(tr("mặc định", "default"))],
|
|
60
|
+
...rows.map((r) => [r.name, PROVIDERS[r.entry.provider].label, r.entry.model ?? PROVIDERS[r.entry.provider].defaultModel ?? c.dim(tr("(nêu khi dùng)", "(name it when used)")), PROVIDERS[r.entry.provider].needsKey ? (r.hasKey ? c.green("✓") : c.yellow(tr("chưa có", "missing"))) : "—", r.isDefault ? c.green(tr("mặc định", "default")) : ""]),
|
|
61
|
+
]));
|
|
62
|
+
if (!rows.length)
|
|
63
|
+
info(c.dim(`\n ${b.bin} model add claude --key-from-stdin < key.txt · ${b.bin} model add ollama --url http://localhost:11434 --name qwen3:32b`));
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
model
|
|
67
|
+
.command("add <provider>")
|
|
68
|
+
.description(tr(`thêm nhà model: ${Object.keys(PROVIDERS).join(", ")}`, `add a provider: ${Object.keys(PROVIDERS).join(", ")}`))
|
|
69
|
+
.option("--key-from-stdin", tr("đọc khoá API từ stdin (không bao giờ đưa khoá lên dòng lệnh)", "read the API key from stdin (never put keys on the command line)"))
|
|
70
|
+
.option("--name <model>", tr("model dùng khi chỉ ghi tên nhà, vd gpt → --name <tên model>", "model used when only the provider is given"))
|
|
71
|
+
.option("--url <url>", tr("địa chỉ máy chủ (Ollama hoặc cổng tương thích)", "server address (Ollama or a compatible endpoint)"))
|
|
72
|
+
.action(async (provider, o) => {
|
|
73
|
+
if (!isProvider(provider))
|
|
74
|
+
throw new CliError(tr(`Không biết nhà "${provider}".`, `Unknown provider "${provider}".`), Exit.usage, Object.keys(PROVIDERS).join(", "));
|
|
75
|
+
const info0 = PROVIDERS[provider];
|
|
76
|
+
let key;
|
|
77
|
+
if (info0.needsKey) {
|
|
78
|
+
if (o.keyFromStdin)
|
|
79
|
+
key = await readAll();
|
|
80
|
+
else if (hasTty())
|
|
81
|
+
key = await readHidden(tr(`Khoá API ${info0.label}: `, `${info0.label} API key: `));
|
|
82
|
+
else
|
|
83
|
+
throw new CliError(tr("Không có terminal: dùng --key-from-stdin.", "No terminal: use --key-from-stdin."), Exit.usage);
|
|
84
|
+
if (!key)
|
|
85
|
+
throw new CliError(tr("Khoá rỗng.", "Empty key."), Exit.usage);
|
|
86
|
+
}
|
|
87
|
+
addModel(provider, { key, model: o.name, url: o.url });
|
|
88
|
+
ok(tr(`Đã thêm ${info0.label}. Dùng: ${b.bin} --model ${provider}${o.name || info0.defaultModel ? "" : "/<model>"}`, `Added ${info0.label}. Use: ${b.bin} --model ${provider}${o.name || info0.defaultModel ? "" : "/<model>"}`));
|
|
89
|
+
});
|
|
90
|
+
model
|
|
91
|
+
.command("remove <name>")
|
|
92
|
+
.description(tr("gỡ model và xoá khoá khỏi keychain", "remove a model and delete its key"))
|
|
93
|
+
.action((name) => {
|
|
94
|
+
if (!removeModel(name))
|
|
95
|
+
throw new CliError(tr(`Chưa có model "${name}".`, `No model "${name}".`), Exit.notFound);
|
|
96
|
+
ok(tr(`Đã gỡ ${name}.`, `Removed ${name}.`));
|
|
97
|
+
});
|
|
98
|
+
model
|
|
99
|
+
.command("default <name>")
|
|
100
|
+
.description(tr(`model mặc định khi mở chat (${b.bin} = ${b.aiName})`, `model used by default (${b.bin} = ${b.aiName})`))
|
|
101
|
+
.action((name) => {
|
|
102
|
+
const m = resolveModel(name);
|
|
103
|
+
setDefaultModel(m ? name : undefined);
|
|
104
|
+
ok(tr(`Mặc định: ${m ? m.label : b.aiName}`, `Default: ${m ? m.label : b.aiName}`));
|
|
105
|
+
});
|
|
106
|
+
}
|
package/dist/commands/tools.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Option } from "commander";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
+
import { context } from "../core/context.js";
|
|
3
4
|
import { brand, envVar } from "../core/brand.js";
|
|
4
5
|
import { CliError, Exit } from "../core/errors.js";
|
|
5
6
|
import { currentLang, tr } from "../core/i18n.js";
|
|
@@ -11,9 +12,18 @@ import { authorizeMcp, readGrant, revokeGrant } from "../auth/mcp-oauth.js";
|
|
|
11
12
|
import { browserAvailable } from "./auth.js";
|
|
12
13
|
let publicCache;
|
|
13
14
|
export async function publicTools() {
|
|
14
|
-
publicCache ??= (await get("/ai/public/tools", { anonymous: true }))
|
|
15
|
+
publicCache ??= (brand().platform === "nextfarm" ? await nextFarmPublicTools() : await get("/ai/public/tools", { anonymous: true }))
|
|
16
|
+
.filter((t) => !brand().modules || brand().modules.includes(t.module));
|
|
15
17
|
return publicCache;
|
|
16
18
|
}
|
|
19
|
+
/** NextFarm: `{ version, tools: [{ name, service, scope, title, title_en, summary, summary_en, parameters, example_args }] }`, read verbatim so schema property names stay as the tool expects them. */
|
|
20
|
+
async function nextFarmPublicTools() {
|
|
21
|
+
const r = await get("/ai/public/tools", { anonymous: true, verbatim: true });
|
|
22
|
+
return r.tools.map((t) => ({
|
|
23
|
+
name: t.name, module: t.service, title: t.title, summary: t.summary, summaryEn: t.summary_en, scope: t.scope,
|
|
24
|
+
kind: t.scope?.endsWith(":write") ? "write" : "read", parameters: t.parameters, exampleArgs: t.example_args,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
17
27
|
/** MCP names may differ from catalog names (module prefix, dots); match loosely. */
|
|
18
28
|
export function sameTool(a, b) {
|
|
19
29
|
const n = (s) => s.toLowerCase().replace(/[.\-]/g, "_");
|
|
@@ -151,7 +161,21 @@ export function registerTools(program) {
|
|
|
151
161
|
.action(async (name, o) => {
|
|
152
162
|
const args = parseArgs(o);
|
|
153
163
|
let value;
|
|
154
|
-
if (envVar("API_KEY")) {
|
|
164
|
+
if (envVar("API_KEY") && brand().platform === "nextfarm") {
|
|
165
|
+
// NextFarm has no REST door for tools: the API key speaks MCP through the gateway, which keeps its read-only route rules.
|
|
166
|
+
const client = new McpClient(false, envVar("API_KEY"), `${context().endpoints.api}/api/v1/mcp`);
|
|
167
|
+
const tool = (await client.listTools()).find((t) => sameTool(t.name, name));
|
|
168
|
+
if (!tool)
|
|
169
|
+
throw new CliError(tr("Khoá này không thấy công cụ đó (thiếu phạm vi?).", "This key cannot see that tool (missing scope?)."), Exit.notFound, `${brand().bin} tools list`);
|
|
170
|
+
const r = await client.callTool(tool.name, args);
|
|
171
|
+
value = resultValue(r);
|
|
172
|
+
if (r.isError) {
|
|
173
|
+
emit({ error: value }, () => info(c.red(typeof value === "string" ? value : JSON.stringify(value, null, 2))));
|
|
174
|
+
process.exitCode = Exit.general;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
else if (envVar("API_KEY")) {
|
|
155
179
|
const pub = (await publicTools()).find((p) => sameTool(p.name, name));
|
|
156
180
|
if (!pub)
|
|
157
181
|
throw new CliError(tr("Không thấy công cụ công khai này.", "Public tool not found."), Exit.notFound);
|
package/dist/commands/util.js
CHANGED
|
@@ -57,7 +57,7 @@ export async function doctor() {
|
|
|
57
57
|
const me = await get("/iam/auth/me");
|
|
58
58
|
add(tr("Danh tính", "Identity"), true, `${src} · ${me.email ?? ""} · ${me.workspace?.name ?? ""} · ${me.permissions?.length ?? 0} ${tr("quyền", "permissions")}`);
|
|
59
59
|
try {
|
|
60
|
-
const u = await
|
|
60
|
+
const u = await (await import("../ai/client.js")).usage();
|
|
61
61
|
add(tr("Gói AI", "AI plan"), u.limit === 0 ? "warn" : true, `${u.planName ?? "-"}${u.limit ? ` · ${tr("còn", "left")} ${u.remaining}/${u.limit}` : ""}`);
|
|
62
62
|
}
|
|
63
63
|
catch {
|