@stevederico/dotbot 0.24.0 → 0.26.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,20 @@
1
+ 0.26
2
+
3
+ Update REPL prompt style
4
+ Add visible thinking output
5
+ Add /help, /show, /bye commands
6
+ Add multi-line input mode
7
+
8
+ 0.25
9
+
10
+ Add delete subcommands for memory, jobs, tasks, sessions
11
+ Add --json flag for machine-readable output
12
+ Add doctor command for environment check
13
+ Add ~/.dotbotrc config file support
14
+ Add --openai flag for OpenAI-compatible API
15
+ Add pipe support (stdin input)
16
+ Add --session flag to resume conversations
17
+
1
18
  0.24
2
19
 
3
20
  Add --system flag for custom prompts
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <img src="https://img.shields.io/github/stars/stevederico/dotbot?style=social" alt="GitHub stars">
14
14
  </a>
15
15
  <a href="https://github.com/stevederico/dotbot">
16
- <img src="https://img.shields.io/badge/version-0.24-green" alt="version">
16
+ <img src="https://img.shields.io/badge/version-0.25-green" alt="version">
17
17
  </a>
18
18
  <img src="https://img.shields.io/badge/LOC-11k-orange" alt="Lines of Code">
19
19
  </p>
@@ -154,28 +154,38 @@ for await (const event of agent.chat({
154
154
  ## CLI Reference
155
155
 
156
156
  ```
157
- dotbot v0.24 — AI agent CLI
157
+ dotbot v0.25 — AI agent CLI
158
158
 
159
159
  Usage:
160
160
  dotbot "message" One-shot query
161
161
  dotbot Interactive chat
162
162
  dotbot serve [--port N] Start HTTP server (default: 3000)
163
+ dotbot serve --openai Start OpenAI-compatible API server
164
+ echo "msg" | dotbot Pipe input from stdin
163
165
 
164
166
  Commands:
167
+ doctor Check environment and configuration
165
168
  tools List all available tools
166
169
  stats Show database statistics
167
170
  memory [list|search <q>] Manage saved memories
171
+ memory delete <key> Delete a memory by key
168
172
  jobs List scheduled jobs
173
+ jobs delete <id> Delete a scheduled job
169
174
  tasks List active tasks
175
+ tasks delete <id> Delete a task
170
176
  sessions List chat sessions
177
+ sessions delete <id> Delete a session
171
178
  events [--summary] View audit log
172
179
 
173
180
  Options:
174
181
  --provider, -p AI provider: xai, anthropic, openai, ollama (default: xai)
175
182
  --model, -m Model name (default: grok-4-1-fast-reasoning)
176
183
  --system, -s Custom system prompt (prepended to default)
184
+ --session Resume a specific session by ID
177
185
  --db SQLite database path (default: ./dotbot.db)
178
186
  --port Server port for 'serve' command
187
+ --openai Enable OpenAI-compatible API endpoints
188
+ --json Output as JSON (for inspection commands)
179
189
  --verbose Show initialization logs
180
190
  --help, -h Show help
181
191
  --version, -v Show version
@@ -185,6 +195,9 @@ Environment Variables:
185
195
  ANTHROPIC_API_KEY API key for Anthropic
186
196
  OPENAI_API_KEY API key for OpenAI
187
197
  OLLAMA_BASE_URL Base URL for Ollama (default: http://localhost:11434)
198
+
199
+ Config File:
200
+ ~/.dotbotrc JSON config for defaults (provider, model, db)
188
201
  ```
189
202
 
190
203
  <br />
package/bin/dotbot.js CHANGED
@@ -22,11 +22,13 @@ process.emit = function (event, error) {
22
22
  */
23
23
 
24
24
  import { parseArgs } from 'node:util';
25
- import { readFileSync } from 'node:fs';
25
+ import { readFileSync, existsSync } from 'node:fs';
26
26
  import { fileURLToPath } from 'node:url';
27
27
  import { dirname, join } from 'node:path';
28
+ import { homedir } from 'node:os';
28
29
  import * as readline from 'node:readline';
29
30
  import { createServer } from 'node:http';
31
+ import { randomUUID } from 'node:crypto';
30
32
 
31
33
  // Read version from package.json
32
34
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -59,6 +61,7 @@ async function loadModules() {
59
61
  }
60
62
  const DEFAULT_PORT = 3000;
61
63
  const DEFAULT_DB = './dotbot.db';
64
+ const CONFIG_PATH = join(homedir(), '.dotbotrc');
62
65
 
63
66
  // Spinner for tool execution feedback
64
67
  let spinnerInterval = null;
@@ -93,22 +96,32 @@ Usage:
93
96
  dotbot "message" One-shot query
94
97
  dotbot Interactive chat
95
98
  dotbot serve [--port N] Start HTTP server (default: ${DEFAULT_PORT})
99
+ dotbot serve --openai Start OpenAI-compatible API server
100
+ echo "msg" | dotbot Pipe input from stdin
96
101
 
97
102
  Commands:
103
+ doctor Check environment and configuration
98
104
  tools List all available tools
99
105
  stats Show database statistics
100
106
  memory [list|search <q>] Manage saved memories
107
+ memory delete <key> Delete a memory by key
101
108
  jobs List scheduled jobs
109
+ jobs delete <id> Delete a scheduled job
102
110
  tasks List active tasks
111
+ tasks delete <id> Delete a task
103
112
  sessions List chat sessions
113
+ sessions delete <id> Delete a session
104
114
  events [--summary] View audit log
105
115
 
106
116
  Options:
107
117
  --provider, -p AI provider: xai, anthropic, openai, ollama (default: xai)
108
118
  --model, -m Model name (default: grok-4-1-fast-reasoning)
109
119
  --system, -s Custom system prompt (prepended to default)
120
+ --session Resume a specific session by ID
110
121
  --db SQLite database path (default: ${DEFAULT_DB})
111
122
  --port Server port for 'serve' command (default: ${DEFAULT_PORT})
123
+ --openai Enable OpenAI-compatible API endpoints (/v1/chat/completions, /v1/models)
124
+ --json Output as JSON (for inspection commands)
112
125
  --verbose Show initialization logs
113
126
  --help, -h Show this help
114
127
  --version, -v Show version
@@ -119,20 +132,51 @@ Environment Variables:
119
132
  OPENAI_API_KEY API key for OpenAI
120
133
  OLLAMA_BASE_URL Base URL for Ollama (default: http://localhost:11434)
121
134
 
135
+ Config File:
136
+ ~/.dotbotrc JSON config for defaults (provider, model, db)
137
+
122
138
  Examples:
123
139
  dotbot "What's the weather in SF?"
124
140
  dotbot
125
141
  dotbot serve --port 8080
142
+ dotbot doctor
126
143
  dotbot tools
127
144
  dotbot memory search "preferences"
145
+ dotbot memory delete user_pref
146
+ dotbot stats --json
128
147
  dotbot --system "You are a pirate" "Hello"
148
+ dotbot --session abc-123 "follow up question"
149
+ echo "What is 2+2?" | dotbot
150
+ cat question.txt | dotbot
129
151
  `);
130
152
  }
131
153
 
132
154
  /**
133
- * Parse CLI arguments.
155
+ * Load config from ~/.dotbotrc if it exists.
156
+ *
157
+ * @returns {Object} Config object or empty object if not found
158
+ */
159
+ function loadConfig() {
160
+ if (!existsSync(CONFIG_PATH)) {
161
+ return {};
162
+ }
163
+ try {
164
+ const content = readFileSync(CONFIG_PATH, 'utf8');
165
+ return JSON.parse(content);
166
+ } catch (err) {
167
+ console.error(`Warning: Invalid config file at ${CONFIG_PATH}: ${err.message}`);
168
+ return {};
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Parse CLI arguments with config file fallback.
174
+ *
175
+ * @returns {Object} Merged CLI args and config values
134
176
  */
135
177
  function parseCliArgs() {
178
+ const config = loadConfig();
179
+
136
180
  try {
137
181
  const { values, positionals } = parseArgs({
138
182
  allowPositionals: true,
@@ -140,15 +184,29 @@ function parseCliArgs() {
140
184
  help: { type: 'boolean', short: 'h', default: false },
141
185
  version: { type: 'boolean', short: 'v', default: false },
142
186
  verbose: { type: 'boolean', default: false },
143
- provider: { type: 'string', short: 'p', default: 'xai' },
144
- model: { type: 'string', short: 'm', default: 'grok-4-1-fast-reasoning' },
145
- system: { type: 'string', short: 's', default: '' },
187
+ provider: { type: 'string', short: 'p' },
188
+ model: { type: 'string', short: 'm' },
189
+ system: { type: 'string', short: 's' },
146
190
  summary: { type: 'boolean', default: false },
147
- db: { type: 'string', default: DEFAULT_DB },
148
- port: { type: 'string', default: String(DEFAULT_PORT) },
191
+ json: { type: 'boolean', default: false },
192
+ db: { type: 'string' },
193
+ port: { type: 'string' },
194
+ openai: { type: 'boolean', default: false },
195
+ session: { type: 'string', default: '' },
149
196
  },
150
197
  });
151
- return { ...values, positionals };
198
+
199
+ // Merge: CLI args > config file > hardcoded defaults
200
+ return {
201
+ ...values,
202
+ provider: values.provider ?? config.provider ?? 'xai',
203
+ model: values.model ?? config.model ?? 'grok-4-1-fast-reasoning',
204
+ system: values.system ?? config.system ?? '',
205
+ db: values.db ?? config.db ?? DEFAULT_DB,
206
+ port: values.port ?? config.port ?? String(DEFAULT_PORT),
207
+ session: values.session ?? '',
208
+ positionals,
209
+ };
152
210
  } catch (err) {
153
211
  console.error(`Error: ${err.message}`);
154
212
  process.exit(1);
@@ -251,7 +309,20 @@ async function runChat(message, options) {
251
309
  const storesObj = await initStores(options.db, options.verbose, options.system);
252
310
  const provider = await getProviderConfig(options.provider);
253
311
 
254
- const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
312
+ let session;
313
+ let messages;
314
+
315
+ if (options.session) {
316
+ session = await storesObj.sessionStore.getSession(options.session, 'cli-user');
317
+ if (!session) {
318
+ console.error(`Error: Session not found: ${options.session}`);
319
+ process.exit(1);
320
+ }
321
+ messages = [...(session.messages || []), { role: 'user', content: message }];
322
+ } else {
323
+ session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
324
+ messages = [{ role: 'user', content: message }];
325
+ }
255
326
 
256
327
  const context = {
257
328
  userID: 'cli-user',
@@ -260,10 +331,8 @@ async function runChat(message, options) {
260
331
  ...storesObj,
261
332
  };
262
333
 
263
- const messages = [{ role: 'user', content: message }];
264
-
265
- process.stdout.write('\n[thinking] ');
266
- startSpinner();
334
+ let isThinking = false;
335
+ let thinkingDone = false;
267
336
 
268
337
  for await (const event of agentLoop({
269
338
  model: options.model,
@@ -274,14 +343,28 @@ async function runChat(message, options) {
274
343
  })) {
275
344
  switch (event.type) {
276
345
  case 'thinking':
277
- // Already showing spinner, ignore thinking events
346
+ if (!isThinking) {
347
+ process.stdout.write('Thinking...\n');
348
+ isThinking = true;
349
+ }
350
+ if (event.text) {
351
+ process.stdout.write(event.text);
352
+ }
278
353
  break;
279
354
  case 'text_delta':
280
- stopSpinner(''); // Stop thinking spinner silently
355
+ if (isThinking && !thinkingDone) {
356
+ stopSpinner('');
357
+ process.stdout.write('\n...done thinking.\n\n');
358
+ thinkingDone = true;
359
+ }
281
360
  process.stdout.write(event.text);
282
361
  break;
283
362
  case 'tool_start':
284
- stopSpinner(''); // Stop thinking spinner silently
363
+ if (isThinking && !thinkingDone) {
364
+ stopSpinner('');
365
+ process.stdout.write('\n...done thinking.\n\n');
366
+ thinkingDone = true;
367
+ }
285
368
  process.stdout.write(`[${event.name}] `);
286
369
  startSpinner();
287
370
  break;
@@ -310,8 +393,20 @@ async function runRepl(options) {
310
393
  const storesObj = await initStores(options.db, options.verbose, options.system);
311
394
  const provider = await getProviderConfig(options.provider);
312
395
 
313
- const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
314
- const messages = [];
396
+ let session;
397
+ let messages;
398
+
399
+ if (options.session) {
400
+ session = await storesObj.sessionStore.getSession(options.session, 'cli-user');
401
+ if (!session) {
402
+ console.error(`Error: Session not found: ${options.session}`);
403
+ process.exit(1);
404
+ }
405
+ messages = [...(session.messages || [])];
406
+ } else {
407
+ session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
408
+ messages = [];
409
+ }
315
410
 
316
411
  const context = {
317
412
  userID: 'cli-user',
@@ -326,18 +421,58 @@ async function runRepl(options) {
326
421
  });
327
422
 
328
423
  console.log(`\ndotbot v${VERSION} — ${options.provider}/${options.model}`);
329
- console.log('Type /quit to exit, /clear to reset conversation\n');
424
+ if (options.session) {
425
+ console.log(`Resuming session: ${session.id}`);
426
+ }
427
+ console.log('Type /? for help\n');
428
+
429
+ const showHelp = () => {
430
+ console.log('Available Commands:');
431
+ console.log(' /show Show model information');
432
+ console.log(' /clear Clear session context');
433
+ console.log(' /bye Exit');
434
+ console.log(' /?, /help Help for a command');
435
+ console.log('');
436
+ console.log('Use """ to begin a multi-line message.\n');
437
+ };
438
+
439
+ const showModel = () => {
440
+ console.log(` Model: ${options.model}`);
441
+ console.log(` Provider: ${options.provider}`);
442
+ console.log(` Session: ${session.id}\n`);
443
+ };
330
444
 
331
- const prompt = () => {
332
- rl.question('> ', async (input) => {
445
+ const promptUser = () => {
446
+ rl.question('>>> ', async (input) => {
333
447
  const trimmed = input.trim();
334
448
 
335
449
  if (!trimmed) {
336
- prompt();
450
+ promptUser();
337
451
  return;
338
452
  }
339
453
 
340
- if (trimmed === '/quit' || trimmed === '/exit') {
454
+ // Multi-line input mode
455
+ if (trimmed === '"""') {
456
+ let multiLine = '';
457
+ const collectLines = () => {
458
+ rl.question('... ', (line) => {
459
+ if (line.trim() === '"""') {
460
+ if (multiLine.trim()) {
461
+ handleMessage(multiLine.trim());
462
+ } else {
463
+ promptUser();
464
+ }
465
+ } else {
466
+ multiLine += (multiLine ? '\n' : '') + line;
467
+ collectLines();
468
+ }
469
+ });
470
+ };
471
+ collectLines();
472
+ return;
473
+ }
474
+
475
+ if (trimmed === '/bye' || trimmed === '/quit' || trimmed === '/exit') {
341
476
  console.log('Goodbye!');
342
477
  rl.close();
343
478
  process.exit(0);
@@ -346,64 +481,94 @@ async function runRepl(options) {
346
481
  if (trimmed === '/clear') {
347
482
  messages.length = 0;
348
483
  console.log('Conversation cleared.\n');
349
- prompt();
484
+ promptUser();
485
+ return;
486
+ }
487
+
488
+ if (trimmed === '/?' || trimmed === '/help') {
489
+ showHelp();
490
+ promptUser();
350
491
  return;
351
492
  }
352
493
 
353
- messages.push({ role: 'user', content: trimmed });
494
+ if (trimmed === '/show') {
495
+ showModel();
496
+ promptUser();
497
+ return;
498
+ }
354
499
 
355
- process.stdout.write('\n[thinking] ');
356
- startSpinner();
357
- let assistantContent = '';
500
+ await handleMessage(trimmed);
501
+ });
502
+ };
358
503
 
359
- try {
360
- for await (const event of agentLoop({
361
- model: options.model,
362
- messages: [...messages],
363
- tools: coreTools,
364
- provider,
365
- context,
366
- })) {
367
- switch (event.type) {
368
- case 'thinking':
369
- // Already showing spinner, ignore thinking events
370
- break;
371
- case 'text_delta':
372
- stopSpinner(''); // Stop thinking spinner silently
504
+ const handleMessage = async (text) => {
505
+ messages.push({ role: 'user', content: text });
506
+
507
+ let isThinking = false;
508
+ let thinkingDone = false;
509
+ let assistantContent = '';
510
+
511
+ try {
512
+ for await (const event of agentLoop({
513
+ model: options.model,
514
+ messages: [...messages],
515
+ tools: coreTools,
516
+ provider,
517
+ context,
518
+ })) {
519
+ switch (event.type) {
520
+ case 'thinking':
521
+ if (!isThinking) {
522
+ process.stdout.write('Thinking...\n');
523
+ isThinking = true;
524
+ }
525
+ if (event.text) {
373
526
  process.stdout.write(event.text);
374
- assistantContent += event.text;
375
- break;
376
- case 'tool_start':
377
- stopSpinner(''); // Stop thinking spinner silently
378
- process.stdout.write(`[${event.name}] `);
379
- startSpinner();
380
- break;
381
- case 'tool_result':
382
- stopSpinner('done');
383
- break;
384
- case 'tool_error':
385
- stopSpinner('error');
386
- break;
387
- case 'error':
388
- stopSpinner();
389
- console.error(`\nError: ${event.error}`);
390
- break;
391
- }
527
+ }
528
+ break;
529
+ case 'text_delta':
530
+ if (isThinking && !thinkingDone) {
531
+ stopSpinner('');
532
+ process.stdout.write('\n...done thinking.\n\n');
533
+ thinkingDone = true;
534
+ }
535
+ process.stdout.write(event.text);
536
+ assistantContent += event.text;
537
+ break;
538
+ case 'tool_start':
539
+ if (isThinking && !thinkingDone) {
540
+ stopSpinner('');
541
+ process.stdout.write('\n...done thinking.\n\n');
542
+ thinkingDone = true;
543
+ }
544
+ process.stdout.write(`[${event.name}] `);
545
+ startSpinner();
546
+ break;
547
+ case 'tool_result':
548
+ stopSpinner('done');
549
+ break;
550
+ case 'tool_error':
551
+ stopSpinner('error');
552
+ break;
553
+ case 'error':
554
+ stopSpinner();
555
+ console.error(`\nError: ${event.error}`);
556
+ break;
392
557
  }
558
+ }
393
559
 
394
- if (assistantContent) {
395
- messages.push({ role: 'assistant', content: assistantContent });
396
- }
397
- } catch (err) {
398
- console.error(`\nError: ${err.message}`);
560
+ if (assistantContent) {
561
+ messages.push({ role: 'assistant', content: assistantContent });
399
562
  }
563
+ } catch (err) {
564
+ console.error(`\nError: ${err.message}`);
565
+ }
400
566
 
401
- process.stdout.write('\n\n');
402
- prompt();
403
- });
567
+ process.stdout.write('\n\n');
568
+ promptUser();
404
569
  };
405
570
 
406
- prompt();
571
+ promptUser();
407
572
  }
408
573
 
409
574
  /**
@@ -436,6 +601,140 @@ async function runServer(options) {
436
601
  return;
437
602
  }
438
603
 
604
+ // OpenAI-compatible endpoints (when --openai flag is set)
605
+ if (options.openai) {
606
+ // GET /v1/models - list available models
607
+ if (req.method === 'GET' && url.pathname === '/v1/models') {
608
+ const models = [
609
+ { id: 'grok-3', object: 'model', owned_by: 'xai' },
610
+ { id: 'grok-4-1-fast-reasoning', object: 'model', owned_by: 'xai' },
611
+ { id: 'claude-sonnet-4-5', object: 'model', owned_by: 'anthropic' },
612
+ { id: 'claude-opus-4', object: 'model', owned_by: 'anthropic' },
613
+ { id: 'gpt-4o', object: 'model', owned_by: 'openai' },
614
+ ];
615
+ res.writeHead(200, { 'Content-Type': 'application/json' });
616
+ res.end(JSON.stringify({ object: 'list', data: models }));
617
+ return;
618
+ }
619
+
620
+ // POST /v1/chat/completions - OpenAI-compatible chat endpoint
621
+ if (req.method === 'POST' && url.pathname === '/v1/chat/completions') {
622
+ let body = '';
623
+ for await (const chunk of req) body += chunk;
624
+
625
+ try {
626
+ const { model = 'grok-4-1-fast-reasoning', messages: reqMessages, stream = true } = JSON.parse(body);
627
+
628
+ if (!reqMessages || !Array.isArray(reqMessages)) {
629
+ res.writeHead(400, { 'Content-Type': 'application/json' });
630
+ res.end(JSON.stringify({ error: { message: 'messages array required', type: 'invalid_request_error' } }));
631
+ return;
632
+ }
633
+
634
+ // Determine provider from model name
635
+ let providerId = 'xai';
636
+ if (model.startsWith('claude')) providerId = 'anthropic';
637
+ else if (model.startsWith('gpt')) providerId = 'openai';
638
+ else if (model.startsWith('llama') || model.startsWith('mistral')) providerId = 'ollama';
639
+
640
+ const provider = await getProviderConfig(providerId);
641
+ const session = await storesObj.sessionStore.createSession('api-user', model, providerId);
642
+
643
+ const context = {
644
+ userID: 'api-user',
645
+ sessionId: session.id,
646
+ providers: { [providerId]: { apiKey: process.env[AI_PROVIDERS[providerId]?.envKey] } },
647
+ ...storesObj,
648
+ };
649
+
650
+ const completionId = `chatcmpl-${randomUUID()}`;
651
+ const created = Math.floor(Date.now() / 1000);
652
+
653
+ if (stream) {
654
+ res.writeHead(200, {
655
+ 'Content-Type': 'text/event-stream',
656
+ 'Cache-Control': 'no-cache',
657
+ 'Connection': 'keep-alive',
658
+ });
659
+
660
+ // Send initial role chunk
661
+ const roleChunk = {
662
+ id: completionId,
663
+ object: 'chat.completion.chunk',
664
+ created,
665
+ model,
666
+ choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
667
+ };
668
+ res.write(`data: ${JSON.stringify(roleChunk)}\n\n`);
669
+
670
+ for await (const event of agentLoop({
671
+ model,
672
+ messages: reqMessages,
673
+ tools: coreTools,
674
+ provider,
675
+ context,
676
+ })) {
677
+ if (event.type === 'text_delta') {
678
+ const chunk = {
679
+ id: completionId,
680
+ object: 'chat.completion.chunk',
681
+ created,
682
+ model,
683
+ choices: [{ index: 0, delta: { content: event.text }, finish_reason: null }],
684
+ };
685
+ res.write(`data: ${JSON.stringify(chunk)}\n\n`);
686
+ } else if (event.type === 'done') {
687
+ const finalChunk = {
688
+ id: completionId,
689
+ object: 'chat.completion.chunk',
690
+ created,
691
+ model,
692
+ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
693
+ };
694
+ res.write(`data: ${JSON.stringify(finalChunk)}\n\n`);
695
+ }
696
+ }
697
+
698
+ res.write('data: [DONE]\n\n');
699
+ res.end();
700
+ } else {
701
+ // Non-streaming response
702
+ let fullContent = '';
703
+ for await (const event of agentLoop({
704
+ model,
705
+ messages: reqMessages,
706
+ tools: coreTools,
707
+ provider,
708
+ context,
709
+ })) {
710
+ if (event.type === 'text_delta') {
711
+ fullContent += event.text;
712
+ }
713
+ }
714
+
715
+ const response = {
716
+ id: completionId,
717
+ object: 'chat.completion',
718
+ created,
719
+ model,
720
+ choices: [{
721
+ index: 0,
722
+ message: { role: 'assistant', content: fullContent },
723
+ finish_reason: 'stop',
724
+ }],
725
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
726
+ };
727
+ res.writeHead(200, { 'Content-Type': 'application/json' });
728
+ res.end(JSON.stringify(response));
729
+ }
730
+ } catch (err) {
731
+ res.writeHead(500, { 'Content-Type': 'application/json' });
732
+ res.end(JSON.stringify({ error: { message: err.message, type: 'server_error' } }));
733
+ }
734
+ return;
735
+ }
736
+ }
737
+
439
738
  // Chat endpoint
440
739
  if (req.method === 'POST' && url.pathname === '/chat') {
441
740
  let body = '';
@@ -503,15 +802,30 @@ async function runServer(options) {
503
802
  console.log(`Listening on http://localhost:${port}`);
504
803
  console.log(`\nEndpoints:`);
505
804
  console.log(` GET /health Health check`);
506
- console.log(` POST /chat Send message (SSE stream)\n`);
805
+ console.log(` POST /chat Send message (SSE stream)`);
806
+ if (options.openai) {
807
+ console.log(`\nOpenAI-compatible API:`);
808
+ console.log(` GET /v1/models List available models`);
809
+ console.log(` POST /v1/chat/completions Chat completions (SSE stream)`);
810
+ }
811
+ console.log();
507
812
  });
508
813
  }
509
814
 
510
815
  /**
511
816
  * List all available tools.
817
+ *
818
+ * @param {Object} options - CLI options
512
819
  */
513
- async function runTools() {
820
+ async function runTools(options) {
514
821
  await loadModules();
822
+
823
+ if (options.json) {
824
+ const toolList = coreTools.map((t) => ({ name: t.name, description: t.description }));
825
+ console.log(JSON.stringify(toolList));
826
+ return;
827
+ }
828
+
515
829
  console.log(`\ndotbot tools (${coreTools.length})\n`);
516
830
 
517
831
  // Group tools by category based on name prefix
@@ -539,27 +853,39 @@ async function runTools() {
539
853
  async function runStats(options) {
540
854
  const storesObj = await initStores(options.db, options.verbose, options.system);
541
855
 
542
- console.log(`\ndotbot stats\n`);
543
- console.log(` Database: ${options.db}`);
544
-
545
856
  // Sessions
546
857
  const sessions = await storesObj.sessionStore.listSessions('cli-user');
547
- console.log(` Sessions: ${sessions.length}`);
548
858
 
549
859
  // Memory
550
860
  const memories = await storesObj.memoryStore.getAllMemories('cli-user');
551
- console.log(` Memories: ${memories.length}`);
552
861
 
553
862
  // Jobs (need to get session IDs first)
554
863
  const jobs = await storesObj.cronStore.listTasksBySessionIds(['default'], 'cli-user');
555
- console.log(` Jobs: ${jobs.length}`);
556
864
 
557
865
  // Tasks
558
866
  const tasks = await storesObj.taskStore.getTasks('cli-user');
559
- console.log(` Tasks: ${tasks.length}`);
560
867
 
561
868
  // Triggers
562
869
  const triggers = await storesObj.triggerStore.listTriggers('cli-user');
870
+
871
+ if (options.json) {
872
+ console.log(JSON.stringify({
873
+ database: options.db,
874
+ sessions: sessions.length,
875
+ memories: memories.length,
876
+ jobs: jobs.length,
877
+ tasks: tasks.length,
878
+ triggers: triggers.length,
879
+ }));
880
+ return;
881
+ }
882
+
883
+ console.log(`\ndotbot stats\n`);
884
+ console.log(` Database: ${options.db}`);
885
+ console.log(` Sessions: ${sessions.length}`);
886
+ console.log(` Memories: ${memories.length}`);
887
+ console.log(` Jobs: ${jobs.length}`);
888
+ console.log(` Tasks: ${tasks.length}`);
563
889
  console.log(` Triggers: ${triggers.length}`);
564
890
 
565
891
  console.log();
@@ -569,14 +895,28 @@ async function runStats(options) {
569
895
  * Manage memories.
570
896
  *
571
897
  * @param {Object} options - CLI options
572
- * @param {string} subcommand - list or search
573
- * @param {string} query - Search query
898
+ * @param {string} subcommand - list, search, or delete
899
+ * @param {string} query - Search query or key to delete
574
900
  */
575
901
  async function runMemory(options, subcommand, query) {
576
902
  const storesObj = await initStores(options.db, options.verbose, options.system);
577
903
 
904
+ if (subcommand === 'delete' && query) {
905
+ const result = await storesObj.memoryStore.deleteMemory('cli-user', query);
906
+ if (options.json) {
907
+ console.log(JSON.stringify({ deleted: result, key: query }));
908
+ } else {
909
+ console.log(result ? `\nDeleted memory: ${query}\n` : `\nMemory not found: ${query}\n`);
910
+ }
911
+ return;
912
+ }
913
+
578
914
  if (subcommand === 'search' && query) {
579
915
  const results = await storesObj.memoryStore.readMemoryPattern('cli-user', `%${query}%`);
916
+ if (options.json) {
917
+ console.log(JSON.stringify(results));
918
+ return;
919
+ }
580
920
  console.log(`\nMemory search: "${query}" (${results.length} results)\n`);
581
921
  for (const mem of results) {
582
922
  const val = typeof mem.value === 'string' ? mem.value : JSON.stringify(mem.value);
@@ -584,6 +924,10 @@ async function runMemory(options, subcommand, query) {
584
924
  }
585
925
  } else {
586
926
  const memories = await storesObj.memoryStore.getAllMemories('cli-user');
927
+ if (options.json) {
928
+ console.log(JSON.stringify(memories));
929
+ return;
930
+ }
587
931
  console.log(`\nMemories (${memories.length})\n`);
588
932
  for (const mem of memories) {
589
933
  const val = typeof mem.value === 'string' ? mem.value : JSON.stringify(mem.value);
@@ -594,14 +938,32 @@ async function runMemory(options, subcommand, query) {
594
938
  }
595
939
 
596
940
  /**
597
- * List scheduled jobs.
941
+ * Manage scheduled jobs.
598
942
  *
599
943
  * @param {Object} options - CLI options
944
+ * @param {string} subcommand - list or delete
945
+ * @param {string} jobId - Job ID to delete
600
946
  */
601
- async function runJobs(options) {
947
+ async function runJobs(options, subcommand, jobId) {
602
948
  const storesObj = await initStores(options.db, options.verbose, options.system);
603
949
 
950
+ if (subcommand === 'delete' && jobId) {
951
+ const result = await storesObj.cronStore.deleteTask(jobId);
952
+ if (options.json) {
953
+ console.log(JSON.stringify({ deleted: result, id: jobId }));
954
+ } else {
955
+ console.log(result ? `\nDeleted job: ${jobId}\n` : `\nJob not found: ${jobId}\n`);
956
+ }
957
+ return;
958
+ }
959
+
604
960
  const jobs = await storesObj.cronStore.listTasksBySessionIds(['default'], 'cli-user');
961
+
962
+ if (options.json) {
963
+ console.log(JSON.stringify(jobs));
964
+ return;
965
+ }
966
+
605
967
  console.log(`\nScheduled jobs (${jobs.length})\n`);
606
968
 
607
969
  for (const job of jobs) {
@@ -617,14 +979,32 @@ async function runJobs(options) {
617
979
  }
618
980
 
619
981
  /**
620
- * List active tasks.
982
+ * Manage active tasks.
621
983
  *
622
984
  * @param {Object} options - CLI options
985
+ * @param {string} subcommand - list or delete
986
+ * @param {string} taskId - Task ID to delete
623
987
  */
624
- async function runTasks(options) {
988
+ async function runTasks(options, subcommand, taskId) {
625
989
  const storesObj = await initStores(options.db, options.verbose, options.system);
626
990
 
991
+ if (subcommand === 'delete' && taskId) {
992
+ const result = await storesObj.taskStore.deleteTask('cli-user', taskId);
993
+ if (options.json) {
994
+ console.log(JSON.stringify({ deleted: result, id: taskId }));
995
+ } else {
996
+ console.log(result ? `\nDeleted task: ${taskId}\n` : `\nTask not found: ${taskId}\n`);
997
+ }
998
+ return;
999
+ }
1000
+
627
1001
  const tasks = await storesObj.taskStore.getTasks('cli-user');
1002
+
1003
+ if (options.json) {
1004
+ console.log(JSON.stringify(tasks));
1005
+ return;
1006
+ }
1007
+
628
1008
  console.log(`\nTasks (${tasks.length})\n`);
629
1009
 
630
1010
  for (const task of tasks) {
@@ -639,14 +1019,32 @@ async function runTasks(options) {
639
1019
  }
640
1020
 
641
1021
  /**
642
- * List chat sessions.
1022
+ * Manage chat sessions.
643
1023
  *
644
1024
  * @param {Object} options - CLI options
1025
+ * @param {string} subcommand - list or delete
1026
+ * @param {string} sessionId - Session ID to delete
645
1027
  */
646
- async function runSessions(options) {
1028
+ async function runSessions(options, subcommand, sessionId) {
647
1029
  const storesObj = await initStores(options.db, options.verbose, options.system);
648
1030
 
1031
+ if (subcommand === 'delete' && sessionId) {
1032
+ const result = await storesObj.sessionStore.deleteSession(sessionId, 'cli-user');
1033
+ if (options.json) {
1034
+ console.log(JSON.stringify({ deleted: result, id: sessionId }));
1035
+ } else {
1036
+ console.log(result ? `\nDeleted session: ${sessionId}\n` : `\nSession not found: ${sessionId}\n`);
1037
+ }
1038
+ return;
1039
+ }
1040
+
649
1041
  const sessions = await storesObj.sessionStore.listSessions('cli-user');
1042
+
1043
+ if (options.json) {
1044
+ console.log(JSON.stringify(sessions));
1045
+ return;
1046
+ }
1047
+
650
1048
  console.log(`\nSessions (${sessions.length})\n`);
651
1049
 
652
1050
  for (const session of sessions) {
@@ -670,6 +1068,10 @@ async function runEvents(options) {
670
1068
 
671
1069
  if (options.summary) {
672
1070
  const summary = await storesObj.eventStore.summary({ userId: 'cli-user' });
1071
+ if (options.json) {
1072
+ console.log(JSON.stringify(summary));
1073
+ return;
1074
+ }
673
1075
  console.log(`\nEvent summary\n`);
674
1076
  console.log(` Total events: ${summary.total || 0}`);
675
1077
  if (summary.breakdown) {
@@ -679,6 +1081,10 @@ async function runEvents(options) {
679
1081
  }
680
1082
  } else {
681
1083
  const events = await storesObj.eventStore.query({ userId: 'cli-user', limit: 20 });
1084
+ if (options.json) {
1085
+ console.log(JSON.stringify(events));
1086
+ return;
1087
+ }
682
1088
  console.log(`\nRecent events (${events.length})\n`);
683
1089
 
684
1090
  for (const event of events) {
@@ -693,6 +1099,71 @@ async function runEvents(options) {
693
1099
  console.log();
694
1100
  }
695
1101
 
1102
+ /**
1103
+ * Check environment and configuration.
1104
+ *
1105
+ * @param {Object} options - CLI options
1106
+ */
1107
+ async function runDoctor(options) {
1108
+ console.log(`\ndotbot doctor\n`);
1109
+
1110
+ // Node.js version
1111
+ const nodeVersion = process.version;
1112
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
1113
+ const nodeOk = nodeMajor >= 22;
1114
+ console.log(` Node.js: ${nodeVersion} ${nodeOk ? '\u2713' : '\u2717 (requires >= 22.0.0)'}`);
1115
+
1116
+ // Database check
1117
+ const dbPath = options.db;
1118
+ let dbOk = false;
1119
+ try {
1120
+ if (existsSync(dbPath)) {
1121
+ // Try to open database
1122
+ const { DatabaseSync } = await import('node:sqlite');
1123
+ const db = new DatabaseSync(dbPath, { open: true });
1124
+ db.close();
1125
+ dbOk = true;
1126
+ } else {
1127
+ dbOk = true; // Will be created on first use
1128
+ }
1129
+ } catch {
1130
+ dbOk = false;
1131
+ }
1132
+ console.log(` Database: ${dbPath} ${dbOk ? '\u2713' : '\u2717 not accessible'}`);
1133
+
1134
+ // Config file check
1135
+ let configOk = false;
1136
+ let configMsg = '';
1137
+ if (existsSync(CONFIG_PATH)) {
1138
+ try {
1139
+ const content = readFileSync(CONFIG_PATH, 'utf8');
1140
+ JSON.parse(content);
1141
+ configOk = true;
1142
+ configMsg = `${CONFIG_PATH} \u2713`;
1143
+ } catch (err) {
1144
+ configMsg = `${CONFIG_PATH} \u2717 invalid JSON`;
1145
+ }
1146
+ } else {
1147
+ configMsg = `${CONFIG_PATH} (not found)`;
1148
+ }
1149
+ console.log(` Config: ${configMsg}`);
1150
+
1151
+ // API Keys
1152
+ console.log(`\n API Keys:`);
1153
+ const apiKeys = [
1154
+ { name: 'XAI_API_KEY', env: process.env.XAI_API_KEY },
1155
+ { name: 'ANTHROPIC_API_KEY', env: process.env.ANTHROPIC_API_KEY },
1156
+ { name: 'OPENAI_API_KEY', env: process.env.OPENAI_API_KEY },
1157
+ ];
1158
+
1159
+ for (const key of apiKeys) {
1160
+ const isSet = Boolean(key.env);
1161
+ console.log(` ${key.name}: ${isSet ? '\u2713 set' : '\u2717 not set'}`);
1162
+ }
1163
+
1164
+ console.log();
1165
+ }
1166
+
696
1167
  /**
697
1168
  * Main entry point.
698
1169
  */
@@ -711,12 +1182,28 @@ async function main() {
711
1182
 
712
1183
  const command = args.positionals[0];
713
1184
 
1185
+ // Handle piped input from stdin
1186
+ if (!process.stdin.isTTY && !command) {
1187
+ let input = '';
1188
+ for await (const chunk of process.stdin) {
1189
+ input += chunk;
1190
+ }
1191
+ const message = input.trim();
1192
+ if (message) {
1193
+ await runChat(message, args);
1194
+ return;
1195
+ }
1196
+ }
1197
+
714
1198
  switch (command) {
1199
+ case 'doctor':
1200
+ await runDoctor(args);
1201
+ break;
715
1202
  case 'serve':
716
1203
  await runServer(args);
717
1204
  break;
718
1205
  case 'tools':
719
- await runTools();
1206
+ await runTools(args);
720
1207
  break;
721
1208
  case 'stats':
722
1209
  await runStats(args);
@@ -725,13 +1212,13 @@ async function main() {
725
1212
  await runMemory(args, args.positionals[1], args.positionals.slice(2).join(' '));
726
1213
  break;
727
1214
  case 'jobs':
728
- await runJobs(args);
1215
+ await runJobs(args, args.positionals[1], args.positionals[2]);
729
1216
  break;
730
1217
  case 'tasks':
731
- await runTasks(args);
1218
+ await runTasks(args, args.positionals[1], args.positionals[2]);
732
1219
  break;
733
1220
  case 'sessions':
734
- await runSessions(args);
1221
+ await runSessions(args, args.positionals[1], args.positionals[2]);
735
1222
  break;
736
1223
  case 'events':
737
1224
  await runEvents(args);
package/dotbot.db CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevederico/dotbot",
3
- "version": "0.24.0",
3
+ "version": "0.26.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",