@tiens.nguyen/gonext-cli 1.0.354 → 1.0.356

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.
Files changed (2) hide show
  1. package/gonext-repl.mjs +84 -36
  2. package/package.json +1 -1
package/gonext-repl.mjs CHANGED
@@ -55,6 +55,18 @@ const green = (s) => `\x1b[32m${s}\x1b[0m`;
55
55
  const red = (s) => `\x1b[31m${s}\x1b[0m`;
56
56
  const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
57
57
  const white = (s) => `\x1b[37m${s}\x1b[0m`;
58
+ // Left gutter so terminal output doesn't hug the edge (task #128, Phase 1). A terminal has
59
+ // no CSS padding — a "left margin" is just a space prefix on every printed line, applied
60
+ // consistently at the prompt, the ● bullets, plain replies, the banner and the live status
61
+ // so they share ONE clean left edge. Existing secondary indents already use 2 spaces, so a
62
+ // 2-space gutter converges everything to the same column. `gutterLines` insets a multi-line
63
+ // block (blank lines left bare — no trailing whitespace).
64
+ const GUTTER = " ";
65
+ const gutterLines = (s) =>
66
+ String(s)
67
+ .split("\n")
68
+ .map((l) => (l.length ? GUTTER + l : l))
69
+ .join("\n");
58
70
  // ANSI 90 ("bright black") — a reliable neutral grey across terminal themes, unlike
59
71
  // code 34 (blue) which many dark-theme palettes render as purple/indigo instead.
60
72
  const codeColor = (s) => `\x1b[90m${s}\x1b[0m`;
@@ -278,7 +290,7 @@ const rl = createInterface({
278
290
  terminal: IS_TTY,
279
291
  completer: slashCompleter,
280
292
  });
281
- const PROMPT = cyan(">> ");
293
+ const PROMPT = GUTTER + cyan(">> ");
282
294
  rl.setPrompt(PROMPT);
283
295
  // Side effect of terminal:false above: readline no longer intercepts arrow keys for
284
296
  // command-history recall, so pressing ↑/↓/→/← (or Home/End/Delete) sends the RAW escape
@@ -383,7 +395,9 @@ let secretInput = false;
383
395
 
384
396
  // Visible width of PROMPT (">> ") — used to put the caret back on the input line by
385
397
  // COLUMN, since the string itself carries color escapes that don't occupy cells.
386
- const PROMPT_COLS = 3;
398
+ // Visible width of PROMPT = the gutter + ">> " (task #128). Used by the slash-overlay
399
+ // cursor math to park the caret at the right column, so it must include the gutter.
400
+ const PROMPT_COLS = GUTTER.length + 3;
387
401
 
388
402
  // Truncate to `max` VISIBLE columns, keeping the ANSI color escapes (which occupy no
389
403
  // cells) intact. Essential for the overlay: every row it draws must fit on ONE physical
@@ -428,8 +442,12 @@ function clipVisible(s, max) {
428
442
  function slashOverlaySeq(line, cursor, menu, cols) {
429
443
  const width = Math.max(20, cols || 80);
430
444
  const n = menu.length;
445
+ // The input line already insets via PROMPT (= GUTTER + ">> "). Inset the MENU rows too
446
+ // (task #128 Phase 2), clipping to width-1-GUTTER so gutter+row stays on ONE physical row
447
+ // — the #109 no-wrap invariant that keeps the \x1b[nA row math exact.
431
448
  let out = "\r\x1b[2K" + clipVisible(PROMPT + line, width - 1);
432
- for (const r of menu) out += "\n\x1b[2K" + clipVisible(r, width - 1);
449
+ for (const r of menu)
450
+ out += "\n\x1b[2K" + GUTTER + clipVisible(r, width - 1 - GUTTER.length);
433
451
  // Wipe anything left below (the filtered list can SHRINK as you type), then walk back
434
452
  // up to the input line and park the caret at the typed-text offset.
435
453
  out += "\x1b[J";
@@ -600,7 +618,10 @@ const ask = async (q) => {
600
618
  // Route the question through readline's own prompt so its redraw logic stays
601
619
  // consistent (a raw stdout write here would re-create the stale-cursor artifact
602
620
  // that terminal:true mode is designed to avoid). Restored to ">> " afterwards.
603
- rl.setPrompt(q);
621
+ // Inset the question by the gutter (task #128) so Y/n prompts share the left edge —
622
+ // gutterLines so a MULTI-line question ("…commands\n(npm test…)?") insets every line, not
623
+ // just the first. All ask() callers pass un-indented questions, so it never double-indents.
624
+ rl.setPrompt(gutterLines(q));
604
625
  rl.prompt();
605
626
  const a = await nextLine();
606
627
  rl.setPrompt(PROMPT);
@@ -659,6 +680,7 @@ async function ensureWorkspace() {
659
680
  }
660
681
  wsSync = { enabled: hit.allowSync !== false, name: hit.name, path: hit.path };
661
682
  console.log(
683
+ GUTTER +
662
684
  dim(
663
685
  `workspace: ${hit.name} → ${hit.path} (run ${hit.allowRun ? "enabled" : "disabled"}` +
664
686
  `${hit.allowSync !== false ? ", cloud sync on" : ""})`
@@ -667,12 +689,12 @@ async function ensureWorkspace() {
667
689
  return;
668
690
  }
669
691
  if (!existsSync(cwd) || !statSync(cwd).isDirectory()) return;
670
- console.log(`This folder is not a registered workspace:\n ${cwd}`);
692
+ console.log(gutterLines(`This folder is not a registered workspace:\n ${cwd}`));
671
693
  const register = await askYesNo(
672
694
  "Register it so the agent can read/edit code here?", true /* default Yes */
673
695
  );
674
696
  if (!register) {
675
- console.log(dim("Skipped — the agent will not be able to touch files here."));
697
+ console.log(GUTTER + dim("Skipped — the agent will not be able to touch files here."));
676
698
  return;
677
699
  }
678
700
  const allowRun = await askYesNo(
@@ -700,7 +722,8 @@ async function ensureWorkspace() {
700
722
  await writeFile(WS_FILE, JSON.stringify({ workspaces: next }, null, 2) + "\n");
701
723
  wsSync = { enabled: allowSync, name: basename(cwd), path: cwd };
702
724
  console.log(
703
- green(`✓ workspace registered: ${basename(cwd)}`) +
725
+ GUTTER +
726
+ green(`✓ workspace registered: ${basename(cwd)}`) +
704
727
  (allowRun ? green(" (run-commands enabled)") : dim(" (run-commands disabled)")) +
705
728
  (allowSync ? green(" · cloud sync on") : dim(" · cloud sync off"))
706
729
  );
@@ -1151,15 +1174,19 @@ let clearActiveStatus = null;
1151
1174
  function drawApprovalOptions(redraw) {
1152
1175
  // Two option lines, redrawn in place. On redraw the cursor sits one row below "No";
1153
1176
  // step up 2 rows, clear+rewrite both, landing back where we started.
1177
+ // Gutter (task #128 Phase 2): inset each option after the line-clear. The ▸/spaces marker
1178
+ // is 2 cols so Yes/No align; the \x1b[2A row-up count is unchanged (still 2 rows).
1154
1179
  const yes = approvalSel === 0 ? green("▸ Yes") : dim(" Yes");
1155
1180
  const no = approvalSel === 1 ? green("▸ No") : dim(" No");
1156
- process.stdout.write((redraw ? "\x1b[2A" : "") + "\x1b[K" + yes + "\n\x1b[K" + no + "\n");
1181
+ process.stdout.write(
1182
+ (redraw ? "\x1b[2A" : "") + "\x1b[K" + GUTTER + yes + "\n\x1b[K" + GUTTER + no + "\n"
1183
+ );
1157
1184
  }
1158
1185
  function finishApproval(allow) {
1159
1186
  if (!approvalActive) return;
1160
1187
  approvalActive = false;
1161
1188
  // Settle the picker: overwrite the two option lines with a single result line.
1162
- process.stdout.write("\x1b[2A\x1b[J" + (allow ? green(" ▸ Yes — running it") : red(" ▸ No — skipped")) + "\n");
1189
+ process.stdout.write("\x1b[2A\x1b[J" + GUTTER + (allow ? green("▸ Yes — running it") : red("▸ No — skipped")) + "\n");
1163
1190
  const r = approvalResolve;
1164
1191
  approvalResolve = null;
1165
1192
  if (r) r(!!allow);
@@ -1230,7 +1257,7 @@ async function approvalPrompt(command) {
1230
1257
  }
1231
1258
  const allow = await new Promise((resolve) => {
1232
1259
  if (!process.stdin.isTTY) {
1233
- process.stdout.write(dim(`\n(approval needed for: ${command} — no interactive terminal, skipping)\n`));
1260
+ process.stdout.write("\n" + GUTTER + dim(`(approval needed for: ${command} — no interactive terminal, skipping)`) + "\n");
1234
1261
  resolve(false);
1235
1262
  return;
1236
1263
  }
@@ -1260,8 +1287,10 @@ let listItems = [];
1260
1287
  let listResolve = null;
1261
1288
  function drawList(redraw) {
1262
1289
  const n = listItems.length;
1290
+ // Gutter (task #128 Phase 2): inset each row after its line-clear. Items are short (model
1291
+ // / server names), so no clip is needed; the \x1b[nA row-up count is unchanged (n rows).
1263
1292
  const body = listItems
1264
- .map((it, i) => "\x1b[K" + (i === listSel ? green("▸ " + it) : dim(" " + it)))
1293
+ .map((it, i) => "\x1b[K" + GUTTER + (i === listSel ? green("▸ " + it) : dim(" " + it)))
1265
1294
  .join("\n");
1266
1295
  process.stdout.write((redraw ? `\x1b[${n}A` : "") + body + "\n");
1267
1296
  }
@@ -1778,7 +1807,11 @@ async function runAgentTurn(history) {
1778
1807
  // underlined bright-green hover style when the mouse is over a clickable thought. Then the
1779
1808
  // GREY expanded lines (if any), then LAST the SPINNER + playful word + token labels
1780
1809
  // (task #83/#84), e.g. " ⠙ Caffeinating… · 12.3k tok · 8.1k max".
1781
- let out = `${green(BULLET)} ${hovering ? hoverThought(label) : green(label)}`;
1810
+ // Gutter the live status block's first line (task #128) so the blinking ● lines up with
1811
+ // the finished ● bullets during a turn (no left-edge jump). The sub-lines below already
1812
+ // use a 2-space indent = the gutter. Safe for the redraw: clearStatus clears whole lines
1813
+ // and the hover/click hit-testing is ROW-based, so a horizontal offset changes nothing.
1814
+ let out = `${GUTTER}${green(BULLET)} ${hovering ? hoverThought(label) : green(label)}`;
1782
1815
  for (const wl of expLines) out += "\n " + dim(wl);
1783
1816
  out += "\n" + ` ${color256(wc, `${glyph} ${thinkWord}…`)}${tokLabel}`;
1784
1817
  process.stdout.write(out);
@@ -1921,7 +1954,7 @@ async function runAgentTurn(history) {
1921
1954
  if (isEditCardHeader(line)) {
1922
1955
  onContent();
1923
1956
  inEditCard = true;
1924
- process.stdout.write(green("⏺") + bold(line.slice(1)) + "\n");
1957
+ process.stdout.write(GUTTER + green("⏺") + bold(line.slice(1)) + "\n");
1925
1958
  lastWasBlank = false;
1926
1959
  lastContentAt = Date.now();
1927
1960
  continue;
@@ -1933,9 +1966,9 @@ async function runAgentTurn(history) {
1933
1966
  if (m) {
1934
1967
  // " 19 - old text" → dim line number, red/green signed content.
1935
1968
  const paintDiff = m[3] === "+" ? green : red;
1936
- process.stdout.write(`${m[1]}${dim(m[2])} ${paintDiff(`${m[3]} ${m[4]}`)}\n`);
1969
+ process.stdout.write(`${GUTTER}${m[1]}${dim(m[2])} ${paintDiff(`${m[3]} ${m[4]}`)}\n`);
1937
1970
  } else {
1938
- process.stdout.write(dim(line) + "\n"); // "└ summary" / "… (+N more)"
1971
+ process.stdout.write(GUTTER + dim(line) + "\n"); // "└ summary" / "… (+N more)"
1939
1972
  }
1940
1973
  lastWasBlank = false;
1941
1974
  lastContentAt = Date.now();
@@ -1959,7 +1992,7 @@ async function runAgentTurn(history) {
1959
1992
  }
1960
1993
  if (rest) {
1961
1994
  onContent();
1962
- process.stdout.write(codeColor(rest) + "\n");
1995
+ process.stdout.write(GUTTER + codeColor(rest) + "\n");
1963
1996
  lastWasBlank = false;
1964
1997
  }
1965
1998
  lastContentAt = Date.now();
@@ -1978,7 +2011,7 @@ async function runAgentTurn(history) {
1978
2011
  const before = t.replace(CODE_CLOSE_RE, "").trim();
1979
2012
  if (before) {
1980
2013
  onContent();
1981
- process.stdout.write(codeColor(before) + "\n");
2014
+ process.stdout.write(GUTTER + codeColor(before) + "\n");
1982
2015
  lastWasBlank = false;
1983
2016
  }
1984
2017
  inCode = false;
@@ -1987,7 +2020,7 @@ async function runAgentTurn(history) {
1987
2020
  }
1988
2021
  if (t === "") { lastContentAt = Date.now(); continue; } // skip blank lines in code
1989
2022
  onContent();
1990
- process.stdout.write(codeColor(line) + "\n");
2023
+ process.stdout.write(GUTTER + codeColor(line) + "\n");
1991
2024
  lastWasBlank = false;
1992
2025
  continue;
1993
2026
  }
@@ -2024,14 +2057,18 @@ async function runAgentTurn(history) {
2024
2057
  if (plainReplyFlow) {
2025
2058
  // Plain-reply content IS the answer — print it in the default color and remember
2026
2059
  // it's visible so the caller doesn't reprint it once the turn completes.
2027
- process.stdout.write(line.slice(alreadyFlushed) + "\n");
2060
+ // Gutter (task #128) ONLY at line-start: when alreadyFlushed>0 this is a re-flush of
2061
+ // a growing SAME physical line, so a gutter here would land mid-line.
2062
+ process.stdout.write(
2063
+ (alreadyFlushed === 0 ? GUTTER : "") + line.slice(alreadyFlushed) + "\n"
2064
+ );
2028
2065
  liveAnswer += line.slice(alreadyFlushed) + "\n";
2029
2066
  answerShownLive = true;
2030
2067
  } else {
2031
2068
  // The remaining out-of-fence ACTION events ("Command passed → …", "Searching code
2032
2069
  // → …", "Server up → …", "Composing answer…") each print as ONE solid bullet —
2033
2070
  // the clean, summarized action list (task #41).
2034
- process.stdout.write(white(BULLET) + " " + line.trim() + "\n");
2071
+ process.stdout.write(GUTTER + white(BULLET) + " " + line.trim() + "\n");
2035
2072
  }
2036
2073
  lastWasBlank = false;
2037
2074
  }
@@ -2041,7 +2078,10 @@ async function runAgentTurn(history) {
2041
2078
  // this was meant to fix. Only safe in the plain-reply flow (see plainReplyFlow above).
2042
2079
  if (plainReplyFlow && !inCode && carry.length > carryFlushedLen) {
2043
2080
  if (carryFlushedLen === 0) onContent();
2044
- process.stdout.write(carry.slice(carryFlushedLen)); // default color this IS the answer
2081
+ // Gutter (task #128) only at the pending line's START; a continuation appends bare.
2082
+ process.stdout.write(
2083
+ (carryFlushedLen === 0 ? GUTTER : "") + carry.slice(carryFlushedLen)
2084
+ ); // default color — this IS the answer
2045
2085
  liveAnswer += carry.slice(carryFlushedLen);
2046
2086
  carryFlushedLen = carry.length;
2047
2087
  answerShownLive = true;
@@ -2161,7 +2201,14 @@ async function runAgentTurn(history) {
2161
2201
  }
2162
2202
  if (rest) {
2163
2203
  onContent();
2164
- process.stdout.write(rest);
2204
+ // Gutter (task #128 Phase 2): this tail CONTINUES the already-streamed answer, so
2205
+ // inset only NEW lines within it (\n followed by content), and prepend the gutter
2206
+ // only when the cursor is already at a line start (liveAnswer empty or ended with
2207
+ // a newline). Rare path — an old worker that replaced the streamed text.
2208
+ const atLineStart = liveAnswer === "" || /\n$/.test(liveAnswer);
2209
+ process.stdout.write(
2210
+ (atLineStart ? GUTTER : "") + rest.replace(/\n(?=[^\n])/g, "\n" + GUTTER)
2211
+ );
2165
2212
  lastWasBlank = false;
2166
2213
  }
2167
2214
  }
@@ -2232,7 +2279,7 @@ async function runAgentTurn(history) {
2232
2279
  // killed the Python agent. Print a quiet note and return an empty, non-persisted
2233
2280
  // turn (main() drops it from history).
2234
2281
  clearStatus();
2235
- process.stdout.write(dim("\n(cancelled — the agent was stopped)\n"));
2282
+ process.stdout.write("\n" + GUTTER + dim("(cancelled — the agent was stopped)") + "\n");
2236
2283
  return { text: "", shown: false, cancelled: true };
2237
2284
  }
2238
2285
  if (job.jobStatus === "failed") {
@@ -2272,7 +2319,7 @@ async function runAgentTurn(history) {
2272
2319
 
2273
2320
  // ---------- REPL ----------
2274
2321
  async function main() {
2275
- console.log(cyan("GoNext agent REPL") + dim(` v${REPL_VERSION} · API ${apiBase} · key ${workerKey.slice(0, 8)}…`));
2322
+ console.log(GUTTER + cyan("GoNext agent REPL") + dim(` v${REPL_VERSION} · API ${apiBase} · key ${workerKey.slice(0, 8)}…`));
2276
2323
  // Task #127, Phase 2: the REPL enqueues jobs that the polling DAEMON executes, so ensure
2277
2324
  // one is running — no GoTerminal needed. Idempotent + BC4-safe: startDaemon() no-ops when
2278
2325
  // a daemon (ours, GoTerminal's, or a manual one) is already alive. Best-effort; a failure
@@ -2319,6 +2366,7 @@ async function main() {
2319
2366
  await saveSessionRagMode(resolve(process.cwd()), sessionRagMode);
2320
2367
  }
2321
2368
  console.log(
2369
+ GUTTER +
2322
2370
  dim(
2323
2371
  `agent model: ${p.agentModelId || probe?.modelKey || "?"}` +
2324
2372
  (p.codingModelId ? ` · coder: ${p.codingModelId}` : "") +
@@ -2385,14 +2433,14 @@ async function main() {
2385
2433
  }
2386
2434
  process.exit(1);
2387
2435
  }
2388
- console.log(dim("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.\n"));
2436
+ console.log(GUTTER + dim("Ask about this repo. Type / to see commands (Tab completes). Ctrl-C aborts a running turn.\n"));
2389
2437
 
2390
2438
  const cwd = resolve(process.cwd());
2391
2439
  const history = await loadSession(cwd);
2392
2440
  if (history.length > 0) {
2393
2441
  const turns = history.filter((m) => m.role === "user").length;
2394
2442
  console.log(
2395
- dim(`Resumed session: ${turns} prior turn(s) from last time in this folder — /reset to start fresh.\n`)
2443
+ GUTTER + dim(`Resumed session: ${turns} prior turn(s) from last time in this folder — /reset to start fresh.\n`)
2396
2444
  );
2397
2445
  }
2398
2446
  // Ctrl-C arrives on ONE of two paths depending on the readline mode: with
@@ -2413,7 +2461,7 @@ async function main() {
2413
2461
  // agent; the poll loop then returns cleanly with the "(cancelled)" note.
2414
2462
  cancelRequested = true;
2415
2463
  const jobId = currentJobId;
2416
- process.stdout.write(red("\n^C ") + dim("stopping the agent…\n"));
2464
+ process.stdout.write("\n" + GUTTER + red("^C ") + dim("stopping the agent…\n"));
2417
2465
  fetch(`${apiBase}/api/worker/jobs/cancel`, {
2418
2466
  method: "POST",
2419
2467
  headers: { "Content-Type": "application/json", "X-Worker-Key": workerKey },
@@ -2425,12 +2473,12 @@ async function main() {
2425
2473
  // Second Ctrl+C (or no jobId yet): give up waiting locally. The worker keeps
2426
2474
  // trying to stop, but we stop following.
2427
2475
  followAborted = true;
2428
- process.stdout.write(red("\n^C "));
2476
+ process.stdout.write("\n" + GUTTER + red("^C "));
2429
2477
  }
2430
2478
  } else {
2431
2479
  // Ctrl+C at a prompt with the slash overlay open = dismiss the overlay first (#109).
2432
2480
  if (slashOverlayActive) closeSlashOverlay();
2433
- process.stdout.write(dim("\n(/exit to quit)\n"));
2481
+ process.stdout.write("\n" + GUTTER + dim("(/exit to quit)") + "\n");
2434
2482
  // Discard anything half-typed (Ctrl-C at a prompt means "never mind") and
2435
2483
  // redraw through readline so its cursor tracking stays right.
2436
2484
  rl.line = "";
@@ -2503,7 +2551,7 @@ async function main() {
2503
2551
  if (line === "/reset" || line === "/new") {
2504
2552
  history.length = 0;
2505
2553
  await clearSession(cwd);
2506
- console.log(dim("Session cleared — starting fresh.\n"));
2554
+ console.log(GUTTER + dim("Session cleared — starting fresh.\n"));
2507
2555
  continue;
2508
2556
  }
2509
2557
  if (line === "/output-token") {
@@ -2531,15 +2579,15 @@ async function main() {
2531
2579
  false
2532
2580
  );
2533
2581
  if (!ok) {
2534
- console.log(dim("Cancelled — output-token totals unchanged.\n"));
2582
+ console.log(GUTTER + dim("Cancelled — output-token totals unchanged.\n"));
2535
2583
  continue;
2536
2584
  }
2537
2585
  const done = await resetGlobalOutputTokens();
2538
2586
  if (done) {
2539
2587
  globalOutputTokens = 0;
2540
- console.log(green("✓ global + workspace output-token totals reset to 0.\n"));
2588
+ console.log(GUTTER + green("✓ global + workspace output-token totals reset to 0.\n"));
2541
2589
  } else {
2542
- console.log(dim("Could not reset the output-token totals (API error).\n"));
2590
+ console.log(GUTTER + dim("Could not reset the output-token totals (API error).\n"));
2543
2591
  }
2544
2592
  continue;
2545
2593
  }
@@ -2560,7 +2608,7 @@ async function main() {
2560
2608
  // question; point the user at /help (all real commands were handled above).
2561
2609
  if (line.startsWith("/")) {
2562
2610
  const cmd = line.split(/\s+/)[0];
2563
- console.log(dim(`unknown command: ${cmd} — type /help (or / then Tab) to list commands\n`));
2611
+ console.log(GUTTER + dim(`unknown command: ${cmd} — type /help (or / then Tab) to list commands\n`));
2564
2612
  continue;
2565
2613
  }
2566
2614
  // Resume a cancelled task: a BARE "continue" right after a Ctrl+C re-runs the ORIGINAL
@@ -2576,7 +2624,7 @@ async function main() {
2576
2624
  ) {
2577
2625
  taskLine = pendingResume;
2578
2626
  const shown = pendingResume.length > 80 ? pendingResume.slice(0, 80) + "…" : pendingResume;
2579
- console.log(dim(`↻ resuming the cancelled task: ${shown}\n`));
2627
+ console.log(GUTTER + dim(`↻ resuming the cancelled task: ${shown}\n`));
2580
2628
  }
2581
2629
  history.push({ role: "user", content: taskLine });
2582
2630
  try {
@@ -2590,7 +2638,7 @@ async function main() {
2590
2638
  await saveSession(cwd, history);
2591
2639
  // A plain-reply answer already streamed live in the loop above — printing it
2592
2640
  // again here would just duplicate it; a blank line is enough for spacing.
2593
- console.log(shown ? "" : `\n\n${answer}\n`);
2641
+ console.log(shown ? "" : `\n\n${gutterLines(answer)}\n`);
2594
2642
  // Task #104: persistent token footer at the bottom of the screen — this turn's
2595
2643
  // code-model INPUT + this turn's OUTPUT (↓ out), then the model's GLOBAL lifetime
2596
2644
  // output total (all turns for this user + coding server), labelled with the model
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tiens.nguyen/gonext-cli",
3
- "version": "1.0.354",
3
+ "version": "1.0.356",
4
4
  "description": "GoNext CLI — the gonext terminal plus the local worker that runs agent / OCR / PDF / embedding jobs on your Mac (Ollama / MLX / OpenAI-compatible).",
5
5
  "type": "module",
6
6
  "license": "MIT",