@nextxai/cli 0.3.5 → 0.3.7

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 CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.7
4
+
5
+ - `-p` on a terminal now renders the answer: headings, bold, lists and tables. Piped or redirected output stays the raw text.
6
+ - `webhooks events` and `status` show only the modules and roles of your product.
7
+
3
8
  ## 0.3.5
4
9
 
5
10
  - 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]`).
@@ -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
- process.stdout.write(ch.text ?? "");
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
- if (outcome.text && !outcome.text.endsWith("\n"))
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
- stdout.write(ch.text ?? "");
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
- if (outcome.text && !outcome.text.endsWith("\n"))
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)))
@@ -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() {
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextxai/cli",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
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",