@stevederico/dotbot 0.27.0 → 0.28.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/CHANGELOG.md +12 -0
- package/bin/dotbot.js +336 -8
- package/dotbot.db +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
0.28
|
|
2
|
+
|
|
3
|
+
Add --sandbox mode
|
|
4
|
+
Add --allow domain presets
|
|
5
|
+
Add domain-gated web_fetch
|
|
6
|
+
Add domain-gated browser_navigate
|
|
7
|
+
Add preset-unlocked tools
|
|
8
|
+
Add API key validation
|
|
9
|
+
Add models CLI command
|
|
10
|
+
Add /models REPL command
|
|
11
|
+
Add /load model command
|
|
12
|
+
|
|
1
13
|
0.27
|
|
2
14
|
|
|
3
15
|
Add interactive API key prompt
|
package/bin/dotbot.js
CHANGED
|
@@ -100,6 +100,7 @@ Usage:
|
|
|
100
100
|
echo "msg" | dotbot Pipe input from stdin
|
|
101
101
|
|
|
102
102
|
Commands:
|
|
103
|
+
models List available models from provider
|
|
103
104
|
doctor Check environment and configuration
|
|
104
105
|
tools List all available tools
|
|
105
106
|
stats Show database statistics
|
|
@@ -121,6 +122,8 @@ Options:
|
|
|
121
122
|
--db SQLite database path (default: ${DEFAULT_DB})
|
|
122
123
|
--port Server port for 'serve' command (default: ${DEFAULT_PORT})
|
|
123
124
|
--openai Enable OpenAI-compatible API endpoints (/v1/chat/completions, /v1/models)
|
|
125
|
+
--sandbox Restrict tools to safe subset (no files, code, browser, messages)
|
|
126
|
+
--allow Allow domain/preset in sandbox (github, slack, discord, npm, etc.)
|
|
124
127
|
--json Output as JSON (for inspection commands)
|
|
125
128
|
--verbose Show initialization logs
|
|
126
129
|
--help, -h Show this help
|
|
@@ -138,16 +141,20 @@ Config File:
|
|
|
138
141
|
Examples:
|
|
139
142
|
dotbot "What's the weather in SF?"
|
|
140
143
|
dotbot
|
|
144
|
+
dotbot -p anthropic -m claude-sonnet-4-5 "Hello"
|
|
145
|
+
dotbot -p ollama -m llama3 "Summarize this"
|
|
146
|
+
dotbot -p openai -m gpt-4o
|
|
147
|
+
dotbot models
|
|
141
148
|
dotbot serve --port 8080
|
|
142
149
|
dotbot doctor
|
|
143
150
|
dotbot tools
|
|
144
151
|
dotbot memory search "preferences"
|
|
145
|
-
dotbot memory delete user_pref
|
|
146
152
|
dotbot stats --json
|
|
153
|
+
dotbot --sandbox "What is 2+2?"
|
|
154
|
+
dotbot --sandbox --allow github "Check my repo"
|
|
147
155
|
dotbot --system "You are a pirate" "Hello"
|
|
148
156
|
dotbot --session abc-123 "follow up question"
|
|
149
157
|
echo "What is 2+2?" | dotbot
|
|
150
|
-
cat question.txt | dotbot
|
|
151
158
|
`);
|
|
152
159
|
}
|
|
153
160
|
|
|
@@ -204,10 +211,17 @@ function parseCliArgs() {
|
|
|
204
211
|
port: { type: 'string' },
|
|
205
212
|
openai: { type: 'boolean', default: false },
|
|
206
213
|
session: { type: 'string', default: '' },
|
|
214
|
+
sandbox: { type: 'boolean', default: false },
|
|
215
|
+
allow: { type: 'string', multiple: true },
|
|
207
216
|
},
|
|
208
217
|
});
|
|
209
218
|
|
|
210
219
|
// Merge: CLI args > config file > hardcoded defaults
|
|
220
|
+
// Build sandbox domain allowlist from --allow flags, presets, and config
|
|
221
|
+
const allowFlags = values.allow || [];
|
|
222
|
+
const configAllow = config.sandboxAllow || [];
|
|
223
|
+
const allAllow = [...allowFlags, ...configAllow];
|
|
224
|
+
|
|
211
225
|
return {
|
|
212
226
|
...values,
|
|
213
227
|
provider: values.provider ?? config.provider ?? 'xai',
|
|
@@ -216,6 +230,8 @@ function parseCliArgs() {
|
|
|
216
230
|
db: values.db ?? config.db ?? DEFAULT_DB,
|
|
217
231
|
port: values.port ?? config.port ?? String(DEFAULT_PORT),
|
|
218
232
|
session: values.session ?? '',
|
|
233
|
+
sandbox: values.sandbox || config.sandbox || false,
|
|
234
|
+
sandboxAllow: allAllow,
|
|
219
235
|
positionals,
|
|
220
236
|
};
|
|
221
237
|
} catch (err) {
|
|
@@ -297,6 +313,18 @@ async function getProviderConfig(providerId) {
|
|
|
297
313
|
process.exit(1);
|
|
298
314
|
}
|
|
299
315
|
|
|
316
|
+
// Validate key by fetching models
|
|
317
|
+
process.stdout.write('Validating');
|
|
318
|
+
startSpinner();
|
|
319
|
+
const { ok } = await fetchProviderModels(providerId, apiKey);
|
|
320
|
+
if (ok) {
|
|
321
|
+
stopSpinner('valid');
|
|
322
|
+
} else {
|
|
323
|
+
stopSpinner('failed');
|
|
324
|
+
console.error(`Could not authenticate with ${base.name}. Check your API key and try again.`);
|
|
325
|
+
process.exit(1);
|
|
326
|
+
}
|
|
327
|
+
|
|
300
328
|
const save = await askUser('Save to ~/.dotbotrc for next time? (Y/n) ');
|
|
301
329
|
if (save.toLowerCase() !== 'n') {
|
|
302
330
|
saveToConfig('env', { ...loadConfig().env, [envKey]: apiKey });
|
|
@@ -328,6 +356,201 @@ function getProviderSignupUrl(providerId) {
|
|
|
328
356
|
return urls[providerId] || 'the provider\'s website';
|
|
329
357
|
}
|
|
330
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Fetch available models from a provider's API.
|
|
361
|
+
*
|
|
362
|
+
* @param {string} providerId - Provider ID
|
|
363
|
+
* @param {string} apiKey - API key for authentication
|
|
364
|
+
* @returns {Promise<{ok: boolean, models: Array<{id: string, name: string}>}>} Validation result with model list
|
|
365
|
+
*/
|
|
366
|
+
async function fetchProviderModels(providerId, apiKey) {
|
|
367
|
+
await loadModules();
|
|
368
|
+
const base = AI_PROVIDERS[providerId];
|
|
369
|
+
if (!base) return { ok: false, models: [] };
|
|
370
|
+
|
|
371
|
+
let url;
|
|
372
|
+
let headers;
|
|
373
|
+
|
|
374
|
+
if (providerId === 'ollama') {
|
|
375
|
+
const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
|
|
376
|
+
url = `${baseUrl}/api/tags`;
|
|
377
|
+
headers = { 'Content-Type': 'application/json' };
|
|
378
|
+
} else {
|
|
379
|
+
url = `${base.apiUrl}/models`;
|
|
380
|
+
headers = base.headers(apiKey);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
try {
|
|
384
|
+
const res = await fetch(url, { headers, signal: AbortSignal.timeout(10000) });
|
|
385
|
+
if (!res.ok) return { ok: false, models: [] };
|
|
386
|
+
|
|
387
|
+
const data = await res.json();
|
|
388
|
+
|
|
389
|
+
let models = [];
|
|
390
|
+
if (providerId === 'ollama') {
|
|
391
|
+
models = (data.models || []).map((m) => ({ id: m.name, name: m.name }));
|
|
392
|
+
} else if (providerId === 'anthropic') {
|
|
393
|
+
models = (data.data || []).map((m) => ({ id: m.id, name: m.display_name || m.id }));
|
|
394
|
+
} else {
|
|
395
|
+
models = (data.data || []).map((m) => ({ id: m.id, name: m.id }));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { ok: true, models };
|
|
399
|
+
} catch {
|
|
400
|
+
return { ok: false, models: [] };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Tools always allowed in sandbox mode (safe, internal-only).
|
|
406
|
+
*/
|
|
407
|
+
const SANDBOX_ALLOWED_TOOLS = new Set([
|
|
408
|
+
'memory_save', 'memory_search', 'memory_delete', 'memory_list', 'memory_read', 'memory_update',
|
|
409
|
+
'web_search', 'grokipedia_search',
|
|
410
|
+
'file_read', 'file_list',
|
|
411
|
+
'run_code',
|
|
412
|
+
'weather_get',
|
|
413
|
+
'event_query', 'events_summary',
|
|
414
|
+
'task_create', 'task_list', 'task_plan', 'task_work', 'task_step_done', 'task_complete',
|
|
415
|
+
'task_delete', 'task_search', 'task_stats',
|
|
416
|
+
'trigger_create', 'trigger_list', 'trigger_toggle', 'trigger_delete',
|
|
417
|
+
'schedule_job', 'list_jobs', 'toggle_job', 'cancel_job',
|
|
418
|
+
]);
|
|
419
|
+
|
|
420
|
+
/** Tools that are allowed in sandbox but domain-gated via allowlist. */
|
|
421
|
+
const SANDBOX_GATED_TOOLS = new Set(['web_fetch', 'browser_navigate']);
|
|
422
|
+
|
|
423
|
+
/** Tools unlocked in sandbox when their preset is in the --allow list. */
|
|
424
|
+
const SANDBOX_PRESET_TOOLS = {
|
|
425
|
+
messages: ['message_list', 'message_send', 'message_delete', 'message_read'],
|
|
426
|
+
images: ['image_generate', 'image_list', 'image_search'],
|
|
427
|
+
notifications: ['notify_user'],
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Domain presets matching NemoClaw's policy preset pattern.
|
|
432
|
+
* Each preset maps to a list of allowed domains.
|
|
433
|
+
*/
|
|
434
|
+
const DOMAIN_PRESETS = {
|
|
435
|
+
github: ['github.com', 'api.github.com', 'raw.githubusercontent.com'],
|
|
436
|
+
slack: ['slack.com', 'api.slack.com'],
|
|
437
|
+
discord: ['discord.com', 'api.discord.com'],
|
|
438
|
+
npm: ['registry.npmjs.org', 'www.npmjs.com'],
|
|
439
|
+
pypi: ['pypi.org', 'files.pythonhosted.org'],
|
|
440
|
+
jira: ['atlassian.net', 'jira.atlassian.com'],
|
|
441
|
+
huggingface: ['huggingface.co', 'api-inference.huggingface.co'],
|
|
442
|
+
docker: ['hub.docker.com', 'registry-1.docker.io'],
|
|
443
|
+
telegram: ['api.telegram.org'],
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Resolve --allow values into a Set of allowed domains.
|
|
448
|
+
* Accepts preset names (e.g., "github") or raw domains (e.g., "api.example.com").
|
|
449
|
+
*
|
|
450
|
+
* @param {Array<string>} allowList - Preset names or domain names
|
|
451
|
+
* @returns {Set<string>} Resolved domain set
|
|
452
|
+
*/
|
|
453
|
+
function resolveSandboxDomains(allowList = []) {
|
|
454
|
+
const domains = new Set();
|
|
455
|
+
for (const entry of allowList) {
|
|
456
|
+
const lower = entry.toLowerCase();
|
|
457
|
+
if (DOMAIN_PRESETS[lower]) {
|
|
458
|
+
for (const d of DOMAIN_PRESETS[lower]) domains.add(d);
|
|
459
|
+
} else {
|
|
460
|
+
domains.add(lower);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return domains;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Check if a URL's hostname matches the sandbox domain allowlist.
|
|
468
|
+
*
|
|
469
|
+
* @param {string} urlStr - URL to check
|
|
470
|
+
* @param {Set<string>} allowedDomains - Set of allowed domain names
|
|
471
|
+
* @returns {boolean} Whether the domain is allowed
|
|
472
|
+
*/
|
|
473
|
+
function isDomainAllowed(urlStr, allowedDomains) {
|
|
474
|
+
try {
|
|
475
|
+
const { hostname } = new URL(urlStr);
|
|
476
|
+
if (allowedDomains.has(hostname)) return true;
|
|
477
|
+
// Support wildcard subdomains (e.g., "atlassian.net" matches "myteam.atlassian.net")
|
|
478
|
+
for (const domain of allowedDomains) {
|
|
479
|
+
if (hostname.endsWith(`.${domain}`)) return true;
|
|
480
|
+
}
|
|
481
|
+
return false;
|
|
482
|
+
} catch {
|
|
483
|
+
return false;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Wrap a tool's execute function with domain enforcement.
|
|
489
|
+
*
|
|
490
|
+
* @param {Object} tool - Tool definition
|
|
491
|
+
* @param {Set<string>} allowedDomains - Allowed domains
|
|
492
|
+
* @returns {Object} Tool with domain-gated execute
|
|
493
|
+
*/
|
|
494
|
+
function wrapWithDomainGate(tool, allowedDomains) {
|
|
495
|
+
const original = tool.execute;
|
|
496
|
+
return {
|
|
497
|
+
...tool,
|
|
498
|
+
description: `${tool.description} [SANDBOX: restricted to allowed domains]`,
|
|
499
|
+
execute: async (input, signal, context) => {
|
|
500
|
+
const url = input.url;
|
|
501
|
+
if (!url || !isDomainAllowed(url, allowedDomains)) {
|
|
502
|
+
const allowed = [...allowedDomains].join(', ') || 'none';
|
|
503
|
+
return `Blocked by sandbox policy. Domain not in allowlist.\nAllowed domains: ${allowed}`;
|
|
504
|
+
}
|
|
505
|
+
return original(input, signal, context);
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* Get tools filtered by sandbox mode with domain-gated network access.
|
|
512
|
+
*
|
|
513
|
+
* Mirrors NemoClaw's deny-by-default policy:
|
|
514
|
+
* - No filesystem access (file_*)
|
|
515
|
+
* - No code execution (run_code)
|
|
516
|
+
* - No outbound messaging (message_*)
|
|
517
|
+
* - No image generation, notifications, or app scaffolding
|
|
518
|
+
* - Network tools (web_fetch, browser_navigate) restricted to domain allowlist
|
|
519
|
+
* - Curated search APIs (web_search, grokipedia_search) always allowed
|
|
520
|
+
*
|
|
521
|
+
* @param {boolean} sandbox - Whether sandbox mode is active
|
|
522
|
+
* @param {Array<string>} allowList - Domain presets or raw domains to allow
|
|
523
|
+
* @returns {Array} Filtered and gated tools
|
|
524
|
+
*/
|
|
525
|
+
function getActiveTools(sandbox = false, allowList = []) {
|
|
526
|
+
if (!sandbox) return coreTools;
|
|
527
|
+
|
|
528
|
+
const allowedDomains = resolveSandboxDomains(allowList);
|
|
529
|
+
const allowLower = new Set(allowList.map((a) => a.toLowerCase()));
|
|
530
|
+
|
|
531
|
+
// Build set of preset-unlocked tool names
|
|
532
|
+
const presetUnlocked = new Set();
|
|
533
|
+
for (const [preset, toolNames] of Object.entries(SANDBOX_PRESET_TOOLS)) {
|
|
534
|
+
if (allowLower.has(preset)) {
|
|
535
|
+
for (const name of toolNames) presetUnlocked.add(name);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const tools = [];
|
|
540
|
+
|
|
541
|
+
for (const tool of coreTools) {
|
|
542
|
+
if (SANDBOX_ALLOWED_TOOLS.has(tool.name)) {
|
|
543
|
+
tools.push(tool);
|
|
544
|
+
} else if (SANDBOX_GATED_TOOLS.has(tool.name) && allowedDomains.size > 0) {
|
|
545
|
+
tools.push(wrapWithDomainGate(tool, allowedDomains));
|
|
546
|
+
} else if (presetUnlocked.has(tool.name)) {
|
|
547
|
+
tools.push(tool);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
return tools;
|
|
552
|
+
}
|
|
553
|
+
|
|
331
554
|
/**
|
|
332
555
|
* Initialize stores.
|
|
333
556
|
*
|
|
@@ -421,7 +644,7 @@ async function runChat(message, options) {
|
|
|
421
644
|
for await (const event of agentLoop({
|
|
422
645
|
model: options.model,
|
|
423
646
|
messages,
|
|
424
|
-
tools:
|
|
647
|
+
tools: getActiveTools(options.sandbox, options.sandboxAllow),
|
|
425
648
|
provider,
|
|
426
649
|
context,
|
|
427
650
|
})) {
|
|
@@ -511,7 +734,7 @@ async function runRepl(options) {
|
|
|
511
734
|
output: process.stdout,
|
|
512
735
|
});
|
|
513
736
|
|
|
514
|
-
console.log(`\ndotbot v${VERSION} — ${options.provider}/${options.model}`);
|
|
737
|
+
console.log(`\ndotbot v${VERSION} — ${options.provider}/${options.model}${options.sandbox ? ' (sandbox)' : ''}`);
|
|
515
738
|
if (options.session) {
|
|
516
739
|
console.log(`Resuming session: ${session.id}`);
|
|
517
740
|
}
|
|
@@ -519,6 +742,8 @@ async function runRepl(options) {
|
|
|
519
742
|
|
|
520
743
|
const showHelp = () => {
|
|
521
744
|
console.log('Available Commands:');
|
|
745
|
+
console.log(' /models List available models from provider');
|
|
746
|
+
console.log(' /load <model> Switch to a different model');
|
|
522
747
|
console.log(' /show Show model information');
|
|
523
748
|
console.log(' /clear Clear session context');
|
|
524
749
|
console.log(' /bye Exit');
|
|
@@ -588,6 +813,50 @@ async function runRepl(options) {
|
|
|
588
813
|
return;
|
|
589
814
|
}
|
|
590
815
|
|
|
816
|
+
if (trimmed === '/models') {
|
|
817
|
+
const apiKey = process.env[AI_PROVIDERS[options.provider]?.envKey];
|
|
818
|
+
process.stdout.write('Fetching models');
|
|
819
|
+
startSpinner();
|
|
820
|
+
const { ok, models } = await fetchProviderModels(options.provider, apiKey);
|
|
821
|
+
if (ok && models.length) {
|
|
822
|
+
stopSpinner('');
|
|
823
|
+
console.log('');
|
|
824
|
+
for (const m of models) {
|
|
825
|
+
const active = m.id === options.model ? ' (active)' : '';
|
|
826
|
+
console.log(` ${m.id}${active}`);
|
|
827
|
+
}
|
|
828
|
+
console.log('');
|
|
829
|
+
} else {
|
|
830
|
+
stopSpinner('');
|
|
831
|
+
// Fall back to static list
|
|
832
|
+
const base = AI_PROVIDERS[options.provider];
|
|
833
|
+
if (base.models?.length) {
|
|
834
|
+
console.log('');
|
|
835
|
+
for (const m of base.models) {
|
|
836
|
+
const active = m.id === options.model ? ' (active)' : '';
|
|
837
|
+
console.log(` ${m.id}${active}`);
|
|
838
|
+
}
|
|
839
|
+
console.log('');
|
|
840
|
+
} else {
|
|
841
|
+
console.log('\nNo models found.\n');
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
promptUser();
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
if (trimmed.startsWith('/load ')) {
|
|
849
|
+
const newModel = trimmed.slice(6).trim();
|
|
850
|
+
if (!newModel) {
|
|
851
|
+
console.log('Usage: /load <model-name>\n');
|
|
852
|
+
} else {
|
|
853
|
+
options.model = newModel;
|
|
854
|
+
console.log(`Switched to ${newModel}\n`);
|
|
855
|
+
}
|
|
856
|
+
promptUser();
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
|
|
591
860
|
await handleMessage(trimmed);
|
|
592
861
|
});
|
|
593
862
|
};
|
|
@@ -606,7 +875,7 @@ async function runRepl(options) {
|
|
|
606
875
|
for await (const event of agentLoop({
|
|
607
876
|
model: options.model,
|
|
608
877
|
messages: [...messages],
|
|
609
|
-
tools:
|
|
878
|
+
tools: getActiveTools(options.sandbox, options.sandboxAllow),
|
|
610
879
|
provider,
|
|
611
880
|
context,
|
|
612
881
|
})) {
|
|
@@ -771,7 +1040,7 @@ async function runServer(options) {
|
|
|
771
1040
|
for await (const event of agentLoop({
|
|
772
1041
|
model,
|
|
773
1042
|
messages: reqMessages,
|
|
774
|
-
tools:
|
|
1043
|
+
tools: getActiveTools(options.sandbox, options.sandboxAllow),
|
|
775
1044
|
provider,
|
|
776
1045
|
context,
|
|
777
1046
|
})) {
|
|
@@ -804,7 +1073,7 @@ async function runServer(options) {
|
|
|
804
1073
|
for await (const event of agentLoop({
|
|
805
1074
|
model,
|
|
806
1075
|
messages: reqMessages,
|
|
807
|
-
tools:
|
|
1076
|
+
tools: getActiveTools(options.sandbox, options.sandboxAllow),
|
|
808
1077
|
provider,
|
|
809
1078
|
context,
|
|
810
1079
|
})) {
|
|
@@ -878,7 +1147,7 @@ async function runServer(options) {
|
|
|
878
1147
|
for await (const event of agentLoop({
|
|
879
1148
|
model,
|
|
880
1149
|
messages,
|
|
881
|
-
tools:
|
|
1150
|
+
tools: getActiveTools(options.sandbox, options.sandboxAllow),
|
|
882
1151
|
provider,
|
|
883
1152
|
context,
|
|
884
1153
|
})) {
|
|
@@ -1265,6 +1534,62 @@ async function runDoctor(options) {
|
|
|
1265
1534
|
console.log();
|
|
1266
1535
|
}
|
|
1267
1536
|
|
|
1537
|
+
/**
|
|
1538
|
+
* List available models from the provider's API.
|
|
1539
|
+
*
|
|
1540
|
+
* @param {Object} options - CLI options
|
|
1541
|
+
*/
|
|
1542
|
+
async function runModels(options) {
|
|
1543
|
+
await loadModules();
|
|
1544
|
+
const base = AI_PROVIDERS[options.provider];
|
|
1545
|
+
if (!base) {
|
|
1546
|
+
console.error(`Unknown provider: ${options.provider}`);
|
|
1547
|
+
process.exit(1);
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
const apiKey = process.env[base.envKey];
|
|
1551
|
+
if (!apiKey && options.provider !== 'ollama') {
|
|
1552
|
+
console.error(`Missing ${base.envKey} — set it or run dotbot to configure interactively.`);
|
|
1553
|
+
process.exit(1);
|
|
1554
|
+
}
|
|
1555
|
+
|
|
1556
|
+
process.stdout.write('Fetching models');
|
|
1557
|
+
startSpinner();
|
|
1558
|
+
|
|
1559
|
+
const { ok, models } = await fetchProviderModels(options.provider, apiKey);
|
|
1560
|
+
|
|
1561
|
+
if (!ok) {
|
|
1562
|
+
stopSpinner('failed');
|
|
1563
|
+
console.error(`Could not fetch models from ${base.name}. Check your API key.`);
|
|
1564
|
+
|
|
1565
|
+
// Fall back to static list
|
|
1566
|
+
if (base.models?.length) {
|
|
1567
|
+
console.log(`\nLocal model list (${base.name}):\n`);
|
|
1568
|
+
for (const m of base.models) {
|
|
1569
|
+
const active = m.id === options.model ? ' (active)' : '';
|
|
1570
|
+
console.log(` ${m.id}${active}`);
|
|
1571
|
+
}
|
|
1572
|
+
console.log();
|
|
1573
|
+
}
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
stopSpinner('');
|
|
1578
|
+
|
|
1579
|
+
if (options.json) {
|
|
1580
|
+
console.log(JSON.stringify(models));
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
console.log(`\n${base.name} models (${models.length})\n`);
|
|
1585
|
+
for (const m of models) {
|
|
1586
|
+
const active = m.id === options.model ? ' (active)' : '';
|
|
1587
|
+
const label = m.name !== m.id ? ` — ${m.name}` : '';
|
|
1588
|
+
console.log(` ${m.id}${label}${active}`);
|
|
1589
|
+
}
|
|
1590
|
+
console.log();
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1268
1593
|
/**
|
|
1269
1594
|
* Main entry point.
|
|
1270
1595
|
*/
|
|
@@ -1297,6 +1622,9 @@ async function main() {
|
|
|
1297
1622
|
}
|
|
1298
1623
|
|
|
1299
1624
|
switch (command) {
|
|
1625
|
+
case 'models':
|
|
1626
|
+
await runModels(args);
|
|
1627
|
+
break;
|
|
1300
1628
|
case 'doctor':
|
|
1301
1629
|
await runDoctor(args);
|
|
1302
1630
|
break;
|
package/dotbot.db
CHANGED
|
Binary file
|
package/package.json
CHANGED