lazyclaw 6.1.0 → 6.3.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/commands/chat.mjs +17 -18
- package/commands/config_step.mjs +39 -0
- package/package.json +1 -1
- package/providers/codex_cli.mjs +17 -1
- package/providers/compat_vendors.mjs +161 -0
- package/providers/gemini_cli.mjs +19 -3
- package/providers/model_catalogue.mjs +142 -9
- package/providers/registry.mjs +31 -162
- package/tui/config_picker.mjs +54 -0
- package/tui/editor.mjs +17 -1
- package/tui/pickers.mjs +3 -3
- package/tui/provider_families.mjs +8 -6
- package/tui/slash_commands.mjs +2 -1
- package/tui/slash_dispatcher.mjs +8 -9
package/commands/chat.mjs
CHANGED
|
@@ -23,17 +23,16 @@ import { SLASH_COMMANDS } from '../tui/slash_commands.mjs';
|
|
|
23
23
|
|
|
24
24
|
// Legacy (non-Ink) slash routing for dispatcher-style, ctx-only commands.
|
|
25
25
|
// The Ink REPL routes every slash through _dispatchSlash/SLASH_HANDLERS, but
|
|
26
|
-
// the legacy readline path uses a hand-written switch (in cmdChat).
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
// drive — this is the seam that proves /config reaches setup on the legacy
|
|
30
|
-
// path (the post-loop guard re-runs cmdSetup when ctx.requestSetup is set).
|
|
31
|
-
// Returns 'EXIT' when the loop must break, or undefined when the command is
|
|
32
|
-
// not one this helper owns (caller falls through to its own handling).
|
|
26
|
+
// the legacy readline path uses a hand-written switch (in cmdChat). This
|
|
27
|
+
// exported helper is the wiring BOTH that switch and the regression test
|
|
28
|
+
// drive. Returns 'EXIT' to break the loop, undefined when not owned here.
|
|
33
29
|
export function legacySlashRoute(cmd, ctx) {
|
|
34
30
|
switch (cmd) {
|
|
31
|
+
// Legacy readline path has no modal picker, so BOTH /setup and /config
|
|
32
|
+
// route to the full wizard here (the Ink path gives /config its
|
|
33
|
+
// single-setting picker via tui/config_picker.mjs).
|
|
35
34
|
case '/config':
|
|
36
|
-
|
|
35
|
+
case '/setup':
|
|
37
36
|
ctx.requestSetup = true;
|
|
38
37
|
return 'EXIT';
|
|
39
38
|
default:
|
|
@@ -297,9 +296,13 @@ export async function cmdChat(flags = {}) {
|
|
|
297
296
|
pickerRef: _inkPickerRef,
|
|
298
297
|
}), { exitOnCtrlC: true, patchConsole: true });
|
|
299
298
|
await ink.waitUntilExit();
|
|
300
|
-
// /
|
|
301
|
-
//
|
|
299
|
+
// /setup → full wizard (then shell). /config single step → run JUST
|
|
300
|
+
// that step now that Ink released stdin, then re-enter chat.
|
|
302
301
|
if (_inkCtx.requestSetup) await (await import('./setup.mjs')).cmdSetup(undefined, [], {});
|
|
302
|
+
else if (_inkCtx.requestConfigStep) {
|
|
303
|
+
await (await import('./config_step.mjs')).runConfigStep(_inkCtx.requestConfigStep);
|
|
304
|
+
return cmdChat(flags);
|
|
305
|
+
}
|
|
303
306
|
return;
|
|
304
307
|
} catch (e) {
|
|
305
308
|
// Fall through to legacy path on any ink failure (missing import,
|
|
@@ -1110,14 +1113,10 @@ export async function cmdChat(flags = {}) {
|
|
|
1110
1113
|
} catch { /* /exit must never hang or throw */ }
|
|
1111
1114
|
return 'EXIT';
|
|
1112
1115
|
}
|
|
1113
|
-
case '/config':
|
|
1114
|
-
|
|
1115
|
-
// tests/f-config-slash-splash.test.mjs)
|
|
1116
|
-
//
|
|
1117
|
-
// _legacyCtx.requestSetup and returns 'EXIT'; the post-loop guard at the
|
|
1118
|
-
// bottom of cmdChat re-runs the setup wizard when requestSetup is set.
|
|
1119
|
-
// Without this case the legacy readline path fell through to `default:`
|
|
1120
|
-
// and printed "unknown slash" instead of launching setup.
|
|
1116
|
+
case '/config':
|
|
1117
|
+
case '/setup': {
|
|
1118
|
+
// Shared legacySlashRoute wiring (tests/f-config-slash-splash.test.mjs):
|
|
1119
|
+
// sets requestSetup + 'EXIT'; the post-loop guard runs the wizard.
|
|
1121
1120
|
return legacySlashRoute(cmd, _legacyCtx);
|
|
1122
1121
|
}
|
|
1123
1122
|
default: {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// commands/config_step.mjs — run ONE setup step outside the full wizard.
|
|
2
|
+
//
|
|
3
|
+
// Backs the in-chat `/config` picker: credential steps (channel tokens,
|
|
4
|
+
// outbound webhook) need raw readline prompts that can't run inside the Ink
|
|
5
|
+
// REPL, so the REPL unmounts with ctx.requestConfigStep set, chat.mjs calls
|
|
6
|
+
// runConfigStep(step) here, and then re-enters chat — the user changes one
|
|
7
|
+
// value (e.g. a webhook URL) without re-walking every wizard step.
|
|
8
|
+
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { configPath } from '../lib/config.mjs';
|
|
11
|
+
import { _quickPrompt } from '../tui/pickers.mjs';
|
|
12
|
+
import { runChannelStep, runWebhookStep } from './setup_channels.mjs';
|
|
13
|
+
|
|
14
|
+
const COLORS = {
|
|
15
|
+
accent: (s) => `\x1b[38;2;217;179;90m${s}\x1b[0m`,
|
|
16
|
+
bold: (s) => `\x1b[1m${s}\x1b[0m`,
|
|
17
|
+
dim: (s) => `\x1b[2m${s}\x1b[0m`,
|
|
18
|
+
ok: (s) => `\x1b[32m${s}\x1b[0m`,
|
|
19
|
+
warn: (s) => `\x1b[33m${s}\x1b[0m`,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export async function runConfigStep(step, deps = {}) {
|
|
23
|
+
const prompt = deps.prompt || _quickPrompt;
|
|
24
|
+
const colors = deps.colors || COLORS;
|
|
25
|
+
const write = deps.write || ((s) => process.stdout.write(s));
|
|
26
|
+
const cfgDir = deps.cfgDir || path.dirname(configPath());
|
|
27
|
+
|
|
28
|
+
write(`\n ${colors.bold(`⚙ config — ${step}`)}\n`);
|
|
29
|
+
if (step === 'channel') {
|
|
30
|
+
await runChannelStep({ cfgDir, prompt, colors, write });
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
if (step === 'webhook') {
|
|
34
|
+
await runWebhookStep({ prompt, colors, write });
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
write(` ${colors.warn(`unknown config step: ${step}`)}\n`);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.0",
|
|
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",
|
package/providers/codex_cli.mjs
CHANGED
|
@@ -144,6 +144,16 @@ export const codexCliProvider = {
|
|
|
144
144
|
const onAbort = () => { try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ } };
|
|
145
145
|
if (opts.signal) opts.signal.addEventListener('abort', onAbort);
|
|
146
146
|
|
|
147
|
+
// A missing binary surfaces as an ASYNC ChildProcess 'error' event on
|
|
148
|
+
// some platforms (the sync try/catch above doesn't see it). Without a
|
|
149
|
+
// listener that's an uncaughtException that kills the WHOLE process mid
|
|
150
|
+
// `providers test` — capture it and surface a per-provider CliMissingError
|
|
151
|
+
// instead (same fix claude_cli.mjs received in F8).
|
|
152
|
+
let spawnError = null;
|
|
153
|
+
const spawnErrorPromise = new Promise((resolve) => {
|
|
154
|
+
proc.once('error', (err) => { spawnError = err; resolve(); });
|
|
155
|
+
});
|
|
156
|
+
|
|
147
157
|
let stderr = '';
|
|
148
158
|
proc.stderr.setEncoding('utf8');
|
|
149
159
|
proc.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
@@ -184,7 +194,13 @@ export const codexCliProvider = {
|
|
|
184
194
|
if (text) yield text;
|
|
185
195
|
} catch (_) { /* incomplete tail — drop */ }
|
|
186
196
|
}
|
|
187
|
-
|
|
197
|
+
// Wait for either a clean exit or an async spawn error. On ENOENT the
|
|
198
|
+
// process never starts, so 'close' never fires — racing against
|
|
199
|
+
// spawnErrorPromise keeps this from hanging forever.
|
|
200
|
+
await Promise.race([exitPromise, spawnErrorPromise]);
|
|
201
|
+
if (spawnError) {
|
|
202
|
+
throw spawnError.code === 'ENOENT' ? new CliMissingError() : spawnError;
|
|
203
|
+
}
|
|
188
204
|
if (exitInfo && exitInfo.code !== 0 && !opts.signal?.aborted) {
|
|
189
205
|
throw new CliExitError(exitInfo.code, exitInfo.signal, stderr);
|
|
190
206
|
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// providers/compat_vendors.mjs — the built-in OpenAI-compatible vendor
|
|
2
|
+
// catalogue, split out of registry.mjs (file-size ratchet). Pure data:
|
|
3
|
+
// registry.mjs imports this and expands it into PROVIDERS / PROVIDER_INFO.
|
|
4
|
+
|
|
5
|
+
// Built-in OpenAI-compatible vendors. Same wire format → one factory call
|
|
6
|
+
// each. The picker treats these like first-class providers so users don't
|
|
7
|
+
// have to walk through "+ Add a custom endpoint" for the popular ones.
|
|
8
|
+
//
|
|
9
|
+
// Each entry must define baseUrl + envKey (the env var the chat path
|
|
10
|
+
// consults when no api-key is configured) + suggestedModels (curated list
|
|
11
|
+
// shown before the user fetches the live /v1/models catalogue).
|
|
12
|
+
//
|
|
13
|
+
// Adding a new vendor: drop a row here. The PROVIDERS / PROVIDER_INFO loops
|
|
14
|
+
// below pick it up automatically.
|
|
15
|
+
export const OPENAI_COMPAT_BUILTINS = {
|
|
16
|
+
nim: {
|
|
17
|
+
label: 'NVIDIA NIM',
|
|
18
|
+
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
|
19
|
+
envKey: 'NVIDIA_API_KEY',
|
|
20
|
+
altEnvKeys: ['NIM_API_KEY'],
|
|
21
|
+
keyPrefix: 'nvapi-',
|
|
22
|
+
docs: 'NVIDIA NIM hosted catalogue (Llama 3.x, Nemotron, DeepSeek-R1, Mixtral, Phi-3, Qwen, etc.). Auth: NVIDIA_API_KEY env var or in-app api-key. Endpoint speaks the OpenAI v1 wire format.',
|
|
23
|
+
defaultModel: 'meta/llama-3.1-405b-instruct',
|
|
24
|
+
suggestedModels: [
|
|
25
|
+
'meta/llama-3.1-405b-instruct',
|
|
26
|
+
'meta/llama-3.1-70b-instruct',
|
|
27
|
+
'meta/llama-3.1-8b-instruct',
|
|
28
|
+
'nvidia/llama-3.1-nemotron-70b-instruct',
|
|
29
|
+
'nvidia/nemotron-mini-4b-instruct',
|
|
30
|
+
'nvidia/llama-3.3-nemotron-super-49b-v1',
|
|
31
|
+
'mistralai/mistral-nemo-12b-instruct',
|
|
32
|
+
'mistralai/mixtral-8x22b-instruct-v0.1',
|
|
33
|
+
'microsoft/phi-3-medium-4k-instruct',
|
|
34
|
+
'deepseek-ai/deepseek-r1',
|
|
35
|
+
'qwen/qwen2.5-7b-instruct',
|
|
36
|
+
'qwen/qwen2.5-coder-32b-instruct',
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
openrouter: {
|
|
40
|
+
label: 'OpenRouter',
|
|
41
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
42
|
+
envKey: 'OPENROUTER_API_KEY',
|
|
43
|
+
keyPrefix: 'sk-or-',
|
|
44
|
+
docs: 'OpenRouter unified gateway — 200+ models behind one OpenAI-compatible endpoint. Auth: OPENROUTER_API_KEY env var or in-app api-key. Uses x-title/HTTP-Referer headers for attribution.',
|
|
45
|
+
defaultModel: 'anthropic/claude-3.5-sonnet',
|
|
46
|
+
headers: { 'http-referer': 'https://github.com/cmblir/lazyclaude', 'x-title': 'lazyclaw' },
|
|
47
|
+
suggestedModels: [
|
|
48
|
+
'anthropic/claude-3.5-sonnet',
|
|
49
|
+
'anthropic/claude-3-opus',
|
|
50
|
+
'openai/gpt-4o',
|
|
51
|
+
'openai/gpt-4o-mini',
|
|
52
|
+
'openai/o1-preview',
|
|
53
|
+
'meta-llama/llama-3.1-405b-instruct',
|
|
54
|
+
'meta-llama/llama-3.3-70b-instruct',
|
|
55
|
+
'google/gemini-2.0-flash-exp:free',
|
|
56
|
+
'google/gemini-pro-1.5',
|
|
57
|
+
'deepseek/deepseek-chat',
|
|
58
|
+
'deepseek/deepseek-r1',
|
|
59
|
+
'qwen/qwen-2.5-coder-32b-instruct',
|
|
60
|
+
'mistralai/mistral-large',
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
groq: {
|
|
64
|
+
label: 'Groq',
|
|
65
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
66
|
+
envKey: 'GROQ_API_KEY',
|
|
67
|
+
keyPrefix: 'gsk_',
|
|
68
|
+
docs: 'Groq LPU inference — fastest-token-per-second tier for Llama / Mixtral / Gemma. Auth: GROQ_API_KEY env var or in-app api-key.',
|
|
69
|
+
defaultModel: 'llama-3.3-70b-versatile',
|
|
70
|
+
suggestedModels: [
|
|
71
|
+
'llama-3.3-70b-versatile',
|
|
72
|
+
'llama-3.1-70b-versatile',
|
|
73
|
+
'llama-3.1-8b-instant',
|
|
74
|
+
'llama-3.2-90b-vision-preview',
|
|
75
|
+
'mixtral-8x7b-32768',
|
|
76
|
+
'gemma2-9b-it',
|
|
77
|
+
'qwen-2.5-coder-32b',
|
|
78
|
+
'qwen-2.5-32b',
|
|
79
|
+
'deepseek-r1-distill-llama-70b',
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
together: {
|
|
83
|
+
label: 'Together AI',
|
|
84
|
+
baseUrl: 'https://api.together.xyz/v1',
|
|
85
|
+
envKey: 'TOGETHER_API_KEY',
|
|
86
|
+
docs: 'Together AI hosted inference for open-weight models (Llama, Mixtral, Qwen, DeepSeek, etc.). Auth: TOGETHER_API_KEY env var or in-app api-key.',
|
|
87
|
+
defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
88
|
+
suggestedModels: [
|
|
89
|
+
'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
90
|
+
'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
|
|
91
|
+
'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
|
|
92
|
+
'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
|
|
93
|
+
'mistralai/Mixtral-8x22B-Instruct-v0.1',
|
|
94
|
+
'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
|
95
|
+
'Qwen/Qwen2.5-72B-Instruct-Turbo',
|
|
96
|
+
'Qwen/Qwen2.5-Coder-32B-Instruct',
|
|
97
|
+
'deepseek-ai/DeepSeek-V3',
|
|
98
|
+
'deepseek-ai/DeepSeek-R1',
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
xai: {
|
|
102
|
+
label: 'xAI (Grok)',
|
|
103
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
104
|
+
envKey: 'XAI_API_KEY',
|
|
105
|
+
altEnvKeys: ['GROK_API_KEY'],
|
|
106
|
+
keyPrefix: 'xai-',
|
|
107
|
+
docs: 'xAI Grok models. Auth: XAI_API_KEY env var or in-app api-key.',
|
|
108
|
+
defaultModel: 'grok-2-latest',
|
|
109
|
+
suggestedModels: [
|
|
110
|
+
'grok-2-latest',
|
|
111
|
+
'grok-2-1212',
|
|
112
|
+
'grok-2-vision-1212',
|
|
113
|
+
'grok-beta',
|
|
114
|
+
'grok-vision-beta',
|
|
115
|
+
],
|
|
116
|
+
},
|
|
117
|
+
deepseek: {
|
|
118
|
+
label: 'DeepSeek',
|
|
119
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
120
|
+
envKey: 'DEEPSEEK_API_KEY',
|
|
121
|
+
keyPrefix: 'sk-',
|
|
122
|
+
docs: 'DeepSeek (deepseek-chat / deepseek-reasoner). Auth: DEEPSEEK_API_KEY env var or in-app api-key.',
|
|
123
|
+
defaultModel: 'deepseek-chat',
|
|
124
|
+
suggestedModels: [
|
|
125
|
+
'deepseek-chat',
|
|
126
|
+
'deepseek-reasoner',
|
|
127
|
+
'deepseek-coder',
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
mistral: {
|
|
131
|
+
label: 'Mistral La Plateforme',
|
|
132
|
+
baseUrl: 'https://api.mistral.ai/v1',
|
|
133
|
+
envKey: 'MISTRAL_API_KEY',
|
|
134
|
+
docs: 'Mistral La Plateforme (mistral-large, codestral, ministral, pixtral). Auth: MISTRAL_API_KEY env var or in-app api-key.',
|
|
135
|
+
defaultModel: 'mistral-large-latest',
|
|
136
|
+
suggestedModels: [
|
|
137
|
+
'mistral-large-latest',
|
|
138
|
+
'mistral-small-latest',
|
|
139
|
+
'codestral-latest',
|
|
140
|
+
'ministral-8b-latest',
|
|
141
|
+
'ministral-3b-latest',
|
|
142
|
+
'pixtral-large-latest',
|
|
143
|
+
'open-mistral-nemo',
|
|
144
|
+
],
|
|
145
|
+
},
|
|
146
|
+
fireworks: {
|
|
147
|
+
label: 'Fireworks AI',
|
|
148
|
+
baseUrl: 'https://api.fireworks.ai/inference/v1',
|
|
149
|
+
envKey: 'FIREWORKS_API_KEY',
|
|
150
|
+
docs: 'Fireworks AI hosted models. Auth: FIREWORKS_API_KEY env var or in-app api-key.',
|
|
151
|
+
defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
152
|
+
suggestedModels: [
|
|
153
|
+
'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
154
|
+
'accounts/fireworks/models/llama-v3p1-405b-instruct',
|
|
155
|
+
'accounts/fireworks/models/qwen2p5-coder-32b-instruct',
|
|
156
|
+
'accounts/fireworks/models/deepseek-r1',
|
|
157
|
+
'accounts/fireworks/models/deepseek-v3',
|
|
158
|
+
'accounts/fireworks/models/mixtral-8x22b-instruct',
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
};
|
package/providers/gemini_cli.mjs
CHANGED
|
@@ -133,6 +133,16 @@ export const geminiCliProvider = {
|
|
|
133
133
|
const onAbort = () => { try { proc.kill('SIGTERM'); } catch (_) { /* ignore */ } };
|
|
134
134
|
if (opts.signal) opts.signal.addEventListener('abort', onAbort);
|
|
135
135
|
|
|
136
|
+
// A missing binary surfaces as an ASYNC ChildProcess 'error' event on
|
|
137
|
+
// some platforms (the sync try/catch above doesn't see it). Without a
|
|
138
|
+
// listener that's an uncaughtException that kills the WHOLE process mid
|
|
139
|
+
// `providers test` — capture it and surface a per-provider CliMissingError
|
|
140
|
+
// instead (same fix claude_cli.mjs received in F8).
|
|
141
|
+
let spawnError = null;
|
|
142
|
+
const spawnErrorPromise = new Promise((resolve) => {
|
|
143
|
+
proc.once('error', (err) => { spawnError = err; resolve(); });
|
|
144
|
+
});
|
|
145
|
+
|
|
136
146
|
let stdout = '';
|
|
137
147
|
let stderr = '';
|
|
138
148
|
proc.stdout.setEncoding('utf8');
|
|
@@ -140,9 +150,15 @@ export const geminiCliProvider = {
|
|
|
140
150
|
proc.stdout.on('data', (c) => { stdout += c; });
|
|
141
151
|
proc.stderr.on('data', (c) => { stderr += c; });
|
|
142
152
|
|
|
143
|
-
const exitInfo = await
|
|
144
|
-
proc.on('close', (code, signal) => resolve({ code, signal }));
|
|
145
|
-
|
|
153
|
+
const exitInfo = await Promise.race([
|
|
154
|
+
new Promise((resolve) => { proc.on('close', (code, signal) => resolve({ code, signal })); }),
|
|
155
|
+
spawnErrorPromise.then(() => null),
|
|
156
|
+
]);
|
|
157
|
+
if (spawnError) {
|
|
158
|
+
if (opts.signal) opts.signal.removeEventListener('abort', onAbort);
|
|
159
|
+
if (spawnError.code === 'ENOENT') throw new CliMissingError();
|
|
160
|
+
throw spawnError;
|
|
161
|
+
}
|
|
146
162
|
|
|
147
163
|
try {
|
|
148
164
|
if (opts.signal?.aborted) throw new AbortError('aborted mid-run');
|
|
@@ -11,13 +11,18 @@
|
|
|
11
11
|
// Dependency-injected (no cli.mjs internals) so it stays import-light and
|
|
12
12
|
// unit-testable with no network.
|
|
13
13
|
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import os from 'node:os';
|
|
16
|
+
import path from 'node:path';
|
|
17
|
+
|
|
14
18
|
/**
|
|
15
|
-
* Whether a provider exposes
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* endpoints
|
|
20
|
-
*
|
|
19
|
+
* Whether a provider exposes a model catalogue we can live-fetch. True for
|
|
20
|
+
* openai, ollama, any builtin OpenAI-compat vendor (nim / openrouter / groq /
|
|
21
|
+
* together / xai / deepseek / mistral / fireworks), any provider carrying an
|
|
22
|
+
* explicit `baseUrl` (custom endpoints), and — via their NATIVE list
|
|
23
|
+
* endpoints — anthropic (`GET /v1/models`) and gemini
|
|
24
|
+
* (`GET /v1beta/models`). False for claude-cli (keyless subprocess, no
|
|
25
|
+
* catalogue endpoint) / mock / orchestrator.
|
|
21
26
|
*
|
|
22
27
|
* @param {object} meta PROVIDER_INFO[providerId]
|
|
23
28
|
* @param {string} providerId
|
|
@@ -28,9 +33,70 @@ export function supportsLiveFetch(meta, providerId) {
|
|
|
28
33
|
return !!m.baseUrl
|
|
29
34
|
|| providerId === 'openai'
|
|
30
35
|
|| providerId === 'ollama'
|
|
36
|
+
|| providerId === 'anthropic'
|
|
37
|
+
|| providerId === 'gemini'
|
|
38
|
+
// Keyless CLI providers borrow the credential their vendor accepts
|
|
39
|
+
// (anthropic key / Claude Code OAuth token; gemini key; openai key or
|
|
40
|
+
// a plain key stored in ~/.codex/auth.json) — best-effort with an
|
|
41
|
+
// honest, actionable error when none is available.
|
|
42
|
+
|| providerId === 'claude-cli'
|
|
43
|
+
|| providerId === 'gemini-cli'
|
|
44
|
+
|| providerId === 'codex-cli'
|
|
31
45
|
|| !!m.builtinOpenAICompat;
|
|
32
46
|
}
|
|
33
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Live-list Anthropic models via the native Models API. Surfaces newly
|
|
50
|
+
* released models (e.g. claude-fable-5) the day they ship instead of waiting
|
|
51
|
+
* for a curated-list update. Sorted, deduped.
|
|
52
|
+
*
|
|
53
|
+
* @param {{apiKey:string, fetchImpl?:typeof fetch}} opts
|
|
54
|
+
* @returns {Promise<string[]>}
|
|
55
|
+
*/
|
|
56
|
+
export async function fetchAnthropicModels({ apiKey, oauthToken, fetchImpl } = {}) {
|
|
57
|
+
if (!apiKey && !oauthToken) throw new Error('anthropic model listing requires an api key (set ANTHROPIC_API_KEY or configure the provider)');
|
|
58
|
+
const f = fetchImpl || globalThis.fetch;
|
|
59
|
+
// OAuth tokens (Claude Code subscription login) authenticate with a Bearer
|
|
60
|
+
// header plus the documented oauth beta header; api keys use x-api-key.
|
|
61
|
+
const auth = apiKey
|
|
62
|
+
? { 'x-api-key': apiKey }
|
|
63
|
+
: { 'authorization': `Bearer ${oauthToken}`, 'anthropic-beta': 'oauth-2025-04-20' };
|
|
64
|
+
const res = await f('https://api.anthropic.com/v1/models?limit=1000', {
|
|
65
|
+
method: 'GET',
|
|
66
|
+
headers: { ...auth, 'anthropic-version': '2023-06-01', 'accept': 'application/json' },
|
|
67
|
+
});
|
|
68
|
+
if (!res.ok) throw new Error(`anthropic /v1/models returned HTTP ${res.status}`);
|
|
69
|
+
const obj = await res.json();
|
|
70
|
+
const ids = (Array.isArray(obj?.data) ? obj.data : [])
|
|
71
|
+
.map((m) => m && m.id)
|
|
72
|
+
.filter((id) => typeof id === 'string');
|
|
73
|
+
return Array.from(new Set(ids)).sort((a, b) => a.localeCompare(b));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Live-list Gemini models via the Generative Language API, keeping only
|
|
78
|
+
* chat-capable entries (supportedGenerationMethods includes
|
|
79
|
+
* generateContent) and stripping the `models/` resource prefix.
|
|
80
|
+
*
|
|
81
|
+
* @param {{apiKey:string, fetchImpl?:typeof fetch}} opts
|
|
82
|
+
* @returns {Promise<string[]>}
|
|
83
|
+
*/
|
|
84
|
+
export async function fetchGeminiModels({ apiKey, fetchImpl } = {}) {
|
|
85
|
+
if (!apiKey) throw new Error('gemini model listing requires an api key (set GEMINI_API_KEY or configure the provider)');
|
|
86
|
+
const f = fetchImpl || globalThis.fetch;
|
|
87
|
+
const res = await f(`https://generativelanguage.googleapis.com/v1beta/models?pageSize=1000&key=${encodeURIComponent(apiKey)}`, {
|
|
88
|
+
method: 'GET',
|
|
89
|
+
headers: { 'accept': 'application/json' },
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) throw new Error(`gemini models list returned HTTP ${res.status}`);
|
|
92
|
+
const obj = await res.json();
|
|
93
|
+
const ids = (Array.isArray(obj?.models) ? obj.models : [])
|
|
94
|
+
.filter((m) => Array.isArray(m?.supportedGenerationMethods) && m.supportedGenerationMethods.includes('generateContent'))
|
|
95
|
+
.map((m) => String(m.name || '').replace(/^models\//, ''))
|
|
96
|
+
.filter(Boolean);
|
|
97
|
+
return Array.from(new Set(ids)).sort((a, b) => a.localeCompare(b));
|
|
98
|
+
}
|
|
99
|
+
|
|
34
100
|
/**
|
|
35
101
|
* Resolve `{ baseUrl, apiKey }` for a provider's OpenAI-compatible
|
|
36
102
|
* `/v1/models` endpoint. Returns `null` when the provider has no such
|
|
@@ -75,12 +141,79 @@ export function modelCatalogueFor({ cfg, registryMod, resolveAuthKey, providerId
|
|
|
75
141
|
* @param {object} deps same shape as {@link modelCatalogueFor}
|
|
76
142
|
* @returns {Promise<string[]>}
|
|
77
143
|
*/
|
|
144
|
+
// Claude Code OAuth token from the credential store `claude login` writes on
|
|
145
|
+
// Linux / non-keychain setups. On macOS the token lives in the OS Keychain
|
|
146
|
+
// (no file), so this returns null there — the caller falls through to its
|
|
147
|
+
// honest "set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN" error. Read-only;
|
|
148
|
+
// the token is only ever sent to api.anthropic.com, never logged.
|
|
149
|
+
export function _claudeCodeOAuthToken({ home, readFileSync } = {}) {
|
|
150
|
+
const h = home || os.homedir();
|
|
151
|
+
const read = readFileSync || fs.readFileSync;
|
|
152
|
+
for (const rel of ['.claude/.credentials.json', '.config/claude/.credentials.json']) {
|
|
153
|
+
try {
|
|
154
|
+
const j = JSON.parse(read(path.join(h, rel), 'utf8'));
|
|
155
|
+
const tok = j?.claudeAiOauth?.accessToken || j?.accessToken || j?.access_token;
|
|
156
|
+
if (typeof tok === 'string' && tok) return tok;
|
|
157
|
+
} catch { /* missing / unreadable / not JSON — try the next location */ }
|
|
158
|
+
}
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// A plain API key stored by `codex login --api-key` in ~/.codex/auth.json.
|
|
163
|
+
// ChatGPT-OAuth logins store an empty OPENAI_API_KEY object plus OAuth
|
|
164
|
+
// tokens — those do NOT authenticate the platform /v1/models endpoint, so
|
|
165
|
+
// they are deliberately ignored.
|
|
166
|
+
export function _codexStoredApiKey({ home, readFileSync } = {}) {
|
|
167
|
+
const h = home || os.homedir();
|
|
168
|
+
const read = readFileSync || fs.readFileSync;
|
|
169
|
+
try {
|
|
170
|
+
const j = JSON.parse(read(path.join(h, '.codex/auth.json'), 'utf8'));
|
|
171
|
+
const k = j?.OPENAI_API_KEY;
|
|
172
|
+
if (typeof k === 'string' && k) return k;
|
|
173
|
+
if (k && typeof k === 'object' && typeof k.value === 'string' && k.value) return k.value;
|
|
174
|
+
} catch { /* missing / unreadable */ }
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
78
178
|
export async function fetchModelsForProvider(deps) {
|
|
79
|
-
const c = modelCatalogueFor(deps);
|
|
80
179
|
const providerId = deps && deps.providerId;
|
|
180
|
+
const key = (id) => (typeof deps?.resolveAuthKey === 'function' ? deps.resolveAuthKey(id) : '') || '';
|
|
181
|
+
const credReader = deps?._credReader; // test seam: () => token|null per helper
|
|
182
|
+
// Native-API providers list through their own endpoints (they are not
|
|
183
|
+
// OpenAI-compatible). Env fallbacks cover the common keyless-config case.
|
|
184
|
+
if (providerId === 'anthropic') {
|
|
185
|
+
return fetchAnthropicModels({ apiKey: key('anthropic') || process.env.ANTHROPIC_API_KEY || '', fetchImpl: deps?.fetchImpl });
|
|
186
|
+
}
|
|
187
|
+
if (providerId === 'gemini') {
|
|
188
|
+
return fetchGeminiModels({ apiKey: key('gemini') || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || '', fetchImpl: deps?.fetchImpl });
|
|
189
|
+
}
|
|
190
|
+
// Keyless CLI providers: borrow the credential their vendor accepts.
|
|
191
|
+
if (providerId === 'claude-cli') {
|
|
192
|
+
const apiKey = key('claude-cli') || key('anthropic') || process.env.ANTHROPIC_API_KEY || '';
|
|
193
|
+
if (apiKey) return fetchAnthropicModels({ apiKey, fetchImpl: deps?.fetchImpl });
|
|
194
|
+
const oauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN
|
|
195
|
+
|| (credReader ? credReader('claude') : _claudeCodeOAuthToken());
|
|
196
|
+
if (oauthToken) return fetchAnthropicModels({ oauthToken, fetchImpl: deps?.fetchImpl });
|
|
197
|
+
throw new Error('claude-cli model listing needs a credential: set ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN (on macOS the `claude` login lives in the Keychain, which is not readable here)');
|
|
198
|
+
}
|
|
199
|
+
if (providerId === 'gemini-cli') {
|
|
200
|
+
const apiKey = key('gemini-cli') || key('gemini') || process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY || '';
|
|
201
|
+
if (apiKey) return fetchGeminiModels({ apiKey, fetchImpl: deps?.fetchImpl });
|
|
202
|
+
throw new Error('gemini-cli model listing needs a credential: set GEMINI_API_KEY or GOOGLE_API_KEY (the `gemini` CLI login token cannot list models)');
|
|
203
|
+
}
|
|
204
|
+
if (providerId === 'codex-cli') {
|
|
205
|
+
const apiKey = key('codex-cli') || key('openai') || process.env.OPENAI_API_KEY
|
|
206
|
+
|| (credReader ? credReader('codex') : _codexStoredApiKey()) || '';
|
|
207
|
+
if (apiKey) {
|
|
208
|
+
const { fetchOpenAICompatModels } = await import('./openai_compat.mjs');
|
|
209
|
+
return fetchOpenAICompatModels({ baseUrl: 'https://api.openai.com/v1', apiKey, fetch: deps?.fetchImpl });
|
|
210
|
+
}
|
|
211
|
+
throw new Error('codex-cli model listing needs a credential: set OPENAI_API_KEY (a ChatGPT-plan `codex` login has no platform API key)');
|
|
212
|
+
}
|
|
213
|
+
const c = modelCatalogueFor(deps);
|
|
81
214
|
if (!c) {
|
|
82
|
-
throw new Error(`provider "${providerId}" does not expose
|
|
215
|
+
throw new Error(`provider "${providerId}" does not expose a model catalogue endpoint`);
|
|
83
216
|
}
|
|
84
217
|
const { fetchOpenAICompatModels } = await import('./openai_compat.mjs');
|
|
85
|
-
return fetchOpenAICompatModels({ baseUrl: c.baseUrl, apiKey: c.apiKey });
|
|
218
|
+
return fetchOpenAICompatModels({ baseUrl: c.baseUrl, apiKey: c.apiKey, fetch: deps?.fetchImpl });
|
|
86
219
|
}
|
package/providers/registry.mjs
CHANGED
|
@@ -12,9 +12,12 @@ import { openaiProvider } from './openai.mjs';
|
|
|
12
12
|
import { ollamaProvider } from './ollama.mjs';
|
|
13
13
|
import { geminiProvider } from './gemini.mjs';
|
|
14
14
|
import { claudeCliProvider } from './claude_cli.mjs';
|
|
15
|
+
import { geminiCliProvider } from './gemini_cli.mjs';
|
|
16
|
+
import { codexCliProvider } from './codex_cli.mjs';
|
|
15
17
|
import { hasClaudeCliSession } from './claude_cli_detect.mjs';
|
|
16
18
|
import { makeOpenAICompatProvider, fetchOpenAICompatModels } from './openai_compat.mjs';
|
|
17
19
|
import { makeOrchestratorProvider } from './orchestrator.mjs';
|
|
20
|
+
import { OPENAI_COMPAT_BUILTINS } from './compat_vendors.mjs';
|
|
18
21
|
|
|
19
22
|
/**
|
|
20
23
|
* @typedef {{ role: 'user'|'assistant'|'system', content: string }} ChatMessage
|
|
@@ -116,163 +119,10 @@ export function resolveTrainer(cfg, opts = {}) {
|
|
|
116
119
|
return { provider: t.provider, model: t.model || chatModel };
|
|
117
120
|
}
|
|
118
121
|
|
|
119
|
-
// Built-in OpenAI-compatible
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
|
|
123
|
-
// Each entry must define baseUrl + envKey (the env var the chat path
|
|
124
|
-
// consults when no api-key is configured) + suggestedModels (curated list
|
|
125
|
-
// shown before the user fetches the live /v1/models catalogue).
|
|
126
|
-
//
|
|
127
|
-
// Adding a new vendor: drop a row here. The PROVIDERS / PROVIDER_INFO loops
|
|
128
|
-
// below pick it up automatically.
|
|
129
|
-
export const OPENAI_COMPAT_BUILTINS = {
|
|
130
|
-
nim: {
|
|
131
|
-
label: 'NVIDIA NIM',
|
|
132
|
-
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
|
133
|
-
envKey: 'NVIDIA_API_KEY',
|
|
134
|
-
altEnvKeys: ['NIM_API_KEY'],
|
|
135
|
-
keyPrefix: 'nvapi-',
|
|
136
|
-
docs: 'NVIDIA NIM hosted catalogue (Llama 3.x, Nemotron, DeepSeek-R1, Mixtral, Phi-3, Qwen, etc.). Auth: NVIDIA_API_KEY env var or in-app api-key. Endpoint speaks the OpenAI v1 wire format.',
|
|
137
|
-
defaultModel: 'meta/llama-3.1-405b-instruct',
|
|
138
|
-
suggestedModels: [
|
|
139
|
-
'meta/llama-3.1-405b-instruct',
|
|
140
|
-
'meta/llama-3.1-70b-instruct',
|
|
141
|
-
'meta/llama-3.1-8b-instruct',
|
|
142
|
-
'nvidia/llama-3.1-nemotron-70b-instruct',
|
|
143
|
-
'nvidia/nemotron-mini-4b-instruct',
|
|
144
|
-
'nvidia/llama-3.3-nemotron-super-49b-v1',
|
|
145
|
-
'mistralai/mistral-nemo-12b-instruct',
|
|
146
|
-
'mistralai/mixtral-8x22b-instruct-v0.1',
|
|
147
|
-
'microsoft/phi-3-medium-4k-instruct',
|
|
148
|
-
'deepseek-ai/deepseek-r1',
|
|
149
|
-
'qwen/qwen2.5-7b-instruct',
|
|
150
|
-
'qwen/qwen2.5-coder-32b-instruct',
|
|
151
|
-
],
|
|
152
|
-
},
|
|
153
|
-
openrouter: {
|
|
154
|
-
label: 'OpenRouter',
|
|
155
|
-
baseUrl: 'https://openrouter.ai/api/v1',
|
|
156
|
-
envKey: 'OPENROUTER_API_KEY',
|
|
157
|
-
keyPrefix: 'sk-or-',
|
|
158
|
-
docs: 'OpenRouter unified gateway — 200+ models behind one OpenAI-compatible endpoint. Auth: OPENROUTER_API_KEY env var or in-app api-key. Uses x-title/HTTP-Referer headers for attribution.',
|
|
159
|
-
defaultModel: 'anthropic/claude-3.5-sonnet',
|
|
160
|
-
headers: { 'http-referer': 'https://github.com/cmblir/lazyclaude', 'x-title': 'lazyclaw' },
|
|
161
|
-
suggestedModels: [
|
|
162
|
-
'anthropic/claude-3.5-sonnet',
|
|
163
|
-
'anthropic/claude-3-opus',
|
|
164
|
-
'openai/gpt-4o',
|
|
165
|
-
'openai/gpt-4o-mini',
|
|
166
|
-
'openai/o1-preview',
|
|
167
|
-
'meta-llama/llama-3.1-405b-instruct',
|
|
168
|
-
'meta-llama/llama-3.3-70b-instruct',
|
|
169
|
-
'google/gemini-2.0-flash-exp:free',
|
|
170
|
-
'google/gemini-pro-1.5',
|
|
171
|
-
'deepseek/deepseek-chat',
|
|
172
|
-
'deepseek/deepseek-r1',
|
|
173
|
-
'qwen/qwen-2.5-coder-32b-instruct',
|
|
174
|
-
'mistralai/mistral-large',
|
|
175
|
-
],
|
|
176
|
-
},
|
|
177
|
-
groq: {
|
|
178
|
-
label: 'Groq',
|
|
179
|
-
baseUrl: 'https://api.groq.com/openai/v1',
|
|
180
|
-
envKey: 'GROQ_API_KEY',
|
|
181
|
-
keyPrefix: 'gsk_',
|
|
182
|
-
docs: 'Groq LPU inference — fastest-token-per-second tier for Llama / Mixtral / Gemma. Auth: GROQ_API_KEY env var or in-app api-key.',
|
|
183
|
-
defaultModel: 'llama-3.3-70b-versatile',
|
|
184
|
-
suggestedModels: [
|
|
185
|
-
'llama-3.3-70b-versatile',
|
|
186
|
-
'llama-3.1-70b-versatile',
|
|
187
|
-
'llama-3.1-8b-instant',
|
|
188
|
-
'llama-3.2-90b-vision-preview',
|
|
189
|
-
'mixtral-8x7b-32768',
|
|
190
|
-
'gemma2-9b-it',
|
|
191
|
-
'qwen-2.5-coder-32b',
|
|
192
|
-
'qwen-2.5-32b',
|
|
193
|
-
'deepseek-r1-distill-llama-70b',
|
|
194
|
-
],
|
|
195
|
-
},
|
|
196
|
-
together: {
|
|
197
|
-
label: 'Together AI',
|
|
198
|
-
baseUrl: 'https://api.together.xyz/v1',
|
|
199
|
-
envKey: 'TOGETHER_API_KEY',
|
|
200
|
-
docs: 'Together AI hosted inference for open-weight models (Llama, Mixtral, Qwen, DeepSeek, etc.). Auth: TOGETHER_API_KEY env var or in-app api-key.',
|
|
201
|
-
defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
202
|
-
suggestedModels: [
|
|
203
|
-
'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
204
|
-
'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
|
|
205
|
-
'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
|
|
206
|
-
'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
|
|
207
|
-
'mistralai/Mixtral-8x22B-Instruct-v0.1',
|
|
208
|
-
'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
|
209
|
-
'Qwen/Qwen2.5-72B-Instruct-Turbo',
|
|
210
|
-
'Qwen/Qwen2.5-Coder-32B-Instruct',
|
|
211
|
-
'deepseek-ai/DeepSeek-V3',
|
|
212
|
-
'deepseek-ai/DeepSeek-R1',
|
|
213
|
-
],
|
|
214
|
-
},
|
|
215
|
-
xai: {
|
|
216
|
-
label: 'xAI (Grok)',
|
|
217
|
-
baseUrl: 'https://api.x.ai/v1',
|
|
218
|
-
envKey: 'XAI_API_KEY',
|
|
219
|
-
altEnvKeys: ['GROK_API_KEY'],
|
|
220
|
-
keyPrefix: 'xai-',
|
|
221
|
-
docs: 'xAI Grok models. Auth: XAI_API_KEY env var or in-app api-key.',
|
|
222
|
-
defaultModel: 'grok-2-latest',
|
|
223
|
-
suggestedModels: [
|
|
224
|
-
'grok-2-latest',
|
|
225
|
-
'grok-2-1212',
|
|
226
|
-
'grok-2-vision-1212',
|
|
227
|
-
'grok-beta',
|
|
228
|
-
'grok-vision-beta',
|
|
229
|
-
],
|
|
230
|
-
},
|
|
231
|
-
deepseek: {
|
|
232
|
-
label: 'DeepSeek',
|
|
233
|
-
baseUrl: 'https://api.deepseek.com/v1',
|
|
234
|
-
envKey: 'DEEPSEEK_API_KEY',
|
|
235
|
-
keyPrefix: 'sk-',
|
|
236
|
-
docs: 'DeepSeek (deepseek-chat / deepseek-reasoner). Auth: DEEPSEEK_API_KEY env var or in-app api-key.',
|
|
237
|
-
defaultModel: 'deepseek-chat',
|
|
238
|
-
suggestedModels: [
|
|
239
|
-
'deepseek-chat',
|
|
240
|
-
'deepseek-reasoner',
|
|
241
|
-
'deepseek-coder',
|
|
242
|
-
],
|
|
243
|
-
},
|
|
244
|
-
mistral: {
|
|
245
|
-
label: 'Mistral La Plateforme',
|
|
246
|
-
baseUrl: 'https://api.mistral.ai/v1',
|
|
247
|
-
envKey: 'MISTRAL_API_KEY',
|
|
248
|
-
docs: 'Mistral La Plateforme (mistral-large, codestral, ministral, pixtral). Auth: MISTRAL_API_KEY env var or in-app api-key.',
|
|
249
|
-
defaultModel: 'mistral-large-latest',
|
|
250
|
-
suggestedModels: [
|
|
251
|
-
'mistral-large-latest',
|
|
252
|
-
'mistral-small-latest',
|
|
253
|
-
'codestral-latest',
|
|
254
|
-
'ministral-8b-latest',
|
|
255
|
-
'ministral-3b-latest',
|
|
256
|
-
'pixtral-large-latest',
|
|
257
|
-
'open-mistral-nemo',
|
|
258
|
-
],
|
|
259
|
-
},
|
|
260
|
-
fireworks: {
|
|
261
|
-
label: 'Fireworks AI',
|
|
262
|
-
baseUrl: 'https://api.fireworks.ai/inference/v1',
|
|
263
|
-
envKey: 'FIREWORKS_API_KEY',
|
|
264
|
-
docs: 'Fireworks AI hosted models. Auth: FIREWORKS_API_KEY env var or in-app api-key.',
|
|
265
|
-
defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
266
|
-
suggestedModels: [
|
|
267
|
-
'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
268
|
-
'accounts/fireworks/models/llama-v3p1-405b-instruct',
|
|
269
|
-
'accounts/fireworks/models/qwen2p5-coder-32b-instruct',
|
|
270
|
-
'accounts/fireworks/models/deepseek-r1',
|
|
271
|
-
'accounts/fireworks/models/deepseek-v3',
|
|
272
|
-
'accounts/fireworks/models/mixtral-8x22b-instruct',
|
|
273
|
-
],
|
|
274
|
-
},
|
|
275
|
-
};
|
|
122
|
+
// Built-in OpenAI-compatible vendor catalogue — moved to
|
|
123
|
+
// providers/compat_vendors.mjs (pure data; add new vendors THERE).
|
|
124
|
+
// Re-exported below so existing importers keep working.
|
|
125
|
+
export { OPENAI_COMPAT_BUILTINS } from './compat_vendors.mjs';
|
|
276
126
|
|
|
277
127
|
// Insertion order is the picker order. The list goes first-to-last in
|
|
278
128
|
// rough "user-familiar / popular" order so a first-time onboard lands
|
|
@@ -280,8 +130,11 @@ export const OPENAI_COMPAT_BUILTINS = {
|
|
|
280
130
|
// user feedback ("gemini, codex 이런거 먼저 나오게끔").
|
|
281
131
|
export const PROVIDERS = {
|
|
282
132
|
// Tier 1 — popular / brand-name vendors users come in looking for.
|
|
133
|
+
// Keyless CLI variants sit next to their API siblings.
|
|
283
134
|
gemini: geminiProvider,
|
|
135
|
+
'gemini-cli': geminiCliProvider, // keyless — local `gemini` CLI login
|
|
284
136
|
openai: openaiProvider, // surfaces gpt-5-codex / gpt-5 / o3-pro etc.
|
|
137
|
+
'codex-cli': codexCliProvider, // keyless — local `codex` CLI (ChatGPT plan)
|
|
285
138
|
// Tier 2 — Claude. CLI variant first because it's keyless.
|
|
286
139
|
'claude-cli': claudeCliProvider,
|
|
287
140
|
anthropic: anthropicProvider,
|
|
@@ -340,7 +193,10 @@ export const PROVIDER_INFO = {
|
|
|
340
193
|
endpoint: 'subprocess: claude -p',
|
|
341
194
|
defaultModel: 'claude-opus-4-7',
|
|
342
195
|
suggestedModels: [
|
|
196
|
+
'claude-fable-5',
|
|
197
|
+
'claude-opus-4-8',
|
|
343
198
|
'claude-opus-4-7',
|
|
199
|
+
'claude-opus-4-6',
|
|
344
200
|
'claude-sonnet-4-6',
|
|
345
201
|
'claude-haiku-4-5',
|
|
346
202
|
'opus',
|
|
@@ -348,6 +204,22 @@ export const PROVIDER_INFO = {
|
|
|
348
204
|
'haiku',
|
|
349
205
|
],
|
|
350
206
|
},
|
|
207
|
+
'gemini-cli': {
|
|
208
|
+
name: 'gemini-cli',
|
|
209
|
+
requiresApiKey: false,
|
|
210
|
+
docs: 'Google Gemini via the local `gemini` CLI (free Google-account login). No API key — auth flows through whatever account `gemini` is logged in with. Requires @google/gemini-cli installed.',
|
|
211
|
+
endpoint: 'subprocess: gemini -p',
|
|
212
|
+
defaultModel: 'gemini-2.5-pro',
|
|
213
|
+
suggestedModels: ['gemini-2.5-pro', 'gemini-2.5-flash', 'pro', 'flash'],
|
|
214
|
+
},
|
|
215
|
+
'codex-cli': {
|
|
216
|
+
name: 'codex-cli',
|
|
217
|
+
requiresApiKey: false,
|
|
218
|
+
docs: 'OpenAI via the local `codex` CLI (ChatGPT Plus/Pro subscription). No API key — auth flows through `codex` login. Requires the codex CLI installed.',
|
|
219
|
+
endpoint: 'subprocess: codex exec',
|
|
220
|
+
defaultModel: 'gpt-5-codex',
|
|
221
|
+
suggestedModels: ['gpt-5-codex', 'gpt-5', 'o3'],
|
|
222
|
+
},
|
|
351
223
|
anthropic: {
|
|
352
224
|
name: 'anthropic',
|
|
353
225
|
requiresApiKey: true,
|
|
@@ -356,13 +228,13 @@ export const PROVIDER_INFO = {
|
|
|
356
228
|
endpoint: 'https://api.anthropic.com/v1/messages',
|
|
357
229
|
defaultModel: 'claude-opus-4-7',
|
|
358
230
|
suggestedModels: [
|
|
231
|
+
'claude-fable-5',
|
|
232
|
+
'claude-opus-4-8',
|
|
359
233
|
'claude-opus-4-7',
|
|
360
234
|
'claude-opus-4-6',
|
|
361
235
|
'claude-sonnet-4-6',
|
|
362
236
|
'claude-sonnet-4-5',
|
|
363
237
|
'claude-haiku-4-5',
|
|
364
|
-
'claude-3-5-sonnet-20241022',
|
|
365
|
-
'claude-3-5-haiku-20241022',
|
|
366
238
|
],
|
|
367
239
|
},
|
|
368
240
|
openai: {
|
|
@@ -395,9 +267,6 @@ export const PROVIDER_INFO = {
|
|
|
395
267
|
'gemini-2.5-pro',
|
|
396
268
|
'gemini-2.5-flash',
|
|
397
269
|
'gemini-2.0-flash',
|
|
398
|
-
'gemini-2.0-flash-thinking-exp',
|
|
399
|
-
'gemini-1.5-pro',
|
|
400
|
-
'gemini-1.5-flash',
|
|
401
270
|
],
|
|
402
271
|
},
|
|
403
272
|
ollama: {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// tui/config_picker.mjs — the `/config` slash: change ONE setting without
|
|
2
|
+
// re-running the whole wizard.
|
|
3
|
+
//
|
|
4
|
+
// Split of duties (user-requested):
|
|
5
|
+
// /setup — first-run / full re-setup: leaves chat and runs EVERY wizard
|
|
6
|
+
// step (the behavior /config used to have).
|
|
7
|
+
// /config — settings editor: pick a single item. In-chat items (provider /
|
|
8
|
+
// model / context / trainer / orchestrator) delegate to their
|
|
9
|
+
// existing slash handlers and stay inside chat; credential items
|
|
10
|
+
// (channel tokens, outbound webhook) need readline prompts, so
|
|
11
|
+
// they unmount, run JUST that step, and re-enter chat.
|
|
12
|
+
//
|
|
13
|
+
// On the legacy readline path (no ctx.openPicker modal) /config falls back
|
|
14
|
+
// to the full wizard — same as before, no silent degradation.
|
|
15
|
+
|
|
16
|
+
const CONFIG_ITEMS = [
|
|
17
|
+
{ id: 'provider', label: 'provider', desc: 'switch the chat provider (family → vendor picker)' },
|
|
18
|
+
{ id: 'model', label: 'model', desc: 'switch the model (live list when the provider supports it)' },
|
|
19
|
+
{ id: 'context', label: 'context window', desc: 'history turns / token budget sent per turn' },
|
|
20
|
+
{ id: 'trainer', label: 'trainer', desc: 'learning-loop provider/model (auto = $0 on claude-cli)' },
|
|
21
|
+
{ id: 'orchestrator', label: 'orchestrator', desc: 'multi-agent on/off, planner, workers' },
|
|
22
|
+
{ id: 'channel', label: 'channel credentials', desc: 'Slack/Telegram/Matrix tokens — leaves chat for the prompts, then returns' },
|
|
23
|
+
{ id: 'webhook', label: 'outbound webhook', desc: 'message-send webhook URL — leaves chat, then returns' },
|
|
24
|
+
{ id: 'wizard', label: 'everything (full wizard)', desc: 'rerun all setup steps — same as /setup' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export async function runConfigSlash(_args, ctx, handlers) {
|
|
28
|
+
if (typeof ctx.openPicker !== 'function') {
|
|
29
|
+
// Legacy readline path has no modal picker — keep the old /config
|
|
30
|
+
// behavior there (full wizard) rather than failing.
|
|
31
|
+
ctx.requestSetup = true;
|
|
32
|
+
return 'EXIT';
|
|
33
|
+
}
|
|
34
|
+
const picked = await ctx.openPicker({
|
|
35
|
+
kind: 'config-item',
|
|
36
|
+
title: 'config — change one setting',
|
|
37
|
+
subtitle: 'Enter to edit · Esc to cancel · /setup reruns the whole wizard',
|
|
38
|
+
items: CONFIG_ITEMS.map((i) => ({ id: i.id, label: i.label, desc: i.desc })),
|
|
39
|
+
});
|
|
40
|
+
const id = typeof picked === 'string' ? picked : (picked && picked.id);
|
|
41
|
+
if (!id || id === 'CANCEL') return 'config: cancelled';
|
|
42
|
+
if (id === 'wizard') { ctx.requestSetup = true; return 'EXIT'; }
|
|
43
|
+
if (id === 'channel' || id === 'webhook') {
|
|
44
|
+
// These steps need raw readline prompts (secrets), so the REPL unmounts,
|
|
45
|
+
// chat.mjs runs the single step, and chat restarts automatically.
|
|
46
|
+
ctx.requestConfigStep = id;
|
|
47
|
+
return 'EXIT';
|
|
48
|
+
}
|
|
49
|
+
const handler = handlers && handlers.get && handlers.get(`/${id}`);
|
|
50
|
+
if (!handler) return `config: no in-chat editor for "${id}"`;
|
|
51
|
+
return handler('', ctx);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export { CONFIG_ITEMS };
|
package/tui/editor.mjs
CHANGED
|
@@ -422,7 +422,12 @@ export function Editor({
|
|
|
422
422
|
try {
|
|
423
423
|
process.stdout.write(`${undo}\x1b[${rowsUp}A\x1b[${colTarget}G\x1b[?25h`);
|
|
424
424
|
} catch { /* stdout closed — swallow */ }
|
|
425
|
-
}
|
|
425
|
+
});
|
|
426
|
+
// ^ NO dependency array — re-anchor after EVERY commit. Renders triggered
|
|
427
|
+
// by OTHER components (status-bar ticks, streaming output) redraw the
|
|
428
|
+
// frame and park the terminal cursor below it; with buffer-only deps the
|
|
429
|
+
// anchor didn't re-run and the cursor drifted out of the box whenever the
|
|
430
|
+
// user wasn't typing. The pending-offset undo above makes repeats safe.
|
|
426
431
|
|
|
427
432
|
const lines = state.buffer.split('\n');
|
|
428
433
|
// Ink's <Text wrap="wrap"> uses wrap-ansi (string-width aware) but the
|
|
@@ -446,6 +451,17 @@ export function Editor({
|
|
|
446
451
|
});
|
|
447
452
|
}
|
|
448
453
|
}
|
|
454
|
+
// Always-visible caret: an inverse-video cell drawn AT the cursor (the
|
|
455
|
+
// editor has no mid-line cursor movement, so the cursor is always at the
|
|
456
|
+
// end of the buffer — including column 0 of an empty box). The real
|
|
457
|
+
// terminal cursor is anchored to the same cell for IME pre-edit; this
|
|
458
|
+
// glyph keeps the position visible even between anchor writes (e.g.
|
|
459
|
+
// while another component renders). Hidden while a modal picker owns
|
|
460
|
+
// the keyboard so it can't masquerade as an active prompt.
|
|
461
|
+
if (!modalOpen && renderedLines.length > 0) {
|
|
462
|
+
const last = renderedLines[renderedLines.length - 1];
|
|
463
|
+
last.text = `${last.text}\x1b[7m \x1b[27m`;
|
|
464
|
+
}
|
|
449
465
|
return React.createElement(
|
|
450
466
|
Box,
|
|
451
467
|
{
|
package/tui/pickers.mjs
CHANGED
|
@@ -417,13 +417,13 @@ export async function _arrowMenu({ title, subtitle, footer, items, defaultIdx =
|
|
|
417
417
|
// here (rather than registry.mjs) because it's a UX concept, not
|
|
418
418
|
// an intrinsic provider attribute.
|
|
419
419
|
export function _providerFamilies() {
|
|
420
|
-
// Membership (api/cli/mock
|
|
421
|
-
//
|
|
422
|
-
// The ANSI tags below are readline-specific, so they're applied here.
|
|
420
|
+
// Membership (api/cli/meta/mock) is shared with the Ink picker via
|
|
421
|
+
// tui/provider_families.mjs; the ANSI tags below are readline-specific.
|
|
423
422
|
const b = _bucketProviders(getRegistry());
|
|
424
423
|
return {
|
|
425
424
|
api: { label: 'API key', desc: 'paste an sk-... key during setup', tag: '\x1b[38;5;245m[needs key]\x1b[0m', members: b.api },
|
|
426
425
|
cli: { label: 'CLI / Local', desc: 'keyless — uses an existing CLI login or a local daemon', tag: '\x1b[38;5;208m[no key]\x1b[0m', members: b.cli },
|
|
426
|
+
meta: { label: 'Multi-agent', desc: 'orchestrator — fan a task out to a planner + workers (advanced)', tag: '\x1b[38;5;245m[meta]\x1b[0m', members: b.meta },
|
|
427
427
|
mock: { label: 'Mock', desc: 'offline echo, only useful for testing', tag: '\x1b[38;5;245m[test]\x1b[0m', members: b.mock },
|
|
428
428
|
};
|
|
429
429
|
}
|
|
@@ -5,17 +5,18 @@
|
|
|
5
5
|
// (API key / CLI-Local / Mock) as shared, unit-testable data so both the
|
|
6
6
|
// readline picker (cli.mjs) and the Ink dispatcher use one bucketing rule.
|
|
7
7
|
|
|
8
|
-
// Bucket every registered provider into one of
|
|
9
|
-
// orchestrator
|
|
10
|
-
//
|
|
11
|
-
//
|
|
8
|
+
// Bucket every registered provider into one of four auth families.
|
|
9
|
+
// orchestrator sits in its own `meta` family — visible and pickable, but
|
|
10
|
+
// never a wizard default (the cursor always starts on api/cli — see the
|
|
11
|
+
// cli.mjs v5.3.2 note). Returns { api:[], cli:[], mock:[], meta:[] } of
|
|
12
|
+
// provider-id strings.
|
|
12
13
|
export function bucketProviders(registry) {
|
|
13
14
|
const info = (registry && registry.PROVIDER_INFO) || {};
|
|
14
15
|
const all = Object.keys((registry && registry.PROVIDERS) || {});
|
|
15
|
-
const out = { api: [], cli: [], mock: [] };
|
|
16
|
+
const out = { api: [], cli: [], mock: [], meta: [] };
|
|
16
17
|
for (const name of all) {
|
|
17
18
|
if (name === 'mock') out.mock.push(name);
|
|
18
|
-
else if (name === 'orchestrator')
|
|
19
|
+
else if (name === 'orchestrator') out.meta.push(name);
|
|
19
20
|
else if ((info[name] || {}).requiresApiKey) out.api.push(name);
|
|
20
21
|
else out.cli.push(name);
|
|
21
22
|
}
|
|
@@ -29,6 +30,7 @@ export function providerFamilies(registry) {
|
|
|
29
30
|
return {
|
|
30
31
|
api: { id: 'api', label: 'API key', desc: 'paste an sk-... key', tag: 'needs key', members: b.api },
|
|
31
32
|
cli: { id: 'cli', label: 'CLI / Local', desc: 'keyless — an existing CLI login or a local daemon', tag: 'no key', members: b.cli },
|
|
33
|
+
meta: { id: 'meta', label: 'Multi-agent', desc: 'orchestrator — fan a task out to a planner + workers (advanced)', tag: 'meta', members: b.meta },
|
|
32
34
|
mock: { id: 'mock', label: 'Mock', desc: 'offline echo, only useful for testing', tag: 'test', members: b.mock },
|
|
33
35
|
};
|
|
34
36
|
}
|
package/tui/slash_commands.mjs
CHANGED
|
@@ -26,7 +26,8 @@ export const SLASH_COMMANDS = [
|
|
|
26
26
|
{ cmd: '/personality', help: 'pick a personality (or sub: list|show|install|remove|use)' },
|
|
27
27
|
{ cmd: '/dashboard', help: 'open the lazyclaw web UI in your browser' },
|
|
28
28
|
{ cmd: '/menu', help: 'browse the full subcommand catalog (command palette)' },
|
|
29
|
-
{ cmd: '/
|
|
29
|
+
{ cmd: '/setup', help: 'first-run / full re-setup: leave chat and run every wizard step' },
|
|
30
|
+
{ cmd: '/config', help: 'change ONE setting: provider, model, context, channel creds, webhook, …' },
|
|
30
31
|
{ cmd: '/channels', help: 'view configured channels; /channels <name> on|off to toggle' },
|
|
31
32
|
{ cmd: '/orchestrator', help: 'multi-agent: status | on | off | planner <spec> | worker add|remove <spec>' },
|
|
32
33
|
{ cmd: '/context', help: 'chat history window: status | turns <N> | tokens <N>' },
|
package/tui/slash_dispatcher.mjs
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
// tui/slash_dispatcher.mjs — single source of slash-command routing for the
|
|
2
2
|
// Ink chat REPL (v5.4). Lifted from cli.mjs's legacy readline handler so the
|
|
3
|
-
// Ink branch
|
|
4
|
-
// channels (Slack/Telegram inline commands, /handoff cross-channel control)
|
|
5
|
-
// can share one dispatch table.
|
|
3
|
+
// Ink branch and future channel surfaces share one dispatch table.
|
|
6
4
|
//
|
|
7
5
|
// Contract:
|
|
8
6
|
// dispatchSlash(cmd, args, ctx, write) → Promise<string|'EXIT'|void>
|
|
@@ -1814,17 +1812,18 @@ export const SLASH_HANDLERS = new Map([
|
|
|
1814
1812
|
['/channels', _channels],
|
|
1815
1813
|
['/orchestrator', _orchestrator],
|
|
1816
1814
|
['/context', _context],
|
|
1817
|
-
// /
|
|
1818
|
-
|
|
1815
|
+
// /setup — full wizard (every step); /config — pick ONE setting to change
|
|
1816
|
+
// (in-chat where possible; credential steps unmount, run, re-enter chat).
|
|
1817
|
+
['/setup', async (_a, ctx) => { ctx.requestSetup = true; return 'EXIT'; }],
|
|
1818
|
+
['/config', async (a, ctx) => (await import('./config_picker.mjs')).runConfigSlash(a, ctx, SLASH_HANDLERS)],
|
|
1819
1819
|
['/exit', async () => 'EXIT'],
|
|
1820
1820
|
['/quit', async () => 'EXIT'],
|
|
1821
1821
|
]);
|
|
1822
1822
|
|
|
1823
1823
|
/**
|
|
1824
|
-
* Primary entry point. Resolves the command name to a
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1827
|
-
* so the user sees feedback instead of an error toast.
|
|
1824
|
+
* Primary entry point. Resolves the command name to a SLASH_HANDLERS entry
|
|
1825
|
+
* and invokes it. Unknown commands return a friendly "unknown" string
|
|
1826
|
+
* (rendered to scrollback) rather than throwing.
|
|
1828
1827
|
*/
|
|
1829
1828
|
export async function dispatchSlash(cmd, args, ctx, write) {
|
|
1830
1829
|
const handler = SLASH_HANDLERS.get(cmd);
|