@oxecli/oxe 1.0.87 → 1.0.89

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/dist/cli.js CHANGED
@@ -18,6 +18,7 @@ export class CLI {
18
18
  story = [];
19
19
  sessionId = null;
20
20
  promptHistory = [];
21
+ lastPrompt = "";
21
22
  interruptPending = false;
22
23
  constructor() {
23
24
  clearScreen();
@@ -395,10 +396,50 @@ export class CLI {
395
396
  const used = estimateTokens(this.inputItems);
396
397
  const budget = max_context_tokens - context_overhead_margin;
397
398
  const pct = Math.max(0, Math.min(100, Math.round(((budget - used) / budget) * 100)));
399
+ // Queries run through onSubmit so the prompt box stays focused and
400
+ // editable while the AI streams its reply and tool calls above it.
401
+ // Commands (slash inputs) settle back to this loop for handling.
402
+ const onSubmit = async (input, spans) => {
403
+ this.interruptPending = false;
404
+ this.promptHistory.push(input);
405
+ this.lastPrompt = input;
406
+ dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
407
+ queryStarted = true;
408
+ try {
409
+ await engine.executeQuery(input, this.inputItems, this.story, spans);
410
+ }
411
+ catch (qerr) {
412
+ if (qerr?.message === "interrupt") {
413
+ if (!engine.queryHasOutput && !engine.queryCalledTool) {
414
+ dockAppendContent(aiMarkdown("What else can I help you with?") + "\n");
415
+ }
416
+ engine.inQuery = false;
417
+ stripOrphanCalls(this.inputItems);
418
+ if (this.inputItems.length &&
419
+ this.inputItems[this.inputItems.length - 1]["role"] === "user") {
420
+ this.inputItems.pop();
421
+ }
422
+ if (this.story.length &&
423
+ this.story[this.story.length - 1]["type"] === "user") {
424
+ this.story.pop();
425
+ }
426
+ this.interruptPending = true;
427
+ dockAppendContent("\x1b[90mInterrupted agent. Press Ctrl+C again to exit, or type a new prompt.\x1b[0m\n");
428
+ }
429
+ else if (qerr?.message !== "eof") {
430
+ throw qerr;
431
+ }
432
+ }
433
+ finally {
434
+ queryStarted = false;
435
+ }
436
+ this.saveSession(engine);
437
+ };
438
+ const onInterrupt = () => engine.interrupt();
398
439
  const [input, spans] = await askBottomPrompt("You", "❯", this.promptHistory, {
399
440
  left: "\x1b[90mPress \x1b[1;36mCtrl+C\x1b[0m\x1b[90m to interrupt\x1b[0m",
400
441
  right: `\x1b[1;36m${pct}%\x1b[0m \x1b[90muntil auto-compact\x1b[0m`,
401
- });
442
+ }, this.lastPrompt, onSubmit, onInterrupt);
402
443
  if (!input.trim())
403
444
  continue;
404
445
  this.interruptPending = false;
@@ -450,11 +491,6 @@ export class CLI {
450
491
  }
451
492
  continue;
452
493
  }
453
- dockAppendContent(`\x1b[1;36m❯\x1b[0m ${userDisplayText(input, spans)}\n`);
454
- queryStarted = true;
455
- await engine.executeQuery(input, this.inputItems, this.story, spans);
456
- queryStarted = false;
457
- this.saveSession(engine);
458
494
  }
459
495
  catch (err) {
460
496
  if (err?.message === "eof") {
package/dist/config.js CHANGED
@@ -191,7 +191,7 @@ export async function loadOrPrompt() {
191
191
  break;
192
192
  }
193
193
  else {
194
- process.stdout.write(`\x1b[31mAuthentication Error:\x1b[0m ${validation.error}\n\n`);
194
+ process.stdout.write(`\x1b[31mAuthentication Error:\x1b[0m ${validation.error}\n`);
195
195
  api_key = "";
196
196
  }
197
197
  }
package/dist/engine.js CHANGED
@@ -542,7 +542,7 @@ export class InferenceEngine {
542
542
  // Print the tool label the instant the command starts, then show a
543
543
  // "╰─ Working..." dots line that swaps to the real result in place.
544
544
  dockAppendContent(`${started}\n`);
545
- dockTransientStart();
545
+ dockTransientStart("flush");
546
546
  const toolSpinner = new Spinner();
547
547
  toolSpinner.startDots("\x1b[90m╰─\x1b[0m Working");
548
548
  const rawResult = await this.runTool(c.name, c.arguments);
@@ -564,10 +564,10 @@ export class InferenceEngine {
564
564
  // produced a diff it renders directly below the action line with no
565
565
  // gap; otherwise the dock gap separates the action from what follows.
566
566
  toolSpinner.stop();
567
- dockReplaceTransient(action + "\n");
567
+ dockReplaceTransient(action);
568
568
  if (diff) {
569
- dockTransientStart();
570
- dockReplaceTransient(diff + "\n");
569
+ dockTransientStart("flush");
570
+ dockReplaceTransient(diff);
571
571
  }
572
572
  const truncatedResult = c.name === "read_file"
573
573
  ? truncateToolOutput(rawResult, max_read_file_stored_chars)
package/dist/tools.js CHANGED
@@ -31,6 +31,36 @@ function truncateDiffLine(line) {
31
31
  }
32
32
  return line;
33
33
  }
34
+ /**
35
+ * Recolor bracket syntax characters (`( ) [ ] { }`) inside an already-ANSI
36
+ * rendered string to `fg`, restoring the row's white base after each bracket.
37
+ * ANSI escape sequences are skipped so the injected color codes are never
38
+ * corrupted. Used so a deleted (red) diff row's bracket syntax matches the
39
+ * red of its line number instead of inheriting the syntax highlighter's color.
40
+ */
41
+ function recolorBrackets(text, fg, base) {
42
+ let out = "";
43
+ let i = 0;
44
+ while (i < text.length) {
45
+ if (text[i] === "\x1b") {
46
+ const m = text.slice(i).match(/^\x1b\[[0-9;]*[a-zA-Z]/);
47
+ if (m) {
48
+ out += m[0];
49
+ i += m[0].length;
50
+ continue;
51
+ }
52
+ }
53
+ const ch = text[i];
54
+ if (ch === "(" || ch === ")" || ch === "[" || ch === "]" || ch === "{" || ch === "}") {
55
+ out += fg + ch + base;
56
+ }
57
+ else {
58
+ out += ch;
59
+ }
60
+ i++;
61
+ }
62
+ return out;
63
+ }
34
64
  // Diff output is deferred (not written straight to stdout) so it doesn't
35
65
  // collide with the tool spinner that is still animating while the tool runs.
36
66
  const pendingDiffOutput = [];
@@ -148,8 +178,14 @@ function displayDiff(pathName, oldContent, newContent) {
148
178
  // the whole body.
149
179
  const raw = highlightCodeLine(v.body);
150
180
  const body = truncateStyled(raw.replace(/\x1b\[0m/g, `\x1b[0m${bg}\x1b[97m`), bodyMax);
181
+ // Deleted rows: recolor bracket syntax to the same red as the line
182
+ // number (fg) so brackets match the row's deleted color. Added rows keep
183
+ // their green highlighter as-is.
184
+ const shown = isAdd
185
+ ? body
186
+ : recolorBrackets(body, fg, "\x1b[97m");
151
187
  const fill = Math.max(0, termW - ind.length - numW - 4 - plainLen(body));
152
- pendingDiffOutput.push(`${ind}${bg}${fg}${numStr} ${sign} \x1b[0m${bg}\x1b[97m${body}${bg}\x1b[97m${" ".repeat(fill)}\x1b[0m`);
188
+ pendingDiffOutput.push(`${ind}${bg}${fg}${numStr} ${sign} \x1b[0m${bg}\x1b[97m${shown}${bg}\x1b[97m${" ".repeat(fill)}\x1b[0m`);
153
189
  }
154
190
  else {
155
191
  const raw = v.kind === "\\" ? v.body : highlightCodeLine(v.body);
package/dist/ui.js CHANGED
@@ -293,6 +293,12 @@ export function markdownToAnsi(text) {
293
293
  out.push(`\x1b[90m${"─".repeat(Math.min(width, 60))}\x1b[0m`);
294
294
  continue;
295
295
  }
296
+ // Collapse consecutive blank lines to a single empty row (never a leading one)
297
+ if (!raw.trim()) {
298
+ if (out.length && out[out.length - 1] !== "")
299
+ out.push("");
300
+ continue;
301
+ }
296
302
  let line = raw;
297
303
  // Headers (render as styled headings with the '#' markers stripped)
298
304
  const h1 = line.match(/^#\s+(.+)$/);
@@ -505,13 +511,16 @@ export function displayRows(text) {
505
511
  // one blank row is preserved between the latest content and the box.
506
512
  //
507
513
  // dockRel: how many rows the cursor sits above boxTop. 0 = parked at boxTop,
508
- // 1 = one row up (the gap row), where transient spinners render.
514
+ // 2 = transient content row (spinner/thinking line), with the blank gap row
515
+ // (boxTop-1) kept between it and the box.
516
+ // dockTransient: true while a transient spinner is parked on the content row.
509
517
  // dockGap: true when a blank row already exists above the current row (after
510
518
  // the box is torn down), so askBottomPrompt does not add a second one.
511
519
  let docked = false;
512
520
  let dockRel = 0;
513
521
  let dockBoxRows = 0;
514
522
  let dockGap = false;
523
+ let dockTransient = false;
515
524
  let dockFrameLines = [];
516
525
  export function dockIsDocked() {
517
526
  return docked;
@@ -528,12 +537,14 @@ export function dockSetInactive() {
528
537
  dockRel = 0;
529
538
  dockBoxRows = 0;
530
539
  dockGap = false;
540
+ dockTransient = false;
531
541
  dockFrameLines = [];
532
542
  }
533
543
  export function dockEnterDocked() {
534
544
  docked = true;
535
545
  dockRel = 0;
536
546
  dockGap = false;
547
+ dockTransient = false;
537
548
  }
538
549
  /** Erase the docked box and leave the cursor at boxTop with the gap above it,
539
550
  * so a pager/replay can write there and a later prompt re-docks cleanly. */
@@ -546,55 +557,48 @@ export function dockTearDown() {
546
557
  dockFrameLines = [];
547
558
  }
548
559
  }
549
- /** Park the cursor on the gap row (above the box) for a transient spinner. */
550
- export function dockTransientStart() {
560
+ /** Park a transient spinner two rows above the box (dockRel=2), keeping boxTop-1
561
+ * as the single blank gap. "flush" parks the spinner directly under the
562
+ * preceding content (label, spinner, blank, box); "blank" gives it its own
563
+ * blank row above (echo, blank, spinner, blank, box). Idempotent while parked. */
564
+ export function dockTransientStart(mode = "blank") {
551
565
  if (!docked || dockBoxRows < 1)
552
566
  return;
553
- if (dockRel === 0) {
554
- process.stdout.write("\x1b[1A\r");
555
- dockRel = 1;
556
- }
567
+ if (dockTransient)
568
+ return;
569
+ const insert = mode === "flush" ? 1 : 2;
570
+ process.stdout.write(`\x1b[${insert}L`);
571
+ if (mode === "flush")
572
+ process.stdout.write("\x1b[1A");
573
+ process.stdout.write("\r");
574
+ dockRel = 2;
575
+ dockTransient = true;
557
576
  }
577
+ /** Erase the box and any transient rows above it, leaving the cursor at boxTop
578
+ * with the gap row intact above. */
558
579
  function dockEraseBox() {
559
580
  if (!docked || dockBoxRows < 1)
560
581
  return;
561
- process.stdout.write(`\x1b[${dockBoxRows - 1 + dockRel}B`);
582
+ const rel = dockRel;
583
+ process.stdout.write(`\x1b[${dockBoxRows - 1 + rel}B`);
562
584
  for (let i = 0; i < dockBoxRows; i++) {
563
585
  process.stdout.write("\r\x1b[K");
564
586
  if (i < dockBoxRows - 1)
565
587
  process.stdout.write("\x1b[1A");
566
588
  }
567
- docked = false;
568
- dockRel = 0;
569
- dockGap = true;
570
- }
571
- function dockEraseBoxKeep() {
572
- if (!docked || dockBoxRows < 1)
573
- return;
574
- process.stdout.write(`\x1b[${dockBoxRows - 1 + dockRel}B`);
575
- for (let i = 0; i < dockBoxRows; i++) {
576
- process.stdout.write("\r\x1b[K");
577
- if (i < dockBoxRows - 1)
578
- process.stdout.write("\x1b[1A");
589
+ if (rel > 0) {
590
+ for (let i = 0; i < rel; i++)
591
+ process.stdout.write("\x1b[1A\r\x1b[K");
592
+ process.stdout.write(`\x1b[${rel}B`);
579
593
  }
580
- process.stdout.write(`\x1b[${dockRel}A`);
581
594
  docked = false;
582
595
  dockRel = 0;
583
596
  dockGap = true;
597
+ dockTransient = false;
584
598
  }
585
- /** Write exactly one blank row then the box frame below the current row, and
586
- * park the cursor at the box top. */
587
- function dockRenderBox() {
588
- if (dockBoxRows < 1)
589
- return;
590
- process.stdout.write("\n\n" + dockFrameLines.join("\n"));
591
- process.stdout.write(`\x1b[${dockBoxRows - 1}A\r`);
592
- dockRel = 0;
593
- docked = true;
594
- dockGap = false;
595
- }
596
- /** Commit content at the box top, pushing the box down, preserving exactly one
597
- * blank row between the content and the box. */
599
+ /** Commit content at the box top: insert `rows+1` lines to push the box down
600
+ * (no erase/redraw), write the body, leave exactly one blank row between the
601
+ * content and the box, and park the cursor at the new boxTop. */
598
602
  export function dockAppendContent(text) {
599
603
  if (!text)
600
604
  return;
@@ -602,14 +606,18 @@ export function dockAppendContent(text) {
602
606
  process.stdout.write(text);
603
607
  return;
604
608
  }
605
- dockEraseBox();
606
- process.stdout.write(text);
607
- const tb = displayRows(text) - displayRows(text.replace(/\n+$/, ""));
608
- if (tb > 0)
609
- process.stdout.write(`\x1b[${tb}A`);
610
- dockRenderBox();
609
+ const body = text.replace(/\n+$/, "");
610
+ if (!body)
611
+ return;
612
+ const rows = displayRows(body);
613
+ process.stdout.write(`\x1b[${rows + 1}L`);
614
+ process.stdout.write(body + "\n\r");
615
+ process.stdout.write("\x1b[1B\r");
611
616
  }
612
- /** Replace a transient spinner row (the gap row) with content, then re-dock. */
617
+ /** Replace a transient spinner row (dockRel=2, two rows above the box) with
618
+ * real (possibly multi-row) content: erase the spinner line, push the box
619
+ * down by `rows-1` if the content is taller than the transient row, write the
620
+ * content, and park the cursor at the boxTop with one blank row above it. */
613
621
  export function dockReplaceTransient(text) {
614
622
  if (!text)
615
623
  return;
@@ -617,12 +625,22 @@ export function dockReplaceTransient(text) {
617
625
  process.stdout.write(text);
618
626
  return;
619
627
  }
620
- dockEraseBoxKeep();
621
- process.stdout.write(text);
622
- const tb = displayRows(text) - displayRows(text.replace(/\n+$/, ""));
623
- if (tb > 0)
624
- process.stdout.write(`\x1b[${tb}A`);
625
- dockRenderBox();
628
+ if (!dockTransient) {
629
+ dockAppendContent(text);
630
+ return;
631
+ }
632
+ const body = text.replace(/\n+$/, "");
633
+ if (!body)
634
+ return;
635
+ const rows = displayRows(body);
636
+ const push = rows + 1 - dockRel;
637
+ process.stdout.write("\r\x1b[K");
638
+ if (push > 0)
639
+ process.stdout.write(`\x1b[${push}L`);
640
+ process.stdout.write(body);
641
+ process.stdout.write("\x1b[2B\r");
642
+ dockRel = 0;
643
+ dockTransient = false;
626
644
  }
627
645
  // ---------------------------------------------------------------------------
628
646
  // Cursor & Spinner
@@ -903,9 +921,23 @@ export function cursorLineCol(buffer, cursor) {
903
921
  function endsWithWs(s) {
904
922
  return s.length > 0 && /[ \t\n]/.test(s[s.length - 1]);
905
923
  }
924
+ /**
925
+ * Whether a spacer should be inserted between the previous segment and the
926
+ * current one. A space is added when the previous segment doesn't already end
927
+ * in whitespace — EXCEPT for non-block (inline) pastes, which keep their
928
+ * leading space but never force a trailing space after them. Collapsed pasted
929
+ * blocks always get spaced on both sides.
930
+ */
931
+ function joinNeedsSpace(prev) {
932
+ if (endsWithWs(prev[2]))
933
+ return false;
934
+ if (prev[1] === "text" && prev[3])
935
+ return false; // non-block inline paste
936
+ return true;
937
+ }
906
938
  export function splitBlocks(buffer, pasteSpans) {
907
939
  if (!buffer)
908
- return [["", "text", ""]];
940
+ return [["", "text", "", false]];
909
941
  const pts = new Set([0, buffer.length]);
910
942
  for (const [s, e] of pasteSpans) {
911
943
  pts.add(s);
@@ -922,10 +954,10 @@ export function splitBlocks(buffer, pasteSpans) {
922
954
  const text = buffer.slice(a, b);
923
955
  if (isPaste(a, b) && shouldCollapsePaste(text)) {
924
956
  const n = text.split("\n").length;
925
- segs.push(["", "collapsed", `[Pasted text, ${n} lines]`]);
957
+ segs.push(["", "collapsed", `[Pasted text, ${n} lines]`, true]);
926
958
  }
927
959
  else {
928
- segs.push(["", "text", text]);
960
+ segs.push(["", "text", text, isPaste(a, b)]);
929
961
  }
930
962
  }
931
963
  return segs;
@@ -1056,8 +1088,8 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
1056
1088
  }
1057
1089
  }
1058
1090
  for (let i = 0; i < segs.length; i++) {
1059
- const [, kind, disp] = segs[i];
1060
- if (i && !endsWithWs(segs[i - 1][2]))
1091
+ const [, kind, disp, pasted] = segs[i];
1092
+ if (i && joinNeedsSpace(segs[i - 1]))
1061
1093
  runs.push({ text: " ", style: "" });
1062
1094
  if (i === target) {
1063
1095
  if (kind === "collapsed") {
@@ -1131,7 +1163,7 @@ function isEnterKey(str, key) {
1131
1163
  seq === "\x1bOM" // application keypad
1132
1164
  );
1133
1165
  }
1134
- export function askBottomPrompt(label = "You", prefix = "❯", history = [], hints) {
1166
+ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hints, initialBuffer = "", onSubmit, onInterrupt) {
1135
1167
  return new Promise((resolve, reject) => {
1136
1168
  const isTTY = process.stdin.isTTY;
1137
1169
  if (!isTTY) {
@@ -1147,8 +1179,8 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1147
1179
  });
1148
1180
  return;
1149
1181
  }
1150
- let buffer = "";
1151
- let cursor = 0;
1182
+ let buffer = initialBuffer;
1183
+ let cursor = buffer.length;
1152
1184
  let pasteSpans = [];
1153
1185
  const hist = history.slice();
1154
1186
  let histIdx = hist.length;
@@ -1169,6 +1201,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1169
1201
  let lastCursorRow = 0;
1170
1202
  let lastTotalRows = 0;
1171
1203
  let prevFrameLines = null;
1204
+ // True while an onSubmit query is running. While it runs the box stays
1205
+ // focused and editable; Enter won't re-submit and Ctrl+C interrupts it.
1206
+ let submitting = false;
1172
1207
  const clearBox = () => {
1173
1208
  process.stdout.write(`\x1b[${lastCursorRow}A`);
1174
1209
  for (let i = 0; i < lastTotalRows; i++) {
@@ -1221,12 +1256,14 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1221
1256
  process.stdout.write("\n");
1222
1257
  }
1223
1258
  prevFrameLines = newLines;
1224
- lastCursorRow = cursorRow;
1259
+ lastCursorRow = 0;
1225
1260
  lastTotalRows = totalRows;
1226
1261
  dockSetFrame(frame, totalRows);
1227
1262
  dockEnterDocked();
1228
- const up = totalRows - 1 - cursorRow;
1229
- process.stdout.write(`\x1b[${up}A\r\x1b[${cursorCol}C`);
1263
+ // Park the cursor at the box top so concurrent dock content (AI
1264
+ // streaming, tool results) inserts above the box rather than into it.
1265
+ const up = totalRows - 1;
1266
+ process.stdout.write(`\x1b[${up}A\r`);
1230
1267
  }
1231
1268
  else {
1232
1269
  process.stdout.write(`\x1b[${lastCursorRow}A\r`);
@@ -1240,13 +1277,11 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1240
1277
  process.stdout.write("\n");
1241
1278
  }
1242
1279
  const atLine = maxLen - 1;
1243
- if (atLine > cursorRow)
1244
- process.stdout.write(`\x1b[${atLine - cursorRow}A`);
1245
- else if (atLine < cursorRow)
1246
- process.stdout.write(`\x1b[${cursorRow - atLine}B`);
1247
- process.stdout.write(`\r\x1b[${cursorCol}C`);
1280
+ // Return the cursor to the box top so concurrent dock content keeps
1281
+ // inserting above the box.
1282
+ process.stdout.write(`\x1b[${atLine}A\r`);
1248
1283
  prevFrameLines = newLines;
1249
- lastCursorRow = cursorRow;
1284
+ lastCursorRow = 0;
1250
1285
  lastTotalRows = totalRows;
1251
1286
  dockSetFrame(frame, totalRows);
1252
1287
  }
@@ -1531,6 +1566,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1531
1566
  pasteBurstTimer = null;
1532
1567
  }
1533
1568
  if (key && key.ctrl && key.name === "c") {
1569
+ // While a query is running, Ctrl+C interrupts the query instead of
1570
+ // leaving the prompt (the box stays focused for editing).
1571
+ if (submitting && onInterrupt) {
1572
+ onInterrupt();
1573
+ return;
1574
+ }
1534
1575
  settle(new Error("interrupt"), true);
1535
1576
  return;
1536
1577
  }
@@ -1571,9 +1612,31 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1571
1612
  return;
1572
1613
  }
1573
1614
  if (isEnterKey(str, key)) {
1574
- if (buffer.trim()) {
1575
- settle([buffer, pasteSpans], false);
1615
+ const trimmed = buffer.trim();
1616
+ if (!trimmed)
1617
+ return;
1618
+ // While a query is running the box stays focused and editable; Enter is
1619
+ // ignored (even for commands) so the query finishes cleanly first.
1620
+ if (submitting)
1621
+ return;
1622
+ // With an onSubmit handler, ordinary prompts are submitted in place so
1623
+ // the box stays focused and editable while the query streams above it.
1624
+ // Slash commands (and any prompt when no handler is wired) settle so the
1625
+ // caller's existing command loop handles them.
1626
+ if (onSubmit && !trimmed.startsWith("/")) {
1627
+ submitting = true;
1628
+ const submitted = buffer;
1629
+ const submittedSpans = pasteSpans.slice();
1630
+ Promise.resolve()
1631
+ .then(() => onSubmit(submitted, submittedSpans))
1632
+ .then(() => {
1633
+ submitting = false;
1634
+ }, () => {
1635
+ submitting = false;
1636
+ });
1637
+ return;
1576
1638
  }
1639
+ settle([buffer, pasteSpans], false);
1577
1640
  return;
1578
1641
  }
1579
1642
  if (key && key.name === "backspace") {
@@ -1653,7 +1716,7 @@ export function userDisplayText(payload, pasteSpans) {
1653
1716
  let out = "";
1654
1717
  for (let i = 0; i < segs.length; i++) {
1655
1718
  const [, kind, disp] = segs[i];
1656
- if (i && !endsWithWs(segs[i - 1][2]))
1719
+ if (i && joinNeedsSpace(segs[i - 1]))
1657
1720
  out += " ";
1658
1721
  out += kind === "collapsed" ? `\x1b[1;36m${disp}\x1b[0m` : disp;
1659
1722
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.87",
3
+ "version": "1.0.89",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },