@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 +10 -4
- package/dist/engine.js +4 -6
- package/dist/tools.js +16 -38
- package/dist/ui.js +74 -52
- package/package.json +1 -1
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,
|
|
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(
|
|
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:
|
|
157
|
-
text:
|
|
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,
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
125
|
-
|
|
126
|
-
|
|
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,
|
|
146
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
530
|
-
|
|
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
|
|
616
|
+
// Tool Call & Result Formatter
|
|
617
617
|
// ---------------------------------------------------------------------------
|
|
618
|
-
|
|
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
|
|
627
|
-
|
|
628
|
-
|
|
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 "
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
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
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
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
|