@oxecli/oxe 1.0.52 → 1.0.53

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
@@ -3,7 +3,7 @@ import { createRequire } from "node:module";
3
3
  import { fileURLToPath } from "node:url";
4
4
  import { loadOrPrompt, default_reasoning_effort, default_model, max_action_chars, max_resume_history_items, runtimeOsSummary, } from "./config.js";
5
5
  import { InferenceEngine, estimateTokens } from "./engine.js";
6
- import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, toolOutputFailed, } from "./sessions.js";
6
+ import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, stripOrphanCalls, toolOutputFailed, } from "./sessions.js";
7
7
  import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, formatToolAction, truncateEllipsis, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, } from "./ui.js";
8
8
  const __dirname = path.dirname(fileURLToPath(import.meta.url));
9
9
  const require = createRequire(import.meta.url);
@@ -26,7 +26,7 @@ export class CLI {
26
26
  }
27
27
  renderHeader(model = default_model) {
28
28
  const version = packageInfo.version ?? "unknown";
29
- const cellWidth = 23;
29
+ const cellWidth = 22;
30
30
  const centeredCell = (value, render) => {
31
31
  const left = Math.max(0, Math.floor((cellWidth - value.length) / 2));
32
32
  const right = Math.max(0, cellWidth - value.length - left);
@@ -457,6 +457,7 @@ export class CLI {
457
457
  process.stdout.write(aiMarkdown("What else can I help you with?") + "\n");
458
458
  }
459
459
  engine.inQuery = false;
460
+ stripOrphanCalls(this.inputItems);
460
461
  if (this.inputItems.length &&
461
462
  this.inputItems[this.inputItems.length - 1]["role"] === "user") {
462
463
  this.inputItems.pop();
package/dist/engine.js CHANGED
@@ -307,9 +307,11 @@ export class InferenceEngine {
307
307
  }
308
308
  const usage = response?.usage;
309
309
  if (usage) {
310
- stats["input"] = (stats["input"] ?? 0) + (usage?.input_tokens ?? 0);
311
- const outTok = usage?.output_tokens ?? usage?.total_tokens ?? 0;
312
- stats["tokens"] = (stats["tokens"] ?? 0) + outTok;
310
+ const inTok = usage?.input_tokens ?? 0;
311
+ const outTok = usage?.output_tokens ?? 0;
312
+ const totalTok = usage?.total_tokens ?? inTok + outTok;
313
+ stats["input"] = (stats["input"] ?? 0) + inTok;
314
+ stats["tokens"] = (stats["tokens"] ?? 0) + totalTok;
313
315
  const reasoningTok = usage?.output_tokens_details?.reasoning_tokens ?? 0;
314
316
  stats["reasoning"] = (stats["reasoning"] ?? 0) + reasoningTok;
315
317
  }
@@ -447,7 +449,18 @@ export class InferenceEngine {
447
449
  input: 0,
448
450
  };
449
451
  const queryStart = Date.now();
450
- const footerText = () => `\x1b[2m(Thought for ${tickDuration(stats["thinking"])} · Worked for ${tickDuration((Date.now() - queryStart) / 1000)} · Used \x1b[22;37m${stats["tokens"].toLocaleString()}\x1b[2m tokens · Effort: \x1b[1;36m${this.reasoningEffort}\x1b[0m\x1b[2m)\x1b[0m`;
452
+ const footerText = () => {
453
+ const parts = [];
454
+ if (stats["thinking"] > 0) {
455
+ parts.push(`Thought for ${tickDuration(stats["thinking"])}`);
456
+ }
457
+ parts.push(`Worked for ${tickDuration((Date.now() - queryStart) / 1000)}`);
458
+ if (stats["tokens"] > 0) {
459
+ parts.push(`Used \x1b[22;37m${stats["tokens"].toLocaleString()}\x1b[2m tokens`);
460
+ }
461
+ parts.push(`Effort: \x1b[1;36m${this.reasoningEffort}\x1b[0m\x1b[2m`);
462
+ return `\x1b[2m(${parts.join(" · ")})\x1b[0m`;
463
+ };
451
464
  const summary = () => mutedMarkdown(footerText());
452
465
  const attachFooter = (items) => {
453
466
  const footer = footerText();
package/dist/tools.js CHANGED
@@ -6,6 +6,17 @@ import { max_diff_source_chars, max_diff_lines, max_diff_context_lines, max_diff
6
6
  import { toolLoadSkill } from "./skills.js";
7
7
  export { toolLoadSkill };
8
8
  // ---------------------------------------------------------------------------
9
+ // Path sanitization helper
10
+ // ---------------------------------------------------------------------------
11
+ function sanitizePath(p) {
12
+ if (!p)
13
+ return "";
14
+ return String(p).trim().replace(/^["']|["']$/g, "");
15
+ }
16
+ function escapeRegExp(string) {
17
+ return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
18
+ }
19
+ // ---------------------------------------------------------------------------
9
20
  // Natural sort + diff helpers
10
21
  // ---------------------------------------------------------------------------
11
22
  export function naturalSortKey(s) {
@@ -215,7 +226,9 @@ function streamReadRange(p, start, end) {
215
226
  return [i, numbered];
216
227
  }
217
228
  export function toolReadFile(pathName, startLine = 1, endLine) {
218
- const p = pathName;
229
+ const p = sanitizePath(pathName);
230
+ if (!p)
231
+ return "Error: path must not be empty";
219
232
  if (!fs.existsSync(p))
220
233
  return `Error: file not found: ${pathName}`;
221
234
  if (!fs.statSync(p).isFile())
@@ -302,7 +315,9 @@ export function toolReadFile(pathName, startLine = 1, endLine) {
302
315
  // write_file
303
316
  // ---------------------------------------------------------------------------
304
317
  export function toolWriteFile(pathName, content) {
305
- const p = pathName;
318
+ const p = sanitizePath(pathName);
319
+ if (!p)
320
+ return "Error: path must not be empty";
306
321
  if (fs.existsSync(p) && fs.statSync(p).isDirectory()) {
307
322
  return `Error: ${pathName} is a directory`;
308
323
  }
@@ -378,7 +393,9 @@ function findWsBlocks(text, needle) {
378
393
  return blocks;
379
394
  }
380
395
  export function toolEditFile(pathName, oldString, newString, replaceAll = false) {
381
- const p = pathName;
396
+ const p = sanitizePath(pathName);
397
+ if (!p)
398
+ return "Error: path must not be empty";
382
399
  if (!fs.existsSync(p))
383
400
  return `Error: file not found: ${pathName}`;
384
401
  if (!fs.statSync(p).isFile())
@@ -467,20 +484,24 @@ export function toolBash(command, timeout = 60, cwd) {
467
484
  if (Number.isNaN(t))
468
485
  t = 60;
469
486
  t = t > 0 ? Math.min(t, max_bash_timeout_seconds) : 60;
487
+ const workingDir = cwd ? sanitizePath(cwd) : undefined;
488
+ if (workingDir && !fs.existsSync(workingDir)) {
489
+ return Promise.resolve(`Error: working directory not found: ${cwd}`);
490
+ }
470
491
  return new Promise((resolve) => {
471
492
  let child;
472
493
  try {
473
494
  if (process.platform === "win32") {
474
495
  child = spawn(command, {
475
496
  shell: true,
476
- cwd: cwd || undefined,
477
- windowsHide: false,
497
+ cwd: workingDir,
498
+ windowsHide: true,
478
499
  });
479
500
  }
480
501
  else {
481
502
  child = spawn(command, {
482
503
  shell: true,
483
- cwd: cwd || undefined,
504
+ cwd: workingDir,
484
505
  detached: true,
485
506
  });
486
507
  }
@@ -630,7 +651,7 @@ function rglobPruned(base, pattern) {
630
651
  return out;
631
652
  }
632
653
  export function toolGlob(pattern, pathName = ".", limit = 200) {
633
- const base = pathName;
654
+ const base = sanitizePath(pathName) || ".";
634
655
  if (!fs.existsSync(base))
635
656
  return `Error: path not found: ${pathName}`;
636
657
  const lim = typeof limit === "number" ? limit : parseInt(String(limit), 10) || 200;
@@ -652,15 +673,15 @@ export function toolGlob(pattern, pathName = ".", limit = 200) {
652
673
  return result;
653
674
  }
654
675
  export function toolGrep(pattern, pathName = ".", glob = "*", limit = 200) {
655
- const base = pathName;
676
+ const base = sanitizePath(pathName) || ".";
656
677
  if (!fs.existsSync(base))
657
678
  return `Error: path not found: ${pathName}`;
658
679
  let regex;
659
680
  try {
660
681
  regex = new RegExp(pattern, "i");
661
682
  }
662
- catch (err) {
663
- return `Error: invalid regex pattern: ${err}`;
683
+ catch {
684
+ regex = new RegExp(escapeRegExp(pattern), "i");
664
685
  }
665
686
  glob = glob || "*";
666
687
  const lim = typeof limit === "number" ? limit : parseInt(String(limit), 10) || 200;
package/dist/ui.js CHANGED
@@ -9,8 +9,6 @@ export function enableRawStdin() {
9
9
  readline.emitKeypressEvents(process.stdin);
10
10
  if (process.stdin.isTTY) {
11
11
  process.stdin.setRawMode(true);
12
- // Enable the kitty keyboard protocol (CSI u) so modified Enter (Shift /
13
- // Ctrl+Enter) arrives as \x1b[13;<mod>u rather than a plain \r.
14
12
  process.stdout.write("\x1b[>1u");
15
13
  }
16
14
  process.stdin.resume();
@@ -136,11 +134,9 @@ const KEYWORDS = new Set([
136
134
  function highlightCodeLine(line, _lang = "") {
137
135
  if (!line)
138
136
  return "";
139
- // Comments
140
137
  if (/^\s*(\/\/|#|\/\*)/.test(line)) {
141
138
  return `\x1b[2;37m${line}\x1b[0m`;
142
139
  }
143
- // Tokenize words, strings, numbers
144
140
  return line.replace(/("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`)|(\b\d+\.?\d*\b)|(\b[a-zA-Z_$][\w$]*\b)|(\/\/[^\n]*|#[^\n]*)/g, (m, str, num, ident, comment) => {
145
141
  if (str)
146
142
  return `\x1b[32m${str}\x1b[0m`;
@@ -153,6 +149,60 @@ function highlightCodeLine(line, _lang = "") {
153
149
  return m;
154
150
  });
155
151
  }
152
+ function formatInlineMarkdown(text) {
153
+ let line = text;
154
+ line = line.replace(/\*\*\*([^*]+)\*\*\*/g, "\x1b[1;3m$1\x1b[0m");
155
+ line = line.replace(/\*\*([^*]+)\*\*/g, "\x1b[1m$1\x1b[0m");
156
+ line = line.replace(/(^|[^\w*])\*([^*\n]+)\*(?=$|[^\w*])/g, "$1\x1b[3m$2\x1b[0m");
157
+ line = line.replace(/`([^`]+)`/g, "\x1b[1;36m`$1`\x1b[0m");
158
+ line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1b[4;36m$1\x1b[0m \x1b[2m($2)\x1b[0m");
159
+ return line;
160
+ }
161
+ // ---------------------------------------------------------------------------
162
+ // Markdown Table Formatter
163
+ // ---------------------------------------------------------------------------
164
+ function formatMarkdownTable(tableLines) {
165
+ if (tableLines.length < 2)
166
+ return tableLines;
167
+ const parseRow = (row) => row
168
+ .replace(/^\||\|$/g, "")
169
+ .split("|")
170
+ .map((c) => c.trim());
171
+ const header = parseRow(tableLines[0]);
172
+ const rows = tableLines.slice(2).map(parseRow);
173
+ const allRows = [header, ...rows];
174
+ const colWidths = [];
175
+ for (let c = 0; c < header.length; c++) {
176
+ let mw = 0;
177
+ for (const r of allRows) {
178
+ if (r[c])
179
+ mw = Math.max(mw, plainLen(formatInlineMarkdown(r[c])));
180
+ }
181
+ colWidths.push(Math.max(mw, 3));
182
+ }
183
+ const out = [];
184
+ const topBorder = `\x1b[90m┌${colWidths.map((w) => "─".repeat(w + 2)).join("┬")}┐\x1b[0m`;
185
+ const midBorder = `\x1b[90m├${colWidths.map((w) => "─".repeat(w + 2)).join("┼")}┤\x1b[0m`;
186
+ const botBorder = `\x1b[90m└${colWidths.map((w) => "─".repeat(w + 2)).join("┴")}┘\x1b[0m`;
187
+ out.push(topBorder);
188
+ const headCells = header.map((h, i) => {
189
+ const formatted = `\x1b[1;37m${formatInlineMarkdown(h)}\x1b[0m`;
190
+ const pad = colWidths[i] - plainLen(formatted);
191
+ return ` ${formatted}${" ".repeat(Math.max(pad, 0))} `;
192
+ });
193
+ out.push(`\x1b[90m│\x1b[0m${headCells.join("\x1b[90m│\x1b[0m")}\x1b[90m│\x1b[0m`);
194
+ out.push(midBorder);
195
+ for (const r of rows) {
196
+ const rowCells = header.map((_, i) => {
197
+ const cell = r[i] ? formatInlineMarkdown(r[i]) : "";
198
+ const pad = colWidths[i] - plainLen(cell);
199
+ return ` ${cell}${" ".repeat(Math.max(pad, 0))} `;
200
+ });
201
+ out.push(`\x1b[90m│\x1b[0m${rowCells.join("\x1b[90m│\x1b[0m")}\x1b[90m│\x1b[0m`);
202
+ }
203
+ out.push(botBorder);
204
+ return out;
205
+ }
156
206
  // ---------------------------------------------------------------------------
157
207
  // Rich Markdown -> ANSI Terminal Renderer
158
208
  // ---------------------------------------------------------------------------
@@ -163,6 +213,7 @@ export function markdownToAnsi(text) {
163
213
  let inCode = false;
164
214
  let codeLang = "";
165
215
  let codeBuffer = [];
216
+ let tableBuffer = [];
166
217
  const flushCodeBlock = () => {
167
218
  if (!codeBuffer.length && !codeLang)
168
219
  return;
@@ -177,10 +228,18 @@ export function markdownToAnsi(text) {
177
228
  codeBuffer = [];
178
229
  codeLang = "";
179
230
  };
231
+ const flushTable = () => {
232
+ if (!tableBuffer.length)
233
+ return;
234
+ const formatted = formatMarkdownTable(tableBuffer);
235
+ out.push(...formatted);
236
+ tableBuffer = [];
237
+ };
180
238
  for (let i = 0; i < rawLines.length; i++) {
181
239
  const raw = rawLines[i];
182
240
  const fenceMatch = raw.match(/^(`{3,})(.*)$/);
183
241
  if (fenceMatch) {
242
+ flushTable();
184
243
  if (!inCode) {
185
244
  inCode = true;
186
245
  codeLang = fenceMatch[2].trim().toLowerCase();
@@ -197,44 +256,54 @@ export function markdownToAnsi(text) {
197
256
  codeBuffer.push(raw);
198
257
  continue;
199
258
  }
259
+ // Markdown Table rows
260
+ if (/^\s*\|.*\|\s*$/.test(raw)) {
261
+ tableBuffer.push(raw.trim());
262
+ continue;
263
+ }
264
+ else if (tableBuffer.length) {
265
+ flushTable();
266
+ }
267
+ // Horizontal rules
268
+ if (/^(\*{3,}|-{3,}|_{3,})$/.test(raw.trim())) {
269
+ out.push(`\x1b[90m${"─".repeat(Math.min(width, 60))}\x1b[0m`);
270
+ continue;
271
+ }
200
272
  let line = raw;
201
273
  // Headers
202
274
  const h1 = line.match(/^#\s+(.+)$/);
203
275
  if (h1) {
204
- out.push(`\x1b[1;36m# ${h1[1]}\x1b[0m`);
276
+ out.push(`\x1b[1;36m# ${formatInlineMarkdown(h1[1])}\x1b[0m`);
205
277
  continue;
206
278
  }
207
279
  const h2 = line.match(/^##\s+(.+)$/);
208
280
  if (h2) {
209
- out.push(`\x1b[1;34m## ${h2[1]}\x1b[0m`);
281
+ out.push(`\x1b[1;34m## ${formatInlineMarkdown(h2[1])}\x1b[0m`);
210
282
  continue;
211
283
  }
212
284
  const h3 = line.match(/^###\s+(.+)$/);
213
285
  if (h3) {
214
- out.push(`\x1b[1;37m### ${h3[1]}\x1b[0m`);
286
+ out.push(`\x1b[1;37m### ${formatInlineMarkdown(h3[1])}\x1b[0m`);
215
287
  continue;
216
288
  }
217
289
  // Blockquote
218
290
  const bq = line.match(/^>\s*(.+)$/);
219
291
  if (bq) {
220
- out.push(`\x1b[90m│\x1b[0m \x1b[3m${bq[1]}\x1b[0m`);
292
+ out.push(`\x1b[90m│\x1b[0m \x1b[3m${formatInlineMarkdown(bq[1])}\x1b[0m`);
221
293
  continue;
222
294
  }
223
295
  // Unordered List item
224
- line = line.replace(/^(\s*)[-*+]\s+(.+)$/, "$1\x1b[36m•\x1b[0m $2");
296
+ line = line.replace(/^(\s*)[-*+]\s+(.+)$/, (_, indent, item) => {
297
+ return `${indent}\x1b[36m•\x1b[0m ${formatInlineMarkdown(item)}`;
298
+ });
225
299
  // Ordered List item
226
- line = line.replace(/^(\s*)(\d+)\.\s+(.+)$/, "$1\x1b[36m$2.\x1b[0m $3");
227
- // Inline formatting:
228
- // Bold + Italic (***text*** or ___text___)
229
- line = line.replace(/\*\*\*([^*]+)\*\*\*/g, "\x1b[1;3m$1\x1b[0m");
230
- // Bold (**text** or __text__)
231
- line = line.replace(/\*\*([^*]+)\*\*/g, "\x1b[1m$1\x1b[0m");
232
- // Italic (*text* or _text_)
233
- line = line.replace(/(^|[^\w*])\*([^*\n]+)\*(?=$|[^\w*])/g, "$1\x1b[3m$2\x1b[0m");
234
- // Inline code (`code`)
235
- line = line.replace(/`([^`]+)`/g, "\x1b[1;36m`$1`\x1b[0m");
236
- // Links [text](url) -> text (url)
237
- line = line.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "\x1b[4;36m$1\x1b[0m \x1b[2m($2)\x1b[0m");
300
+ line = line.replace(/^(\s*)(\d+)\.\s+(.+)$/, (_, indent, num, item) => {
301
+ return `${indent}\x1b[36m${num}.\x1b[0m ${formatInlineMarkdown(item)}`;
302
+ });
303
+ // Inline formatting if not a list
304
+ if (!line.includes("\x1b[36m•\x1b[0m") && !line.includes(".\x1b[0m ")) {
305
+ line = formatInlineMarkdown(line);
306
+ }
238
307
  // Word wrap paragraph line if too long
239
308
  if (plainLen(line) > width && !line.startsWith(" ")) {
240
309
  const words = line.split(" ");
@@ -256,9 +325,9 @@ export function markdownToAnsi(text) {
256
325
  out.push(line);
257
326
  }
258
327
  }
259
- if (inCode) {
328
+ flushTable();
329
+ if (inCode)
260
330
  flushCodeBlock();
261
- }
262
331
  return out.join("\n");
263
332
  }
264
333
  export function aiMarkdown(text) {
@@ -286,11 +355,13 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
286
355
  if (subtitle)
287
356
  maxLineW = Math.max(maxLineW, plainLen(subtitle) + 4);
288
357
  const termW = terminalWidth();
289
- const boxW = expand ? Math.max(termW - 4, 20) : Math.min(maxLineW + 4, termW - 4);
290
- const innerW = boxW - 4;
358
+ const boxW = expand
359
+ ? Math.max(termW - 4, 20)
360
+ : Math.min(Math.max(maxLineW + 4, 20), termW - 4);
361
+ const innerW = Math.max(boxW - 4, 1);
291
362
  const embed = (text, align) => {
292
363
  if (!text)
293
- return "─".repeat(boxW - 2);
364
+ return "─".repeat(Math.max(boxW - 2, 0));
294
365
  const inner = ` ${text} `;
295
366
  const fill = Math.max(boxW - 2 - plainLen(inner), 0);
296
367
  if (align === "left") {
@@ -306,8 +377,15 @@ function panelString(content, title = "", subtitle = "", expand = true, borderSt
306
377
  for (let i = 0; i < styledLines.length; i++) {
307
378
  const line = styledLines[i];
308
379
  const plain = plainLines[i] ?? "";
309
- const pad = Math.max(innerW - plain.length, 0);
310
- out.push(`\x1b[${borderStyle}m│\x1b[0m ${line}${" ".repeat(pad)} \x1b[${borderStyle}m│\x1b[0m`);
380
+ if (plain.length > innerW) {
381
+ // Truncate cleanly if single line overflows max box width
382
+ const truncated = line.slice(0, innerW);
383
+ out.push(`\x1b[${borderStyle}m│\x1b[0m ${truncated} \x1b[${borderStyle}m│\x1b[0m`);
384
+ }
385
+ else {
386
+ const pad = Math.max(innerW - plain.length, 0);
387
+ out.push(`\x1b[${borderStyle}m│\x1b[0m ${line}${" ".repeat(pad)} \x1b[${borderStyle}m│\x1b[0m`);
388
+ }
311
389
  }
312
390
  const bottom = `╰${embed(subtitle, "center")}╯`;
313
391
  out.push(`\x1b[${borderStyle}m${bottom}\x1b[0m`);
@@ -351,19 +429,12 @@ export function renderTableString(rows, opts = {}) {
351
429
  // Durations & Text Formatting
352
430
  // ---------------------------------------------------------------------------
353
431
  export function formatDuration(seconds) {
354
- const total = Math.max(0, Math.round(seconds));
355
- const hours = Math.floor(total / 3600);
356
- const rem = total % 3600;
357
- const minutes = Math.floor(rem / 60);
358
- const secs = rem % 60;
359
- const parts = [];
360
- if (hours)
361
- parts.push(`${hours}h`);
362
- if (minutes)
363
- parts.push(`${minutes}min`);
364
- if (secs || !parts.length)
365
- parts.push(`${secs}s`);
366
- return parts.join(" ");
432
+ const total = Math.max(0, Math.round(seconds * 10) / 10);
433
+ if (total < 60)
434
+ return `${total}s`;
435
+ const mins = Math.floor(total / 60);
436
+ const secs = Math.round(total % 60);
437
+ return `${mins}m ${secs}s`;
367
438
  }
368
439
  export function tickDuration(seconds) {
369
440
  return `\x1b[22;37m${formatDuration(seconds)}\x1b[2m`;
@@ -657,7 +728,6 @@ function wrapRuns(runs, width) {
657
728
  export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor) {
658
729
  const termW = terminalWidth();
659
730
  const boxW = Math.max(termW - 4, 16);
660
- // Content inside border: `│ ` on left (2 chars) and ` │` on right (2 chars)
661
731
  const innerW = boxW - 4;
662
732
  const runs = [];
663
733
  runs.push({ text: prefix, style: "\x1b[1;36m" });
@@ -743,8 +813,6 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
743
813
  }
744
814
  }
745
815
  const { rows, caretRow, caretCol } = wrapRuns(runs, innerW);
746
- // Math for exact box alignment:
747
- // Top: `╭─ ` (3 chars) + `label` (L chars) + ` ` (1 char) + topPad + `╮` (1 char) = boxW
748
816
  const labelLen = plainLen(label);
749
817
  const topPad = Math.max(0, boxW - labelLen - 5);
750
818
  const top = `\x1b[90m╭─ \x1b[1m\x1b[36m${label}\x1b[0m\x1b[90m ${"─".repeat(topPad)}╮\x1b[0m`;
@@ -754,7 +822,6 @@ export function renderBufferWithCursor(buffer, pasteSpans, prefix, label, cursor
754
822
  const pad = Math.max(0, innerW - plain.length);
755
823
  body.push(`\x1b[90m│\x1b[0m ${l}${" ".repeat(pad)} \x1b[90m│\x1b[0m`);
756
824
  }
757
- // Bottom: `╰` (1 char) + `─` * (boxW - 2) + `╯` (1 char) = boxW
758
825
  const bottom = `\x1b[90m╰${"─".repeat(Math.max(0, boxW - 2))}╯\x1b[0m`;
759
826
  const frame = [top, ...body, bottom].join("\n");
760
827
  const cursorRow = (caretRow === -1 ? 0 : caretRow) + 1;
@@ -932,6 +999,36 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
932
999
  }
933
1000
  pasteSpans = adjusted.filter(([s, e]) => s < e);
934
1001
  };
1002
+ const deleteChar = () => {
1003
+ if (cursor >= buffer.length)
1004
+ return;
1005
+ if (histIdx !== hist.length) {
1006
+ draft = { buffer, cursor, spans: pasteSpans.slice() };
1007
+ histIdx = hist.length;
1008
+ }
1009
+ const containing = pasteSpans.find(([s, e]) => s <= cursor && cursor < e);
1010
+ if (containing) {
1011
+ const [start, end] = containing;
1012
+ buffer = buffer.slice(0, start) + buffer.slice(end);
1013
+ const delta = start - end;
1014
+ pasteSpans = pasteSpans
1015
+ .filter(([s, e]) => s !== start || e !== end)
1016
+ .map(([s, e]) => (s >= end ? [s + delta, e + delta] : [s, e]));
1017
+ return;
1018
+ }
1019
+ buffer = buffer.slice(0, cursor) + buffer.slice(cursor + 1);
1020
+ };
1021
+ const deleteWordBefore = () => {
1022
+ if (cursor <= 0)
1023
+ return;
1024
+ const before = buffer.slice(0, cursor);
1025
+ const trimmed = before.replace(/\s+$/, "");
1026
+ const lastSpace = Math.max(trimmed.lastIndexOf(" "), trimmed.lastIndexOf("\t"), trimmed.lastIndexOf("\n"));
1027
+ const targetPos = lastSpace === -1 ? 0 : lastSpace + 1;
1028
+ buffer = buffer.slice(0, targetPos) + buffer.slice(cursor);
1029
+ cursor = targetPos;
1030
+ pasteSpans = [];
1031
+ };
935
1032
  const moveLeft = () => {
936
1033
  if (cursor <= 0)
937
1034
  return;
@@ -1138,6 +1235,33 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
1138
1235
  settle(new Error("eof"), true);
1139
1236
  return;
1140
1237
  }
1238
+ if (key && key.ctrl && key.name === "a") {
1239
+ cursor = 0;
1240
+ repaint();
1241
+ return;
1242
+ }
1243
+ if (key && key.ctrl && key.name === "e") {
1244
+ cursor = buffer.length;
1245
+ repaint();
1246
+ return;
1247
+ }
1248
+ if (key && key.ctrl && key.name === "u") {
1249
+ buffer = "";
1250
+ cursor = 0;
1251
+ pasteSpans = [];
1252
+ repaint();
1253
+ return;
1254
+ }
1255
+ if (key && key.ctrl && key.name === "k") {
1256
+ buffer = buffer.slice(0, cursor);
1257
+ repaint();
1258
+ return;
1259
+ }
1260
+ if (key && key.ctrl && key.name === "w") {
1261
+ deleteWordBefore();
1262
+ repaint();
1263
+ return;
1264
+ }
1141
1265
  if (isNewlineKey(key) ||
1142
1266
  isModifiedEnterInput(str, key) ||
1143
1267
  (pasteBurst && isSubmitEnterKey(key))) {
@@ -1156,6 +1280,21 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = []) {
1156
1280
  repaint();
1157
1281
  return;
1158
1282
  }
1283
+ if (key && (key.name === "delete" || key.sequence === "\x1b[3~")) {
1284
+ deleteChar();
1285
+ repaint();
1286
+ return;
1287
+ }
1288
+ if (key && (key.name === "home" || key.sequence === "\x1b[H" || key.sequence === "\x1b[1~")) {
1289
+ cursor = 0;
1290
+ repaint();
1291
+ return;
1292
+ }
1293
+ if (key && (key.name === "end" || key.sequence === "\x1b[F" || key.sequence === "\x1b[4~")) {
1294
+ cursor = buffer.length;
1295
+ repaint();
1296
+ return;
1297
+ }
1159
1298
  if (key && key.name === "left") {
1160
1299
  moveLeft();
1161
1300
  repaint();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.52",
3
+ "version": "1.0.53",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },