@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 +14 -0
- package/bin/dotbot.js +248 -66
- package/dotbot.db +0 -0
- package/package.json +1 -1
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
|
-
|
|
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,7 +412,10 @@ async function runChat(message, options) {
|
|
|
331
412
|
...storesObj,
|
|
332
413
|
};
|
|
333
414
|
|
|
334
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
416
|
-
|
|
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
|
-
|
|
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
|
-
|
|
575
|
+
promptUser();
|
|
434
576
|
return;
|
|
435
577
|
}
|
|
436
578
|
|
|
437
|
-
|
|
579
|
+
if (trimmed === '/?' || trimmed === '/help') {
|
|
580
|
+
showHelp();
|
|
581
|
+
promptUser();
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
438
584
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
585
|
+
if (trimmed === '/show') {
|
|
586
|
+
showModel();
|
|
587
|
+
promptUser();
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
442
590
|
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
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
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
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
|
-
|
|
479
|
-
|
|
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
|
-
|
|
486
|
-
|
|
487
|
-
});
|
|
668
|
+
process.stdout.write('\n\n');
|
|
669
|
+
promptUser();
|
|
488
670
|
};
|
|
489
671
|
|
|
490
|
-
|
|
672
|
+
promptUser();
|
|
491
673
|
}
|
|
492
674
|
|
|
493
675
|
/**
|
package/dotbot.db
CHANGED
|
Binary file
|
package/package.json
CHANGED