@stevederico/dotbot 0.16.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +380 -0
  3. package/bin/dotbot.js +461 -0
  4. package/core/agent.js +779 -0
  5. package/core/compaction.js +261 -0
  6. package/core/cron_handler.js +262 -0
  7. package/core/events.js +229 -0
  8. package/core/failover.js +193 -0
  9. package/core/gptoss_tool_parser.js +173 -0
  10. package/core/init.js +154 -0
  11. package/core/normalize.js +324 -0
  12. package/core/trigger_handler.js +148 -0
  13. package/docs/core.md +103 -0
  14. package/docs/protected-files.md +59 -0
  15. package/examples/sqlite-session-example.js +69 -0
  16. package/index.js +341 -0
  17. package/observer/index.js +164 -0
  18. package/package.json +42 -0
  19. package/storage/CronStore.js +145 -0
  20. package/storage/EventStore.js +71 -0
  21. package/storage/MemoryStore.js +175 -0
  22. package/storage/MongoAdapter.js +291 -0
  23. package/storage/MongoCronAdapter.js +347 -0
  24. package/storage/MongoTaskAdapter.js +242 -0
  25. package/storage/MongoTriggerAdapter.js +158 -0
  26. package/storage/SQLiteAdapter.js +382 -0
  27. package/storage/SQLiteCronAdapter.js +562 -0
  28. package/storage/SQLiteEventStore.js +300 -0
  29. package/storage/SQLiteMemoryAdapter.js +240 -0
  30. package/storage/SQLiteTaskAdapter.js +419 -0
  31. package/storage/SQLiteTriggerAdapter.js +262 -0
  32. package/storage/SessionStore.js +149 -0
  33. package/storage/TaskStore.js +100 -0
  34. package/storage/TriggerStore.js +90 -0
  35. package/storage/cron_constants.js +48 -0
  36. package/storage/index.js +21 -0
  37. package/tools/appgen.js +311 -0
  38. package/tools/browser.js +634 -0
  39. package/tools/code.js +101 -0
  40. package/tools/events.js +145 -0
  41. package/tools/files.js +201 -0
  42. package/tools/images.js +253 -0
  43. package/tools/index.js +97 -0
  44. package/tools/jobs.js +159 -0
  45. package/tools/memory.js +332 -0
  46. package/tools/messages.js +135 -0
  47. package/tools/notify.js +42 -0
  48. package/tools/tasks.js +404 -0
  49. package/tools/triggers.js +159 -0
  50. package/tools/weather.js +82 -0
  51. package/tools/web.js +283 -0
  52. package/utils/providers.js +136 -0
package/bin/dotbot.js ADDED
@@ -0,0 +1,461 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * dotbot CLI
5
+ *
6
+ * Usage:
7
+ * dotbot chat "What's the weather?" One-shot query
8
+ * dotbot repl Interactive chat session
9
+ * dotbot serve --port 3000 Start HTTP server
10
+ * dotbot --help Show help
11
+ *
12
+ * Requires Node.js 22.5+ with --experimental-sqlite flag, or Node.js 23+
13
+ */
14
+
15
+ import { parseArgs } from 'node:util';
16
+ import * as readline from 'node:readline';
17
+ import { createServer } from 'node:http';
18
+
19
+ // Lazy-loaded modules (avoid SQLite import on --help)
20
+ let stores = null;
21
+ let coreTools = null;
22
+ let AI_PROVIDERS = null;
23
+ let agentLoop = null;
24
+
25
+ /**
26
+ * Lazy-load dotbot modules.
27
+ */
28
+ async function loadModules() {
29
+ if (stores) return;
30
+ const mod = await import('../index.js');
31
+ stores = {
32
+ SQLiteSessionStore: mod.SQLiteSessionStore,
33
+ SQLiteCronStore: mod.SQLiteCronStore,
34
+ SQLiteTaskStore: mod.SQLiteTaskStore,
35
+ SQLiteTriggerStore: mod.SQLiteTriggerStore,
36
+ SQLiteMemoryStore: mod.SQLiteMemoryStore,
37
+ SQLiteEventStore: mod.SQLiteEventStore,
38
+ };
39
+ coreTools = mod.coreTools;
40
+ AI_PROVIDERS = mod.AI_PROVIDERS;
41
+ agentLoop = mod.agentLoop;
42
+ }
43
+
44
+ const VERSION = '0.16.0';
45
+ const DEFAULT_PORT = 3000;
46
+ const DEFAULT_DB = './dotbot.db';
47
+
48
+ /**
49
+ * Print help message.
50
+ */
51
+ function printHelp() {
52
+ console.log(`
53
+ dotbot v${VERSION} — AI agent CLI
54
+
55
+ Usage:
56
+ dotbot chat "message" Send a one-shot message
57
+ dotbot repl Interactive chat session
58
+ dotbot serve [--port N] Start HTTP server (default: ${DEFAULT_PORT})
59
+
60
+ Options:
61
+ --provider, -p AI provider: xai, anthropic, openai, ollama (default: xai)
62
+ --model, -m Model name (default: grok-3)
63
+ --db SQLite database path (default: ${DEFAULT_DB})
64
+ --port Server port for 'serve' command (default: ${DEFAULT_PORT})
65
+ --help, -h Show this help
66
+ --version, -v Show version
67
+
68
+ Environment Variables:
69
+ XAI_API_KEY API key for xAI
70
+ ANTHROPIC_API_KEY API key for Anthropic
71
+ OPENAI_API_KEY API key for OpenAI
72
+ OLLAMA_BASE_URL Base URL for Ollama (default: http://localhost:11434)
73
+
74
+ Examples:
75
+ dotbot chat "What's the weather in SF?"
76
+ dotbot repl --provider anthropic --model claude-sonnet-4-5
77
+ dotbot serve --port 8080
78
+ `);
79
+ }
80
+
81
+ /**
82
+ * Parse CLI arguments.
83
+ */
84
+ function parseCliArgs() {
85
+ try {
86
+ const { values, positionals } = parseArgs({
87
+ allowPositionals: true,
88
+ options: {
89
+ help: { type: 'boolean', short: 'h', default: false },
90
+ version: { type: 'boolean', short: 'v', default: false },
91
+ provider: { type: 'string', short: 'p', default: 'xai' },
92
+ model: { type: 'string', short: 'm', default: 'grok-3' },
93
+ db: { type: 'string', default: DEFAULT_DB },
94
+ port: { type: 'string', default: String(DEFAULT_PORT) },
95
+ },
96
+ });
97
+ return { ...values, positionals };
98
+ } catch (err) {
99
+ console.error(`Error: ${err.message}`);
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Get provider config with API key from environment.
106
+ *
107
+ * @param {string} providerId - Provider ID
108
+ * @returns {Object} Provider config with headers
109
+ */
110
+ async function getProviderConfig(providerId) {
111
+ await loadModules();
112
+ const base = AI_PROVIDERS[providerId];
113
+ if (!base) {
114
+ console.error(`Unknown provider: ${providerId}`);
115
+ console.error(`Available: ${Object.keys(AI_PROVIDERS).join(', ')}`);
116
+ process.exit(1);
117
+ }
118
+
119
+ const envKey = base.envKey;
120
+ const apiKey = process.env[envKey];
121
+
122
+ if (!apiKey && providerId !== 'ollama') {
123
+ console.error(`Missing ${envKey} environment variable`);
124
+ process.exit(1);
125
+ }
126
+
127
+ if (providerId === 'ollama') {
128
+ const baseUrl = process.env.OLLAMA_BASE_URL || 'http://localhost:11434';
129
+ return { ...base, apiUrl: `${baseUrl}/api/chat` };
130
+ }
131
+
132
+ return {
133
+ ...base,
134
+ headers: () => base.headers(apiKey),
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Initialize stores.
140
+ *
141
+ * @param {string} dbPath - Path to SQLite database
142
+ * @returns {Promise<Object>} Initialized stores
143
+ */
144
+ async function initStores(dbPath) {
145
+ await loadModules();
146
+
147
+ const sessionStore = new stores.SQLiteSessionStore();
148
+ await sessionStore.init(dbPath, {
149
+ prefsFetcher: async () => ({ agentName: 'Dotbot', agentPersonality: '' }),
150
+ });
151
+
152
+ const cronStore = new stores.SQLiteCronStore();
153
+ await cronStore.init({ dbPath });
154
+
155
+ const taskStore = new stores.SQLiteTaskStore();
156
+ await taskStore.init({ dbPath });
157
+
158
+ const triggerStore = new stores.SQLiteTriggerStore();
159
+ await triggerStore.init({ dbPath });
160
+
161
+ const memoryStore = new stores.SQLiteMemoryStore();
162
+ await memoryStore.init({ dbPath });
163
+
164
+ const eventStore = new stores.SQLiteEventStore();
165
+ await eventStore.init({ dbPath });
166
+
167
+ return { sessionStore, cronStore, taskStore, triggerStore, memoryStore, eventStore };
168
+ }
169
+
170
+ /**
171
+ * Run a single chat message and stream output.
172
+ *
173
+ * @param {string} message - User message
174
+ * @param {Object} options - CLI options
175
+ */
176
+ async function runChat(message, options) {
177
+ const storesObj = await initStores(options.db);
178
+ const provider = await getProviderConfig(options.provider);
179
+
180
+ const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
181
+
182
+ const context = {
183
+ userID: 'cli-user',
184
+ sessionId: session.id,
185
+ providers: { [options.provider]: { apiKey: process.env[AI_PROVIDERS[options.provider]?.envKey] } },
186
+ ...storesObj,
187
+ };
188
+
189
+ const messages = [{ role: 'user', content: message }];
190
+
191
+ process.stdout.write('\n');
192
+
193
+ for await (const event of agentLoop({
194
+ model: options.model,
195
+ messages,
196
+ tools: coreTools,
197
+ provider,
198
+ context,
199
+ })) {
200
+ switch (event.type) {
201
+ case 'text_delta':
202
+ process.stdout.write(event.text);
203
+ break;
204
+ case 'tool_start':
205
+ process.stdout.write(`\n[${event.name}] `);
206
+ break;
207
+ case 'tool_result':
208
+ process.stdout.write(`done\n`);
209
+ break;
210
+ case 'error':
211
+ console.error(`\nError: ${event.error}`);
212
+ break;
213
+ }
214
+ }
215
+
216
+ process.stdout.write('\n\n');
217
+ process.exit(0);
218
+ }
219
+
220
+ /**
221
+ * Run interactive REPL.
222
+ *
223
+ * @param {Object} options - CLI options
224
+ */
225
+ async function runRepl(options) {
226
+ const storesObj = await initStores(options.db);
227
+ const provider = await getProviderConfig(options.provider);
228
+
229
+ const session = await storesObj.sessionStore.createSession('cli-user', options.model, options.provider);
230
+ const messages = [];
231
+
232
+ const context = {
233
+ userID: 'cli-user',
234
+ sessionId: session.id,
235
+ providers: { [options.provider]: { apiKey: process.env[AI_PROVIDERS[options.provider]?.envKey] } },
236
+ ...storesObj,
237
+ };
238
+
239
+ const rl = readline.createInterface({
240
+ input: process.stdin,
241
+ output: process.stdout,
242
+ });
243
+
244
+ console.log(`\ndotbot v${VERSION} — ${options.provider}/${options.model}`);
245
+ console.log('Type /quit to exit, /clear to reset conversation\n');
246
+
247
+ const prompt = () => {
248
+ rl.question('> ', async (input) => {
249
+ const trimmed = input.trim();
250
+
251
+ if (!trimmed) {
252
+ prompt();
253
+ return;
254
+ }
255
+
256
+ if (trimmed === '/quit' || trimmed === '/exit') {
257
+ console.log('Goodbye!');
258
+ rl.close();
259
+ process.exit(0);
260
+ }
261
+
262
+ if (trimmed === '/clear') {
263
+ messages.length = 0;
264
+ console.log('Conversation cleared.\n');
265
+ prompt();
266
+ return;
267
+ }
268
+
269
+ messages.push({ role: 'user', content: trimmed });
270
+
271
+ process.stdout.write('\n');
272
+ let assistantContent = '';
273
+
274
+ try {
275
+ for await (const event of agentLoop({
276
+ model: options.model,
277
+ messages: [...messages],
278
+ tools: coreTools,
279
+ provider,
280
+ context,
281
+ })) {
282
+ switch (event.type) {
283
+ case 'text_delta':
284
+ process.stdout.write(event.text);
285
+ assistantContent += event.text;
286
+ break;
287
+ case 'tool_start':
288
+ process.stdout.write(`\n[${event.name}] `);
289
+ break;
290
+ case 'tool_result':
291
+ process.stdout.write(`done\n`);
292
+ break;
293
+ case 'error':
294
+ console.error(`\nError: ${event.error}`);
295
+ break;
296
+ }
297
+ }
298
+
299
+ if (assistantContent) {
300
+ messages.push({ role: 'assistant', content: assistantContent });
301
+ }
302
+ } catch (err) {
303
+ console.error(`\nError: ${err.message}`);
304
+ }
305
+
306
+ process.stdout.write('\n\n');
307
+ prompt();
308
+ });
309
+ };
310
+
311
+ prompt();
312
+ }
313
+
314
+ /**
315
+ * Run HTTP server.
316
+ *
317
+ * @param {Object} options - CLI options
318
+ */
319
+ async function runServer(options) {
320
+ const port = parseInt(options.port, 10);
321
+ const storesObj = await initStores(options.db);
322
+
323
+ const server = createServer(async (req, res) => {
324
+ // CORS headers
325
+ res.setHeader('Access-Control-Allow-Origin', '*');
326
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
327
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
328
+
329
+ if (req.method === 'OPTIONS') {
330
+ res.writeHead(204);
331
+ res.end();
332
+ return;
333
+ }
334
+
335
+ const url = new URL(req.url, `http://localhost:${port}`);
336
+
337
+ // Health check
338
+ if (req.method === 'GET' && url.pathname === '/health') {
339
+ res.writeHead(200, { 'Content-Type': 'application/json' });
340
+ res.end(JSON.stringify({ status: 'ok', version: VERSION }));
341
+ return;
342
+ }
343
+
344
+ // Chat endpoint
345
+ if (req.method === 'POST' && url.pathname === '/chat') {
346
+ let body = '';
347
+ for await (const chunk of req) body += chunk;
348
+
349
+ try {
350
+ const { message, provider: providerId = 'anthropic', model = 'claude-sonnet-4-5', sessionId } = JSON.parse(body);
351
+
352
+ if (!message) {
353
+ res.writeHead(400, { 'Content-Type': 'application/json' });
354
+ res.end(JSON.stringify({ error: 'message required' }));
355
+ return;
356
+ }
357
+
358
+ const provider = await getProviderConfig(providerId);
359
+ let session;
360
+
361
+ if (sessionId) {
362
+ session = await storesObj.sessionStore.getSessionInternal(sessionId);
363
+ }
364
+ if (!session) {
365
+ session = await storesObj.sessionStore.createSession('api-user', model, providerId);
366
+ }
367
+
368
+ const context = {
369
+ userID: 'api-user',
370
+ sessionId: session.id,
371
+ providers: { [providerId]: { apiKey: process.env[AI_PROVIDERS[providerId]?.envKey] } },
372
+ ...storesObj,
373
+ };
374
+
375
+ const messages = [...(session.messages || []), { role: 'user', content: message }];
376
+
377
+ res.writeHead(200, {
378
+ 'Content-Type': 'text/event-stream',
379
+ 'Cache-Control': 'no-cache',
380
+ 'Connection': 'keep-alive',
381
+ });
382
+
383
+ for await (const event of agentLoop({
384
+ model,
385
+ messages,
386
+ tools: coreTools,
387
+ provider,
388
+ context,
389
+ })) {
390
+ res.write(`data: ${JSON.stringify(event)}\n\n`);
391
+ }
392
+
393
+ res.end();
394
+ } catch (err) {
395
+ res.writeHead(500, { 'Content-Type': 'application/json' });
396
+ res.end(JSON.stringify({ error: err.message }));
397
+ }
398
+ return;
399
+ }
400
+
401
+ // 404
402
+ res.writeHead(404, { 'Content-Type': 'application/json' });
403
+ res.end(JSON.stringify({ error: 'Not found' }));
404
+ });
405
+
406
+ server.listen(port, () => {
407
+ console.log(`\ndotbot server v${VERSION}`);
408
+ console.log(`Listening on http://localhost:${port}`);
409
+ console.log(`\nEndpoints:`);
410
+ console.log(` GET /health Health check`);
411
+ console.log(` POST /chat Send message (SSE stream)\n`);
412
+ });
413
+ }
414
+
415
+ /**
416
+ * Main entry point.
417
+ */
418
+ async function main() {
419
+ const args = parseCliArgs();
420
+
421
+ if (args.version) {
422
+ console.log(`dotbot v${VERSION}`);
423
+ process.exit(0);
424
+ }
425
+
426
+ if (args.help || args.positionals.length === 0) {
427
+ printHelp();
428
+ process.exit(0);
429
+ }
430
+
431
+ const command = args.positionals[0];
432
+
433
+ switch (command) {
434
+ case 'chat':
435
+ const message = args.positionals.slice(1).join(' ');
436
+ if (!message) {
437
+ console.error('Usage: dotbot chat "your message"');
438
+ process.exit(1);
439
+ }
440
+ await runChat(message, args);
441
+ break;
442
+
443
+ case 'repl':
444
+ await runRepl(args);
445
+ break;
446
+
447
+ case 'serve':
448
+ await runServer(args);
449
+ break;
450
+
451
+ default:
452
+ console.error(`Unknown command: ${command}`);
453
+ printHelp();
454
+ process.exit(1);
455
+ }
456
+ }
457
+
458
+ main().catch((err) => {
459
+ console.error(`Fatal error: ${err.message}`);
460
+ process.exit(1);
461
+ });