@oxecli/oxe 1.0.104 → 1.0.106

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/README.md CHANGED
@@ -36,5 +36,9 @@ Install Oxe globally with npm:
36
36
 
37
37
 
38
38
 
39
+ **Info**
40
+
41
+
42
+
39
43
  Oxe can inspect your project, make the required changes, run commands, and show you what it is doing along the way.
40
44
 
package/dist/engine.js CHANGED
@@ -82,7 +82,7 @@ export class InferenceEngine {
82
82
  }
83
83
  }
84
84
  }
85
- async runTool(name, argumentsJson) {
85
+ async runTool(name, argumentsJson, signal) {
86
86
  let args = {};
87
87
  try {
88
88
  args = argumentsJson ? JSON.parse(argumentsJson) : {};
@@ -103,7 +103,7 @@ export class InferenceEngine {
103
103
  result = toolEditFile(args.path, args.old_string, args.new_string, args.replace_all);
104
104
  break;
105
105
  case "bash":
106
- result = await toolBash(args.command, args.timeout, args.cwd);
106
+ result = await toolBash(args.command, args.timeout, args.cwd, signal);
107
107
  break;
108
108
  case "glob":
109
109
  result = toolGlob(args.pattern, args.path, args.limit, args.depth);
@@ -591,7 +591,7 @@ export class InferenceEngine {
591
591
  dockTransientStart("flush");
592
592
  const toolSpinner = new Spinner();
593
593
  toolSpinner.startDots("\x1b[90m╰─\x1b[0m Working");
594
- const rawResult = await this.runTool(c.name, c.arguments);
594
+ const rawResult = await this.runTool(c.name, c.arguments, this.activeAbort?.signal);
595
595
  // A tool call was made, which interrupts any current working phase,
596
596
  // so the next agent iteration may commit a fresh "Worked for …" block.
597
597
  this.workedCommitted = false;
package/dist/tools.js CHANGED
@@ -586,7 +586,7 @@ function terminateProcessTree(proc) {
586
586
  }
587
587
  }
588
588
  }
589
- export function toolBash(command, timeout = 60, cwd) {
589
+ export function toolBash(command, timeout = 60, cwd, signal) {
590
590
  try {
591
591
  let t = typeof timeout === "number" ? timeout : parseInt(String(timeout), 10);
592
592
  if (Number.isNaN(t))
@@ -597,6 +597,10 @@ export function toolBash(command, timeout = 60, cwd) {
597
597
  return Promise.resolve(`Error: working directory not found: ${cwd}`);
598
598
  }
599
599
  return new Promise((resolve) => {
600
+ if (signal && signal.aborted) {
601
+ resolve("Error: command aborted");
602
+ return;
603
+ }
600
604
  let child;
601
605
  try {
602
606
  if (process.platform === "win32") {
@@ -621,6 +625,18 @@ export function toolBash(command, timeout = 60, cwd) {
621
625
  let stdout = "";
622
626
  let stderr = "";
623
627
  let finished = false;
628
+ const onAbort = () => {
629
+ if (finished)
630
+ return;
631
+ terminateProcessTree({
632
+ pid: child.pid,
633
+ kill: (sig) => child.kill(sig),
634
+ });
635
+ finished = true;
636
+ clearTimeout(timer);
637
+ resolve("Error: command aborted");
638
+ };
639
+ signal?.addEventListener("abort", onAbort, { once: true });
624
640
  const timer = setTimeout(() => {
625
641
  if (finished)
626
642
  return;
@@ -633,6 +649,8 @@ export function toolBash(command, timeout = 60, cwd) {
633
649
  if (output)
634
650
  msg += `\n${output}`;
635
651
  finished = true;
652
+ clearTimeout(timer);
653
+ signal?.removeEventListener("abort", onAbort);
636
654
  resolve(msg);
637
655
  }, t * 1000);
638
656
  child.stdout?.on("data", (d) => (stdout += d.toString("utf-8")));
@@ -642,6 +660,7 @@ export function toolBash(command, timeout = 60, cwd) {
642
660
  return;
643
661
  finished = true;
644
662
  clearTimeout(timer);
663
+ signal?.removeEventListener("abort", onAbort);
645
664
  resolve(`Error: ${err}`);
646
665
  });
647
666
  child.on("close", (code) => {
@@ -649,6 +668,7 @@ export function toolBash(command, timeout = 60, cwd) {
649
668
  return;
650
669
  finished = true;
651
670
  clearTimeout(timer);
671
+ signal?.removeEventListener("abort", onAbort);
652
672
  const output = (stdout + stderr).trim() || "(no output)";
653
673
  resolve(`exit code: ${code}\n${output}`);
654
674
  });
package/dist/ui.js CHANGED
@@ -1394,7 +1394,6 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1394
1394
  let draft = null;
1395
1395
  let bracketedPaste = false;
1396
1396
  let bracketedPasteBuffer = "";
1397
- let bracketedPasteSince = 0;
1398
1397
  let pasteBurst = false;
1399
1398
  let pasteBurstTimer = null;
1400
1399
  readline.emitKeypressEvents(process.stdin);
@@ -1719,19 +1718,17 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1719
1718
  const end = "\x1b[201~";
1720
1719
  const sequence = key?.sequence || "";
1721
1720
  const value = str || "";
1722
- if (!bracketedPaste && (sequence === start || value === start)) {
1721
+ // A new paste-start while already inside a paste means the previous paste
1722
+ // was aborted (its end marker never arrived). Reset and start over so we
1723
+ // never swallow subsequent typing.
1724
+ if ((key?.name === "paste-start" || sequence === start || value === start)) {
1723
1725
  bracketedPaste = true;
1724
- bracketedPasteSince = Date.now();
1725
1726
  bracketedPasteBuffer = "";
1726
1727
  const inline = value === start ? "" : value.startsWith(start) ? value.slice(start.length) : "";
1727
1728
  if (inline)
1728
1729
  bracketedPasteBuffer = inline;
1729
1730
  if (inline.includes(end)) {
1730
- const endAt = inline.indexOf(end);
1731
- const pasted = inline
1732
- .slice(0, endAt)
1733
- .replace(/\r\n/g, "\n")
1734
- .replace(/\r/g, "\n");
1731
+ const pasted = inline.slice(0, inline.indexOf(end)).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1735
1732
  bracketedPaste = false;
1736
1733
  bracketedPasteBuffer = "";
1737
1734
  if (pasted)
@@ -1742,30 +1739,12 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1742
1739
  }
1743
1740
  if (!bracketedPaste)
1744
1741
  return false;
1745
- // Safety net: if the paste-end marker (\x1b[201~) was lost interrupted
1746
- // paste, split escape sequence, or a terminal quirk bracketedPaste stays
1747
- // true and every later keystroke is swallowed here, which looks like the
1748
- // input field locked up (even Ctrl+C is captured as a paste chunk). If the
1749
- // paste has been open far longer than any real paste takes and a fresh
1750
- // single-printable-char keystroke arrives, flush the buffered text as
1751
- // literal input and reset so typing works again.
1752
- if (Date.now() - bracketedPasteSince > 3000 &&
1753
- str &&
1754
- str.length === 1 &&
1755
- !key?.ctrl &&
1756
- !key?.meta) {
1757
- const pasted = bracketedPasteBuffer
1758
- .replace(/\r\n/g, "\n")
1759
- .replace(/\r/g, "\n");
1760
- bracketedPaste = false;
1761
- bracketedPasteBuffer = "";
1762
- if (pasted) {
1763
- insert(pasted, true);
1764
- repaint();
1765
- }
1766
- return false; // let this keystroke be handled normally below
1767
- }
1768
- if (sequence === end || value === end) {
1742
+ // Close on the end marker. readline emits it as a dedicated keypress
1743
+ // (key.name === "paste-end", sequence === \x1b[201~); also scan the raw
1744
+ // value in case a terminal delivers it embedded in a larger chunk.
1745
+ const endAt = value.indexOf(end);
1746
+ if (key?.name === "paste-end" || sequence === end || value === end || endAt >= 0) {
1747
+ bracketedPasteBuffer += endAt >= 0 ? value.slice(0, endAt) : "";
1769
1748
  const pasted = bracketedPasteBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1770
1749
  bracketedPaste = false;
1771
1750
  bracketedPasteBuffer = "";
@@ -1775,21 +1754,8 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1775
1754
  return true;
1776
1755
  }
1777
1756
  const chunk = value || (sequence && !sequence.startsWith("\x1b[") ? sequence : "");
1778
- if (chunk) {
1779
- const endAt = chunk.indexOf(end);
1780
- if (endAt >= 0) {
1781
- bracketedPasteBuffer += chunk.slice(0, endAt);
1782
- const pasted = bracketedPasteBuffer.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1783
- bracketedPaste = false;
1784
- bracketedPasteBuffer = "";
1785
- if (pasted)
1786
- insert(pasted, true);
1787
- repaint();
1788
- }
1789
- else {
1790
- bracketedPasteBuffer += chunk;
1791
- }
1792
- }
1757
+ if (chunk)
1758
+ bracketedPasteBuffer += chunk;
1793
1759
  return true;
1794
1760
  };
1795
1761
  const markPasteBurst = () => {
@@ -1802,14 +1768,9 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1802
1768
  }, 500);
1803
1769
  };
1804
1770
  const onKeypress = (str, key) => {
1805
- if (consumeBracketedPaste(str, key))
1806
- return;
1807
- if (pasteBurst && str && str.length === 1 && !key?.ctrl && !key?.meta) {
1808
- pasteBurst = false;
1809
- if (pasteBurstTimer)
1810
- clearTimeout(pasteBurstTimer);
1811
- pasteBurstTimer = null;
1812
- }
1771
+ // Interrupts (Ctrl+C/D/Z) must be handled BEFORE bracketed-paste capture so
1772
+ // that even a stuck/interrupted paste can always be escaped — otherwise the
1773
+ // paste handler swallows them and the field appears locked with no way out.
1813
1774
  if (key && key.ctrl && key.name === "c") {
1814
1775
  settle(new Error("interrupt"), true);
1815
1776
  return;
@@ -1818,6 +1779,14 @@ export function askBottomPrompt(label = "You", prefix = "❯", history = [], hin
1818
1779
  settle(new Error("eof"), true);
1819
1780
  return;
1820
1781
  }
1782
+ if (consumeBracketedPaste(str, key))
1783
+ return;
1784
+ if (pasteBurst && str && str.length === 1 && !key?.ctrl && !key?.meta) {
1785
+ pasteBurst = false;
1786
+ if (pasteBurstTimer)
1787
+ clearTimeout(pasteBurstTimer);
1788
+ pasteBurstTimer = null;
1789
+ }
1821
1790
  if (key && key.ctrl && key.name === "a") {
1822
1791
  cursor = 0;
1823
1792
  repaint();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxecli/oxe",
3
- "version": "1.0.104",
3
+ "version": "1.0.106",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },