@oxecli/oxe 1.0.74 → 1.0.76

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
@@ -4,7 +4,7 @@ import { fileURLToPath } from "node:url";
4
4
  import { loadOrPrompt, default_reasoning_effort, max_action_chars, max_resume_history_items, runtimeOsSummary, } from "./config.js";
5
5
  import { InferenceEngine, estimateTokens } from "./engine.js";
6
6
  import { saveSession, loadSession, listSessions, nextSessionId, conversationLabel, COMMAND_HELP, stripOrphanCalls, toolOutputFailed, } from "./sessions.js";
7
- import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, formatToolAction, truncateEllipsis, renderPanel, renderTableString, enableRawStdin, disableRawStdin, waitRawKey, } from "./ui.js";
7
+ import { clearScreen, enableAnsi, askBottomPrompt, userDisplayText, aiMarkdown, mutedMarkdown, messageText, toolCallLabel, formatToolResult, truncateEllipsis, stripAnsi, 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);
10
10
  const packageInfo = require("../package.json");
@@ -46,9 +46,15 @@ export class CLI {
46
46
  });
47
47
  }
48
48
  printToolEntry(started, text, status) {
49
+ const label = started && String(started).trim();
50
+ const body = String(text).trim();
51
+ if (label && stripAnsi(body).startsWith("⎿")) {
52
+ process.stdout.write(`${label}\n${body}\n`);
53
+ return;
54
+ }
49
55
  const icon = status === "failed" ? "\x1b[31m✗\x1b[0m" : "\x1b[32m✓\x1b[0m";
50
56
  const style = status === "failed" ? "\x1b[31m" : "\x1b[32m";
51
- process.stdout.write(`${icon} ${style}${truncateEllipsis(String(text), max_action_chars, "text")}\x1b[0m\n`);
57
+ process.stdout.write(`${icon} ${style}${truncateEllipsis(body, max_action_chars, "text")}\x1b[0m\n`);
52
58
  }
53
59
  printAssistantBlock(text) {
54
60
  const rendered = aiMarkdown(text);
@@ -153,8 +159,8 @@ export class CLI {
153
159
  const argumentsJson = it["arguments"] ?? "";
154
160
  renders.push({
155
161
  kind: "tool",
156
- started: formatToolAction(name, argumentsJson, "started"),
157
- text: formatToolAction(name, argumentsJson, failed ? "failed" : "ok"),
162
+ started: toolCallLabel(name, argumentsJson),
163
+ text: formatToolResult(rawResult, failed),
158
164
  status: failed ? "failed" : "ok",
159
165
  });
160
166
  }
package/dist/engine.js CHANGED
@@ -2,7 +2,7 @@ import OpenAI from "openai";
2
2
  import { max_output_tokens, max_empty_retries, max_agent_steps, max_context_tokens, context_overhead_margin, compact_keep_recent_turns, max_summary_source_chars, max_read_file_stored_chars, } from "./config.js";
3
3
  import { getSystemPrompt } from "./system.js";
4
4
  import { buildTools, truncateToolOutput, toolReadFile, toolWriteFile, toolEditFile, toolBash, toolGlob, toolGrep, toolLoadSkill, } from "./tools.js";
5
- import { mutedMarkdown, aiMarkdown, tickDuration, displayRows, safeCommitPoint, printAiChunk, formatToolAction, renderPanel, Spinner, hideCursor, } from "./ui.js";
5
+ import { mutedMarkdown, aiMarkdown, tickDuration, displayRows, safeCommitPoint, printAiChunk, toolCallLabel, formatToolResult, renderPanel, Spinner, hideCursor, } from "./ui.js";
6
6
  import { reportUsage } from "./api.js";
7
7
  import { stripOrphanCalls, persistCompactionSummary, toolOutputFailed, } from "./sessions.js";
8
8
  import { flushPendingDiffOutput } from "./tools.js";
@@ -576,7 +576,7 @@ export class InferenceEngine {
576
576
  }
577
577
  for (const c of calls) {
578
578
  this.workActive = false;
579
- const started = formatToolAction(c.name, c.arguments, "started");
579
+ const started = toolCallLabel(c.name, c.arguments);
580
580
  const toolSpinner = new Spinner();
581
581
  toolSpinner.start(started);
582
582
  const rawResult = await this.runTool(c.name, c.arguments);
@@ -584,16 +584,14 @@ export class InferenceEngine {
584
584
  if (this.interrupted)
585
585
  throw new Error("interrupt");
586
586
  const failed = toolOutputFailed(c.name, rawResult);
587
- const action = formatToolAction(c.name, c.arguments, failed ? "failed" : "ok");
587
+ const action = formatToolResult(rawResult, failed);
588
588
  story.push({
589
589
  type: "tool",
590
590
  started,
591
591
  text: action,
592
592
  status: failed ? "failed" : "ok",
593
593
  });
594
- const icon = failed ? "\x1b[31m✗\x1b[0m" : "\x1b[32m✓\x1b[0m";
595
- const actionStyle = failed ? "\x1b[31m" : "\x1b[32m";
596
- process.stdout.write(`${icon} ${actionStyle}${action}\x1b[0m\n\n`);
594
+ process.stdout.write(`${started}\n${action}\n\n`);
597
595
  // Any diff the tool produced is flushed only now, after the spinner
598
596
  // has stopped, so the box renders cleanly below the action line.
599
597
  flushPendingDiffOutput();
package/dist/tools.js CHANGED
@@ -44,7 +44,7 @@ function displayDiff(pathName, oldContent, newContent) {
44
44
  if (oldContent.length + newContent.length > max_diff_source_chars) {
45
45
  pendingDiffOutput.push(`\x1b[90m${pathName}: ${oldContent.length.toLocaleString()} chars -> ${newContent.length.toLocaleString()} chars ` +
46
46
  `(diff hidden: exceeds ${max_diff_source_chars.toLocaleString()} char limit)\x1b[0m`);
47
- return;
47
+ return null;
48
48
  }
49
49
  const patch = structuredPatch(pathName, pathName, oldContent, newContent, "", "", { context: max_diff_context_lines });
50
50
  const contentLines = [];
@@ -74,7 +74,7 @@ function displayDiff(pathName, oldContent, newContent) {
74
74
  const added = contentLines.filter((c) => c.kind === "+").length;
75
75
  const removed = contentLines.filter((c) => c.kind === "-").length;
76
76
  if (!added && !removed)
77
- return;
77
+ return null;
78
78
  const shown = [];
79
79
  let run = [];
80
80
  let lastKind = null;
@@ -121,51 +121,30 @@ function displayDiff(pathName, oldContent, newContent) {
121
121
  if (v.newNum)
122
122
  maxNum = Math.max(maxNum, v.newNum);
123
123
  }
124
- const numW = Math.max(String(maxNum).length, 2);
125
- let contentW = 0;
126
- for (const v of visible) {
127
- const len = v.kind === "~" ? plainLen(v.body) : numW + 1 + 2 + plainLen(v.body);
128
- contentW = Math.max(contentW, len);
129
- }
130
- const headerPlain = `${pathName} +${added} -${removed}`;
131
- const boxW = Math.min(Math.max(contentW + 6, plainLen(headerPlain) + 6), Math.max(termW - 2, 20));
132
- const innerW = Math.max(boxW - 4, 1);
133
- const maxPath = Math.max(10, boxW - 2 - plainLen(` +${added} -${removed}`) - 3);
134
- let pathDisp = pathName;
135
- if (plainLen(pathDisp) > maxPath)
136
- pathDisp = truncateStyled(pathDisp, Math.max(maxPath - 1, 1)) + "…";
137
- const header = `\x1b[1m${pathDisp}\x1b[0m \x1b[32m+${added}\x1b[0m \x1b[31m-${removed}\x1b[0m`;
138
- const headerStr = `─ ${header} `;
139
- const top = `╭${headerStr}${"─".repeat(Math.max(0, boxW - 2 - plainLen(headerStr)))}╮`;
140
- pendingDiffOutput.push(`\x1b[90m${top}\x1b[0m`);
141
- const bodyMax = Math.max(innerW - numW - 3, 1);
124
+ const numW = String(maxNum).length;
125
+ const indent = " ";
126
+ const bodyMax = Math.max(termW - plainLen(indent) - numW - 4, 20);
142
127
  const gap = " ".repeat(numW);
143
128
  for (const v of visible) {
144
129
  if (v.kind === "~") {
145
- const body = truncateStyled(v.body, innerW - 2);
146
- const text = `\x1b[90m${gap} ${body}\x1b[0m`;
147
- const fill = Math.max(0, innerW - plainLen(text));
148
- pendingDiffOutput.push(`\x1b[90m│\x1b[0m ${text}${" ".repeat(fill)} \x1b[90m│\x1b[0m`);
130
+ const body = truncateStyled(v.body, bodyMax);
131
+ pendingDiffOutput.push(`\x1b[90m${indent}${gap} ${body}\x1b[0m`);
149
132
  continue;
150
133
  }
151
134
  const num = v.kind === "+" ? v.newNum : v.oldNum;
152
135
  const numStr = num ? String(num).padStart(numW, " ") : gap;
153
136
  if (v.kind === "+" || v.kind === "-") {
154
137
  const color = v.kind === "+" ? "\x1b[32m" : "\x1b[31m";
138
+ const sign = v.kind === "+" ? "+" : "-";
155
139
  const body = truncateStyled(v.body, bodyMax);
156
- const text = `${numStr} ${v.kind === "+" ? "+ " : "- "}${body}`;
157
- const fill = Math.max(0, innerW - plainLen(text));
158
- // Whole line is painted green/red, filling to the right edge.
159
- pendingDiffOutput.push(`\x1b[90m│\x1b[0m ${color}${text}${" ".repeat(fill)}\x1b[0m \x1b[90m│\x1b[0m`);
140
+ pendingDiffOutput.push(`${indent}${color}${numStr} ${sign} ${body}\x1b[0m`);
160
141
  }
161
142
  else {
162
143
  const body = truncateStyled(v.kind === "\\" ? v.body : highlightCodeLine(v.body), bodyMax);
163
- const text = `\x1b[90m${numStr} ${body}\x1b[0m`;
164
- const fill = Math.max(0, innerW - plainLen(text));
165
- pendingDiffOutput.push(`\x1b[90m│\x1b[0m ${text}${" ".repeat(fill)} \x1b[90m│\x1b[0m`);
144
+ pendingDiffOutput.push(`\x1b[90m${indent}${numStr} ${body}\x1b[0m`);
166
145
  }
167
146
  }
168
- pendingDiffOutput.push(`\x1b[90m╰${"─".repeat(boxW - 2)}╯\x1b[0m`);
147
+ return { added, removed };
169
148
  }
170
149
  export function truncateToolOutput(output, maxChars = max_output_chars) {
171
150
  if (output.length > maxChars) {
@@ -491,7 +470,6 @@ export function toolEditFile(pathName, oldString, newString, replaceAll = false)
491
470
  const text = data.toString("utf-8");
492
471
  const newline = detectNewline(text);
493
472
  let match;
494
- let normalized = false;
495
473
  if (text.includes(oldString)) {
496
474
  match = oldString;
497
475
  }
@@ -499,7 +477,6 @@ export function toolEditFile(pathName, oldString, newString, replaceAll = false)
499
477
  const norm = withNewlines(oldString, newline);
500
478
  if (text.includes(norm)) {
501
479
  match = norm;
502
- normalized = true;
503
480
  }
504
481
  else {
505
482
  const blocks = findWsBlocks(text, oldString);
@@ -508,7 +485,6 @@ export function toolEditFile(pathName, oldString, newString, replaceAll = false)
508
485
  "(line endings and per-line leading/trailing whitespace are normalized for matching)");
509
486
  }
510
487
  match = blocks[0];
511
- normalized = true;
512
488
  }
513
489
  }
514
490
  const count = text.split(match).length - 1;
@@ -519,15 +495,17 @@ export function toolEditFile(pathName, oldString, newString, replaceAll = false)
519
495
  const newContent = replaceAll
520
496
  ? text.split(match).join(insert)
521
497
  : text.replace(match, insert);
522
- displayDiff(pathName, text, newContent);
498
+ const summary = displayDiff(pathName, text, newContent);
523
499
  try {
524
500
  fs.writeFileSync(p, newContent, "utf-8");
525
501
  }
526
502
  catch (err) {
527
503
  return `Error: ${err}`;
528
504
  }
529
- return (`Edited ${pathName} (${count} occurrence${count !== 1 ? "s" : ""} replaced` +
530
- `${normalized ? "; line endings and per-line whitespace normalized" : ""})`);
505
+ if (!summary)
506
+ return `Edited ${pathName}`;
507
+ return (`Added ${summary.added} line${summary.added !== 1 ? "s" : ""}, ` +
508
+ `removed ${summary.removed} line${summary.removed !== 1 ? "s" : ""}`);
531
509
  }
532
510
  // ---------------------------------------------------------------------------
533
511
  // bash
package/dist/ui.js CHANGED
@@ -556,7 +556,7 @@ export class Spinner {
556
556
  draw() {
557
557
  const text = this.render ? this.render() : "";
558
558
  const icon = SPINNER_FRAMES[this.frame % SPINNER_FRAMES.length];
559
- const rendered = `\x1b[36m${icon}\x1b[0m \x1b[90m${text}\x1b[0m`;
559
+ const rendered = `\x1b[1;36m${icon}\x1b[0m \x1b[90m${text}\x1b[0m`;
560
560
  // Nothing changed on this tick -> do not rewrite the line (avoids flicker).
561
561
  if (rendered === this.lastRendered)
562
562
  return;
@@ -613,9 +613,22 @@ export class Spinner {
613
613
  }
614
614
  }
615
615
  // ---------------------------------------------------------------------------
616
- // Tool Action Formatter
616
+ // Tool Call & Result Formatter
617
617
  // ---------------------------------------------------------------------------
618
- export function formatToolAction(name, argumentsJson, status = "ok") {
618
+ const TOOL_DISPLAY_NAMES = {
619
+ read_file: "Read",
620
+ write_file: "Write",
621
+ edit_file: "Edit",
622
+ bash: "Bash",
623
+ glob: "Glob",
624
+ grep: "Grep",
625
+ load_skill: "Load",
626
+ };
627
+ const TOOL_RESULT_MAX_CHARS = 400;
628
+ const TOOL_RESULT_MAX_LINES = 6;
629
+ const TOOL_RESULT_MAX_LINE_CHARS = 160;
630
+ /** First line of a tool entry: `Bash(rm -f "…")` — cyan tool name, primary arg in parens. */
631
+ export function toolCallLabel(name, argumentsJson) {
619
632
  let args = {};
620
633
  try {
621
634
  args = argumentsJson ? JSON.parse(argumentsJson) : {};
@@ -623,58 +636,67 @@ export function formatToolAction(name, argumentsJson, status = "ok") {
623
636
  catch {
624
637
  args = {};
625
638
  }
626
- const phrase = (verbOk, verbIng, verbFail, detail) => {
627
- if (status === "started")
628
- return `${verbIng} ${detail}`;
629
- if (status === "failed")
630
- return `${verbFail} ${detail}`;
631
- return `${verbOk} ${detail}`;
632
- };
633
- const p = args["path"] ?? "";
639
+ const display = TOOL_DISPLAY_NAMES[name] ??
640
+ (name ? name[0].toUpperCase() + name.slice(1) : "Tool");
641
+ let arg = "";
634
642
  switch (name) {
635
- case "read_file": {
636
- const start = args["start_line"];
637
- const end = args["end_line"];
638
- const span = start != null || end != null
639
- ? ` lines ${start ?? 1}-${end ?? "end"}`
640
- : "";
641
- return phrase("Read", "Reading", "Failed to read", `${p}${span}`);
642
- }
643
+ case "bash":
644
+ arg = String(args["command"] ?? "");
645
+ break;
646
+ case "read_file":
643
647
  case "write_file":
644
- return phrase("Wrote", "Writing", "Failed to write", `${p} · ${String(args["content"] ?? "").length.toLocaleString()} chars`);
645
- case "edit_file": {
646
- const suffix = args["replace_all"] ? " · replace all" : "";
647
- return phrase("Edited", "Editing", "Failed to edit", `${p}${suffix}`);
648
- }
649
- case "bash": {
650
- const cwd = args["cwd"];
651
- const location = cwd ? ` in ${cwd}` : "";
652
- const command = truncateEllipsis(String(args["command"] ?? ""), 400, "command");
653
- if (status === "started")
654
- return `Running command${location}: ${command}`;
655
- if (status === "failed")
656
- return `Command failed${location}: ${command}`;
657
- return `Ran command${location}: ${command}`;
658
- }
659
- case "glob": {
660
- const detail = String(args["pattern"] ?? "");
661
- if (status === "failed")
662
- return `Search failed for: ${detail}`;
663
- return phrase("Searched files for", "Searching files for", "Search failed for", detail);
664
- }
665
- case "grep": {
666
- const detail = String(args["pattern"] ?? "");
667
- if (status === "failed")
668
- return `Grep failed for: ${detail}`;
669
- return phrase("Grepped for", "Grepping for", "Grep failed for", detail);
670
- }
671
- case "load_skill": {
672
- const skill = String(args["skill_name"] ?? "");
673
- return phrase("Loaded skill", "Loading skill", "Failed to load skill", skill);
674
- }
675
- default:
676
- return phrase("Executed", "Executing", "Failed to execute", name);
648
+ case "edit_file":
649
+ arg = String(args["path"] ?? "");
650
+ break;
651
+ case "glob":
652
+ case "grep":
653
+ arg = String(args["pattern"] ?? "");
654
+ break;
655
+ case "load_skill":
656
+ arg = String(args["skill_name"] ?? "");
657
+ break;
677
658
  }
659
+ arg = arg.trim();
660
+ if (!arg)
661
+ return `\x1b[1;36m${display}\x1b[0m`;
662
+ if (arg.length > 400)
663
+ arg = arg.slice(0, 400) + "…";
664
+ return `\x1b[1;36m${display}\x1b[0m(${arg})`;
665
+ }
666
+ /**
667
+ * Second line of a tool entry: `⎿ Done` on a quiet success, otherwise the
668
+ * tool's output / error, collapsed and truncated so it never floods the screen.
669
+ */
670
+ export function formatToolResult(rawResult, failed) {
671
+ const raw = stripAnsi(rawResult || "")
672
+ .replace(/\r\n/g, "\n")
673
+ .trim();
674
+ const code = raw.match(/^exit code: (\d+)/)?.[1] ?? null;
675
+ const clean = raw.replace(/^exit code: \d+\n?/, "").trim();
676
+ const corner = "\x1b[90m⎿\x1b[0m ";
677
+ if (!failed && (!clean || clean === "(no output)")) {
678
+ return `${corner}\x1b[32mDone\x1b[0m`;
679
+ }
680
+ if (failed && (!clean || clean === "(no output)") && code) {
681
+ return `${corner}\x1b[31mexit code: ${code}\x1b[0m`;
682
+ }
683
+ let body = clean;
684
+ const clipped = body.length > TOOL_RESULT_MAX_CHARS;
685
+ if (clipped)
686
+ body = body.slice(0, TOOL_RESULT_MAX_CHARS);
687
+ const lines = body
688
+ .split("\n")
689
+ .slice(0, TOOL_RESULT_MAX_LINES)
690
+ .map((ln) => ln.length > TOOL_RESULT_MAX_LINE_CHARS
691
+ ? ln.slice(0, TOOL_RESULT_MAX_LINE_CHARS) + "…"
692
+ : ln);
693
+ if (clipped || body.split("\n").length > TOOL_RESULT_MAX_LINES)
694
+ lines.push("…");
695
+ const styleOpen = failed ? "\x1b[31m" : "\x1b[90m";
696
+ const inner = lines
697
+ .map((ln, i) => (i === 0 ? corner : " ") + styleOpen + ln)
698
+ .join("\n");
699
+ return `${inner}\x1b[0m`;
678
700
  }
679
701
  // ---------------------------------------------------------------------------
680
702
  // Prompt Editing & Interactive Input
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.74",
3
+ "version": "1.0.76",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },