@stevederico/dotbot 0.24.0 → 0.25.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,13 @@
1
+ 0.25
2
+
3
+ Add delete subcommands for memory, jobs, tasks, sessions
4
+ Add --json flag for machine-readable output
5
+ Add doctor command for environment check
6
+ Add ~/.dotbotrc config file support
7
+ Add --openai flag for OpenAI-compatible API
8
+ Add pipe support (stdin input)
9
+ Add --session flag to resume conversations
10
+
1
11
  0.24
2
12
 
3
13
  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,8 +331,6 @@ async function runChat(message, options) {
260
331
  ...storesObj,
261
332
  };
262
333
 
263
- const messages = [{ role: 'user', content: message }];
264
-
265
334
  process.stdout.write('\n[thinking] ');
266
335
  startSpinner();
267
336
 
@@ -310,8 +379,20 @@ async function runRepl(options) {
310
379
  const storesObj = await initStores(options.db, options.verbose, options.system);
311
380
  const provider = await getProviderConfig(options.provider);
312
381
 
313
- const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
314
- const messages = [];
382
+ let session;
383
+ let messages;
384
+
385
+ if (options.session) {
386
+ session = await storesObj.sessionStore.getSession(options.session, 'cli-user');
387
+ if (!session) {
388
+ console.error(`Error: Session not found: ${options.session}`);
389
+ process.exit(1);
390
+ }
391
+ messages = [...(session.messages || [])];
392
+ } else {
393
+ session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
394
+ messages = [];
395
+ }
315
396
 
316
397
  const context = {
317
398
  userID: 'cli-user',
@@ -326,6 +407,9 @@ async function runRepl(options) {
326
407
  });
327
408
 
328
409
  console.log(`\ndotbot v${VERSION} — ${options.provider}/${options.model}`);
410
+ if (options.session) {
411
+ console.log(`Resuming session: ${session.id}`);
412
+ }
329
413
  console.log('Type /quit to exit, /clear to reset conversation\n');
330
414
 
331
415
  const prompt = () => {
@@ -436,6 +520,140 @@ async function runServer(options) {
436
520
  return;
437
521
  }
438
522
 
523
+ // OpenAI-compatible endpoints (when --openai flag is set)
524
+ if (options.openai) {
525
+ // GET /v1/models - list available models
526
+ if (req.method === 'GET' && url.pathname === '/v1/models') {
527
+ const models = [
528
+ { id: 'grok-3', object: 'model', owned_by: 'xai' },
529
+ { id: 'grok-4-1-fast-reasoning', object: 'model', owned_by: 'xai' },
530
+ { id: 'claude-sonnet-4-5', object: 'model', owned_by: 'anthropic' },
531
+ { id: 'claude-opus-4', object: 'model', owned_by: 'anthropic' },
532
+ { id: 'gpt-4o', object: 'model', owned_by: 'openai' },
533
+ ];
534
+ res.writeHead(200, { 'Content-Type': 'application/json' });
535
+ res.end(JSON.stringify({ object: 'list', data: models }));
536
+ return;
537
+ }
538
+
539
+ // POST /v1/chat/completions - OpenAI-compatible chat endpoint
540
+ if (req.method === 'POST' && url.pathname === '/v1/chat/completions') {
541
+ let body = '';
542
+ for await (const chunk of req) body += chunk;
543
+
544
+ try {
545
+ const { model = 'grok-4-1-fast-reasoning', messages: reqMessages, stream = true } = JSON.parse(body);
546
+
547
+ if (!reqMessages || !Array.isArray(reqMessages)) {
548
+ res.writeHead(400, { 'Content-Type': 'application/json' });
549
+ res.end(JSON.stringify({ error: { message: 'messages array required', type: 'invalid_request_error' } }));
550
+ return;
551
+ }
552
+
553
+ // Determine provider from model name
554
+ let providerId = 'xai';
555
+ if (model.startsWith('claude')) providerId = 'anthropic';
556
+ else if (model.startsWith('gpt')) providerId = 'openai';
557
+ else if (model.startsWith('llama') || model.startsWith('mistral')) providerId = 'ollama';
558
+
559
+ const provider = await getProviderConfig(providerId);
560
+ const session = await storesObj.sessionStore.createSession('api-user', model, providerId);
561
+
562
+ const context = {
563
+ userID: 'api-user',
564
+ sessionId: session.id,
565
+ providers: { [providerId]: { apiKey: process.env[AI_PROVIDERS[providerId]?.envKey] } },
566
+ ...storesObj,
567
+ };
568
+
569
+ const completionId = `chatcmpl-${randomUUID()}`;
570
+ const created = Math.floor(Date.now() / 1000);
571
+
572
+ if (stream) {
573
+ res.writeHead(200, {
574
+ 'Content-Type': 'text/event-stream',
575
+ 'Cache-Control': 'no-cache',
576
+ 'Connection': 'keep-alive',
577
+ });
578
+
579
+ // Send initial role chunk
580
+ const roleChunk = {
581
+ id: completionId,
582
+ object: 'chat.completion.chunk',
583
+ created,
584
+ model,
585
+ choices: [{ index: 0, delta: { role: 'assistant' }, finish_reason: null }],
586
+ };
587
+ res.write(`data: ${JSON.stringify(roleChunk)}\n\n`);
588
+
589
+ for await (const event of agentLoop({
590
+ model,
591
+ messages: reqMessages,
592
+ tools: coreTools,
593
+ provider,
594
+ context,
595
+ })) {
596
+ if (event.type === 'text_delta') {
597
+ const chunk = {
598
+ id: completionId,
599
+ object: 'chat.completion.chunk',
600
+ created,
601
+ model,
602
+ choices: [{ index: 0, delta: { content: event.text }, finish_reason: null }],
603
+ };
604
+ res.write(`data: ${JSON.stringify(chunk)}\n\n`);
605
+ } else if (event.type === 'done') {
606
+ const finalChunk = {
607
+ id: completionId,
608
+ object: 'chat.completion.chunk',
609
+ created,
610
+ model,
611
+ choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
612
+ };
613
+ res.write(`data: ${JSON.stringify(finalChunk)}\n\n`);
614
+ }
615
+ }
616
+
617
+ res.write('data: [DONE]\n\n');
618
+ res.end();
619
+ } else {
620
+ // Non-streaming response
621
+ let fullContent = '';
622
+ for await (const event of agentLoop({
623
+ model,
624
+ messages: reqMessages,
625
+ tools: coreTools,
626
+ provider,
627
+ context,
628
+ })) {
629
+ if (event.type === 'text_delta') {
630
+ fullContent += event.text;
631
+ }
632
+ }
633
+
634
+ const response = {
635
+ id: completionId,
636
+ object: 'chat.completion',
637
+ created,
638
+ model,
639
+ choices: [{
640
+ index: 0,
641
+ message: { role: 'assistant', content: fullContent },
642
+ finish_reason: 'stop',
643
+ }],
644
+ usage: { prompt_tokens: 0, completion_tokens: 0, total_tokens: 0 },
645
+ };
646
+ res.writeHead(200, { 'Content-Type': 'application/json' });
647
+ res.end(JSON.stringify(response));
648
+ }
649
+ } catch (err) {
650
+ res.writeHead(500, { 'Content-Type': 'application/json' });
651
+ res.end(JSON.stringify({ error: { message: err.message, type: 'server_error' } }));
652
+ }
653
+ return;
654
+ }
655
+ }
656
+
439
657
  // Chat endpoint
440
658
  if (req.method === 'POST' && url.pathname === '/chat') {
441
659
  let body = '';
@@ -503,15 +721,30 @@ async function runServer(options) {
503
721
  console.log(`Listening on http://localhost:${port}`);
504
722
  console.log(`\nEndpoints:`);
505
723
  console.log(` GET /health Health check`);
506
- console.log(` POST /chat Send message (SSE stream)\n`);
724
+ console.log(` POST /chat Send message (SSE stream)`);
725
+ if (options.openai) {
726
+ console.log(`\nOpenAI-compatible API:`);
727
+ console.log(` GET /v1/models List available models`);
728
+ console.log(` POST /v1/chat/completions Chat completions (SSE stream)`);
729
+ }
730
+ console.log();
507
731
  });
508
732
  }
509
733
 
510
734
  /**
511
735
  * List all available tools.
736
+ *
737
+ * @param {Object} options - CLI options
512
738
  */
513
- async function runTools() {
739
+ async function runTools(options) {
514
740
  await loadModules();
741
+
742
+ if (options.json) {
743
+ const toolList = coreTools.map((t) => ({ name: t.name, description: t.description }));
744
+ console.log(JSON.stringify(toolList));
745
+ return;
746
+ }
747
+
515
748
  console.log(`\ndotbot tools (${coreTools.length})\n`);
516
749
 
517
750
  // Group tools by category based on name prefix
@@ -539,27 +772,39 @@ async function runTools() {
539
772
  async function runStats(options) {
540
773
  const storesObj = await initStores(options.db, options.verbose, options.system);
541
774
 
542
- console.log(`\ndotbot stats\n`);
543
- console.log(` Database: ${options.db}`);
544
-
545
775
  // Sessions
546
776
  const sessions = await storesObj.sessionStore.listSessions('cli-user');
547
- console.log(` Sessions: ${sessions.length}`);
548
777
 
549
778
  // Memory
550
779
  const memories = await storesObj.memoryStore.getAllMemories('cli-user');
551
- console.log(` Memories: ${memories.length}`);
552
780
 
553
781
  // Jobs (need to get session IDs first)
554
782
  const jobs = await storesObj.cronStore.listTasksBySessionIds(['default'], 'cli-user');
555
- console.log(` Jobs: ${jobs.length}`);
556
783
 
557
784
  // Tasks
558
785
  const tasks = await storesObj.taskStore.getTasks('cli-user');
559
- console.log(` Tasks: ${tasks.length}`);
560
786
 
561
787
  // Triggers
562
788
  const triggers = await storesObj.triggerStore.listTriggers('cli-user');
789
+
790
+ if (options.json) {
791
+ console.log(JSON.stringify({
792
+ database: options.db,
793
+ sessions: sessions.length,
794
+ memories: memories.length,
795
+ jobs: jobs.length,
796
+ tasks: tasks.length,
797
+ triggers: triggers.length,
798
+ }));
799
+ return;
800
+ }
801
+
802
+ console.log(`\ndotbot stats\n`);
803
+ console.log(` Database: ${options.db}`);
804
+ console.log(` Sessions: ${sessions.length}`);
805
+ console.log(` Memories: ${memories.length}`);
806
+ console.log(` Jobs: ${jobs.length}`);
807
+ console.log(` Tasks: ${tasks.length}`);
563
808
  console.log(` Triggers: ${triggers.length}`);
564
809
 
565
810
  console.log();
@@ -569,14 +814,28 @@ async function runStats(options) {
569
814
  * Manage memories.
570
815
  *
571
816
  * @param {Object} options - CLI options
572
- * @param {string} subcommand - list or search
573
- * @param {string} query - Search query
817
+ * @param {string} subcommand - list, search, or delete
818
+ * @param {string} query - Search query or key to delete
574
819
  */
575
820
  async function runMemory(options, subcommand, query) {
576
821
  const storesObj = await initStores(options.db, options.verbose, options.system);
577
822
 
823
+ if (subcommand === 'delete' && query) {
824
+ const result = await storesObj.memoryStore.deleteMemory('cli-user', query);
825
+ if (options.json) {
826
+ console.log(JSON.stringify({ deleted: result, key: query }));
827
+ } else {
828
+ console.log(result ? `\nDeleted memory: ${query}\n` : `\nMemory not found: ${query}\n`);
829
+ }
830
+ return;
831
+ }
832
+
578
833
  if (subcommand === 'search' && query) {
579
834
  const results = await storesObj.memoryStore.readMemoryPattern('cli-user', `%${query}%`);
835
+ if (options.json) {
836
+ console.log(JSON.stringify(results));
837
+ return;
838
+ }
580
839
  console.log(`\nMemory search: "${query}" (${results.length} results)\n`);
581
840
  for (const mem of results) {
582
841
  const val = typeof mem.value === 'string' ? mem.value : JSON.stringify(mem.value);
@@ -584,6 +843,10 @@ async function runMemory(options, subcommand, query) {
584
843
  }
585
844
  } else {
586
845
  const memories = await storesObj.memoryStore.getAllMemories('cli-user');
846
+ if (options.json) {
847
+ console.log(JSON.stringify(memories));
848
+ return;
849
+ }
587
850
  console.log(`\nMemories (${memories.length})\n`);
588
851
  for (const mem of memories) {
589
852
  const val = typeof mem.value === 'string' ? mem.value : JSON.stringify(mem.value);
@@ -594,14 +857,32 @@ async function runMemory(options, subcommand, query) {
594
857
  }
595
858
 
596
859
  /**
597
- * List scheduled jobs.
860
+ * Manage scheduled jobs.
598
861
  *
599
862
  * @param {Object} options - CLI options
863
+ * @param {string} subcommand - list or delete
864
+ * @param {string} jobId - Job ID to delete
600
865
  */
601
- async function runJobs(options) {
866
+ async function runJobs(options, subcommand, jobId) {
602
867
  const storesObj = await initStores(options.db, options.verbose, options.system);
603
868
 
869
+ if (subcommand === 'delete' && jobId) {
870
+ const result = await storesObj.cronStore.deleteTask(jobId);
871
+ if (options.json) {
872
+ console.log(JSON.stringify({ deleted: result, id: jobId }));
873
+ } else {
874
+ console.log(result ? `\nDeleted job: ${jobId}\n` : `\nJob not found: ${jobId}\n`);
875
+ }
876
+ return;
877
+ }
878
+
604
879
  const jobs = await storesObj.cronStore.listTasksBySessionIds(['default'], 'cli-user');
880
+
881
+ if (options.json) {
882
+ console.log(JSON.stringify(jobs));
883
+ return;
884
+ }
885
+
605
886
  console.log(`\nScheduled jobs (${jobs.length})\n`);
606
887
 
607
888
  for (const job of jobs) {
@@ -617,14 +898,32 @@ async function runJobs(options) {
617
898
  }
618
899
 
619
900
  /**
620
- * List active tasks.
901
+ * Manage active tasks.
621
902
  *
622
903
  * @param {Object} options - CLI options
904
+ * @param {string} subcommand - list or delete
905
+ * @param {string} taskId - Task ID to delete
623
906
  */
624
- async function runTasks(options) {
907
+ async function runTasks(options, subcommand, taskId) {
625
908
  const storesObj = await initStores(options.db, options.verbose, options.system);
626
909
 
910
+ if (subcommand === 'delete' && taskId) {
911
+ const result = await storesObj.taskStore.deleteTask('cli-user', taskId);
912
+ if (options.json) {
913
+ console.log(JSON.stringify({ deleted: result, id: taskId }));
914
+ } else {
915
+ console.log(result ? `\nDeleted task: ${taskId}\n` : `\nTask not found: ${taskId}\n`);
916
+ }
917
+ return;
918
+ }
919
+
627
920
  const tasks = await storesObj.taskStore.getTasks('cli-user');
921
+
922
+ if (options.json) {
923
+ console.log(JSON.stringify(tasks));
924
+ return;
925
+ }
926
+
628
927
  console.log(`\nTasks (${tasks.length})\n`);
629
928
 
630
929
  for (const task of tasks) {
@@ -639,14 +938,32 @@ async function runTasks(options) {
639
938
  }
640
939
 
641
940
  /**
642
- * List chat sessions.
941
+ * Manage chat sessions.
643
942
  *
644
943
  * @param {Object} options - CLI options
944
+ * @param {string} subcommand - list or delete
945
+ * @param {string} sessionId - Session ID to delete
645
946
  */
646
- async function runSessions(options) {
947
+ async function runSessions(options, subcommand, sessionId) {
647
948
  const storesObj = await initStores(options.db, options.verbose, options.system);
648
949
 
950
+ if (subcommand === 'delete' && sessionId) {
951
+ const result = await storesObj.sessionStore.deleteSession(sessionId, 'cli-user');
952
+ if (options.json) {
953
+ console.log(JSON.stringify({ deleted: result, id: sessionId }));
954
+ } else {
955
+ console.log(result ? `\nDeleted session: ${sessionId}\n` : `\nSession not found: ${sessionId}\n`);
956
+ }
957
+ return;
958
+ }
959
+
649
960
  const sessions = await storesObj.sessionStore.listSessions('cli-user');
961
+
962
+ if (options.json) {
963
+ console.log(JSON.stringify(sessions));
964
+ return;
965
+ }
966
+
650
967
  console.log(`\nSessions (${sessions.length})\n`);
651
968
 
652
969
  for (const session of sessions) {
@@ -670,6 +987,10 @@ async function runEvents(options) {
670
987
 
671
988
  if (options.summary) {
672
989
  const summary = await storesObj.eventStore.summary({ userId: 'cli-user' });
990
+ if (options.json) {
991
+ console.log(JSON.stringify(summary));
992
+ return;
993
+ }
673
994
  console.log(`\nEvent summary\n`);
674
995
  console.log(` Total events: ${summary.total || 0}`);
675
996
  if (summary.breakdown) {
@@ -679,6 +1000,10 @@ async function runEvents(options) {
679
1000
  }
680
1001
  } else {
681
1002
  const events = await storesObj.eventStore.query({ userId: 'cli-user', limit: 20 });
1003
+ if (options.json) {
1004
+ console.log(JSON.stringify(events));
1005
+ return;
1006
+ }
682
1007
  console.log(`\nRecent events (${events.length})\n`);
683
1008
 
684
1009
  for (const event of events) {
@@ -693,6 +1018,71 @@ async function runEvents(options) {
693
1018
  console.log();
694
1019
  }
695
1020
 
1021
+ /**
1022
+ * Check environment and configuration.
1023
+ *
1024
+ * @param {Object} options - CLI options
1025
+ */
1026
+ async function runDoctor(options) {
1027
+ console.log(`\ndotbot doctor\n`);
1028
+
1029
+ // Node.js version
1030
+ const nodeVersion = process.version;
1031
+ const nodeMajor = parseInt(nodeVersion.slice(1).split('.')[0], 10);
1032
+ const nodeOk = nodeMajor >= 22;
1033
+ console.log(` Node.js: ${nodeVersion} ${nodeOk ? '\u2713' : '\u2717 (requires >= 22.0.0)'}`);
1034
+
1035
+ // Database check
1036
+ const dbPath = options.db;
1037
+ let dbOk = false;
1038
+ try {
1039
+ if (existsSync(dbPath)) {
1040
+ // Try to open database
1041
+ const { DatabaseSync } = await import('node:sqlite');
1042
+ const db = new DatabaseSync(dbPath, { open: true });
1043
+ db.close();
1044
+ dbOk = true;
1045
+ } else {
1046
+ dbOk = true; // Will be created on first use
1047
+ }
1048
+ } catch {
1049
+ dbOk = false;
1050
+ }
1051
+ console.log(` Database: ${dbPath} ${dbOk ? '\u2713' : '\u2717 not accessible'}`);
1052
+
1053
+ // Config file check
1054
+ let configOk = false;
1055
+ let configMsg = '';
1056
+ if (existsSync(CONFIG_PATH)) {
1057
+ try {
1058
+ const content = readFileSync(CONFIG_PATH, 'utf8');
1059
+ JSON.parse(content);
1060
+ configOk = true;
1061
+ configMsg = `${CONFIG_PATH} \u2713`;
1062
+ } catch (err) {
1063
+ configMsg = `${CONFIG_PATH} \u2717 invalid JSON`;
1064
+ }
1065
+ } else {
1066
+ configMsg = `${CONFIG_PATH} (not found)`;
1067
+ }
1068
+ console.log(` Config: ${configMsg}`);
1069
+
1070
+ // API Keys
1071
+ console.log(`\n API Keys:`);
1072
+ const apiKeys = [
1073
+ { name: 'XAI_API_KEY', env: process.env.XAI_API_KEY },
1074
+ { name: 'ANTHROPIC_API_KEY', env: process.env.ANTHROPIC_API_KEY },
1075
+ { name: 'OPENAI_API_KEY', env: process.env.OPENAI_API_KEY },
1076
+ ];
1077
+
1078
+ for (const key of apiKeys) {
1079
+ const isSet = Boolean(key.env);
1080
+ console.log(` ${key.name}: ${isSet ? '\u2713 set' : '\u2717 not set'}`);
1081
+ }
1082
+
1083
+ console.log();
1084
+ }
1085
+
696
1086
  /**
697
1087
  * Main entry point.
698
1088
  */
@@ -711,12 +1101,28 @@ async function main() {
711
1101
 
712
1102
  const command = args.positionals[0];
713
1103
 
1104
+ // Handle piped input from stdin
1105
+ if (!process.stdin.isTTY && !command) {
1106
+ let input = '';
1107
+ for await (const chunk of process.stdin) {
1108
+ input += chunk;
1109
+ }
1110
+ const message = input.trim();
1111
+ if (message) {
1112
+ await runChat(message, args);
1113
+ return;
1114
+ }
1115
+ }
1116
+
714
1117
  switch (command) {
1118
+ case 'doctor':
1119
+ await runDoctor(args);
1120
+ break;
715
1121
  case 'serve':
716
1122
  await runServer(args);
717
1123
  break;
718
1124
  case 'tools':
719
- await runTools();
1125
+ await runTools(args);
720
1126
  break;
721
1127
  case 'stats':
722
1128
  await runStats(args);
@@ -725,13 +1131,13 @@ async function main() {
725
1131
  await runMemory(args, args.positionals[1], args.positionals.slice(2).join(' '));
726
1132
  break;
727
1133
  case 'jobs':
728
- await runJobs(args);
1134
+ await runJobs(args, args.positionals[1], args.positionals[2]);
729
1135
  break;
730
1136
  case 'tasks':
731
- await runTasks(args);
1137
+ await runTasks(args, args.positionals[1], args.positionals[2]);
732
1138
  break;
733
1139
  case 'sessions':
734
- await runSessions(args);
1140
+ await runSessions(args, args.positionals[1], args.positionals[2]);
735
1141
  break;
736
1142
  case 'events':
737
1143
  await runEvents(args);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevederico/dotbot",
3
- "version": "0.24.0",
3
+ "version": "0.25.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",