@ouro.bot/cli 0.1.0-alpha.251 → 0.1.0-alpha.252

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,10 +1,16 @@
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.252",
6
+ "changes": [
7
+ "fix(tui): polished history preview (spacing, shadow color, showing-recent label), full-width separators, backslash+enter and ctrl+j for newlines."
8
+ ]
9
+ },
4
10
  {
5
11
  "version": "0.1.0-alpha.251",
6
12
  "changes": [
7
- "fix(bluebubbles): images sent via iMessage now reach the model — adds capability-gated image hydration with a MiniMax VLM fallback. Reasoning MiniMax chat models (M2/M2.1/M2.5/M2.7) silently drop OpenAI-style `image_url` content parts, so previously slugger answered image questions from fabricated context. Now, when the active chat model lacks the new `vision: true` capability flag, inbound screenshots are auto-described at ingestion via `/v1/coding_plan/vlm` and the description text replaces the `image_url` part before the turn reaches the model. Vision-capable models (claude-opus/sonnet-4-6, gpt-5.4, MiniMax-Text-01, MiniMax-VL-01) continue to see images natively via pass-through.",
13
+ "fix(bluebubbles): images sent via iMessage now reach the model \u2014 adds capability-gated image hydration with a MiniMax VLM fallback. Reasoning MiniMax chat models (M2/M2.1/M2.5/M2.7) silently drop OpenAI-style `image_url` content parts, so previously slugger answered image questions from fabricated context. Now, when the active chat model lacks the new `vision: true` capability flag, inbound screenshots are auto-described at ingestion via `/v1/coding_plan/vlm` and the description text replaces the `image_url` part before the turn reaches the model. Vision-capable models (claude-opus/sonnet-4-6, gpt-5.4, MiniMax-Text-01, MiniMax-VL-01) continue to see images natively via pass-through.",
8
14
  "feat(tools): new `describe_image` agent tool registered into the BlueBubbles tool set when the chat model lacks vision. Lets the agent re-interrogate an attachment with a targeted prompt (e.g. 'what's the flight number in the bottom-right?') after ingestion. Backed by a bounded in-memory attachment cache populated during hydration; handler re-downloads bytes and calls the same VLM client.",
9
15
  "fix(bluebubbles): `formatMessageText` now preserves the attachment marker when a message has BOTH text and attachments (B2). Previously the marker was dropped whenever text was present, hiding attachments from the agent's view of the message.",
10
16
  "feat(heart): `ModelCapabilities` gains `vision?: boolean` and `audio?: boolean` flags (B4). Vision rows populated for claude-opus-4-6, claude-sonnet-4-6, claude-opus-4.6, claude-sonnet-4.6, gpt-5.4, MiniMax-Text-01, MiniMax-VL-01. M2.1/M2.5/M2.7 intentionally left unset.",
@@ -14,7 +20,7 @@
14
20
  {
15
21
  "version": "0.1.0-alpha.250",
16
22
  "changes": [
17
- "fix(sync): surface 'bundle is not a git repo' as an actionable error instead of silently failing. Previously, enabling `sync.enabled` on a bundle that had never been `git init`'d produced a generic `git status` failure buried in nerves logs; the agent saw nothing in its start-of-turn packet and the user saw nothing in `ouro status`. Now: (1) `preTurnPull` and `postTurnPush` detect the missing `.git` directory before touching git and return an actionable error with the bundle path and the `git init` hint; (2) this error propagates via `ctx.syncFailure` into the agent's Sync warning, so the agent can offer to run `git init` or just do it; (3) `ouro status` shows a red `error` state with `not a git repo — run \\`git init\\` to enable sync` next to the offending bundle. New nerves event: `heart.sync_not_a_repo`."
23
+ "fix(sync): surface 'bundle is not a git repo' as an actionable error instead of silently failing. Previously, enabling `sync.enabled` on a bundle that had never been `git init`'d produced a generic `git status` failure buried in nerves logs; the agent saw nothing in its start-of-turn packet and the user saw nothing in `ouro status`. Now: (1) `preTurnPull` and `postTurnPush` detect the missing `.git` directory before touching git and return an actionable error with the bundle path and the `git init` hint; (2) this error propagates via `ctx.syncFailure` into the agent's Sync warning, so the agent can offer to run `git init` or just do it; (3) `ouro status` shows a red `error` state with `not a git repo \u2014 run \\`git init\\` to enable sync` next to the offending bundle. New nerves event: `heart.sync_not_a_repo`."
18
24
  ]
19
25
  },
20
26
  {
@@ -27,7 +33,7 @@
27
33
  {
28
34
  "version": "0.1.0-alpha.248",
29
35
  "changes": [
30
- "feat(daemon): `ouro status` Git Sync now resolves and shows the actual remote URL via `git remote get-url`. Three states: `origin → git@github.com:me/foo.git` when the remote resolves, `local only` when sync is enabled but no remote is configured, `disabled` when sync is off. Previously you had to `cd` into the bundle and run `git remote -v` to find out where it pushes.",
36
+ "feat(daemon): `ouro status` Git Sync now resolves and shows the actual remote URL via `git remote get-url`. Three states: `origin \u2192 git@github.com:me/foo.git` when the remote resolves, `local only` when sync is enabled but no remote is configured, `disabled` when sync is off. Previously you had to `cd` into the bundle and run `git remote -v` to find out where it pushes.",
31
37
  "fix(heart/sync): `preTurnPull` now skips the pull when no git remote is configured, mirroring the existing `postTurnPush` behavior. Closes the half-implemented 'no-remote sync' (local-only commit log) story \u2014 previously, enabling sync without a remote produced a `syncFailure` on every turn from the failing `git pull` call."
32
38
  ]
33
39
  },
@@ -41,10 +41,14 @@ function ringColor(elapsedSec) {
41
41
  return OURO.scale;
42
42
  }
43
43
  // ─── Header ─────────────────────────────────────────────────────────
44
- /** Safe terminal width: capped at 200, with 2-char margin */
44
+ /** Terminal width for content (capped at 200, with 2-char margin) */
45
45
  function safeWidth() {
46
46
  return Math.min(process.stdout.columns || 80, 200) - 2;
47
47
  }
48
+ /** Full terminal width for edge-to-edge elements (separators) */
49
+ function termWidth() {
50
+ return process.stdout.columns || 80;
51
+ }
48
52
  function Header({ agentName, model, contextPercent, cwd }) {
49
53
  const showCtx = contextPercent > 0;
50
54
  const info = [agentName, model, cwd, showCtx ? `ctx ${contextPercent}%` : ""].filter(Boolean).join(" · ");
@@ -81,18 +85,18 @@ function MessageBlock({ msg }) {
81
85
  return (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" });
82
86
  return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: visibleCalls.map((tc, i) => (0, jsx_runtime_1.jsx)(ToolResultLine, { tc: tc }, i)) }));
83
87
  }
84
- // ── Session history (dimmed replay of last exchanges) ──
88
+ // ── Session history (dimmed recap of recent exchanges) ──
85
89
  if (msg.role === "history-summary") {
86
- return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: msg.content }) }));
90
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginTop: 1, marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: msg.content }) }));
87
91
  }
88
92
  if (msg.role === "history-user") {
89
- return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: (0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, bold: true, children: ") " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, bold: true, children: msg.content })] }) }));
93
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: [(0, jsx_runtime_1.jsxs)(ink_1.Box, { children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, bold: true, children: ") " }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, bold: true, children: msg.content })] }), (0, jsx_runtime_1.jsx)(ink_1.Box, { marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) })] }));
90
94
  }
91
95
  if (msg.role === "history-assistant") {
92
- return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginBottom: 1, children: msg.content ? (0, jsx_runtime_1.jsx)(streaming_markdown_1.StreamingMarkdown, { text: msg.content, maxWidth: safeWidth() }) : null }));
96
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginBottom: 1, children: [msg.content ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, children: msg.content }) : null, (0, jsx_runtime_1.jsx)(ink_1.Box, { marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) })] }));
93
97
  }
94
98
  if (msg.role === "history-end") {
95
- return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", children: (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(safeWidth() + 2) }) }));
99
+ return ((0, jsx_runtime_1.jsx)(ink_1.Box, { flexDirection: "column", marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.separator, children: "─".repeat(termWidth()) }) }));
96
100
  }
97
101
  if (msg.role === "user") {
98
102
  return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginTop: 1, children: [msg.content ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.bone, bold: true, children: msg.content }) : null, (0, jsx_runtime_1.jsx)(ink_1.Box, { marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) })] }));
@@ -171,6 +175,23 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
171
175
  const cursorRef = (0, react_1.useRef)(0);
172
176
  const historyIdx = (0, react_1.useRef)(-1);
173
177
  const savedInput = (0, react_1.useRef)("");
178
+ // Raw stdin handler for Alt+Enter (ESC + CR/LF) — Ink 3.2's useInput splits
179
+ // \x1b\r into separate events, so we catch the combined sequence here.
180
+ const { stdin } = (0, ink_1.useStdin)();
181
+ (0, react_1.useEffect)(() => {
182
+ if (!stdin)
183
+ return;
184
+ const onData = (data) => {
185
+ const str = data.toString("utf-8");
186
+ if (str === "\x1b\r" || str === "\x1b\n") {
187
+ const before = inputRef.current.slice(0, cursorRef.current);
188
+ const after = inputRef.current.slice(cursorRef.current);
189
+ updateInput(before + "\n" + after, cursorRef.current + 1);
190
+ }
191
+ };
192
+ stdin.on("data", onData);
193
+ return () => { stdin.off("data", onData); };
194
+ }, [stdin]); // eslint-disable-line react-hooks/exhaustive-deps
174
195
  const tooltipTimerRef = (0, react_1.useRef)(null);
175
196
  (0, react_1.useEffect)(() => () => {
176
197
  if (tooltipTimerRef.current)
@@ -234,13 +255,20 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
234
255
  return;
235
256
  }
236
257
  if (key.return) {
237
- if (key.meta) {
238
- // Alt+Enter: insert newline
258
+ // Alt+Enter or Shift+Enter: insert newline
259
+ if (key.meta || key.shift) {
239
260
  const before = inputRef.current.slice(0, cursorRef.current);
240
261
  const after = inputRef.current.slice(cursorRef.current);
241
262
  updateInput(before + "\n" + after, cursorRef.current + 1);
242
263
  return;
243
264
  }
265
+ // Backslash+Enter: insert newline (like Claude Code)
266
+ if (cursorRef.current > 0 && inputRef.current[cursorRef.current - 1] === "\\") {
267
+ const before = inputRef.current.slice(0, cursorRef.current - 1);
268
+ const after = inputRef.current.slice(cursorRef.current);
269
+ updateInput(before + "\n" + after, cursorRef.current);
270
+ return;
271
+ }
244
272
  const text = inputRef.current;
245
273
  if (text.trim())
246
274
  onSubmit(text);
@@ -376,17 +404,15 @@ function InputArea({ onSubmit, onCtrlC, history, queuedInputs, onPopQueue, agent
376
404
  historyIdx.current = -1;
377
405
  }
378
406
  });
379
- // Get terminal width (capped for sanity)
380
- const cols = safeWidth();
381
407
  const isMultiline = input.includes("\n");
382
408
  const inputLines = input.split("\n");
383
- return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(cols + 2) }), 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, { color: OURO.scale, 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 ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: "█" }) : (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 + (cursorVisible ? 0 : 1)) })] }))] })), (0, jsx_runtime_1.jsx)(ink_1.Text, { dimColor: true, children: "─".repeat(cols + 2) }), (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" })] })] }));
409
+ 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, { color: OURO.scale, 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 ? (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.scale, children: "█" }) : (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 + (cursorVisible ? 0 : 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" })] })] }));
384
410
  }
385
411
  // ─── Main TUI Component ─────────────────────────────────────────────
386
412
  function OuroTui({ agentName, model, completedMessages, inputHistory, queuedInputs, live, elapsedSeconds, contextPercent, onSubmit, onCtrlC, onPopQueue, cwd, }) {
387
413
  return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", children: [(0, jsx_runtime_1.jsx)(ink_1.Static, { items: [{ id: "__header__" }, ...completedMessages], children: (item, index) => {
388
414
  if (index === 0) {
389
- return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginBottom: 2, children: [(0, jsx_runtime_1.jsx)(ink_1.Box, { marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }), (0, jsx_runtime_1.jsx)(Header, { agentName: agentName, model: model, contextPercent: contextPercent, cwd: cwd }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, dimColor: true, children: " Ctrl-C twice to exit \u00b7 \u2191\u2193 history \u00b7 Esc clear \u00b7 Alt+Enter newline" })] }, "header"));
415
+ return ((0, jsx_runtime_1.jsxs)(ink_1.Box, { flexDirection: "column", marginBottom: 2, children: [(0, jsx_runtime_1.jsx)(ink_1.Box, { marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }), (0, jsx_runtime_1.jsx)(Header, { agentName: agentName, model: model, contextPercent: contextPercent, cwd: cwd }), (0, jsx_runtime_1.jsx)(ink_1.Text, { color: OURO.shadow, dimColor: true, children: " Ctrl-C twice to exit \u00b7 \u2191\u2193 history \u00b7 Esc clear \u00b7 opt+Enter newline" })] }, "header"));
390
416
  }
391
417
  return (0, jsx_runtime_1.jsx)(MessageBlock, { msg: item }, item.id);
392
418
  } }), (live.loading || live.streamingText || live.activeTool) ? (0, jsx_runtime_1.jsx)(ink_1.Box, { marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }) : null, (0, jsx_runtime_1.jsx)(LiveArea, { live: live, elapsed: elapsedSeconds }), (live.loading || live.streamingText || live.activeTool) ? (0, jsx_runtime_1.jsx)(ink_1.Box, { marginBottom: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }) : null, queuedInputs.length > 0 ? (0, jsx_runtime_1.jsx)(QueuedMessages, { items: queuedInputs }) : null, (0, jsx_runtime_1.jsx)(ink_1.Box, { marginTop: 1, children: (0, jsx_runtime_1.jsx)(ink_1.Text, { children: "" }) }), (0, jsx_runtime_1.jsx)(InputArea, { onSubmit: onSubmit, onCtrlC: onCtrlC, history: inputHistory, queuedInputs: queuedInputs, onPopQueue: onPopQueue, agentName: agentName, model: model })] }));
@@ -546,7 +546,7 @@ async function runCliSession(options) {
546
546
  }
547
547
  const msgCount = messages.filter(m => m.role === "user" || m.role === "assistant").length;
548
548
  const timeAgo = options.lastActivityAt ? formatTimeAgo(new Date(options.lastActivityAt)) : null;
549
- const summary = ` resuming session (${msgCount} messages${timeAgo ? ` · last active ${timeAgo}` : ""})`;
549
+ const summary = ` resuming session · ${msgCount} messages${timeAgo ? ` · last active ${timeAgo}` : ""} · showing recent`;
550
550
  tuiStore.addSessionHistory(summary, lastExchanges);
551
551
  }
552
552
  /* v8 ignore stop */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.251",
3
+ "version": "0.1.0-alpha.252",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",