@nextxai/cli 0.1.1 → 0.3.1
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 +19 -1
- package/dist/ai/client.js +32 -10
- package/dist/ai/local.js +286 -0
- package/dist/ai/models.js +87 -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.js +122 -19
- package/dist/commands/api.js +3 -2
- package/dist/commands/auth.js +7 -3
- package/dist/commands/ci-policy.js +98 -0
- package/dist/commands/ci.js +96 -0
- package/dist/commands/help.js +2 -2
- package/dist/commands/keys.js +2 -1
- package/dist/commands/mcp.js +5 -3
- package/dist/commands/model.js +106 -0
- package/dist/commands/tools.js +5 -1
- package/dist/commands/util.js +1 -1
- package/dist/commands/webhooks.js +65 -14
- package/dist/core/brand.js +6 -3
- package/dist/core/errors.js +4 -0
- package/dist/core/http.js +15 -4
- package/dist/mcp/client.js +5 -2
- package/dist/program.js +5 -0
- package/dist/repl/App.js +53 -18
- package/dist/repl/index.js +1 -1
- package/dist/repl/slash.js +11 -7
- package/package.json +2 -1
|
@@ -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
|
@@ -11,9 +11,13 @@ import { authorizeMcp, readGrant, revokeGrant } from "../auth/mcp-oauth.js";
|
|
|
11
11
|
import { browserAvailable } from "./auth.js";
|
|
12
12
|
let publicCache;
|
|
13
13
|
export async function publicTools() {
|
|
14
|
-
publicCache ??= (await
|
|
14
|
+
publicCache ??= (await loadPublicTools())
|
|
15
|
+
.filter((t) => !brand().modules || brand().modules.includes(t.module));
|
|
15
16
|
return publicCache;
|
|
16
17
|
}
|
|
18
|
+
async function loadPublicTools() {
|
|
19
|
+
return get("/ai/public/tools", { anonymous: true });
|
|
20
|
+
}
|
|
17
21
|
/** MCP names may differ from catalog names (module prefix, dots); match loosely. */
|
|
18
22
|
export function sameTool(a, b) {
|
|
19
23
|
const n = (s) => s.toLowerCase().replace(/[.\-]/g, "_");
|
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 {
|
|
@@ -9,21 +9,53 @@ 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
|
+
/** Platforms whose webhook contract differs (event list shape, endpoints with many events, per-endpoint deliveries, test call). */
|
|
13
|
+
let farm = () => false;
|
|
14
|
+
/** Signature headers per platform: `…-Signature-256` by default (sha256=HMAC(ts.body)). */
|
|
15
|
+
function sigHeader() {
|
|
16
|
+
return "x-nextx-signature-256";
|
|
17
|
+
}
|
|
18
|
+
function tsHeader() {
|
|
19
|
+
return "x-nextx-timestamp";
|
|
20
|
+
}
|
|
21
|
+
function sigScheme() {
|
|
22
|
+
return "Signature-256";
|
|
23
|
+
}
|
|
12
24
|
export async function eventTypes() {
|
|
13
25
|
return get("/webhook/public/event-types", { anonymous: true });
|
|
14
26
|
}
|
|
27
|
+
/** Some platforms send `eventTypes` per endpoint and no single module. */
|
|
28
|
+
async function listEndpoints() {
|
|
29
|
+
const list = await get("/webhook/endpoints");
|
|
30
|
+
return farm() ? list.map((e) => ({ ...e, events: e.eventTypes ?? [], status: e.isActive ? "active" : "disabled" })) : list;
|
|
31
|
+
}
|
|
32
|
+
/** Some platforms keep deliveries per endpoint only; the CLI merges them. */
|
|
33
|
+
async function listDeliveries(o) {
|
|
34
|
+
if (!farm()) {
|
|
35
|
+
const path = o.endpoint ? `/webhook/endpoints/${(await findEndpoint(o.endpoint)).id}/deliveries` : "/webhook/deliveries";
|
|
36
|
+
const r = await get(path, { query: { status: o.status, pageSize: o.limit } });
|
|
37
|
+
return Array.isArray(r) ? r : r.items;
|
|
38
|
+
}
|
|
39
|
+
const eps = o.endpoint ? [await findEndpoint(o.endpoint)] : await listEndpoints();
|
|
40
|
+
const all = (await Promise.all(eps.map((e) => get(`/webhook/endpoints/${e.id}/deliveries`, { query: { limit: o.limit } })))).flat();
|
|
41
|
+
return all
|
|
42
|
+
.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 }))
|
|
43
|
+
.filter((d) => !o.status || d.status === o.status)
|
|
44
|
+
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
|
|
45
|
+
.slice(0, o.limit);
|
|
46
|
+
}
|
|
15
47
|
/** Checks both the NextX header (sha256=HMAC(ts.body)) and Standard Webhooks (v1,base64 HMAC(id.ts.body)). */
|
|
16
48
|
export function verifySignature(secret, body, headers, toleranceSec = 300) {
|
|
17
49
|
const h = Object.fromEntries(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), v]));
|
|
18
50
|
const eq = (a, b) => a.length === b.length && timingSafeEqual(Buffer.from(a), Buffer.from(b));
|
|
19
|
-
const ts = h[
|
|
51
|
+
const ts = h[tsHeader()] ?? h["webhook-timestamp"];
|
|
20
52
|
if (ts && Math.abs(Date.now() / 1000 - Number(ts)) > toleranceSec)
|
|
21
53
|
return { ok: false, reason: tr("dấu thời gian quá cũ", "timestamp too old") };
|
|
22
|
-
if (h[
|
|
54
|
+
if (h[sigHeader()] && ts) {
|
|
23
55
|
const expected = "sha256=" + createHmac("sha256", secret).update(`${ts}.${body}`).digest("hex");
|
|
24
|
-
const got = [h[
|
|
56
|
+
const got = [h[sigHeader()], h[`${sigHeader()}-previous`]].filter(Boolean);
|
|
25
57
|
if (got.some((g) => eq(g, expected)))
|
|
26
|
-
return { ok: true, scheme:
|
|
58
|
+
return { ok: true, scheme: sigScheme() };
|
|
27
59
|
}
|
|
28
60
|
if (h["webhook-signature"] && h["webhook-id"] && ts) {
|
|
29
61
|
const key = secret.startsWith("whsec_") ? Buffer.from(secret.slice(6), "base64") : Buffer.from(secret);
|
|
@@ -34,7 +66,7 @@ export function verifySignature(secret, body, headers, toleranceSec = 300) {
|
|
|
34
66
|
return { ok: false, reason: tr("chữ ký không khớp", "signature mismatch") };
|
|
35
67
|
}
|
|
36
68
|
async function findEndpoint(id) {
|
|
37
|
-
const e = (await
|
|
69
|
+
const e = (await listEndpoints()).find((x) => x.id.startsWith(id) || x.url === id);
|
|
38
70
|
if (!e)
|
|
39
71
|
throw new CliError(tr("Không thấy endpoint này.", "Endpoint not found."), Exit.notFound);
|
|
40
72
|
return e;
|
|
@@ -46,7 +78,7 @@ export function registerWebhooks(program) {
|
|
|
46
78
|
let list = await eventTypes();
|
|
47
79
|
if (o.module)
|
|
48
80
|
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 ?? ""]))));
|
|
81
|
+
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
82
|
});
|
|
51
83
|
wh.command("schema <event>").description(tr("schema và ví dụ payload", "payload schema and example"))
|
|
52
84
|
.action(async (event) => {
|
|
@@ -63,7 +95,7 @@ export function registerWebhooks(program) {
|
|
|
63
95
|
});
|
|
64
96
|
const ep = wh.command("endpoints").description(tr("endpoint nhận webhook của workspace", "your webhook endpoints"));
|
|
65
97
|
ep.command("list", { isDefault: true }).action(async () => {
|
|
66
|
-
const list = await
|
|
98
|
+
const list = await listEndpoints();
|
|
67
99
|
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
100
|
});
|
|
69
101
|
ep.command("create <url>")
|
|
@@ -71,6 +103,19 @@ export function registerWebhooks(program) {
|
|
|
71
103
|
.option("--description <text>")
|
|
72
104
|
.action(async (url, o) => {
|
|
73
105
|
const events = o.events.split(",").map((s) => s.trim()).filter(Boolean);
|
|
106
|
+
if (farm()) {
|
|
107
|
+
const known = new Set((await eventTypes()).map((t) => t.event));
|
|
108
|
+
const unknown = events.filter((e) => !known.has(e));
|
|
109
|
+
if (unknown.length)
|
|
110
|
+
throw new CliError(tr(`Không có sự kiện: ${unknown.join(", ")}`, `Unknown events: ${unknown.join(", ")}`), Exit.usage, `${brand().bin} webhooks events`);
|
|
111
|
+
const r = await post("/webhook/endpoints", { url, description: o.description, eventTypes: events });
|
|
112
|
+
emit(r, () => {
|
|
113
|
+
ok(tr("Đã tạo endpoint.", "Endpoint created."));
|
|
114
|
+
if (r.secret)
|
|
115
|
+
info(` ${tr("Khoá ký", "Signing secret")}: ${c.bold(String(r.secret))} ${c.dim(tr("(chỉ hiện một lần)", "(shown once)"))}`);
|
|
116
|
+
});
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
74
119
|
const types = await eventTypes();
|
|
75
120
|
const services = [...new Set(events.map((e) => types.find((t) => t.event === e)?.service).filter(Boolean))];
|
|
76
121
|
if (services.length !== 1)
|
|
@@ -98,10 +143,18 @@ export function registerWebhooks(program) {
|
|
|
98
143
|
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
144
|
.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
145
|
.action(async (event, o) => {
|
|
101
|
-
const list = await
|
|
146
|
+
const list = await listEndpoints();
|
|
102
147
|
const target = o.endpoint ? await findEndpoint(o.endpoint) : list.find((e) => e.events.includes(event));
|
|
103
148
|
if (!target)
|
|
104
149
|
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.`));
|
|
150
|
+
if (farm()) {
|
|
151
|
+
// This platform sends a signed `webhook.test` and answers with the receiver's status straight away.
|
|
152
|
+
const t = await post(`/webhook/endpoints/${target.id}/test`, {});
|
|
153
|
+
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`));
|
|
154
|
+
if (!t.ok)
|
|
155
|
+
process.exitCode = Exit.general;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
105
158
|
const r = await post(`/webhook/endpoints/${target.id}/test`, { eventType: event });
|
|
106
159
|
emit(r, () => ok(tr(`Đã gửi ${event} → ${target.url}`, `Sent ${event} → ${target.url}`) + (r.responseStatus ? ` · HTTP ${r.responseStatus}` : "")));
|
|
107
160
|
});
|
|
@@ -110,15 +163,13 @@ export function registerWebhooks(program) {
|
|
|
110
163
|
.option("--endpoint <id>")
|
|
111
164
|
.option("--limit <n>", "", "30")
|
|
112
165
|
.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;
|
|
166
|
+
const list = await listDeliveries({ endpoint: o.endpoint, status: o.status, limit: Number(o.limit) });
|
|
116
167
|
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
168
|
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
169
|
]))));
|
|
119
170
|
});
|
|
120
171
|
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
|
|
172
|
+
const full = id.length >= 32 ? id : (await listDeliveries({ limit: 100 })).find((d) => d.id.startsWith(id))?.id;
|
|
122
173
|
if (!full)
|
|
123
174
|
throw new CliError(tr("Không thấy lượt giao này.", "Delivery not found."), Exit.notFound);
|
|
124
175
|
const r = await post(`/webhook/deliveries/${full}/retry`, {});
|
|
@@ -139,9 +190,9 @@ export function registerWebhooks(program) {
|
|
|
139
190
|
headers[h.slice(0, i).trim()] = h.slice(i + 1).trim();
|
|
140
191
|
}
|
|
141
192
|
if (o.signature)
|
|
142
|
-
headers[
|
|
193
|
+
headers[sigHeader()] = o.signature;
|
|
143
194
|
if (o.timestamp)
|
|
144
|
-
headers[
|
|
195
|
+
headers[tsHeader()] = o.timestamp;
|
|
145
196
|
const r = verifySignature(o.secret, body, headers);
|
|
146
197
|
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
198
|
if (!r.ok)
|
package/dist/core/brand.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// One codebase, several branded binaries. The entry file (src/bin/<brand>.ts) picks the brand before
|
|
2
2
|
// anything else is imported; every user-facing name, host and storage location comes from here.
|
|
3
|
-
// <brand:nextx>
|
|
4
3
|
const NEXTX = {
|
|
5
4
|
id: "nextx",
|
|
5
|
+
platform: "nextx",
|
|
6
6
|
bin: "nextx",
|
|
7
7
|
name: "NextX",
|
|
8
8
|
aiName: "NextX AI",
|
|
@@ -10,6 +10,10 @@ const NEXTX = {
|
|
|
10
10
|
clientId: "nextx-cli",
|
|
11
11
|
envPrefix: "NEXTX",
|
|
12
12
|
mobileApp: "NextX",
|
|
13
|
+
examples: { scopes: "crm.contacts.view,sales.orders.view", ciKinds: "crm.tasks.create" },
|
|
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: {
|
|
@@ -30,10 +34,9 @@ const NEXTX = {
|
|
|
30
34
|
},
|
|
31
35
|
},
|
|
32
36
|
};
|
|
33
|
-
// </brand:nextx>
|
|
34
37
|
// Exported trees keep only their own brand (scripts/export.sh strips the other block and its line here).
|
|
35
38
|
const BRANDS = {
|
|
36
|
-
nextx: NEXTX,
|
|
39
|
+
nextx: NEXTX,
|
|
37
40
|
};
|
|
38
41
|
let current = Object.values(BRANDS)[0];
|
|
39
42
|
export function setBrand(id) {
|
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,13 +1,22 @@
|
|
|
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
10
|
import { currentAuth, refreshSession } from "../auth/session.js";
|
|
11
|
+
/** Key case on the wire: some services speak snake_case; the CLI keeps camelCase inside. */
|
|
12
|
+
function toWire(url, body) {
|
|
13
|
+
void url;
|
|
14
|
+
return body;
|
|
15
|
+
}
|
|
16
|
+
function fromWire(url, body) {
|
|
17
|
+
void url;
|
|
18
|
+
return body;
|
|
19
|
+
}
|
|
11
20
|
setGlobalDispatcher(new EnvHttpProxyAgent());
|
|
12
21
|
export const userAgent = () => `${brand().bin}-cli/${VERSION} (${platform()}; ${arch()}; node ${process.versions.node})`;
|
|
13
22
|
export function apiPath(path) {
|
|
@@ -46,10 +55,11 @@ async function send(method, url, opts, auth) {
|
|
|
46
55
|
};
|
|
47
56
|
let body;
|
|
48
57
|
if (opts.body !== undefined) {
|
|
49
|
-
|
|
58
|
+
// Multipart: undici sets the boundary itself, so no Content-Type here.
|
|
59
|
+
if (opts.body instanceof FormData || typeof opts.body === "string")
|
|
50
60
|
body = opts.body;
|
|
51
61
|
else {
|
|
52
|
-
body = JSON.stringify(opts.body);
|
|
62
|
+
body = JSON.stringify(opts.verbatim ? opts.body : toWire(url, opts.body));
|
|
53
63
|
headers["Content-Type"] ??= "application/json";
|
|
54
64
|
}
|
|
55
65
|
}
|
|
@@ -158,7 +168,8 @@ export async function request(method, path, opts = {}) {
|
|
|
158
168
|
const requestId = res.headers.get("x-request-id") ?? undefined;
|
|
159
169
|
if (opts.raw)
|
|
160
170
|
return { status: res.status, data: res, headers: res.headers, requestId };
|
|
161
|
-
const
|
|
171
|
+
const body = await readBody(res);
|
|
172
|
+
const data = opts.verbatim ? body : fromWire(res.url || path, body);
|
|
162
173
|
if (!res.ok && !opts.allow?.includes(res.status))
|
|
163
174
|
throw errorFor(res.status, data, res);
|
|
164
175
|
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 (API keys that 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,9 @@ 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";
|
|
23
25
|
const MODES = ["propose", "auto", "read"];
|
|
24
26
|
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
27
|
let seq = 0;
|
|
@@ -90,6 +92,7 @@ export function App(props) {
|
|
|
90
92
|
const [modal, setModal] = useState(null);
|
|
91
93
|
const [mode, setMode] = useState(context().mode);
|
|
92
94
|
const [agent, setAgent] = useState(context().agent);
|
|
95
|
+
const [local, setLocal] = useState(() => (props.model ? new LocalChat(props.model, true, context().mode === "read") : undefined));
|
|
93
96
|
const [sessionId, setSessionId] = useState(props.sessionId);
|
|
94
97
|
const [pending, setPending] = useState([]);
|
|
95
98
|
const [usageText, setUsageText] = useState("");
|
|
@@ -124,7 +127,7 @@ export function App(props) {
|
|
|
124
127
|
const line2 = ` ${c.dim(`${process.cwd().replace(process.env.HOME ?? "", "~")}${notes ? ` (${notesFileName()})` : ""} · CLI ${VERSION}`)}`;
|
|
125
128
|
const strip = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
|
|
126
129
|
const pad = (s) => s + " ".repeat(Math.max(0, inner + 1 - [...strip(s)].length));
|
|
127
|
-
const title = `─ ${b.aiName} `;
|
|
130
|
+
const title = local ? `─ ${b.name} · model: ${local.model.label} · ${tr("công cụ qua MCP", "tools over MCP")} ` : `─ ${b.aiName} `;
|
|
128
131
|
say([
|
|
129
132
|
c.cyan(`╭${title}${"─".repeat(Math.max(0, inner - title.length + 2))}╮`),
|
|
130
133
|
`${c.cyan("│")}${pad(line1)} ${c.cyan("│")}`,
|
|
@@ -191,6 +194,8 @@ export function App(props) {
|
|
|
191
194
|
say(proposalCard(p).join("\n"));
|
|
192
195
|
if (mode === "read")
|
|
193
196
|
return say(c.dim(tr(" Chế độ chỉ đọc: không ghi. Shift+Tab để đổi.", " Read-only mode: nothing written. Shift+Tab to change.")));
|
|
197
|
+
if (risk === "device")
|
|
198
|
+
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
199
|
if (risk === "finance")
|
|
195
200
|
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
201
|
const yes = mode === "auto" && risk === "normal" ? true : await askConfirm(p);
|
|
@@ -245,26 +250,38 @@ export function App(props) {
|
|
|
245
250
|
const ctl = new AbortController();
|
|
246
251
|
abortRef.current = ctl;
|
|
247
252
|
try {
|
|
253
|
+
const onChunk = (ch) => {
|
|
254
|
+
const l = liveRef.current;
|
|
255
|
+
if (ch.type === "token")
|
|
256
|
+
l.text += ch.text ?? "";
|
|
257
|
+
else if (ch.type === "activity" && ch.activity)
|
|
258
|
+
l.activities.push(ch.activity);
|
|
259
|
+
else if (ch.type === "agent" && ch.agent)
|
|
260
|
+
l.agent = ch.agent;
|
|
261
|
+
l.reconnecting = undefined;
|
|
262
|
+
};
|
|
263
|
+
if (local) {
|
|
264
|
+
const skipped = atts.filter((a) => a.kind !== "text");
|
|
265
|
+
if (skipped.length)
|
|
266
|
+
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(", ")}.`)));
|
|
267
|
+
const out = await local.ask({ text: composeText(clean, atts.filter((a) => a.kind === "text"), notes?.text), signal: ctl.signal, onChunk });
|
|
268
|
+
setLastOutcome(out);
|
|
269
|
+
say(renderAnswer(out));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
248
272
|
const out = await ask({
|
|
249
273
|
sessionId,
|
|
250
274
|
text: composeText(clean, atts, notes?.text),
|
|
251
275
|
attachments: atts,
|
|
252
276
|
agent: thisAgent,
|
|
253
277
|
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
|
-
},
|
|
278
|
+
onChunk,
|
|
264
279
|
onRetry: (n) => liveRef.current && (liveRef.current.reconnecting = n),
|
|
265
280
|
});
|
|
266
281
|
setLastOutcome(out);
|
|
267
|
-
|
|
282
|
+
if (out.sessionId && out.sessionId !== sessionId)
|
|
283
|
+
setSessionId(out.sessionId);
|
|
284
|
+
updateProfile(context().profile, { lastConversation: out.sessionId || sessionId });
|
|
268
285
|
say(renderAnswer(out));
|
|
269
286
|
if (out.proposedAction)
|
|
270
287
|
await handleProposal(out.proposedAction);
|
|
@@ -320,11 +337,16 @@ export function App(props) {
|
|
|
320
337
|
const cmd = (cmd0 ?? "").toLowerCase();
|
|
321
338
|
const arg = rest.join(" ").trim();
|
|
322
339
|
try {
|
|
340
|
+
// Commands that belong to another backend behave like unknown ones.
|
|
341
|
+
if (SLASH.some((s) => s.name === cmd) && !slashCommands().some((s) => s.name === cmd)) {
|
|
342
|
+
say(`${c.yellow("⚠")} ${tr(`Không có lệnh /${cmd}.`, `No command /${cmd}.`)} ${c.dim("/help")}`);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
323
345
|
switch (cmd) {
|
|
324
346
|
case "help":
|
|
325
347
|
case "?": {
|
|
326
|
-
const w = Math.max(...
|
|
327
|
-
say([...
|
|
348
|
+
const w = Math.max(...slashCommands().map((s) => s.name.length)) + 2;
|
|
349
|
+
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
350
|
break;
|
|
329
351
|
}
|
|
330
352
|
case "new": {
|
|
@@ -393,9 +415,22 @@ export function App(props) {
|
|
|
393
415
|
say(` ${modeLabel(m)}`);
|
|
394
416
|
break;
|
|
395
417
|
}
|
|
396
|
-
case "model":
|
|
397
|
-
|
|
418
|
+
case "model": {
|
|
419
|
+
if (!arg) {
|
|
420
|
+
const rows = listModels();
|
|
421
|
+
say([
|
|
422
|
+
` ${tr("Đang dùng", "Using")} ${c.bold(local ? local.model.label : b.aiName)}`,
|
|
423
|
+
` ${c.cyan(b.bin.padEnd(10))} ${b.aiName}`,
|
|
424
|
+
...rows.map((r) => ` ${c.cyan(r.name.padEnd(10))} ${r.entry.model ?? ""}${r.hasKey ? "" : c.yellow(tr(" (chưa có khoá)", " (no key)"))}`),
|
|
425
|
+
c.dim(` /model ${b.bin} · /model claude · /model ollama/<model> · ${b.bin} model add <nhà>`),
|
|
426
|
+
].join("\n"));
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
const m = resolveModel(arg);
|
|
430
|
+
setLocal(m ? new LocalChat(m, true, mode === "read") : undefined);
|
|
431
|
+
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
432
|
break;
|
|
433
|
+
}
|
|
399
434
|
case "attach":
|
|
400
435
|
if (!arg)
|
|
401
436
|
throw new Error(tr("Cần đường dẫn tệp.", "Pass a file path."));
|
|
@@ -765,7 +800,7 @@ export function App(props) {
|
|
|
765
800
|
});
|
|
766
801
|
// ── view ──────────────────────────────────────────────────────────────────────────────────────
|
|
767
802
|
const p = profile();
|
|
768
|
-
const footerRight = c.dim(`${p?.workspaceName ?? ""} · ${context().env}${usageText ? ` · ${usageText}` : ""}${agent ? ` · ${agent}` : ""}`);
|
|
803
|
+
const footerRight = c.dim(`${p?.workspaceName ?? ""} · ${context().env}${local ? ` · ${local.model.label}` : usageText ? ` · ${usageText}` : ""}${agent && !local ? ` · ${agent}` : ""}`);
|
|
769
804
|
const lastReason = live?.activities.filter((a) => a.kind !== "tool").at(-1);
|
|
770
805
|
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
806
|
}
|
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();
|