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 +13 -0
- package/cli.mjs +301 -33
- package/package.json +1 -1
- package/providers/orchestrator.mjs +8 -0
- package/web/dashboard.html +39 -32
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
|
-
|
|
1469
|
-
|
|
1470
|
-
//
|
|
1471
|
-
//
|
|
1472
|
-
//
|
|
1473
|
-
|
|
1474
|
-
//
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
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
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
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.
|
|
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)
|
package/web/dashboard.html
CHANGED
|
@@ -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:
|
|
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
|
-
/*
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.mascot
|
|
52
|
-
.mascot
|
|
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
|
|
290
|
-
|
|
291
|
-
<
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
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>
|
|
311
|
+
<span>lazyclaw</span>
|
|
305
312
|
</div>
|
|
306
313
|
<div class="ver" id="version">…</div>
|
|
307
314
|
</header>
|