lazyclaw 5.3.1 → 5.3.2

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
@@ -853,6 +853,7 @@ async function cmdDoctor() {
853
853
  await ensureRegistry();
854
854
  const cfg = readConfig();
855
855
  const issues = [];
856
+ const warnings = [];
856
857
  if (!cfg.provider) issues.push('config.provider is missing — run `lazyclaw onboard`');
857
858
  // Only flag a missing api-key when the picked provider actually
858
859
  // requires one. claude-cli / ollama / mock all run keylessly, so the
@@ -864,6 +865,27 @@ async function cmdDoctor() {
864
865
  if (cfg.provider && !PROVIDERS_HAS(_registryMod.PROVIDERS, cfg.provider)) {
865
866
  issues.push(`unknown provider "${cfg.provider}" — registered: ${Object.keys(_registryMod.PROVIDERS).join(', ')}`);
866
867
  }
868
+ // v5.3.2 soft-migration — pre-5.3.2 wizards could write
869
+ // `provider: 'orchestrator'` even when the user never configured the
870
+ // orchestrator section (planner / workers). On those installs the
871
+ // first chat turn dies with an opaque "orchestrator not configured"
872
+ // error. Surface a warning + the fix hint, but never auto-rewrite
873
+ // cfg.json — the user might have legitimately picked orchestrator
874
+ // and just hasn't finished setup yet.
875
+ if (cfg.provider === 'orchestrator') {
876
+ const orch = cfg.orchestrator;
877
+ const configured = orch && typeof orch === 'object'
878
+ && typeof orch.planner === 'string' && orch.planner
879
+ && Array.isArray(orch.workers) && orch.workers.length > 0;
880
+ if (!configured) {
881
+ warnings.push(
882
+ 'config.provider is "orchestrator" but cfg.orchestrator is missing/empty. '
883
+ + 'Pre-v5.3.2 setup wizards could leave you in this half-configured state. '
884
+ + 'Either finish orchestrator setup (`lazyclaw orchestrator set-planner …` + `lazyclaw orchestrator workers add …`) '
885
+ + 'or switch to a single concrete provider: `lazyclaw config set provider claude-cli`.'
886
+ );
887
+ }
888
+ }
867
889
  // C12 — MinGit / Windows safety net. mas/tools/git.mjs shells out to
868
890
  // `git`; on a stripped Windows PATH (no Git-for-Windows installed) or
869
891
  // a minimal Docker base image, that spawnSync ENOENTs and any agent
@@ -957,6 +979,7 @@ async function cmdDoctor() {
957
979
  nodeVersion: process.version,
958
980
  platform: `${process.platform}-${process.arch}`,
959
981
  issues,
982
+ warnings,
960
983
  knownProviders: Object.keys(_registryMod.PROVIDERS),
961
984
  workflows,
962
985
  git: gitInfo,
@@ -1941,7 +1964,15 @@ function _providerFamilies() {
1941
1964
  mock: { label: 'Mock', desc: 'offline echo, only useful for testing', tag: '\x1b[38;5;245m[test]\x1b[0m', members: [] },
1942
1965
  };
1943
1966
  for (const name of all) {
1967
+ // v5.3.2 — orchestrator is strictly opt-in. It's still registered so
1968
+ // `lazyclaw orchestrator …` and explicit `--provider orchestrator`
1969
+ // keep working, but the setup wizard must never land on it as a
1970
+ // default. Previously a fresh user picking "CLI/Local" got
1971
+ // orchestrator bucketed alongside claude-cli/ollama and could end
1972
+ // up with `{ provider: 'orchestrator', model: 'orchestrator' }`
1973
+ // written to cfg.json.
1944
1974
  if (name === 'mock') buckets.mock.members.push(name);
1975
+ else if (name === 'orchestrator') continue;
1945
1976
  else if ((info[name] || {}).requiresApiKey) buckets.api.members.push(name);
1946
1977
  else buckets.cli.members.push(name);
1947
1978
  }
@@ -1975,7 +2006,11 @@ async function _pickProviderInteractive() {
1975
2006
  };
1976
2007
  process.stdin.on('data', onData);
1977
2008
  });
1978
- return { provider: ans || providers[0], model: null };
2009
+ // v5.3.2 non-TTY fallback used to be `providers[0]`, which was
2010
+ // whatever happened to be first in the registry (currently
2011
+ // anthropic). Pin to claude-cli to match the interactive onboard
2012
+ // hint at cmdOnboard (the keyless subscription path).
2013
+ return { provider: ans || 'claude-cli', model: null };
1979
2014
  }
1980
2015
 
1981
2016
  // ── Step 1 — auth family ──────────────────────────────────────
@@ -2070,17 +2105,13 @@ async function _pickProviderInteractive() {
2070
2105
  provider = picked;
2071
2106
  }
2072
2107
 
2073
- // ── Step 3 — model (or, for composite providers, a config wizard) ───
2074
- // The orchestrator (and any future composite provider) has no model
2075
- // of its own it dispatches to other providers. Step 3 routes
2076
- // through a custom wizard instead of the standard model picker.
2077
- const providerMeta = (_registryMod.PROVIDER_INFO || {})[provider.id] || {};
2078
- if (providerMeta.composite || provider.id === 'orchestrator') {
2079
- const result = await _setupOrchestratorInteractive();
2080
- if (result === 'CANCEL') return null;
2081
- if (result === 'BACK') return _pickProviderInteractive();
2082
- return { provider: provider.id, model: 'orchestrator' };
2083
- }
2108
+ // ── Step 3 — model picker ────────────────────────────────────────
2109
+ // v5.3.2 the setup wizard no longer surfaces composite providers
2110
+ // (orchestrator is filtered out of _providerFamilies above), so this
2111
+ // step is just the regular model picker. The orchestrator wizard
2112
+ // (_setupOrchestratorInteractive) stays reachable via the dedicated
2113
+ // `lazyclaw orchestrator …` subcommand and an explicit
2114
+ // `--provider orchestrator` invocation.
2084
2115
  const picked = await _pickModelInteractive(provider.id, {
2085
2116
  titlePrefix: 'LazyClaw setup — Step 3 of 3:',
2086
2117
  onBack: 'restart',
@@ -2471,7 +2502,11 @@ async function cmdChat(flags = {}) {
2471
2502
  if (picked.model) activeModel = picked.model;
2472
2503
  }
2473
2504
  }
2474
- if (!activeProvName) activeProvName = 'mock';
2505
+ // v5.3.2 last-resort safety net used to fall through to 'mock' (the
2506
+ // offline echo provider), which silently degraded a wiped config into
2507
+ // garbage replies. Default to claude-cli so the user lands on the
2508
+ // keyless subscription path instead.
2509
+ if (!activeProvName) activeProvName = 'claude-cli';
2475
2510
  let prov = lookupProv(activeProvName);
2476
2511
  if (!prov) { console.error(`unknown provider: ${activeProvName}`); process.exit(2); }
2477
2512
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazyclaw",
3
- "version": "5.3.1",
3
+ "version": "5.3.2",
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",
@@ -124,18 +124,33 @@ 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`;
127
+ // Unconfigured-orchestrator path (v5.3.2 fix): when cfg.orchestrator
128
+ // is missing OR has no workers configured, the multi-agent pipeline
129
+ // is unjustified there is no second backend to delegate to. The
130
+ // previous behaviour printed a "single-agent chain" banner and then
131
+ // still ran Plan → Execute(N) → Synthesis against the same backend,
132
+ // turning a trivial question into a 4-subtask decomposition. That
133
+ // violates §1 truthfulness (the banner promised a single chain) and
134
+ // the user's stated intent. Do a real passthrough instead.
135
+ const hasWorkers = Array.isArray(o.workers) && o.workers.length > 0;
136
+ if (!cfg.orchestrator || !hasWorkers) {
137
+ const direct = _lookupProvider(fallbackSpec);
138
+ if (!direct || direct.name === 'orchestrator') {
139
+ yield `⚠ orchestrator: not configured and fallback provider \`${fallbackSpec}\` is not registered. ` +
140
+ `Set \`cfg.orchestrator.planner\` + \`cfg.orchestrator.workers\`, or set \`cfg.provider\` to a real backend.\n`;
141
+ return;
142
+ }
143
+ yield `> Orchestrator not configured — using single-shot \`${direct.name}${direct.model ? ':' + direct.model : ''}\`. ` +
144
+ `Run \`lazyclaw orchestrator set-planner ${fallbackSpec}\` then \`lazyclaw orchestrator workers add <provider:model>\` to enable multi-agent.\n\n`;
145
+ for await (const chunk of direct.prov.sendMessage(messages, {
146
+ apiKey: keyResolver(cfg, direct.name),
147
+ model: direct.model || undefined,
148
+ signal: callerOpts.signal,
149
+ })) yield String(chunk);
150
+ return;
134
151
  }
135
152
  const plannerSpec = String(o.planner || fallbackSpec);
136
- const workerSpecs = Array.isArray(o.workers) && o.workers.length
137
- ? o.workers.map(String)
138
- : [plannerSpec];
153
+ const workerSpecs = o.workers.map(String);
139
154
  const maxSubtasks = Number.isFinite(o.maxSubtasks) && o.maxSubtasks > 0 ? Math.min(10, o.maxSubtasks) : 5;
140
155
 
141
156
  const planner = _lookupProvider(plannerSpec);
package/tui/editor.mjs CHANGED
@@ -17,10 +17,54 @@
17
17
  // amber notes. The box auto-fills the available terminal width via
18
18
  // Ink's flex defaults and grows vertically as the buffer wraps onto
19
19
  // new lines (Shift+Enter).
20
+ //
21
+ // v5.3.2: CJK / wide-character correctness. `string.length` returns the
22
+ // UTF-16 code-unit count, which is wrong for column math — a Hangul or
23
+ // Han glyph occupies 2 terminal cells but reports `.length === 1`. We
24
+ // keep `state.cursor` in codepoint-index units (so `buffer.slice(0,
25
+ // cursor)`, Backspace, and history recall still work), but expose all
26
+ // display-column math through the `displayWidth()` helper and the
27
+ // derived `cursorDisplayCol` field. The render also pins the Box to
28
+ // `width: '100%'` so Ink's wrap-ansi (already string-width aware) gets
29
+ // the full terminal budget — fixes the perceived right-edge truncation
30
+ // on long Korean buffers.
20
31
  import React, { useState, useEffect } from 'react';
21
32
  import { Box, Text, useInput } from 'ink';
33
+ import stringWidth from 'string-width';
22
34
  import { theme } from './theme.mjs';
23
35
 
36
+ // The accent prompt (`› `) prepended to the first rendered line. Its
37
+ // display width matters for any caller that wants to know the usable
38
+ // inner width of the editor box. Defined once so the value stays in
39
+ // sync if the prompt glyph ever changes.
40
+ export const PROMPT_PREFIX = '› ';
41
+ export const PROMPT_WIDTH = stringWidth(PROMPT_PREFIX);
42
+ export const CONTINUATION_GUTTER = ' ';
43
+ export const CONTINUATION_WIDTH = stringWidth(CONTINUATION_GUTTER);
44
+
45
+ // Public helper: display width of a buffer (or any substring of it),
46
+ // counting wide chars (CJK, fullwidth, most emoji) as 2 cells and
47
+ // ignoring ANSI escapes. Use this — never `.length` — for column math.
48
+ export function displayWidth(text) {
49
+ if (!text) return 0;
50
+ return stringWidth(String(text));
51
+ }
52
+
53
+ // Display column of the caret given a state. Counts wide chars as 2.
54
+ // On the first rendered line this is offset by PROMPT_WIDTH; on
55
+ // continuation lines (after a Shift+Enter) it is offset by
56
+ // CONTINUATION_WIDTH. Callers that only need the in-buffer column can
57
+ // pass `{ withPrefix: false }`.
58
+ export function cursorDisplayCol(state, { withPrefix = true } = {}) {
59
+ const before = String(state.buffer || '').slice(0, state.cursor || 0);
60
+ const newlineIdx = before.lastIndexOf('\n');
61
+ const lineSlice = newlineIdx === -1 ? before : before.slice(newlineIdx + 1);
62
+ const inLine = stringWidth(lineSlice);
63
+ if (!withPrefix) return inLine;
64
+ const prefix = newlineIdx === -1 ? PROMPT_WIDTH : CONTINUATION_WIDTH;
65
+ return prefix + inLine;
66
+ }
67
+
24
68
  export function makeEditorState({ history = [] } = {}) {
25
69
  return {
26
70
  buffer: '',
@@ -181,11 +225,17 @@ export function Editor({
181
225
  paddingX: 1,
182
226
  flexDirection: 'column',
183
227
  flexShrink: 0,
228
+ // Pin to full terminal width. Ink's wrap-ansi (which is
229
+ // string-width aware) then has the correct cell budget for
230
+ // wrapping long CJK buffers — fixes the right-edge truncation
231
+ // perceived on Hangul / Han input. See `displayWidth`/
232
+ // `cursorDisplayCol` above for the public width helpers.
233
+ width: '100%',
184
234
  },
185
235
  lines.map((ln, i) => React.createElement(
186
236
  Text,
187
237
  { key: i },
188
- i === 0 ? theme.accent('› ') + ln : ' ' + ln,
238
+ i === 0 ? theme.accent(PROMPT_PREFIX) + ln : CONTINUATION_GUTTER + ln,
189
239
  )),
190
240
  );
191
241
  }