@nextxai/cli 0.3.5 → 0.3.8
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 +9 -0
- package/dist/ai/client.js +10 -0
- package/dist/commands/ai.js +16 -6
- package/dist/commands/auth.js +2 -2
- package/dist/commands/webhooks.js +6 -2
- package/dist/repl/markdown.js +38 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.8
|
|
4
|
+
|
|
5
|
+
- A workspace without an AI plan can still ask how-to questions in chat and with `-p`, up to the free daily limit.
|
|
6
|
+
|
|
7
|
+
## 0.3.7
|
|
8
|
+
|
|
9
|
+
- `-p` on a terminal now renders the answer: headings, bold, lists and tables. Piped or redirected output stays the raw text.
|
|
10
|
+
- `webhooks events` and `status` show only the modules and roles of your product.
|
|
11
|
+
|
|
3
12
|
## 0.3.5
|
|
4
13
|
|
|
5
14
|
- Vietnamese help keeps technical terms in English (API key, session, scope, signature, secret, retry, exit code, permission mode, output format, sandbox), and argument placeholders are English everywhere (`/rename <name>`, `/attach <file>`, `/config [key value]`).
|
package/dist/ai/client.js
CHANGED
|
@@ -105,12 +105,21 @@ export function composeText(question, attachments, projectNotes) {
|
|
|
105
105
|
parts.push(`\n\n--- ${tr(`Chỉ dẫn của dự án (${brand().bin.toUpperCase()}.md)`, `Project instructions (${brand().bin.toUpperCase()}.md)`)} ---\n${projectNotes}\n---`);
|
|
106
106
|
return parts.join("");
|
|
107
107
|
}
|
|
108
|
+
/** A workspace without an AI plan still gets how-to answers: the server's guidance door, same as the web app. */
|
|
109
|
+
export const modeForPlan = (planCode) => (planCode ? undefined : "guidance");
|
|
110
|
+
let cachedMode;
|
|
111
|
+
const askMode = () => {
|
|
112
|
+
// A failed plan lookup keeps the normal path: the server still decides what this workspace may do.
|
|
113
|
+
cachedMode ??= usage().then((u) => modeForPlan(u.planCode), () => undefined);
|
|
114
|
+
return cachedMode;
|
|
115
|
+
};
|
|
108
116
|
/**
|
|
109
117
|
* Sends one question and streams the answer. If the connection drops, the same Idempotency-Key is resent,
|
|
110
118
|
* so the server replays the stored answer instead of answering (and charging) twice.
|
|
111
119
|
*/
|
|
112
120
|
export async function ask(input) {
|
|
113
121
|
const key = randomUUID();
|
|
122
|
+
const mode = await askMode();
|
|
114
123
|
const body = {
|
|
115
124
|
sessionId: input.sessionId,
|
|
116
125
|
text: input.text,
|
|
@@ -118,6 +127,7 @@ export async function ask(input) {
|
|
|
118
127
|
fileKeys: input.attachments?.filter((a) => a.fileKey).map((a) => a.fileKey) ?? undefined,
|
|
119
128
|
agentCode: input.agent,
|
|
120
129
|
clientPlatform: "cli",
|
|
130
|
+
...(mode ? { mode } : {}),
|
|
121
131
|
};
|
|
122
132
|
let lastErr;
|
|
123
133
|
for (let attempt = 0; attempt < 3; attempt++) {
|
package/dist/commands/ai.js
CHANGED
|
@@ -14,6 +14,7 @@ import { ask, attach, attachmentKind, composeText, confirmAction, listConversati
|
|
|
14
14
|
import { LocalChat } from "../ai/local.js";
|
|
15
15
|
import { mcpToken } from "../auth/mcp-oauth.js";
|
|
16
16
|
import { defaultModel, resolveModel } from "../ai/models.js";
|
|
17
|
+
import { markdownStream } from "../repl/markdown.js";
|
|
17
18
|
// ── NEXTX.md ───────────────────────────────────────────────────────────────────────────────────────
|
|
18
19
|
const NOTES_MAX = 4096;
|
|
19
20
|
export function notesFileName() {
|
|
@@ -214,6 +215,15 @@ export async function runRoot(o, words) {
|
|
|
214
215
|
await withWorkspace(context().workspace, () => (model ? printLocal(o, model, question, piped) : printAsk(o, question, piped)));
|
|
215
216
|
}
|
|
216
217
|
/** -p with --model: same output contract as the platform assistant, answered by the user's model. */
|
|
218
|
+
/** On a terminal the answer is rendered; when piped or redirected it stays the raw text scripts expect. */
|
|
219
|
+
function answerWriter(fmt) {
|
|
220
|
+
const stdout = process.stdout;
|
|
221
|
+
if (fmt === "text" && stdout.isTTY) {
|
|
222
|
+
const md = markdownStream((t) => stdout.write(t));
|
|
223
|
+
return { push: (t) => md.push(t), end: () => md.end() };
|
|
224
|
+
}
|
|
225
|
+
return { push: (t) => void stdout.write(t), end: (full) => void (full && !full.endsWith("\n") && stdout.write("\n")) };
|
|
226
|
+
}
|
|
217
227
|
async function printLocal(o, model, question, piped) {
|
|
218
228
|
const attachments = [];
|
|
219
229
|
for (const f of (o.file ?? []).slice(0, 3)) {
|
|
@@ -225,6 +235,7 @@ async function printLocal(o, model, question, piped) {
|
|
|
225
235
|
attachments.push({ path: "-", name: "stdin", kind: "text", text: piped.slice(0, 20_000) });
|
|
226
236
|
const fmt = outputFormat();
|
|
227
237
|
const chat = new LocalChat(model, hasTty(), context().mode === "read");
|
|
238
|
+
const answer = answerWriter(fmt);
|
|
228
239
|
const outcome = await chat.ask({
|
|
229
240
|
text: composeText(question, attachments, findProjectNotes()?.text),
|
|
230
241
|
onChunk: (ch) => {
|
|
@@ -233,7 +244,7 @@ async function printLocal(o, model, question, piped) {
|
|
|
233
244
|
if (fmt !== "text")
|
|
234
245
|
return;
|
|
235
246
|
if (ch.type === "token")
|
|
236
|
-
|
|
247
|
+
answer.push(ch.text ?? "");
|
|
237
248
|
else if (ch.type === "activity" && ch.activity && !o.quiet)
|
|
238
249
|
process.stderr.write(c.dim(`⏺ ${ch.activity.label}\n`));
|
|
239
250
|
},
|
|
@@ -241,8 +252,7 @@ async function printLocal(o, model, question, piped) {
|
|
|
241
252
|
if (fmt === "json")
|
|
242
253
|
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 });
|
|
243
254
|
else if (fmt === "text") {
|
|
244
|
-
|
|
245
|
-
process.stdout.write("\n");
|
|
255
|
+
answer.end(outcome.text);
|
|
246
256
|
for (const n of outcome.notices)
|
|
247
257
|
process.stderr.write(`${c.yellow("⚠")} ${n.message}\n`);
|
|
248
258
|
}
|
|
@@ -261,6 +271,7 @@ async function printAsk(o, question, piped) {
|
|
|
261
271
|
const text = composeText(question, attachments, notes?.text);
|
|
262
272
|
const stdout = process.stdout;
|
|
263
273
|
const stderr = process.stderr;
|
|
274
|
+
const answer = answerWriter(fmt);
|
|
264
275
|
const outcome = await ask({
|
|
265
276
|
sessionId,
|
|
266
277
|
text,
|
|
@@ -272,7 +283,7 @@ async function printAsk(o, question, piped) {
|
|
|
272
283
|
if (fmt !== "text")
|
|
273
284
|
return;
|
|
274
285
|
if (ch.type === "token")
|
|
275
|
-
|
|
286
|
+
answer.push(ch.text ?? "");
|
|
276
287
|
else if (ch.type === "activity" && ch.activity?.kind === "tool" && !o.quiet)
|
|
277
288
|
stderr.write(c.dim(`⏺ ${ch.activity.label}\n`));
|
|
278
289
|
},
|
|
@@ -314,8 +325,7 @@ async function printAsk(o, question, piped) {
|
|
|
314
325
|
});
|
|
315
326
|
}
|
|
316
327
|
else if (fmt === "text") {
|
|
317
|
-
|
|
318
|
-
stdout.write("\n");
|
|
328
|
+
answer.end(outcome.text);
|
|
319
329
|
// Notices the answer already says in words are not repeated.
|
|
320
330
|
for (const n of outcome.notices)
|
|
321
331
|
if (n.message && !outcome.text.includes(n.message.slice(0, 24)))
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { brand } from "../core/brand.js";
|
|
1
|
+
import { brand, visibleModule } from "../core/brand.js";
|
|
2
2
|
import { context } from "../core/context.js";
|
|
3
3
|
import { CliError, Exit } from "../core/errors.js";
|
|
4
4
|
import { tr } from "../core/i18n.js";
|
|
@@ -125,7 +125,7 @@ export function registerAuth(program) {
|
|
|
125
125
|
};
|
|
126
126
|
emit(data, () => {
|
|
127
127
|
info(`${c.bold(me.fullName ?? "")} ${c.dim(me.email ?? "")}`);
|
|
128
|
-
info(` ${tr("Workspace", "Workspace")} ${c.bold(me.workspace?.name ?? "")} ${c.dim(me.workspace?.code ?? "")} · ${(me.groups ?? []).join(", ")}`);
|
|
128
|
+
info(` ${tr("Workspace", "Workspace")} ${c.bold(me.workspace?.name ?? "")} ${c.dim(me.workspace?.code ?? "")} · ${(me.groups ?? []).filter(visibleModule).join(", ")}`);
|
|
129
129
|
info(` ${tr("Môi trường", "Environment")} ${context().env === "prod" ? c.red("prod") : c.yellow("sandbox")} · ${context().endpoints.api}`);
|
|
130
130
|
if (u)
|
|
131
131
|
info(` ${tr("Gói AI", "AI plan")} ${u.planName ?? "-"}${u.limit ? ` · ${tr("đã dùng", "used")} ${u.used}/${u.limit}, ${tr("mở lại", "resets")} ${u.resetsAt?.slice(0, 10)}` : ""}`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { fetch } from "undici";
|
|
4
|
-
import { brand } from "../core/brand.js";
|
|
4
|
+
import { brand, visibleModule } from "../core/brand.js";
|
|
5
5
|
import { context } from "../core/context.js";
|
|
6
6
|
import { CliError, Exit } from "../core/errors.js";
|
|
7
7
|
import { currentLang, tr } from "../core/i18n.js";
|
|
@@ -22,7 +22,11 @@ function sigScheme() {
|
|
|
22
22
|
return "Signature-256";
|
|
23
23
|
}
|
|
24
24
|
export async function eventTypes() {
|
|
25
|
-
return get("/webhook/public/event-types", { anonymous: true });
|
|
25
|
+
return visibleEvents(await get("/webhook/public/event-types", { anonymous: true }));
|
|
26
|
+
}
|
|
27
|
+
/** The event catalogue is shared by every product; each one shows only its own modules. */
|
|
28
|
+
export function visibleEvents(list) {
|
|
29
|
+
return list.filter((e) => visibleModule(e.service));
|
|
26
30
|
}
|
|
27
31
|
/** Some platforms send `eventTypes` per endpoint and no single module. */
|
|
28
32
|
async function listEndpoints() {
|
package/dist/repl/markdown.js
CHANGED
|
@@ -55,3 +55,41 @@ export function markdown(src) {
|
|
|
55
55
|
}
|
|
56
56
|
return out.join("\n");
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Renders a streamed answer as it arrives. Text is held until a blank line outside a code fence closes the block, so
|
|
60
|
+
* bold markers and tables split across tokens still render whole.
|
|
61
|
+
*/
|
|
62
|
+
export function markdownStream(write) {
|
|
63
|
+
let buf = "";
|
|
64
|
+
const flush = (block) => {
|
|
65
|
+
if (block.trim())
|
|
66
|
+
write(`${markdown(block)}\n\n`);
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
push(text) {
|
|
70
|
+
buf += text.replace(/\r/g, "");
|
|
71
|
+
for (;;) {
|
|
72
|
+
let fence = false;
|
|
73
|
+
let at = -1;
|
|
74
|
+
let pos = 0;
|
|
75
|
+
for (const line of buf.split("\n").slice(0, -1)) {
|
|
76
|
+
if (/^```/.test(line))
|
|
77
|
+
fence = !fence;
|
|
78
|
+
else if (!fence && line.trim() === "" && pos > 0) {
|
|
79
|
+
at = pos;
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
pos += line.length + 1;
|
|
83
|
+
}
|
|
84
|
+
if (at < 0)
|
|
85
|
+
return;
|
|
86
|
+
flush(buf.slice(0, at));
|
|
87
|
+
buf = buf.slice(at + 1);
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
end() {
|
|
91
|
+
flush(buf);
|
|
92
|
+
buf = "";
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|