@ouro.bot/cli 0.1.0-alpha.257 → 0.1.0-alpha.258

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/changelog.json CHANGED
@@ -1,6 +1,14 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.258",
6
+ "changes": [
7
+ "fix(tui): deferred ESC handling fixes arrow keys and Alt+Enter.",
8
+ "fix(tui): green cursor background.",
9
+ "fix(tui): clipboard fallback for macOS temp screenshots, proper path splitting and unescaping for image drag-drop."
10
+ ]
11
+ },
4
12
  {
5
13
  "version": "0.1.0-alpha.257",
6
14
  "changes": [
@@ -50,6 +50,7 @@ exports.resolveImageContent = resolveImageContent;
50
50
  * files, and produces OpenAI-compatible `image_url` content blocks.
51
51
  */
52
52
  const promises_1 = require("fs/promises");
53
+ const child_process_1 = require("child_process");
53
54
  const path = __importStar(require("path"));
54
55
  const runtime_1 = require("../../nerves/runtime");
55
56
  /** Matches image file extensions (case-insensitive) */
@@ -58,10 +59,14 @@ exports.IMAGE_EXTENSION_REGEX = /\.(png|jpe?g|gif|webp)$/i;
58
59
  function isImagePath(str) {
59
60
  return exports.IMAGE_EXTENSION_REGEX.test(unescapePath(str));
60
61
  }
61
- /** Convert backslash-escaped spaces to plain spaces (macOS drag-drop format) */
62
+ /** Strip shell escape backslashes from a path (macOS/Linux drag-drop format) */
62
63
  function unescapePath(str) {
63
- return str.replace(/\\ /g, " ");
64
+ // Handle double-backslashes first (actual backslash in filename)
65
+ // then remove single escape backslashes
66
+ return str.replace(/\\(.)/g, "$1");
64
67
  }
68
+ /** Detect temp screenshot paths from macOS screencapture */
69
+ const TEMP_SCREENSHOT_RE = /\/TemporaryItems\/.*screencaptureui.*\/Screenshot/i;
65
70
  /** Map file extension to MIME media type */
66
71
  function extensionToMediaType(ext) {
67
72
  switch (ext.toLowerCase()) {
@@ -80,41 +85,85 @@ function extensionToMediaType(ext) {
80
85
  }
81
86
  /**
82
87
  * Read an image file and return base64 + media type.
83
- * Returns null on any error (file not found, permission denied, etc.)
88
+ * Falls back to clipboard for temp screenshot paths (macOS screencapture
89
+ * creates ephemeral files in TemporaryItems that are cleaned up quickly).
90
+ * Returns null on any error.
84
91
  */
85
92
  async function tryReadImage(absolutePath) {
93
+ // Try reading the file directly first
86
94
  try {
87
95
  const buffer = await (0, promises_1.readFile)(absolutePath);
88
- const ext = path.extname(absolutePath);
96
+ if (buffer.length > 0) {
97
+ const ext = path.extname(absolutePath);
98
+ return {
99
+ base64: Buffer.from(buffer).toString("base64"),
100
+ mediaType: extensionToMediaType(ext),
101
+ };
102
+ }
103
+ }
104
+ catch {
105
+ // File not found or permission denied — try clipboard fallback
106
+ }
107
+ // Clipboard fallback for macOS temp screenshots
108
+ /* v8 ignore start -- clipboard fallback requires real macOS screencapture @preserve */
109
+ if (process.platform === "darwin" && TEMP_SCREENSHOT_RE.test(absolutePath)) {
110
+ const clipboardImage = await getImageFromClipboard();
111
+ if (clipboardImage)
112
+ return clipboardImage;
113
+ }
114
+ /* v8 ignore stop */
115
+ (0, runtime_1.emitNervesEvent)({
116
+ component: "senses",
117
+ event: "senses.image_read_error",
118
+ message: `Failed to read image: ${absolutePath}`,
119
+ meta: { path: absolutePath },
120
+ });
121
+ return null;
122
+ }
123
+ /**
124
+ * Read image data from macOS clipboard via osascript.
125
+ * Used as fallback when temp screenshot files are already cleaned up.
126
+ */
127
+ /* v8 ignore start -- macOS clipboard integration @preserve */
128
+ async function getImageFromClipboard() {
129
+ const tmpPath = "/tmp/ouro_clipboard_image.png";
130
+ try {
131
+ // Check if clipboard has image data
132
+ (0, child_process_1.execFileSync)("osascript", ["-e", "the clipboard as «class PNGf»"], { stdio: "pipe" });
133
+ // Save clipboard image to temp file
134
+ (0, child_process_1.execFileSync)("osascript", [
135
+ "-e", "set png_data to (the clipboard as «class PNGf»)",
136
+ "-e", `set fp to open for access POSIX file "${tmpPath}" with write permission`,
137
+ "-e", "write png_data to fp",
138
+ "-e", "close access fp",
139
+ ], { stdio: "pipe" });
140
+ const buffer = await (0, promises_1.readFile)(tmpPath);
141
+ void (0, promises_1.unlink)(tmpPath).catch(() => { });
142
+ if (buffer.length === 0)
143
+ return null;
89
144
  return {
90
145
  base64: Buffer.from(buffer).toString("base64"),
91
- mediaType: extensionToMediaType(ext),
146
+ mediaType: "image/png",
92
147
  };
93
148
  }
94
149
  catch {
95
- (0, runtime_1.emitNervesEvent)({
96
- component: "senses",
97
- event: "senses.image_read_error",
98
- message: `Failed to read image: ${absolutePath}`,
99
- meta: { path: absolutePath },
100
- });
101
150
  return null;
102
151
  }
103
152
  }
153
+ /* v8 ignore stop */
104
154
  /** Format an image reference placeholder */
105
155
  function formatImageRef(n) {
106
156
  return `[Image #${n}]`;
107
157
  }
108
158
  /**
109
- * Scan input for absolute path tokens ending with image extensions.
110
- * Matches `/` followed by non-whitespace chars (allowing `\ ` for escaped spaces)
111
- * ending with an image extension.
159
+ * Scan input for absolute image file paths and replace with [Image #N] references.
160
+ * Handles backslash-escaped characters (macOS drag-drop) and paths with spaces.
112
161
  */
113
162
  function replacePathsWithRefs(input) {
114
163
  const images = new Map();
115
- // Match absolute paths starting with / that end with an image extension.
116
- // Allow backslash-escaped spaces (like `\ ` from macOS drag-drop).
117
- const pathRegex = /(\/(?:[^\s]|\\ )+\.(?:png|jpe?g|gif|webp))/gi;
164
+ // Match absolute paths: start with /, contain non-whitespace or backslash-escaped chars,
165
+ // end with an image extension. The (?:\\.|\S) alternation allows `\ ` sequences.
166
+ const pathRegex = /(?:\/(?:\\.|[^\s])+\.(?:png|jpe?g|gif|webp))/gi;
118
167
  let counter = 0;
119
168
  const text = input.replace(pathRegex, (match) => {
120
169
  counter++;
@@ -163,9 +163,10 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
163
163
  const cursorRef = (0, react_1.useRef)(0);
164
164
  const historyIdx = (0, react_1.useRef)(-1);
165
165
  const savedInput = (0, react_1.useRef)("");
166
- // Track last ESC timestamp for Alt+Enter detection.
167
- // Ink 3.2 splits \x1b\r into separate escape + return events.
168
- // We detect the pattern by checking if Return arrives within 50ms of Escape.
166
+ // Deferred ESC handling: Ink 3.2 fires escape events for ESC prefix of arrow
167
+ // keys (\x1b[D) and Alt+Enter (\x1b\r). We defer ESC actions by 80ms — if
168
+ // another key arrives in that window, it was an escape sequence, not standalone ESC.
169
+ const escTimerRef = (0, react_1.useRef)(null);
169
170
  const lastEscTime = (0, react_1.useRef)(0);
170
171
  const tooltipTimerRef = (0, react_1.useRef)(null);
171
172
  (0, react_1.useEffect)(() => () => {
@@ -211,25 +212,36 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
211
212
  setTooltip("");
212
213
  if (key.escape) {
213
214
  lastEscTime.current = Date.now();
214
- if (inputRef.current) {
215
- updateInput("");
216
- historyIdx.current = -1;
217
- setTooltip("Esc again to clear");
218
- if (tooltipTimerRef.current)
219
- clearTimeout(tooltipTimerRef.current);
220
- tooltipTimerRef.current = setTimeout(() => setTooltip(""), 2000);
221
- }
222
- else if (queuedInputs.length > 0) {
223
- // Pop all queued messages into input for editing
224
- const items = onPopQueue();
225
- updateInput(items.join("\n"));
226
- historyIdx.current = -1;
227
- }
228
- else {
229
- setTooltip("");
230
- }
215
+ // Defer ESC action — if another key arrives within 80ms, this was an escape
216
+ // sequence prefix (arrow key, Alt+Enter), not a standalone Escape press.
217
+ if (escTimerRef.current)
218
+ clearTimeout(escTimerRef.current);
219
+ escTimerRef.current = setTimeout(() => {
220
+ escTimerRef.current = null;
221
+ if (inputRef.current) {
222
+ updateInput("");
223
+ historyIdx.current = -1;
224
+ setTooltip("Esc again to clear");
225
+ if (tooltipTimerRef.current)
226
+ clearTimeout(tooltipTimerRef.current);
227
+ tooltipTimerRef.current = setTimeout(() => setTooltip(""), 2000);
228
+ }
229
+ else if (queuedInputs.length > 0) {
230
+ const items = onPopQueue();
231
+ updateInput(items.join("\n"));
232
+ historyIdx.current = -1;
233
+ }
234
+ else {
235
+ setTooltip("");
236
+ }
237
+ }, 80);
231
238
  return;
232
239
  }
240
+ // Cancel pending ESC action — this key is part of an escape sequence
241
+ if (escTimerRef.current) {
242
+ clearTimeout(escTimerRef.current);
243
+ escTimerRef.current = null;
244
+ }
233
245
  if (key.return) {
234
246
  // Alt+Enter: detect via key.meta OR recent ESC (within 50ms — Ink splits \x1b\r)
235
247
  const recentEsc = (Date.now() - lastEscTime.current) < 50;
@@ -389,8 +401,8 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
389
401
  });
390
402
  const isMultiline = input.includes("\n");
391
403
  const inputLines = input.split("\n");
392
- return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), isMultiline ? ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: inputLines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: i === 0 ? ") " : "· " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: line }), i === inputLines.length - 1 ? (cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { inverse: true, children: " " }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { children: " " })) : null] }, i))) })) : ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: ") " }), !input && queuedInputs.length > 0 ? ((0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: "Press up to edit queued messages" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input.slice(0, cursorPos) }), cursorVisible
393
- ? (0, jsx_runtime_1.jsx)(ink_1.Text, { inverse: true, children: input[cursorPos] ?? " " })
404
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), isMultiline ? ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: inputLines.map((line, i) => ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: i === 0 ? ") " : "· " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: line }), i === inputLines.length - 1 ? (cursorVisible ? (0, jsx_runtime_1.jsx)(ink_1.Text, { backgroundColor: OURO.scale, color: "#000000", children: " " }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { children: " " })) : null] }, i))) })) : ((0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.teal, bold: true, children: ") " }), !input && queuedInputs.length > 0 ? ((0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: "Press up to edit queued messages" })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input.slice(0, cursorPos) }), cursorVisible
405
+ ? (0, jsx_runtime_1.jsx)(ink_1.Text, { backgroundColor: OURO.scale, color: "#000000", children: input[cursorPos] ?? " " })
394
406
  : (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input[cursorPos] ?? " " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, children: input.slice(cursorPos + 1) })] }))] })), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(termWidth()) }), (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsxs)(ink_1.Text, { dimColor: true, children: [" ", agentName, model ? ` · ${model}` : "", " \u00B7 /help"] }), (0, jsx_runtime_1.jsx)(ink_1.Box, { flexGrow: 1 }), tooltip ? (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: tooltip }) : (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "opt+enter for newline" })] })] }));
395
407
  }
396
408
  // ─── Main TUI Component ─────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.257",
3
+ "version": "0.1.0-alpha.258",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",