@stevederico/dotbot 0.26.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,10 @@
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
+
1
8
  0.26
2
9
 
3
10
  Update REPL prompt style
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,9 +412,12 @@ async function runChat(message, options) {
331
412
  ...storesObj,
332
413
  };
333
414
 
334
- let isThinking = false;
415
+ let hasThinkingText = false;
335
416
  let thinkingDone = false;
336
417
 
418
+ process.stdout.write('Thinking');
419
+ startSpinner();
420
+
337
421
  for await (const event of agentLoop({
338
422
  model: options.model,
339
423
  messages,
@@ -343,26 +427,33 @@ async function runChat(message, options) {
343
427
  })) {
344
428
  switch (event.type) {
345
429
  case 'thinking':
346
- if (!isThinking) {
347
- process.stdout.write('Thinking...\n');
348
- isThinking = true;
349
- }
350
430
  if (event.text) {
431
+ if (!hasThinkingText) {
432
+ stopSpinner('');
433
+ process.stdout.write('\n');
434
+ hasThinkingText = true;
435
+ }
351
436
  process.stdout.write(event.text);
352
437
  }
353
438
  break;
354
439
  case 'text_delta':
355
- if (isThinking && !thinkingDone) {
356
- stopSpinner('');
357
- process.stdout.write('\n...done thinking.\n\n');
440
+ if (!thinkingDone) {
441
+ if (hasThinkingText) {
442
+ process.stdout.write('\n...done thinking.\n\n');
443
+ } else {
444
+ stopSpinner('');
445
+ }
358
446
  thinkingDone = true;
359
447
  }
360
448
  process.stdout.write(event.text);
361
449
  break;
362
450
  case 'tool_start':
363
- if (isThinking && !thinkingDone) {
364
- stopSpinner('');
365
- process.stdout.write('\n...done thinking.\n\n');
451
+ if (!thinkingDone) {
452
+ if (hasThinkingText) {
453
+ process.stdout.write('\n...done thinking.\n\n');
454
+ } else {
455
+ stopSpinner('');
456
+ }
366
457
  thinkingDone = true;
367
458
  }
368
459
  process.stdout.write(`[${event.name}] `);
@@ -504,10 +595,13 @@ async function runRepl(options) {
504
595
  const handleMessage = async (text) => {
505
596
  messages.push({ role: 'user', content: text });
506
597
 
507
- let isThinking = false;
598
+ let hasThinkingText = false;
508
599
  let thinkingDone = false;
509
600
  let assistantContent = '';
510
601
 
602
+ process.stdout.write('Thinking');
603
+ startSpinner();
604
+
511
605
  try {
512
606
  for await (const event of agentLoop({
513
607
  model: options.model,
@@ -518,27 +612,34 @@ async function runRepl(options) {
518
612
  })) {
519
613
  switch (event.type) {
520
614
  case 'thinking':
521
- if (!isThinking) {
522
- process.stdout.write('Thinking...\n');
523
- isThinking = true;
524
- }
525
615
  if (event.text) {
616
+ if (!hasThinkingText) {
617
+ stopSpinner('');
618
+ process.stdout.write('\n');
619
+ hasThinkingText = true;
620
+ }
526
621
  process.stdout.write(event.text);
527
622
  }
528
623
  break;
529
624
  case 'text_delta':
530
- if (isThinking && !thinkingDone) {
531
- stopSpinner('');
532
- process.stdout.write('\n...done thinking.\n\n');
625
+ if (!thinkingDone) {
626
+ if (hasThinkingText) {
627
+ process.stdout.write('\n...done thinking.\n\n');
628
+ } else {
629
+ stopSpinner('');
630
+ }
533
631
  thinkingDone = true;
534
632
  }
535
633
  process.stdout.write(event.text);
536
634
  assistantContent += event.text;
537
635
  break;
538
636
  case 'tool_start':
539
- if (isThinking && !thinkingDone) {
540
- stopSpinner('');
541
- process.stdout.write('\n...done thinking.\n\n');
637
+ if (!thinkingDone) {
638
+ if (hasThinkingText) {
639
+ process.stdout.write('\n...done thinking.\n\n');
640
+ } else {
641
+ stopSpinner('');
642
+ }
542
643
  thinkingDone = true;
543
644
  }
544
645
  process.stdout.write(`[${event.name}] `);
package/dotbot.db CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevederico/dotbot",
3
- "version": "0.26.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",