lazyclaw 5.0.5 → 5.0.6

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
@@ -1011,6 +1011,8 @@ const SUBCOMMANDS = [
1011
1011
  'agent', 'team', 'task',
1012
1012
  // v5.0 Phase G — persona compose + cross-tool import (spec §9, §10)
1013
1013
  'personality', 'migrate', 'hermes', 'openclaw',
1014
+ // v5.0.6 — arrow-key launcher menu (was the no-arg default in v5.0.5-)
1015
+ 'menu',
1014
1016
  // v5.0 Phase H1 — trajectory exporter (spec §2.7)
1015
1017
  'trajectories',
1016
1018
  ];
@@ -1634,30 +1636,32 @@ function _renderBanner(version) {
1634
1636
  ];
1635
1637
  }
1636
1638
 
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.
1639
+ // v5 hero banner — ANSI Shadow LAZYCLAW wordmark stacked on top of the
1640
+ // braille sloth (tui/banner.generated.mjs + tui/wordmark.mjs). Left-aligned
1641
+ // with a 2-cell margin so wide terminals don't push the art to the right.
1640
1642
  // 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;
1643
+ let _bannerAssetsCache = null;
1644
+ async function _loadBannerAssets() {
1645
+ if (_bannerAssetsCache !== null) return _bannerAssetsCache;
1644
1646
  try {
1645
1647
  const { banner } = await import('./tui/banner.generated.mjs');
1646
- _slothBannerRowsCache = banner;
1648
+ const { wordmark } = await import('./tui/wordmark.mjs');
1649
+ _bannerAssetsCache = { banner, wordmark };
1647
1650
  } catch {
1648
- _slothBannerRowsCache = null;
1651
+ _bannerAssetsCache = null;
1649
1652
  }
1650
- return _slothBannerRowsCache;
1653
+ return _bannerAssetsCache;
1651
1654
  }
1652
1655
 
1653
1656
  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));
1657
+ const a = await _loadBannerAssets();
1658
+ if (!a) return _renderBanner(version); // missing tarball asset → v4 figlet
1659
1659
  const v = String(version || '?.?.?');
1660
- rows.push(_orange((' '.repeat(pad) + `lazyclaw v${v}`).padEnd(TERM_W)));
1660
+ const rows = [];
1661
+ for (const r of a.wordmark.rows) rows.push(_orange(' ' + r));
1662
+ rows.push('');
1663
+ for (const r of a.banner.rows) rows.push(_orange(' ' + r));
1664
+ rows.push(_orange(' ' + `lazyclaw v${v}`));
1661
1665
  return rows;
1662
1666
  }
1663
1667
 
@@ -6944,18 +6948,17 @@ async function main() {
6944
6948
  const argv = process.argv.slice(2);
6945
6949
  const cmd = argv[0];
6946
6950
  const rest = parseArgs(argv.slice(1));
6947
- // No subcommand at all: drop into the interactive launcher when we
6948
- // can render one (TTY both ways), otherwise fall through to the
6949
- // historical "Usage: ..." line so scripts / piped callers stay
6950
- // predictable.
6951
+ // No subcommand at all: drop into chat REPL (v5.0.6 default). The
6952
+ // arrow-key launcher menu is still available via `lazyclaw menu`.
6953
+ // Non-TTY callers (pipes, scripts) get the historical usage line.
6951
6954
  if (cmd === undefined) {
6952
6955
  if (process.stdin.isTTY && process.stdout.isTTY) {
6953
- await cmdLauncher();
6954
- return;
6956
+ process.argv.splice(2, 0, 'chat');
6957
+ return main();
6955
6958
  }
6956
6959
  console.error('Usage: lazyclaw <' + SUBCOMMANDS.join('|') + '> ...');
6957
6960
  console.error('Run `lazyclaw help` for a one-line summary of each subcommand.');
6958
- console.error('Tip: launch in an interactive terminal to get the arrow-key menu.');
6961
+ console.error('Tip: launch in an interactive terminal to drop into chat.');
6959
6962
  process.exit(2);
6960
6963
  }
6961
6964
  switch (cmd) {
@@ -7349,6 +7352,11 @@ async function main() {
7349
7352
  cmdHelp(rest.positional[0]);
7350
7353
  break;
7351
7354
  }
7355
+ case 'menu': {
7356
+ // v5.0.6 — explicit arrow-key launcher (was the no-arg default in v5.0.5-).
7357
+ await cmdLauncher();
7358
+ break;
7359
+ }
7352
7360
  default:
7353
7361
  console.error('Usage: lazyclaw <' + SUBCOMMANDS.join('|') + '> ...');
7354
7362
  console.error('Run `lazyclaw help` for a one-line summary of each subcommand.');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyclaw",
3
- "version": "5.0.5",
3
+ "version": "5.0.6",
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",
package/tui/splash.mjs CHANGED
@@ -1,21 +1,32 @@
1
- // tui/splash.mjs — hero-banner launch splash.
1
+ // tui/splash.mjs — Hermes-style hero splash.
2
2
  //
3
- // Public surface:
4
- // - <Splash {...props} /> ink component for live REPL mount
5
- // - renderSplashToString(props) pure string builder used by tests
6
- // and by the non-TTY path.
3
+ // Layout (terminal-width responsive):
7
4
  //
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.
5
+ // ██╗ █████╗ ... ← wordmark (top, single-tone orange)
6
+ // ...
7
+ //
8
+ // ╭───── lazyclaw vX.Y.Z · trainer-split + FTS5 recall ─────────────╮
9
+ // │ [sloth braille] Available Tools │
10
+ // │ fs read · write · ... │
11
+ // │ Available Skills │
12
+ // │ N tools · M skills · /help for commands │
13
+ // ╰──────────────────────────────────────────────────────────────────╯
14
+ //
15
+ // provider · X · Y trainer · A · B
16
+ // /Users/o/cwd
17
+ // Session: 20260605_180543_2e0351
18
+ //
19
+ // Welcome to lazyclaw. Type your message or /help for commands.
20
+ // + Tip: ...
11
21
  import React from 'react';
12
22
  import { Box, Text } from 'ink';
13
23
  import stringWidth from 'string-width';
14
24
  import { theme } from './theme.mjs';
15
25
  import { banner } from './banner.generated.mjs';
26
+ import { wordmark } from './wordmark.mjs';
16
27
 
17
- const TERM_WIDTH = 80;
18
- const COL_WIDTH = 36; // 2 (lpad) + 36 + 2 (mid) + 36 + 2 (rpad) = 78 ≈ 80 with slack
28
+ const LMARGIN = ' ';
29
+ const TITLE = ' trainer-split · FTS5 recall · 6-backend sandbox ';
19
30
 
20
31
  function fit(text, max) {
21
32
  if (stringWidth(text) <= max) return text.padEnd(max);
@@ -35,74 +46,84 @@ function shortCwd(cwd) {
35
46
  }
36
47
 
37
48
  function toolRow({ category, sensitive, verbs }) {
38
- const label = sensitive ? `(sensitive) ${category}` : category;
39
- const labelWidth = sensitive ? 20 : 9;
49
+ const label = sensitive ? `${category}*` : category;
40
50
  const tail = verbs.slice(0, 6).join(' · ');
41
- const more = verbs.length > 6 ? ` (${verbs.length - 6} more)` : '';
42
- return `${fit(label, labelWidth)}${tail}${more}`;
51
+ return `${label.padEnd(12)} ${tail}`;
43
52
  }
44
53
 
45
54
  function skillRow({ group, names }) {
46
55
  const tail = names.slice(0, 6).join(' · ');
47
- const more = names.length > 6 ? ` (${names.length - 6} more)` : '';
48
- return `${fit(group, 9)}${tail}${more}`;
56
+ return `${group.padEnd(12)} ${tail}`;
49
57
  }
50
58
 
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
- }
59
+ export function renderSplashToString(props, opts = {}) {
60
+ const cols = opts.columns || process.stdout.columns || 100;
61
+ const TERM = Math.max(80, cols);
62
+ const PANEL_W = TERM - LMARGIN.length * 2;
63
+ const INNER = PANEL_W - 4; // 2 border + 2 padding
64
+ const SLOTH_W = banner.width;
65
+ const RIGHT_W = Math.max(40, INNER - SLOTH_W - 2);
57
66
 
58
- function buildToolsAndSkills(props) {
59
- const { tools = [], skills = [] } = props;
60
- const SECT_WIDTH = TERM_WIDTH - 4;
61
67
  const lines = [];
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)}`);
68
+
69
+ // Wordmark
70
+ for (const r of wordmark.rows) lines.push(LMARGIN + r);
66
71
  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)}`);
71
- return lines;
72
- }
73
72
 
74
- function buildFooter(props) {
73
+ // Panel top with title
74
+ const versionLabel = ` lazyclaw ${props.version || ''} ·${TITLE} `;
75
+ const dashLeft = '─'.repeat(8);
76
+ const dashRight = '─'.repeat(Math.max(2, PANEL_W - 2 - dashLeft.length - stringWidth(versionLabel)));
77
+ lines.push(`${LMARGIN}╭${dashLeft}${versionLabel}${dashRight}╮`);
78
+
79
+ // Right column content
80
+ const { tools = [], skills = [] } = props;
81
+ const right = [];
82
+ right.push('Available Tools');
83
+ for (const t of tools.slice(0, 8)) right.push(toolRow(t));
84
+ if (tools.length > 8) right.push(`(and ${tools.length - 8} more tool groups...)`);
85
+ right.push('');
86
+ right.push('Available Skills');
87
+ for (const s of skills.slice(0, 8)) right.push(skillRow(s));
88
+ if (skills.length > 8) right.push(`(and ${skills.length - 8} more skill groups...)`);
89
+ right.push('');
90
+ right.push(`${tools.length} tools · ${skills.length} skills · /help for commands`);
91
+
92
+ const sloth = banner.rows.slice();
93
+ while (sloth.length < right.length) sloth.push(' '.repeat(SLOTH_W));
94
+ while (right.length < sloth.length) right.push('');
95
+
96
+ for (let i = 0; i < sloth.length; i++) {
97
+ const l = sloth[i] || ' '.repeat(SLOTH_W);
98
+ const r = fit(right[i] || '', RIGHT_W);
99
+ lines.push(`${LMARGIN}│ ${l} ${r} │`);
100
+ }
101
+ lines.push(`${LMARGIN}╰${'─'.repeat(PANEL_W - 2)}╯`);
102
+ lines.push('');
103
+
104
+ // Provider / session info
75
105
  const { provider, model, trainer = {}, sessionId, cwd } = props;
76
106
  const tProv = trainer.provider || provider;
77
107
  const tModel = trainer.model || model;
78
- const sid = (sessionId || '').slice(0, 8);
79
- const cwdShort = shortCwd(cwd || process.cwd());
80
- return [
81
- fit(` provider · ${provider} · ${model}`, 56) + fit(`cwd · ${cwdShort}`, 22),
82
- fit(` trainer · ${tProv} · ${tModel} session ${sid}`, 78),
83
- fit(' slash · /help · /model · /trainer · /skills · /tools · /exit', 78),
84
- fit(' hint · Shift+Enter newline · Ctrl-R recall · Esc interrupt', 78),
85
- ];
86
- }
108
+ lines.push(`${LMARGIN}${provider} · ${model} · trainer ${tProv} · ${tModel}`);
109
+ lines.push(`${LMARGIN}${shortCwd(cwd || process.cwd())}`);
110
+ if (sessionId) lines.push(`${LMARGIN}Session: ${sessionId}`);
111
+ lines.push('');
112
+ lines.push(`${LMARGIN}Welcome to lazyclaw. Type your message or /help for commands.`);
113
+ lines.push(`${LMARGIN}+ Tip: trainer learns from your Claude Pro subscription at $0.`);
87
114
 
88
- export function renderSplashToString(props, { columns = 80 } = {}) {
89
- void columns; // splash is fixed-width 80
90
- return [
91
- ...buildBanner(),
92
- '',
93
- ...buildToolsAndSkills(props),
94
- '',
95
- ...buildFooter(props),
96
- ].join('\n');
115
+ return lines.join('\n');
97
116
  }
98
117
 
99
118
  export function Splash(props) {
100
119
  const lines = renderSplashToString(props).split('\n');
120
+ // Color the wordmark and sloth rows; everything else stays default.
121
+ const heroRowCount = wordmark.height + 1 + 1 + banner.height + 1; // word + blank + top + sloth + bottom
101
122
  return React.createElement(
102
123
  Box,
103
124
  { flexDirection: 'column' },
104
125
  lines.map((line, i) =>
105
- React.createElement(Text, { key: i, color: i < banner.height ? theme.fg : undefined }, line)
126
+ React.createElement(Text, { key: i, color: i < heroRowCount ? theme.fg : undefined }, line)
106
127
  )
107
128
  );
108
129
  }
@@ -0,0 +1,16 @@
1
+ // LAZYCLAW wordmark — ANSI Shadow style block-character rendering, shared
2
+ // between the chat splash and the no-arg launcher so the two entry points
3
+ // present identical visual identity. 67 cells × 6 rows.
4
+ export const wordmark = {
5
+ rows: [
6
+ "██╗ █████╗ ███████╗██╗ ██╗ ██████╗██╗ █████╗ ██╗ ██╗",
7
+ "██║ ██╔══██╗╚══███╔╝╚██╗ ██╔╝██╔════╝██║ ██╔══██╗██║ ██║",
8
+ "██║ ███████║ ███╔╝ ╚████╔╝ ██║ ██║ ███████║██║ █╗ ██║",
9
+ "██║ ██╔══██║ ███╔╝ ╚██╔╝ ██║ ██║ ██╔══██║██║███╗██║",
10
+ "███████╗██║ ██║███████╗ ██║ ╚██████╗███████╗██║ ██║╚███╔███╔╝",
11
+ "╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚══╝╚══╝ ",
12
+ ],
13
+ width: 67,
14
+ height: 6,
15
+ fg: "#FFB347"
16
+ };