lazyclaw 3.99.21 → 3.99.23

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
@@ -111,6 +111,19 @@ Then `lazyclaw chat` (or any other entry point that ends up calling a provider
111
111
 
112
112
  Defaults fall back gracefully: `planner` defaults to `cfg.provider`/`cfg.model`, `workers` defaults to `[planner]` (single-agent chain, still benefits from plan + synthesis structure). Self-recursion (`planner: "orchestrator"`) is rejected up front.
113
113
 
114
+ You can skip the JSON entirely and configure via `lazyclaw onboard` / `lazyclaw setup` (the picker lands on the orchestrator and walks you through a planner + workers wizard) **or** via the dedicated CLI:
115
+
116
+ ```bash
117
+ lazyclaw orchestrator status
118
+ lazyclaw orchestrator set-planner claude-cli:claude-opus-4-7
119
+ lazyclaw orchestrator workers add openai:gpt-4o
120
+ lazyclaw orchestrator workers add gemini:gemini-2.5-pro
121
+ lazyclaw orchestrator workers set claude-cli:claude-sonnet-4-6,nim:meta/llama-3.1-405b-instruct # bulk replace
122
+ lazyclaw orchestrator set-max-subtasks 5
123
+ lazyclaw orchestrator clear # wipe cfg.orchestrator
124
+ lazyclaw config set provider orchestrator # route chats through it
125
+ ```
126
+
114
127
  ## Launcher (no-arg `lazyclaw`)
115
128
 
116
129
  Running `lazyclaw` with no subcommand drops into an arrow-key launcher with every subcommand laid out as a menu. Navigation:
package/cli.mjs CHANGED
@@ -914,6 +914,8 @@ const SUBCOMMANDS = [
914
914
  'auth', 'pairing', 'nodes', 'message', 'workspace', 'browse', 'cron',
915
915
  // v3.99.6 — multi-step setup wizard + lazyclaw-only dashboard
916
916
  'setup', 'dashboard',
917
+ // v3.99.22 — multi-agent orchestrator config
918
+ 'orchestrator',
917
919
  ];
918
920
 
919
921
  const SUBCOMMAND_SUBS = {
@@ -929,6 +931,7 @@ const SUBCOMMAND_SUBS = {
929
931
  message: ['list', 'add', 'remove', 'send'],
930
932
  workspace: ['list', 'init', 'show', 'remove', 'path'],
931
933
  cron: ['list', 'add', 'remove', 'show', 'sync', 'run'],
934
+ orchestrator: ['status', 'set-planner', 'workers', 'set-max-subtasks', 'clear'],
932
935
  };
933
936
 
934
937
  function bashCompletion() {
@@ -1185,6 +1188,7 @@ const HELP_DETAILS = {
1185
1188
  cron: 'Usage: lazyclaw cron <list | add <name> "<cron-spec>" -- <cmd> ... | remove <name> | show <name> | sync | run <name>>\n Schedule recurring agent runs. macOS uses launchd (~/Library/LaunchAgents/com.lazyclaw.<name>.plist); Linux / WSL uses the user crontab.\n Cron spec is the standard 5-field form (minute hour dom month dow). Supports *, range a-b, list a,b,c, step */N.\n add: pass the command after `--`. Typical use:\n lazyclaw cron add daily-summary "0 9 * * 1-5" -- lazyclaw agent "Summarise today\'s TODOs"\n list / show: read from cfg.cron[name] (config is the source of truth).\n sync: re-installs every job in cfg.cron into the system scheduler — handy after a reinstall.\n run: one-shot in-process execution of the named job; the OS scheduler does the same thing on its trigger.\n Logs: ~/.lazyclaw/logs/cron-<name>.{out,err}.log (macOS launchd path).',
1186
1189
  setup: 'Usage: lazyclaw setup [--skip-test]\n OpenClaw-style multi-step first-run wizard. Walks through:\n 1. Provider + model + api-key (delegates to onboard --pick)\n 2. Optional workspace init (AGENTS.md / SOUL.md / TOOLS.md)\n 3. Optional skill bundle install from GitHub\n 4. Optional outbound webhook (Slack / Discord)\n 5. Reachability test against the picked provider\n Each optional step takes Enter or "skip" to bypass. Re-runnable safely.\n Also fires automatically on first run when `lazyclaw` is invoked with no config.',
1187
1190
  dashboard: 'Usage: lazyclaw dashboard [--port <N>] [--no-open]\n Launches the lazyclaw-only web UI on http://127.0.0.1:<port> (default 19600) and opens it in the default browser.\n Wraps `lazyclaw daemon` + a static HTML; no Python / lazyclaude dashboard required.\n Tabs: Chat · Sessions · Skills · Workspace · Providers · Status. Each tab calls existing daemon endpoints.\n --no-open keeps the browser closed (handy for SSH / headless / dev). The bound URL is always printed to stdout.',
1191
+ orchestrator: 'Usage: lazyclaw orchestrator <status | set-planner <provider[:model]> | workers add <spec> | workers remove <spec> | workers set <spec,spec,...> | workers clear | set-max-subtasks <N> | clear>\n Read/write cfg.orchestrator without editing config.json by hand.\n status — print {planner, workers, maxSubtasks} as JSON; lists registered providers for reference.\n set-planner — replace the planner spec ("provider" or "provider:model"). "orchestrator" itself is rejected (self-recursion).\n workers add — append a worker (idempotent — duplicates skipped).\n workers remove — drop a worker by exact match. Idempotent.\n workers set — replace the whole list (comma-separated specs).\n workers clear — empty the workers list.\n set-max-subtasks <N> — cap subtasks per request, clamped 1..10 (default 5).\n clear — delete the cfg.orchestrator block entirely.\n Pair with: `lazyclaw config set provider orchestrator` to route chats through it.',
1188
1192
  };
1189
1193
 
1190
1194
  function cmdHelp(name) {
@@ -1465,42 +1469,52 @@ function _attachGhostAutocomplete(rl) {
1465
1469
  // two of the inner lines were 33 cols vs the others' 32, so the
1466
1470
  // ╮ rendered into the next line).
1467
1471
  function _renderBanner(version) {
1468
- const W = 30;
1469
- const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
1470
- // 24-bit color so the mascot reads in the same warm orange as the
1471
- // dist/index.html SVG (#d97757). Falls back gracefully on terminals
1472
- // that ignore truecolor — the glyphs are visible regardless.
1473
- const orange = (s) => `\x1b[38;2;217;119;87m${s}\x1b[0m`;
1474
- // Inner content of each banner row — DO NOT pad here, the wrapper
1475
- // does it. Backslashes are JS-escaped so each `\\` renders as one
1476
- // literal `\` in the output.
1477
- const inner = [
1478
- ' _',
1479
- ' | |__ _ _____ _ _',
1480
- " | / _` |_ / || | '_|",
1481
- ' |_\\__,_/__\\_, |_|',
1482
- ' LazyClaw |__/ ' + String(version || '?.?.?').padEnd(10).slice(0, 10),
1472
+ // Rebuilt from the Claude Design handoff bundle (v0.1 mascot sheet):
1473
+ // Claude's asterisk star wearing a crab/crustacean helmet with two
1474
+ // antenna-claws. 10-line "big ASCII" form fits a terminal banner
1475
+ // without zoom and reads at any monospace font that has the
1476
+ // box-drawing + geometric-shape glyphs.
1477
+ //
1478
+ // Palette (CLAUDE ORIGINAL): helmet body #c33d2a, helmet shadow
1479
+ // #7a1f15, star body #d97757, star shadow #a04f32, eyes ink
1480
+ // #c7bca6 / muted slits. Truecolor ANSI; degrades gracefully on
1481
+ // terminals that ignore it.
1482
+ const helmet = (s) => `\x1b[38;2;195;61;42m${s}\x1b[0m`; // #c33d2a
1483
+ const star = (s) => `\x1b[38;2;217;119;87m${s}\x1b[0m`; // #d97757 — Claude orange
1484
+ const ink = (s) => `\x1b[38;2;241;234;217m${s}\x1b[0m`; // #f1ead9 paper-ink
1485
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1486
+ const muted = (s) => `\x1b[38;2;122;110;95m${s}\x1b[0m`; // #7a6e5f
1487
+
1488
+ const v = String(version || '?.?.?');
1489
+ // Left column — sprite. Right column — wordmark + tagline, aligned
1490
+ // to the helmet's eye-row / brim. The trailing spaces preserve the
1491
+ // grid so any caller padding the lines (or copying them) doesn't
1492
+ // get ragged edges.
1493
+ const left = [
1494
+ ` ${helmet('▲')} ${helmet('▲')} `,
1495
+ ` ${helmet('│')} ${helmet('│')} `,
1496
+ ` ${helmet('╔══════════╗')} `,
1497
+ ` ${helmet('║')} ${helmet('║')} `,
1498
+ ` ${helmet('║')} ${muted('──')} ${muted('──')} ${helmet('║')} `,
1499
+ ` ${helmet('║')} ${helmet('║')} `,
1500
+ ` ${helmet('╚══════════╝')} `,
1501
+ ` ${star('✦')} `,
1502
+ ` ${star('╱|╲')} `,
1503
+ ` ${star('╱ | ╲')} `,
1483
1504
  ];
1484
- // Pixel-art mascot mirrored from the lazyclaude SPA's #claudeMascot
1485
- // SVG (orange rectangles → block characters). Squashed to 5 rows so
1486
- // it lines up with `inner` in the banner. Eye sockets are left blank
1487
- // (the SVG fills them with #000); a hollow gap reads as eyes against
1488
- // the orange body in any monospace font.
1489
- const mascot = [
1505
+ const right = [
1506
+ '',
1507
+ '',
1508
+ '',
1509
+ ` ${ink('lazyclaw')} ${dim('v' + v)}`,
1510
+ ` ${dim('a sleepy 8-bit')}`,
1511
+ ` ${dim('terminal assistant')}`,
1512
+ '',
1513
+ '',
1490
1514
  '',
1491
- orange(' ██ ██'),
1492
- orange(' ██████████████'),
1493
- orange(' ██ ') + '██' + orange(' ') + '██' + orange(' ██'),
1494
- orange(' ██████████████'),
1495
- orange(' ██ ██'),
1496
1515
  '',
1497
1516
  ];
1498
- const banner = [
1499
- '╭' + '─'.repeat(W) + '╮',
1500
- ...inner.map((s) => '│' + s.padEnd(W).slice(0, W) + '│'),
1501
- '╰' + '─'.repeat(W) + '╯',
1502
- ];
1503
- return banner.map((l, i) => ' ' + accent(l) + (mascot[i] ? ' ' + mascot[i] : ''));
1517
+ return left.map((l, i) => ' ' + l + (right[i] || ''));
1504
1518
  }
1505
1519
 
1506
1520
  function _printChatBanner(activeProvName, activeModel, version) {
@@ -1835,7 +1849,17 @@ async function _pickProviderInteractive() {
1835
1849
  provider = picked;
1836
1850
  }
1837
1851
 
1838
- // ── Step 3 — model ────────────────────────────────────────────
1852
+ // ── Step 3 — model (or, for composite providers, a config wizard) ───
1853
+ // The orchestrator (and any future composite provider) has no model
1854
+ // of its own — it dispatches to other providers. Step 3 routes
1855
+ // through a custom wizard instead of the standard model picker.
1856
+ const providerMeta = (_registryMod.PROVIDER_INFO || {})[provider.id] || {};
1857
+ if (providerMeta.composite || provider.id === 'orchestrator') {
1858
+ const result = await _setupOrchestratorInteractive();
1859
+ if (result === 'CANCEL') return null;
1860
+ if (result === 'BACK') return _pickProviderInteractive();
1861
+ return { provider: provider.id, model: 'orchestrator' };
1862
+ }
1839
1863
  const picked = await _pickModelInteractive(provider.id, {
1840
1864
  titlePrefix: 'LazyClaw setup — Step 3 of 3:',
1841
1865
  onBack: 'restart',
@@ -1845,6 +1869,125 @@ async function _pickProviderInteractive() {
1845
1869
  return { provider: provider.id, model: picked };
1846
1870
  }
1847
1871
 
1872
+ // Step-3 alternative for composite providers (currently only the
1873
+ // orchestrator). Builds `cfg.orchestrator = { planner, workers,
1874
+ // maxSubtasks }` interactively and persists it before returning.
1875
+ //
1876
+ // planner: single picker over registered non-composite providers.
1877
+ // workers: multi-select with a running list + add/remove/done loop.
1878
+ // maxSubtasks: typed integer, default 5.
1879
+ async function _setupOrchestratorInteractive() {
1880
+ const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
1881
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1882
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1883
+ const ok = (s) => `\x1b[32m${s}\x1b[0m`;
1884
+ const info = _registryMod.PROVIDER_INFO || {};
1885
+ const eligibleNames = Object.keys(_registryMod.PROVIDERS).filter((n) => n !== 'orchestrator' && n !== 'mock');
1886
+ if (eligibleNames.length === 0) {
1887
+ process.stdout.write('\n' + accent('orchestrator setup') + ': no eligible workers — register a real provider first.\n');
1888
+ await _quickPrompt(' press Enter to continue ');
1889
+ return 'CANCEL';
1890
+ }
1891
+ const cfg = readConfig();
1892
+ const existing = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
1893
+
1894
+ // ── Pick planner ─────────────────────────────────────────────────
1895
+ const plannerItems = eligibleNames.map((name) => {
1896
+ const m = info[name] || {};
1897
+ const defaultModel = m.defaultModel || '';
1898
+ return {
1899
+ id: `${name}${defaultModel ? ':' + defaultModel : ''}`,
1900
+ label: m.label && m.label !== name ? `${name} — ${m.label}` : name,
1901
+ desc: defaultModel ? `default model: ${defaultModel}` : '',
1902
+ };
1903
+ });
1904
+ const plannerPick = await _arrowMenu({
1905
+ title: 'LazyClaw setup — Step 3 of 3: orchestrator — pick the planner',
1906
+ subtitle: 'The planner decomposes the user request into subtasks and writes the final synthesis. Strong reasoning models work best here.',
1907
+ items: plannerItems,
1908
+ searchable: true,
1909
+ defaultIdx: Math.max(0, plannerItems.findIndex((p) => p.id === existing.planner)),
1910
+ });
1911
+ if (plannerPick === 'CANCEL') return 'CANCEL';
1912
+ if (plannerPick === 'BACK') return 'BACK';
1913
+ const planner = plannerPick.id;
1914
+
1915
+ // ── Pick workers (iterative add/remove) ──────────────────────────
1916
+ const workers = Array.isArray(existing.workers) ? existing.workers.slice() : [];
1917
+ while (true) {
1918
+ process.stdout.write('\x1b[2J\x1b[H');
1919
+ process.stdout.write(accent('Orchestrator workers') + '\n');
1920
+ process.stdout.write(dim('Subtasks are dispatched round-robin across this list.') + '\n\n');
1921
+ if (workers.length === 0) {
1922
+ process.stdout.write(' ' + dim('(none yet — add at least one)') + '\n\n');
1923
+ } else {
1924
+ workers.forEach((w, i) => {
1925
+ process.stdout.write(` ${i + 1}. ${ok(w)}\n`);
1926
+ });
1927
+ process.stdout.write('\n');
1928
+ }
1929
+ const items = [
1930
+ { id: '__add__', label: '+ Add a worker', desc: 'pick from registered providers' },
1931
+ { id: '__remove__', label: '- Remove a worker', desc: workers.length ? 'pick which entry to drop' : '(nothing to remove)' },
1932
+ { id: '__done__', label: `Done${workers.length ? ` (${workers.length} worker${workers.length === 1 ? '' : 's'})` : ' — at least one worker required'}`, desc: workers.length ? 'save cfg.orchestrator and finish' : 'add one worker first' },
1933
+ ];
1934
+ const action = await _arrowMenu({
1935
+ title: 'LazyClaw setup — orchestrator workers',
1936
+ subtitle: `Planner: ${planner}`,
1937
+ items,
1938
+ });
1939
+ if (action === 'CANCEL') return 'CANCEL';
1940
+ if (action === 'BACK') return 'BACK';
1941
+ if (action.id === '__add__') {
1942
+ const wPick = await _arrowMenu({
1943
+ title: 'Add worker',
1944
+ subtitle: 'Picked entries are appended to the workers list.',
1945
+ items: plannerItems.filter((p) => !workers.includes(p.id)),
1946
+ searchable: true,
1947
+ });
1948
+ if (wPick === 'CANCEL' || wPick === 'BACK') continue;
1949
+ workers.push(wPick.id);
1950
+ continue;
1951
+ }
1952
+ if (action.id === '__remove__') {
1953
+ if (!workers.length) continue;
1954
+ const rPick = await _arrowMenu({
1955
+ title: 'Remove worker',
1956
+ subtitle: 'Highlighted entry is removed from the list.',
1957
+ items: workers.map((w) => ({ id: w, label: w })),
1958
+ });
1959
+ if (rPick === 'CANCEL' || rPick === 'BACK') continue;
1960
+ const idx = workers.indexOf(rPick.id);
1961
+ if (idx >= 0) workers.splice(idx, 1);
1962
+ continue;
1963
+ }
1964
+ if (action.id === '__done__') {
1965
+ if (workers.length === 0) continue;
1966
+ break;
1967
+ }
1968
+ }
1969
+
1970
+ // ── maxSubtasks ──────────────────────────────────────────────────
1971
+ const defaultMax = Number.isFinite(existing.maxSubtasks) && existing.maxSubtasks > 0
1972
+ ? Math.min(10, existing.maxSubtasks)
1973
+ : 5;
1974
+ const rawMax = (await _quickPrompt(` ${bold('maxSubtasks')} ${dim(`(2..10, blank → ${defaultMax}):`)} `)).trim();
1975
+ let maxSubtasks = defaultMax;
1976
+ if (rawMax) {
1977
+ const n = parseInt(rawMax, 10);
1978
+ if (Number.isFinite(n) && n >= 1) maxSubtasks = Math.min(10, Math.max(1, n));
1979
+ }
1980
+
1981
+ // ── Persist ──────────────────────────────────────────────────────
1982
+ cfg.orchestrator = { planner, workers, maxSubtasks };
1983
+ writeConfig(cfg);
1984
+ process.stdout.write('\n');
1985
+ process.stdout.write(` ${ok('✓ orchestrator saved')} ${dim('→')} ` +
1986
+ `planner ${ok(planner)} · ${workers.length} worker${workers.length === 1 ? '' : 's'} · maxSubtasks ${maxSubtasks}\n`);
1987
+ await _quickPrompt(' press Enter to continue ');
1988
+ return { ok: true };
1989
+ }
1990
+
1848
1991
  // Pause the chat REPL's readline + ghost-autocomplete while a sub-picker
1849
1992
  // (provider / model arrow menu) takes over the terminal. The sub-picker
1850
1993
  // installs its own `keypress` listener and toggles raw mode; the chat's
@@ -3722,6 +3865,126 @@ async function cmdProviders(sub, positional, flags = {}) {
3722
3865
  }
3723
3866
  }
3724
3867
 
3868
+ // `lazyclaw orchestrator` — read/write the cfg.orchestrator section
3869
+ // without editing config.json by hand. Mirrors the shape `lazyclaw
3870
+ // providers` / `lazyclaw rates` already use.
3871
+ //
3872
+ // Subcommands:
3873
+ // status Print current planner / workers / maxSubtasks as JSON.
3874
+ // set-planner <provider[:model]> Replace the planner spec.
3875
+ // workers add <provider[:model]> Append a worker (idempotent — duplicates skipped).
3876
+ // workers remove <provider[:model]> Drop a worker by exact match. Idempotent.
3877
+ // workers clear Empty the workers list.
3878
+ // workers set <provider[:model],...> Replace the whole list (comma-separated).
3879
+ // set-max-subtasks <N> Cap the number of subtasks (clamped 1..10).
3880
+ // clear Delete the entire cfg.orchestrator block.
3881
+ async function cmdOrchestrator(sub, positional, _flags = {}) {
3882
+ await ensureRegistry();
3883
+ const cfg = readConfig();
3884
+ const orch = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
3885
+ const known = Object.keys(_registryMod.PROVIDERS);
3886
+ const validateSpec = (spec) => {
3887
+ if (!spec) throw new Error('provider spec required (e.g. "claude-cli" or "openai:gpt-4o")');
3888
+ const colon = spec.indexOf(':');
3889
+ const provName = colon > 0 ? spec.slice(0, colon) : spec;
3890
+ if (provName === 'orchestrator') throw new Error('"orchestrator" cannot reference itself — pick a real provider');
3891
+ if (!known.includes(provName)) {
3892
+ throw new Error(`unknown provider "${provName}" — registered: ${known.join(', ')}`);
3893
+ }
3894
+ return spec;
3895
+ };
3896
+ const saveAndPrint = (next) => {
3897
+ if (next === null) delete cfg.orchestrator;
3898
+ else cfg.orchestrator = next;
3899
+ writeConfig(cfg);
3900
+ console.log(JSON.stringify(cfg.orchestrator || null, null, 2));
3901
+ };
3902
+ switch (sub) {
3903
+ case undefined:
3904
+ case 'status': {
3905
+ console.log(JSON.stringify({
3906
+ ok: true,
3907
+ configured: !!cfg.orchestrator,
3908
+ planner: orch.planner || null,
3909
+ workers: Array.isArray(orch.workers) ? orch.workers : [],
3910
+ maxSubtasks: Number.isFinite(orch.maxSubtasks) ? orch.maxSubtasks : null,
3911
+ knownProviders: known,
3912
+ }, null, 2));
3913
+ return;
3914
+ }
3915
+ case 'set-planner': {
3916
+ try {
3917
+ const spec = validateSpec(positional[0]);
3918
+ saveAndPrint({ ...orch, planner: spec });
3919
+ } catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
3920
+ return;
3921
+ }
3922
+ case 'workers': {
3923
+ const wsub = positional[0];
3924
+ const workers = Array.isArray(orch.workers) ? orch.workers.slice() : [];
3925
+ switch (wsub) {
3926
+ case 'add': {
3927
+ try {
3928
+ const spec = validateSpec(positional[1]);
3929
+ if (!workers.includes(spec)) workers.push(spec);
3930
+ saveAndPrint({ ...orch, workers });
3931
+ } catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
3932
+ return;
3933
+ }
3934
+ case 'remove': {
3935
+ const spec = positional[1];
3936
+ if (!spec) { console.error('orchestrator: workers remove <provider[:model]>'); process.exit(2); }
3937
+ const idx = workers.indexOf(spec);
3938
+ if (idx >= 0) workers.splice(idx, 1);
3939
+ saveAndPrint({ ...orch, workers });
3940
+ return;
3941
+ }
3942
+ case 'clear': {
3943
+ saveAndPrint({ ...orch, workers: [] });
3944
+ return;
3945
+ }
3946
+ case 'set': {
3947
+ const raw = positional[1] || '';
3948
+ const specs = raw.split(',').map((s) => s.trim()).filter(Boolean);
3949
+ try {
3950
+ specs.forEach(validateSpec);
3951
+ saveAndPrint({ ...orch, workers: specs });
3952
+ } catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
3953
+ return;
3954
+ }
3955
+ default: {
3956
+ console.error('Usage: lazyclaw orchestrator workers <add <spec> | remove <spec> | clear | set <spec,spec,...>>');
3957
+ process.exit(2);
3958
+ }
3959
+ }
3960
+ }
3961
+ case 'set-max-subtasks': {
3962
+ const n = parseInt(positional[0], 10);
3963
+ if (!Number.isFinite(n) || n < 1) { console.error('orchestrator: set-max-subtasks <N> (1..10)'); process.exit(2); }
3964
+ saveAndPrint({ ...orch, maxSubtasks: Math.min(10, Math.max(1, n)) });
3965
+ return;
3966
+ }
3967
+ case 'clear': {
3968
+ saveAndPrint(null);
3969
+ return;
3970
+ }
3971
+ default: {
3972
+ console.error(
3973
+ 'Usage:\n' +
3974
+ ' lazyclaw orchestrator status\n' +
3975
+ ' lazyclaw orchestrator set-planner <provider[:model]>\n' +
3976
+ ' lazyclaw orchestrator workers add <provider[:model]>\n' +
3977
+ ' lazyclaw orchestrator workers remove <provider[:model]>\n' +
3978
+ ' lazyclaw orchestrator workers set <provider[:model],...>\n' +
3979
+ ' lazyclaw orchestrator workers clear\n' +
3980
+ ' lazyclaw orchestrator set-max-subtasks <N>\n' +
3981
+ ' lazyclaw orchestrator clear'
3982
+ );
3983
+ process.exit(2);
3984
+ }
3985
+ }
3986
+ }
3987
+
3725
3988
  async function cmdSessions(sub, positional, flags = {}) {
3726
3989
  const sessionsMod = await import('./sessions.mjs');
3727
3990
  const cfgDir = path.dirname(configPath());
@@ -4614,6 +4877,11 @@ async function main() {
4614
4877
  await cmdProviders(sub, rest.positional.slice(1), rest.flags);
4615
4878
  break;
4616
4879
  }
4880
+ case 'orchestrator': {
4881
+ const sub = rest.positional[0];
4882
+ await cmdOrchestrator(sub, rest.positional.slice(1), rest.flags);
4883
+ break;
4884
+ }
4617
4885
  case 'skills': {
4618
4886
  const sub = rest.positional[0];
4619
4887
  await cmdSkills(sub, rest.positional.slice(1), rest.flags);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyclaw",
3
- "version": "3.99.21",
3
+ "version": "3.99.23",
4
4
  "description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama and orchestrating multi-step LLM workflows. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
5
5
  "keywords": [
6
6
  "claude",
@@ -124,6 +124,14 @@ export function makeOrchestratorProvider(opts = {}) {
124
124
  const fallbackSpec = cfg.provider && cfg.provider !== 'orchestrator'
125
125
  ? `${cfg.provider}${cfg.model ? ':' + cfg.model : ''}`
126
126
  : 'claude-cli';
127
+ // First-run hint when cfg.orchestrator is missing entirely. The
128
+ // fallback path below still works (planner = cfg.provider, single
129
+ // worker = same), but the user almost certainly meant to opt in
130
+ // explicitly — surface the shortest valid CLI to set it up.
131
+ if (!cfg.orchestrator) {
132
+ yield `> orchestrator: \`cfg.orchestrator\` is not set. Defaulting to a single-agent chain on \`${fallbackSpec}\`.\n` +
133
+ `> Configure properly: \`lazyclaw orchestrator set-planner ${fallbackSpec}\` then \`lazyclaw orchestrator workers add <provider:model>\` (one per agent).\n\n`;
134
+ }
127
135
  const plannerSpec = String(o.planner || fallbackSpec);
128
136
  const workerSpecs = Array.isArray(o.workers) && o.workers.length
129
137
  ? o.workers.map(String)
@@ -42,24 +42,14 @@
42
42
  gap: 14px;
43
43
  }
44
44
  .logo { font-weight: 700; font-size: 16px; color: var(--accent); display: flex; align-items: center; gap: 10px; }
45
- .logo .mascot { width: 36px; height: 32px; flex: none; }
45
+ .logo .mascot { width: 44px; height: 44px; flex: none; image-rendering: pixelated; image-rendering: crisp-edges; }
46
46
  .ver { color: var(--dim); font-size: 11px; }
47
- /* lazyclaude pixel-mascot — copied verbatim from dist/index.html so the
48
- lazyclaw web dashboard wears the same character as the larger SPA. */
49
- .mascot .mj-body { animation: mj-jump 1s ease-in-out infinite; transform-origin: center bottom; }
50
- .mascot .mj-shadow { animation: mj-sh 1s ease-in-out infinite; }
51
- .mascot .mj-la { animation: mj-wl 1s ease-in-out infinite; transform-origin: right center; }
52
- .mascot .mj-ra { animation: mj-wr 1s ease-in-out infinite; transform-origin: left center; }
53
- .mascot .mj-le { animation: mj-ear 1s ease-in-out infinite; transform-origin: center bottom; }
54
- .mascot .mj-re { animation: mj-ear 1s ease-in-out infinite .1s; transform-origin: center bottom; }
55
- @keyframes mj-jump { 0%, 100% { transform: translateY(0) scaleY(1) scaleX(1); } 30% { transform: translateY(-10px) scaleY(1.1) scaleX(.95); } 50% { transform: translateY(-12px) scaleY(1.05) scaleX(.98); } 80% { transform: translateY(-3px) scaleY(.95) scaleX(1.05); } }
56
- @keyframes mj-sh { 0%, 100% { transform: scaleX(1); opacity: .25; } 50% { transform: scaleX(.4); opacity: .08; } }
57
- @keyframes mj-wl { 0%, 100% { transform: rotate(0); } 50% { transform: rotate(-25deg); } }
58
- @keyframes mj-wr { 0%, 100% { transform: rotate(0); } 50% { transform: rotate(25deg); } }
59
- @keyframes mj-ear { 0%, 100% { transform: scaleY(1); } 40% { transform: scaleY(1.2); } 60% { transform: scaleY(.85); } }
60
- @media (prefers-reduced-motion: reduce) {
61
- .mascot .mj-body, .mascot .mj-shadow, .mascot .mj-la, .mascot .mj-ra, .mascot .mj-le, .mascot .mj-re { animation: none; }
62
- }
47
+ /* lazyclaw 16x16 pixel mascot — Claude Design handoff (mascot sheet
48
+ v0.1, "claude original" palette). Claude's asterisk star (#d97757)
49
+ worn under a crustacean helmet (#c33d2a) with two antenna-claws.
50
+ Idle pose (sleepy slits). Hover gently brightens the helmet. */
51
+ .mascot { transition: filter 0.2s ease; }
52
+ .logo:hover .mascot { filter: drop-shadow(0 0 6px rgba(217, 119, 87, 0.45)); }
63
53
  nav.tabs {
64
54
  display: flex;
65
55
  gap: 2px;
@@ -286,22 +276,39 @@
286
276
  <body>
287
277
  <header>
288
278
  <div class="logo">
289
- <svg class="mascot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 90" aria-hidden="true">
290
- <ellipse class="mj-shadow" cx="50" cy="82" rx="22" ry="5" fill="#000"/>
291
- <g class="mj-body">
292
- <rect class="mj-le" x="22" y="10" width="8" height="14" fill="#d97757"/>
293
- <rect class="mj-re" x="70" y="10" width="8" height="14" fill="#d97757"/>
294
- <rect x="18" y="24" width="64" height="4" fill="#d97757"/>
295
- <rect x="14" y="28" width="72" height="32" fill="#d97757"/>
296
- <rect x="30" y="34" width="8" height="10" fill="#000"/>
297
- <rect x="62" y="34" width="8" height="10" fill="#000"/>
298
- <rect class="mj-la" x="2" y="36" width="12" height="8" fill="#d97757"/>
299
- <rect class="mj-ra" x="86" y="36" width="12" height="8" fill="#d97757"/>
300
- <rect x="24" y="60" width="12" height="14" fill="#d97757"/>
301
- <rect x="64" y="60" width="12" height="14" fill="#d97757"/>
302
- </g>
279
+ <svg class="mascot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" shape-rendering="crispEdges" aria-label="lazyclaw mascot" role="img">
280
+ <!-- antennae / claws -->
281
+ <rect x="3" y="1" width="1" height="1" fill="#a02b1c"/>
282
+ <rect x="12" y="1" width="1" height="1" fill="#a02b1c"/>
283
+ <rect x="3" y="2" width="1" height="1" fill="#a02b1c"/>
284
+ <rect x="12" y="2" width="1" height="1" fill="#a02b1c"/>
285
+ <!-- helmet -->
286
+ <rect x="3" y="3" width="10" height="1" fill="#c33d2a"/>
287
+ <rect x="2" y="4" width="12" height="1" fill="#c33d2a"/>
288
+ <rect x="1" y="5" width="2" height="1" fill="#c33d2a"/>
289
+ <rect x="3" y="5" width="1" height="1" fill="#ff8a6a"/>
290
+ <rect x="4" y="5" width="8" height="1" fill="#c33d2a"/>
291
+ <rect x="12" y="5" width="1" height="1" fill="#ff8a6a"/>
292
+ <rect x="13" y="5" width="2" height="1" fill="#c33d2a"/>
293
+ <rect x="1" y="6" width="14" height="1" fill="#c33d2a"/>
294
+ <rect x="2" y="7" width="12" height="1" fill="#c33d2a"/>
295
+ <!-- star body (Claude asterisk) — top edge below helmet brim -->
296
+ <rect x="3" y="8" width="10" height="1" fill="#d97757"/>
297
+ <!-- face row (idle: sleepy slit eyes) -->
298
+ <rect x="2" y="9" width="3" height="1" fill="#d97757"/>
299
+ <rect x="5" y="9" width="2" height="1" fill="#1a1410"/>
300
+ <rect x="7" y="9" width="2" height="1" fill="#d97757"/>
301
+ <rect x="9" y="9" width="2" height="1" fill="#1a1410"/>
302
+ <rect x="11" y="9" width="3" height="1" fill="#d97757"/>
303
+ <!-- star arms + tip -->
304
+ <rect x="1" y="10" width="14" height="1" fill="#d97757"/>
305
+ <rect x="0" y="11" width="16" height="1" fill="#d97757"/>
306
+ <rect x="1" y="12" width="14" height="1" fill="#d97757"/>
307
+ <rect x="3" y="13" width="10" height="1" fill="#d97757"/>
308
+ <rect x="5" y="14" width="6" height="1" fill="#d97757"/>
309
+ <rect x="7" y="15" width="2" height="1" fill="#d97757"/>
303
310
  </svg>
304
- <span>LazyClaw</span>
311
+ <span>lazyclaw</span>
305
312
  </div>
306
313
  <div class="ver" id="version">…</div>
307
314
  </header>