@stevederico/dotbot 0.25.0 → 0.27.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 CHANGED
@@ -1,3 +1,17 @@
1
+ 0.27
2
+
3
+ Add interactive API key prompt
4
+ Add key saving to dotbotrc
5
+ Add provider signup URLs
6
+ Update thinking spinner display
7
+
8
+ 0.26
9
+
10
+ Update REPL prompt style
11
+ Add visible thinking output
12
+ Add /help, /show, /bye commands
13
+ Add multi-line input mode
14
+
1
15
  0.25
2
16
 
3
17
  Add delete subcommands for memory, jobs, tasks, sessions
package/bin/dotbot.js CHANGED
@@ -22,7 +22,7 @@ process.emit = function (event, error) {
22
22
  */
23
23
 
24
24
  import { parseArgs } from 'node:util';
25
- import { readFileSync, existsSync } from 'node:fs';
25
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
26
26
  import { fileURLToPath } from 'node:url';
27
27
  import { dirname, join } from 'node:path';
28
28
  import { homedir } from 'node:os';
@@ -162,7 +162,18 @@ function loadConfig() {
162
162
  }
163
163
  try {
164
164
  const content = readFileSync(CONFIG_PATH, 'utf8');
165
- return JSON.parse(content);
165
+ const config = JSON.parse(content);
166
+
167
+ // Inject saved API keys into process.env (don't override existing)
168
+ if (config.env && typeof config.env === 'object') {
169
+ for (const [key, value] of Object.entries(config.env)) {
170
+ if (!process.env[key]) {
171
+ process.env[key] = value;
172
+ }
173
+ }
174
+ }
175
+
176
+ return config;
166
177
  } catch (err) {
167
178
  console.error(`Warning: Invalid config file at ${CONFIG_PATH}: ${err.message}`);
168
179
  return {};
@@ -214,7 +225,42 @@ function parseCliArgs() {
214
225
  }
215
226
 
216
227
  /**
217
- * Get provider config with API key from environment.
228
+ * Prompt user for input via readline.
229
+ *
230
+ * @param {string} question - Prompt text
231
+ * @returns {Promise<string>} User input
232
+ */
233
+ function askUser(question) {
234
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
235
+ return new Promise((resolve) => {
236
+ rl.question(question, (answer) => {
237
+ rl.close();
238
+ resolve(answer.trim());
239
+ });
240
+ });
241
+ }
242
+
243
+ /**
244
+ * Save or update a key in ~/.dotbotrc config file.
245
+ *
246
+ * @param {string} key - Config key
247
+ * @param {*} value - Config value
248
+ */
249
+ function saveToConfig(key, value) {
250
+ let config = {};
251
+ if (existsSync(CONFIG_PATH)) {
252
+ try {
253
+ config = JSON.parse(readFileSync(CONFIG_PATH, 'utf8'));
254
+ } catch {
255
+ config = {};
256
+ }
257
+ }
258
+ config[key] = value;
259
+ writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
260
+ }
261
+
262
+ /**
263
+ * Get provider config with API key from environment or interactive prompt.
218
264
  *
219
265
  * @param {string} providerId - Provider ID
220
266
  * @returns {Object} Provider config with headers
@@ -228,25 +274,60 @@ async function getProviderConfig(providerId) {
228
274
  process.exit(1);
229
275
  }
230
276
 
231
- const envKey = base.envKey;
232
- const apiKey = process.env[envKey];
233
-
234
- if (!apiKey && providerId !== 'ollama') {
235
- console.error(`Missing ${envKey} environment variable`);
236
- process.exit(1);
237
- }
238
-
239
277
  if (providerId === 'ollama') {
240
278
  const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
241
279
  return { ...base, apiUrl: `${baseUrl}/api/chat` };
242
280
  }
243
281
 
282
+ const envKey = base.envKey;
283
+ let apiKey = process.env[envKey];
284
+
285
+ if (!apiKey) {
286
+ if (!process.stdin.isTTY) {
287
+ console.error(`Missing ${envKey} environment variable.`);
288
+ console.error(`Get one at: ${getProviderSignupUrl(providerId)}`);
289
+ process.exit(1);
290
+ }
291
+
292
+ console.log(`\n${base.name} API key not found (${envKey}). Get one at: ${getProviderSignupUrl(providerId)}\n`);
293
+
294
+ apiKey = await askUser(`Paste your ${base.name} API key: `);
295
+ if (!apiKey) {
296
+ console.error('No API key provided.');
297
+ process.exit(1);
298
+ }
299
+
300
+ const save = await askUser('Save to ~/.dotbotrc for next time? (Y/n) ');
301
+ if (save.toLowerCase() !== 'n') {
302
+ saveToConfig('env', { ...loadConfig().env, [envKey]: apiKey });
303
+ console.log(`Saved to ${CONFIG_PATH}\n`);
304
+ }
305
+
306
+ process.env[envKey] = apiKey;
307
+ }
308
+
244
309
  return {
245
310
  ...base,
246
311
  headers: () => base.headers(apiKey),
247
312
  };
248
313
  }
249
314
 
315
+ /**
316
+ * Get signup/API key URL for a provider.
317
+ *
318
+ * @param {string} providerId - Provider ID
319
+ * @returns {string} URL to get an API key
320
+ */
321
+ function getProviderSignupUrl(providerId) {
322
+ const urls = {
323
+ xai: 'https://console.x.ai',
324
+ openai: 'https://platform.openai.com/api-keys',
325
+ anthropic: 'https://console.anthropic.com/settings/keys',
326
+ cerebras: 'https://cloud.cerebras.ai',
327
+ };
328
+ return urls[providerId] || 'the provider\'s website';
329
+ }
330
+
250
331
  /**
251
332
  * Initialize stores.
252
333
  *
@@ -331,7 +412,10 @@ async function runChat(message, options) {
331
412
  ...storesObj,
332
413
  };
333
414
 
334
- process.stdout.write('\n[thinking] ');
415
+ let hasThinkingText = false;
416
+ let thinkingDone = false;
417
+
418
+ process.stdout.write('Thinking');
335
419
  startSpinner();
336
420
 
337
421
  for await (const event of agentLoop({
@@ -343,14 +427,35 @@ async function runChat(message, options) {
343
427
  })) {
344
428
  switch (event.type) {
345
429
  case 'thinking':
346
- // Already showing spinner, ignore thinking events
430
+ if (event.text) {
431
+ if (!hasThinkingText) {
432
+ stopSpinner('');
433
+ process.stdout.write('\n');
434
+ hasThinkingText = true;
435
+ }
436
+ process.stdout.write(event.text);
437
+ }
347
438
  break;
348
439
  case 'text_delta':
349
- stopSpinner(''); // Stop thinking spinner silently
440
+ if (!thinkingDone) {
441
+ if (hasThinkingText) {
442
+ process.stdout.write('\n...done thinking.\n\n');
443
+ } else {
444
+ stopSpinner('');
445
+ }
446
+ thinkingDone = true;
447
+ }
350
448
  process.stdout.write(event.text);
351
449
  break;
352
450
  case 'tool_start':
353
- stopSpinner(''); // Stop thinking spinner silently
451
+ if (!thinkingDone) {
452
+ if (hasThinkingText) {
453
+ process.stdout.write('\n...done thinking.\n\n');
454
+ } else {
455
+ stopSpinner('');
456
+ }
457
+ thinkingDone = true;
458
+ }
354
459
  process.stdout.write(`[${event.name}] `);
355
460
  startSpinner();
356
461
  break;
@@ -410,18 +515,55 @@ async function runRepl(options) {
410
515
  if (options.session) {
411
516
  console.log(`Resuming session: ${session.id}`);
412
517
  }
413
- console.log('Type /quit to exit, /clear to reset conversation\n');
518
+ console.log('Type /? for help\n');
519
+
520
+ const showHelp = () => {
521
+ console.log('Available Commands:');
522
+ console.log(' /show Show model information');
523
+ console.log(' /clear Clear session context');
524
+ console.log(' /bye Exit');
525
+ console.log(' /?, /help Help for a command');
526
+ console.log('');
527
+ console.log('Use """ to begin a multi-line message.\n');
528
+ };
414
529
 
415
- const prompt = () => {
416
- rl.question('> ', async (input) => {
530
+ const showModel = () => {
531
+ console.log(` Model: ${options.model}`);
532
+ console.log(` Provider: ${options.provider}`);
533
+ console.log(` Session: ${session.id}\n`);
534
+ };
535
+
536
+ const promptUser = () => {
537
+ rl.question('>>> ', async (input) => {
417
538
  const trimmed = input.trim();
418
539
 
419
540
  if (!trimmed) {
420
- prompt();
541
+ promptUser();
542
+ return;
543
+ }
544
+
545
+ // Multi-line input mode
546
+ if (trimmed === '"""') {
547
+ let multiLine = '';
548
+ const collectLines = () => {
549
+ rl.question('... ', (line) => {
550
+ if (line.trim() === '"""') {
551
+ if (multiLine.trim()) {
552
+ handleMessage(multiLine.trim());
553
+ } else {
554
+ promptUser();
555
+ }
556
+ } else {
557
+ multiLine += (multiLine ? '\n' : '') + line;
558
+ collectLines();
559
+ }
560
+ });
561
+ };
562
+ collectLines();
421
563
  return;
422
564
  }
423
565
 
424
- if (trimmed === '/quit' || trimmed === '/exit') {
566
+ if (trimmed === '/bye' || trimmed === '/quit' || trimmed === '/exit') {
425
567
  console.log('Goodbye!');
426
568
  rl.close();
427
569
  process.exit(0);
@@ -430,64 +572,104 @@ async function runRepl(options) {
430
572
  if (trimmed === '/clear') {
431
573
  messages.length = 0;
432
574
  console.log('Conversation cleared.\n');
433
- prompt();
575
+ promptUser();
434
576
  return;
435
577
  }
436
578
 
437
- messages.push({ role: 'user', content: trimmed });
579
+ if (trimmed === '/?' || trimmed === '/help') {
580
+ showHelp();
581
+ promptUser();
582
+ return;
583
+ }
438
584
 
439
- process.stdout.write('\n[thinking] ');
440
- startSpinner();
441
- let assistantContent = '';
585
+ if (trimmed === '/show') {
586
+ showModel();
587
+ promptUser();
588
+ return;
589
+ }
442
590
 
443
- try {
444
- for await (const event of agentLoop({
445
- model: options.model,
446
- messages: [...messages],
447
- tools: coreTools,
448
- provider,
449
- context,
450
- })) {
451
- switch (event.type) {
452
- case 'thinking':
453
- // Already showing spinner, ignore thinking events
454
- break;
455
- case 'text_delta':
456
- stopSpinner(''); // Stop thinking spinner silently
591
+ await handleMessage(trimmed);
592
+ });
593
+ };
594
+
595
+ const handleMessage = async (text) => {
596
+ messages.push({ role: 'user', content: text });
597
+
598
+ let hasThinkingText = false;
599
+ let thinkingDone = false;
600
+ let assistantContent = '';
601
+
602
+ process.stdout.write('Thinking');
603
+ startSpinner();
604
+
605
+ try {
606
+ for await (const event of agentLoop({
607
+ model: options.model,
608
+ messages: [...messages],
609
+ tools: coreTools,
610
+ provider,
611
+ 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
+ }
457
621
  process.stdout.write(event.text);
458
- assistantContent += event.text;
459
- break;
460
- case 'tool_start':
461
- stopSpinner(''); // Stop thinking spinner silently
462
- process.stdout.write(`[${event.name}] `);
463
- startSpinner();
464
- break;
465
- case 'tool_result':
466
- stopSpinner('done');
467
- break;
468
- case 'tool_error':
469
- stopSpinner('error');
470
- break;
471
- case 'error':
472
- stopSpinner();
473
- console.error(`\nError: ${event.error}`);
474
- break;
475
- }
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;
476
658
  }
659
+ }
477
660
 
478
- if (assistantContent) {
479
- messages.push({ role: 'assistant', content: assistantContent });
480
- }
481
- } catch (err) {
482
- console.error(`\nError: ${err.message}`);
661
+ if (assistantContent) {
662
+ messages.push({ role: 'assistant', content: assistantContent });
483
663
  }
664
+ } catch (err) {
665
+ console.error(`\nError: ${err.message}`);
666
+ }
484
667
 
485
- process.stdout.write('\n\n');
486
- prompt();
487
- });
668
+ process.stdout.write('\n\n');
669
+ promptUser();
488
670
  };
489
671
 
490
- prompt();
672
+ promptUser();
491
673
  }
492
674
 
493
675
  /**
package/dotbot.db CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevederico/dotbot",
3
- "version": "0.25.0",
3
+ "version": "0.27.0",
4
4
  "description": "AI agent CLI and library for Node.js — streaming, multi-provider, tool execution, autonomous tasks",
5
5
  "type": "module",
6
6
  "main": "index.js",