@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 +7 -0
- package/bin/dotbot.js +134 -33
- package/dotbot.db +0 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
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
|
-
|
|
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
|
-
*
|
|
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
|
|
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 (
|
|
356
|
-
|
|
357
|
-
|
|
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 (
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
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 (
|
|
531
|
-
|
|
532
|
-
|
|
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 (
|
|
540
|
-
|
|
541
|
-
|
|
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