lazyclaw 3.99.12 → 3.99.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +53 -3
- package/cli.mjs +175 -27
- package/package.json +1 -1
- package/providers/registry.mjs +222 -1
package/README.md
CHANGED
|
@@ -54,6 +54,54 @@ lazyclaw onboard --non-interactive --provider openai \
|
|
|
54
54
|
|
|
55
55
|
`onboard` only prompts for an api-key when the picked provider's `requiresApiKey` is true (the picker labels each row `[subscription]` / `[api key]` / `[no key]` so the choice is explicit).
|
|
56
56
|
|
|
57
|
+
### Built-in OpenAI-compatible vendors
|
|
58
|
+
|
|
59
|
+
Eight popular OpenAI-compatible services ship as first-class providers — pick one in the setup picker (no `+ Add custom` walkthrough needed) or set the matching environment variable and they Just Work:
|
|
60
|
+
|
|
61
|
+
| Provider | Models include | Env var |
|
|
62
|
+
|---|---|---|
|
|
63
|
+
| `nim` | `meta/llama-3.1-405b-instruct`, `nvidia/llama-3.1-nemotron-70b-instruct`, `deepseek-ai/deepseek-r1`, `mistralai/mixtral-8x22b-instruct-v0.1` | `NVIDIA_API_KEY` (or `NIM_API_KEY`) |
|
|
64
|
+
| `openrouter` | `anthropic/claude-3.5-sonnet`, `openai/gpt-4o`, `meta-llama/llama-3.1-405b-instruct`, `deepseek/deepseek-r1` | `OPENROUTER_API_KEY` |
|
|
65
|
+
| `groq` | `llama-3.3-70b-versatile`, `mixtral-8x7b-32768`, `deepseek-r1-distill-llama-70b` | `GROQ_API_KEY` |
|
|
66
|
+
| `together` | `meta-llama/Llama-3.3-70B-Instruct-Turbo`, `Qwen/Qwen2.5-72B-Instruct-Turbo`, `deepseek-ai/DeepSeek-V3` | `TOGETHER_API_KEY` |
|
|
67
|
+
| `xai` | `grok-2-latest`, `grok-2-vision-1212` | `XAI_API_KEY` (or `GROK_API_KEY`) |
|
|
68
|
+
| `deepseek` | `deepseek-chat`, `deepseek-reasoner` | `DEEPSEEK_API_KEY` |
|
|
69
|
+
| `mistral` | `mistral-large-latest`, `codestral-latest`, `pixtral-large-latest` | `MISTRAL_API_KEY` |
|
|
70
|
+
| `fireworks` | `accounts/fireworks/models/llama-v3p3-70b-instruct`, `…/deepseek-r1` | `FIREWORKS_API_KEY` |
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# NVIDIA NIM via env var — no `lazyclaw onboard` needed
|
|
74
|
+
export NVIDIA_API_KEY=nvapi-...
|
|
75
|
+
lazyclaw chat --provider nim --model meta/llama-3.1-405b-instruct
|
|
76
|
+
|
|
77
|
+
# Or commit the choice to ~/.lazyclaw/config.json
|
|
78
|
+
lazyclaw onboard --non-interactive --provider nim \
|
|
79
|
+
--model nvidia/llama-3.1-nemotron-70b-instruct --api-key nvapi-...
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Need a vendor that's **not** built-in? `+ Add a custom OpenAI-compatible endpoint…` inside the setup picker (or `lazyclaw providers add <name> --base-url <url>`) still works for vLLM / LM Studio / private gateways / anything else that speaks the OpenAI v1 wire format.
|
|
83
|
+
|
|
84
|
+
## Launcher (no-arg `lazyclaw`)
|
|
85
|
+
|
|
86
|
+
Running `lazyclaw` with no subcommand drops into an arrow-key launcher with every subcommand laid out as a menu. Navigation:
|
|
87
|
+
|
|
88
|
+
| Key | What it does |
|
|
89
|
+
|---|---|
|
|
90
|
+
| `↑` / `↓` / `Home` / `End` / `PgUp` / `PgDn` | Move the selection |
|
|
91
|
+
| `Enter` | Run the highlighted item |
|
|
92
|
+
| `q` / `Esc` / `Ctrl-C` | Leave lazyclaw |
|
|
93
|
+
| `/` | Open an inline slash-command prompt |
|
|
94
|
+
|
|
95
|
+
Slash commands at the launcher (typed after `/`):
|
|
96
|
+
|
|
97
|
+
| Slash | What it does |
|
|
98
|
+
|---|---|
|
|
99
|
+
| `/exit` / `/quit` | Leave lazyclaw |
|
|
100
|
+
| `/help` | List launcher slash commands inline |
|
|
101
|
+
| `/version` | Print version + node + platform |
|
|
102
|
+
|
|
103
|
+
The slash buffer lives just below the menu — backspace edits it, deleting past `/` returns to menu mode, and `Esc` cancels slash mode without leaving lazyclaw.
|
|
104
|
+
|
|
57
105
|
## Interactive chat
|
|
58
106
|
|
|
59
107
|
```bash
|
|
@@ -88,12 +136,14 @@ Slash commands inside the REPL:
|
|
|
88
136
|
|---|---|
|
|
89
137
|
| `/help` | List slash commands |
|
|
90
138
|
| `/status` | Print provider + model + masked key |
|
|
91
|
-
| `/provider
|
|
92
|
-
| `/
|
|
139
|
+
| `/provider` | Open the family / provider / model arrow picker |
|
|
140
|
+
| `/provider X` | Switch active provider directly by name |
|
|
141
|
+
| `/model` | Open the per-provider model picker (type-filter + live `/v1/models` fetch) |
|
|
142
|
+
| `/model X` | Switch model directly. Accepts unified `provider/model` form |
|
|
93
143
|
| `/skill a,b` | Replace the system prompt with a composition of named skills |
|
|
94
144
|
| `/usage` | Message count + chars + cumulative token totals |
|
|
95
145
|
| `/new` / `/reset` | Wipe history and start over |
|
|
96
|
-
| `/exit` |
|
|
146
|
+
| `/exit` | Leave the chat REPL (returns to the launcher when chat was opened from it) |
|
|
97
147
|
|
|
98
148
|
**Cursor-style ghost autocomplete**: type `/` and the longest matching slash command appears in dim grey after the cursor. **`→`** accepts; **`Tab`** cycles. **Ctrl-C** during a streaming reply aborts that turn (not the whole process); **Ctrl-C** at an empty prompt exits.
|
|
99
149
|
|
package/cli.mjs
CHANGED
|
@@ -33,19 +33,26 @@ function writeConfig(cfg) {
|
|
|
33
33
|
// without forcing the dynamic import on every hot-path call.
|
|
34
34
|
// 1. cfg.authProfiles[provider] active label, if set
|
|
35
35
|
// 2. first profile in the array
|
|
36
|
-
// 3.
|
|
36
|
+
// 3. customProviders[<provider>].apiKey (custom OpenAI-compat entries)
|
|
37
|
+
// 4. PROVIDER_INFO[<provider>].envKey / altEnvKeys env var (built-in
|
|
38
|
+
// OpenAI-compat: nim → NVIDIA_API_KEY, openrouter → OPENROUTER_API_KEY, …)
|
|
39
|
+
// 5. legacy single `cfg["api-key"]` (pre-v3.93 configs)
|
|
37
40
|
function _resolveAuthKey(cfg, provider) {
|
|
38
41
|
const arr = (cfg.authProfiles || {})[provider] || [];
|
|
39
42
|
const active = (cfg.authActiveProfile || {})[provider];
|
|
40
43
|
const hit = arr.find((p) => p && p.label === active) || arr[0];
|
|
41
44
|
if (hit?.key) return hit.key;
|
|
42
|
-
// Custom OpenAI-compatible providers store their api-key inline in the
|
|
43
|
-
// customProviders[] entry. Honour that before falling back to the
|
|
44
|
-
// legacy single-key cfg['api-key'].
|
|
45
45
|
const custom = Array.isArray(cfg.customProviders)
|
|
46
46
|
? cfg.customProviders.find((p) => p && p.name === provider)
|
|
47
47
|
: null;
|
|
48
48
|
if (custom?.apiKey) return custom.apiKey;
|
|
49
|
+
// Built-in OpenAI-compat env var fallback. Skipped silently when the
|
|
50
|
+
// registry module isn't loaded yet (every chat / agent path calls
|
|
51
|
+
// ensureRegistry() before _resolveAuthKey, so this is just defence-in-depth).
|
|
52
|
+
if (_registryMod && typeof _registryMod.resolveBuiltinEnvKey === 'function') {
|
|
53
|
+
const envHit = _registryMod.resolveBuiltinEnvKey(provider);
|
|
54
|
+
if (envHit) return envHit;
|
|
55
|
+
}
|
|
49
56
|
return cfg['api-key'] || '';
|
|
50
57
|
}
|
|
51
58
|
|
|
@@ -1798,17 +1805,58 @@ async function _pickProviderInteractive() {
|
|
|
1798
1805
|
}
|
|
1799
1806
|
|
|
1800
1807
|
// ── Step 3 — model ────────────────────────────────────────────
|
|
1801
|
-
const
|
|
1808
|
+
const picked = await _pickModelInteractive(provider.id, {
|
|
1809
|
+
titlePrefix: 'LazyClaw setup — Step 3 of 3:',
|
|
1810
|
+
onBack: 'restart',
|
|
1811
|
+
});
|
|
1812
|
+
if (picked === 'CANCEL') return null;
|
|
1813
|
+
if (picked === 'BACK') return _pickProviderInteractive();
|
|
1814
|
+
return { provider: provider.id, model: picked };
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
// Pause the chat REPL's readline + ghost-autocomplete while a sub-picker
|
|
1818
|
+
// (provider / model arrow menu) takes over the terminal. The sub-picker
|
|
1819
|
+
// installs its own `keypress` listener and toggles raw mode; the chat's
|
|
1820
|
+
// readline would race it for stdin if we left it active. After `body`
|
|
1821
|
+
// returns we re-emit keypress events, restore raw mode, and re-prompt
|
|
1822
|
+
// so the chat resumes cleanly. `body` is awaited — exceptions propagate.
|
|
1823
|
+
async function _pauseChatForSubMenu(rl, ghost, body) {
|
|
1824
|
+
if (ghost && typeof ghost.suspend === 'function') ghost.suspend();
|
|
1825
|
+
try { rl.pause(); } catch (_) {}
|
|
1826
|
+
// Drop the readline keypress hook so the picker's own listener has
|
|
1827
|
+
// sole ownership while it's open. We re-arm it on the way out.
|
|
1828
|
+
if (process.stdin.setRawMode) {
|
|
1829
|
+
try { process.stdin.setRawMode(false); } catch (_) {}
|
|
1830
|
+
}
|
|
1831
|
+
try {
|
|
1832
|
+
await body();
|
|
1833
|
+
} finally {
|
|
1834
|
+
const readline = await import('node:readline');
|
|
1835
|
+
try { readline.emitKeypressEvents(process.stdin); } catch (_) {}
|
|
1836
|
+
if (process.stdin.setRawMode && process.stdin.isTTY) {
|
|
1837
|
+
try { process.stdin.setRawMode(false); } catch (_) {}
|
|
1838
|
+
}
|
|
1839
|
+
process.stdin.resume();
|
|
1840
|
+
if (process.stdin.ref) process.stdin.ref();
|
|
1841
|
+
if (ghost && typeof ghost.resume === 'function') ghost.resume();
|
|
1842
|
+
try { rl.resume(); } catch (_) {}
|
|
1843
|
+
try { rl.prompt(); } catch (_) {}
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
// Standalone model picker for the chat REPL's `/model` slash. Returns
|
|
1848
|
+
// the chosen model id (string), 'BACK', or 'CANCEL'. Falls through to
|
|
1849
|
+
// null when the provider has no curated models and no live-fetch surface
|
|
1850
|
+
// (mock) — the caller should treat that as "use the provider default".
|
|
1851
|
+
async function _pickModelInteractive(providerId, opts = {}) {
|
|
1852
|
+
const info = _registryMod.PROVIDER_INFO || {};
|
|
1853
|
+
const meta = info[providerId] || {};
|
|
1802
1854
|
const baseModels = Array.isArray(meta.suggestedModels) ? meta.suggestedModels.slice() : [];
|
|
1803
1855
|
const isCustom = !!meta.custom;
|
|
1804
|
-
const
|
|
1856
|
+
const isBuiltinCompat = !!meta.builtinOpenAICompat;
|
|
1857
|
+
const supportsLiveFetch = !!meta.baseUrl || providerId === 'openai' || providerId === 'ollama' || isBuiltinCompat;
|
|
1805
1858
|
|
|
1806
|
-
if (!baseModels.length && !supportsLiveFetch)
|
|
1807
|
-
// Provider has no curated models AND no live-fetch surface (mock) —
|
|
1808
|
-
// return without a model so the underlying call uses the provider
|
|
1809
|
-
// default.
|
|
1810
|
-
return { provider: provider.id, model: null };
|
|
1811
|
-
}
|
|
1859
|
+
if (!baseModels.length && !supportsLiveFetch) return null;
|
|
1812
1860
|
|
|
1813
1861
|
let dynamicModels = [];
|
|
1814
1862
|
while (true) {
|
|
@@ -1818,7 +1866,7 @@ async function _pickProviderInteractive() {
|
|
|
1818
1866
|
modelItems.unshift({
|
|
1819
1867
|
id: '__fetch_models__',
|
|
1820
1868
|
label: '↻ Fetch live model list from /v1/models',
|
|
1821
|
-
desc: isCustom ? `GET ${meta.baseUrl}/models` : 'pulls the up-to-date catalogue from the provider',
|
|
1869
|
+
desc: isCustom || isBuiltinCompat ? `GET ${meta.baseUrl}/models` : 'pulls the up-to-date catalogue from the provider',
|
|
1822
1870
|
tag: '\x1b[38;5;245m[live]\x1b[0m',
|
|
1823
1871
|
});
|
|
1824
1872
|
}
|
|
@@ -1832,24 +1880,25 @@ async function _pickProviderInteractive() {
|
|
|
1832
1880
|
const defaultIdx = supportsLiveFetch
|
|
1833
1881
|
? Math.max(0, 1 + allModels.indexOf(meta.defaultModel || allModels[0]))
|
|
1834
1882
|
: Math.max(0, allModels.indexOf(meta.defaultModel || allModels[0]));
|
|
1883
|
+
const titlePrefix = opts.titlePrefix ? `${opts.titlePrefix} ` : '';
|
|
1835
1884
|
const picked = await _arrowMenu({
|
|
1836
|
-
title:
|
|
1885
|
+
title: `${titlePrefix}pick a model for ${providerId}`,
|
|
1837
1886
|
subtitle: `Type to filter ${allModels.length} model(s). Enter to confirm. Backspace clears one char, Ctrl+U clears the filter.`,
|
|
1838
1887
|
items: modelItems,
|
|
1839
1888
|
defaultIdx,
|
|
1840
1889
|
searchable: true,
|
|
1841
1890
|
});
|
|
1842
|
-
if (picked === 'CANCEL') return
|
|
1843
|
-
if (picked === 'BACK') return
|
|
1891
|
+
if (picked === 'CANCEL') return 'CANCEL';
|
|
1892
|
+
if (picked === 'BACK') return 'BACK';
|
|
1844
1893
|
if (picked.id === '__custom_model__') {
|
|
1845
|
-
const typed = (await _quickPrompt(` model id for ${
|
|
1894
|
+
const typed = (await _quickPrompt(` model id for ${providerId}: `)).trim();
|
|
1846
1895
|
if (!typed) continue;
|
|
1847
|
-
return
|
|
1896
|
+
return typed;
|
|
1848
1897
|
}
|
|
1849
1898
|
if (picked.id === '__fetch_models__') {
|
|
1850
1899
|
try {
|
|
1851
|
-
process.stdout.write(`\n fetching ${
|
|
1852
|
-
const fetched = await _fetchModelsForProvider(
|
|
1900
|
+
process.stdout.write(`\n fetching ${providerId} model list…\n`);
|
|
1901
|
+
const fetched = await _fetchModelsForProvider(providerId);
|
|
1853
1902
|
if (!fetched.length) {
|
|
1854
1903
|
process.stdout.write(` ${'\x1b[33m'}no models returned${'\x1b[0m'} — falling back to the suggested list.\n`);
|
|
1855
1904
|
await _quickPrompt(' press Enter to continue ');
|
|
@@ -1864,7 +1913,7 @@ async function _pickProviderInteractive() {
|
|
|
1864
1913
|
}
|
|
1865
1914
|
continue;
|
|
1866
1915
|
}
|
|
1867
|
-
return
|
|
1916
|
+
return picked.id;
|
|
1868
1917
|
}
|
|
1869
1918
|
}
|
|
1870
1919
|
|
|
@@ -1878,6 +1927,12 @@ function _modelCatalogueFor(providerId) {
|
|
|
1878
1927
|
const entry = (cfg.customProviders || []).find((p) => p && p.name === providerId) || {};
|
|
1879
1928
|
return { baseUrl: meta.baseUrl, apiKey: entry.apiKey || cfg['api-key'] || '' };
|
|
1880
1929
|
}
|
|
1930
|
+
// Built-in OpenAI-compatible vendors (nim / openrouter / groq / together /
|
|
1931
|
+
// xai / deepseek / mistral / fireworks). The registry exposes a baseUrl
|
|
1932
|
+
// and the auth-key resolver already knows about the env-var fallback.
|
|
1933
|
+
if (meta.builtinOpenAICompat && meta.baseUrl) {
|
|
1934
|
+
return { baseUrl: meta.baseUrl, apiKey: _resolveAuthKey(cfg, providerId) };
|
|
1935
|
+
}
|
|
1881
1936
|
if (providerId === 'openai') {
|
|
1882
1937
|
return { baseUrl: 'https://api.openai.com/v1', apiKey: _resolveAuthKey(cfg, 'openai') };
|
|
1883
1938
|
}
|
|
@@ -2133,10 +2188,28 @@ async function cmdChat(flags = {}) {
|
|
|
2133
2188
|
// `/provider <name>` switches the active provider for subsequent
|
|
2134
2189
|
// turns. The conversation history stays put — the next user
|
|
2135
2190
|
// message goes to the new provider with the existing context.
|
|
2136
|
-
// `/provider` (no arg)
|
|
2191
|
+
// `/provider` (no arg) opens the family/provider/model picker so
|
|
2192
|
+
// the user can switch with arrow keys instead of memorising names.
|
|
2137
2193
|
const arg = line.slice('/provider'.length).trim();
|
|
2138
2194
|
if (!arg) {
|
|
2139
|
-
|
|
2195
|
+
if (!useTerminal) {
|
|
2196
|
+
process.stdout.write(`provider: ${activeProvName}\n`);
|
|
2197
|
+
return true;
|
|
2198
|
+
}
|
|
2199
|
+
await _pauseChatForSubMenu(rl, _ghost, async () => {
|
|
2200
|
+
const picked = await _pickProviderInteractive();
|
|
2201
|
+
if (picked && picked.provider) {
|
|
2202
|
+
const next = lookupProv(picked.provider);
|
|
2203
|
+
if (!next) {
|
|
2204
|
+
process.stdout.write(`unknown provider: ${picked.provider}\n`);
|
|
2205
|
+
return;
|
|
2206
|
+
}
|
|
2207
|
+
activeProvName = picked.provider;
|
|
2208
|
+
prov = next;
|
|
2209
|
+
if (picked.model) activeModel = picked.model;
|
|
2210
|
+
process.stdout.write(`provider → ${activeProvName}${picked.model ? ` · model → ${picked.model}` : ''}\n`);
|
|
2211
|
+
}
|
|
2212
|
+
});
|
|
2140
2213
|
return true;
|
|
2141
2214
|
}
|
|
2142
2215
|
const next = lookupProv(arg);
|
|
@@ -2151,10 +2224,20 @@ async function cmdChat(flags = {}) {
|
|
|
2151
2224
|
}
|
|
2152
2225
|
case '/model': {
|
|
2153
2226
|
// `/model <name>` updates the active model without touching the
|
|
2154
|
-
// provider. `/model` (no arg)
|
|
2227
|
+
// provider. `/model` (no arg) opens the per-provider model picker
|
|
2228
|
+
// — same UX as setup step 3, scoped to the active provider.
|
|
2155
2229
|
const arg = line.slice('/model'.length).trim();
|
|
2156
2230
|
if (!arg) {
|
|
2157
|
-
|
|
2231
|
+
if (!useTerminal) {
|
|
2232
|
+
process.stdout.write(`model: ${activeModel || '(default)'}\n`);
|
|
2233
|
+
return true;
|
|
2234
|
+
}
|
|
2235
|
+
await _pauseChatForSubMenu(rl, _ghost, async () => {
|
|
2236
|
+
const chosen = await _pickModelInteractive(activeProvName, { titlePrefix: 'LazyClaw chat —' });
|
|
2237
|
+
if (chosen === 'CANCEL' || chosen === 'BACK' || !chosen) return;
|
|
2238
|
+
activeModel = chosen;
|
|
2239
|
+
process.stdout.write(`model → ${activeModel}\n`);
|
|
2240
|
+
});
|
|
2158
2241
|
return true;
|
|
2159
2242
|
}
|
|
2160
2243
|
// Honor unified provider/model: `/model anthropic/claude-opus-4-7`
|
|
@@ -4084,7 +4167,7 @@ async function cmdLauncher() {
|
|
|
4084
4167
|
process.stdout.write(` ${dim('model ·')} ${ok(model)}\n`);
|
|
4085
4168
|
process.stdout.write(` ${dim('config ·')} ${dim(configPath())}\n`);
|
|
4086
4169
|
process.stdout.write('\n');
|
|
4087
|
-
process.stdout.write(` ${dim('↑/↓ to move · Enter to select · q or Esc to quit')}\n\n`);
|
|
4170
|
+
process.stdout.write(` ${dim('↑/↓ to move · Enter to select · / for slash command (e.g. /exit) · q or Esc to quit')}\n\n`);
|
|
4088
4171
|
const rowsAvail = Math.max(items.length, (process.stdout.rows || 30) - 14);
|
|
4089
4172
|
const fromIdx = Math.max(0, Math.min(items.length - rowsAvail, idx - Math.floor(rowsAvail / 2)));
|
|
4090
4173
|
const toIdx = Math.min(items.length, fromIdx + rowsAvail);
|
|
@@ -4097,10 +4180,74 @@ async function cmdLauncher() {
|
|
|
4097
4180
|
process.stdout.write('\n');
|
|
4098
4181
|
};
|
|
4099
4182
|
|
|
4183
|
+
// Slash-command mini prompt rendered just below the menu. Lets users
|
|
4184
|
+
// type `/exit` / `/quit` / `/help` to leave (or get a list of slash
|
|
4185
|
+
// commands) without hunting for the right special key. The menu is
|
|
4186
|
+
// raw-mode and never sees a newline-terminated line, so we accumulate
|
|
4187
|
+
// keystrokes locally instead of round-tripping through readline.
|
|
4188
|
+
let slashBuffer = null; // null = menu mode; string = slash mode (always starts with '/')
|
|
4189
|
+
let slashNotice = ''; // one-line hint shown after the buffer (e.g. "unknown command")
|
|
4190
|
+
const LAUNCHER_SLASH_HELP = [
|
|
4191
|
+
{ cmd: '/exit', help: 'leave lazyclaw' },
|
|
4192
|
+
{ cmd: '/quit', help: 'alias for /exit' },
|
|
4193
|
+
{ cmd: '/help', help: 'list slash commands' },
|
|
4194
|
+
{ cmd: '/version', help: 'print version + node + platform' },
|
|
4195
|
+
];
|
|
4196
|
+
const drawWithSlash = () => {
|
|
4197
|
+
draw();
|
|
4198
|
+
process.stdout.write(` ${dim('slash ›')} ${slashBuffer}`);
|
|
4199
|
+
if (slashNotice) process.stdout.write(` ${slashNotice}`);
|
|
4200
|
+
process.stdout.write('\x1b[?25h'); // show cursor while typing
|
|
4201
|
+
};
|
|
4202
|
+
|
|
4100
4203
|
draw();
|
|
4101
4204
|
const picked = await new Promise((resolve) => {
|
|
4102
|
-
const onKey = (
|
|
4205
|
+
const onKey = (str, key) => {
|
|
4103
4206
|
if (!key) return;
|
|
4207
|
+
|
|
4208
|
+
// ── Slash-command input mode ─────────────────────────────────
|
|
4209
|
+
if (slashBuffer !== null) {
|
|
4210
|
+
if (key.ctrl && key.name === 'c') { cleanup(); resolve({ id: 'quit', argv: null }); return; }
|
|
4211
|
+
if (key.name === 'escape') { slashBuffer = null; slashNotice = ''; draw(); return; }
|
|
4212
|
+
if (key.name === 'return') {
|
|
4213
|
+
const cmd = slashBuffer.trim().toLowerCase();
|
|
4214
|
+
if (cmd === '/exit' || cmd === '/quit') { cleanup(); resolve({ id: 'quit', argv: null }); return; }
|
|
4215
|
+
if (cmd === '/help') {
|
|
4216
|
+
slashBuffer = '/';
|
|
4217
|
+
slashNotice = dim(LAUNCHER_SLASH_HELP.map(c => `${c.cmd} (${c.help})`).join(' · '));
|
|
4218
|
+
drawWithSlash();
|
|
4219
|
+
return;
|
|
4220
|
+
}
|
|
4221
|
+
if (cmd === '/version') {
|
|
4222
|
+
const v = readVersionFromRepo();
|
|
4223
|
+
slashNotice = ok(`v${v} · node ${process.version} · ${process.platform}-${process.arch}`);
|
|
4224
|
+
drawWithSlash();
|
|
4225
|
+
return;
|
|
4226
|
+
}
|
|
4227
|
+
// Unknown command — keep the buffer so the user can edit it
|
|
4228
|
+
// rather than retyping from scratch. Esc / Backspace bails.
|
|
4229
|
+
slashNotice = warn(`unknown — try ${LAUNCHER_SLASH_HELP.map(c => c.cmd).join(' · ')}`);
|
|
4230
|
+
drawWithSlash();
|
|
4231
|
+
return;
|
|
4232
|
+
}
|
|
4233
|
+
if (key.name === 'backspace') {
|
|
4234
|
+
slashNotice = '';
|
|
4235
|
+
if (slashBuffer.length > 1) slashBuffer = slashBuffer.slice(0, -1);
|
|
4236
|
+
else slashBuffer = null;
|
|
4237
|
+
slashBuffer === null ? draw() : drawWithSlash();
|
|
4238
|
+
return;
|
|
4239
|
+
}
|
|
4240
|
+
// Append printable characters. Filter control / meta chords so
|
|
4241
|
+
// Ctrl+L etc. don't pollute the buffer.
|
|
4242
|
+
if (str && str.length === 1 && !key.ctrl && !key.meta && str >= ' ') {
|
|
4243
|
+
slashBuffer += str;
|
|
4244
|
+
slashNotice = '';
|
|
4245
|
+
drawWithSlash();
|
|
4246
|
+
}
|
|
4247
|
+
return;
|
|
4248
|
+
}
|
|
4249
|
+
|
|
4250
|
+
// ── Menu navigation mode ─────────────────────────────────────
|
|
4104
4251
|
if (key.name === 'up') { idx = (idx - 1 + items.length) % items.length; draw(); }
|
|
4105
4252
|
else if (key.name === 'down') { idx = (idx + 1) % items.length; draw(); }
|
|
4106
4253
|
else if (key.name === 'home') { idx = 0; draw(); }
|
|
@@ -4110,6 +4257,7 @@ async function cmdLauncher() {
|
|
|
4110
4257
|
else if (key.name === 'return') { cleanup(); resolve(items[idx]); }
|
|
4111
4258
|
else if (key.ctrl && key.name === 'c') { cleanup(); resolve({ id: 'quit', argv: null }); }
|
|
4112
4259
|
else if (key.name === 'escape' || key.name === 'q') { cleanup(); resolve({ id: 'quit', argv: null }); }
|
|
4260
|
+
else if (str === '/') { slashBuffer = '/'; slashNotice = ''; drawWithSlash(); }
|
|
4113
4261
|
function cleanup() {
|
|
4114
4262
|
process.stdin.off('keypress', onKey);
|
|
4115
4263
|
if (process.stdin.setRawMode) process.stdin.setRawMode(false);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lazyclaw",
|
|
3
|
-
"version": "3.99.
|
|
3
|
+
"version": "3.99.14",
|
|
4
4
|
"description": "Lazy, elegant terminal CLI for chatting with Claude / OpenAI / Gemini / Ollama and orchestrating multi-step LLM workflows. Banner-on-launch, slash-command ghost autocomplete, persistent sessions, local HTTP gateway.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/providers/registry.mjs
CHANGED
|
@@ -52,6 +52,164 @@ export const mockProvider = {
|
|
|
52
52
|
export { anthropicProvider, openaiProvider, ollamaProvider, geminiProvider, claudeCliProvider };
|
|
53
53
|
export { makeOpenAICompatProvider, fetchOpenAICompatModels };
|
|
54
54
|
|
|
55
|
+
// Built-in OpenAI-compatible vendors. Same wire format → one factory call
|
|
56
|
+
// each. The picker treats these like first-class providers so users don't
|
|
57
|
+
// have to walk through "+ Add a custom endpoint" for the popular ones.
|
|
58
|
+
//
|
|
59
|
+
// Each entry must define baseUrl + envKey (the env var the chat path
|
|
60
|
+
// consults when no api-key is configured) + suggestedModels (curated list
|
|
61
|
+
// shown before the user fetches the live /v1/models catalogue).
|
|
62
|
+
//
|
|
63
|
+
// Adding a new vendor: drop a row here. The PROVIDERS / PROVIDER_INFO loops
|
|
64
|
+
// below pick it up automatically.
|
|
65
|
+
export const OPENAI_COMPAT_BUILTINS = {
|
|
66
|
+
nim: {
|
|
67
|
+
label: 'NVIDIA NIM',
|
|
68
|
+
baseUrl: 'https://integrate.api.nvidia.com/v1',
|
|
69
|
+
envKey: 'NVIDIA_API_KEY',
|
|
70
|
+
altEnvKeys: ['NIM_API_KEY'],
|
|
71
|
+
keyPrefix: 'nvapi-',
|
|
72
|
+
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.',
|
|
73
|
+
defaultModel: 'meta/llama-3.1-405b-instruct',
|
|
74
|
+
suggestedModels: [
|
|
75
|
+
'meta/llama-3.1-405b-instruct',
|
|
76
|
+
'meta/llama-3.1-70b-instruct',
|
|
77
|
+
'meta/llama-3.1-8b-instruct',
|
|
78
|
+
'nvidia/llama-3.1-nemotron-70b-instruct',
|
|
79
|
+
'nvidia/nemotron-mini-4b-instruct',
|
|
80
|
+
'nvidia/llama-3.3-nemotron-super-49b-v1',
|
|
81
|
+
'mistralai/mistral-nemo-12b-instruct',
|
|
82
|
+
'mistralai/mixtral-8x22b-instruct-v0.1',
|
|
83
|
+
'microsoft/phi-3-medium-4k-instruct',
|
|
84
|
+
'deepseek-ai/deepseek-r1',
|
|
85
|
+
'qwen/qwen2.5-7b-instruct',
|
|
86
|
+
'qwen/qwen2.5-coder-32b-instruct',
|
|
87
|
+
],
|
|
88
|
+
},
|
|
89
|
+
openrouter: {
|
|
90
|
+
label: 'OpenRouter',
|
|
91
|
+
baseUrl: 'https://openrouter.ai/api/v1',
|
|
92
|
+
envKey: 'OPENROUTER_API_KEY',
|
|
93
|
+
keyPrefix: 'sk-or-',
|
|
94
|
+
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.',
|
|
95
|
+
defaultModel: 'anthropic/claude-3.5-sonnet',
|
|
96
|
+
headers: { 'http-referer': 'https://github.com/cmblir/lazyclaude', 'x-title': 'lazyclaw' },
|
|
97
|
+
suggestedModels: [
|
|
98
|
+
'anthropic/claude-3.5-sonnet',
|
|
99
|
+
'anthropic/claude-3-opus',
|
|
100
|
+
'openai/gpt-4o',
|
|
101
|
+
'openai/gpt-4o-mini',
|
|
102
|
+
'openai/o1-preview',
|
|
103
|
+
'meta-llama/llama-3.1-405b-instruct',
|
|
104
|
+
'meta-llama/llama-3.3-70b-instruct',
|
|
105
|
+
'google/gemini-2.0-flash-exp:free',
|
|
106
|
+
'google/gemini-pro-1.5',
|
|
107
|
+
'deepseek/deepseek-chat',
|
|
108
|
+
'deepseek/deepseek-r1',
|
|
109
|
+
'qwen/qwen-2.5-coder-32b-instruct',
|
|
110
|
+
'mistralai/mistral-large',
|
|
111
|
+
],
|
|
112
|
+
},
|
|
113
|
+
groq: {
|
|
114
|
+
label: 'Groq',
|
|
115
|
+
baseUrl: 'https://api.groq.com/openai/v1',
|
|
116
|
+
envKey: 'GROQ_API_KEY',
|
|
117
|
+
keyPrefix: 'gsk_',
|
|
118
|
+
docs: 'Groq LPU inference — fastest-token-per-second tier for Llama / Mixtral / Gemma. Auth: GROQ_API_KEY env var or in-app api-key.',
|
|
119
|
+
defaultModel: 'llama-3.3-70b-versatile',
|
|
120
|
+
suggestedModels: [
|
|
121
|
+
'llama-3.3-70b-versatile',
|
|
122
|
+
'llama-3.1-70b-versatile',
|
|
123
|
+
'llama-3.1-8b-instant',
|
|
124
|
+
'llama-3.2-90b-vision-preview',
|
|
125
|
+
'mixtral-8x7b-32768',
|
|
126
|
+
'gemma2-9b-it',
|
|
127
|
+
'qwen-2.5-coder-32b',
|
|
128
|
+
'qwen-2.5-32b',
|
|
129
|
+
'deepseek-r1-distill-llama-70b',
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
together: {
|
|
133
|
+
label: 'Together AI',
|
|
134
|
+
baseUrl: 'https://api.together.xyz/v1',
|
|
135
|
+
envKey: 'TOGETHER_API_KEY',
|
|
136
|
+
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.',
|
|
137
|
+
defaultModel: 'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
138
|
+
suggestedModels: [
|
|
139
|
+
'meta-llama/Llama-3.3-70B-Instruct-Turbo',
|
|
140
|
+
'meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo',
|
|
141
|
+
'meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo',
|
|
142
|
+
'meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo',
|
|
143
|
+
'mistralai/Mixtral-8x22B-Instruct-v0.1',
|
|
144
|
+
'mistralai/Mixtral-8x7B-Instruct-v0.1',
|
|
145
|
+
'Qwen/Qwen2.5-72B-Instruct-Turbo',
|
|
146
|
+
'Qwen/Qwen2.5-Coder-32B-Instruct',
|
|
147
|
+
'deepseek-ai/DeepSeek-V3',
|
|
148
|
+
'deepseek-ai/DeepSeek-R1',
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
xai: {
|
|
152
|
+
label: 'xAI (Grok)',
|
|
153
|
+
baseUrl: 'https://api.x.ai/v1',
|
|
154
|
+
envKey: 'XAI_API_KEY',
|
|
155
|
+
altEnvKeys: ['GROK_API_KEY'],
|
|
156
|
+
keyPrefix: 'xai-',
|
|
157
|
+
docs: 'xAI Grok models. Auth: XAI_API_KEY env var or in-app api-key.',
|
|
158
|
+
defaultModel: 'grok-2-latest',
|
|
159
|
+
suggestedModels: [
|
|
160
|
+
'grok-2-latest',
|
|
161
|
+
'grok-2-1212',
|
|
162
|
+
'grok-2-vision-1212',
|
|
163
|
+
'grok-beta',
|
|
164
|
+
'grok-vision-beta',
|
|
165
|
+
],
|
|
166
|
+
},
|
|
167
|
+
deepseek: {
|
|
168
|
+
label: 'DeepSeek',
|
|
169
|
+
baseUrl: 'https://api.deepseek.com/v1',
|
|
170
|
+
envKey: 'DEEPSEEK_API_KEY',
|
|
171
|
+
keyPrefix: 'sk-',
|
|
172
|
+
docs: 'DeepSeek (deepseek-chat / deepseek-reasoner). Auth: DEEPSEEK_API_KEY env var or in-app api-key.',
|
|
173
|
+
defaultModel: 'deepseek-chat',
|
|
174
|
+
suggestedModels: [
|
|
175
|
+
'deepseek-chat',
|
|
176
|
+
'deepseek-reasoner',
|
|
177
|
+
'deepseek-coder',
|
|
178
|
+
],
|
|
179
|
+
},
|
|
180
|
+
mistral: {
|
|
181
|
+
label: 'Mistral La Plateforme',
|
|
182
|
+
baseUrl: 'https://api.mistral.ai/v1',
|
|
183
|
+
envKey: 'MISTRAL_API_KEY',
|
|
184
|
+
docs: 'Mistral La Plateforme (mistral-large, codestral, ministral, pixtral). Auth: MISTRAL_API_KEY env var or in-app api-key.',
|
|
185
|
+
defaultModel: 'mistral-large-latest',
|
|
186
|
+
suggestedModels: [
|
|
187
|
+
'mistral-large-latest',
|
|
188
|
+
'mistral-small-latest',
|
|
189
|
+
'codestral-latest',
|
|
190
|
+
'ministral-8b-latest',
|
|
191
|
+
'ministral-3b-latest',
|
|
192
|
+
'pixtral-large-latest',
|
|
193
|
+
'open-mistral-nemo',
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
fireworks: {
|
|
197
|
+
label: 'Fireworks AI',
|
|
198
|
+
baseUrl: 'https://api.fireworks.ai/inference/v1',
|
|
199
|
+
envKey: 'FIREWORKS_API_KEY',
|
|
200
|
+
docs: 'Fireworks AI hosted models. Auth: FIREWORKS_API_KEY env var or in-app api-key.',
|
|
201
|
+
defaultModel: 'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
202
|
+
suggestedModels: [
|
|
203
|
+
'accounts/fireworks/models/llama-v3p3-70b-instruct',
|
|
204
|
+
'accounts/fireworks/models/llama-v3p1-405b-instruct',
|
|
205
|
+
'accounts/fireworks/models/qwen2p5-coder-32b-instruct',
|
|
206
|
+
'accounts/fireworks/models/deepseek-r1',
|
|
207
|
+
'accounts/fireworks/models/deepseek-v3',
|
|
208
|
+
'accounts/fireworks/models/mixtral-8x22b-instruct',
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
55
213
|
// Insertion order is the picker order. The list goes first-to-last in
|
|
56
214
|
// rough "user-familiar / popular" order so a first-time onboard lands
|
|
57
215
|
// the cursor on a vendor most users recognise. v3.99.5 reordered per
|
|
@@ -63,11 +221,34 @@ export const PROVIDERS = {
|
|
|
63
221
|
// Tier 2 — Claude. CLI variant first because it's keyless.
|
|
64
222
|
'claude-cli': claudeCliProvider,
|
|
65
223
|
anthropic: anthropicProvider,
|
|
66
|
-
// Tier 3 —
|
|
224
|
+
// Tier 3 — popular OpenAI-compatible aggregators / hosted catalogues.
|
|
225
|
+
// Inserted by the loop below from OPENAI_COMPAT_BUILTINS so the order
|
|
226
|
+
// here mirrors that object's insertion order.
|
|
227
|
+
// Tier 4 — local + dev/test.
|
|
67
228
|
ollama: ollamaProvider,
|
|
68
229
|
mock: mockProvider,
|
|
69
230
|
};
|
|
70
231
|
|
|
232
|
+
// Wire each OpenAI-compat builtin into PROVIDERS as a callable provider.
|
|
233
|
+
// Insertion is between Tier 2 (anthropic) and Tier 4 (ollama) by reordering
|
|
234
|
+
// the keys after the loop runs — JS objects honour insertion order and
|
|
235
|
+
// cmdLauncher's families helper relies on that for the picker.
|
|
236
|
+
{
|
|
237
|
+
const local = { ollama: PROVIDERS.ollama, mock: PROVIDERS.mock };
|
|
238
|
+
delete PROVIDERS.ollama;
|
|
239
|
+
delete PROVIDERS.mock;
|
|
240
|
+
for (const [name, def] of Object.entries(OPENAI_COMPAT_BUILTINS)) {
|
|
241
|
+
PROVIDERS[name] = makeOpenAICompatProvider({
|
|
242
|
+
name,
|
|
243
|
+
baseUrl: def.baseUrl,
|
|
244
|
+
defaultModel: def.defaultModel,
|
|
245
|
+
headers: def.headers,
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
PROVIDERS.ollama = local.ollama;
|
|
249
|
+
PROVIDERS.mock = local.mock;
|
|
250
|
+
}
|
|
251
|
+
|
|
71
252
|
// Static metadata for `lazyclaw providers list/info`. Kept next to PROVIDERS
|
|
72
253
|
// so adding a provider in one place can't drift from the list shown to users.
|
|
73
254
|
export const PROVIDER_INFO = {
|
|
@@ -167,6 +348,44 @@ export const PROVIDER_INFO = {
|
|
|
167
348
|
},
|
|
168
349
|
};
|
|
169
350
|
|
|
351
|
+
// Mirror the OpenAI-compat builtins into PROVIDER_INFO so picker / docs /
|
|
352
|
+
// `lazyclaw providers info` see them with the same shape as the hand-written
|
|
353
|
+
// entries above.
|
|
354
|
+
for (const [name, def] of Object.entries(OPENAI_COMPAT_BUILTINS)) {
|
|
355
|
+
PROVIDER_INFO[name] = {
|
|
356
|
+
name,
|
|
357
|
+
label: def.label,
|
|
358
|
+
requiresApiKey: true,
|
|
359
|
+
keyPrefix: def.keyPrefix,
|
|
360
|
+
envKey: def.envKey,
|
|
361
|
+
altEnvKeys: Array.isArray(def.altEnvKeys) ? def.altEnvKeys.slice() : [],
|
|
362
|
+
docs: def.docs,
|
|
363
|
+
endpoint: `${def.baseUrl}/chat/completions`,
|
|
364
|
+
defaultModel: def.defaultModel,
|
|
365
|
+
suggestedModels: Array.isArray(def.suggestedModels) ? def.suggestedModels.slice() : [],
|
|
366
|
+
builtin: true,
|
|
367
|
+
builtinOpenAICompat: true,
|
|
368
|
+
baseUrl: def.baseUrl,
|
|
369
|
+
headers: def.headers,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Resolve an api-key for a built-in OpenAI-compatible provider from the
|
|
375
|
+
* environment, scanning {envKey} then any {altEnvKeys}. Returns '' when
|
|
376
|
+
* no env var is set so the caller can fall through to its config-based
|
|
377
|
+
* lookup chain.
|
|
378
|
+
*/
|
|
379
|
+
export function resolveBuiltinEnvKey(provider) {
|
|
380
|
+
const meta = PROVIDER_INFO[provider];
|
|
381
|
+
if (!meta || !meta.builtinOpenAICompat) return '';
|
|
382
|
+
const candidates = [meta.envKey, ...(meta.altEnvKeys || [])].filter(Boolean);
|
|
383
|
+
for (const k of candidates) {
|
|
384
|
+
if (process.env[k]) return process.env[k];
|
|
385
|
+
}
|
|
386
|
+
return '';
|
|
387
|
+
}
|
|
388
|
+
|
|
170
389
|
/**
|
|
171
390
|
* Split a unified "provider/model" string (OpenClaw style:
|
|
172
391
|
* "anthropic/claude-opus-4-7"). Also accepts a bare model id and returns
|
|
@@ -198,6 +417,8 @@ export function parseProviderModel(s) {
|
|
|
198
417
|
// uses internally. Custom registrations must not collide with these.
|
|
199
418
|
const RESERVED_PROVIDER_NAMES = new Set([
|
|
200
419
|
'mock', 'claude-cli', 'anthropic', 'openai', 'gemini', 'ollama',
|
|
420
|
+
// OpenAI-compatible builtins (kept in lockstep with OPENAI_COMPAT_BUILTINS).
|
|
421
|
+
...Object.keys(OPENAI_COMPAT_BUILTINS),
|
|
201
422
|
'__add_custom__', '__custom_model__', '__fetch_models__',
|
|
202
423
|
]);
|
|
203
424
|
|