lazyclaw 5.3.1 → 5.3.3
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 +48 -13
- package/package.json +2 -1
- package/providers/orchestrator.mjs +25 -10
- package/tui/editor.mjs +91 -3
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
|
-
|
|
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
|
|
2074
|
-
//
|
|
2075
|
-
//
|
|
2076
|
-
//
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "5.3.3",
|
|
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",
|
|
@@ -87,6 +87,7 @@
|
|
|
87
87
|
"better-sqlite3": "^11.10.0",
|
|
88
88
|
"chalk": "^5.3.0",
|
|
89
89
|
"ink": "^5.0.1",
|
|
90
|
+
"ink-testing-library": "^4.0.0",
|
|
90
91
|
"node-ssh": "^13.2.0",
|
|
91
92
|
"string-width": "^7.2.0",
|
|
92
93
|
"undici": "^6.21.0"
|
|
@@ -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
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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 =
|
|
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: '',
|
|
@@ -173,6 +217,49 @@ export function Editor({
|
|
|
173
217
|
}, [state.lastSubmit]);
|
|
174
218
|
|
|
175
219
|
const lines = state.buffer.split('\n');
|
|
220
|
+
// Manual cell-aware wrapping. Ink's <Text wrap="wrap"> uses wrap-ansi,
|
|
221
|
+
// which IS string-width aware, but the box's width:'100%' resolves
|
|
222
|
+
// against ink-testing-library's stdout shim (and some real terminals
|
|
223
|
+
// when columns is reset by SIGWINCH late) at 100 cols regardless of
|
|
224
|
+
// the actual viewport — so wide CJK buffers visibly bleed past the
|
|
225
|
+
// box right edge in narrow terminals. Pre-wrap to the actual cell
|
|
226
|
+
// budget so Ink never has to guess.
|
|
227
|
+
const TERM = Math.max(20, process.stdout.columns || 80);
|
|
228
|
+
// Box overhead: 1 border + 1 padX on each side = 4 cells; first row
|
|
229
|
+
// also reserves PROMPT_WIDTH; continuation rows reserve CONTINUATION_WIDTH.
|
|
230
|
+
function wrapToBudget(text, firstBudget, contBudget) {
|
|
231
|
+
if (!text) return [''];
|
|
232
|
+
const out = [];
|
|
233
|
+
let line = '';
|
|
234
|
+
let lineW = 0;
|
|
235
|
+
let budget = firstBudget;
|
|
236
|
+
for (const ch of text) {
|
|
237
|
+
const w = stringWidth(ch);
|
|
238
|
+
if (lineW + w > budget) {
|
|
239
|
+
out.push(line);
|
|
240
|
+
line = ch;
|
|
241
|
+
lineW = w;
|
|
242
|
+
budget = contBudget;
|
|
243
|
+
} else {
|
|
244
|
+
line += ch;
|
|
245
|
+
lineW += w;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
out.push(line);
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
const innerCells = Math.max(8, TERM - 4); // 2 border + 2 padX
|
|
252
|
+
const renderedLines = [];
|
|
253
|
+
for (let li = 0; li < lines.length; li++) {
|
|
254
|
+
const wrapped = wrapToBudget(lines[li], innerCells - PROMPT_WIDTH, innerCells - CONTINUATION_WIDTH);
|
|
255
|
+
for (let wi = 0; wi < wrapped.length; wi++) {
|
|
256
|
+
const isFirstLogical = li === 0 && wi === 0;
|
|
257
|
+
renderedLines.push({
|
|
258
|
+
prefix: isFirstLogical ? theme.accent(PROMPT_PREFIX) : CONTINUATION_GUTTER,
|
|
259
|
+
text: wrapped[wi],
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
176
263
|
return React.createElement(
|
|
177
264
|
Box,
|
|
178
265
|
{
|
|
@@ -181,11 +268,12 @@ export function Editor({
|
|
|
181
268
|
paddingX: 1,
|
|
182
269
|
flexDirection: 'column',
|
|
183
270
|
flexShrink: 0,
|
|
271
|
+
width: TERM,
|
|
184
272
|
},
|
|
185
|
-
|
|
273
|
+
renderedLines.map((row, i) => React.createElement(
|
|
186
274
|
Text,
|
|
187
|
-
{ key: i },
|
|
188
|
-
|
|
275
|
+
{ key: i, wrap: 'truncate' },
|
|
276
|
+
row.prefix + row.text,
|
|
189
277
|
)),
|
|
190
278
|
);
|
|
191
279
|
}
|