lazyclaw 6.3.1 → 6.4.0
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.ko.md +5 -1
- package/README.md +17 -13
- package/agents.mjs +54 -4
- package/channels-discord/index.mjs +5 -1
- package/channels-email/index.mjs +4 -3
- package/channels-whatsapp/index.mjs +3 -2
- package/chat_window.mjs +11 -2
- package/cli.mjs +103 -3
- package/commands/agents.mjs +6 -1
- package/commands/auth_nodes.mjs +19 -1
- package/commands/automation.mjs +20 -5
- package/commands/channels.mjs +46 -3
- package/commands/chat.mjs +90 -33
- package/commands/chat_hardening.mjs +23 -0
- package/commands/daemon.mjs +99 -1
- package/commands/gateway.mjs +123 -4
- package/commands/mcp.mjs +150 -0
- package/commands/misc.mjs +20 -0
- package/commands/sessions.mjs +68 -8
- package/commands/setup.mjs +44 -8
- package/commands/setup_channels.mjs +219 -47
- package/commands/skills.mjs +19 -1
- package/commands/workflow_named.mjs +137 -0
- package/config-validate.mjs +10 -1
- package/config_features.mjs +45 -0
- package/cron.mjs +19 -8
- package/daemon/lib/auth.mjs +25 -0
- package/daemon/lib/cost.mjs +36 -0
- package/daemon/lib/respond.mjs +20 -1
- package/daemon/lib/team_inbound.mjs +69 -0
- package/daemon/route_table.mjs +5 -1
- package/daemon/routes/_deps.mjs +2 -2
- package/daemon/routes/config.mjs +11 -6
- package/daemon/routes/conversation.mjs +103 -40
- package/daemon/routes/events.mjs +45 -0
- package/daemon/routes/meta.mjs +38 -7
- package/daemon/routes/providers.mjs +26 -6
- package/daemon/routes/workflows.mjs +30 -0
- package/daemon.mjs +26 -2
- package/dotenv_min.mjs +15 -3
- package/gateway/challenge_registry.mjs +90 -0
- package/gateway/device_auth.mjs +71 -97
- package/gateway/http_gateway.mjs +29 -3
- package/lib/args.mjs +69 -3
- package/lib/config.mjs +85 -3
- package/lib/render.mjs +43 -0
- package/lib/service_install.mjs +15 -1
- package/mas/agent_turn.mjs +61 -12
- package/mas/confidence.mjs +20 -1
- package/mas/embedder.mjs +100 -0
- package/mas/events.mjs +57 -0
- package/mas/index_db.mjs +103 -10
- package/mas/learning.mjs +96 -72
- package/mas/mention_router.mjs +48 -90
- package/mas/prompt_stack.mjs +60 -3
- package/mas/recall_blend.mjs +56 -0
- package/mas/redact.mjs +55 -0
- package/mas/router_posting.mjs +96 -0
- package/mas/router_termination.mjs +63 -0
- package/mas/scrub_env.mjs +21 -6
- package/mas/skill_synth.mjs +7 -33
- package/mas/tool_runner.mjs +3 -2
- package/mas/tools/bash.mjs +9 -2
- package/mas/tools/coding.mjs +28 -5
- package/mas/tools/delegation.mjs +34 -4
- package/mas/tools/git.mjs +19 -9
- package/mas/tools/ha.mjs +2 -0
- package/mas/tools/learning.mjs +55 -0
- package/mas/tools/media.mjs +21 -3
- package/mas/tools/os.mjs +70 -16
- package/mas/tools/recall.mjs +28 -2
- package/mcp/client.mjs +8 -1
- package/mcp/server_spawn.mjs +5 -1
- package/memory.mjs +24 -0
- package/package.json +3 -1
- package/providers/anthropic.mjs +101 -6
- package/providers/cache.mjs +9 -1
- package/providers/claude_cli.mjs +104 -22
- package/providers/claude_cli_session.mjs +166 -0
- package/providers/cli_error.mjs +38 -0
- package/providers/cli_login.mjs +179 -0
- package/providers/codex_cli.mjs +66 -11
- package/providers/gemini.mjs +101 -3
- package/providers/gemini_cli.mjs +62 -15
- package/providers/model_catalogue.mjs +33 -2
- package/providers/ollama.mjs +104 -3
- package/providers/openai.mjs +110 -8
- package/providers/openai_compat.mjs +97 -6
- package/providers/orchestrator.mjs +112 -12
- package/providers/registry.mjs +15 -9
- package/providers/retry.mjs +14 -2
- package/providers/tool_use/anthropic.mjs +17 -3
- package/providers/tool_use/claude_cli.mjs +72 -20
- package/providers/tool_use/gemini.mjs +18 -2
- package/providers/tool_use/openai.mjs +28 -5
- package/sandbox/confiners/seatbelt.mjs +37 -12
- package/sandbox/index.mjs +49 -0
- package/sandbox/local.mjs +7 -1
- package/sandbox/spawn.mjs +144 -0
- package/sandbox.mjs +5 -1
- package/scripts/loop-worker.mjs +25 -1
- package/sessions.mjs +0 -0
- package/skills/channel-style.md +20 -0
- package/skills/code-review.md +33 -0
- package/skills/commit-message.md +30 -0
- package/skills/concise.md +24 -0
- package/skills/debug-coach.md +25 -0
- package/skills/explain.md +24 -0
- package/skills/korean.md +25 -0
- package/skills/summarize.md +33 -0
- package/skills.mjs +24 -2
- package/skills_curator.mjs +6 -0
- package/skills_install.mjs +10 -2
- package/tasks.mjs +6 -1
- package/teams.mjs +78 -0
- package/tui/chat_mode_slash.mjs +59 -0
- package/tui/config_picker.mjs +1 -0
- package/tui/editor.mjs +178 -151
- package/tui/editor_keys.mjs +275 -0
- package/tui/hud.mjs +111 -0
- package/tui/login_flow.mjs +113 -0
- package/tui/modal_picker.mjs +10 -1
- package/tui/model_pick.mjs +287 -0
- package/tui/orchestrator_flow.mjs +164 -0
- package/tui/pickers.mjs +196 -80
- package/tui/repl.mjs +106 -44
- package/tui/repl_reset.mjs +37 -0
- package/tui/run_turn.mjs +228 -26
- package/tui/slash_args.mjs +159 -0
- package/tui/slash_commands.mjs +7 -5
- package/tui/slash_dispatcher.mjs +537 -344
- package/tui/slash_popup.mjs +5 -1
- package/tui/splash.mjs +15 -2
- package/tui/status_bar.mjs +26 -0
- package/tui/theme.mjs +28 -0
- package/web/avatars/01.png +0 -0
- package/web/avatars/02.png +0 -0
- package/web/avatars/03.png +0 -0
- package/web/avatars/04.png +0 -0
- package/web/avatars/05.png +0 -0
- package/web/avatars/06.png +0 -0
- package/web/avatars/07.png +0 -0
- package/web/avatars/08.png +0 -0
- package/web/avatars/09.png +0 -0
- package/web/avatars/10.png +0 -0
- package/web/avatars/11.png +0 -0
- package/web/avatars/12.png +0 -0
- package/web/avatars/13.png +0 -0
- package/web/avatars/14.png +0 -0
- package/web/avatars/15.png +0 -0
- package/web/avatars/16.png +0 -0
- package/web/avatars/17.png +0 -0
- package/web/avatars/18.png +0 -0
- package/web/avatars/19.png +0 -0
- package/web/avatars/20.png +0 -0
- package/web/dashboard.css +77 -0
- package/web/dashboard.html +29 -2
- package/web/dashboard.js +296 -33
- package/workflow/builtin_caps.mjs +94 -0
- package/workflow/declarative.mjs +101 -0
- package/workflow/named.mjs +71 -0
- package/workflow/named_cron.mjs +50 -0
- package/workflow/nodes.mjs +67 -0
- package/workflow/run_request.mjs +74 -0
- package/workflow/yaml_min.mjs +97 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
// tui/model_pick.mjs — the single canonical provider→model picker for the Ink
|
|
2
|
+
// REPL. Hoisted out of slash_dispatcher.mjs so /model, /provider, /trainer,
|
|
3
|
+
// /orchestrator and /agent all pick a model the same way: a family→provider
|
|
4
|
+
// drill-in, a model loop with live-fetch + a free-text custom-id row, and a
|
|
5
|
+
// "⇄ switch provider" row. Reuses ctx.openPicker (the host modal).
|
|
6
|
+
//
|
|
7
|
+
// Exports:
|
|
8
|
+
// pickProviderModel(ctx, registry, opts) → { provider, model } | null
|
|
9
|
+
// plus the lower-level helpers the dispatcher still references by name.
|
|
10
|
+
|
|
11
|
+
import { providerFamilies, providerTag } from './provider_families.mjs';
|
|
12
|
+
import { supportsLiveFetch, fetchModelsForProvider } from '../providers/model_catalogue.mjs';
|
|
13
|
+
|
|
14
|
+
export function providerLookup(registry, name) {
|
|
15
|
+
if (typeof registry.lookupProv === 'function') return registry.lookupProv(name);
|
|
16
|
+
return registry.PROVIDERS ? registry.PROVIDERS[name] : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function infoFor(registry, provName) {
|
|
20
|
+
return (registry.PROVIDER_INFO && registry.PROVIDER_INFO[provName]) || {};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// A composite provider (orchestrator) has no real model list — its only
|
|
24
|
+
// "model" is the provider name itself, so the model picker would dead-end
|
|
25
|
+
// on a single bogus row. Detect it so we can redirect to a provider pick.
|
|
26
|
+
export function isCompositeProvider(info, provName) {
|
|
27
|
+
if (info && info.composite) return true;
|
|
28
|
+
const s = info && Array.isArray(info.suggestedModels) ? info.suggestedModels : null;
|
|
29
|
+
return !!(s && s.length === 1 && s[0] === provName);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Whether the active provider exposes any selectable model (suggested ≠ provider name, or live-fetchable).
|
|
33
|
+
export function hasRealModels(info, provName) {
|
|
34
|
+
if (supportsLiveFetch(info, provName)) return true;
|
|
35
|
+
const s = info && Array.isArray(info.suggestedModels) ? info.suggestedModels : [];
|
|
36
|
+
return s.some((m) => m && m !== provName);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Pick a provider via the family drill-in (API key / CLI-Local / Mock),
|
|
40
|
+
// mirroring the legacy readline wizard. Single-option steps auto-advance.
|
|
41
|
+
// orchestrator is never listed. Returns a provider id or null on cancel.
|
|
42
|
+
export async function pickProviderDrillIn(ctx, registry) {
|
|
43
|
+
const info = registry.PROVIDER_INFO || {};
|
|
44
|
+
const families = providerFamilies(registry);
|
|
45
|
+
const nonEmpty = Object.values(families).filter((f) => f.members.length > 0);
|
|
46
|
+
if (!nonEmpty.length) return null;
|
|
47
|
+
|
|
48
|
+
// ── Step 1 — auth family (skipped when only one is populated) ──
|
|
49
|
+
let family = nonEmpty[0];
|
|
50
|
+
if (nonEmpty.length > 1) {
|
|
51
|
+
const picked = await ctx.openPicker({
|
|
52
|
+
kind: 'provider-family',
|
|
53
|
+
title: 'select provider — how do you want to auth?',
|
|
54
|
+
subtitle: 'API: bring your own key · CLI/Local: use this machine · Mock: offline',
|
|
55
|
+
items: nonEmpty.map((f) => ({
|
|
56
|
+
id: f.id,
|
|
57
|
+
label: f.label,
|
|
58
|
+
desc: `${f.desc} · ${f.members.slice(0, 3).join(' / ')}${f.members.length > 3 ? ` (+${f.members.length - 3})` : ''}`,
|
|
59
|
+
tag: f.tag,
|
|
60
|
+
})),
|
|
61
|
+
});
|
|
62
|
+
if (!picked || typeof picked !== 'string') return null;
|
|
63
|
+
family = families[picked];
|
|
64
|
+
if (!family || !family.members.length) return null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ── Step 2 — provider in that family (auto-advances on a single member) ──
|
|
68
|
+
// The API-key family also offers "+ add a custom endpoint" so NIM /
|
|
69
|
+
// OpenRouter / vLLM / etc. can be registered without leaving chat. The
|
|
70
|
+
// add-custom row is never auto-advanced past, so don't single-skip it in.
|
|
71
|
+
const items = family.members.map((id) => {
|
|
72
|
+
const meta = info[id] || {};
|
|
73
|
+
let desc = '';
|
|
74
|
+
if (meta.custom) desc = `custom · ${meta.baseUrl || ''}`;
|
|
75
|
+
else if (meta.builtinOpenAICompat) desc = meta.label || meta.baseUrl || '';
|
|
76
|
+
else if (meta.label && meta.label !== id) desc = meta.label;
|
|
77
|
+
return { id, label: id, desc, tag: providerTag(meta) };
|
|
78
|
+
});
|
|
79
|
+
if (family.id === 'api') {
|
|
80
|
+
items.push({
|
|
81
|
+
id: '__add_custom__',
|
|
82
|
+
label: '+ add a custom OpenAI-compatible endpoint…',
|
|
83
|
+
desc: 'NIM · OpenRouter · Together · Groq · vLLM · LM Studio',
|
|
84
|
+
tag: 'new',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (items.length === 1) return items[0].id;
|
|
88
|
+
const picked = await ctx.openPicker({
|
|
89
|
+
kind: 'provider',
|
|
90
|
+
title: `select provider — ${family.label}`,
|
|
91
|
+
subtitle: `current: ${ctx.getActiveProvName()}`,
|
|
92
|
+
items,
|
|
93
|
+
});
|
|
94
|
+
return typeof picked === 'string' ? picked : null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Build the model-picker rows: suggested + live-fetched models (deduped, default
|
|
98
|
+
// tagged) plus pinned sentinel rows for live-fetch and free-text custom entry.
|
|
99
|
+
//
|
|
100
|
+
// opts (all optional, defaults preserve the original /model behavior):
|
|
101
|
+
// includeDefault — prepend a "▷ provider's own default model" row (→ '')
|
|
102
|
+
// includeSwitch — show the "⇄ pick a different provider" row (default true)
|
|
103
|
+
export function buildModelItems(info, provName, dynamicModels, opts = {}) {
|
|
104
|
+
const base = Array.isArray(info.suggestedModels) ? info.suggestedModels : [];
|
|
105
|
+
const all = Array.from(new Set([...(dynamicModels || []), ...base])).filter((m) => m && m !== provName);
|
|
106
|
+
const items = [];
|
|
107
|
+
if (opts.includeDefault) {
|
|
108
|
+
items.push({
|
|
109
|
+
id: '__default__',
|
|
110
|
+
label: "▷ provider's own default model",
|
|
111
|
+
desc: 'no -m override',
|
|
112
|
+
pinned: true,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (supportsLiveFetch(info, provName)) {
|
|
116
|
+
items.push({
|
|
117
|
+
id: '__fetch_models__',
|
|
118
|
+
label: '↻ fetch live model list',
|
|
119
|
+
desc: 'pull the current catalogue (may take a few seconds)',
|
|
120
|
+
pinned: true,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
for (const m of all) {
|
|
124
|
+
items.push({ id: m, label: m, desc: info.defaultModel === m ? '(default)' : '' });
|
|
125
|
+
}
|
|
126
|
+
items.push({
|
|
127
|
+
id: '__custom_model__',
|
|
128
|
+
label: '… type a custom model id',
|
|
129
|
+
desc: 'type the id into the filter above, then pick this row',
|
|
130
|
+
pinned: true,
|
|
131
|
+
freeText: true,
|
|
132
|
+
});
|
|
133
|
+
// Reach another provider's models (e.g. claude-cli's opus) without leaving
|
|
134
|
+
// /model — the active provider isn't the only place to pick a model.
|
|
135
|
+
if (opts.includeSwitch !== false) {
|
|
136
|
+
items.push({
|
|
137
|
+
id: '__switch_provider__',
|
|
138
|
+
label: '⇄ pick a different provider…',
|
|
139
|
+
desc: 'switch provider (e.g. claude-cli for opus/sonnet), then its model',
|
|
140
|
+
pinned: true,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
return items;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Run the model picker for `provName`, looping on the live-fetch row and
|
|
147
|
+
// resolving the free-text row from the typed filter. Returns a concrete model
|
|
148
|
+
// id, '' for the provider-default row, '__switch_provider__', or null on cancel.
|
|
149
|
+
export async function pickModelLoop(ctx, registry, provName, opts = {}) {
|
|
150
|
+
const info = infoFor(registry, provName);
|
|
151
|
+
let dynamic = [];
|
|
152
|
+
let note = '';
|
|
153
|
+
for (let guard = 0; guard < 50; guard++) {
|
|
154
|
+
const items = buildModelItems(info, provName, dynamic, opts);
|
|
155
|
+
const cur = typeof ctx.getActiveModel === 'function' ? ctx.getActiveModel() : '';
|
|
156
|
+
const picked = await ctx.openPicker({
|
|
157
|
+
kind: 'model',
|
|
158
|
+
title: `select model for ${provName}`,
|
|
159
|
+
subtitle: note || `current: ${cur || '(default)'}`,
|
|
160
|
+
items,
|
|
161
|
+
});
|
|
162
|
+
if (picked == null) return null;
|
|
163
|
+
if (typeof picked === 'object') {
|
|
164
|
+
// free-text custom row → { id, query }
|
|
165
|
+
const typed = String(picked.query || '').trim();
|
|
166
|
+
if (!typed) { note = 'type a model id into the filter first'; continue; }
|
|
167
|
+
return typed;
|
|
168
|
+
}
|
|
169
|
+
if (picked === '__default__') return '';
|
|
170
|
+
if (picked === '__fetch_models__') {
|
|
171
|
+
try {
|
|
172
|
+
const fetcher = typeof ctx.fetchModels === 'function'
|
|
173
|
+
? ctx.fetchModels
|
|
174
|
+
: (provId) => fetchModelsForProvider({
|
|
175
|
+
cfg: ctx.cfg,
|
|
176
|
+
registryMod: registry,
|
|
177
|
+
resolveAuthKey: (id) => (ctx.resolveAuthKey ? ctx.resolveAuthKey(id) : ''),
|
|
178
|
+
providerId: provId,
|
|
179
|
+
});
|
|
180
|
+
const fetched = await fetcher(provName);
|
|
181
|
+
if (Array.isArray(fetched) && fetched.length) {
|
|
182
|
+
dynamic = fetched;
|
|
183
|
+
note = `fetched ${fetched.length} model(s) — pick one or type a custom id`;
|
|
184
|
+
} else {
|
|
185
|
+
note = 'no models returned — using the suggested list';
|
|
186
|
+
}
|
|
187
|
+
} catch (e) {
|
|
188
|
+
note = `fetch failed: ${e && e.message ? e.message : e}`;
|
|
189
|
+
}
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
// Switch to a different provider's models (e.g. claude-cli's opus).
|
|
193
|
+
if (picked === '__switch_provider__') return '__switch_provider__';
|
|
194
|
+
return picked;
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Flat provider picker used to escape a composite/model-less active provider
|
|
200
|
+
// or to switch providers from inside /model. Hides composites + mock,
|
|
201
|
+
// mirroring the legacy wizard's filter (cli.mjs:1979).
|
|
202
|
+
//
|
|
203
|
+
// opts (optional): { exclude: string[], includeAuto: boolean }. `exclude`
|
|
204
|
+
// hides extra providers (orchestrator self-reference); `includeAuto` adds an
|
|
205
|
+
// "auto" row (trainer) that resolves to the '__auto__' sentinel.
|
|
206
|
+
export async function pickProviderForModel(ctx, registry, subtitle, opts = {}) {
|
|
207
|
+
const exclude = new Set(opts.exclude || []);
|
|
208
|
+
const info = registry.PROVIDER_INFO || {};
|
|
209
|
+
const known = Object.keys(registry.PROVIDERS || {})
|
|
210
|
+
.filter((id) => id !== 'mock' && !((info[id] || {}).composite) && !exclude.has(id))
|
|
211
|
+
.sort();
|
|
212
|
+
const items = known.map((id) => ({
|
|
213
|
+
id,
|
|
214
|
+
label: id,
|
|
215
|
+
desc: info[id] && info[id].docs ? String(info[id].docs).split('\n')[0].slice(0, 60) : '',
|
|
216
|
+
}));
|
|
217
|
+
if (opts.includeAuto) {
|
|
218
|
+
items.unshift({
|
|
219
|
+
id: '__auto__',
|
|
220
|
+
label: 'auto — let the trainer pick',
|
|
221
|
+
desc: 'claude-cli on a Pro/Max session, otherwise mirrors the chat model',
|
|
222
|
+
pinned: true,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
const active = typeof ctx.getActiveProvName === 'function' ? ctx.getActiveProvName() : '';
|
|
226
|
+
const picked = await ctx.openPicker({
|
|
227
|
+
kind: 'provider',
|
|
228
|
+
title: 'select provider (then a model)',
|
|
229
|
+
subtitle: subtitle || `${active} has no selectable models — pick a provider`,
|
|
230
|
+
items,
|
|
231
|
+
});
|
|
232
|
+
return typeof picked === 'string' ? picked : null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// The canonical entry point. Drill provider → model, honoring `opts`, and
|
|
236
|
+
// return { provider, model } (model '' = provider default) or null on cancel.
|
|
237
|
+
//
|
|
238
|
+
// opts (all optional):
|
|
239
|
+
// includeSwitch — show "⇄ pick a different provider" in the model list (default true)
|
|
240
|
+
// includeAuto — always open the provider step first with an "auto" row (trainer)
|
|
241
|
+
// pickProvider — always open the provider step (orchestrator planner/worker)
|
|
242
|
+
// includeDefault — add the "▷ provider's own default model" row (orchestrator/agent)
|
|
243
|
+
// exclude — provider ids to hide (orchestrator: ['orchestrator','mock'])
|
|
244
|
+
// startProvider — begin at this provider (skip the active-provider seed)
|
|
245
|
+
// title — provider-step title override
|
|
246
|
+
export async function pickProviderModel(ctx, registry, opts = {}) {
|
|
247
|
+
const includeSwitch = opts.includeSwitch !== false;
|
|
248
|
+
const exclude = opts.exclude || [];
|
|
249
|
+
const loopOpts = { includeSwitch, includeDefault: !!opts.includeDefault };
|
|
250
|
+
|
|
251
|
+
let provName = opts.startProvider || (typeof ctx.getActiveProvName === 'function' ? ctx.getActiveProvName() : '');
|
|
252
|
+
let info = infoFor(registry, provName);
|
|
253
|
+
let switched = !!opts.startProvider;
|
|
254
|
+
|
|
255
|
+
// Open the provider step when the active provider can't offer a model, or
|
|
256
|
+
// when the caller always wants a provider choice (orchestrator's pickProvider,
|
|
257
|
+
// trainer's includeAuto). startProvider skips the step (provider already chosen).
|
|
258
|
+
const mustPickProvider = !opts.startProvider && (
|
|
259
|
+
!!opts.pickProvider
|
|
260
|
+
|| !!opts.includeAuto
|
|
261
|
+
|| isCompositeProvider(info, provName)
|
|
262
|
+
|| !hasRealModels(info, provName)
|
|
263
|
+
);
|
|
264
|
+
if (mustPickProvider) {
|
|
265
|
+
const picked = await pickProviderForModel(ctx, registry, opts.title, { exclude, includeAuto: opts.includeAuto });
|
|
266
|
+
if (picked == null) return null;
|
|
267
|
+
if (picked === '__auto__') return { provider: 'auto', model: '' };
|
|
268
|
+
if (picked !== provName) { provName = picked; switched = true; info = infoFor(registry, provName); }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
for (let guard = 0; guard < 25; guard++) {
|
|
272
|
+
const model = await pickModelLoop(ctx, registry, provName, loopOpts);
|
|
273
|
+
if (model === '__switch_provider__') {
|
|
274
|
+
const np = await pickProviderForModel(ctx, registry, `current: ${provName} — pick a provider`, { exclude });
|
|
275
|
+
if (np == null) continue; // cancelled the switch → back to the model list
|
|
276
|
+
if (np === '__auto__') return { provider: 'auto', model: '' };
|
|
277
|
+
if (np !== provName) { provName = np; switched = true; info = infoFor(registry, provName); }
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
// Cancelled the model step. Report the (possibly switched) provider with a
|
|
281
|
+
// null model so /model can keep a provider switch while leaving the model
|
|
282
|
+
// unchanged; spec/record callers treat a null model as a full cancel.
|
|
283
|
+
if (model == null) return { provider: provName, model: null };
|
|
284
|
+
return { provider: provName, model };
|
|
285
|
+
}
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// tui/orchestrator_flow.mjs — interactive (fetch + pick) editing for the
|
|
2
|
+
// orchestrator's planner / workers and for picking a provider's model, so the
|
|
3
|
+
// user never has to type "provider:model" specs by hand. Driven by the Ink
|
|
4
|
+
// modal picker (ctx.openPicker); kept out of slash_dispatcher.mjs (at the
|
|
5
|
+
// file-size ratchet).
|
|
6
|
+
|
|
7
|
+
import { orchestratorGet, orchestratorSet, orchestratorEnable } from '../config_features.mjs';
|
|
8
|
+
import { readConfig as _readConfig, writeConfig as _writeConfig } from '../lib/config.mjs';
|
|
9
|
+
import { pickProviderModel } from './model_pick.mjs';
|
|
10
|
+
|
|
11
|
+
// Pick a provider (excluding orchestrator/mock) then its model → a spec string
|
|
12
|
+
// "provider" or "provider:model". Returns null on cancel. Wraps the canonical
|
|
13
|
+
// picker (pickProvider forces the provider step; the chat provider is never
|
|
14
|
+
// assumed for a planner/worker). includeSwitch is off — an orchestrator role
|
|
15
|
+
// is one explicit provider:model, not a hop between providers.
|
|
16
|
+
async function pickProviderModelSpec(ctx, registry, title) {
|
|
17
|
+
const r = await pickProviderModel(ctx, registry, {
|
|
18
|
+
title, exclude: ['orchestrator', 'mock'], pickProvider: true, includeDefault: true, includeSwitch: false,
|
|
19
|
+
});
|
|
20
|
+
if (!r || r.model == null) return null;
|
|
21
|
+
return r.model ? `${r.provider}:${r.model}` : r.provider;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Apply an interactive orchestrator edit. `action` ∈ planner | worker-add |
|
|
25
|
+
// worker-remove | maxsubtasks. Returns a status string (or null if unknown).
|
|
26
|
+
export async function orchestratorAction(ctx, registry, action) {
|
|
27
|
+
const read = typeof ctx.readConfig === 'function' ? ctx.readConfig : null;
|
|
28
|
+
const write = typeof ctx.writeConfig === 'function' ? ctx.writeConfig : null;
|
|
29
|
+
if (!read || !write) return 'orchestrator: config not writable in this session';
|
|
30
|
+
const cfg = read();
|
|
31
|
+
const persist = () => { write(cfg); if (ctx.cfg) ctx.cfg.orchestrator = cfg.orchestrator; };
|
|
32
|
+
|
|
33
|
+
if (action === 'planner') {
|
|
34
|
+
const spec = await pickProviderModelSpec(ctx, registry, 'Orchestrator — pick the planner');
|
|
35
|
+
if (!spec) return 'planner: cancelled';
|
|
36
|
+
orchestratorSet(cfg, { planner: spec }); persist();
|
|
37
|
+
return `planner → ${spec}`;
|
|
38
|
+
}
|
|
39
|
+
if (action === 'worker-add') {
|
|
40
|
+
const spec = await pickProviderModelSpec(ctx, registry, 'Orchestrator — add a worker');
|
|
41
|
+
if (!spec) return 'worker: cancelled';
|
|
42
|
+
const workers = [...orchestratorGet(cfg).workers];
|
|
43
|
+
if (!workers.includes(spec)) workers.push(spec);
|
|
44
|
+
orchestratorSet(cfg, { workers }); persist();
|
|
45
|
+
return `workers: ${workers.join(', ')}`;
|
|
46
|
+
}
|
|
47
|
+
if (action === 'worker-remove') {
|
|
48
|
+
const workers = orchestratorGet(cfg).workers;
|
|
49
|
+
if (!workers.length) return 'workers: (none to remove)';
|
|
50
|
+
const picked = await ctx.openPicker({ kind: 'menu', title: 'Remove a worker', items: workers.map((w) => ({ id: w, label: w })) });
|
|
51
|
+
const w = picked && typeof picked === 'object' ? picked.id : picked;
|
|
52
|
+
if (!w) return 'worker: cancelled';
|
|
53
|
+
const next = workers.filter((x) => x !== w);
|
|
54
|
+
orchestratorSet(cfg, { workers: next }); persist();
|
|
55
|
+
return `workers: ${next.join(', ') || '(none)'}`;
|
|
56
|
+
}
|
|
57
|
+
if (action === 'maxsubtasks') {
|
|
58
|
+
const cur = orchestratorGet(cfg).maxSubtasks;
|
|
59
|
+
const picked = await ctx.openPicker({
|
|
60
|
+
kind: 'menu', title: 'Max subtasks per request', subtitle: `currently ${cur}`,
|
|
61
|
+
items: Array.from({ length: 10 }, (_, i) => ({ id: String(i + 1), label: String(i + 1) })),
|
|
62
|
+
});
|
|
63
|
+
const n = parseInt(picked && typeof picked === 'object' ? picked.id : picked, 10);
|
|
64
|
+
if (!Number.isFinite(n)) return 'maxSubtasks: cancelled';
|
|
65
|
+
orchestratorSet(cfg, { maxSubtasks: Math.max(1, Math.min(10, n)) }); persist();
|
|
66
|
+
return `maxSubtasks → ${n}`;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Pick a model for the active provider and persist it as cfg.model. Returns a
|
|
72
|
+
// status fragment, or null on cancel. Used to chain provider→model in /provider.
|
|
73
|
+
export async function pickAndSetModel(ctx, registry, prov) {
|
|
74
|
+
const r = await pickProviderModel(ctx, registry, { startProvider: prov, includeDefault: true, includeSwitch: false });
|
|
75
|
+
if (!r || r.model == null) return null;
|
|
76
|
+
const m = r.model;
|
|
77
|
+
if (ctx.setActiveModel) ctx.setActiveModel(m || null);
|
|
78
|
+
try {
|
|
79
|
+
const c = (ctx.readConfig || _readConfig)();
|
|
80
|
+
if (m) c.model = m; else delete c.model;
|
|
81
|
+
(ctx.writeConfig || _writeConfig)(c);
|
|
82
|
+
if (ctx.cfg) { if (m) ctx.cfg.model = m; else delete ctx.cfg.model; }
|
|
83
|
+
} catch (_) { /* best-effort */ }
|
|
84
|
+
return m ? `model → ${m}` : 'model → (default)';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// `/orchestrator [status|on|off|planner|worker add|remove|maxsubtasks ...]`.
|
|
88
|
+
// With no subcommand and a modal available, opens an arrow-key menu where every
|
|
89
|
+
// edit (planner, workers, maxSubtasks) is fetch+pick — no typed specs.
|
|
90
|
+
export async function orchestratorSlash(args, ctx = {}) {
|
|
91
|
+
const read = typeof ctx.readConfig === 'function' ? ctx.readConfig : _readConfig;
|
|
92
|
+
const write = typeof ctx.writeConfig === 'function' ? ctx.writeConfig : _writeConfig;
|
|
93
|
+
const persist = (cfg) => { write(cfg); if (ctx.cfg) ctx.cfg.orchestrator = cfg.orchestrator; };
|
|
94
|
+
const parts = String(args || '').trim().split(/\s+/).filter(Boolean);
|
|
95
|
+
const fmt = () => {
|
|
96
|
+
const s = orchestratorGet(read());
|
|
97
|
+
return `orchestrator: ${s.active ? 'ON' : 'off'} · planner: ${s.planner || '(default)'} · workers: ${s.workers.length ? s.workers.join(', ') : '(none)'} · maxSubtasks: ${s.maxSubtasks}`;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
if (parts.length === 0 && typeof ctx.openPicker === 'function') {
|
|
101
|
+
const s = orchestratorGet(read());
|
|
102
|
+
const picked = await ctx.openPicker({
|
|
103
|
+
kind: 'menu',
|
|
104
|
+
title: 'Orchestration',
|
|
105
|
+
subtitle: `now ${s.active ? 'ON' : 'off'} · planner ${s.planner || '(default)'} · ${s.workers.length} worker(s) · max ${s.maxSubtasks}`,
|
|
106
|
+
items: [
|
|
107
|
+
{ id: 'planner', label: 'Set planner…', desc: 'pick provider + model' },
|
|
108
|
+
{ id: 'worker-add', label: 'Add worker…', desc: 'pick provider + model' },
|
|
109
|
+
{ id: 'worker-remove', label: 'Remove worker…', desc: 'pick from current workers' },
|
|
110
|
+
{ id: 'maxsubtasks', label: 'Max subtasks…', desc: 'cap subtasks per request (1–10)' },
|
|
111
|
+
{ id: 'on', label: 'Turn ON', desc: 'route chats through planner + workers' },
|
|
112
|
+
{ id: 'off', label: 'Turn OFF', desc: 'back to a single provider' },
|
|
113
|
+
{ id: 'status', label: 'Status', desc: 'show current config' },
|
|
114
|
+
],
|
|
115
|
+
});
|
|
116
|
+
const id = picked && typeof picked === 'object' ? picked.id : picked;
|
|
117
|
+
if (!id || typeof id !== 'string') return fmt();
|
|
118
|
+
if (id === 'planner' || id === 'worker-add' || id === 'worker-remove' || id === 'maxsubtasks') {
|
|
119
|
+
const registry = await import('../providers/registry.mjs');
|
|
120
|
+
const r = await orchestratorAction(ctx, registry, id);
|
|
121
|
+
return `${r}\n${fmt()}`;
|
|
122
|
+
}
|
|
123
|
+
return orchestratorSlash(id, ctx);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const sub = (parts[0] || 'status').toLowerCase();
|
|
127
|
+
if (sub === 'status') return fmt();
|
|
128
|
+
const cfg = read();
|
|
129
|
+
if (sub === 'on' || sub === 'enable') {
|
|
130
|
+
if (!orchestratorGet(cfg).planner) {
|
|
131
|
+
const base = cfg.provider && cfg.provider !== 'orchestrator' ? cfg.provider : 'claude-cli';
|
|
132
|
+
orchestratorSet(cfg, { planner: base });
|
|
133
|
+
}
|
|
134
|
+
orchestratorEnable(cfg, true); persist(cfg);
|
|
135
|
+
const after = orchestratorGet(read());
|
|
136
|
+
return after.workers.length ? 'orchestration ON.\n' + fmt() : 'orchestration ON — but no workers yet. Add one: /orchestrator worker add <provider[:model]>';
|
|
137
|
+
}
|
|
138
|
+
if (sub === 'off' || sub === 'disable') { orchestratorEnable(cfg, false); persist(cfg); return 'orchestration off. provider → ' + read().provider; }
|
|
139
|
+
if (sub === 'planner') {
|
|
140
|
+
// No spec + a modal → drill the picker instead of erroring.
|
|
141
|
+
if (!parts[1] && typeof ctx.openPicker === 'function') {
|
|
142
|
+
const registry = await import('../providers/registry.mjs');
|
|
143
|
+
const r = await orchestratorAction(ctx, registry, 'planner');
|
|
144
|
+
return `${r}\n${fmt()}`;
|
|
145
|
+
}
|
|
146
|
+
if (!parts[1]) return 'usage: /orchestrator planner <provider[:model]>'; orchestratorSet(cfg, { planner: parts[1] }); persist(cfg); return 'planner → ' + parts[1];
|
|
147
|
+
}
|
|
148
|
+
if (sub === 'maxsubtasks') { const n = parseInt(parts[1], 10); if (!Number.isFinite(n)) return 'usage: /orchestrator maxsubtasks <N>'; orchestratorSet(cfg, { maxSubtasks: Math.max(1, Math.min(10, n)) }); persist(cfg); return fmt(); }
|
|
149
|
+
if (sub === 'worker') {
|
|
150
|
+
const action = (parts[1] || '').toLowerCase(); const spec = parts[2];
|
|
151
|
+
const workers = [...orchestratorGet(cfg).workers];
|
|
152
|
+
// worker add / remove with no spec + a modal → route through the picker
|
|
153
|
+
// (add → provider/model pick; remove → pick from current workers).
|
|
154
|
+
if ((action === 'add' || action === 'remove' || action === 'rm' || action === 'delete') && !spec && typeof ctx.openPicker === 'function') {
|
|
155
|
+
const registry = await import('../providers/registry.mjs');
|
|
156
|
+
const r = await orchestratorAction(ctx, registry, action === 'add' ? 'worker-add' : 'worker-remove');
|
|
157
|
+
return `${r}\n${fmt()}`;
|
|
158
|
+
}
|
|
159
|
+
if (action === 'add' && spec) { if (!workers.includes(spec)) workers.push(spec); orchestratorSet(cfg, { workers }); persist(cfg); return 'workers: ' + workers.join(', '); }
|
|
160
|
+
if ((action === 'remove' || action === 'rm' || action === 'delete') && spec) { const next = workers.filter((w) => w !== spec); orchestratorSet(cfg, { workers: next }); persist(cfg); return 'workers: ' + (next.join(', ') || '(none)'); }
|
|
161
|
+
return 'usage: /orchestrator worker add|remove <provider[:model]>';
|
|
162
|
+
}
|
|
163
|
+
return 'usage: /orchestrator [status|on|off|planner <spec>|worker add|remove <spec>|maxsubtasks <N>]';
|
|
164
|
+
}
|