@stevederico/dotbot 0.23.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,24 @@
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
+
11
+ 0.24
12
+
13
+ Add --system flag for custom prompts
14
+ Add tools command
15
+ Add stats command
16
+ Add memory command
17
+ Add jobs command
18
+ Add tasks command
19
+ Add sessions command
20
+ Add events command
21
+
1
22
  0.23
2
23
 
3
24
  Fix no-args launches interactive
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  <h1 align="center" style="border-bottom: none; margin-bottom: 0;">dotbot</h1>
4
4
  <h3 align="center" style="margin-top: 0; font-weight: normal;">
5
5
  The ultra-lean AI agent.<br>
6
- 11k lines. 47 tools. 0 dependencies.
6
+ 11k lines. 53 tools. 0 dependencies.
7
7
  </h3>
8
8
  <p align="center">
9
9
  <a href="https://opensource.org/licenses/mit">
@@ -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.20-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>
@@ -28,7 +28,7 @@
28
28
  | | dotbot | nanobot | OpenClaw |
29
29
  |---|:---:|:---:|:---:|
30
30
  | **Lines of Code** | **11k** | 22k | 1M+ |
31
- | **Tools** | **47** | ~10 | ~50 |
31
+ | **Tools** | **53** | ~10 | ~50 |
32
32
  | **Dependencies** | Minimal | Heavy | Heavy |
33
33
 
34
34
  Everything you need for AI agents. Nothing you don't. No bloated abstractions. No dependency hell. Just a clean, focused agent that works.
@@ -42,8 +42,9 @@ A **streaming AI agent** with tool execution, autonomous tasks, and scheduled jo
42
42
  **As a CLI:**
43
43
  ```bash
44
44
  dotbot "What's the weather in San Francisco?"
45
- dotbot repl
45
+ dotbot # Interactive mode
46
46
  dotbot serve --port 3000
47
+ dotbot tools # List all 53 tools
47
48
  ```
48
49
 
49
50
  **As a library:**
@@ -67,11 +68,16 @@ export XAI_API_KEY=xai-...
67
68
  # Chat
68
69
  dotbot "Summarize the top 3 AI news stories today"
69
70
 
70
- # Interactive REPL
71
- dotbot repl
71
+ # Interactive mode
72
+ dotbot
72
73
 
73
74
  # Start HTTP server
74
75
  dotbot serve --port 3000
76
+
77
+ # Inspect data
78
+ dotbot tools
79
+ dotbot stats
80
+ dotbot memory
75
81
  ```
76
82
 
77
83
  ### Library Usage
@@ -116,7 +122,7 @@ for await (const event of agent.chat({
116
122
  - **Abort support** via AbortSignal
117
123
  - **Automatic retries** with provider failover
118
124
 
119
- ### 🔧 **47 Built-in Tools**
125
+ ### 🔧 **53 Built-in Tools**
120
126
  - **Memory** — save, search, update, delete long-term memory
121
127
  - **Web** — search, fetch, browser automation with Playwright
122
128
  - **Files** — read, write, list, delete, move files
@@ -148,18 +154,39 @@ for await (const event of agent.chat({
148
154
  ## CLI Reference
149
155
 
150
156
  ```
151
- dotbot v0.19 — AI agent CLI
157
+ dotbot v0.25 — AI agent CLI
152
158
 
153
159
  Usage:
154
- dotbot "message" Send a message (default)
155
- dotbot repl Interactive chat session
160
+ dotbot "message" One-shot query
161
+ dotbot Interactive chat
156
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
165
+
166
+ Commands:
167
+ doctor Check environment and configuration
168
+ tools List all available tools
169
+ stats Show database statistics
170
+ memory [list|search <q>] Manage saved memories
171
+ memory delete <key> Delete a memory by key
172
+ jobs List scheduled jobs
173
+ jobs delete <id> Delete a scheduled job
174
+ tasks List active tasks
175
+ tasks delete <id> Delete a task
176
+ sessions List chat sessions
177
+ sessions delete <id> Delete a session
178
+ events [--summary] View audit log
157
179
 
158
180
  Options:
159
181
  --provider, -p AI provider: xai, anthropic, openai, ollama (default: xai)
160
182
  --model, -m Model name (default: grok-4-1-fast-reasoning)
183
+ --system, -s Custom system prompt (prepended to default)
184
+ --session Resume a specific session by ID
161
185
  --db SQLite database path (default: ./dotbot.db)
162
186
  --port Server port for 'serve' command
187
+ --openai Enable OpenAI-compatible API endpoints
188
+ --json Output as JSON (for inspection commands)
189
+ --verbose Show initialization logs
163
190
  --help, -h Show help
164
191
  --version, -v Show version
165
192
 
@@ -168,6 +195,9 @@ Environment Variables:
168
195
  ANTHROPIC_API_KEY API key for Anthropic
169
196
  OPENAI_API_KEY API key for OpenAI
170
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)
171
201
  ```
172
202
 
173
203
  <br />
@@ -230,7 +260,7 @@ for await (const event of agent.chat({
230
260
 
231
261
  <br />
232
262
 
233
- ## Built-in Tools (47)
263
+ ## Built-in Tools (53)
234
264
 
235
265
  | Category | Tools |
236
266
  |----------|-------|
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,12 +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
101
+
102
+ Commands:
103
+ doctor Check environment and configuration
104
+ tools List all available tools
105
+ stats Show database statistics
106
+ memory [list|search <q>] Manage saved memories
107
+ memory delete <key> Delete a memory by key
108
+ jobs List scheduled jobs
109
+ jobs delete <id> Delete a scheduled job
110
+ tasks List active tasks
111
+ tasks delete <id> Delete a task
112
+ sessions List chat sessions
113
+ sessions delete <id> Delete a session
114
+ events [--summary] View audit log
96
115
 
97
116
  Options:
98
117
  --provider, -p AI provider: xai, anthropic, openai, ollama (default: xai)
99
118
  --model, -m Model name (default: grok-4-1-fast-reasoning)
119
+ --system, -s Custom system prompt (prepended to default)
120
+ --session Resume a specific session by ID
100
121
  --db SQLite database path (default: ${DEFAULT_DB})
101
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)
102
125
  --verbose Show initialization logs
103
126
  --help, -h Show this help
104
127
  --version, -v Show version
@@ -109,17 +132,51 @@ Environment Variables:
109
132
  OPENAI_API_KEY API key for OpenAI
110
133
  OLLAMA_BASE_URL Base URL for Ollama (default: http://localhost:11434)
111
134
 
135
+ Config File:
136
+ ~/.dotbotrc JSON config for defaults (provider, model, db)
137
+
112
138
  Examples:
113
139
  dotbot "What's the weather in SF?"
114
140
  dotbot
115
141
  dotbot serve --port 8080
142
+ dotbot doctor
143
+ dotbot tools
144
+ dotbot memory search "preferences"
145
+ dotbot memory delete user_pref
146
+ dotbot stats --json
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
116
151
  `);
117
152
  }
118
153
 
119
154
  /**
120
- * 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
121
176
  */
122
177
  function parseCliArgs() {
178
+ const config = loadConfig();
179
+
123
180
  try {
124
181
  const { values, positionals } = parseArgs({
125
182
  allowPositionals: true,
@@ -127,13 +184,29 @@ function parseCliArgs() {
127
184
  help: { type: 'boolean', short: 'h', default: false },
128
185
  version: { type: 'boolean', short: 'v', default: false },
129
186
  verbose: { type: 'boolean', default: false },
130
- provider: { type: 'string', short: 'p', default: 'xai' },
131
- model: { type: 'string', short: 'm', default: 'grok-4-1-fast-reasoning' },
132
- db: { type: 'string', default: DEFAULT_DB },
133
- port: { type: 'string', default: String(DEFAULT_PORT) },
187
+ provider: { type: 'string', short: 'p' },
188
+ model: { type: 'string', short: 'm' },
189
+ system: { type: 'string', short: 's' },
190
+ summary: { type: 'boolean', default: false },
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: '' },
134
196
  },
135
197
  });
136
- 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
+ };
137
210
  } catch (err) {
138
211
  console.error(`Error: ${err.message}`);
139
212
  process.exit(1);
@@ -179,20 +252,30 @@ async function getProviderConfig(providerId) {
179
252
  *
180
253
  * @param {string} dbPath - Path to SQLite database
181
254
  * @param {boolean} verbose - Show initialization logs
255
+ * @param {string} customSystemPrompt - Custom system prompt to prepend
182
256
  * @returns {Promise<Object>} Initialized stores
183
257
  */
184
- async function initStores(dbPath, verbose = false) {
258
+ async function initStores(dbPath, verbose = false, customSystemPrompt = '') {
185
259
  await loadModules();
186
260
 
261
+ // Import defaultSystemPrompt for custom builder
262
+ const { defaultSystemPrompt } = await import('../storage/SQLiteAdapter.js');
263
+
187
264
  // Suppress init logs unless verbose
188
265
  const originalLog = console.log;
189
266
  if (!verbose) {
190
267
  console.log = () => {};
191
268
  }
192
269
 
270
+ // Build custom systemPromptBuilder that prepends user's text
271
+ const systemPromptBuilder = customSystemPrompt
272
+ ? (prefs) => `${customSystemPrompt}\n\n${defaultSystemPrompt(prefs)}`
273
+ : undefined;
274
+
193
275
  const sessionStore = new stores.SQLiteSessionStore();
194
276
  await sessionStore.init(dbPath, {
195
277
  prefsFetcher: async () => ({ agentName: 'Dotbot', agentPersonality: '' }),
278
+ ...(systemPromptBuilder && { systemPromptBuilder }),
196
279
  });
197
280
 
198
281
  const cronStore = new stores.SQLiteCronStore();
@@ -223,10 +306,23 @@ async function initStores(dbPath, verbose = false) {
223
306
  * @param {Object} options - CLI options
224
307
  */
225
308
  async function runChat(message, options) {
226
- const storesObj = await initStores(options.db, options.verbose);
309
+ const storesObj = await initStores(options.db, options.verbose, options.system);
227
310
  const provider = await getProviderConfig(options.provider);
228
311
 
229
- 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
+ }
230
326
 
231
327
  const context = {
232
328
  userID: 'cli-user',
@@ -235,8 +331,6 @@ async function runChat(message, options) {
235
331
  ...storesObj,
236
332
  };
237
333
 
238
- const messages = [{ role: 'user', content: message }];
239
-
240
334
  process.stdout.write('\n[thinking] ');
241
335
  startSpinner();
242
336
 
@@ -282,11 +376,23 @@ async function runChat(message, options) {
282
376
  * @param {Object} options - CLI options
283
377
  */
284
378
  async function runRepl(options) {
285
- const storesObj = await initStores(options.db, options.verbose);
379
+ const storesObj = await initStores(options.db, options.verbose, options.system);
286
380
  const provider = await getProviderConfig(options.provider);
287
381
 
288
- const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
289
- 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
+ }
290
396
 
291
397
  const context = {
292
398
  userID: 'cli-user',
@@ -301,6 +407,9 @@ async function runRepl(options) {
301
407
  });
302
408
 
303
409
  console.log(`\ndotbot v${VERSION} — ${options.provider}/${options.model}`);
410
+ if (options.session) {
411
+ console.log(`Resuming session: ${session.id}`);
412
+ }
304
413
  console.log('Type /quit to exit, /clear to reset conversation\n');
305
414
 
306
415
  const prompt = () => {
@@ -388,7 +497,7 @@ async function runRepl(options) {
388
497
  */
389
498
  async function runServer(options) {
390
499
  const port = parseInt(options.port, 10);
391
- const storesObj = await initStores(options.db, options.verbose);
500
+ const storesObj = await initStores(options.db, options.verbose, options.system);
392
501
 
393
502
  const server = createServer(async (req, res) => {
394
503
  // CORS headers
@@ -411,6 +520,140 @@ async function runServer(options) {
411
520
  return;
412
521
  }
413
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
+
414
657
  // Chat endpoint
415
658
  if (req.method === 'POST' && url.pathname === '/chat') {
416
659
  let body = '';
@@ -478,10 +721,368 @@ async function runServer(options) {
478
721
  console.log(`Listening on http://localhost:${port}`);
479
722
  console.log(`\nEndpoints:`);
480
723
  console.log(` GET /health Health check`);
481
- 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();
482
731
  });
483
732
  }
484
733
 
734
+ /**
735
+ * List all available tools.
736
+ *
737
+ * @param {Object} options - CLI options
738
+ */
739
+ async function runTools(options) {
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
+
748
+ console.log(`\ndotbot tools (${coreTools.length})\n`);
749
+
750
+ // Group tools by category based on name prefix
751
+ const categories = {};
752
+ for (const tool of coreTools) {
753
+ const prefix = tool.name.split('_')[0];
754
+ if (!categories[prefix]) categories[prefix] = [];
755
+ categories[prefix].push(tool.name);
756
+ }
757
+
758
+ for (const [category, tools] of Object.entries(categories).sort()) {
759
+ console.log(` ${category} (${tools.length})`);
760
+ for (const name of tools.sort()) {
761
+ console.log(` ${name}`);
762
+ }
763
+ }
764
+ console.log();
765
+ }
766
+
767
+ /**
768
+ * Show database statistics.
769
+ *
770
+ * @param {Object} options - CLI options
771
+ */
772
+ async function runStats(options) {
773
+ const storesObj = await initStores(options.db, options.verbose, options.system);
774
+
775
+ // Sessions
776
+ const sessions = await storesObj.sessionStore.listSessions('cli-user');
777
+
778
+ // Memory
779
+ const memories = await storesObj.memoryStore.getAllMemories('cli-user');
780
+
781
+ // Jobs (need to get session IDs first)
782
+ const jobs = await storesObj.cronStore.listTasksBySessionIds(['default'], 'cli-user');
783
+
784
+ // Tasks
785
+ const tasks = await storesObj.taskStore.getTasks('cli-user');
786
+
787
+ // Triggers
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}`);
808
+ console.log(` Triggers: ${triggers.length}`);
809
+
810
+ console.log();
811
+ }
812
+
813
+ /**
814
+ * Manage memories.
815
+ *
816
+ * @param {Object} options - CLI options
817
+ * @param {string} subcommand - list, search, or delete
818
+ * @param {string} query - Search query or key to delete
819
+ */
820
+ async function runMemory(options, subcommand, query) {
821
+ const storesObj = await initStores(options.db, options.verbose, options.system);
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
+
833
+ if (subcommand === 'search' && query) {
834
+ const results = await storesObj.memoryStore.readMemoryPattern('cli-user', `%${query}%`);
835
+ if (options.json) {
836
+ console.log(JSON.stringify(results));
837
+ return;
838
+ }
839
+ console.log(`\nMemory search: "${query}" (${results.length} results)\n`);
840
+ for (const mem of results) {
841
+ const val = typeof mem.value === 'string' ? mem.value : JSON.stringify(mem.value);
842
+ console.log(` [${mem.key}] ${val.substring(0, 60)}${val.length > 60 ? '...' : ''}`);
843
+ }
844
+ } else {
845
+ const memories = await storesObj.memoryStore.getAllMemories('cli-user');
846
+ if (options.json) {
847
+ console.log(JSON.stringify(memories));
848
+ return;
849
+ }
850
+ console.log(`\nMemories (${memories.length})\n`);
851
+ for (const mem of memories) {
852
+ const val = typeof mem.value === 'string' ? mem.value : JSON.stringify(mem.value);
853
+ console.log(` [${mem.key}] ${val.substring(0, 60)}${val.length > 60 ? '...' : ''}`);
854
+ }
855
+ }
856
+ console.log();
857
+ }
858
+
859
+ /**
860
+ * Manage scheduled jobs.
861
+ *
862
+ * @param {Object} options - CLI options
863
+ * @param {string} subcommand - list or delete
864
+ * @param {string} jobId - Job ID to delete
865
+ */
866
+ async function runJobs(options, subcommand, jobId) {
867
+ const storesObj = await initStores(options.db, options.verbose, options.system);
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
+
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
+
886
+ console.log(`\nScheduled jobs (${jobs.length})\n`);
887
+
888
+ for (const job of jobs) {
889
+ const status = job.enabled ? 'active' : 'paused';
890
+ const next = job.nextRunAt ? job.nextRunAt.toLocaleString() : 'N/A';
891
+ const interval = job.intervalMs ? `${Math.round(job.intervalMs / 60000)}m` : 'once';
892
+ console.log(` [${job.id}] ${job.name} (${status})`);
893
+ console.log(` Interval: ${interval}`);
894
+ console.log(` Next: ${next}`);
895
+ console.log(` Prompt: ${job.prompt.substring(0, 50)}${job.prompt.length > 50 ? '...' : ''}`);
896
+ console.log();
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Manage active tasks.
902
+ *
903
+ * @param {Object} options - CLI options
904
+ * @param {string} subcommand - list or delete
905
+ * @param {string} taskId - Task ID to delete
906
+ */
907
+ async function runTasks(options, subcommand, taskId) {
908
+ const storesObj = await initStores(options.db, options.verbose, options.system);
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
+
920
+ const tasks = await storesObj.taskStore.getTasks('cli-user');
921
+
922
+ if (options.json) {
923
+ console.log(JSON.stringify(tasks));
924
+ return;
925
+ }
926
+
927
+ console.log(`\nTasks (${tasks.length})\n`);
928
+
929
+ for (const task of tasks) {
930
+ const steps = task.steps ? JSON.parse(task.steps) : [];
931
+ const progress = `${task.current_step || 0}/${steps.length}`;
932
+ console.log(` [${task.id}] ${task.status} (${progress})`);
933
+ console.log(` Description: ${task.description?.substring(0, 50) || 'N/A'}${task.description?.length > 50 ? '...' : ''}`);
934
+ console.log(` Mode: ${task.mode || 'auto'}`);
935
+ console.log(` Created: ${new Date(task.created_at).toLocaleString()}`);
936
+ console.log();
937
+ }
938
+ }
939
+
940
+ /**
941
+ * Manage chat sessions.
942
+ *
943
+ * @param {Object} options - CLI options
944
+ * @param {string} subcommand - list or delete
945
+ * @param {string} sessionId - Session ID to delete
946
+ */
947
+ async function runSessions(options, subcommand, sessionId) {
948
+ const storesObj = await initStores(options.db, options.verbose, options.system);
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
+
960
+ const sessions = await storesObj.sessionStore.listSessions('cli-user');
961
+
962
+ if (options.json) {
963
+ console.log(JSON.stringify(sessions));
964
+ return;
965
+ }
966
+
967
+ console.log(`\nSessions (${sessions.length})\n`);
968
+
969
+ for (const session of sessions) {
970
+ const updated = new Date(session.updatedAt).toLocaleString();
971
+ const msgCount = session.messageCount || 0;
972
+ console.log(` [${session.id}]`);
973
+ console.log(` Title: ${session.title || 'Untitled'}`);
974
+ console.log(` Messages: ${msgCount}`);
975
+ console.log(` Updated: ${updated}`);
976
+ console.log();
977
+ }
978
+ }
979
+
980
+ /**
981
+ * View audit log events.
982
+ *
983
+ * @param {Object} options - CLI options
984
+ */
985
+ async function runEvents(options) {
986
+ const storesObj = await initStores(options.db, options.verbose, options.system);
987
+
988
+ if (options.summary) {
989
+ const summary = await storesObj.eventStore.summary({ userId: 'cli-user' });
990
+ if (options.json) {
991
+ console.log(JSON.stringify(summary));
992
+ return;
993
+ }
994
+ console.log(`\nEvent summary\n`);
995
+ console.log(` Total events: ${summary.total || 0}`);
996
+ if (summary.breakdown) {
997
+ for (const [type, count] of Object.entries(summary.breakdown)) {
998
+ console.log(` ${type}: ${count}`);
999
+ }
1000
+ }
1001
+ } else {
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
+ }
1007
+ console.log(`\nRecent events (${events.length})\n`);
1008
+
1009
+ for (const event of events) {
1010
+ const time = new Date(event.timestamp).toLocaleString();
1011
+ console.log(` [${time}] ${event.type}`);
1012
+ if (event.data) {
1013
+ const data = typeof event.data === 'string' ? event.data : JSON.stringify(event.data);
1014
+ console.log(` ${data.substring(0, 60)}${data.length > 60 ? '...' : ''}`);
1015
+ }
1016
+ }
1017
+ }
1018
+ console.log();
1019
+ }
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
+
485
1086
  /**
486
1087
  * Main entry point.
487
1088
  */
@@ -500,14 +1101,54 @@ async function main() {
500
1101
 
501
1102
  const command = args.positionals[0];
502
1103
 
503
- if (command === 'serve') {
504
- await runServer(args);
505
- } else {
506
- const message = args.positionals.join(' ');
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();
507
1111
  if (message) {
508
1112
  await runChat(message, args);
509
- } else {
510
- await runRepl(args);
1113
+ return;
1114
+ }
1115
+ }
1116
+
1117
+ switch (command) {
1118
+ case 'doctor':
1119
+ await runDoctor(args);
1120
+ break;
1121
+ case 'serve':
1122
+ await runServer(args);
1123
+ break;
1124
+ case 'tools':
1125
+ await runTools(args);
1126
+ break;
1127
+ case 'stats':
1128
+ await runStats(args);
1129
+ break;
1130
+ case 'memory':
1131
+ await runMemory(args, args.positionals[1], args.positionals.slice(2).join(' '));
1132
+ break;
1133
+ case 'jobs':
1134
+ await runJobs(args, args.positionals[1], args.positionals[2]);
1135
+ break;
1136
+ case 'tasks':
1137
+ await runTasks(args, args.positionals[1], args.positionals[2]);
1138
+ break;
1139
+ case 'sessions':
1140
+ await runSessions(args, args.positionals[1], args.positionals[2]);
1141
+ break;
1142
+ case 'events':
1143
+ await runEvents(args);
1144
+ break;
1145
+ default: {
1146
+ const message = args.positionals.join(' ');
1147
+ if (message) {
1148
+ await runChat(message, args);
1149
+ } else {
1150
+ await runRepl(args);
1151
+ }
511
1152
  }
512
1153
  }
513
1154
  }
package/dotbot.db CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stevederico/dotbot",
3
- "version": "0.23.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",