lazyclaw 5.0.2 → 5.0.4

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/cli.mjs CHANGED
@@ -1634,31 +1634,31 @@ function _renderBanner(version) {
1634
1634
  ];
1635
1635
  }
1636
1636
 
1637
- // v5 sloth banner — shared with the ink splash (tui/splash.mjs).
1638
- // Single-tone orange like _renderBanner so the no-arg launcher and the
1639
- // chat splash share a visual identity. Opt out with LAZYCLAW_LEGACY_MENU=1
1640
- // to fall back to the boxed figlet variant above.
1641
- let _v5BannerRowsCache = null;
1642
- async function _renderV5Banner(version) {
1643
- if (!_v5BannerRowsCache) {
1644
- try {
1645
- const { banner } = await import('./tui/banner.generated.mjs');
1646
- _v5BannerRowsCache = banner.rows;
1647
- } catch {
1648
- _v5BannerRowsCache = null;
1649
- }
1637
+ // v5 hero banner — the chafa-rendered braille sloth shared with the chat
1638
+ // splash (tui/banner.generated.mjs). Centered in TERM_WIDTH at runtime so
1639
+ // the launcher and chat splash present identical visual identity.
1640
+ // Opt out with LAZYCLAW_LEGACY_MENU=1 to fall back to the v4 figlet box.
1641
+ let _slothBannerRowsCache = null;
1642
+ async function _loadSlothBanner() {
1643
+ if (_slothBannerRowsCache !== null) return _slothBannerRowsCache;
1644
+ try {
1645
+ const { banner } = await import('./tui/banner.generated.mjs');
1646
+ _slothBannerRowsCache = banner;
1647
+ } catch {
1648
+ _slothBannerRowsCache = null;
1650
1649
  }
1651
- if (!_v5BannerRowsCache) return _renderBanner(version);
1650
+ return _slothBannerRowsCache;
1651
+ }
1652
+
1653
+ async function _renderV5Banner(version) {
1654
+ const b = await _loadSlothBanner();
1655
+ if (!b) return _renderBanner(version); // missing tarball asset → v4 figlet
1656
+ const TERM_W = Math.max(80, process.stdout.columns || 80);
1657
+ const pad = Math.max(0, Math.floor((TERM_W - b.width) / 2));
1658
+ const rows = b.rows.map(r => _orange(' '.repeat(pad) + r));
1652
1659
  const v = String(version || '?.?.?');
1653
- return _v5BannerRowsCache.map((row, i) => {
1654
- if (i === 8) {
1655
- // Caption row — overlay "lazyclaw v<version>" centered on the 24-wide art
1656
- const cap = `lazyclaw v${v}`;
1657
- const pad = Math.max(0, Math.floor((24 - cap.length) / 2));
1658
- return _orange(' '.repeat(pad) + cap + ' '.repeat(24 - pad - cap.length));
1659
- }
1660
- return _orange(row);
1661
- });
1660
+ rows.push(_orange((' '.repeat(pad) + `lazyclaw v${v}`).padEnd(TERM_W)));
1661
+ return rows;
1662
1662
  }
1663
1663
 
1664
1664
  function _printChatBanner(activeProvName, activeModel, version) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyclaw",
3
- "version": "5.0.2",
3
+ "version": "5.0.4",
4
4
  "description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama, orchestrating multi-step LLM workflows, and running multi-agent Slack teams with cross-task memory. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
5
5
  "keywords": [
6
6
  "claude",
@@ -1,18 +1,46 @@
1
- // AUTO-GENERATED placeholder — replace by running `npm run build:splash`.
1
+ // Auto-generated by `chafa --format=symbols --symbols=braille --invert
2
+ // --size=48x35 --fg-only docs/assets/sleepy-sloth-source.png`. Source:
3
+ // Pexels three-toed sloth by Tomas Malik (free license). Style is the
4
+ // dense inverted braille that user signed off on in v5.0.4 review.
2
5
  export const banner = {
3
6
  rows: [
4
- " ⣀⣠⣤⣶⣶⣦⣄⡀ ",
5
- " ⢠⣾⠟⠉ ⠈⠙⢿⣦ ",
6
- " ⣿⠁ ● ● ⣿ ",
7
- " ⣿⡀ ⠈⠉⠉⠁ ⣿ ",
8
- " ⠘⢿⣦⡀ ⢀⣴⡿⠃ ",
9
- " ⠈⠙⠻⠷⠶⠶⠿⠟⠋ ",
10
- " ⢸⠁ ⢸ ",
11
- " ⢸⠉ ⠉⠉⠉⠉ ",
12
- " lazyclaw ",
13
- " "
7
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡀ ⢸⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
8
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
9
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣏⠉⠉⠉⠛⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
10
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ ⣼⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
11
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⡄ ⣠⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
12
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡇ ⢻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
13
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠁ ⢘⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
14
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
15
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
16
+ "⣯⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
17
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
18
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
19
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
20
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
21
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
22
+ " ⠁ ⠚⠒⣚⣻⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⠿⣿⣿⣿⠟⠻⠭⠻⢻⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
23
+ " ⢀⣠⣤⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⣺⡋⡂⡺⠿⢋⣷⣲⣰⢀⣤⣴⣬⣏⣽⡮⠟⢿⣿⣿⣿⣿⣿⣿⣿⣿",
24
+ "⣤⣴⣶⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣽⢶⠈ ⢈⠿⠮⠭⢿⠕⡼⣿⠿⡚⣯⡙⠚⣚⢽⣿⣿⣿⣿⣿⣿⣿",
25
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣼⣷⡒⢉⣽⣿⣾⡜⡀⡀⡐⠁⣑⢯⡜⠃⣀⡿⡿⣿⣿⣿⣿⣿⣿",
26
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⣿⣌⢻⠝⣞⠞⣒⣲⡤⣾⣽⡕⣘⣾⣿⣿⣿⣿⣿",
27
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⡿⡿⣿⣿⣿⣿⣿⣿⣄⣂⢸⣿⣗⢿⣺⣳⠶⢤⣶⠏⠁⠴⢛⣻⣿⣿⣿⣿",
28
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣠⣀⡀ ⢉⣿⣿⣿⣿⣿⡅⣿⣿⡕⠋⠠⡕⢕⠟⢀⠈ ⢐⡱⣿⣿⣿⣿⣿",
29
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣿⣶⣿⣿⣿⣿⣿⣜⣿⣯⡃⢢⠴⠰⠘⡂⠾⠆⣀ ⢙⣽⣿⣟⣿",
30
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣦⣽⣿⣪⡫⠊⠄ ⡈⢉⣛⣿",
31
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⠟⣟⣻⣟⣻⡿ ⠛⣺⡡⣔⠋ ⠈⣽⣿",
32
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣶⡅⡖⢮⣺⣷⣴⣧⣀⣙⣧⣮⠦ ⠨⣷⣿",
33
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡷⢎⢶⡹⡝⡯⢮⡛⠼⠟⢙⡳⢡⣵⣾⣥⣾⣿",
34
+ "⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⠿⢷⣿⣷⢻⠱⢳⣮⡜⣰⣻⣷⣴⣻⣽⣿⣿⣿⣿",
35
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣗⡃⠺⠿⠗⢿⣧⠗⢑⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
36
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⡿⢽⣟⡦⢂⣀⣀⣠⣾⣿⣿⣿⣿⣿⣽⣿⣿⣿⣿⣿⣿⣿⣿⣿",
37
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣯⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣷⣷⣂⡽⣵⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣟⣟⣿⣿⣿⣿⣿",
38
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢹⣻⢻⡿⠿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣾⣿⣿⡿⣿",
39
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣧⣿⣿⣾⣷⣶⣯⣭⣛⣻⠿⣿⣷⣿⣿⣿⣿⣿⣿⣿⡿⠿⡿⣟",
40
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⢿⣿⣿⣿⢿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣶⣶⣶⣷⣶⣿⣶⣶⣾⣿⣿⣿",
41
+ "⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣯⣷⣟⣀⣤⣾⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿⣿",
14
42
  ],
15
- width: 24,
16
- height: 10,
43
+ width: 47,
44
+ height: 35,
17
45
  fg: "#FFB347"
18
46
  };
package/tui/splash.mjs CHANGED
@@ -1,21 +1,21 @@
1
- // tui/splash.mjs — two-column launch splash (spec §5.1).
1
+ // tui/splash.mjs — hero-banner launch splash.
2
2
  //
3
3
  // Public surface:
4
4
  // - <Splash {...props} /> ink component for live REPL mount
5
5
  // - renderSplashToString(props) pure string builder used by tests
6
6
  // and by the non-TTY path.
7
7
  //
8
- // Layout: 24-cell sloth gutter (cols 0-23) | 2-cell separator (24-25)
9
- // | 52-cell right column (cols 26-77) | 2-cell right padding.
10
- // Footer: exactly 4 lines, blank row separates body from footer.
8
+ // Layout: 47-cell-wide hero banner centered in 80-col terminal,
9
+ // followed by a two-column tools|skills section (36 cells each)
10
+ // and a 4-line footer.
11
11
  import React from 'react';
12
12
  import { Box, Text } from 'ink';
13
13
  import stringWidth from 'string-width';
14
14
  import { theme } from './theme.mjs';
15
15
  import { banner } from './banner.generated.mjs';
16
16
 
17
- const RIGHT_COL_WIDTH = 52;
18
- const GUTTER_WIDTH = 24;
17
+ const TERM_WIDTH = 80;
18
+ const COL_WIDTH = 36; // 2 (lpad) + 36 + 2 (mid) + 36 + 2 (rpad) = 78 ≈ 80 with slack
19
19
 
20
20
  function fit(text, max) {
21
21
  if (stringWidth(text) <= max) return text.padEnd(max);
@@ -48,29 +48,26 @@ function skillRow({ group, names }) {
48
48
  return `${fit(group, 9)}${tail}${more}`;
49
49
  }
50
50
 
51
- function buildBody(props) {
52
- const { tools = [], skills = [] } = props;
53
- const right = [];
54
- right.push('Available Tools');
55
- right.push(''.repeat(45));
56
- for (const t of tools.slice(0, 8)) right.push(toolRow(t));
57
- if (tools.length > 8) right.push(`... and ${tools.length - 8} more tool groups`);
58
- right.push('');
59
- right.push('Available Skills');
60
- right.push('─'.repeat(45));
61
- for (const s of skills.slice(0, 8)) right.push(skillRow(s));
62
- if (skills.length > 8) right.push(`... and ${skills.length - 8} more skill groups`);
63
-
64
- const left = banner.rows.slice();
65
- while (left.length < right.length) left.push('');
66
- while (right.length < left.length) right.push('');
51
+ function buildBanner() {
52
+ // Center the hero banner inside TERM_WIDTH. banner.width may be < TERM_WIDTH;
53
+ // pad each side equally so it floats over the centerline.
54
+ const pad = Math.max(0, Math.floor((TERM_WIDTH - banner.width) / 2));
55
+ return banner.rows.map(row => ' '.repeat(pad) + row);
56
+ }
67
57
 
58
+ function buildToolsAndSkills(props) {
59
+ const { tools = [], skills = [] } = props;
60
+ const SECT_WIDTH = TERM_WIDTH - 4;
68
61
  const lines = [];
69
- for (let i = 0; i < left.length; i++) {
70
- const lhs = fit(left[i], GUTTER_WIDTH);
71
- const rhs = fit(right[i], RIGHT_COL_WIDTH);
72
- lines.push(` ${lhs} ${rhs}`);
73
- }
62
+ lines.push(` ${fit('Available Tools', SECT_WIDTH)}`);
63
+ lines.push(` ${'─'.repeat(SECT_WIDTH)}`);
64
+ for (const t of tools.slice(0, 8)) lines.push(` ${fit(toolRow(t), SECT_WIDTH)}`);
65
+ if (tools.length > 8) lines.push(` ${fit(`... and ${tools.length - 8} more tool groups`, SECT_WIDTH)}`);
66
+ lines.push('');
67
+ lines.push(` ${fit('Available Skills', SECT_WIDTH)}`);
68
+ lines.push(` ${'─'.repeat(SECT_WIDTH)}`);
69
+ for (const s of skills.slice(0, 8)) lines.push(` ${fit(skillRow(s), SECT_WIDTH)}`);
70
+ if (skills.length > 8) lines.push(` ${fit(`... and ${skills.length - 8} more skill groups`, SECT_WIDTH)}`);
74
71
  return lines;
75
72
  }
76
73
 
@@ -89,10 +86,14 @@ function buildFooter(props) {
89
86
  }
90
87
 
91
88
  export function renderSplashToString(props, { columns = 80 } = {}) {
92
- void columns; // currently fixed-width at 80; <72 fallback is in cli.mjs
93
- const body = buildBody(props);
94
- const footer = buildFooter(props);
95
- return [...body, '', ...footer].join('\n');
89
+ void columns; // splash is fixed-width 80
90
+ return [
91
+ ...buildBanner(),
92
+ '',
93
+ ...buildToolsAndSkills(props),
94
+ '',
95
+ ...buildFooter(props),
96
+ ].join('\n');
96
97
  }
97
98
 
98
99
  export function Splash(props) {