lazyclaw 3.99.21 → 3.99.22

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) {
@@ -1835,7 +1839,17 @@ async function _pickProviderInteractive() {
1835
1839
  provider = picked;
1836
1840
  }
1837
1841
 
1838
- // ── Step 3 — model ────────────────────────────────────────────
1842
+ // ── Step 3 — model (or, for composite providers, a config wizard) ───
1843
+ // The orchestrator (and any future composite provider) has no model
1844
+ // of its own — it dispatches to other providers. Step 3 routes
1845
+ // through a custom wizard instead of the standard model picker.
1846
+ const providerMeta = (_registryMod.PROVIDER_INFO || {})[provider.id] || {};
1847
+ if (providerMeta.composite || provider.id === 'orchestrator') {
1848
+ const result = await _setupOrchestratorInteractive();
1849
+ if (result === 'CANCEL') return null;
1850
+ if (result === 'BACK') return _pickProviderInteractive();
1851
+ return { provider: provider.id, model: 'orchestrator' };
1852
+ }
1839
1853
  const picked = await _pickModelInteractive(provider.id, {
1840
1854
  titlePrefix: 'LazyClaw setup — Step 3 of 3:',
1841
1855
  onBack: 'restart',
@@ -1845,6 +1859,125 @@ async function _pickProviderInteractive() {
1845
1859
  return { provider: provider.id, model: picked };
1846
1860
  }
1847
1861
 
1862
+ // Step-3 alternative for composite providers (currently only the
1863
+ // orchestrator). Builds `cfg.orchestrator = { planner, workers,
1864
+ // maxSubtasks }` interactively and persists it before returning.
1865
+ //
1866
+ // planner: single picker over registered non-composite providers.
1867
+ // workers: multi-select with a running list + add/remove/done loop.
1868
+ // maxSubtasks: typed integer, default 5.
1869
+ async function _setupOrchestratorInteractive() {
1870
+ const accent = (s) => `\x1b[38;5;208m${s}\x1b[0m`;
1871
+ const dim = (s) => `\x1b[2m${s}\x1b[0m`;
1872
+ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
1873
+ const ok = (s) => `\x1b[32m${s}\x1b[0m`;
1874
+ const info = _registryMod.PROVIDER_INFO || {};
1875
+ const eligibleNames = Object.keys(_registryMod.PROVIDERS).filter((n) => n !== 'orchestrator' && n !== 'mock');
1876
+ if (eligibleNames.length === 0) {
1877
+ process.stdout.write('\n' + accent('orchestrator setup') + ': no eligible workers — register a real provider first.\n');
1878
+ await _quickPrompt(' press Enter to continue ');
1879
+ return 'CANCEL';
1880
+ }
1881
+ const cfg = readConfig();
1882
+ const existing = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
1883
+
1884
+ // ── Pick planner ─────────────────────────────────────────────────
1885
+ const plannerItems = eligibleNames.map((name) => {
1886
+ const m = info[name] || {};
1887
+ const defaultModel = m.defaultModel || '';
1888
+ return {
1889
+ id: `${name}${defaultModel ? ':' + defaultModel : ''}`,
1890
+ label: m.label && m.label !== name ? `${name} — ${m.label}` : name,
1891
+ desc: defaultModel ? `default model: ${defaultModel}` : '',
1892
+ };
1893
+ });
1894
+ const plannerPick = await _arrowMenu({
1895
+ title: 'LazyClaw setup — Step 3 of 3: orchestrator — pick the planner',
1896
+ subtitle: 'The planner decomposes the user request into subtasks and writes the final synthesis. Strong reasoning models work best here.',
1897
+ items: plannerItems,
1898
+ searchable: true,
1899
+ defaultIdx: Math.max(0, plannerItems.findIndex((p) => p.id === existing.planner)),
1900
+ });
1901
+ if (plannerPick === 'CANCEL') return 'CANCEL';
1902
+ if (plannerPick === 'BACK') return 'BACK';
1903
+ const planner = plannerPick.id;
1904
+
1905
+ // ── Pick workers (iterative add/remove) ──────────────────────────
1906
+ const workers = Array.isArray(existing.workers) ? existing.workers.slice() : [];
1907
+ while (true) {
1908
+ process.stdout.write('\x1b[2J\x1b[H');
1909
+ process.stdout.write(accent('Orchestrator workers') + '\n');
1910
+ process.stdout.write(dim('Subtasks are dispatched round-robin across this list.') + '\n\n');
1911
+ if (workers.length === 0) {
1912
+ process.stdout.write(' ' + dim('(none yet — add at least one)') + '\n\n');
1913
+ } else {
1914
+ workers.forEach((w, i) => {
1915
+ process.stdout.write(` ${i + 1}. ${ok(w)}\n`);
1916
+ });
1917
+ process.stdout.write('\n');
1918
+ }
1919
+ const items = [
1920
+ { id: '__add__', label: '+ Add a worker', desc: 'pick from registered providers' },
1921
+ { id: '__remove__', label: '- Remove a worker', desc: workers.length ? 'pick which entry to drop' : '(nothing to remove)' },
1922
+ { 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' },
1923
+ ];
1924
+ const action = await _arrowMenu({
1925
+ title: 'LazyClaw setup — orchestrator workers',
1926
+ subtitle: `Planner: ${planner}`,
1927
+ items,
1928
+ });
1929
+ if (action === 'CANCEL') return 'CANCEL';
1930
+ if (action === 'BACK') return 'BACK';
1931
+ if (action.id === '__add__') {
1932
+ const wPick = await _arrowMenu({
1933
+ title: 'Add worker',
1934
+ subtitle: 'Picked entries are appended to the workers list.',
1935
+ items: plannerItems.filter((p) => !workers.includes(p.id)),
1936
+ searchable: true,
1937
+ });
1938
+ if (wPick === 'CANCEL' || wPick === 'BACK') continue;
1939
+ workers.push(wPick.id);
1940
+ continue;
1941
+ }
1942
+ if (action.id === '__remove__') {
1943
+ if (!workers.length) continue;
1944
+ const rPick = await _arrowMenu({
1945
+ title: 'Remove worker',
1946
+ subtitle: 'Highlighted entry is removed from the list.',
1947
+ items: workers.map((w) => ({ id: w, label: w })),
1948
+ });
1949
+ if (rPick === 'CANCEL' || rPick === 'BACK') continue;
1950
+ const idx = workers.indexOf(rPick.id);
1951
+ if (idx >= 0) workers.splice(idx, 1);
1952
+ continue;
1953
+ }
1954
+ if (action.id === '__done__') {
1955
+ if (workers.length === 0) continue;
1956
+ break;
1957
+ }
1958
+ }
1959
+
1960
+ // ── maxSubtasks ──────────────────────────────────────────────────
1961
+ const defaultMax = Number.isFinite(existing.maxSubtasks) && existing.maxSubtasks > 0
1962
+ ? Math.min(10, existing.maxSubtasks)
1963
+ : 5;
1964
+ const rawMax = (await _quickPrompt(` ${bold('maxSubtasks')} ${dim(`(2..10, blank → ${defaultMax}):`)} `)).trim();
1965
+ let maxSubtasks = defaultMax;
1966
+ if (rawMax) {
1967
+ const n = parseInt(rawMax, 10);
1968
+ if (Number.isFinite(n) && n >= 1) maxSubtasks = Math.min(10, Math.max(1, n));
1969
+ }
1970
+
1971
+ // ── Persist ──────────────────────────────────────────────────────
1972
+ cfg.orchestrator = { planner, workers, maxSubtasks };
1973
+ writeConfig(cfg);
1974
+ process.stdout.write('\n');
1975
+ process.stdout.write(` ${ok('✓ orchestrator saved')} ${dim('→')} ` +
1976
+ `planner ${ok(planner)} · ${workers.length} worker${workers.length === 1 ? '' : 's'} · maxSubtasks ${maxSubtasks}\n`);
1977
+ await _quickPrompt(' press Enter to continue ');
1978
+ return { ok: true };
1979
+ }
1980
+
1848
1981
  // Pause the chat REPL's readline + ghost-autocomplete while a sub-picker
1849
1982
  // (provider / model arrow menu) takes over the terminal. The sub-picker
1850
1983
  // installs its own `keypress` listener and toggles raw mode; the chat's
@@ -3722,6 +3855,126 @@ async function cmdProviders(sub, positional, flags = {}) {
3722
3855
  }
3723
3856
  }
3724
3857
 
3858
+ // `lazyclaw orchestrator` — read/write the cfg.orchestrator section
3859
+ // without editing config.json by hand. Mirrors the shape `lazyclaw
3860
+ // providers` / `lazyclaw rates` already use.
3861
+ //
3862
+ // Subcommands:
3863
+ // status Print current planner / workers / maxSubtasks as JSON.
3864
+ // set-planner <provider[:model]> Replace the planner spec.
3865
+ // workers add <provider[:model]> Append a worker (idempotent — duplicates skipped).
3866
+ // workers remove <provider[:model]> Drop a worker by exact match. Idempotent.
3867
+ // workers clear Empty the workers list.
3868
+ // workers set <provider[:model],...> Replace the whole list (comma-separated).
3869
+ // set-max-subtasks <N> Cap the number of subtasks (clamped 1..10).
3870
+ // clear Delete the entire cfg.orchestrator block.
3871
+ async function cmdOrchestrator(sub, positional, _flags = {}) {
3872
+ await ensureRegistry();
3873
+ const cfg = readConfig();
3874
+ const orch = cfg.orchestrator && typeof cfg.orchestrator === 'object' ? cfg.orchestrator : {};
3875
+ const known = Object.keys(_registryMod.PROVIDERS);
3876
+ const validateSpec = (spec) => {
3877
+ if (!spec) throw new Error('provider spec required (e.g. "claude-cli" or "openai:gpt-4o")');
3878
+ const colon = spec.indexOf(':');
3879
+ const provName = colon > 0 ? spec.slice(0, colon) : spec;
3880
+ if (provName === 'orchestrator') throw new Error('"orchestrator" cannot reference itself — pick a real provider');
3881
+ if (!known.includes(provName)) {
3882
+ throw new Error(`unknown provider "${provName}" — registered: ${known.join(', ')}`);
3883
+ }
3884
+ return spec;
3885
+ };
3886
+ const saveAndPrint = (next) => {
3887
+ if (next === null) delete cfg.orchestrator;
3888
+ else cfg.orchestrator = next;
3889
+ writeConfig(cfg);
3890
+ console.log(JSON.stringify(cfg.orchestrator || null, null, 2));
3891
+ };
3892
+ switch (sub) {
3893
+ case undefined:
3894
+ case 'status': {
3895
+ console.log(JSON.stringify({
3896
+ ok: true,
3897
+ configured: !!cfg.orchestrator,
3898
+ planner: orch.planner || null,
3899
+ workers: Array.isArray(orch.workers) ? orch.workers : [],
3900
+ maxSubtasks: Number.isFinite(orch.maxSubtasks) ? orch.maxSubtasks : null,
3901
+ knownProviders: known,
3902
+ }, null, 2));
3903
+ return;
3904
+ }
3905
+ case 'set-planner': {
3906
+ try {
3907
+ const spec = validateSpec(positional[0]);
3908
+ saveAndPrint({ ...orch, planner: spec });
3909
+ } catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
3910
+ return;
3911
+ }
3912
+ case 'workers': {
3913
+ const wsub = positional[0];
3914
+ const workers = Array.isArray(orch.workers) ? orch.workers.slice() : [];
3915
+ switch (wsub) {
3916
+ case 'add': {
3917
+ try {
3918
+ const spec = validateSpec(positional[1]);
3919
+ if (!workers.includes(spec)) workers.push(spec);
3920
+ saveAndPrint({ ...orch, workers });
3921
+ } catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
3922
+ return;
3923
+ }
3924
+ case 'remove': {
3925
+ const spec = positional[1];
3926
+ if (!spec) { console.error('orchestrator: workers remove <provider[:model]>'); process.exit(2); }
3927
+ const idx = workers.indexOf(spec);
3928
+ if (idx >= 0) workers.splice(idx, 1);
3929
+ saveAndPrint({ ...orch, workers });
3930
+ return;
3931
+ }
3932
+ case 'clear': {
3933
+ saveAndPrint({ ...orch, workers: [] });
3934
+ return;
3935
+ }
3936
+ case 'set': {
3937
+ const raw = positional[1] || '';
3938
+ const specs = raw.split(',').map((s) => s.trim()).filter(Boolean);
3939
+ try {
3940
+ specs.forEach(validateSpec);
3941
+ saveAndPrint({ ...orch, workers: specs });
3942
+ } catch (e) { console.error(`orchestrator: ${e.message}`); process.exit(2); }
3943
+ return;
3944
+ }
3945
+ default: {
3946
+ console.error('Usage: lazyclaw orchestrator workers <add <spec> | remove <spec> | clear | set <spec,spec,...>>');
3947
+ process.exit(2);
3948
+ }
3949
+ }
3950
+ }
3951
+ case 'set-max-subtasks': {
3952
+ const n = parseInt(positional[0], 10);
3953
+ if (!Number.isFinite(n) || n < 1) { console.error('orchestrator: set-max-subtasks <N> (1..10)'); process.exit(2); }
3954
+ saveAndPrint({ ...orch, maxSubtasks: Math.min(10, Math.max(1, n)) });
3955
+ return;
3956
+ }
3957
+ case 'clear': {
3958
+ saveAndPrint(null);
3959
+ return;
3960
+ }
3961
+ default: {
3962
+ console.error(
3963
+ 'Usage:\n' +
3964
+ ' lazyclaw orchestrator status\n' +
3965
+ ' lazyclaw orchestrator set-planner <provider[:model]>\n' +
3966
+ ' lazyclaw orchestrator workers add <provider[:model]>\n' +
3967
+ ' lazyclaw orchestrator workers remove <provider[:model]>\n' +
3968
+ ' lazyclaw orchestrator workers set <provider[:model],...>\n' +
3969
+ ' lazyclaw orchestrator workers clear\n' +
3970
+ ' lazyclaw orchestrator set-max-subtasks <N>\n' +
3971
+ ' lazyclaw orchestrator clear'
3972
+ );
3973
+ process.exit(2);
3974
+ }
3975
+ }
3976
+ }
3977
+
3725
3978
  async function cmdSessions(sub, positional, flags = {}) {
3726
3979
  const sessionsMod = await import('./sessions.mjs');
3727
3980
  const cfgDir = path.dirname(configPath());
@@ -4614,6 +4867,11 @@ async function main() {
4614
4867
  await cmdProviders(sub, rest.positional.slice(1), rest.flags);
4615
4868
  break;
4616
4869
  }
4870
+ case 'orchestrator': {
4871
+ const sub = rest.positional[0];
4872
+ await cmdOrchestrator(sub, rest.positional.slice(1), rest.flags);
4873
+ break;
4874
+ }
4617
4875
  case 'skills': {
4618
4876
  const sub = rest.positional[0];
4619
4877
  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.22",
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)