@stevederico/dotbot 0.27.0 → 0.29.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/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
  *
@@ -381,50 +604,21 @@ async function initStores(dbPath, verbose = false, customSystemPrompt = '') {
381
604
  }
382
605
 
383
606
  /**
384
- * Run a single chat message and stream output.
607
+ * Stream events from an agentLoop iterable to stdout.
608
+ * Handles thinking markers, text deltas, tool status, and errors.
385
609
  *
386
- * @param {string} message - User message
387
- * @param {Object} options - CLI options
610
+ * @param {AsyncIterable<Object>} events - Async iterable of agentLoop events
611
+ * @returns {Promise<string>} Accumulated assistant text content
388
612
  */
389
- async function runChat(message, options) {
390
- const storesObj = await initStores(options.db, options.verbose, options.system);
391
- const provider = await getProviderConfig(options.provider);
392
-
393
- let session;
394
- let messages;
395
-
396
- if (options.session) {
397
- session = await storesObj.sessionStore.getSession(options.session, 'cli-user');
398
- if (!session) {
399
- console.error(`Error: Session not found: ${options.session}`);
400
- process.exit(1);
401
- }
402
- messages = [...(session.messages || []), { role: 'user', content: message }];
403
- } else {
404
- session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
405
- messages = [{ role: 'user', content: message }];
406
- }
407
-
408
- const context = {
409
- userID: 'cli-user',
410
- sessionId: session.id,
411
- providers: { [options.provider]: { apiKey: process.env[AI_PROVIDERS[options.provider]?.envKey] } },
412
- ...storesObj,
413
- };
414
-
613
+ async function streamEvents(events) {
415
614
  let hasThinkingText = false;
416
615
  let thinkingDone = false;
616
+ let assistantContent = '';
417
617
 
418
618
  process.stdout.write('Thinking');
419
619
  startSpinner();
420
620
 
421
- for await (const event of agentLoop({
422
- model: options.model,
423
- messages,
424
- tools: coreTools,
425
- provider,
426
- context,
427
- })) {
621
+ for await (const event of events) {
428
622
  switch (event.type) {
429
623
  case 'thinking':
430
624
  if (event.text) {
@@ -446,6 +640,7 @@ async function runChat(message, options) {
446
640
  thinkingDone = true;
447
641
  }
448
642
  process.stdout.write(event.text);
643
+ assistantContent += event.text;
449
644
  break;
450
645
  case 'tool_start':
451
646
  if (!thinkingDone) {
@@ -466,11 +661,55 @@ async function runChat(message, options) {
466
661
  stopSpinner('error');
467
662
  break;
468
663
  case 'error':
664
+ stopSpinner();
469
665
  console.error(`\nError: ${event.error}`);
470
666
  break;
471
667
  }
472
668
  }
473
669
 
670
+ return assistantContent;
671
+ }
672
+
673
+ /**
674
+ * Run a single chat message and stream output.
675
+ *
676
+ * @param {string} message - User message
677
+ * @param {Object} options - CLI options
678
+ */
679
+ async function runChat(message, options) {
680
+ const storesObj = await initStores(options.db, options.verbose, options.system);
681
+ const provider = await getProviderConfig(options.provider);
682
+
683
+ let session;
684
+ let messages;
685
+
686
+ if (options.session) {
687
+ session = await storesObj.sessionStore.getSession(options.session, 'cli-user');
688
+ if (!session) {
689
+ console.error(`Error: Session not found: ${options.session}`);
690
+ process.exit(1);
691
+ }
692
+ messages = [...(session.messages || []), { role: 'user', content: message }];
693
+ } else {
694
+ session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
695
+ messages = [{ role: 'user', content: message }];
696
+ }
697
+
698
+ const context = {
699
+ userID: 'cli-user',
700
+ sessionId: session.id,
701
+ providers: { [options.provider]: { apiKey: process.env[AI_PROVIDERS[options.provider]?.envKey] } },
702
+ ...storesObj,
703
+ };
704
+
705
+ await streamEvents(agentLoop({
706
+ model: options.model,
707
+ messages,
708
+ tools: getActiveTools(options.sandbox, options.sandboxAllow),
709
+ provider,
710
+ context,
711
+ }));
712
+
474
713
  process.stdout.write('\n\n');
475
714
  process.exit(0);
476
715
  }
@@ -511,7 +750,7 @@ async function runRepl(options) {
511
750
  output: process.stdout,
512
751
  });
513
752
 
514
- console.log(`\ndotbot v${VERSION} — ${options.provider}/${options.model}`);
753
+ console.log(`\ndotbot v${VERSION} — ${options.provider}/${options.model}${options.sandbox ? ' (sandbox)' : ''}`);
515
754
  if (options.session) {
516
755
  console.log(`Resuming session: ${session.id}`);
517
756
  }
@@ -519,6 +758,8 @@ async function runRepl(options) {
519
758
 
520
759
  const showHelp = () => {
521
760
  console.log('Available Commands:');
761
+ console.log(' /models List available models from provider');
762
+ console.log(' /load <model> Switch to a different model');
522
763
  console.log(' /show Show model information');
523
764
  console.log(' /clear Clear session context');
524
765
  console.log(' /bye Exit');
@@ -588,6 +829,50 @@ async function runRepl(options) {
588
829
  return;
589
830
  }
590
831
 
832
+ if (trimmed === '/models') {
833
+ const apiKey = process.env[AI_PROVIDERS[options.provider]?.envKey];
834
+ process.stdout.write('Fetching models');
835
+ startSpinner();
836
+ const { ok, models } = await fetchProviderModels(options.provider, apiKey);
837
+ if (ok && models.length) {
838
+ stopSpinner('');
839
+ console.log('');
840
+ for (const m of models) {
841
+ const active = m.id === options.model ? ' (active)' : '';
842
+ console.log(` ${m.id}${active}`);
843
+ }
844
+ console.log('');
845
+ } else {
846
+ stopSpinner('');
847
+ // Fall back to static list
848
+ const base = AI_PROVIDERS[options.provider];
849
+ if (base.models?.length) {
850
+ console.log('');
851
+ for (const m of base.models) {
852
+ const active = m.id === options.model ? ' (active)' : '';
853
+ console.log(` ${m.id}${active}`);
854
+ }
855
+ console.log('');
856
+ } else {
857
+ console.log('\nNo models found.\n');
858
+ }
859
+ }
860
+ promptUser();
861
+ return;
862
+ }
863
+
864
+ if (trimmed.startsWith('/load ')) {
865
+ const newModel = trimmed.slice(6).trim();
866
+ if (!newModel) {
867
+ console.log('Usage: /load <model-name>\n');
868
+ } else {
869
+ options.model = newModel;
870
+ console.log(`Switched to ${newModel}\n`);
871
+ }
872
+ promptUser();
873
+ return;
874
+ }
875
+
591
876
  await handleMessage(trimmed);
592
877
  });
593
878
  };
@@ -595,68 +880,14 @@ async function runRepl(options) {
595
880
  const handleMessage = async (text) => {
596
881
  messages.push({ role: 'user', content: text });
597
882
 
598
- let hasThinkingText = false;
599
- let thinkingDone = false;
600
- let assistantContent = '';
601
-
602
- process.stdout.write('Thinking');
603
- startSpinner();
604
-
605
883
  try {
606
- for await (const event of agentLoop({
884
+ const assistantContent = await streamEvents(agentLoop({
607
885
  model: options.model,
608
886
  messages: [...messages],
609
- tools: coreTools,
887
+ tools: getActiveTools(options.sandbox, options.sandboxAllow),
610
888
  provider,
611
889
  context,
612
- })) {
613
- switch (event.type) {
614
- case 'thinking':
615
- if (event.text) {
616
- if (!hasThinkingText) {
617
- stopSpinner('');
618
- process.stdout.write('\n');
619
- hasThinkingText = true;
620
- }
621
- process.stdout.write(event.text);
622
- }
623
- break;
624
- case 'text_delta':
625
- if (!thinkingDone) {
626
- if (hasThinkingText) {
627
- process.stdout.write('\n...done thinking.\n\n');
628
- } else {
629
- stopSpinner('');
630
- }
631
- thinkingDone = true;
632
- }
633
- process.stdout.write(event.text);
634
- assistantContent += event.text;
635
- break;
636
- case 'tool_start':
637
- if (!thinkingDone) {
638
- if (hasThinkingText) {
639
- process.stdout.write('\n...done thinking.\n\n');
640
- } else {
641
- stopSpinner('');
642
- }
643
- thinkingDone = true;
644
- }
645
- process.stdout.write(`[${event.name}] `);
646
- startSpinner();
647
- break;
648
- case 'tool_result':
649
- stopSpinner('done');
650
- break;
651
- case 'tool_error':
652
- stopSpinner('error');
653
- break;
654
- case 'error':
655
- stopSpinner();
656
- console.error(`\nError: ${event.error}`);
657
- break;
658
- }
659
- }
890
+ }));
660
891
 
661
892
  if (assistantContent) {
662
893
  messages.push({ role: 'assistant', content: assistantContent });
@@ -771,7 +1002,7 @@ async function runServer(options) {
771
1002
  for await (const event of agentLoop({
772
1003
  model,
773
1004
  messages: reqMessages,
774
- tools: coreTools,
1005
+ tools: getActiveTools(options.sandbox, options.sandboxAllow),
775
1006
  provider,
776
1007
  context,
777
1008
  })) {
@@ -804,7 +1035,7 @@ async function runServer(options) {
804
1035
  for await (const event of agentLoop({
805
1036
  model,
806
1037
  messages: reqMessages,
807
- tools: coreTools,
1038
+ tools: getActiveTools(options.sandbox, options.sandboxAllow),
808
1039
  provider,
809
1040
  context,
810
1041
  })) {
@@ -878,7 +1109,7 @@ async function runServer(options) {
878
1109
  for await (const event of agentLoop({
879
1110
  model,
880
1111
  messages,
881
- tools: coreTools,
1112
+ tools: getActiveTools(options.sandbox, options.sandboxAllow),
882
1113
  provider,
883
1114
  context,
884
1115
  })) {
@@ -1265,6 +1496,62 @@ async function runDoctor(options) {
1265
1496
  console.log();
1266
1497
  }
1267
1498
 
1499
+ /**
1500
+ * List available models from the provider's API.
1501
+ *
1502
+ * @param {Object} options - CLI options
1503
+ */
1504
+ async function runModels(options) {
1505
+ await loadModules();
1506
+ const base = AI_PROVIDERS[options.provider];
1507
+ if (!base) {
1508
+ console.error(`Unknown provider: ${options.provider}`);
1509
+ process.exit(1);
1510
+ }
1511
+
1512
+ const apiKey = process.env[base.envKey];
1513
+ if (!apiKey && options.provider !== 'ollama') {
1514
+ console.error(`Missing ${base.envKey} — set it or run dotbot to configure interactively.`);
1515
+ process.exit(1);
1516
+ }
1517
+
1518
+ process.stdout.write('Fetching models');
1519
+ startSpinner();
1520
+
1521
+ const { ok, models } = await fetchProviderModels(options.provider, apiKey);
1522
+
1523
+ if (!ok) {
1524
+ stopSpinner('failed');
1525
+ console.error(`Could not fetch models from ${base.name}. Check your API key.`);
1526
+
1527
+ // Fall back to static list
1528
+ if (base.models?.length) {
1529
+ console.log(`\nLocal model list (${base.name}):\n`);
1530
+ for (const m of base.models) {
1531
+ const active = m.id === options.model ? ' (active)' : '';
1532
+ console.log(` ${m.id}${active}`);
1533
+ }
1534
+ console.log();
1535
+ }
1536
+ return;
1537
+ }
1538
+
1539
+ stopSpinner('');
1540
+
1541
+ if (options.json) {
1542
+ console.log(JSON.stringify(models));
1543
+ return;
1544
+ }
1545
+
1546
+ console.log(`\n${base.name} models (${models.length})\n`);
1547
+ for (const m of models) {
1548
+ const active = m.id === options.model ? ' (active)' : '';
1549
+ const label = m.name !== m.id ? ` — ${m.name}` : '';
1550
+ console.log(` ${m.id}${label}${active}`);
1551
+ }
1552
+ console.log();
1553
+ }
1554
+
1268
1555
  /**
1269
1556
  * Main entry point.
1270
1557
  */
@@ -1297,6 +1584,9 @@ async function main() {
1297
1584
  }
1298
1585
 
1299
1586
  switch (command) {
1587
+ case 'models':
1588
+ await runModels(args);
1589
+ break;
1300
1590
  case 'doctor':
1301
1591
  await runDoctor(args);
1302
1592
  break;
package/core/agent.js CHANGED
@@ -31,7 +31,7 @@ const OLLAMA_BASE = "http://localhost:11434";
31
31
  * @param {Array} options.tools - Tool definitions from tools.js
32
32
  * @param {AbortSignal} [options.signal] - Optional abort signal
33
33
  * @param {Object} [options.provider] - Provider config from AI_PROVIDERS. Defaults to Ollama.
34
- * @param {Object} [options.context] - Execution context passed to tool execute functions (e.g. databaseManager, dbConfig, userID).
34
+ * @param {Object} [options.context] - Execution context passed to tool execute functions (e.g. providers, userID).
35
35
  * @yields {Object} Stream events for the frontend
36
36
  */
37
37
  export async function* agentLoop({ model, messages, tools, signal, provider, context, maxTurns }) {